Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deleted comments #362

Merged
merged 12 commits into from
Dec 13, 2021
59 changes: 50 additions & 9 deletions democracy/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.contrib.admin.utils import NestedObjects
from django.contrib.gis.db.models import ManyToManyField
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.html import format_html
Expand Down Expand Up @@ -257,7 +258,7 @@ def delete_queryset(self, request, queryset):
# since we are only performing soft delete, we must soft_delete related objects too, if possible
for obj in to_delete:
if hasattr(obj, 'soft_delete'):
obj.soft_delete()
obj.soft_delete(user=request.user)

def delete_model(self, request, obj):
using = router.db_for_write(obj._meta.model)
Expand Down Expand Up @@ -308,15 +309,32 @@ class ContactPersonAdmin(TranslatableAdmin, admin.ModelAdmin):


class CommentAdmin(admin.ModelAdmin):
list_display = ('id', 'section', 'author_name', 'content')
list_filter = ('section__hearing__slug',)
list_display = ('id', 'section', 'author_name', 'content', 'is_published')
list_filter = ('section__hearing__slug', 'deleted')
search_fields = ('section__id', 'author_name', 'title', 'content')
fields = ('title', 'content', 'reply_to', 'author_name', 'organization', 'geojson', 'map_comment_text',
'plugin_identifier', 'plugin_data', 'pinned', 'label', 'language_code', 'voters', 'section',
'created_by_user')
readonly_fields = ('reply_to', 'author_name', 'organization', 'geojson',
'plugin_identifier', 'plugin_data', 'label', 'language_code', 'voters', 'section',
'created_by_user')
'created_by_user', 'deleted_at', 'deleted_by')
change_form_template = 'admin/comment_change_form.html'

def get_fields(self, request, obj=None):
"""Display deleted-related fields only if comment is deleted"""

fields = [
'title', 'content', 'reply_to', 'author_name', 'organization', 'geojson', 'map_comment_text',
'plugin_identifier', 'plugin_data', 'pinned', 'label', 'language_code', 'voters', 'section',
'created_by_user', 'delete_reason'
]
if obj and obj.deleted:
fields += ['deleted_at', 'deleted_by']
return fields

def is_published(self, obj):
"""Invert deleted field for readability in admin"""

return not obj.deleted

is_published.boolean = True # Make field use green/red icons in list view

def created_by_user(self, obj):
# returns a link to the user that created the comment.
Expand All @@ -332,11 +350,34 @@ def created_by_user(self, obj):
def delete_queryset(self, request, queryset):
# this method is called by delete_selected and can be overridden
for comment in queryset:
comment.soft_delete()
comment.soft_delete(user=request.user)

def delete_model(self, request, obj):
# this method is called by the admin form and can be overridden
obj.soft_delete()
obj.soft_delete(user=request.user)

def get_queryset(self, request):
"""Override parent's method in order to return even deleted comments"""
qs = self.model._default_manager.everything()
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs

def response_change(self, request, obj):
# Handle undeleting of comment
if "_undeleteobject" in request.POST:
obj.undelete()
return super().response_change(request, obj)

def changelist_view(self, request, extra_context=None):
"""Use deleted=False filter by default"""
if (
not request.META['QUERY_STRING']
and not request.META.get('HTTP_REFERER', '').startswith(request.build_absolute_uri())
):
return HttpResponseRedirect(request.path + "?deleted__exact=0")
return super().changelist_view(request, extra_context=extra_context)


class ProjectPhaseInline(TranslatableStackedInline, NestedStackedInline):
Expand Down
172 changes: 172 additions & 0 deletions democracy/migrations/0055_add_deletion_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Generated by Django 2.2.20 on 2021-12-03 13:52

from django.conf import settings
import django.core.files.storage
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('democracy', '0054_hearing_geometry'),
]

operations = [
migrations.AddField(
model_name='commentimage',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='commentimage',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='commentimage_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='contactperson',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='contactperson',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contactperson_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='hearing',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='hearing',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hearing_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='label',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='label',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='label_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='organization',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='organization',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organization_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='project',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='project',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='project_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='projectphase',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='projectphase',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projectphase_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='section',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='section',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='section_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='sectioncomment',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='sectioncomment',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sectioncomment_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='sectionfile',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='sectionfile',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sectionfile_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='sectionimage',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='sectionimage',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sectionimage_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='sectionpoll',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='sectionpoll',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sectionpoll_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='sectionpollanswer',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='sectionpollanswer',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sectionpollanswer_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='sectionpolloption',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='sectionpolloption',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sectionpolloption_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AddField(
model_name='sectiontype',
name='deleted_at',
field=models.DateTimeField(blank=True, default=None, editable=False, null=True, verbose_name='time of deletion'),
),
migrations.AddField(
model_name='sectiontype',
name='deleted_by',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sectiontype_deleted', to=settings.AUTH_USER_MODEL, verbose_name='deleted by'),
),
migrations.AlterField(
model_name='sectionfile',
name='file',
field=models.FileField(max_length=2048, storage=django.core.files.storage.FileSystemStorage(location='/Users/eemeliranta/Projects/kerrokantasi/protected_media'), upload_to='files/%Y/%m', verbose_name='file'),
),
]
18 changes: 18 additions & 0 deletions democracy/migrations/0056_comment_delete_reason.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.20 on 2021-12-03 15:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('democracy', '0055_add_deletion_details'),
]

