8

Compare commits

..

6 Commits

Author SHA1 Message Date
Jasper Berghoef
985c9cbb95 Fixes color names 2018-05-26 15:56:27 +02:00
Jasper Berghoef
ad79fa3b2f Adds match icons 2018-05-26 15:56:27 +02:00
Jasper Berghoef
389dc243e1 More styling improvements 2018-05-26 15:56:27 +02:00
Jasper Berghoef
47bfa384f3 New dashboard markup and styling
Work in progress
2018-05-26 15:56:27 +02:00
Jasper Berghoef
83c2a4289e Adjust README for ordering 2018-05-26 12:56:18 +02:00
Mike Dingjan
9b25cd2a94 Add missing dependency `pytest-pythonpath` 2018-03-16 11:10:45 +01:00
23 changed files with 337 additions and 815 deletions

View File

@@ -7,6 +7,30 @@ matrix:
# Django 1.9, Wagtail 1.9
- python: 2.7
env: TOXENV=py27-django19-wagtail19
- python: 3.5
env: TOXENV=py35-django19-wagtail19
- python: 3.6
env: TOXENV=py36-django19-wagtail19
# Django 1.10, Wagtail 1.10
- python: 2.7
env: TOXENV=py27-django110-wagtail110
- python: 3.5
env: TOXENV=py35-django110-wagtail110
- python: 3.6
env: TOXENV=py36-django110-wagtail110
# Django 1.11, Wagtail 1.10
- python: 2.7
env: TOXENV=py27-django111-wagtail110
- python: 3.5
env: TOXENV=py35-django111-wagtail110
- python: 3.6
env: TOXENV=py36-django111-wagtail110
allow_failures:
- python: 3.5
env: TOXENV=lint
install:
- pip install tox codecov
@@ -17,13 +41,3 @@ script:
after_success:
- tox -e coverage-report
- codecov
deploy:
provider: pypi
distributions: sdist bdist_wheel
user: praekelt.org
password:
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
on:
tags: true
all_branches: true

View File

@@ -1,7 +1,3 @@
0.10.0
==================
- Adds static and dynamic segments
0.9.1 (tbd)
==================

View File

@@ -1,6 +1,3 @@
include README.rst
recursive-include src *
recursive-exclude src __pycache__
recursive-exclude src *.py[co]
recursive-include src

View File

@@ -14,6 +14,11 @@
.. end-no-pypi
.. image:: logo.png
:scale: 50 %
:alt: Wagxperience
:align: center
Wagtail Personalisation
=======================
@@ -24,11 +29,6 @@ in the admin interface.
.. _Wagtail CMS: http://wagtail.io/
.. image:: logo.png
:scale: 50 %
:alt: Wagxperience
:align: center
.. image:: screenshot.png

View File

