require permissions to do non-versioned stuff
[redakcja.git] / apps / catalogue / views.py
1 from datetime import datetime
2 import logging
3 import os
4 from StringIO import StringIO
5
6 from django.contrib import auth
7 from django.contrib.auth.models import User
8 from django.contrib.auth.decorators import login_required, permission_required
9 from django.core.urlresolvers import reverse
10 from django.db.models import Count, Q
11 from django import http
12 from django.http import Http404
13 from django.shortcuts import get_object_or_404, render
14 from django.utils.http import urlquote_plus
15 from django.utils.translation import ugettext_lazy as _
16 from django.views.decorators.http import require_POST
17 from django.views.generic.simple import direct_to_template
18
19 import librarian.html
20 import librarian.text
21
22 from apiclient import NotAuthorizedError
23 from catalogue import forms
24 from catalogue import helpers
25 from catalogue.helpers import active_tab
26 from catalogue.models import Book, Chunk, BookPublishRecord, ChunkPublishRecord
27 from catalogue import xml_tools
28
29 #
30 # Quick hack around caching problems, TODO: use ETags
31 #
32 from django.views.decorators.cache import never_cache
33
34 logger = logging.getLogger("fnp.catalogue")
35
36
37 @active_tab('all')
38 @never_cache
39 def document_list(request):
40     return render(request, 'catalogue/document_list.html')
41
42
43 @never_cache
44 def user(request, username):
45     user = get_object_or_404(User, username=username)
46     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
47
48
49 @login_required
50 @active_tab('my')
51 @never_cache
52 def my(request):
53     return render(request, 'catalogue/my_page.html', {
54         'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
55                         key=lambda x: x[1]['time'], reverse=True),
56         })
57
58
59 @active_tab('users')
60 def users(request):
61     return direct_to_template(request, 'catalogue/user_list.html', extra_context={
62         'users': User.objects.all().annotate(count=Count('chunk')).order_by(
63             '-count', 'last_name', 'first_name'),
64     })
65
66
67 @active_tab('activity')
68 def activity(request):
69     return render(request, 'catalogue/activity.html')
70
71
72 @never_cache
73 def logout_then_redirect(request):
74     auth.logout(request)
75     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
76
77
78 @permission_required('catalogue.add_book')
79 @active_tab('create')
80 def create_missing(request, slug=None):
81     if slug is None:
82         slug = ''
83     slug = slug.replace(' ', '-')
84
85     if request.method == "POST":
86         form = forms.DocumentCreateForm(request.POST, request.FILES)
87         if form.is_valid():
88             
89             if request.user.is_authenticated():
90                 creator = request.user
91             else:
92                 creator = None
93             book = Book.create(
94                 text=form.cleaned_data['text'],
95                 creator=creator,
96                 slug=form.cleaned_data['slug'],
97                 title=form.cleaned_data['title'],
98             )
99
100             return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
101     else:
102         form = forms.DocumentCreateForm(initial={
103                 "slug": slug,
104                 "title": slug.replace('-', ' ').title(),
105         })
106
107     return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={
108         "slug": slug,
109         "form": form,
110     })
111
112
113 @permission_required('catalogue.add_book')
114 @active_tab('upload')
115 def upload(request):
116     if request.method == "POST":
117         form = forms.DocumentsUploadForm(request.POST, request.FILES)
118         if form.is_valid():
119             import slughifi
120
121             if request.user.is_authenticated():
122                 creator = request.user
123             else:
124                 creator = None
125
126             zip = form.cleaned_data['zip']
127             skipped_list = []
128             ok_list = []
129             error_list = []
130             slugs = {}
131             existing = [book.slug for book in Book.objects.all()]
132             for filename in zip.namelist():
133                 if filename[-1] == '/':
134                     continue
135                 title = os.path.basename(filename)[:-4]
136                 slug = slughifi(title)
137                 if not (slug and filename.endswith('.xml')):
138                     skipped_list.append(filename)
139                 elif slug in slugs:
140                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
141                 elif slug in existing:
142                     error_list.append((filename, slug, _('Slug already used in repository.')))
143                 else:
144                     try:
145                         zip.read(filename).decode('utf-8') # test read
146                         ok_list.append((filename, slug, title))
147                     except UnicodeDecodeError:
148                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
149                     slugs[slug] = filename
150
151             if not error_list:
152                 for filename, slug, title in ok_list:
153                     book = Book.create(
154                         text=zip.read(filename).decode('utf-8'),
155                         creator=creator,
156                         slug=slug,
157                         title=title,
158                     )
159
160             return direct_to_template(request, "catalogue/document_upload.html", extra_context={
161                 "form": form,
162                 "ok_list": ok_list,
163                 "skipped_list": skipped_list,
164                 "error_list": error_list,
165             })
166     else:
167         form = forms.DocumentsUploadForm()
168
169     return direct_to_template(request, "catalogue/document_upload.html", extra_context={
170         "form": form,
171     })
172
173
174 @never_cache
175 def book_xml(request, slug):
176     xml = get_object_or_404(Book, slug=slug).materialize()
177
178     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
179     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
180     return response
181
182
183 @never_cache
184 def book_txt(request, slug):
185     xml = get_object_or_404(Book, slug=slug).materialize()
186     output = StringIO()
187     # errors?
188     librarian.text.transform(StringIO(xml), output)
189     text = output.getvalue()
190     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
191     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
192     return response
193
194
195 @never_cache
196 def book_html(request, slug):
197     xml = get_object_or_404(Book, slug=slug).materialize()
198     output = StringIO()
199     # errors?
200     librarian.html.transform(StringIO(xml), output, parse_dublincore=False,
201                              flags=['full-page'])
202     html = output.getvalue()
203     response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
204     return response
205
206
207 @never_cache
208 def revision(request, slug, chunk=None):
209     try:
210         doc = Chunk.get(slug, chunk)
211     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
212         raise Http404
213     return http.HttpResponse(str(doc.revision()))
214
215
216 def book(request, slug):
217     book = get_object_or_404(Book, slug=slug)
218
219     # TODO: most of this should go somewhere else
220
221     # do we need some automation?
222     first_master = None
223     chunks = []
224     need_fixing = False
225     choose_master = False
226
227     length = book.chunk_set.count()
228     for i, chunk in enumerate(book):
229         chunk_dict = {
230             "chunk": chunk,
231             "fix": [],
232             "grade": ""
233             }
234         graded = xml_tools.GradedText(chunk.materialize())
235         if graded.is_wl():
236             master = graded.master()
237             if first_master is None:
238                 first_master = master
239             elif master != first_master:
240                 chunk_dict['fix'].append('bad-master')
241
242             if i > 0 and not graded.has_trim_begin():
243                 chunk_dict['fix'].append('trim-begin')
244             if i < length - 1 and not graded.has_trim_end():
245                 chunk_dict['fix'].append('trim-end')
246
247             if chunk_dict['fix']:
248                 chunk_dict['grade'] = 'wl-fix'
249             else:
250                 chunk_dict['grade'] = 'wl'
251
252         elif graded.is_broken_wl():
253             chunk_dict['grade'] = 'wl-broken'
254         elif graded.is_xml():
255             chunk_dict['grade'] = 'xml'
256         else:
257             chunk_dict['grade'] = 'plain'
258             chunk_dict['fix'].append('wl')
259             choose_master = True
260
261         if chunk_dict['fix']:
262             need_fixing = True
263         chunks.append(chunk_dict)
264
265     if first_master or not need_fixing:
266         choose_master = False
267
268     if request.method == "POST":
269         form = forms.ChooseMasterForm(request.POST)
270         if not choose_master or form.is_valid():
271             if choose_master:
272                 first_master = form.cleaned_data['master']
273
274             # do the actual fixing
275             for c in chunks:
276                 if not c['fix']:
277                     continue
278
279                 text = c['chunk'].materialize()
280                 for fix in c['fix']:
281                     if fix == 'bad-master':
282                         text = xml_tools.change_master(text, first_master)
283                     elif fix == 'trim-begin':
284                         text = xml_tools.add_trim_begin(text)
285                     elif fix == 'trim-end':
286                         text = xml_tools.add_trim_end(text)
287                     elif fix == 'wl':
288                         text = xml_tools.basic_structure(text, first_master)
289                 author = request.user if request.user.is_authenticated() else None
290                 description = "auto-fix: " + ", ".join(c['fix'])
291                 c['chunk'].commit(text=text, author=author, 
292                     description=description)
293
294             return http.HttpResponseRedirect(book.get_absolute_url())
295     elif choose_master:
296         form = forms.ChooseMasterForm()
297     else:
298         form = None
299
300     try:
301         book.assert_publishable()
302     except AssertionError, e:
303         publishable = False
304         publishable_error = e
305     else:
306         publishable = True
307         publishable_error = None
308
309     return direct_to_template(request, "catalogue/book_detail.html", extra_context={
310         "book": book,
311         "publishable": publishable,
312         "publishable_error": publishable_error,
313         "chunks": chunks,
314         "need_fixing": need_fixing,
315         "choose_master": choose_master,
316         "first_master": first_master,
317         "form": form,
318     })
319
320
321 @permission_required('catalogue.add_chunk')
322 def chunk_add(request, slug, chunk):
323     try:
324         doc = Chunk.get(slug, chunk)
325     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
326         raise Http404
327
328     if request.method == "POST":
329         form = forms.ChunkAddForm(request.POST, instance=doc)
330         if form.is_valid():
331             if request.user.is_authenticated():
332                 creator = request.user
333             else:
334                 creator = None
335             doc.split(creator=creator,
336                 slug=form.cleaned_data['slug'],
337                 title=form.cleaned_data['title'],
338             )
339
340             return http.HttpResponseRedirect(doc.book.get_absolute_url())
341     else:
342         form = forms.ChunkAddForm(initial={
343                 "slug": str(doc.number + 1),
344                 "title": "cz. %d" % (doc.number + 1, ),
345         })
346
347     return direct_to_template(request, "catalogue/chunk_add.html", extra_context={
348         "chunk": doc,
349         "form": form,
350     })
351
352
353 def chunk_edit(request, slug, chunk):
354     try:
355         doc = Chunk.get(slug, chunk)
356     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
357         raise Http404
358     if request.method == "POST":
359         form = forms.ChunkForm(request.POST, instance=doc)
360         if form.is_valid():
361             form.save()
362             return http.HttpResponseRedirect(doc.book.get_absolute_url())
363     else:
364         form = forms.ChunkForm(instance=doc)
365     return direct_to_template(request, "catalogue/chunk_edit.html", extra_context={
366         "chunk": doc,
367         "form": form,
368     })
369
370
371 @permission_required('catalogue.change_book')
372 def book_append(request, slug):
373     book = get_object_or_404(Book, slug=slug)
374     if request.method == "POST":
375         form = forms.BookAppendForm(book, request.POST)
376         if form.is_valid():
377             append_to = form.cleaned_data['append_to']
378             append_to.append(book)
379             return http.HttpResponseRedirect(append_to.get_absolute_url())
380     else:
381         form = forms.BookAppendForm(book)
382     return direct_to_template(request, "catalogue/book_append_to.html", extra_context={
383         "book": book,
384         "form": form,
385     })
386
387
388 @permission_required('catalogue.change_book')
389 def book_edit(request, slug):
390     book = get_object_or_404(Book, slug=slug)
391     if request.method == "POST":
392         form = forms.BookForm(request.POST, instance=book)
393         if form.is_valid():
394             form.save()
395             return http.HttpResponseRedirect(book.get_absolute_url())
396     else:
397         form = forms.BookForm(instance=book)
398     return direct_to_template(request, "catalogue/book_edit.html", extra_context={
399         "book": book,
400         "form": form,
401     })
402
403
404 @require_POST
405 @login_required
406 def publish(request, slug):
407     book = get_object_or_404(Book, slug=slug)
408     try:
409         book.publish(request.user)
410     except NotAuthorizedError:
411         return http.HttpResponseRedirect(reverse('apiclient_oauth'))
412     except BaseException, e:
413         return http.HttpResponse(e)
414     else:
415         return http.HttpResponseRedirect(book.get_absolute_url())