From 1c201b7735ac94a501a55e70f7342286d942e939 Mon Sep 17 00:00:00 2001 From: Tomasz Knapik Date: Wed, 31 May 2017 12:46:51 +0100 Subject: [PATCH] Add basic personalisation blocks --- src/wagtail_personalisation/blocks.py | 146 ++++++++++++++++-- .../migrations/0003_personalisedfieldspage.py | 32 ++++ .../blocks/personalised_block_template.html | 9 ++ .../page_with_personalisable_fields.html | 15 ++ tests/unit/test_blocks.py | 36 +++++ 5 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 tests/sandbox/pages/migrations/0003_personalisedfieldspage.py create mode 100644 tests/sandbox/templates/blocks/personalised_block_template.html create mode 100644 tests/sandbox/templates/pages/page_with_personalisable_fields.html create mode 100644 tests/unit/test_blocks.py diff --git a/src/wagtail_personalisation/blocks.py b/src/wagtail_personalisation/blocks.py index 6aaa1fe2..6136962b 100644 --- a/src/wagtail_personalisation/blocks.py +++ b/src/wagtail_personalisation/blocks.py @@ -1,25 +1,78 @@ from __future__ import absolute_import, unicode_literals +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ + from wagtail.wagtailcore import blocks +from wagtail.wagtailimages.blocks import ImageChooserBlock from wagtail_personalisation.adapters import get_segment_adapter from wagtail_personalisation.models import Segment def list_segment_choices(): + """Get a list of segment choices visible in the admin site when editing + BasePersonalisedStructBlock and its derived classes.""" + yield (-1, _('Visible to everyone')) + for pk, name in Segment.objects.values_list('pk', 'name'): yield pk, name -class PersonalisedStructBlock(blocks.StructBlock): - """Struct block that allows personalisation per block.""" - +class BasePersonalisedStructBlock(blocks.StructBlock): + """Base class for personalised struct blocks.""" segment = blocks.ChoiceBlock( choices=list_segment_choices, required=False, label=_("Personalisation segment"), help_text=_("Only show this content block for users in this segment")) + def __init__(self, *args, **kwargs): + """Instantiate personalised struct block. + + The arguments are the same as for the blocks.StructBlock constructor and + one addtional one. + + Keyword Arguments: + render_fields: List with field names to be rendered or None to use + the default block rendering. + """ + render_fields = kwargs.pop('render_fields', + self._meta_class.render_fields) + super(BasePersonalisedStructBlock, self).__init__(*args, **kwargs) + + if isinstance(render_fields, tuple): + render_fields = list(render_fields) + + if render_fields is not None \ + and not isinstance(render_fields, list): + raise ValueError('"render_fields" has to be a list or None.') + elif isinstance(render_fields, list) \ + and not set(render_fields).issubset(self.child_blocks): + raise ValueError('"render_fields" has to contain name(s) of the ' + 'specified blocks.') + else: + setattr(self._meta_class, 'render_fields', render_fields) + + + def is_visible(self, value, request): + """Check whether user should see the block based on their segments. + + :param value: The value from the block. + :type value: dict + :returns: True if user should see the block. + :rtype: bool + + """ + if int(value['segment']) == -1: + return True + + if value['segment']: + for segment in get_segment_adapter(request).get_segments(): + if int(segment['id']) == int(value['segment']): + return True + + return False + def render(self, value, context=None): """Only render this content block for users in this segment. @@ -31,14 +84,83 @@ def render(self, value, context=None): :rtype: blocks.StructBlock or empty str """ - request = context['request'] - adapter = get_segment_adapter(request) - user_segments = adapter.get_segments() + if not self.is_visible(value, context['request']): + return "" + + if self._meta_class.render_fields is None: + return super(BasePersonalisedStructBlock, self).render( + value, context) + + if isinstance(self._meta_class.render_fields, list): + render_value = '' + for field_name in self._meta_class.render_fields: + if hasattr(value.bound_blocks[field_name], 'render_as_block'): + block_value = value.bound_blocks[field_name] \ + .render_as_block(context=context) + else: + block_value = force_text(value[field_name]) + + if block_value != 'None': + render_value += block_value + + return render_value + + raise RuntimeError('"render_fields" is neither "None" or "list" ' + 'during rendering.') + + class Meta: + """ + Setting render field will define which field gets rendered. + Please use a name of the field. If none, it will render the whole block. + """ + render_fields = None + + +class PersonalisedStructBlock(BasePersonalisedStructBlock): + """Struct block that allows personalisation per block.""" + + class Meta: + label = _('Personalised Block') + render_fields = None + + +class PersonalisedRichTextBlock(BasePersonalisedStructBlock): + """Rich text block that allows personalisation.""" + rich_text = blocks.RichTextBlock(label=_('Rich Text')) + + class Meta: + icon = blocks.RichTextBlock._meta_class.icon + label = _('Personalised Rich Text') + render_fields = ['rich_text'] + + +class PersonalisedTextBlock(BasePersonalisedStructBlock): + """Text block that allows personalisation.""" + text = blocks.TextBlock(label=_('Mutli-line Text')) + + class Meta: + icon = blocks.TextBlock._meta_class.icon + label = _('Personalised Multi-line Text') + render_fields = ['text'] + + +class PersonalisedCharBlock(BasePersonalisedStructBlock): + """Char block that allows personalisation.""" + char = blocks.CharBlock(label=_('Text')) + + class Meta: + icon = blocks.CharBlock._meta_class.icon + label = _('Personalised Single-line Text') + render_fields = ['char'] + + +class PersonalisedImageChooserBlock(BasePersonalisedStructBlock): + """Image chooser block that allows personalisation.""" + image = ImageChooserBlock(label=_('Image')) + + class Meta: + icon = ImageChooserBlock._meta_class.icon + label = _('Personalised Image') + render_fields = ['image'] - if value['segment']: - for segment in user_segments: - if segment['id'] == int(value['segment']): - return super(PersonalisedStructBlock, self).render( - value, context) - return "" diff --git a/tests/sandbox/pages/migrations/0003_personalisedfieldspage.py b/tests/sandbox/pages/migrations/0003_personalisedfieldspage.py new file mode 100644 index 00000000..ef3d4995 --- /dev/null +++ b/tests/sandbox/pages/migrations/0003_personalisedfieldspage.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.1 on 2017-05-31 11:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import wagtail.wagtailcore.blocks +import wagtail.wagtailcore.fields +import wagtail.wagtailimages.blocks +import wagtail_personalisation.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0033_remove_golive_expiry_help_text'), + ('pages', '0002_auto_20170531_0915'), + ] + + operations = [ + migrations.CreateModel( + name='PersonalisedFieldsPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('body', wagtail.wagtailcore.fields.StreamField((('personalised_block', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('heading', wagtail.wagtailcore.blocks.CharBlock()), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())))), ('personalised_block_template', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('heading', wagtail.wagtailcore.blocks.CharBlock()), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())), label='Block with template', template='blocks/personalised_block_template.html')), ('personalised_rich_text_block', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('rich_text', wagtail.wagtailcore.blocks.RichTextBlock(label='Rich Text'))))), ('personalised_image', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock(label='Image'))))), ('personalised_char', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('char', wagtail.wagtailcore.blocks.CharBlock(label='Text'))))), ('personalised_text', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('text', wagtail.wagtailcore.blocks.TextBlock(label='Mutli-line Text')))))))), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/tests/sandbox/templates/blocks/personalised_block_template.html b/tests/sandbox/templates/blocks/personalised_block_template.html new file mode 100644 index 00000000..dde8b256 --- /dev/null +++ b/tests/sandbox/templates/blocks/personalised_block_template.html @@ -0,0 +1,9 @@ +{% load wagtailcore_tags %} + +
+

