tutorial and help texts for tags in new resource form
[redakcja.git] / apps / catalogue / views.py
index e436b0b..4c1a21c 100644 (file)
@@ -1,29 +1,39 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-from datetime import date, timedelta
+#
+# This file is part of MIL/PEER, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
 import logging
 import os
 import shutil
 import logging
 import os
 import shutil
+import subprocess
+from tempfile import NamedTemporaryFile
 
 from django.conf import settings
 from django.contrib import auth
 from django.contrib.auth.models import User
 from django.contrib.auth.decorators import login_required
 
 from django.conf import settings
 from django.contrib import auth
 from django.contrib.auth.models import User
 from django.contrib.auth.decorators import login_required
+from django.contrib.sites.models import Site
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
-from django.db.models import Count
 from django import http
 from django import http
-from django.http import Http404
+from django.http import Http404, HttpResponse, HttpResponseForbidden
 from django.shortcuts import get_object_or_404, render, redirect
 from django.shortcuts import get_object_or_404, render, redirect
+from django.utils.encoding import force_str
 from django.utils.http import urlquote_plus
 from django.views.decorators.http import require_POST
 from django.utils.http import urlquote_plus
 from django.views.decorators.http import require_POST
+from unidecode import unidecode
 
 from catalogue import forms
 
 from catalogue import forms
-from catalogue import helpers
+from catalogue.forms import TagMultipleForm, TagSingleForm
 from catalogue.helpers import active_tab
 from catalogue.helpers import active_tab
+from catalogue.models import Category
+from librarian import BuildError
+from redakcja.utlis import send_notify_email
 from .constants import STAGES
 from .models import Document, Plan
 from dvcs.models import Revision
 from organizations.models import Organization
 from .constants import STAGES
 from .models import Document, Plan
 from dvcs.models import Revision
 from organizations.models import Organization
-from fileupload.views import UploadView, PackageView
+from fileupload.views import UploadView
 
 #
 # Quick hack around caching problems, TODO: use ETags
 
 #
 # Quick hack around caching problems, TODO: use ETags
@@ -34,12 +44,6 @@ from django.views.decorators.cache import never_cache
 logger = logging.getLogger("fnp.catalogue")
 
 
 logger = logging.getLogger("fnp.catalogue")
 
 
-@active_tab('all')
-@never_cache
-def document_list(request):
-    return render(request, 'catalogue/document_list.html')
-
-
 @never_cache
 def user(request, username):
     user = get_object_or_404(User, username=username)
 @never_cache
 def user(request, username):
     user = get_object_or_404(User, username=username)
@@ -58,44 +62,22 @@ def my(request):
         })
 
 
         })
 
 
-@active_tab('users')
-def users(request):
-    return render(request, 'catalogue/user_list.html', {
-        'users': User.objects.all().annotate(count=Count('chunk')).order_by(
-            '-count', 'last_name', 'first_name'),
-    })
-
-
-@active_tab('activity')
-def activity(request, isodate=None):
-    today = date.today()
-    try:
-        day = helpers.parse_isodate(isodate)
-    except ValueError:
-        day = today
-
-    if day > today:
-        raise Http404
-    if day != today:
-        next_day = day + timedelta(1)
-    prev_day = day - timedelta(1)
-
-    return render(request, 'catalogue/activity.html', locals())
-
-
 @never_cache
 def logout_then_redirect(request):
     auth.logout(request)
     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
 
 
 @never_cache
 def logout_then_redirect(request):
     auth.logout(request)
     return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?='))
 
 
-# @permission_required('catalogue.add_book')
 @login_required
 @active_tab('create')
 def create_missing(request):
     if request.method == "POST":
         form = forms.DocumentCreateForm(request.POST, request.FILES)
 @login_required
 @active_tab('create')
 def create_missing(request):
     if request.method == "POST":
         form = forms.DocumentCreateForm(request.POST, request.FILES)
-        if form.is_valid():
+        tag_forms = [
+            (TagMultipleForm if category.multiple else TagSingleForm)(
+                category=category, data=request.POST, prefix=category.dc_tag)
+            for category in Category.objects.all()]
+        if form.is_valid() and all(tag_form.is_valid() for tag_form in tag_forms):
             
             if request.user.is_authenticated():
                 creator = request.user
             
             if request.user.is_authenticated():
                 creator = request.user
@@ -118,7 +100,10 @@ def create_missing(request):
                 path = settings.MEDIA_ROOT + uppath
                 if not os.path.isdir(path):
                     os.makedirs(path)
                 path = settings.MEDIA_ROOT + uppath
                 if not os.path.isdir(path):
                     os.makedirs(path)
-                dest_path = path + cover.name   # UNSAFE
+                cover.name = unidecode(cover.name)
+                dest_path = path + cover.name
+                if not os.path.abspath(dest_path).startswith(os.path.abspath(path)):
+                    raise Http404
                 with open(dest_path, 'w') as destination:
                     for chunk in cover.chunks():
                         destination.write(chunk)
                 with open(dest_path, 'w') as destination:
                     for chunk in cover.chunks():
                         destination.write(chunk)
@@ -162,103 +147,19 @@ def create_missing(request):
 
         form = forms.DocumentCreateForm(initial={'owner_organization': org})
 
 
         form = forms.DocumentCreateForm(initial={'owner_organization': org})
 
+        tag_forms = [
+            (TagMultipleForm if category.multiple else TagSingleForm)(
+                category=category, tutorial_no=i, prefix=category.dc_tag)
+            for i, category in enumerate(Category.objects.all(), start=2)]
+
     return render(request, "catalogue/document_create_missing.html", {
         "form": form,
     return render(request, "catalogue/document_create_missing.html", {
         "form": form,
+        "tag_forms": tag_forms,
 
         "logout_to": '/',
     })
 
 
 
         "logout_to": '/',
     })
 
 
-# @permission_required('catalogue.add_book')
-# @active_tab('upload')
-# def upload(request):
-#     if request.method == "POST":
-#         form = forms.DocumentsUploadForm(request.POST, request.FILES)
-#         if form.is_valid():
-#             import slughifi
-#
-#             if request.user.is_authenticated():
-#                 creator = request.user
-#             else:
-#                 creator = None
-#
-#             zip = form.cleaned_data['zip']
-#             skipped_list = []
-#             ok_list = []
-#             error_list = []
-#             slugs = {}
-#             existing = [book.slug for book in Book.objects.all()]
-#             for filename in zip.namelist():
-#                 if filename[-1] == '/':
-#                     continue
-#                 title = os.path.basename(filename)[:-4]
-#                 slug = slughifi(title)
-#                 if not (slug and filename.endswith('.xml')):
-#                     skipped_list.append(filename)
-#                 elif slug in slugs:
-#                     error_list.append((filename, slug, _('Slug already used for %s' % slugs[slug])))
-#                 elif slug in existing:
-#                     error_list.append((filename, slug, _('Slug already used in repository.')))
-#                 else:
-#                     try:
-#                         zip.read(filename).decode('utf-8') # test read
-#                         ok_list.append((filename, slug, title))
-#                     except UnicodeDecodeError:
-#                         error_list.append((filename, title, _('File should be UTF-8 encoded.')))
-#                     slugs[slug] = filename
-#
-#             if not error_list:
-#                 for filename, slug, title in ok_list:
-#                     book = Book.create(
-#                         text=zip.read(filename).decode('utf-8'),
-#                         creator=creator,
-#                         slug=slug,
-#                         title=title,
-#                     )
-#
-#             return render(request, "catalogue/document_upload.html", {
-#                 "form": form,
-#                 "ok_list": ok_list,
-#                 "skipped_list": skipped_list,
-#                 "error_list": error_list,
-#
-#                 "logout_to": '/',
-#             })
-#     else:
-#         form = forms.DocumentsUploadForm()
-#
-#     return render(request, "catalogue/document_upload.html", {
-#         "form": form,
-#
-#         "logout_to": '/',
-#     })
-
-
-# @never_cache
-# def book_xml(request, slug):
-#     book = get_object_or_404(Book, slug=slug)
-#     if not book.accessible(request):
-#         return HttpResponseForbidden("Not authorized.")
-#     xml = book.materialize()
-#
-#     response = http.HttpResponse(xml, content_type='application/xml', mimetype='application/wl+xml')
-#     response['Content-Disposition'] = 'attachment; filename=%s.xml' % slug
-#     return response
-
-
-# @never_cache
-# def book_txt(request, slug):
-#     book = get_object_or_404(Book, slug=slug)
-#     if not book.accessible(request):
-#         return HttpResponseForbidden("Not authorized.")
-#
-#     doc = book.wldocument()
-#     text = doc.as_text().get_string()
-#     response = http.HttpResponse(text, content_type='text/plain', mimetype='text/plain')
-#     response['Content-Disposition'] = 'attachment; filename=%s.txt' % slug
-#     return response
-
-
 @never_cache
 def book_html(request, pk, rev_pk=None, preview=False):
     from librarian.document import Document as SST
 @never_cache
 def book_html(request, pk, rev_pk=None, preview=False):
     from librarian.document import Document as SST
