Owocna współpraca Django z JavaScriptem

Współpraca backendu z frontendem, czy backendu z kodem JavaScript frontendu nie jest sprawą oczywistą. Jeżeli nie chcemy "hardkodować" linków i danych w plikach js to musimy je w zwinny sposób przekazać z backendu do JavaScriptu. Pobierając dane poprzez żądania AJAX zazwyczaj będziemy chcieli mieć dane w formacie JSON. W tym artykule przedstawię kilka rozwiązań i dodatków do Django ułatwiających współpracę backedu Django z JavaScriptem.

django-javascript-settings

django-javascript-settings to aplikacja Django, która w poręczny sposób zapewnia przekazywanie danych z backendu do JavaScriptu. Instalujemy standardowo:
pip install django-javascript-settings
Następnie dodajemy 'javascript_settings', do INSTALLED_APPS. Do głównego szablonu HTML dodajemy (w sekcji HEAD):
{% load javascript_settings_tags %}
<script type="text/javascript">{% javascript_settings %}</script>
W efekcie otrzymamy w miejscu wywołania taga "javascript_settings":
<script type="text/javascript">var configuration = {};</script>

Tworzona jest zmienna "configuration". Obecnie to pusta tablica.

By dodać coś do tej zmiennej wystarczy w urls.py wybranej aplikacji Django dodać funkcję javascript_settings zwracającą słownik, np.:
def javascript_settings():
    js_conf = {
        'page_title': 'Home',
    }
    return js_conf
W efekcie otrzymujemy coś takiego:
<script type="text/javascript">var configuration = {"jsdemo": {"page_title": "Home"}};</script>
Umieściłem tą funkcję w aplikacji "jsdemo" więc mam configuration['jsdemo']['page_title'].

django-javascript-settings można wykorzystać do przekazywania adresów URL widoków (stosujemy reverse w urls.py i dzięki temu nigdzie nie hardkodujemy adresów URL), linków do np. grafik, ikon potrzebnych w JavaScripcie, czy inne podstawowe dane.

Żądania AJAX

Samo wysyłanie żądań AJAX, np. za pomocą jQuery nie jest specjalnie trudne. Na potrzeby tego przykładu stworzyłem taki oto szablon strony głównej:
{% load javascript_settings_tags %}
<!DOCTYPE html>
<html> 
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <script type="text/javascript">{% javascript_settings %}</script>
    <script src="{{ STATIC_URL }}scripts.js"></script>
</head> 
<body> 
    <h1>Demo</h1>
</body>
</html>

W katalogu "static" znajdującym się w katalogu aplikacji stworzyłem plik scripts.js. W tym pliku chcę napisać kod wysyłający żądanie AJAX do innego widoku.

Na początek proste widoki:
from django.views.generic import TemplateView, View
from django.http import HttpResponse

class HomeView(TemplateView):
    template_name = "home.html"

home = HomeView.as_view()


class AjaxView(View):
    def get(self, request, **kwargs):
        return HttpResponse('ok')

ajax_view = AjaxView.as_view()
Strona główna i drugi widok dla żądania AJAX. W urls.py aplikacji mapuję widok AjaxView oraz przekazuję jego URL do JavaScriptu:
from django.conf.urls import patterns, url
from django.core.urlresolvers import reverse

urlpatterns = patterns('jsdemo.views',
    url(r'^ajax/$', 'ajax_view', name='ajax-view'),
)

def javascript_settings():
    js_conf = {
        'ajax_view': reverse('ajax-view'),
    }
    return js_conf
Oczywiście plik ten musi być załączony w głównym pliku urls.py:
url(r'^demo/', include('jsdemo.urls')),
Na koniec kod JavaScript umieszczony w pliku scripts.js:
$(document).ready(function(){
    $.ajax({
	url: configuration['jsdemo']['ajax_view'],
	cache: false,
	type: "GET",
	success: function(data){
	}
    });
});
Mamy już całą ścieżkę, która powinna działać. W Firebugu czy innej konsoli developerskiej przeglądarki powinniśmy widzieć wykonane żądanie i "ok" jako odpowiedź na to żądanie. Powyższy kod wyśle żądanie GET. Przesyłając jakiś zbiór danych do widoku lepiej byłoby wysłać żądanie metodą POST:
$(document).ready(function(){
    $.ajax({
	url: configuration['jsdemo']['ajax_view'],
	cache: false,
	type: "POST",
	data: {"key": "value"}, 
	success: function(data){
	}
    });
});

W widoku "AjaxView" zmieniamy metodę "get" na "post" i odświeżamy stronę główną. Żądanie AJAX nie powiedzie się. Odpowiedź serwera - 403, "CSRF verification failed. Request aborted.". Nie załączyliśmy tokena {% csrf_token %} jak to ma miejsce w formularzach. Można na widok nałożyć dekorator csrf_exempt, ale nie jest to najlepsze rozwiązanie.

W dokumentacji csrf Django znajdziemy rozwiązanie - dla żądań AJAX ustawiamy nagłówek X-CSRFToken. Jako wartość nagłówka podajemy wartość obecnego csrf tokena - pobraną z ciasteczka "csrftoken". Wykorzystując jQuery Cookie rozwiązanie to będzie krótkie i proste:

$(document).ready(function(){
    $.ajax({
	url: configuration['jsdemo']['ajax_view'],
	cache: false,
	type: "POST",
	data: {"key": "value"}, 
	success: function(data){
	},
	beforeSend: function(xhr, settings){
	    xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken'));
	}
    });
});
Stosując beforeSend ustawiamy nagłówek X-CSRFToken i gotowe... prawie. Jeżeli masz ustawione cookie "csrftoken" to żądanie AJAX będzie już działać. Jeżeli nie to nadal będzie 403. Django nie tworzy tego ciasteczka jeżeli nie musi (np. obecność formularza). By zapewnić obecność ciasteczka należy dodać 'django.views.decorators.csrf._EnsureCsrfCookie', do MIDDLEWARE_CLASSES.

Mamy już w pełni działające żądania AJAX. Teraz czas na wysyłanie danych z widoku. Sam tekst jest mało przydatny. Zazwyczaj będziemy mieć jakiś zestaw danych do przekazania. Można zwrócić dane w formacie JSON.

Importujemy moduł "json" i zwracamy dane w tym formacie:
class AjaxView(View):
    def post(self, request, **kwargs):
        data = {'user_id': self.request.user.id,
                'name': self.request.user.username}
        data = json.dumps(data)
        return HttpResponse(data)

ajax_view = AjaxView.as_view()

Django-annoying

django-annoying to zbiór dekoratorów i innych pomocnych funkcji skracających różne często wykonywane w kodzie czynności. Jednym z dekoratorów jest ajax_request. Zwróci on JsonResponse z danymi w formacie JSON. Dane pochodzić będą ze słownika, jaki ma zwrócić widok. Dzięki temu dekoratorowi w widoku nie będziemy musieli konwertować danych do JSONa. Zwróci on także poprawny typ MIME dla danych w formacie JSON.

Django annoying instalujemy standardowo:
pip install django-annoying
Nie dodajemy niczego do INSTALLED_APPS. Po prostu importujemy i używamy dekoratora:
from annoying.decorators import ajax_request
from django.views.generic import TemplateView, View

class HomeView(TemplateView):
    template_name = "home.html"

home = HomeView.as_view()


class AjaxView(View):
    def post(self, request, **kwargs):
        return {'user_id': self.request.user.id,
                'name': self.request.user.username}

ajax_view = ajax_request(AjaxView.as_view())
I gotowe. Pokazane w tym artykule rozwiązania powinny uporządkować współpracę backendu z JavaScriptem w aplikacjach Django.
RkBlog

Django, 8 July 2012

Comment article
Comment article RkBlog main page Search RSS Contact