Compare commits

..

No commits in common. "master" and "0.2.0" have entirely different histories.

22 changed files with 69 additions and 628 deletions

View File

@ -1,18 +0,0 @@
language: python
python:
- "2.6"
- "2.7"
- "3.3"
env:
- DJANGO_PACKAGE="Django<1.5"
- DJANGO_PACKAGE="Django>=1.5,<1.6"
- DJANGO_PACKAGE="Django>=1.6,<1.7"
install: pip install $DJANGO_PACKAGE markdown docutils django-markupfield --use-mirrors
script: django-admin.py test --settings=example.settings --pythonpath=.
matrix:
exclude:
- python: "3.3"
env: DJANGO_PACKAGE="Django<1.5"
notifications:
email:
- james.p.turk@gmail.com

View File

@ -1,21 +1,9 @@
0.3.0
=====
- lots of tests
- new escape_html option
- csrf token
- autolockarticles management command
- null editors
- fix revert
0.2.0
===================
- simplification of status
- addition of optional comment fields on edit
- cache-based lock for writing
- rename view
- added MARKUPWIKI_ settings
- RSS for articles and wiki as a whole
0.1.0
=====

View File

@ -10,85 +10,33 @@ functionality. Pages can be edited, locked, and deleted. Revisions can be
viewed, reverted, and compared. If you need much more than that markupwiki
might not be for you.
Requirements
============
django-markupwiki depends on django >= 1.2, django-markupfield >= 1.0.0b and
Requirements
------------
django-markupwiki depends on django 1.2+, django-markupfield 1.0.0b+ and
libraries for whichever markup options you wish to include.
Usage
=====
Like any django application, the first step when using django-markupwiki is
to add ``markupwiki`` to your ``INSTALLED_APPS``.
urls
----
To use django-markupwiki's urls you should also add a line like::
url(r'^wiki/', include('markupwiki.urls')),
to your urlconf.
This will make the following views available (assuming the defined root is /wiki/):
/wiki/rss/
RSS feed of latest changes to wiki
/wiki/*article*/
view the latest version of an article
/wiki/*article*/rss/
RSS feed of changes to an article
/wiki/*article*/edit/
edit (or create) an article
/wiki/*article*/history/
history view for an article
/wiki/*article*/history/*revision*/
view a specific version of an article
/wiki/*article*/diff/
compare a two revisions of an article
Settings
========
article names
~~~~~~~~~~~~~
``MARKUPWIKI_WRITE_LOCK_SECONDS`` - number of seconds that a user can hold a
write lock (default: 300)
*article* in all of the above URLs is the name of an article: which is basically any string with limited restrictions. There are a few basic guidelines:
``MARKUPWIKI_CREATE_MISSING_ARTICLES`` - if True when attempting to go to an
article that doesn't exist, user will be redirected to the /edit/ page. If
False user will get a 404. (default: True)
* Spaces in the URL will automatically be converted to underscores.
* When displaying an article, anything before a / will be linked to an article with that name
(eg. /wiki/category/article/ will have a link in the header to /wiki/category/)
``MARKUPWIKI_DEFAULT_MARKUP_TYPE`` - default markup type to use
(default: markdown)
``MARKUPWIKI_MARKUP_TYPE_EDITABLE`` - if False user won't have option to change
markup type (default: True)
interwiki links
---------------
While you are free to use whatever markup you desire, most markup types (ReST, markdown, etc) do not include a standard syntax for interwiki links. As a result all markup types are augmented with a post-processor that adds support for interwiki links in the [[link text|link]] format.
[[link text|page]] produces a link to an article named 'page' with the text before the | as the anchor.
[[page]] produces a link to an article named 'page' with using the page name as the anchor.
settings
--------
django-markupwiki provides a number of optional settings that you may wish to use
to customize the behavior.
``MARKUPWIKI_WRITE_LOCK_SECONDS``
number of seconds that a user can hold a write lock (default: 300)
``MARKUPWIKI_CREATE_MISSING_ARTICLES``
if True when attempting to go to an article that doesn't exist, user will be redirected to the /edit/ page. If False user will get a 404. (default: True)
``MARKUPWIKI_DEFAULT_MARKUP_TYPE``
default markup type to use (default: markdown)
``MARKUPWIKI_MARKUP_TYPE_EDITABLE``
if False user won't have option to change markup type (default: True)
``MARKUPWIKI_MARKUP_TYPES``
a tuple of string and callable pairs the callable is used to 'render' a markup type.
``MARKUPWIKI_AUTOLOCK_TIMEDELTA``
a datetime.timedelta object that defines the age at which articles get automatically locked by the *autolockarticles* management command.
Example::
``MARKUPWIKI_MARKUP_TYPES`` - a tuple of string and callable pairs the
callable is used to 'render' a markup type. Example::
import markdown
from docutils.core import publish_parts

