1 # -*- coding: utf-8 -*-
 
   3 # This file is part of MIL/PEER, licensed under GNU Affero GPLv3 or later.
 
   4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 
  10 from tempfile import NamedTemporaryFile
 
  12 from django.conf import settings
 
  13 from django.contrib import auth
 
  14 from django.contrib.auth.models import User
 
  15 from django.contrib.auth.decorators import login_required
 
  16 from django.contrib.sites.models import Site
 
  17 from django.core.urlresolvers import reverse
 
  18 from django import http
 
  19 from django.http import Http404, HttpResponse, HttpResponseForbidden
 
  20 from django.shortcuts import get_object_or_404, render, redirect
 
  21 from django.utils.encoding import force_str
 
  22 from django.utils.http import urlquote_plus
 
  23 from django.views.decorators.http import require_POST
 
  24 from unidecode import unidecode
 
  26 from catalogue import forms
 
  27 from catalogue.forms import TagMultipleForm, TagSingleForm
 
  28 from catalogue.helpers import active_tab
 
  29 from catalogue.models import Category
 
  30 from librarian import BuildError
 
  31 from redakcja.utlis import send_notify_email
 
  32 from .constants import STAGES
 
  33 from .models import Document, Plan
 
  34 from dvcs.models import Revision
 
  35 from organizations.models import Organization
 
  36 from fileupload.views import UploadView
 
  39 # Quick hack around caching problems, TODO: use ETags
 
  41 from django.views.decorators.cache import never_cache
 
  42 # from fnpdjango.utils.text.slughifi import slughifi
 
  44 logger = logging.getLogger("fnp.catalogue")
 
  48 def user(request, username):
 
  49     user = get_object_or_404(User, username=username)
 
  50     return render(request, 'catalogue/user_page.html', {"viewed_user": user})
 
  57     return render(request, 'catalogue/my_page.html', {
 
  59             request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
 
  66 def logout_then_redirect(request):
 
  68     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
 
  73 def create_missing(request):
 
  74     if request.method == "POST":
 
  75         form = forms.DocumentCreateForm(request.POST, request.FILES)
 
  77             (TagMultipleForm if category.multiple else TagSingleForm)(
 
  78                 category=category, data=request.POST, prefix=category.dc_tag)
 
  79             for category in Category.objects.all()]
 
  80         if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms):
 
  82             if request.user.is_authenticated():
 
  83                 creator = request.user
 
  87             title = form.cleaned_data['title']
 
  89                 org = request.user.membership_set.get(
 
  90                     organization=int(form.cleaned_data['owner_organization'])).organization
 
  91                 kwargs = {'owner_organization': org}
 
  93                 kwargs = {'owner_user': request.user}
 
  95             doc = Document.objects.create(**kwargs)
 
  97             for tag_form in tag_forms:
 
  98                 tag_form.save(instance=doc)
 
 100             cover = request.FILES.get('cover')
 
 102                 uppath = 'uploads/%d/' % doc.pk
 
 103                 path = settings.MEDIA_ROOT + uppath
 
 104                 if not os.path.isdir(path):
 
 106                 cover.name = unidecode(cover.name)
 
 107                 dest_path = path + cover.name
 
 108                 if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
 
 110                 with open(dest_path, 'w') as destination:
 
 111                     for chunk in cover.chunks():
 
 112                         destination.write(chunk)
 
 113                 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
 
 118                 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
 
 120                     <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
 
 121                     <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
 
 122                     ''' + '\n'.join(tag_form.metadata_rows() for tag_form in tag_forms) + '''
 
 123                     <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
 
 125                 <header>''' + title + '''</header>
 
 126                 <div class="p"> </div>
 
 130             doc.assigned_to = request.user
 
 133             return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
 
 135         org_pk = request.GET.get('organization')
 
 138                 org = Organization.objects.get(pk=org_pk)
 
 139             except Organization.DoesNotExist:
 
 142                 if not org.is_member(request.user):
 
 149         form = forms.DocumentCreateForm(initial={'owner_organization': org})
 
 152             (TagMultipleForm if category.multiple else TagSingleForm)(
 
 153                 category=category, tutorial_no=i, prefix=category.dc_tag)
 
 154             for i, category in enumerate(Category.objects.all(), start=2)]
 
 156     return render(request, "catalogue/document_create_missing.html", {
 
 158         "tag_forms": tag_forms,
 
 165 def book_html(request, pk, rev_pk=None, preview=False):
 
 166     from librarian.document import Document as SST
 
 167     from librarian.formats.html import HtmlFormat
 
 169     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 172         published_revision = doc.publish_log.all()[0].revision
 
 174         published_revision = None
 
 178             revision = doc.revision
 
 180             if published_revision is not None:
 
 181                 revision = published_revision
 
 183                 # No published version, fallback to preview mode.
 
 185                 revision = doc.revision
 
 187         revision = get_object_or_404(Revision, pk=rev_pk)
 
 189     was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
 
 192         sst = SST.from_string(revision.materialize())
 
 193     except ValueError as e:
 
 196         html = HtmlFormat(sst).build(
 
 197             files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
 
 199     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
 
 202     # for fragment in book.fragments.all().iterator():
 
 203     #     for theme in fragment.tags.filter(category='theme').iterator():
 
 204     #         book_themes.setdefault(theme, []).append(fragment)
 
 206     # book_themes = book_themes.items()
 
 207     # book_themes.sort(key=lambda s: s[0].sort_key)
 
 208     return render(request, 'catalogue/book_text.html', {
 
 211         'revision': revision,
 
 212         'published_revision': published_revision,
 
 213         'specific': rev_pk is not None,
 
 215         'can_edit': doc.can_edit(request.user) if doc else None,
 
 216         'was_published': was_published,
 
 221 def book_pdf(request, pk, rev_pk):
 
 222     from librarian.utils import Context
 
 223     from librarian.document import Document as SST
 
 224     from librarian.formats.pdf import PdfFormat
 
 226     doc = get_object_or_404(Document, pk=pk)
 
 227     rev = get_object_or_404(Revision, pk=rev_pk)
 
 231         sst = SST.from_string(rev.materialize())
 
 232     except ValueError as e:
 
 233         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 236         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
 
 237         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
 
 239     if doc.owner_organization is not None and doc.owner_organization.logo:
 
 240         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
 
 242         pdf_file = PdfFormat(sst).build(ctx)
 
 243     except BuildError as e:
 
 244         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 246     from catalogue.ebook_utils import serve_file
 
 247     return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
 
 251 def book_epub(request, pk, rev_pk):
 
 252     from librarian.utils import Context
 
 253     from librarian.document import Document as SST
 
 254     from librarian.formats.epub import EpubFormat
 
 256     doc = get_object_or_404(Document, pk=pk)
 
 257     rev = get_object_or_404(Revision, pk=rev_pk)
 
 261         sst = SST.from_string(rev.materialize())
 
 262     except ValueError as e:
 
 263         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 266         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
 
 267         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
 
 269     if doc.owner_organization is not None and doc.owner_organization.logo:
 
 270         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
 
 272         epub_file = EpubFormat(sst).build(ctx)
 
 273     except BuildError as e:
 
 274         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 276     from catalogue.ebook_utils import serve_file
 
 277     return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
 
 281 def book_mobi(request, pk, rev_pk):
 
 282     from librarian.utils import Context
 
 283     from librarian.document import Document as SST
 
 284     from librarian.formats.epub import EpubFormat
 
 286     doc = get_object_or_404(Document, pk=pk)
 
 287     rev = get_object_or_404(Revision, pk=rev_pk)
 
 290         sst = SST.from_string(rev.materialize())
 
 291     except ValueError as e:
 
 292         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 295         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
 
 296         source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
 
 298     if doc.owner_organization is not None and doc.owner_organization.logo:
 
 299         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
 
 301         epub_file = EpubFormat(sst).build(ctx)
 
 302     except BuildError as e:
 
 303         return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
 305     output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
 
 307     subprocess.check_call(
 
 308         ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
 
 310     from catalogue.ebook_utils import serve_file
 
 311     return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
 
 315 # def revision(request, slug, chunk=None):
 
 317 #         doc = Chunk.get(slug, chunk)
 
 318 #     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
 
 320 #     if not doc.book.accessible(request):
 
 321 #         return HttpResponseForbidden("Not authorized.")
 
 322 #     return http.HttpResponse(str(doc.revision()))
 
 326 def book_schedule(request, pk):
 
 327     book = get_object_or_404(Document, pk=pk, deleted=False)
 
 328     if not book.can_edit(request.user):
 
 329         return HttpResponseForbidden("Not authorized.")
 
 330     if request.method == 'POST':
 
 331         Plan.objects.filter(document=book).delete()
 
 332         for i, (s, name) in enumerate(STAGES):
 
 333             user_id = request.POST.get('s%d-user' % i)
 
 334             deadline = request.POST.get('s%d-deadline' % i) or None
 
 335             Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
 
 337         book.set_stage(request.POST.get('stage', ''))
 
 338         return redirect('catalogue_user')
 
 341     for p in Plan.objects.filter(document=book):
 
 342         current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
 
 344     schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
 
 346     if book.owner_organization:
 
 347         people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
 
 349         people = [book.owner_user]
 
 350     return render(request, 'catalogue/book_schedule.html', {
 
 352         'schedule': schedule,
 
 358 def book_owner(request, pk):
 
 359     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 360     if not doc.can_edit(request.user):
 
 361         return HttpResponseForbidden("Not authorized.")
 
 365     if request.method == 'POST':
 
 367         new_org_pk = request.POST.get('owner_organization')
 
 369             doc.owner_organization = None
 
 370             doc.owner_user = request.user
 
 373             org = Organization.objects.get(pk=new_org_pk)
 
 374             if not org.is_member(request.user):
 
 375                 error = 'Bad organization'
 
 377                 doc.owner_organization = org
 
 378                 doc.owner_user = None
 
 381             return redirect('catalogue_user')
 
 383     return render(request, 'catalogue/book_owner.html', {
 
 390 def book_delete(request, pk):
 
 391     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 392     if not doc.can_edit(request.user):
 
 393         return HttpResponseForbidden("Not authorized.")
 
 395     if request.method == 'POST':
 
 398         return redirect('catalogue_user')
 
 400     return render(request, 'catalogue/book_delete.html', {
 
 407 def publish(request, pk):
 
 408     from wiki import forms
 
 409     from .models import PublishRecord
 
 410     from dvcs.models import Revision
 
 412     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 413     if not doc.can_edit(request.user):
 
 414         return HttpResponseForbidden("Not authorized.")
 
 415     form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
 
 417         rev = Revision.objects.get(pk=form.cleaned_data['revision'])
 
 418         # FIXME: check if in tree
 
 419         # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
 
 420         #     return http.HttpResponse('exists')
 
 421         if not doc.published:
 
 422             site = Site.objects.get_current()
 
 424                 'New published document in MIL/PEER',
 
 425                 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
 
 428 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
 
 429         PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
 
 432         if request.is_ajax():
 
 433             return http.HttpResponse('ok')
 
 435             return redirect('catalogue_html', doc.pk)
 
 437         if request.is_ajax():
 
 438             return http.HttpResponse('error')
 
 441                 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
 
 443                 return redirect('catalogue_preview', doc.pk)
 
 448 def unpublish(request, pk):
 
 449     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 450     if not doc.can_edit(request.user):
 
 451         return HttpResponseForbidden("Not authorized.")
 
 453     doc.publish_log.all().delete()
 
 454     if request.is_ajax():
 
 455         return http.HttpResponse('ok')
 
 457         return redirect('catalogue_html', doc.pk)
 
 460 class GalleryMixin(object):
 
 461     def get_directory(self):
 
 462         # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
 
 463         return "uploads/%d/" % self.doc.pk
 
 466 class GalleryView(GalleryMixin, UploadView):
 
 468     def breadcrumbs(self):
 
 470                 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
 
 473     def get_object(self, request, pk=None):
 
 474         self.doc = Document.objects.get(pk=pk, deleted=False)
 
 478 def fork(request, pk):
 
 479     doc = get_object_or_404(Document, pk=pk, deleted=False)
 
 480     if request.method == "POST":
 
 481         form = forms.DocumentForkForm(request.POST, request.FILES)
 
 484                 org = request.user.membership_set.get(
 
 485                     organization=int(form.cleaned_data['owner_organization'])).organization
 
 486                 kwargs = {'owner_organization': org}
 
 488                 kwargs = {'owner_user': request.user}
 
 490             new_doc = Document.objects.create(revision=doc.revision, **kwargs)
 
 492             if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
 
 494                     settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
 
 495                     settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
 
 498             new_doc.assigned_to = request.user
 
 501             return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
 
 503         form = forms.DocumentForkForm()
 
 505     return render(request, "catalogue/document_fork.html", {
 
 512 def upcoming(request):
 
 513     return render(request, "catalogue/upcoming.html", {
 
 514         'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
 
 518 def finished(request):
 
 519     return render(request, "catalogue/finished.html", {
 
 520         'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),