editing and merging books, adding and editing book chunks,
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 1 Jun 2011 15:30:57 +0000 (17:30 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Wed, 1 Jun 2011 15:30:57 +0000 (17:30 +0200)
some chunk management stub functionality,
moved text compiling to xml_tools

13 files changed:
apps/dvcs/models.py
apps/wiki/constants.py
apps/wiki/forms.py
apps/wiki/models.py
apps/wiki/templates/wiki/book_append_to.html [new file with mode: 0755]
apps/wiki/templates/wiki/book_detail.html
apps/wiki/templates/wiki/book_edit.html [new file with mode: 0755]
apps/wiki/templates/wiki/chunk_add.html [new file with mode: 0755]
apps/wiki/templates/wiki/chunk_edit.html [new file with mode: 0755]
apps/wiki/urls.py
apps/wiki/views.py
apps/wiki/xml_tools.py [new file with mode: 0755]
redakcja/static/css/filelist.css

index 6c5796a..5ce00c0 100644 (file)
@@ -167,10 +167,11 @@ class Document(models.Model):
     """
         File in repository.        
     """
     """
         File in repository.        
     """
-    creator = models.ForeignKey(User, null=True, blank=True)
+    creator = models.ForeignKey(User, null=True, blank=True, editable=False)
     head = models.ForeignKey(Change,
                     null=True, blank=True, default=None,
     head = models.ForeignKey(Change,
                     null=True, blank=True, default=None,
-                    help_text=_("This document's current head."))
+                    help_text=_("This document's current head."),
+                    editable=False)
 
     def __unicode__(self):
         return u"{0}, HEAD: {1}".format(self.id, self.head_id)
 
     def __unicode__(self):
         return u"{0}, HEAD: {1}".format(self.id, self.head_id)
index 6781a48..fe0a446 100644 (file)
@@ -1,21 +1,5 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
-from django.utils.translation import ugettext_lazy as _
+import re
 
 
-DOCUMENT_STAGES = (
-    ("", u"-----"),
-    ("first_correction", _(u"First correction")),
-    ("tagging", _(u"Tagging")),
-    ("proofreading", _(u"Initial Proofreading")),
-    ("annotation-proofreading", _(u"Annotation Proofreading")),
-    ("modernisation", _(u"Modernisation")),
-    ("annotations", _(u"Annotations")),
-    ("themes", _(u"Themes")),
-    ("editor-proofreading", _(u"Editor's Proofreading")),
-    ("technical-editor-proofreading", _(u"Technical Editor's Proofreading")),
-)
-
-DOCUMENT_TAGS = DOCUMENT_STAGES + \
-    (("ready-to-publish", _(u"Ready to publish")),)
-
-DOCUMENT_TAGS_DICT = dict(DOCUMENT_TAGS)
-DOCUMENT_STAGES_DICT = dict(DOCUMENT_STAGES)
+RE_TRIM_BEGIN = re.compile("^<!-- TRIM_BEGIN -->$", re.M)
+RE_TRIM_END = re.compile("^<!-- TRIM_END -->$", re.M)
index f3362e8..7cabe32 100644 (file)
@@ -4,7 +4,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import forms
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from django import forms
-from wiki.models import Book
+from wiki.models import Book, Chunk
 from django.utils.translation import ugettext_lazy as _
 
 from dvcs.models import Tag
 from django.utils.translation import ugettext_lazy as _
 
 from dvcs.models import Tag
@@ -135,3 +135,57 @@ class DocumentTextRevertForm(forms.Form):
         label=_(u"Your comments"),
         help_text=_(u"Describe the reason for reverting."),
     )
         label=_(u"Your comments"),
         help_text=_(u"Describe the reason for reverting."),
     )