@@ -55,10 +55,10 @@ author = 'Lab Digital BV'
# built documents.
#
# The short X.Y version.
version = '0.10.1'
version = '0.9.1'
# The full version, including alpha/beta/rc tags.
release = '0.10.1'
release = '0.9.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@@ -1,132 +1,199 @@
.nice-padding {
padding-left: 50px;
padding-right: 50px;
}
$color-gray: #D0D2D3;
$color-light-gray: #EBEBEB;
$color-lightest-gray: #F6F6F6;
$color-dark-gray: #727272;
$block-spacing: 20px;
.block_container {
display: block;
margin-top: 30px;
}
.block_container .block {
display: block;
float: left;
box-sizing: border-box;
position: relative;
width: calc(50% - 10px);
min-height: 216px;
padding: 10px 20px;
margin-bottom: 20px;
border: 1px solid #d9d9d9;
border-radius: 3px;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);
transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);
cursor: pointer;
}
.block_container .block--disabled h2,
.block_container .block--disabled .inspect_container {
opacity: 0.5;
}
.block_container .block h2 {
display: inline-block;
width: auto;
}
.block_container .block:nth-child(odd) {
margin-right: 20px;
}
.block_container .block .block_actions {
list-style: none;
margin: 0;
padding: 0;
}
.block_container .block .block_actions li {
float: left;
margin-right: 10px;
}
.block_container .block .block_actions li:last-child {
margin-right: 0;
}
.block_container .block.suggestion {
border: 1px dashed #d9d9d9;
}
.block_container .block:hover {
border: 1px solid #fff;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
@media (max-width: 699px) {
.block_container .block {
width: 100%;
margin-right: 0;
}
}
.block_container .block .inspect_container {
.block-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: stretch;
margin-bottom: 10px;
}
flex-wrap: wrap;
justify-content: flex-start;
.block_container .block .inspect_container .inspect {
display: block;
float: left;
width: calc(50% - 10px);
padding: 0;
margin: 0;
list-style: none;
.stat_card {
display: inline-block;
margin-bottom: 5px;
margin-right: 10px;
}
}
.block_container .block span.icon::before {
margin-right: 0.3em;
vertical-align: bottom;
}
.block_container .block .inspect_container .inspect li span {
.block {
display: block;
font-size: 20px;
font-weight: bold;
margin: 5px 0;
overflow-wrap: break-word;
}
.block_container .block .inspect_container .inspect li pre {
position: relative;
box-sizing: border-box;
width: auto;
background-color: #eee;
border: 1px solid #ccc;
margin: 5px 0 5px 21px;
padding: 2px 5px;
word-wrap: break-word;
word-break: break-all;
overflow-x: hidden;
width: 22%;
max-width: 320px;
min-width: 280px;
min-height: 260px;
margin: 1.5%;
border: 1px solid $color-gray;
border-radius: 3px;
padding-bottom: 62px;
&__segment {
position: relative;
.block-header,
.block-rules,
.block-footer {
display: block;
box-sizing: border-box;
}
.block-header {
padding: $block-spacing;
border-bottom: 1px solid $color-gray;
h2 {
margin-top: 0;
}
}
.block-rules {
padding: $block-spacing 0;
}
.block-footer {
position: absolute;
width: 100%;
bottom: 0;
padding: $block-spacing / 2 $block-spacing;
border-top: 1px solid $color-gray;
background-color: $color-lightest-gray;
}
.block-data {
margin-bottom: $block-spacing;
margin-right: $block-spacing / 2;
&__description {
margin-bottom: 3px;
font-size: 14px;
color: $color-dark-gray;
}
&__content {
margin-bottom: 0;
font-size: 18px;
&--code {
display: inline-block;
width: auto;
padding-left: 3px !important;
padding-right: 3px;
padding-top: 1px;
padding-bottom: 1px;
margin-left: $block-spacing + 10px;
margin-top: 0;
border: 1px solid $color-gray;
border-radius: 3px;
font-size: 14px;
background-color: $color-lightest-gray;
}
}
&--icon {
position: relative;
&:before {
display: block;
position: absolute;
top: 50%;
left: 0;
transform: translate(-20%, -50%);
margin-right: 0;
font-size: 32px;
color: $color-light-gray;
}
.block-data__description,
.block-data__content {
padding-left: 30px;
}
}
&:last-child {
margin-bottom: 0;
}
}
.block-stats {
&__content {
margin-bottom: 3px;
}
}
.block-actions {
margin: 0;
padding: 0;
list-style: none;
&__action {
display: inline-block;
margin-right: 5px;
&:last-child {
margin-right: 0;
}
}
}
}
&-suggestion {
position: relative;
border: 1px dashed $color-gray;
&__text {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
text-align: center;
font-size: 16px;
color: $color-gray;
}
&:hover {
.block-suggestion {
&__text {
color: $color-dark-gray;
}
}
}
}
}
.block_container .block.suggestion .suggestive_text {
display: block;
position: absolute;
width: calc(100% - 40px);
text-align: center;
top: 50%;
transform: translateY(-50%);
color: #d9d9d9;
font-size: 20px;
font-weight: 100;
.segment {
&--match-all,
&--match-any {
.block-data--icon {
position: relative;
&:after {
position: absolute;
left: 5px;
padding-left: 5px;
border-left: 1px solid $color-light-gray;
color: $color-light-gray;
text-align: left;
font-family: "FontAwesome";
font-size: 16px;
font-weight: 500;
}
&:last-child:after {
display: none;
}
}
}
&--match-all {
.block-data--icon {
&:after {
content: "\f0c1";
}
}
}
&--match-any {
.block-data--icon {
&:after {
content: "\f127";
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.10.1
current_version = 0.9.1
commit = true
tag = true
tag_name = {new_version}
@@ -15,14 +15,14 @@ python_paths = .
[flake8]
ignore = E731
max-line-length = 120
exclude =
exclude =
src/**/migrations/*.py
[wheel]
universal = 1
[coverage:run]
omit =
omit =
src/**/migrations/*.py
[bumpversion:file:setup.py]

View File

@@ -1,6 +1,7 @@
import re
from setuptools import find_packages, setup
install_requires = [
'wagtail>=1.9,<1.11',
'user-agents>=1.0.1',
@@ -9,15 +10,15 @@ install_requires = [
tests_require = [
'factory_boy==2.8.1',
'flake8',
'flake8-blind-except',
'flake8-debugger',
'flake8-imports',
'flake8',
'freezegun==0.3.8',
'pytest-cov==2.4.0',
'pytest-django==3.1.2',
'pytest-pythonpath==0.7.2',
'pytest-sugar==0.7.1',
'pytest-mock==1.6.3',
'pytest==3.1.0',
'wagtail_factories==0.3.0',
]
@@ -31,12 +32,12 @@ with open('README.rst') as fh:
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
setup(
name='wagtail-personalisation-molo',
version='0.10.1',
description='A forked version of Wagtail add-on for showing personalized content',
author='Praekelt.org',
author_email='dev@praekeltfoundation.org',
url='https://github.com/praekeltfoundation/wagtail-personalisation/',
name='wagtail-personalisation',
version='0.9.1',
description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV',
author_email='opensource@labdigital.nl',
url='http://labdigital.nl',
install_requires=install_requires,
tests_require=tests_require,
extras_require={

View File

@@ -3,7 +3,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 django.utils import timezone
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import AbstractBaseRule
@@ -144,7 +143,7 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
def get_visit_count(self, page=None):
"""Return the number of visits on the current request or given page"""
path = page.url_path if page else self.request.path
path = page.path if page else self.request.path
visit_count = self.request.session.setdefault('visit_count', [])
for visit in visit_count:
if visit['path'] == path:
@@ -175,22 +174,15 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
# Run tests on all remaining enabled segments to verify applicability.
additional_segments = []
for segment in enabled_segments:
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
segment_rules = []
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)
if result:
additional_segments.append(segment)
elif not segment.is_static or not segment.is_full:
segment_rules = []
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)
if result and segment.is_static and not segment.is_full:
if self.request.user.is_authenticated():
segment.static_users.add(self.request.user)
if result:
additional_segments.append(segment)
self.set_segments(current_segments + additional_segments)
self.update_visit_count()

View File

@@ -1,107 +0,0 @@
from __future__ import absolute_import, unicode_literals
from importlib import import_module
from itertools import takewhile
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.models import Session
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.core.exceptions import ValidationError
from django.test.client import RequestFactory
from django.utils import timezone
from django.utils.lru_cache import lru_cache
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.forms import WagtailAdminModelForm
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@lru_cache(maxsize=1000)
def user_from_data(user_id):
User = get_user_model()
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
class SegmentAdminForm(WagtailAdminModelForm):
def clean(self):
cleaned_data = super(SegmentAdminForm, self).clean()
Segment = self._meta.model
rules = [
form.instance for formset in self.formsets.values()
for form in formset
if form not in formset.deleted_forms
]
consistent = rules and Segment.all_static(rules)
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
if self.instance.id and self.instance.is_static:
if self.has_changed():
self.add_error_to_fields(self, excluded=['name', 'enabled'])
for formset in self.formsets.values():
if formset.has_changed():
for form in formset:
if form not in formset.deleted_forms:
self.add_error_to_fields(form)
return cleaned_data
def add_error_to_fields(self, form, excluded=list()):
for field in form.changed_data:
if field not in excluded:
form.add_error(field, _('Cannot update a static segment'))
def save(self, *args, **kwargs):
is_new = not self.instance.id
if not self.instance.is_static:
self.instance.count = 0
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
if is_new and instance.is_static and instance.all_rules_static:
from .adapters import get_segment_adapter
request = RequestFactory().get('/')
request.session = SessionStore()
adapter = get_segment_adapter(request)
users_to_add = []
sessions = Session.objects.iterator()
take_session = takewhile(
lambda x: instance.count == 0 or len(users_to_add) <= instance.count,
sessions
)
for session in take_session:
session_data = session.get_decoded()
user = user_from_data(session_data.get('_auth_user_id'))
if user.is_authenticated():
request.user = user
request.session = SessionStore(session_key=session.session_key)
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
if passes:
users_to_add.append(user)
instance.static_users.add(*users_to_add)
return instance
@property
def media(self):
media = super(SegmentAdminForm, self).media
media.add_js(
[static('js/segment_form_control.js')]
)
return media

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-17 11:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sessions', '0001_initial'),
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
]
operations = [
migrations.AddField(
model_name='segment',
name='count',
field=models.PositiveSmallIntegerField(default=0, help_text='If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.'),
),
migrations.AddField(
model_name='segment',
name='sessions',
field=models.ManyToManyField(to='sessions.Session'),
),
migrations.AddField(
model_name='segment',
name='type',
field=models.CharField(choices=[('dynamic', 'Dynamic'), ('static', 'Static')], default='dynamic', help_text='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
),
]

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-01 15:58
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtail_personalisation', '0013_add_dynamic_static_to_segment'),
]
operations = [
migrations.RemoveField(
model_name='segment',
name='sessions',
),
migrations.AddField(
model_name='segment',
name='static_users',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,28 +1,18 @@
from __future__ import absolute_import, unicode_literals
from django import forms
from django.conf import settings
from django.contrib.sessions.models import Session
from django.db import models, transaction
from django.template.defaultfilters import slugify
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
)
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
from wagtail.wagtailcore.models import Page
from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import count_active_days
from .forms import SegmentAdminForm
class SegmentQuerySet(models.QuerySet):
def enabled(self):
@@ -40,14 +30,6 @@ class Segment(ClusterableModel):
(STATUS_DISABLED, _('Disabled')),
)
TYPE_DYNAMIC = 'dynamic'
TYPE_STATIC = 'static'
TYPE_CHOICES = (
(TYPE_DYNAMIC, _('Dynamic')),
(TYPE_STATIC, _('Static')),
)
name = models.CharField(max_length=255)
create_date = models.DateTimeField(auto_now_add=True)
edit_date = models.DateTimeField(auto_now=True)
@@ -62,35 +44,9 @@ class Segment(ClusterableModel):
default=False,
help_text=_("Should the segment match all the rules or just one of them?")
)
type = models.CharField(
max_length=20,
choices=TYPE_CHOICES,
default=TYPE_DYNAMIC,
help_text=mark_safe(_("""
</br></br><strong>Dynamic:</strong> Users in this segment will change
as more or less meet the rules specified in the segment.
</br><strong>Static:</strong> If the segment contains only static
compatible rules the segment will contain the members that pass
those rules when the segment is created. Mixed static segments or
those containing entirely non static compatible rules will be
populated using the count variable.
"""))
)
count = models.PositiveSmallIntegerField(
default=0,
help_text=_(
"If this number is set for a static segment users will be added to the "
"set until the number is reached. After this no more users will be added."
)
)
static_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
)
objects = SegmentQuerySet.as_manager()
base_form_class = SegmentAdminForm
def __init__(self, *args, **kwargs):
Segment.panels = [
MultiFieldPanel([
@@ -100,16 +56,11 @@ class Segment(ClusterableModel):
FieldPanel('persistent'),
]),
FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count', classname='count_field'),
], heading="Segment"),
MultiFieldPanel([
InlinePanel(
"{}_related".format(rule_model._meta.db_table),
label='{}{}'.format(
rule_model._meta.verbose_name,
' ({})'.format(_('Static compatible')) if rule_model.static else ''
),
label=rule_model._meta.verbose_name,
) for rule_model in AbstractBaseRule.__subclasses__()
], heading=_("Rules")),
]
@@ -119,23 +70,6 @@ class Segment(ClusterableModel):
def __str__(self):
return self.name
@property
def is_static(self):
return self.type == self.TYPE_STATIC
@classmethod
def all_static(cls, rules):
return all(rule.static for rule in rules)
@property
def all_rules_static(self):
rules = self.get_rules()
return rules and self.all_static(rules)
@property
def is_full(self):
return self.static_users.count() >= self.count
def encoded_name(self):
"""Return a string with a slug for the segment."""
return slugify(self.name.lower())

View File

@@ -18,7 +18,6 @@ from wagtail.wagtailadmin.edit_handlers import (
class AbstractBaseRule(models.Model):
"""Base for creating rules to segment users with."""
icon = 'fa-circle-o'
static = False
segment = ParentalKey(
'wagtail_personalisation.Segment',
@@ -191,7 +190,6 @@ class VisitCountRule(AbstractBaseRule):
"""
icon = 'fa-calculator'
static = True
OPERATOR_CHOICES = (
('more_than', _("More than")),
@@ -229,7 +227,7 @@ class VisitCountRule(AbstractBaseRule):
adapter = get_segment_adapter(request)
visit_count = adapter.get_visit_count(self.counted_page)
visit_count = adapter.get_visit_count()
if visit_count and operator == "more_than":
if visit_count > segment_count:
return True

View File

@@ -1,2 +1,2 @@
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block .inspect_container .inspect .stat_card{display:inline-block;margin-bottom:5px;margin-right:10px}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
.block-container{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.block-container .block{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;overflow-x:hidden;width:22%;max-width:320px;min-width:280px;min-height:260px;margin:1.5%;border:1px solid #d0d2d3;border-radius:3px;padding-bottom:62px}.block-container .block__segment{position:relative}.block-container .block__segment .block-footer,.block-container .block__segment .block-header,.block-container .block__segment .block-rules{display:block;-webkit-box-sizing:border-box;box-sizing:border-box}.block-container .block__segment .block-header{padding:20px;border-bottom:1px solid #d0d2d3}.block-container .block__segment .block-header h2{margin-top:0}.block-container .block__segment .block-rules{padding:20px 0}.block-container .block__segment .block-footer{position:absolute;width:100%;bottom:0;padding:10px 20px;border-top:1px solid #d0d2d3;background-color:#f6f6f6}.block-container .block__segment .block-data{margin-bottom:20px;margin-right:10px}.block-container .block__segment .block-data__description{margin-bottom:3px;font-size:14px;color:#727272}.block-container .block__segment .block-data__content{margin-bottom:0;font-size:18px}.block-container .block__segment .block-data__content--code{display:inline-block;width:auto;padding-left:3px!important;padding-right:3px;padding-top:1px;padding-bottom:1px;margin-left:30px;margin-top:0;border:1px solid #d0d2d3;border-radius:3px;font-size:14px;background-color:#f6f6f6}.block-container .block__segment .block-data--icon{position:relative}.block-container .block__segment .block-data--icon:before{display:block;position:absolute;top:50%;left:0;-webkit-transform:translate(-20%,-50%);transform:translate(-20%,-50%);margin-right:0;font-size:32px;color:#ebebeb}.block-container .block__segment .block-data--icon .block-data__content,.block-container .block__segment .block-data--icon .block-data__description{padding-left:30px}.block-container .block__segment .block-data:last-child{margin-bottom:0}.block-container .block__segment .block-stats__content{margin-bottom:3px}.block-container .block__segment .block-actions{margin:0;padding:0;list-style:none}.block-container .block__segment .block-actions__action{display:inline-block;margin-right:5px}.block-container .block__segment .block-actions__action:last-child{margin-right:0}.block-container .block-suggestion{position:relative;border:1px dashed #d0d2d3}.block-container .block-suggestion__text{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);width:100%;text-align:center;font-size:16px;color:#d0d2d3}.block-container .block-suggestion:hover .block-suggestion__text{color:#727272}.block-container .segment--match-all .block-data--icon,.block-container .segment--match-any .block-data--icon{position:relative}.block-container .segment--match-all .block-data--icon:after,.block-container .segment--match-any .block-data--icon:after{position:absolute;left:5px;padding-left:5px;border-left:1px solid #ebebeb;color:#ebebeb;text-align:left;font-family:FontAwesome;font-size:16px;font-weight:500}.block-container .segment--match-all .block-data--icon:last-child:after,.block-container .segment--match-any .block-data--icon:last-child:after{display:none}.block-container .segment--match-all .block-data--icon:after{content:"\F0C1"}.block-container .segment--match-any .block-data--icon:after{content:"\F127"}
/*# sourceMappingURL=dashboard.css.map*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +0,0 @@
(function($) {
$(document).ready( () => {
var count = $('.count_field');
var typeRadio = $('input:radio[name="type"]');
var updateCountDispay = function(value) {
if (value == 'dynamic') {
count.slideUp(250);
} else {
count.slideDown(250);
}
};
updateCountDispay(typeRadio.filter(':checked').val());
typeRadio.change( event => {
updateCountDispay(event.target.value);
});
});
})(jQuery);

View File

@@ -3,124 +3,93 @@
{% block toggle_view %}to List {% endblock%}
{% block content_main %}
<div>
<div class="row">
{% block content_cols %}
{% block result_list %}
<div class="block-container">
{% if all_count %}
{% for segment in object_list %}
<div class="block block__segment block__segment--{{ segment.status }} segment--match-{{ segment.match_any|yesno:"any,all" }} segment--{{ segment.persistent|yesno:"persistent,fleeting" }}">
{% block filters %}
{% if view.has_filters and all_count %}
<div class="changelist-filter col3">
<h2>{% trans 'Filter' %}</h2>
{% for spec in view.filter_specs %}{% admin_list_filter view spec %}{% endfor %}
</div>
{% endif %}
{% endblock %}
<div>
{% block result_list %}
<div class="nice-padding block_container">
{% if all_count %}
{% for segment in object_list %}
<div class="block block--{{ segment.status }}" onclick="location.href = 'edit/{{ segment.pk }}'">
<h2>{{ segment }}</h2>
<div class="inspect_container">
<ul class="inspect segment_stats">
<li class="stat_card">
{% trans "This segment has been visited" %}
<span class="icon icon-fa-rocket">{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
</li>
<li class="stat_card">
{% trans "This segment has been active for" %}
<span class="icon icon-fa-calendar">{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
</li>
{% if segment.is_static %}
<li class="stat_card">
{% trans "This segment is Static" %}
<span class="icon icon-fa-user">
{{ segment.sessions.count|localize }}
{% if segment.sessions.count < segment.count %}
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
{% else %}
{% trans "member" %}{{ segment.sessions.count|pluralize }}
{% endif %}
</span>
</li>
{% endif %}
</ul>
<hr />
<ul class="inspect segment_rules">
<li class="stat_card {{ segment.match_any|yesno:"any,all" }}">
{% trans "The visitor must match" %}
{% if segment.match_any %}
<span class="icon icon-fa-cube">{% trans "Any rule" %}</span>
{% else %}
<span class="icon icon-fa-cubes">{% trans "All rules" %}</span>
{% endif %}
</li>
<li class="stat_card {{ segment.persistent|yesno:"persistent,fleeting" }}">
{% trans "The persistence of this segment is" %}
{% if segment.persistent %}
<span class="icon icon-fa-bookmark" title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
{% else %}
<span class="icon icon-fa-bookmark-o" title="{% trans "This segment is reevaluated on every visit" %}">{% trans "Fleeting" %}</span>
{% endif %}
</li>
{% for rule in segment.get_rules %}
<li class="stat_card {{ rule.encoded_name }}">
{{ rule.description.title }}
{% if rule.description.code %}
<pre>{{ rule.description.value }}</pre>
{% else %}
<span class="icon icon-{{ rule.icon }}">{{ rule.description.value }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% if user_can_create %}
<ul class="block_actions">
{% if segment.status == segment.STATUS_DISABLED %}
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
{% elif segment.status == segment.STATUS_ENABLED %}
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
{% endif %}
<li><a href="edit/{{ segment.pk }}" title="{% trans "Configure this segment" %}">configure this</a></li>
</ul>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% if user_can_create %}
{% blocktrans with url=view.create_url name=view.verbose_name %}
<a class="block suggestion" href="{{ url }}">
<span class="suggestive_text">Add a new {{name}}</span>
</a>
{% endblocktrans %}
{% endif %}
<div class="block-header">
<h2>{{ segment.name }}</h2>
<div class="block-data">
<p class="block-data__description">{% trans "This segment has been visited" %}</p>
<p class="block-data__content">
<strong>{{ segment.visit_count|localize }}</strong>
{% trans "time" %}{{ segment.visit_count|pluralize }} {% trans "in" %}
<strong>{{ segment.enable_date|days_since:segment.disable_date }}</strong>
{% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}
</p>
</div>
{% endblock %}
</div>
</div>
{% block pagination %}
{% if paginator.num_pages > 1 %}
<div class="pagination {% if view.has_filters and all_count %}col9{% else %}col12{% endif %}">
<p>{% blocktrans with page_obj.number as current_page and paginator.num_pages as num_pages %}Page {{ current_page }} of {{ num_pages }}.{% endblocktrans %}</p>
<ul>
{% pagination_link_previous page_obj view %}
{% pagination_link_next page_obj view %}
<div class="block-rules">
{% for rule in segment.get_rules %}
<div class="block-data block-data--icon icon icon-{{ rule.icon }}">
<p class="block-data__description">{{ rule.description.title }}</p>
{% if rule.description.code %}
<pre class="block-data__content block-data__content--code">{{ rule.description.value }}</pre>
{% else %}
<p class="block-data__content block-data__content--text">
<strong>{{ rule.description.value }}</strong>
</p>
{% endif %}
</div>
{% endfor %}
</div>
<div class="block-footer">
<div class="block-stats">
<p class="block-stats__content">
{% trans "Active on" %}
<strong>{{ segment.get_used_pages|length }}</strong>
{% trans "page" %}{{ segment.get_used_pages|length|pluralize }} {% trans "and" %}
<strong>{{ segment.get_created_variants|length }}</strong>
{% trans "variant" %}{{ segment.get_created_variants|length|pluralize }}.
</p>
</div>
<ul class="block-actions">
{% if segment.status == segment.STATUS_DISABLED %}
<li class="block-actions__action">
{% if user_can_create %}
<a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a>
{% else %}
<span>enabled</span>
{% endif %}
</li>
{% elif segment.status == segment.STATUS_ENABLED %}
<li class="block-actions__action">
{% if user_can_create %}
<a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a>
{% else %}
<span>disabled</span>
{% endif %}
</li>
{% endif %}
{% if user_can_create %}
<li class="block-actions__action">
<a href="{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}" title="{% trans "Edit this segment" %}">edit</a>
</li>
<li class="block-actions__action">
<a href="{% url 'wagtail_personalisation_segment_modeladmin_delete' segment.pk %}" title="{% trans "Delete this segment" %}">delete</a>
</li>
{% endif %}
<li class="block-actions__action">
<a href="{% url 'wagtail_personalisation_segment_modeladmin_inspect' segment.pk %}" title="{% trans "Inspect this segment" %}">inspect</a>
</li>
</ul>
</div>
{% endif %}
{% endblock %}
{% endblock %}
</div>
</div>
{% endfor %}
{% endif %}
{% if user_can_create %}
{% blocktrans with url=view.create_url name=view.verbose_name %}
<a class="block block-suggestion" href="{{ url }}">
<span class="block-suggestion__text">Add a new {{ name }}</span>
</a>
{% endblocktrans %}
{% endif %}
</div>
{% endblock %}

View File

@@ -46,6 +46,7 @@ class SegmentModelAdmin(ModelAdmin):
index_view_extra_css = ['css/index.css']
form_view_extra_js = ['js/commons.js', 'js/form.js']
form_view_extra_css = ['css/form.css']
inspect_view_enabled = True
def index_view(self, request):
kwargs = {'model_admin': self}

View File

@@ -44,8 +44,3 @@ class RequestFactory(BaseRequestFactory):
request.session = SessionStore()
request._messages = FallbackStorage(request)
return request
@pytest.fixture
def user(django_user_model):
return django_user_model.objects.create(username='user')

View File

@@ -1,248 +0,0 @@
from __future__ import absolute_import, unicode_literals
import datetime
from django.forms.models import model_to_dict
from django.contrib.sessions.backends.db import SessionStore
import pytest
from wagtail_personalisation.forms import SegmentAdminForm
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import TimeRule, VisitCountRule
from tests.factories.segment import SegmentFactory
def form_with_data(segment, *rules):
model_fields = ['type', 'status', 'count', 'name', 'match_any']
class TestSegmentAdminForm(SegmentAdminForm):
class Meta:
model = Segment
fields = model_fields
data = model_to_dict(segment, model_fields)
for formset in TestSegmentAdminForm().formsets.values():
rule_data = {}
count = 0
for rule in rules:
if isinstance(rule, formset.model):
rule_data = model_to_dict(rule)
for key, value in rule_data.items():
data['{}-{}-{}'.format(formset.prefix, count, key)] = value
count += 1
data['{}-INITIAL_FORMS'.format(formset.prefix)] = 0
data['{}-TOTAL_FORMS'.format(formset.prefix)] = count
return TestSegmentAdminForm(data)
@pytest.mark.django_db
def test_session_added_to_static_segment_at_creation(site, client, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
assert user in instance.static_users.all()
@pytest.mark.django_db
def test_anonymous_user_not_added_to_static_segment_at_creation(site, client):
session = client.session
session.save()
client.get(site.root_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
assert not instance.static_users.all()
@pytest.mark.django_db
def test_match_any_correct_populates(site, client, django_user_model):
user = django_user_model.objects.create(username='first')
session = client.session
client.force_login(user)
client.get(site.root_page.url)
other_user = django_user_model.objects.create(username='second')
client.cookies.clear()
second_session = client.session
other_page = site.root_page.get_last_child()
client.force_login(other_user)
client.get(other_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, match_any=True)
rule_1 = VisitCountRule(counted_page=site.root_page)
rule_2 = VisitCountRule(counted_page=other_page)
form = form_with_data(segment, rule_1, rule_2)
instance = form.save()
assert session.session_key != second_session.session_key
assert user in instance.static_users.all()
assert other_user in instance.static_users.all()
@pytest.mark.django_db
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, client, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
static_rule = VisitCountRule(counted_page=site.root_page)
non_static_rule = TimeRule(
start_time=datetime.time(0, 0, 0),
end_time=datetime.time(23, 59, 59),
)
form = form_with_data(segment, static_rule, non_static_rule)
instance = form.save()
assert not instance.static_users.all()
@pytest.mark.django_db
def test_session_not_added_to_static_segment_after_creation(site, client, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
assert not instance.static_users.all()
@pytest.mark.django_db
def test_session_added_to_static_segment_after_creation(site, client, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
assert user in instance.static_users.all()
@pytest.mark.django_db
def test_anonymou_user_not_added_to_static_segment_after_creation(site, client):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
session = client.session
session.save()
client.get(site.root_page.url)
assert not instance.static_users.all()
@pytest.mark.django_db
def test_session_not_added_to_static_segment_after_full(site, client, django_user_model):
user = django_user_model.objects.create(username='first')
other_user = django_user_model.objects.create(username='second')
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
assert not instance.static_users.all()
session = client.session
client.force_login(user)
client.get(site.root_page.url)
assert instance.static_users.count() == 1
client.cookies.clear()
second_session = client.session
client.force_login(other_user)
client.get(site.root_page.url)
assert session.session_key != second_session.session_key
assert instance.static_users.count() == 1
assert user in instance.static_users.all()
assert other_user not in instance.static_users.all()
@pytest.mark.django_db
def test_sessions_not_added_to_static_segment_if_rule_not_static(client, site, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = TimeRule(
start_time=datetime.time(0, 0, 0),
end_time=datetime.time(23, 59, 59),
segment=segment,
)
form = form_with_data(segment, rule)
instance = form.save()
assert not instance.static_users.all()
@pytest.mark.django_db
def test_does_not_calculate_the_segment_again(site, client, mocker, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
form = form_with_data(segment, rule)
instance = form.save()
assert user in instance.static_users.all()
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
client.get(site.root_page.url)
assert mock_test_rule.call_count == 0
@pytest.mark.django_db
def test_non_static_rules_have_a_count():
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
rule = TimeRule(
start_time=datetime.time(0, 0, 0),
end_time=datetime.time(23, 59, 59),
segment=segment,
)
form = form_with_data(segment, rule)
assert not form.is_valid()
@pytest.mark.django_db
def test_static_segment_with_static_rules_needs_no_count(site):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
form = form_with_data(segment, rule)
assert form.is_valid()
@pytest.mark.django_db
def test_dynamic_segment_with_non_static_rules_have_a_count():
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, count=0)
rule = TimeRule(
start_time=datetime.time(0, 0, 0),
end_time=datetime.time(23, 59, 59),
)
form = form_with_data(segment, rule)
assert form.is_valid(), form.errors

View File

@@ -1,16 +1,18 @@
[tox]
envlist = py{27}-django{19}-wagtail{19},lint
envlist = py{27,35,36}-django{19,110,111}-wagtail{19,110},lint
[testenv]
commands = coverage run --parallel -m pytest {posargs}
extras = test
deps =
django19: django>=1.9,<1.10
django110: django>=1.10<1.11
django111: django>=1.11,<1.12
wagtail19: wagtail>=1.9,<1.10
wagtail110: wagtail>=1.10,<1.11
[testenv:coverage-report]
basepython = python2.7
basepython = python3.5
deps = coverage
pip_pre = true
skip_install = true
@@ -20,7 +22,7 @@ commands =
[testenv:lint]
basepython = python2.7
basepython = python3.5
deps = flake8
commands =
flake8 src tests setup.py