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