3
TODO
View File

@ -3,3 +3,6 @@
* detect broken wiki links
* only store diffs?
* anonymous edits?
add options:
* MARKUPWIKI_WIKIWORD_RE

View File

View File

@ -1,11 +0,0 @@
#!/usr/bin/env python
from django.core.management import execute_manager
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)

View File

@ -1,43 +0,0 @@
import os
DEBUG = True
TEMPLATE_DEBUG = DEBUG
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'example',
}
}
ADMIN_MEDIA_PREFIX = '/media/'
SECRET_KEY = 'h%+o+&fe3r4j0z=9ghk=!divcta%zh%&=k8d^r08$cgr@3k3-&'
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
)
ROOT_URLCONF = 'example.urls'
TEMPLATE_DIRS = ( os.path.join(os.path.dirname(__file__), 'templates'), )
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'markupwiki',
)
SITE_ID = 1

View File

@ -1 +0,0 @@
404 not found

View File

@ -1,9 +0,0 @@
{% if messages %}
<ul id="message_list">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% block content %} {% endblock %}

View File

@ -1,17 +0,0 @@
from django.conf.urls import *
# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()
urlpatterns = patterns('',
# Example:
(r'^wiki/', include('markupwiki.urls')),
# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
# (r'^admin/', include(admin.site.urls)),
)

View File

@ -1,22 +0,0 @@
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Min
from markupwiki.models import Article, PUBLIC, LOCKED, DELETED
import datetime
class Command(BaseCommand):
help = 'Auto-locks articles based on time and other factors'
def handle(self, *args, **options):
''' Lock any public article that was created earlier than
MARKUPWIKI_AUTOLOCK_TIMEDELTA ago.
'''
timedelta = getattr(settings, 'MARKUPWIKI_AUTOLOCK_TIMEDELTA', None)
if timedelta is not None:
ts = datetime.datetime.now() - timedelta
qs = Article.objects.filter(status=PUBLIC).annotate(
timestamp=Min('versions__timestamp')).filter(timestamp__lte=ts)
qs.update(status=LOCKED)

View File

