From 32974185d5e2b1bdc197b4f5dcab259b5de3c6b4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Rekucki?= Date: Tue, 13 Apr 2010 11:08:27 +0200 Subject: [PATCH] Publication button works, but need better error messages. --- apps/wiki/constants.py | 24 +-- apps/wiki/forms.py | 53 +++--- apps/wiki/helpers.py | 6 +- apps/wiki/models.py | 29 +-- apps/wiki/nice_diff.py | 3 - apps/wiki/templates/wiki/base.html | 24 +++ .../wiki/document_create_missing.html | 13 ++ apps/wiki/templates/wiki/document_list.html | 21 +-- apps/wiki/templates/wiki/history_view.html | 14 +- apps/wiki/templates/wiki/save_dialog.html | 24 +-- apps/wiki/templates/wiki/summary_view.html | 6 +- apps/wiki/templates/wiki/tag_dialog.html | 16 +- apps/wiki/urls.py | 4 + apps/wiki/views.py | 83 +++++++-- lib/test_wlapi.py | 37 ++++ lib/vstorage.py | 118 +++++++------ lib/wlapi.py | 36 +++- platforma/compress_settings.py | 72 ++++++++ platforma/context_processors.py | 3 +- platforma/settings.py | 8 + platforma/static/css/summary.css | 8 +- platforma/static/js/wiki/view_history.js | 2 +- platforma/static/js/wiki/view_summary.js | 18 ++ platforma/static/js/wiki/wikiapi.js | 166 ++++++++++-------- platforma/urls.py | 4 +- 25 files changed, 532 insertions(+), 260 deletions(-) create mode 100644 apps/wiki/templates/wiki/base.html create mode 100644 apps/wiki/templates/wiki/document_create_missing.html create mode 100644 lib/test_wlapi.py create mode 100644 platforma/compress_settings.py diff --git a/apps/wiki/constants.py b/apps/wiki/constants.py index 472bbb60..562421a1 100644 --- a/apps/wiki/constants.py +++ b/apps/wiki/constants.py @@ -2,23 +2,23 @@ from django.utils.translation import ugettext_lazy as _ DOCUMENT_TAGS = ( - ("source", _(u"Tekst źródłowy")), - ("first_correction", _(u"Po autokorekcie")), - ("tagged", _(u"Tekst otagowany")), - ("second_correction", _(u"Po korekcie")), - ("source_annotations", _(u"Sprawdzone przypisy źródła")), - ("language_updates", _(u"Uwspółcześnienia")), - ("ready_to_publish", _(u"Tekst do publikacji")), + ("source", _("Tekst źródłowy")), + ("first_correction", _("Po autokorekcie")), + ("tagged", _("Tekst otagowany")), + ("second_correction", _("Po korekcie")), + ("source_annotations", _("Sprawdzone przypisy źródła")), + ("language_updates", _("Uwspółcześnienia")), + ("ready_to_publish", _("Tekst do publikacji")), ) DOCUMENT_TAGS_DICT = dict(DOCUMENT_TAGS) DOCUMENT_STAGES = ( - ("first_correction", _(u"Autokorekta")), - ("tagged", _(u"Tagowanie")), - ("second_correction", _(u"Korekta")), - ("source_annotations", _(u"Przypisy źródła")), - ("language_updates", _(u"Uwspółcześnienia")), + ("first_correction", _("Autokorekta")), + ("tagged", _("Tagowanie")), + ("second_correction", _("Korekta")), + ("source_annotations", _("Przypisy źródła")), + ("language_updates", _("Uwspółcześnienia")), ) DOCUMENT_STAGES_DICT = dict(DOCUMENT_STAGES) diff --git a/apps/wiki/forms.py b/apps/wiki/forms.py index 4fb71a5a..64dac29c 100644 --- a/apps/wiki/forms.py +++ b/apps/wiki/forms.py @@ -4,49 +4,46 @@ # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. # from django import forms -from wiki.models import Document, getstorage from wiki.constants import DOCUMENT_TAGS, DOCUMENT_STAGES from django.utils.translation import ugettext_lazy as _ -class DocumentForm(forms.Form): - """ Old form for saving document's text """ +class DocumentTagForm(forms.Form): + """ + Form for tagging revisions. + """ - name = forms.CharField(widget=forms.HiddenInput) - text = forms.CharField(widget=forms.Textarea) + id = forms.CharField(widget=forms.HiddenInput) + tag = forms.ChoiceField(choices=DOCUMENT_TAGS) revision = forms.IntegerField(widget=forms.HiddenInput) - comment = forms.CharField() - - def __init__(self, *args, **kwargs): - document = kwargs.pop('instance', None) - super(DocumentForm, self).__init__(*args, **kwargs) - if document: - self.fields['name'].initial = document.name - self.fields['text'].initial = document.text - self.fields['revision'].initial = document.revision() - - def save(self, document_author='anonymous'): - storage = getstorage() - document = Document(storage, name=self.cleaned_data['name'], text=self.cleaned_data['text']) - storage.put(document, - author=document_author, - comment=self.cleaned_data['comment'], - parent=self.cleaned_data['revision']) +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) - return storage.get(self.cleaned_data['name']) + 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.") -class DocumentTagForm(forms.Form): + if not self.cleaned_data["text"]: + raise forms.ValidationError("You must either enter text or upload a file") - id = forms.CharField(widget=forms.HiddenInput) - tag = forms.ChoiceField(choices=DOCUMENT_TAGS) - revision = forms.IntegerField(widget=forms.HiddenInput) + return self.cleaned_data class DocumentTextSaveForm(forms.Form): - """ + """if Form for saving document's text: * name - document's storage identifier. diff --git a/apps/wiki/helpers.py b/apps/wiki/helpers.py index 553b1e44..9b326d54 100644 --- a/apps/wiki/helpers.py +++ b/apps/wiki/helpers.py @@ -24,9 +24,9 @@ class JSONResponse(http.HttpResponse): # get rid of mimetype kwargs.pop('mimetype', None) - super(JSONResponse, self).__init__( - json.dumps(data, cls=ExtendedEncoder), - mimetype="application/json", **kwargs) + data = json.dumps(data, cls=ExtendedEncoder) + print data + super(JSONResponse, self).__init__(data, mimetype="application/json", **kwargs) # return errors diff --git a/apps/wiki/models.py b/apps/wiki/models.py index 49bd06c1..ad15c5ee 100644 --- a/apps/wiki/models.py +++ b/apps/wiki/models.py @@ -20,11 +20,12 @@ class DocumentStorage(object): self.vstorage = vstorage.VersionedStorage(path) def get(self, name, revision=None): - if revision is None: - text = self.vstorage.page_text(name) - else: - text = self.vstorage.revision_text(name, revision) - return Document(self, name=name, text=text) + text, rev = self.vstorage.page_text(name, revision) + return Document(self, name=name, text=text, revision=rev) + + def get_by_tag(self, name, tag): + text, rev = self.vstorage.page_text(name, tag) + return Document(self, name=name, text=text, revision=rev) def get_or_404(self, *args, **kwargs): try: @@ -40,6 +41,18 @@ class DocumentStorage(object): comment=comment, parent=parent) + return document + + def create_document(self, id, text, title=None): + if title is None: + title = id.title() + + if text is None: + text = u'' + + document = Document(self, name=id, text=text, title=title) + return self.put(document, u"", u"Document created.", None) + def delete(self, name, author, comment): self.vstorage.delete_page(name, author, comment) @@ -61,12 +74,6 @@ class Document(object): for attr, value in kwargs.iteritems(): setattr(self, attr, value) - def revision(self): - try: - return self.storage._info(self.name)[0] - except DocumentNotFound: - return - 1 - def add_tag(self, tag, revision, author): """ Add document specific tag """ logger.debug("Adding tag %s to doc %s version %d", tag, self.name, revision) diff --git a/apps/wiki/nice_diff.py b/apps/wiki/nice_diff.py index b228fad9..355600b5 100755 --- a/apps/wiki/nice_diff.py +++ b/apps/wiki/nice_diff.py @@ -17,15 +17,12 @@ NAMES = {'+': 'added', '-': 'removed', '^': 'changed'} def diff_replace(match): return """""" % NAMES[match.group(1)] - def filter_line(line): return DIFF_RE.sub(diff_replace, html_escape(line)).replace('\x01', '') - 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) diff --git a/apps/wiki/templates/wiki/base.html b/apps/wiki/templates/wiki/base.html new file mode 100644 index 00000000..5680e0df --- /dev/null +++ b/apps/wiki/templates/wiki/base.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load compressed %} + +{% block title %}{{ document_name }} - {{ block.super }}{% endblock %} + +{% block extrahead %} +{% compressed_css 'listing' %} +{% endblock %} + +{% block extrabody %} +{% compressed_js 'listing' %} +{% endblock %} + +{% block maincontent %} +

