Refactor of original branch
This commit is contained in:
@ -24,7 +24,6 @@
|
||||
"edit_date": "2017-06-02T10:58:39.399Z",
|
||||
"enable_date": "2017-06-02T10:58:39.389Z",
|
||||
"disable_date": "2017-06-02T10:34:51.722Z",
|
||||
"visit_count": 0,
|
||||
"status": "enabled",
|
||||
"persistent": false,
|
||||
"match_any": false
|
||||
@ -38,7 +37,6 @@
|
||||
"edit_date": "2017-06-02T10:57:44.504Z",
|
||||
"enable_date": "2017-06-02T10:57:44.497Z",
|
||||
"disable_date": "2017-06-02T10:57:39.984Z",
|
||||
"visit_count": 1,
|
||||
"status": "enabled",
|
||||
"persistent": false,
|
||||
"match_any": false
|
||||
|
@ -1,7 +1,6 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from wagtail_personalisation.models import Segment
|
||||
@ -36,7 +35,7 @@ class BaseSegmentsAdapter(object):
|
||||
def refresh(self):
|
||||
"""Refresh the segments stored in the adapter storage."""
|
||||
|
||||
def _test_rules(self, rules, request, match_any=False):
|
||||
def _test_rules(self, rules, match_any=False):
|
||||
"""Tests the provided rules to see if the request still belongs
|
||||
to a segment.
|
||||
:param rules: The rules to test for
|
||||
@ -50,9 +49,20 @@ class BaseSegmentsAdapter(object):
|
||||
"""
|
||||
if not rules:
|
||||
return False
|
||||
|
||||
if not hasattr(self.request, 'matched_rules'):
|
||||
self.request.matched_rules = []
|
||||
|
||||
results = []
|
||||
for rule in rules:
|
||||
validation = rule.test_user(self.request)
|
||||
if validation:
|
||||
self.request.matched_rules.append(rule.pk)
|
||||
results.append(validation)
|
||||
|
||||
if match_any:
|
||||
return any(rule.test_user(request) for rule in rules)
|
||||
return all(rule.test_user(request) for rule in rules)
|
||||
return any(results)
|
||||
return all(results)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -150,17 +160,6 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
return visit['count']
|
||||
return 0
|
||||
|
||||
def update_visit_count(self):
|
||||
"""Update the visit count for all segments in the request session."""
|
||||
segments = self.request.session['segments']
|
||||
segment_pks = [s['id'] for s in segments]
|
||||
|
||||
# Update counts
|
||||
(Segment.objects
|
||||
.enabled()
|
||||
.filter(pk__in=segment_pks)
|
||||
.update(visit_count=F('visit_count') + 1))
|
||||
|
||||
def refresh(self):
|
||||
"""Retrieve the request session segments and verify whether or not they
|
||||
still apply to the requesting visitor.
|
||||
@ -178,14 +177,13 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
for rule_model in rule_models:
|
||||
segment_rules.extend(rule_model.objects.filter(segment=segment))
|
||||
|
||||
result = self._test_rules(segment_rules, self.request,
|
||||
match_any=segment.match_any)
|
||||
result = self._test_rules(
|
||||
segment_rules, match_any=segment.match_any)
|
||||
|
||||
if result:
|
||||
additional_segments.append(segment)
|
||||
|
||||
self.set_segments(current_segments + additional_segments)
|
||||
self.update_visit_count()
|
||||
|
||||
|
||||
SEGMENT_ADAPTER_CLASS = import_string(getattr(
|
||||
|
@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-24 15:03
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SegmentVisit',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('session', models.CharField(db_index=True, editable=False, max_length=64, null=True)),
|
||||
('visit_date', models.DateTimeField(auto_now_add=True)),
|
||||
('page', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailcore.Page')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SegmentVisitMetadata',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('matched_rules', models.CharField(max_length=255, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z', 32), code='invalid', message='Enter only digits separated by commas.')])),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='segment',
|
||||
name='visit_count',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segmentvisitmetadata',
|
||||
name='segment',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtail_personalisation.Segment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segmentvisitmetadata',
|
||||
name='visit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wagtail_personalisation.SegmentVisit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segmentvisit',
|
||||
name='segments',
|
||||
field=models.ManyToManyField(through='wagtail_personalisation.SegmentVisitMetadata', to='wagtail_personalisation.Segment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segmentvisit',
|
||||
name='served_segment',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='served_segment', to='wagtail_personalisation.Segment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segmentvisit',
|
||||
name='served_variant',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='served_variant', to='wagtailcore.Page'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segmentvisit',
|
||||
name='user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -1,5 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.validators import validate_comma_separated_integer_list
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
@ -35,7 +37,6 @@ class Segment(ClusterableModel):
|
||||
edit_date = models.DateTimeField(auto_now=True)
|
||||
enable_date = models.DateTimeField(null=True, editable=False)
|
||||
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)
|
||||
persistent = models.BooleanField(
|
||||
@ -74,10 +75,27 @@ class Segment(ClusterableModel):
|
||||
"""Return a string with a slug for the segment."""
|
||||
return slugify(self.name.lower())
|
||||
|
||||
def get_active_days(self):
|
||||
@property
|
||||
def active_days(self):
|
||||
"""Return the amount of days the segment has been active."""
|
||||
return count_active_days(self.enable_date, self.disable_date)
|
||||
|
||||
def get_visits(self):
|
||||
"""Return the segment visits."""
|
||||
return SegmentVisit.objects.filter(segments=self)
|
||||
|
||||
@property
|
||||
def visit_count(self):
|
||||
"""Returns the total amount of segment visits."""
|
||||
return self.get_visits().count()
|
||||
|
||||
def get_serves(self):
|
||||
return SegmentVisit.objects.filter(served_segment=self)
|
||||
|
||||
@property
|
||||
def serve_count(self):
|
||||
return self.get_serves().count()
|
||||
|
||||
def get_used_pages(self):
|
||||
"""Return the pages that have variants using this segment."""
|
||||
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
|
||||
@ -107,6 +125,84 @@ class Segment(ClusterableModel):
|
||||
self.save()
|
||||
|
||||
|
||||
class SegmentVisitMetadata(models.Model):
|
||||
visit = models.ForeignKey(
|
||||
'wagtail_personalisation.SegmentVisit', on_delete=models.CASCADE)
|
||||
segment = models.ForeignKey(
|
||||
'wagtail_personalisation.Segment', on_delete=models.SET_NULL, null=True)
|
||||
matched_rules = models.CharField(
|
||||
max_length=255, validators=[validate_comma_separated_integer_list])
|
||||
|
||||
|
||||
class SegmentVisit(models.Model):
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True)
|
||||
page = models.ForeignKey(Page, on_delete=models.SET_NULL, null=True)
|
||||
segments = models.ManyToManyField(Segment, through=SegmentVisitMetadata)
|
||||
served_segment = models.ForeignKey(
|
||||
Segment, on_delete=models.CASCADE,
|
||||
related_name='served_segment', null=True)
|
||||
served_variant = models.ForeignKey(
|
||||
Page, on_delete=models.SET_NULL,
|
||||
related_name='served_variant', null=True)
|
||||
session = models.CharField(
|
||||
max_length=64, editable=False, null=True, db_index=True)
|
||||
visit_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@classmethod
|
||||
def create_segment_visit(cls, page, request, metadata=None):
|
||||
"""Create a segment visit object.
|
||||
:param page: The page being visited
|
||||
:type page: wagtail.wagtailcore.models.Page
|
||||
:param request: The http request
|
||||
:type request: django.http.HttpRequest
|
||||
:param metadata: A list of personalisable page metadata
|
||||
:type page: wagtail_personalisation.models.PersonalisablePageMetadata
|
||||
:returns: A committed Segment Visit object
|
||||
:rtype: wagtail_personalisation.models.SegmentVisit
|
||||
"""
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
user_segments = adapter.get_segments()
|
||||
|
||||
if not metadata:
|
||||
metadata = page.personalisation_metadata
|
||||
metadata = metadata.metadata_for_segments(user_segments)
|
||||
|
||||
user = request.user if request.user.is_authenticated() else None
|
||||
visit = cls.objects.create(
|
||||
user=user,
|
||||
page=page,
|
||||
served_segment=metadata.first().segment,
|
||||
served_variant=metadata.first().variant,
|
||||
session=request.session.session_key
|
||||
)
|
||||
|
||||
for segment in user_segments:
|
||||
rules = [rule for rule in segment.get_rules()
|
||||
if rule.pk in request.matched_rules]
|
||||
|
||||
SegmentVisitMetadata.objects.create(
|
||||
visit=visit,
|
||||
segment=segment,
|
||||
matched_rules=','.join(str(rule.pk) for rule in rules)
|
||||
)
|
||||
|
||||
return visit
|
||||
|
||||
@classmethod
|
||||
def reverse_match(cls, user):
|
||||
# TODO: Find a way to automate this, preferably without celery.
|
||||
user_visits = cls.objects.filter(user=user)
|
||||
|
||||
for visit in user_visits:
|
||||
cls.objects.filter(
|
||||
session=visit.session,
|
||||
user__isnull=True
|
||||
).update(user=user)
|
||||
|
||||
|
||||
class PersonalisablePageMetadata(ClusterableModel):
|
||||
"""The personalisable page model. Allows creation of variants with linked
|
||||
segments.
|
||||
|
@ -14,7 +14,6 @@ def check_status_change(sender, instance, *args, **kwargs):
|
||||
if original_status != instance.status:
|
||||
if instance.status == instance.STATUS_ENABLED:
|
||||
instance.enable_date = timezone.now()
|
||||
instance.visit_count = 0
|
||||
return instance
|
||||
if instance.status == instance.STATUS_DISABLED:
|
||||
instance.disable_date = timezone.now()
|
||||
|
@ -28,7 +28,7 @@
|
||||
<ul class="inspect segment_stats">
|
||||
<li class="visit_stat">
|
||||
{% trans "This segment has been visited" %}
|
||||
<span class="icon icon-fa-rocket">{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
|
||||
<span class="icon icon-fa-rocket">{{ segment.visit_count }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
|
||||
</li>
|
||||
<li class="days_stat">
|
||||
{% trans "This segment has been active for" %}
|
||||
|
@ -0,0 +1,38 @@
|
||||
{% extends "modeladmin/inspect.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load wagtailadmin_tags %}
|
||||
|
||||
{% block content_main %}
|
||||
<div class="row">
|
||||
<div class="col12">
|
||||
<div class="nice-padding">
|
||||
<p class="back"><a href="{{ view.index_url }}" class="icon icon-arrow-left">{% blocktrans with view.verbose_name as model_name %}Back to {{ model_name }} list{% endblocktrans %}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% block fields_output %}
|
||||
<div class="col6">
|
||||
<div class="nice-padding">
|
||||
{{ block.super }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block rule_data %}
|
||||
<div class="col6">
|
||||
<div class="nice-padding">
|
||||
{% if instance.rules %}
|
||||
<dl>
|
||||
{% for rule in instance.get_rules %}
|
||||
<dt class="{{ field.type|lower }}">{{ rule }}</dt>
|
||||
<dd>{{ rule.description.title }} {{ rule.description.value|lower }} ({{ rule.get_hit_percentage }}%)</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -5,11 +5,12 @@ from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
||||
from wagtail.contrib.modeladmin.options import (
|
||||
ModelAdmin, ModelAdminGroup, modeladmin_register)
|
||||
from wagtail.contrib.modeladmin.views import IndexView
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.models import Segment, SegmentVisit
|
||||
|
||||
|
||||
class SegmentModelIndexView(IndexView):
|
||||
@ -32,16 +33,18 @@ class SegmentModelDashboardView(IndexView):
|
||||
]
|
||||
|
||||
|
||||
@modeladmin_register
|
||||
class SegmentModelAdmin(ModelAdmin):
|
||||
"""The model admin for the Segments administration interface."""
|
||||
model = Segment
|
||||
index_view_class = SegmentModelIndexView
|
||||
dashboard_view_class = SegmentModelDashboardView
|
||||
inspect_view_enabled = True
|
||||
menu_icon = 'fa-snowflake-o'
|
||||
add_to_settings_menu = False
|
||||
list_display = ('name', 'persistent', 'match_any', 'status',
|
||||
'page_count', 'variant_count', 'statistics')
|
||||
# TODO: Only show additional fields when 'detailed visits' is enabled.
|
||||
list_display = (
|
||||
'name', 'persistent', 'match_any', 'status', 'page_count',
|
||||
'variant_count', 'active_days', 'visit_count', 'serve_count')
|
||||
index_view_extra_js = ['js/commons.js', 'js/index.js']
|
||||
index_view_extra_css = ['css/index.css']
|
||||
form_view_extra_js = ['js/commons.js', 'js/form.js']
|
||||
@ -63,9 +66,28 @@ class SegmentModelAdmin(ModelAdmin):
|
||||
def variant_count(self, obj):
|
||||
return len(obj.get_created_variants())
|
||||
|
||||
def statistics(self, obj):
|
||||
return _("{visits} visits in {days} days").format(
|
||||
visits=obj.visit_count, days=obj.get_active_days())
|
||||
|
||||
class SegmentVisitModelAdmin(ModelAdmin):
|
||||
model = SegmentVisit
|
||||
menu_icon = 'fa-rocket'
|
||||
list_display = (
|
||||
'page', 'segments_display', 'served_segment', 'served_variant', 'user',
|
||||
'session', 'visit_date')
|
||||
list_filter = ('page', 'served_segment', 'served_variant', 'user')
|
||||
search_fields = ('segments', 'user' 'session')
|
||||
|
||||
def segments_display(self, obj):
|
||||
return [segment.__str__() for segment in obj.segments.all()]
|
||||
|
||||
segments_display.short_description = 'Segments'
|
||||
|
||||
|
||||
class PersonalisationAdminGroup(ModelAdminGroup):
|
||||
menu_label = 'Personalisation'
|
||||
menu_icon = 'fa-magic'
|
||||
items = (SegmentModelAdmin, SegmentVisitModelAdmin)
|
||||
|
||||
modeladmin_register(PersonalisationAdminGroup)
|
||||
|
||||
|
||||
def toggle_segment_view(request):
|
||||
|
@ -14,6 +14,7 @@ from wagtail.wagtailcore.models import Page
|
||||
|
||||
from wagtail_personalisation import admin_urls, models, utils
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import SegmentVisit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -85,6 +86,7 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
||||
metadata = metadata.metadata_for_segments(user_segments)
|
||||
if metadata:
|
||||
variant = metadata.first().variant.specific
|
||||
SegmentVisit.create_segment_visit(page, request, metadata)
|
||||
return variant.serve(request, *serve_args, **serve_kwargs)
|
||||
|
||||
|
||||
|
@ -67,36 +67,3 @@ def test_add_page_visit(rf, site):
|
||||
assert request.session['visit_count'][0]['count'] == 2
|
||||
|
||||
assert adapter.get_visit_count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_visit_count(rf, site):
|
||||
request = rf.get('/')
|
||||
|
||||
adapter = adapters.SessionSegmentsAdapter(request)
|
||||
|
||||
segment_1 = SegmentFactory(name='segment-1', persistent=True, visit_count=0)
|
||||
segment_2 = SegmentFactory(name='segment-2', persistent=True, visit_count=0)
|
||||
|
||||
adapter.set_segments([segment_1, segment_2])
|
||||
adapter.update_visit_count()
|
||||
|
||||
segment_1.refresh_from_db()
|
||||
segment_2.refresh_from_db()
|
||||
|
||||
assert segment_1.visit_count == 1
|
||||
assert segment_2.visit_count == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_visit_count_deleted_segment(rf, site):
|
||||
request = rf.get('/')
|
||||
|
||||
adapter = adapters.SessionSegmentsAdapter(request)
|
||||
|
||||
segment_1 = SegmentFactory(name='segment-1', persistent=True, visit_count=0)
|
||||
segment_2 = SegmentFactory(name='segment-2', persistent=True, visit_count=0)
|
||||
|
||||
adapter.set_segments([segment_1, segment_2])
|
||||
segment_2.delete()
|
||||
adapter.update_visit_count()
|
||||
|
Reference in New Issue
Block a user