| | 1 | |
| | 2 | |
| | 3 | {{{ |
| | 4 | #!html |
| | 5 | <h1>Form submission </h1> |
| | 6 | }}} |
| | 7 | '''Inner links''' |
| | 8 | {{{ |
| | 9 | #!html |
| | 10 | Part 1 <br/> <a href="#P1">Ajax basic form submission, Django server answers Ajax call.</a><br/> |
| | 11 | Part 2 <br/><a href="#P2">Handling the form when JavaScript is deactivated.</a><br/> |
| | 12 | Part 3 <br/><a href="#P3">Fixing the frozen fading when user resend the form without waiting for the first fading to end.</a> |
| | 13 | }}} |
| | 14 | |
| | 15 | |
| | 16 | {{{ |
| | 17 | #!html |
| | 18 | <a name="P1"><h1>Ajax basic form submission, Django server answers Ajax call.</h1></a> |
| | 19 | }}} |
| | 20 | '''Ajax form submission using Dojo for the client side javaScript toolkit, and simpleJson for the client/server communication.''' |
| | 21 | |
| | 22 | This will take you through all the steps required to get started and to develop a handy form submission without reload. |
| | 23 | |
| | 24 | |
| | 25 | == What do you need == |
| | 26 | |
| | 27 | - Django |
| | 28 | - Dojo (v0.3) [http://dojotoolkit.org/] an open source javascript toolkit. |
| | 29 | - Simple_Json (v1.3) [http://svn.red-bean.com/bob/simplejson/tags/simplejson-1.3/docs/index.html] used for javascript <-> python communication. |
| | 30 | |
| | 31 | |
| | 32 | == What do we want to achieve == |
| | 33 | I will use the model of my current website, a recipe one. |
| | 34 | When a registered user sees the details of a recipe, the user can rate (mark) it or update their rating (mark) by selecting the mark with a small drop down list. |
| | 35 | But when the user clicks ok the whole page reloads. We will use some Ajax to make it transparent and fancy effect to show the change to the user. |
| | 36 | we can add a fading status message like "Your rating has been updated". |
| | 37 | The select box proposes a rating from 1 to 5, it is actually a form which is sent to the server via POST, which in django links to a method |
| | 38 | in the view.py : ''def details(request)'' which does the innerwork for the details page generation. |
| | 39 | |
| | 40 | |
| | 41 | == Installing Json == |
| | 42 | get the SimpleJson from svn. |
| | 43 | |
| | 44 | {{{ |
| | 45 | svn co http://svn.red-bean.com/bob/simplejson/trunk/ json |
| | 46 | cd json |
| | 47 | sudo python setup.py install |
| | 48 | }}} |
| | 49 | |
| | 50 | == Django part == |
| | 51 | '''view.py''' |
| | 52 | {{{ |
| | 53 | #!python |
| | 54 | def details(request): |
| | 55 | [more stuff] |
| | 56 | if request.POST: |
| | 57 | # Get all the mark for this recipe |
| | 58 | # its not the best way i sould hava an oter entry or table, with total and nbr of marks for |
| | 59 | # each recipe. |
| | 60 | list_mark = Mark.objects.values('mark').filter(recipe__pk=r.id) |
| | 61 | # loop to get the total |
| | 62 | total = 0 |
| | 63 | for element in list_mark: |
| | 64 | total+= element['mark'] |
| | 65 | # round it |
| | 66 | total = round((float(total) / len(list_mark)),1) |
| | 67 | # update the total |
| | 68 | r.total_mark= total |
| | 69 | # save the user mark |
| | 70 | r.save() |
| | 71 | |
| | 72 | # Now the intersting part for this tut |
| | 73 | import simple_json |
| | 74 | # it was a french string, if we dont't use unicode |
| | 75 | # the result at the output of json in Dojo is wrong. |
| | 76 | message = unicode( message, "utf-8" ) |
| | 77 | # |
| | 78 | jsonList = simple_json.dumps((my_mark, total, form_message ,message)) |
| | 79 | return HttpResponse(jsonList) |
| | 80 | [more stuff, if not POST return render_to_response('recettes/details.html' ...] |
| | 81 | }}} |
| | 82 | |
| | 83 | '''url.py''' |
| | 84 | |
| | 85 | Just normal url.py, remember the path which will point to the wanted method. |
| | 86 | {{{ |
| | 87 | #!python |
| | 88 | from django.conf.urls.defaults import * |
| | 89 | |
| | 90 | urlpatterns = patterns('', |
| | 91 | [...more...] |
| | 92 | (r'^recettes/(?P<r_cat>[-\w]+)/(?P<r_slug>[-\w]+)/$', 'cefinban.recettes.views.details'), |
| | 93 | [...more...] |
| | 94 | }}} |
| | 95 | |
| | 96 | |
| | 97 | == Html template with Dojo javascript == |
| | 98 | '''Dojo use''' |
| | 99 | |
| | 100 | {{{ |
| | 101 | {% load i18n %} |
| | 102 | {% extends "base.html" %} |
| | 103 | {% block script %} |
| | 104 | <script type="text/javascript" src="/media/js/dojo/dojo.js"></script> |
| | 105 | <script type="text/javascript"> |
| | 106 | |
| | 107 | dojo.require("dojo.widget.Tooltip"); |
| | 108 | dojo.require("dojo.fx.html"); |
| | 109 | dojo.require("dojo.event.*"); |
| | 110 | dojo.require("dojo.json"); |
| | 111 | |
| | 112 | // point to the same url details.html |
| | 113 | function sendForm() |
| | 114 | { |
| | 115 | dojo.io.bind({ |
| | 116 | url: '.', |
| | 117 | handler: sendFormCallback, |
| | 118 | formNode: dojo.byId('myForm') |
| | 119 | }); |
| | 120 | } |
| | 121 | |
| | 122 | function sendFormCallback(type, data, evt) |
| | 123 | { |
| | 124 | if (type == 'error') |
| | 125 | alert('Error when retrieving data from the server!'); |
| | 126 | else |
| | 127 | // de code the simpleJson answer from django ''details'' method. |
| | 128 | // it populates a Js array ! straigth so cool ! |
| | 129 | arrayData = dojo.json.evalJson(data); |
| | 130 | // now we update the html using the css id as a pointer to the part |
| | 131 | // we want to update |
| | 132 | dojo.byId("mark_total").innerHTML = arrayData[1]; |
| | 133 | dojo.byId("mark_message").innerHTML = arrayData[2]; |
| | 134 | dojo.byId("mark_status").innerHTML = arrayData[3]; |
| | 135 | // and the fancy fading effect |
| | 136 | dojo.lfx.html.highlight("mark_status", [255, 151, 58], 700).play(300); |
| | 137 | } |
| | 138 | |
| | 139 | function init() |
| | 140 | { |
| | 141 | var sendFormButton = dojo.byId('sendFormButton'); |
| | 142 | dojo.event.connect(sendFormButton, 'onclick', 'sendForm') |
| | 143 | } |
| | 144 | |
| | 145 | dojo.addOnLoad(init); |
| | 146 | |
| | 147 | </script> |
| | 148 | }}} |
| | 149 | |
| | 150 | the following ''HTML code'' just comes after the upper code snipset. |
| | 151 | |
| | 152 | {{{ |
| | 153 | [... total mark get updated, lets put css id="mark_total" ...] |
| | 154 | {% if r.total_mark %}<li><b>Score</b>: <span id="mark_total">{{ r.total_mark }}</span>/5</li>{% endif %} |
| | 155 | [....] |
| | 156 | {% if not user.is_anonymous %} |
| | 157 | {% ifnotequal user.id r.owner_id %} |
| | 158 | <form enctype="multipart/form-data" id="myForm" method="post"> |
| | 159 | <span id="mark_message">{{mark_message}}</span> |
| | 160 | <select name="select_mark"> |
| | 161 | <option value ="1" {% ifequal my_mark 1 %} selected {% endifequal %}>1</option> |
| | 162 | <option value ="2" {% ifequal my_mark 2 %} selected {% endifequal %}>2</option> |
| | 163 | <option value ="3" {% ifequal my_mark 3 %} selected {% endifequal %}>3</option> |
| | 164 | <option value ="4" {% ifequal my_mark 4 %} selected {% endifequal %}>4</option> |
| | 165 | <option value ="5" {% ifequal my_mark 5 %} selected {% endifequal %}>5</option> |
| | 166 | </select> |
| | 167 | </form> |
| | 168 | <button id="sendFormButton">Notez</button> |
| | 169 | <br/> |
| | 170 | <span id="mark_status">{{ mark_status }}</span> |
| | 171 | {% endifnotequal %} |
| | 172 | {% endif %} |
| | 173 | }}} |
| | 174 | |
| | 175 | And, voila. |
| | 176 | |
| | 177 | To have a demo use guest as login, guest as password here [http://ozserver.no-ip.com:345] or if not working here [http://www.cefinban.net]. |
| | 178 | Go to index and pick a recipe, update the rating. |
| | 179 | |
| | 180 | You can also have a look at the screenshot here : [http://ozserver.no-ip.com/~greg/images/ajaxdjango.png] |
| | 181 | |
| | 182 | == Dreamhost and Simplejson == |
| | 183 | |
| | 184 | If you are using dreamhost for hosting please be aware that simplejson is not installed. |
| | 185 | Instead you will have to install the source of simplejson in a folder in your home directory eg /proz/json/simple_json |
| | 186 | The simple_json directory contains the required __init__.py for it to be loaded as a python module. |
| | 187 | |
| | 188 | Then in your ~/.bash_profile add the directory to your python path like below. |
| | 189 | |
| | 190 | {{{ |
| | 191 | export PYTHONPATH=$PYTHONPATH:$HOME/django/django_src:$HOME/django/django_projects:$HOME/progz/json |
| | 192 | }}} |
| | 193 | |
| | 194 | That will allow yout to use simpl_json in python shell. |
| | 195 | But '''dont forget''' to change django.fcgi ! |
| | 196 | Add |
| | 197 | {{{ |
| | 198 | sys.path +=['/home/coulix/progz/json'] |
| | 199 | }}} |
| | 200 | log out/in and try import simple_json (or simplejson depends on what source you picked) |
| | 201 | |
| | 202 | {{{ |
| | 203 | #!html |
| | 204 | <a name="P2"><h1>Handling the form when JavaScript is deactivated.</h1></a> |
| | 205 | }}} |
| | 206 | If a user has deactivated his browser's javascript support, or is using a text mode browser, we need a way of making the previous rating button submit the rating to the server which should this time return an html template instead of data to the Ajax call. |
| | 207 | |
| | 208 | == Updating the form HTML (details.html template) == |
| | 209 | This time we put a submit type input inside the form instead of the button type in part 1. |
| | 210 | type="submit" as indicates its name, submits the form to the server, we will need a way of stopping this behavior using javaScript. |
| | 211 | |
| | 212 | {{{ |
| | 213 | <form enctype="multipart/form-data" id="myForm" method="post"> |
| | 214 | <span id="mark_message">{{mark_message}}</span> |
| | 215 | <select name="select_mark"> |
| | 216 | [...] |
| | 217 | </select> |
| | 218 | <input id="sendFormButton" type="submit" value="Notez" /> |
| | 219 | </form> |
| | 220 | }}} |
| | 221 | |
| | 222 | Now, how can we tell our details method in view.py to know if it comes from a normal submit request or an Ajax request ? |
| | 223 | Two solutions, |
| | 224 | |
| | 225 | The '''first''' uses a hidden variable in form.html and an added content element in the JS part. |
| | 226 | |
| | 227 | {{{ |
| | 228 | function sendForm() |
| | 229 | { |
| | 230 | dojo.byId("mark_status").innerHTML = "Loading ..."; |
| | 231 | dojo.io.bind({ |
| | 232 | url: '.', |
| | 233 | handler: sendFormCallback, |
| | 234 | content: {"js", "true"}, |
| | 235 | formNode: dojo.byId('myForm') |
| | 236 | }); |
| | 237 | } |
| | 238 | |
| | 239 | |
| | 240 | [...] |
| | 241 | <form enctype="multipart/form-data" id="myForm" method="post"> |
| | 242 | [...] |
| | 243 | <input type="hidden" name="js" value="false"> |
| | 244 | </form> |
| | 245 | }}} |
| | 246 | |
| | 247 | With this, in our django method in view.py we can test for request["js"]=="true" it would means that Js is activatd and we return the appropriate answer to the Ajax request. |
| | 248 | |
| | 249 | The '''second''' uses the url to pass a new variable ajax_or_not to the detail method. |
| | 250 | |
| | 251 | {{{ |
| | 252 | #!python |
| | 253 | def details(request, r_slug, r_cat, ajax_or_not=None): |
| | 254 | [...] |
| | 255 | }}} |
| | 256 | |
| | 257 | We modify the url.py to accept this new parameter. |
| | 258 | {{{ |
| | 259 | #!python |
| | 260 | (r'^recettes/(?P<r_cat>[-\w]+)/(?P<r_slug>[-\w]+)/(?P<ajax_or_not>.*)$', 'cefinban.recettes.views.details'), |
| | 261 | }}} |
| | 262 | |
| | 263 | The dojo binding needs to append a variable to the original document url, to make ajax_or_not not None. |
| | 264 | |
| | 265 | {{{ |
| | 266 | function sendForm() |
| | 267 | { |
| | 268 | dojo.byId("mark_status").innerHTML = "Loading ..."; |
| | 269 | dojo.io.bind({ |
| | 270 | url: './ajax/', |
| | 271 | handler: sendFormCallback, |
| | 272 | formNode: dojo.byId('myForm') |
| | 273 | }); |
| | 274 | } |
| | 275 | }}} |
| | 276 | |
| | 277 | == New details method in view.py == |
| | 278 | |
| | 279 | We just need to test for the existence of ajax_or_not |
| | 280 | |
| | 281 | {{{ |
| | 282 | #!python |
| | 283 | def details(request, r_slug, r_cat, ajax_or_not=None): |
| | 284 | [...] |
| | 285 | if request.POST: |
| | 286 | [...] same as part 1 |
| | 287 | # except here we check ajax_or_not |
| | 288 | if ajax_or_not: |
| | 289 | # use json for python js exchange |
| | 290 | # it was a french string, if we dont't use unicode |
| | 291 | # the result at the output of json in Dojo is wrong. |
| | 292 | message = unicode( message, "utf-8" ) |
| | 293 | jsonList = simple_json.dumps((my_mark, total, form_message ,message)) |
| | 294 | return HttpResponse(jsonList) |
| | 295 | |
| | 296 | return render_to_response('recettes/details.html', {'r': r, 'section':'index', |
| | 297 | 'mark_status':message , 'mark_message':form_message, 'my_mark': my_mark}, |
| | 298 | context_instance=RequestContext(request),) |
| | 299 | }}} |
| | 300 | |
| | 301 | {{{ |
| | 302 | #!html |
| | 303 | <a name="P3"><h1>Fixing the frozen fading when user resend the form without waiting for the first fading to end.</h1></a> |
| | 304 | }}} |
| | 305 | If you haven't realised yet, if two or more calls are sent to the javascript function sendForm in a short time, the fading effect of the current sendForm Callback method might get stuck / froze / bugged. |
| | 306 | We need a way of avoiding this by desactivating the connection between the submit button and the sendForm method while the fading '''animation''' is active. |
| | 307 | Thanks Dojo there is such things ! in two lines of code its done. |
| | 308 | |
| | 309 | |
| | 310 | {{{ |
| | 311 | function sendFormCallback(type, data, evt) |
| | 312 | { |
| | 313 | [...as before ...] |
| | 314 | // and the fancy fading effect |
| | 315 | |
| | 316 | // first disconnect the listener ! |
| | 317 | dojo.event.disconnect(sendFormButton, 'onclick', 'sendForm'); |
| | 318 | // assign our fading effect to an anim variable. |
| | 319 | var anim = dojo.lfx.html.highlight("mark_status", [255, 151, 58], 700).play(300); |
| | 320 | // When this anim is finish, reconnect |
| | 321 | dojo.event.connect(anim, "onEnd", function() { dojo.event.connect(sendFormButton, 'onclick', 'sendForm'); }); |
| | 322 | } |
| | 323 | }}} |
| | 324 | |
| | 325 | how nice is this ! |
| | 326 | Careful, while talking about how to fix the problem using onEnd in Dojo IRC chanel, they realised play() method didnt behave properly and updated it to react to onEnd and such. |
| | 327 | su you need at least revision '''4286'''. |
| | 328 | Update your dojo source |
| | 329 | |
| | 330 | {{{ |
| | 331 | svn co http://svn.dojotoolkit.org/dojo/trunk dojo |
| | 332 | }}} |
| | 333 | |
| | 334 | |
| | 335 | |
| | 336 | |
| | 337 | '''Note''' It might be completely wrong. |
| | 338 | More questions / complaints: coulix@gmail.com |
| | 339 | |