Skip to content

Commit 543fb8a

Browse files
committed
Add basic personalisation blocks
1 parent 94c947a commit 543fb8a

5 files changed

Lines changed: 225 additions & 12 deletions

File tree

src/wagtail_personalisation/blocks.py

Lines changed: 133 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,78 @@
11
from __future__ import absolute_import, unicode_literals
22

3+
from django.utils.encoding import force_text
34
from django.utils.translation import ugettext_lazy as _
5+
46
from wagtail.wagtailcore import blocks
7+
from wagtail.wagtailimages.blocks import ImageChooserBlock
58

69
from wagtail_personalisation.adapters import get_segment_adapter
710
from wagtail_personalisation.models import Segment
811

912

1013
def list_segment_choices():
14+
"""Get a list of segment choices visible in the admin site when editing
15+
BasePersonalisedStructBlock and its derived classes."""
16+
yield (-1, _('Visible to everyone'))
17+
1118
for pk, name in Segment.objects.values_list('pk', 'name'):
1219
yield pk, name
1320

1421

15-
class PersonalisedStructBlock(blocks.StructBlock):
16-
"""Struct block that allows personalisation per block."""
17-
22+
class BasePersonalisedStructBlock(blocks.StructBlock):
23+
"""Base class for personalised struct blocks."""
1824
segment = blocks.ChoiceBlock(
1925
choices=list_segment_choices,
2026
required=False, label=_("Personalisation segment"),
2127
help_text=_("Only show this content block for users in this segment"))
2228

29+
def __init__(self, *args, **kwargs):
30+
"""Instantiate personalised struct block.
31+
32+
The arguments are the same as for the blocks.StructBlock constructor and
33+
one addtional one.
34+
35+
Keyword Arguments:
36+
render_fields: List with field names to be rendered or None to use
37+
the default block rendering.
38+
"""
39+
render_fields = kwargs.pop('render_fields',
40+
self._meta_class.render_fields)
41+
super(BasePersonalisedStructBlock, self).__init__(*args, **kwargs)
42+
43+
if isinstance(render_fields, tuple):
44+
render_fields = list(render_fields)
45+
46+
if render_fields is not None \
47+
and not isinstance(render_fields, list):
48+
raise ValueError('"render_fields" has to be a list or None.')
49+
elif isinstance(render_fields, list) \
50+
and not set(render_fields).issubset(self.child_blocks):
51+
raise ValueError('"render_fields" has to contain name(s) of the '
52+
'specified blocks.')
53+
else:
54+
setattr(self._meta_class, 'render_fields', render_fields)
55+
56+
57+
def is_visible(self, value, request):
58+
"""Check whether user should see the block based on their segments.
59+
60+
:param value: The value from the block.
61+
:type value: dict
62+
:returns: True if user should see the block.
63+
:rtype: bool
64+
65+
"""
66+
if int(value['segment']) == -1:
67+
return True
68+
69+
if value['segment']:
70+
for segment in get_segment_adapter(request).get_segments():
71+
if segment.id == int(value['segment']):
72+
return True
73+
74+
return False
75+
2376
def render(self, value, context=None):
2477
"""Only render this content block for users in this segment.
2578
@@ -31,14 +84,82 @@ def render(self, value, context=None):
3184
:rtype: blocks.StructBlock or empty str
3285
3386
"""
34-
request = context['request']
35-
adapter = get_segment_adapter(request)
36-
user_segments = adapter.get_segments()
87+
if not self.is_visible(value, context['request']):
88+
return ""
3789

