Commit 832d7903 by benjaoming

Loads of changes in permission system. Many aspects are now configurable.

parent 495d70ee
......@@ -16,7 +16,8 @@ def get_notifications(request, latest_id=None, is_viewed=False, max_results=10):
if not latest_id is None:
notifications = notifications.filter(latest_id__gt=latest_id)
notifications = notifications.prefetch_related('subscription')
notifications = notifications[:max_results]
from django.contrib.humanize.templatetags.humanize import naturaltime
......
......@@ -28,15 +28,26 @@ LOG_IPS_USERS = getattr(django_settings, 'WIKI_LOG_IPS_USERS', False)
# PERMISSIONS AND ACCOUNT HANDLING #
####################################
# NB! None of these callables need to handle anonymous users as they are treated
# in separate settings...
# A function returning True/False if a user has permission to assign
# permissions on an article
# Relevance: changing owner and group membership
CAN_ASSIGN = getattr(django_settings, 'WIKI_CAN_ASSIGN', lambda article, user: user.has_perm('wiki.assign'))
# A function returning True/False if the owner of an article has permission to change
# the group to a user's own groups
# Relevance: changing group membership
CAN_ASSIGN_OWNER = getattr(django_settings, 'WIKI_ASSIGN_OWNER', lambda article, user: False)
# A function returning True/False if a user has permission to change
# read/write access for groups and others
CAN_CHANGE_PERMISSIONS = getattr(django_settings, 'WIKI_CAN_CHANGE_PERMISSIONS', lambda article, user: article.owner == user or user.has_perm('wiki.assign'))
# Specifies if a user has access to soft deletion of articles
CAN_DELETE = getattr(django_settings, 'WIKI_CAN_DELETE', lambda article, user: article.can_write(user=user))
# A function returning True/False if a user has permission to change
# moderate, ie. lock articles and permanently delete content.
CAN_MODERATE = getattr(django_settings, 'WIKI_CAN_MODERATE', lambda article, user: user.has_perm('wiki.moderate'))
......
from wiki.conf import settings
# Article settings.
def can_assign(article, user):
return not user.is_anonymous() and settings.CAN_ASSIGN(article, user)
def can_assign_owner(article, user):
return not user.is_anonymous() and settings.CAN_ASSIGN_OWNER(article, user)
def can_change_permissions(article, user):
return not user.is_anonymous() and settings.CAN_CHANGE_PERMISSIONS(article, user)
def can_delete(article, user):
return not user.is_anonymous() and settings.CAN_DELETE(article, user)
def can_moderate(article, user):
return not user.is_anonymous() and settings.CAN_MODERATE(article, user)
def can_admin(article, user):
return not user.is_anonymous() and settings.CAN_ADMIN(article, user)
......@@ -20,7 +20,17 @@ def json_view(func):
return response
return wrap
def get_article(func=None, can_read=True, can_write=False, deleted_contents=False, not_locked=False):
def response_forbidden(request, article, urlpath):
if request.user.is_anonymous():
return redirect(django_settings.LOGIN_URL)
else:
c = RequestContext(request, {'article': article,
'urlpath' : urlpath})
return HttpResponseForbidden(render_to_string("wiki/permission_denied.html", context_instance=c))
def get_article(func=None, can_read=True, can_write=False,
deleted_contents=False, not_locked=False,
can_delete=False, can_moderate=False):
"""View decorator for processing standard url keyword args: Intercepts the
keyword args path or article_id and looks up an article, calling the decorated
func with this ID.
......@@ -88,20 +98,6 @@ def get_article(func=None, can_read=True, can_write=False, deleted_contents=Fals
else:
raise TypeError('You should specify either article_id or path')
if can_read and not article.can_read(user=request.user):
if request.user.is_anonymous():
return redirect(django_settings.LOGIN_URL)
else:
c = RequestContext(request, {'urlpath' : urlpath})
return HttpResponseForbidden(render_to_string("wiki/permission_denied.html", context_instance=c))
if can_write and not article.can_write(user=request.user):
if request.user.is_anonymous():
return redirect(django_settings.LOGIN_URL)
else:
c = RequestContext(request, {'urlpath' : urlpath})
return HttpResponseForbidden(render_to_string("wiki/permission_denied.html", context_instance=c))
# If the article has been deleted, show a special page.
if not deleted_contents and article.current_revision and article.current_revision.deleted:
if urlpath:
......@@ -109,11 +105,20 @@ def get_article(func=None, can_read=True, can_write=False, deleted_contents=Fals
else:
return redirect('wiki:deleted', article_id=article.id)
# The article is locked and this request is invalid because the view specified
# not to be requested for locked contents
if article.current_revision.locked and not_locked:
c = RequestContext(request, {'urlpath' : urlpath})
return HttpResponseForbidden(render_to_string("wiki/permission_denied.html", context_instance=c))
return response_forbidden(request, article, urlpath)
if can_read and not article.can_read(user=request.user):
return response_forbidden(request, article, urlpath)
if can_write and not article.can_write(user=request.user):
return response_forbidden(request, article, urlpath)
if can_delete and not article.can_delete(request.user):
return response_forbidden(request, article, urlpath)
if can_moderate and not article.can_moderate(request.user):
return response_forbidden(request, article, urlpath)
kwargs['urlpath'] = urlpath
......@@ -124,5 +129,6 @@ def get_article(func=None, can_read=True, can_write=False, deleted_contents=Fals
else:
return lambda func: get_article(func, can_read=can_read, can_write=can_write,
deleted_contents=deleted_contents,
not_locked=not_locked)
not_locked=not_locked,can_delete=can_delete,
can_moderate=can_moderate)
......@@ -14,6 +14,7 @@ from wiki.core.diff import simple_merge
from django.forms.widgets import HiddenInput
from wiki.core.plugins.base import PluginSettingsFormMixin
from django.contrib.auth.models import User
from wiki.core import permissions
class SpamProtectionMixin():
......@@ -24,7 +25,10 @@ class SpamProtectionMixin():
current_revision can be any object inheriting from models.BaseRevisionMixin
"""
ipaddress = request.META.get('REMOTE_ADDR', None)
if not ipaddress == "127.0.0.1":
raise forms.ValidationError(_('Only localhost... muahahaha'))
# TODO: Finish this stuff and integrate it in forms....
class CreateRootForm(forms.Form):
......@@ -269,25 +273,25 @@ class PermissionsForm(PluginSettingsFormMixin, forms.ModelForm):
kwargs['instance'] = article
kwargs['initial'] = {'locked': article.current_revision.locked}
super(PermissionsForm, self).__init__(*args, **kwargs)
self.can_change_groups = True
self.can_change_groups = False
self.can_assign = False
if request.user.has_perm("wiki.assign"):
if permissions.can_assign(article, request.user):
self.can_assign = True
self.fields['group'].queryset = models.Group.objects.all()
elif permissions.can_assign_owner(article, request.user):
self.fields['group'].queryset = models.Group.objects.filter(user=request.user)
self.can_change_groups = True
else:
self.fields['group'].widget = forms.HiddenInput()
self.fields['group_read'].widget = forms.HiddenInput()
self.fields['group_write'].widget = forms.HiddenInput()
if not self.can_assign:
self.fields['owner_username'].widget = forms.HiddenInput()
self.fields['recursive'].widget = forms.HiddenInput()
self.fields['locked'].widget = forms.HiddenInput()
groups = models.Group.objects.filter(user=request.user)
self.fields['group'].queryset = groups
# Sanity: If somehow the article belongs to a group that the
# owner is not a member of, don't let the owner make any decisions
# for group permissions.
if article.group and not request.user in article.group.user_set.all():
self.can_change_groups = False
self.fields['group'].widget = forms.HiddenInput()
self.fields['group_read'].widget = forms.HiddenInput()
self.fields['group_write'].widget = forms.HiddenInput()
self.fields['owner_username'].initial = article.owner.username if article.owner else ""
......
......@@ -103,7 +103,7 @@ class ArticleManager(models.Manager):
class ArticleFkManager(models.Manager):
def get_empty_query_set(self):
return ArticleFkEmptyQuerySet()
return ArticleFkEmptyQuerySet(model=self.model)
def get_query_set(self):
return ArticleFkQuerySet(self.model, using=self._db)
def active(self):
......
......@@ -7,7 +7,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from wiki.conf import settings
from wiki.core import article_markdown
from wiki.core import article_markdown, permissions
from wiki.core.plugins import registry as plugin_registry
from wiki import managers
from mptt.models import MPTTModel
......@@ -39,36 +39,57 @@ class Article(models.Model):
other_read = models.BooleanField(default=True, verbose_name=_(u'others read access'))
other_write = models.BooleanField(default=True, verbose_name=_(u'others write access'))
def can_read(self, user=None, group=None):
is_other = (user and not user.is_anonymous() ) or settings.ANONYMOUS
if is_other and self.other_read:
# TODO: Do not use kwargs, it can lead to dangerous situations with bad
# permission checking patterns. Also, since there are no other keywords,
# it doesn't make much sense.
def can_read(self, user=None):
# Deny reading access to deleted articles if user has no delete access
if self.current_revision and self.current_revision.deleted and not self.can_delete(user):
return False
# Check access for other users...
if user.is_anonymous() and not settings.ANONYMOUS:
return False
elif self.other_read:
return True
elif user.is_anonymous():
return False
if user == self.owner:
return True
if self.group_read:
if self.group and group == self.group:
return True
if self.group and user and user.groups.filter(id=self.group.id):
if self.group and user.groups.filter(id=self.group.id):
return True
if user and user.has_perm('wiki_moderator'):
if self.can_moderate(user):
return True
return False
def can_write(self, user=None, group=None):
is_other = (user and not user.is_anonymous() ) or settings.ANONYMOUS_WRITE
if is_other and self.other_write:
def can_write(self, user=None):
# Deny writing access to deleted articles if user has no delete access
if self.current_revision and self.current_revision.deleted and not self.can_delete(user):
return False
# Check access for other users...
if user.is_anonymous() and not settings.ANONYMOUS_WRITE:
return False
elif self.other_write:
return True
elif user.is_anonymous():
return False
if user == self.owner:
return True
if self.group_write:
if self.group and group == self.group:
return True
if self.group and user and user.groups.filter(id=self.group.id):
return True
if user and user.has_perm('wiki_moderator'):
if self.can_moderate(user):
return True
return False
def can_delete(self, user):
return permissions.can_delete(self, user)
def can_moderate(self, user):
return permissions.can_moderate(self, user)
def can_assign(self, user):
return permissions.can_assign(self, user)
def descendant_objects(self):
"""NB! This generator is expensive, so use it with care!!"""
for obj in self.articleforobject_set.filter(is_mptt=True):
......@@ -148,7 +169,7 @@ class Article(models.Model):
class Meta:
app_label = settings.APP_LABEL
permissions = (
("moderator", "Can edit all articles and lock/unlock/restore"),
("moderate", "Can edit all articles and lock/unlock/restore"),
("assign", "Can change ownership of any article"),
("grant", "Can assign permissions to other users"),
)
......
......@@ -42,6 +42,16 @@ class ArticlePlugin(models.Model):
created = models.DateTimeField(auto_now_add=True)
# Permission methods - you should override these, if they don't fit your logic.
def can_read(self, **kwargs):
return self.article.can_read(**kwargs)
def can_write(self, **kwargs):
return self.article.can_write(**kwargs)
def can_delete(self, user):
return self.article.can_delete(user)
def can_moderate(self, user):
return self.article.can_moderate(user)
def purge(self):
"""Remove related contents completely, ie. media files."""
pass
......@@ -52,8 +62,15 @@ class ArticlePlugin(models.Model):
class ReusablePlugin(ArticlePlugin):
"""Extend from this model if you have a plugin that may be related to many
articles. Please note that the ArticlePlugin.article ForeignKey STAYS! This
is in order to maintain an explicit set of permissions. If you do not like this,
you can override can_read and can_write."""
is in order to maintain an explicit set of permissions.
In general, it's quite complicated to maintain plugin content that's shared
between different articles. The best way to go is to avoid this. For inspiration,
look at wiki.plugins.attachments
You might have to override the permission methods (can_read, can_write etc.)
if you have certain needs for logic in your reusable plugin.
"""
# The article on which the plugin was originally created.
# Used to apply permissions.
ArticlePlugin.article.on_delete=models.SET_NULL
......@@ -64,17 +81,17 @@ class ReusablePlugin(ArticlePlugin):
articles = models.ManyToManyField(Article, related_name='shared_plugins_set')
# Permission methods - you may override these, if they don't fit your logic.
# Since the article relation may be None, we have to check for this
# before handling permissions....
def can_read(self, **kwargs):
if self.article:
return self.article.can_read(**kwargs)
return False
return self.article.can_read(**kwargs) if self.article else False
def can_write(self, **kwargs):
if self.article:
return self.article.can_write(**kwargs)
return False
return self.article.can_write(**kwargs) if self.article else False
def can_delete(self, user):
return self.article.can_delete(user) if self.article else False
def can_moderate(self, user):
return self.article.can_moderate(user) if self.article else False
def save(self, *args, **kwargs):
# Automatically make the original article the first one in the added set
......@@ -151,12 +168,6 @@ class RevisionPlugin(ArticlePlugin):
'If you need to do a roll-back, simply change the value of this field.'),
)
# Permissions... overwrite if necessary
def can_read(self, **kwargs):
return self.article.can_read(**kwargs)
def can_write(self, **kwargs):
return self.article.can_write(**kwargs)
def add_revision(self, new_revision, save=True):
"""
Sets the properties of a revision and ensures its the current
......
......@@ -26,7 +26,7 @@ class AttachmentPreprocessor(markdown.preprocessors.Preprocessor):
attachment_id = m.group('id').strip()
try:
attachment = models.Attachment.objects.get(articles=self.markdown.article,
id=attachment_id)
id=attachment_id, current_revision__deleted=False)
url = reverse('wiki:attachments_download', kwargs={'article_id': self.markdown.article.id,
'attachment_id':attachment.id,})
line = line.replace(m.group(1), u"""<span class="attachment"><a href="%s" title="%s">%s</a>""" %
......
......@@ -28,7 +28,10 @@ class Attachment(ReusablePlugin):
if not settings.ANONYMOUS and (not user or user.is_anonymous()):
return False
return ReusablePlugin.can_write(self, **kwargs)
def can_delete(self, user):
return self.can_write(user=user)
class Meta:
verbose_name = _(u'attachment')
verbose_name_plural = _(u'attachments')
......
......@@ -23,7 +23,7 @@
{% if revision.deleted %}<span class="badge badge-important">{% trans "deleted" %}</span>{% endif %}
</td>
<td>
{% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{% include "wiki/includes/revision_info.html" with revision=attachment.current_revision hidedate=1 hidenumber=1 %}
</td>
<td>{{ revision.description|default:_("<em>No description</em>")|safe }}</td>
<td>{{ revision.get_filename }}</td>
......
......@@ -6,7 +6,7 @@
{% block wiki_contents_tab %}
<div class="row-fluid">
<div class="span7">
<div class="span8">
<p class="lead">{% trans "The following files are available for this article. Copy the markdown tag to directly refer to a file from the article text." %}</p>
{% for attachment in attachments %}
<table class="table table-bordered table-striped" style="width: 100%;">
......@@ -26,7 +26,7 @@
<th>{% trans "Markdown tag" %}</th>
<th>{% trans "Uploaded by" %}</th>
<th>{% trans "Size" %}</th>
<td style="text-align: right;" rowspan="2">
<td style="text-align: right; white-space: nowrap;" rowspan="2">
{% if attachment|can_write:user %}
<p>
{% if not attachment.current_revision.deleted %}
......@@ -59,7 +59,7 @@
<tr>
<td><code>[attachment:{{ attachment.id }}]</code></td>
<td>
{% if attachment.current_revision.user %}{{ attachment.current_revision.user }}{% else %}{% if user|is_moderator %}{{ attachment.current_revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{% include "wiki/includes/revision_info.html" with revision=attachment.current_revision hidedate=1 hidenumber=1 %}
</td>
<td>{{ attachment.current_revision.get_size|filesizeformat }}</td>
</tr>
......@@ -69,7 +69,7 @@
{% endfor %}
</div>
{% if article|can_write:user %}
<div class="span5" style="min-width: 330px;">
<div class="span4" style="min-width: 330px;">
<div class="accordion" id="accordion_upload">
<div class="accordion-group">
......
......@@ -40,7 +40,7 @@
{% if attachment.current_revision.deleted %}<span class="badge badge-important">{% trans "deleted" %}</span>{% endif %}
</td>
<td>
{% if attachment.current_revision.user %}{{ attachment.current_revision.user }}{% else %}{% if user|is_moderator %}{{ attachment.current_revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{% include "wiki/includes/revision_info.html" with revision=attachment.current_revision hidedate=1 hidenumber=1 %}
</td>
<td>{{ attachment.current_revision.file.size|filesizeformat }}</td>
<td style="text-align: right">
......
# -*- coding: utf-8 -*-
from django.conf import settings as django_settings
from django.contrib import messages
from django.db import transaction
from django.db.models import Q
......@@ -11,9 +10,8 @@ from django.views.generic.base import TemplateView, View
from django.views.generic.edit import FormView
from django.views.generic.list import ListView
from wiki.conf import settings as wiki_settings
from wiki.core.http import send_file
from wiki.decorators import get_article
from wiki.decorators import get_article, response_forbidden
from wiki.plugins.attachments import models, settings, forms
from wiki.views.mixins import ArticleMixin
......@@ -25,7 +23,7 @@ class AttachmentView(ArticleMixin, FormView):
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, *args, **kwargs):
if request.user.has_perm('wiki.moderator'):
if article.can_moderate(request.user):
self.attachments = models.Attachment.objects.filter(articles=article).order_by('current_revision__deleted', 'original_filename')
else:
self.attachments = models.Attachment.objects.active().filter(articles=article)
......@@ -36,8 +34,9 @@ class AttachmentView(ArticleMixin, FormView):
# WARNING! The below decorator silences other exceptions that may occur!
@transaction.commit_manually
def form_valid(self, form):
if self.request.user.is_anonymous() and not settings.ANONYMOUS:
return redirect(django_settings.LOGIN_URL)
if (self.request.user.is_anonymous() and not settings.ANONYMOUS or
not self.article.can_write(self.request.user)):
return response_forbidden(self.request, self.article, self.urlpath)
try:
attachment_revision = form.save(commit=False)
......@@ -74,7 +73,7 @@ class AttachmentHistoryView(ArticleMixin, TemplateView):
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
if request.user.has_perm('wiki.moderator'):
if article.can_moderate(request.user):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
else:
self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
......@@ -92,11 +91,14 @@ class AttachmentReplaceView(ArticleMixin, FormView):
form_class = forms.AttachmentForm
template_name="wiki/plugins/attachments/replace.html"
@method_decorator(get_article(can_read=True))
@method_decorator(get_article(can_write=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
if not self.attachment.can_write(user=request.user):
return redirect(wiki_settings.LOGIN_URL)
if self.request.user.is_anonymous() and not settings.ANONYMOUS:
return response_forbidden(request, article, kwargs.get('urlpath', None))
if article.can_moderate(request.user):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
else:
self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
return super(AttachmentReplaceView, self).dispatch(request, article, *args, **kwargs)
def form_valid(self, form):
......@@ -138,7 +140,10 @@ class AttachmentDownloadView(ArticleMixin, View):
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
if article.can_moderate(request.user):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
else:
self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
revision_id = kwargs.get('revision_id', None)
if revision_id:
self.revision = get_object_or_404(models.AttachmentRevision, id=revision_id, attachment__articles=article)
......@@ -162,7 +167,7 @@ class AttachmentChangeRevisionView(ArticleMixin, View):
@method_decorator(get_article(can_write=True))
def dispatch(self, request, article, attachment_id, revision_id, *args, **kwargs):
if request.user.has_perm('wiki.moderator'):
if article.can_moderate(request.user):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
else:
self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
......@@ -188,8 +193,9 @@ class AttachmentAddView(ArticleMixin, View):
return super(AttachmentAddView, self).dispatch(request, article, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.attachment.articles.add(self.article)
self.attachment.save()
if self.attachment.articles.filter(id=self.article.id):
self.attachment.articles.add(self.article)
self.attachment.save()
messages.success(self.request, _(u'Added a reference to "%(att)s" from "%(art)s".') %
{'att': self.attachment.original_filename,
'art': self.article.current_revision.title})
......@@ -203,10 +209,9 @@ class AttachmentDeleteView(ArticleMixin, FormView):
@method_decorator(get_article(can_write=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
if request.user.has_perm("wiki.moderator"):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
else:
self.attachment = get_object_or_404(models.Attachment.objects.active(), id=attachment_id, articles=article)
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
if not self.attachment.can_delete(request.user):
return response_forbidden(request, article, kwargs.get('urlpath', None))
return super(AttachmentDeleteView, self).dispatch(request, article, *args, **kwargs)
def form_valid(self, form):
......@@ -254,7 +259,6 @@ class AttachmentSearchView(ArticleMixin, ListView):
qs = qs.filter(Q(original_filename__contains=self.query) |
Q(current_revision__description__contains=self.query) |
Q(article__current_revision__title__contains=self.query))
qs = qs.exclude(articles=self.article)
return qs
def get_context_data(self, **kwargs):
......
......@@ -33,6 +33,9 @@ class Image(RevisionPlugin):
return False
return RevisionPlugin.can_write(self, **kwargs)
def can_delete(self, user):
return self.can_write(user=user)
class Meta:
verbose_name = _(u'image')
verbose_name_plural = _(u'images')
......
......@@ -45,7 +45,7 @@
{% trans "Remove image" %}
</a>
{% endif %}
{% if user|is_moderator %}
{% if article|can_moderate:user %}
<br />
<a href="{% url 'wiki:images_purge' path=urlpath.path article_id=article.id image_id=image.id %}">
<span class="icon-trash"></span>
......
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, redirect
from django.utils.decorators import method_decorator
......@@ -27,8 +26,8 @@ class ImageView(ArticleMixin, ListView):
return super(ImageView, self).dispatch(request, article, *args, **kwargs)
def get_queryset(self):
if (self.request.user.has_perm('wiki.moderator') or
self.article.owner == self.request.user):
if (self.article.can_moderate(self.request.user) or
self.article.can_delete(self.request.user)):
images = models.Image.objects.filter(article=self.article)
else:
images = models.Image.objects.filter(article=self.article,
......@@ -76,8 +75,7 @@ class PurgeView(ArticleMixin, FormView):
permanent = False
form_class = forms.PurgeForm
@method_decorator(permission_required('wiki.moderator', login_url=wiki_settings.LOGIN_URL))
@method_decorator(get_article(can_write=True))
@method_decorator(get_article(can_write=True, can_moderate=True))
def dispatch(self, request, article, *args, **kwargs):
self.image = get_object_or_404(models.Image, article=article,
id=kwargs.get('image_id', None))
......
......@@ -4,11 +4,13 @@ from django.utils.decorators import method_decorator
class QueryUrlPath(View):
# TODO: get_article does not actually support JSON responses
@method_decorator(json_view)
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, *args, **kwargs):
max_num = kwargs.pop('max_num', 20)
# TODO: Move this import when
# TODO: Move this import when circularity issue is resolved
# https://github.com/benjaoming/django-wiki/issues/23
from wiki import models
query = request.GET.get('query', None)
......
* Create signal for new articles and automatically subscribe users creating the article to notifications?
......@@ -22,11 +22,13 @@
#id_title {font-size: 20px; height: 30px; padding: 6px; width: 98%;}
#id_summary {width: 98%; padding: 6px;}
.table { font-size: 90%;}
#article_edit_form label {max-width: 100px;}
#article_edit_form .controls {margin-left: 120px;}
.form-horizontal label { font-size: 16px; font-weight: normal; color: #777;}
.settings-form label {min-width: 250px; font-size: inherit; font-weight: normal;}
.settings-form .controls {margin-left: 270px;}
.settings-form select {}
......
......@@ -19,7 +19,7 @@
<div class="row-fluid">
{% if not article.current_revision.locked or user|is_moderator %}
{% if not article.current_revision.locked or article|can_delete:user %}
<div class="span6">
<div class="well">
<h2>{% trans "Restore" %}</h2>
......@@ -34,7 +34,7 @@
</div>
{% endif %}
{% if user|is_moderator %}
{% if article|can_moderate:user %}
<div class="span6">
<div class="well">
<h2>{% trans "Purge deletion" %}</h2>
......
......@@ -34,12 +34,14 @@
<a class="btn btn-large btn-primary" onclick="document.getElementById('article_edit_form').target=''; document.getElementById('article_edit_form').action='{% url 'wiki:edit' path=urlpath.path article_id=article.id %}'; $('#article_edit_form').submit();" href="#">
<span class="icon-ok"></span>
{% trans "Save changes" %}
</button>
</a>
{% if article|can_delete:user %}
<a href="{% url 'wiki:delete' path=urlpath.path article_id=article.id %}" class="pull-right btn">
<span class="icon-trash"></span>
{% trans "Delete article" %}
</a>
{% endif %}
</div>
<div class="modal hide fade" id="previewModal" style="width: 80%; min-height: 500px; margin-left: -40%;">
......
......@@ -7,7 +7,7 @@
{% load wiki_tags i18n %}
{{ revision.created }} (#{{ revision.revision_number }}) {% trans "by" %} {% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{% if not hidedate %}{{ revision.created }}{% endif %} {% if not hidenumber %}(#{{ revision.revision_number }}) {% trans "by" %}{% endif %} {% if revision.user %}{{ revision.user }}{% else %}{% if article|can_moderate:user %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{% if revision == current_revision %}
<strong>*</strong>
{% endif %}
......
......@@ -63,9 +63,20 @@ def can_read(obj, user):
@register.filter
def can_write(obj, user):
"""Articles and plugins have a can_write method..."""
return obj.can_write(**{'user': user})
return obj.can_write(user=user)
@register.filter
def can_delete(obj, user):
"""Articles and plugins have a can_delete method..."""
return obj.can_delete(user)
@register.filter
def can_moderate(obj, user):
"""Articles and plugins have a can_moderate method..."""
return obj.can_moderate(user)
@register.filter
def is_moderator(user):
"""Tells if a user is a moderator"""
return user.has_perm('wiki.moderator')
return user.has_perm('wiki.moderate')
......@@ -21,8 +21,7 @@ from django.core.urlresolvers import reverse
from django.db import transaction
from wiki.core.exceptions import NoRootURL
from django_notify.decorators import disable_notify
from django.http import HttpResponseForbidden
from django.template.loader import render_to_string
from wiki.core import permissions
class ArticleView(ArticleMixin, TemplateView):
......@@ -83,14 +82,13 @@ class Create(FormView, ArticleMixin):
'other_read': self.article.other_read,
'other_write': self.article.other_write,
})
# TODO: Subscribe user to new article and send notifications that user was subscribed.
messages.success(self.request, _(u"New article '%s' created.") % self.newpath.article.current_revision.title)
transaction.commit()
# TODO: Handle individual exceptions better and give good feedback.
except Exception, e:
transaction.rollback()
if self.request.user.has_perm('wiki.moderator'):
if self.request.user.is_superuser():
messages.error(self.request, _(u"There was an error creating this article: %s") % str(e))
else:
messages.error(self.request, _(u"There was an error creating this article."))
......@@ -117,7 +115,7 @@ class Delete(FormView, ArticleMixin):
form_class = forms.DeleteForm
template_name="wiki/delete.html"
@method_decorator(get_article(can_write=True, not_locked=True))
@method_decorator(get_article(can_write=True, not_locked=True, can_delete=True))
def dispatch(self, request, article, *args, **kwargs):
return self.dispatch1(request, article, *args, **kwargs)
......@@ -147,7 +145,7 @@ class Delete(FormView, ArticleMixin):
def get_form(self, form_class):
form = super(Delete, self).get_form(form_class)
if self.request.user.has_perm('wiki.moderator'):
if self.article.can_delete(self.request.user):
form.fields['purge'].widget = forms.forms.CheckboxInput()
return form
......@@ -179,7 +177,8 @@ class Delete(FormView, ArticleMixin):
cd = form.cleaned_data
cannot_delete_children = False
if self.children_slice and not self.request.user.has_perm('wiki.moderator'):
can_moderate = self.article.can_moderate(self.request.user)
if self.children_slice and not can_moderate:
cannot_delete_children = True
if self.cannot_delete_root or cannot_delete_children:
......@@ -189,7 +188,7 @@ class Delete(FormView, ArticleMixin):
# First, remove children
self.delete_children(purge=cd['purge'])
if self.request.user.has_perm('wiki.moderator') and cd['purge']:
if can_moderate and cd['purge']:
self.article.delete()
messages.success(self.request, _(u'This article together with all its contents are now completely gone! Thanks!'))
else:
......@@ -206,7 +205,7 @@ class Delete(FormView, ArticleMixin):
def get_context_data(self, **kwargs):
cannot_delete_children = False
if self.children_slice and not self.request.user.has_perm('wiki.moderator'):
if self.children_slice and not self.article.can_moderate(self.request.user):
cannot_delete_children = True
kwargs['delete_form'] = kwargs.pop('form', None)
......@@ -336,7 +335,8 @@ class Deleted(Delete):
# Restore
if (request.GET.get('restore', False) and
(not article.current_revision.locked or request.user.has_perm('wiki.moderator'))):
(not article.current_revision.locked and article.can_delete(request.user)) or
article.can_moderate(request.user)):
self.delete_children(restore=True)
revision = models.ArticleRevision()
revision.inherit_predecessor(self.article)
......@@ -409,9 +409,13 @@ class Dir(ListView, ArticleMixin):
model = models.URLPath
paginate_by = 30
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, *args, **kwargs):
return super(Dir, self).dispatch(request, article, *args, **kwargs)
def get_queryset(self):
children = self.urlpath.get_children().can_read(self.request.user).select_related_common().order_by('article__current_revision__title')
if not self.request.user.has_perm('wiki.moderator'):
if not self.article.can_moderate(self.request.user):
children = children.active()
return children
......@@ -431,14 +435,6 @@ class Dir(ListView, ArticleMixin):
return kwargs
def get_template_names(self):
#WHY IS THIS CALLED???????
return [self.__class__.template_name]
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, *args, **kwargs):
return super(Dir, self).dispatch(request, article, *args, **kwargs)
class Plugin(View):
......@@ -464,8 +460,7 @@ class Settings(ArticleMixin, TemplateView):
Return all settings forms that can be filled in
"""
settings_forms = [F for F in plugin_registry.get_settings_forms()]
if (self.request.user.has_perm('wiki.assign') or
self.article.owner == self.request.user):
if permissions.can_change_permissions(self.article, self.request.user):
settings_forms.append(self.permission_form_class)
settings_forms.sort(key=lambda form: form.settings_order)
for i in range(len(settings_forms)):
......@@ -627,12 +622,14 @@ def root_create(request):
try:
root = models.URLPath.root()
if not root.article:
# TODO: This is too dangerous... let's say there is no root.article and we end up here,
# then it might cascade to delete a lot of things on an existing installation.... / benjaoming
root.delete()
raise NoRootURL
return redirect('wiki:get', path=root.path)
except NoRootURL:
pass
if not request.user.has_perm('wiki.add_article'):
if not request.user.is_superuser():
return redirect(settings.LOGIN_URL + "?next=" + reverse("wiki:root_create"))
if request.method == 'POST':
create_form = forms.CreateRootForm(request.POST)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment