Commit 8ce22364 by benjaoming

Deletion for articles: Soft delete and purge. Also soft deletes or purges…

Deletion for articles: Soft delete and purge. Also soft deletes or purges children. A soft deletion creates a new revision in which the article is marked as deleted.
parent c1514ab7
......@@ -17,6 +17,7 @@ Not implemented - will be ASAP
* View source for read-only articles + locked status
* Global moderator permission **Almost done** (need to add grant form for users with *grant* permissions)
* Are you sure you wanna leave this page?
* requirements.txt : are the `>` really correct???
Ideas
=====
......
......@@ -21,4 +21,7 @@ if settings.DEBUG:
from wiki.urls import get_pattern as get_wiki_pattern
from django_notify.urls import get_pattern as get_notify_pattern
urlpatterns += patterns('', (r'^notify/', get_notify_pattern()), (r'', get_wiki_pattern()))
\ No newline at end of file
urlpatterns += patterns('',
(r'^notify/', get_notify_pattern()),
(r'', get_wiki_pattern())
)
\ No newline at end of file
......@@ -20,6 +20,9 @@ LOG_IPS_USERS = getattr(django_settings, 'WIKI_LOG_IPS_USERS', False)
ACCOUNT_HANDLING = getattr(django_settings, 'WIKI_ACCOUNT_HANDLING', True)
# Maximum amount of children to display in a menu before going "+more"
SHOW_MAX_CHILDREN = getattr(django_settings, 'WIKI_SHOW_MAX_CHILDREN', 20)
####################
# PLANNED SETTINGS #
####################
......
......@@ -5,6 +5,7 @@ from django.http import HttpResponse, HttpResponseNotFound
from django.utils import simplejson as json
from wiki.core.exceptions import NoRootURL
from django.contrib.contenttypes.models import ContentType
def json_view(func):
def wrap(request, *args, **kwargs):
......@@ -16,11 +17,11 @@ def json_view(func):
return response
return wrap
def get_article(func=None, can_read=True, can_write=False):
def get_article(func=None, can_read=True, can_write=False, deleted_contents=False):
"""Intercepts the keyword args path or article_id and looks up an article,
calling the decorated func with this ID."""
def the_func(request, *args, **kwargs):
def wrapper(request, *args, **kwargs):
import models
path = kwargs.pop('path', None)
......@@ -38,7 +39,7 @@ def get_article(func=None, can_read=True, can_write=False):
if article_id:
article = get_object_or_404(articles, id=article_id)
try:
urlpath = models.URLPath.objects.get(articles=article)
urlpath = models.URLPath.objects.get(articles__article=article)
except models.URLPath.DoesNotExist, models.URLPath.MultipleObjectsReturned:
urlpath = None
else:
......@@ -62,15 +63,21 @@ def get_article(func=None, can_read=True, can_write=False):
# Somehow article is gone
return_url = reverse('wiki:get', kwargs={'path': urlpath.parent.path})
urlpath.delete()
return return_url
return redirect(return_url)
# 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:
return redirect('wiki:deleted', path=urlpath.path)
else:
return redirect('wiki:deleted', article_id=article.id)
kwargs['urlpath'] = urlpath
return func(request, article, *args, **kwargs)
if func:
return the_func
return wrapper
else:
return lambda func: get_article(func, can_read=can_read, can_write=can_write)
......@@ -189,11 +189,11 @@ class CreateForm(forms.Form):
already_existing_slug = models.URLPath.objects.filter(slug=slug, parent=self.urlpath_parent)
if already_existing_slug:
slug = already_existing_slug[0]
if slug.deleted:
if slug.article and slug.article.deleted:
raise forms.ValidationError(_(u'A deleted article with slug "%s" already exists.') % slug)
else:
raise forms.ValidationError(_(u'A slug named "%s" already exists.') % slug)
return slug
class PermissionsForm(forms.ModelForm):
......@@ -232,14 +232,14 @@ class DeleteForm(forms.Form):
def __init__(self, *args, **kwargs):
self.article = kwargs.pop('article')
self.children = kwargs.pop('children')
self.has_children = kwargs.pop('has_children')
super(DeleteForm, self).__init__(*args, **kwargs)
confirm = forms.BooleanField(required=False,
label=_(u'Confirm'))
purge = forms.BooleanField(widget=HiddenInput(), required=False,
label=_(u'Purge'),
help_text=_(u'Purge the article: Completely remove it (and all its contents) with no undo.'))
help_text=_(u'Purge the article: Completely remove it (and all its contents) with no undo. Purging is a good idea if you want to free the slug such that users can create new articles in its place.'))
revision = forms.ModelChoiceField(models.ArticleRevision.objects.all(),
widget=HiddenInput(), required=False)
......
......@@ -72,13 +72,13 @@ class Article(models.Model):
for decendant in obj.content_object.get_decendants():
yield decendant
def get_children(self, max_num=None):
def get_children(self, max_num=None, **kwargs):
"""NB! This generator is expensive, so use it with care!!"""
cnt = 0
for obj in self.articleforobject_set.filter(is_mptt=True):
for child in obj.content_object.get_children():
for child in obj.content_object.get_children().filter(**kwargs):
cnt += 1
if cnt > max_num: return
if max_num and cnt > max_num: return
yield child
# All recursive permission methods will use decendant_objects to access
......
......@@ -119,10 +119,12 @@ class URLPath(MPTTModel):
return root
@classmethod
def create_article(cls, parent, slug, site=None, title="Root", **kwargs):
def create_article(cls, parent, slug, site=None, title="Root", article_kwargs={}, **kwargs):
"""Utility function:
Create a new urlpath with an article and a new revision for the article"""
if not site: site = Site.objects.get_current()
newpath = cls.objects.create(site=site, parent=parent, slug=slug)
article = Article()
article = Article(**article_kwargs)
article.add_revision(ArticleRevision(title=title, **kwargs),
save=True)
article.add_object_relation(newpath)
......@@ -145,6 +147,7 @@ def on_article_delete(instance, *args, **kwargs):
# But move all descendants to a lost-and-found node.
site = Site.objects.get_current()
# Get the Lost-and-found path or create a new one
try:
lost_and_found = URLPath.objects.get(slug=settings.LOST_AND_FOUND_SLUG,
parent=URLPath.root(),
......@@ -163,7 +166,10 @@ def on_article_delete(instance, *args, **kwargs):
title=_(u"Lost and found")))
for urlpath in URLPath.objects.filter(articles__article=instance, site=site):
# Delete the children
for child in urlpath.get_children():
child.move_to(lost_and_found)
# ...and finally delete the path itself
urlpath.delete()
pre_delete.connect(on_article_delete, Article)
......@@ -5,6 +5,25 @@
{% block pagetitle %}{% trans "Add new article" %}{% endblock %}
{% block wiki_contents %}
{% addtoblock "js" %}
<script type="text/javascript" src="{{ STATIC_URL }}admin/js/urlify.js "></script>
<script type="text/javascript">
//<![CDATA[
(function($) {
$(document).ready(function (){
$("#id_title").keyup(function () {
var e = $("#id_slug")[0];
if(!e._changed) {
e.value = URLify(this.value, 64);
}
});
});
})(jQuery);
//]]>
</script>
{% endaddtoblock %}
{% include "wiki/includes/editormedia.html" %}
<h1 class="page-header">{% trans "Add new article" %}</h1>
......
......@@ -18,33 +18,37 @@
{% endif %}
{% if children %}
{% if delete_children %}
<p class="lead">{% trans "You are deleting an article. This means that its children will loose their connection to the rest of the tree and moved to Lost and Found. Only do this if you know what you're doing." %}</p>
<h2>{% trans "Articles that will be orphans" %}</h2>
<ul>
{% for child in children %}
<li><a href="{% url 'wiki:get' path=child.article.path %}" target="_blank">{{ child.article }}</a></li>
{% for child in delete_children %}
<li><a href="{% url 'wiki:get' article_id=child.article.id %}" target="_blank">{{ child.article }}</a></li>
{% if delete_children_more %}
<li><em>{% trans "...and more!" %}</em></li>
{% endif %}
{% endfor %}
</ul>
{% else %}
<p class="lead">{% trans "You are deleting an article. Please confirm." %}</p>
{% endif %}
{% if not cannot_delete_children %}
<p class="lead">{% trans "You are deleting an article. Please confirm." %}</p>
<form method="POST" class="form-horizontal">
{% wiki_form delete_form %}
<script type="text/javascript">
$('#id_revision').val('{{ article.current_revision.id }}');
</script>
<div class="form-actions">
<a href="{% url 'wiki:get' path=urlpath.path article_id=article.id %}" class="btn btn-large">
<span class="icon-circle-arrow-left"></span>
{% trans "Go back" %}
</a>
<button type="submit" name="save_changes" class="btn btn-critical btn-large">
<button type="submit" name="save_changes" class="btn btn-danger btn-large">
<span class="icon-plus"></span>
{% trans "Delete article" %}
</button>
......
......@@ -64,6 +64,9 @@
{% if revision == article.current_revision %}
<strong>*</strong>
{% endif %}
{% if revision.deleted %}
<span class="badge badge-important">{% trans "deleted" %}</span>
{% endif %}
<div style="color: #CCC;">
<small>
{% if revision.user_message %}
......
......@@ -16,15 +16,18 @@
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
{% for child in urlpath.get_children %}
{% for child in children_slice %}
<li>
<a href="{% url 'wiki:get' path=child.path %}">
{{ child.article.current_revision.title }}
</a>
<a href="{% url 'wiki:get' path=child.path %}">
{{ child.article.current_revision.title }}
</a>
</li>
{% empty %}
<li><a href="#"><em>{% trans "No sub-articles" %}</em></a></li>
{% endfor %}
{% if children_slice_more %}
<li><a href="#"><em>{% trans "...and more" %}</em></a></li>
{% endif %}
<li class="divider"></li>
<li>
<a href="" onclick="alert('TODO')">{% trans "List sub-pages" %} &raquo;</a>
......
......@@ -2,9 +2,11 @@
{% block wiki_contents %}
{% if not preview %}
{% cache 1 article article.current_revision.id %}
{{ article.render }}
{% endcache %}
{% if article.current_revision %}
{% cache 1 article article.current_revision.id %}
{{ article.render }}
{% endcache %}
{% endif %}
{% else %}
{{ content|default:"" }}
{% endif %}
......
......@@ -65,14 +65,23 @@ class Create(FormView, ArticleMixin):
elif settings.LOG_IPS_ANONYMOUS:
ip_address = self.request.META.get('REMOTE_ADDR', None)
try:
self.newpath = models.URLPath.create_article(self.urlpath,
form.cleaned_data['slug'],
title=form.cleaned_data['title'],
content=form.cleaned_data['content'],
user_message=form.cleaned_data['summary'],
user=user,
ip_address=ip_address)
messages.success(self.request, _(u"New article '%s' created.") % self.newpath.article.title)
self.newpath = models.URLPath.create_article(
self.urlpath,
form.cleaned_data['slug'],
title=form.cleaned_data['title'],
content=form.cleaned_data['content'],
user_message=form.cleaned_data['summary'],
user=user,
ip_address=ip_address,
article_kwargs={'owner': user,
'group': self.article.group,
'group_read': self.article.group_read,
'group_write': self.article.group_write,
'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.
......@@ -123,15 +132,6 @@ class Delete(FormView, ArticleMixin):
else:
self.cannot_delete_root = True
# Fetch children if necessary
self.children = []
if not self.cannot_delete_root:
self.children = article.get_children()
self.cannot_delete_children = False
if self.children and not request.user.has_perm('wiki.moderator'):
self.cannot_delete_children = True
return super(Delete, self).dispatch(request, article, *args, **kwargs)
def get_initial(self):
......@@ -146,25 +146,31 @@ class Delete(FormView, ArticleMixin):
def get_form_kwargs(self):
kwargs = FormView.get_form_kwargs(self)
kwargs['article'] = self.article
kwargs['children'] = self.children
kwargs['has_children'] = bool(self.children_slice)
return kwargs
@disable_notify
def delete_children(self, purge=False):
if purge:
for child in self.children:
for child in self.article.get_children(articles__article__current_revision__deleted=False):
child.delete()
else:
for child in self.children:
for child in self.article.get_children(articles__article__current_revision__deleted=False):
revision = models.ArticleRevision()
revision.inherit_predecessor(child)
revision.inherit_predecessor(child.article)
revision.set_from_request(self.request)
revision.automatic_log = _(u'Deleting children of "%s"') % self.article.current_revision.title
revision.deleted = True
child.add_revision(revision)
child.article.add_revision(revision)
def form_valid(self, form):
cd = form.cleaned_data
if self.cannot_delete_root or self.cannot_delete_children:
cannot_delete_children = False
if self.children_slice and not self.request.user.has_perm('wiki.moderator'):
cannot_delete_children = True
if self.cannot_delete_root or cannot_delete_children:
messages.error(self.request, _(u'This article cannot be deleted because it has children or is a root article.'))
return redirect('wiki:get', article_id=self.article.id)
......@@ -177,24 +183,25 @@ class Delete(FormView, ArticleMixin):
else:
revision = models.ArticleRevision()
revision.inherit_predecessor(self.article)
revision.set_from_request(self.request)
revision.deleted = True
self.article.add_revision(revision)
messages.success(self.request, _(u'This article is now marked as deleted! Thanks for keeping the site free from unwanted material!'))
messages.success(self.request, _(u'The article "%s" is now marked as deleted! Thanks for keeping the site free from unwanted material!') % revision.title)
return self.get_success_url()
def get_success_url(self):
return redirect(self.next)
def get_context_data(self, **kwargs):
cannot_delete_children = False
if self.children_slice and not self.request.user.has_perm('wiki.moderator'):
cannot_delete_children = True
kwargs['delete_form'] = kwargs.pop('form', None)
kwargs['cannot_delete_root'] = self.cannot_delete_root
children = []
cnt = 0
for child in self.children:
if cnt > 21: break
children.append(child)
kwargs['children'] = children[:20]
kwargs['children_more'] = len(children) > 20
kwargs['cannot_delete_children'] = self.cannot_delete_children
kwargs['delete_children'] = self.children_slice[:20]
kwargs['delete_children_more'] = len(self.children_slice) > 20
kwargs['cannot_delete_children'] = cannot_delete_children
return super(Delete, self).get_context_data(**kwargs)
......@@ -219,6 +226,7 @@ class Edit(FormView, ArticleMixin):
revision.title = form.cleaned_data['title']
revision.content = form.cleaned_data['content']
revision.user_message = form.cleaned_data['summary']
revision.deleted = False
revision.set_from_request(self.request)
self.article.add_revision(revision)
messages.success(self.request, _(u'A new revision of the article was succesfully added.'))
......@@ -237,6 +245,11 @@ class Edit(FormView, ArticleMixin):
return super(Edit, self).get_context_data(**kwargs)
# TODO: ...
class Source(ArticleMixin, TemplateView):
pass
class History(ListView, ArticleMixin):
template_name="wiki/history.html"
......
from django.views.generic.base import TemplateResponseMixin
from wiki.core import plugins_registry
from wiki.conf import settings
class ArticleMixin(TemplateResponseMixin):
"""A mixin that receives an article object as a parameter (usually from a wiki
......@@ -10,10 +11,17 @@ class ArticleMixin(TemplateResponseMixin):
def dispatch(self, request, article, *args, **kwargs):
self.urlpath = kwargs.pop('urlpath', None)
self.article = article
self.children_slice = []
for child in self.article.get_children(max_num=settings.SHOW_MAX_CHILDREN+1,
articles__article__current_revision__deleted=False):
self.children_slice.append(child)
return super(ArticleMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['urlpath'] = self.urlpath
kwargs['article'] = self.article
kwargs['plugins'] = plugins_registry._cache.values()
kwargs['children_slice'] = self.children_slice[:20]
kwargs['children_slice_more'] = len(self.children_slice) > 20
return kwargs
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