@@ -286,9 +187,13 @@ def book_html(request, pk, rev_pk=None, preview=False):
 
     was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
 
 
     was_published = revision == published_revision or doc.publish_log.filter(revision=revision).exists()
 
-    sst = SST.from_string(revision.materialize())
-    html = HtmlFormat(sst).build(
-        files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
+    try:
+        sst = SST.from_string(revision.materialize())
+    except ValueError as e:
+        html = e
+    else:
+        html = HtmlFormat(sst).build(
+            files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk)).get_string()
 
     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
     # return response
 
     # response = http.HttpResponse(html, content_type='text/html', mimetype='text/html')
     # return response
@@ -321,7 +226,10 @@ def book_pdf(request, pk, rev_pk):
     rev = get_object_or_404(Revision, pk=rev_pk)
     # Test
 
     rev = get_object_or_404(Revision, pk=rev_pk)
     # Test
 
-    sst = SST.from_string(rev.materialize())
+    try:
+        sst = SST.from_string(rev.materialize())
+    except ValueError as e:
+        return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
     
     ctx = Context(
         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
     
     ctx = Context(
         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
@@ -329,7 +237,10 @@ def book_pdf(request, pk, rev_pk):
     )
     if doc.owner_organization is not None and doc.owner_organization.logo:
         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
     )
     if doc.owner_organization is not None and doc.owner_organization.logo:
         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
-    pdf_file = PdfFormat(sst).build(ctx)
+    try:
+        pdf_file = PdfFormat(sst).build(ctx)
+    except BuildError as e:
+        return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
     from catalogue.ebook_utils import serve_file
     return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
 
     from catalogue.ebook_utils import serve_file
     return serve_file(pdf_file.get_filename(), '%d.pdf' % doc.pk, 'application/pdf')
@@ -345,7 +256,10 @@ def book_epub(request, pk, rev_pk):
     rev = get_object_or_404(Revision, pk=rev_pk)
     # Test
 
     rev = get_object_or_404(Revision, pk=rev_pk)
     # Test
 
-    sst = SST.from_string(rev.materialize())
+    try:
+        sst = SST.from_string(rev.materialize())
+    except ValueError as e:
+        return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
     ctx = Context(
         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
 
     ctx = Context(
         files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
@@ -353,12 +267,49 @@ def book_epub(request, pk, rev_pk):
     )
     if doc.owner_organization is not None and doc.owner_organization.logo:
         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
     )
     if doc.owner_organization is not None and doc.owner_organization.logo:
         ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
-    epub_file = EpubFormat(sst).build()
+    try:
+        epub_file = EpubFormat(sst).build(ctx)
+    except BuildError as e:
+        return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
 
     from catalogue.ebook_utils import serve_file
     return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
 
 
 
     from catalogue.ebook_utils import serve_file
     return serve_file(epub_file.get_filename(), '%d.epub' % doc.pk, 'application/epub+zip')
 
 
+@never_cache
+def book_mobi(request, pk, rev_pk):
+    from librarian.utils import Context
+    from librarian.document import Document as SST
+    from librarian.formats.epub import EpubFormat
+
+    doc = get_object_or_404(Document, pk=pk)
+    rev = get_object_or_404(Revision, pk=rev_pk)
+
+    try:
+        sst = SST.from_string(rev.materialize())
+    except ValueError as e:
+        return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
+
+    ctx = Context(
+        files_path='http://%s/media/dynamic/uploads/%s/' % (request.get_host(), pk),
+        source_url='http://%s%s' % (request.get_host(), reverse('catalogue_html', args=[doc.pk])),
+    )
+    if doc.owner_organization is not None and doc.owner_organization.logo:
+        ctx.cover_logo = 'http://%s%s' % (request.get_host(), doc.owner_organization.logo.url)
+    try:
+        epub_file = EpubFormat(sst).build(ctx)
+    except BuildError as e:
+        return HttpResponse(content=force_str(e.message), content_type='text/plain', status='400')
+
+    output_file = NamedTemporaryFile(prefix='librarian', suffix='.mobi', delete=False)
+    output_file.close()
+    subprocess.check_call(
+        ['ebook-convert', epub_file.get_filename(), output_file.name, '--no-inline-toc'])
+
+    from catalogue.ebook_utils import serve_file
+    return serve_file(output_file.name, '%d.mobi' % doc.pk, 'application/epub+zip')
+
+
 # @never_cache
 # def revision(request, slug, chunk=None):
 #     try:
 # @never_cache
 # def revision(request, slug, chunk=None):
 #     try:
@@ -373,9 +324,11 @@ def book_epub(request, pk, rev_pk):
 @login_required
 def book_schedule(request, pk):
     book = get_object_or_404(Document, pk=pk, deleted=False)
 @login_required
 def book_schedule(request, pk):
     book = get_object_or_404(Document, pk=pk, deleted=False)
+    if not book.can_edit(request.user):
+        return HttpResponseForbidden("Not authorized.")
     if request.method == 'POST':
         Plan.objects.filter(document=book).delete()
     if request.method == 'POST':
         Plan.objects.filter(document=book).delete()
-        for i, s in enumerate(STAGES):
+        for i, (s, name) in enumerate(STAGES):
             user_id = request.POST.get('s%d-user' % i)
             deadline = request.POST.get('s%d-deadline' % i) or None
             Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
             user_id = request.POST.get('s%d-user' % i)
             deadline = request.POST.get('s%d-deadline' % i) or None
             Plan.objects.create(document=book, stage=s, user_id=user_id, deadline=deadline)
@@ -387,7 +340,7 @@ def book_schedule(request, pk):
     for p in Plan.objects.filter(document=book):
         current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
 
     for p in Plan.objects.filter(document=book):
         current[p.stage] = (getattr(p.user, 'pk', None), (p.deadline.isoformat() if p.deadline else None))
 
-    schedule = [(i, s, current.get(s, ())) for (i, s) in enumerate(STAGES)]
+    schedule = [(i, s, current.get(s, ())) for i, (s, name) in enumerate(STAGES)]
     
     if book.owner_organization:
         people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
     
     if book.owner_organization:
         people = [m.user for m in book.owner_organization.membership_set.exclude(status='pending')]
@@ -403,9 +356,8 @@ def book_schedule(request, pk):
 @login_required
 def book_owner(request, pk):
     doc = get_object_or_404(Document, pk=pk, deleted=False)
 @login_required
 def book_owner(request, pk):
     doc = get_object_or_404(Document, pk=pk, deleted=False)
-    user_is_owner = doc.owner_organization and doc.owner_organization.is_member(request.user)
-    if not (doc.owner_user == request.user or user_is_owner):
-        raise Http404
+    if not doc.can_edit(request.user):
+        return HttpResponseForbidden("Not authorized.")
 
     error = ''
 
 
     error = ''
 
@@ -436,8 +388,8 @@ def book_owner(request, pk):
 @login_required
 def book_delete(request, pk):
     doc = get_object_or_404(Document, pk=pk, deleted=False)
 @login_required
 def book_delete(request, pk):
     doc = get_object_or_404(Document, pk=pk, deleted=False)
-    if not (doc.owner_user == request.user or doc.owner_organization.is_member(request.user)):
-        raise Http404
+    if not doc.can_edit(request.user):
+        return HttpResponseForbidden("Not authorized.")
 
     if request.method == 'POST':
         doc.deleted = True
 
     if request.method == 'POST':
         doc.deleted = True
@@ -449,192 +401,6 @@ def book_delete(request, pk):
     })
 
 
     })
 
 