@ -8,17 +8,9 @@ from markupfield.fields import MarkupField
from markupfield import markup
from markupwiki.utils import wikify_markup_wrapper
DEFAULT_MARKUP_TYPE = getattr(settings, 'MARKUPWIKI_DEFAULT_MARKUP_TYPE',
'markdown')
DEFAULT_MARKUP_TYPE = getattr(settings, 'MARKUPWIKI_DEFAULT_MARKUP_TYPE', 'markdown')
WRITE_LOCK_SECONDS = getattr(settings, 'MARKUPWIKI_WRITE_LOCK_SECONDS', 300)
MARKUP_TYPES = getattr(settings, 'MARKUPWIKI_MARKUP_TYPES',
markup.DEFAULT_MARKUP_TYPES)
ESCAPE_HTML = getattr(settings, 'MARKUPWIKI_ESCAPE_HTML',
True)
EDITOR_TEST_FUNC = getattr(settings, 'MARKUPWIKI_EDITOR_TEST_FUNC',
lambda u: u.is_authenticated())
MODERATOR_TEST_FUNC = getattr(settings, 'MARKUPWIKI_MODERATOR_TEST_FUNC',
lambda u: u.is_staff)
MARKUP_TYPES = getattr(settings, 'MARKUPWIKI_MARKUP_TYPES', markup.DEFAULT_MARKUP_TYPES)
# add make_wiki_links to MARKUP_TYPES
WIKI_MARKUP_TYPES = []
@ -34,9 +26,9 @@ ARTICLE_STATUSES = (
class Article(models.Model):
title = models.CharField(max_length=200)
creator = models.ForeignKey(User, related_name='wiki_articles', blank=True, null=True)
creator = models.ForeignKey(User, related_name='wiki_articles')
status = models.IntegerField(choices=ARTICLE_STATUSES, default=PUBLIC)
redirect_to = models.ForeignKey('self', blank=True, null=True)
redirect_to = models.ForeignKey('self', null=True)
def __unicode__(self):
return self.title
@ -50,11 +42,6 @@ class Article(models.Model):
if '/' in self.title:
return self.title.rsplit('/',1)[0]
# def save(self, **kwargs):
# if self.creator is not None and self.creator.is_anonymous():
# self.creator = None
# super(Article, self).save(**kwargs)
def get_absolute_url(self):
return reverse('view_article', args=[self.title])
@ -69,33 +56,27 @@ class Article(models.Model):
def is_editable_by_user(self, user):
if self.status in (LOCKED, DELETED):
return MODERATOR_TEST_FUNC(user)
return user.is_staff
else:
return EDITOR_TEST_FUNC(user)
return user.is_authenticated()
def get_write_lock(self, user_or_request, release=False):
if hasattr(user_or_request, 'session'):
lock_id = user_or_request.session.session_key
else:
lock_id = user_or_request.id
def get_write_lock(self, user, release=False):
cache_key = 'markupwiki_articlelock_%s' % self.id
lock = cache.get(cache_key)
if lock:
if release:
cache.delete(cache_key)
return lock == lock_id
return lock == user.id
if not release:
cache.set(cache_key, lock_id, WRITE_LOCK_SECONDS)
cache.set(cache_key, user.id, WRITE_LOCK_SECONDS)
return True
class ArticleVersion(models.Model):
article = models.ForeignKey(Article, related_name='versions')
author = models.ForeignKey(User, related_name='article_versions', blank=True, null=True)
author = models.ForeignKey(User, related_name='article_versions')
number = models.PositiveIntegerField()
body = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
markup_choices=WIKI_MARKUP_TYPES,
escape_html=ESCAPE_HTML)
markup_choices=WIKI_MARKUP_TYPES)
comment = models.CharField(max_length=200, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
@ -107,11 +88,6 @@ class ArticleVersion(models.Model):
def __unicode__(self):
return '%s rev #%s' % (self.article, self.number)
# def save(self, **kwargs):
# if self.author is not None and self.author.is_anonymous():
# self.author = None
# super(ArticleVersion, self).save(**kwargs)
def get_absolute_url(self):
return reverse('article_version', args=[self.article.title, self.number])

View File

@ -6,7 +6,7 @@
{% if article and mod_form %}
<div class="article_moderation">
<form method="POST" action="{% url "update_article_status" article.title %}">
<form method="POST" action="{% url update_article_status article.title %}">
<ul>
<li>{{mod_form.status.label_tag}} {{ mod_form.status }}</li>
<li>
@ -16,7 +16,7 @@
</li>
</ul>
</form>
<form method="POST" action="{% url "rename_article" article.title %}">
<form method="POST" action="{% url rename_article article.title %}">
<ul>
{{ rename_form.as_ul}}
<li>
@ -31,7 +31,7 @@
<h2 class="article_title">
{% block article_title %}
{% if article.section_name %}<a href="{% url "view_article" article.section_name %}">{{article.section_name}}</a> / {% endif %}
{% if article.section_name %}<a href="{% url view_article article.section_name %}">{{article.section_name}}</a> / {% endif %}
{{article.display_title}}
{% if article.is_deleted %} [deleted] {% endif %}
@ -42,10 +42,10 @@
<div class="article_meta">
{% block article_meta %}
{% if article.editable %}
<a href="{% url "edit_article" article.title %}">edit article</a> |
<a href="{% url edit_article article.title %}">edit article</a> |
{% endif %}
{% if article %}
<a href="{% url "article_history" article.title %}">view history</a>
<a href="{% url article_history article.title %}">view history</a>
{% endif %}
{% endblock %}
</div>

View File

@ -10,14 +10,13 @@
{% block article_meta %}
{% if article %}
<a href="{% url "view_article" article.title %}">view article</a> |
<a href="{% url "article_history" article.title %}">view history</a>
<a href="{% url view_article article.title %}">view article</a> |
<a href="{% url article_history article.title %}">view history</a>
{% endif %}
{% endblock %}
{% block article_body %}
<form method="POST" action=".">
{% csrf_token %}
<ul>
<li>{{form.body}}</li>
<li>{{form.comment.label_tag}} {{ form.comment }} </li>

View File

@ -12,32 +12,6 @@
{% endblock %}
{% block article_body %}
<form action="{% url revert article.title %}" method="post">
{% csrf_token %}
<label for="revert-version">Revert to</label>
<select name="revision" id="revert-version">
{% for version in versions reversed %}
{% if not forloop.first %}
<option value="{{ version.number }}">
{% if version.number == 0 %}
Initial
{% else %}
{{ version.number }}
{% endif %}
</option>
{% endif %}
{% endfor %}
</select>
<button class="compareBtn" type="submit">
<span>Revert to version</span>
</button>
</form>
<table>
<thead> <tr>
<th>Version</th>
@ -72,5 +46,4 @@
<span>Compare Selected Versions</span>
</button>
</form>
{% endblock %}

View File

@ -1,329 +0,0 @@
import time
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import Client
from django.http import HttpRequest
from django.contrib.auth.models import User, AnonymousUser
from markupwiki.models import Article, ArticleVersion, PUBLIC, LOCKED, DELETED
from markupwiki import models
from markupwiki.utils import make_wiki_links, wikify_markup_wrapper
from markupwiki import views
class ArticleTests(TestCase):
def test_display_title(self):
a = Article(title='section/name_with_spaces')
self.assertEquals(a.display_title, 'name with spaces')
def test_section_name(self):
a = Article(title='section/name_with_spaces')
self.assertEquals(a.section_name, 'section')
def test_is_editable_by_user(self):
public_article = Article(title='public', status=PUBLIC)
locked_article = Article(title='locked', status=LOCKED)
deleted_article = Article(title='deleted', status=DELETED)
user = User(is_staff=False)
staff_user = User(is_staff=True)
anon_user = AnonymousUser()
# check that anonymous users cannot edit
self.assertFalse(public_article.is_editable_by_user(anon_user))
self.assertFalse(locked_article.is_editable_by_user(anon_user))
self.assertFalse(deleted_article.is_editable_by_user(anon_user))
# check that user can only edit public articles
self.assert_(public_article.is_editable_by_user(user))
self.assertFalse(locked_article.is_editable_by_user(user))
self.assertFalse(deleted_article.is_editable_by_user(user))
# check that staff can edit any article
self.assert_(locked_article.is_editable_by_user(staff_user))
self.assert_(deleted_article.is_editable_by_user(staff_user))
models.WRITE_LOCK_SECONDS = 1
class ArticleWriteLockTests(TestCase):
alice = User(id=1, username='alice')
bob = User(id=2, username='bob')
article = Article(id=1, title='locktest')
def setUp(self):
cache.clear()
def test_simple_lock(self):
''' test that bob can't grab the lock immediately after alice does '''
alice_initial_lock = self.article.get_write_lock(self.alice)
bob_immediate_lock = self.article.get_write_lock(self.bob)
alice_retained_lock = self.article.get_write_lock(self.alice)
self.assertTrue(alice_initial_lock)
self.assertFalse(bob_immediate_lock)
self.assertTrue(alice_retained_lock)
def test_lock_timeout(self):
''' test that the lock times out properly '''
alice_initial_lock = self.article.get_write_lock(self.alice)
time.sleep(2)
bob_wait_lock = self.article.get_write_lock(self.bob)
self.assertTrue(alice_initial_lock)
self.assertTrue(bob_wait_lock)
def test_lock_release(self):
''' test that lock is released properly '''
alice_initial_lock = self.article.get_write_lock(self.alice)
alice_release_lock = self.article.get_write_lock(self.alice, release=True)
bob_immediate_lock = self.article.get_write_lock(self.bob)
self.assertTrue(alice_initial_lock)
self.assertTrue(alice_release_lock)
self.assertTrue(bob_immediate_lock)
def test_release_on_acquire(self):
''' test that if release is True on acquire lock is not set '''
alice_initial_lock = self.article.get_write_lock(self.alice, release=True)
bob_immediate_lock = self.article.get_write_lock(self.bob)
self.assertTrue(alice_initial_lock)
self.assertTrue(bob_immediate_lock)
class WikifyTests(TestCase):
urls = 'markupwiki.urls'
def _get_url(self, link, name=None):
return '<a href="%s">%s</a>' % (reverse('view_article', args=[link]),
name or link)
def test_make_wiki_links_simple(self):
result = make_wiki_links('[[test]]')
self.assertEquals(result, self._get_url('test'))
result = make_wiki_links('[[two words ]]')
self.assertEquals(result, self._get_url('two words'))
result_ws = make_wiki_links('[[ test ]]')
self.assertEquals(result_ws, self._get_url('test'))
def test_make_wiki_links_named(self):
result = make_wiki_links('[[test|this link has a name]]')
self.assertEquals(result, self._get_url('test', 'this link has a name'))
def test_wikify_markup_wrapper(self):
wrapped_upper_filter = wikify_markup_wrapper(lambda text: text.upper())
result = wrapped_upper_filter('[[test]]')
self.assertEquals(result, self._get_url('TEST'))
def test_wikify_markup_wrapper_double_wrap(self):
''' ensure that wrapped functions can't be double wrapped '''
wrapped_upper_filter = wikify_markup_wrapper(lambda text: text.upper())
self.assertEquals(wrapped_upper_filter,
wikify_markup_wrapper(wrapped_upper_filter))
class ViewTestsBase(TestCase):
urls = 'example.urls'
def setUp(self):
self.admin = User.objects.create_superuser('admin', 'admin@admin.com',
'password')
self.frank = User.objects.create_user('frank', 'frank@example.com',
'password')
self.test_article = Article.objects.create(title='test',
creator=self.admin)
ArticleVersion.objects.create(article=self.test_article,
author=self.admin,
number=0,
body='this is a test')
ArticleVersion.objects.create(article=self.test_article,
author=self.frank,
number=1,
body='this is an update')
ArticleVersion.objects.create(article=self.test_article,
author=self.frank,
number=2,
body='this is the final update')
# article with space in title
self.two_word_article = Article.objects.create(title='two_words',
creator=self.admin)
ArticleVersion.objects.create(article=self.two_word_article,
author=self.frank,
number=0,
body='this article title has a space')
# locked article
self.locked = Article.objects.create(title='locked', creator=self.admin,
status=LOCKED)
ArticleVersion.objects.create(article=self.locked, author=self.frank,
number=0, body='lockdown')
# clear cache at start of every test
cache.clear()
def login_as_user(self):
self.client.login(username='frank', password='password')
def login_as_admin(self):
self.client.login(username='admin', password='password')
class ViewArticleTests(ViewTestsBase):
def test_normal(self):
''' test accessing an article without a version specified '''
resp = self.client.get('/wiki/test/')
self.assertContains(resp, 'this is the final update')
def test_specific_version(self):
''' test accessing a specific version of an article '''
resp = self.client.get('/wiki/test/history/1/')
self.assertContains(resp, 'this is an update')
def test_name_with_spaces(self):
''' test that a name with spaces is properly converted into a name with underscores '''
resp = self.client.get('/wiki/two words/')
self.assertRedirects(resp, '/wiki/two_words/', status_code=301)
def test_redirect(self):
''' test that a 302 is given for any article with a redirect_to '''
redirect = Article.objects.create(title='redirect', creator=self.admin,
redirect_to=self.test_article)
resp = self.client.get('/wiki/redirect/')
self.assertRedirects(resp, '/wiki/test/', status_code=302)
def test_missing_edit(self):
''' test that a 302 is given to the edit page if CREATE_MISSING_ARTICLE is True '''
views.CREATE_MISSING_ARTICLE = True
self.login_as_user()
resp = self.client.get('/wiki/newpage/')
self.assertRedirects(resp, '/wiki/newpage/edit/', status_code=302)
def test_missing_404(self):
''' test that a 404 is given if CREATE_MISSING_ARTICLE is False '''
views.CREATE_MISSING_ARTICLE = False
self.login_as_user()
resp = self.client.get('/wiki/newpage/')
self.assertContains(resp, '', status_code=404)
def test_staff_forms(self):
''' ensure that only admins can see the admin form '''
# make sure a normal user doesn't see the admin form
self.login_as_user()
resp = self.client.get('/wiki/test/')
self.assertNotContains(resp, '<label for="id_status">')
# ...but an admin does
self.login_as_admin()
resp = self.client.get('/wiki/test/')
self.assertContains(resp, '<label for="id_status">')
class EditArticleTests(ViewTestsBase):
def test_edit_article_GET(self):
''' ensure that logged in users get edit form for articles '''
self.login_as_user()
resp = self.client.get('/wiki/test/edit/')
self.assertContains(resp, '<textarea id="id_body', status_code=200)
def test_create_article_GET(self):
''' ensure that logged in users get edit form for new articles '''
self.login_as_user()
resp = self.client.get('/wiki/newarticle/edit/')
self.assertContains(resp, '<textarea id="id_body', status_code=200)
def test_article_locked(self):
''' ensure that only staff members can edit locked articles '''
# ensure that a normal user can't edit a locked article
self.login_as_user()
resp = self.client.get('/wiki/locked/edit/')
self.assertContains(resp, 'not authorized to edit', status_code=403)
# ensure that an admin can
self.login_as_admin()
resp = self.client.get('/wiki/locked/edit/')
self.assertNotContains(resp, 'not authorized to edit', status_code=200)
# also test that permissions are checked on POST
self.login_as_user()
resp = self.client.post('/wiki/locked/edit/')
self.assertContains(resp, 'not authorized to edit', status_code=403)
def test_edit_article_POST(self):
''' test that articles can be edited by logged in users '''
postdata = {'body': 'edit article test',
'comment': 'edit article test',
'body_markup_type': 'markdown'}
# post to the form
self.login_as_user()
resp = self.client.post('/wiki/test/edit/', postdata)
self.assertRedirects(resp, '/wiki/test/')
# make sure changes are present
resp = self.client.get('/wiki/test/')
self.assertContains(resp, 'edit article test')
def test_create_article_POST(self):
''' test that articles can be created by logged in users '''
postdata = {'body': 'new article test',
'comment': 'new article',
'body_markup_type': 'markdown'}
# post to the form
self.login_as_user()
resp = self.client.post('/wiki/new/edit/', postdata)
self.assertRedirects(resp, '/wiki/new/')
# make sure changes are present
resp = self.client.get('/wiki/new/')
self.assertContains(resp, 'new article test')
def test_write_lock_message_GET(self):
''' ensure that a user attempting to edit a write locked page will be denied '''
self.login_as_user()
self.client.get('/wiki/test/edit/') # acquire lock
self.login_as_admin()
resp = self.client.get('/wiki/test/edit/', follow=True)
self.assertRedirects(resp, '/wiki/test/')
self.assertContains(resp, 'Someone else is currently editing this page')
def test_write_lock_message_POST(self):
''' ensure that a user attempting to post to a write locked page will be denied '''
postdata = {'body': 'edit article test',
'comment': 'edit article test',
'body_markup_type': 'markdown'}
self.login_as_user()
self.client.get('/wiki/test/edit/') # acquire lock
self.login_as_admin()
resp = self.client.post('/wiki/test/edit/', postdata, follow=True)
self.assertRedirects(resp, '/wiki/test/')
self.assertContains(resp, 'Your session timed out')
class RenameTests(ViewTestsBase):
def test_rename(self):
''' test that rename moves all versions and creates a redirect '''
# post to rename as admin
self.login_as_admin()
resp = self.client.post('/wiki/two_words/rename_article/',
{'new_title': 'now 3 words'})
self.assertRedirects(resp, '/wiki/now_3_words/')
# check that version(s) move
three = Article.objects.get(title='now_3_words')
self.assertEquals(three.versions.count(), 1)
# check that redirect points to three
two = Article.objects.get(title='two_words')
self.assertEquals(two.redirect_to, three)

View File

@ -1,4 +1,4 @@
from django.conf.urls import *
from django.conf.urls.defaults import *
from markupwiki.feeds import LatestEditsFeed, LatestArticleEditsFeed
WIKI_REGEX = r'^(?P<title>.+)'
@ -12,6 +12,5 @@ urlpatterns = patterns('markupwiki.views',
url(WIKI_REGEX + '/history/$', 'article_history', name='article_history'),
url(WIKI_REGEX + '/history/(?P<n>\d+)/$', 'view_article', name='article_version'),
url(WIKI_REGEX + '/diff/$', 'article_diff', name='article_diff'),
url(WIKI_REGEX + '/revert/$', 'revert', name='revert'),
url(WIKI_REGEX + '/$', 'view_article', name='view_article'),
)

View File

@ -7,7 +7,20 @@ from django.core.urlresolvers import reverse
link_re = re.compile('\[\[(?P<link>.*?)(?:\|(?P<name>.*?))?\]\]')
def _link_repl_func(match_obj):
__sample_content = '''
this is a sample
[[testlink]]
[[testlink|with a name]]
[[another test link]]
[[multi
line]]
'''
def link_repl_func(match_obj):
gd = match_obj.groupdict()
name = gd['name'] or gd['link']
name = name.strip()
@ -15,7 +28,7 @@ def _link_repl_func(match_obj):
return '<a href="%s">%s</a>' % (link, name)
def make_wiki_links(text):
return link_re.sub(_link_repl_func, text)
return link_re.sub(link_repl_func, text)
def wikify_markup_wrapper(f):
if not hasattr(f, 'wikified_markup'):

View File

@ -8,23 +8,16 @@ from django.contrib import messages
from django.http import Http404
from django.template import RequestContext
from django.utils.functional import wraps
from markupwiki.models import Article, ArticleVersion, PUBLIC, DELETED, LOCKED
from markupwiki.models import Article, PUBLIC, DELETED, LOCKED
from markupwiki.forms import ArticleForm, StaffModerationForm, ArticleRenameForm
CREATE_MISSING_ARTICLE = getattr(settings,
'MARKUPWIKI_CREATE_MISSING_ARTICLES', True)
EDITOR_TEST_FUNC = getattr(settings, 'MARKUPWIKI_EDITOR_TEST_FUNC',
lambda u: u.is_authenticated())
MODERATOR_TEST_FUNC = getattr(settings, 'MARKUPWIKI_MODERATOR_TEST_FUNC',
lambda u: u.is_staff)
CREATE_MISSING_ARTICLE = getattr(settings, 'MARKUPWIKI_CREATE_MISSING_ARTICLES', True)
def title_check(view):
def new_view(request, title, *args, **kwargs):
newtitle = title.replace(' ', '_')
if newtitle != title:
return redirect(request.path.replace(title, newtitle),
permanent=True)
return redirect(request.path.replace(title, newtitle))
else:
return view(request, title, *args, **kwargs)
return wraps(view)(new_view)
@ -77,7 +70,7 @@ def view_article(request, title, n=None):
context_instance=RequestContext(request))
@title_check
@user_passes_test(EDITOR_TEST_FUNC)
@login_required
def edit_article(request, title):
''' edit (or create) an article
@ -115,13 +108,11 @@ def edit_article(request, title):
form = ArticleForm()
elif request.method == 'POST':
form = ArticleForm(request.POST)
user = None if request.user.is_anonymous() else request.user
if form.is_valid():
if not article:
# if article doesn't exist create it and start num at 0
article = Article.objects.create(title=title,
creator=user)
creator=request.user)
num = 0
else:
if not article.get_write_lock(request.user):
@ -135,11 +126,11 @@ def edit_article(request, title):
# create a new version attached to article specified in name
version = form.save(False)
version.article = article
version.author = user
version.author = request.user
version.number = num
version.save()
article.get_write_lock(user or request, release=True)
article.get_write_lock(request.user, release=True)
# redirect to view article on save
return redirect(article)
@ -150,7 +141,7 @@ def edit_article(request, title):
@require_POST
@user_passes_test(MODERATOR_TEST_FUNC)
@user_passes_test(lambda u: u.is_staff)
@title_check
def article_status(request, title):
''' POST-only view to update article status (staff-only)
@ -162,14 +153,14 @@ def article_status(request, title):
return redirect(article)
@require_POST
@user_passes_test(MODERATOR_TEST_FUNC)
@user_passes_test(lambda u: u.is_staff)
@title_check
def revert(request, title):
''' POST-only view to revert article to a specific revision
'''
article = get_object_or_404(Article, title=title)
revision_id = int(request.POST['revision'])
revision = get_object_or_404(article.versions, number=revision_id)
revision = get_object_or_404(revision, number=revision_id)
ArticleVersion.objects.create(article=article, author=request.user,
number=article.versions.latest().number,
comment='reverted to r%s' % revision_id,
@ -178,17 +169,18 @@ def revert(request, title):
return redirect(article)
@require_POST
@user_passes_test(MODERATOR_TEST_FUNC)
@user_passes_test(lambda u: u.is_staff)
@title_check
def rename(request, title):
''' POST-only view to rename article '''
article = get_object_or_404(Article, title=title)
new_title = request.POST['new_title']
article.title = new_title.replace(' ', '_')
article.title = new_title
print new_title
article.save()
new_article = Article.objects.create(title=title, creator=request.user,
redirect_to=article)
return redirect(article)
return redirect(new_article)
@title_check
def article_history(request, title):

View File

@ -4,7 +4,7 @@ long_description = open('README.rst').read()
setup(
name='django-markupwiki',
version="0.3.0",
version="0.2.0",
packages=['markupwiki'],
package_dir={'markupwiki': 'markupwiki'},
package_data={'markupwiki': ['templates/markupwiki/*.html']},