Delete related variants when deleting the segment (#183)
* Delete related variants when deleting the segment Closes #155 * Fix typo * Fix migration ordering * Split metadata migrations
This commit is contained in:
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.7 on 2018-07-04 15:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0020_rules_delete_relatedqueryname'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='personalisablepagemetadata',
|
||||||
|
name='segment',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='page_metadata', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
]
|
@ -7,7 +7,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('wagtail_personalisation', '0020_rules_delete_relatedqueryname'),
|
('wagtail_personalisation', '0021_personalisablepagemetadata_segment_set_on_delete_protect'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -16,9 +16,4 @@ class Migration(migrations.Migration):
|
|||||||
name='canonical_page',
|
name='canonical_page',
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='personalisable_canonical_metadata', to='wagtailcore.Page'),
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='personalisable_canonical_metadata', to='wagtailcore.Page'),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
|
||||||
model_name='personalisablepagemetadata',
|
|
||||||
name='variant',
|
|
||||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page'),
|
|
||||||
),
|
|
||||||
]
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.5 on 2018-07-19 09:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0022_personalisablepagemetadata_canonical_protect'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='personalisablepagemetadata',
|
||||||
|
name='variant',
|
||||||
|
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page'),
|
||||||
|
),
|
||||||
|
]
|
@ -216,10 +216,8 @@ class PersonalisablePageMetadata(ClusterableModel):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
segment = models.ForeignKey(
|
segment = models.ForeignKey(Segment, models.PROTECT, null=True,
|
||||||
Segment, related_name='page_metadata',
|
related_name='page_metadata')
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True, blank=True)
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def has_variants(self):
|
def has_variants(self):
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "modeladmin/delete.html" %}
|
||||||
|
|
||||||
|
{% load i18n modeladmin_tags %}
|
||||||
|
|
||||||
|
{% block content_main %}
|
||||||
|
<div class="nice-padding">
|
||||||
|
{% if protected_error %}
|
||||||
|
<h2>{% blocktrans with view.verbose_name|capfirst as model_name %}{{ model_name }} could not be deleted{% endblocktrans %}</h2>
|
||||||
|
<p>{% blocktrans with instance as instance_name %}'{{ instance_name }}' is currently referenced by other objects, and cannot be deleted without jeopardising data integrity. To delete it successfully, first remove references from the following objects, then try again:{% endblocktrans %}</p>
|
||||||
|
<ul>
|
||||||
|
{% for obj in linked_objects %}<li><b>{{ obj|get_content_type_for_obj|title }}:</b> {{ obj }}</li>{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p><a href="{{ view.index_url }}" class="button">{% trans 'Go back to listing' %}</a></p>
|
||||||
|
{% elif cannot_delete_page_variants_error %}
|
||||||
|
<h2>{% blocktrans %}Cannot delete all the page variants{% endblocktrans %}</h2>
|
||||||
|
<p>{% blocktrans %}You need to have permissions to delete the page variants associated with this segment.{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% with page_variants=view.get_affected_page_objects %}
|
||||||
|
{% if page_variants %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}Deleting the segment will also mean deleting all the page variants associated with it. Do you want to continue?{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}The page objects that <strong>will be deleted</strong> are:{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{% for variant in page_variants %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'wagtailadmin_explore' variant.pk %}">
|
||||||
|
{{ variant }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% trans 'Yes, delete the segment and associated page variants' as submit_button_value %}
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}Do you want to continue deleting this segment?{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% trans 'Yes, delete the segment' as submit_button_value %}
|
||||||
|
{% endif %}
|
||||||
|
<form action="{{ view.delete_url }}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="submit" value="{{ submit_button_value }}" class="button serious" />
|
||||||
|
</form>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -109,3 +109,10 @@ def exclude_variants(pages):
|
|||||||
canonical_page_id=F('variant_id')
|
canonical_page_id=F('variant_id')
|
||||||
).values_list('variant_id')
|
).values_list('variant_id')
|
||||||
return pages.exclude(pk__in=excluded_variant_pages)
|
return pages.exclude(pk__in=excluded_variant_pages)
|
||||||
|
|
||||||
|
|
||||||
|
def can_delete_pages(pages, user):
|
||||||
|
for variant in pages:
|
||||||
|
if not variant.permissions_for_user(user).can_delete():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
@ -3,16 +3,19 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import csv
|
import csv
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.db import transaction
|
||||||
from django.http import (
|
from django.http import (
|
||||||
HttpResponse, HttpResponseForbidden, HttpResponseRedirect)
|
HttpResponse, HttpResponseForbidden, HttpResponseRedirect)
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
||||||
from wagtail.contrib.modeladmin.views import IndexView
|
from wagtail.contrib.modeladmin.views import DeleteView, IndexView
|
||||||
from wagtail.core.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
|
from wagtail_personalisation.utils import can_delete_pages
|
||||||
|
|
||||||
|
|
||||||
class SegmentModelIndexView(IndexView):
|
class SegmentModelIndexView(IndexView):
|
||||||
@ -35,12 +38,49 @@ class SegmentModelDashboardView(IndexView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SegmentModelDeleteView(DeleteView):
|
||||||
|
def get_affected_page_objects(self):
|
||||||
|
return Page.objects.filter(pk__in=(
|
||||||
|
self.instance.get_used_pages().values_list('variant_id', flat=True)
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_template_names(self):
|
||||||
|
return [
|
||||||
|
'modeladmin/wagtail_personalisation/segment/delete.html',
|
||||||
|
'modeladmin/delete.html',
|
||||||
|
]
|
||||||
|
|
||||||
|
def delete_instance(self):
|
||||||
|
page_variants = self.get_affected_page_objects()
|
||||||
|
if not can_delete_pages(page_variants, self.request.user):
|
||||||
|
raise PermissionDenied(
|
||||||
|
'User has no permission to delete variant page objects.'
|
||||||
|
)
|
||||||
|
# Deleting page objects triggers deletion of the personalisation
|
||||||
|
# metadata too because of models.CASCADE.
|
||||||
|
with transaction.atomic():
|
||||||
|
for variant in page_variants.iterator():
|
||||||
|
# Delete each one separately so signals are called.
|
||||||
|
variant.delete()
|
||||||
|
super().delete_instance()
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if not can_delete_pages(self.get_affected_page_objects(),
|
||||||
|
self.request.user):
|
||||||
|
context = self.get_context_data(
|
||||||
|
cannot_delete_page_variants_error=True,
|
||||||
|
)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@modeladmin_register
|
@modeladmin_register
|
||||||
class SegmentModelAdmin(ModelAdmin):
|
class SegmentModelAdmin(ModelAdmin):
|
||||||
"""The model admin for the Segments administration interface."""
|
"""The model admin for the Segments administration interface."""
|
||||||
model = Segment
|
model = Segment
|
||||||
index_view_class = SegmentModelIndexView
|
index_view_class = SegmentModelIndexView
|
||||||
dashboard_view_class = SegmentModelDashboardView
|
dashboard_view_class = SegmentModelDashboardView
|
||||||
|
delete_view_class = SegmentModelDeleteView
|
||||||
menu_icon = 'fa-snowflake-o'
|
menu_icon = 'fa-snowflake-o'
|
||||||
add_to_settings_menu = False
|
add_to_settings_menu = False
|
||||||
list_display = ('name', 'persistent', 'match_any', 'status',
|
list_display = ('name', 'persistent', 'match_any', 'status',
|
||||||
|
@ -52,3 +52,11 @@ def test_canonical_page_deletion_is_protected(segmented_page):
|
|||||||
# an error. All variants should be deleted beforehand.
|
# an error. All variants should be deleted beforehand.
|
||||||
with pytest.raises(ProtectedError):
|
with pytest.raises(ProtectedError):
|
||||||
segmented_page.personalisation_metadata.canonical_page.delete()
|
segmented_page.personalisation_metadata.canonical_page.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_page_protection_when_deleting_segment(segmented_page):
|
||||||
|
segment = segmented_page.personalisation_metadata.segment
|
||||||
|
assert len(segment.get_used_pages())
|
||||||
|
with pytest.raises(ProtectedError):
|
||||||
|
segment.delete()
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.factories.page import ContentPageFactory
|
from tests.factories.page import ContentPageFactory
|
||||||
from wagtail_personalisation.utils import impersonate_other_page
|
from wagtail_personalisation.utils import (
|
||||||
|
can_delete_pages, impersonate_other_page)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -24,3 +25,14 @@ def test_impersonate_other_page(page, otherpage):
|
|||||||
impersonate_other_page(page, otherpage)
|
impersonate_other_page(page, otherpage)
|
||||||
assert page.title == otherpage.title == 'Bye'
|
assert page.title == otherpage.title == 'Bye'
|
||||||
assert page.path == otherpage.path
|
assert page.path == otherpage.path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_can_delete_pages_with_superuser(rf, user, segmented_page):
|
||||||
|
user.is_superuser = True
|
||||||
|
assert can_delete_pages([segmented_page], user)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cannot_delete_pages_with_standard_user(user, segmented_page):
|
||||||
|
assert not can_delete_pages([segmented_page], user)
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
from wagtail_personalisation.rules import VisitCountRule
|
from wagtail_personalisation.rules import VisitCountRule
|
||||||
|
from wagtail_personalisation.views import (
|
||||||
|
SegmentModelDeleteView, SegmentModelAdmin)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@ -51,3 +55,56 @@ def test_segment_user_data_view(site, client, mocker, django_user_model):
|
|||||||
assert data_lines[0] == 'Username,Visit count - Test page,Visit count - Regular page\r'
|
assert data_lines[0] == 'Username,Visit count - Test page,Visit count - Regular page\r'
|
||||||
assert data_lines[1] == 'first,3,9\r'
|
assert data_lines[1] == 'first,3,9\r'
|
||||||
assert data_lines[2] == 'second,0,1\r'
|
assert data_lines[2] == 'second,0,1\r'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_segment_delete_view_delete_instance(rf, segmented_page, user):
|
||||||
|
user.is_superuser = True
|
||||||
|
user.save()
|
||||||
|
segment = segmented_page.personalisation_metadata.segment
|
||||||
|
canonical_page = segmented_page.personalisation_metadata.canonical_page
|
||||||
|
variants_metadata = segment.get_used_pages()
|
||||||
|
page_variants = Page.objects.filter(pk__in=(
|
||||||
|
variants_metadata.values_list('variant_id', flat=True)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Make sure all canonical page, variants and variants metadata exist
|
||||||
|
assert canonical_page
|
||||||
|
assert page_variants
|
||||||
|
assert variants_metadata
|
||||||
|
|
||||||
|
# Delete the segment via the method on the view.
|
||||||
|
request = rf.get('/'.format(segment.pk))
|
||||||
|
request.user = user
|
||||||
|
view = SegmentModelDeleteView(
|
||||||
|
instance_pk=str(segment.pk),
|
||||||
|
model_admin=SegmentModelAdmin()
|
||||||
|
)
|
||||||
|
view.request = request
|
||||||
|
view.delete_instance()
|
||||||
|
|
||||||
|
# Segment has been deleted.
|
||||||
|
with pytest.raises(segment.DoesNotExist):
|
||||||
|
segment.refresh_from_db()
|
||||||
|
|
||||||
|
# Canonical page stayed intact.
|
||||||
|
canonical_page.refresh_from_db()
|
||||||
|
|
||||||
|
# Variant pages and their metadata have been deleted.
|
||||||
|
assert not page_variants.all()
|
||||||
|
assert not variants_metadata.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_segment_delete_view_raises_permission_denied(rf, segmented_page, user):
|
||||||
|
segment = segmented_page.personalisation_metadata.segment
|
||||||
|
request = rf.get('/'.format(segment.pk))
|
||||||
|
request.user = user
|
||||||
|
view = SegmentModelDeleteView(
|
||||||
|
instance_pk=str(segment.pk),
|
||||||
|
model_admin=SegmentModelAdmin()
|
||||||
|
)
|
||||||
|
view.request = request
|
||||||
|
message = 'User have no permission to delete variant page objects.'
|
||||||
|
with pytest.raises(PermissionDenied, message=message):
|
||||||
|
view.delete_instance()
|
||||||
|
Reference in New Issue
Block a user