This is a block with template.

+

Heading: {{ value.heading }}

+
+ {{ value.paragraph|richtext }} +
+
diff --git a/tests/sandbox/templates/pages/page_with_personalisable_fields.html b/tests/sandbox/templates/pages/page_with_personalisable_fields.html new file mode 100644 index 00000000..af3338e3 --- /dev/null +++ b/tests/sandbox/templates/pages/page_with_personalisable_fields.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load wagtailcore_tags %} + +{% block body_class %}template-homepage{% endblock %} + +{% block content %} +

{{ page.title }}

+ {% for block in page.body %} +
+

Section for {{ block.block_type }}.

+ {% include_block block %} +
+
+ {% endfor %} +{% endblock %} diff --git a/tests/unit/test_blocks.py b/tests/unit/test_blocks.py new file mode 100644 index 00000000..040f3489 --- /dev/null +++ b/tests/unit/test_blocks.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import, unicode_literals + +import pytest + +from tests.factories.segment import SegmentFactory +from test.factories.pages import PersonalisedFieldsPageFactory +from tests.utils import render_template + +@pytest.mark.django_db +def test_render_block(rf): + SegmentFactory(name='test', persistent=True) + + request = rf.get('/') + + request.session['segments'] = [{ + "encoded_name": 'test', + "id": 1, + "timestamp": int(time.time()), + "persistent": True + }] + + PersonalisedFieldsPageFactory(body=) + + content = render_template(""" + {% load wagtail_personalisation_tags %} + {% segment name='test' %}Test{% endsegment %} + """, request=request).strip() + + assert content == "Test" + + content = render_template(""" + {% load wagtail_personalisation_tags %} + {% segment name='test2' %}Test{% endsegment %} + """, request=request).strip() + + assert content == ""