Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2aff42e4a9 | ||
![]() |
74a4f00ac3 | ||
![]() |
6cc6f46348 | ||
![]() |
236239e774 | ||
![]() |
a20417af0b | ||
![]() |
b674c84144 | ||
![]() |
56f37a55d1 | ||
![]() |
2c3e259b36 | ||
![]() |
d40076186b | ||
![]() |
89713013dc | ||
![]() |
9160ed2fd9 | ||
![]() |
0864edd921 | ||
![]() |
e41f77fa72 | ||
![]() |
aff4f04d5e | ||
![]() |
a255c92198 | ||
![]() |
e9703a0e93 | ||
![]() |
84ea94c78c | ||
![]() |
20a483e754 | ||
![]() |
85f2bac6fd | ||
![]() |
2e04c8be6b | ||
![]() |
36f3cba179 | ||
![]() |
806ed7fe8d | ||
![]() |
7752e62dce | ||
![]() |
d65e992cd7 | ||
![]() |
218f268cca | ||
![]() |
4d7f8a65e7 | ||
![]() |
6d4f8105e5 | ||
![]() |
2fa60828fa | ||
![]() |
7f14aa327a |
18
.travis.yml
Normal file
18
.travis.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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,9 +1,21 @@
|
|||||||
|
0.3.0
|
||||||
|
=====
|
||||||
|
- lots of tests
|
||||||
|
- new escape_html option
|
||||||
|
- csrf token
|
||||||
|
- autolockarticles management command
|
||||||
|
- null editors
|
||||||
|
- fix revert
|
||||||
|
|
||||||
|
|
||||||
0.2.0
|
0.2.0
|
||||||
===================
|
===================
|
||||||
- simplification of status
|
- simplification of status
|
||||||
- addition of optional comment fields on edit
|
- addition of optional comment fields on edit
|
||||||
- cache-based lock for writing
|
- cache-based lock for writing
|
||||||
|
- rename view
|
||||||
- added MARKUPWIKI_ settings
|
- added MARKUPWIKI_ settings
|
||||||
|
- RSS for articles and wiki as a whole
|
||||||
|
|
||||||
0.1.0
|
0.1.0
|
||||||
=====
|
=====
|
||||||
|
84
README.rst
84
README.rst
@ -10,33 +10,85 @@ functionality. Pages can be edited, locked, and deleted. Revisions can be
|
|||||||
viewed, reverted, and compared. If you need much more than that markupwiki
|
viewed, reverted, and compared. If you need much more than that markupwiki
|
||||||
might not be for you.
|
might not be for you.
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
============
|
||||||
|
|
||||||
django-markupwiki depends on django 1.2+, django-markupfield 1.0.0b+ and
|
django-markupwiki depends on django >= 1.2, django-markupfield >= 1.0.0b and
|
||||||
libraries for whichever markup options you wish to include.
|
libraries for whichever markup options you wish to include.
|
||||||
|
|
||||||
|
|
||||||
Settings
|
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
|
||||||
|
|
||||||
|
|
||||||
``MARKUPWIKI_WRITE_LOCK_SECONDS`` - number of seconds that a user can hold a
|
article names
|
||||||
write lock (default: 300)
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
``MARKUPWIKI_CREATE_MISSING_ARTICLES`` - if True when attempting to go to an
|
*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:
|
||||||
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
|
* Spaces in the URL will automatically be converted to underscores.
|
||||||
(default: markdown)
|
* 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_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
|
interwiki links
|
||||||
callable is used to 'render' a markup type. Example::
|
---------------
|
||||||
|
|
||||||
|
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::
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
from docutils.core import publish_parts
|
from docutils.core import publish_parts
|
||||||
|
3
TODO
3
TODO
@ -3,6 +3,3 @@
|
|||||||
* detect broken wiki links
|
* detect broken wiki links
|
||||||
* only store diffs?
|
* only store diffs?
|
||||||
* anonymous edits?
|
* anonymous edits?
|
||||||
|
|
||||||
add options:
|
|
||||||
* MARKUPWIKI_WIKIWORD_RE
|
|
||||||
|
0
example/__init__.py
Normal file
0
example/__init__.py
Normal file
11
example/manage.py
Executable file
11
example/manage.py
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/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)
|
43
example/settings.py
Normal file
43
example/settings.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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
example/templates/404.html
Normal file
1
example/templates/404.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
404 not found
|
9
example/templates/base.html
Normal file
9
example/templates/base.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% 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 %}
|
17
example/urls.py
Normal file
17
example/urls.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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)),
|
||||||
|
)
|
0
markupwiki/management/__init__.py
Normal file
0
markupwiki/management/__init__.py
Normal file
0
markupwiki/management/commands/__init__.py
Normal file
0
markupwiki/management/commands/__init__.py
Normal file
22
markupwiki/management/commands/autolockarticles.py
Normal file
22
markupwiki/management/commands/autolockarticles.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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,9 +8,17 @@ from markupfield.fields import MarkupField
|
|||||||
from markupfield import markup
|
from markupfield import markup
|
||||||
from markupwiki.utils import wikify_markup_wrapper
|
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)
|
WRITE_LOCK_SECONDS = getattr(settings, 'MARKUPWIKI_WRITE_LOCK_SECONDS', 300)
|
||||||
MARKUP_TYPES = getattr(settings, 'MARKUPWIKI_MARKUP_TYPES', markup.DEFAULT_MARKUP_TYPES)
|
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)
|
||||||
|
|
||||||
# add make_wiki_links to MARKUP_TYPES
|
# add make_wiki_links to MARKUP_TYPES
|
||||||
WIKI_MARKUP_TYPES = []
|
WIKI_MARKUP_TYPES = []
|
||||||
@ -26,9 +34,9 @@ ARTICLE_STATUSES = (
|
|||||||
|
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
creator = models.ForeignKey(User, related_name='wiki_articles')
|
creator = models.ForeignKey(User, related_name='wiki_articles', blank=True, null=True)
|
||||||
status = models.IntegerField(choices=ARTICLE_STATUSES, default=PUBLIC)
|
status = models.IntegerField(choices=ARTICLE_STATUSES, default=PUBLIC)
|
||||||
redirect_to = models.ForeignKey('self', null=True)
|
redirect_to = models.ForeignKey('self', blank=True, null=True)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
@ -42,6 +50,11 @@ class Article(models.Model):
|
|||||||
if '/' in self.title:
|
if '/' in self.title:
|
||||||
return self.title.rsplit('/',1)[0]
|
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):
|
def get_absolute_url(self):
|
||||||
return reverse('view_article', args=[self.title])
|
return reverse('view_article', args=[self.title])
|
||||||
|
|
||||||
@ -56,27 +69,33 @@ class Article(models.Model):
|
|||||||
|
|
||||||
def is_editable_by_user(self, user):
|
def is_editable_by_user(self, user):
|
||||||
if self.status in (LOCKED, DELETED):
|
if self.status in (LOCKED, DELETED):
|
||||||
return user.is_staff
|
return MODERATOR_TEST_FUNC(user)
|
||||||
else:
|
else:
|
||||||
return user.is_authenticated()
|
return EDITOR_TEST_FUNC(user)
|
||||||
|
|
||||||
def get_write_lock(self, user, release=False):
|
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
|
||||||
cache_key = 'markupwiki_articlelock_%s' % self.id
|
cache_key = 'markupwiki_articlelock_%s' % self.id
|
||||||
lock = cache.get(cache_key)
|
lock = cache.get(cache_key)
|
||||||
if lock:
|
if lock:
|
||||||
if release:
|
if release:
|
||||||
cache.delete(cache_key)
|
cache.delete(cache_key)
|
||||||
return lock == user.id
|
return lock == lock_id
|
||||||
|
|
||||||
cache.set(cache_key, user.id, WRITE_LOCK_SECONDS)
|
if not release:
|
||||||
|
cache.set(cache_key, lock_id, WRITE_LOCK_SECONDS)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class ArticleVersion(models.Model):
|
class ArticleVersion(models.Model):
|
||||||
article = models.ForeignKey(Article, related_name='versions')
|
article = models.ForeignKey(Article, related_name='versions')
|
||||||
author = models.ForeignKey(User, related_name='article_versions')
|
author = models.ForeignKey(User, related_name='article_versions', blank=True, null=True)
|
||||||
number = models.PositiveIntegerField()
|
number = models.PositiveIntegerField()
|
||||||
body = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
|
body = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE,
|
||||||
markup_choices=WIKI_MARKUP_TYPES)
|
markup_choices=WIKI_MARKUP_TYPES,
|
||||||
|
escape_html=ESCAPE_HTML)
|
||||||
comment = models.CharField(max_length=200, blank=True)
|
comment = models.CharField(max_length=200, blank=True)
|
||||||
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
@ -89,5 +108,10 @@ class ArticleVersion(models.Model):
|
|||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return '%s rev #%s' % (self.article, self.number)
|
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):
|
def get_absolute_url(self):
|
||||||
return reverse('article_version', args=[self.article.title, self.number])
|
return reverse('article_version', args=[self.article.title, self.number])
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
{% if article and mod_form %}
|
{% if article and mod_form %}
|
||||||
<div class="article_moderation">
|
<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>
|
<ul>
|
||||||
<li>{{mod_form.status.label_tag}} {{ mod_form.status }}</li>
|
<li>{{mod_form.status.label_tag}} {{ mod_form.status }}</li>
|
||||||
<li>
|
<li>
|
||||||
@ -16,7 +16,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" action="{% url rename_article article.title %}">
|
<form method="POST" action="{% url "rename_article" article.title %}">
|
||||||
<ul>
|
<ul>
|
||||||
{{ rename_form.as_ul}}
|
{{ rename_form.as_ul}}
|
||||||
<li>
|
<li>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<h2 class="article_title">
|
<h2 class="article_title">
|
||||||
{% block 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}}
|
{{article.display_title}}
|
||||||
|
|
||||||
{% if article.is_deleted %} [deleted] {% endif %}
|
{% if article.is_deleted %} [deleted] {% endif %}
|
||||||
@ -42,10 +42,10 @@
|
|||||||
<div class="article_meta">
|
<div class="article_meta">
|
||||||
{% block article_meta %}
|
{% block article_meta %}
|
||||||
{% if article.editable %}
|
{% if article.editable %}
|
||||||
<a href="{% url edit_article article.title %}">edit article</a> |
|
<a href="{% url "edit_article" article.title %}">edit article</a> |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if article %}
|
{% if article %}
|
||||||
<a href="{% url article_history article.title %}">view history</a>
|
<a href="{% url "article_history" article.title %}">view history</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,13 +10,14 @@
|
|||||||
|
|
||||||
{% block article_meta %}
|
{% block article_meta %}
|
||||||
{% if article %}
|
{% if article %}
|
||||||
<a href="{% url view_article article.title %}">view article</a> |
|
<a href="{% url "view_article" article.title %}">view article</a> |
|
||||||
<a href="{% url article_history article.title %}">view history</a>
|
<a href="{% url "article_history" article.title %}">view history</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block article_body %}
|
{% block article_body %}
|
||||||
<form method="POST" action=".">
|
<form method="POST" action=".">
|
||||||
|
{% csrf_token %}
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{form.body}}</li>
|
<li>{{form.body}}</li>
|
||||||
<li>{{form.comment.label_tag}} {{ form.comment }} </li>
|
<li>{{form.comment.label_tag}} {{ form.comment }} </li>
|
||||||
|
@ -12,6 +12,32 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block article_body %}
|
{% 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>
|
<table>
|
||||||
<thead> <tr>
|
<thead> <tr>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
@ -46,4 +72,5 @@
|
|||||||
<span>Compare Selected Versions</span>
|
<span>Compare Selected Versions</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
329
markupwiki/tests.py
Normal file
329
markupwiki/tests.py
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
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.defaults import *
|
from django.conf.urls import *
|
||||||
from markupwiki.feeds import LatestEditsFeed, LatestArticleEditsFeed
|
from markupwiki.feeds import LatestEditsFeed, LatestArticleEditsFeed
|
||||||
|
|
||||||
WIKI_REGEX = r'^(?P<title>.+)'
|
WIKI_REGEX = r'^(?P<title>.+)'
|
||||||
@ -12,5 +12,6 @@ urlpatterns = patterns('markupwiki.views',
|
|||||||
url(WIKI_REGEX + '/history/$', 'article_history', name='article_history'),
|
url(WIKI_REGEX + '/history/$', 'article_history', name='article_history'),
|
||||||
url(WIKI_REGEX + '/history/(?P<n>\d+)/$', 'view_article', name='article_version'),
|
url(WIKI_REGEX + '/history/(?P<n>\d+)/$', 'view_article', name='article_version'),
|
||||||
url(WIKI_REGEX + '/diff/$', 'article_diff', name='article_diff'),
|
url(WIKI_REGEX + '/diff/$', 'article_diff', name='article_diff'),
|
||||||
|
url(WIKI_REGEX + '/revert/$', 'revert', name='revert'),
|
||||||
url(WIKI_REGEX + '/$', 'view_article', name='view_article'),
|
url(WIKI_REGEX + '/$', 'view_article', name='view_article'),
|
||||||
)
|
)
|
||||||
|
@ -7,20 +7,7 @@ from django.core.urlresolvers import reverse
|
|||||||
|
|
||||||
link_re = re.compile('\[\[(?P<link>.*?)(?:\|(?P<name>.*?))?\]\]')
|
link_re = re.compile('\[\[(?P<link>.*?)(?:\|(?P<name>.*?))?\]\]')
|
||||||
|
|
||||||
__sample_content = '''
|
def _link_repl_func(match_obj):
|
||||||
this is a sample
|
|
||||||
|
|
||||||
[[testlink]]
|
|
||||||
|
|
||||||
[[testlink|with a name]]
|
|
||||||
|
|
||||||
[[another test link]]
|
|
||||||
|
|
||||||
[[multi
|
|
||||||
line]]
|
|
||||||
'''
|
|
||||||
|
|
||||||
def link_repl_func(match_obj):
|
|
||||||
gd = match_obj.groupdict()
|
gd = match_obj.groupdict()
|
||||||
name = gd['name'] or gd['link']
|
name = gd['name'] or gd['link']
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
@ -28,7 +15,7 @@ def link_repl_func(match_obj):
|
|||||||
return '<a href="%s">%s</a>' % (link, name)
|
return '<a href="%s">%s</a>' % (link, name)
|
||||||
|
|
||||||
def make_wiki_links(text):
|
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):
|
def wikify_markup_wrapper(f):
|
||||||
if not hasattr(f, 'wikified_markup'):
|
if not hasattr(f, 'wikified_markup'):
|
||||||
|
@ -8,16 +8,23 @@ from django.contrib import messages
|
|||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from django.utils.functional import wraps
|
from django.utils.functional import wraps
|
||||||
from markupwiki.models import Article, PUBLIC, DELETED, LOCKED
|
from markupwiki.models import Article, ArticleVersion, PUBLIC, DELETED, LOCKED
|
||||||
from markupwiki.forms import ArticleForm, StaffModerationForm, ArticleRenameForm
|
from markupwiki.forms import ArticleForm, StaffModerationForm, ArticleRenameForm
|
||||||
|
|
||||||
CREATE_MISSING_ARTICLE = getattr(settings, 'MARKUPWIKI_CREATE_MISSING_ARTICLES', True)
|
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)
|
||||||
|
|
||||||
def title_check(view):
|
def title_check(view):
|
||||||
def new_view(request, title, *args, **kwargs):
|
def new_view(request, title, *args, **kwargs):
|
||||||
newtitle = title.replace(' ', '_')
|
newtitle = title.replace(' ', '_')
|
||||||
if newtitle != title:
|
if newtitle != title:
|
||||||
return redirect(request.path.replace(title, newtitle))
|
return redirect(request.path.replace(title, newtitle),
|
||||||
|
permanent=True)
|
||||||
else:
|
else:
|
||||||
return view(request, title, *args, **kwargs)
|
return view(request, title, *args, **kwargs)
|
||||||
return wraps(view)(new_view)
|
return wraps(view)(new_view)
|
||||||
@ -70,7 +77,7 @@ def view_article(request, title, n=None):
|
|||||||
context_instance=RequestContext(request))
|
context_instance=RequestContext(request))
|
||||||
|
|
||||||
@title_check
|
@title_check
|
||||||
@login_required
|
@user_passes_test(EDITOR_TEST_FUNC)
|
||||||
def edit_article(request, title):
|
def edit_article(request, title):
|
||||||
''' edit (or create) an article
|
''' edit (or create) an article
|
||||||
|
|
||||||
@ -108,11 +115,13 @@ def edit_article(request, title):
|
|||||||
form = ArticleForm()
|
form = ArticleForm()
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
form = ArticleForm(request.POST)
|
form = ArticleForm(request.POST)
|
||||||
|
user = None if request.user.is_anonymous() else request.user
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
if not article:
|
if not article:
|
||||||
# if article doesn't exist create it and start num at 0
|
# if article doesn't exist create it and start num at 0
|
||||||
article = Article.objects.create(title=title,
|
article = Article.objects.create(title=title,
|
||||||
creator=request.user)
|
creator=user)
|
||||||
num = 0
|
num = 0
|
||||||
else:
|
else:
|
||||||
if not article.get_write_lock(request.user):
|
if not article.get_write_lock(request.user):
|
||||||
@ -126,11 +135,11 @@ def edit_article(request, title):
|
|||||||
# create a new version attached to article specified in name
|
# create a new version attached to article specified in name
|
||||||
version = form.save(False)
|
version = form.save(False)
|
||||||
version.article = article
|
version.article = article
|
||||||
version.author = request.user
|
version.author = user
|
||||||
version.number = num
|
version.number = num
|
||||||
version.save()
|
version.save()
|
||||||
|
|
||||||
article.get_write_lock(request.user, release=True)
|
article.get_write_lock(user or request, release=True)
|
||||||
|
|
||||||
# redirect to view article on save
|
# redirect to view article on save
|
||||||
return redirect(article)
|
return redirect(article)
|
||||||
@ -141,7 +150,7 @@ def edit_article(request, title):
|
|||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@user_passes_test(lambda u: u.is_staff)
|
@user_passes_test(MODERATOR_TEST_FUNC)
|
||||||
@title_check
|
@title_check
|
||||||
def article_status(request, title):
|
def article_status(request, title):
|
||||||
''' POST-only view to update article status (staff-only)
|
''' POST-only view to update article status (staff-only)
|
||||||
@ -153,14 +162,14 @@ def article_status(request, title):
|
|||||||
return redirect(article)
|
return redirect(article)
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@user_passes_test(lambda u: u.is_staff)
|
@user_passes_test(MODERATOR_TEST_FUNC)
|
||||||
@title_check
|
@title_check
|
||||||
def revert(request, title):
|
def revert(request, title):
|
||||||
''' POST-only view to revert article to a specific revision
|
''' POST-only view to revert article to a specific revision
|
||||||
'''
|
'''
|
||||||
article = get_object_or_404(Article, title=title)
|
article = get_object_or_404(Article, title=title)
|
||||||
revision_id = int(request.POST['revision'])
|
revision_id = int(request.POST['revision'])
|
||||||
revision = get_object_or_404(revision, number=revision_id)
|
revision = get_object_or_404(article.versions, number=revision_id)
|
||||||
ArticleVersion.objects.create(article=article, author=request.user,
|
ArticleVersion.objects.create(article=article, author=request.user,
|
||||||
number=article.versions.latest().number,
|
number=article.versions.latest().number,
|
||||||
comment='reverted to r%s' % revision_id,
|
comment='reverted to r%s' % revision_id,
|
||||||
@ -169,18 +178,17 @@ def revert(request, title):
|
|||||||
return redirect(article)
|
return redirect(article)
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
@user_passes_test(lambda u: u.is_staff)
|
@user_passes_test(MODERATOR_TEST_FUNC)
|
||||||
@title_check
|
@title_check
|
||||||
def rename(request, title):
|
def rename(request, title):
|
||||||
''' POST-only view to rename article '''
|
''' POST-only view to rename article '''
|
||||||
article = get_object_or_404(Article, title=title)
|
article = get_object_or_404(Article, title=title)
|
||||||
new_title = request.POST['new_title']
|
new_title = request.POST['new_title']
|
||||||
article.title = new_title
|
article.title = new_title.replace(' ', '_')
|
||||||
print new_title
|
|
||||||
article.save()
|
article.save()
|
||||||
new_article = Article.objects.create(title=title, creator=request.user,
|
new_article = Article.objects.create(title=title, creator=request.user,
|
||||||
redirect_to=article)
|
redirect_to=article)
|
||||||
return redirect(new_article)
|
return redirect(article)
|
||||||
|
|
||||||
@title_check
|
@title_check
|
||||||
def article_history(request, title):
|
def article_history(request, title):
|
||||||
|
2
setup.py
2
setup.py
@ -4,7 +4,7 @@ long_description = open('README.rst').read()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-markupwiki',
|
name='django-markupwiki',
|
||||||
version="0.2.0",
|
version="0.3.0",
|
||||||
packages=['markupwiki'],
|
packages=['markupwiki'],
|
||||||
package_dir={'markupwiki': 'markupwiki'},
|
package_dir={'markupwiki': 'markupwiki'},
|
||||||
package_data={'markupwiki': ['templates/markupwiki/*.html']},
|
package_data={'markupwiki': ['templates/markupwiki/*.html']},
|
||||||
|
Loading…
Reference in New Issue
Block a user