-# def book(request, slug):
-#     book = get_object_or_404(Book, slug=slug)
-#     if not book.accessible(request):
-#         return HttpResponseForbidden("Not authorized.")
-#
-#     if request.user.has_perm('catalogue.change_book'):
-#         if request.method == "POST":
-#             form = forms.BookForm(request.POST, instance=book)
-#             if form.is_valid():
-#                 form.save()
-#                 return http.HttpResponseRedirect(book.get_absolute_url())
-#         else:
-#             form = forms.BookForm(instance=book)
-#         editable = True
-#     else:
-#         form = forms.ReadonlyBookForm(instance=book)
-#         editable = False
-#
-#     publish_error = book.publishable_error()
-#     publishable = publish_error is None
-#
-#     return render(request, "catalogue/book_detail.html", {
-#         "book": book,
-#         "publishable": publishable,
-#         "publishable_error": publish_error,
-#         "form": form,
-#         "editable": editable,
-#     })
-
-
-# @permission_required('catalogue.add_chunk')
-# def chunk_add(request, slug, chunk):
-#     try:
-#         doc = Chunk.get(slug, chunk)
-#     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
-#         raise Http404
-#     if not doc.book.accessible(request):
-#         return HttpResponseForbidden("Not authorized.")
-#
-#     if request.method == "POST":
-#         form = forms.ChunkAddForm(request.POST, instance=doc)
-#         if form.is_valid():
-#             if request.user.is_authenticated():
-#                 creator = request.user
-#             else:
-#                 creator = None
-#             doc.split(creator=creator,
-#                 slug=form.cleaned_data['slug'],
-#                 title=form.cleaned_data['title'],
-#                 gallery_start=form.cleaned_data['gallery_start'],
-#                 user=form.cleaned_data['user'],
-#                 stage=form.cleaned_data['stage']
-#             )
-#
-#             return http.HttpResponseRedirect(doc.book.get_absolute_url())
-#     else:
-#         form = forms.ChunkAddForm(initial={
-#                 "slug": str(doc.number + 1),
-#                 "title": "cz. %d" % (doc.number + 1, ),
-#         })
-#
-#     return render(request, "catalogue/chunk_add.html", {
-#         "chunk": doc,
-#         "form": form,
-#     })
-
-
-# @login_required
-# def chunk_edit(request, slug, chunk):
-#     try:
-#         doc = Chunk.get(slug, chunk)
-#     except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
-#         raise Http404
-#     if not doc.book.accessible(request):
-#         return HttpResponseForbidden("Not authorized.")
-#
-#     if request.method == "POST":
-#         form = forms.ChunkForm(request.POST, instance=doc)
-#         if form.is_valid():
-#             form.save()
-#             go_next = request.GET.get('next', None)
-#             if go_next:
-#                 go_next = urlquote_plus(unquote(iri_to_uri(go_next)), safe='/?=&')
-#             else:
-#                 go_next = doc.book.get_absolute_url()
-#             return http.HttpResponseRedirect(go_next)
-#     else:
-#         form = forms.ChunkForm(instance=doc)
-#
-#     referer = request.META.get('HTTP_REFERER')
-#     if referer:
-#         parts = urlsplit(referer)
-#         parts = ['', ''] + list(parts[2:])
-#         go_next = urlquote_plus(urlunsplit(parts))
-#     else:
-#         go_next = ''
-#
-#     return render(request, "catalogue/chunk_edit.html", {
-#         "chunk": doc,
-#         "form": form,
-#         "go_next": go_next,
-#     })
-
-
-# @transaction.atomic
-# @login_required
-# def chunk_mass_edit(request):
-#     if request.method == 'POST':
-#         ids = map(int, filter(lambda i: i.strip()!='', request.POST.get('ids').split(',')))
-#         chunks = map(lambda i: Chunk.objects.get(id=i), ids)
-#
-#         stage = request.POST.get('stage')
-#         if stage:
-#             try:
-#                 stage = Chunk.tag_model.objects.get(slug=stage)
-#             except Chunk.DoesNotExist, e:
-#                 stage = None
-#
-#             for c in chunks: c.stage = stage
-#
-#         username = request.POST.get('user')
-#         logger.info("username: %s" % username)
-#         logger.info(request.POST)
-#         if username:
-#             try:
-#                 user = User.objects.get(username=username)
-#             except User.DoesNotExist, e:
-#                 user = None
-#
-#             for c in chunks: c.user = user
-#
-#         status = request.POST.get('status')
-#         if status:
-#             books_affected = set()
-#             for c in chunks:
-#                 if status == 'publish':
-#                     c.head.publishable = True
-#                     c.head.save()
-#                 elif status == 'unpublish':
-#                     c.head.publishable = False
-#                     c.head.save()
-#                 c.touch()  # cache
-#                 books_affected.add(c.book)
-#             for b in books_affected:
-#                 b.touch()  # cache
-#
-#         project_id = request.POST.get('project')
-#         if project_id:
-#             try:
-#                 project = Project.objects.get(pk=int(project_id))
-#             except (Project.DoesNotExist, ValueError), e:
-#                 project = None
-#             for c in chunks:
-#                 book = c.book
-#                 book.project = project
-#                 book.save()
-#
-#         for c in chunks: c.save()
-#
-#         return HttpResponse("", content_type="text/plain")
-#     else:
-#         raise Http404
-
-
-# @permission_required('catalogue.change_book')
-# def book_append(request, slug):
-#     book = get_object_or_404(Book, slug=slug)
-#     if not book.accessible(request):
-#         return HttpResponseForbidden("Not authorized.")
-#
-#     if request.method == "POST":
-#         form = forms.BookAppendForm(book, request.POST)
-#         if form.is_valid():
-#             append_to = form.cleaned_data['append_to']
-#             append_to.append(book)
-#             return http.HttpResponseRedirect(append_to.get_absolute_url())
-#     else:
-#         form = forms.BookAppendForm(book)
-#     return render(request, "catalogue/book_append_to.html", {
-#         "book": book,
-#         "form": form,
-#
-#         "logout_to": '/',
-#     })
-
-
 @require_POST
 @login_required
 def publish(request, pk):
 @require_POST
 @login_required
 def publish(request, pk):
