Ponieważ napisałem stronę od nowa, postanowiłem zrobić to lepiej niż ostatnim razem. Jednym z ulepszeń jest dodanie JavaScript, na przykład do obsługi komentarzy. Zanim jednak wszystko zaczęło działać, musiałem trochę poszukać i potestować.
Spróbuję w miarę przystępnie opisać jak osiągnąć ładną obsługę komentarzy. JavaScript jest językiem którego dopiero się uczę, więc kod może nie być całkowicie poprawny (nie mniej, w moim przypadku działa).
Helpers.
Z czasów kiedy interesowałem się głównie pylons pamiętam, że dostępne tam były helpery. Podobne udogodnienie dostępne jest w RoR i TurboGears. Dzięki nim, nie mając zielonego pojęcia o JavaScript, dodać można do strony parę przydatnych elementów. Aby mieć taką funkcjonalność w Django, trzeba zainstalować je jako dodatkowy moduł, ponieważ nie są bezpośrednio dołączone do projektu.
Skoro w Django również można mieć helpery, to po co samemu pisać kod? Po części kierowałem się wpisem na the B-list. Ale wiedziałem też, że przyjdzie taki moment kiedy bez znajomości JavaScript ani rusz.
MochiKit
Dostępnych jest sporo bibliotek, które kuszą ładnymi efektami i prostotą implementacji w projekcie. Przejrzałem parę i wybrałem MochiKit, ponieważ podobno dzięki niej JavaScript suck less. Czego więcej może chcieć początkujący? Nie znalazłem żadnych tutoriali i z tego co przeczytałem na grupie, żadne nie istnieją. Ale dostępna jest obszerna dokumentacja co w pełni wystarcza.
Jeszcze parę linków
Prosty przykład AJAX w Django z wykorzystaniem biblioteki YUI znaleźć można na the B-list. Ponieważ jednak wybrałem MochiKit, trochę bardziej przydatna okazała się grupa dyskusyjna.
A co jak przeglądarka nie obsługuje JavaScript?
Obsługę formularza napiszemy najpierw w starym stylu - z przeładowaniem strony, a dopiero potem dopiszemy coś w JavaScript tak, aby po jego wyłączeniu, formularze dalej działały. Ktoś nadał temu nazwę hijax i jest to chyba właściwe podejście do problemu.
Zaczynamy
Tabela do której zapisywane będą komentarze, wygląda tak:
class Comments(models.Model): text = models.ForeignKey(Text) name = models.CharField(blank=False, max_length=30) mail = models.EmailField(blank=False, max_length=60) url = models.URLField(blank=True, max_length=60) date = models.DateField(auto_now_add=True) message = models.TextField(blank=False) class CommentsForm(forms.ModelForm): class Meta: model = Comments exclude = ('text',)
,część szablonu blog/show_entry.html:
<form id="commentForm" action="." method="POST"> <p> imię/nick:{{ cform.name }} <div class="error" id="error_name">{{ cform.name.errors }}</div> </p> <p> mail: {{ cform.mail }} <div class="error" id="error_mail">{{ cform.mail.errors }} </div> </p> <p> strona: {{ cform.url }} <div class="error" id="error_url">{{ cform.url.errors }}</div> </p> <p> {{ cform.message }} <div class="error" id="error_message">{{ cform.message.errors }}</div> </p> <p> <input type="submit" id="submitter" value=" Wyślij "> </p> </form>
i view który będzie go obsługiwał:
def show_entry(request, msg_url=None, template='blog/show_entry.html'): text = Text.objects.get(url=msg_url) comments = Comments.objects.filter(text=text.id) cform = CommentsForm(initial=request.session) if request.method == 'POST' form = CommentsForm(request.POST, instance=Comments(text=text) if form.is_valid(): instance = form.save() request.session['name'] = instance.name request.session['mail'] = instance.mail request.session['url'] = instance.url else: cform = form return render_to_response(template, { "text" : text, "cform" : cform, "comments" : comments, })
Otrzymaliśmy sprawną obsługę komentarzy. Jedyna wada takiego rozwiązania to uciążliwe przeładowywanie strony, po każdym wysłaniu formularza. I właśnie tu do akcji wkracza AJAX.
JSON
Do komunikacji użyję JSON. Biblioteka simplesjon jest częścią Django i można ją zaimportować z django.utils. Ponieważ jednak jestem leniwy, stworzyłem plik blog/json.py, a w nim klasę która wykona za mnie całą pracę związana z translacją:
from django.utils.functional import Promise from django.utils.encoding import force_unicode from django.core.serializers import json from django.utils import simplejson class LazyEncoder(simplejson.JSONEncoder, json.Serializer): def default(self, obj): if isinstance(obj, Promise): return force_unicode(obj) return obj def __call__(self, obj): try: return self.encode(obj) except ValueError: return self.serialize(obj)
Modernizacja views.py
Gdy będziemy wysyłać zapytanie za pomocą JavaScript, serwer przyjmie dane, które pobrać możemy z request.POST. Aby jednak przekonać się czy zapytanie wysyłane jest za pomocą JavaScript, sprawdzamy klucze request.GET. Powinna się tam znajdować zdefiniowana w skrypcie forms.js wartość xhr. W zależności od wyniku, przygotowujemy dane i wysyłamy odpowiedź.
from weblog.blog.json import LazyEncoder def show_entry(request, msg_url=None, template='blog/show_entry.html'): text = Text.objects.get(url=msg_url) comments = Comments.objects.filter(text=text.id) cform = CommentsForm(initial=request.session) # jeśli zapytanie pochodzi od JavaScript - True xhr = request.GET.has_key('xhr') # jeśli nie jest wysyłany komentarz, renderuj strone if not request.method == 'POST': return render_to_response(template, { "text" : text, "cform" : cform, "comments" : comments, }) form = CommentsForm(request.POST, instance=Comments(text=text)) if xhr: # szablon wiadomości to_return = {'valid': False, 'errors': None, 'msg': None } if form.is_valid(): instance = form.save() if xhr: # to może powodować przekłamanie po stronie serwera c = Comments.objects.all().order_by('-id')[:1] c = c.get() # uzupełniamy wiadomość o odpowiednie informacje to_return['msg'] = {'name': c.name, 'date': c.date.strftime("%Y-%m-%d"), 'message': c.message } to_return['valid'] = True # zapis sesji request.session['name'] = instance.name request.session['mail'] = instance.mail request.session['url'] = instance.url else: cform = form if xhr: # uzypełniamy wiadomość informacją o niepoprawnym # wypełnieniu formularza to_return['errors'] = form.errors.items() if xhr: lenc = LazyEncoder() # konwersja python -> JSON to_return = lenc(to_return) # wsyłamy JSON return HttpResponse(to_return, mimetype="application/javascript") # jeśli obsługa JavaScript nie jest dostępna, renderujemy stronę, # uwzględniając informację o błędach w formularzu return render_to_response(template, { "text" : text, "cform" : cform, "comments" : comments, })
I tyle ze strony serwera. Obsługa zapytań w Django gotowa.
forms.js
W funcji submitMaker tworzone jest zapytanie, które następnie wysyłamy do serwera. Ten sprawdza ich poprawność i zwraca odpowiedź. W zależności od tego czy komunikacja z serwerem przebiegła poprawnie, wywoływana jest funkcja handleServerFeedback lub handleServerError.
// wartosci ktore nalezy ustawic var KEY_ARRAY = ['id_name', 'id_mail', 'id_url', 'id_message']; var ERROR_ARRAY = ['error_name', 'error_mail', 'error_url', 'error_message']; var DESTINATION_URL = '?xhr'; var SUBMIT_ID = 'submitter'; window.onload = function() { $('commentForm').onsubmit = validForm; } function validForm(event) { event.preventDefault(); var submit = submitMaker(KEY_ARRAY, ERROR_ARRAY); submit(); return false; } // czyszczenie informacji o bledach function clearErrinfo(error_array) { for (i=0; i<error_array.length; ++i) { $(error_array[i]).innerHTML = ""; } } // blokowanie przycisku `wyslij` function lockSubmit(submit_id) { $(submit_id).disabled = true; } // czasowe blokowanie przycisku // rownie dobrze mozna by blokowac, a potem recznie zwalniac function holdSubmit(submit_id, time) { lockSubmit(submit_id); MochiKit.Async.callLater(time, function() {$(submit_id).disabled = false}); } function formValue(key_array) { return $(key_array).value; } // komunikacja z serwerem var submitMaker = function(KEY_ARRAY, ERROR_ARRAY) { submitHandler = function() { // czyscimy informacje o bledach clearErrinfo(ERROR_ARRAY); // blokujemy przycisk wysylania na 3s holdSubmit(SUBMIT_ID, 3.0); // odetnij `id_` z nazwy var keyArrayStrip = function(key) { return MochiKit.Format.lstrip(key, 'id_'); } // obcina nazwy w tablicy KEY_ARRAY var strip_key = MochiKit.Iter.imap(keyArrayStrip, KEY_ARRAY); // pobiez wartosci z formularzy var key_values = MochiKit.Iter.imap(formValue, KEY_ARRAY); // utworz zapytanie var data_querystring = queryString(list(strip_key),list(key_values)); var data_setup = getXMLHttpRequest(); data_setup.open("POST", DESTINATION_URL, true); data_setup.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); data_setup.setRequestHeader('Cache-Control', 'no-cache'); // wyslij zapytanie var d = sendXMLHttpRequest(data_setup, data_querystring); // podlaczenie sygnalow d.addCallbacks(handleServerFeedback, handleServerError); } return submitHandler; } // funckja podpieta pod sygnal gdy wszystko poszlo dobrze function handleServerFeedback(data_setup) { clearErrinfo(ERROR_ARRAY); msg_obj = evalJSONRequest(data_setup); if (msg_obj.valid) { // formularz jest poprawny, komentarz zostal zapisany do bazy... // moznaby wyswietlic wiadomosc } else { // formularz zawiera bledy var err = msg_obj.errors for (i=0; i<err.length; ++i) { error_id = "error_" + err[i][0]; $(error_id).innerHTML = err[i][1]; } } } // funkcja podpieta pod sygnal bledu function handleServerError() { alert("Error while transfering data."); }
Biblioteki
Ostatnią czynnością będzie dodanie w szablonie blog/show_entry.html bibliotek MochiKit (kolejność ma znaczenie):
<script type="text/javascript" src="/javascript/MochiKit/Base.js"></script> <script type="text/javascript" src="/javascript/MochiKit/DOM.js"></script> <script type="text/javascript" src="/javascript/MochiKit/Async.js"></script> <script type="text/javascript" src="/javascript/MochiKit/Iter.js"></script> <script type="text/javascript" src="/javascript/MochiKit/Format.js"></script> <script type="text/javascript" src="/javascript/forms.js"></script>
pats
24.08.2009
Świetny wpis, właśnie tego teraz szukałem,
dzięki
gather
18.11.2009
bardzo "przyswajalnie" opisane :)