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.filters import DocumentFilterSet
28 from catalogue.forms import TagMultipleForm, TagSingleForm
29 from catalogue.helpers import active_tab
30 from catalogue.models import Category
31 from librarian import BuildError
32 from redakcja.utlis import send_notify_email
33 from .constants import STAGES
34 from .models import Document, Plan
35 from dvcs.models import Revision
36 from organizations.models import Organization
37 from fileupload.views import UploadView
40 # Quick hack around caching problems, TODO: use ETags
42 from django.views.decorators.cache import never_cache
43 # from fnpdjango.utils.text.slughifi import slughifi
45 logger = logging.getLogger("fnp.catalogue")
49 def user(request, username):
50 user = get_object_or_404(User, username=username)
51 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
58 return render(request, 'catalogue/my_page.html', {
60 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
67 def logout_then_redirect(request):
69 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
74 def create_missing(request):
75 if request.method == "POST":
76 form = forms.DocumentCreateForm(request.POST, request.FILES)
78 (TagMultipleForm if category.multiple else TagSingleForm)(
79 category=category, data=request.POST, prefix=category.dc_tag)
80 for category in Category.objects.all()]
81 if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms):
83 if request.user.is_authenticated():
84 creator = request.user
88 title = form.cleaned_data['title']
90 org = request.user.membership_set.get(
91 organization=int(form.cleaned_data['owner_organization'])).organization
92 kwargs = {'owner_organization': org}
94 kwargs = {'owner_user': request.user}
96 doc = Document.objects.create(**kwargs)
98 for tag_form in tag_forms:
99 tag_form.save(instance=doc)
101 cover = request.FILES.get('cover')
103 uppath = 'uploads/%d/' % doc.pk
104 path = settings.MEDIA_ROOT + uppath
105 if not os.path.isdir(path):
107 cover.name = unidecode(cover.name)
108 dest_path = path + cover.name
109 if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
111 with open(dest_path, 'w') as destination:
112 for chunk in cover.chunks():
113 destination.write(chunk)
114 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
119 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
121 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
122 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
123 ''' + '\n'.join(tag_form.metadata_rows() for tag_form in tag_forms) + '''
124 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
126 <header>''' + title + '''</header>
127 <div class="p"> </div>
131 doc.assigned_to = request.user
134 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
136 org_pk = request.GET.get('organization')
139 org = Organization.objects.get(pk=org_pk)
140 except Organization.DoesNotExist:
143 if not org.is_member(request.user):
150 form = forms.DocumentCreateForm(initial={'owner_organization': org})
153 (TagMultipleForm if category.multiple else TagSingleForm)(
154 category=category, tutorial_no=i, prefix=category.dc_tag)
155 for i, category in enumerate(Category.objects.all(), start=2)]
157 return render(request, "catalogue/document_create_missing.html", {
159 "tag_forms": tag_forms,
166 def book_html(request, pk, rev_pk=None, preview=False):
167 from librarian.document import Document as SST
168 from librarian.formats.html import HtmlFormat
170 doc = get_object_or_404(Document, pk=pk, deleted=False)
173 published_revision = doc.publish_log.all()[0].revision
175 published_revision = None
179 revision = doc.revision
181 if published_revision is not None:
182 revision = published_revision
184 # No published version, fallback to preview mode.
186 revision = doc.revision
188 revision = get_object_or_404(Revision, pk=rev_pk)
190 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
193 sst = SST.from_string(revision.materialize())
194 except ValueError as e:
197 html = HtmlFormat(sst).build(
198 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
200 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
203 # for fragment in book.fragments.all().iterator():
204 # for theme in fragment.tags.filter(category='theme').iterator():
205 # book_themes.setdefault(theme, []).append(fragment)
207 # book_themes = book_themes.items()
208 # book_themes.sort(key=lambda s: s[0].sort_key)
209 return render(request, 'catalogue/book_text.html', {
212 'revision': revision,
213 'published_revision': published_revision,
214 'specific': rev_pk is not None,
216 'can_edit': doc.can_edit(request.user) if doc else None,
217 'was_published': was_published,
222 def book_pdf(request, pk, rev_pk):
223 from librarian.utils import Context
224 from librarian.document import Document as SST
225 from librarian.formats.pdf import PdfFormat
227 doc = get_object_or_404(Document, pk=pk)
228 rev = get_object_or_404(Revision, pk=rev_pk)
232 sst = SST.from_string(rev.materialize())
233 except ValueError as e:
234 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
237 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
238 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
240 if doc.owner_organization is not None and doc.owner_organization.logo:
241 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
243 pdf_file = PdfFormat(sst).build(ctx)
244 except BuildError as e:
245 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
247 from catalogue.ebook_utils import serve_file
248 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
252 def book_epub(request, pk, rev_pk):
253 from librarian.utils import Context
254 from librarian.document import Document as SST
255 from librarian.formats.epub import EpubFormat
257 doc = get_object_or_404(Document, pk=pk)
258 rev = get_object_or_404(Revision, pk=rev_pk)
262 sst = SST.from_string(rev.materialize())
263 except ValueError as e:
264 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
267 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
268 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
270 if doc.owner_organization is not None and doc.owner_organization.logo:
271 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
273 epub_file = EpubFormat(sst).build(ctx)
274 except BuildError as e:
275 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
277 from catalogue.ebook_utils import serve_file
278 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
282 def book_mobi(request, pk, rev_pk):
283 from librarian.utils import Context
284 from librarian.document import Document as SST
285 from librarian.formats.epub import EpubFormat
287 doc = get_object_or_404(Document, pk=pk)
288 rev = get_object_or_404(Revision, pk=rev_pk)
291 sst = SST.from_string(rev.materialize())
292 except ValueError as e:
293 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
296 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
297 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
299 if doc.owner_organization is not None and doc.owner_organization.logo:
300 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
302 epub_file = EpubFormat(sst).build(ctx)
303 except BuildError as e:
304 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
306 output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
308 subprocess.check_call(
309 ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
311 from catalogue.ebook_utils import serve_file
312 return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
316 # def revision(request, slug, chunk=None):
318 # doc = Chunk.get(slug, chunk)
319 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
321 # if not doc.book.accessible(request):
322 # return HttpResponseForbidden("Not authorized.")
323 # return http.HttpResponse(str(doc.revision()))
327 def book_schedule(request, pk):
328 book = get_object_or_404(Document, pk=pk, deleted=False)
329 if not book.can_edit(request.user):
330 return HttpResponseForbidden("Not authorized.")
331 if request.method == 'POST':
332 Plan.objects.filter(document=book).delete()
333 for i, (s, name) in enumerate(STAGES):
334 user_id = request.POST.get('s%d-user' % i)
335 deadline = request.POST.get('s%d-deadline' % i) or None
336 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
338 book.set_stage(request.POST.get('stage', ''))
339 return redirect('catalogue_user')
342 for p in Plan.objects.filter(document=book):
343 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
345 schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
347 if book.owner_organization:
348 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
350 people = [book.owner_user]
351 return render(request, 'catalogue/book_schedule.html', {
353 'schedule': schedule,
359 def book_owner(request, pk):
360 doc = get_object_or_404(Document, pk=pk, deleted=False)
361 if not doc.can_edit(request.user):
362 return HttpResponseForbidden("Not authorized.")
366 if request.method == 'POST':
368 new_org_pk = request.POST.get('owner_organization')
370 doc.owner_organization = None
371 doc.owner_user = request.user
374 org = Organization.objects.get(pk=new_org_pk)
375 if not org.is_member(request.user):
376 error = 'Bad organization'
378 doc.owner_organization = org
379 doc.owner_user = None
382 return redirect('catalogue_user')
384 return render(request, 'catalogue/book_owner.html', {
391 def book_delete(request, pk):
392 doc = get_object_or_404(Document, pk=pk, deleted=False)
393 if not doc.can_edit(request.user):
394 return HttpResponseForbidden("Not authorized.")
396 if request.method == 'POST':
399 return redirect('catalogue_user')
401 return render(request, 'catalogue/book_delete.html', {
408 def publish(request, pk):
409 from wiki import forms
410 from .models import PublishRecord
411 from dvcs.models import Revision
413 doc = get_object_or_404(Document, pk=pk, deleted=False)
414 if not doc.can_edit(request.user):
415 return HttpResponseForbidden("Not authorized.")
416 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
418 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
419 # FIXME: check if in tree
420 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
421 # return http.HttpResponse('exists')
422 if not doc.published:
423 site = Site.objects.get_current()
425 'New published document in MIL/PEER',
426 '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
429 MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
430 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
433 if request.is_ajax():
434 return http.HttpResponse('ok')
436 return redirect('catalogue_html', doc.pk)
438 if request.is_ajax():
439 return http.HttpResponse('error')
442 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
444 return redirect('catalogue_preview', doc.pk)
449 def unpublish(request, pk):
450 doc = get_object_or_404(Document, pk=pk, deleted=False)
451 if not doc.can_edit(request.user):
452 return HttpResponseForbidden("Not authorized.")
454 doc.publish_log.all().delete()
455 if request.is_ajax():
456 return http.HttpResponse('ok')
458 return redirect('catalogue_html', doc.pk)
461 class GalleryMixin(object):
462 def get_directory(self):
463 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
464 return "uploads/%d/" % self.doc.pk
467 class GalleryView(GalleryMixin, UploadView):
469 def breadcrumbs(self):
471 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
474 def get_object(self, request, pk=None):
475 self.doc = Document.objects.get(pk=pk, deleted=False)
479 def fork(request, pk):
480 doc = get_object_or_404(Document, pk=pk, deleted=False)
481 if request.method == "POST":
482 form = forms.DocumentForkForm(request.POST, request.FILES)
485 org = request.user.membership_set.get(
486 organization=int(form.cleaned_data['owner_organization'])).organization
487 kwargs = {'owner_organization': org}
489 kwargs = {'owner_user': request.user}
491 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
493 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
495 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
496 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
499 new_doc.assigned_to = request.user
502 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
504 form = forms.DocumentForkForm()
506 return render(request, "catalogue/document_fork.html", {
513 def upcoming(request):
514 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).filter(publish_log=None))
515 return render(request, "catalogue/upcoming.html", {
517 'link_url': 'catalogue_preview',
521 def finished(request):
522 f = DocumentFilterSet(request.GET, queryset=Document.objects.filter(deleted=False).exclude(publish_log=None))
523 return render(request, "catalogue/finished.html", {
525 'link_url': 'catalogue_html',