total mess: some random experiments with images and lqc's dvcs
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Fri, 10 Dec 2010 14:24:59 +0000 (15:24 +0100)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Tue, 18 Jan 2011 11:03:50 +0000 (12:03 +0100)
57 files changed:
apps/dvcs/__init__.py [new file with mode: 0644]
apps/dvcs/admin.py [new file with mode: 0644]
apps/dvcs/models.py [new file with mode: 0644]
apps/dvcs/tests.py [new file with mode: 0644]
apps/dvcs/urls.py [new file with mode: 0644]
apps/dvcs/views.py [new file with mode: 0644]
apps/toolbar/admin.py
apps/wiki/admin.py
apps/wiki_img/__init__.py [new file with mode: 0644]
apps/wiki_img/admin.py [new file with mode: 0644]
apps/wiki_img/constants.py [new file with mode: 0644]
apps/wiki_img/forms.py [new file with mode: 0644]
apps/wiki_img/helpers.py [new file with mode: 0644]
apps/wiki_img/locale/pl/LC_MESSAGES/django.mo [new file with mode: 0644]
apps/wiki_img/locale/pl/LC_MESSAGES/django.po [new file with mode: 0644]
apps/wiki_img/models.py [new file with mode: 0644]
apps/wiki_img/nice_diff.py [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/base.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/diff_table.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/document_create_missing.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/document_details.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/document_details_base.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/document_details_readonly.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/document_list.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/save_dialog.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/motifs_editor.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/motifs_editor_item.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/objects_editor.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/objects_editor_item.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/source_editor.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/source_editor_item.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/summary_view.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tabs/summary_view_item.html [new file with mode: 0644]
apps/wiki_img/templates/wiki_img/tag_dialog.html [new file with mode: 0644]
apps/wiki_img/tests.py [new file with mode: 0644]
apps/wiki_img/urls.py [new file with mode: 0644]
apps/wiki_img/views.py [new file with mode: 0644]
redakcja/settings/common.py
redakcja/settings/compress.py
redakcja/static/css/imgareaselect-default.css [new file with mode: 0644]
redakcja/static/css/master.css
redakcja/static/img/jquery.imgareaselect/border-anim-h.gif [new file with mode: 0644]
redakcja/static/img/jquery.imgareaselect/border-anim-v.gif [new file with mode: 0644]
redakcja/static/img/jquery.imgareaselect/border-h.gif [new file with mode: 0644]
redakcja/static/img/jquery.imgareaselect/border-v.gif [new file with mode: 0644]
redakcja/static/js/lib/jquery/jquery.imgareaselect.js [new file with mode: 0644]
redakcja/static/js/wiki_img/base.js [new file with mode: 0644]
redakcja/static/js/wiki_img/dialog_addtag.js [new file with mode: 0644]
redakcja/static/js/wiki_img/dialog_save.js [new file with mode: 0644]
redakcja/static/js/wiki_img/loader.js [new file with mode: 0644]
redakcja/static/js/wiki_img/toolbar.js [new file with mode: 0644]
redakcja/static/js/wiki_img/view_editor_motifs.js [new file with mode: 0644]
redakcja/static/js/wiki_img/view_editor_objects.js [new file with mode: 0644]
redakcja/static/js/wiki_img/view_editor_source.js [new file with mode: 0644]
redakcja/static/js/wiki_img/view_summary.js [new file with mode: 0644]
redakcja/static/js/wiki_img/wikiapi.js [new file with mode: 0644]
redakcja/urls.py

diff --git a/apps/dvcs/__init__.py b/apps/dvcs/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/dvcs/admin.py b/apps/dvcs/admin.py
new file mode 100644 (file)
index 0000000..c81d3b7
--- /dev/null
@@ -0,0 +1,5 @@
+from django.contrib.admin import site
+from dvcs.models import Document, Change
+
+site.register(Document)
+site.register(Change)
diff --git a/apps/dvcs/models.py b/apps/dvcs/models.py
new file mode 100644 (file)
index 0000000..ea83ff0
--- /dev/null
@@ -0,0 +1,151 @@
+from django.db import models
+from django.contrib.auth.models import User
+from django.utils.translation import ugettext_lazy as _
+from mercurial import mdiff, simplemerge
+import pickle
+
+class Change(models.Model):
+    """
+        Single document change related to previous change. The "parent"
+        argument points to the version against which this change has been 
+        recorded. Initial text will have a null parent.
+        
+        Data contains a pickled diff needed to reproduce the initial document.
+    """
+    author = models.ForeignKey(User, null=True, blank=True) 
+    patch = models.TextField(blank=True)
+    tree = models.ForeignKey('Document')
+
+    parent = models.ForeignKey('self',
+                        null=True, blank=True, default=None,
+                        related_name="children")
+
+    merge_parent = models.ForeignKey('self',
+                        null=True, blank=True, default=None,
+                        related_name="merge_children")
+
+    description = models.TextField(blank=True, default='')
+    created_at = models.DateTimeField(auto_now_add=True)
+
+    class Meta:
+        ordering = ('created_at',)
+
+    def __unicode__(self):
+        return u"Id: %r, Tree %r, Parent %r, Patch '''\n%s'''" % (self.id, self.tree_id, self.parent_id, self.patch)
+
+    @staticmethod
+    def make_patch(src, dst):
+        if isinstance(src, unicode):
+            src = src.encode('utf-8')
+        if isinstance(dst, unicode):
+            dst = dst.encode('utf-8')
+        return pickle.dumps(mdiff.textdiff(src, dst))
+
+    def materialize(self):
+        changes = Change.objects.exclude(parent=None).filter(
+                        tree=self.tree,
+                        created_at__lte=self.created_at).order_by('created_at')
+        text = u''
+        for change in changes:
+            text = change.apply_to(text)
+        return text
+
+    def make_child(self, patch, author, description):
+        return self.children.create(patch=patch,
+                        tree=self.tree, author=author,
+                        description=description)
+
+    def make_merge_child(self, patch, author, description):
+        return self.merge_children.create(patch=patch,
+                        tree=self.tree, author=author,
+                        description=description)
+
+    def apply_to(self, text):
+        return mdiff.patch(text, pickle.loads(self.patch.encode('ascii')))
+
+    def merge_with(self, other, author, description=u"Automatic merge."):
+        assert self.tree_id == other.tree_id  # same tree
+        if other.parent_id == self.pk:
+            # immediate child 
+            return other
+
+        local = self.materialize()
+        base = other.merge_parent.materialize()
+        remote = other.apply_to(base)
+
+        merge = simplemerge.Merge3Text(base, local, remote)
+        result = ''.join(merge.merge_lines())
+        patch = self.make_patch(local, result)
+        return self.children.create(
+                    patch=patch, merge_parent=other, tree=self.tree,
+                    author=author, description=description)
+
+
+class Document(models.Model):
+    """
+        File in repository.        
+    """
+    creator = models.ForeignKey(User, null=True, blank=True)
+    head = models.ForeignKey(Change,
+                    null=True, blank=True, default=None,
+                    help_text=_("This document's current head."))
+
+    def __unicode__(self):
+        return u"{0}, HEAD: {1}".format(self.id, self.head_id)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('dvcs.views.document_data', (), {
+                        'document_id': self.id,
+                        'version': self.head_id,
+        })
+
+    def materialize(self, version=None):
+        if self.head is None:
+            return u''
+        if version is None:
+            version = self.head
+        elif not isinstance(version, Change):
+            version = self.change_set.get(pk=version)
+        return version.materialize()
+
+    def commit(self, **kwargs):
+        if 'parent' not in kwargs:
+            parent = self.head
+        else:
+            parent = kwargs['parent']
+            if not isinstance(parent, Change):
+                parent = Change.objects.get(pk=kwargs['parent'])
+
+        if 'patch' not in kwargs:
+            if 'text' not in kwargs:
+                raise ValueError("You must provide either patch or target document.")
+            patch = Change.make_patch(self.materialize(version=parent), kwargs['text'])
+        else:
+            if 'text' in kwargs:
+                raise ValueError("You can provide only text or patch - not both")
+            patch = kwargs['patch']
+
+        old_head = self.head
+        if parent != old_head:
+            change = parent.make_merge_child(patch, kwargs['author'], kwargs.get('description', ''))
+            # not Fast-Forward - perform a merge
+            self.head = old_head.merge_with(change, author=kwargs['author'])
+        else:
+            self.head = parent.make_child(patch, kwargs['author'], kwargs.get('description', ''))
+        self.save()
+        return self.head
+
+    def history(self):
+        return self.changes.all()
+
+    @staticmethod
+    def listener_initial_commit(sender, instance, created, **kwargs):
+        if created:
+            instance.head = Change.objects.create(
+                    author=instance.creator,
+                    patch=pickle.dumps(mdiff.textdiff('', '')),
+                    tree=instance)
+            instance.save()
+
+models.signals.post_save.connect(Document.listener_initial_commit, sender=Document)
diff --git a/apps/dvcs/tests.py b/apps/dvcs/tests.py
new file mode 100644 (file)
index 0000000..0c71295
--- /dev/null
@@ -0,0 +1,164 @@
+from django.test import TestCase
+from dvcs.models import Change, Document
+from django.contrib.auth.models import User
+
+class DocumentModelTests(TestCase):
+
+    def setUp(self):
+        self.user = User.objects.create_user("tester", "tester@localhost.local")
+
+    def assertTextEqual(self, given, expected):
+        return self.assertEqual(given, expected,
+            "Expected '''%s'''\n differs from text: '''%s'''" % (expected, given)
+        )
+
+    def test_empty_file(self):
+        doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+        self.assert_(doc.head is not None)
+        self.assertEqual(doc.materialize(), u"")
+
+    def test_single_commit(self):
+        doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+        doc.commit(text=u"Ala ma kota", description="Commit #1", author=self.user)
+        self.assert_(doc.head is not None)
+        self.assertEqual(doc.change_set.count(), 2)
+        self.assertEqual(doc.materialize(), u"Ala ma kota")
+
+    def test_chained_commits(self):
+        doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+        c1 = doc.commit(description="Commit #1", text=u"""
+            Line #1
+            Line #2 is cool
+        """, author=self.user)
+        c2 = doc.commit(description="Commit #2", text=u"""
+            Line #1
+            Line #2 is hot
+        """, author=self.user)
+        c3 = doc.commit(description="Commit #3", text=u"""
+            Line #1
+            ... is hot
+            Line #3 ate Line #2
+        """, author=self.user)
+        self.assert_(doc.head is not None)
+        self.assertEqual(doc.change_set.count(), 4)
+
+        self.assertEqual(doc.materialize(), u"""
+            Line #1
+            ... is hot
+            Line #3 ate Line #2
+        """)
+        self.assertEqual(doc.materialize(version=c3), u"""
+            Line #1
+            ... is hot
+            Line #3 ate Line #2
+        """)
+        self.assertEqual(doc.materialize(version=c2), u"""
+            Line #1
+            Line #2 is hot
+        """)
+        self.assertEqual(doc.materialize(version=c1), """
+            Line #1
+            Line #2 is cool
+        """)
+
+
+    def test_parallel_commit_noconflict(self):
+        doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+        self.assert_(doc.head is not None)
+        base = doc.head
+        base = doc.commit(description="Commit #1", text=u"""
+            Line #1
+            Line #2
+""", author=self.user)
+
+        c1 = doc.commit(description="Commit #2", text=u"""
+            Line #1 is hot
+            Line #2
+""", parent=base, author=self.user)
+        self.assertTextEqual(c1.materialize(), u"""
+            Line #1 is hot
+            Line #2
+""")
+        c2 = doc.commit(description="Commit #3", text=u"""
+            Line #1
+            Line #2
+            Line #3
+""", parent=base, author=self.user)
+        self.assertEqual(doc.change_set.count(), 5)
+        self.assertTextEqual(doc.materialize(), u"""
+            Line #1 is hot
+            Line #2
+            Line #3
+""")
+
+    def test_parallel_commit_conflict(self):
+        doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+        self.assert_(doc.head is not None)
+        base = doc.head
+        base = doc.commit(description="Commit #1", text=u"""
+Line #1
+Line #2
+Line #3
+""", author=self.user)
+
+        c1 = doc.commit(description="Commit #2", text=u"""
+Line #1
+Line #2 is hot
+Line #3
+""", parent=base, author=self.user)
+        c2 = doc.commit(description="Commit #3", text=u"""
+Line #1
+Line #2 is cool
+Line #3
+""", parent=base, author=self.user)
+        self.assertEqual(doc.change_set.count(), 5)
+        self.assertTextEqual(doc.materialize(), u"""
+Line #1
+<<<<<<<
+Line #2 is hot
+=======
+Line #2 is cool
+>>>>>>>
+Line #3
+""")
+
+    def test_multiply_parallel_commits(self):
+        doc = Document.objects.create(name=u"Sample Document", creator=self.user)
+        self.assert_(doc.head is not None)
+        c1 = doc.commit(description="Commit A1", text=u"""
+Line #1
+
+Line #2
+
+Line #3
+""", author=self.user)
+        c2 = doc.commit(description="Commit A2", text=u"""
+Line #1 *
+
+Line #2
+
+Line #3
+""", author=self.user)
+        c3 = doc.commit(description="Commit B1", text=u"""
+Line #1
+
+Line #2 **
+
+Line #3
+""", parent=c1, author=self.user)
+        c4 = doc.commit(description="Commit C1", text=u"""
+Line #1 *
+
+Line #2
+
+Line #3 ***
+""", parent=c2, author=self.user)
+        self.assertEqual(doc.change_set.count(), 7)
+        self.assertTextEqual(doc.materialize(), u"""
+Line #1 *
+
+Line #2 **
+
+Line #3 ***
+""")
+
diff --git a/apps/dvcs/urls.py b/apps/dvcs/urls.py
new file mode 100644 (file)
index 0000000..d1e1e29
--- /dev/null
@@ -0,0 +1,6 @@
+# -*- coding: utf-8
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('dvcs.views',
+    url(r'^data/(?P<document_id>[^/]+)/(?P<version>.*)$', 'document_data', name='storage_document_data'),
+)
diff --git a/apps/dvcs/views.py b/apps/dvcs/views.py
new file mode 100644 (file)
index 0000000..7918e96
--- /dev/null
@@ -0,0 +1,21 @@
+# Create your views here.
+from django.views.generic.simple import direct_to_template
+from django import http
+from dvcs.models import Document
+
+def document_list(request, template_name="dvcs/document_list.html"):
+    return direct_to_template(request, template_name, {
+        "documents": Document.objects.all(),
+    })
+
+def document_data(request, document_id, version=None):
+    doc = Document.objects.get(pk=document_id)
+    return http.HttpResponse(doc.materialize(version or None), content_type="text/plain")
+
+def document_history(request, docid, template_name="dvcs/document_history.html"):
+    document = Document.objects.get(pk=docid)
+    return direct_to_template(request, template_name, {
+        "document": document,
+        "changes": document.history(),
+    })
+
index 283ab78..ea87936 100644 (file)
@@ -25,6 +25,6 @@ class ButtonAdmin(admin.ModelAdmin):
     list_editable = ('label', 'tooltip', 'accesskey')
     prepopulated_fields = {'slug': ('label',)}
 
-admin.site.register(models.Button, ButtonAdmin)
-admin.site.register(models.ButtonGroup)
-admin.site.register(models.Scriptlet)
+#admin.site.register(models.Button, ButtonAdmin)
+#admin.site.register(models.ButtonGroup)
+#admin.site.register(models.Scriptlet)
index 1a61b66..9c32b43 100644 (file)
@@ -2,4 +2,4 @@ from django.contrib import admin
 
 from wiki import models
 
-admin.site.register(models.Theme)
+#admin.site.register(models.Theme)
diff --git a/apps/wiki_img/__init__.py b/apps/wiki_img/__init__.py
new file mode 100644 (file)
index 0000000..c53f0e7
--- /dev/null
@@ -0,0 +1 @@
+  # pragma: no cover
diff --git a/apps/wiki_img/admin.py b/apps/wiki_img/admin.py
new file mode 100644 (file)
index 0000000..80f7b36
--- /dev/null
@@ -0,0 +1,4 @@
+from django.contrib.admin import site
+from wiki_img.models import ImageDocument
+
+site.register(ImageDocument)
diff --git a/apps/wiki_img/constants.py b/apps/wiki_img/constants.py
new file mode 100644 (file)
index 0000000..6781a48
--- /dev/null
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from django.utils.translation import ugettext_lazy as _
+
+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)
diff --git a/apps/wiki_img/forms.py b/apps/wiki_img/forms.py
new file mode 100644 (file)
index 0000000..25f9413
--- /dev/null
@@ -0,0 +1,97 @@
+# -*- 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 import forms
+from wiki.constants import DOCUMENT_TAGS, DOCUMENT_STAGES
+from django.utils.translation import ugettext_lazy as _
+
+
+class DocumentTagForm(forms.Form):
+    """
+        Form for tagging revisions.
+    """
+
+    id = forms.CharField(widget=forms.HiddenInput)
+    tag = forms.ChoiceField(choices=DOCUMENT_TAGS)
+    revision = forms.IntegerField(widget=forms.HiddenInput)
+
+
+class DocumentCreateForm(forms.Form):
+    """
+        Form used for creating new documents.
+    """
+    title = forms.CharField()
+    id = forms.RegexField(regex=ur"^[-\wąćęłńóśźżĄĆĘŁŃÓŚŹŻ]+$")
+    file = forms.FileField(required=False)
+    text = forms.CharField(required=False, widget=forms.Textarea)
+
+    def clean(self):
+        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:
+
+        * name - document's storage identifier.
+        * parent_revision - revision which the modified text originated from.
+        * comment - user's verbose comment; will be used in commit.
+        * stage_completed - mark this change as end of given stage.
+
+    """
+
+    id = forms.CharField(widget=forms.HiddenInput)
+    parent_revision = forms.IntegerField(widget=forms.HiddenInput)
+    text = forms.CharField(widget=forms.HiddenInput)
+
+    author_name = forms.CharField(
+        required=False,
+        label=_(u"Author"),
+        help_text=_(u"Your name"),
+    )
+
+    author_email = forms.EmailField(
+        required=False,
+        label=_(u"Author's email"),
+        help_text=_(u"Your email address, so we can show a gravatar :)"),
+    )
+
+    comment = forms.CharField(
+        required=True,
+        widget=forms.Textarea,
+        label=_(u"Your comments"),
+        help_text=_(u"Describe changes you made."),
+    )
diff --git a/apps/wiki_img/helpers.py b/apps/wiki_img/helpers.py
new file mode 100644 (file)
index 0000000..f072ef9
--- /dev/null
@@ -0,0 +1,131 @@
+from django import http
+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):
+
+    def default(self, obj):
+        if isinstance(obj, Promise):
+            return unicode(obj)
+
+        if isinstance(obj, datetime):
+            return datetime.ctime(obj) + " " + (datetime.tzname(obj) or 'GMT')
+
+        return json.JSONEncoder.default(self, obj)
+
+
+# shortcut for JSON reponses
+class JSONResponse(http.HttpResponse):
+
+    def __init__(self, data={}, **kwargs):
+        # get rid of mimetype
+        kwargs.pop('mimetype', None)
+
+        data = json.dumps(data, cls=ExtendedEncoder)
+        super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs)
+
+
+# return errors
+class JSONFormInvalid(JSONResponse):
+    def __init__(self, form):
+        super(JSONFormInvalid, self).__init__(form.errors, status=400)
+
+
+class JSONServerError(JSONResponse):
+    def __init__(self, *args, **kwargs):
+        kwargs['status'] = 500
+        super(JSONServerError, self).__init__(*args, **kwargs)
+
+
+def ajax_login_required(view):
+    @wraps(view)
+    def authenticated_view(request, *args, **kwargs):
+        if not request.user.is_authenticated():
+            return http.HttpResponse("Login required.", status=401, mimetype="text/plain")
+        return view(request, *args, **kwargs)
+    return authenticated_view
+
+
+def ajax_require_permission(permission):
+    def decorator(view):
+        @wraps(view)
+        def authorized_view(request, *args, **kwargs):
+            if not request.user.has_perm(permission):
+                return http.HttpResponse("Access Forbidden.", status=403, mimetype="text/plain")
+            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))
diff --git a/apps/wiki_img/locale/pl/LC_MESSAGES/django.mo b/apps/wiki_img/locale/pl/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..c334ead
Binary files /dev/null and b/apps/wiki_img/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/apps/wiki_img/locale/pl/LC_MESSAGES/django.po b/apps/wiki_img/locale/pl/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..568e844
--- /dev/null
@@ -0,0 +1,346 @@
+# 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 <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Platforma Redakcyjna\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-29 15:34+0200\n"
+"PO-Revision-Date: 2010-09-29 15:36+0100\n"
+"Last-Translator: Radek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>\n"
+"Language-Team: Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: constants.py:6
+msgid "First correction"
+msgstr "Autokorekta"
+
+#: constants.py:7
+msgid "Tagging"
+msgstr "Tagowanie"
+
+#: constants.py:8
+msgid "Initial Proofreading"
+msgstr "Korekta"
+
+#: constants.py:9
+msgid "Annotation Proofreading"
+msgstr "Sprawdzenie przypisów źródła"
+
+#: constants.py:10
+msgid "Modernisation"
+msgstr "Uwspółcześnienie"
+
+#: constants.py:11
+#: templates/wiki/tabs/annotations_view_item.html:3
+msgid "Annotations"
+msgstr "Przypisy"
+
+#: constants.py:12
+msgid "Themes"
+msgstr "Motywy"
+
+#: constants.py:13
+msgid "Editor's Proofreading"
+msgstr "Ostateczna redakcja literacka"
+
+#: constants.py:14
+msgid "Technical Editor's Proofreading"
+msgstr "Ostateczna redakcja techniczna"
+
+#: constants.py:18
+msgid "Ready to publish"
+msgstr "Gotowe do publikacji"
+
+#: forms.py:49
+msgid "ZIP file"
+msgstr "Plik ZIP"
+
+#: forms.py:82
+msgid "Author"
+msgstr "Autor"
+
+#: forms.py:83
+msgid "Your name"
+msgstr "Imię i nazwisko"
+
+#: forms.py:88
+msgid "Author's email"
+msgstr "E-mail autora"
+
+#: forms.py:89
+msgid "Your email address, so we can show a gravatar :)"
+msgstr "Adres e-mail, żebyśmy mogli pokazać gravatar :)"
+
+#: forms.py:95
+msgid "Your comments"
+msgstr "Twój komentarz"
+
+#: forms.py:96
+msgid "Describe changes you made."
+msgstr "Opisz swoje zmiany"
+
+#: forms.py:102
+msgid "Completed"
+msgstr "Ukończono"
+
+#: forms.py:103
+msgid "If you completed a life cycle stage, select it."
+msgstr "Jeśli został ukończony etap prac, wskaż go."
+
+#: models.py:93
+#, python-format
+msgid "Finished stage: %s"
+msgstr "Ukończony etap: %s"
+
+#: models.py:152
+msgid "name"
+msgstr "nazwa"
+
+#: models.py:156
+msgid "theme"
+msgstr "motyw"
+
+#: models.py:157
+msgid "themes"
+msgstr "motywy"
+
+#: views.py:167
+#, python-format
+msgid "Title already used for %s"
+msgstr "Nazwa taka sama jak dla pliku %s"
+
+#: views.py:169
+msgid "Title already used in repository."
+msgstr "Plik o tej nazwie już istnieje w repozytorium."
+
+#: views.py:175
+msgid "File should be UTF-8 encoded."
+msgstr "Plik powinien mieć kodowanie UTF-8."
+
+#: views.py:358
+msgid "Tag added"
+msgstr "Dodano tag"
+
+#: templates/wiki/base.html:15
+msgid "Platforma Redakcyjna"
+msgstr ""
+
+#: 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:36
+msgid "Help"
+msgstr "Pomoc"
+
+#: templates/wiki/document_details_base.html:38
+msgid "Version"
+msgstr "Wersja"
+
+#: templates/wiki/document_details_base.html:38
+msgid "Unknown"
+msgstr "nieznana"
+
+#: templates/wiki/document_details_base.html:40
+#: templates/wiki/tag_dialog.html:15
+msgid "Save"
+msgstr "Zapisz"
+
+#: templates/wiki/document_details_base.html:41
+msgid "Save attempt in progress"
+msgstr "Trwa zapisywanie"
+
+#: templates/wiki/document_details_base.html:42
+msgid "There is a newer version of this document!"
+msgstr "Istnieje nowsza wersja tego dokumentu!"
+
+#: templates/wiki/document_list.html:30
+msgid "Clear filter"
+msgstr "Wyczyść filtr"
+
+#: templates/wiki/document_list.html:48
+msgid "Your last edited documents"
+msgstr "Twoje ostatnie edycje"
+
+#: templates/wiki/document_upload.html:9
+msgid "Bulk documents upload"
+msgstr "Hurtowe dodawanie dokumentów"
+
+#: templates/wiki/document_upload.html:12
+msgid "Please submit a ZIP with UTF-8 encoded XML files. Files not ending with <code>.xml</code> will be ignored."
+msgstr "Proszę wskazać archiwum ZIP z plikami XML w kodowaniu UTF-8. Pliki nie kończące się na <code>.xml</code> zostaną zignorowane."
+
+#: templates/wiki/document_upload.html:17
+msgid "Upload"
+msgstr "Dodaj"
+
+#: templates/wiki/document_upload.html:24
+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:25
+msgid "Offending files"
+msgstr "Błędne pliki"
+
+#: templates/wiki/document_upload.html:33
+msgid "Correct files"
+msgstr "Poprawne pliki"
+
+#: templates/wiki/document_upload.html:44
+msgid "Files have been successfully uploaded to the repository."
+msgstr "Pliki zostały dodane do repozytorium."
+
+#: templates/wiki/document_upload.html:45
+msgid "Uploaded files"
+msgstr "Dodane pliki"
+
+#: templates/wiki/document_upload.html:55
+msgid "Skipped files"
+msgstr "Pominięte pliki"
+
+#: templates/wiki/document_upload.html:56
+msgid "Files skipped due to no <code>.xml</code> extension"
+msgstr "Pliki pominięte z powodu braku rozszerzenia <code>.xml</code>."
+
+#: templates/wiki/tag_dialog.html:16
+msgid "Cancel"
+msgstr "Anuluj"
+
+#: templates/wiki/tabs/annotations_view.html:5
+msgid "Refresh"
+msgstr "Odśwież"
+
+#: 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 version"
+msgstr "Oznacz wersję"
+
+#: 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:10
+msgid "Title"
+msgstr "Tytuł"
+
+#: templates/wiki/tabs/summary_view.html:15
+msgid "Document ID"
+msgstr "ID dokumentu"
+
+#: templates/wiki/tabs/summary_view.html:19
+msgid "Current version"
+msgstr "Aktualna wersja"
+
+#: templates/wiki/tabs/summary_view.html:22
+msgid "Last edited by"
+msgstr "Ostatnio edytowane przez"
+
+#: templates/wiki/tabs/summary_view.html:26
+msgid "Link to gallery"
+msgstr "Link do galerii"
+
+#: templates/wiki/tabs/summary_view.html:31
+msgid "Publish"
+msgstr "Opublikuj"
+
+#: templates/wiki/tabs/summary_view_item.html:4
+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.html:15
+msgid "Insert special character"
+msgstr "Wstaw znak specjalny"
+
+#: templates/wiki/tabs/wysiwyg_editor_item.html:3
+msgid "Visual editor"
+msgstr "Edytor wizualny"
+
diff --git a/apps/wiki_img/models.py b/apps/wiki_img/models.py
new file mode 100644 (file)
index 0000000..dd16a87
--- /dev/null
@@ -0,0 +1,29 @@
+# -*- 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.db import models
+from django.contrib.auth.models import User
+from django.utils.translation import ugettext_lazy as _
+from dvcs.models import Document
+
+
+class ImageDocument(models.Model):
+    slug = models.SlugField(_('slug'), max_length=120)
+    name = models.CharField(_('name'), max_length=120)
+    image = models.ImageField(_('image'), upload_to='wiki_img')
+    doc = models.OneToOneField(Document, null=True, blank=True)
+    creator = models.ForeignKey(User, null=True, blank=True)
+
+    @staticmethod
+    def listener_initial_commit(sender, instance, created, **kwargs):
+        if created:
+            instance.doc = Document.objects.create(creator=instance.creator)
+            instance.save()
+
+    def __unicode__(self):
+        return self.name
+
+
+models.signals.post_save.connect(ImageDocument.listener_initial_commit, sender=ImageDocument)
diff --git a/apps/wiki_img/nice_diff.py b/apps/wiki_img/nice_diff.py
new file mode 100644 (file)
index 0000000..b228fad
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- 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.
+#
+import difflib
+import re
+from collections import deque
+
+from django.template.loader import render_to_string
+from django.utils.html import escape as html_escape
+
+DIFF_RE = re.compile(r"""\x00([+^-])""", re.UNICODE)
+NAMES = {'+': 'added', '-': 'removed', '^': 'changed'}
+
+
+def diff_replace(match):
+    return """<span class="diff_mark diff_mark_%s">""" % NAMES[match.group(1)]
+
+
+def filter_line(line):
+    return  DIFF_RE.sub(diff_replace, html_escape(line)).replace('\x01', '</span>')
+
+
+def format_changeset(a, b, change):
+    return (a[0], filter_line(a[1]), b[0], filter_line(b[1]), change)
+
+
+def html_diff_table(la, lb, context=None):
+    all_changes = difflib._mdiff(la, lb)
+
+    if context is None:
+        changes = (format_changeset(*c) for c in all_changes)
+    else:
+        changes = []
+        q = deque()
+        after_change = False
+
+        for changeset in all_changes:
+            q.append(changeset)
+
+            if changeset[2]:
+                after_change = True
+                if not after_change:
+                    changes.append((0, '-----', 0, '-----', False))
+                changes.extend(format_changeset(*c) for c in q)
+                q.clear()
+            else:
+                if len(q) == context and after_change:
+                    changes.extend(format_changeset(*c) for c in q)
+                    q.clear()
+                    after_change = False
+                elif len(q) > context:
+                    q.popleft()
+
+    return render_to_string("wiki/diff_table.html", {
+        "changes": changes,
+    })
+
+
+__all__ = ['html_diff_table']
diff --git a/apps/wiki_img/templates/wiki_img/base.html b/apps/wiki_img/templates/wiki_img/base.html
new file mode 100644 (file)
index 0000000..f88fac3
--- /dev/null
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+{% load compressed i18n %}
+
+{% block title %}{{ document_name }} - {{ block.super }}{% endblock %}
+
+{% block extrahead %}
+{% compressed_css 'listing' %}
+{% endblock %}
+
+{% block extrabody %}
+{% compressed_js 'listing' %}
+{% endblock %}
+
+{% block maincontent %}
+<h1><img src="{{ STATIC_URL }}img/logo.png">{% trans "Platforma Redakcyjna" %}</h1>
+<div id="wiki_layout_left_column">
+       {% block leftcolumn %}
+       {% endblock leftcolumn %}
+</div>
+<div id="wiki_layout_right_column">
+       {% block rightcolumn %}
+       {% endblock rightcolumn %}
+</div>
+{% endblock maincontent %}
\ No newline at end of file
diff --git a/apps/wiki_img/templates/wiki_img/diff_table.html b/apps/wiki_img/templates/wiki_img/diff_table.html
new file mode 100644 (file)
index 0000000..818c38c
--- /dev/null
@@ -0,0 +1,21 @@
+{% load i18n %}
+<table class="diff_table">
+       <thead>
+               <tr>
+                       <th colspan="2">{% trans "Old version" %}</th>
+                       <th colspan="2">{% trans "New version" %}</th>
+               </tr>
+       </thead>
+<tbody>
+{% for an, a, bn, b, has_change in changes %}
+
+<tr class="{% if has_change %}change{% endif %}">
+<td>{{an}}</td>
+<td class="left">{{ a|safe }}&nbsp;</td>
+<td>{{bn}}</td>
+<td class="right">{{ b|safe }}&nbsp;</td>
+</tr>
+
+{% endfor %}
+</tbody>
+</table>
\ No newline at end of file
diff --git a/apps/wiki_img/templates/wiki_img/document_create_missing.html b/apps/wiki_img/templates/wiki_img/document_create_missing.html
new file mode 100644 (file)
index 0000000..351e87a
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "wiki/base.html" %}
+{% load i18n %}
+
+{% block leftcolumn %}
+       <form enctype="multipart/form-data" method="POST" action="">
+       {{ form.as_p }}
+
+       <p><button type="submit">{% trans "Create document" %}</button></p>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+{% endblock rightcolumn %}
\ No newline at end of file
diff --git a/apps/wiki_img/templates/wiki_img/document_details.html b/apps/wiki_img/templates/wiki_img/document_details.html
new file mode 100644 (file)
index 0000000..d03a0bf
--- /dev/null
@@ -0,0 +1,32 @@
+{% extends "wiki_img/document_details_base.html" %}
+{% load i18n %}
+
+{% block extrabody %}
+{{ block.super }}
+<script src="{{ STATIC_URL }}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
+</script>
+<script src="{{ STATIC_URL }}js/wiki_img/loader.js" type="text/javascript" charset="utf-8"> </script>
+{% endblock %}
+
+{% block tabs-menu %}
+    {% include "wiki_img/tabs/summary_view_item.html" %}
+    {% include "wiki_img/tabs/motifs_editor_item.html" %}
+    {% include "wiki_img/tabs/objects_editor_item.html" %}
+    {% include "wiki_img/tabs/source_editor_item.html" %}
+{% endblock %}
+
+{% block tabs-content %}
+    {% include "wiki_img/tabs/summary_view.html" %}
+    {% include "wiki_img/tabs/motifs_editor.html" %}
+    {% include "wiki_img/tabs/objects_editor.html" %}
+    {% include "wiki_img/tabs/source_editor.html" %}
+{% endblock %}
+
+{% block dialogs %}
+    {% include "wiki_img/save_dialog.html" %}
+{% endblock %}
+
+{% block editor-class %}
+    sideless
+{% endblock %}
+
diff --git a/apps/wiki_img/templates/wiki_img/document_details_base.html b/apps/wiki_img/templates/wiki_img/document_details_base.html
new file mode 100644 (file)
index 0000000..30accf2
--- /dev/null
@@ -0,0 +1,57 @@
+{% extends "base.html" %}
+{% load toolbar_tags i18n %}
+
+{% block title %}{{ document.name }} - {{ block.super }}{% endblock %}
+{% block extrahead %}
+{% load compressed %}
+{% compressed_css 'detail' %}
+{% endblock %}
+
+{% block extrabody %}
+<script type="text/javascript" charset="utf-8">
+    var STATIC_URL = '{{STATIC_URL}}';
+</script>
+{% compressed_js 'wiki_img' %}
+{% endblock %}
+
+{% block maincontent %}
+<div id="document-meta"
+       data-document-name="{{ document.slug }}" style="display:none">
+
+       {% for k, v in document_meta.items %}
+               <span data-key="{{ k }}">{{ v }}</span>
+       {% endfor %}
+
+       {% for k, v in document_info.items %}
+               <span data-key="{{ k }}">{{ v }}</span>
+       {% endfor %}
+
+       {% block meta-extra %} {% endblock %}
+</div>
+
+<div id="header">
+    <h1><a href="{% url wiki_document_list %}"><img src="{{STATIC_URL}}icons/go-home.png"/><a href="{% url wiki_document_list %}">Strona<br>główna</a></h1>
+    <div id="tools">
+        <a href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
+        {% trans "Help" %}</a>
+        | {% include "registration/head_login.html" %}
+        | {% trans "Version" %}: <span id="document-revision">{% trans "Unknown" %}</span>
+               {% if not readonly %}
+            | <button style="margin-left: 6px" id="save-button">{% trans "Save" %}</button>
+                       <span id='save-attempt-info'>{% trans "Save attempt in progress" %}</span>
+            <span id='out-of-date-info'>{% trans "There is a newer version of this document!" %}</span>
+               {% endif %}
+    </div>
+    <ol id="tabs" class="tabs">
+       {% block tabs-menu %} {% endblock %}
+    </ol>
+</div>
+<div id="splitter">
+    <div id="editor" class="{% block editor-class %} {% endblock %}">
+       {% block tabs-content %} {% endblock %}
+    </div>
+</div>
+
+{% block dialogs %} {% endblock %}
+
+{% endblock %}
diff --git a/apps/wiki_img/templates/wiki_img/document_details_readonly.html b/apps/wiki_img/templates/wiki_img/document_details_readonly.html
new file mode 100644 (file)
index 0000000..71556a1
--- /dev/null
@@ -0,0 +1,27 @@
+{% extends "wiki/document_details_base.html" %}
+{% load i18n %}
+
+{% block editor-class %}readonly{% endblock %}
+
+{% block extrabody %}
+{{ block.super }}
+<script src="{{STATIC_URL}}js/lib/codemirror-0.8/codemirror.js" type="text/javascript" charset="utf-8">
+</script>
+<script src="{{STATIC_URL}}js/wiki/loader_readonly.js" type="text/javascript" charset="utf-8"> </script>
+{% endblock %}
+
+{% block tabs-menu %}
+    {% include "wiki/tabs/wysiwyg_editor_item.html" %}
+       {% include "wiki/tabs/source_editor_item.html" %}
+{% endblock %}
+
+{% block tabs-content %}
+    {% include "wiki/tabs/wysiwyg_editor.html" %}
+       {% include "wiki/tabs/source_editor.html" %}
+{% endblock %}
+
+{% block splitter-extra %}
+{% endblock %}
+
+{% block dialogs %}
+{% endblock %}
\ No newline at end of file
diff --git a/apps/wiki_img/templates/wiki_img/document_list.html b/apps/wiki_img/templates/wiki_img/document_list.html
new file mode 100644 (file)
index 0000000..cf10cde
--- /dev/null
@@ -0,0 +1,56 @@
+{% extends "wiki/base.html" %}
+
+{% load i18n %}
+{% load wiki %}
+
+{% block extrabody %}
+{{ block.super }}
+<script type="text/javascript" charset="utf-8">
+$(function() {
+       function search(event) {
+        event.preventDefault();
+        var expr = new RegExp(slugify($('#file-list-filter').val()), 'i');
+        $('#file-list tbody tr').hide().filter(function(index) {
+            return expr.test(slugify( $('a', this).attr('data-id') ));
+        }).show();
+    }
+
+    $('#file-list-find-button').click(search).hide();
+       $('#file-list-filter').bind('keyup change DOMAttrModified', search);
+});
+</script>
+{% endblock %}
+
+{% block leftcolumn %}
+       <form method="get" action="#">
+    <table  id="file-list">
+       <thead>
+               <tr><th>Filtr:</th>
+                       <th><input autocomplete="off" name="filter" id="file-list-filter" type="text" size="40" /></th>
+                       <th><input type="reset" value="{% trans "Clear filter" %}" id="file-list-reset-button"/></th>
+                       </tr>
+               </thead>
+               <tbody>
+       {% for doc in object_list %}
+            <tr>
+               <td colspan="3"><a target="_blank" data-id="{{doc.name}}"
+                                       href="{% url wiki_img_editor doc.slug %}">{{ doc.name }}</a></td>
+                               <!-- placeholder </td> -->
+                       </tr>
+       {% endfor %}
+               </tbody>
+    </table>
+       </form>
+{% endblock leftcolumn %}
+
+{% block rightcolumn %}
+       <div id="last-edited-list">
+               <h2>{% trans "Your last edited documents" %}</h2>
+           <ol>
+                       {% for name, date in last_docs %}
+                       <li><a href="{% url wiki_editor name %}"
+                               target="_blank">{{ name|wiki_title }}</a><br/><span class="date">({{ date|date:"H:i:s, d/m/Y" }})</span></li>
+                       {% endfor %}
+               </ol>
+       </div>
+{% endblock rightcolumn %}
diff --git a/apps/wiki_img/templates/wiki_img/save_dialog.html b/apps/wiki_img/templates/wiki_img/save_dialog.html
new file mode 100644 (file)
index 0000000..fc239d2
--- /dev/null
@@ -0,0 +1,24 @@
+{% load i18n %}
+<div id="save_dialog" class="dialog" data-ui-jsclass="SaveDialog">
+       <form method="POST" action="">
+       <p>{{ forms.text_save.comment.label }}</p>
+       <p class="help_text">
+               {{ forms.text_save.comment.help_text}}
+               <span data-ui-error-for="{{ forms.text_save.comment.name }}"> </span>
+       </p>
+       {{forms.text_save.comment }}
+
+
+
+       {% for f in forms.text_save.hidden_fields %}
+               {{ f }}
+       {% endfor %}
+
+       <p data-ui-error-for="__all__"> </p>
+
+       <p class="action_area">
+               <button type="submit" class"ok" data-ui-action="save">Zapisz</button>
+               <button type="button" class="cancel" data-ui-action="cancel">Anuluj</button>
+       </p>
+       </form>
+</div>
diff --git a/apps/wiki_img/templates/wiki_img/tabs/motifs_editor.html b/apps/wiki_img/templates/wiki_img/tabs/motifs_editor.html
new file mode 100644 (file)
index 0000000..b5b6588
--- /dev/null
@@ -0,0 +1,17 @@
+{% load i18n %}
+<div id="motifs-editor" class="editor" style="display: none">
+    <div class="toolbar">
+        <input id='tag-name' title='{% trans "Motifs" %}' />
+        <button id='add'>{% trans "Add" %}</button>
+
+        <span id="objects-list">
+        </span>
+
+        <div class="toolbar-end">
+        </div>
+    </div>
+
+    <div class='scrolled'>
+        <img src="{{ document.image.url }}" class='area-selectable' />
+    </div>
+</div>
diff --git a/apps/wiki_img/templates/wiki_img/tabs/motifs_editor_item.html b/apps/wiki_img/templates/wiki_img/tabs/motifs_editor_item.html
new file mode 100644 (file)
index 0000000..33ad25b
--- /dev/null
@@ -0,0 +1,4 @@
+{% load i18n %}
+<li id="MotifsPerspective" data-ui-related="motifs-editor" data-ui-jsclass="MotifsPerspective">
+    <span>{% trans "Motifs" %}</span>
+</li>
diff --git a/apps/wiki_img/templates/wiki_img/tabs/objects_editor.html b/apps/wiki_img/templates/wiki_img/tabs/objects_editor.html
new file mode 100644 (file)
index 0000000..686cfa2
--- /dev/null
@@ -0,0 +1,17 @@
+{% load i18n %}
+<div id="objects-editor" class="editor" style="display: none">
+    <div class="toolbar">
+        <input id='tag-name' title='{% trans "Object name" %}' />
+        <button id='add'>{% trans "Add" %}</button>
+
+        <span id="objects-list">
+        </span>
+
+        <div class="toolbar-end">
+        </div>
+    </div>
+
+    <div class='scrolled'>
+        <img src="{{ document.image.url }}" class='area-selectable' />
+    </div>
+</div>
diff --git a/apps/wiki_img/templates/wiki_img/tabs/objects_editor_item.html b/apps/wiki_img/templates/wiki_img/tabs/objects_editor_item.html
new file mode 100644 (file)
index 0000000..d5b0832
--- /dev/null
@@ -0,0 +1,4 @@
+{% load i18n %}
+<li id="ObjectsPerspective" data-ui-related="objects-editor" data-ui-jsclass="ObjectsPerspective">
+    <span>{% trans "Objects" %}</span>
+</li>
diff --git a/apps/wiki_img/templates/wiki_img/tabs/source_editor.html b/apps/wiki_img/templates/wiki_img/tabs/source_editor.html
new file mode 100644 (file)
index 0000000..a1316a7
--- /dev/null
@@ -0,0 +1,4 @@
+{% load toolbar_tags i18n %}
+<div id="source-editor" class="editor">
+    <textarea id="codemirror_placeholder">&lt;br/&gt;</textarea>
+</div>
\ No newline at end of file
diff --git a/apps/wiki_img/templates/wiki_img/tabs/source_editor_item.html b/apps/wiki_img/templates/wiki_img/tabs/source_editor_item.html
new file mode 100644 (file)
index 0000000..89e0fae
--- /dev/null
@@ -0,0 +1,6 @@
+{% load i18n %}
+<li id="CodeMirrorPerspective"
+       data-ui-related="source-editor"
+       data-ui-jsclass="CodeMirrorPerspective">
+    <span>{% trans "Source code" %}</span>
+</li>
\ No newline at end of file
diff --git a/apps/wiki_img/templates/wiki_img/tabs/summary_view.html b/apps/wiki_img/templates/wiki_img/tabs/summary_view.html
new file mode 100644 (file)
index 0000000..a908f55
--- /dev/null
@@ -0,0 +1,24 @@
+{% load i18n %}
+{% load wiki %}
+<div id="summary-view-editor" class="editor" style="display: none">
+    <!-- <div class="toolbar">
+    </div> -->
+    <div id="summary-view">
+               <h2>
+                       <label for="title">{% trans "Title" %}:</label>
+                       <span data-ui-editable="true" data-edit-target="meta.displayTitle"
+                       >{{ document.name|wiki_title }}</span>
+               </h2>
+               <p>
+                       <label>{% trans "Document ID" %}:</label>
+                       <span>{{ document.name }}</span>
+               </p>
+               <p>
+                       <label>{% trans "Current version" %}:</label>
+                       {{ document_info.revision }} ({{document_info.date}})
+               <p>
+                       <label>{% trans "Last edited by" %}:</label>
+                       {{document_info.author}}
+               </p>
+       </div>
+</div>
diff --git a/apps/wiki_img/templates/wiki_img/tabs/summary_view_item.html b/apps/wiki_img/templates/wiki_img/tabs/summary_view_item.html
new file mode 100644 (file)
index 0000000..2b4daeb
--- /dev/null
@@ -0,0 +1,5 @@
+{% load i18n %}
+{% load wiki %}
+<li id="SummaryPerspective" data-ui-related="summary-view-editor" data-ui-jsclass="SummaryPerspective">
+    <span>{% trans "Summary" %}</span>
+</li>
diff --git a/apps/wiki_img/templates/wiki_img/tag_dialog.html b/apps/wiki_img/templates/wiki_img/tag_dialog.html
new file mode 100644 (file)
index 0000000..bc601cb
--- /dev/null
@@ -0,0 +1,19 @@
+{% load i18n %}
+<div id="add_tag_dialog" class="dialog" data-ui-jsclass="AddTagDialog">
+       <form method="POST" action="#">
+               {% for field in forms.add_tag.visible_fields %}
+               <p>{{ field.label_tag }} {{ field }} <span data-ui-error-for="{{ field.name }}"> </span></p>
+               <p>{{ field.help_text }}</p>
+               {% endfor %}
+
+               {% for f in forms.add_tag.hidden_fields %}
+                       {{ f }}
+               {% endfor %}
+               <p data-ui-error-for="__all__"> </p>
+
+               <p class="action_area">
+                       <button type="submit" class"ok" data-ui-action="save">{% trans "Save" %}</button>
+                       <button type="button" class="cancel" data-ui-action="cancel">{% trans "Cancel" %}</button>
+               </p>
+       </form>
+</div>
diff --git a/apps/wiki_img/tests.py b/apps/wiki_img/tests.py
new file mode 100644 (file)
index 0000000..6577737
--- /dev/null
@@ -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/wiki_img/urls.py b/apps/wiki_img/urls.py
new file mode 100644 (file)
index 0000000..075e5ad
--- /dev/null
@@ -0,0 +1,20 @@
+# -*- coding: utf-8
+from django.conf.urls.defaults import *
+from django.conf import settings
+from django.views.generic.list_detail import object_list
+
+from wiki_img.models import ImageDocument
+
+
+PART = ur"""[ ĄĆĘŁŃÓŚŻŹąćęłńóśżź0-9\w_.-]+"""
+
+urlpatterns = patterns('wiki_img.views',
+    url(r'^$', object_list, {'queryset': ImageDocument.objects.all(), "template_name": "wiki_img/document_list.html"}),
+
+    url(r'^edit/(?P<slug>%s)$' % PART,
+        'editor', name="wiki_img_editor"),
+
+    url(r'^(?P<slug>[^/]+)/text$',
+        'text', name="wiki_img_text"),
+
+)
diff --git a/apps/wiki_img/views.py b/apps/wiki_img/views.py
new file mode 100644 (file)
index 0000000..c11549a
--- /dev/null
@@ -0,0 +1,69 @@
+import os
+import functools
+import logging
+logger = logging.getLogger("fnp.wiki")
+
+from django.views.generic.simple import direct_to_template
+from django.core.urlresolvers import reverse
+from wiki.helpers import JSONResponse
+from django import http
+from django.shortcuts import get_object_or_404
+from django.conf import settings
+
+from wiki_img.models import ImageDocument
+from wiki_img.forms import DocumentTextSaveForm
+
+#
+# Quick hack around caching problems, TODO: use ETags
+#
+from django.views.decorators.cache import never_cache
+
+
+@never_cache
+def editor(request, slug, template_name='wiki_img/document_details.html'):
+    doc = get_object_or_404(ImageDocument, slug=slug)
+
+    return direct_to_template(request, template_name, extra_context={
+        'document': doc,
+        'forms': {
+            "text_save": DocumentTextSaveForm(prefix="textsave"),
+        },
+        'REDMINE_URL': settings.REDMINE_URL,
+    })
+
+
+@never_cache
+def text(request, slug):
+    if request.method == 'POST':
+        form = DocumentTextSaveForm(request.POST, prefix="textsave")
+        if form.is_valid():
+            document = get_object_or_404(ImageDocument, slug=slug)
+            revision = form.cleaned_data['parent_revision']
+
+            comment = form.cleaned_data['comment']
+
+            if request.user.is_authenticated():
+                user = request.user
+            else:
+                user = None
+
+            document.doc.commit(
+                parent=revision,
+                text=form.cleaned_data['text'],
+                author=user,
+                description=comment
+            )
+
+            return JSONResponse({
+                'text': document.doc.materialize(),
+                'revision': document.doc.change_set.count(),
+            })
+        else:
+            return JSONFormInvalid(form)
+    else:
+        doc = get_object_or_404(ImageDocument, slug=slug)
+        return JSONResponse({
+            'text': doc.doc.materialize(),
+            'revision': doc.doc.change_set.count()
+        })
+
index 8d032ef..5acf265 100644 (file)
@@ -118,7 +118,9 @@ INSTALLED_APPS = (
     'sorl.thumbnail',
     'filebrowser',
 
+    'dvcs',
     'wiki',
+    'wiki_img',
     'toolbar',
 )
 
index 2ce6ac9..714e4c2 100644 (file)
@@ -9,6 +9,7 @@ COMPRESS_CSS = {
             'css/summary.css',
             'css/html.css',
             'css/jquery.autocomplete.css',
+            'css/imgareaselect-default.css', #img!
             'css/dialogs.css',
         ),
         'output_filename': 'compressed/detail_styles_?.css',
@@ -57,6 +58,36 @@ COMPRESS_JS = {
         ),
         'output_filename': 'compressed/detail_scripts_?.js',
      },
+    'wiki_img': {
+        'source_filenames': (
+                # libraries
+                'js/lib/jquery-1.4.2.min.js',
+                'js/lib/jquery/jquery.autocomplete.js',
+                'js/lib/jquery/jquery.blockui.js',
+                'js/lib/jquery/jquery.elastic.js',
+                'js/lib/jquery/jquery.imgareaselect.js',
+                'js/button_scripts.js',
+                'js/slugify.js',
+
+                # wiki scripts
+                'js/wiki_img/wikiapi.js',
+
+                # base UI
+                'js/wiki_img/base.js',
+                'js/wiki_img/toolbar.js',
+
+                # dialogs
+                'js/wiki_img/dialog_save.js',
+                'js/wiki_img/dialog_addtag.js',
+
+                # views
+                'js/wiki_img/view_summary.js',
+                'js/wiki_img/view_editor_objects.js',
+                'js/wiki_img/view_editor_motifs.js',
+                'js/wiki/view_editor_source.js',
+        ),
+        'output_filename': 'compressed/detail_img_scripts_?.js',
+     },
     'listing': {
         'source_filenames': (
                 'js/lib/jquery-1.4.2.min.js',
diff --git a/redakcja/static/css/imgareaselect-default.css b/redakcja/static/css/imgareaselect-default.css
new file mode 100644 (file)
index 0000000..e9c8592
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * imgAreaSelect default style
+ */
+
+.imgareaselect-border1 {
+       background: url(/media/static/img/jquery.imgareaselect/border-v.gif) repeat-y left top;
+}
+
+.imgareaselect-border2 {
+    background: url(/media/static/img/jquery.imgareaselect/border-h.gif) repeat-x left top;
+}
+
+.imgareaselect-border3 {
+    background: url(/media/static/img/jquery.imgareaselect/border-v.gif) repeat-y right top;
+}
+
+.imgareaselect-border4 {
+    background: url(/media/static/img/jquery.imgareaselect/border-h.gif) repeat-x left bottom;
+}
+
+.imgareaselect-border1, .imgareaselect-border2,
+.imgareaselect-border3, .imgareaselect-border4 {
+    opacity: 0.5;
+    filter: alpha(opacity=50);
+}
+
+.imgareaselect-handle {
+    background-color: #fff;
+    border: solid 1px #000;
+    opacity: 0.5;
+    filter: alpha(opacity=50);
+}
+
+.imgareaselect-outer {
+    background-color: #000;
+    opacity: 0.5;
+    filter: alpha(opacity=50);
+}
+
+.imgareaselect-selection {  
+}
index 84c1ca5..1c56e54 100644 (file)
@@ -57,6 +57,29 @@ body {
     overflow: hidden;
 }
 
+.sideless .editor {
+    right: 0;
+}
+.image-object {
+    padding-left: 1em;
+    font: 12px Sans, Helvetica, Verdana, sans-serif;
+}
+.image-object:hover {
+    cursor: pointer;
+}
+#objects-list .delete {
+    padding-left: 3px;
+    font: 10px Sans, Helvetica, Verdana, sans-serif;
+}
+#objects-list .delete:hover {
+    cursor: pointer;
+}
+
+#objects-list .active {
+    color: #800;
+}
+
+
 #editor.readonly .editor {
        right: 0px;
 }
@@ -99,6 +122,8 @@ body {
 
     font: 11px Helvetica, Verdana, sans-serif;
     font-weight: bold;
+
+    z-index: 100;
 }
 
 
@@ -345,3 +370,14 @@ img.tabclose {
 .saveNotify span {
     font-weight: bold;
 }
+
+
+
+.scrolled {
+    position: absolute;
+    top: 29px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    overflow: auto;
+}
diff --git a/redakcja/static/img/jquery.imgareaselect/border-anim-h.gif b/redakcja/static/img/jquery.imgareaselect/border-anim-h.gif
new file mode 100644 (file)
index 0000000..ec9f5da
Binary files /dev/null and b/redakcja/static/img/jquery.imgareaselect/border-anim-h.gif differ
diff --git a/redakcja/static/img/jquery.imgareaselect/border-anim-v.gif b/redakcja/static/img/jquery.imgareaselect/border-anim-v.gif
new file mode 100644 (file)
index 0000000..331cc90
Binary files /dev/null and b/redakcja/static/img/jquery.imgareaselect/border-anim-v.gif differ
diff --git a/redakcja/static/img/jquery.imgareaselect/border-h.gif b/redakcja/static/img/jquery.imgareaselect/border-h.gif
new file mode 100644 (file)
index 0000000..a2aa5b0
Binary files /dev/null and b/redakcja/static/img/jquery.imgareaselect/border-h.gif differ
diff --git a/redakcja/static/img/jquery.imgareaselect/border-v.gif b/redakcja/static/img/jquery.imgareaselect/border-v.gif
new file mode 100644 (file)
index 0000000..4bfd555
Binary files /dev/null and b/redakcja/static/img/jquery.imgareaselect/border-v.gif differ
diff --git a/redakcja/static/js/lib/jquery/jquery.imgareaselect.js b/redakcja/static/js/lib/jquery/jquery.imgareaselect.js
new file mode 100644 (file)
index 0000000..d981fdc
--- /dev/null
@@ -0,0 +1,716 @@
+/*
+ * imgAreaSelect jQuery plugin
+ * version 0.9.3
+ *
+ * Copyright (c) 2008-2010 Michal Wojciechowski (odyniec.net)
+ *
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://odyniec.net/projects/imgareaselect/
+ *
+ */
+
+(function($) {
+
+var abs = Math.abs,
+    max = Math.max,
+    min = Math.min,
+    round = Math.round;
+
+function div() {
+    return $('<div/>');
+}
+
+$.imgAreaSelect = function (img, options) {
+    var
+
+        $img = $(img),
+
+        imgLoaded,
+
+        $box = div(),
+        $area = div(),
+        $border = div().add(div()).add(div()).add(div()),
+        $outer = div().add(div()).add(div()).add(div()),
+        $handles = $([]),
+
+        $areaOpera,
+
+        left, top,
+
+        imgOfs,
+
+        imgWidth, imgHeight,
+
+        $parent,
+
+        parOfs,
+
+        zIndex = 0,
+
+        position = 'absolute',
+
+        startX, startY,
+
+        scaleX, scaleY,
+
+        resizeMargin = 10,
+
+        resize,
+
+        minWidth, minHeight, maxWidth, maxHeight,
+
+        aspectRatio,
+
+        shown,
+
+        x1, y1, x2, y2,
+
+        selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 },
+
+        docElem = document.documentElement,
+
+        $p, d, i, o, w, h, adjusted;
+
+    function viewX(x) {
+        return x + imgOfs.left - parOfs.left;
+    }
+
+    function viewY(y) {
+        return y + imgOfs.top - parOfs.top;
+    }
+
+    function selX(x) {
+        return x - imgOfs.left + parOfs.left;
+    }
+
+    function selY(y) {
+        return y - imgOfs.top + parOfs.top;
+    }
+
+    function evX(event) {
+        return event.pageX - parOfs.left;
+    }
+
+    function evY(event) {
+        return event.pageY - parOfs.top;
+    }
+
+    function getSelection(noScale) {
+        var sx = noScale || scaleX, sy = noScale || scaleY;
+
+        return { x1: round(selection.x1 * sx),
+            y1: round(selection.y1 * sy),
+            x2: round(selection.x2 * sx),
+            y2: round(selection.y2 * sy),
+            width: round(selection.x2 * sx) - round(selection.x1 * sx),
+            height: round(selection.y2 * sy) - round(selection.y1 * sy) };
+    }
+
+    function setSelection(x1, y1, x2, y2, noScale) {
+        var sx = noScale || scaleX, sy = noScale || scaleY;
+
+        selection = {
+            x1: round(x1 / sx),
+            y1: round(y1 / sy),
+            x2: round(x2 / sx),
+            y2: round(y2 / sy)
+        };
+
+        selection.width = selection.x2 - selection.x1;
+        selection.height = selection.y2 - selection.y1;
+    }
+
+    function adjust() {
+        if (!$img.width())
+            return;
+
+        imgOfs = { left: round($img.offset().left), top: round($img.offset().top) };
+
+        imgWidth = $img.width();
+        imgHeight = $img.height();
+
+        minWidth = options.minWidth || 0;
+        minHeight = options.minHeight || 0;
+        maxWidth = min(options.maxWidth || 1<<24, imgWidth);
+        maxHeight = min(options.maxHeight || 1<<24, imgHeight);
+
+        if ($().jquery == '1.3.2' && position == 'fixed' &&
+            !docElem['getBoundingClientRect'])
+        {
+            imgOfs.top += max(document.body.scrollTop, docElem.scrollTop);
+            imgOfs.left += max(document.body.scrollLeft, docElem.scrollLeft);
+        }
+
+        parOfs = $.inArray($parent.css('position'), ['absolute', 'relative']) + 1 ?
+            { left: round($parent.offset().left) - $parent.scrollLeft(),
+                top: round($parent.offset().top) - $parent.scrollTop() } :
+            position == 'fixed' ?
+                { left: $(document).scrollLeft(), top: $(document).scrollTop() } :
+                { left: 0, top: 0 };
+
+        left = viewX(0);
+        top = viewY(0);
+
+        if (selection.x2 > imgWidth || selection.y2 > imgHeight)
+            doResize();
+    }
+
+    function update(resetKeyPress) {
+        if (!shown) return;
+
+        $box.css({ left: viewX(selection.x1), top: viewY(selection.y1) })
+            .add($area).width(w = selection.width).height(h = selection.height);
+
+        $area.add($border).add($handles).css({ left: 0, top: 0 });
+
+        $border
+            .width(max(w - $border.outerWidth() + $border.innerWidth(), 0))
+            .height(max(h - $border.outerHeight() + $border.innerHeight(), 0));
+
+        $($outer[0]).css({ left: left, top: top,
+            width: selection.x1, height: imgHeight });
+        $($outer[1]).css({ left: left + selection.x1, top: top,
+            width: w, height: selection.y1 });
+        $($outer[2]).css({ left: left + selection.x2, top: top,
+            width: imgWidth - selection.x2, height: imgHeight });
+        $($outer[3]).css({ left: left + selection.x1, top: top + selection.y2,
+            width: w, height: imgHeight - selection.y2 });
+
+        w -= $handles.outerWidth();
+        h -= $handles.outerHeight();
+
+        switch ($handles.length) {
+        case 8:
+            $($handles[4]).css({ left: w / 2 });
+            $($handles[5]).css({ left: w, top: h / 2 });
+            $($handles[6]).css({ left: w / 2, top: h });
+            $($handles[7]).css({ top: h / 2 });
+        case 4:
+            $handles.slice(1,3).css({ left: w });
+            $handles.slice(2,4).css({ top: h });
+        }
+
+        if (resetKeyPress !== false) {
+            if ($.imgAreaSelect.keyPress != docKeyPress)
+                $(document).unbind($.imgAreaSelect.keyPress,
+                    $.imgAreaSelect.onKeyPress);
+
+            if (options.keys)
+                $(document)[$.imgAreaSelect.keyPress](
+                    $.imgAreaSelect.onKeyPress = docKeyPress);
+        }
+
+        if ($.browser.msie && $border.outerWidth() - $border.innerWidth() == 2) {
+            $border.css('margin', 0);
+            setTimeout(function () { $border.css('margin', 'auto'); }, 0);
+        }
+    }
+
+    function doUpdate(resetKeyPress) {
+        adjust();
+        update(resetKeyPress);
+        x1 = viewX(selection.x1); y1 = viewY(selection.y1);
+        x2 = viewX(selection.x2); y2 = viewY(selection.y2);
+    }
+
+    function hide($elem, fn) {
+        options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide();
+
+    }
+
+    function areaMouseMove(event) {
+        var x = selX(evX(event)) - selection.x1,
+            y = selY(evY(event)) - selection.y1;
+
+        if (!adjusted) {
+            adjust();
+            adjusted = true;
+
+            $box.one('mouseout', function () { adjusted = false; });
+        }
+
+        resize = '';
+
+        if (options.resizable) {
+            if (y <= resizeMargin)
+                resize = 'n';
+            else if (y >= selection.height - resizeMargin)
+                resize = 's';
+            if (x <= resizeMargin)
+                resize += 'w';
+            else if (x >= selection.width - resizeMargin)
+                resize += 'e';
+        }
+
+        $box.css('cursor', resize ? resize + '-resize' :
+            options.movable ? 'move' : '');
+        if ($areaOpera)
+            $areaOpera.toggle();
+    }
+
+    function docMouseUp(event) {
+        $('body').css('cursor', '');
+
+        if (options.autoHide || selection.width * selection.height == 0)
+            hide($box.add($outer), function () { $(this).hide(); });
+
+        options.onSelectEnd(img, getSelection());
+
+        $(document).unbind('mousemove', selectingMouseMove);
+        $box.mousemove(areaMouseMove);
+    }
+
+    function areaMouseDown(event) {
+        if (event.which != 1) return false;
+
+        adjust();
+
+        if (resize) {
+            $('body').css('cursor', resize + '-resize');
+
+            x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']);
+            y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']);
+
+            $(document).mousemove(selectingMouseMove)
+                .one('mouseup', docMouseUp);
+            $box.unbind('mousemove', areaMouseMove);
+        }
+        else if (options.movable) {
+            startX = left + selection.x1 - evX(event);
+            startY = top + selection.y1 - evY(event);
+
+            $box.unbind('mousemove', areaMouseMove);
+
+            $(document).mousemove(movingMouseMove)
+                .one('mouseup', function () {
+                    options.onSelectEnd(img, getSelection());
+
+                    $(document).unbind('mousemove', movingMouseMove);
+                    $box.mousemove(areaMouseMove);
+                });
+        }
+        else
+            $img.mousedown(event);
+
+        return false;
+    }
+
+    function fixAspectRatio(xFirst) {
+        if (aspectRatio)
+            if (xFirst) {
+                x2 = max(left, min(left + imgWidth,
+                    x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)));
+
+                y2 = round(max(top, min(top + imgHeight,
+                    y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))));
+                x2 = round(x2);
+            }
+            else {
+                y2 = max(top, min(top + imgHeight,
+                    y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)));
+                x2 = round(max(left, min(left + imgWidth,
+                    x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))));
+                y2 = round(y2);
+            }
+    }
+
+    function doResize() {
+        x1 = min(x1, left + imgWidth);
+        y1 = min(y1, top + imgHeight);
+
+        if (abs(x2 - x1) < minWidth) {
+            x2 = x1 - minWidth * (x2 < x1 || -1);
+
+            if (x2 < left)
+                x1 = left + minWidth;
+            else if (x2 > left + imgWidth)
+                x1 = left + imgWidth - minWidth;
+        }
+
+        if (abs(y2 - y1) < minHeight) {
+            y2 = y1 - minHeight * (y2 < y1 || -1);
+
+            if (y2 < top)
+                y1 = top + minHeight;
+            else if (y2 > top + imgHeight)
+                y1 = top + imgHeight - minHeight;
+        }
+
+        x2 = max(left, min(x2, left + imgWidth));
+        y2 = max(top, min(y2, top + imgHeight));
+
+        fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio);
+
+        if (abs(x2 - x1) > maxWidth) {
+            x2 = x1 - maxWidth * (x2 < x1 || -1);
+            fixAspectRatio();
+        }
+
+        if (abs(y2 - y1) > maxHeight) {
+            y2 = y1 - maxHeight * (y2 < y1 || -1);
+            fixAspectRatio(true);
+        }
+
+        selection = { x1: selX(min(x1, x2)), x2: selX(max(x1, x2)),
+            y1: selY(min(y1, y2)), y2: selY(max(y1, y2)),
+            width: abs(x2 - x1), height: abs(y2 - y1) };
+
+        update();
+
+        options.onSelectChange(img, getSelection());
+    }
+
+    function selectingMouseMove(event) {
+        x2 = resize == '' || /w|e/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
+        y2 = resize == '' || /n|s/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);
+
+        doResize();
+
+        return false;
+
+    }
+
+    function doMove(newX1, newY1) {
+        x2 = (x1 = newX1) + selection.width;
+        y2 = (y1 = newY1) + selection.height;
+
+        $.extend(selection, { x1: selX(x1), y1: selY(y1), x2: selX(x2),
+            y2: selY(y2) });
+
+        update();
+
+        options.onSelectChange(img, getSelection());
+    }
+
+    function movingMouseMove(event) {
+        x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width));
+        y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height));
+
+        doMove(x1, y1);
+
+        event.preventDefault();
+
+        return false;
+    }
+
+    function startSelection() {
+        adjust();
+
+        x2 = x1;
+        y2 = y1;
+
+        doResize();
+
+        resize = '';
+
+        if ($outer.is(':not(:visible)'))
+            $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
+
+        shown = true;
+
+        $(document).unbind('mouseup', cancelSelection)
+            .mousemove(selectingMouseMove).one('mouseup', docMouseUp);
+        $box.unbind('mousemove', areaMouseMove);
+
+        options.onSelectStart(img, getSelection());
+    }
+
+    function cancelSelection() {
+        $(document).unbind('mousemove', startSelection);
+        hide($box.add($outer));
+
+        selection = { x1: selX(x1), y1: selY(y1), x2: selX(x1), y2: selY(y1),
+                width: 0, height: 0 };
+
+        options.onSelectChange(img, getSelection());
+        options.onSelectEnd(img, getSelection());
+    }
+
+    function imgMouseDown(event) {
+        if (event.which != 1 || $outer.is(':animated')) return false;
+
+        adjust();
+        startX = x1 = evX(event);
+        startY = y1 = evY(event);
+
+        $(document).one('mousemove', startSelection)
+            .one('mouseup', cancelSelection);
+
+        return false;
+    }
+
+    function windowResize() {
+        doUpdate(false);
+    }
+
+    function imgLoad() {
+        imgLoaded = true;
+
+        setOptions(options = $.extend({
+            classPrefix: 'imgareaselect',
+            movable: true,
+            resizable: true,
+            parent: 'body',
+            onInit: function () {},
+            onSelectStart: function () {},
+            onSelectChange: function () {},
+            onSelectEnd: function () {}
+        }, options));
+
+        $box.add($outer).css({ visibility: '' });
+
+        if (options.show) {
+            shown = true;
+            adjust();
+            update();
+            $box.add($outer).hide().fadeIn(options.fadeSpeed||0);
+        }
+
+        setTimeout(function () { options.onInit(img, getSelection()); }, 0);
+    }
+
+    var docKeyPress = function(event) {
+        var k = options.keys, d, t, key = event.keyCode;
+
+        d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt :
+            !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl :
+            !isNaN(k.shift) && event.shiftKey ? k.shift :
+            !isNaN(k.arrows) ? k.arrows : 10;
+
+        if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) ||
+            (k.ctrl == 'resize' && event.ctrlKey) ||
+            (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey)))
+        {
+            switch (key) {
+            case 37:
+                d = -d;
+            case 39:
+                t = max(x1, x2);
+                x1 = min(x1, x2);
+                x2 = max(t + d, x1);
+                fixAspectRatio();
+                break;
+            case 38:
+                d = -d;
+            case 40:
+                t = max(y1, y2);
+                y1 = min(y1, y2);
+                y2 = max(t + d, y1);
+                fixAspectRatio(true);
+                break;
+            default:
+                return;
+            }
+
+            doResize();
+        }
+        else {
+            x1 = min(x1, x2);
+            y1 = min(y1, y2);
+
+            switch (key) {
+            case 37:
+                doMove(max(x1 - d, left), y1);
+                break;
+            case 38:
+                doMove(x1, max(y1 - d, top));
+                break;
+            case 39:
+                doMove(x1 + min(d, imgWidth - selX(x2)), y1);
+                break;
+            case 40:
+                doMove(x1, y1 + min(d, imgHeight - selY(y2)));
+                break;
+            default:
+                return;
+            }
+        }
+
+        return false;
+    };
+
+    function styleOptions($elem, props) {
+        for (option in props)
+            if (options[option] !== undefined)
+                $elem.css(props[option], options[option]);
+    }
+
+    function setOptions(newOptions) {
+        if (newOptions.parent)
+            ($parent = $(newOptions.parent)).append($box.add($outer));
+
+        $.extend(options, newOptions);
+
+        adjust();
+
+        if (newOptions.handles != null) {
+            $handles.remove();
+            $handles = $([]);
+
+            i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0;
+
+            while (i--)
+                $handles = $handles.add(div());
+
+            $handles.addClass(options.classPrefix + '-handle').css({
+                position: 'absolute',
+                fontSize: 0,
+                zIndex: zIndex + 1 || 1
+            });
+
+            if (!parseInt($handles.css('width')) >= 0)
+                $handles.width(5).height(5);
+
+            if (o = options.borderWidth)
+                $handles.css({ borderWidth: o, borderStyle: 'solid' });
+
+            styleOptions($handles, { borderColor1: 'border-color',
+                borderColor2: 'background-color',
+                borderOpacity: 'opacity' });
+        }
+
+        scaleX = options.imageWidth / imgWidth || 1;
+        scaleY = options.imageHeight / imgHeight || 1;
+
+        if (newOptions.x1 != null) {
+            setSelection(newOptions.x1, newOptions.y1, newOptions.x2,
+                newOptions.y2);
+            newOptions.show = !newOptions.hide;
+        }
+
+        if (newOptions.keys)
+            options.keys = $.extend({ shift: 1, ctrl: 'resize' },
+                newOptions.keys);
+
+        $outer.addClass(options.classPrefix + '-outer');
+        $area.addClass(options.classPrefix + '-selection');
+        for (i = 0; i++ < 4;)
+            $($border[i-1]).addClass(options.classPrefix + '-border' + i);
+
+        styleOptions($area, { selectionColor: 'background-color',
+            selectionOpacity: 'opacity' });
+        styleOptions($border, { borderOpacity: 'opacity',
+            borderWidth: 'border-width' });
+        styleOptions($outer, { outerColor: 'background-color',
+            outerOpacity: 'opacity' });
+        if (o = options.borderColor1)
+            $($border[0]).css({ borderStyle: 'solid', borderColor: o });
+        if (o = options.borderColor2)
+            $($border[1]).css({ borderStyle: 'dashed', borderColor: o });
+
+        $box.append($area.add($border).add($handles).add($areaOpera));
+
+        if ($.browser.msie) {
+            if (o = $outer.css('filter').match(/opacity=([0-9]+)/))
+                $outer.css('opacity', o[1]/100);
+            if (o = $border.css('filter').match(/opacity=([0-9]+)/))
+                $border.css('opacity', o[1]/100);
+        }
+
+        if (newOptions.hide)
+            hide($box.add($outer));
+        else if (newOptions.show && imgLoaded) {
+            shown = true;
+            $box.add($outer).fadeIn(options.fadeSpeed||0);
+            doUpdate();
+        }
+
+        aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1];
+
+        $img.add($outer).unbind('mousedown', imgMouseDown);
+
+        if (options.disable || options.enable === false) {
+            $box.unbind('mousemove', areaMouseMove).unbind('mousedown', areaMouseDown);
+            $(window).unbind('resize', windowResize);
+        }
+        else {
+            if (options.enable || options.disable === false) {
+                if (options.resizable || options.movable)
+                    $box.mousemove(areaMouseMove).mousedown(areaMouseDown);
+
+                $(window).resize(windowResize);
+            }
+
+            if (!options.persistent)
+                $img.add($outer).mousedown(imgMouseDown);
+        }
+
+        options.enable = options.disable = undefined;
+    }
+
+    this.remove = function () {
+        $img.unbind('mousedown', imgMouseDown);
+        $box.add($outer).remove();
+    };
+
+    this.getOptions = function () { return options; };
+
+    this.setOptions = setOptions;
+
+    this.getSelection = getSelection;
+
+    this.setSelection = setSelection;
+
+    this.update = doUpdate;
+
+    $p = $img;
+
+    while ($p.length) {
+        zIndex = max(zIndex,
+            !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex);
+        if ($p.css('position') == 'fixed')
+            position = 'fixed';
+
+        $p = $p.parent(':not(body)');
+    }
+
+    zIndex = options.zIndex || zIndex;
+
+    if ($.browser.msie)
+        $img.attr('unselectable', 'on');
+
+    $.imgAreaSelect.keyPress = $.browser.msie ||
+        $.browser.safari ? 'keydown' : 'keypress';
+
+    if ($.browser.opera)
+        $areaOpera = div().css({ width: '100%', height: '100%',
+            position: 'absolute', zIndex: zIndex + 2 || 2 });
+
+    $box.add($outer).css({ visibility: 'hidden', position: position,
+        overflow: 'hidden', zIndex: zIndex || '0' });
+    $box.css({ zIndex: zIndex + 2 || 2 });
+    $area.add($border).css({ position: 'absolute', fontSize: 0 });
+
+    img.complete || img.readyState == 'complete' || !$img.is('img') ?
+        imgLoad() : $img.one('load', imgLoad);
+};
+
+$.fn.imgAreaSelect = function (options) {
+    options = options || {};
+
+    this.each(function () {
+        if ($(this).data('imgAreaSelect')) {
+            if (options.remove) {
+                $(this).data('imgAreaSelect').remove();
+                $(this).removeData('imgAreaSelect');
+            }
+            else
+                $(this).data('imgAreaSelect').setOptions(options);
+        }
+        else if (!options.remove) {
+            if (options.enable === undefined && options.disable === undefined)
+                options.enable = true;
+
+            $(this).data('imgAreaSelect', new $.imgAreaSelect(this, options));
+        }
+    });
+
+    if (options.instance)
+        return $(this).data('imgAreaSelect');
+
+    return this;
+};
+
+})(jQuery);
diff --git a/redakcja/static/js/wiki_img/base.js b/redakcja/static/js/wiki_img/base.js
new file mode 100644 (file)
index 0000000..ffe5a01
--- /dev/null
@@ -0,0 +1,329 @@
+(function($)
+{
+       var noop = function() { };
+
+       $.wiki = {
+               perspectives: {},
+               cls: {},
+               state: {
+                       "version": 1,
+                       "perspectives": {
+                "CodeMirrorPerspective": {}
+                       }
+               }
+       };
+
+       $.wiki.loadConfig = function() {
+               if(!window.localStorage)
+                       return;
+
+               try {
+                       var value = window.localStorage.getItem(CurrentDocument.id) || "{}";
+                       var config = JSON.parse(value);
+
+                       if (config.version == $.wiki.state.version) {
+                               $.wiki.state.perspectives = $.extend($.wiki.state.perspectives, config.perspectives);
+                       }
+               } catch(e) {
+                       console.log("Failed to load config, using default.");
+               }
+
+               console.log("Loaded:", $.wiki.state, $.wiki.state.version);
+       };
+
+       $(window).bind('unload', function() {
+               if(window.localStorage)
+                       window.localStorage.setItem(CurrentDocument.id, JSON.stringify($.wiki.state));
+       })
+
+
+       $.wiki.activePerspective = function() {
+               return this.perspectives[$("#tabs li.active").attr('id')];
+       };
+
+       $.wiki.exitContext = function() {
+               var ap = this.activePerspective();
+               if(ap) ap.onExit();
+               return ap;
+       };
+
+       $.wiki.enterContext = function(ap) {
+               if(ap) ap.onEnter();
+       };
+
+       $.wiki.isDirty = function() {
+               var ap = this.activePerspective();
+               return (!!CurrentDocument && CurrentDocument.has_local_changes) || ap.dirty();
+       };
+
+       $.wiki.newTab = function(doc, title, klass) {
+               var base_id = 'id' + Math.floor(Math.random()* 5000000000);
+               var id = (''+klass)+'_' + base_id;
+               var $tab = $('<li id="'+id+'" data-ui-related="'+base_id+'" data-ui-jsclass="'+klass+'" >'
+                               + title + '<img src="'+STATIC_URL+'icons/close.png" class="tabclose"></li>');
+               var $view = $('<div class="editor '+klass+'" id="'+base_id+'"> </div>');
+
+               this.perspectives[id] = new $.wiki[klass]({
+                       doc: doc,
+                       id: id,
+                       base_id: base_id,
+               });
+
+               $('#tabs').append($tab);
+               $view.hide().appendTo('#editor');
+               return {
+                       tab: $tab[0],
+                       view: $view[0],
+               };
+       };
+
+       $.wiki.initTab = function(options) {
+               var klass = $(options.tab).attr('data-ui-jsclass');
+
+               return new $.wiki[klass]({
+                       doc: options.doc,
+                       id: $(options.tab).attr('id'),
+                       callback: function() {
+                               $.wiki.perspectives[this.perspective_id] = this;
+                               if(options.callback)
+                                       options.callback.call(this);
+                       }
+               });
+       };
+
+       $.wiki.perspectiveForTab = function(tab) { // element or id
+               return this.perspectives[ $(tab).attr('id')];
+       }
+
+       $.wiki.switchToTab = function(tab){
+               var self = this;
+               var $tab = $(tab);
+
+               if($tab.length != 1)
+                       $tab = $(DEFAULT_PERSPECTIVE);
+
+               var $old = $tab.closest('.tabs').find('.active');
+
+               $old.each(function(){
+                       $(this).removeClass('active');
+                       self.perspectives[$(this).attr('id')].onExit();
+                       $('#' + $(this).attr('data-ui-related')).hide();
+               });
+
+               /* show new */
+               $tab.addClass('active');
+               $('#' + $tab.attr('data-ui-related')).show();
+
+               console.log($tab);
+               console.log($.wiki.perspectives);
+
+               $.wiki.perspectives[$tab.attr('id')].onEnter();
+       };
+
+       /*
+        * Basic perspective.
+        */
+       $.wiki.Perspective = function(options) {
+               if(!options) return;
+
+               this.doc = options.doc;
+               if (options.id) {
+                       this.perspective_id = options.id;
+               }
+               else {
+                       this.perspective_id = '';
+               }
+
+               if(options.callback)
+                       options.callback.call(this);
+       };
+
+       $.wiki.Perspective.prototype.config = function() {
+               return $.wiki.state.perspectives[this.perspective_id];
+       }
+
+       $.wiki.Perspective.prototype.toString = function() {
+               return this.perspective_id;
+       };
+
+       $.wiki.Perspective.prototype.dirty = function() {
+               return true;
+       };
+
+       $.wiki.Perspective.prototype.onEnter = function () {
+               // called when perspective in initialized
+               if (!this.noupdate_hash_onenter) {
+                       document.location.hash = '#' + this.perspective_id;
+               }
+       };
+
+       $.wiki.Perspective.prototype.onExit = function () {
+               // called when user switches to another perspective
+               if (!this.noupdate_hash_onenter) {
+                       document.location.hash = '';
+               }
+       };
+
+       $.wiki.Perspective.prototype.destroy = function() {
+               // pass
+       };
+
+       $.wiki.Perspective.prototype.freezeState = function () {
+               // free UI state (don't store data here)
+       };
+
+       $.wiki.Perspective.prototype.unfreezeState = function (frozenState) {
+               // restore UI state
+       };
+
+       /*
+        * Stub rendering (used in generating history)
+        */
+       $.wiki.renderStub = function(params)
+       {
+               params = $.extend({ 'filters': {} }, params);
+               var $elem = params.stub.clone();
+               $elem.removeClass('row-stub');
+               params.container.append($elem);
+
+               $('*[data-stub-value]', $elem).each(function() {
+                       var $this = $(this);
+                       var field = $this.attr('data-stub-value');
+
+                       var value = params.data[field];
+
+                       if(params.filters[field])
+                               value = params.filters[field](value);
+
+                       if(value === null || value === undefined) return;
+
+                       if(!$this.attr('data-stub-target')) {
+                               $this.text(value);
+                       }
+                       else {
+                               $this.attr($this.attr('data-stub-target'), value);
+                               $this.removeAttr('data-stub-target');
+                               $this.removeAttr('data-stub-value');
+                       }
+               });
+
+               $elem.show();
+               return $elem;
+       };
+
+       /*
+        * Dialogs
+        */
+       function GenericDialog(element) {
+               if(!element) return;
+
+               var self = this;
+
+               self.$elem = $(element);
+
+               if(!self.$elem.attr('data-ui-initialized')) {
+                       console.log("Initializing dialog", this);
+                       self.initialize();
+                       self.$elem.attr('data-ui-initialized', true);
+               }
+
+               self.show();
+       };
+
+       GenericDialog.prototype = {
+
+               /*
+               * Steps to follow when the dialog in first loaded on page.
+               */
+               initialize: function(){
+                       var self = this;
+
+                       /* bind buttons */
+                       $('button[data-ui-action]', self.$elem).click(function(event) {
+                               event.preventDefault();
+
+                               var action = $(this).attr('data-ui-action');
+                               console.log("Button pressed, action: ", action);
+
+                               try {
+                                       self[action + "Action"].call(self);
+                               } catch(e) {
+                                       console.log("Action failed:", e);
+                                       // always hide on cancel
+                                       if(action == 'cancel')
+                                               self.hide();
+                               }
+                       });
+               },
+
+               /*
+                * Prepare dialog for user. Clear any unnessary data.
+               */
+               show: function() {
+                       $.blockUI({
+                               message: this.$elem,
+                               css: {
+                                       'top': '25%',
+                                       'left': '25%',
+                                       'width': '50%'
+                               }
+                       });
+               },
+
+               hide: function(){
+                       $.unblockUI();
+               },
+
+               cancelAction: function() {
+                       this.hide();
+               },
+
+               doneAction: function() {
+                       this.hide();
+               },
+
+               clearForm: function() {
+                       $("*[data-ui-error-for]", this.$elem).text('');
+               },
+
+               reportErrors: function(errors) {
+                       var global = $("*[data-ui-error-for='__all__']", this.$elem);
+                       var unassigned = [];
+
+                       for (var field_name in errors)
+                       {
+                               var span = $("*[data-ui-error-for='"+field_name+"']", this.$elem);
+
+                               if(!span.length) {
+                                       unassigned.push(field_name);
+                                       continue;
+                               }
+
+                               span.text(errors[field_name].join(' '));
+                       }
+
+                       if(unassigned.length > 0)
+                               global.text( global.text() + 'W formularzu wystąpiły błędy');
+               }
+       };
+
+       $.wiki.cls.GenericDialog = GenericDialog;
+
+       $.wiki.showDialog = function(selector, options) {
+               var elem = $(selector);
+
+               if(elem.length != 1) {
+                       console.log("Failed to show dialog:", selector, elem);
+                       return false;
+               }
+
+               try {
+                   var klass = elem.attr('data-ui-jsclass');
+                       return new $.wiki.cls[klass](elem, options);
+               } catch(e) {
+                       console.log("Failed to show dialog", selector, klass, e);
+                       return false;
+               }
+       };
+
+})(jQuery);
diff --git a/redakcja/static/js/wiki_img/dialog_addtag.js b/redakcja/static/js/wiki_img/dialog_addtag.js
new file mode 100644 (file)
index 0000000..1a90ccf
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Dialog for saving document to the server
+ *
+ */
+(function($){
+
+    function AddTagDialog(element, options){
+        if (!options.revision  && options.revision != 0)
+            throw "AddTagDialog needs a revision number.";
+
+        this.ctx = $.wiki.exitContext();
+        this.clearForm();
+
+        /* fill out hidden fields */
+        this.$form = $('form', element);
+
+        $("input[name='addtag-id']", this.$form).val(CurrentDocument.id);
+        $("input[name='addtag-revision']", this.$form).val(options.revision);
+
+        $.wiki.cls.GenericDialog.call(this, element);
+    };
+
+    AddTagDialog.prototype = $.extend(new $.wiki.cls.GenericDialog(), {
+        cancelAction: function(){
+            $.wiki.enterContext(this.ctx);
+            this.hide();
+        },
+
+        saveAction: function(){
+            var self = this;
+
+            self.$elem.block({
+                message: "Dodawanie tagu",
+                fadeIn: 0,
+            });
+
+            CurrentDocument.setTag({
+                form: self.$form,
+                success: function(doc, changed, info){
+                    self.$elem.block({
+                        message: info,
+                        timeout: 2000,
+                        fadeOut: 0,
+                        onUnblock: function(){
+                            self.hide();
+                            $.wiki.enterContext(self.ctx);
+                        }
+                    });
+                },
+                failure: function(doc, info){
+                    console.log("Failure", info);
+                    self.reportErrors(info);
+                    self.$elem.unblock();
+                }
+            });
+        }
+    });
+
+    /* make it global */
+    $.wiki.cls.AddTagDialog = AddTagDialog;
+})(jQuery);
diff --git a/redakcja/static/js/wiki_img/dialog_save.js b/redakcja/static/js/wiki_img/dialog_save.js
new file mode 100644 (file)
index 0000000..aa9258d
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Dialog for saving document to the server
+ *
+ */
+(function($) {
+
+       function SaveDialog(element) {
+               this.ctx = $.wiki.exitContext();
+               this.clearForm();
+
+               /* fill out hidden fields */
+               this.$form = $('form', element);
+
+               $("input[name='textsave-id']", this.$form).val(CurrentDocument.id);
+               $("input[name='textsave-parent_revision']", this.$form).val(CurrentDocument.revision);
+
+               $.wiki.cls.GenericDialog.call(this, element);
+       };
+
+       SaveDialog.prototype = new $.wiki.cls.GenericDialog();
+
+       SaveDialog.prototype.cancelAction = function() {
+               $.wiki.enterContext(this.ctx);
+               this.hide();
+       };
+
+       SaveDialog.prototype.saveAction = function() {
+                       var self = this;
+
+                       self.$elem.block({
+                               message: "Zapisywanie... <br/><button id='save-hide'>ukryj</button>",
+                               fadeIn: 0,
+                       });
+            $.wiki.blocking = self.$elem;
+
+                       try {
+
+                               CurrentDocument.save({
+                                       form: self.$form,
+                                       success: function(doc, changed, info){
+                                               self.$elem.block({
+                                                       message: info,
+                                                       timeout: 2000,
+                                                       fadeOut: 0,
+                                                       onUnblock: function() {
+                                                               self.hide();
+                                                               $.wiki.enterContext(self.ctx);
+                                                       }
+                                               });
+                                       },
+                                       failure: function(doc, info) {
+                                               console.log("Failure", info);
+                                               self.reportErrors(info);
+                                               self.$elem.unblock();
+                                       }
+                               });
+                       } catch(e) {
+                               console.log('Exception:', e)
+                               self.$elem.unblock();
+                       }
+       }; /* end of save dialog */
+
+       /* make it global */
+       $.wiki.cls.SaveDialog = SaveDialog;
+})(jQuery);
diff --git a/redakcja/static/js/wiki_img/loader.js b/redakcja/static/js/wiki_img/loader.js
new file mode 100644 (file)
index 0000000..41a029d
--- /dev/null
@@ -0,0 +1,159 @@
+if (!window.console) {
+    window.console = {
+        log: function(){
+        }
+    }
+}
+
+DEFAULT_PERSPECTIVE = "#SummaryPerspective";
+
+$(function()
+{
+       var tabs = $('ol#tabs li');
+       var gallery = null;
+       CurrentDocument = new $.wikiapi.WikiDocument("document-meta");
+
+       $.blockUI.defaults.baseZ = 10000;
+
+    function initialize()
+       {
+               $(document).keydown(function(event) {
+                       console.log("Received key:", event);
+               });
+
+               /* The save button */
+        $('#save-button').click(function(event){
+            event.preventDefault();
+                       $.wiki.showDialog('#save_dialog');
+        });
+
+               $('.editor').hide();
+
+               /*
+                * TABS
+                */
+        $('.tabs li').live('click', function(event, callback) {
+                       $.wiki.switchToTab(this);
+        });
+
+               $('#tabs li > .tabclose').live('click', function(event, callback) {
+                       var $tab = $(this).parent();
+
+                       if($tab.is('.active'))
+                               $.wiki.switchToTab(DEFAULT_PERSPECTIVE);
+
+                       var p = $.wiki.perspectiveForTab($tab);
+                       p.destroy();
+
+                       return false;
+        });
+
+
+        /*$(window).resize(function(){
+            $('iframe').height($(window).height() - $('#tabs').outerHeight() - $('#source-editor .toolbar').outerHeight());
+        });
+
+        $(window).resize();*/
+
+        /*$('.vsplitbar').toggle(
+                       function() {
+                               $.wiki.state.perspectives.ScanGalleryPerspective.show = true;
+                               $('#sidebar').show();
+                               $('.vsplitbar').css('right', 480).addClass('active');
+                               $('#editor .editor').css('right', 510);
+                               $(window).resize();
+                               $.wiki.perspectiveForTab('#tabs-right .active').onEnter();
+                       },
+                       function() {
+                           var active_right = $.wiki.perspectiveForTab('#tabs-right .active');
+                               $.wiki.state.perspectives.ScanGalleryPerspective.show = false;
+                               $('#sidebar').hide();
+                               $('.vsplitbar').css('right', 0).removeClass('active');
+                               $(".vsplitbar-title").html("&uarr;&nbsp;" + active_right.vsplitbar + "&nbsp;&uarr;");
+                               $('#editor .editor').css('right', 30);
+                               $(window).resize();
+                               active_right.onExit();
+                       }
+               );*/
+
+        window.onbeforeunload = function(e) {
+            if($.wiki.isDirty()) {
+                               e.returnValue = "Na stronie mogą być nie zapisane zmiany.";
+                               return "Na stronie mogą być nie zapisane zmiany.";
+                       };
+        };
+
+               console.log("Fetching document's text");
+
+               $(document).bind('wlapi_document_changed', function(event, doc) {
+                       try {
+                               $('#document-revision').text(doc.revision);
+                       } catch(e) {
+                               console.log("Failed handler", e);
+                       }
+               });
+
+               CurrentDocument.fetch({
+                       success: function(){
+                               console.log("Fetch success");
+                               $('#loading-overlay').fadeOut();
+                               var active_tab = document.location.hash || DEFAULT_PERSPECTIVE;
+
+                               console.log("Initial tab is:", active_tab)
+                               $.wiki.switchToTab(active_tab);
+                       },
+                       failure: function() {
+                               $('#loading-overlay').fadeOut();
+                               alert("FAILURE");
+                       }
+               });
+    }; /* end of initialize() */
+
+
+       /* Load configuration */
+       $.wiki.loadConfig();
+
+       var initAll = function(a, f) {
+               if (a.length == 0) return f();
+
+               $.wiki.initTab({
+                       tab: a.pop(),
+                       doc: CurrentDocument,
+                       callback: function(){
+                               initAll(a, f);
+                       }
+               });
+       };
+
+
+       /*
+        * Initialize all perspectives
+        */
+       initAll( $.makeArray($('.tabs li')), initialize);
+       console.log(location.hash);
+});
+
+
+// Wykonuje block z załadowanymi kanonicznymi motywami
+function withThemes(code_block, onError)
+{
+    if (typeof withThemes.canon == 'undefined') {
+        $.ajax({
+            url: '/themes',
+            dataType: 'text',
+            success: function(data) {
+                withThemes.canon = data.split('\n');
+                code_block(withThemes.canon);
+            },
+            error: function() {
+                withThemes.canon = null;
+                code_block(withThemes.canon);
+            }
+        })
+    }
+    else {
+        code_block(withThemes.canon);
+    }
+}
+
+
diff --git a/redakcja/static/js/wiki_img/toolbar.js b/redakcja/static/js/wiki_img/toolbar.js
new file mode 100644 (file)
index 0000000..3eafdae
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Toolbar plugin.
+ */
+(function($) {
+
+    $.fn.toolbarize = function(options) {
+        var $toolbar = $(this);
+        var $container = $('.button_group_container', $toolbar);
+
+        $('.button_group button', $toolbar).click(function(event){
+            event.preventDefault();
+
+            var params = eval("(" + $(this).attr('data-ui-action-params') + ")");
+
+            scriptletCenter.callInteractive({
+                action: $(this).attr('data-ui-action'),
+                context: options.actionContext,
+                extra: params
+            });
+        });
+
+        $toolbar.children().filter('select').change(function(event){
+            var slug = $(this).val();
+            $container.scrollLeft(0);
+            $('*[data-group]').hide().filter('[data-group=' + slug + ']').show();
+        }).change();
+
+        $('button.next', $toolbar).click(function() {
+            var $current_group = $('.button_group:visible', $toolbar);
+            var scroll = $container.scrollLeft();
+
+            var $hidden = $("li", $current_group).filter(function() {
+                return ($(this).position().left + $(this).outerWidth()) > $container.width();
+            }).first();
+
+            if($hidden.length > 0) {
+                scroll = $hidden.position().left + scroll + $hidden.outerWidth() - $container.width() + 1;
+                $container.scrollLeft(scroll);
+            };
+        });
+
+        $('button.prev', $toolbar).click(function() {
+            var $current_group = $('.button_group:visible', $toolbar);
+            var scroll = $container.scrollLeft();
+
+            /* first not visible element is: */
+            var $hidden = $("li", $current_group).filter(function() {
+                return $(this).position().left < 0;
+            }).last();
+
+            if( $hidden.length > 0)
+            {
+                scroll = scroll + $hidden.position().left;
+                $container.scrollLeft(scroll);
+            };
+        });
+
+    };
+
+})(jQuery);
\ No newline at end of file
diff --git a/redakcja/static/js/wiki_img/view_editor_motifs.js b/redakcja/static/js/wiki_img/view_editor_motifs.js
new file mode 100644 (file)
index 0000000..7ce9665
--- /dev/null
@@ -0,0 +1,151 @@
+(function($){
+
+    function MotifsPerspective(options){
+
+        var old_callback = options.callback;
+
+        options.callback = function(){
+            var self = this;
+
+            self.$tag_name = $('#motifs-editor #tag-name');
+            withThemes(function(canonThemes){
+                self.$tag_name.autocomplete(canonThemes, {
+                    autoFill: true,
+                    multiple: true,
+                    selectFirst: true,
+                    highlight: false
+                });
+            })
+
+            self.$objects_list = $('#motifs-editor #objects-list');
+
+            self.x1 = null;
+            self.x2 = null;
+            self.y1 = null;
+            self.y2 = null;
+
+            if (!CurrentDocument.readonly) {
+                self.ias = $('#motifs-editor img.area-selectable').imgAreaSelect({ handles: true, onSelectEnd: self._fillCoords(self), instance: true });
+                $('#motifs-editor #add').click(self._addObject(self));
+
+                $('.delete', self.$objects_list).live('click', function() {
+                    $(this).prev().trigger('click');
+                    if (window.confirm("Czy na pewno chcesz usunąć ten motyw?")) {
+                        $(this).prev().remove();
+                        $(this).remove();
+                    }
+                    self._resetSelection();
+                    return false;
+                });
+            }
+
+            $('.image-object', self.$objects_list).live('click', function(){
+                $('.active', self.$objects_list).removeClass('active');
+                $(this).addClass('active');
+                var coords = $(this).data('coords');
+                if (coords) {
+                    self.ias.setSelection.apply(self.ias, coords);
+                    self.ias.setOptions({ show: true });
+                }
+                else {
+                    self._resetSelection();
+                }
+            });
+
+            old_callback.call(this);
+        };
+
+        $.wiki.Perspective.call(this, options);
+    };
+
+    MotifsPerspective.prototype = new $.wiki.Perspective();
+
+    MotifsPerspective.prototype.freezeState = function(){
+
+    };
+
+    MotifsPerspective.prototype._resetSelection = function() {
+        var self = this;
+        self.x1 = self.x2 = self.y1 = self.y2 = null;
+        self.ias.setOptions({ hide: true });
+    }
+
+
+    MotifsPerspective.prototype._push = function(name, x1, y1, x2, y2) {
+        var $e = $('<span class="image-object"></span>')
+        $e.text(name);
+        if (x1 !== null)
+            $e.data('coords', [x1, y1, x2, y2]);
+        this.$objects_list.append($e);
+        this.$objects_list.append('<span class="delete">(x)</span>');
+    }
+
+
+    MotifsPerspective.prototype._addObject = function(self) {
+        return function() {
+            outputs = [];
+            chunks = self.$tag_name.val().split(',');
+            for (i in chunks) {
+                item = chunks[i].trim();
+                if (item == '')
+                    continue;
+                outputs.push(item.trim());
+            }
+            output = outputs.join(', ');
+
+            self._push(output, self.x1, self.y1, self.x2, self.y2);
+            self._resetSelection();
+        }
+    }
+
+    MotifsPerspective.prototype._fillCoords = function(self) {
+        return function(img, selection) {
+            $('.active', self.$objects_list).removeClass('active');
+            if (selection.x1 != selection.x2 && selection.y1 != selection.y2) {
+                self.x1 = selection.x1;
+                self.x2 = selection.x2;
+                self.y1 = selection.y1;
+                self.y2 = selection.y2;
+            }
+            else {
+                self.x1 = self.x2 = self.y1 = self.y2 = null;
+            }
+        }
+    }
+
+    MotifsPerspective.prototype.onEnter = function(success, failure){
+        var self = this;
+        this.$objects_list.children().remove();
+
+        $.each(this.doc.getImageItems('motyw'), function(i, e) {
+            self._push.apply(self, e);
+        });
+
+        if (this.x1 !== null)
+            this.ias.setOptions({enable: true, show: true});
+        else
+            this.ias.setOptions({enable: true});
+
+        $.wiki.Perspective.prototype.onEnter.call(this);
+
+    };
+
+    MotifsPerspective.prototype.onExit = function(success, failure){
+        var self = this;
+        var motifs = [];
+        this.$objects_list.children(".image-object").each(function(i, e) {
+            var args = $(e).data('coords');
+            if (!args)
+                args = [null, null, null, null];
+            args.unshift($(e).text());
+            motifs.push(args);
+        })
+        self.doc.setImageItems('motyw', motifs);
+
+        this.ias.setOptions({disable: true, hide: true});
+
+    };
+
+    $.wiki.MotifsPerspective = MotifsPerspective;
+
+})(jQuery);
diff --git a/redakcja/static/js/wiki_img/view_editor_objects.js b/redakcja/static/js/wiki_img/view_editor_objects.js
new file mode 100644 (file)
index 0000000..9e6a3cb
--- /dev/null
@@ -0,0 +1,142 @@
+(function($){
+
+    function ObjectsPerspective(options){
+
+        var old_callback = options.callback;
+
+        options.callback = function(){
+            var self = this;
+
+            self.$tag_name = $('#objects-editor #tag-name');
+            self.$objects_list = $('#objects-editor #objects-list');
+
+            self.x1 = null;
+            self.x2 = null;
+            self.y1 = null;
+            self.y2 = null;
+
+            if (!CurrentDocument.readonly) {
+                self.ias = $('#objects-editor img.area-selectable').imgAreaSelect({ handles: true, onSelectEnd: self._fillCoords(self), instance: true });
+                $('#objects-editor #add').click(self._addObject(self));
+
+                $('.delete', self.$objects_list).live('click', function() {
+                    $(this).prev().trigger('click');
+                    if (window.confirm("Czy na pewno chcesz usunąć ten obiekt?")) {
+                        $(this).prev().remove();
+                        $(this).remove();
+                    }
+                    self._resetSelection();
+                    return false;
+                });
+            }
+
+            $('.image-object', self.$objects_list).live('click', function(){
+                $('.active', self.$objects_list).removeClass('active');
+                $(this).addClass('active');
+                var coords = $(this).data('coords');
+                if (coords) {
+                    self.ias.setSelection.apply(self.ias, coords);
+                    self.ias.setOptions({ show: true });
+                }
+                else {
+                    self._resetSelection();
+                }
+            });
+
+            old_callback.call(this);
+        };
+
+        $.wiki.Perspective.call(this, options);
+    };
+
+    ObjectsPerspective.prototype = new $.wiki.Perspective();
+
+    ObjectsPerspective.prototype.freezeState = function(){
+
+    };
+
+    ObjectsPerspective.prototype._resetSelection = function() {
+        var self = this;
+        self.x1 = self.x2 = self.y1 = self.y2 = null;
+        self.ias.setOptions({ hide: true });
+    }
+
+
+    ObjectsPerspective.prototype._push = function(name, x1, y1, x2, y2) {
+        var $e = $('<span class="image-object"></span>')
+        $e.text(name);
+        if (x1 !== null)
+            $e.data('coords', [x1, y1, x2, y2]);
+        this.$objects_list.append($e);
+        this.$objects_list.append('<span class="delete">(x)</span>');
+    }
+
+
+    ObjectsPerspective.prototype._addObject = function(self) {
+        return function() {
+            outputs = [];
+            chunks = self.$tag_name.val().split(',');
+            for (i in chunks) {
+                item = chunks[i].trim();
+                if (item == '')
+                    continue;
+                outputs.push(item.trim());
+            }
+            output = outputs.join(', ');
+
+            self._push(output, self.x1, self.y1, self.x2, self.y2);
+            self._resetSelection();
+        }
+    }
+
+    ObjectsPerspective.prototype._fillCoords = function(self) {
+        return function(img, selection) {
+            $('.active', self.$objects_list).removeClass('active');
+            if (selection.x1 != selection.x2 && selection.y1 != selection.y2) {
+                self.x1 = selection.x1;
+                self.x2 = selection.x2;
+                self.y1 = selection.y1;
+                self.y2 = selection.y2;
+            }
+            else {
+                self.x1 = self.x2 = self.y1 = self.y2 = null;
+            }
+        }
+    }
+
+    ObjectsPerspective.prototype.onEnter = function(success, failure){
+        var self = this;
+        this.$objects_list.children().remove();
+
+        $.each(this.doc.getImageItems('obiekt'), function(i, e) {
+            self._push.apply(self, e);
+        });
+
+        if (this.x1 !== null)
+            this.ias.setOptions({enable: true, show: true});
+        else
+            this.ias.setOptions({enable: true});
+
+        $.wiki.Perspective.prototype.onEnter.call(this);
+
+    };
+
+    ObjectsPerspective.prototype.onExit = function(success, failure){
+        var self = this;
+        var objects = [];
+        this.$objects_list.children(".image-object").each(function(i, e) {
+            var args = $(e).data('coords');
+            if (!args)
+                args = [null, null, null, null];
+            args.unshift($(e).text());
+            objects.push(args);
+        })
+        self.doc.setImageItems('obiekt', objects);
+
+        this.ias.setOptions({disable: true, hide: true});
+
+    };
+
+    $.wiki.ObjectsPerspective = ObjectsPerspective;
+
+})(jQuery);
diff --git a/redakcja/static/js/wiki_img/view_editor_source.js b/redakcja/static/js/wiki_img/view_editor_source.js
new file mode 100644 (file)
index 0000000..8fb9358
--- /dev/null
@@ -0,0 +1,109 @@
+/* COMMENT */
+(function($) {
+
+       function CodeMirrorPerspective(options)
+       {
+               var old_callback = options.callback;
+        options.callback = function(){
+                       var self = this;
+
+                       this.codemirror = CodeMirror.fromTextArea('codemirror_placeholder', {
+                               parserfile: 'parsexml.js',
+                               path: STATIC_URL + "js/lib/codemirror-0.8/",
+                               stylesheet: STATIC_URL + "css/xmlcolors_20100906.css",
+                               parserConfig: {
+                                       useHTMLKludges: false
+                               },
+                               iframeClass: 'xml-iframe',
+                               textWrapping: true,
+                               lineNumbers: true,
+                               width: "100%",
+                               height: "100%",
+                               tabMode: 'spaces',
+                               indentUnit: 0,
+                               readOnly: CurrentDocument.readonly || false,
+                               initCallback: function(){
+
+                                       self.codemirror.grabKeys(function(event) {
+                                               if (event.button) {
+                                                       $(event.button).trigger('click');
+                                                       event.button = null;
+                                               }
+                                       }, function(keycode, event) {
+                                               if(!event.altKey)
+                                                       return false;
+
+                                               var c = String.fromCharCode(keycode).toLowerCase();
+                                               var button = $("#source-editor button[data-ui-accesskey='"+c+"']");
+                                               if(button.length == 0)
+                                                       return false;
+
+                                               /* it doesn't matter which button we pick - all do the same */
+                                               event.button = button[0];
+                                               return true;
+                                       });
+
+                                       $('#source-editor .toolbar').toolbarize({
+                                           actionContext: self.codemirror
+                                       });
+
+                                       console.log("Initialized CodeMirror");
+
+                                       // textarea is no longer needed
+                                       $('codemirror_placeholder').remove();
+
+                                       old_callback.call(self);
+                               }
+                       });
+               };
+
+               $.wiki.Perspective.call(this, options);
+       };
+
+
+       CodeMirrorPerspective.prototype = new $.wiki.Perspective();
+
+       CodeMirrorPerspective.prototype.freezeState = function() {
+               this.config().position = this.codemirror.win.scrollY || 0;
+       };
+
+       CodeMirrorPerspective.prototype.unfreezeState = function () {
+               this.codemirror.win.scroll(0, this.config().position || 0);
+       };
+
+       CodeMirrorPerspective.prototype.onEnter = function(success, failure) {
+               $.wiki.Perspective.prototype.onEnter.call(this);
+
+               console.log('Entering', this.doc);
+               this.codemirror.setCode(this.doc.text);
+
+               /* fix line numbers bar */
+               var $nums = $('.CodeMirror-line-numbers');
+           var barWidth = $nums.width();
+
+               $(this.codemirror.frame.contentDocument.body).css('padding-left', barWidth);
+               // $nums.css('left', -barWidth);
+
+               $(window).resize();
+               this.unfreezeState(this._uistate);
+
+               if(success) success();
+       }
+
+       CodeMirrorPerspective.prototype.onExit = function(success, failure) {
+               this.freezeState();
+
+               $.wiki.Perspective.prototype.onExit.call(this);
+               console.log('Exiting', this.doc);
+               this.doc.setText(this.codemirror.getCode());
+
+        if ($('.vsplitbar').hasClass('active') && $('#SearchPerspective').hasClass('active')) {
+            $.wiki.switchToTab('#ScanGalleryPerspective');
+        }
+
+               if(success) success();
+       }
+
+       $.wiki.CodeMirrorPerspective = CodeMirrorPerspective;
+
+})(jQuery);
diff --git a/redakcja/static/js/wiki_img/view_summary.js b/redakcja/static/js/wiki_img/view_summary.js
new file mode 100644 (file)
index 0000000..5d06647
--- /dev/null
@@ -0,0 +1,25 @@
+(function($){
+
+       function SummaryPerspective(options) {
+               var old_callback = options.callback;
+               var self = this;
+
+               $.wiki.Perspective.call(this, options);
+    };
+
+    SummaryPerspective.prototype = new $.wiki.Perspective();
+
+    SummaryPerspective.prototype.freezeState = function(){
+        // must
+    };
+
+       SummaryPerspective.prototype.onEnter = function(success, failure){
+               $.wiki.Perspective.prototype.onEnter.call(this);
+
+               console.log("Entered summery view");
+       };
+
+       $.wiki.SummaryPerspective = SummaryPerspective;
+
+})(jQuery);
+
diff --git a/redakcja/static/js/wiki_img/wikiapi.js b/redakcja/static/js/wiki_img/wikiapi.js
new file mode 100644 (file)
index 0000000..2e4682c
--- /dev/null
@@ -0,0 +1,307 @@
+(function($) {
+       $.wikiapi = {};
+       var noop = function() {
+       };
+       var noops = {
+               success: noop,
+               failure: noop
+       };
+       /*
+        * Return absolute reverse path of given named view. (at least he have it
+        * hard-coded in one place)
+        *
+        * TODO: think of a way, not to hard-code it here ;)
+        *
+        */
+       function reverse() {
+               var vname = arguments[0];
+               var base_path = "/images";
+
+               if (vname == "ajax_document_text") {
+                       var path = "/" + arguments[1] + "/text";
+
+                   if (arguments[2] !== undefined)
+                               path += "/" + arguments[2];
+
+                       return base_path + path;
+               }
+
+               /*if (vname == "ajax_document_history") {
+
+                       return base_path + "/" + arguments[1] + "/history";
+               }
+*/
+               if (vname == "ajax_document_gallery") {
+
+                       return base_path + "/" + arguments[1] + "/gallery";
+               }
+/*
+               if (vname == "ajax_document_diff")
+                       return base_path + "/" + arguments[1] + "/diff";
+
+        if (vname == "ajax_document_rev")
+            return base_path + "/" + arguments[1] + "/rev";
+
+               if (vname == "ajax_document_addtag")
+                       return base_path + "/" + arguments[1] + "/tags";
+
+               if (vname == "ajax_publish")
+                       return base_path + "/" + arguments[1] + "/publish";*/
+
+               console.log("Couldn't reverse match:", vname);
+               return "/404.html";
+       };
+
+       /*
+        * Document Abstraction
+        */
+       function WikiDocument(element_id) {
+               var meta = $('#' + element_id);
+               this.id = meta.attr('data-document-name');
+
+               this.revision = $("*[data-key='revision']", meta).text();
+               this.readonly = !!$("*[data-key='readonly']", meta).text();
+
+               this.galleryLink = $("*[data-key='gallery']", meta).text();
+               this.galleryImages = [];
+               this.text = null;
+               this.has_local_changes = false;
+               this._lock = -1;
+               this._context_lock = -1;
+               this._lock_count = 0;
+       };
+
+       WikiDocument.prototype.triggerDocumentChanged = function() {
+               $(document).trigger('wlapi_document_changed', this);
+       };
+       /*
+        * Fetch text of this document.
+        */
+       WikiDocument.prototype.fetch = function(params) {
+               params = $.extend({}, noops, params);
+               var self = this;
+               $.ajax({
+                       method: "GET",
+                       url: reverse("ajax_document_text", self.id),
+                       data: {"revision": self.revision},
+                       dataType: 'json',
+                       success: function(data) {
+                               var changed = false;
+
+                               if (self.text === null || self.revision !== data.revision) {
+                                       self.text = data.text;
+                                       if (self.text === '') {
+                                           self.text = '<obraz></obraz>';
+                                       }
+                                       self.revision = data.revision;
+                                       changed = true;
+                                       self.triggerDocumentChanged();
+                               };
+
+                               self.has_local_changes = false;
+                               params['success'](self, changed);
+                       },
+                       error: function() {
+                               params['failure'](self, "Nie udało się wczytać treści dokumentu.");
+                       }
+               });
+       };
+
+       /*
+        * Set document's text
+        */
+       WikiDocument.prototype.setText = function(text) {
+               this.text = text;
+               this.has_local_changes = true;
+       };
+
+       /*
+        * Save text back to the server
+        */
+       WikiDocument.prototype.save = function(params) {
+               params = $.extend({}, noops, params);
+               var self = this;
+
+               if (!self.has_local_changes) {
+                       console.log("Abort: no changes.");
+                       return params['success'](self, false, "Nie ma zmian do zapisania.");
+               };
+
+               // Serialize form to dictionary
+               var data = {};
+               $.each(params['form'].serializeArray(), function() {
+                       data[this.name] = this.value;
+               });
+
+               data['textsave-text'] = self.text;
+
+               $.ajax({
+                       url: reverse("ajax_document_text", self.id),
+                       type: "POST",
+                       dataType: "json",
+                       data: data,
+                       success: function(data) {
+                               var changed = false;
+
+                $('#header').removeClass('saving');
+
+                               if (data.text) {
+                                       self.text = data.text;
+                                       self.revision = data.revision;
+                                       changed = true;
+                                       self.triggerDocumentChanged();
+                               };
+
+                               params['success'](self, changed, ((changed && "Udało się zapisać :)") || "Twoja wersja i serwera jest identyczna"));
+                       },
+                       error: function(xhr) {
+                if ($('#header').hasClass('saving')) {
+                    $('#header').removeClass('saving');
+                    $.blockUI({
+                        message: "<p>Nie udało się zapisać zmian. <br/><button onclick='$.unblockUI()'>OK</button></p>"
+                    })
+                }
+                else {
+                    try {
+                        params['failure'](self, $.parseJSON(xhr.responseText));
+                    }
+                    catch (e) {
+                        params['failure'](self, {
+                            "__message": "<p>Nie udało się zapisać - błąd serwera.</p>"
+                        });
+                    };
+                }
+
+                       }
+               });
+
+        $('#save-hide').click(function(){
+            $('#header').addClass('saving');
+            $.unblockUI();
+            $.wiki.blocking.unblock();
+        });
+       }; /* end of save() */
+
+       WikiDocument.prototype.publish = function(params) {
+               params = $.extend({}, noops, params);
+               var self = this;
+               $.ajax({
+                       url: reverse("ajax_publish", self.id),
+                       type: "POST",
+                       dataType: "json",
+                       success: function(data) {
+                               params.success(self, data);
+                       },
+                       error: function(xhr) {
+                               if (xhr.status == 403 || xhr.status == 401) {
+                                       params.failure(self, "Nie masz uprawnień lub nie jesteś zalogowany.");
+                               }
+                               else {
+                                       try {
+                                               params.failure(self, xhr.responseText);
+                                       }
+                                       catch (e) {
+                                               params.failure(self, "Nie udało się - błąd serwera.");
+                                       };
+                               };
+
+                       }
+               });
+       };
+       WikiDocument.prototype.setTag = function(params) {
+               params = $.extend({}, noops, params);
+               var self = this;
+               var data = {
+                       "addtag-id": self.id,
+               };
+
+               /* unpack form */
+               $.each(params.form.serializeArray(), function() {
+                       data[this.name] = this.value;
+               });
+
+               $.ajax({
+                       url: reverse("ajax_document_addtag", self.id),
+                       type: "POST",
+                       dataType: "json",
+                       data: data,
+                       success: function(data) {
+                               params.success(self, data.message);
+                       },
+                       error: function(xhr) {
+                               if (xhr.status == 403 || xhr.status == 401) {
+                                       params.failure(self, {
+                                               "__all__": ["Nie masz uprawnień lub nie jesteś zalogowany."]
+                                       });
+                               }
+                               else {
+                                       try {
+                                               params.failure(self, $.parseJSON(xhr.responseText));
+                                       }
+                                       catch (e) {
+                                               params.failure(self, {
+                                                       "__all__": ["Nie udało się - błąd serwera."]
+                                               });
+                                       };
+                               };
+                       }
+               });
+       };
+
+    WikiDocument.prototype.getImageItems = function(tag) {
+        var self = this;
+
+        var parser = new DOMParser();
+        var doc = parser.parseFromString(self.text, 'text/xml');
+        var error = $('parsererror', doc);
+
+        if (error.length != 0) {
+            return null;
+        }
+
+        var a = [];
+        $(tag, doc).each(function(i, e) {
+            var $e = $(e);
+            a.push([
+                $e.text(),
+                $e.attr('x1'),
+                $e.attr('y1'),
+                $e.attr('x2'),
+                $e.attr('y2')
+            ]);
+        });
+
+        return a;
+    }
+
+    WikiDocument.prototype.setImageItems = function(tag, items) {
+        var self = this;
+
+        var parser = new DOMParser();
+        var doc = parser.parseFromString(self.text, 'text/xml');
+        var serializer = new XMLSerializer();
+        var error = $('parsererror', doc);
+
+        if (error.length != 0) {
+            return null;
+        }
+
+        $(tag, doc).remove();
+        $root = $(doc.firstChild);
+        $.each(items, function(i, e) {
+            var el = $(doc.createElement(tag));
+            el.text(e[0]);
+            if (e[1] !== null) {
+                el.attr('x1', e[1]);
+                el.attr('y1', e[2]);
+                el.attr('x2', e[3]);
+                el.attr('y2', e[4]);
+            }
+            $root.append(el);
+        });
+        self.setText(serializer.serializeToString(doc));
+    }
+
+
+       $.wikiapi.WikiDocument = WikiDocument;
+})(jQuery);
index dd7f884..08073a4 100644 (file)
@@ -18,8 +18,9 @@ urlpatterns = patterns('',
     url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
     (r'^admin/', include(admin.site.urls)),
 
-    url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/documents/'}),
+    url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/images/'}),
     url(r'^documents/', include('wiki.urls')),
+    url(r'^images/', include('wiki_img.urls')),
 
     # Static files (should be served by Apache)
     url(r'^%s(?P<path>.+)$' % settings.MEDIA_URL[1:], 'django.views.static.serve',