Platforma Redakcyjna

+
+ {% block leftcolumn %} + {% endblock leftcolumn %} +
+
+ {% block rightcolumn %} + {% endblock rightcolumn %} +
+{% endblock maincontent %} \ No newline at end of file diff --git a/apps/wiki/templates/wiki/document_create_missing.html b/apps/wiki/templates/wiki/document_create_missing.html new file mode 100644 index 00000000..a230133e --- /dev/null +++ b/apps/wiki/templates/wiki/document_create_missing.html @@ -0,0 +1,13 @@ +{% extends "wiki/base.html" %} + +{% block leftcolumn %} +
+ {{ form.as_p }} + +

+
+{% endblock leftcolumn %} + +{% block rightcolumn %} + +{% endblock rightcolumn %} \ No newline at end of file diff --git a/apps/wiki/templates/wiki/document_list.html b/apps/wiki/templates/wiki/document_list.html index a47609ae..2ab5ab9c 100644 --- a/apps/wiki/templates/wiki/document_list.html +++ b/apps/wiki/templates/wiki/document_list.html @@ -1,12 +1,8 @@ -{% extends "base.html" %} -{% load compressed %} +{% extends "wiki/base.html" %} -{% block extrahead %} -{% compressed_css 'listing' %} -{% endblock extrahead %} {% block extrabody %} -{% compressed_js 'listing' %} +{{ block.super }} {% endblock %} -{% block maincontent %} -

Platforma Redakcyjna

