From 2f9c60b76f3ab4e69d794a6bb14388a81ff29eb7 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 4 Jul 2011 18:08:20 +0200 Subject: [PATCH 1/1] refactor catalogue to separate app, some minor fixes --- apps/catalogue/__init__.py | 1 + apps/catalogue/admin.py | 12 + apps/{wiki => catalogue}/constants.py | 0 apps/{wiki => catalogue}/fixtures/stages.json | 18 +- apps/catalogue/forms.py | 124 +++++ apps/catalogue/helpers.py | 65 +++ .../catalogue/locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 6633 bytes .../catalogue/locale/pl/LC_MESSAGES/django.po | 508 ++++++++++++++++++ .../management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/assign_from_redmine.py | 39 +- .../migrations/0001_initial.py} | 112 ++-- .../migrations}/__init__.py | 0 apps/catalogue/models.py | 178 ++++++ .../templates/catalogue}/base.html | 8 +- .../templates/catalogue}/book_append_to.html | 2 +- .../templates/catalogue}/book_detail.html | 24 +- .../templates/catalogue}/book_edit.html | 2 +- .../templates/catalogue}/chunk_add.html | 2 +- .../templates/catalogue}/chunk_edit.html | 2 +- .../catalogue}/document_create_missing.html | 2 +- .../templates/catalogue}/document_list.html | 16 +- .../templates/catalogue}/document_upload.html | 2 +- .../templates/catalogue}/main_tabs.html | 0 .../templates/catalogue}/user_list.html | 4 +- .../templates/catalogue}/wall.html | 4 +- apps/catalogue/templatetags/__init__.py | 0 .../templatetags/catalogue.py} | 18 +- apps/catalogue/tests.py | 19 + apps/catalogue/urls.py | 41 ++ apps/catalogue/views.py | 418 ++++++++++++++ apps/{wiki => catalogue}/xml_tools.py | 2 +- apps/wiki/admin.py | 8 - apps/wiki/forms.py | 129 +---- apps/wiki/helpers.py | 139 +---- apps/wiki/models.py | 178 ------ .../templates/wiki/document_details_base.html | 2 +- apps/wiki/templates/wiki/tag_dialog.html | 20 - apps/wiki/urls.py | 45 +- apps/wiki/views.py | 437 +-------------- redakcja/settings/common.py | 1 + redakcja/settings/compress.py | 1 - redakcja/static/js/wiki/dialog_addtag.js | 61 --- redakcja/static/js/wiki/view_history.js | 16 - redakcja/static/js/wiki/wikiapi.js | 47 +- redakcja/static/js/wiki/xslt.js | 2 +- redakcja/urls.py | 10 +- 47 files changed, 1524 insertions(+), 1195 deletions(-) create mode 100644 apps/catalogue/__init__.py create mode 100644 apps/catalogue/admin.py rename apps/{wiki => catalogue}/constants.py (100%) rename apps/{wiki => catalogue}/fixtures/stages.json (81%) create mode 100644 apps/catalogue/forms.py create mode 100644 apps/catalogue/helpers.py create mode 100644 apps/catalogue/locale/pl/LC_MESSAGES/django.mo create mode 100644 apps/catalogue/locale/pl/LC_MESSAGES/django.po rename apps/{wiki => catalogue}/management/__init__.py (100%) rename apps/{wiki => catalogue}/management/commands/__init__.py (100%) rename apps/{wiki => catalogue}/management/commands/assign_from_redmine.py (78%) rename apps/{wiki/migrations/0003_add_dvcs.py => catalogue/migrations/0001_initial.py} (84%) rename apps/{wiki/templatetags => catalogue/migrations}/__init__.py (100%) create mode 100644 apps/catalogue/models.py rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/base.html (85%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/book_append_to.html (89%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/book_detail.html (74%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/book_edit.html (88%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/chunk_add.html (88%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/chunk_edit.html (88%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/document_create_missing.html (89%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/document_list.html (77%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/document_upload.html (98%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/main_tabs.html (100%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/user_list.html (75%) rename apps/{wiki/templates/wiki => catalogue/templates/catalogue}/wall.html (84%) create mode 100644 apps/catalogue/templatetags/__init__.py rename apps/{wiki/templatetags/wiki.py => catalogue/templatetags/catalogue.py} (84%) create mode 100644 apps/catalogue/tests.py create mode 100644 apps/catalogue/urls.py create mode 100644 apps/catalogue/views.py rename apps/{wiki => catalogue}/xml_tools.py (98%) delete mode 100644 apps/wiki/templates/wiki/tag_dialog.html delete mode 100644 redakcja/static/js/wiki/dialog_addtag.js diff --git a/apps/catalogue/__init__.py b/apps/catalogue/__init__.py new file mode 100644 index 00000000..c53f0e73 --- /dev/null +++ b/apps/catalogue/__init__.py @@ -0,0 +1 @@ + # pragma: no cover diff --git a/apps/catalogue/admin.py b/apps/catalogue/admin.py new file mode 100644 index 00000000..70e20d20 --- /dev/null +++ b/apps/catalogue/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + +from catalogue import models + +class BookAdmin(admin.ModelAdmin): + prepopulated_fields = {'slug': ['title']} + + +admin.site.register(models.Book, BookAdmin) +admin.site.register(models.Chunk) + +admin.site.register(models.Chunk.tag_model) diff --git a/apps/wiki/constants.py b/apps/catalogue/constants.py similarity index 100% rename from apps/wiki/constants.py rename to apps/catalogue/constants.py diff --git a/apps/wiki/fixtures/stages.json b/apps/catalogue/fixtures/stages.json similarity index 81% rename from apps/wiki/fixtures/stages.json rename to apps/catalogue/fixtures/stages.json index 992ebc71..5a46ec04 100644 --- a/apps/wiki/fixtures/stages.json +++ b/apps/catalogue/fixtures/stages.json @@ -1,7 +1,7 @@ [ { "pk": 1, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 1, "name": "Autokorekta", @@ -10,7 +10,7 @@ }, { "pk": 2, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 2, "name": "Tagowanie", @@ -19,7 +19,7 @@ }, { "pk": 3, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 3, "name": "Korekta", @@ -28,7 +28,7 @@ }, { "pk": 4, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 4, "name": "Sprawdzenie przypis\u00f3w \u017ar\u00f3d\u0142a", @@ -37,7 +37,7 @@ }, { "pk": 5, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 5, "name": "Uwsp\u00f3\u0142cze\u015bnienie", @@ -46,7 +46,7 @@ }, { "pk": 6, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 6, "name": "Przypisy", @@ -55,7 +55,7 @@ }, { "pk": 7, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 7, "name": "Motywy", @@ -64,7 +64,7 @@ }, { "pk": 8, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 8, "name": "Ostateczna redakcja literacka", @@ -73,7 +73,7 @@ }, { "pk": 9, - "model": "wiki.chunktag", + "model": "catalogue.chunktag", "fields": { "ordering": 9, "name": "Ostateczna redakcja techniczna", diff --git a/apps/catalogue/forms.py b/apps/catalogue/forms.py new file mode 100644 index 00000000..33ccbe6f --- /dev/null +++ b/apps/catalogue/forms.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.contrib.auth.models import User +from django.db.models import Count +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from catalogue.constants import MASTERS +from catalogue.models import Book, Chunk + +class DocumentCreateForm(forms.ModelForm): + """ + Form used for creating new documents. + """ + file = forms.FileField(required=False) + text = forms.CharField(required=False, widget=forms.Textarea) + + class Meta: + model = Book + exclude = ['gallery', 'parent', 'parent_number'] + prepopulated_fields = {'slug': ['title']} + + def clean(self): + super(DocumentCreateForm, self).clean() + file = self.cleaned_data['file'] + + if file is not None: + try: + self.cleaned_data['text'] = file.read().decode('utf-8') + except UnicodeDecodeError: + raise forms.ValidationError("Text file must be UTF-8 encoded.") + + if not self.cleaned_data["text"]: + raise forms.ValidationError("You must either enter text or upload a file") + + return self.cleaned_data + + +class DocumentsUploadForm(forms.Form): + """ + Form used for uploading new documents. + """ + file = forms.FileField(required=True, label=_('ZIP file')) + + def clean(self): + file = self.cleaned_data['file'] + + import zipfile + try: + z = self.cleaned_data['zip'] = zipfile.ZipFile(file) + except zipfile.BadZipfile: + raise forms.ValidationError("Should be a ZIP file.") + if z.testzip(): + raise forms.ValidationError("ZIP file corrupt.") + + return self.cleaned_data + + +class ChunkForm(forms.ModelForm): + """ + Form used for editing a chunk. + """ + user = forms.ModelChoiceField(queryset= + User.objects.annotate(count=Count('chunk')). + order_by('-count', 'last_name', 'first_name')) + + + 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.instance: + 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(), + label=_("Append to")) + + +class BookForm(forms.ModelForm): + """ + Form used for editing a Book. + """ + + class Meta: + model = Book + + +class ChooseMasterForm(forms.Form): + """ + Form used for fixing the chunks in a book. + """ + + master = forms.ChoiceField(choices=((m, m) for m in MASTERS)) diff --git a/apps/catalogue/helpers.py b/apps/catalogue/helpers.py new file mode 100644 index 00000000..c9dc0bd9 --- /dev/null +++ b/apps/catalogue/helpers.py @@ -0,0 +1,65 @@ +from functools import wraps + +from django.db.models import Count + + +def active_tab(tab): + """ + View decorator, which puts tab info on a request. + """ + def wrapper(f): + @wraps(f) + def wrapped(request, *args, **kwargs): + request.catalogue_active_tab = tab + return f(request, *args, **kwargs) + return wrapped + return wrapper + + +class ChunksList(object): + def __init__(self, chunk_qs): + self.chunk_qs = chunk_qs.annotate( + book_length=Count('book__chunk')).select_related( + 'book', 'stage__name', + 'user') + + self.book_qs = chunk_qs.values('book_id') + + def __getitem__(self, key): + if isinstance(key, slice): + return self.get_slice(key) + elif isinstance(key, int): + return self.get_slice(slice(key, key+1))[0] + else: + raise TypeError('Unsupported list index. Must be a slice or an int.') + + def __len__(self): + return self.book_qs.count() + + def get_slice(self, slice_): + book_ids = [x['book_id'] for x in self.book_qs[slice_]] + chunk_qs = self.chunk_qs.filter(book__in=book_ids) + + chunks_list = [] + book = None + for chunk in chunk_qs: + if chunk.book != book: + book = chunk.book + chunks_list.append(ChoiceChunks(book, [chunk], chunk.book_length)) + else: + chunks_list[-1].chunks.append(chunk) + return chunks_list + + +class ChoiceChunks(object): + """ + Associates the given chunks iterable for a book. + """ + + chunks = None + + def __init__(self, book, chunks, book_length): + self.book = book + self.chunks = chunks + self.book_length = book_length + diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.mo b/apps/catalogue/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f841945b2f770acbdbc63e6e93d5659c3ac0f72c GIT binary patch literal 6633 zcmai%Ym6P&UBC}%pu~lg7HS%3!wDgz#=h%ar#0@z4Oy?(@z(b4;=OAp?u&D0&fcA! zxpQvkk(-%*sBmL}3TmMOsaB(?QY#_R52y|K608ah@>78p@~1w@&riWmh5Fxt_fmci%Ftg61X-*QTR#tR@g%M?mE01em>Oy9z0I@Irt9v=TQ2+0%gDd8u&j@)^`t$KMC)HRH+`6 z@jeKp{jtEcz>h=emqWjfP`(a#ss9|5b$&0j--R-y{~;*-4?~*N{ZRCN6v{j&0?$C1 z|1x|xyb7h?7`_uuAb;w!{7Cy3p@v_FGS6Q_ndg;I{%0t5`7d}L{xAF`xPmesfMY2B z^96{?)fb_B{}t%{0`jN6!H@L24W<3hpzPz@kRjDS2EGdCDZdWI-bdJ+%zqY&Ue=&| ze>Idp0maXE;9+GuKnTR)`K6QTWYsAMbZ@1czMFHrXL8kGHrQN(}lfili} zp~S@zDBrC>{#2hInSTgnoDmd%{}hz<+zjO}!(X8MO~}&KccH|~e?yj}4l|k9>0v12 zpMX-o1f~CFNLMw0vW^;xoqiX}`ksd}{vX0$gx`k00$+pj-AR-x89{CQGb^Ryg&sR#nchM+%d;s1DkHVjZD^T|JF(~WK zpv>DqS=XoGXW(Z;`Cgnx#<>s5_zyx^_bDj-9}m0$Wq((o=qrJu_coM&J(RxyF|E28 z>c0l>r+f>Zfd2qxKlid&AIDJUIU0Bpo};`3MZdoZAAx@aWxnr1+25;B^!UBNyHJ{p z_qM=yL+Sqzl>Wz{%=>8I1t{zJ2t*a?b5Qp6RVd@W2t_a74CQY@S4pMST1UvVh1wKZ0x_vQ~NIEW3tW zMJ^zhkm>UGIfop4-cP|I zV#||=#OzUI6_Mw32k(ROdJd6%DUZah#L`Elz_W&kjekxGJRd`T)xTj!_;`4~94LMz zwwFgEk0BDvV&6U@&nEJuf9rmRK>S^N;vyox^eFOi6Pa*OQkOVo8oI+ycEyyxLXfjzGi|A;l&Qsn-Nv@W% zOfBWPt4x)+yi`l$vCU&WaE>0`d*$}uw5xV(F>B5my`4N``K&CHVQynxIX$v!sjhZh z5%6%S?Z_mVI#XwQ?P)nvIT`CN@aS-298IWW4NKoGFvoi*^S1gu=;6Rlzx{| zk=e>JyDJJCRWb_OSZ4gn&eo`wYg+Sa`k^{&%cw{O78RO&XiMF=T8~U@=k}XWkYlvS7&J6>aG5{-YISK$i+6EQ_@=M9rLuM z(dN3WqsW%!cAcUAPKll3yP3xh6}qCCepsdnc8qmgTTxH$^l2IRvAJhP+3Dl{6@^x! zd6%nmg&XOpE=tUIjv4fmmoBO&f*#NJFI}81`n=7?>O6*EF15PtRS`|~s*TQ)ZS>Sc zvbD+#?J=yIiK>%oPOau8TG3|adD9y9fd5=HWuOsT3Z^uJ%&NqoMnd(HWjLQ}&irmDf{Es$OWk+Ldlk0Ke zh6VnmHi@oiCje8$_;NSbW^k!aQ=11vvo0~2`29l+@TLZaXN;MHZo0aNa1z{6ZPp`J z)u{fH{h5yYhM12D0@Ww$*n=_PMpoORa78(%F|A+8fp-6u_{jIiL^lu##pv8lNM*ss z+HH4`Fd^iZaABgp$(}6lcf=J8r^ehJ|_BRHVbj zBi#qtfe@PRsqC6^Lx_pL%CgioN5pdyc)Ge$Ori&-Orn|N5WqzpRdr#zMmxliRgsK1 zL57;(_SJH3ys?tA#UCW{Q>sDXQqtFSF{;a<_+m-^mQk>mlAu?9fs(QFN_EhAJsObZ zOWAUWd+ob*3B&)Es`3WmSa#(KZ!&j|vlcb4N~QJ%|Mxq2bbf(;lI(i@!sI2ZaK)m&%z={X<<{(`MxQQB>}Z+HMZV&3 zw&tSNmbn?btW3?ED~5C9>@mj4hc$_|*SBW0sLzq+OtfnVkLd?`!+)VqZ-=%YJQy3k zw0db}UsVfp^M{sQ&UwM2n=!i=tsW;aB>AI~O^ec2AM#0Xzn^Sw+ZMeQIn(f&MSW_J zREIbbW3$`+>ZrYQbMH%cextjXaIO@xdUdsIo3ct1i{aX4oRkgy_paZ$5w$uklRG!| zZl!l_?$^cc;5&|;+ud2mL|tuCmz!PPjuMkM(=kmhhrS-=OvBxU5M1fWl|>sV7)-^{%??+T!IG_HN|~<4+QsrB3$4LFn%%VSBpORWtJU znZ%CtZoPL)vsuR6wX7~`8~+iZSrzI+mZW*IdrX@_qu*D!Bkd%~@7!QeV!##&8{cqD zI*~1r%}U1DV%le;*5!`2abYIxZOk}{+NvfmzfiQZwX3DHBJ1R)`_ypGC1tD22>~)y zP**i}d>L~zw9SO+BFi2*#Lv!#s?}AQ-DtVjs12sme3P&%4x7ZX;MB#gQ=Rt>PBmGR zZXK6{z3YuvZD$2JA=(DLC-rFBZR|)jY?sSfDWHyhN5mvyU`3l3TvxhULhX!@|-`%*SrsII5#f)3QQaw&5*`WHn90n1Yb268H^;eEYj`NVGux7++|>PT%C(2IF8*6*cDQ%lb!Nuv z&k#~K|6w|*R8)1dBx!u_;zb!M!{|6UX&7oPq+}K2UT>9 zrP#e^@dQsM@nJ8HOtz7%(!F+3KuCq1IFsD6R2TX}E1C#%i2PRU=_h`xC5 zC5@AWsc@mGyNWK7?kZFnwp|*6-td1=X1tzixhb6u*+g?;3CW>;V`|V7e{tsvU=B+Eyq`9Rb1zQ%Nx~is(SCbcVEdh zXqq5Ve2gi)9rmu97(4q-&C=T5t)?Po@+4})&biDCBvvNkvsiRR0UUGp?oY0_aXM`M@x{BNVG&bq;zT>`Fb%D;L8%dr#o-l+crJ&Vp9 literal 0 HcmV?d00001 diff --git a/apps/catalogue/locale/pl/LC_MESSAGES/django.po b/apps/catalogue/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 00000000..c760f3a7 --- /dev/null +++ b/apps/catalogue/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,508 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Platforma Redakcyjna\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-06-21 13:41+0200\n" +"PO-Revision-Date: 2011-06-21 13:46+0100\n" +"Last-Translator: Radek Czajka \n" +"Language-Team: Fundacja Nowoczesna Polska \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:32 +msgid "Publishable" +msgstr "Gotowe do publikacji" + +#: forms.py:68 +msgid "ZIP file" +msgstr "Plik ZIP" + +#: forms.py:99 +#: forms.py:137 +msgid "Author" +msgstr "Autor" + +#: forms.py:100 +#: forms.py:138 +msgid "Your name" +msgstr "Imię i nazwisko" + +#: forms.py:105 +#: forms.py:143 +msgid "Author's email" +msgstr "E-mail autora" + +#: forms.py:106 +#: forms.py:144 +msgid "Your email address, so we can show a gravatar :)" +msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)" + +#: forms.py:112 +#: forms.py:150 +msgid "Your comments" +msgstr "Twój komentarz" + +#: forms.py:113 +msgid "Describe changes you made." +msgstr "Opisz swoje zmiany" + +#: forms.py:119 +msgid "Completed" +msgstr "Ukończono" + +#: forms.py:120 +msgid "If you completed a life cycle stage, select it." +msgstr "Jeśli został ukończony etap prac, wskaż go." + +#: forms.py:151 +msgid "Describe the reason for reverting." +msgstr "Opisz powód przywrócenia." + +#: forms.py:176 +#: forms.py:190 +msgid "Chunk with this slug already exists" +msgstr "Część z tym slugiem już istnieje" + +#: forms.py:202 +msgid "Append to" +msgstr "Dołącz do" + +#: models.py:25 +msgid "title" +msgstr "tytuł" + +#: models.py:26 +msgid "slug" +msgstr "" + +#: models.py:27 +msgid "scan gallery name" +msgstr "nazwa galerii skanów" + +#: models.py:29 +msgid "parent" +msgstr "rodzic" + +#: models.py:30 +msgid "parent number" +msgstr "numeracja rodzica" + +#: models.py:40 +msgid "book" +msgstr "książka" + +#: models.py:41 +msgid "books" +msgstr "książki" + +#: models.py:206 +msgid "name" +msgstr "nazwa" + +#: models.py:210 +msgid "theme" +msgstr "motyw" + +#: models.py:211 +msgid "themes" +msgstr "motywy" + +#: views.py:241 +#, python-format +msgid "Slug already used for %s" +msgstr "Slug taki sam jak dla pliku %s" + +#: views.py:243 +msgid "Slug already used in repository." +msgstr "Dokument o tym slugu już istnieje w repozytorium." + +#: views.py:249 +msgid "File should be UTF-8 encoded." +msgstr "Plik powinien mieć kodowanie UTF-8." + +#: views.py:655 +msgid "Tag added" +msgstr "Dodano tag" + +#: views.py:677 +msgid "Revision marked" +msgstr "Wersja oznaczona" + +#: views.py:679 +msgid "Nothing changed" +msgstr "Nic nie uległo zmianie" + +#: templates/wiki/base.html:9 +msgid "Platforma Redakcyjna" +msgstr "" + +#: templates/wiki/book_append_to.html:8 +msgid "Append book" +msgstr "Dołącz książkę" + +#: templates/wiki/book_detail.html:6 +#: templates/wiki/book_detail.html.py:46 +msgid "edit" +msgstr "edytuj" + +#: templates/wiki/book_detail.html:16 +msgid "add basic document structure" +msgstr "dodaj podstawową strukturę dokumentu" + +#: templates/wiki/book_detail.html:20 +msgid "change master tag to" +msgstr "zmień tak master na" + +#: templates/wiki/book_detail.html:24 +msgid "add begin trimming tag" +msgstr "dodaj początkowy ogranicznik" + +#: templates/wiki/book_detail.html:28 +msgid "add end trimming tag" +msgstr "dodaj końcowy ogranicznik" + +#: templates/wiki/book_detail.html:34 +msgid "unstructured text" +msgstr "tekst bez struktury" + +#: templates/wiki/book_detail.html:38 +msgid "unknown XML" +msgstr "nieznany XML" + +#: templates/wiki/book_detail.html:42 +msgid "broken document" +msgstr "uszkodzony dokument" + +#: templates/wiki/book_detail.html:60 +msgid "Apply fixes" +msgstr "Wykonaj zmiany" + +#: templates/wiki/book_detail.html:66 +msgid "Append to other book" +msgstr "Dołącz do innej książki" + +#: templates/wiki/book_detail.html:68 +msgid "Last published" +msgstr "Ostatnio opublikowano" + +#: templates/wiki/book_detail.html:72 +msgid "Full XML" +msgstr "Pełny XML" + +#: templates/wiki/book_detail.html:73 +msgid "HTML version" +msgstr "Wersja HTML" + +#: templates/wiki/book_detail.html:74 +msgid "TXT version" +msgstr "Wersja TXT" + +#: templates/wiki/book_detail.html:76 +msgid "EPUB version" +msgstr "Wersja EPUB" + +#: templates/wiki/book_detail.html:77 +msgid "PDF version" +msgstr "Wersja PDF" + +#: templates/wiki/book_detail.html:90 +#: templates/wiki/tabs/summary_view.html:30 +msgid "Publish" +msgstr "Opublikuj" + +#: templates/wiki/book_detail.html:94 +msgid "This book cannot be published yet" +msgstr "Ta książka nie może jeszcze zostać opublikowana" + +#: templates/wiki/book_edit.html:8 +#: templates/wiki/chunk_edit.html:8 +#: templates/wiki/document_details_base.html:35 +#: templates/wiki/pubmark_dialog.html:15 +#: templates/wiki/tag_dialog.html:15 +msgid "Save" +msgstr "Zapisz" + +#: templates/wiki/chunk_add.html:8 +msgid "Add chunk" +msgstr "Dodaj część" + +#: templates/wiki/diff_table.html:5 +msgid "Old version" +msgstr "Stara wersja" + +#: templates/wiki/diff_table.html:6 +msgid "New version" +msgstr "Nowa wersja" + +#: templates/wiki/document_create_missing.html:8 +msgid "Create document" +msgstr "Utwórz dokument" + +#: templates/wiki/document_details.html:32 +msgid "Click to open/close gallery" +msgstr "Kliknij, aby (ro)zwinąć galerię" + +#: templates/wiki/document_details_base.html:31 +msgid "Help" +msgstr "Pomoc" + +#: templates/wiki/document_details_base.html:33 +msgid "Version" +msgstr "Wersja" + +#: templates/wiki/document_details_base.html:33 +msgid "Unknown" +msgstr "nieznana" + +#: templates/wiki/document_details_base.html:36 +msgid "Save attempt in progress" +msgstr "Trwa zapisywanie" + +#: templates/wiki/document_details_base.html:37 +msgid "There is a newer version of this document!" +msgstr "Istnieje nowsza wersja tego dokumentu!" + +#: templates/wiki/document_list.html:31 +msgid "Clear filter" +msgstr "Wyczyść filtr" + +#: templates/wiki/document_list.html:46 +msgid "No books found." +msgstr "Nie znaleziono książek." + +#: templates/wiki/document_list.html:89 +msgid "Your last edited documents" +msgstr "Twoje ostatnie edycje" + +#: templates/wiki/document_upload.html:8 +msgid "Bulk documents upload" +msgstr "Hurtowe dodawanie dokumentów" + +#: templates/wiki/document_upload.html:11 +msgid "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with .xml will be ignored." +msgstr "Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie kończące się na .xml zostaną zignorowane." + +#: templates/wiki/document_upload.html:16 +#: templatetags/wiki.py:36 +msgid "Upload" +msgstr "Załaduj" + +#: templates/wiki/document_upload.html:23 +msgid "There have been some errors. No files have been added to the repository." +msgstr "Wystąpiły błędy. Żadne pliki nie zostały dodane do repozytorium." + +#: templates/wiki/document_upload.html:24 +msgid "Offending files" +msgstr "Błędne pliki" + +#: templates/wiki/document_upload.html:32 +msgid "Correct files" +msgstr "Poprawne pliki" + +#: templates/wiki/document_upload.html:43 +msgid "Files have been successfully uploaded to the repository." +msgstr "Pliki zostały dodane do repozytorium." + +#: templates/wiki/document_upload.html:44 +msgid "Uploaded files" +msgstr "Dodane pliki" + +#: templates/wiki/document_upload.html:54 +msgid "Skipped files" +msgstr "Pominięte pliki" + +#: templates/wiki/document_upload.html:55 +msgid "Files skipped due to no .xml extension" +msgstr "Pliki pominięte z powodu braku rozszerzenia .xml." + +#: templates/wiki/pubmark_dialog.html:16 +#: templates/wiki/revert_dialog.html:39 +#: templates/wiki/tag_dialog.html:16 +msgid "Cancel" +msgstr "Anuluj" + +#: templates/wiki/revert_dialog.html:38 +msgid "Revert" +msgstr "Przywróć" + +#: templates/wiki/user_list.html:7 +#: templatetags/wiki.py:33 +msgid "Users" +msgstr "Użytkownicy" + +#: templates/wiki/tabs/annotations_view.html:9 +msgid "all" +msgstr "wszystkie" + +#: templates/wiki/tabs/annotations_view_item.html:3 +msgid "Annotations" +msgstr "Przypisy" + +#: templates/wiki/tabs/gallery_view.html:7 +msgid "Previous" +msgstr "Poprzednie" + +#: templates/wiki/tabs/gallery_view.html:13 +msgid "Next" +msgstr "Następne" + +#: templates/wiki/tabs/gallery_view.html:15 +msgid "Zoom in" +msgstr "Powiększ" + +#: templates/wiki/tabs/gallery_view.html:16 +msgid "Zoom out" +msgstr "Zmniejsz" + +#: templates/wiki/tabs/gallery_view_item.html:3 +msgid "Gallery" +msgstr "Galeria" + +#: templates/wiki/tabs/history_view.html:5 +msgid "Compare versions" +msgstr "Porównaj wersje" + +#: templates/wiki/tabs/history_view.html:7 +msgid "Mark for publishing" +msgstr "Oznacz do publikacji" + +#: templates/wiki/tabs/history_view.html:9 +msgid "Revert document" +msgstr "Przywróć wersję" + +#: templates/wiki/tabs/history_view.html:12 +msgid "View version" +msgstr "Zobacz wersję" + +#: templates/wiki/tabs/history_view_item.html:3 +msgid "History" +msgstr "Historia" + +#: templates/wiki/tabs/search_view.html:3 +#: templates/wiki/tabs/search_view.html:5 +msgid "Search" +msgstr "Szukaj" + +#: templates/wiki/tabs/search_view.html:8 +msgid "Replace with" +msgstr "Zamień na" + +#: templates/wiki/tabs/search_view.html:10 +msgid "Replace" +msgstr "Zamień" + +#: templates/wiki/tabs/search_view.html:13 +msgid "Options" +msgstr "Opcje" + +#: templates/wiki/tabs/search_view.html:15 +msgid "Case sensitive" +msgstr "Rozróżniaj wielkość liter" + +#: templates/wiki/tabs/search_view.html:17 +msgid "From cursor" +msgstr "Zacznij od kursora" + +#: templates/wiki/tabs/search_view_item.html:3 +msgid "Search and replace" +msgstr "Znajdź i zamień" + +#: templates/wiki/tabs/source_editor_item.html:5 +msgid "Source code" +msgstr "Kod źródłowy" + +#: templates/wiki/tabs/summary_view.html:9 +msgid "Title" +msgstr "Tytuł" + +#: templates/wiki/tabs/summary_view.html:14 +msgid "Document ID" +msgstr "ID dokumentu" + +#: templates/wiki/tabs/summary_view.html:18 +msgid "Current version" +msgstr "Aktualna wersja" + +#: templates/wiki/tabs/summary_view.html:21 +msgid "Last edited by" +msgstr "Ostatnio edytowane przez" + +#: templates/wiki/tabs/summary_view.html:25 +msgid "Link to gallery" +msgstr "Link do galerii" + +#: templates/wiki/tabs/summary_view_item.html:3 +msgid "Summary" +msgstr "Podsumowanie" + +#: templates/wiki/tabs/wysiwyg_editor.html:9 +msgid "Insert theme" +msgstr "Wstaw motyw" + +#: templates/wiki/tabs/wysiwyg_editor.html:12 +msgid "Insert annotation" +msgstr "Wstaw przypis" + +#: templates/wiki/tabs/wysiwyg_editor_item.html:3 +msgid "Visual editor" +msgstr "Edytor wizualny" + +#: templatetags/wiki.py:30 +msgid "Assigned to me" +msgstr "Przypisane do mnie" + +#: templatetags/wiki.py:32 +msgid "Unassigned" +msgstr "Nie przypisane" + +#: templatetags/wiki.py:34 +msgid "All" +msgstr "Wszystkie" + +#: templatetags/wiki.py:35 +msgid "Add" +msgstr "Dodaj" + +#: templatetags/wiki.py:39 +msgid "Admin" +msgstr "Administracja" + +#~ msgid "First correction" +#~ msgstr "Autokorekta" + +#~ msgid "Tagging" +#~ msgstr "Tagowanie" + +#~ msgid "Initial Proofreading" +#~ msgstr "Korekta" + +#~ msgid "Annotation Proofreading" +#~ msgstr "Sprawdzenie przypisów źródła" + +#~ msgid "Modernisation" +#~ msgstr "Uwspółcześnienie" + +#~ msgid "Themes" +#~ msgstr "Motywy" + +#~ msgid "Editor's Proofreading" +#~ msgstr "Ostateczna redakcja literacka" + +#~ msgid "Technical Editor's Proofreading" +#~ msgstr "Ostateczna redakcja techniczna" + +#~ msgid "Finished stage: %s" +#~ msgstr "Ukończony etap: %s" + +#~ msgid "Refresh" +#~ msgstr "Odśwież" + +#~ msgid "Insert special character" +#~ msgstr "Wstaw znak specjalny" diff --git a/apps/wiki/management/__init__.py b/apps/catalogue/management/__init__.py similarity index 100% rename from apps/wiki/management/__init__.py rename to apps/catalogue/management/__init__.py diff --git a/apps/wiki/management/commands/__init__.py b/apps/catalogue/management/commands/__init__.py similarity index 100% rename from apps/wiki/management/commands/__init__.py rename to apps/catalogue/management/commands/__init__.py diff --git a/apps/wiki/management/commands/assign_from_redmine.py b/apps/catalogue/management/commands/assign_from_redmine.py similarity index 78% rename from apps/wiki/management/commands/assign_from_redmine.py rename to apps/catalogue/management/commands/assign_from_redmine.py index 45177497..9f7b12d4 100755 --- a/apps/wiki/management/commands/assign_from_redmine.py +++ b/apps/catalogue/management/commands/assign_from_redmine.py @@ -13,7 +13,7 @@ from django.core.management.color import color_style from django.db import transaction from slughifi import slughifi -from wiki.models import Chunk +from catalogue.models import Chunk REDMINE_CSV = 'http://redmine.nowoczesnapolska.org.pl/projects/wl-publikacje/issues.csv' @@ -58,9 +58,9 @@ class Command(BaseCommand): done_tickets = 0 done_chunks = 0 empty_users = 0 - unknown_users = 0 - unknown_books = 0 - forced = 0 + unknown_users = {} + unknown_books = [] + forced = [] if verbose: print 'Downloading CSV file' @@ -81,9 +81,8 @@ class Command(BaseCommand): user = User.objects.get(first_name=first_name, last_name=last_name) except User.DoesNotExist: print self.style.ERROR('Unknown user: ' + username) - print "'%s' '%s'" % (first_name, last_name) - print type(last_name) - unknown_users += 1 + unknown_users.setdefault(username, 0) + unknown_users[username] += 1 continue ticket_done = False @@ -97,7 +96,7 @@ class Command(BaseCommand): chunks = Chunk.objects.filter(book__slug=fname) if not chunks: print self.style.ERROR('Unknown book: ' + fname) - unknown_books += 1 + unknown_books.append(fname) continue all_chunks += chunks.count() @@ -106,7 +105,7 @@ class Command(BaseCommand): if chunk.user == user: continue else: - forced += 1 + forced.append((chunk, chunk.user, user)) if force: print self.style.WARNING( '%s assigned to %s, forcing change to %s.' % @@ -128,12 +127,24 @@ class Command(BaseCommand): # Print results print print "Results:" - print "Done %d/%d tickets, assigned %d/%d book chunks." % ( + print "Assignments imported from %d/%d tickets to %d/%d relevalt chunks." % ( done_tickets, all_tickets, done_chunks, all_chunks) - print "%d tickets unassigned, for %d chunks assignment differed." % ( - empty_users, forced) - print "Unrecognized: %d books, %d users." % ( - unknown_books, unknown_users) + if empty_users: + print "%d tickets were unassigned." % empty_users + if forced: + print "%d assignments conficts (%s):" % ( + len(forced), "changed" if force else "left") + for chunk, orig, user in forced: + print " %s: \t%s \t-> %s" % ( + chunk.pretty_name(), orig.username, user.username) + if unknown_books: + print "%d unknown books:" % len(unknown_books) + for fname in unknown_books: + print " %s" % fname + if unknown_users: + print "%d unknown users:" % len(unknown_users) + for name in unknown_users: + print " %s (%d tickets)" % (name, unknown_users[name]) print diff --git a/apps/wiki/migrations/0003_add_dvcs.py b/apps/catalogue/migrations/0001_initial.py similarity index 84% rename from apps/wiki/migrations/0003_add_dvcs.py rename to apps/catalogue/migrations/0001_initial.py index 87ac321e..4ebde478 100644 --- a/apps/wiki/migrations/0003_add_dvcs.py +++ b/apps/catalogue/migrations/0001_initial.py @@ -86,7 +86,7 @@ def migrate_file_from_hg(orm, fname, entry): book=book, number=1, slug='1') - head = orm['wiki.ChunkChange'].objects.create( + head = orm.ChunkChange.objects.create( tree=chunk, revision=-1, patch=make_patch('', ''), @@ -95,7 +95,7 @@ def migrate_file_from_hg(orm, fname, entry): ) chunk.head = head try: - chunk.stage = orm['wiki.ChunkTag'].objects.order_by('ordering')[0] + chunk.stage = orm.ChunkTag.objects.order_by('ordering')[0] except IndexError: chunk.stage = None old_data = '' @@ -112,12 +112,12 @@ def migrate_file_from_hg(orm, fname, entry): # get tags from description description = fctx.description().decode("utf-8", 'replace') tags = STAGE_TAGS_RE.findall(description) - tags = [orm['wiki.ChunkTag'].objects.get(slug=slug.strip()) for slug in tags] + tags = [orm.ChunkTag.objects.get(slug=slug.strip()) for slug in tags] if tags: max_ordering = max(tags, key=lambda x: x.ordering).ordering try: - chunk.stage = orm['wiki.ChunkTag'].objects.filter(ordering__gt=max_ordering).order_by('ordering')[0] + chunk.stage = orm.ChunkTag.objects.filter(ordering__gt=max_ordering).order_by('ordering')[0] except IndexError: chunk.stage = None @@ -135,7 +135,7 @@ def migrate_file_from_hg(orm, fname, entry): else: author_name = author_desc - head = orm['wiki.ChunkChange'].objects.create( + head = orm.ChunkChange.objects.create( tree=chunk, revision=rev + 1, patch=make_patch(old_data, data), @@ -176,73 +176,73 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'Book' - db.create_table('wiki_book', ( + db.create_table('catalogue_book', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('title', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), ('slug', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=128, db_index=True)), ('gallery', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['wiki.Book'])), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='children', null=True, to=orm['catalogue.Book'])), ('parent_number', self.gf('django.db.models.fields.IntegerField')(db_index=True, null=True, blank=True)), ('last_published', self.gf('django.db.models.fields.DateTimeField')(null=True, db_index=True)), )) - db.send_create_signal('wiki', ['Book']) + db.send_create_signal('catalogue', ['Book']) # Adding model 'Chunk' - db.create_table('wiki_chunk', ( + db.create_table('catalogue_chunk', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('creator', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='created_documents', null=True, to=orm['auth.User'])), ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), - ('book', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wiki.Book'])), + ('book', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.Book'])), ('number', self.gf('django.db.models.fields.IntegerField')()), ('slug', self.gf('django.db.models.fields.SlugField')(max_length=50, db_index=True)), ('comment', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), - ('stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['wiki.ChunkTag'], null=True, blank=True)), - ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['wiki.ChunkChange'], null=True, blank=True)), + ('stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['catalogue.ChunkTag'], null=True, blank=True)), + ('head', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['catalogue.ChunkChange'], null=True, blank=True)), )) - db.send_create_signal('wiki', ['Chunk']) + db.send_create_signal('catalogue', ['Chunk']) # Adding unique constraint on 'Chunk', fields ['book', 'number'] - db.create_unique('wiki_chunk', ['book_id', 'number']) + db.create_unique('catalogue_chunk', ['book_id', 'number']) # Adding unique constraint on 'Chunk', fields ['book', 'slug'] - db.create_unique('wiki_chunk', ['book_id', 'slug']) + db.create_unique('catalogue_chunk', ['book_id', 'slug']) # Adding model 'ChunkTag' - db.create_table('wiki_chunktag', ( + db.create_table('catalogue_chunktag', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('name', self.gf('django.db.models.fields.CharField')(max_length=64)), ('slug', self.gf('django.db.models.fields.SlugField')(db_index=True, max_length=64, unique=True, null=True, blank=True)), ('ordering', self.gf('django.db.models.fields.IntegerField')()), )) - db.send_create_signal('wiki', ['ChunkTag']) + db.send_create_signal('catalogue', ['ChunkTag']) # Adding model 'ChunkChange' - db.create_table('wiki_chunkchange', ( + db.create_table('catalogue_chunkchange', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('author', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), ('author_name', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)), ('author_email', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)), ('patch', self.gf('django.db.models.fields.TextField')(blank=True)), ('revision', self.gf('django.db.models.fields.IntegerField')(db_index=True)), - ('parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='children', null=True, blank=True, to=orm['wiki.ChunkChange'])), - ('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['wiki.ChunkChange'])), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='children', null=True, blank=True, to=orm['catalogue.ChunkChange'])), + ('merge_parent', self.gf('django.db.models.fields.related.ForeignKey')(default=None, related_name='merge_children', null=True, blank=True, to=orm['catalogue.ChunkChange'])), ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)), ('created_at', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, db_index=True)), ('publishable', self.gf('django.db.models.fields.BooleanField')(default=False)), - ('tree', self.gf('django.db.models.fields.related.ForeignKey')(related_name='change_set', to=orm['wiki.Chunk'])), + ('tree', self.gf('django.db.models.fields.related.ForeignKey')(related_name='change_set', to=orm['catalogue.Chunk'])), )) - db.send_create_signal('wiki', ['ChunkChange']) + db.send_create_signal('catalogue', ['ChunkChange']) # Adding unique constraint on 'ChunkChange', fields ['tree', 'revision'] - db.create_unique('wiki_chunkchange', ['tree_id', 'revision']) + db.create_unique('catalogue_chunkchange', ['tree_id', 'revision']) # Adding M2M table for field tags on 'ChunkChange' - db.create_table('wiki_chunkchange_tags', ( + db.create_table('catalogue_chunkchange_tags', ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('chunkchange', models.ForeignKey(orm['wiki.chunkchange'], null=False)), - ('chunktag', models.ForeignKey(orm['wiki.chunktag'], null=False)) + ('chunkchange', models.ForeignKey(orm['catalogue.chunkchange'], null=False)), + ('chunktag', models.ForeignKey(orm['catalogue.chunktag'], null=False)) )) - db.create_unique('wiki_chunkchange_tags', ['chunkchange_id', 'chunktag_id']) + db.create_unique('catalogue_chunkchange_tags', ['chunkchange_id', 'chunktag_id']) if not db.dry_run: from django.core.management import call_command @@ -250,31 +250,32 @@ class Migration(SchemaMigration): migrate_from_hg(orm) + def backwards(self, orm): # Removing unique constraint on 'ChunkChange', fields ['tree', 'revision'] - db.delete_unique('wiki_chunkchange', ['tree_id', 'revision']) + db.delete_unique('catalogue_chunkchange', ['tree_id', 'revision']) # Removing unique constraint on 'Chunk', fields ['book', 'slug'] - db.delete_unique('wiki_chunk', ['book_id', 'slug']) + db.delete_unique('catalogue_chunk', ['book_id', 'slug']) # Removing unique constraint on 'Chunk', fields ['book', 'number'] - db.delete_unique('wiki_chunk', ['book_id', 'number']) + db.delete_unique('catalogue_chunk', ['book_id', 'number']) # Deleting model 'Book' - db.delete_table('wiki_book') + db.delete_table('catalogue_book') # Deleting model 'Chunk' - db.delete_table('wiki_chunk') + db.delete_table('catalogue_chunk') # Deleting model 'ChunkTag' - db.delete_table('wiki_chunktag') + db.delete_table('catalogue_chunktag') # Deleting model 'ChunkChange' - db.delete_table('wiki_chunkchange') + db.delete_table('catalogue_chunkchange') # Removing M2M table for field tags on 'ChunkChange' - db.delete_table('wiki_chunkchange_tags') + db.delete_table('catalogue_chunkchange_tags') models = { @@ -307,36 +308,29 @@ class Migration(SchemaMigration): 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'wiki.book': { + 'catalogue.book': { 'Meta': {'ordering': "['parent_number', 'title']", 'object_name': 'Book'}, 'gallery': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'last_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}), - 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['wiki.Book']"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['catalogue.Book']"}), 'parent_number': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) }, - 'wiki.chunk': { + 'catalogue.chunk': { 'Meta': {'ordering': "['number']", 'unique_together': "[['book', 'number'], ['book', 'slug']]", 'object_name': 'Chunk'}, - 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.Book']"}), + 'book': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Book']"}), 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}), - 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['wiki.ChunkChange']", 'null': 'True', 'blank': 'True'}), + 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['catalogue.ChunkChange']", 'null': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'number': ('django.db.models.fields.IntegerField', [], {}), 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), - 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['wiki.ChunkTag']", 'null': 'True', 'blank': 'True'}), + 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ChunkTag']", 'null': 'True', 'blank': 'True'}), 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}) }, - 'wiki.chunkchange': { + 'catalogue.chunkchange': { 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'ChunkChange'}, 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), 'author_email': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), @@ -344,26 +338,28 @@ class Migration(SchemaMigration): 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}), 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['wiki.ChunkChange']"}), - 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['wiki.ChunkChange']"}), + 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['catalogue.ChunkChange']"}), 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), - 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['wiki.ChunkTag']"}), - 'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['wiki.Chunk']"}) + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'change_set'", 'symmetrical': 'False', 'to': "orm['catalogue.ChunkTag']"}), + 'tree': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'change_set'", 'to': "orm['catalogue.Chunk']"}) }, - 'wiki.chunktag': { + 'catalogue.chunktag': { 'Meta': {'ordering': "['ordering']", 'object_name': 'ChunkTag'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), 'ordering': ('django.db.models.fields.IntegerField', [], {}), 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}) }, - 'wiki.theme': { - 'Meta': {'ordering': "('name',)", 'object_name': 'Theme'}, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}) + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) } } - complete_apps = ['wiki'] + complete_apps = ['catalogue'] diff --git a/apps/wiki/templatetags/__init__.py b/apps/catalogue/migrations/__init__.py similarity index 100% rename from apps/wiki/templatetags/__init__.py rename to apps/catalogue/migrations/__init__.py diff --git a/apps/catalogue/models.py b/apps/catalogue/models.py new file mode 100644 index 00000000..ff3d434e --- /dev/null +++ b/apps/catalogue/models.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from dvcs import models as dvcs_models +from catalogue.xml_tools import compile_text + +import logging +logger = logging.getLogger("fnp.catalogue") + + +class Book(models.Model): + """ A document edited on the wiki """ + + title = models.CharField(_('title'), max_length=255, db_index=True) + slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True) + gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True) + + parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children") + parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True) + last_published = models.DateTimeField(null=True, editable=False, db_index=True) + + class NoTextError(BaseException): + pass + + class Meta: + ordering = ['parent_number', 'title'] + verbose_name = _('book') + verbose_name_plural = _('books') + + def __unicode__(self): + return self.title + + def get_absolute_url(self): + return reverse("catalogue_book", args=[self.slug]) + + @classmethod + def create(cls, creator=None, text=u'', *args, **kwargs): + """ + >>> Book.create(slug='x', text='abc').materialize() + 'abc' + """ + instance = cls(*args, **kwargs) + instance.save() + instance[0].commit(author=creator, text=text) + return instance + + def __iter__(self): + return iter(self.chunk_set.all()) + + def __getitem__(self, chunk): + return self.chunk_set.all()[chunk] + + def materialize(self, publishable=True): + """ + Get full text of the document compiled from chunks. + Takes the current versions of all texts + or versions most recently tagged for publishing. + """ + if publishable: + changes = [chunk.publishable() for chunk in self] + else: + changes = [chunk.head for chunk in self] + if None in changes: + raise self.NoTextError('Some chunks have no available text.') + return compile_text(change.materialize() for change in changes) + + def publishable(self): + if not len(self): + return False + for chunk in self: + if not chunk.publishable(): + 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: + instance.chunk_set.create(number=1, slug='1') + +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. """ + + book = models.ForeignKey(Book, editable=False) + number = models.IntegerField() + slug = models.SlugField() + comment = models.CharField(max_length=255, blank=True) + + class Meta: + unique_together = [['book', 'number'], ['book', 'slug']] + ordering = ['number'] + + def __unicode__(self): + return "%d-%d: %s" % (self.book_id, self.number, self.comment) + + def get_absolute_url(self): + return reverse("wiki_editor", args=[self.book.slug, self.slug]) + + @classmethod + def get(cls, slug, chunk=None): + if chunk is None: + return cls.objects.get(book__slug=slug, number=1) + else: + return cls.objects.get(book__slug=slug, slug=chunk) + + def pretty_name(self, book_length=None): + title = self.book.title + if self.comment: + title += ", %s" % self.comment + if book_length > 1: + title += " (%d/%d)" % (self.number, book_length) + return title + + 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: + # save book so that its _list_html is reset + instance.book.save() + +models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk) diff --git a/apps/wiki/templates/wiki/base.html b/apps/catalogue/templates/catalogue/base.html similarity index 85% rename from apps/wiki/templates/wiki/base.html rename to apps/catalogue/templates/catalogue/base.html index 83cfb7c5..4ba8a0ea 100644 --- a/apps/wiki/templates/wiki/base.html +++ b/apps/catalogue/templates/catalogue/base.html @@ -1,5 +1,5 @@ {% load compressed i18n %} -{% load wiki %} +{% load catalogue %} @@ -12,7 +12,7 @@
- + @@ -30,11 +30,11 @@
{% block content %} -
+
{% block leftcolumn %} {% endblock leftcolumn %}
-
+
{% block rightcolumn %} {% endblock rightcolumn %}
diff --git a/apps/wiki/templates/wiki/book_append_to.html b/apps/catalogue/templates/catalogue/book_append_to.html similarity index 89% rename from apps/wiki/templates/wiki/book_append_to.html rename to apps/catalogue/templates/catalogue/book_append_to.html index da6594da..76a59621 100755 --- a/apps/wiki/templates/wiki/book_append_to.html +++ b/apps/catalogue/templates/catalogue/book_append_to.html @@ -1,4 +1,4 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} {% block leftcolumn %} diff --git a/apps/wiki/templates/wiki/book_detail.html b/apps/catalogue/templates/catalogue/book_detail.html similarity index 74% rename from apps/wiki/templates/wiki/book_detail.html rename to apps/catalogue/templates/catalogue/book_detail.html index 5ea0e3df..1c0178c5 100755 --- a/apps/wiki/templates/wiki/book_detail.html +++ b/apps/catalogue/templates/catalogue/book_detail.html @@ -1,9 +1,9 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load comments i18n %} {% block leftcolumn %} -{% trans "edit" %} +{% trans "edit" %}

{{ book.title }}

@@ -43,12 +43,12 @@ {% endifequal %} - + - + {% endfor %} {% if need_fixing %} @@ -64,18 +64,18 @@ {% endif %}
[{% trans "edit" %}][{% trans "edit" %}] {% if c.chunk.publishable %}P{% endif %} {% if c.chunk.user.is_authenticated %} - {{ c.chunk.user }} + {{ c.chunk.user }} {% endif %}[+][+]
-

{% trans "Append to other book" %}

+

{% trans "Append to other book" %}

{% trans "Last published" %}: {{ book.last_published }}

{% if book.publishable %}

- {% trans "Full XML" %}
- {% trans "HTML version" %}
- {% trans "TXT version" %}
+ {% trans "Full XML" %}
+ {% trans "HTML version" %}
+ {% trans "TXT version" %}
{% comment %} - {% trans "EPUB version" %}
- {% trans "PDF version" %}
+ {% trans "EPUB version" %}
+ {% trans "PDF version" %}
{% endcomment %}

@@ -85,7 +85,7 @@ mira66 (http://www.flickr.com/photos/21804434@N02/) / CC BY 2.0 (http://creativecommons.org/licenses/by/2.0/) --> -
{% csrf_token %} + {% csrf_token %} diff --git a/apps/wiki/templates/wiki/book_edit.html b/apps/catalogue/templates/catalogue/book_edit.html similarity index 88% rename from apps/wiki/templates/wiki/book_edit.html rename to apps/catalogue/templates/catalogue/book_edit.html index 8bc7ea7f..3fffa963 100755 --- a/apps/wiki/templates/wiki/book_edit.html +++ b/apps/catalogue/templates/catalogue/book_edit.html @@ -1,4 +1,4 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} {% block leftcolumn %} diff --git a/apps/wiki/templates/wiki/chunk_add.html b/apps/catalogue/templates/catalogue/chunk_add.html similarity index 88% rename from apps/wiki/templates/wiki/chunk_add.html rename to apps/catalogue/templates/catalogue/chunk_add.html index 836c34d6..800a7e45 100755 --- a/apps/wiki/templates/wiki/chunk_add.html +++ b/apps/catalogue/templates/catalogue/chunk_add.html @@ -1,4 +1,4 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} {% block leftcolumn %} diff --git a/apps/wiki/templates/wiki/chunk_edit.html b/apps/catalogue/templates/catalogue/chunk_edit.html similarity index 88% rename from apps/wiki/templates/wiki/chunk_edit.html rename to apps/catalogue/templates/catalogue/chunk_edit.html index 8bc7ea7f..3fffa963 100755 --- a/apps/wiki/templates/wiki/chunk_edit.html +++ b/apps/catalogue/templates/catalogue/chunk_edit.html @@ -1,4 +1,4 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} {% block leftcolumn %} diff --git a/apps/wiki/templates/wiki/document_create_missing.html b/apps/catalogue/templates/catalogue/document_create_missing.html similarity index 89% rename from apps/wiki/templates/wiki/document_create_missing.html rename to apps/catalogue/templates/catalogue/document_create_missing.html index 9414f7cb..dcb61752 100644 --- a/apps/wiki/templates/wiki/document_create_missing.html +++ b/apps/catalogue/templates/catalogue/document_create_missing.html @@ -1,4 +1,4 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} {% block leftcolumn %} diff --git a/apps/wiki/templates/wiki/document_list.html b/apps/catalogue/templates/catalogue/document_list.html similarity index 77% rename from apps/wiki/templates/wiki/document_list.html rename to apps/catalogue/templates/catalogue/document_list.html index f68ba3eb..9a40dfcc 100644 --- a/apps/wiki/templates/wiki/document_list.html +++ b/apps/catalogue/templates/catalogue/document_list.html @@ -1,8 +1,8 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} {% load pagination_tags %} -{% load wiki %} +{% load catalogue %} {% block extrabody %} {{ block.super }} @@ -51,30 +51,30 @@ $(function() { {% ifequal item.book_length 1 %} {% with item.chunks.0 as chunk %} - [B] - [c] + [B] + [c] {{ book.title }} ({{ chunk.stage }}) - {% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %} + {% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %} {% endwith %} {% else %} - [B] + [B] {{ book.title }} {% for chunk in item.chunks %} - [c] + [c] {{ chunk.number }}. {{ chunk.comment }} ({{ chunk.stage }}) - {% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %} + {% if chunk.user %}{{ chunk.user.first_name }} {{ chunk.user.last_name }}{% endif %} {% endfor %} {% endifequal %} diff --git a/apps/wiki/templates/wiki/document_upload.html b/apps/catalogue/templates/catalogue/document_upload.html similarity index 98% rename from apps/wiki/templates/wiki/document_upload.html rename to apps/catalogue/templates/catalogue/document_upload.html index f7af2cef..87e93e0a 100644 --- a/apps/wiki/templates/wiki/document_upload.html +++ b/apps/catalogue/templates/catalogue/document_upload.html @@ -1,4 +1,4 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} diff --git a/apps/wiki/templates/wiki/main_tabs.html b/apps/catalogue/templates/catalogue/main_tabs.html similarity index 100% rename from apps/wiki/templates/wiki/main_tabs.html rename to apps/catalogue/templates/catalogue/main_tabs.html diff --git a/apps/wiki/templates/wiki/user_list.html b/apps/catalogue/templates/catalogue/user_list.html similarity index 75% rename from apps/wiki/templates/wiki/user_list.html rename to apps/catalogue/templates/catalogue/user_list.html index 8996cc09..9e1e83e5 100755 --- a/apps/wiki/templates/wiki/user_list.html +++ b/apps/catalogue/templates/catalogue/user_list.html @@ -1,4 +1,4 @@ -{% extends "wiki/base.html" %} +{% extends "catalogue/base.html" %} {% load i18n %} @@ -8,7 +8,7 @@
    {% for user in users %} -
  • +
  • {{ forloop.counter }}. {{ user.first_name }} {{ user.last_name }} ({{ user.count }})
  • diff --git a/apps/wiki/templates/wiki/wall.html b/apps/catalogue/templates/catalogue/wall.html similarity index 84% rename from apps/wiki/templates/wiki/wall.html rename to apps/catalogue/templates/catalogue/wall.html index ed4685a0..0e4597e5 100755 --- a/apps/wiki/templates/wiki/wall.html +++ b/apps/catalogue/templates/catalogue/wall.html @@ -10,13 +10,13 @@
    {% endif %} - {% trans item.tag %} +
{{ item.timestamp }}
{% if item.user %} - + {{ item.user.first_name }} {{ item.user.last_name }} <{{ item.user.email }}> {% else %} diff --git a/apps/catalogue/templatetags/__init__.py b/apps/catalogue/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/wiki/templatetags/wiki.py b/apps/catalogue/templatetags/catalogue.py similarity index 84% rename from apps/wiki/templatetags/wiki.py rename to apps/catalogue/templatetags/catalogue.py index 3a1cc172..cdb19404 100644 --- a/apps/wiki/templatetags/wiki.py +++ b/apps/catalogue/templatetags/catalogue.py @@ -7,7 +7,7 @@ from django.template.defaultfilters import stringfilter from django import template from django.utils.translation import ugettext as _ -from wiki.models import Book, Chunk +from catalogue.models import Book, Chunk register = template.Library() @@ -23,19 +23,19 @@ class Tab(object): self.url = url -@register.inclusion_tag("wiki/main_tabs.html", takes_context=True) +@register.inclusion_tag("catalogue/main_tabs.html", takes_context=True) def main_tabs(context): - active = getattr(context['request'], 'wiki_active_tab', None) + active = getattr(context['request'], 'catalogue_active_tab', None) tabs = [] user = context['user'] if user.is_authenticated(): - tabs.append(Tab('my', _('My page'), reverse("wiki_user"))) + tabs.append(Tab('my', _('My page'), reverse("catalogue_user"))) - tabs.append(Tab('unassigned', _('Unassigned'), reverse("wiki_unassigned"))) - tabs.append(Tab('users', _('Users'), reverse("wiki_users"))) - tabs.append(Tab('create', _('Add'), reverse("wiki_create_missing"))) - tabs.append(Tab('upload', _('Upload'), reverse("wiki_upload"))) + tabs.append(Tab('unassigned', _('Unassigned'), reverse("catalogue_unassigned"))) + tabs.append(Tab('users', _('Users'), reverse("catalogue_users"))) + tabs.append(Tab('create', _('Add'), reverse("catalogue_create_missing"))) + tabs.append(Tab('upload', _('Upload'), reverse("catalogue_upload"))) if user.is_staff: tabs.append(Tab('admin', _('Admin'), reverse("admin:index"))) @@ -129,7 +129,7 @@ def big_wall(max_len, *args): del subwalls[i] -@register.inclusion_tag("wiki/wall.html", takes_context=True) +@register.inclusion_tag("catalogue/wall.html", takes_context=True) def wall(context, max_len=10): return { "request": context['request'], diff --git a/apps/catalogue/tests.py b/apps/catalogue/tests.py new file mode 100644 index 00000000..65777379 --- /dev/null +++ b/apps/catalogue/tests.py @@ -0,0 +1,19 @@ +from nose.tools import * +import wiki.models as models +import shutil +import tempfile + + +class TestStorageBase: + def setUp(self): + self.dirpath = tempfile.mkdtemp(prefix='nosetest_') + + def tearDown(self): + shutil.rmtree(self.dirpath) + + +class TestDocumentStorage(TestStorageBase): + + def test_storage_empty(self): + storage = models.DocumentStorage(self.dirpath) + eq_(storage.all(), []) diff --git a/apps/catalogue/urls.py b/apps/catalogue/urls.py new file mode 100644 index 00000000..93c02b13 --- /dev/null +++ b/apps/catalogue/urls.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 +from django.conf.urls.defaults import * +from django.views.generic.simple import redirect_to + + +urlpatterns = patterns('catalogue.views', + url(r'^$', redirect_to, {'url': 'catalogue/'}), + + url(r'^catalogue/$', 'document_list', name='catalogue_document_list'), + url(r'^unassigned/$', 'unassigned', name='catalogue_unassigned'), + url(r'^user/$', 'my', name='catalogue_user'), + url(r'^user/(?P[^/]+)/$', 'user', name='catalogue_user'), + url(r'^users/$', 'users', name='catalogue_users'), + + url(r'^upload/$', + 'upload', name='catalogue_upload'), + + url(r'^create/(?P[^/]*)/', + 'create_missing', name='catalogue_create_missing'), + url(r'^create/', + 'create_missing', name='catalogue_create_missing'), + + url(r'^book/(?P[^/]+)/publish$', 'publish', name="catalogue_publish"), + #url(r'^(?P[^/]+)/publish/(?P\d+)$', 'publish', name="catalogue_publish"), + + url(r'^book/(?P[^/]+)/$', 'book', name="catalogue_book"), + url(r'^book/(?P[^/]+)/xml$', 'book_xml', name="catalogue_book_xml"), + url(r'^book/(?P[^/]+)/txt$', 'book_txt', name="catalogue_book_txt"), + url(r'^book/(?P[^/]+)/html$', 'book_html', name="catalogue_book_html"), + #url(r'^book/(?P[^/]+)/epub$', 'book_epub', name="catalogue_book_epub"), + #url(r'^book/(?P[^/]+)/pdf$', 'book_pdf', name="catalogue_book_pdf"), + url(r'^chunk_add/(?P[^/]+)/(?P[^/]+)/$', + 'chunk_add', name="catalogue_chunk_add"), + url(r'^chunk_edit/(?P[^/]+)/(?P[^/]+)/$', + 'chunk_edit', name="catalogue_chunk_edit"), + url(r'^book_append/(?P[^/]+)/$', + 'book_append', name="catalogue_book_append"), + url(r'^book_edit/(?P[^/]+)/$', + 'book_edit', name="catalogue_book_edit"), + +) diff --git a/apps/catalogue/views.py b/apps/catalogue/views.py new file mode 100644 index 00000000..a44c7e81 --- /dev/null +++ b/apps/catalogue/views.py @@ -0,0 +1,418 @@ +from datetime import datetime +import logging +import os +from StringIO import StringIO + +from django.contrib import auth +from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse +from django.db.models import Count +from django import http +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.utils.http import urlquote_plus +from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.http import require_POST +from django.views.generic.simple import direct_to_template + +import librarian.html +import librarian.text + +from apiclient import api_call +from catalogue import forms +from catalogue import helpers +from catalogue.helpers import active_tab +from catalogue.models import Book, Chunk +from catalogue import xml_tools + +# +# Quick hack around caching problems, TODO: use ETags +# +from django.views.decorators.cache import never_cache + +logger = logging.getLogger("fnp.catalogue") + + +@active_tab('all') +@never_cache +def document_list(request): + chunks_list = helpers.ChunksList(Chunk.objects.order_by( + 'book__title', 'book', 'number')) + + return direct_to_template(request, 'catalogue/document_list.html', extra_context={ + 'books': chunks_list, + #'books': [helpers.BookChunks(b) for b in Book.objects.all().select_related()], + 'last_books': sorted(request.session.get("wiki_last_books", {}).items(), + key=lambda x: x[1]['time'], reverse=True), + }) + + +@active_tab('unassigned') +@never_cache +def unassigned(request): + chunks_list = helpers.ChunksList(Chunk.objects.filter( + user=None).order_by('book__title', 'book__id', 'number')) + + return direct_to_template(request, 'catalogue/document_list.html', extra_context={ + 'books': chunks_list, + 'last_books': sorted(request.session.get("wiki_last_books", {}).items(), + key=lambda x: x[1]['time'], reverse=True), + }) + + +@never_cache +def user(request, username=None): + if username is None: + if request.user.is_authenticated(): + user = request.user + else: + raise Http404 + else: + user = get_object_or_404(User, username=username) + + chunks_list = helpers.ChunksList(Chunk.objects.filter( + user=user).order_by('book__title', 'book', 'number')) + + return direct_to_template(request, 'catalogue/document_list.html', extra_context={ + 'books': chunks_list, + 'last_books': sorted(request.session.get("wiki_last_books", {}).items(), + key=lambda x: x[1]['time'], reverse=True), + }) +my = login_required(active_tab('my')(user)) + + +@active_tab('users') +def users(request): + return direct_to_template(request, 'catalogue/user_list.html', extra_context={ + 'users': User.objects.all().annotate(count=Count('chunk')).order_by( + '-count', 'last_name', 'first_name'), + }) + + +@never_cache +def logout_then_redirect(request): + auth.logout(request) + return http.HttpResponseRedirect(urlquote_plus(request.GET.get('next', '/'), safe='/?=')) + + +@active_tab('create') +def create_missing(request, slug=None): + if slug is None: + slug = '' + slug = slug.replace(' ', '-') + + if request.method == "POST": + form = forms.DocumentCreateForm(request.POST, request.FILES) + if form.is_valid(): + + if request.user.is_authenticated(): + creator = request.user + else: + creator = None + book = Book.create(creator=creator, + slug=form.cleaned_data['slug'], + title=form.cleaned_data['title'], + text=form.cleaned_data['text'], + ) + + return http.HttpResponseRedirect(reverse("wiki_editor", args=[book.slug])) + else: + form = forms.DocumentCreateForm(initial={ + "slug": slug, + "title": slug.replace('-', ' ').title(), + }) + + return direct_to_template(request, "catalogue/document_create_missing.html", extra_context={ + "slug": slug, + "form": form, + }) + + +@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.create(creator=creator, + slug=slug, + title=title, + text=zip.read(filename).decode('utf-8'), + ) + + return direct_to_template(request, "catalogue/document_upload.html", extra_context={ + "form": form, + "ok_list": ok_list, + "skipped_list": skipped_list, + "error_list": error_list, + }) + else: + form = forms.DocumentsUploadForm() + + return direct_to_template(request, "catalogue/document_upload.html", extra_context={ + "form": form, + }) + + +@never_cache +def book_xml(request, slug): + xml = get_object_or_404(Book, slug=slug).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): + xml = get_object_or_404(Book, slug=slug).materialize() + output = StringIO() + # errors? + librarian.text.transform(StringIO(xml), output) + text = output.getvalue() + 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, slug): + xml = get_object_or_404(Book, slug=slug).materialize() + output = StringIO() + # errors? + librarian.html.transform(StringIO(xml), output, parse_dublincore=False, + flags=['full-page']) + html = output.getvalue() + response = http.HttpResponse(html, content_type='text/html', mimetype='text/html') + return response + + +@never_cache +def revision(request, slug, chunk=None): + try: + doc = Chunk.get(slug, chunk) + except (Chunk.MultipleObjectsReturned, Chunk.DoesNotExist): + raise Http404 + return http.HttpResponse(str(doc.revision())) + + +def book(request, slug): + book = get_object_or_404(Book, slug=slug) + + # TODO: most of this should go somewhere else + + # do we need some automation? + first_master = None + chunks = [] + need_fixing = False + choose_master = False + + length = book.chunk_set.count() + for i, chunk in enumerate(book): + chunk_dict = { + "chunk": chunk, + "fix": [], + "grade": "" + } + graded = xml_tools.GradedText(chunk.materialize()) + if graded.is_wl(): + master = graded.master() + if first_master is None: + first_master = master + elif master != first_master: + chunk_dict['fix'].append('bad-master') + + if i > 0 and not graded.has_trim_begin(): + chunk_dict['fix'].append('trim-begin') + if i < length - 1 and not graded.has_trim_end(): + chunk_dict['fix'].append('trim-end') + + if chunk_dict['fix']: + chunk_dict['grade'] = 'wl-fix' + else: + chunk_dict['grade'] = 'wl' + + elif graded.is_broken_wl(): + chunk_dict['grade'] = 'wl-broken' + elif graded.is_xml(): + chunk_dict['grade'] = 'xml' + else: + chunk_dict['grade'] = 'plain' + chunk_dict['fix'].append('wl') + choose_master = True + + if chunk_dict['fix']: + need_fixing = True + chunks.append(chunk_dict) + + if first_master or not need_fixing: + choose_master = False + + if request.method == "POST": + form = forms.ChooseMasterForm(request.POST) + if not choose_master or form.is_valid(): + if choose_master: + first_master = form.cleaned_data['master'] + + # do the actual fixing + for c in chunks: + if not c['fix']: + continue + + text = c['chunk'].materialize() + for fix in c['fix']: + if fix == 'bad-master': + text = xml_tools.change_master(text, first_master) + elif fix == 'trim-begin': + text = xml_tools.add_trim_begin(text) + elif fix == 'trim-end': + text = xml_tools.add_trim_end(text) + elif fix == 'wl': + text = xml_tools.basic_structure(text, first_master) + author = request.user if request.user.is_authenticated() else None + description = "auto-fix: " + ", ".join(c['fix']) + c['chunk'].commit(text=text, author=author, + description=description) + + return http.HttpResponseRedirect(book.get_absolute_url()) + elif choose_master: + form = forms.ChooseMasterForm() + else: + form = None + + return direct_to_template(request, "catalogue/book_detail.html", extra_context={ + "book": book, + "chunks": chunks, + "need_fixing": need_fixing, + "choose_master": choose_master, + "first_master": first_master, + "form": form, + }) + + +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, "catalogue/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, "catalogue/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, "catalogue/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, "catalogue/book_edit.html", extra_context={ + "book": book, + "form": form, + }) + + +@require_POST +@login_required +def publish(request, slug): + book = get_object_or_404(Book, slug=slug) + try: + ret = api_call(request.user, "books", {"book_xml": book.materialize()}) + except BaseException, e: + return http.HttpResponse(e) + else: + book.last_published = datetime.now() + book.save() + return http.HttpResponseRedirect(book.get_absolute_url()) diff --git a/apps/wiki/xml_tools.py b/apps/catalogue/xml_tools.py similarity index 98% rename from apps/wiki/xml_tools.py rename to apps/catalogue/xml_tools.py index 6dc50893..928e57be 100755 --- a/apps/wiki/xml_tools.py +++ b/apps/catalogue/xml_tools.py @@ -2,7 +2,7 @@ from functools import wraps import re from lxml import etree -from wiki.constants import TRIM_BEGIN, TRIM_END, MASTERS +from catalogue.constants import TRIM_BEGIN, TRIM_END, MASTERS RE_TRIM_BEGIN = re.compile("^$" % TRIM_BEGIN, re.M) RE_TRIM_END = re.compile("^$" % TRIM_END, re.M) diff --git a/apps/wiki/admin.py b/apps/wiki/admin.py index b742f70d..1a61b660 100644 --- a/apps/wiki/admin.py +++ b/apps/wiki/admin.py @@ -2,12 +2,4 @@ from django.contrib import admin from wiki import models -class BookAdmin(admin.ModelAdmin): - prepopulated_fields = {'slug': ['title']} - - -admin.site.register(models.Book, BookAdmin) -admin.site.register(models.Chunk) admin.site.register(models.Theme) - -admin.site.register(models.Chunk.tag_model) diff --git a/apps/wiki/forms.py b/apps/wiki/forms.py index 6b10909b..a8a57c88 100644 --- a/apps/wiki/forms.py +++ b/apps/wiki/forms.py @@ -3,22 +3,10 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -from django.contrib.auth.models import User -from django.db.models import Count from django import forms from django.utils.translation import ugettext_lazy as _ -from wiki.constants import MASTERS -from wiki.models import Book, Chunk - -class DocumentTagForm(forms.Form): - """ - Form for tagging revisions. - """ - - id = forms.CharField(widget=forms.HiddenInput) - tag = forms.ModelChoiceField(queryset=Chunk.tag_model.objects.all()) - revision = forms.IntegerField(widget=forms.HiddenInput) +from catalogue.models import Chunk class DocumentPubmarkForm(forms.Form): @@ -32,54 +20,6 @@ class DocumentPubmarkForm(forms.Form): revision = forms.IntegerField(widget=forms.HiddenInput) -class DocumentCreateForm(forms.ModelForm): - """ - Form used for creating new documents. - """ - file = forms.FileField(required=False) - text = forms.CharField(required=False, widget=forms.Textarea) - - class Meta: - model = Book - exclude = ['gallery', 'parent', 'parent_number'] - prepopulated_fields = {'slug': ['title']} - - def clean(self): - super(DocumentCreateForm, self).clean() - file = self.cleaned_data['file'] - - if file is not None: - try: - self.cleaned_data['text'] = file.read().decode('utf-8') - except UnicodeDecodeError: - raise forms.ValidationError("Text file must be UTF-8 encoded.") - - if not self.cleaned_data["text"]: - raise forms.ValidationError("You must either enter text or upload a file") - - return self.cleaned_data - - -class DocumentsUploadForm(forms.Form): - """ - Form used for uploading new documents. - """ - file = forms.FileField(required=True, label=_('ZIP file')) - - def clean(self): - file = self.cleaned_data['file'] - - import zipfile - try: - z = self.cleaned_data['zip'] = zipfile.ZipFile(file) - except zipfile.BadZipfile: - raise forms.ValidationError("Should be a ZIP file.") - if z.testzip(): - raise forms.ValidationError("ZIP file corrupt.") - - return self.cleaned_data - - class DocumentTextSaveForm(forms.Form): """ Form for saving document's text: @@ -149,70 +89,3 @@ class DocumentTextRevertForm(forms.Form): label=_(u"Your comments"), help_text=_(u"Describe the reason for reverting."), ) - - -class ChunkForm(forms.ModelForm): - """ - Form used for editing a chunk. - """ - user = forms.ModelChoiceField(queryset= - User.objects.annotate(count=Count('chunk')). - order_by('-count', 'last_name', 'first_name')) - - - 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.instance: - 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(), - label=_("Append to")) - - -class BookForm(forms.ModelForm): - """ - Form used for editing a Book. - """ - - class Meta: - model = Book - - -class ChooseMasterForm(forms.Form): - """ - Form used for fixing the chunks in a book. - """ - - master = forms.ChoiceField(choices=((m, m) for m in MASTERS)) diff --git a/apps/wiki/helpers.py b/apps/wiki/helpers.py index 3e0267ed..dace3d00 100644 --- a/apps/wiki/helpers.py +++ b/apps/wiki/helpers.py @@ -1,9 +1,9 @@ +from datetime import datetime +from functools import wraps + from django import http -from django.db.models import Count from django.utils import simplejson as json from django.utils.functional import Promise -from datetime import datetime -from functools import wraps class ExtendedEncoder(json.JSONEncoder): @@ -59,136 +59,3 @@ def ajax_require_permission(permission): return view(request, *args, **kwargs) return authorized_view return decorator - -import collections - -def recursive_groupby(iterable): - """ -# >>> recursive_groupby([1,2,3,4,5]) -# [1, 2, 3, 4, 5] - - >>> recursive_groupby([[1]]) - [1] - - >>> recursive_groupby([('a', 1),('a', 2), 3, ('b', 4), 5]) - ['a', [1, 2], 3, 'b', [4], 5] - - >>> recursive_groupby([('a', 'x', 1),('a', 'x', 2), ('a', 'x', 3)]) - ['a', ['x', [1, 2, 3]]] - - """ - - def _generator(iterator): - group = None - grouper = None - - for item in iterator: - if not isinstance(item, collections.Sequence): - if grouper is not None: - yield grouper - if len(group): - yield recursive_groupby(group) - group = None - grouper = None - yield item - continue - elif len(item) == 1: - if grouper is not None: - yield grouper - if len(group): - yield recursive_groupby(group) - group = None - grouper = None - yield item[0] - continue - elif not len(item): - continue - - if grouper is None: - group = [item[1:]] - grouper = item[0] - continue - - if grouper != item[0]: - if grouper is not None: - yield grouper - if len(group): - yield recursive_groupby(group) - group = None - grouper = None - group = [item[1:]] - grouper = item[0] - continue - - group.append(item[1:]) - - if grouper is not None: - yield grouper - if len(group): - yield recursive_groupby(group) - group = None - grouper = None - - return list(_generator(iterable)) - - -def active_tab(tab): - """ - View decorator, which puts tab info on a request. - """ - def wrapper(f): - @wraps(f) - def wrapped(request, *args, **kwargs): - request.wiki_active_tab = tab - return f(request, *args, **kwargs) - return wrapped - return wrapper - - -class ChunksList(object): - def __init__(self, chunk_qs): - self.chunk_qs = chunk_qs.annotate( - book_length=Count('book__chunk')).select_related( - 'book', 'stage__name', - 'user') - - self.book_qs = chunk_qs.values('book_id') - - def __getitem__(self, key): - if isinstance(key, slice): - return self.get_slice(key) - elif isinstance(key, int): - return self.get_slice(slice(key, key+1))[0] - else: - raise TypeError('Unsupported list index. Must be a slice or an int.') - - def __len__(self): - return self.book_qs.count() - - def get_slice(self, slice_): - book_ids = [x['book_id'] for x in self.book_qs[slice_]] - chunk_qs = self.chunk_qs.filter(book__in=book_ids) - - chunks_list = [] - book = None - for chunk in chunk_qs: - if chunk.book != book: - book = chunk.book - chunks_list.append(ChoiceChunks(book, [chunk], chunk.book_length)) - else: - chunks_list[-1].chunks.append(chunk) - return chunks_list - - -class ChoiceChunks(object): - """ - Associates the given chunks iterable for a book. - """ - - chunks = None - - def __init__(self, book, chunks, book_length): - self.book = book - self.chunks = chunks - self.book_length = book_length - diff --git a/apps/wiki/models.py b/apps/wiki/models.py index 20c8b869..c539908d 100644 --- a/apps/wiki/models.py +++ b/apps/wiki/models.py @@ -3,191 +3,13 @@ # This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later. # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # -import itertools -import re - -from django.core.urlresolvers import reverse from django.db import models -from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ -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") -class Book(models.Model): - """ A document edited on the wiki """ - - title = models.CharField(_('title'), max_length=255, db_index=True) - slug = models.SlugField(_('slug'), max_length=128, unique=True, db_index=True) - gallery = models.CharField(_('scan gallery name'), max_length=255, blank=True) - - parent = models.ForeignKey('self', null=True, blank=True, verbose_name=_('parent'), related_name="children") - parent_number = models.IntegerField(_('parent number'), null=True, blank=True, db_index=True) - last_published = models.DateTimeField(null=True, editable=False, db_index=True) - - class NoTextError(BaseException): - pass - - class Meta: - ordering = ['parent_number', 'title'] - verbose_name = _('book') - verbose_name_plural = _('books') - - def __unicode__(self): - return self.title - - def get_absolute_url(self): - return reverse("wiki_book", args=[self.slug]) - - @classmethod - def create(cls, creator=None, text=u'', *args, **kwargs): - """ - >>> Book.create(slug='x', text='abc').materialize() - 'abc' - """ - instance = cls(*args, **kwargs) - instance.save() - instance[0].commit(author=creator, text=text) - return instance - - def __iter__(self): - return iter(self.chunk_set.all()) - - def __getitem__(self, chunk): - return self.chunk_set.all()[chunk] - - def materialize(self, publishable=True): - """ - Get full text of the document compiled from chunks. - Takes the current versions of all texts - or versions most recently tagged for publishing. - """ - if publishable: - changes = [chunk.publishable() for chunk in self] - else: - changes = [chunk.head for chunk in self] - if None in changes: - raise self.NoTextError('Some chunks have no available text.') - return compile_text(change.materialize() for change in changes) - - def publishable(self): - if not len(self): - return False - for chunk in self: - if not chunk.publishable(): - 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: - instance.chunk_set.create(number=1, slug='1') - -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. """ - - book = models.ForeignKey(Book, editable=False) - number = models.IntegerField() - slug = models.SlugField() - comment = models.CharField(max_length=255, blank=True) - - class Meta: - unique_together = [['book', 'number'], ['book', 'slug']] - ordering = ['number'] - - def __unicode__(self): - return "%d-%d: %s" % (self.book_id, self.number, self.comment) - - def get_absolute_url(self): - return reverse("wiki_editor", args=[self.book.slug, self.slug]) - - @classmethod - def get(cls, slug, chunk=None): - if chunk is None: - return cls.objects.get(book__slug=slug, number=1) - else: - return cls.objects.get(book__slug=slug, slug=chunk) - - def pretty_name(self, book_length=None): - title = self.book.title - if self.comment: - title += ", %s" % self.comment - if book_length > 1: - title += " (%d/%d)" % (self.number, book_length) - return title - - 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 - - def list_html(self): - _list_html = render_to_string('wiki/chunk_list_item.html', - {'chunk': self}) - return mark_safe(_list_html) - - @staticmethod - def listener_saved(sender, instance, created, **kwargs): - if instance.book: - # save book so that its _list_html is reset - instance.book.save() - -models.signals.post_save.connect(Chunk.listener_saved, sender=Chunk) - - class Theme(models.Model): name = models.CharField(_('name'), max_length=50, unique=True) diff --git a/apps/wiki/templates/wiki/document_details_base.html b/apps/wiki/templates/wiki/document_details_base.html index 5f98b733..76711dd9 100644 --- a/apps/wiki/templates/wiki/document_details_base.html +++ b/apps/wiki/templates/wiki/document_details_base.html @@ -26,7 +26,7 @@