add event organizer merge tool
This commit is contained in:
@@ -0,0 +1,221 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import transaction
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.http import urlencode
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic import FormView
|
||||||
|
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
|
||||||
|
from wagtail.models import ReferenceIndex, Revision
|
||||||
|
|
||||||
|
from dnscms.utils import slugify
|
||||||
|
from events.models import EventOrganizer, EventOrganizerLink, EventPage
|
||||||
|
from events.views import event_organizer_chooser_viewset
|
||||||
|
|
||||||
|
|
||||||
|
def _check_relations_are_handled():
|
||||||
|
"""Refuse to merge if EventOrganizer has gained a relation this module doesn't handle."""
|
||||||
|
for relation in EventOrganizer._meta.related_objects:
|
||||||
|
if relation.many_to_many and relation.through is EventOrganizerLink:
|
||||||
|
continue
|
||||||
|
if relation.related_model is EventOrganizerLink and relation.field.name == "organizer":
|
||||||
|
continue
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"merge_event_organizers does not handle the {relation.name!r} relation"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _repoint_links(survivor, loser):
|
||||||
|
loser_links = EventOrganizerLink.objects.filter(organizer=loser)
|
||||||
|
affected_event_ids = set(loser_links.values_list("event_id", flat=True))
|
||||||
|
# drop the loser's link where the event links both, to respect the unique constraint
|
||||||
|
doubly_linked = EventOrganizerLink.objects.filter(
|
||||||
|
organizer=survivor, event_id__in=affected_event_ids
|
||||||
|
).values_list("event_id", flat=True)
|
||||||
|
loser_links.filter(event_id__in=doubly_linked).delete()
|
||||||
|
loser_links.update(organizer=survivor)
|
||||||
|
return affected_event_ids
|
||||||
|
|
||||||
|
|
||||||
|
def _rewrite_revisions(survivor, loser):
|
||||||
|
"""Rewrite organizer ids in revision JSON, which would otherwise dangle after the delete."""
|
||||||
|
affected_event_ids = set()
|
||||||
|
event_content_type = ContentType.objects.get_for_model(EventPage)
|
||||||
|
for revision in Revision.objects.filter(content_type=event_content_type).iterator():
|
||||||
|
links = revision.content.get("organizer_links") or []
|
||||||
|
if not any(link.get("organizer") == loser.pk for link in links):
|
||||||
|
continue
|
||||||
|
seen_organizer_ids = set()
|
||||||
|
rewritten = []
|
||||||
|
for link in links:
|
||||||
|
if link.get("organizer") == loser.pk:
|
||||||
|
link = {**link, "organizer": survivor.pk}
|
||||||
|
if link.get("organizer") in seen_organizer_ids:
|
||||||
|
continue
|
||||||
|
seen_organizer_ids.add(link.get("organizer"))
|
||||||
|
rewritten.append(link)
|
||||||
|
revision.content["organizer_links"] = rewritten
|
||||||
|
revision.save(update_fields=["content"])
|
||||||
|
affected_event_ids.add(int(revision.object_id))
|
||||||
|
return affected_event_ids
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def merge_event_organizers(organizer_a, organizer_b, *, name=None, slug=None):
|
||||||
|
"""
|
||||||
|
Merge two organizers, keeping the one with the lowest primary key. Event
|
||||||
|
links and event page revisions are repointed before the other organizer is
|
||||||
|
deleted. ``name``/``slug`` override the survivor's fields; a blank
|
||||||
|
``association``/``external_url`` is filled in from the deleted organizer.
|
||||||
|
"""
|
||||||
|
if organizer_a.pk == organizer_b.pk:
|
||||||
|
raise ValueError("Cannot merge an organizer with itself.")
|
||||||
|
_check_relations_are_handled()
|
||||||
|
|
||||||
|
survivor, loser = sorted([organizer_a, organizer_b], key=lambda organizer: organizer.pk)
|
||||||
|
|
||||||
|
affected_event_ids = _repoint_links(survivor, loser)
|
||||||
|
affected_event_ids |= _rewrite_revisions(survivor, loser)
|
||||||
|
|
||||||
|
if name:
|
||||||
|
survivor.name = name
|
||||||
|
if slug:
|
||||||
|
survivor.slug = slug
|
||||||
|
if not survivor.association_id:
|
||||||
|
survivor.association_id = loser.association_id
|
||||||
|
if not survivor.external_url:
|
||||||
|
survivor.external_url = loser.external_url
|
||||||
|
survivor.save()
|
||||||
|
|
||||||
|
loser.delete()
|
||||||
|
|
||||||
|
# the bulk link update bypasses the save-time reference index maintenance
|
||||||
|
for event in EventPage.objects.filter(pk__in=affected_event_ids):
|
||||||
|
ReferenceIndex.create_or_update_for_object(event)
|
||||||
|
|
||||||
|
return survivor
|
||||||
|
|
||||||
|
|
||||||
|
CUSTOM_VALUE = "__custom__"
|
||||||
|
|
||||||
|
|
||||||
|
class MergeOrganizersSelectForm(forms.Form):
|
||||||
|
organizer_a = forms.ModelChoiceField(
|
||||||
|
label=_("First organizer"),
|
||||||
|
queryset=EventOrganizer.objects.all(),
|
||||||
|
widget=event_organizer_chooser_viewset.widget_class(),
|
||||||
|
)
|
||||||
|
organizer_b = forms.ModelChoiceField(
|
||||||
|
label=_("Second organizer"),
|
||||||
|
queryset=EventOrganizer.objects.all(),
|
||||||
|
widget=event_organizer_chooser_viewset.widget_class(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned = super().clean()
|
||||||
|
organizer_a = cleaned.get("organizer_a")
|
||||||
|
organizer_b = cleaned.get("organizer_b")
|
||||||
|
if organizer_a and organizer_b and organizer_a.pk == organizer_b.pk:
|
||||||
|
raise ValidationError(_("Pick two different organizers."))
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
class MergeOrganizersConfirmForm(forms.Form):
|
||||||
|
name = forms.ChoiceField(label=_("Name to keep"), widget=forms.RadioSelect)
|
||||||
|
name_custom = forms.CharField(label=_("Custom name"), required=False, max_length=100)
|
||||||
|
slug = forms.ChoiceField(label=_("Slug to keep"), widget=forms.RadioSelect)
|
||||||
|
slug_custom = forms.CharField(label=_("Custom slug"), required=False, max_length=255)
|
||||||
|
|
||||||
|
def __init__(self, *args, survivor, loser, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields["name"].choices = self._choices(survivor.name, loser.name)
|
||||||
|
self.fields["name"].initial = survivor.name
|
||||||
|
self.fields["slug"].choices = self._choices(survivor.slug, loser.slug)
|
||||||
|
self.fields["slug"].initial = survivor.slug
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _choices(*values):
|
||||||
|
unique_values = dict.fromkeys(values)
|
||||||
|
return [(value, value) for value in unique_values] + [
|
||||||
|
(CUSTOM_VALUE, _("Something else (enter below)"))
|
||||||
|
]
|
||||||
|
|
||||||
|
def _resolve(self, cleaned, field):
|
||||||
|
choice = cleaned.get(field)
|
||||||
|
custom = (cleaned.get(f"{field}_custom") or "").strip()
|
||||||
|
if choice == CUSTOM_VALUE:
|
||||||
|
if not custom:
|
||||||
|
self.add_error(
|
||||||
|
f"{field}_custom", _("Enter a value, or pick an existing one above.")
|
||||||
|
)
|
||||||
|
return custom
|
||||||
|
return choice
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned = super().clean()
|
||||||
|
cleaned["final_name"] = self._resolve(cleaned, "name")
|
||||||
|
cleaned["final_slug"] = slugify(self._resolve(cleaned, "slug") or "")
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
class MergeOrganizersSelectView(WagtailAdminTemplateMixin, FormView):
|
||||||
|
template_name = "events/merge_organizers_select.html"
|
||||||
|
form_class = MergeOrganizersSelectForm
|
||||||
|
page_title = _("Merge organizers")
|
||||||
|
header_icon = "group"
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
params = urlencode(
|
||||||
|
{
|
||||||
|
"organizer_a": form.cleaned_data["organizer_a"].pk,
|
||||||
|
"organizer_b": form.cleaned_data["organizer_b"].pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return redirect(f"{reverse('events_merge_organizers_confirm')}?{params}")
|
||||||
|
|
||||||
|
|
||||||
|
class MergeOrganizersConfirmView(WagtailAdminTemplateMixin, FormView):
|
||||||
|
template_name = "events/merge_organizers_confirm.html"
|
||||||
|
form_class = MergeOrganizersConfirmForm
|
||||||
|
page_title = _("Merge organizers")
|
||||||
|
header_icon = "group"
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
pks = {int(request.GET.get(param, "")) for param in ("organizer_a", "organizer_b")}
|
||||||
|
except ValueError:
|
||||||
|
pks = set()
|
||||||
|
organizers = list(EventOrganizer.objects.filter(pk__in=pks).order_by("pk"))
|
||||||
|
if len(organizers) != 2:
|
||||||
|
messages.error(request, _("Pick two different organizers to merge."))
|
||||||
|
return redirect("events_merge_organizers")
|
||||||
|
self.survivor, self.loser = organizers
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
return {**super().get_form_kwargs(), "survivor": self.survivor, "loser": self.loser}
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["survivor"] = self.survivor
|
||||||
|
context["loser"] = self.loser
|
||||||
|
context["survivor_event_count"] = self.survivor.organized_events.count()
|
||||||
|
context["loser_event_count"] = self.loser.organized_events.count()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
survivor = merge_event_organizers(
|
||||||
|
self.survivor,
|
||||||
|
self.loser,
|
||||||
|
name=form.cleaned_data["final_name"],
|
||||||
|
slug=form.cleaned_data["final_slug"],
|
||||||
|
)
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
_('Merged "%(loser)s" into "%(survivor)s".')
|
||||||
|
% {"loser": self.loser.name, "survivor": survivor.name},
|
||||||
|
)
|
||||||
|
return redirect(reverse("wagtailsnippets_events_eventorganizer:list"))
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{% extends "wagtailadmin/generic/base.html" %}
|
||||||
|
{% load i18n wagtailadmin_tags %}
|
||||||
|
|
||||||
|
{% block main_content %}
|
||||||
|
<table class="listing">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Outcome" %}</th>
|
||||||
|
<th>{% trans "ID" %}</th>
|
||||||
|
<th>{% trans "Name" %}</th>
|
||||||
|
<th>{% trans "Slug" %}</th>
|
||||||
|
<th>{% trans "Association" %}</th>
|
||||||
|
<th>{% trans "External URL" %}</th>
|
||||||
|
<th>{% trans "Events" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{% trans "Survives" %}</strong></td>
|
||||||
|
<td>{{ survivor.pk }}</td>
|
||||||
|
<td>{{ survivor.name }}</td>
|
||||||
|
<td>{{ survivor.slug }}</td>
|
||||||
|
<td>{{ survivor.association|default:"—" }}</td>
|
||||||
|
<td>{{ survivor.external_url|default:"—" }}</td>
|
||||||
|
<td>{{ survivor_event_count }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{% trans "Is deleted" %}</strong></td>
|
||||||
|
<td>{{ loser.pk }}</td>
|
||||||
|
<td>{{ loser.name }}</td>
|
||||||
|
<td>{{ loser.slug }}</td>
|
||||||
|
<td>{{ loser.association|default:"—" }}</td>
|
||||||
|
<td>{{ loser.external_url|default:"—" }}</td>
|
||||||
|
<td>{{ loser_event_count }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="help-block help-warning">
|
||||||
|
{% blocktrans with loser_name=loser.name survivor_name=survivor.name trimmed %}
|
||||||
|
Events organized by “{{ loser_name }}” will be moved to “{{ survivor_name }}”,
|
||||||
|
and “{{ loser_name }}” will be deleted. This cannot be undone.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
<ul class="fields">
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
<li>{% formattedfield field %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<button type="submit" class="button serious">{% trans "Merge organizers" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends "wagtailadmin/generic/base.html" %}
|
||||||
|
{% load i18n wagtailadmin_tags %}
|
||||||
|
|
||||||
|
{% block main_content %}
|
||||||
|
<p>
|
||||||
|
{% trans "Pick the two organizers to merge. The one with the lowest ID survives; the other one is deleted after its events have been moved over." %}
|
||||||
|
</p>
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="help-block help-critical">{{ form.non_field_errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<ul class="fields">
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
<li>{% formattedfield field %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<button type="submit" class="button">{% trans "Continue" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.css }}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,8 +1,46 @@
|
|||||||
|
from django.urls import path, reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from wagtail import hooks
|
from wagtail import hooks
|
||||||
|
from wagtail.admin.auth import user_passes_test
|
||||||
|
from wagtail.admin.menu import MenuItem
|
||||||
|
|
||||||
from .admin import event_sidebar_viewset, event_explorer_viewset
|
from .admin import event_sidebar_viewset, event_explorer_viewset
|
||||||
|
from .merge_organizers import MergeOrganizersConfirmView, MergeOrganizersSelectView
|
||||||
from .views import event_organizer_chooser_viewset
|
from .views import event_organizer_chooser_viewset
|
||||||
|
|
||||||
|
superuser_only = user_passes_test(lambda user: user.is_superuser)
|
||||||
|
|
||||||
|
|
||||||
|
class SuperuserMenuItem(MenuItem):
|
||||||
|
def is_shown(self, request):
|
||||||
|
return request.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register("register_admin_urls")
|
||||||
|
def register_merge_organizers_urls():
|
||||||
|
return [
|
||||||
|
path(
|
||||||
|
"merge-organizers/",
|
||||||
|
superuser_only(MergeOrganizersSelectView.as_view()),
|
||||||
|
name="events_merge_organizers",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"merge-organizers/confirm/",
|
||||||
|
superuser_only(MergeOrganizersConfirmView.as_view()),
|
||||||
|
name="events_merge_organizers_confirm",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register("register_settings_menu_item")
|
||||||
|
def register_merge_organizers_menu_item():
|
||||||
|
return SuperuserMenuItem(
|
||||||
|
_("Merge organizers"),
|
||||||
|
reverse_lazy("events_merge_organizers"),
|
||||||
|
icon_name="group",
|
||||||
|
order=1100,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@hooks.register("register_admin_viewset")
|
@hooks.register("register_admin_viewset")
|
||||||
def register_viewset():
|
def register_viewset():
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import pytest
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.urls import reverse
|
||||||
|
from wagtail.models import ReferenceIndex
|
||||||
|
|
||||||
|
from events.merge_organizers import CUSTOM_VALUE, merge_event_organizers
|
||||||
|
from events.models import EventOrganizer, EventOrganizerLink, EventPage
|
||||||
|
from tests.conftest import AssociationPageFactory, EventPageFactory
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def organizers(db):
|
||||||
|
keeper = EventOrganizer.objects.create(name="Kulturutvalget", slug="kulturutvalget")
|
||||||
|
goner = EventOrganizer.objects.create(name="KU", slug="ku")
|
||||||
|
return keeper, goner
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def editor_client(client, django_user_model):
|
||||||
|
user = django_user_model.objects.create_user(username="editor", password="pw")
|
||||||
|
user.user_permissions.add(Permission.objects.get(codename="access_admin"))
|
||||||
|
client.force_login(user)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_repoints_event_links_and_deletes_loser(event_index, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
event = EventPageFactory(parent=event_index)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||||
|
|
||||||
|
merged = merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
assert merged.pk == keeper.pk
|
||||||
|
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||||
|
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||||
|
|
||||||
|
|
||||||
|
def test_lowest_pk_survives_regardless_of_argument_order(organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
|
||||||
|
merged = merge_event_organizers(goner, keeper)
|
||||||
|
|
||||||
|
assert merged.pk == keeper.pk
|
||||||
|
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_applies_chosen_name_and_slug(organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
|
||||||
|
merged = merge_event_organizers(keeper, goner, name="KU", slug="ku")
|
||||||
|
|
||||||
|
assert merged.name == "KU"
|
||||||
|
assert merged.slug == "ku"
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_keeps_survivor_name_and_slug_by_default(organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
|
||||||
|
merged = merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
assert merged.name == "Kulturutvalget"
|
||||||
|
assert merged.slug == "kulturutvalget"
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_collapses_duplicate_links_on_same_event(event_index, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
event = EventPageFactory(parent=event_index)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=keeper)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||||
|
|
||||||
|
merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_fills_blank_fields_from_loser(association_index, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
association = AssociationPageFactory(parent=association_index)
|
||||||
|
goner.association = association
|
||||||
|
goner.external_url = "https://example.com"
|
||||||
|
goner.save()
|
||||||
|
|
||||||
|
merged = merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
assert merged.association_id == association.pk
|
||||||
|
assert merged.external_url == "https://example.com"
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_keeps_survivor_fields_when_set(organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
keeper.external_url = "https://keeper.example.com"
|
||||||
|
keeper.save()
|
||||||
|
goner.external_url = "https://goner.example.com"
|
||||||
|
goner.save()
|
||||||
|
|
||||||
|
merged = merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
assert merged.external_url == "https://keeper.example.com"
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_rewrites_event_page_revisions(event_index, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
event = EventPageFactory(parent=event_index)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||||
|
revision = EventPage.objects.get(pk=event.pk).save_revision()
|
||||||
|
assert [link["organizer"] for link in revision.content["organizer_links"]] == [goner.pk]
|
||||||
|
|
||||||
|
merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
revision.refresh_from_db()
|
||||||
|
assert [link["organizer"] for link in revision.content["organizer_links"]] == [keeper.pk]
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_dedupes_organizers_within_revisions(event_index, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
event = EventPageFactory(parent=event_index)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=keeper)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||||
|
revision = EventPage.objects.get(pk=event.pk).save_revision()
|
||||||
|
|
||||||
|
merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
revision.refresh_from_db()
|
||||||
|
assert [link["organizer"] for link in revision.content["organizer_links"]] == [keeper.pk]
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_updates_reference_index(event_index, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
event = EventPageFactory(parent=event_index)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||||
|
ReferenceIndex.create_or_update_for_object(EventPage.objects.get(pk=event.pk))
|
||||||
|
organizer_ct = ContentType.objects.get_for_model(EventOrganizer)
|
||||||
|
assert ReferenceIndex.objects.filter(
|
||||||
|
to_content_type=organizer_ct, to_object_id=str(goner.pk)
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
merge_event_organizers(keeper, goner)
|
||||||
|
|
||||||
|
assert not ReferenceIndex.objects.filter(
|
||||||
|
to_content_type=organizer_ct, to_object_id=str(goner.pk)
|
||||||
|
).exists()
|
||||||
|
assert ReferenceIndex.objects.filter(
|
||||||
|
to_content_type=organizer_ct, to_object_id=str(keeper.pk)
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_refuses_same_organizer(organizers):
|
||||||
|
keeper, _ = organizers
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
merge_event_organizers(keeper, keeper)
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_views_require_superuser(editor_client, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
confirm_url = reverse("events_merge_organizers_confirm")
|
||||||
|
urls = [
|
||||||
|
reverse("events_merge_organizers"),
|
||||||
|
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
response = editor_client.get(url)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == reverse("wagtailadmin_home")
|
||||||
|
assert EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_select_view_redirects_to_confirm(admin_client, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
|
||||||
|
response = admin_client.post(
|
||||||
|
reverse("events_merge_organizers"),
|
||||||
|
{"organizer_a": keeper.pk, "organizer_b": goner.pk},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
confirm_url = reverse("events_merge_organizers_confirm")
|
||||||
|
assert response.url == f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_select_view_rejects_same_organizer_twice(admin_client, organizers):
|
||||||
|
keeper, _ = organizers
|
||||||
|
|
||||||
|
response = admin_client.post(
|
||||||
|
reverse("events_merge_organizers"),
|
||||||
|
{"organizer_a": keeper.pk, "organizer_b": keeper.pk},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Pick two different organizers." in response.content.decode()
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_view_shows_both_organizers(admin_client, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
confirm_url = reverse("events_merge_organizers_confirm")
|
||||||
|
|
||||||
|
response = admin_client.get(f"{confirm_url}?organizer_a={goner.pk}&organizer_b={keeper.pk}")
|
||||||
|
|
||||||
|
content = response.content.decode()
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Kulturutvalget" in content
|
||||||
|
assert "KU" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_view_rejects_missing_or_equal_organizers(admin_client, organizers):
|
||||||
|
keeper, _ = organizers
|
||||||
|
confirm_url = reverse("events_merge_organizers_confirm")
|
||||||
|
queries = [
|
||||||
|
"",
|
||||||
|
f"?organizer_a={keeper.pk}&organizer_b={keeper.pk}",
|
||||||
|
f"?organizer_a=abc&organizer_b={keeper.pk}",
|
||||||
|
f"?organizer_a={keeper.pk}&organizer_b=999999",
|
||||||
|
]
|
||||||
|
|
||||||
|
for query in queries:
|
||||||
|
response = admin_client.get(confirm_url + query)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == reverse("events_merge_organizers")
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_view_merges_with_picked_values(admin_client, event_index, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
event = EventPageFactory(parent=event_index)
|
||||||
|
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||||
|
confirm_url = reverse("events_merge_organizers_confirm")
|
||||||
|
|
||||||
|
response = admin_client.post(
|
||||||
|
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||||
|
{"name": goner.name, "name_custom": "", "slug": goner.slug, "slug_custom": ""},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == reverse("wagtailsnippets_events_eventorganizer:list")
|
||||||
|
keeper.refresh_from_db()
|
||||||
|
assert keeper.name == "KU"
|
||||||
|
assert keeper.slug == "ku"
|
||||||
|
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||||
|
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_view_merges_with_custom_values(admin_client, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
confirm_url = reverse("events_merge_organizers_confirm")
|
||||||
|
|
||||||
|
response = admin_client.post(
|
||||||
|
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||||
|
{
|
||||||
|
"name": CUSTOM_VALUE,
|
||||||
|
"name_custom": "Kulturutvalget (KU)",
|
||||||
|
"slug": CUSTOM_VALUE,
|
||||||
|
"slug_custom": "Kulturutvalget KU",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
keeper.refresh_from_db()
|
||||||
|
assert keeper.name == "Kulturutvalget (KU)"
|
||||||
|
assert keeper.slug == "kulturutvalget-ku"
|
||||||
|
|
||||||
|
|
||||||
|
def test_confirm_view_requires_custom_value_when_picked(admin_client, organizers):
|
||||||
|
keeper, goner = organizers
|
||||||
|
confirm_url = reverse("events_merge_organizers_confirm")
|
||||||
|
|
||||||
|
response = admin_client.post(
|
||||||
|
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||||
|
{"name": CUSTOM_VALUE, "name_custom": "", "slug": keeper.slug, "slug_custom": ""},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||||
Reference in New Issue
Block a user