- - -
+{% block leftcolumn %}
@@ -46,7 +38,9 @@ $(function() {
+{% endblock leftcolumn %} +{% block rightcolumn %}

Twoje ostatnio otwierane dokumenty:

    @@ -56,7 +50,4 @@ $(function() { {% endfor %}
-
- - -{% endblock maincontent %} +{% endblock rightcolumn %} diff --git a/apps/wiki/templates/wiki/history_view.html b/apps/wiki/templates/wiki/history_view.html index 58b54156..66271713 100644 --- a/apps/wiki/templates/wiki/history_view.html +++ b/apps/wiki/templates/wiki/history_view.html @@ -1,26 +1,26 @@
- + - + - + - -

,
+
+
diff --git a/apps/wiki/templates/wiki/save_dialog.html b/apps/wiki/templates/wiki/save_dialog.html index 30039917..aba61cca 100644 --- a/apps/wiki/templates/wiki/save_dialog.html +++ b/apps/wiki/templates/wiki/save_dialog.html @@ -1,40 +1,40 @@
-
+

{{ forms.text_save.comment.label }}

{{ forms.text_save.comment.help_text}}

{{forms.text_save.comment }} - - - + + + {% if request.user.is_anonymous %}

- {{ forms.text_save.author.label }}: + {{ forms.text_save.author.label }}: {{ forms.text_save.author }} {{ forms.text_save.author.help_text }}

{% else %}

- {{ forms.text_save.stage_completed.label }}: + {{ forms.text_save.stage_completed.label }}: {{ forms.text_save.stage_completed }} {{ forms.text_save.stage_completed.help_text }}

{% endif %} - - + + {% for f in forms.text_save.hidden_fields %} {{ f }} {% endfor %} - +

- +

-

-
+

+
diff --git a/apps/wiki/templates/wiki/summary_view.html b/apps/wiki/templates/wiki/summary_view.html index 2e83cea8..5e1f82a1 100644 --- a/apps/wiki/templates/wiki/summary_view.html +++ b/apps/wiki/templates/wiki/summary_view.html @@ -3,7 +3,7 @@ -->
-
+

{{ document_meta.gallery}}

- - + +

\ No newline at end of file diff --git a/apps/wiki/templates/wiki/tag_dialog.html b/apps/wiki/templates/wiki/tag_dialog.html index a79a6165..7212e36b 100644 --- a/apps/wiki/templates/wiki/tag_dialog.html +++ b/apps/wiki/templates/wiki/tag_dialog.html @@ -1,19 +1,19 @@
{% for field in forms.add_tag.visible_fields %} -

{{ field.label_tag }} {{ field }}

-

{{ field.help_text }}

+

{{ field.label_tag }} {{ field }}

+

{{ field.help_text }}

{% endfor %} - - - {% for f in forms.add_tag.hidden_fields %} + + + {% for f in forms.text_save.hidden_fields %} {{ f }} {% endfor %} -

- +

+

-

+

diff --git a/apps/wiki/urls.py b/apps/wiki/urls.py index fae11efa..d649ba5a 100644 --- a/apps/wiki/urls.py +++ b/apps/wiki/urls.py @@ -4,6 +4,8 @@ from django.conf import settings urlpatterns = patterns('wiki.views', url(r'^$', 'document_list', name='wiki_doclist'), + url(r'^create/(?P[^/]+)', + 'document_create_missing', name='wiki_create_missing'), url(r'^gallery/(?P[^/]+)$', 'document_gallery', name="wiki_gallery"), url(r'^(?P[^/]+)/history$', @@ -16,6 +18,8 @@ urlpatterns = patterns('wiki.views', 'document_diff', name="wiki_diff"), url(r'^(?P[^/]+)/tags$', 'document_add_tag', name="wiki_add_tag"), + url(r'^(?P[^/]+)/tags$', + 'document_publish'), url(r'^(?P[^/]+)$', 'document_detail', name="wiki_details"), ) diff --git a/apps/wiki/views.py b/apps/wiki/views.py index dbc05df5..e466d6a2 100644 --- a/apps/wiki/views.py +++ b/apps/wiki/views.py @@ -4,11 +4,12 @@ from django.conf import settings from django.views.generic.simple import direct_to_template from django.views.decorators.http import require_POST +from django.core.urlresolvers import reverse from wiki.helpers import JSONResponse, JSONFormInvalid, JSONServerError, ajax_require_permission from django import http -from wiki.models import getstorage -from wiki.forms import DocumentTextSaveForm, DocumentTagForm +from wiki.models import getstorage, DocumentNotFound +from wiki.forms import DocumentTextSaveForm, DocumentTagForm, DocumentCreateForm from datetime import datetime from django.utils.encoding import smart_unicode from django.utils.translation import ugettext_lazy as _ @@ -42,7 +43,10 @@ def document_list(request, template_name='wiki/document_list.html'): @never_cache def document_detail(request, name, template_name='wiki/document_details.html'): - document = getstorage().get_or_404(name) + try: + document = getstorage().get(name) + except DocumentNotFound: + return http.HttpResponseRedirect(reverse("wiki_create_missing", args=[name])) access_time = datetime.now() last_documents = request.session.get("wiki_last_docs", {}) @@ -55,22 +59,51 @@ def document_detail(request, name, template_name='wiki/document_details.html'): return direct_to_template(request, template_name, extra_context={ 'document': document, + 'document_name': document.name, 'document_info': document.info, 'document_meta': document.meta, - 'forms': {"text_save": DocumentTextSaveForm(), "add_tag": DocumentTagForm()}, + 'forms': { + "text_save": DocumentTextSaveForm(prefix="textsave"), + "add_tag": DocumentTagForm(prefix="addtag") + }, + }) + + +def document_create_missing(request, name): + storage = getstorage() + + if request.method == "POST": + form = DocumentCreateForm(request.POST, request.FILES) + if form.is_valid(): + doc = storage.create_document( + id=form.cleaned_data['id'], + text=form.cleaned_data['text'], + ) + + return http.HttpResponseRedirect(reverse("wiki_details", args=[doc.name])) + else: + form = DocumentCreateForm(initial={ + "id": name.replace(" ", "_"), + "title": name.title(), + }) + + return direct_to_template(request, "wiki/document_create_missing.html", extra_context={ + "document_name": name, + "form": form, }) @never_cache def document_text(request, name): storage = getstorage() - document = storage.get_or_404(name) if request.method == 'POST': - form = DocumentTextSaveForm(request.POST) + form = DocumentTextSaveForm(request.POST, prefix="textsave") if form.is_valid(): revision = form.cleaned_data['parent_revision'] + + document = storage.get_or_404(name, revision) document.text = form.cleaned_data['text'] storage.put(document, @@ -79,18 +112,34 @@ def document_text(request, name): parent=revision, ) + document = storage.get(name) + return JSONResponse({ - 'text': document.plain_text if revision != document.revision() else None, + 'text': document.plain_text if revision != document.revision else None, 'meta': document.meta(), - 'revision': document.revision(), + 'revision': document.revision, }) else: return JSONFormInvalid(form) else: + revision = request.GET.get("revision", None) + + try: + try: + revision = revision and int(revision) + logger.info("Fetching %s", revision) + document = storage.get(name, revision) + except ValueError: + # treat as a tag + logger.info("Fetching tag %s", revision) + document = storage.get_by_tag(name, revision) + except DocumentNotFound: + raise http.Http404 + return JSONResponse({ 'text': document.plain_text, 'meta': document.meta(), - 'revision': document.revision(), + 'revision': document.revision, }) @@ -116,9 +165,8 @@ def document_gallery(request, directory): images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)] images.sort() return JSONResponse(images) - except (IndexError, OSError), exc: - import traceback - traceback.print_exc() + except (IndexError, OSError) as e: + logger.exception("Unable to fetch gallery") raise http.Http404 @@ -147,7 +195,7 @@ def document_history(request, name): storage = getstorage() # TODO: pagination - changesets = storage.history(name) + changesets = list(storage.history(name)) return JSONResponse(changesets) @@ -157,7 +205,7 @@ def document_history(request, name): def document_add_tag(request, name): storage = getstorage() - form = DocumentTagForm(request.POST) + form = DocumentTagForm(request.POST, prefix="addtag") if form.is_valid(): doc = storage.get_or_404(form.cleaned_data['id']) doc.add_tag(tag=form.cleaned_data['tag'], @@ -170,13 +218,12 @@ def document_add_tag(request, name): @require_POST @ajax_require_permission('wiki.can_publish') -def document_publish(request, name, version): +def document_publish(request, name): storage = getstorage() + document = storage.get_by_tag(name, "ready_to_publish") - # get the document - document = storage.get_or_404(name, revision=int(version)) + api = wlapi.WLAPI(**settings.WL_API_CONFIG) - api = wlapi.WLAPI(settings.WL_API_CONFIG) try: return JSONResponse({"result": api.publish_book(document)}) except wlapi.APICallException, e: diff --git a/lib/test_wlapi.py b/lib/test_wlapi.py new file mode 100644 index 00000000..a12ab068 --- /dev/null +++ b/lib/test_wlapi.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# -*- 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 nose.tools import * +from nose.core import runmodule + +import wlapi + + +class FakeDocument(): + + def __init__(self): + self.text = "Some Text" + + +class TestWLAPI(object): + + def setUp(self): + self.api = wlapi.WLAPI( + URL="http://localhost:7000/api/", + AUTH_REALM="WL API", + AUTH_USER="platforma", + AUTH_PASSWD="platforma", + ) + + def test_basic_call(self): + assert_equal(self.api.list_books(), []) + + def test_publish_book(self): + self.api.publish_book(FakeDocument()) + +if __name__ == '__main__': + runmodule() diff --git a/lib/vstorage.py b/lib/vstorage.py index f16496de..4cfd59aa 100644 --- a/lib/vstorage.py +++ b/lib/vstorage.py @@ -197,10 +197,12 @@ class VersionedStorage(object): @with_working_copy_locked @with_storage_locked - def save_file(self, title, file_name, author=u'', comment=u'', parent=None): + def save_file(self, title, file_name, **kwargs): """Save an existing file as specified page.""" - user = author.encode('utf-8') or u'anonymous'.encode('utf-8') - text = comment.encode('utf-8') or u'comment'.encode('utf-8') + + author = kwargs.get('author', u'anonymous').encode('utf-8') + comment = kwargs.get('comment', u'Empty comment.').encode('utf-8') + parent = kwargs.get('parent', None) repo_file = self._title_to_file(title) file_path = self._file_path(title) @@ -215,11 +217,11 @@ class VersionedStorage(object): current_page_rev = -1 if parent is not None and current_page_rev != parent: - msg = self.merge_changes(changectx, repo_file, text, user, parent) - user = '' - text = msg.encode('utf-8') + msg = self.merge_changes(changectx, repo_file, comment, author, parent) + author = '' + comment = msg.encode('utf-8') - self._commit([repo_file], text, user) + self._commit([repo_file], comment, author) def save_data(self, title, data, **kwargs): """Save data as specified page.""" @@ -229,7 +231,8 @@ class VersionedStorage(object): f = open(file_path, "wb") f.write(data) f.close() - self.save_file(title=title, file_name=file_path, **kwargs) + + return self.save_file(title=title, file_name=file_path, **kwargs) finally: try: os.unlink(file_path) @@ -240,23 +243,14 @@ class VersionedStorage(object): except OSError: pass - def save_text(self, text, **kwargs): + def save_text(self, **kwargs): """Save text as specified page, encoded to charset.""" - self.save_data(data=text.encode(self.charset), **kwargs) + text = kwargs.pop('text') + return self.save_data(data=text.encode(self.charset), **kwargs) - def _commit(self, files, text, user): + def _commit(self, files, comment, user): match = mercurial.match.exact(self.repo_path, '', list(files)) - return self.repo.commit(match=match, text=text, user=user, force=True) - - def page_text(self, title): - """Read unicode text of a page.""" - data = self.open_page(title).read() - text = unicode(data, self.charset, 'replace') - return text - - def page_lines(self, page): - for data in page: - yield unicode(data, self.charset, 'replace') + return self.repo.commit(match=match, text=comment, user=user, force=True) @with_working_copy_locked @with_storage_locked @@ -272,25 +266,41 @@ class VersionedStorage(object): self.repo.remove([repo_file]) self._commit([repo_file], text, user) - @with_working_copy_locked - def open_page(self, title): - if title not in self: - raise DocumentNotFound() +# @with_working_copy_locked +# def _open_page(self, title): +# if title not in self: +# raise DocumentNotFound() +# +# path = self._title_to_file(title) +# logger.debug("Opening page %s", path) +# try: +# return self.repo.wfile(path, 'rb') +# except IOError: +# logger.exception("Failed to open page %s", title) +# raise DocumentNotFound() + + def page_text(self, title, revision=None): + """Read unicode text of a page.""" + ctx = self._find_filectx(title, revision) + return ctx.data().decode(self.charset, 'replace'), ctx.filerev() + + def page_text_by_tag(self, title, tag): + """Read unicode text of a taged page.""" + tag = u"{title}#{tag}".format(**locals()).encode('utf-8') + fname = self._title_to_file(title) - path = self._title_to_file(title) - logger.debug("Opening page %s", path) try: - return self.repo.wfile(path, 'rb') - except IOError: - logger.exception("Failed to open page %s", title) + ctx = self.repo[tag][fname] + return ctx.data().decode(self.charset, 'replace'), ctx.filerev() + except IndexError: raise DocumentNotFound() @with_working_copy_locked def page_file_meta(self, title): """Get page's inode number, size and last modification time.""" try: - (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, - st_atime, st_mtime, st_ctime) = os.stat(self._file_path(title)) + (_st_mode, st_ino, _st_dev, _st_nlink, _st_uid, _st_gid, st_size, + _st_atime, st_mtime, _st_ctime) = os.stat(self._file_path(title)) except OSError: return 0, 0, 0 return st_ino, st_size, st_mtime @@ -307,9 +317,8 @@ class VersionedStorage(object): rev = filectx_tip.filerev() filectx = filectx_tip.filectx(rev) date = datetime.datetime.fromtimestamp(filectx.date()[0]) - author = unicode(filectx.user(), "utf-8", - 'replace').split('<')[0].strip() - comment = unicode(filectx.description(), "utf-8", 'replace') + author = filectx.user().decode("utf-8", 'replace') + comment = filectx.description().decode("utf-8", 'replace') return rev, date, author, comment def repo_revision(self): @@ -329,11 +338,14 @@ class VersionedStorage(object): """Find the last revision in which the file existed.""" repo_file = self._title_to_file(title) - changectx = self._changectx() + + changectx = self._changectx() # start with tip stack = [changectx] + while repo_file not in changectx: if not stack: return None + changectx = stack.pop() for parent in changectx.parents(): if parent != changectx: @@ -341,8 +353,13 @@ class VersionedStorage(object): try: fctx = changectx[repo_file] - return fctx if rev is None else fctx.filectx(rev) - except IndexError, LookupError: + + if rev is not None: + fctx = fctx.filectx(rev) + fctx.filerev() + + return fctx + except (IndexError, LookupError) as e: raise DocumentNotFound() def page_history(self, title): @@ -355,9 +372,8 @@ class VersionedStorage(object): for rev in range(maxrev, minrev - 1, -1): filectx = filectx_tip.filectx(rev) date = datetime.datetime.fromtimestamp(filectx.date()[0]) - author = unicode(filectx.user(), "utf-8", - 'replace').split('<')[0].strip() - comment = unicode(filectx.description(), "utf-8", 'replace') + author = filectx.user().decode('utf-8', 'replace') + comment = filectx.description().decode("utf-8", 'replace') tags = [t.rsplit('#', 1)[-1] for t in filectx.changectx().tags() if '#' in t] yield { @@ -368,21 +384,12 @@ class VersionedStorage(object): "tag": tags, } - def page_revision(self, title, rev): - """Get unicode contents of specified revision of the page.""" - return self._find_filectx(title, rev).data() - - def revision_text(self, title, rev): - data = self.page_revision(title, rev) - text = unicode(data, self.charset, 'replace') - return text - @with_working_copy_locked def add_page_tag(self, title, rev, tag, user, doctag=True): if doctag: - tag = "{title}#{tag}".format(**locals()) + tag = u"{title}#{tag}".format(**locals()).encode('utf-8') - message = "Assigned tag {tag} to version {rev} of {title}".format(**locals()) + message = u"Assigned tag {tag!r} to version {rev!r} of {title!r}".format(**locals()).encode('utf-8') fctx = self._find_filectx(title, rev) self.repo.tag( @@ -399,9 +406,8 @@ class VersionedStorage(object): for wiki_rev in range(maxrev, minrev - 1, -1): change = self.repo.changectx(wiki_rev) date = datetime.datetime.fromtimestamp(change.date()[0]) - author = unicode(change.user(), "utf-8", - 'replace').split('<')[0].strip() - comment = unicode(change.description(), "utf-8", 'replace') + author = change.user().decode('utf-8', 'replace') + comment = change.description().decode("utf-8", 'replace') for repo_file in change.files(): if repo_file.startswith(self.repo_prefix): title = self._file_to_title(repo_file) diff --git a/lib/wlapi.py b/lib/wlapi.py index ba1be146..0a674c8c 100644 --- a/lib/wlapi.py +++ b/lib/wlapi.py @@ -14,7 +14,16 @@ logger = logging.getLogger("fnp.lib.wlapi") class APICallException(Exception): - pass + + def __init__(self, cause=None): + super(Exception, self).__init__() + self.cause = cause + + def __unicode__(self): + return u"%s, cause: %s" % (type(self).__name__, repr(self.cause)) + + def __str__(self): + return self.__unicode__().encode('utf-8') def api_call(path, format="json"): @@ -29,7 +38,7 @@ def api_call(path, format="json"): # prepare request rq = urllib2.Request(self.base_url + path + ".json") - # will send POST when there is data, GET otherwise + # will send POST when there is data, GET otherwise if data is not None: rq.add_data(json.dumps(data)) rq.add_header("Content-Type", "application/json") @@ -51,23 +60,34 @@ def api_call(path, format="json"): class WLAPI(object): - def __init__(self, config_dict): + def __init__(self, **config_dict): self.base_url = config_dict['URL'] self.auth_realm = config_dict['AUTH_REALM'] self.auth_user = config_dict['AUTH_USER'] - auth_handler = urllib2.HTTPDigestAuthHandler() - auth_handler.add_password( + digest_handler = urllib2.HTTPDigestAuthHandler() + digest_handler.add_password( + realm=self.auth_realm, uri=self.base_url, + user=self.auth_user, passwd=config_dict['AUTH_PASSWD']) + + basic_handler = urllib2.HTTPBasicAuthHandler() + basic_handler.add_password( realm=self.auth_realm, uri=self.base_url, user=self.auth_user, passwd=config_dict['AUTH_PASSWD']) - self.opener = urllib2.build_opener(auth_handler) + self.opener = urllib2.build_opener(digest_handler, basic_handler) def _http_error(self, error): - return self._error() + message = error.read() + logger.debug("HTTP ERROR: %s", message) + return self._error(message) def _error(self, error): - raise APICallException(cause=error) + raise APICallException(error) + + @api_call("books") + def list_books(self): + yield @api_call("books") def publish_book(self, document): diff --git a/platforma/compress_settings.py b/platforma/compress_settings.py new file mode 100644 index 00000000..a2870014 --- /dev/null +++ b/platforma/compress_settings.py @@ -0,0 +1,72 @@ +# CSS and JS files to compress +COMPRESS_CSS = { + 'detail': { + 'source_filenames': ( + 'css/master.css', + 'css/gallery.css', + 'css/history.css', + 'css/summary.css', + 'css/html.css', + 'css/jquery.autocomplete.css', + 'css/dialogs.css', + ), + 'output_filename': 'compressed/detail_styles_?.css', + }, + 'listing': { + 'source_filenames': ( + 'css/filelist.css', + ), + 'output_filename': 'compressed/listing_styles_?.css', + } +} + +COMPRESS_JS = { + # everything except codemirror + 'detail': { + 'source_filenames': ( + # libraries + 'js/jquery-1.4.2.min.js', + 'js/jquery.autocomplete.js', + 'js/jquery.blockui.js', + 'js/jquery.elastic.js', + 'js/button_scripts.js', + 'js/slugify.js', + + # wiki scripts + 'js/wiki/wikiapi.js', + 'js/wiki/xslt.js', + + # base UI + 'js/wiki/base.js', + + # dialogs + 'js/wiki/dialog_save.js', + + # views + 'js/wiki/view_history.js', + 'js/wiki/view_summary.js', + 'js/wiki/view_editor_source.js', + 'js/wiki/view_editor_wysiwyg.js', + 'js/wiki/view_gallery.js', + 'js/wiki/view_column_diff.js', + + # bootstrap + 'js/wiki/loader.js', + ), + 'output_filename': 'compressed/detail_scripts_?.js', + }, + 'listing': { + 'source_filenames': ( + 'js/jquery-1.4.2.min.js', + 'js/slugify.js', + ), + 'output_filename': 'compressed/listing_scripts_?.js', + } +} + +COMPRESS = True +COMPRESS_CSS_FILTERS = None +COMPRESS_JS_FILTERS = None +COMPRESS_AUTO = False +COMPRESS_VERSION = True +COMPRESS_VERSIONING = 'compress.versioning.hash.MD5Versioning' diff --git a/platforma/context_processors.py b/platforma/context_processors.py index 4ba9bb7f..db6c98b4 100644 --- a/platforma/context_processors.py +++ b/platforma/context_processors.py @@ -4,4 +4,5 @@ def settings(request): from django.conf import settings return {'MEDIA_URL': settings.MEDIA_URL, - 'STATIC_URL': settings.STATIC_URL} + 'STATIC_URL': settings.STATIC_URL, + 'REDMINE_URL': settings.REDMINE_URL} diff --git a/platforma/settings.py b/platforma/settings.py index e7eab7a2..4ec680fb 100644 --- a/platforma/settings.py +++ b/platforma/settings.py @@ -153,6 +153,14 @@ FILEBROWSER_DEFAULT_ORDER = "path_relative" # REPOSITORY_PATH = '/Users/zuber/Projekty/platforma/files/books' IMAGE_DIR = 'images' + +WL_API_CONFIG = { + "URL": "http://localhost:7000/api/", + "AUTH_REALM": "wlapi", + "AUTH_USER": "platforma", + "AUTH_PASSWD": "platforma", +} + # Import localsettings file, which may override settings defined here try: from localsettings import * diff --git a/platforma/static/css/summary.css b/platforma/static/css/summary.css index 9f8def02..8fe3e73d 100644 --- a/platforma/static/css/summary.css +++ b/platforma/static/css/summary.css @@ -9,13 +9,17 @@ #summary-view .book-cover { float: left; margin: 1em; + min-height: 300px; + min-width: 240px; + max-height: 300px; + max-width: 240px; } -#summary-view form p { +#summary-view p { margin: 0.5em; } -#summary-view form label { +#summary-view label { font-weight: bold; } diff --git a/platforma/static/js/wiki/view_history.js b/platforma/static/js/wiki/view_history.js index f7f2a287..b2fe0b00 100644 --- a/platforma/static/js/wiki/view_history.js +++ b/platforma/static/js/wiki/view_history.js @@ -71,7 +71,7 @@ var $stub = $('#history-view .row-stub'); changes_list.html(''); - var tags = $('select#id_tag option'); + var tags = $('select#id_addtag_tag option'); $.each(data, function(){ $.wiki.renderStub({ diff --git a/platforma/static/js/wiki/view_summary.js b/platforma/static/js/wiki/view_summary.js index f86d9754..736f4e33 100644 --- a/platforma/static/js/wiki/view_summary.js +++ b/platforma/static/js/wiki/view_summary.js @@ -2,7 +2,25 @@ function SummaryPerspective(options) { var old_callback = options.callback; + var self = this; + options.callback = function(){ + $('#publish_button').click(function() { + $.blockUI({message: "Oczekiwanie na odpowiedź serwera..."}); + self.doc.publish({ + success: function(doc, data) { + $.blockUI({message: "Udało się", timeout: 2000}); + }, + failure: function(doc, message) { + $.blockUI({ + message: message, + timeout: 5000 + }); + } + + }); + }); + old_callback.call(this); }; diff --git a/platforma/static/js/wiki/wikiapi.js b/platforma/static/js/wiki/wikiapi.js index ac4c5b11..a2883979 100644 --- a/platforma/static/js/wiki/wikiapi.js +++ b/platforma/static/js/wiki/wikiapi.js @@ -1,9 +1,11 @@ (function($) { - $.wikiapi = {}; - var noop = function() {}; - var noops = {success: noop, failure: noop}; - + 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) @@ -11,80 +13,82 @@ * TODO: think of a way, not to hard-code it here ;) * */ - function reverse() { + function reverse() { var vname = arguments[0]; - - if(vname == "ajax_document_text") { + var base_path = "/documents"; + + if (vname == "ajax_document_text") { var path = "/" + arguments[1] + "/text"; - if (arguments[2] !== undefined) + + if (arguments[2] !== undefined) path += "/" + arguments[2]; - return path; + + return base_path + path; } - + if (vname == "ajax_document_history") { - return "/" + arguments[1] + "/history"; + + return base_path + "/" + arguments[1] + "/history"; } - + if (vname == "ajax_document_gallery") { - return "/gallery/" + arguments[1]; + + return base_path + "/gallery/" + arguments[1]; } - - if(vname == "ajax_document_diff") - return "/" + arguments[1] + "/diff"; - - if(vname == "ajax_document_addtag") - return "/" + arguments[1] + "/tags"; - - console.log("Couldn't reverse match:", vname); + + if (vname == "ajax_document_diff") + return base_path + "/" + arguments[1] + "/diff"; + + 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); - + var meta = $('#' + element_id); this.id = meta.attr('data-document-name'); this.revision = $("*[data-key='revision']", 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), dataType: 'json', - success: function(data) - { + success: function(data) { var changed = false; - - if (self.text === null || self.revision !== data.revision) { + +if (self.text === null || self.revision !== data.revision) { self.text = data.text; self.revision = data.revision; self.gallery = data.gallery; changed = true; self.triggerDocumentChanged(); }; - + self.has_local_changes = false; params['success'](self, changed); }, @@ -93,7 +97,6 @@ } }); }; - /* * Fetch history of this document. * @@ -105,12 +108,14 @@ /* this doesn't modify anything, so no locks */ params = $.extend({}, noops, params); var self = this; - $.ajax({ method: "GET", url: reverse("ajax_document_history", self.id), dataType: 'json', - data: {"from": params['from'], "upto": params['upto']}, + data: { + "from": params['from'], + "upto": params['upto'] + }, success: function(data) { params['success'](self, data); }, @@ -119,21 +124,21 @@ } }); }; - WikiDocument.prototype.fetchDiff = function(params) { /* this doesn't modify anything, so no locks */ var self = this; - params = $.extend({ 'from': self.revision, 'to': self.revision }, noops, params); - $.ajax({ method: "GET", url: reverse("ajax_document_diff", self.id), dataType: 'html', - data: {"from": params['from'], "to": params['to']}, + data: { + "from": params['from'], + "to": params['to'] + }, success: function(data) { params['success'](self, data); }, @@ -142,14 +147,12 @@ } }); }; - /* * Fetch gallery */ WikiDocument.prototype.refreshGallery = function(params) { params = $.extend({}, noops, params); var self = this; - $.ajax({ method: "GET", url: reverse("ajax_document_gallery", self.galleryLink), @@ -165,7 +168,6 @@ } }); }; - /* * Set document's text */ @@ -173,7 +175,6 @@ this.text = text; this.has_local_changes = true; }; - /* * Set document's gallery link */ @@ -181,82 +182,105 @@ this.galleryLink = gallery; this.has_local_changes = true; }; - /* * Save text back to the server */ - WikiDocument.prototype.save = function(params){ + WikiDocument.prototype.save = function(params) { params = $.extend({}, noops, params); var self = this; - + if (!self.has_local_changes) { - console.log("Abort: no 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; }); - var metaComment = '\n' - - data.text = metaComment + self.text; - data.comment = data.comment; - + + data['textsave-text'] = metaComment + self.text; + $.ajax({ url: reverse("ajax_document_text", self.id), type: "POST", dataType: "json", data: data, - success: function(data){ + success: function(data) { var changed = false; + if (data.text) { self.text = data.text; self.revision = data.revision; self.gallery = data.gallery; changed = true; self.triggerDocumentChanged(); - } - params['success'](self, changed, - ((changed && "Udało się zapisać :)") || "Twoja wersja i serwera jest identyczna") ); + }; + + params['success'](self, changed, ((changed && "Udało się zapisać :)") || "Twoja wersja i serwera jest identyczna")); }, error: function(xhr) { try { params['failure'](self, $.parseJSON(xhr.responseText)); - } - catch(e) { - params['failure'](self, {"__message": "

Nie udało się zapisać - błąd serwera.

"}); + } + catch (e) { + params['failure'](self, { + "__message": "

Nie udało się zapisać - błąd serwera.

" + }); }; + } }); }; /* 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 = { "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){ + success: function(data) { params.success(self, data.message); }, - error: function(xhr) { + error: function(xhr) { if (xhr.status == 403 || xhr.status == 401) { params.failure(self, { "__all__": ["Nie masz uprawnień lub nie jesteś zalogowany."] @@ -265,17 +289,17 @@ else { try { params.failure(self, $.parseJSON(xhr.responseText)); - } + } catch (e) { params.failure(self, { "__all__": ["Nie udało się - błąd serwera."] }); }; + }; + } }); }; - $.wikiapi.WikiDocument = WikiDocument; - })(jQuery); diff --git a/platforma/urls.py b/platforma/urls.py index 2e50e4b0..3c2b6c8a 100644 --- a/platforma/urls.py +++ b/platforma/urls.py @@ -23,5 +23,7 @@ urlpatterns = patterns('', {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), url(r'^%s(?P.+)$' % settings.STATIC_URL[1:], 'django.views.static.serve', {'document_root': settings.STATIC_ROOT, 'show_indexes': True}), - url(r'^', include('wiki.urls')), + + url(r'^$', 'redirect_to', {'url': '/documents/'}), + url(r'^documents/', include('wiki.urls')), ) -- 2.20.1