From 6e32a2e6a3bdef46eebad2a40501be5c4ca694eb Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Sun, 18 Dec 2016 11:38:19 +0100 Subject: [PATCH 1/8] adds first quick adapter draft --- src/personalisation/adapters.py | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/personalisation/adapters.py diff --git a/src/personalisation/adapters.py b/src/personalisation/adapters.py new file mode 100644 index 0000000..6be4cd7 --- /dev/null +++ b/src/personalisation/adapters.py @@ -0,0 +1,55 @@ +from personalisation.models import Segment + +class BaseSegmentsAdapter(object): + """Base adapter with required functions predefined""" + def __init__(self): + return + + def get(self): + return + + def add(self): + return + + def refresh(self): + return + + def check_segment_exists(self): + return + + class Meta: + abstract = True + + +class SessionSegmentsAdapter(BaseSegmentsAdapter): + """Segments adapter that uses Django's SessionMiddleware to store segments""" + # Setup + def __init__(self, request): + self.request = request + + # Set up segments dictionary object in the session + if 'segments' not in self.request.session: + self.request.session['segments'] = [] + + # Get segments + def get(self): + return self.request.session['segments'] + + # Add segments + def add(self, segment): + self.request.session['segments'].append(segment) + + def refresh(self): + current_segments = self.request.session['segments'] + persistent_segments = Segment.objects.filter(persistent=True) + + current_segments = [item for item in current_segments if + any(seg.pk for seg in persistent_segments) == item['id']] + + self.request.session['segments'] = current_segments + + # Quick checking logic to see if a segment exists + def check_segment_exists(self, segment): + segments = self.request.session['segments'] + + return any(item for item in self.request.session['segments'] if segment.pk == item.id) From 24847c18280ddb87a0ca17f1688117d6f945c8df Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Mon, 19 Dec 2016 10:33:12 +0100 Subject: [PATCH 2/8] adds settings logic for your own segments adapter --- README.rst | 11 +++++++++++ src/personalisation/app_settings.py | 7 +++++++ 2 files changed, 18 insertions(+) create mode 100644 src/personalisation/app_settings.py diff --git a/README.rst b/README.rst index 9314600..364f045 100644 --- a/README.rst +++ b/README.rst @@ -23,3 +23,14 @@ Next, include the ``personalisation`` and ``wagtail.contrib.modeladmin`` app in ] Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has been added in first, this is a prerequisite for this project. + +Changing segments adapter +------------------------- +To change the segments adapter, first make a new one based on the ``BaseSegmentsAdapter`` + +.. code-block:: python + +class YourSegmentsAdapter(BaseSegmentsAdapter): + # Add your own logic here + +Add the ``PERSONALISATION_SEGMENTS_ADAPTER`` setting to your settings.py and choose your own adapter. diff --git a/src/personalisation/app_settings.py b/src/personalisation/app_settings.py new file mode 100644 index 0000000..ec40c29 --- /dev/null +++ b/src/personalisation/app_settings.py @@ -0,0 +1,7 @@ +from django.conf import settings + +from personalisation.adapters import SessionSegmentsAdapter + +settings_adapter = getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', SessionSegmentsAdapter) + +segments_adapter = settings_adapter() From 6ecc15c1ddd546652e0ca16f0d5b4efb1798a84a Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Mon, 19 Dec 2016 11:01:48 +0100 Subject: [PATCH 3/8] adds adapter logic to wagtail hook --- README.rst | 4 +- src/personalisation/adapters.py | 57 ++++++++++++++++++++-- src/personalisation/app_settings.py | 3 +- src/personalisation/wagtail_hooks.py | 70 ++-------------------------- 4 files changed, 61 insertions(+), 73 deletions(-) diff --git a/README.rst b/README.rst index 364f045..b352ea9 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ To change the segments adapter, first make a new one based on the ``BaseSegments .. code-block:: python -class YourSegmentsAdapter(BaseSegmentsAdapter): - # Add your own logic here + class YourSegmentsAdapter(BaseSegmentsAdapter): + # Add your own logic here Add the ``PERSONALISATION_SEGMENTS_ADAPTER`` setting to your settings.py and choose your own adapter. diff --git a/src/personalisation/adapters.py b/src/personalisation/adapters.py index 6be4cd7..9af4fbc 100644 --- a/src/personalisation/adapters.py +++ b/src/personalisation/adapters.py @@ -1,8 +1,10 @@ -from personalisation.models import Segment +import time + +from personalisation.models import AbstractBaseRule, Segment class BaseSegmentsAdapter(object): """Base adapter with required functions predefined""" - def __init__(self): + def setup(self): return def get(self): @@ -24,7 +26,7 @@ class BaseSegmentsAdapter(object): class SessionSegmentsAdapter(BaseSegmentsAdapter): """Segments adapter that uses Django's SessionMiddleware to store segments""" # Setup - def __init__(self, request): + def setup(self, request): self.request = request # Set up segments dictionary object in the session @@ -37,7 +39,21 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): # Add segments def add(self, segment): - self.request.session['segments'].append(segment) + def check_if_segmented(item): + """Check if the user has been segmented""" + for seg in self.request.session['segments']: + if seg['encoded_name'] == item.encoded_name(): + return True + return False + + if not check_if_segmented(segment): + segdict = { + "encoded_name": segment.encoded_name(), + "id": segment.pk, + "timestamp": int(time.time()), + "persistent": segment.persistent, + } + self.request.session['segments'].append(segdict) def refresh(self): current_segments = self.request.session['segments'] @@ -48,8 +64,41 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): self.request.session['segments'] = current_segments + segments = Segment.objects.all().filter(status='enabled') + + for segment in segments: + rules = AbstractBaseRule.__subclasses__() + segment_rules = [] + for rule in rules: + queried_rules = rule.objects.filter(segment=segment) + for result in queried_rules: + segment_rules.append(result) + result = _test_rules(segment_rules, self.request) + + if result: + self.add(segment) + + + for seg in self.request.session['segments']: + segment = Segment.objects.get(pk=seg['id']) + segment.visit_count = segment.visit_count + 1 + segment.save() + # Quick checking logic to see if a segment exists def check_segment_exists(self, segment): segments = self.request.session['segments'] return any(item for item in self.request.session['segments'] if segment.pk == item.id) + + +def _test_rules(rules, request): + """Test whether the user matches a segment's rules'""" + if len(rules) > 0: + for rule in rules: + result = rule.test_user(request) + + if result is False: + return False + + return True + return False diff --git a/src/personalisation/app_settings.py b/src/personalisation/app_settings.py index ec40c29..340cb7d 100644 --- a/src/personalisation/app_settings.py +++ b/src/personalisation/app_settings.py @@ -2,6 +2,5 @@ from django.conf import settings from personalisation.adapters import SessionSegmentsAdapter -settings_adapter = getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', SessionSegmentsAdapter) +segments_adapter = getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', SessionSegmentsAdapter) -segments_adapter = settings_adapter() diff --git a/src/personalisation/wagtail_hooks.py b/src/personalisation/wagtail_hooks.py index 7f23eba..c81cc47 100644 --- a/src/personalisation/wagtail_hooks.py +++ b/src/personalisation/wagtail_hooks.py @@ -13,11 +13,13 @@ from wagtail.wagtailadmin.widgets import ( from wagtail.wagtailcore import hooks from personalisation import admin_urls +from personalisation.app_settings import segments_adapter from personalisation.models import (AbstractBaseRule, PersonalisablePage, Segment) from personalisation.utils import impersonate_other_page logger = logging.getLogger() +adapter = segments_adapter() @hooks.register('register_admin_urls') @@ -78,71 +80,9 @@ def set_visit_count(page, request, serve_args, serve_kwargs): @hooks.register('before_serve_page') def segment_user(page, request, serve_args, serve_kwargs): - if 'segments' not in request.session: - request.session['segments'] = [] - - current_segments = request.session['segments'] - persistent_segments = Segment.objects.filter(persistent=True) - - current_segments = [item for item in current_segments if any(seg.pk for seg in persistent_segments) == item['id']] - - request.session['segments'] = current_segments - - segments = Segment.objects.all().filter(status='enabled') - - for segment in segments: - rules = AbstractBaseRule.__subclasses__() - segment_rules = [] - for rule in rules: - queried_rules = rule.objects.filter(segment=segment) - for result in queried_rules: - segment_rules.append(result) - result = _test_rules(segment_rules, request) - - if result: - _add_segment_to_user(segment, request) - - if request.session['segments']: - logger.info("User has been added to the following segments: {}" - .format(request.session['segments'])) - - for seg in request.session['segments']: - segment = Segment.objects.get(pk=seg['id']) - segment.visit_count = segment.visit_count + 1 - segment.save() - - -def _test_rules(rules, request): - """Test whether the user matches a segment's rules'""" - if len(rules) > 0: - for rule in rules: - result = rule.test_user(request) - - if result is False: - return False - - return True - return False - - -def _add_segment_to_user(segment, request): - """Save the segment in the user session""" - - def check_if_segmented(segment): - """Check if the user has been segmented""" - for seg in request.session['segments']: - if seg['encoded_name'] == segment.encoded_name(): - return True - return False - - if not check_if_segmented(segment): - segdict = { - "encoded_name": segment.encoded_name(), - "id": segment.pk, - "timestamp": int(time.time()), - "persistent": segment.persistent, - } - request.session['segments'].append(segdict) + # Always run setup first on each segment, should have logic to not overwrite + adapter.setup(request) + adapter.refresh() @hooks.register('before_serve_page') From 94b54bfcf7acdbb5b705423658778b7054e2f1af Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Mon, 19 Dec 2016 14:15:25 +0100 Subject: [PATCH 4/8] fixes imports and abstract class --- src/personalisation/adapters.py | 41 +++++++---------------------- src/personalisation/app_settings.py | 5 ++-- src/personalisation/utils.py | 6 +++++ 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/personalisation/adapters.py b/src/personalisation/adapters.py index 9af4fbc..fa8259a 100644 --- a/src/personalisation/adapters.py +++ b/src/personalisation/adapters.py @@ -2,30 +2,24 @@ import time from personalisation.models import AbstractBaseRule, Segment + class BaseSegmentsAdapter(object): - """Base adapter with required functions predefined""" - def setup(self): - return + def _test_rules(self, rules, request): + if len(rules) > 0: + for rule in rules: + result = rule.test_user(request) - def get(self): - return + if result is False: + return False - def add(self): - return - - def refresh(self): - return - - def check_segment_exists(self): - return + return True + return False class Meta: abstract = True class SessionSegmentsAdapter(BaseSegmentsAdapter): - """Segments adapter that uses Django's SessionMiddleware to store segments""" - # Setup def setup(self, request): self.request = request @@ -33,11 +27,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): if 'segments' not in self.request.session: self.request.session['segments'] = [] - # Get segments def get(self): return self.request.session['segments'] - # Add segments def add(self, segment): def check_if_segmented(item): """Check if the user has been segmented""" @@ -73,7 +65,7 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): queried_rules = rule.objects.filter(segment=segment) for result in queried_rules: segment_rules.append(result) - result = _test_rules(segment_rules, self.request) + result = self._test_rules(segment_rules, self.request) if result: self.add(segment) @@ -84,21 +76,8 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): segment.visit_count = segment.visit_count + 1 segment.save() - # Quick checking logic to see if a segment exists def check_segment_exists(self, segment): segments = self.request.session['segments'] return any(item for item in self.request.session['segments'] if segment.pk == item.id) - -def _test_rules(rules, request): - """Test whether the user matches a segment's rules'""" - if len(rules) > 0: - for rule in rules: - result = rule.test_user(request) - - if result is False: - return False - - return True - return False diff --git a/src/personalisation/app_settings.py b/src/personalisation/app_settings.py index 340cb7d..c6fce6a 100644 --- a/src/personalisation/app_settings.py +++ b/src/personalisation/app_settings.py @@ -1,6 +1,7 @@ from django.conf import settings -from personalisation.adapters import SessionSegmentsAdapter +from personalisation.utils import import_class -segments_adapter = getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', SessionSegmentsAdapter) + +segments_adapter = import_class(getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', 'personalisation.adapters.SessionSegmentsAdapter')) diff --git a/src/personalisation/utils.py b/src/personalisation/utils.py index c58ef30..ab01ee2 100644 --- a/src/personalisation/utils.py +++ b/src/personalisation/utils.py @@ -1,5 +1,11 @@ +import importlib + def impersonate_other_page(page, other_page): page.path = other_page.path page.depth = other_page.depth page.url_path = other_page.url_path page.title = other_page.title + +def import_class(name): + module_name, class_name = name.rsplit('.', 1) + return getattr(importlib.import_module(module_name), class_name) From c058ab18d72d05a3a388cbb31df2760cd27bb078 Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Thu, 22 Dec 2016 10:10:41 +0100 Subject: [PATCH 5/8] fixes adapter instantiating so it can be used outside of the hooks --- src/personalisation/app_settings.py | 2 +- src/personalisation/wagtail_hooks.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/personalisation/app_settings.py b/src/personalisation/app_settings.py index c6fce6a..093b967 100644 --- a/src/personalisation/app_settings.py +++ b/src/personalisation/app_settings.py @@ -3,5 +3,5 @@ from django.conf import settings from personalisation.utils import import_class -segments_adapter = import_class(getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', 'personalisation.adapters.SessionSegmentsAdapter')) +segments_adapter = import_class(getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', 'personalisation.adapters.SessionSegmentsAdapter'))() diff --git a/src/personalisation/wagtail_hooks.py b/src/personalisation/wagtail_hooks.py index c81cc47..5f28a9f 100644 --- a/src/personalisation/wagtail_hooks.py +++ b/src/personalisation/wagtail_hooks.py @@ -19,7 +19,6 @@ from personalisation.models import (AbstractBaseRule, PersonalisablePage, from personalisation.utils import impersonate_other_page logger = logging.getLogger() -adapter = segments_adapter() @hooks.register('register_admin_urls') @@ -81,8 +80,8 @@ def set_visit_count(page, request, serve_args, serve_kwargs): @hooks.register('before_serve_page') def segment_user(page, request, serve_args, serve_kwargs): # Always run setup first on each segment, should have logic to not overwrite - adapter.setup(request) - adapter.refresh() + segments_adapter.setup(request) + segments_adapter.refresh() @hooks.register('before_serve_page') From 8d257867b818e88edcfb638a510c22669604e1b4 Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Thu, 22 Dec 2016 10:13:42 +0100 Subject: [PATCH 6/8] removes hardcoded sessions reference --- src/personalisation/wagtail_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/personalisation/wagtail_hooks.py b/src/personalisation/wagtail_hooks.py index 5f28a9f..36a1e24 100644 --- a/src/personalisation/wagtail_hooks.py +++ b/src/personalisation/wagtail_hooks.py @@ -88,7 +88,7 @@ def segment_user(page, request, serve_args, serve_kwargs): def serve_variation(page, request, serve_args, serve_kwargs): user_segments = [] - for segment in request.session['segments']: + for segment in segments_adapter.get(): try: user_segment = Segment.objects.get(pk=segment['id'], status='enabled') From 6b779f29b05e3461f6bef8208df7bb9a79138214 Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Thu, 22 Dec 2016 12:35:03 +0100 Subject: [PATCH 7/8] fixes a few issues --- src/personalisation/adapters.py | 29 ++++++++++++++-------------- src/personalisation/app_settings.py | 5 ++--- src/personalisation/utils.py | 6 ------ src/personalisation/wagtail_hooks.py | 2 +- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/personalisation/adapters.py b/src/personalisation/adapters.py index fa8259a..4979b5e 100644 --- a/src/personalisation/adapters.py +++ b/src/personalisation/adapters.py @@ -1,9 +1,14 @@ import time +from django.db.models import F + from personalisation.models import AbstractBaseRule, Segment class BaseSegmentsAdapter(object): + def setup(self): + return None + def _test_rules(self, rules, request): if len(rules) > 0: for rule in rules: @@ -23,20 +28,18 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): def setup(self, request): self.request = request - # Set up segments dictionary object in the session - if 'segments' not in self.request.session: - self.request.session['segments'] = [] + self.request.session.setdefault('segments', []) - def get(self): + def get_all_segments(self): return self.request.session['segments'] + def get_segment(self, segment_id): + return next(item for item in self.request.session['segments'] if item.id == segment_id) + def add(self, segment): def check_if_segmented(item): """Check if the user has been segmented""" - for seg in self.request.session['segments']: - if seg['encoded_name'] == item.encoded_name(): - return True - return False + return any(seg['encoded_name'] == item.encoded_name for seg in self.request.session['segments']) if not check_if_segmented(segment): segdict = { @@ -56,15 +59,13 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): self.request.session['segments'] = current_segments - segments = Segment.objects.all().filter(status='enabled') + segments = Segment.objects.filter(status='enabled').prefetch_related('rules') for segment in segments: rules = AbstractBaseRule.__subclasses__() segment_rules = [] for rule in rules: - queried_rules = rule.objects.filter(segment=segment) - for result in queried_rules: - segment_rules.append(result) + segment_rules += rule.objects.filter(segment=segment) result = self._test_rules(segment_rules, self.request) if result: @@ -73,11 +74,11 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): for seg in self.request.session['segments']: segment = Segment.objects.get(pk=seg['id']) - segment.visit_count = segment.visit_count + 1 + segment.visit_count = F('visit_count') + 1 segment.save() def check_segment_exists(self, segment): segments = self.request.session['segments'] - return any(item for item in self.request.session['segments'] if segment.pk == item.id) + return any(item for item in segments if segment.pk == item.id) diff --git a/src/personalisation/app_settings.py b/src/personalisation/app_settings.py index 093b967..a64ceec 100644 --- a/src/personalisation/app_settings.py +++ b/src/personalisation/app_settings.py @@ -1,7 +1,6 @@ from django.conf import settings - -from personalisation.utils import import_class +from django.utils.module_loading import import_string -segments_adapter = import_class(getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', 'personalisation.adapters.SessionSegmentsAdapter'))() +segments_adapter = import_string(getattr(settings, 'PERSONALISATION_SEGMENTS_ADAPTER', 'personalisation.adapters.SessionSegmentsAdapter'))() diff --git a/src/personalisation/utils.py b/src/personalisation/utils.py index ab01ee2..c58ef30 100644 --- a/src/personalisation/utils.py +++ b/src/personalisation/utils.py @@ -1,11 +1,5 @@ -import importlib - def impersonate_other_page(page, other_page): page.path = other_page.path page.depth = other_page.depth page.url_path = other_page.url_path page.title = other_page.title - -def import_class(name): - module_name, class_name = name.rsplit('.', 1) - return getattr(importlib.import_module(module_name), class_name) diff --git a/src/personalisation/wagtail_hooks.py b/src/personalisation/wagtail_hooks.py index 36a1e24..6428b60 100644 --- a/src/personalisation/wagtail_hooks.py +++ b/src/personalisation/wagtail_hooks.py @@ -88,7 +88,7 @@ def segment_user(page, request, serve_args, serve_kwargs): def serve_variation(page, request, serve_args, serve_kwargs): user_segments = [] - for segment in segments_adapter.get(): + for segment in segments_adapter.get_all_segments(): try: user_segment = Segment.objects.get(pk=segment['id'], status='enabled') From b86259a0dc089b5af7fd34173c34d3c3ade6a272 Mon Sep 17 00:00:00 2001 From: Boris Besemer Date: Thu, 22 Dec 2016 13:42:54 +0100 Subject: [PATCH 8/8] adds abstract functions --- src/personalisation/adapters.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/personalisation/adapters.py b/src/personalisation/adapters.py index 4979b5e..5891c5a 100644 --- a/src/personalisation/adapters.py +++ b/src/personalisation/adapters.py @@ -9,6 +9,21 @@ class BaseSegmentsAdapter(object): def setup(self): return None + def get_all_segments(self): + return None + + def get_segment(self): + return None + + def add(self): + return None + + def refresh(self): + return None + + def check_segment_exists(self): + return None + def _test_rules(self, rules, request): if len(rules) > 0: for rule in rules: @@ -65,7 +80,7 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter): rules = AbstractBaseRule.__subclasses__() segment_rules = [] for rule in rules: - segment_rules += rule.objects.filter(segment=segment) + segment_rules += segments.rules.filter(segment=segment, pk=rule.pk) result = self._test_rules(segment_rules, self.request) if result: