fix in editor
[redakcja.git] / apps / catalogue / views.py
1 # -*- coding: utf-8 -*-
2 from datetime import datetime, date, timedelta
3 import logging
4 import os
5 import shutil
6 from StringIO import StringIO
7 from urllib import unquote
8 from urlparse import urlsplit, urlunsplit
9
10 from django.conf import settings
11 from django.contrib import auth
12 from django.contrib.auth.models import User
13 from django.contrib.auth.decorators import login_required, permission_required
14 from django.core.urlresolvers import reverse
15 from django.db.models import Count, Q
16 from django.db import transaction
17 from django import http
18 from django.http import Http404, HttpResponse, HttpResponseForbidden
19 from django.shortcuts import get_object_or_404, render, render_to_response, redirect
20 from django.utils.encoding import iri_to_uri
21 from django.utils.http import urlquote_plus
22 from django.utils.translation import ugettext_lazy as _
23 from django.views.decorators.http import require_POST
24 from django.template import RequestContext
25
26 from catalogue import forms
27 from catalogue import helpers
28 from catalogue.helpers import active_tab, sstdocument
29 from .constants import STAGES
30 from .models import Document, Plan
31 from dvcs.models import Revision
32 from organizations.models import Organization
33 from fileupload.views import UploadView, PackageView
34
35 #
36 # Quick hack around caching problems, TODO: use ETags
37 #
38 from django.views.decorators.cache import never_cache
39 #from fnpdjango.utils.text.slughifi import slughifi
40
41 logger = logging.getLogger("fnp.catalogue")
42
43
44 @active_tab('all')
45 @never_cache
46 def document_list(request):
47     return render(request, 'catalogue/document_list.html')
48
49
50 @never_cache
51 def user(request, username):
52     user = get_object_or_404(User, username=username)
53     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
54
55
56 @login_required
57 @active_tab('my')
58 @never_cache
59 def my(request):
60     return render(request, 'catalogue/my_page.html', {
61         'last_books': sorted(request.session.get("wiki_last_books", {}).items(),
62                         key=lambda x: x[1]['time'], reverse=True),
63
64         "logout_to": '/',
65         })
66
67
68 @active_tab('users')
69 def users(request):
70     return render(request, 'catalogue/user_list.html', {
71         'users': User.objects.all().annotate(count=Count('chunk')).order_by(
72             '-count', 'last_name', 'first_name'),
73     })
74
75
76 @active_tab('activity')
77 def activity(request, isodate=None):
78     today = date.today()
79     try:
80         day = helpers.parse_isodate(isodate)
81     except ValueError:
82         day = today
83
84     if day > today:
85         raise Http404
86     if day != today:
87         next_day = day + timedelta(1)
88     prev_day = day - timedelta(1)
89
90     return render(request, 'catalogue/activity.html', locals())
91
92
93 @never_cache
94 def logout_then_redirect(request):
95     auth.logout(request)
96     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
97
98
99 #@permission_required('catalogue.add_book')
100 @login_required
101 @active_tab('create')
102 def create_missing(request):
103     if request.method == "POST":
104         form = forms.DocumentCreateForm(request.POST, request.FILES)
105         if form.is_valid():
106             
107             if request.user.is_authenticated():
108                 creator = request.user
109             else:
110                 creator = None
111
112             title = form.cleaned_data['title']
113             try:
114                 org = request.user.membership_set.get(
115                     organization=int(form.cleaned_data['owner_organization'])).organization
116                 kwargs = {'owner_organization': org}
117             except:
118                 kwargs = {'owner_user': request.user}
119
120             doc = Document.objects.create(**kwargs)
121
122             cover = request.FILES.get('cover')
123             if cover:
124                 uppath = 'uploads/%d/' % doc.pk
125                 path = settings.MEDIA_ROOT + uppath
126                 if not os.path.isdir(path):
127                     os.makedirs(path)
128                 dest_path = path + cover.name   # UNSAFE
129                 with open(dest_path, 'w') as destination:
130                     for chunk in cover.chunks():
131                         destination.write(chunk)
132                 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
133             else:
134                 cover_url = ''
135
136             doc.commit(
137                 text = '''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
138                 <metadata>
139                     <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
140                     <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
141                     <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
142                     <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
143                     <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
144                     <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
145                 </metadata>
146                 <header>''' + title + '''</header>
147                 <div class="p"> </div>
148                 </section>''',
149                 author=creator
150             )
151             doc.assigned_to = request.user
152             doc.save()
153
154             return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
155     else:
156         org_pk = request.GET.get('organization')
157         if org_pk:
158             try:
159                 org = Organization.objects.get(pk=org_pk)
160             except Organization.DoesNotExist:
161                 org = None
162             else:
163                 if not org.is_member(request.user):
164                     org = None
165         else:
166             org = None
167         if org is not None:
168             org = org.pk
169
170         form = forms.DocumentCreateForm(initial={'owner_organization': org})
171
172     return render(request, "catalogue/document_create_missing.html", {
173         "form": form,
174
175         "logout_to": '/',
176     })
177
178
179 @permission_required('catalogue.add_book')
180 @active_tab('upload')
181 def upload(request):
182     if request.method == "POST":
183         form = forms.DocumentsUploadForm(request.POST, request.FILES)
184         if form.is_valid():
185             import slughifi
186
187             if request.user.is_authenticated():
188                 creator = request.user
189             else:
190                 creator = None
191
192             zip = form.cleaned_data['zip']
193             skipped_list = []
194             ok_list = []
195             error_list = []
196             slugs = {}
197             existing = [book.slug for book in Book.objects.all()]
198             for filename in zip.namelist():
199                 if filename[-1] == '/':
200                     continue
201                 title = os.path.basename(filename)[:-4]
202                 slug = slughifi(title)
203                 if not (slug and filename.endswith('.xml')):
204                     skipped_list.append(filename)
205                 elif slug in slugs:
206                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
207                 elif slug in existing:
208                     error_list.append((filename, slug, _('Slug already used in repository.')))
209                 else:
210                     try:
211                         zip.read(filename).decode('utf-8') # test read
212                         ok_list.append((filename, slug, title))
213                     except UnicodeDecodeError:
214                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
215                     slugs[slug] = filename
216
217             if not error_list:
218                 for filename, slug, title in ok_list:
219                     book = Book.create(
220                         text=zip.read(filename).decode('utf-8'),
221                         creator=creator,
222                         slug=slug,
223                         title=title,
224                     )
225
226             return render(request, "catalogue/document_upload.html", {
227                 "form": form,
228                 "ok_list": ok_list,
229                 "skipped_list": skipped_list,
230                 "error_list": error_list,
231
232                 "logout_to": '/',
233             })
234     else:
235         form = forms.DocumentsUploadForm()
236
237     return render(request, "catalogue/document_upload.html", {
238         "form": form,
239
240         "logout_to": '/',
241     })
242
243
244 @never_cache
245 def book_xml(request, slug):
246     book = get_object_or_404(Book, slug=slug)
247     if not book.accessible(request):
248         return HttpResponseForbidden("Not authorized.")
249     xml = book.materialize()
250
251     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
252     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
253     return response
254
255
256 @never_cache
257 def book_txt(request, slug):
258     book = get_object_or_404(Book, slug=slug)
259     if not book.accessible(request):
260         return HttpResponseForbidden("Not authorized.")
261
262     doc = book.wldocument()
263     text = doc.as_text().get_string()
264     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
265     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
266     return response
267
268
269 @never_cache
270 def book_html(request, pk, rev_pk=None, preview=False):
271     from librarian.document import Document as SST
272     from librarian.formats.html import HtmlFormat
273
274     doc = get_object_or_404(Document, pk=pk, deleted=False)
275
276     try:
277         published_revision = doc.publish_log.all()[0].revision
278     except IndexError:
279         published_revision = None
280
281     if rev_pk is None:
282         if preview:
283             revision = doc.revision
284         else:
285             if published_revision is not None:
286                 revision = published_revision
287             else:
288                 # No published version, fallback to preview mode.
289                 preview = True
290                 revision = doc.revision
291     else:
292         revision = get_object_or_404(Revision, pk=rev_pk)
293
294     was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
295
296     sst = SST.from_string(revision.materialize())
297     html = HtmlFormat(sst).build(files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
298
299     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
300     # return response
301     # book_themes = {}
302     # for fragment in book.fragments.all().iterator():
303     #     for theme in fragment.tags.filter(category='theme').iterator():
304     #         book_themes.setdefault(theme, []).append(fragment)
305
306     # book_themes = book_themes.items()
307     # book_themes.sort(key=lambda s: s[0].sort_key)
308     return render(request, 'catalogue/book_text.html', {
309         'doc': doc,
310         'preview': preview,
311         'revision': revision,
312         'published_revision': published_revision,
313         'specific': rev_pk is not None,
314         'html': html,
315         'can_edit': doc.can_edit(request.user) if doc else None,
316         'was_published': was_published,
317     })
318
319
320 @never_cache
321 def book_pdf(request, pk, rev_pk):
322     from librarian.utils import Context
323     from librarian.document import Document as SST
324     from librarian.formats.pdf import PdfFormat
325
326     doc = get_object_or_404(Document, pk=pk)
327     rev = get_object_or_404(Revision, pk=rev_pk)
328     # Test
329
330     sst = SST.from_string(rev.materialize())
331     
332     ctx = Context(
333         files_path = 'http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
334         source_url = 'http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
335         )
336     if doc.owner_organization is not None and doc.owner_organization.logo:
337         ctx.cover_logo = 'http://%s%s' %(request.get_host(), doc.owner_organization.logo.url)
338     pdf_file = PdfFormat(sst).build(ctx)
339
340     from catalogue.ebook_utils import serve_file
341     return serve_file(pdf_file.get_filename(),
342                 '%d.pdf' % doc.pk, 'application/pdf')
343
344
345 @never_cache
346 def book_epub(request, slug):
347     book = get_object_or_404(Book, slug=slug)
348     if not book.accessible(request):
349         return HttpResponseForbidden("Not authorized.")
350
351     # TODO: move to celery
352     doc = book.wldocument()
353     # TODO: error handling
354     epub = doc.as_epub().get_string()
355     response = HttpResponse(mimetype='application/epub+zip')
356     response['Content-Disposition'] = 'attachment; filename=%s' % book.slug + '.epub'
357     response.write(epub)
358     return response
359
360
361 @never_cache
362 def revision(request, slug, chunk=None):
363     try:
364         doc = Chunk.get(slug, chunk)
365     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
366         raise Http404
367     if not doc.book.accessible(request):
368         return HttpResponseForbidden("Not authorized.")
369     return http.HttpResponse(str(doc.revision()))
370
371
372 @login_required
373 def book_schedule(request, pk):
374     book = get_object_or_404(Document, pk=pk, deleted=False)
375     if request.method == 'POST':
376         Plan.objects.filter(document=book).delete()
377         for i, s in enumerate(STAGES):
378             user_id = request.POST.get('s%d-user' % i)
379             deadline = request.POST.get('s%d-deadline' % i) or None
380             Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
381
382         book.set_stage(request.POST.get('stage', ''))
383         return redirect('catalogue_user')
384
385     current = {}
386     for p in Plan.objects.filter(document=book):
387         current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
388
389     schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
390     
391     if book.owner_organization:
392         people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
393     else:
394         people = [book.owner_user]
395     return render(request, 'catalogue/book_schedule.html', {
396         'book': book,
397         'schedule': schedule,
398         'people': people,
399     })
400
401
402 @login_required
403 def book_owner(request, pk):
404     doc = get_object_or_404(Document, pk=pk, deleted=False)
405     if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
406         raise Http404
407
408     error = ''
409
410     if request.method == 'POST':
411         # TODO: real form
412         new_org_pk = request.POST.get('owner_organization')
413         if not new_org_pk:
414             doc.owner_organization = None
415             doc.owner_user = request.user
416             doc.save()
417         else:
418             org = Organization.objects.get(pk=new_org_pk)
419             if not org.is_member(request.user):
420                 error = 'Bad organization'
421             else:
422                 doc.owner_organization = org
423                 doc.owner_user = None
424                 doc.save()
425         if not error:
426             return redirect('catalogue_user')
427
428     return render(request, 'catalogue/book_owner.html', {
429         'doc': doc,
430         'error': error,
431     })
432
433
434 @login_required
435 def book_delete(request, pk):
436     doc = get_object_or_404(Document, pk=pk, deleted=False)
437     if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
438         raise Http404
439
440     if request.method == 'POST':
441         doc.deleted = True
442         doc.save()
443         return redirect('catalogue_user')
444
445     return render(request, 'catalogue/book_delete.html', {
446         'doc': doc,
447     })
448
449
450
451 def book(request, slug):
452     book = get_object_or_404(Book, slug=slug)
453     if not book.accessible(request):
454         return HttpResponseForbidden("Not authorized.")
455
456     if request.user.has_perm('catalogue.change_book'):
457         if request.method == "POST":
458             form = forms.BookForm(request.POST, instance=book)
459             if form.is_valid():
460                 form.save()
461                 return http.HttpResponseRedirect(book.get_absolute_url())
462         else:
463             form = forms.BookForm(instance=book)
464         editable = True
465     else:
466         form = forms.ReadonlyBookForm(instance=book)
467         editable = False
468
469     publish_error = book.publishable_error()
470     publishable = publish_error is None
471
472     return render(request, "catalogue/book_detail.html", {
473         "book": book,
474         "publishable": publishable,
475         "publishable_error": publish_error,
476         "form": form,
477         "editable": editable,
478     })
479
480
481 @permission_required('catalogue.add_chunk')
482 def chunk_add(request, slug, chunk):
483     try:
484         doc = Chunk.get(slug, chunk)
485     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
486         raise Http404
487     if not doc.book.accessible(request):
488         return HttpResponseForbidden("Not authorized.")
489
490     if request.method == "POST":
491         form = forms.ChunkAddForm(request.POST, instance=doc)
492         if form.is_valid():
493             if request.user.is_authenticated():
494                 creator = request.user
495             else:
496                 creator = None
497             doc.split(creator=creator,
498                 slug=form.cleaned_data['slug'],
499                 title=form.cleaned_data['title'],
500                 gallery_start=form.cleaned_data['gallery_start'],
501                 user=form.cleaned_data['user'],
502                 stage=form.cleaned_data['stage']
503             )
504
505             return http.HttpResponseRedirect(doc.book.get_absolute_url())
506     else:
507         form = forms.ChunkAddForm(initial={
508                 "slug": str(doc.number + 1),
509                 "title": "cz. %d" % (doc.number + 1, ),
510         })
511
512     return render(request, "catalogue/chunk_add.html", {
513         "chunk": doc,
514         "form": form,
515     })
516
517
518 @login_required
519 def chunk_edit(request, slug, chunk):
520     try:
521         doc = Chunk.get(slug, chunk)
522     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
523         raise Http404
524     if not doc.book.accessible(request):
525         return HttpResponseForbidden("Not authorized.")
526
527     if request.method == "POST":
528         form = forms.ChunkForm(request.POST, instance=doc)
529         if form.is_valid():
530             form.save()
531             go_next = request.GET.get('next', None)
532             if go_next:
533                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
534             else:
535                 go_next = doc.book.get_absolute_url()
536             return http.HttpResponseRedirect(go_next)
537     else:
538         form = forms.ChunkForm(instance=doc)
539
540     referer = request.META.get('HTTP_REFERER')
541     if referer:
542         parts = urlsplit(referer)
543         parts = ['', ''] + list(parts[2:])
544         go_next = urlquote_plus(urlunsplit(parts))
545     else:
546         go_next = ''
547
548     return render(request, "catalogue/chunk_edit.html", {
549         "chunk": doc,
550         "form": form,
551         "go_next": go_next,
552     })
553
554
555 @transaction.atomic
556 @login_required
557 def chunk_mass_edit(request):
558     if request.method == 'POST':
559         ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
560         chunks = map(lambda i: Chunk.objects.get(id=i), ids)
561         
562         stage = request.POST.get('stage')
563         if stage:
564             try:
565                 stage = Chunk.tag_model.objects.get(slug=stage)
566             except Chunk.DoesNotExist, e:
567                 stage = None
568            
569             for c in chunks: c.stage = stage
570
571         username = request.POST.get('user')
572         logger.info("username: %s" % username)
573         logger.info(request.POST)
574         if username:
575             try:
576                 user = User.objects.get(username=username)
577             except User.DoesNotExist, e:
578                 user = None
579                 
580             for c in chunks: c.user = user
581
582         status = request.POST.get('status')
583         if status:
584             books_affected = set()
585             for c in chunks:
586                 if status == 'publish':
587                     c.head.publishable = True
588                     c.head.save()
589                 elif status == 'unpublish':
590                     c.head.publishable = False
591                     c.head.save()
592                 c.touch()  # cache
593                 books_affected.add(c.book)
594             for b in books_affected:
595                 b.touch()  # cache
596
597         project_id = request.POST.get('project')
598         if project_id:
599             try:
600                 project = Project.objects.get(pk=int(project_id))
601             except (Project.DoesNotExist, ValueError), e:
602                 project = None
603             for c in chunks:
604                 book = c.book
605                 book.project = project
606                 book.save()
607
608         for c in chunks: c.save()
609
610         return HttpResponse("", content_type="text/plain")
611     else:
612         raise Http404
613
614
615 @permission_required('catalogue.change_book')
616 def book_append(request, slug):
617     book = get_object_or_404(Book, slug=slug)
618     if not book.accessible(request):
619         return HttpResponseForbidden("Not authorized.")
620
621     if request.method == "POST":
622         form = forms.BookAppendForm(book, request.POST)
623         if form.is_valid():
624             append_to = form.cleaned_data['append_to']
625             append_to.append(book)
626             return http.HttpResponseRedirect(append_to.get_absolute_url())
627     else:
628         form = forms.BookAppendForm(book)
629     return render(request, "catalogue/book_append_to.html", {
630         "book": book,
631         "form": form,
632
633         "logout_to": '/',
634     })
635
636
637 @require_POST
638 @login_required
639 def publish(request, pk):
640     from wiki import forms
641     from .models import PublishRecord
642     from dvcs.models import Revision
643
644     # FIXME: check permissions
645
646     doc = get_object_or_404(Document, pk=pk, deleted=False)
647     form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
648     if form.is_valid():
649         rev = Revision.objects.get(pk=form.cleaned_data['revision'])
650         # FIXME: check if in tree
651         #if PublishRecord.objects.filter(revision=rev, document=doc).exists():
652         #    return http.HttpResponse('exists')
653         pr = PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
654         if request.is_ajax():
655             return http.HttpResponse('ok')
656         else:
657             return redirect('catalogue_html', doc.pk)
658     else:
659         if request.is_ajax():
660             return http.HttpResponse('error')
661         else:
662             try:
663                 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
664             except KeyError:
665                 return redirect('catalogue_preview', doc.pk)
666
667
668 @require_POST
669 @login_required
670 def unpublish(request, pk):
671     from wiki import forms
672     from .models import PublishRecord
673     from dvcs.models import Revision
674
675     # FIXME: check permissions
676
677     doc = get_object_or_404(Document, pk=pk, deleted=False)
678     doc.publish_log.all().delete()
679     if request.is_ajax():
680         return http.HttpResponse('ok')
681     else:
682         return redirect('catalogue_html', doc.pk)
683
684
685
686 class GalleryMixin(object):
687     def get_directory(self):
688         #return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
689         return "uploads/%d/" % (self.doc.pk)
690
691
692 class GalleryView(GalleryMixin, UploadView):
693
694     def breadcrumbs(self):
695         return [
696                 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
697             ]
698
699     def get_object(self, request, pk=None):
700         self.doc = Document.objects.get(pk=pk, deleted=False)
701
702
703 class GalleryPackageView(GalleryMixin, PackageView):
704
705     def get_redirect_url(self, slug):
706         return reverse('catalogue_book_gallery', kwargs = dict(slug=slug))
707
708 @login_required
709 def fork(request, pk):
710     doc = get_object_or_404(Document, pk=pk, deleted=False)
711     if request.method == "POST":
712         form = forms.DocumentForkForm(request.POST, request.FILES)
713         if form.is_valid():
714             
715             if request.user.is_authenticated():
716                 creator = request.user
717             else:
718                 creator = None
719
720             try:
721                 org = request.user.membership_set.get(
722                     organization=int(form.cleaned_data['owner_organization'])).organization
723                 kwargs = {'owner_organization': org}
724             except:
725                 kwargs = {'owner_user': request.user}
726
727             new_doc = Document.objects.create(revision=doc.revision, **kwargs)
728
729             if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % (doc.pk)):
730                 shutil.copytree(
731                     settings.MEDIA_ROOT + "uploads/%d" % (doc.pk),
732                     settings.MEDIA_ROOT + "uploads/%d" % (new_doc.pk)
733                 )
734
735             new_doc.assigned_to = request.user
736             new_doc.save()
737
738             return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
739     else:
740         form = forms.DocumentForkForm()
741
742     return render(request, "catalogue/document_fork.html", {
743         "form": form,
744
745         "logout_to": '/',
746     })
747
748
749 def upcoming(request):
750     return render(request, "catalogue/upcoming.html", {
751         'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
752     })
753
754 def finished(request):
755     return render(request, "catalogue/finished.html", {
756         'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),
757     })