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