+
+
+class ChunkForm(forms.ModelForm):
+    """
+        Form used for editing a chunk.
+    """
+
+    class Meta:
+        model = Chunk
+        exclude = ['number']
+
+    def clean_slug(self):
+        slug = self.cleaned_data['slug']
+        try:
+            chunk = Chunk.objects.get(book=self.instance.book, slug=slug)
+        except Chunk.DoesNotExist:
+            return slug
+        if chunk == self:
+            return slug
+        raise forms.ValidationError(_('Chunk with this slug already exists'))
+
+
+class ChunkAddForm(ChunkForm):
+    """
+        Form used for adding a chunk to a document.
+    """
+
+    def clean_slug(self):
+        slug = self.cleaned_data['slug']
+        try:
+            user = Chunk.objects.get(book=self.instance.book, slug=slug)
+        except Chunk.DoesNotExist:
+            return slug
+        raise forms.ValidationError(_('Chunk with this slug already exists'))
+
+
+
+
+class BookAppendForm(forms.Form):
+    """
+        Form for appending a book to another book.
+        It means moving all chunks from book A to book B and deleting A.
+    """
+
+    append_to = forms.ModelChoiceField(queryset=Book.objects.all())
+
+
+class BookForm(forms.ModelForm):
+    """
+        Form used for editing a Book.
+    """
+
+    class Meta:
+        model = Book
index 6070206..4dc7017 100644 (file)
@@ -13,16 +13,12 @@ from django.utils.translation import ugettext_lazy as _
 from django.template.loader import render_to_string
 
 from dvcs import models as dvcs_models
 from django.template.loader import render_to_string
 
 from dvcs import models as dvcs_models
-
+from wiki.xml_tools import compile_text
 
 import logging
 logger = logging.getLogger("fnp.wiki")
 
 
 
 import logging
 logger = logging.getLogger("fnp.wiki")
 
 
-RE_TRIM_BEGIN = re.compile("^<!-- TRIM_BEGIN -->$", re.M)
-RE_TRIM_END = re.compile("^<!-- TRIM_END -->$", re.M)
-
-
 class Book(models.Model):
     """ A document edited on the wiki """
 
 class Book(models.Model):
     """ A document edited on the wiki """
 
@@ -46,6 +42,9 @@ class Book(models.Model):
     def __unicode__(self):
         return self.title
 
     def __unicode__(self):
         return self.title
 
+    def get_absolute_url(self):
+        return reverse("wiki_book", args=[self.slug])
+
     def save(self, reset_list_html=True, *args, **kwargs):
         if reset_list_html:
             self._list_html = None
     def save(self, reset_list_html=True, *args, **kwargs):
         if reset_list_html:
             self._list_html = None
@@ -79,18 +78,6 @@ class Book(models.Model):
             self.save(reset_list_html=False)
         return mark_safe(self._list_html)
 
             self.save(reset_list_html=False)
         return mark_safe(self._list_html)
 
-    @staticmethod
-    def trim(text, trim_begin=True, trim_end=True):
-        """ 
-            Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
-            that eg. one big XML file can be compiled from many small XML files.
-        """
-        if trim_begin:
-            text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
-        if trim_end:
-            text = RE_TRIM_END.split(text, maxsplit=1)[0]
-        return text
-
     @staticmethod
     def publish_tag():
         return dvcs_models.Tag.get('publish')
     @staticmethod
     def publish_tag():
         return dvcs_models.Tag.get('publish')
@@ -98,11 +85,8 @@ class Book(models.Model):
     def materialize(self, tag=None):
         """ 
             Get full text of the document compiled from chunks.
     def materialize(self, tag=None):
         """ 
             Get full text of the document compiled from chunks.
-            Takes the current versions of all texts for now, but it should
-            be possible to specify a tag or a point in time for compiling.
-
-            First non-empty text's beginning isn't trimmed,
-            and last non-empty text's end isn't trimmed.
+            Takes the current versions of all texts
+            or versions most recently tagged by a given tag.
         """
         if tag:
             changes = [chunk.last_tagged(tag) for chunk in self]
         """
         if tag:
             changes = [chunk.last_tagged(tag) for chunk in self]
@@ -110,23 +94,7 @@ class Book(models.Model):
             changes = [chunk.head for chunk in self]
         if None in changes:
             raise self.NoTextError('Some chunks have no available text.')
             changes = [chunk.head for chunk in self]
         if None in changes:
             raise self.NoTextError('Some chunks have no available text.')
-        texts = []
-        trim_begin = False
-        text = ''
-        for chunk in changes:
-            next_text = chunk.materialize()
-            if not next_text:
-                continue
-            if text:
-                # trim the end, because there's more non-empty text
-                # don't trim beginning, if `text' is the first non-empty part
-                texts.append(self.trim(text, trim_begin=trim_begin))
-                trim_begin = True
-            text = next_text
-        # don't trim the end, because there's no more text coming after `text'
-        # only trim beginning if it's not still the first non-empty
-        texts.append(self.trim(text, trim_begin=trim_begin, trim_end=False))
-        return "".join(texts)
+        return compile_text(change.materialize() for change in changes)
 
     def publishable(self):
         if not len(self):
 
     def publishable(self):
         if not len(self):
@@ -136,6 +104,48 @@ class Book(models.Model):
                 return False
         return True
 
                 return False
         return True
 
+    def make_chunk_slug(self, proposed):
+        """ 
+            Finds a chunk slug not yet used in the book.
+        """
+        slugs = set(c.slug for c in self)
+        i = 1
+        new_slug = proposed
+        while new_slug in slugs:
+            new_slug = "%s-%d" % (proposed, i)
+            i += 1
+        return new_slug
+
+    def append(self, other):
+        number = self[len(self) - 1].number + 1
+        single = len(other) == 1
+        for chunk in other:
+            # move chunk to new book
+            chunk.book = self
+            chunk.number = number
+
+            # try some title guessing
+            if other.title.startswith(self.title):
+                other_title_part = other.title[len(self.title):].lstrip(' /')
+            else:
+                other_title_part = other.title
+
+            if single:
+                # special treatment for appending one-parters:
+                # just use the guessed title and original book slug
+                chunk.comment = other_title_part
+                if other.slug.startswith(self.slug):
+                    chunk_slug = other.slug[len(self.slug):].lstrip('-_')
+                else:
+                    chunk_slug = other.slug
+                chunk.slug = self.make_chunk_slug(chunk_slug)
+            else:
+                chunk.comment = "%s, %s" % (other_title_part, chunk.comment)
+                chunk.slug = self.make_chunk_slug(chunk.slug)
+            chunk.save()
+            number += 1
+        other.delete()
+
     @staticmethod
     def listener_create(sender, instance, created, **kwargs):
         if created:
     @staticmethod
     def listener_create(sender, instance, created, **kwargs):
         if created:
@@ -147,7 +157,7 @@ models.signals.post_save.connect(Book.listener_create, sender=Book)
 class Chunk(dvcs_models.Document):
     """ An editable chunk of text. Every Book text is divided into chunks. """
 
 class Chunk(dvcs_models.Document):
     """ An editable chunk of text. Every Book text is divided into chunks. """
 
-    book = models.ForeignKey(Book)
+    book = models.ForeignKey(Book, editable=False)
     number = models.IntegerField()
     slug = models.SlugField()
     comment = models.CharField(max_length=255)
     number = models.IntegerField()
     slug = models.SlugField()
     comment = models.CharField(max_length=255)
@@ -176,6 +186,14 @@ class Chunk(dvcs_models.Document):
     def publishable(self):
         return self.last_tagged(Book.publish_tag())
 
     def publishable(self):
         return self.last_tagged(Book.publish_tag())
 
+    def split(self, slug, comment='', creator=None):
+        """ Create an empty chunk after this one """
+        self.book.chunk_set.filter(number__gt=self.number).update(
+                number=models.F('number')+1)
+        new_chunk = self.book.chunk_set.create(number=self.number+1,
+                creator=creator, slug=slug, comment=comment)
+        return new_chunk
+
     @staticmethod
     def listener_saved(sender, instance, created, **kwargs):
         if instance.book:
     @staticmethod
     def listener_saved(sender, instance, created, **kwargs):
         if instance.book:
diff --git a/apps/wiki/templates/wiki/book_append_to.html b/apps/wiki/templates/wiki/book_append_to.html
new file mode 100755 (executable)
index 0000000..0350aa6
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "wiki/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Append book" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
index 0a03d8b..d72befa 100755 (executable)
@@ -3,25 +3,51 @@
 
 {% block leftcolumn %}
 
 
 {% block leftcolumn %}
 
