7

Refactor of original branch

This commit is contained in:
Jasper Berghoef
2017-08-24 17:32:58 +02:00
parent 83c2a4289e
commit c3e237b970
10 changed files with 256 additions and 65 deletions

View File

@ -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

View File

@ -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(

View File

@ -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),
),
]

View File

@ -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.

View File

@ -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()

View File

@ -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" %}

View File

@ -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 %}

View File

@ -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):

View File

@ -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)

View File

@ -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()