38-
if value['segment']:
39-
for segment in user_segments:
40-
if segment.id == int(value['segment']):
41-
return super(PersonalisedStructBlock, self).render(
42-
value, context)
90+
if self._meta_class.render_fields is None:
91+
return super(BasePersonalisedStructBlock, self).render(
92+
value, context)
93+
94+
if isinstance(self._meta_class.render_fields, list):
95+
render_value = ''
96+
for field_name in self._meta_class.render_fields:
97+
if hasattr(value.bound_blocks[field_name], 'render_as_block'):
98+
block_value = value.bound_blocks[field_name] \
99+
.render_as_block(context=context)
100+
else:
101+
block_value = force_text(value[field_name])
102+
103+
if block_value != 'None':
104+
render_value += block_value
105+
106+
return render_value
107+
108+
raise RuntimeError('"render_fields" is neither "None" or "list" '
109+
'during rendering.')
110+
111+
class Meta:
112+
"""
113+
Setting render field will define which field gets rendered.
114+
Please use a name of the field. If none, it will render the whole block.
115+
"""
116+
render_fields = None
117+
118+
119+
class PersonalisedStructBlock(BasePersonalisedStructBlock):
120+
"""Struct block that allows personalisation per block."""
121+
122+
class Meta:
123+
label = _('Personalised Block')
124+
render_fields = None
125+
126+
127+
class PersonalisedRichTextBlock(BasePersonalisedStructBlock):
128+
"""Rich text block that allows personalisation."""
129+
rich_text = blocks.RichTextBlock(label=_('Rich Text'))
130+
131+
class Meta:
132+
icon = blocks.RichTextBlock._meta_class.icon
133+
label = _('Personalised Rich Text')
134+
render_fields = ['rich_text']
135+
136+
137+
class PersonalisedTextBlock(BasePersonalisedStructBlock):
138+
"""Text block that allows personalisation."""
139+
text = blocks.TextBlock(label=_('Mutli-line Text'))
140+
141+
class Meta:
142+
icon = blocks.TextBlock._meta_class.icon
143+
label = _('Personalised Multi-line Text')
144+
render_fields = ['text']
145+
146+
147+
class PersonalisedCharBlock(BasePersonalisedStructBlock):
148+
"""Char block that allows personalisation."""
149+
char = blocks.CharBlock(label=_('Text'))
150+
151+
class Meta:
152+
icon = blocks.CharBlock._meta_class.icon
153+
label = _('Personalised Single-line Text')
154+
render_fields = ['char']
155+
156+
157+
class PersonalisedImageChooserBlock(BasePersonalisedStructBlock):
158+
"""Image chooser block that allows personalisation."""
159+
image = ImageChooserBlock(label=_('Image'))
160+
161+
class Meta:
162+
icon = ImageChooserBlock._meta_class.icon
163+
label = _('Personalised Image')
164+
render_fields = ['image']
43165

44-
return ""
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.1 on 2017-05-31 11:29
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
import wagtail.wagtailcore.blocks
8+
import wagtail.wagtailcore.fields
9+
import wagtail.wagtailimages.blocks
10+
import wagtail_personalisation.blocks
11+
12+
13+
class Migration(migrations.Migration):
14+
15+
dependencies = [
16+
('wagtailcore', '0033_remove_golive_expiry_help_text'),
17+
('pages', '0002_auto_20170531_0915'),
18+
]
19+
20+
operations = [
21+
migrations.CreateModel(
22+
name='PersonalisedFieldsPage',
23+
fields=[
24+
('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')),
25+
('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')))))))),
26+
],
27+
options={
28+
'abstract': False,
29+
},
30+
bases=('wagtailcore.page',),
31+
),
32+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% load wagtailcore_tags %}
2+
3+
<div class="personalisation-block-template">
4+
<p>This is a block with <strong>template</strong>.</p>
5+
<h2>Heading: {{ value.heading }}</h2>
6+
<div>
7+
{{ value.paragraph|richtext }}
8+
</div>
9+
</div>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{% extends "base.html" %}
2+
{% load wagtailcore_tags %}
3+
4+
{% block body_class %}template-homepage{% endblock %}
5+
6+
{% block content %}
7+
<h1>{{ page.title }}</h1>
8+
{% for block in page.body %}
9+
<section class="section-{{ block.block_type }}">
10+
<p><em>Section for {{ block.block_type }}.</em></p>
11+
{% include_block block %}
12+
</section>
13+
<hr>
14+
{% endfor %}
15+
{% endblock %}

tests/unit/test_blocks.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import absolute_import, unicode_literals
2+
3+
import pytest
4+
5+
from tests.factories.segment import SegmentFactory
6+
from test.factories.pages import PersonalisedFieldsPageFactory
7+
from tests.utils import render_template
8+
9+
@pytest.mark.django_db
10+
def test_render_block(rf):
11+
SegmentFactory(name='test', persistent=True)
12+
13+
request = rf.get('/')
14+
15+
request.session['segments'] = [{
16+
"encoded_name": 'test',
17+
"id": 1,
18+
"timestamp": int(time.time()),
19+
"persistent": True
20+
}]
21+
22+
PersonalisedFieldsPageFactory(body=)
23+
24+
content = render_template("""
25+
{% load wagtail_personalisation_tags %}
26+
{% segment name='test' %}Test{% endsegment %}
27+
""", request=request).strip()
28+
29+
assert content == "Test"
30+
31+
content = render_template("""
32+
{% load wagtail_personalisation_tags %}
33+
{% segment name='test2' %}Test{% endsegment %}
34+
""", request=request).strip()
35+
36+
assert content == ""

0 commit comments

Comments
 (0)