From: Łukasz Rekucki Date: Tue, 18 May 2010 08:55:52 +0000 (+0200) Subject: Cleanup for easier builds using hudson. X-Git-Url: https://git.mdrn.pl/redakcja.git/commitdiff_plain/b9055c7fc8caed025fd28c12532bd462edb8c78d Cleanup for easier builds using hudson. --- diff --git a/apps/toolbar/__init__.py b/apps/toolbar/__init__.py index e69de29b..c53f0e73 100644 --- a/apps/toolbar/__init__.py +++ b/apps/toolbar/__init__.py @@ -0,0 +1 @@ + # pragma: no cover diff --git a/apps/toolbar/migrations/__init__.py b/apps/toolbar/migrations/__init__.py index e69de29b..9012566c 100644 --- a/apps/toolbar/migrations/__init__.py +++ b/apps/toolbar/migrations/__init__.py @@ -0,0 +1 @@ +# pragma: no cover diff --git a/apps/wiki/__init__.py b/apps/wiki/__init__.py index e69de29b..c53f0e73 100644 --- a/apps/wiki/__init__.py +++ b/apps/wiki/__init__.py @@ -0,0 +1 @@ + # pragma: no cover diff --git a/apps/wiki/nice_diff.py b/apps/wiki/nice_diff.py old mode 100755 new mode 100644 diff --git a/apps/wiki/templatetags/__init__.py b/apps/wiki/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/wiki/templatetags/wiki.py b/apps/wiki/templatetags/wiki.py new file mode 100644 index 00000000..614858df --- /dev/null +++ b/apps/wiki/templatetags/wiki.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import + +from django.template.defaultfilters import stringfilter +from django import template + +register = template.Library() + +# from wiki.models import split_name + +@register.filter +@stringfilter +def wiki_title(value): + parts = (p.replace('_', ' ').title() for p in split_name(value)) + return ' / '.join(parts) diff --git a/hudson_build.sh b/hudson_build.sh new file mode 100644 index 00000000..c2b85df0 --- /dev/null +++ b/hudson_build.sh @@ -0,0 +1,6 @@ +cd $WORKSPACE +virtualenv -q ve +source ./ve/bin/activate +pip install -q -E ./ve -r requirements.txt +pip install -q -E ./ve -r requirements-test.txt +django-admin.py test --settings=redakcja.settings.test diff --git a/lib/test_vstorage.py b/lib/test_vstorage.py deleted file mode 100644 index 23375f06..00000000 --- a/lib/test_vstorage.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/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. -# - -import os -import tempfile -from nose.tools import * -from nose.core import runmodule - -import vstorage - -NULL_PARENT = -1 - - -def clear_directory(top): - for root, dirs, files in os.walk(top, topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - for name in dirs: - os.rmdir(os.path.join(root, name)) - try: - os.removedirs(top) - except OSError: - pass - - -class TestVersionedStorage(object): - def setUp(self): - self.repo_path = tempfile.mkdtemp() - self.repo = vstorage.VersionedStorage(self.repo_path) - - def tearDown(self): - clear_directory(self.repo_path) - - def test_save_text(self): - text = u"test text" - title = u"test title" - author = u"test author" - comment = u"test comment" - - self.repo.save_text( - title=title, - text=text, - author=author, - comment=comment, - parent=NULL_PARENT, - ) - - saved = self.repo.open_page(title).read() - assert_equal(saved, text) - - def test_save_text_noparent(self): - text = u"test text" - title = u"test title" - author = u"test author" - comment = u"test comment" - - self.repo.save_text(title=title, - text=text, author=author, - comment=comment, parent=None) - - saved = self.repo.open_page(title).read() - assert_equal(saved, text) - - def test_save_merge_no_conflict(self): - text = u"test\ntext" - title = u"test title" - author = u"test author" - comment = u"test comment" - self.repo.save_text(title=title, - text=text, author=author, - comment=comment, parent=NULL_PARENT) - self.repo.save_text(title=title, - text=text, author=author, - comment=comment, parent=NULL_PARENT) - saved = self.repo.open_page(title).read() - assert_equal(saved, text) - - def test_save_merge_line_conflict(self): - text = u"test\ntest\n" - text1 = u"test\ntext\n" - text2 = u"text\ntest\n" - title = u"test title" - author = u"test author" - comment = u"test comment" - - self.repo.save_text(title=title, - text=text, author=author, - comment=comment, parent=NULL_PARENT) - - self.repo.save_text(title=title, - text=text1, author=author, - comment=comment, parent=0) - - self.repo.save_text(title=title, - text=text2, author=author, - comment=comment, parent=0) - - saved = self.repo.open_page(title).read() - - # Other conflict markers placement can also be correct - assert_equal(saved, u'''\ -text -test -<<<<<<< local -======= -text ->>>>>>> other -''') - - def test_delete(self): - text = u"text test" - title = u"test title" - author = u"test author" - comment = u"test comment" - self.repo.save_text(title=title, - text=text, author=author, - comment=comment, parent=NULL_PARENT) - - assert title in self.repo - - self.repo.delete_page(title, author, comment) - - assert title not in self.repo - - @raises(vstorage.DocumentNotFound) - def test_document_not_found(self): - self.repo.open_page(u'unknown entity') - - def test_open_existing_repository(self): - self.repo.save_text(title=u'Python!', text=u'ham and spam') - current_repo_revision = self.repo.repo_revision() - same_repo = vstorage.VersionedStorage(self.repo_path) - assert_equal(same_repo.repo_revision(), current_repo_revision) - - def test_history(self): - COMMITS = [ - {"author": "bunny", "text":"1", "comment": "Oh yeah!"}, - {"author": "frank", "text":"2", "comment": "Second is the best!"}, - {"text":"3", "comment": "Third"}, - {"author": "welma", "text":"4", "comment": "Fourth"}, - ] - - for commit in COMMITS: - self.repo.save_text(title=u"Sample", **commit) - - for n, entry in enumerate(reversed(list(self.repo.page_history(u"Sample")))): - assert_equal(entry["version"], n) - assert_equal(entry["author"], COMMITS[n].get("author", "anonymous")) - assert_equal(entry["description"], COMMITS[n]["comment"]) - assert_equal(entry["tag"], []) - - -class TestVSTags(object): - - TITLE_1 = "Sample" - - COMMITS = [ - {"author": "bunny", "text":"1", "comment": "Oh yeah!"}, - {"author": "frank", "text":"2", "comment": "Second is the best!"}, - {"text":"3", "comment": "Third"}, - {"author": "welma", "text":"4", "comment": "Fourth"}, - ] - - def setUp(self): - self.repo_path = tempfile.mkdtemp() - self.repo = vstorage.VersionedStorage(self.repo_path) - - # generate some history - for commit in self.COMMITS: - self.repo.save_text(title=u"Sample", **commit) - - # verify - for n, entry in enumerate(reversed(list(self.repo.page_history(self.TITLE_1)))): - assert_equal(entry["tag"], []) - - def tearDown(self): - clear_directory(self.repo_path) - - def test_add_tag(self): - TAG_USER = "mike_the_tagger" - TAG_NAME = "production" - TAG_VERSION = 2 - - # Add tag - self.repo.add_page_tag(self.TITLE_1, TAG_VERSION, TAG_NAME, TAG_USER) - - # check history again - history = list(self.repo.page_history(self.TITLE_1)) - for entry in reversed(history): - if entry["version"] == TAG_VERSION: - assert_equal(entry["tag"], [TAG_NAME]) - else: - assert_equal(entry["tag"], []) - - def test_add_many_tags(self): - TAG_USER = "mike_the_tagger" - tags = [ - (2, "production", "mike"), - (2, "finished", "jeremy"), - (0, "original", "jeremy"), - ] - - for rev, name, user in tags: - self.repo.add_page_tag(self.TITLE_1, rev, name, user) - - # check history again - history = list(self.repo.page_history(self.TITLE_1)) - for entry in reversed(history): - expected = [tag[1] for tag in tags if tag[0] == entry["version"]] - assert_equal(set(entry["tag"]), set(expected)) - - -if __name__ == '__main__': - runmodule() diff --git a/lib/test_wlapi.py b/lib/test_wlapi.py deleted file mode 100644 index a12ab068..00000000 --- a/lib/test_wlapi.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/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 deleted file mode 100644 index b4acaf3a..00000000 --- a/lib/vstorage.py +++ /dev/null @@ -1,433 +0,0 @@ -# -*- 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 os -import tempfile -import datetime -import mimetypes -import urllib -import functools - -import logging -logger = logging.getLogger('fnp.hazlenut.vstorage') - -# Note: we have to set these before importing Mercurial -os.environ['HGENCODING'] = 'utf-8' -os.environ['HGMERGE'] = "internal:merge" - -import mercurial.hg -import mercurial.ui -import mercurial.revlog -import mercurial.util - - -def urlquote(url, safe='/'): - """Quotes URL - - >>> urlquote(u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144') - 'Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84' - """ - return urllib.quote(url.replace(' ', '_').encode('utf-8', 'ignore'), safe) - - -def urlunquote(url): - """Unqotes URL - - # >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84') - # u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144' - """ - return unicode(urllib.unquote(url), 'utf-8', 'ignore').replace('_', ' ') - - -def find_repo_path(path): - """Go up the directory tree looking for a Mercurial repository (a directory containing a .hg subdirectory).""" - while not os.path.isdir(os.path.join(path, ".hg")): - old_path, path = path, os.path.dirname(path) - if path == old_path: - return None - return path - - -def with_working_copy_locked(func): - """A decorator for locking the repository when calling a method.""" - - @functools.wraps(func) - def wrapped(self, *args, **kwargs): - """Wrap the original function in locks.""" - wlock = self.repo.wlock() - try: - return func(self, *args, **kwargs) - finally: - wlock.release() - return wrapped - - -def with_storage_locked(func): - """A decorator for locking the repository when calling a method.""" - - @functools.wraps(func) - def wrapped(self, *args, **kwargs): - """Wrap the original function in locks.""" - lock = self.repo.lock() - try: - return func(self, *args, **kwargs) - finally: - lock.release() - return wrapped - - -def guess_mime(file_name): - """ - Guess file's mime type based on extension. - Default of text/x-wiki for files without an extension. - - >>> guess_mime('something.txt') - 'text/plain' - >>> guess_mime('SomePage') - 'text/x-wiki' - >>> guess_mime(u'ąęśUnicodePage') - 'text/x-wiki' - >>> guess_mime('image.png') - 'image/png' - >>> guess_mime('style.css') - 'text/css' - >>> guess_mime('archive.tar.gz') - 'archive/gzip' - """ - - mime, encoding = mimetypes.guess_type(file_name, strict=False) - if encoding: - mime = 'archive/%s' % encoding - if mime is None: - mime = 'text/x-wiki' - return mime - - -class DocumentNotFound(Exception): - pass - - -class VersionedStorage(object): - """ - Provides means of storing text pages and keeping track of their - change history, using Mercurial repository as the storage method. - """ - - def __init__(self, path, charset=None): - """ - Takes the path to the directory where the pages are to be kept. - If the directory doen't exist, it will be created. If it's inside - a Mercurial repository, that repository will be used, otherwise - a new repository will be created in it. - """ - - self.charset = charset or 'utf-8' - self.path = path - if not os.path.exists(self.path): - os.makedirs(self.path) - self.repo_path = find_repo_path(self.path) - - self.ui = mercurial.ui.ui() - self.ui.quiet = True - self.ui._report_untrusted = False - self.ui.setconfig('ui', 'interactive', False) - - if self.repo_path is None: - self.repo_path = self.path - create = True - else: - create = False - - self.repo_prefix = self.path[len(self.repo_path):].strip('/') - self.repo = mercurial.hg.repository(self.ui, self.repo_path, - create=create) - - def reopen(self): - """Close and reopen the repo, to make sure we are up to date.""" - self.repo = mercurial.hg.repository(self.ui, self.repo_path) - - def _file_path(self, title): - return os.path.join(self.path, urlquote(title, safe='')) - - def _title_to_file(self, title): - return os.path.join(self.repo_prefix, urlquote(title, safe='')) - - def _file_to_title(self, filename): - assert filename.startswith(self.repo_prefix) - name = filename[len(self.repo_prefix):].strip('/') - return urlunquote(name) - - def __contains__(self, title): - return urlquote(title) in self.repo['tip'] - - def __iter__(self): - return self.all_pages() - - def merge_changes(self, changectx, repo_file, text, user, parent): - """Commits and merges conflicting changes in the repository.""" - tip_node = changectx.node() - filectx = changectx[repo_file].filectx(parent) - parent_node = filectx.changectx().node() - - self.repo.dirstate.setparents(parent_node) - node = self._commit([repo_file], text, user) - - partial = lambda filename: repo_file == filename - - # If p1 is equal to p2, there is no work to do. Even the dirstate is correct. - p1, p2 = self.repo[None].parents()[0], self.repo[tip_node] - if p1 == p2: - return text - - try: - mercurial.merge.update(self.repo, tip_node, True, False, partial) - msg = 'merge of edit conflict' - except mercurial.util.Abort: - msg = 'failed merge of edit conflict' - - self.repo.dirstate.setparents(tip_node, node) - # Mercurial 1.1 and later need updating the merge state - try: - mercurial.merge.mergestate(self.repo).mark(repo_file, "r") - except (AttributeError, KeyError): - pass - return msg - - @with_working_copy_locked - @with_storage_locked - def save_file(self, title, file_name, **kwargs): - """Save an existing file as specified page.""" - - 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) - mercurial.util.rename(file_name, file_path) - changectx = self._changectx() - - try: - filectx_tip = changectx[repo_file] - current_page_rev = filectx_tip.filerev() - except mercurial.revlog.LookupError: - self.repo.add([repo_file]) - current_page_rev = -1 - - if parent is not None and current_page_rev != parent: - msg = self.merge_changes(changectx, repo_file, comment, author, parent) - author = '' - comment = msg.encode('utf-8') - - self._commit([repo_file], comment, author) - - def save_data(self, title, data, **kwargs): - """Save data as specified page.""" - try: - temp_path = tempfile.mkdtemp(dir=self.path) - file_path = os.path.join(temp_path, 'saved') - f = open(file_path, "wb") - f.write(data) - f.close() - - return self.save_file(title=title, file_name=file_path, **kwargs) - finally: - try: - os.unlink(file_path) - except OSError: - pass - try: - os.rmdir(temp_path) - except OSError: - pass - - def save_text(self, **kwargs): - """Save text as specified page, encoded to charset.""" - text = kwargs.pop('text') - return self.save_data(data=text.encode(self.charset), **kwargs) - - def _commit(self, files, comment, user): - match = mercurial.match.exact(self.repo_path, '', list(files)) - return self.repo.commit(match=match, text=comment, user=user, force=True) - - @with_working_copy_locked - @with_storage_locked - def delete_page(self, title, author=u'', comment=u''): - user = author.encode('utf-8') or 'anon' - text = comment.encode('utf-8') or 'deleted' - repo_file = self._title_to_file(title) - file_path = self._file_path(title) - try: - os.unlink(file_path) - except OSError: - pass - self.repo.remove([repo_file]) - self._commit([repo_file], text, user) - - def page_text(self, title, revision=None): - """Read unicode text of a page.""" - ctx = self._find_filectx(title, revision) - - if ctx is None: - raise DocumentNotFound(title) - - return ctx.data().decode(self.charset, 'replace'), ctx.filerev() - - def page_text_by_tag(self, title, tag): - """Read unicode text of a taged page.""" - fname = self._title_to_file(title) - tag = u"{fname}#{tag}".format(**locals()).encode('utf-8') - - try: - ctx = self.repo[tag][fname] - return ctx.data().decode(self.charset, 'replace'), ctx.filerev() - except IndexError: - raise DocumentNotFound(fname) - - @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)) - except OSError: - return 0, 0, 0 - return st_ino, st_size, st_mtime - - @with_working_copy_locked - def page_meta(self, title, revision=None): - """Get page's revision, date, last editor and his edit comment.""" - fctx = self._find_filectx(title, revision) - - if fctx is None: - raise DocumentNotFound(title) - - return { - "revision": fctx.filerev(), - "date": datetime.datetime.fromtimestamp(fctx.date()[0]), - "author": fctx.user().decode("utf-8", 'replace'), - "comment": fctx.description().decode("utf-8", 'replace'), - } - - def repo_revision(self): - return self.repo['tip'].rev() - - def _changectx(self): - return self.repo['tip'] - - def page_mime(self, title): - """ - Guess page's mime type based on corresponding file name. - Default ot text/x-wiki for files without an extension. - """ - return guess_mime(self._file_path(title)) - - def _find_filectx(self, title, rev=None): - """Find the last revision in which the file existed.""" - repo_file = self._title_to_file(title) - changectx = self._changectx() # start with tip - visited = set() - - stack = [changectx] - visited.add(changectx) - - while repo_file not in changectx: - if not stack: - raise DocumentNotFound(title) - - changectx = stack.pop() - for parent in changectx.parents(): - if parent not in visited: - stack.append(parent) - visited.add(parent) - - try: - fctx = changectx[repo_file] - - if rev is not None: - fctx = fctx.filectx(rev) - fctx.filerev() - - return fctx - except (IndexError, LookupError) as e: - raise DocumentNotFound(title) - - def page_history(self, title): - """Iterate over the page's history.""" - - filectx_tip = self._find_filectx(title) - - maxrev = filectx_tip.filerev() - minrev = 0 - for rev in range(maxrev, minrev - 1, -1): - filectx = filectx_tip.filectx(rev) - date = datetime.datetime.fromtimestamp(filectx.date()[0]) - 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 { - "version": rev, - "date": date, - "author": author, - "description": comment, - "tag": tags, - } - - @with_working_copy_locked - def add_page_tag(self, title, rev, tag, user, doctag=True): - ctitle = self._title_to_file(title) - - if doctag: - tag = u"{ctitle}#{tag}".format(**locals()).encode('utf-8') - - message = u"Assigned tag {tag!r} to version {rev!r} of {ctitle!r}".format(**locals()).encode('utf-8') - - fctx = self._find_filectx(title, rev) - self.repo.tag( - names=tag, node=fctx.node(), local=False, - user=user, message=message, date=None, - ) - - def history(self): - """Iterate over the history of entire wiki.""" - - changectx = self._changectx() - maxrev = changectx.rev() - minrev = 0 - for wiki_rev in range(maxrev, minrev - 1, -1): - change = self.repo.changectx(wiki_rev) - date = datetime.datetime.fromtimestamp(change.date()[0]) - 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) - try: - rev = change[repo_file].filerev() - except mercurial.revlog.LookupError: - rev = -1 - yield title, rev, date, author, comment - - def all_pages(self): - tip = self.repo['tip'] - """Iterate over the titles of all pages in the wiki.""" - return [urlunquote(filename) for filename in tip] - - def changed_since(self, rev): - """Return all pages that changed since specified repository revision.""" - - try: - last = self.repo.lookup(int(rev)) - except IndexError: - for page in self.all_pages(): - yield page - return - current = self.repo.lookup('tip') - status = self.repo.status(current, last) - modified, added, removed, deleted, unknown, ignored, clean = status - for filename in modified + added + removed + deleted: - if filename.startswith(self.repo_prefix): - yield self._file_to_title(filename) diff --git a/lib/vstorage/__init__.py b/lib/vstorage/__init__.py new file mode 100644 index 00000000..b4acaf3a --- /dev/null +++ b/lib/vstorage/__init__.py @@ -0,0 +1,433 @@ +# -*- 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 os +import tempfile +import datetime +import mimetypes +import urllib +import functools + +import logging +logger = logging.getLogger('fnp.hazlenut.vstorage') + +# Note: we have to set these before importing Mercurial +os.environ['HGENCODING'] = 'utf-8' +os.environ['HGMERGE'] = "internal:merge" + +import mercurial.hg +import mercurial.ui +import mercurial.revlog +import mercurial.util + + +def urlquote(url, safe='/'): + """Quotes URL + + >>> urlquote(u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144') + 'Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84' + """ + return urllib.quote(url.replace(' ', '_').encode('utf-8', 'ignore'), safe) + + +def urlunquote(url): + """Unqotes URL + + # >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84') + # u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144' + """ + return unicode(urllib.unquote(url), 'utf-8', 'ignore').replace('_', ' ') + + +def find_repo_path(path): + """Go up the directory tree looking for a Mercurial repository (a directory containing a .hg subdirectory).""" + while not os.path.isdir(os.path.join(path, ".hg")): + old_path, path = path, os.path.dirname(path) + if path == old_path: + return None + return path + + +def with_working_copy_locked(func): + """A decorator for locking the repository when calling a method.""" + + @functools.wraps(func) + def wrapped(self, *args, **kwargs): + """Wrap the original function in locks.""" + wlock = self.repo.wlock() + try: + return func(self, *args, **kwargs) + finally: + wlock.release() + return wrapped + + +def with_storage_locked(func): + """A decorator for locking the repository when calling a method.""" + + @functools.wraps(func) + def wrapped(self, *args, **kwargs): + """Wrap the original function in locks.""" + lock = self.repo.lock() + try: + return func(self, *args, **kwargs) + finally: + lock.release() + return wrapped + + +def guess_mime(file_name): + """ + Guess file's mime type based on extension. + Default of text/x-wiki for files without an extension. + + >>> guess_mime('something.txt') + 'text/plain' + >>> guess_mime('SomePage') + 'text/x-wiki' + >>> guess_mime(u'ąęśUnicodePage') + 'text/x-wiki' + >>> guess_mime('image.png') + 'image/png' + >>> guess_mime('style.css') + 'text/css' + >>> guess_mime('archive.tar.gz') + 'archive/gzip' + """ + + mime, encoding = mimetypes.guess_type(file_name, strict=False) + if encoding: + mime = 'archive/%s' % encoding + if mime is None: + mime = 'text/x-wiki' + return mime + + +class DocumentNotFound(Exception): + pass + + +class VersionedStorage(object): + """ + Provides means of storing text pages and keeping track of their + change history, using Mercurial repository as the storage method. + """ + + def __init__(self, path, charset=None): + """ + Takes the path to the directory where the pages are to be kept. + If the directory doen't exist, it will be created. If it's inside + a Mercurial repository, that repository will be used, otherwise + a new repository will be created in it. + """ + + self.charset = charset or 'utf-8' + self.path = path + if not os.path.exists(self.path): + os.makedirs(self.path) + self.repo_path = find_repo_path(self.path) + + self.ui = mercurial.ui.ui() + self.ui.quiet = True + self.ui._report_untrusted = False + self.ui.setconfig('ui', 'interactive', False) + + if self.repo_path is None: + self.repo_path = self.path + create = True + else: + create = False + + self.repo_prefix = self.path[len(self.repo_path):].strip('/') + self.repo = mercurial.hg.repository(self.ui, self.repo_path, + create=create) + + def reopen(self): + """Close and reopen the repo, to make sure we are up to date.""" + self.repo = mercurial.hg.repository(self.ui, self.repo_path) + + def _file_path(self, title): + return os.path.join(self.path, urlquote(title, safe='')) + + def _title_to_file(self, title): + return os.path.join(self.repo_prefix, urlquote(title, safe='')) + + def _file_to_title(self, filename): + assert filename.startswith(self.repo_prefix) + name = filename[len(self.repo_prefix):].strip('/') + return urlunquote(name) + + def __contains__(self, title): + return urlquote(title) in self.repo['tip'] + + def __iter__(self): + return self.all_pages() + + def merge_changes(self, changectx, repo_file, text, user, parent): + """Commits and merges conflicting changes in the repository.""" + tip_node = changectx.node() + filectx = changectx[repo_file].filectx(parent) + parent_node = filectx.changectx().node() + + self.repo.dirstate.setparents(parent_node) + node = self._commit([repo_file], text, user) + + partial = lambda filename: repo_file == filename + + # If p1 is equal to p2, there is no work to do. Even the dirstate is correct. + p1, p2 = self.repo[None].parents()[0], self.repo[tip_node] + if p1 == p2: + return text + + try: + mercurial.merge.update(self.repo, tip_node, True, False, partial) + msg = 'merge of edit conflict' + except mercurial.util.Abort: + msg = 'failed merge of edit conflict' + + self.repo.dirstate.setparents(tip_node, node) + # Mercurial 1.1 and later need updating the merge state + try: + mercurial.merge.mergestate(self.repo).mark(repo_file, "r") + except (AttributeError, KeyError): + pass + return msg + + @with_working_copy_locked + @with_storage_locked + def save_file(self, title, file_name, **kwargs): + """Save an existing file as specified page.""" + + 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) + mercurial.util.rename(file_name, file_path) + changectx = self._changectx() + + try: + filectx_tip = changectx[repo_file] + current_page_rev = filectx_tip.filerev() + except mercurial.revlog.LookupError: + self.repo.add([repo_file]) + current_page_rev = -1 + + if parent is not None and current_page_rev != parent: + msg = self.merge_changes(changectx, repo_file, comment, author, parent) + author = '' + comment = msg.encode('utf-8') + + self._commit([repo_file], comment, author) + + def save_data(self, title, data, **kwargs): + """Save data as specified page.""" + try: + temp_path = tempfile.mkdtemp(dir=self.path) + file_path = os.path.join(temp_path, 'saved') + f = open(file_path, "wb") + f.write(data) + f.close() + + return self.save_file(title=title, file_name=file_path, **kwargs) + finally: + try: + os.unlink(file_path) + except OSError: + pass + try: + os.rmdir(temp_path) + except OSError: + pass + + def save_text(self, **kwargs): + """Save text as specified page, encoded to charset.""" + text = kwargs.pop('text') + return self.save_data(data=text.encode(self.charset), **kwargs) + + def _commit(self, files, comment, user): + match = mercurial.match.exact(self.repo_path, '', list(files)) + return self.repo.commit(match=match, text=comment, user=user, force=True) + + @with_working_copy_locked + @with_storage_locked + def delete_page(self, title, author=u'', comment=u''): + user = author.encode('utf-8') or 'anon' + text = comment.encode('utf-8') or 'deleted' + repo_file = self._title_to_file(title) + file_path = self._file_path(title) + try: + os.unlink(file_path) + except OSError: + pass + self.repo.remove([repo_file]) + self._commit([repo_file], text, user) + + def page_text(self, title, revision=None): + """Read unicode text of a page.""" + ctx = self._find_filectx(title, revision) + + if ctx is None: + raise DocumentNotFound(title) + + return ctx.data().decode(self.charset, 'replace'), ctx.filerev() + + def page_text_by_tag(self, title, tag): + """Read unicode text of a taged page.""" + fname = self._title_to_file(title) + tag = u"{fname}#{tag}".format(**locals()).encode('utf-8') + + try: + ctx = self.repo[tag][fname] + return ctx.data().decode(self.charset, 'replace'), ctx.filerev() + except IndexError: + raise DocumentNotFound(fname) + + @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)) + except OSError: + return 0, 0, 0 + return st_ino, st_size, st_mtime + + @with_working_copy_locked + def page_meta(self, title, revision=None): + """Get page's revision, date, last editor and his edit comment.""" + fctx = self._find_filectx(title, revision) + + if fctx is None: + raise DocumentNotFound(title) + + return { + "revision": fctx.filerev(), + "date": datetime.datetime.fromtimestamp(fctx.date()[0]), + "author": fctx.user().decode("utf-8", 'replace'), + "comment": fctx.description().decode("utf-8", 'replace'), + } + + def repo_revision(self): + return self.repo['tip'].rev() + + def _changectx(self): + return self.repo['tip'] + + def page_mime(self, title): + """ + Guess page's mime type based on corresponding file name. + Default ot text/x-wiki for files without an extension. + """ + return guess_mime(self._file_path(title)) + + def _find_filectx(self, title, rev=None): + """Find the last revision in which the file existed.""" + repo_file = self._title_to_file(title) + changectx = self._changectx() # start with tip + visited = set() + + stack = [changectx] + visited.add(changectx) + + while repo_file not in changectx: + if not stack: + raise DocumentNotFound(title) + + changectx = stack.pop() + for parent in changectx.parents(): + if parent not in visited: + stack.append(parent) + visited.add(parent) + + try: + fctx = changectx[repo_file] + + if rev is not None: + fctx = fctx.filectx(rev) + fctx.filerev() + + return fctx + except (IndexError, LookupError) as e: + raise DocumentNotFound(title) + + def page_history(self, title): + """Iterate over the page's history.""" + + filectx_tip = self._find_filectx(title) + + maxrev = filectx_tip.filerev() + minrev = 0 + for rev in range(maxrev, minrev - 1, -1): + filectx = filectx_tip.filectx(rev) + date = datetime.datetime.fromtimestamp(filectx.date()[0]) + 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 { + "version": rev, + "date": date, + "author": author, + "description": comment, + "tag": tags, + } + + @with_working_copy_locked + def add_page_tag(self, title, rev, tag, user, doctag=True): + ctitle = self._title_to_file(title) + + if doctag: + tag = u"{ctitle}#{tag}".format(**locals()).encode('utf-8') + + message = u"Assigned tag {tag!r} to version {rev!r} of {ctitle!r}".format(**locals()).encode('utf-8') + + fctx = self._find_filectx(title, rev) + self.repo.tag( + names=tag, node=fctx.node(), local=False, + user=user, message=message, date=None, + ) + + def history(self): + """Iterate over the history of entire wiki.""" + + changectx = self._changectx() + maxrev = changectx.rev() + minrev = 0 + for wiki_rev in range(maxrev, minrev - 1, -1): + change = self.repo.changectx(wiki_rev) + date = datetime.datetime.fromtimestamp(change.date()[0]) + 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) + try: + rev = change[repo_file].filerev() + except mercurial.revlog.LookupError: + rev = -1 + yield title, rev, date, author, comment + + def all_pages(self): + tip = self.repo['tip'] + """Iterate over the titles of all pages in the wiki.""" + return [urlunquote(filename) for filename in tip] + + def changed_since(self, rev): + """Return all pages that changed since specified repository revision.""" + + try: + last = self.repo.lookup(int(rev)) + except IndexError: + for page in self.all_pages(): + yield page + return + current = self.repo.lookup('tip') + status = self.repo.status(current, last) + modified, added, removed, deleted, unknown, ignored, clean = status + for filename in modified + added + removed + deleted: + if filename.startswith(self.repo_prefix): + yield self._file_to_title(filename) diff --git a/lib/vstorage/tests.py b/lib/vstorage/tests.py new file mode 100644 index 00000000..23375f06 --- /dev/null +++ b/lib/vstorage/tests.py @@ -0,0 +1,218 @@ +#!/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. +# + +import os +import tempfile +from nose.tools import * +from nose.core import runmodule + +import vstorage + +NULL_PARENT = -1 + + +def clear_directory(top): + for root, dirs, files in os.walk(top, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + try: + os.removedirs(top) + except OSError: + pass + + +class TestVersionedStorage(object): + def setUp(self): + self.repo_path = tempfile.mkdtemp() + self.repo = vstorage.VersionedStorage(self.repo_path) + + def tearDown(self): + clear_directory(self.repo_path) + + def test_save_text(self): + text = u"test text" + title = u"test title" + author = u"test author" + comment = u"test comment" + + self.repo.save_text( + title=title, + text=text, + author=author, + comment=comment, + parent=NULL_PARENT, + ) + + saved = self.repo.open_page(title).read() + assert_equal(saved, text) + + def test_save_text_noparent(self): + text = u"test text" + title = u"test title" + author = u"test author" + comment = u"test comment" + + self.repo.save_text(title=title, + text=text, author=author, + comment=comment, parent=None) + + saved = self.repo.open_page(title).read() + assert_equal(saved, text) + + def test_save_merge_no_conflict(self): + text = u"test\ntext" + title = u"test title" + author = u"test author" + comment = u"test comment" + self.repo.save_text(title=title, + text=text, author=author, + comment=comment, parent=NULL_PARENT) + self.repo.save_text(title=title, + text=text, author=author, + comment=comment, parent=NULL_PARENT) + saved = self.repo.open_page(title).read() + assert_equal(saved, text) + + def test_save_merge_line_conflict(self): + text = u"test\ntest\n" + text1 = u"test\ntext\n" + text2 = u"text\ntest\n" + title = u"test title" + author = u"test author" + comment = u"test comment" + + self.repo.save_text(title=title, + text=text, author=author, + comment=comment, parent=NULL_PARENT) + + self.repo.save_text(title=title, + text=text1, author=author, + comment=comment, parent=0) + + self.repo.save_text(title=title, + text=text2, author=author, + comment=comment, parent=0) + + saved = self.repo.open_page(title).read() + + # Other conflict markers placement can also be correct + assert_equal(saved, u'''\ +text +test +<<<<<<< local +======= +text +>>>>>>> other +''') + + def test_delete(self): + text = u"text test" + title = u"test title" + author = u"test author" + comment = u"test comment" + self.repo.save_text(title=title, + text=text, author=author, + comment=comment, parent=NULL_PARENT) + + assert title in self.repo + + self.repo.delete_page(title, author, comment) + + assert title not in self.repo + + @raises(vstorage.DocumentNotFound) + def test_document_not_found(self): + self.repo.open_page(u'unknown entity') + + def test_open_existing_repository(self): + self.repo.save_text(title=u'Python!', text=u'ham and spam') + current_repo_revision = self.repo.repo_revision() + same_repo = vstorage.VersionedStorage(self.repo_path) + assert_equal(same_repo.repo_revision(), current_repo_revision) + + def test_history(self): + COMMITS = [ + {"author": "bunny", "text":"1", "comment": "Oh yeah!"}, + {"author": "frank", "text":"2", "comment": "Second is the best!"}, + {"text":"3", "comment": "Third"}, + {"author": "welma", "text":"4", "comment": "Fourth"}, + ] + + for commit in COMMITS: + self.repo.save_text(title=u"Sample", **commit) + + for n, entry in enumerate(reversed(list(self.repo.page_history(u"Sample")))): + assert_equal(entry["version"], n) + assert_equal(entry["author"], COMMITS[n].get("author", "anonymous")) + assert_equal(entry["description"], COMMITS[n]["comment"]) + assert_equal(entry["tag"], []) + + +class TestVSTags(object): + + TITLE_1 = "Sample" + + COMMITS = [ + {"author": "bunny", "text":"1", "comment": "Oh yeah!"}, + {"author": "frank", "text":"2", "comment": "Second is the best!"}, + {"text":"3", "comment": "Third"}, + {"author": "welma", "text":"4", "comment": "Fourth"}, + ] + + def setUp(self): + self.repo_path = tempfile.mkdtemp() + self.repo = vstorage.VersionedStorage(self.repo_path) + + # generate some history + for commit in self.COMMITS: + self.repo.save_text(title=u"Sample", **commit) + + # verify + for n, entry in enumerate(reversed(list(self.repo.page_history(self.TITLE_1)))): + assert_equal(entry["tag"], []) + + def tearDown(self): + clear_directory(self.repo_path) + + def test_add_tag(self): + TAG_USER = "mike_the_tagger" + TAG_NAME = "production" + TAG_VERSION = 2 + + # Add tag + self.repo.add_page_tag(self.TITLE_1, TAG_VERSION, TAG_NAME, TAG_USER) + + # check history again + history = list(self.repo.page_history(self.TITLE_1)) + for entry in reversed(history): + if entry["version"] == TAG_VERSION: + assert_equal(entry["tag"], [TAG_NAME]) + else: + assert_equal(entry["tag"], []) + + def test_add_many_tags(self): + TAG_USER = "mike_the_tagger" + tags = [ + (2, "production", "mike"), + (2, "finished", "jeremy"), + (0, "original", "jeremy"), + ] + + for rev, name, user in tags: + self.repo.add_page_tag(self.TITLE_1, rev, name, user) + + # check history again + history = list(self.repo.page_history(self.TITLE_1)) + for entry in reversed(history): + expected = [tag[1] for tag in tags if tag[0] == entry["version"]] + assert_equal(set(entry["tag"]), set(expected)) + + +if __name__ == '__main__': + runmodule() diff --git a/lib/wlapi.py b/lib/wlapi.py deleted file mode 100644 index 3284211a..00000000 --- a/lib/wlapi.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- 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. -# -""" - Abstraction over API for wolnelektury.pl -""" -import urllib2 -import functools -import django.utils.simplejson as json -import logging -logger = logging.getLogger("fnp.lib.wlapi") - - -class APICallException(Exception): - - 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"): - def wrapper(func): - - @functools.wraps(func) - def wrapped(self, *args, **kwargs): - generator = func(self, *args, **kwargs) - - data = generator.next() - - # prepare request - rq = urllib2.Request(self.base_url + path + ".json") - - # 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") - - try: - anwser = json.load(self.opener.open(rq)) - return generator.send(anwser) - except StopIteration: - # by default, just return the anwser as a shorthand - return anwser - except urllib2.HTTPError, error: - return self._http_error(error) - except Exception, error: - return self._error(error) - return wrapped - - return wrapper - - -class WLAPI(object): - - 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'] - - 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(digest_handler, basic_handler) - - def _http_error(self, error): - message = error.read() - logger.debug("HTTP ERROR: %s", message) - return self._error(message) - - def _error(self, error): - raise APICallException(error) - - @api_call("books") - def list_books(self): - yield - - @api_call("books") - def publish_book(self, document): - yield {"text": document.text, "compressed": False} diff --git a/lib/wlapi/__init__.py b/lib/wlapi/__init__.py new file mode 100644 index 00000000..3284211a --- /dev/null +++ b/lib/wlapi/__init__.py @@ -0,0 +1,94 @@ +# -*- 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. +# +""" + Abstraction over API for wolnelektury.pl +""" +import urllib2 +import functools +import django.utils.simplejson as json +import logging +logger = logging.getLogger("fnp.lib.wlapi") + + +class APICallException(Exception): + + 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"): + def wrapper(func): + + @functools.wraps(func) + def wrapped(self, *args, **kwargs): + generator = func(self, *args, **kwargs) + + data = generator.next() + + # prepare request + rq = urllib2.Request(self.base_url + path + ".json") + + # 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") + + try: + anwser = json.load(self.opener.open(rq)) + return generator.send(anwser) + except StopIteration: + # by default, just return the anwser as a shorthand + return anwser + except urllib2.HTTPError, error: + return self._http_error(error) + except Exception, error: + return self._error(error) + return wrapped + + return wrapper + + +class WLAPI(object): + + 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'] + + 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(digest_handler, basic_handler) + + def _http_error(self, error): + message = error.read() + logger.debug("HTTP ERROR: %s", message) + return self._error(message) + + def _error(self, error): + raise APICallException(error) + + @api_call("books") + def list_books(self): + yield + + @api_call("books") + def publish_book(self, document): + yield {"text": document.text, "compressed": False} diff --git a/lib/wlapi/tests.py b/lib/wlapi/tests.py new file mode 100644 index 00000000..a12ab068 --- /dev/null +++ b/lib/wlapi/tests.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/redakcja.vhost.template b/redakcja.vhost.template index fbd21a9a..1d24c675 100644 --- a/redakcja.vhost.template +++ b/redakcja.vhost.template @@ -14,7 +14,6 @@ Alias /media $MEDIA_ROOT - Options Indexes, FollowLinks Order allow,deny Allow from all diff --git a/redakcja/settings.py b/redakcja/settings.py deleted file mode 100644 index fb262051..00000000 --- a/redakcja/settings.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 -*- -import os.path - -PROJECT_ROOT = os.path.realpath(os.path.dirname(__file__)) - -DEBUG = False -TEMPLATE_DEBUG = DEBUG - -MAINTENANCE_MODE = False - -ADMINS = ( - # (u'Marek Stępniowski', 'marek@stepniowski.com'), - (u'Łukasz Rekucki', 'lrekucki@gmail.com'), -) - -MANAGERS = ADMINS - -DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. -DATABASE_NAME = PROJECT_ROOT + '/dev.sqlite' # Or path to database file if using sqlite3. -DATABASE_USER = '' # Not used with sqlite3. -DATABASE_PASSWORD = '' # Not used with sqlite3. -DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. -DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'Europe/Warsaw' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'pl' - -#import locale -#locale.setlocale(locale.LC_ALL, '') - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = PROJECT_ROOT + '/media/dynamic' -STATIC_ROOT = PROJECT_ROOT + '/static/' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash if there is a path component (optional in other cases). -# Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '/media/dynamic/' -STATIC_URL = '/media/static/' - -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/admin-media/' - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 'ife@x^_lak+x84=lxtr!-ur$5g$+s6xt85gbbm@e_fk6q3r8=+' -SESSION_COOKIE_NAME = "redakcja_sessionid" - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', -) - -TEMPLATE_CONTEXT_PROCESSORS = ( - "django.core.context_processors.auth", - "django.core.context_processors.debug", - "django.core.context_processors.i18n", - "redakcja.context_processors.settings", # this is instead of media - "django.core.context_processors.request", -) - - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django_cas.middleware.CASMiddleware', - - 'django.middleware.doc.XViewMiddleware', - 'maintenancemode.middleware.MaintenanceModeMiddleware', -) - -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'django_cas.backends.CASBackend', -) - -ROOT_URLCONF = 'redakcja.urls' - -TEMPLATE_DIRS = ( - PROJECT_ROOT + '/templates', -) - -FIREPYTHON_LOGGER_NAME = "fnp" - -# -# Central Auth System -# -## Set this to where the CAS server lives -# CAS_SERVER_URL = "http://cas.fnp.pl/ -CAS_LOGOUT_COMPLETELY = True - -from compress_settings import * - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.admin', - 'django.contrib.admindocs', - - 'django_cas', - 'compress', - 'south', - 'sorl.thumbnail', - 'filebrowser', - - 'wiki', - 'toolbar', -) - - -# -# Nose tests -# - -TEST_RUNNER = 'django_nose.run_tests' -TEST_MODULES = ('wiki', 'toolbar', 'vstorage') -NOSE_ARGS = ( - '--tests=' + ','.join(TEST_MODULES), - '--cover-package=' + ','.join(TEST_MODULES), - '-d', - '--with-coverage', - '--with-doctest', -) - - -FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/' -FILEBROWSER_DIRECTORY = 'images/' -FILEBROWSER_ADMIN_VERSIONS = [] -FILEBROWSER_VERSIONS_BASEDIR = 'thumbnails/' -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": "WL API", - "AUTH_USER": "platforma", - "AUTH_PASSWD": "platforma", -} - -# Import localsettings file, which may override settings defined here -try: - from localsettings import * -except ImportError: - pass - -try: - LOGGING_CONFIG_FILE -except NameError: - LOGGING_CONFIG_FILE = os.path.join(PROJECT_ROOT, 'config', - ('logging.cfg' if not DEBUG else 'logging.cfg.dev')) -try: - import logging - - if os.path.isfile(LOGGING_CONFIG_FILE): - import logging.config - logging.config.fileConfig(LOGGING_CONFIG_FILE) - else: - import sys - logging.basicConfig(stream=sys.stderr) -except ImportError as exc: - raise diff --git a/redakcja/settings/__init__.py b/redakcja/settings/__init__.py new file mode 100644 index 00000000..e6ee2038 --- /dev/null +++ b/redakcja/settings/__init__.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import +from .common import * + +DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_NAME = PROJECT_ROOT + '/dev.sqlite' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +try: + from localsettings import * +except ImportError: + pass + +try: + LOGGING_CONFIG_FILE +except NameError: + LOGGING_CONFIG_FILE = os.path.join(PROJECT_ROOT, 'config', + ('logging.cfg' if not DEBUG else 'logging.cfg.dev')) +try: + import logging + + if os.path.isfile(LOGGING_CONFIG_FILE): + import logging.config + logging.config.fileConfig(LOGGING_CONFIG_FILE) + else: + import sys + logging.basicConfig(stream=sys.stderr) +except ImportError as exc: + raise + diff --git a/redakcja/settings/common.py b/redakcja/settings/common.py new file mode 100644 index 00000000..24a4258c --- /dev/null +++ b/redakcja/settings/common.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +import os.path + +PROJECT_ROOT = os.path.realpath(os.path.dirname(__file__)) + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +MAINTENANCE_MODE = False + +ADMINS = ( + # (u'Marek Stępniowski', 'marek@stepniowski.com'), + (u'Łukasz Rekucki', 'lrekucki@gmail.com'), +) + +MANAGERS = ADMINS + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'Europe/Warsaw' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'pl' + +#import locale +#locale.setlocale(locale.LC_ALL, '') + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = PROJECT_ROOT + '/media/dynamic' +STATIC_ROOT = PROJECT_ROOT + '/static/' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '/media/dynamic/' +STATIC_URL = '/media/static/' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/admin-media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'ife@x^_lak+x84=lxtr!-ur$5g$+s6xt85gbbm@e_fk6q3r8=+' +SESSION_COOKIE_NAME = "redakcja_sessionid" + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', +) + +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.core.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "redakcja.context_processors.settings", # this is instead of media + "django.core.context_processors.request", +) + + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django_cas.middleware.CASMiddleware', + + 'django.middleware.doc.XViewMiddleware', + 'maintenancemode.middleware.MaintenanceModeMiddleware', +) + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'django_cas.backends.CASBackend', +) + +ROOT_URLCONF = 'redakcja.urls' + +TEMPLATE_DIRS = ( + PROJECT_ROOT + '/templates', +) + +FIREPYTHON_LOGGER_NAME = "fnp" + +# +# Central Auth System +# +## Set this to where the CAS server lives +# CAS_SERVER_URL = "http://cas.fnp.pl/ +CAS_LOGOUT_COMPLETELY = True + +from compress_settings import * + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.admin', + 'django.contrib.admindocs', + + 'django_cas', + 'compress', + 'south', + 'sorl.thumbnail', + 'filebrowser', + + 'wiki', + 'toolbar', +) + +FILEBROWSER_URL_FILEBROWSER_MEDIA = STATIC_URL + 'filebrowser/' +FILEBROWSER_DIRECTORY = 'images/' +FILEBROWSER_ADMIN_VERSIONS = [] +FILEBROWSER_VERSIONS_BASEDIR = 'thumbnails/' +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": "WL API", + "AUTH_USER": "platforma", + "AUTH_PASSWD": "platforma", +} + +# Import localsettings file, which may override settings defined here diff --git a/redakcja/settings/test.py b/redakcja/settings/test.py new file mode 100644 index 00000000..118c7ffb --- /dev/null +++ b/redakcja/settings/test.py @@ -0,0 +1,28 @@ +# +# Nose tests +# + +from redakcja.settings.common import * + +# ROOT_URLCONF = 'yourapp.settings.test.urls' + +DATABASE_ENGINE = 'sqlite3' +DATABASE_NAME = ':memory:' + +import tempfile + +WIKI_REPOSITORY_PATH = tempfile.mkdtemp(prefix='wikirepo') + +INSTALLED_APPS += ('django_nose',) + +TEST_RUNNER = 'django_nose.run_tests' +TEST_MODULES = ('wiki', 'toolbar', 'vstorage') +NOSE_ARGS = ( + '--tests=' + ','.join(TEST_MODULES), + '--cover-package=' + ','.join(TEST_MODULES), + '-d', + '--with-coverage', + '--with-doctest', + '--with-xunit', + '--with-xcoverage', +) diff --git a/redakcja/static/js/wiki/wikiapi.js b/redakcja/static/js/wiki/wikiapi.js index 27ab97e2..d62d8fe4 100644 --- a/redakcja/static/js/wiki/wikiapi.js +++ b/redakcja/static/js/wiki/wikiapi.js @@ -7,11 +7,11 @@ failure: noop }; /* - * Return absolute reverse path of given named view. - * (at least he have it hard-coded in one place) - * + * 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]; @@ -103,10 +103,10 @@ }; /* * Fetch history of this document. - * - * from - First revision to fetch (default = 0) - * upto - Last revision to fetch (default = tip) - * + * + * from - First revision to fetch (default = 0) upto - Last revision to + * fetch (default = tip) + * */ WikiDocument.prototype.fetchHistory = function(params) { /* this doesn't modify anything, so no locks */ diff --git a/requirements.txt b/requirements.txt index d247c761..b52baefe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,3 @@ django-maintenancemode>=0.9 # migrations south>=0.6 - -## Debugging utils, uncomment this if you want tests -# django-nose>=0.0.3 -# django-debug-toolbar>=0.8 \ No newline at end of file diff --git a/scripts/crop.py b/scripts/crop.py old mode 100644 new mode 100755 diff --git a/scripts/imgconv.py b/scripts/imgconv.py old mode 100644 new mode 100755 diff --git a/scripts/rip-themes-from-redmine.py b/scripts/rip-themes-from-redmine.py old mode 100644 new mode 100755