1 # -*- coding: utf-8 -*-
2 from datetime import date, timedelta
7 from django.conf import settings
8 from django.contrib import auth
9 from django.contrib.auth.models import User
10 from django.contrib.auth.decorators import login_required
11 from django.core.urlresolvers import reverse
12 from django.db.models import Count
13 from django import http
14 from django.http import Http404
15 from django.shortcuts import get_object_or_404, render, redirect
16 from django.utils.encoding import force_str
17 from django.utils.http import urlquote_plus
18 from django.views.decorators.http import require_POST
20 from catalogue import forms
21 from catalogue import helpers
22 from catalogue.helpers import active_tab
23 from librarian import BuildError
24 from .constants import STAGES
25 from .models import Document, Plan
26 from dvcs.models import Revision
27 from organizations.models import Organization
28 from fileupload.views import UploadView, PackageView
31 # Quick hack around caching problems, TODO: use ETags
33 from django.views.decorators.cache import never_cache
34 # from fnpdjango.utils.text.slughifi import slughifi
36 logger = logging.getLogger("fnp.catalogue")
41 def document_list(request):
42 return render(request, 'catalogue/document_list.html')
46 def user(request, username):
47 user = get_object_or_404(User, username=username)
48 return render(request, 'catalogue/user_page.html', {"viewed_user": user})
55 return render(request, 'catalogue/my_page.html', {
57 request.session.get("wiki_last_books", {}).items(), key=lambda x: x[1]['time'], reverse=True),
65 return render(request, 'catalogue/user_list.html', {
66 'users': User.objects.all().annotate(count=Count('chunk')).order_by(
67 '-count', 'last_name', 'first_name'),
71 @active_tab('activity')
72 def activity(request, isodate=None):
75 day = helpers.parse_isodate(isodate)
82 next_day = day + timedelta(1)
83 prev_day = day - timedelta(1)
85 return render(request, 'catalogue/activity.html', locals())
89 def logout_then_redirect(request):
91 return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
94 # @permission_required('catalogue.add_book')
97 def create_missing(request):
98 if request.method == "POST":
99 form = forms.DocumentCreateForm(request.POST, request.FILES)
102 if request.user.is_authenticated():
103 creator = request.user
107 title = form.cleaned_data['title']
109 org = request.user.membership_set.get(
110 organization=int(form.cleaned_data['owner_organization'])).organization
111 kwargs = {'owner_organization': org}
113 kwargs = {'owner_user': request.user}
115 doc = Document.objects.create(**kwargs)
117 cover = request.FILES.get('cover')
119 uppath = 'uploads/%d/' % doc.pk
120 path = settings.MEDIA_ROOT + uppath
121 if not os.path.isdir(path):
123 dest_path = path + cover.name # UNSAFE
124 with open(dest_path, 'w') as destination:
125 for chunk in cover.chunks():
126 destination.write(chunk)
127 cover_url = 'http://milpeer.eu/media/dynamic/' + uppath + cover.name
132 text='''<section xmlns="http://nowoczesnapolska.org.pl/sst#" xmlns:dc="http://purl.org/dc/elements/1.1/">
134 <dc:publisher>''' + form.cleaned_data['publisher'] + '''</dc:publisher>
135 <dc:description>''' + form.cleaned_data['description'] + '''</dc:description>
136 <dc:language>''' + form.cleaned_data['language'] + '''</dc:language>
137 <dc:rights>''' + form.cleaned_data['rights'] + '''</dc:rights>
138 <dc:audience>''' + form.cleaned_data['audience'] + '''</dc:audience>
139 <dc:relation.coverImage.url>''' + cover_url + '''</dc:relation.coverImage.url>
141 <header>''' + title + '''</header>
142 <div class="p"> </div>
146 doc.assigned_to = request.user
149 return http.HttpResponseRedirect(reverse("wiki_editor", args=[doc.pk]))
151 org_pk = request.GET.get('organization')
154 org = Organization.objects.get(pk=org_pk)
155 except Organization.DoesNotExist:
158 if not org.is_member(request.user):
165 form = forms.DocumentCreateForm(initial={'owner_organization': org})
167 return render(request, "catalogue/document_create_missing.html", {
174 # @permission_required('catalogue.add_book')
175 # @active_tab('upload')
176 # def upload(request):
177 # if request.method == "POST":
178 # form = forms.DocumentsUploadForm(request.POST, request.FILES)
179 # if form.is_valid():
182 # if request.user.is_authenticated():
183 # creator = request.user
187 # zip = form.cleaned_data['zip']
192 # existing = [book.slug for book in Book.objects.all()]
193 # for filename in zip.namelist():
194 # if filename[-1] == '/':
196 # title = os.path.basename(filename)[:-4]
197 # slug = slughifi(title)
198 # if not (slug and filename.endswith('.xml')):
199 # skipped_list.append(filename)
200 # elif slug in slugs:
201 # error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
202 # elif slug in existing:
203 # error_list.append((filename, slug, _('Slug already used in repository.')))
206 # zip.read(filename).decode('utf-8') # test read
207 # ok_list.append((filename, slug, title))
208 # except UnicodeDecodeError:
209 # error_list.append((filename, title, _('File should be UTF-8 encoded.')))
210 # slugs[slug] = filename
213 # for filename, slug, title in ok_list:
214 # book = Book.create(
215 # text=zip.read(filename).decode('utf-8'),
221 # return render(request, "catalogue/document_upload.html", {
223 # "ok_list": ok_list,
224 # "skipped_list": skipped_list,
225 # "error_list": error_list,
230 # form = forms.DocumentsUploadForm()
232 # return render(request, "catalogue/document_upload.html", {
240 # def book_xml(request, slug):
241 # book = get_object_or_404(Book, slug=slug)
242 # if not book.accessible(request):
243 # return HttpResponseForbidden("Not authorized.")
244 # xml = book.materialize()
246 # response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
247 # response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
252 # def book_txt(request, slug):
253 # book = get_object_or_404(Book, slug=slug)
254 # if not book.accessible(request):
255 # return HttpResponseForbidden("Not authorized.")
257 # doc = book.wldocument()
258 # text = doc.as_text().get_string()
259 # response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
260 # response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
265 def book_html(request, pk, rev_pk=None, preview=False):
266 from librarian.document import Document as SST
267 from librarian.formats.html import HtmlFormat
269 doc = get_object_or_404(Document, pk=pk, deleted=False)
272 published_revision = doc.publish_log.all()[0].revision
274 published_revision = None
278 revision = doc.revision
280 if published_revision is not None:
281 revision = published_revision
283 # No published version, fallback to preview mode.
285 revision = doc.revision
287 revision = get_object_or_404(Revision, pk=rev_pk)
289 was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
291 sst = SST.from_string(revision.materialize())
292 html = HtmlFormat(sst).build(
293 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
295 # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
298 # for fragment in book.fragments.all().iterator():
299 # for theme in fragment.tags.filter(category='theme').iterator():
300 # book_themes.setdefault(theme, []).append(fragment)
302 # book_themes = book_themes.items()
303 # book_themes.sort(key=lambda s: s[0].sort_key)
304 return render(request, 'catalogue/book_text.html', {
307 'revision': revision,
308 'published_revision': published_revision,
309 'specific': rev_pk is not None,
311 'can_edit': doc.can_edit(request.user) if doc else None,
312 'was_published': was_published,
317 def book_pdf(request, pk, rev_pk):
318 from librarian.utils import Context
319 from librarian.document import Document as SST
320 from librarian.formats.pdf import PdfFormat
322 doc = get_object_or_404(Document, pk=pk)
323 rev = get_object_or_404(Revision, pk=rev_pk)
326 sst = SST.from_string(rev.materialize())
329 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
330 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
332 if doc.owner_organization is not None and doc.owner_organization.logo:
333 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
334 pdf_file = PdfFormat(sst).build(ctx)
336 from catalogue.ebook_utils import serve_file
337 return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
341 def book_epub(request, pk, rev_pk):
342 from librarian.utils import Context
343 from librarian.document import Document as SST
344 from librarian.formats.epub import EpubFormat
346 doc = get_object_or_404(Document, pk=pk)
347 rev = get_object_or_404(Revision, pk=rev_pk)
350 sst = SST.from_string(rev.materialize())
353 files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
354 source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
356 if doc.owner_organization is not None and doc.owner_organization.logo:
357 ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
359 epub_file = EpubFormat(sst).build(ctx)
360 except BuildError as e:
361 from django.http import HttpResponse
362 return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
364 from catalogue.ebook_utils import serve_file
365 return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
369 # def revision(request, slug, chunk=None):
371 # doc = Chunk.get(slug, chunk)
372 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
374 # if not doc.book.accessible(request):
375 # return HttpResponseForbidden("Not authorized.")
376 # return http.HttpResponse(str(doc.revision()))
380 def book_schedule(request, pk):
381 book = get_object_or_404(Document, pk=pk, deleted=False)
382 if request.method == 'POST':
383 Plan.objects.filter(document=book).delete()
384 for i, s in enumerate(STAGES):
385 user_id = request.POST.get('s%d-user' % i)
386 deadline = request.POST.get('s%d-deadline' % i) or None
387 Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
389 book.set_stage(request.POST.get('stage', ''))
390 return redirect('catalogue_user')
393 for p in Plan.objects.filter(document=book):
394 current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
396 schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
398 if book.owner_organization:
399 people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
401 people = [book.owner_user]
402 return render(request, 'catalogue/book_schedule.html', {
404 'schedule': schedule,
410 def book_owner(request, pk):
411 doc = get_object_or_404(Document, pk=pk, deleted=False)
412 user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
413 if not (doc.owner_user == request.user or user_is_owner):
418 if request.method == 'POST':
420 new_org_pk = request.POST.get('owner_organization')
422 doc.owner_organization = None
423 doc.owner_user = request.user
426 org = Organization.objects.get(pk=new_org_pk)
427 if not org.is_member(request.user):
428 error = 'Bad organization'
430 doc.owner_organization = org
431 doc.owner_user = None
434 return redirect('catalogue_user')
436 return render(request, 'catalogue/book_owner.html', {
443 def book_delete(request, pk):
444 doc = get_object_or_404(Document, pk=pk, deleted=False)
445 if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
448 if request.method == 'POST':
451 return redirect('catalogue_user')
453 return render(request, 'catalogue/book_delete.html', {
458 # def book(request, slug):
459 # book = get_object_or_404(Book, slug=slug)
460 # if not book.accessible(request):
461 # return HttpResponseForbidden("Not authorized.")
463 # if request.user.has_perm('catalogue.change_book'):
464 # if request.method == "POST":
465 # form = forms.BookForm(request.POST, instance=book)
466 # if form.is_valid():
468 # return http.HttpResponseRedirect(book.get_absolute_url())
470 # form = forms.BookForm(instance=book)
473 # form = forms.ReadonlyBookForm(instance=book)
476 # publish_error = book.publishable_error()
477 # publishable = publish_error is None
479 # return render(request, "catalogue/book_detail.html", {
481 # "publishable": publishable,
482 # "publishable_error": publish_error,
484 # "editable": editable,
488 # @permission_required('catalogue.add_chunk')
489 # def chunk_add(request, slug, chunk):
491 # doc = Chunk.get(slug, chunk)
492 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
494 # if not doc.book.accessible(request):
495 # return HttpResponseForbidden("Not authorized.")
497 # if request.method == "POST":
498 # form = forms.ChunkAddForm(request.POST, instance=doc)
499 # if form.is_valid():
500 # if request.user.is_authenticated():
501 # creator = request.user
504 # doc.split(creator=creator,
505 # slug=form.cleaned_data['slug'],
506 # title=form.cleaned_data['title'],
507 # gallery_start=form.cleaned_data['gallery_start'],
508 # user=form.cleaned_data['user'],
509 # stage=form.cleaned_data['stage']
512 # return http.HttpResponseRedirect(doc.book.get_absolute_url())
514 # form = forms.ChunkAddForm(initial={
515 # "slug": str(doc.number + 1),
516 # "title": "cz. %d" % (doc.number + 1, ),
519 # return render(request, "catalogue/chunk_add.html", {
526 # def chunk_edit(request, slug, chunk):
528 # doc = Chunk.get(slug, chunk)
529 # except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
531 # if not doc.book.accessible(request):
532 # return HttpResponseForbidden("Not authorized.")
534 # if request.method == "POST":
535 # form = forms.ChunkForm(request.POST, instance=doc)
536 # if form.is_valid():
538 # go_next = request.GET.get('next', None)
540 # go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
542 # go_next = doc.book.get_absolute_url()
543 # return http.HttpResponseRedirect(go_next)
545 # form = forms.ChunkForm(instance=doc)
547 # referer = request.META.get('HTTP_REFERER')
549 # parts = urlsplit(referer)
550 # parts = ['', ''] + list(parts[2:])
551 # go_next = urlquote_plus(urlunsplit(parts))
555 # return render(request, "catalogue/chunk_edit.html", {
558 # "go_next": go_next,
562 # @transaction.atomic
564 # def chunk_mass_edit(request):
565 # if request.method == 'POST':
566 # ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
567 # chunks = map(lambda i: Chunk.objects.get(id=i), ids)
569 # stage = request.POST.get('stage')
572 # stage = Chunk.tag_model.objects.get(slug=stage)
573 # except Chunk.DoesNotExist, e:
576 # for c in chunks: c.stage = stage
578 # username = request.POST.get('user')
579 # logger.info("username: %s" % username)
580 # logger.info(request.POST)
583 # user = User.objects.get(username=username)
584 # except User.DoesNotExist, e:
587 # for c in chunks: c.user = user
589 # status = request.POST.get('status')
591 # books_affected = set()
593 # if status == 'publish':
594 # c.head.publishable = True
596 # elif status == 'unpublish':
597 # c.head.publishable = False
600 # books_affected.add(c.book)
601 # for b in books_affected:
604 # project_id = request.POST.get('project')
607 # project = Project.objects.get(pk=int(project_id))
608 # except (Project.DoesNotExist, ValueError), e:
612 # book.project = project
615 # for c in chunks: c.save()
617 # return HttpResponse("", content_type="text/plain")
622 # @permission_required('catalogue.change_book')
623 # def book_append(request, slug):
624 # book = get_object_or_404(Book, slug=slug)
625 # if not book.accessible(request):
626 # return HttpResponseForbidden("Not authorized.")
628 # if request.method == "POST":
629 # form = forms.BookAppendForm(book, request.POST)
630 # if form.is_valid():
631 # append_to = form.cleaned_data['append_to']
632 # append_to.append(book)
633 # return http.HttpResponseRedirect(append_to.get_absolute_url())
635 # form = forms.BookAppendForm(book)
636 # return render(request, "catalogue/book_append_to.html", {
646 def publish(request, pk):
647 from wiki import forms
648 from .models import PublishRecord
649 from dvcs.models import Revision
651 # FIXME: check permissions
653 doc = get_object_or_404(Document, pk=pk, deleted=False)
654 form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
656 rev = Revision.objects.get(pk=form.cleaned_data['revision'])
657 # FIXME: check if in tree
658 # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
659 # return http.HttpResponse('exists')
660 PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
661 if request.is_ajax():
662 return http.HttpResponse('ok')
664 return redirect('catalogue_html', doc.pk)
666 if request.is_ajax():
667 return http.HttpResponse('error')
670 return redirect('catalogue_preview_rev', doc.pk, form.cleaned_data['revision'])
672 return redirect('catalogue_preview', doc.pk)
677 def unpublish(request, pk):
678 # FIXME: check permissions
680 doc = get_object_or_404(Document, pk=pk, deleted=False)
681 doc.publish_log.all().delete()
682 if request.is_ajax():
683 return http.HttpResponse('ok')
685 return redirect('catalogue_html', doc.pk)
688 class GalleryMixin(object):
689 def get_directory(self):
690 # return "%s%s/" % (settings.IMAGE_DIR, 'org%d' % self.org.pk if self.org is not None else self.request.user.pk)
691 return "uploads/%d/" % self.doc.pk
694 class GalleryView(GalleryMixin, UploadView):
696 def breadcrumbs(self):
698 (self.doc.meta()['title'], '/documents/%d/' % self.doc.pk),
701 def get_object(self, request, pk=None):
702 self.doc = Document.objects.get(pk=pk, deleted=False)
705 class GalleryPackageView(GalleryMixin, PackageView):
707 def get_redirect_url(self, slug):
708 return reverse('catalogue_book_gallery', kwargs=dict(slug=slug))
712 def fork(request, pk):
713 doc = get_object_or_404(Document, pk=pk, deleted=False)
714 if request.method == "POST":
715 form = forms.DocumentForkForm(request.POST, request.FILES)
718 org = request.user.membership_set.get(
719 organization=int(form.cleaned_data['owner_organization'])).organization
720 kwargs = {'owner_organization': org}
722 kwargs = {'owner_user': request.user}
724 new_doc = Document.objects.create(revision=doc.revision, **kwargs)
726 if os.path.isdir(settings.MEDIA_ROOT + "uploads/%d" % doc.pk):
728 settings.MEDIA_ROOT + "uploads/%d" % doc.pk,
729 settings.MEDIA_ROOT + "uploads/%d" % new_doc.pk
732 new_doc.assigned_to = request.user
735 return http.HttpResponseRedirect(reverse("wiki_editor", args=[new_doc.pk]))
737 form = forms.DocumentForkForm()
739 return render(request, "catalogue/document_fork.html", {
746 def upcoming(request):
747 return render(request, "catalogue/upcoming.html", {
748 'objects_list': Document.objects.filter(deleted=False).filter(publish_log=None),
752 def finished(request):
753 return render(request, "catalogue/finished.html", {
754 'objects_list': Document.objects.filter(deleted=False).exclude(publish_log=None),