Compare commits
No commits in common. "master" and "0.2.0" have entirely different histories.
18
.travis.yml
18
.travis.yml
@ -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
|
12
CHANGELOG
12
CHANGELOG
@ -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
|
||||
=====
|
||||
|
86
README.rst
86
README.rst
@ -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
3
TODO
@ -3,3 +3,6 @@
|
||||
* detect broken wiki links
|
||||
* only store diffs?
|
||||
* anonymous edits?
|
||||
|
||||
add options:
|
||||
* MARKUPWIKI_WIKIWORD_RE
|
||||
|
@ -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)
|
@ -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
|
@ -1 +0,0 @@
|
||||
404 not found
|
@ -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 %}
|
@ -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)),
|
||||
)
|
@ -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)
|
@ -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])
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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)
|
@ -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'),
|
||||
)
|
||||
|
@ -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'):
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user