operations = [
migrations.AddField(
model_name='sectioncomment',
name='delete_reason',
field=models.TextField(blank=True, verbose_name='delete reason'),
),
]
19 changes: 16 additions & 3 deletions democracy/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ class BaseModel(models.Model):
editable=False, on_delete=models.SET_NULL
)
published = models.BooleanField(verbose_name=_('public'), default=True, db_index=True)
deleted_at = models.DateTimeField(
verbose_name=_('time of deletion'), default=None, editable=False, null=True, blank=True
)
deleted_by = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_('deleted by'),
null=True, blank=True, related_name="%(class)s_deleted",
editable=False, on_delete=models.SET_NULL
)
deleted = models.BooleanField(verbose_name=_('deleted'), default=False, db_index=True, editable=False)
objects = BaseModelManager()

Expand All @@ -70,13 +78,18 @@ def save(self, *args, **kwargs):
self.modified_at = timezone.now()
super().save(*args, **kwargs)

def soft_delete(self, using=None):
def soft_delete(self, using=None, user=None):
self.deleted = True
self.save(update_fields=("deleted",), using=using)
self.deleted_at = timezone.now()
if user is not None and user.pk:
self.deleted_by = user
self.save(update_fields=("deleted", "deleted_at", "deleted_by"), using=using)

def undelete(self, using=None):
self.deleted = False
self.save(update_fields=("deleted",), using=using)
self.deleted_at = None
self.deleted_by = None
self.save(update_fields=("deleted", "deleted_at", "deleted_by"), using=using)

def delete(self, using=None):
raise NotImplementedError("This model does not support hard deletion")
Expand Down
4 changes: 2 additions & 2 deletions democracy/models/hearing.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ def is_visible_for(self, user):
return False
return self.organization in user.admin_organizations.all()

def soft_delete(self, using=None):
def soft_delete(self, using=None, user=None):
# we want deleted hearings to give way to new ones, the original slug from a deleted hearing
# is now free to use
self.slug += '-deleted'
self.save()
super().soft_delete(using=using)
super().soft_delete(using=using, user=user)
7 changes: 4 additions & 3 deletions democracy/models/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,17 @@ class SectionComment(Commentable, BaseComment):
content = models.TextField(verbose_name=_('content'), blank=True)
reply_to = models.CharField(verbose_name=_('reply to'), blank=True, max_length=255)
pinned = models.BooleanField(default=False)
delete_reason = models.TextField(verbose_name=_('delete reason'), blank=True)

class Meta:
verbose_name = _('section comment')
verbose_name_plural = _('section comments')
ordering = ('-created_at',)

def soft_delete(self, using=None):
def soft_delete(self, using=None, user=None):
for answer in self.poll_answers.all():
answer.soft_delete()
super().soft_delete(using=using)
answer.soft_delete(user=user)
super().soft_delete(using=using, user=user)

def save(self, *args, **kwargs):
# we may create a comment by referring to another comment instead of section explicitly
Expand Down
5 changes: 5 additions & 0 deletions democracy/templates/admin/comment_change_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "admin/change_form.html" %}
{% load i18n admin_urls static admin_modify custom_tags %}

{% if save_on_top %}{% block submit_buttons_top %}{% undelete_submit_row %}{% endblock %}{% endif %}
{% block submit_buttons_bottom %}{% undelete_submit_row %}{% endblock %}
17 changes: 17 additions & 0 deletions democracy/templates/admin/undelete_submit_line.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% load i18n admin_urls %}
<div class="submit-row">
{% block submit-row %}
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save">{% endif %}
{% if original and original.deleted %}
{% url opts|admin_urlname:'undelete' original.pk|admin_urlquote as undelete_url %}
<input style="float: left; margin: 0;" type="submit" value="{% trans 'Undelete' %}" name="_undeleteobject" />
{% elif show_delete_link and original %}
{% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
<p class="deletelink-box"><a href="{% add_preserved_filters delete_url %}" class="deletelink">{% trans "Delete" %}</a></p>
{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew">{% endif %}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother">{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% if can_change %}{% trans 'Save and continue editing' %}{% else %}{% trans 'Save and view' %}{% endif %}" name="_continue">{% endif %}
{% if show_close %}<a href="{% url opts|admin_urlname:'changelist' %}" class="closelink">{% trans 'Close' %}</a>{% endif %}
{% endblock %}
</div>
Empty file.
10 changes: 10 additions & 0 deletions democracy/templatetags/custom_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib.admin.templatetags.admin_modify import submit_row
from django import template

register = template.Library()


@register.inclusion_tag('admin/undelete_submit_line.html', takes_context=True)
def undelete_submit_row(context):
"""`submit_row` with a different html template"""
return submit_row(context)
Loading