Update the docs to show how badly are changes needed.
[wolnelektury.git] / apps / ajaxable / utils.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 from functools import wraps
6
7 from django.http import (HttpResponse, HttpResponseRedirect,
8         HttpResponseForbidden)
9 from django.shortcuts import render_to_response
10 from django.template import RequestContext
11 from django.utils.encoding import force_unicode
12 from django.utils.functional import Promise
13 from django.utils.http import urlquote_plus
14 import json
15 from django.utils.translation import ugettext_lazy as _
16 from django.views.decorators.vary import vary_on_headers
17 from honeypot.decorators import verify_honeypot_value
18
19
20 class LazyEncoder(json.JSONEncoder):
21     def default(self, obj):
22         if isinstance(obj, Promise):
23             return force_unicode(obj)
24         return obj
25
26 # shortcut for JSON reponses
27 class JSONResponse(HttpResponse):
28     def __init__(self, data={}, callback=None, **kwargs):
29         # get rid of mimetype
30         kwargs.pop('mimetype', None)
31         data = json.dumps(data)
32         if callback:
33             data = callback + "(" + data + ");" 
34         super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs)
35
36
37 def method_decorator(function_decorator):
38     """Converts a function decorator to a method decorator.
39
40     It just makes it ignore first argument.
41     """
42     def decorator(method):
43         @wraps(method)
44         def wrapped_method(self, *args, **kwargs):
45             def function(*fargs, **fkwargs):
46                 return method(self, *fargs, **fkwargs)
47             return function_decorator(function)(*args, **kwargs)
48         return wrapped_method
49     return decorator
50
51
52 def require_login(request):
53     """Return 403 if request is AJAX. Redirect to login page if not."""
54     if request.is_ajax():
55         return HttpResponseForbidden('Not logged in')
56     else:
57         return HttpResponseRedirect('/uzytkownicy/zaloguj')# next?=request.build_full_path())
58
59
60 def placeholdized(form):
61     for field in form.fields.values():
62         field.widget.attrs['placeholder'] = field.label
63     return form
64
65
66 class AjaxableFormView(object):
67     """Subclass this to create an ajaxable view for any form.
68
69     In the subclass, provide at least form_class.
70
71     """
72     form_class = None
73     placeholdize = False
74     # override to customize form look
75     template = "ajaxable/form.html"
76     submit = _('Send')
77     
78     title = ''
79     success_message = ''
80     POST_login = False
81     formname = "form"
82     form_prefix = None
83     full_template = "ajaxable/form_on_page.html"
84     honeypot = False
85
86     @method_decorator(vary_on_headers('X-Requested-With'))
87     def __call__(self, request, *args, **kwargs):
88         """A view displaying a form, or JSON if request is AJAX."""
89         obj = self.get_object(request, *args, **kwargs)
90         form_args, form_kwargs = self.form_args(request, obj)
91         if self.form_prefix:
92             form_kwargs['prefix'] = self.form_prefix
93
94         if request.method == "POST":
95             if self.honeypot:
96                 response = verify_honeypot_value(request, None)
97                 if response:
98                     return response
99
100             # do I need to be logged in?
101             if self.POST_login and not request.user.is_authenticated():
102                 return require_login(request)
103
104             form_kwargs['data'] = request.POST
105             form = self.form_class(*form_args, **form_kwargs)
106             if form.is_valid():
107                 add_args = self.success(form, request)
108                 response_data = {
109                     'success': True, 
110                     'message': self.success_message,
111                     'redirect': request.GET.get('next')
112                     }
113                 if add_args:
114                     response_data.update(add_args)
115                 if not request.is_ajax() and response_data['redirect']:
116                     return HttpResponseRedirect(urlquote_plus(
117                             response_data['redirect'], safe='/?=&'))
118             elif request.is_ajax():
119                 # Form was sent with errors. Send them back.
120                 if self.form_prefix:
121                     errors = {}
122                     for key, value in form.errors.items():
123                         errors["%s-%s" % (self.form_prefix, key)] = value
124                 else:
125                     errors = form.errors
126                 response_data = {'success': False, 'errors': errors}
127             else:
128                 response_data = None
129             if request.is_ajax():
130                 return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
131         else:
132             if (self.POST_login and not request.user.is_authenticated()
133                     and not request.is_ajax()):
134                 return require_login(request)
135
136             form = self.form_class(*form_args, **form_kwargs)
137             response_data = None
138
139         title = self.title
140         if request.is_ajax():
141             template = self.template
142         else:
143             template = self.full_template
144             cd = self.context_description(request, obj)
145             if cd:
146                 title += ": " + cd
147         if self.placeholdize:
148             form = placeholdized(form)
149         context = {
150                 self.formname: form, 
151                 "title": title,
152                 "honeypot": self.honeypot,
153                 "placeholdize": self.placeholdize,
154                 "submit": self.submit,
155                 "response_data": response_data,
156                 "ajax_template": self.template,
157                 "view_args": args,
158                 "view_kwargs": kwargs,
159             }
160         context.update(self.extra_context(request, obj))
161         return render_to_response(template, context,
162             context_instance=RequestContext(request))
163
164     def redirect_or_refresh(self, request, path, message=None):
165         """If the form is AJAX, refresh the page. If not, go to `path`.""" 
166         if request.is_ajax():
167             output = "<script>window.location.reload()</script>"
168             if message:
169                 output = "<div class='normal-text'>" + message + "</div>" + output
170             return HttpResponse(output);
171         else:
172             return HttpResponseRedirect(path)
173
174     def get_object(self, request, *args, **kwargs):
175         """Override to parse view args and get some associated data."""
176         return None
177
178     def form_args(self, request, obj):
179         """Override to parse view args and give additional args to the form."""
180         return (), {}
181
182     def extra_context(self, request, obj):
183         """Override to pass something to template."""
184         return {}
185
186     def context_description(self, request, obj):
187         """Description to appear in standalone form, but not in AJAX form."""
188         return ""
189
190     def success(self, form, request):
191         """What to do when the form is valid.
192         
193         By default, just save the form.
194
195         """
196         return form.save(request)