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