+<a href="{% url wiki_book_edit book.slug %}">{% trans "edit" %}</a>
     <h1>{{ book.title }}</h1>
 
     <h1>{{ book.title }}</h1>
 
-<p>
-    {% for chunk in book %}
-        <a target="_blank" href="{{ chunk.get_absolute_url }}">{{ chunk.comment }}</a><br/>
+<table>
+    {% for c in chunks %}
+        <tr class="chunk-row
+        {% if c.graded.is_wl %}
+            chunk-wl
+            {% if c.graded.bad_master %}
+                chunk-bad-master
+            {% endif %}
+        {% else %}
+            {% if c.graded.is_xml %}
+                chunk-xml
+            {% else %}
+                chunk-plain
+            {% endif %}
+        {% endif %}
+        ">
+        <td><a target="_blank" href="{{ c.chunk.get_absolute_url }}">{{ c.chunk.comment }}</a></td>
+        <td>{% if c.chunk.publishable %}P{% endif %}</td>
+        <td><a href="{% url wiki_chunk_edit book.slug c.chunk.slug%}">[{% trans "edit" %}]</a></td>
+        <td>{% if c.bad_master %}{{ c.bad_master }}{% endif %}</td>
+        <td><a href="{% url wiki_chunk_add book.slug c.chunk.slug %}">[+]</a></td>
+        </tr>
     {% endfor %}
     {% endfor %}
-</p>
-
-<p>
-<a href="{% url wiki_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
-<a target="_blank" href="{% url wiki_book_html book.slug %}">{% trans "HTML version" %}</a><br/>
-<a href="{% url wiki_book_txt book.slug %}">{% trans "TXT version" %}</a><br/>
-{% comment %}
-<a href="{% url wiki_book_epub book.slug %}">{% trans "EPUB version" %}</a><br/>
-<a href="{% url wiki_book_pdf book.slug %}">{% trans "PDF version" %}</a><br/>
-{% endcomment %}
-</p>
-
-<p style='width:200px; height: 75px; border: 1px dotted gray; border-corners: 4px;'></p>
+</table>
+
+<p><a href="{% url wiki_book_append book.slug %}">{% trans "Append to other book" %}</a></p>
+
+{% if book.publishable %}
+    <p>
+    <a href="{% url wiki_book_xml book.slug %}">{% trans "Full XML" %}</a><br/>
+    <a target="_blank" href="{% url wiki_book_html book.slug %}">{% trans "HTML version" %}</a><br/>
+    <a href="{% url wiki_book_txt book.slug %}">{% trans "TXT version" %}</a><br/>
+    {% comment %}
+    <a href="{% url wiki_book_epub book.slug %}">{% trans "EPUB version" %}</a><br/>
+    <a href="{% url wiki_book_pdf book.slug %}">{% trans "PDF version" %}</a><br/>
+    {% endcomment %}
+    </p>
+
+    <p style='width:200px; height: 75px; border: 1px dotted gray; border-corners: 4px;'></p>
+{% else %}
+    {% trans "This book cannot be published yet" %}
+{% endif %}
 
 {% endblock leftcolumn %}
 
 
 {% endblock leftcolumn %}
 
