diff --git a/README.rst b/README.rst index 9fc0b80..64b3f61 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,34 @@ Wagtail personalisation module ============================== -WIP \ No newline at end of file +Personalisation module for Wagtail. This project is still work in progresss. + + +Instructions +------------ +To install the package with pip:: + + pip install wagtail-personalisation + +Next, include the ``personalisation`` and ``wagtail.contrib.modeladmin`` app in your project's ``INSTALLED_APPS``: + +.. code-block:: python + + INSTALLED_APPS = [ + # ... + 'wagtail.contrib.modeladmin', + 'personalisation', + # ... + ] + +Last but not least, add ``personalisation.middleware.SegmentMiddleware`` to your project's ``MIDDLEWARE``: + +.. code-block:: python + + MIDDLEWARE = [ + # ... + 'personalisation.middleware.SegmentMiddleware', + # ... + ] + +Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has been added in first, this is a prerequisite for this project. \ No newline at end of file diff --git a/setup.py b/setup.py index d6144f3..5df0615 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ setup( 'Topic :: Internet :: WWW/HTTP :: Site Management', ], install_requires=[ + 'django-model-utils==2.6', 'wagtail>=1.7', ] -) \ No newline at end of file +) diff --git a/src/personalisation/admin.py b/src/personalisation/admin.py new file mode 100644 index 0000000..c111dea --- /dev/null +++ b/src/personalisation/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from personalisation import models + +class TimeRuleAdmin(admin.ModelAdmin): + list_display = ('name', 'start_time', 'end_time') + +class TimeRuleAdminInline(admin.TabularInline): + model = models.TimeRule + +class SegmentAdmin(admin.ModelAdmin): + list_display = ['name'] + inlines = (TimeRuleAdminInline,) + + +admin.site.register(models.TimeRule, TimeRuleAdmin) +admin.site.register(models.Segment, SegmentAdmin) diff --git a/src/personalisation/middleware.py b/src/personalisation/middleware.py index 0178f91..beb0290 100644 --- a/src/personalisation/middleware.py +++ b/src/personalisation/middleware.py @@ -1,3 +1,5 @@ +from personalisation.models import Segment, AbstractBaseRule, TimeRule + class SegmentMiddleware(object): """Middleware for testing and putting a user in a segment""" @@ -5,9 +7,21 @@ class SegmentMiddleware(object): self.get_response = get_response def __call__(self, request): - request.session['segmented'] = True + segments = Segment.objects.all().filter(status="live") + + chosen_segments = [] + + for segment in segments: + result = False + rules = AbstractBaseRule.objects.filter(segment=segment).select_subclasses() + for rule in rules: + result = rule.test_user() + if result: + chosen_segments.append(segment.encoded_name()) + + request.session['segments'] = chosen_segments response = self.get_response(request) - print(request.session['segmented']) + print(request.session['segments']) return response diff --git a/src/personalisation/migrations/0002_abstractbaserule_timerule.py b/src/personalisation/migrations/0002_abstractbaserule_timerule.py new file mode 100644 index 0000000..2cae5d2 --- /dev/null +++ b/src/personalisation/migrations/0002_abstractbaserule_timerule.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-11-07 13:53 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('personalisation', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AbstractBaseRule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='TimeRule', + fields=[ + ('abstractbaserule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='personalisation.AbstractBaseRule')), + ('start_time', models.TimeField(verbose_name='Starting time')), + ('end_time', models.TimeField(verbose_name='Ending time')), + ], + bases=('personalisation.abstractbaserule',), + ), + ] diff --git a/src/personalisation/migrations/0003_abstractbaserule_segment.py b/src/personalisation/migrations/0003_abstractbaserule_segment.py new file mode 100644 index 0000000..d2ac8b4 --- /dev/null +++ b/src/personalisation/migrations/0003_abstractbaserule_segment.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-11-07 14:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('personalisation', '0002_abstractbaserule_timerule'), + ] + + operations = [ + migrations.AddField( + model_name='abstractbaserule', + name='segment', + field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='segment', to='personalisation.Segment'), + preserve_default=False, + ), + ] diff --git a/src/personalisation/migrations/0004_segment_status.py b/src/personalisation/migrations/0004_segment_status.py new file mode 100644 index 0000000..76ad33b --- /dev/null +++ b/src/personalisation/migrations/0004_segment_status.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-11-07 14:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('personalisation', '0003_abstractbaserule_segment'), + ] + + operations = [ + migrations.AddField( + model_name='segment', + name='status', + field=models.CharField(choices=[('disabled', 'Disabled'), ('live', 'Live'), ('completed', 'Completed')], default='disabled', max_length=20), + ), + ] diff --git a/src/personalisation/models.py b/src/personalisation/models.py index 14399df..2ac3b00 100644 --- a/src/personalisation/models.py +++ b/src/personalisation/models.py @@ -1,10 +1,14 @@ from __future__ import absolute_import, unicode_literals +from datetime import datetime, time + from django.conf import settings from django.db import models from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ from modelcluster.models import ClusterableModel +from model_utils.managers import InheritanceManager from wagtail.wagtailadmin.edit_handlers import FieldPanel @@ -14,10 +18,58 @@ Model for a new segment @python_2_unicode_compatible class Segment(ClusterableModel): name = models.CharField(max_length=255) + STATUS_CHOICES = ( + ('disabled', 'Disabled'), + ('live', 'Live'), + ('completed', 'Completed'), + ) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="disabled") panels = [ FieldPanel('name'), ] def __str__(self): - return self.name \ No newline at end of file + return self.name + + def encoded_name(self): + return "".join(self.name.lower().split()) + + + +""" +Base for creating rules to segment users with +""" +@python_2_unicode_compatible +class AbstractBaseRule(models.Model): + name = models.CharField(max_length=255) + segment = models.ForeignKey(to=Segment, related_name="segment") + objects = InheritanceManager() + + def test_user(self, request=None): + return True + + def return_segment_id(self): + return "".join(self.name.lower().split()) + + def __str__(self): + return self.name + + +""" +Time rule to segment users with +""" +@python_2_unicode_compatible +class TimeRule(AbstractBaseRule): + start_time = models.TimeField(_("Starting time")) + end_time = models.TimeField(_("Ending time")) + + def __init__(self, *args, **kwargs): + super(TimeRule, self).__init__(*args, **kwargs) + + def test_user(self, request=None): + current_time = datetime.now().time() + starting_time = self.start_time + ending_time = self.end_time + + return starting_time <= current_time <= ending_time