@@ -642,16 +408,26 @@ def publish(request, pk):
     from .models import PublishRecord
     from dvcs.models import Revision
 
     from .models import PublishRecord
     from dvcs.models import Revision
 
-    # FIXME: check permissions
-
     doc = get_object_or_404(Document, pk=pk, deleted=False)
     doc = get_object_or_404(Document, pk=pk, deleted=False)
+    if not doc.can_edit(request.user):
+        return HttpResponseForbidden("Not authorized.")
     form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
     if form.is_valid():
         rev = Revision.objects.get(pk=form.cleaned_data['revision'])
         # FIXME: check if in tree
         # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
         #     return http.HttpResponse('exists')
     form = forms.DocumentTextPublishForm(request.POST, prefix="textpublish")
     if form.is_valid():
         rev = Revision.objects.get(pk=form.cleaned_data['revision'])
         # FIXME: check if in tree
         # if PublishRecord.objects.filter(revision=rev, document=doc).exists():
         #     return http.HttpResponse('exists')
+        if not doc.published:
+            site = Site.objects.get_current()
+            send_notify_email(
+                'New published document in MIL/PEER',
+                '''New published document in MIL/PEER: %s. View it in browser: https://%s%s.
+
+--
+MIL/PEER team.''' % (doc.meta()['title'], site.domain, reverse('catalogue_html', args=[doc.pk])))
         PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
         PublishRecord.objects.create(revision=rev, document=doc, user=request.user)
+        doc.published = True
+        doc.save()
         if request.is_ajax():
             return http.HttpResponse('ok')
         else:
         if request.is_ajax():
             return http.HttpResponse('ok')
         else:
@@ -669,9 +445,10 @@ def publish(request, pk):
 @require_POST
 @login_required
 def unpublish(request, pk):
 @require_POST
 @login_required
 def unpublish(request, pk):
-    # FIXME: check permissions
-
     doc = get_object_or_404(Document, pk=pk, deleted=False)
     doc = get_object_or_404(Document, pk=pk, deleted=False)
+    if not doc.can_edit(request.user):
+        return HttpResponseForbidden("Not authorized.")
+
     doc.publish_log.all().delete()
     if request.is_ajax():
         return http.HttpResponse('ok')
     doc.publish_log.all().delete()
     if request.is_ajax():
         return http.HttpResponse('ok')
@@ -696,12 +473,6 @@ class GalleryView(GalleryMixin, UploadView):
         self.doc = Document.objects.get(pk=pk, deleted=False)
 
 
         self.doc = Document.objects.get(pk=pk, deleted=False)
 
 
-class GalleryPackageView(GalleryMixin, PackageView):
-
-    def get_redirect_url(self, slug):
-        return reverse('catalogue_book_gallery', kwargs=dict(slug=slug))
-
-
 @login_required
 def fork(request, pk):
     doc = get_object_or_404(Document, pk=pk, deleted=False)
 @login_required
 def fork(request, pk):
     doc = get_object_or_404(Document, pk=pk, deleted=False)