7

Wagtail 3 changes

This commit is contained in:
Alex Bridge
2022-01-28 11:53:57 +00:00
committed by nick.moreton
parent dd4530203f
commit c7eaec1315
89 changed files with 3003 additions and 1456 deletions

View File

@ -1,6 +1,7 @@
import random
import wagtail
from django import VERSION as DJANGO_VERSION
from django import forms
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
@ -8,11 +9,32 @@ from django.db import models, transaction
from django.template.defaultfilters import slugify
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel
from wagtail.admin.edit_handlers import (
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
from wagtail.core.models import Page
from wagtail import VERSION as WAGTAIL_VERSION
if WAGTAIL_VERSION >= (3, 0):
from wagtail.admin.panels import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
)
from wagtail.models import Page
else:
from wagtail.admin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
)
from wagtail.core.models import Page
from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import count_active_days
@ -22,7 +44,7 @@ from .forms import SegmentAdminForm
class RulePanel(InlinePanel):
def on_model_bound(self):
self.relation_name = self.relation_name.replace('_related', 's')
self.relation_name = self.relation_name.replace("_related", "s")
self.db_field = self.model._meta.get_field(self.relation_name)
manager = getattr(self.model, self.relation_name)
self.related = manager.rel
@ -35,20 +57,21 @@ class SegmentQuerySet(models.QuerySet):
class Segment(ClusterableModel):
"""The segment model."""
STATUS_ENABLED = 'enabled'
STATUS_DISABLED = 'disabled'
STATUS_ENABLED = "enabled"
STATUS_DISABLED = "disabled"
STATUS_CHOICES = (
(STATUS_ENABLED, _('Enabled')),
(STATUS_DISABLED, _('Disabled')),
(STATUS_ENABLED, _("Enabled")),
(STATUS_DISABLED, _("Disabled")),
)
TYPE_DYNAMIC = 'dynamic'
TYPE_STATIC = 'static'
TYPE_DYNAMIC = "dynamic"
TYPE_STATIC = "static"
TYPE_CHOICES = (
(TYPE_DYNAMIC, _('Dynamic')),
(TYPE_STATIC, _('Static')),
(TYPE_DYNAMIC, _("Dynamic")),
(TYPE_STATIC, _("Static")),
)
name = models.CharField(max_length=255)
@ -58,18 +81,22 @@ class Segment(ClusterableModel):
disable_date = models.DateTimeField(null=True, editable=False)
visit_count = models.PositiveIntegerField(default=0, editable=False)
status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED)
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED
)
persistent = models.BooleanField(
default=False, help_text=_("Should the segment persist between visits?"))
default=False, help_text=_("Should the segment persist between visits?")
)
match_any = models.BooleanField(
default=False,
help_text=_("Should the segment match all the rules or just one of them?")
help_text=_("Should the segment match all the rules or just one of them?"),
)
type = models.CharField(
max_length=20,
choices=TYPE_CHOICES,
default=TYPE_DYNAMIC,
help_text=mark_safe(_("""
help_text=mark_safe(
_(
"""
</br></br><strong>Dynamic:</strong> Users in this segment will change
as more or less meet the rules specified in the segment.
</br><strong>Static:</strong> If the segment contains only static
@ -77,37 +104,42 @@ class Segment(ClusterableModel):
those rules when the segment is created. Mixed static segments or
those containing entirely non static compatible rules will be
populated using the count variable.
"""))
"""
)
),
)
count = models.PositiveSmallIntegerField(
default=0,
help_text=_(
"If this number is set for a static segment users will be added to the "
"set until the number is reached. After this no more users will be added."
)
),
)
static_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
)
excluded_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
help_text=_("Users that matched the rules but were excluded from the "
"segment for some reason e.g. randomisation"),
related_name="excluded_segments"
help_text=_(
"Users that matched the rules but were excluded from the "
"segment for some reason e.g. randomisation"
),
related_name="excluded_segments",
)
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
randomisation_percent = models.PositiveSmallIntegerField(
null=True, blank=True, default=None,
null=True,
blank=True,
default=None,
help_text=_(
"If this number is set each user matching the rules will "
"have this percentage chance of being placed in the segment."
), validators=[
MaxValueValidator(100),
MinValueValidator(0)
])
),
validators=[MaxValueValidator(100), MinValueValidator(0)],
)
objects = SegmentQuerySet.as_manager()
@ -115,26 +147,37 @@ class Segment(ClusterableModel):
def __init__(self, *args, **kwargs):
Segment.panels = [
MultiFieldPanel([
FieldPanel('name', classname="title"),
FieldRowPanel([
FieldPanel('status'),
FieldPanel('persistent'),
]),
FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count', classname='count_field'),
FieldPanel('randomisation_percent', classname='percent_field'),
], heading="Segment"),
MultiFieldPanel([
RulePanel(
"{}_related".format(rule_model._meta.db_table),
label='{}{}'.format(
rule_model._meta.verbose_name,
' ({})'.format(_('Static compatible')) if rule_model.static else ''
MultiFieldPanel(
[
FieldPanel("name", classname="title"),
FieldRowPanel(
[
FieldPanel("status"),
FieldPanel("persistent"),
]
),
) for rule_model in AbstractBaseRule.__subclasses__()
], heading=_("Rules")),
FieldPanel("match_any"),
FieldPanel("type", widget=forms.RadioSelect),
FieldPanel("count", classname="count_field"),
FieldPanel("randomisation_percent", classname="percent_field"),
],
heading="Segment",
),
MultiFieldPanel(
[
RulePanel(
"{}_related".format(rule_model._meta.db_table),
label="{}{}".format(
rule_model._meta.verbose_name,
" ({})".format(_("Static compatible"))
if rule_model.static
else "",
),
)
for rule_model in AbstractBaseRule.__subclasses__()
],
heading=_("Rules"),
),
]
super(Segment, self).__init__(*args, **kwargs)
@ -179,20 +222,21 @@ class Segment(ClusterableModel):
"""Retrieve all rules in the segment."""
segment_rules = []
for rule_model in AbstractBaseRule.get_descendant_models():
segment_rules.extend(
rule_model._default_manager.filter(segment=self))
segment_rules.extend(rule_model._default_manager.filter(segment=self))
return segment_rules
def toggle(self, save=True):
self.status = (
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
else self.STATUS_DISABLED)
self.STATUS_ENABLED
if self.status == self.STATUS_DISABLED
else self.STATUS_DISABLED
)
if save:
self.save()
def randomise_into_segment(self):
""" Returns True if randomisation_percent is not set or it generates
"""Returns True if randomisation_percent is not set or it generates
a random number less than the randomisation_percent
This is so there is some randomisation in which users are added to the
segment
@ -210,21 +254,24 @@ class PersonalisablePageMetadata(ClusterableModel):
segments.
"""
# Canonical pages should not ever be deleted if they have variants
# because the variants will be orphaned.
canonical_page = models.ForeignKey(
Page, models.PROTECT, related_name='personalisable_canonical_metadata',
null=True
Page,
models.PROTECT,
related_name="personalisable_canonical_metadata",
null=True,
)
# Delete metadata of the variant if its page gets deleted.
variant = models.OneToOneField(
Page, models.CASCADE, related_name='_personalisable_page_metadata',
null=True
Page, models.CASCADE, related_name="_personalisable_page_metadata", null=True
)
segment = models.ForeignKey(Segment, models.PROTECT, null=True,
related_name='page_metadata')
segment = models.ForeignKey(
Segment, models.PROTECT, null=True, related_name="page_metadata"
)
@cached_property
def has_variants(self):
@ -241,10 +288,12 @@ class PersonalisablePageMetadata(ClusterableModel):
@cached_property
def variants_metadata(self):
return (
PersonalisablePageMetadata.objects
.filter(canonical_page_id=self.canonical_page_id)
PersonalisablePageMetadata.objects.filter(
canonical_page_id=self.canonical_page_id
)
.exclude(variant_id=self.variant_id)
.exclude(variant_id=self.canonical_page_id))
.exclude(variant_id=self.canonical_page_id)
)
@cached_property
def is_canonical(self):
@ -265,33 +314,31 @@ class PersonalisablePageMetadata(ClusterableModel):
slug = "{}-{}".format(page.slug, segment.encoded_name())
title = "{} ({})".format(page.title, segment.name)
update_attrs = {
'title': title,
'slug': slug,
'live': False,
"title": title,
"slug": slug,
"live": False,
}
with transaction.atomic():
new_page = self.canonical_page.copy(
update_attrs=update_attrs, copy_revisions=False)
update_attrs=update_attrs, copy_revisions=False
)
PersonalisablePageMetadata.objects.create(
canonical_page=page,
variant=new_page,
segment=segment)
canonical_page=page, variant=new_page, segment=segment
)
return new_page
def metadata_for_segments(self, segments):
return (
self.__class__.objects
.filter(
canonical_page_id=self.canonical_page_id,
segment__in=segments))
return self.__class__.objects.filter(
canonical_page_id=self.canonical_page_id, segment__in=segments
)
def get_unused_segments(self):
if self.is_canonical:
return (
Segment.objects
.exclude(page_metadata__canonical_page_id=self.canonical_page_id))
return Segment.objects.exclude(
page_metadata__canonical_page_id=self.canonical_page_id
)
return Segment.objects.none()
@ -307,7 +354,8 @@ class PersonalisablePageMixin:
metadata = self._personalisable_page_metadata
except AttributeError:
metadata = PersonalisablePageMetadata.objects.create(
canonical_page=self, variant=self)
canonical_page=self, variant=self
)
return metadata
def get_sitemap_urls(self, request=None):