222 lines
8.7 KiB
Python
222 lines
8.7 KiB
Python
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"))
|