"""
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,
- 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)
# -*- 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)
# 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
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
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")
-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 """
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
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')
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]
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):
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:
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)
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:
--- /dev/null
+{% 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 %}
{% block leftcolumn %}
+<a href="{% url wiki_book_edit book.slug %}">{% trans "edit" %}</a>
<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 %}
-</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 %}
--- /dev/null
+{% 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 %}
--- /dev/null
+{% 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 %}
--- /dev/null
+{% 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 %}
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"),
)
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 _
import librarian.html
import librarian.text
+from wiki.xml_tools import GradedText
#
# Quick hack around caching problems, TODO: use ETags
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,
})
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():
return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug]))
else:
- form = DocumentCreateForm(initial={
+ form = forms.DocumentCreateForm(initial={
"slug": slug,
"title": slug.replace('-', ' ').title(),
})
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
"error_list": error_list,
})
else:
- form = DocumentsUploadForm()
+ form = forms.DocumentsUploadForm()
return direct_to_template(request, "wiki/document_upload.html", extra_context={
"form": form,
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
@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)
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,
+ "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):
- form = DocumentTagForm(request.POST, prefix="addtag")
+ form = forms.DocumentTagForm(request.POST, prefix="addtag")
if form.is_valid():
try:
doc = Chunk.get(slug, chunk)
--- /dev/null
+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)
.chunk-list {
padding-left: 2em;
}
+
+
+.chunk-wl {
+ background-color: #afa;
+}
+.chunk-plain {
+ background-color: #aaa;
+}
+.chunk-xml {
+ background-color: #faa;
+}