diff --git a/apps/wiki/templates/wiki/book_edit.html b/apps/wiki/templates/wiki/book_edit.html
new file mode 100755 (executable)
index 0000000..d5f527d
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "wiki/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Save" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/chunk_add.html b/apps/wiki/templates/wiki/chunk_add.html
new file mode 100755 (executable)
index 0000000..2b8939e
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "wiki/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Add chunk" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/chunk_edit.html b/apps/wiki/templates/wiki/chunk_edit.html
new file mode 100755 (executable)
index 0000000..d5f527d
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "wiki/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Save" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
index 3e5b067..393afa5 100644 (file)
@@ -55,5 +55,13 @@ urlpatterns = patterns('wiki.views',
     url(r'^book/(?P<slug>[^/]+)/html$', 'book_html', name="wiki_book_html"),
     #url(r'^book/(?P<slug>[^/]+)/epub$', 'book_epub', name="wiki_book_epub"),
     #url(r'^book/(?P<slug>[^/]+)/pdf$', 'book_pdf', name="wiki_book_pdf"),
     url(r'^book/(?P<slug>[^/]+)/html$', 'book_html', name="wiki_book_html"),
     #url(r'^book/(?P<slug>[^/]+)/epub$', 'book_epub', name="wiki_book_epub"),
     #url(r'^book/(?P<slug>[^/]+)/pdf$', 'book_pdf', name="wiki_book_pdf"),
+    url(r'^chunk_add/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+        'chunk_add', name="wiki_chunk_add"),
+    url(r'^chunk_edit/(?P<slug>[^/]+)/(?P<chunk>[^/]+)/$',
+        'chunk_edit', name="wiki_chunk_edit"),
+    url(r'^book_append/(?P<slug>[^/]+)/$',
+        'book_append', name="wiki_book_append"),
+    url(r'^book_edit/(?P<slug>[^/]+)/$',
+        'book_edit', name="wiki_book_edit"),
 
 )
 
 )
index 6063078..146db69 100644 (file)
@@ -17,8 +17,7 @@ from django.shortcuts import get_object_or_404, redirect
 from django.http import Http404
 
 from wiki.models import Book, Chunk, Theme
 from django.http import Http404
 
 from wiki.models import Book, Chunk, Theme
-from wiki.forms import (DocumentTextSaveForm, DocumentTextRevertForm, DocumentTagForm, DocumentCreateForm, DocumentsUploadForm,
-        ChunkFormSet)
+from wiki import forms
 from datetime import datetime
 from django.utils.encoding import smart_unicode
 from django.utils.translation import ugettext_lazy as _
 from datetime import datetime
 from django.utils.encoding import smart_unicode
 from django.utils.translation import ugettext_lazy as _
@@ -27,6 +26,7 @@ from django.middleware.gzip import GZipMiddleware
 
 import librarian.html
 import librarian.text
 
 import librarian.html
 import librarian.text
+from wiki.xml_tools import GradedText
 
 #
 # Quick hack around caching problems, TODO: use ETags
 
 #
 # Quick hack around caching problems, TODO: use ETags
@@ -79,9 +79,9 @@ def editor(request, slug, chunk=None, template_name='wiki/document_details.html'
     return direct_to_template(request, template_name, extra_context={
         'chunk': chunk,
         'forms': {
     return direct_to_template(request, template_name, extra_context={
         'chunk': chunk,
         'forms': {
-            "text_save": DocumentTextSaveForm(prefix="textsave"),
-            "text_revert": DocumentTextRevertForm(prefix="textrevert"),
-            "add_tag": DocumentTagForm(prefix="addtag"),
+            "text_save": forms.DocumentTextSaveForm(prefix="textsave"),
+            "text_revert": forms.DocumentTextRevertForm(prefix="textrevert"),
+            "add_tag": forms.DocumentTagForm(prefix="addtag"),
         },
         'REDMINE_URL': settings.REDMINE_URL,
     })
         },
         'REDMINE_URL': settings.REDMINE_URL,
     })
@@ -119,7 +119,7 @@ def create_missing(request, slug):
     slug = slug.replace(' ', '-')
 
     if request.method == "POST":
     slug = slug.replace(' ', '-')
 
     if request.method == "POST":
-        form = DocumentCreateForm(request.POST, request.FILES)
+        form = forms.DocumentCreateForm(request.POST, request.FILES)
         if form.is_valid():
             
             if request.user.is_authenticated():
         if form.is_valid():
             
             if request.user.is_authenticated():
@@ -134,7 +134,7 @@ def create_missing(request, slug):
 
             return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
     else:
 
             return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
     else:
-        form = DocumentCreateForm(initial={
+        form = forms.DocumentCreateForm(initial={
                 "slug": slug,
                 "title": slug.replace('-', ' ').title(),
         })
                 "slug": slug,
                 "title": slug.replace('-', ' ').title(),
         })
@@ -147,7 +147,7 @@ def create_missing(request, slug):
 
 def upload(request):
     if request.method == "POST":
 
 def upload(request):
     if request.method == "POST":
-        form = DocumentsUploadForm(request.POST, request.FILES)
+        form = forms.DocumentsUploadForm(request.POST, request.FILES)
         if form.is_valid():
             import slughifi
 
         if form.is_valid():
             import slughifi
 
@@ -196,7 +196,7 @@ def upload(request):
                 "error_list": error_list,
             })
     else:
                 "error_list": error_list,
             })
     else:
-        form = DocumentsUploadForm()
+        form = forms.DocumentsUploadForm()
 
     return direct_to_template(request, "wiki/document_upload.html", extra_context={
         "form": form,
 
     return direct_to_template(request, "wiki/document_upload.html", extra_context={
         "form": form,
@@ -212,7 +212,7 @@ def text(request, slug, chunk=None):
         raise Http404
 
     if request.method == 'POST':
         raise Http404
 
     if request.method == 'POST':
-        form = DocumentTextSaveForm(request.POST, prefix="textsave")
+        form = forms.DocumentTextSaveForm(request.POST, prefix="textsave")
         if form.is_valid():
             if request.user.is_authenticated():
                 author = request.user
         if form.is_valid():
             if request.user.is_authenticated():
                 author = request.user
@@ -288,7 +288,7 @@ def book_html(request, slug):
 @never_cache
 @require_POST
 def revert(request, slug, chunk=None):
 @never_cache
 @require_POST
 def revert(request, slug, chunk=None):
-    form = DocumentTextRevertForm(request.POST, prefix="textrevert")
+    form = forms.DocumentTextRevertForm(request.POST, prefix="textrevert")
     if form.is_valid():
         try:
             doc = Chunk.get(slug, chunk)
     if form.is_valid():
         try:
             doc = Chunk.get(slug, chunk)
@@ -399,15 +399,118 @@ def history(request, slug, chunk=None):
 def book(request, slug):
     book = get_object_or_404(Book, slug=slug)
 
 def book(request, slug):
     book = get_object_or_404(Book, slug=slug)
 
+    # do we need some automation?
+    some_wl = False
+    first_master = None
+    chunks = []
+
+    for chunk in book:
+        graded = GradedText(chunk.materialize())
+        chunk_dict = {
+            "chunk": chunk,
+            "graded": graded,
+            }
+        if graded.is_wl():
+            some_wl = True
+            master = graded.master()
+            if first_master is None:
+                first_master = master
+            elif master != first_master:
+                chunk_dict['bad_master'] = master
+        chunks.append(chunk_dict)
+
     return direct_to_template(request, "wiki/book_detail.html", extra_context={
         "book": book,
     return direct_to_template(request, "wiki/book_detail.html", extra_context={
         "book": book,
+        "chunks": chunks,
+        "some_wl": some_wl,
+        "first_master": first_master,
+    })
+
+
+def chunk_add(request, slug, chunk):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+
+    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'],
+                comment=form.cleaned_data['comment'],
+            )
+
+            return http.HttpResponseRedirect(doc.book.get_absolute_url())
+    else:
+        form = forms.ChunkAddForm(initial={
+                "slug": str(doc.number + 1),
+                "comment": "cz. %d" % (doc.number + 1, ),
+        })
+
+    return direct_to_template(request, "wiki/chunk_add.html", extra_context={
+        "chunk": doc,
+        "form": form,
+    })
+
+
+def chunk_edit(request, slug, chunk):
+    try:
+        doc = Chunk.get(slug, chunk)
+    except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist):
+        raise Http404
+    if request.method == "POST":
+        form = forms.ChunkForm(request.POST, instance=doc)
+        if form.is_valid():
+            form.save()
+            return http.HttpResponseRedirect(doc.book.get_absolute_url())
+    else:
+        form = forms.ChunkForm(instance=doc)
+    return direct_to_template(request, "wiki/chunk_edit.html", extra_context={
+        "chunk": doc,
+        "form": form,
+    })
+
+
+def book_append(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    if request.method == "POST":
+        form = forms.BookAppendForm(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()
+    return direct_to_template(request, "wiki/book_append_to.html", extra_context={
+        "book": book,
+        "form": form,
+    })
+
+
+def book_edit(request, slug):
+    book = get_object_or_404(Book, slug=slug)
+    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)
+    return direct_to_template(request, "wiki/book_edit.html", extra_context={
+        "book": book,
+        "form": form,
     })
 
 
 @require_POST
 @ajax_require_permission('wiki.can_change_tags')
 def add_tag(request, slug, chunk=None):
     })
 
 
 @require_POST
 @ajax_require_permission('wiki.can_change_tags')
 def add_tag(request, slug, chunk=None):
-    form = DocumentTagForm(request.POST, prefix="addtag")
+    form = forms.DocumentTagForm(request.POST, prefix="addtag")
     if form.is_valid():
         try:
             doc = Chunk.get(slug, chunk)
     if form.is_valid():
         try:
             doc = Chunk.get(slug, chunk)
diff --git a/apps/wiki/xml_tools.py b/apps/wiki/xml_tools.py
new file mode 100755 (executable)
index 0000000..a4de433
--- /dev/null
@@ -0,0 +1,89 @@
+import re
+
+from lxml import etree
+
+from wiki.constants import RE_TRIM_BEGIN, RE_TRIM_END
+
+class GradedText(object):
+    _is_xml = None
+    _edoc = None
+    _is_wl = None
+    _master = None
+
+    ROOT = 'utwor'
+    MASTERS = ['powiesc',
+               'opowiadanie',
+               'liryka_l',
+               'liryka_lp',
+               'dramat_wierszowany_l',
+               'dramat_wierszowany_lp',
+               'dramat_wspolczesny',
+               ]
+    RDF = '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF'
+
+    def __init__(self, text):
+        self._text = text
+
+    def is_xml(self):
+        if self._is_xml is None:
+            try:
+                self._edoc = etree.fromstring(self._text)
+            except etree.XMLSyntaxError:
+                self._is_xml = False
+            else:
+                self._is_xml = True
+            del self._text
+        return self._is_xml
+
+    def is_wl(self):
+        if self._is_wl is None:
+            if self.is_xml():
+                e = self._edoc
+                self._is_wl = e.tag == self.ROOT and (
+                    len(e) == 1 and e[0].tag in self.MASTERS or
+                    len(e) == 2 and e[0].tag == self.RDF 
+                        and e[1].tag in self.MASTERS)
+                if self._is_wl:
+                    self._master = e[-1].tag
+                del self._edoc
+            else:
+                self._is_wl = False
+        return self._is_wl
+
+    def master(self):
+        assert self.is_wl()
+        return self._master
+
+
+def _trim(text, trim_begin=True, trim_end=True):
+    """ 
+        Cut off everything before RE_TRIM_BEGIN and after RE_TRIM_END, so
+        that eg. one big XML file can be compiled from many small XML files.
+    """
+    if trim_begin:
+        text = RE_TRIM_BEGIN.split(text, maxsplit=1)[-1]
+    if trim_end:
+        text = RE_TRIM_END.split(text, maxsplit=1)[0]
+    return text
+
+
+def compile_text(parts):
+    """ 
+        Compiles full text from an iterable of parts,
+        trimming where applicable.
+    """
+    texts = []
+    trim_begin = False
+    text = ''
+    for next_text in parts:
+        if not next_text:
+            continue
+        # trim the end, because there's more non-empty text
+        # don't trim beginning, if `text' is the first non-empty part
+        texts.append(_trim(text, trim_begin=trim_begin))
+        trim_begin = True
+        text = next_text
+    # don't trim the end, because there's no more text coming after `text'
+    # only trim beginning if it's not still the first non-empty
+    texts.append(_trim(text, trim_begin=trim_begin, trim_end=False))
+    return "".join(texts)
index 7dfdf80..c2f59f3 100644 (file)
@@ -98,3 +98,14 @@ td {
 .chunk-list {
     padding-left: 2em;
 }
 .chunk-list {
     padding-left: 2em;
 }
+
+
+.chunk-wl {
+    background-color: #afa;
+}
+.chunk-plain {
+    background-color: #aaa;
+}
+.chunk-xml {
+    background-color: #faa;
+}