import logging from django.conf.urls import include, url from django.db import transaction from django.db.models import F from django.http import Http404 from django.shortcuts import redirect, render from django.template.defaultfilters import pluralize from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from wagtail.admin import messages from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem from wagtail.admin.views.pages import get_valid_next_url_from_request from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook from wagtail.core import hooks from wagtail.core.models import Page from wagtail_personalisation import admin_urls, models, utils from wagtail_personalisation.adapters import get_segment_adapter from wagtail_personalisation.models import PersonalisablePageMetadata logger = logging.getLogger(__name__) @hooks.register('register_admin_urls') def register_admin_urls(): """Adds the administration urls for the personalisation apps.""" return [ url(r'^personalisation/', include( admin_urls, namespace='wagtail_personalisation')), ] @hooks.register('before_serve_page') def set_visit_count(page, request, serve_args, serve_kwargs): """Tests the provided rules to see if the request still belongs to a segment. :param page: The page being served :type page: wagtail.core.models.Page :param request: The http request :type request: django.http.HttpRequest """ adapter = get_segment_adapter(request) adapter.add_page_visit(page) @hooks.register('before_serve_page') def segment_user(page, request, serve_args, serve_kwargs): """Apply a segment to a visitor before serving the page. :param page: The page being served :type page: wagtail.core.models.Page :param request: The http request :type request: django.http.HttpRequest """ adapter = get_segment_adapter(request) adapter.refresh() forced_segment = request.GET.get('segment', None) if request.user.is_superuser and forced_segment is not None: segment = models.Segment.objects.filter(pk=forced_segment).first() if segment: adapter.set_segments([segment]) class UserbarSegmentedLinkItem: def __init__(self, segment): self.segment = segment def render(self, request): return f"""
Show as segment: {self.segment.name}
""" @hooks.register('construct_wagtail_userbar') def add_segment_link_items(request, items): for item in models.Segment.objects.enabled(): items.append(UserbarSegmentedLinkItem(item)) return items @hooks.register('before_serve_page') def serve_variant(page, request, serve_args, serve_kwargs): """Apply a segment to a visitor before serving the page. :param page: The page being served :type page: wagtail.core.models.Page :param request: The http request :type request: django.http.HttpRequest :returns: A variant if one is available for the visitor's segment, otherwise the original page :rtype: wagtail.core.models.Page """ user_segments = [] if not isinstance(page, models.PersonalisablePageMixin): return adapter = get_segment_adapter(request) user_segments = adapter.get_segments() metadata = page.personalisation_metadata # If page is not canonical, don't serve it. if not metadata.is_canonical: raise Http404 if user_segments: # TODO: This is never more then one page? (fix query count) metadata = metadata.metadata_for_segments(user_segments) if metadata: variant = metadata.first().variant.specific return variant.serve(request, *serve_args, **serve_kwargs) @hooks.register('construct_explorer_page_queryset') def dont_show_variant(parent_page, pages, request): return utils.exclude_variants(pages) @hooks.register('register_page_listing_buttons') def page_listing_variant_buttons(page, page_perms, is_parent=False): """Adds page listing buttons to personalisable pages. Shows variants for the page (if any) and a 'Create a new variant' button. """ if not isinstance(page, models.PersonalisablePageMixin): return metadata = page.personalisation_metadata if metadata.is_canonical: yield ButtonWithDropdownFromHook( _('Variants'), hook_name='register_page_listing_variant_buttons', page=page, page_perms=page_perms, is_parent=is_parent, attrs={'target': '_blank', 'title': _('Create or edit a variant')}, priority=100) @hooks.register('register_page_listing_variant_buttons') def page_listing_more_buttons(page, page_perms, is_parent=False): """Adds a 'more' button to personalisable pages allowing users to quickly create a new variant for the selected segment. """ if not isinstance(page, models.PersonalisablePageMixin): return metadata = page.personalisation_metadata for vm in metadata.variants_metadata: yield Button('%s variant' % (vm.segment.name), reverse('wagtailadmin_pages:edit', args=[vm.variant_id]), attrs={"title": _('Edit this variant')}, classes=("icon", "icon-fa-pencil"), priority=0) for segment in metadata.get_unused_segments(): yield Button('%s variant' % (segment.name), reverse('segment:copy_page', args=[page.pk, segment.pk]), attrs={"title": _('Create this variant')}, classes=("icon", "icon-fa-plus"), priority=100) yield Button(_('Create a new segment'), reverse('wagtail_personalisation_segment_modeladmin_create'), attrs={"title": _('Create a new segment')}, classes=("icon", "icon-fa-snowflake-o"), priority=200) class CorrectedPagesSummaryItem(PagesSummaryItem): def get_context(self): # Perform the same check as Wagtail to get the correct count. # Only correct the count when a root page is available to the user. # The `PagesSummaryItem` will return a page count of 0 otherwise. # https://github.com/wagtail/wagtail/blob/5c9ff23e229acabad406c42c4e13cbaea32e6c15/wagtail/admin/site_summary.py#L38 context = super().get_context() root_page = context.get('root_page', None) if root_page: pages = utils.exclude_variants( Page.objects.descendant_of(root_page, inclusive=True)) page_count = pages.count() if root_page.is_root(): page_count -= 1 context['total_pages'] = page_count return context @hooks.register('construct_homepage_summary_items') def add_corrected_pages_summary_panel(request, items): """Replaces the Pages summary panel to hide variants.""" for index, item in enumerate(items): if item.__class__ is PagesSummaryItem: items[index] = CorrectedPagesSummaryItem(request) class SegmentSummaryPanel(SummaryItem): """The segment summary panel showing the total amount of segments on the site and allowing quick access to the Segment dashboard. """ order = 2000 def render(self): segment_count = models.Segment.objects.count() target_url = reverse('wagtail_personalisation_segment_modeladmin_index') title = _("Segments") return mark_safe("""
  • {}{}
  • """.format(target_url, segment_count, title)) class PersonalisedPagesSummaryPanel(PagesSummaryItem): order = 2100 def render(self): page_count = models.PersonalisablePageMetadata.objects.filter(segment__isnull=True).count() title = _("Personalised Page") return mark_safe("""
  • {}{}{}
  • """.format(page_count, title, pluralize(page_count))) class VariantPagesSummaryPanel(PagesSummaryItem): order = 2200 def render(self): page_count = models.PersonalisablePageMetadata.objects.filter( segment__isnull=False).count() title = _("Variant") return mark_safe("""
  • {}{}{}
  • """.format(page_count, title, pluralize(page_count))) @hooks.register('construct_homepage_summary_items') def add_personalisation_summary_panels(request, items): """Adds a summary panel to the Wagtail dashboard showing the total amount of segments on the site and allowing quick access to the Segment dashboard. """ items.append(SegmentSummaryPanel(request)) items.append(PersonalisedPagesSummaryPanel(request)) items.append(VariantPagesSummaryPanel(request)) @hooks.register('before_delete_page') def delete_related_variants(request, page): if not isinstance(page, models.PersonalisablePageMixin) \ or not page.personalisation_metadata.is_canonical: return # Get a list of related personalisation metadata for all the related # variants. variants_metadata = ( page.personalisation_metadata.variants_metadata .select_related('variant') ) next_url = get_valid_next_url_from_request(request) if request.method == 'POST': parent_id = page.get_parent().id with transaction.atomic(): # To ensure variants are deleted for all descendants, start with # the deepest ones, and explicitly delete variants and metadata # for all of them, including the page itself. Otherwise protected # foreign key constraints are violated. Only consider canonical # pages. for metadata in PersonalisablePageMetadata.objects.filter( canonical_page__in=page.get_descendants(inclusive=True), variant=F("canonical_page"), ).order_by('-canonical_page__depth'): for variant_metadata in metadata.variants_metadata.select_related('variant'): # Call delete() on objects to trigger any signals or hooks. variant_metadata.variant.delete() metadata.delete() metadata.canonical_page.delete() msg = _("Page '{0}' and its variants deleted.") messages.success( request, msg.format(page.get_admin_display_title()) ) for fn in hooks.get_hooks('after_delete_page'): result = fn(request, page) if hasattr(result, 'status_code'): return result if next_url: return redirect(next_url) return redirect('wagtailadmin_explore', parent_id) return render( request, 'wagtailadmin/pages/wagtail_personalisation/confirm_delete.html', { 'page': page, 'descendant_count': page.get_descendant_count(), 'next': next_url, 'variants': Page.objects.filter( pk__in=variants_metadata.values_list('variant_id') ) } )