Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2aff42e4a9 | ||
![]() |
74a4f00ac3 | ||
![]() |
6cc6f46348 | ||
![]() |
236239e774 | ||
![]() |
a20417af0b | ||
![]() |
b674c84144 | ||
![]() |
56f37a55d1 | ||
![]() |
2c3e259b36 | ||
![]() |
d40076186b | ||
![]() |
89713013dc | ||
![]() |
9160ed2fd9 | ||
![]() |
0864edd921 | ||
![]() |
e41f77fa72 | ||
![]() |
aff4f04d5e | ||
![]() |
a255c92198 | ||
![]() |
e9703a0e93 | ||
![]() |
84ea94c78c | ||
![]() |
20a483e754 | ||
![]() |
85f2bac6fd |
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
|
@ -1,7 +1,12 @@
|
||||
0.3.0
|
||||
=====
|
||||
- lots of tests
|
||||
-
|
||||
- new escape_html option
|
||||
- csrf token
|
||||
- autolockarticles management command
|
||||
- null editors
|
||||
- fix revert
|
||||
|
||||
|
||||
0.2.0
|
||||
===================
|
||||
|
@ -13,7 +13,7 @@ might not be for you.
|
||||
Requirements
|
||||
============
|
||||
|
||||
django-markupwiki depends on django >= 1.2a, django-markupfield >= 1.0.0a2 and
|
||||
django-markupwiki depends on django >= 1.2, django-markupfield >= 1.0.0b and
|
||||
libraries for whichever markup options you wish to include.
|
||||
|
||||
|
||||
@ -84,7 +84,9 @@ to customize the behavior.
|
||||
``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.
|
||||
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::
|
||||
|
||||
|
BIN
example/example
BIN
example/example
Binary file not shown.
@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
@ -27,7 +29,7 @@ MIDDLEWARE_CLASSES = (
|
||||
|
||||
ROOT_URLCONF = 'example.urls'
|
||||
|
||||
TEMPLATE_DIRS = ( 'templates', )
|
||||
TEMPLATE_DIRS = ( os.path.join(os.path.dirname(__file__), 'templates'), )
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
@ -37,3 +39,5 @@ INSTALLED_APPS = (
|
||||
'django.contrib.messages',
|
||||
'markupwiki',
|
||||
)
|
||||
|
||||
SITE_ID = 1
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.conf.urls.defaults import *
|
||||
from django.conf.urls import *
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
|
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 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)
|
||||
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
|
||||
WIKI_MARKUP_TYPES = []
|
||||
@ -26,9 +34,9 @@ ARTICLE_STATUSES = (
|
||||
|
||||
class Article(models.Model):
|
||||
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)
|
||||
redirect_to = models.ForeignKey('self', null=True)
|
||||
redirect_to = models.ForeignKey('self', blank=True, null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
@ -42,6 +50,11 @@ 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])
|
||||
|
||||
@ -56,28 +69,33 @@ class Article(models.Model):
|
||||
|
||||
def is_editable_by_user(self, user):
|
||||
if self.status in (LOCKED, DELETED):
|
||||
return user.is_staff
|
||||
return MODERATOR_TEST_FUNC(user)
|
||||
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
|
||||
lock = cache.get(cache_key)
|
||||
if lock:
|
||||
if release:
|
||||
cache.delete(cache_key)
|
||||
return lock == user.id
|
||||
return lock == lock_id
|
||||
|
||||
if not release:
|
||||
cache.set(cache_key, user.id, WRITE_LOCK_SECONDS)
|
||||
cache.set(cache_key, lock_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')
|
||||
author = models.ForeignKey(User, related_name='article_versions', blank=True, null=True)
|
||||
number = models.PositiveIntegerField()
|
||||
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)
|
||||
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
@ -89,6 +107,11 @@ 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,13 +10,14 @@
|
||||
|
||||
{% 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,6 +12,32 @@
|
||||
{% 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>
|
||||
@ -46,4 +72,5 @@
|
||||
<span>Compare Selected Versions</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
from django.conf.urls.defaults import *
|
||||
from django.conf.urls import *
|
||||
from markupwiki.feeds import LatestEditsFeed, LatestArticleEditsFeed
|
||||
|
||||
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/(?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'),
|
||||
)
|
||||
|
@ -8,10 +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, PUBLIC, DELETED, LOCKED
|
||||
from markupwiki.models import Article, ArticleVersion, PUBLIC, DELETED, LOCKED
|
||||
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 new_view(request, title, *args, **kwargs):
|
||||
@ -71,7 +77,7 @@ def view_article(request, title, n=None):
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
@title_check
|
||||
@login_required
|
||||
@user_passes_test(EDITOR_TEST_FUNC)
|
||||
def edit_article(request, title):
|
||||
''' edit (or create) an article
|
||||
|
||||
@ -109,11 +115,13 @@ 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=request.user)
|
||||
creator=user)
|
||||
num = 0
|
||||
else:
|
||||
if not article.get_write_lock(request.user):
|
||||
@ -127,11 +135,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 = request.user
|
||||
version.author = user
|
||||
version.number = num
|
||||
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
|
||||
return redirect(article)
|
||||
@ -142,7 +150,7 @@ def edit_article(request, title):
|
||||
|
||||
|
||||
@require_POST
|
||||
@user_passes_test(lambda u: u.is_staff)
|
||||
@user_passes_test(MODERATOR_TEST_FUNC)
|
||||
@title_check
|
||||
def article_status(request, title):
|
||||
''' POST-only view to update article status (staff-only)
|
||||
@ -154,14 +162,14 @@ def article_status(request, title):
|
||||
return redirect(article)
|
||||
|
||||
@require_POST
|
||||
@user_passes_test(lambda u: u.is_staff)
|
||||
@user_passes_test(MODERATOR_TEST_FUNC)
|
||||
@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(revision, number=revision_id)
|
||||
revision = get_object_or_404(article.versions, number=revision_id)
|
||||
ArticleVersion.objects.create(article=article, author=request.user,
|
||||
number=article.versions.latest().number,
|
||||
comment='reverted to r%s' % revision_id,
|
||||
@ -170,7 +178,7 @@ def revert(request, title):
|
||||
return redirect(article)
|
||||
|
||||
@require_POST
|
||||
@user_passes_test(lambda u: u.is_staff)
|
||||
@user_passes_test(MODERATOR_TEST_FUNC)
|
||||
@title_check
|
||||
def rename(request, title):
|
||||
''' POST-only view to rename article '''
|
||||
|
Loading…
Reference in New Issue
Block a user