7

Compare commits

...

574 Commits

Author SHA1 Message Date
cb525a2b39 Bump version: 0.15.1 → 0.15.2 2021-09-24 10:24:41 +02:00
53880228e4 Merge pull request #226 from mikedingjan/feature/remove-staticfiles-tag
Replace staticfiles with static tag (django removed the staticfiles)
2021-08-12 14:20:16 +02:00
2bee66d0ae Replace staticfiles with static tag (django removed the staticfiles) 2021-08-12 10:44:02 +02:00
16e24b6791 Bump version: 0.15.0 → 0.15.1 2021-07-13 17:01:35 +02:00
477bfb9665 Newer versions of Wagtail provide extra args for listing buttons 2021-07-13 16:40:41 +02:00
6108469047 Remove old versions from test matrix 2021-07-13 16:40:23 +02:00
686f180081 Bump version: 0.14.0 → 0.15.0 2021-07-09 11:00:14 +02:00
9b1dbe35cb fix(tox): use correct format command for current package 2021-06-28 12:15:24 +02:00
7e0594e341 fix(tox): add new tox setup for github actions 2021-06-28 12:13:55 +02:00
0c19456053 Merge pull request #212 from marcelhekking/make_compatible_with_latest_wagtail_version
Make compatible with latest wagtail version
2021-06-28 12:10:31 +02:00
18140f76ab chore(ci): trigger github actions on pr 2021-06-28 12:08:58 +02:00
88b17ceeb8 chore(ci): add github actions python test step 2021-06-28 12:06:43 +02:00
570de7d128 Flake-import failed 2021-06-24 08:38:06 +02:00
b82d5165c3 Take up wagtail 2.11 in Travis test matrix and tox settings 2021-06-24 08:16:29 +02:00
8d802dbbf4 Restore original travis settings 2021-06-24 07:58:11 +02:00
9274073c68 Fix test errors 2021-06-24 07:57:31 +02:00
1f1264cf95 Fix typo 2020-11-25 16:40:15 +01:00
3f16ad686e Remove obsolete line 2020-11-25 15:54:32 +01:00
7101b63122 Check backward compatibility with tox 2020-11-25 15:50:52 +01:00
ffd839159b Make changes backwards compatible 2020-11-25 12:08:42 +01:00
d074ef85b9 No need for these settings 2020-11-24 09:10:14 +01:00
f3e403bec6 Make compatible with latest Wagtail version (2.11.2) 2020-11-24 09:05:20 +01:00
137b5b411c Merge pull request #203 from davisnando/master
Fix is_authenticated 'bool' object is not callable error
2020-01-24 08:22:06 +01:00
39f3500813 Bump version: 0.13.0 → 0.14.0 2019-09-27 09:16:15 +02:00
6a6c3e8d7b Merge pull request #202 from wagtail/feature/wagtail-2-6
Update test matrix to include new Django and Wagtail versions
2019-09-26 11:45:29 +02:00
336ed2317c Merge pull request #198 from ixc/198_delete_variants_of_descendants
Variants are not deleted for page descendants
2019-09-19 09:57:18 +02:00
06569a3cc1 Fix 'bool' object is not callable error 2019-08-27 11:43:39 +02:00
da6e5127ed Update test matrix to include new Django and Wagtail versions 2019-08-22 09:36:27 +02:00
3d054ec585 Add migrations for country field on origincountryrule 2019-08-22 08:28:14 +02:00
43b5b62e60 Clean up test_static_dynamic_segments.py so it passes flake8 (#199)
* WP-1 clean up tests to pass flake8

* WP-1 undo yapf!
2019-03-16 08:27:37 +01:00
40a9959680 Bump version: 0.12.1 → 0.13.0 2019-03-15 14:04:42 +01:00
13e13ccae5 update sandbox 2019-03-15 13:54:59 +01:00
318b65b3eb allow wagtail24 2019-03-15 13:52:51 +01:00
6b9b4e0af2 remove extraneous file 2019-03-15 13:50:48 +01:00
69a4514129 update testcode
run isort
2019-03-15 12:58:30 +01:00
585cb0b16a clean out some python2-isms 2019-03-15 11:50:34 +01:00
4ae8a5e60b postmerge fixes 2019-03-15 11:46:44 +01:00
d7ad1be51f Merge preakholt changes 2019-03-15 11:21:14 +01:00
bd5b85cedb Merge branch 'master' into 198_delete_variants_of_descendants 2019-01-25 12:34:58 +11:00
956c1bf4f5 Use timezone-aware dates and times in rules (#197)
* use timezone-aware dates and times re #196

* remove redundant newlines

* Fix flake8 linting errors in python 3.6
2019-01-24 16:27:34 +01:00
d775ef57e6 Ensure variants are deleted for page decendants 2019-01-24 16:59:44 +11:00
d34c449638 Merge tag '1.0.4' into develop
1.0.4
2019-01-16 13:09:24 +02:00
23af862798 Merge branch 'release/1.0.4' 2019-01-16 13:09:15 +02:00
88263dea60 Bump version to 1.0.4 2019-01-16 13:08:58 +02:00
2e1e09f60b Merge pull request #31 from praekeltfoundation/feature/GEWEB-774-fix-segment-admin-js
Add custom js files to segment create view
2019-01-16 12:43:30 +02:00
86e669e4f4 Add custom js files to segment create view 2019-01-16 12:02:14 +02:00
807005461e update version to 1.0.3 2019-01-10 17:13:38 +02:00
7f9e0971f5 Merge tag '1.0.3' into develop
bugfix:exclude variant returns queryset when params is queryset
2019-01-10 16:12:20 +02:00
a4a1a2ddca Merge branch 'release/1.0.3' 2019-01-10 16:12:14 +02:00
9cc6e966ba bugfix:exclude variant returns queryset when params is queryset 2019-01-10 16:12:01 +02:00
311abeb6c8 Merge pull request #30 from praekeltfoundation/feature/GEWEB-746-fix-panel-server-error
exclude variants should return a list when a list if given or a queryset
2019-01-10 15:05:56 +02:00
60675203c6 fixed some comments 2019-01-10 14:51:43 +02:00
ceef806301 add tests for varient exclusion use cases 2019-01-10 14:47:18 +02:00
650e061f91 assign pages on exclude 2019-01-10 14:47:01 +02:00
9235932f00 update exclude varient format and add variants to tests 2019-01-10 12:41:53 +02:00
1e0efc975a Merge tag '1.0.2' into develop
1.0.2
2019-01-09 19:14:24 +02:00
7517dcd051 Merge branch 'release/1.0.2' 2019-01-09 19:14:17 +02:00
94c9efa315 Bump version to 1.0.2 2019-01-09 19:14:07 +02:00
f73e59421b Merge pull request #29 from praekeltfoundation/feature/GEWEB-740-upgrade-to-wagtail-2.2
Upgrade to wagtail 2.2
2019-01-09 17:39:44 +02:00
f054b86e07 fix flake error in conftest 2019-01-09 17:25:56 +02:00
cbb56847ae fix flake8 error 2019-01-09 17:17:51 +02:00
b135e79c77 remove trailing print 2019-01-09 17:04:00 +02:00
5cd8751450 add ve to the gitignore 2019-01-09 16:56:38 +02:00
c07b280276 allow database access for tests 2019-01-09 16:55:06 +02:00
28266c4500 fix flake error 2019-01-09 16:54:49 +02:00
94a5c6b289 tests for querysets in variant_exclude 2019-01-09 16:54:28 +02:00
875d8302de exclude variants should return a list when a list if given or a queryset 2019-01-09 16:54:04 +02:00
4c09ad4ca7 fix flake error W504 2019-01-09 16:52:26 +02:00
0d260a12a4 Tell travis to use wagtail 2.2 2019-01-09 16:28:31 +02:00
7888f0b615 Upgrade to wagtail 2.2 in requirements and tests 2019-01-09 16:12:05 +02:00
02e63ed82c Merge tag '1.0.1' into develop
1.0.1
2019-01-02 17:28:58 +02:00
a411ad1ccc Merge branch 'release/1.0.1' 2019-01-02 17:28:50 +02:00
1a1df18bf3 Bump version to 1.0.1 2019-01-02 17:28:36 +02:00
56d28faec8 Merge branch 'master' into develop 2019-01-02 17:24:40 +02:00
f95b8dcb93 Merge pull request #28 from praekeltfoundation/feature/GEWEB-740-upgrade-to-wagtail-2-and-python-3
Define panel for rules to handle InlinePanel changes
2019-01-02 17:23:32 +02:00
d3f4d42d82 Define panel for rules to handle InlinePanel changes 2019-01-02 16:51:56 +02:00
4c08581919 Merge tag '1.0.0' into develop
1.0.0
2018-12-18 14:03:39 +02:00
11886ae135 Merge branch 'release/1.0.0' 2018-12-18 14:03:29 +02:00
83cc7f790e Bump version to 1.0.0 2018-12-18 14:03:01 +02:00
dcdeb4e9a2 Update imports in examples in docs 2018-12-18 14:02:01 +02:00
2e827be41a Merge pull request #27 from praekeltfoundation/feature/GEWEB-740-upgrade-to-wagtail-2-and-python-3
Upgrade to Python 3 and Wagtail 2
2018-12-18 13:48:31 +02:00
0f9bfb0343 Tell travis to use Wagtail 2.0 2018-12-18 13:37:34 +02:00
1c74e6cfb9 Update Wagtail imports to work for 2.0 2018-12-18 13:32:02 +02:00
9c45ac56db Upgrade Wagtail to 2.0 in requirements and tests 2018-12-18 13:30:27 +02:00
2f7b92fb2e Run tests on python 3.6 2018-12-18 10:40:45 +02:00
1e69d929aa Bump version: 0.12.0 → 0.12.1 2018-09-26 20:42:26 +02:00
a178a8b533 Fix django classifier version number 2018-09-26 20:40:42 +02:00
f2e01c803a Bump version: 0.11.3 → 0.12.0 2018-09-26 14:23:05 +02:00
eb9d4f3e31 Update changelog for version 0.12.0 2018-09-26 14:20:18 +02:00
4ceb59c719 Merge pull request #193 from ixc/relax-wagtail-constraint
Remove overly restrictive wagtail dependency version constraint (#192)
2018-09-26 08:14:59 +02:00
6fcab3ac11 Remove overly restrictive wagtail dependency version constraint (#192) 2018-09-26 11:00:47 +10:00
1f464adaa7 Do not generate sitemap entries for variants (#187) 2018-09-25 07:57:41 +02:00
d15f6c37d3 Return 404 if variant page is accessed directly (#188) 2018-09-25 07:57:06 +02:00
7d679d7111 Add origin country rule (#190) 2018-09-25 07:51:25 +02:00
b11a6ce4ca Add missing TOXENV=lint to Travis (#191) 2018-09-25 07:50:54 +02:00
4e9a6e902d Merge pull request #186 from wagtail/feature/show-to-everyone-block
Add an option to show a personalised block to everyone (no segment)
2018-08-06 19:25:27 +02:00
3ce0aef8d5 Add an option to show a personalised block to everyone 2018-08-06 15:16:36 +01:00
a47803eca5 Delete related variants when deleting the segment (#183)
* Delete related variants when deleting the segment

Closes #155

* Fix typo

* Fix migration ordering

* Split metadata migrations
2018-07-19 12:59:46 +02:00
e42e1a865b Add width and height to logo in README 2018-07-17 17:14:44 +02:00
293004fdc6 Feature/documentation (#170)
* Splits documentation over multiple folders

* Editor guide documentation intro

* Adds segment dashboard documentation

* Adds editor documenation regarding segment creation

* Adds logo with padding for the documentation

* Updates usage guide documentation

* Splits sandbox and custom rules documentation

* Improves ‘Create a variant’ documentation

* Adds documentation regarding streamfield and template tags

* Consistent StreamField references

* Feedback from M. Dingjan

* Remove ‘coming soon’ section

* Enable sandbox debug toolbar

* Updated documentation
2018-07-17 16:59:32 +02:00
6f0425cd5f Merge pull request #184 from wagtail/182-deleting-the-page-deletes-its-variants
Delete page variants when deleting the page
2018-07-09 08:05:26 +02:00
0fd6d4d2e5 Delete variants of a page that is being deleted 2018-07-06 15:11:03 +01:00
e0fbefd53f Update contributors list with all the committers 2018-06-08 18:03:50 +02:00
c87b2936e9 Merge pull request #179 from wagtail/feature/fix-page-summary-item-page-count
Feature/fix page summary item page count
2018-05-31 13:54:58 +02:00
7010f5acea Move comment to top of function 2018-05-31 11:02:39 +02:00
d94890848d Add description of logic 2018-05-31 10:26:17 +02:00
f8d226efaf Prevent corrected summary item from counting the root page 2018-05-31 10:22:22 +02:00
037381f79f Merge pull request #178 from tm-kn/use-the-same-way-as-wagtail-to-count-pages
Use Wagtail's logic in the page count in the dash
2018-05-30 22:26:25 +02:00
0f5501ceef Merge pull request #177 from tm-kn/django-get-field-bug
Fix bug on visiting a segment page in the admin
2018-05-30 22:25:08 +02:00
4e5454b348 Use Wagtail's logic in the page count in the dash
This adds a check if root page is root page in order to calculate the
count properly the same way as Wagtail does [1].

[1] 5c9ff23e22/wagtail/admin/site_summary.py (L38)
2018-05-30 20:13:04 +01:00
9919d76741 Merge pull request #176 from tm-kn/fix-excluding-pages-without-variant
Fix excluding pages without variant
2018-05-30 21:07:32 +02:00
7e9dd8624b Fix bug on visiting a segment page in the admin
Wagtail's InlinePanel's relies on
django.db.models.options.Options.get_field -
bcf6b6da77/wagtail/admin/edit_handlers.py (L688)
which seems to have a bug of using related_query_name rather than
related_name naming and therefore trying to edit segment always
fails - https://code.djangoproject.com/ticket/29458. The bug has not
yet been confirmed by others but it's impossible to edit segments.

Therefore this change deletes related_query_name from relations from
rules to the Segment model.
2018-05-30 20:00:53 +01:00
6514bc1763 Fix excluding pages without variant
Currently count in the admin dashboard shows "-1" and no pages are
displayed in the explorer.
2018-05-30 19:47:28 +01:00
65a46f2bd9 Fix segment model naming for forced_segment and the userbar 2018-05-26 16:37:53 +02:00
f1b62a7546 add missing migration 2018-05-26 16:27:20 +02:00
03e02e8b91 Change all mentions of LabD urls to wagtail urls 2018-05-26 16:26:59 +02:00
8d8975ac36 Merge pull request #126 from wagtail/feature/force-as-segment
adds simple segment forcing for superusers
2018-05-26 16:24:44 +02:00
2324a30afd Merge branch 'feature/djangoconf-sprint' 2018-05-26 16:11:27 +02:00
0bdb80f25a improve SessionSegmentAdapter 2018-05-26 16:04:11 +02:00
1c1a7ce1b8 Remove wagtail icon from segment link in user bar 2018-05-26 15:08:17 +02:00
2a48eb3498 fix django version in tox.ini 2018-05-26 14:59:25 +02:00
4ad097b4fa include wagtail-2.1 in test matrix 2018-05-26 14:57:03 +02:00
939247c147 Add force as segment to the Wagtail user bar 2018-05-26 14:52:09 +02:00
12f110d913 remove customer manager again for now 2018-05-26 14:35:53 +02:00
c8fe62d2b1 remove praekholt deployment target from travis setup 2018-05-26 14:35:53 +02:00
83c2a4289e Adjust README for ordering 2018-05-26 12:56:18 +02:00
84ac76f33e Adjust tox.ini for wagtail 2.1 2018-05-26 12:56:02 +02:00
f6598ca1f7 Adjust requirements 2018-05-26 12:55:29 +02:00
726c0cd70f update travis setup 2018-05-26 12:32:39 +02:00
4f3f9a4d40 lint 2018-05-26 12:28:01 +02:00
3a378830e0 fix basepython 2018-05-26 12:27:52 +02:00
8a151e3bab python2 cleanups 2018-05-26 12:06:35 +02:00
bb34bddaf4 add custom model manager 2018-05-26 12:01:26 +02:00
9710d3b479 post-merge cleanups 2018-05-26 11:45:28 +02:00
5536adc3ec Merge branch 'develop' into feature/djangoconf-sprint 2018-05-26 10:48:33 +02:00
5b8d578493 only test for wagtail2 and django2 on python3 2018-05-26 09:54:56 +02:00
bdba6b65cf use new wagtail_factories package 2018-03-22 14:41:54 +01:00
cbcd80d248 update tox.ini 2018-03-17 11:56:14 +01:00
9b1c5a6ab6 fixes test runs
added dependency link to Makefile until Michael releases new
wagtail-factories
2018-03-17 11:37:11 +01:00
62d258fd9e fixes wagtail2 compatibility
return QuerySets instead of lists
2018-03-17 11:26:56 +01:00
32e73329c3 Revert wagtail-factories setting 2018-03-16 11:51:25 +01:00
fde53ea0ef Fix all tests for django and wagtail 2 2018-03-16 11:45:07 +01:00
22a7367211 Update module paths for tests 2018-03-16 11:16:47 +01:00
0d89d47735 Prevent webpack copy error from img dir 2018-03-16 11:14:19 +01:00
92189a3be8 Fix dashboard edit links 2018-03-16 11:14:19 +01:00
6c9d8b2730 remove typo 2018-03-16 11:14:19 +01:00
e141e5396e make Makefile more portable 2018-03-16 11:14:19 +01:00
c0e2b969e8 Set site ID in sandbox settings 2018-03-16 11:14:19 +01:00
7b5e3d4c9d Fix exampledata 2018-03-16 11:14:19 +01:00
6b7a1ed591 Updated requirements and module paths 2018-03-16 11:14:19 +01:00
9b25cd2a94 Add missing dependency `pytest-pythonpath` 2018-03-16 11:10:45 +01:00
3a86c189dc Merge tag '0.11.3' into develop
Bugfix: Handle errors when testing an invalid visit count rule
2018-03-09 20:35:33 +02:00
82c26f9772 Merge branch 'release/0.11.3' 2018-03-09 20:35:20 +02:00
03eb812e45 Version 0.11.3 2018-03-09 20:35:08 +02:00
e3522d0acb Merge pull request #26 from praekeltfoundation/feature/catch-exceptions-when-visit-count-rule-is-blank
Handle exceptions for empty VisitCountRule
2018-03-09 20:32:05 +02:00
7f5e958ee3 Catch the exception if the visit count rule doesn't have a page 2018-03-09 19:20:30 +02:00
241bfb5240 Merge tag '0.11.2' into develop
Bugfix: Stop populating static segments when the count is reached
2018-03-08 14:00:58 +02:00
d5df6e0e58 Merge branch 'release/0.11.2' 2018-03-08 14:00:38 +02:00
865efd0792 Version 0.11.2 2018-03-08 13:59:23 +02:00
454c936e0f Merge pull request #25 from praekeltfoundation/feature/fix-static-segment-population
Fix off-by-one error in static segment population
2018-03-08 13:24:48 +02:00
74d3123084 Ensure static segments don't have one extra user 2018-03-08 13:14:29 +02:00
9bfd816430 Merge tag '0.11.1' into develop
Populate entirely static segments from registered Users not active Sessions
2018-03-01 16:26:46 +02:00
02e06bd9f3 Merge branch 'release/0.11.1' 2018-03-01 16:26:35 +02:00
c7ad3251cf Version 0.11.1 2018-03-01 16:26:20 +02:00
cb8b7da496 Merge pull request #23 from praekeltfoundation/feature/populate-static-segments-from-db-at-creation
Populate static segments from database
2018-03-01 16:23:10 +02:00
0efd3ae937 Update tests for new static segment population 2018-02-26 14:34:02 +02:00
d335e4fd7b Populate static segments even if the count is 0 2018-02-26 14:31:56 +02:00
db2f82967e Only loop through users once when saving static segments
When saving a new static segment we count the matching users and
populate the segment from the database. Each of these requires us
to loop through all of the users, so it's better to do them at the
same time.
2018-02-25 15:43:12 +02:00
37243365a7 Merge branch 'develop' into feature/populate-static-segments-from-db-at-creation 2018-02-25 15:08:36 +02:00
43a2b590b4 Merge tag '0.11.0' into develop
Bug Fix: Query rule should not be static
Enable retrieval of user data for static rules through csv download
2018-02-23 17:02:53 +02:00
cc1dd337bb Merge branch 'release/0.11.0' 2018-02-23 17:02:34 +02:00
a677846ff7 Bump version to 0.11.0 2018-02-23 17:02:17 +02:00
7d7861b862 Merge pull request #22 from praekeltfoundation/feature/SAS-72-download-cvs-of-user-data
Download CSV of users in a segment
2018-02-23 16:50:52 +02:00
8e854d0abe Merge branch 'develop' into feature/SAS-72-download-cvs-of-user-data 2018-02-23 14:05:31 +02:00
0051061d96 Merge pull request #21 from praekeltfoundation/feature/SAS-72-get-user-info-for-visit-count-rule
Get user info from visit count rule
2018-02-23 14:02:15 +02:00
f898dfe017 Actually add tests for segment users view 2018-02-22 16:12:24 +02:00
8ced5bd81c Fix flake8 2018-02-22 15:15:01 +02:00
9a86b0c8cc Add tests for segment users view 2018-02-22 15:14:34 +02:00
9408f90789 Use mock for testing matching user count
The fake class was causing other tests to fail because it inherits from AbstractBaseRule but isn't in the database.
I removed it and replaced it with mocked calls
2018-02-22 14:23:14 +02:00
ba6056e3f8 Link to download CSV of users in segment 2018-02-22 14:21:43 +02:00
fdc0a7f2e1 Get user info for Visit count rule 2018-02-21 19:08:29 +02:00
12b0cd9231 Make visit count session retrieval seperate method 2018-02-21 19:07:35 +02:00
330557be8d Make VisitCountRule.test_user actually test with only a user 2018-02-21 18:48:44 +02:00
aa917dee9c Merge pull request #20 from praekeltfoundation/feature/SAS-90-revert-query-rule-to-not-be-static
Remove static flag for Query Rule
2018-02-21 15:54:36 +02:00
364cb1a7e6 Query rule should not be static 2018-02-20 15:08:12 +02:00
8f789b3e17 Fill static segments with users from the database at creation 2018-02-15 13:20:48 +02:00
bedbe06c65 Merge branch 'release/0.10.9' into develop 2018-02-13 14:17:55 +02:00
362f15e5ff version bump 0.10.9 2018-02-13 14:17:29 +02:00
8a0dba2efb Merge pull request #18 from praekeltfoundation/feature/issue-18-Display-the-number-of-users-in-a-static-segment-SAS-99
Display the number of users in a static segment SAS-99
2018-02-13 14:11:56 +02:00
59f4877e04 add localize filter 2018-02-13 13:57:00 +02:00
2ff29cc375 get the number of users in a static segment from static_users variable 2018-02-13 13:47:45 +02:00
8527e6ff23 Merge tag '0.10.8' into develop
Don't add users to exclude list for dynamic segments
Store segments a user is excluded from in the session
2018-02-13 10:13:24 +02:00
d7c07cb238 Merge branch 'release/0.10.8' 2018-02-13 10:12:45 +02:00
6e83366df6 Bump version to 0.10.8 2018-02-13 10:12:35 +02:00
55364f8906 Merge pull request #16 from praekeltfoundation/feature/SAS-78-fix-sampling-dynamic-segments
Fix sampling for Dynamic segments
2018-02-13 09:59:28 +02:00
4fd0b30c66 Check rules test skipped if segment excluded by session 2018-02-12 18:56:13 +02:00
c909852b08 Add tests 2018-02-12 18:01:01 +02:00
ea1ecc2a98 Get excluded segments from session and don't check them again 2018-02-12 18:00:38 +02:00
0f0aecf673 Store excluded segments in the session object 2018-02-12 17:57:36 +02:00
c11960f921 Only store excluded users for static segments 2018-02-12 16:58:20 +02:00
37d49dcdfb Merge tag '0.10.7' into develop
Bug Fix: Ensure static segment members are show the survey immediately
Records users excluded by randomisation on the segment
Don't re-check excluded users
2018-02-09 17:01:02 +02:00
869237360d Merge branch 'release/0.10.7' 2018-02-09 17:00:09 +02:00
33277a0b20 Version 0.10.7 2018-02-09 16:59:26 +02:00
2cd643fb2d Merge pull request #15 from praekeltfoundation/feature/SAS-78-store-excluded-users-on-segment
Store users excluded by randomisation
2018-02-09 16:52:44 +02:00
0f18024ebc Tests 2018-02-09 12:36:34 +02:00
521222f748 Don't check if excluded users match segment rules 2018-02-09 12:35:53 +02:00
56a8e106d8 Add users excluded by randomisation to excluded_users list 2018-02-09 12:35:09 +02:00
3162191a16 Add field to segment to store excluded users 2018-02-09 12:32:42 +02:00
8c7e99313b Merge pull request #14 from praekeltfoundation/feature/add-static-segments-to-session
Add static segments to session if rules pass
2018-02-09 12:30:55 +02:00
824e42174f Tests 2018-02-08 19:48:31 +02:00
d114bb2570 Always add the segment to the session if they pass 2018-02-08 19:47:35 +02:00
7bba1e57cc Merge pull request #12 from praekeltfoundation/feature/only-deploy-once-per-build
Only deploy for one environment per travis build
2018-02-06 16:26:43 +02:00
3017f32b6b Add spaces around = in bash deploy condition 2018-02-06 16:18:16 +02:00
6b1a7cf1f2 Only deploy for one environment per travis build 2018-02-06 16:11:20 +02:00
1525b7946c Merge tag '0.10.6' into develop
Accepts and stores randomisation percentage for segment
Adds users to segment based on random number relative to percentage
2018-02-06 15:26:25 +02:00
7bf1bc3f19 Merge branch 'release/0.10.6' 2018-02-06 15:26:02 +02:00
4c60bcbe6b Version 0.10.6 2018-02-06 15:25:51 +02:00
ad4f75d471 Merge pull request #11 from praekeltfoundation/feature/SAS-93-use-randomisation-percentage
Use randomisation percentage
2018-02-06 14:51:56 +02:00
086168954d Test randomisation of static segments at creation 2018-02-05 12:30:12 +02:00
881090f2f9 Test randomisation for dynamic segments 2018-02-05 12:21:09 +02:00
d073c7d268 Randomise into static segments when they are created 2018-02-05 12:20:10 +02:00
7200b5b4c4 If a session passes segment rules randomise them into the segement 2018-02-05 12:19:36 +02:00
6f97c76958 Add method to randomise matching sessions into the segment 2018-02-05 12:18:22 +02:00
ecb4f928fb Merge pull request #10 from praekeltfoundation/feature/SAS-92-store-randomisation-percentage
Add randomisation percentage to segment model
2018-02-05 11:54:23 +02:00
29aa91477e Migrations 2018-02-02 10:15:20 +02:00
5c3acc6661 Display randomisation percentages on segment dashboard 2018-02-02 10:15:04 +02:00
602919d2d4 Test randomisation percentage added to segments 2018-02-02 10:14:18 +02:00
ae97118c3f Store randomisation percentage on segment model 2018-02-02 10:13:18 +02:00
51774b939e Version 0.10.5 2018-01-26 17:57:43 +02:00
908f85e295 Merge pull request #9 from praekeltfoundation/feature/SAS-86-display-record-counter-on-dash
Display record counter on segments in dash
2018-01-26 17:51:34 +02:00
99f9700ed0 Display record counter for active segments 2018-01-26 16:21:15 +02:00
7fa8ee1a46 Merge pull request #8 from praekeltfoundation/feature/SAS-85-only-count-active-frontend-users
Don't include staff and inactive users when counting matched users
2018-01-26 16:01:02 +02:00
5ad70d68f6 Don't include staff and inactive users when counting matched users 2018-01-26 15:38:26 +02:00
06bfe77901 Merge pull request #7 from praekeltfoundation/feature/SAS-85-calculate-matching-users
Count number of users that match static rules for a segment
2018-01-26 08:58:00 +02:00
d5e89d374b Remove unnecessary imports 2018-01-25 19:51:50 +02:00
5b39e82f80 Fixed test for adding user counter to segment 2018-01-25 18:42:38 +02:00
fbcebb43a4 Store record count on a segment when it is created 2018-01-25 15:14:19 +02:00
ef271587ec Test count_matching_users method 2018-01-25 13:26:05 +02:00
786a8801b1 Migrations for Segment.matched_user_count 2018-01-25 11:26:57 +02:00
caf73aa43c Add matched_users_count field to segments 2018-01-25 11:12:46 +02:00
4021d2c915 Add method to calculate the number of users that match a segment 2018-01-24 22:00:28 +02:00
33f96af4a3 Allow test_user() for static rules to accept a user 2018-01-24 15:14:24 +02:00
6299feb497 Merge tag '0.10.4' into develop
0.10.4
2018-01-22 12:58:49 +02:00
7ced6db126 Merge branch 'release/0.10.4' 2018-01-22 12:58:31 +02:00
c6ce67c9c9 Version bump 0.10.4 2018-01-22 12:58:12 +02:00
3df3fc0b16 Merge pull request #6 from praekeltfoundation/feature/SAS-87-fix-static-rules
Set QueryRule to be static
2018-01-22 12:03:29 +02:00
a00929846e Set query rule to be static 2018-01-18 16:17:30 +02:00
49fba11049 Merge pull request #5 from praekeltfoundation/enable-linting
Enable and fix lint
2018-01-08 10:23:36 +00:00
e3488e87ad Enable and fix lint 2018-01-08 09:08:11 +00:00
808aa6d202 Add tests for exclude_variants 2018-01-08 09:07:15 +00:00
efb060cc6e Merge branch 'release/0.10.3' into develop 2018-01-05 19:18:14 +02:00
414afa5269 Merge branch 'release/0.10.3' 2018-01-05 19:17:18 +02:00
b3f0ac2d58 Version Bump 0.10.3 2018-01-05 19:16:45 +02:00
4f9c18d2cf Merge pull request #3 from praekeltfoundation/fix-segment-edit-link
Fix segment edit link
2018-01-05 17:08:12 +00:00
a4a283e4f3 Fix segment edit link
This is hardcoded at the moment which doesn't work unless you
redirect URLs to have a trailing slash.

Using the reverse URL lookup works in all cases.
2018-01-05 17:00:15 +00:00
30318549e2 Merge pull request #4 from praekeltfoundation/feature/issue-4-update-wagtail-django-dependencies
Update wagtail and django dependencies
2018-01-05 18:55:18 +02:00
f19de241b0 Update dependencies for wagtail and django
Only run tests for wagtail v1.13 and django v1.11
2018-01-05 18:28:40 +02:00
95ecd8d200 Merge branch 'release/0.10.2' into develop 2017-11-23 16:33:16 +02:00
6436b85b1d Merge branch 'release/0.10.2' 2017-11-23 16:32:31 +02:00
06471248d3 Version Bump 0.10.2 2017-11-23 16:32:20 +02:00
e3df03f559 Merge pull request #2 from torchbox/fix/visitor-rule-not-updating
Fix visitor rule not updating correct paths
2017-11-23 16:29:13 +02:00
0a42ce3eeb Fix not updating visitor count rule properly 2017-11-23 14:10:16 +00:00
e5068894c3 Merge branch 'release/0.10.1' into develop 2017-11-13 15:03:41 +02:00
fdc2b97194 Merge branch 'release/0.10.1' 2017-11-13 15:03:32 +02:00
a8d3aeab68 Version Bump 0.10.1 2017-11-13 15:02:56 +02:00
c76d6d1617 Update manifest to include missing js files 2017-11-13 14:58:20 +02:00
a8c4b66d6e Merge branch 'release/0.10.0' into develop 2017-11-09 16:53:42 +02:00
f3fbee99a2 Merge branch 'release/0.10.0' 2017-11-09 16:53:37 +02:00
4918c99b5f Version Bump 0.10.0 2017-11-09 16:53:22 +02:00
330c3bd377 Merge branch 'feature/issue-1-create-pypi-release' into develop 2017-11-09 16:49:29 +02:00
9c9a9d3acd add missing function call 2017-11-09 16:37:52 +02:00
51e9aa9724 remove all the extra testing environments 2017-11-09 13:36:43 +02:00
a5705fd53c Removed extra newline 2017-11-09 13:27:31 +02:00
9d1f3074c0 add pypi password 2017-11-09 13:18:22 +02:00
3bfd5b8e8f Add deploy instructions to travis file 2017-11-09 11:54:13 +02:00
232609fb4e Update setup file for new package name 2017-11-09 11:52:55 +02:00
35fd4836b0 update the realease 2017-11-08 13:58:50 +00:00
b786b0a4d2 Merge pull request #1 from torchbox/feature/dynamic-segments
Add the logic for static segments
2017-11-08 11:16:25 +00:00
23b1456438 Add tests which cover anonymous users 2017-11-01 17:10:03 +00:00
1f4a4536ab Make the static elements tracked users only
We cannot track anonymous users as the session expires after 10 minutes of
inactivity. This also avoids an issue where there is an error when the user's
session has expired and they navigate a page
2017-11-01 16:43:22 +00:00
b8bf27fb99 add enabled to the excluded segments checked 2017-10-27 07:29:41 +01:00
d07e06b4f0 lock down the static segment to prevent changes 2017-10-26 14:34:16 +01:00
71d7faba1f Correctly initialise the form 2017-10-26 13:11:16 +01:00
743d3f668e Limit the segemnt to count for all static case 2017-10-26 12:47:59 +01:00
bc0b69fde5 Hide and show the count input as required 2017-10-26 11:47:28 +01:00
7cf22d05f6 Tidy up the logic checks and remove the frozen property 2017-10-26 10:55:13 +01:00
9e0fc8e6fd Make the static segments work with match_any and fix bug in visit count 2017-10-24 10:50:05 +01:00
a116b14d57 Update to use the save method on the form to populate the segments 2017-10-23 15:46:34 +01:00
44cc95617e Use a form to clean the instance 2017-10-23 15:00:31 +01:00
c6ff2801c5 Update to use a post_init signal to populate the segment 2017-10-20 17:33:47 +01:00
0d2834a55f Update the help text migration 2017-10-20 17:17:31 +01:00
ff236a095d Improve the clarity of the help text 2017-10-20 12:18:29 +01:00
ef20580334 Notify users to static compatible rules and update docs 2017-10-20 12:09:25 +01:00
cf41be4b76 Add clean method to ensure mixed static segments are valid 2017-10-20 10:57:19 +01:00
f339879907 Ensure that mixed static and dynamic segments are not populated at runtime 2017-10-20 09:53:18 +01:00
aa2a239aec Update the dashboard to display information about static segments 2017-10-19 17:19:53 +01:00
8c96fffd4e Ensure that the session is checked correctly 2017-10-17 17:35:57 +01:00
675d219f1f Add the logic for static segments 2017-10-17 16:57:07 +01:00
0d9e4aab0c Bump version: 0.9.0 → 0.9.1 2017-07-11 22:22:13 +02:00
ac9f32c570 Indent changelog 2017-07-11 22:21:17 +02:00
bc91d64770 Add Django 1.9/1.10 and Wagtail 1.9 to tests and requirements 2017-07-11 21:58:26 +02:00
821ee5863e Updates changelog 2017-06-16 16:58:02 +02:00
1a2777835c Merge pull request #165 from LabD/feature/dashboard-item-changes
Corrects pages item and adds personalisation pages
2017-06-16 15:38:04 +02:00
d160bb5217 adds documentation to util 2017-06-16 15:37:44 +02:00
b021164309 Cleans imports and created ‘filter variants’ util 2017-06-15 17:01:57 +02:00
6f6d6e3a06 Remove backslashes 2017-06-15 17:01:57 +02:00
fb7ed4936d Small improvements and item fix 2017-06-15 17:01:57 +02:00
80e33a467e Corrects pages item and adds personalisation pages 2017-06-15 17:01:57 +02:00
9c88ec1582 fixes segment adapter add syntax for segment forcing 2017-06-12 11:12:33 +02:00
785d1486e4 adds simple segment forcing for superusers 2017-06-12 07:56:38 +02:00
b553295fc2 Merge pull request #145 from LabD/feature/transifex
adds tx config file
2017-06-09 09:30:09 +02:00
0da7f111e3 Use older migration dependencies that exist in older Wagtail versions 2017-06-05 20:00:08 +02:00
fe6a26e1fd Import reverse from core.urlresolvers instead of shortcuts for the sake of Django 1.9 compatibility 2017-06-05 19:51:53 +02:00
94c947a435 Fixes link name 2017-06-03 10:35:52 +02:00
ef08403ba3 Set correct pypi classifiers 2017-06-02 15:46:20 +02:00
48935218db Add make release 2017-06-02 15:42:54 +02:00
4c315f067a Filter badges for pypi 2017-06-02 15:41:56 +02:00
6e56d8cf4d Forgotten rename (variation->variant) 2017-06-02 15:32:26 +02:00
281086a159 Rename variations to variants 2017-06-02 15:31:46 +02:00
2e74741033 Set version to 0.9.0 2017-06-02 15:20:26 +02:00
537dfb12a6 Fix test 2017-06-02 15:16:59 +02:00
1e885adf83 Simpler origin page check 2017-06-02 15:11:50 +02:00
12853c61e1 Hides all variants in the page explorer 2017-06-02 15:11:50 +02:00
d4ee67b778 Remove unnecessary query 2017-06-02 15:11:37 +02:00
c6e4d9cca8 Adds cool icons. Depends on #123 2017-06-02 15:11:37 +02:00
38aff16044 Adds additional options to the dropdown menu
Allows for quick access to both new and existing variations, as well as creation of new segments.
2017-06-02 15:11:37 +02:00
aafc8c4ed5 Simpler queries 2017-06-02 15:10:54 +02:00
f21c423b1c Updates functions for new mixin 2017-06-02 15:10:54 +02:00
7e24485aaf Adds comments to segment functions 2017-06-02 15:10:54 +02:00
12ae3fa173 Uses the Mixin for page lookups 2017-06-02 15:10:54 +02:00
961a58505a Adds variant count 2017-06-02 15:10:54 +02:00
cb03a36ba2 Work in progress 2017-06-02 15:10:54 +02:00
9605773a74 Update changelog 2017-06-02 15:06:26 +02:00
46d86d852d Adds stream fields to the sandbox 2017-06-02 15:00:34 +02:00
0776d2300a Feature/fa icons for rules (#123)
Integrate font-awesome for the rule icons
2017-06-02 14:53:41 +02:00
38620d916f Resolves page copy error (Fixes #159) 2017-06-02 14:11:49 +02:00
3ee0645267 Fixes summary icon 2017-06-02 13:18:45 +02:00
eda00d624e Merge pull request #157 from LabD/tomdyson-readme-2
Include sandbox admin URL in README
2017-06-02 12:59:27 +02:00
0e24ae17ac Fixes PersonalisableBlock segment validation 2017-06-02 12:58:09 +02:00
39c31dc81a Include sandbox admin URL in README 2017-06-02 10:55:31 +01:00
702fa233a9 Fixes line length and adds Middleware code example 2017-06-02 11:52:31 +02:00
7405c34252 Add missing migration for tests.app.pages (fixes build) 2017-06-02 11:26:57 +02:00
6f96f2f172 Add more tests for wagtail_hooks 2017-06-02 11:23:39 +02:00
559d3c5356 Add unittest for serving a regular page
See #150
2017-06-02 10:32:41 +02:00
5aa754dd80 Rename PersonalisablePageMixin.personalisable_metadata
Use PersonalisablePageMixin.personalisation_metadata instead to mirror
the package name
2017-06-02 10:23:46 +02:00
282baa4787 Add tests for the wagtail_hooks.serve_variaton 2017-06-02 10:19:32 +02:00
35c22cb6af Use correct filenames for the tests (and split some) 2017-06-02 10:11:46 +02:00
6c5ab9c6ae Merge HomePage/SpecialPage to ContentPage in the tests
No need to create separate models with the same functionality
2017-06-02 09:30:04 +02:00
d92fe13d37 Refactor the test structure
This renames the tests.sandbox app to tests.site to clear up some
confusion with the two sandbox app's.
2017-06-02 09:25:51 +02:00
dfb364b7fc Adds additional segment to avoid confusion
As the dropdown checks if a page has been segmented, and disables when there are no segments available
2017-06-02 07:43:56 +02:00
15a0276041 Update import sorting and newlines 2017-06-01 18:06:41 +02:00
c0c3ce19fe Remove a few unused imports 2017-06-01 18:06:41 +02:00
e0fffb70b7 Remove redefinition of `class Meta` warning 2017-06-01 18:06:41 +02:00
7f2882ce0e Adds sandbox example data for personalisation 2017-06-01 16:57:40 +02:00
a629bfc862 Enables rich text for the sanbox home page 2017-06-01 16:23:11 +02:00
e3ceecfa7d Adds a default value to the homepage text content
Ensures the migration prefills the RichTextField, wich in turn prevents validation errors when copying the page for personalisation. Fixes #154.
2017-06-01 16:06:27 +02:00
0f79cf1d15 Revert "Remove manage.py in the root"
This reverts commit dda0bc720e.
2017-06-01 13:55:39 +02:00
29001fac8e Remove custom wagtail page form
THe older page form allowed to change segment settings inline but since
support for this was temporarily removed in an earlier commit we can
clean this up too
2017-06-01 13:51:13 +02:00
dda0bc720e Remove manage.py in the root
This should not be here since it isn't a runnable project
2017-06-01 13:49:28 +02:00
5beef1b27c New icon for the segments menu item (#118)
New icon for the segments menu item
2017-06-01 08:48:34 +02:00
8465e6dcbb Fix serving the variant pages 2017-05-31 21:44:57 +02:00
cf8101156c Extend the sandbox home to show text content 2017-05-31 21:44:50 +02:00
7076973fc8 Refactor personalisable pages
Instead of working with django model mixins it now uses a separate
model entity to keep track of the personalized pages (metadata).

The current downside of this approach is that the segment of an existing
variant is no longer easily adjustable for now.
2017-05-31 21:31:40 +02:00
c2735807b4 Adds ‘Create new segment’ option to dropdown 2017-05-31 20:02:46 +02:00
2651eb0e3c Add related_name to PersonalisablePageMixin 2017-05-31 18:25:34 +02:00
18838b2e8c Revert "Fix migrations (#151)"
This reverts commit d35a7fee57.
2017-05-31 18:21:57 +02:00
763a67e2d4 Switch mixin ordering 2017-05-31 18:12:19 +02:00
d35a7fee57 Fix migrations (#151) 2017-05-31 18:09:50 +02:00
c100dc603c fix regression 2017-05-31 18:01:19 +02:00
d4421eebcb minor cleanup 2017-05-31 17:58:25 +02:00
fea3bc8b8e Updated commons.js.map 2017-05-31 17:56:14 +02:00
38a18f80a4 Merge pull request #146 from LabD/simplifications+optimisations
Handles both Python 2 & 3, and multiple optimisations & simplifications.
2017-05-31 17:50:56 +02:00
85613db363 Merge branch 'simplifications+optimisations' of github.com:LabD/wagtail-personalisation into simplifications+optimisations 2017-05-31 17:43:18 +02:00
5fbfb82480 Switches back to absolute wagtail_personalisation imports. 2017-05-31 17:41:12 +02:00
f88722c827 Further simplifications. 2017-05-31 17:38:51 +02:00
46ad32236c Merge branch 'master' into simplifications+optimisations 2017-05-31 17:21:07 +02:00
e6fac5f7fb Fix templatetag tests 2017-05-31 17:19:24 +02:00
82f2dd460e adds tests for templatetag 2017-05-31 17:19:24 +02:00
4f2dc3a304 Handles both Python 2 & 3, and multiple optimisations & simplifications. 2017-05-31 17:13:33 +02:00
8905f471ee adds tx config file 2017-05-31 17:10:58 +02:00
6587d0fd56 Merge pull request #96 from LabD/feature/translation-support
Fixes a few translation strings and adds translation files
2017-05-31 16:58:50 +02:00
4e221b6666 Update README.rst 2017-05-31 16:52:02 +02:00
99d2e4a347 support multiple uses of the mixin class (#140) 2017-05-31 16:50:56 +02:00
4deaaa985f Redirect to existing variant if present 2017-05-31 16:39:03 +02:00
63d5de9803 Optimize query for the segment visits 2017-05-31 16:37:10 +02:00
18eea8a9b1 updates faulty strings 2017-05-31 16:30:18 +02:00
a4cf8120b4 fixes blocktrans with html 2017-05-31 16:27:16 +02:00
09fbb5d795 adds package name to copyright 2017-05-31 16:23:49 +02:00
d79765efee adds copyright information to translation file 2017-05-31 16:23:49 +02:00
0aa07261ad fixes a few translation strings and adds translation files 2017-05-31 16:23:49 +02:00
361f0b1700 Re-remove the segments adapter instructions
Too specific for the README.
2017-05-31 16:19:33 +02:00
5eefa21699 Add test for sessionadapter.refresh when segment is disable 2017-05-31 16:19:09 +02:00
c5579fa8d4 Sets dashboard as the default view 2017-05-31 16:18:07 +02:00
7bb523d962 Minor codestyle issue in the adapters.refresh() code 2017-05-31 16:13:30 +02:00
03073eb004 Add unittests for the session adapter 2017-05-31 16:13:30 +02:00
e107d73716 Add docstring to set_segments() 2017-05-31 16:13:30 +02:00
cbc2ec7270 Simplify saving/retrieving user segments 2017-05-31 16:13:30 +02:00
2450bd45ac remove redundant migration dependency 2017-05-31 15:59:09 +02:00
1b73119766 post rebase fixes 2017-05-31 15:59:09 +02:00
ebef7f8785 refactor and add tests 2017-05-31 15:59:09 +02:00
623af1a06a start using correct model in tests 2017-05-31 15:59:09 +02:00
f693e62bbf rename base-class to reflect mixin nature 2017-05-31 15:59:09 +02:00
4e61ff0d08 remove all references or uses of old PersonalisablePage model 2017-05-31 15:59:09 +02:00
49062d36b4 remove PersonalisablePage model 2017-05-31 15:59:09 +02:00
66ed40f8ec make personalisedpage class abstract 2017-05-31 15:59:09 +02:00
59b6e7f31e Add django debug toolbar to the sandbox 2017-05-31 15:27:29 +02:00
f2aa8879a9 Add Adapter.set_segments() and use it when refreshig the segment
This makes the internal API a bit more consistent
2017-05-31 15:15:27 +02:00
decfc88efe Fix depth to parent selector 2017-05-31 14:59:39 +02:00
fc442171e4 Move to edit page if variant exists fixes #106 and fixes #89 2017-05-31 14:59:39 +02:00
5076dd60bd Remove the unfinished datalayer template tag 2017-05-31 14:57:17 +02:00
194daba67b Remove postgres dependencies fixes #115 2017-05-31 14:51:50 +02:00
9705947d3f Use the Segment Adapter for retrieving page visits
This fixes the last occurence of directly accessing the session in the
codebase
2017-05-31 14:49:07 +02:00
31f8a329f2 Use constants for enabled/disabling status on Segment 2017-05-31 14:48:46 +02:00
3d920d8ed8 Minor codestyle fix 2017-05-31 14:48:24 +02:00
73ea5157ff Minor code cleanups 2017-05-31 14:48:24 +02:00
e24cb9aee3 Use the segment adapter to retrieve user segments 2017-05-31 14:32:15 +02:00
fb95439d83 adapters docstring typo (#120) 2017-05-31 14:14:02 +02:00
dcb8867dc7 Merge pull request #108 from LabD/feature/segment-template-block
Adds an ifsegment template block
2017-05-31 14:02:08 +02:00
de2c7f9988 fixes newline 2017-05-31 14:01:11 +02:00
d3d4e7ec92 fixes a few code style issues and docs issues 2017-05-31 13:59:41 +02:00
23537ac29b adds docstrings, moves parse_tag to utils and adds documentation 2017-05-31 13:59:34 +02:00
a78290281b fixes adapter issues and fixes block for segments adapter 2017-05-31 13:58:31 +02:00
20011f079d adds simple block templatetag wip 2017-05-31 13:58:31 +02:00
9790a44fd1 Add migrations to the tests.sandbox.pages app
This fixes tests
2017-05-31 13:57:19 +02:00
7436384471 Move visit count logic to the segment adapter
This makes the code also a bit simpler and updates the tests
2017-05-31 13:57:19 +02:00
7034c09d4a Allows switching between list and dashboard view 2017-05-31 13:55:08 +02:00
45f2de62ea Splits segment dashboard view and set default table view 2017-05-31 13:55:08 +02:00
cc38634519 Add adapters.get_segment_adapter to set the segment adapter on the request
object.

Previously the segment adapter was set globally with the request as
attribute. This causes thread safety issues. Instead we now initialise
the segment adapter for each request and set it as attribute
2017-05-31 13:33:22 +02:00
4158bafe58 Python 2.x compatibility fix for sandbox 2017-05-31 13:19:47 +02:00
b55bdb60b9 Remove psycopg2 from sandbox requirements
The sandbox's `settings.py` specifies `django.db.backends.sqlite3`.
2017-05-31 12:44:46 +02:00
531ae6df98 README tweaks
minor style changes, removal of segments adapter section.
2017-05-31 12:17:57 +02:00
699d24bc44 Fix tests on Python < 3.6 (remove f-string) 2017-05-31 12:11:07 +02:00
2977440ee7 Merge pull request #103 from robmoorman/modeladmin-docs
Point out more that modeladmin is required
2017-05-31 11:55:02 +02:00
23a9b1df84 Point out more that modeladmin is required 2017-05-31 11:48:35 +02:00
eb837fa7b2 Ingnore migration files for code coverage 2017-05-31 11:26:56 +02:00
b523327c8c Only 1.10 2017-05-31 11:22:39 +02:00
a2957b7e77 Fix install version 1.10.1 2017-05-31 11:22:39 +02:00
9623e67dd7 set alabaster options 2017-05-31 11:11:52 +02:00
7531eb9451 Minor markup fixes to README 2017-05-31 10:51:13 +02:00
4503ad387e Add travis badges, fix line length 2017-05-31 10:50:46 +02:00
5d1abee76c Fix missing renames from personalisation -> wagtail_personalisation 2017-05-31 10:46:33 +02:00
e2d3b9bf9d Update tox/travis config 2017-05-31 10:37:45 +02:00
dbb0ecde95 Merge branch 'master' of https://github.com/LabD/wagtail-personalisation 2017-05-31 10:34:47 +02:00
325c2d5801 Fixes package name in README 2017-05-31 10:34:39 +02:00
77005097c3 Add MIT license 2017-05-31 10:30:38 +02:00
c1f50a5add Remove users.json 2017-05-31 10:06:57 +02:00
9edb0f736a Remove AUTHORS again, was alreadiy in contributors.rst 2017-05-31 09:56:11 +02:00
39b26be325 Add the AUTHORS file 2017-05-31 09:55:00 +02:00
92f898462c Use opensource@labdigital.nl for the email address 2017-05-31 09:53:01 +02:00
33cf0217da fixes page crashing when visiting a page that doesnt inherit PersonalisablePage 2017-05-30 17:36:32 +02:00
83a7db5952 Renames personalisation module in documentation 2017-05-30 15:46:27 +02:00
a73e356ffe Adds a contributors file 2017-05-29 10:00:41 +02:00
3eac2cd4dd Codestyle fixes (flake8)
This also adds the flake8 dependencies to the test extras for
installation.
2017-05-28 09:13:08 +02:00
55da67523f Puts the receivers in a seperate file and adds appconfig 2017-05-26 16:40:50 +02:00
9a7d41284e Renames template filters 2017-05-26 16:31:54 +02:00
ebde527ae9 Renames the module from ‘personalisation’ to ‘wagtail_personalisation’ 2017-05-26 16:31:54 +02:00
5f1c52c93c Fixes persistent rule persisting 2017-05-24 21:21:24 +02:00
974a4d6f46 Moves the form in to models.py to forms.py 2017-05-24 17:34:34 +02:00
97e4116945 Fix migrations for the sandbox 2017-05-24 17:29:00 +02:00
02e18491bb Fix test dependencies and skip one test for now 2017-05-24 17:05:34 +02:00
5156887a9d Merge pull request #79 from LabD/feature/move-views-in-hooks
Moves segment dashboard views from hooks to views
2017-05-24 15:56:31 +02:00
90c2289396 Merge branch 'master' into feature/move-views-in-hooks 2017-05-24 11:04:59 +02:00
fda9017c38 Assume usage of .js extension for react components 2017-05-24 11:03:35 +02:00
2cff8a01fe Adds front-end tooling 2017-05-24 11:03:35 +02:00
7fda6f411a Moves static files to sub directories 2017-05-24 11:03:35 +02:00
32748b0d6f Moves segment dashboard views from hooks to views
Because it makes more sense
2017-05-23 14:38:40 +02:00
479aec516e Adds front-end tooling 2017-05-23 14:26:36 +02:00
d773d0e8f8 Adds lines between the short and long description 2017-05-23 13:43:00 +02:00
3391087944 Moves static files to sub directories 2017-05-23 13:41:30 +02:00
111e6e1568 Docstrings for thje rules 2017-05-23 13:24:22 +02:00
a34386f811 Model docstrings 2017-05-23 12:53:06 +02:00
6c4178cf21 Merge branch 'master' of https://github.com/LabD/wagtail-personalisation 2017-05-23 12:03:22 +02:00
7e240d50b1 Docstrings for the wagtail hooks 2017-05-23 12:03:19 +02:00
0a4e8af6ad Mergens ‘enable’ and ‘disable’ view and adds security 2017-05-22 17:30:23 +02:00
9dce9578f0 Adds docstrings to BaseSegmentsAdapter 2017-05-22 17:00:53 +02:00
33eabf4b77 Four lines to one line 2017-05-22 16:24:44 +02:00
4f114689a5 Updates docstrings for the views 2017-05-22 16:16:48 +02:00
4afb643d5e Updates docstrings in the utils 2017-05-22 16:05:29 +02:00
24af956913 Fixes too long lines in adapters 2017-05-22 15:59:45 +02:00
4d4445641e Sorts imports and removes trailing whitespace 2017-05-22 15:57:19 +02:00
0ab31bb154 Adds docstring to the personalised struct block 2017-05-22 15:47:47 +02:00
885b378b63 Improve comments and readability for several files
App settings: added a comment.
Admin: consistent docstrings.
Admin urls: better readability.
2017-05-22 15:38:48 +02:00
74cbec77c9 Fixes typo 2017-05-22 15:27:07 +02:00
c2d0812980 Adds docstring to the adapters and updates filters 2017-05-22 14:52:16 +02:00
a7265647ef Fixes typo in docstring 2017-05-22 13:49:21 +02:00
9584da5d19 Adds templatetag docstrings 2017-05-22 13:48:29 +02:00
b7b88b214f Fixes typo in the Readme 2017-05-22 13:19:50 +02:00
d2bb377110 Adds styling to logo 2017-05-22 13:16:51 +02:00
6d46c30270 Actually remove the roadmap file 2017-05-22 13:14:46 +02:00
952b88aba7 Removes roadmap, adds screenshot 2017-05-22 13:13:28 +02:00
bffd13dd3e fixes personalisable homepage in sandbox and try except for segment visit count 2017-05-16 11:46:47 +02:00
e451f792e3 adds sqlite to sandbox and fixes summary panel 2017-05-08 10:39:30 +02:00
47123ce723 small fixes 2017-05-08 10:10:33 +02:00
e5fa590f8e Fix lint/python errors 2017-05-08 10:01:38 +02:00
b62fabd47b Fix PROJECT_DIR path in te sandbox 2017-05-08 09:57:40 +02:00
9cc44a2931 Add sandbox instructions 2017-05-08 09:51:53 +02:00
c70ecbc408 Add a sandbox environment 2017-05-08 09:48:05 +02:00
ec7b00c318 adds a few docblocks and isorts 2017-05-03 15:04:52 +02:00
dd1dafd450 more cleanup on the segments adapter, moves segment dict creator to utils 2017-05-03 14:59:32 +02:00
ecfae3ec19 renames segment variable names to be more descriptive 2017-05-03 14:59:32 +02:00
f41fa062be Refactors the refresh function in the segment adapter 2017-05-03 14:59:32 +02:00
82d11d57aa fixes a few future imports and adds support for new python and wagtail versions 2017-05-03 08:58:17 +02:00
6640bf8d74 Splits test factories and updates documentation 2017-04-25 16:15:33 +02:00
da56a521a9 Merge pull request #47 from LabD/feature/documentation
Feature/documentation
2017-04-25 15:46:17 +02:00
3a00f9c2d7 Adds different styling to disabled segments 2017-04-21 15:34:07 +02:00
b761412aa8 Corrects dashboard display for days rule 2017-04-21 13:40:08 +02:00
e21102dab0 Changes reference to ‘rules’ from ‘models’ in documentation 2017-04-21 12:32:14 +02:00
51084d3d72 Even more rules explained! 2017-04-21 12:29:31 +02:00
f14b941756 Shortens line length to max 79 and adds two more rules 2017-04-21 12:29:31 +02:00
fe3fddab51 WIP Adds default rules documentation 2017-04-21 12:29:31 +02:00
d6b4f45998 refactors week day rule user testing 2017-04-21 09:15:35 +02:00
5a978ed73a Move panels definition to __init__() 2017-04-21 08:55:01 +02:00
bcd4ebd31f Add missing migration for DeviceRule 2017-04-21 08:53:51 +02:00
7f9f11b86e Fix typo - encoded_name -> encoded_name() 2017-04-21 08:51:58 +02:00
7b01913d32 Replaces previous matching rule because it didn’t retain the persistent segments 2017-04-19 11:43:26 +02:00
d468c68970 Changes reference to rule models in adapter and tests 2017-04-19 11:40:21 +02:00
6e566344df Splits the models and rules file 2017-04-19 11:40:21 +02:00
1ef4999b70 Finished rule template
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2017-04-13 08:57:17 +02:00
d1528f1ed4 work in progress
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2017-04-13 08:57:17 +02:00
b5d0a657ed Fixes segment name reference. 2017-04-13 08:38:44 +02:00
f780e325c4 Fixing the datalayer tag. WIP. 2017-04-13 08:38:44 +02:00
762843ce49 adds datalayer template tag 2017-04-13 08:38:44 +02:00
7ff7e51fdd “Continue reading” links 2017-04-12 11:56:48 +02:00
a178c88d63 adds personalisable page documentation 2017-04-12 11:56:48 +02:00
7a6f7d10e6 Getting started titles for better readability 2017-04-12 11:56:48 +02:00
6671c9db45 Consistent config file references. 2017-04-12 11:56:48 +02:00
d69bec8f22 Code example for middleware requirements. 2017-04-12 11:56:48 +02:00
546fa9d513 Added missing import to code example. 2017-04-12 11:56:48 +02:00
4d27306a05 Custom rules documentation 2017-04-12 11:56:48 +02:00
88fda12af1 Even more subjects 2017-04-12 11:56:48 +02:00
fe393fccb8 Template for Implimentation page 2017-04-12 11:56:48 +02:00
64ec6218fc Page for developer details
No content yet
2017-04-12 11:56:48 +02:00
824db4dc7c Personalized content documentation 2017-04-12 11:56:48 +02:00
154442a303 Creating segments documentation 2017-04-12 11:56:48 +02:00
cdb0093d09 Started working on documentation 2017-04-12 11:56:48 +02:00
e07fdde739 adds basic sphinx documentation 2017-04-12 11:56:48 +02:00
472635e63e Add travis/tox 2017-02-18 22:50:42 +01:00
8e754fef07 Use wagtail-factories for testing 2017-02-18 22:46:51 +01:00
08f88181f4 Merge pull request #19 from LabD/feature/personalized-blocks
[WIP] adding personalisable StructBlock for in-page personalisation
2017-01-24 14:04:21 +01:00
456a84d120 Merge pull request #31 from LabD/feature/device-rule
Device detection and accompanying rule
2017-01-24 13:51:04 +01:00
040e181b7b Merge pull request #32 from LabD/feature/unlimited-rules
Implements unlimited rules.
2017-01-24 13:48:00 +01:00
b49d64f82f Implements unlimited rules. 2017-01-24 13:44:44 +01:00
a57e177a94 better implementation 2017-01-24 13:38:43 +01:00
991ae15fce addng TODO statement for better abstraction 2017-01-24 13:38:43 +01:00
e4f16302f4 better discription 2017-01-24 13:38:43 +01:00
cde821a30a implement conditional rendering in block 2017-01-24 13:38:43 +01:00
ed3a9449fd adding personalisable StructBlock for in-page personalisation 2017-01-24 13:38:43 +01:00
1c4062eb7e Device detection and accompanying rule
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2017-01-24 13:38:43 +01:00
597c0a50f0 Device detection and accompanying rule
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2017-01-11 15:28:40 +01:00
a7b477d71f Day rule and accompanying tests
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2017-01-10 15:36:25 +01:00
9b90551e3b Fixed logged in rule string
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2017-01-10 14:10:09 +01:00
b7ca6541f4 fixes match any testing 2017-01-10 12:52:10 +01:00
cf946f2bee removes related prefetch 2017-01-10 12:37:36 +01:00
3e6302ca03 Roadmap semver versioning
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2016-12-29 14:16:49 +01:00
00b337da1e Fixes one last oversight in roadmap image
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2016-12-29 13:02:02 +01:00
9a0d73fc12 Fixes translation mistakes
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2016-12-29 13:00:06 +01:00
f01a4b439c Roadmap and readme update
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2016-12-29 12:57:36 +01:00
f0d260af7f Merge pull request #23 from LabD/feature/segments-adapter
Feature/segments adapter
2016-12-28 12:25:49 +01:00
bcc56877dc Merge branch 'master' into feature/segments-adapter 2016-12-22 15:34:19 +01:00
a2e39154f1 Merge pull request #29 from LabD/feature/match-any
adds match_any option to segments
2016-12-22 15:29:13 +01:00
5f8c768894 adds match_any option to segments 2016-12-22 14:25:00 +01:00
b86259a0dc adds abstract functions 2016-12-22 13:42:54 +01:00
6b779f29b0 fixes a few issues 2016-12-22 12:35:03 +01:00
8d257867b8 removes hardcoded sessions reference 2016-12-22 10:13:42 +01:00
c058ab18d7 fixes adapter instantiating so it can be used outside of the hooks 2016-12-22 10:10:41 +01:00
94b54bfcf7 fixes imports and abstract class 2016-12-19 14:15:25 +01:00
6ecc15c1dd adds adapter logic to wagtail hook 2016-12-19 11:01:48 +01:00
24847c1828 adds settings logic for your own segments adapter 2016-12-19 10:33:12 +01:00
6e32a2e6a3 adds first quick adapter draft 2016-12-18 11:38:19 +01:00
4c0f0760b2 Merge pull request #15 from LabD/feature/logged-in-rule
adding rule for logged in users
2016-12-16 13:56:49 +01:00
a287f4ff04 Merge branch 'master' into feature/logged-in-rule 2016-12-16 13:56:31 +01:00
4484931d4b new new logo
Signed-off-by: Jasper Berghoef <jasper.berghoef@gmail.com>
2016-12-13 14:26:45 +01:00
3fccb0a872 fix copy paste error 2016-12-11 13:22:18 +01:00
1c9e993c21 adding rule for logged in users 2016-12-11 13:17:42 +01:00
205 changed files with 16363 additions and 1367 deletions

View File

@ -11,10 +11,12 @@ line_length = 79
multi_line_output = 4
balanced_wrapping = true
use_parentheses = true
default_section = THIRDPARTY
known_first_party = wagtail_personalisation,tests
[*.json, *.yml, *rc]
indent_size = 2
[Makefile]
indent_style = tab
indent_size = 4
indent_size = 4

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "airbnb"
}

89
.github/workflows/python-test.yml vendored Normal file
View File

@ -0,0 +1,89 @@
---
name: Python Tests
on: [push, pull_request]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: pip install tox
- name: Validate formatting
run: tox -e format
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
tox_env:
- py36-dj22-wt211
- py36-dj22-wt212
- py36-dj22-wt213
- py37-dj22-wt211
- py37-dj22-wt212
- py37-dj22-wt213
- py38-dj22-wt211
- py38-dj22-wt212
- py38-dj22-wt213
- py37-dj30-wt211
- py37-dj30-wt212
- py37-dj30-wt213
- py38-dj30-wt211
- py38-dj30-wt212
- py38-dj30-wt213
include:
- python-version: 3.6
tox_env: py36-dj22-wt211
- python-version: 3.6
tox_env: py36-dj22-wt212
- python-version: 3.6
tox_env: py36-dj22-wt213
- python-version: 3.7
tox_env: py37-dj22-wt211
- python-version: 3.7
tox_env: py37-dj22-wt212
- python-version: 3.7
tox_env: py37-dj22-wt213
- python-version: 3.8
tox_env: py38-dj22-wt211
- python-version: 3.8
tox_env: py38-dj22-wt212
- python-version: 3.8
tox_env: py38-dj22-wt213
- python-version: 3.7
tox_env: py37-dj30-wt211
- python-version: 3.7
tox_env: py37-dj30-wt212
- python-version: 3.7
tox_env: py37-dj30-wt213
- python-version: 3.8
tox_env: py38-dj30-wt211
- python-version: 3.8
tox_env: py38-dj30-wt212
- python-version: 3.8
tox_env: py38-dj30-wt213
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox -e ${{ matrix.tox_env }} --index-url=https://pypi.python.org/simple/
- name: Prepare artifacts
run: mkdir -p .coverage-data && mv .coverage.* .coverage-data/
- uses: actions/upload-artifact@master
with:
name: coverage-data
path: .coverage-data/

13
.gitignore vendored
View File

@ -3,19 +3,26 @@
*.swo
*.python-version
*.coverage
.coverage.*
*.egg-info/
.cache/
.idea/
.tox/
.vscode/
build/
ve/
dist/
tests/sandbox/assets
htmlcov/
docs/_build
coverage.xml
db.sqlite3
.vscode/settings.json
tests/sandbox/assets
node_modules
.DS_Store
.pytest_cache/

9
.tx/config Normal file
View File

@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[wagtail_personalisation]
file_filter = src/wagtail_personalisation/locale/<lang>/LC_MESSAGES/django.po
source_file = src/wagtail_personalisation/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO

96
CHANGES
View File

@ -1,3 +1,93 @@
0.1 (TBD)
====================
- Initial release
0.13.0
=================
- Merged Praekelt fork
- Add custom javascript to segment forms
- bugfix:exclude variant returns queryset when params is queryset
- Added RulePanel, a subclass of InlinePanel, for Rules
- Upgrade to Wagtail > 2.0, drop support for Wagtail < 2
0.12.0
==================
- Fix Django version classifier in setup.py
0.12.0
==================
- Merged forks of Torchbox and Praekelt
- Wagtail 2 compatibility
- Makefile adjustments for portability
- Adds simple segment forcing for superusers
- Fix excluding pages without variant
- Fix bug on visiting a segment page in the admin
- Use Wagtail's logic in the page count in the dash
- Prevent corrected summary item from counting the root page
- Delete variants of a page that is being deleted
- Add end user and developer documentation
- Add an option to show a personalised block to everyone
- Add origin country rule (#190)
- Return 404 if variant page is accessed directly (#188)
- Do not generate sitemap entries for variants (#187)
- Remove restrictive wagtail dependency version constraint (#192)
0.11.3
==================
- Bugfix: Handle errors when testing an invalid visit count rule
0.11.2
==================
- Bugfix: Stop populating static segments when the count is reached
0.11.1
==================
- Populate entirely static segments from registered Users not active Sessions
0.11.0
==================
- Bug Fix: Query rule should not be static
- Enable retrieval of user data for static rules through csv download
0.10.9
==================
- Bug Fix: Display the number of users in a static segment on dashboard
0.10.8
==================
- Don't add users to exclude list for dynamic segments
- Store segments a user is excluded from in the session
0.10.7
==================
- Bug Fix: Ensure static segment members are show the survey immediately
- Records users excluded by randomisation on the segment
- Don't re-check excluded users
0.10.6
==================
- Accepts and stores randomisation percentage for segment
- Adds users to segment based on random number relative to percentage
0.10.5
==================
- Count how many users match a segments rules before saving the segment
- Stores count on the segment and displays in the dashboard
- Enables testing users against rules if there isn't an active request
0.10.0
==================
- Adds static and dynamic segments
0.9.1 (tbd)
==================
- Fixes import for reverse resolver for older Django versions (<1.10)
- Bases migrations off of older wagtail dependencies
- Adds more dashboard panels and fixes exclude variants function
0.9.0 (2017-06-02)
==================
Initial release of wagtail-personalisation. This Wagtail module provides basic
personalisation based on pre-defined rules in the backend.
This module was developed by Boris Besemer (@blurrah) and Jasper Berghoef
(@jberghoef) for Lab Digital (http://labdigital.nl)

20
CONTRIBUTORS.rst Normal file
View File

@ -0,0 +1,20 @@
Authors
=======
* Jasper Berghoef
* Boris Besemer
Contributors
============
* Michael van Tellingen
* Pim Vernooij
* Tomasz Knapik
* Kaitlyn Crawford
* Todd Dembrey
* Nathan Begbie
* Rob Moorman
* Tom Dyson
* Bertrand Bordage
* Alex Muller
* Saeed Marzban
* Milton Madanda
* Mike Dingjan

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Lab Digital
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

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

View File

@ -1,16 +1,18 @@
.PHONY: all clean requirements develop test lint flake8 isort dist
all: clean requirements dist
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
default: develop
all: clean requirements dist
clean:
find src -name '*.pyc' -delete
find tests -name '*.pyc' -delete
find . -name '*.egg-info' -delete
find . -name '*.egg-info' |xargs rm -rf
requirements:
pip install --upgrade -e .
pip install --upgrade -e .[docs,test]
install: develop
develop: clean requirements
@ -21,18 +23,33 @@ retest:
py.test --nomigrations --reuse-db tests/ -vvv
coverage:
py.test --nomigrations --reuse-db tests/ --cov=personalisation --cov-report=term-missing --cov-report=html
py.test --nomigrations --reuse-db tests/ --cov=wagtail_personalisation --cov-report=term-missing --cov-report=html
docs:
$(MAKE) -C docs html
lint: flake8 isort
flake8:
pip install flake8 flake8-debugger flake8-blind-except
flake8 src/
flake8 src/ tests/
isort:
pip install isort
isort --recursive src tests
dist:
./setup.py sdist bdist_wheel
pip install wheel
python ./setup.py sdist bdist_wheel
sandbox:
pip install -r sandbox/requirements.txt
sandbox/manage.py migrate
sandbox/manage.py loaddata sandbox/exampledata/users.json
sandbox/manage.py loaddata sandbox/exampledata/personalisation.json
sandbox/manage.py runserver
release:
pip install twine wheel
rm -rf dist/*
python setup.py sdist bdist_wheel
twine upload -s dist/*

View File

@ -1,25 +1,89 @@
.. image:: logo.png
.. start-no-pypi
Wagtail personalisation
.. image:: https://readthedocs.org/projects/wagtail-personalisation/badge/?version=latest
:target: http://wagtail-personalisation.readthedocs.io/en/latest/?badge=latest
.. image:: https://travis-ci.org/wagtail/wagtail-personalisation.svg?branch=master
:target: https://travis-ci.org/wagtail/wagtail-personalisation
.. image:: http://codecov.io/github/wagtail/wagtail-personalisation/coverage.svg?branch=master
:target: http://codecov.io/github/wagtail/wagtail-personalisation?branch=master
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
:target: https://pypi.python.org/pypi/wagtail-personalisation/
.. end-no-pypi
.. image:: logo.png
:height: 261
:width: 300
:scale: 50
:alt: Wagxperience
:align: center
Wagtail Personalisation
=======================
Wagtail personalisation enables simple content personalisation through segmenting for Wagtail.
Wagtail Personalisation is a fully-featured personalisation module for
`Wagtail CMS`_. It enables editors to create customised pages
- or parts of pages - based on segments whose rules are configured directly
in the admin interface.
.. _Wagtail CMS: http://wagtail.io/
.. image:: screenshot.png
Instructions
------------
To install the package with pip::
Wagtail Personalisation requires Wagtail 2.0 or 2.1 and Django 1.11 or 2.0.
To install the package with pip:
.. code-block:: console
pip install wagtail-personalisation
Next, include the ``personalisation`` and ``wagtail.contrib.modeladmin`` app in your project's ``INSTALLED_APPS``:
Next, include the ``wagtail_personalisation``, ``wagtail.contrib.modeladmin``
and ``wagtailfontawesome`` apps in your project's ``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = [
# ...
'wagtail.contrib.modeladmin',
'personalisation',
'wagtail_personalisation',
'wagtailfontawesome',
# ...
]
Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has been added in first, this is a prerequisite for this project.
Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has
been added in first, this is a prerequisite for this project.
.. code-block:: python
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
# ...
]
Documentation
-------------
You can find more information about installing, extending and using this module
on `Read the Docs`_.
.. _Read the Docs: http://wagtail-personalisation.readthedocs.io
Sandbox
-------
To experiment with the package you can use the sandbox provided in
this repository. To install this you will need to create and activate a
virtualenv and then run ``make sandbox``. This will start a fresh Wagtail
install, with the personalisation module enabled, on http://localhost:8000
and http://localhost:8000/cms/. The superuser credentials are
``superuser@example.com`` with the password ``testing``.

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = wagtail-personalisation
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/_static/images/dual_streamfield.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
docs/_static/images/editing_variant.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
docs/_static/images/variants_button.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

167
docs/conf.py Normal file
View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# wagtail-personalisation documentation build configuration file, created by
# sphinx-quickstart on Mon Dec 19 15:12:32 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd: # only import and set the theme if we're building docs locally
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'wagtail-personalisation'
copyright = '2019, Lab Digital BV'
author = 'Lab Digital BV'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.15.2'
# The full version, including alpha/beta/rc tags.
release = '0.15.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
# language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
html_theme_options = {
'analytics_id': 'UA-100203499-2',
}
html_logo = 'logo.png'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'wagtail-personalisationdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'wagtail-personalisation.tex', 'wagtail-personalisation Documentation',
'Lab Digital BV', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
author, 'wagtail-personalisation', 'One line description of project.',
'Miscellaneous'),
]

177
docs/default_rules.rst Normal file
View File

@ -0,0 +1,177 @@
Included rules
==============
Wagxperience comes with a base set of rules that allow you to start segmenting
your visitors quickly.
Time rule
---------
The time rule allows you to segment visitors based on the time of their visit.
Define a time frame in which visitors are matched to this segment.
================== ==========================================================
Option Description
================== ==========================================================
Start time The start time of your time frame.
End time The end time of your time frame.
================== ==========================================================
``wagtail_personalisation.rules.TimeRule``
Day rule
--------
The day rule allows you to segment visitors based on the day of their visit.
Select one or multiple days on which you would like your segment to be applied.
================== ==========================================================
Option Description
================== ==========================================================
Monday Matches when the visitors visits on a monday.
Tuesday Matches when the visitors visits on a tuesday.
Wednesday Matches when the visitors visits on a wednesday.
Thursday Matches when the visitors visits on a thursday.
Friday Matches when the visitors visits on a friday.
Saturday Matches when the visitors visits on a saturday.
Sunday Matches when the visitors visits on a sunday.
================== ==========================================================
``wagtail_personalisation.rules.DayRule``
Referral rule
-------------
The referral rule allows you to match visitors based on the website they were
referred from. For example:
.. code-block:: bash
example\.com|secondexample\.com|.*subdomain\.com
================== ==========================================================
Option Description
================== ==========================================================
Regex string The regex string to match the referral header to.
================== ==========================================================
``wagtail_personalisation.rules.ReferralRule``
Visit count rule
----------------
The visit count rule allows you to segment a visitor based on the amount of
visits per page. Use the operator to to set a maximum, minimum or equal
amount of visits.
================== ==========================================================
Option Description
================== ==========================================================
Page The page on which visits will be counted.
Count The amount of visits to match.
Operator Whether to match for more than, less than or equal to the
specified visit count.
================== ==========================================================
``wagtail_personalisation.rules.VisitCountRule``
Query rule
----------
The query rule allows you to match a visitor based on the query included in
the url. It let's you define both the parameter and the value. It will look
something like this:
.. code-block:: bash
example.com/?campaign=ourbestoffer
================== ==========================================================
Option Description
================== ==========================================================
Parameter The first part of the query ('campaign').
Value The second part of the query ('ourbestoffer').
================== ==========================================================
``wagtail_personalisation.rules.QueryRule``
Device rule
-----------
The device rule allows you to match visitors by the type of device they are
using. You can select any combination you want.
================== ==========================================================
Option Description
================== ==========================================================
Mobile phone Matches when the visitor uses a mobile phone.
Tablet Matches when the visitor uses a tablet.
Desktop Matches when the visitor uses a desktop.
================== ==========================================================
``wagtail_personalisation.rules.DeviceRule``
User is logged in rule
----------------------
The user is logged in rule allows you to match visitors that are authenticated
and logged in to your app.
================== ==========================================================
Option Description
================== ==========================================================
Is logged in Whether the user is logged in or logged out.
================== ==========================================================
``wagtail_personalisation.rules.UserIsLoggedInRule``
Origin country rule
-------------------
The origin country rule allows you to match visitors based on the origin
country of their request. This rule requires to have set up a way to detect
countries beforehand.
================== ==========================================================
Option Description
================== ==========================================================
Country What country user's request comes from.
================== ==========================================================
You must have one of the following configurations set up in order to
make it work.
- Cloudflare IP Geolocation - ``cf-ipcountry`` HTTP header set with a value of
the alpha-2 country format.
- CloudFront Geo-Targeting - ``cloudfront-viewer-country`` header set with a
value of the alpha-2 country format.
- The last fallback is to use GeoIP2 module that is included with Django. This
requires setting up an IP database beforehand, see the Django's
`GeoIP2 instructions <https://docs.djangoproject.com/en/stable/ref/contrib/gis/geoip2/>`_
for more information. It will use IP of the request, using HTTP header
the ``x-forwarded-for`` HTTP header and ``REMOTE_ADDR`` server value as a
fallback. If you want to use a custom logic when obtaining IP address, please
set the ``WAGTAIL_PERSONALISATION_IP_FUNCTION`` setting to the function that takes a
request as an argument, e.g.
.. code-block:: python
# settings.py
WAGTAIL_PERSONALISATION_IP_FUNCTION = 'yourproject.utils.get_client_ip'
# yourproject/utils.py
def get_client_ip(request):
return request['HTTP_CF_CONNECTING_IP']
``wagtail_personalisation.rules.OriginCountryRule``

View File

@ -0,0 +1,91 @@
Creating personalised content
=============================
Once you've created a segment you can start serving personalised content to your
visitors. To do this, you can choose one of three methods.
1. Create a page variant for a segment.
2. Use StreamField blocks visible for a segment only.
3. Use a template block visible for a segment only.
Method 1: Create a page variant
-------------------------------
**Why you would want to use this method**
* It has absolutely no restrictions, you can change anything you want.
* That's pretty much it.
**Why you would want to use a different method**
* You are editing a page that changes often. You would probably rather not
change the variation(s) every time the original page changes.
To create a variant of a page for a specific Segment (which you can change to
your liking after creating it), simply go to the Explorer section and find the
page you would like to personalize.
.. figure:: ../_static/images/variants_button.png
:alt: The variants button that appears on personalisable pages.
When you hover over a page, you'll notice a "Variants" dropdown button appears.
Click the button and select the segment you would like to create personalised
content for.
Once you've selected the segment, a copy of the original page will be created
with a title that includes the segment. Don't worry, your visitors won't be able
to see this title. It's only there for your reference.
.. figure:: ../_static/images/editing_variant.png
:alt: The newly created page allowing you to change anything you want.
You can change everything on this page you would like. Visitors that are appointed
to your segment will automatically see the new variant you've created for them
when attempting to visit the original page.
Method 2: Use a StreamField block
---------------------------------
Preparing a page and it's StreamField blocks for this method is described in the
Usage guide for developers. Please refer to
:ref:`implementing_streamfield_blocks` for more information.
**Why you would want to use this method**
* Allows you to create personalised content in the original page (without
creating a variant).
* Create multiple StreamField blocks for different segments inline.
**Why you would want to use a different method**
* You need someone tech savvy to change the back-end implementation.
To create personalised StreamField blocks, first select the page you wan't to
create the content for. Note that the personalisable StreamField blocks must be
activated on the page by your developer.
Scroll down to the block containing the StreamField and add a personalisable
block. The first input field in the block is a dropdown allowing you to select
the segment this StreamField block is ment for.
.. figure:: ../_static/images/single_streamfield.png
:alt: Create a new StreamField block and select the segment.
If you want, you can even add multiple blocks and change the segment to show
different content between segments!
.. figure:: ../_static/images/dual_streamfield.png
:alt: You can even create multiple variations of the same block!
Once saved, the page will selectively show StreamField blocks based on the
visitor's segment.
Method 3: Use a template block
------------------------------
Setting up content in this manner is described in the Usage guide for
developers. Please refer to :ref:`implementing_template_blocks` for more
information.

View File

@ -0,0 +1,84 @@
Creating a segment
==================
To create a segment, go to the "Segments dashboard" and click "Add segment".
You can find the segments dashboard in the administration menu on the left of
the page.
.. figure:: ../_static/images/segment_dashboard_header.png
:alt: The segment dashboard header containing the "Add segment" button.
On this page you will be presented with two forms. One with specific information
about your segment, the other allowing you to choose and configure your
rules.
Set segment specific options
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. figure:: ../_static/images/edit_segment_specifics.png
:alt: The form that allows you to configure specific segment options.
1. Enter a name for your segment
Choose something meaningful like "Newsletter campaign visitors". This will
ensure you'll have a general idea which visitors are in this segment in
other parts of the administration interface.
2. Select the status of the segment *Optional*
You will generally keep this one **enabled**. If for some reason you want
to disable the segment, you can change this to **disabled**.
3. Set the segment persistence. *Optional*
When persistence is **enabled**, your segment will stick to the visitor once
applied, even if the rules no longer match the next visit.
4. Select whether to match any or all defined rules. *Optional*
**Match any** will result in a segment that is applied as soon as one of
your rules matches the visitor. When **match all** is selected, all rules
must match before the segment is applied.
5. The segment type *Required*
**Dynamic**: Users in this segment will change as more or less meet the
rules specified in the segment.
**Static**: 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.
6. The segment count *Optional*
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.
7. Randomisation percentage *Optional*
If this number is set each user matching the rules will have this percentage
chance of being placed in the segment.
Defining rules
^^^^^^^^^^^^^^
.. figure:: ../_static/images/edit_segment_rules.png
:alt: The form that allows you to set the rules for a segment.
5. Choose the rules you want to use.
Wagxperience comes with a basic set of :doc:`../default_rules` that allow
you to get started quickly. The rules you define will be evaluated once a
visitor makes a request to your application.
The rules that come with Wagxperience are as follows:
.. toctree::
:maxdepth: 2
../default_rules
Click "save" to store your segment. It will be enabled by default, unless
otherwise defined.

View File

@ -0,0 +1,13 @@
Editor Guide
============
The editor guide is meant for content editors and marketers using Wagxperience
to offer a personalised experience to their visitors.
.. toctree::
:maxdepth: 2
introduction
segments_dashboard
creating_segments
creating_personalised_content

View File

@ -0,0 +1,22 @@
Introduction
============
Wagxperience_ is an open source module developed by `Lab Digital`_ for the
Wagtail_ content management system. It allows editors and marketeers to create
personalised experiences by harnessing the power of segmentation and rules.
.. _Wagxperience: http://wagxperience.io
.. _Wagtail: https://wagtail.io
.. _Lab Digital: http://labdigital.nl
In this guide, we'll take you step by step through the process of offering your
visitors a tailor made online experience. The subjects covered are:
* Using the segments dashboard
* Defining a new segment
* Setting up rules used to match visitors to a segment
* Personalize a page by creating a variant
* Using the StreamField to personalize content blocks
* And even more helpful stuff...
So without further ado, let's get started!

View File

@ -0,0 +1,84 @@
The segments dashboard
======================
Wagxperience comes with two different views for it's segment dashboard. A "list
view" and a "dashboard view". Where the dashboard view attempts to show all
relevant information and statistics in a visually pleasing manner, the list view
is more fitted for sites using large amounts of segments, as it may be
considered more clear in these cases.
Switching between views
-----------------------
By default, Wagxperience's "dashboard view" is active on the segment dashboard.
If you would like to switch between the dashboard view and list view, open the
segment dashboard and click the "Switch view" button in the green header at the
top of the page.
.. figure:: ../_static/images/segment_dashboard_header.png
:alt: The header containing the "Switch view" button.
Using the list view
-------------------
Advantages of using the list view:
* Uses the familiar table view that is used on many other parts of the Wagtail
administration interface.
* Offers a better overview for large amounts of segments.
* Allows for reordering based on fields, such as name or status.
.. figure:: ../_static/images/segment_list_view.png
:alt: The segment list view.
Definitions
^^^^^^^^^^^
Name
The name of your segment.
Persistent
If this is disabled (default), whenever a visitor requests a page, the rules
of this segment are reevaluated. This means that when the rules no longer
match, the visitor is no longer a part of this segment. However, if
persistence is enabled, this segment will "stick" with the visitor, even when
the rules no longer apply.
Match any
If this is disabled (default) all rules of this segment must match a visitor
before the visitor is appointed to this segment. If this is enabled, only 1
rule has to match before the visitor is appointed.
Status
Indicates whether this segment is active (default) or inactive. If it has
been set to 'inactive', visitors will not be appointed to this segment and no
personalised content for this segment will be shown to visitors.
Page count
The amount of pages that have variants using this segment.
Variant count
The total amount of variants for this segment. Does not yet apply, as this
will always match the amount of pages in the "Page count".
Statistics
Shows the amount of visits of this segment and the days it has been
enabled. If the segment is disabled and then re-enabled, these statistics
will reset.
Using the dashboard view
------------------------
Advantages of using the dashboard view:
* Offers a more pleasing visual representation of segments.
* Focused on giving insights about your segments at a glance.
* Shows the actual rules of a segment.
* Gives more wordy explanation about the information shown.
.. figure:: ../_static/images/segment_dashboard_view.png
:alt: The segment dashboard view.

View File

@ -0,0 +1,8 @@
Getting Started
===============
.. toctree::
:maxdepth: 3
installation
sandbox

View File

@ -0,0 +1,37 @@
Installing Wagxperience
=======================
Wagtail Personalisation requires Wagtail_ 2.0 or 2.1 and Django_ 1.11 or 2.0.
.. _Wagtail: https://github.com/wagtail/wagtail
.. _Django: https://github.com/django/django
To install the package with pip:
.. code-block:: console
pip install wagtail-personalisation
Next, include the ``wagtail_personalisation``, ``wagtail.contrib.modeladmin``
and ``wagtailfontawesome`` apps in your project's ``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = [
# ...
'wagtail.contrib.modeladmin',
'wagtail_personalisation',
'wagtailfontawesome',
# ...
]
Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has
been added in first, this is a prerequisite for this project.
.. code-block:: python
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
# ...
]

View File

@ -0,0 +1,14 @@
Using the sandbox
=================
To experiment with the package you can use the sandbox provided in
the repository_. It includes a couple of segments with rules, a personalisable
page with a variant and a personalisable StreamField block.
To install this you will need to create and activate a
virtualenv and then run ``make sandbox``. This will start a fresh Wagtail
install, with the personalisation module enabled, on http://localhost:8000
and http://localhost:8000/cms/. The superuser credentials are
``superuser@example.com`` with the password ``testing``.
.. _repository: https://github.com/LabD/wagtail-personalisation

51
docs/index.rst Normal file
View File

@ -0,0 +1,51 @@
.. wagtail-personalisation documentation master file, created by
sphinx-quickstart on Mon Dec 19 15:12:32 2016.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to the Wagxperience documentation
=========================================
.. image:: https://readthedocs.org/projects/wagtail-personalisation/badge/?version=latest
:target: http://wagtail-personalisation.readthedocs.io/en/latest/?badge=latest
.. image:: https://travis-ci.org/wagtail/wagtail-personalisation.svg?branch=master
:target: https://travis-ci.org/wagtail/wagtail-personalisation
.. image:: http://codecov.io/github/wagtail/wagtail-personalisation/coverage.svg?branch=master
:target: http://codecov.io/github/wagtail/wagtail-personalisation?branch=master
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
:target: https://pypi.python.org/pypi/wagtail-personalisation/
Wagxperience is a fully-featured personalisation module for Wagtail.
It enables editors to create customised pages - or parts of pages - based on
segments whose rules are configured directly in the admin interface.
* **Get up and running**
* :doc:`getting_started/index`
* **For developers**
* :doc:`usage_guide/index`
* **For editors & marketeers**
* :doc:`editor_guide/index`
Index
-----
.. toctree::
:maxdepth: 2
getting_started/index
usage_guide/index
editor_guide/index
default_rules

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

36
docs/make.bat Normal file
View File

@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=wagtail-personalisation
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@ -0,0 +1,17 @@
Creating custom rules
=====================
Rules consist of two important elements, the model fields and the
``test_user`` function. They should inherit the ``AbstractBaseRule`` class from
``wagtail_personalisation.rules``.
A simple example of a rule could look something like this:
.. literalinclude:: ../../src/wagtail_personalisation/rules.py
:pyobject: UserIsLoggedInRule
As you can see, the only real requirement is the ``test_user`` function that
will either return ``True`` or ``False`` based on the model fields and
optionally the request object.
That's it!

View File

@ -0,0 +1,71 @@
Implementation
==============
Extending a page to be personalisable
-------------------------------------
Wagxperience offers a ``PersonalisablePage`` base class to extend from.
This is a standard ``Page`` class with personalisation options added.
Creating a new personalisable page
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Import and extend the ``personalisation.models.PersonalisablePage`` class to
create a personalisable page.
A very simple example for a personalisable homepage:
.. code-block:: python
from wagtail.wagtailcore.models import Page
from wagtail_personalisation.models import PersonalisablePageMixin
class HomePage(PersonalisablePageMixin, Page):
pass
All you need is the ``PersonalisablePageMixin`` mixin and a Wagtail ``Page``
class of your liking.
.. _implementing_streamfield_blocks:
Adding personalisable StreamField blocks
----------------------------------------
Taking things a step further, you can also add personalisable StreamField blocks
to your page models. Below is the full Homepage model used in the sandbox.
.. literalinclude:: ../../sandbox/sandbox/apps/home/models.py
.. _implementing_template_blocks:
Using template blocks for personalisation
-----------------------------------------
*Please note that using the personalisable template tag is not the recommended
method for adding personalisation to your content, as it is largely decoupled
from the administration interface. Use responsibly.*
You can add a template block that only shows its contents to users of a
specific segment. This is done using the "segment" block.
When editing templates make sure to load the ``wagtail_personalisation_tags``
tags library in the template:
.. code-block:: jinja
{% load wagtail_personalisation_tags %}
After that you can add a template block with the name of the segment you want
the content to show up for:
.. code-block:: jinja
{% segment name="My Segment" %}
<p>Only users within "My Segment" see this!</p>
{% endsegment %}
The template block currently only supports one segment at a time. If you want
to target multiple segments you will have to make multiple blocks with the
same content.

View File

@ -0,0 +1,8 @@
Usage Guide
===========
.. toctree::
:maxdepth: 3
implementation
custom_rules

1
frontend/js/dashboard.js Normal file
View File

@ -0,0 +1 @@
import '../scss/dashboard.scss';

1
frontend/js/form.js Normal file
View File

@ -0,0 +1 @@
import '../scss/form.scss';

1
frontend/js/index.js Normal file
View File

@ -0,0 +1 @@
import '../scss/index.scss';

View File

@ -25,6 +25,11 @@
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;
@ -81,11 +86,16 @@
padding: 0;
margin: 0;
list-style: none;
}
.block_container .block .inspect_container .inspect li {
.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 {
@ -96,35 +106,6 @@
overflow-wrap: break-word;
}
.block_container .block .inspect_container .inspect li span::before {
display: inline-block;
content: "";
width: 16px;
height: 16px;
margin-right: 5px;
background-size: contain;
}
.block_container .block .inspect_container .segment_stats .visit_stat span::before {
background-image: url("./rocket_icon.png");
}
.block_container .block .inspect_container .segment_stats .days_stat span::before {
background-image: url("./calendar_icon.png");
}
.block_container .block .inspect_container .segment_rules .persistent_state span::before {
background-image: url("./persistent_icon.png");
}
.block_container .block .inspect_container .segment_rules .persistent_state.fleeting span::before {
transform: rotate(45deg) translateY(-2px);
}
.block_container .block .inspect_container .segment_rules .time_rule span::before {
background-image: url("./time_icon.png");
}
.block_container .block .inspect_container .segment_rules .visit_count_rule span::before {
background-image: url("./visit_count_icon.png");
}
.block_container .block .inspect_container .inspect li pre {
position: relative;
box-sizing: border-box;
@ -138,25 +119,6 @@
border-radius: 3px;
}
.block_container .block .inspect_container .inspect li pre::before {
display: inline-block;
position: absolute;
content: "";
left: -21px;
top: 6px;
width: 16px;
height: 16px;
margin-right: 5px;
background-size: contain;
}
.block_container .block .inspect_container .segment_rules .referral_rule pre::before {
background-image: url("./referral_icon.png");
}
.block_container .block .inspect_container .segment_rules .query_rule pre::before {
background-image: url("./referral_icon.png");
}
.block_container .block.suggestion .suggestive_text {
display: block;
position: absolute;

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 103 KiB

BIN
logo_bw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.sandbox.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"devDependencies": {
"autoprefixer": "^7.1.1",
"babel-core": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.2",
"eslint": "^3.19.0",
"eslint-config-airbnb": "^15.0.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^5.0.3",
"eslint-plugin-react": "^7.0.1",
"extract-text-webpack-plugin": "^2.1.0",
"file-loader": "^0.11.1",
"imagemin-webpack-plugin": "^1.4.4",
"jshint": "^2.9.4",
"mocha": "^3.4.1",
"node-sass": "^4.5.3",
"postcss-loader": "^2.0.5",
"sass-loader": "^6.0.5",
"style-loader": "^0.18.0",
"uglify-js": "^3.0.10",
"uglifyjs-webpack-plugin": "^0.4.3",
"webpack": "^2.6.0"
},
"name": "wagtail-personalisation",
"description": "Wagxperience personalisation module for Wagtail.",
"version": "1.0.0",
"main": "webpack.config.js",
"directories": {
"doc": "docs",
"test": "tests"
},
"scripts": {
"start": "yarn compile && yarn watch",
"compile": "webpack --bail",
"watch": "webpack --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wagtail/wagtail-personalisation.git"
},
"author": "Lab Digital",
"license": "ISC",
"bugs": {
"url": "https://github.com/wagtail/wagtail-personalisation/issues"
},
"homepage": "https://github.com/wagtail/wagtail-personalisation#readme"
}

View File

@ -0,0 +1,148 @@
[{
"model": "wagtail_personalisation.timerule",
"pk": 1,
"fields": {
"segment": 2,
"start_time": "06:00:00",
"end_time": "11:00:00"
}
}, {
"model": "wagtail_personalisation.visitcountrule",
"pk": 1,
"fields": {
"segment": 1,
"operator": "more_than",
"count": 3,
"counted_page": 3
}
}, {
"model": "wagtail_personalisation.segment",
"pk": 1,
"fields": {
"name": "Returning Rook",
"create_date": "2017-06-02T05:38:02.304Z",
"edit_date": "2017-06-02T10:58:39.399Z",
"enable_date": "2017-06-02T10:58:39.389Z",
"disable_date": "2017-06-02T10:34:51.722Z",
"visit_count": 0,
"status": "enabled",
"persistent": false,
"match_any": false
}
}, {
"model": "wagtail_personalisation.segment",
"pk": 2,
"fields": {
"name": "Early Birds",
"create_date": "2017-06-02T05:38:14.749Z",
"edit_date": "2017-06-02T10:57:44.504Z",
"enable_date": "2017-06-02T10:57:44.497Z",
"disable_date": "2017-06-02T10:57:39.984Z",
"visit_count": 1,
"status": "enabled",
"persistent": false,
"match_any": false
}
}, {
"model": "wagtail_personalisation.personalisablepagemetadata",
"pk": 1,
"fields": {
"canonical_page": 3,
"variant": 3,
"segment": null
}
}, {
"model": "wagtail_personalisation.personalisablepagemetadata",
"pk": 2,
"fields": {
"canonical_page": 3,
"variant": 4,
"segment": 1
}
}, {
"model": "home.homepage",
"pk": 3,
"fields": {
"intro": "<p>Thank you for trying <a href=\"http://wagxperience.io\">Wagxperience</a>!</p>",
"body": "[{\"type\": \"personalisable_paragraph\", \"value\": {\"segment\": \"2\", \"paragraph\": \"<p>You are an early bird!</p>\"}}]"
}
}, {
"model": "home.homepage",
"pk": 4,
"fields": {
"intro": "<p>Thank you for trying <a href=\"http://wagxperience.io\">Wagxperience</a>!</p><p>You've visited the homepage more than 3 times!</p>",
"body": "[]"
}
}, {
"model": "wagtailcore.page",
"pk": 1,
"fields": {
"path": "0001",
"depth": 1,
"numchild": 1,
"title": "Root",
"slug": "root",
"content_type": 1,
"live": true,
"has_unpublished_changes": false,
"url_path": "/",
"owner": null,
"seo_title": "",
"show_in_menus": false,
"search_description": "",
"go_live_at": null,
"expire_at": null,
"expired": false,
"locked": false,
"first_published_at": null,
"latest_revision_created_at": null
}
}, {
"model": "wagtailcore.page",
"pk": 3,
"fields": {
"path": "00010001",
"depth": 2,
"numchild": 0,
"title": "Home",
"slug": "home",
"content_type": 2,
"live": true,
"has_unpublished_changes": false,
"url_path": "/home/",
"owner": null,
"seo_title": "",
"show_in_menus": false,
"search_description": "",
"go_live_at": null,
"expire_at": null,
"expired": false,
"locked": false,
"first_published_at": "2017-06-02T10:35:34.706Z",
"latest_revision_created_at": "2017-06-02T10:35:34.565Z"
}
}, {
"model": "wagtailcore.page",
"pk": 4,
"fields": {
"path": "00010002",
"depth": 2,
"numchild": 0,
"title": "Home (Returning Rook)",
"slug": "home-returning-rook",
"content_type": 2,
"live": true,
"has_unpublished_changes": false,
"url_path": "/home-returning-rook/",
"owner": null,
"seo_title": "",
"show_in_menus": false,
"search_description": "",
"go_live_at": null,
"expire_at": null,
"expired": false,
"locked": false,
"first_published_at": "2017-06-02T05:38:53.568Z",
"latest_revision_created_at": "2017-06-02T05:38:53.390Z"
}
}]

View File

@ -0,0 +1,19 @@
[
{
"model": "user.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$36000$jW4Pr4OWkdVf$1KeQmcYPL1/qZvRX9ECQvoYuXTRbs+tlV480K2AqFUM=",
"last_login": "2017-05-08T07:38:49.391Z",
"is_superuser": true,
"first_name": "S.",
"last_name": "Uper",
"email": "superuser@example.com",
"is_staff": true,
"is_active": true,
"date_joined": "2015-10-17T11:32:57.969Z",
"groups": [],
"user_permissions": []
}
}
]

12
sandbox/manage.py Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env python
from __future__ import absolute_import, unicode_literals
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

4
sandbox/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
Django>=2.2,<2.3
wagtail>=2.6,<2.7
django-debug-toolbar==2.0
-e .[docs,test]

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 16:59
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0033_remove_golive_expiry_help_text'),
('wagtail_personalisation', '0011_personalisablepagemetadata'),
]
operations = [
migrations.CreateModel(
name='HomePage',
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')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page', models.Model),
),
]

View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def create_homepage(apps, schema_editor):
# Get models
ContentType = apps.get_model('contenttypes.ContentType')
Page = apps.get_model('wagtailcore.Page')
Site = apps.get_model('wagtailcore.Site')
HomePage = apps.get_model('home.HomePage')
# Delete the default homepage
# If migration is run multiple times, it may have already been deleted
Page.objects.filter(id=2).delete()
# Create content type for homepage model
homepage_content_type, __ = ContentType.objects.get_or_create(
model='homepage', app_label='home')
# Create a new homepage
homepage = HomePage.objects.create(
title="Home",
slug='home',
content_type=homepage_content_type,
path='00010001',
depth=2,
numchild=0,
url_path='/home/',
)
# Create a site with the new homepage set as the root
Site.objects.create(
hostname='localhost', root_page=homepage, is_default_site=True)
def remove_homepage(apps, schema_editor):
# Get models
ContentType = apps.get_model('contenttypes.ContentType')
HomePage = apps.get_model('home.HomePage')
# Delete the default homepage
# Page and Site objects CASCADE
HomePage.objects.filter(slug='home', depth=2).delete()
# Delete content type for homepage model
ContentType.objects.filter(model='homepage', app_label='home').delete()
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.RunPython(create_homepage, remove_homepage),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 19:36
from __future__ import unicode_literals
from django.db import migrations
import wagtail.core.fields
import wagtail_personalisation
class Migration(migrations.Migration):
dependencies = [
('home', '0002_create_homepage'),
]
operations = [
migrations.AddField(
model_name='homepage',
name='intro',
field=wagtail.core.fields.RichTextField(
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
preserve_default=False,
),
migrations.AddField(
model_name='homepage',
name='body',
field=wagtail.core.fields.StreamField((('personalisable_paragraph', wagtail.core.blocks.StructBlock((('segment', wagtail.core.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)), ('paragraph', wagtail.core.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
preserve_default=False,
),
]

View File

@ -0,0 +1,23 @@
from __future__ import absolute_import, unicode_literals
from wagtail.admin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
from wagtail.core import blocks
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Page
from wagtail_personalisation.models import PersonalisablePageMixin
from wagtail_personalisation.blocks import PersonalisedStructBlock
class HomePage(PersonalisablePageMixin, Page):
intro = RichTextField()
body = StreamField([
('personalisable_paragraph', PersonalisedStructBlock([
('paragraph', blocks.RichTextBlock()),
], icon='pilcrow'))
])
content_panels = Page.content_panels + [
RichTextFieldPanel('intro'),
StreamFieldPanel('body'),
]

View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-homepage{% endblock %}
{% block content %}
<h1>Wagtail Personalisation</h1>
<hr>
<h2>{{ self.title }}</h2>
{{ self.intro|richtext }}
{% for block in page.body %}
<div>{% include_block block %}</div>
{% endfor %}
{% endblock %}

View File

View File

@ -0,0 +1,38 @@
{% extends "base.html" %}
{% load static wagtailcore_tags %}
{% block body_class %}template-searchresults{% endblock %}
{% block title %}Search{% endblock %}
{% block content %}
<h1>Search</h1>
<form action="{% url 'search' %}" method="get">
<input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
<input type="submit" value="Search" class="button">
</form>
{% if search_results %}
<ul>
{% for result in search_results %}
<li>
<h4><a href="{% pageurl result %}">{{ result }}</a></h4>
{% if result.search_description %}
{{ result.search_description }}
{% endif %}
</li>
{% endfor %}
</ul>
{% if search_results.has_previous %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.previous_page_number }}">Previous</a>
{% endif %}
{% if search_results.has_next %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.next_page_number }}">Next</a>
{% endif %}
{% elif search_query %}
No results found
{% endif %}
{% endblock %}

View File

@ -0,0 +1,36 @@
from __future__ import absolute_import, unicode_literals
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import render
from wagtail.core.models import Page
from wagtail.search.models import Query
def search(request):
search_query = request.GET.get('query', None)
page = request.GET.get('page', 1)
# Search
if search_query:
search_results = Page.objects.live().search(search_query)
query = Query.get(search_query)
# Record hit
query.add_hit()
else:
search_results = Page.objects.none()
# Pagination
paginator = Paginator(search_results, 10)
try:
search_results = paginator.page(page)
except PageNotAnInteger:
search_results = paginator.page(1)
except EmptyPage:
search_results = paginator.page(paginator.num_pages)
return render(request, 'search/search.html', {
'search_query': search_query,
'search_results': search_results,
})

View File

View File

@ -0,0 +1,42 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from sandbox.apps.user import forms, models
@admin.register(models.User)
class UserAdmin(BaseUserAdmin):
form = forms.UserChangeForm
add_form = forms.UserCreationForm
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ['email']
list_filter = ['is_superuser']
fieldsets = (
(None, {
'fields': ['email', 'password']
}),
('Personal info', {
'fields': ['first_name', 'last_name']
}),
('Permissions', {
'fields': [
'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions'
]
}),
)
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ['email', 'password1', 'password2']
}),
)
search_fields = ['first_name', 'last_name', 'email']
ordering = ['email']
filter_horizontal = []

View File

@ -0,0 +1,63 @@
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.translation import ugettext_lazy as _
from sandbox.apps.user import models
class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password.
"""
password1 = forms.CharField(
label='Password', widget=forms.PasswordInput,
required=False)
password2 = forms.CharField(
label='Password confirmation', widget=forms.PasswordInput,
required=False)
class Meta:
model = models.User
fields = ['email']
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data['password1'])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
"""
password = ReadOnlyPasswordHashField(
label=_("Password"),
help_text=_("Raw passwords are not stored, so there is no way to see "
"this user's password, but you can change the password "
"using <a href=\"password/\">this form</a>."))
class Meta:
model = models.User
fields = [
'email', 'password', 'is_active', 'is_superuser'
]
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial['password']

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 12:22
from __future__ import unicode_literals
import django.contrib.auth.models
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=100, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=100, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.1.7 on 2019-03-15 12:54
from django.db import migrations
import sandbox.apps.user.models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
('objects', sandbox.apps.user.models.UserManager()),
],
),
]

View File

@ -0,0 +1,81 @@
from django.contrib.auth.models import (
AbstractBaseUser, PermissionsMixin, BaseUserManager)
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not email:
raise ValueError('The given email address must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
"""Customized version of the default `AbstractUser` from Django.
"""
first_name = models.CharField(_('first name'), max_length=100, blank=True)
last_name = models.CharField(_('last name'), max_length=100, blank=True)
email = models.EmailField(_('email address'), blank=True, unique=True)
is_staff = models.BooleanField(
_('staff status'), default=False,
help_text=_('Designates whether the user can log into this admin '
'site.'))
is_active = models.BooleanField(
_('active'), default=True,
help_text=_('Designates whether this user should be treated as '
'active. Unselect this instead of deleting accounts.'))
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def get_full_name(self):
"""
Returns the first_name plus the last_name, with a space in between.
"""
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"Returns the short name for the user."
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email], **kwargs)

168
sandbox/sandbox/settings.py Normal file
View File

@ -0,0 +1,168 @@
"""
Django settings for sandbox project.
Generated by 'django-admin startproject' using Django 1.11.1.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
from __future__ import absolute_import, unicode_literals
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from importlib.util import find_spec
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(PROJECT_DIR)
DEBUG = True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
SITE_ID = 1
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail.core',
'wagtail.contrib.modeladmin',
'wagtailfontawesome',
'modelcluster',
'taggit',
'debug_toolbar',
'wagtail_personalisation',
'sandbox.apps.home',
'sandbox.apps.search',
'sandbox.apps.user',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]
if find_spec('wagtail.contrib.legacy'):
MIDDLEWARE += ('wagtail.contrib.legacy.sitemiddleware.SiteMiddleware',)
else:
MIDDLEWARE += ('wagtail.core.middleware.SiteMiddleware', )
ROOT_URLCONF = 'sandbox.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(PROJECT_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'sandbox.wsgi.application'
AUTH_USER_MODEL = 'user.User'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# Wagtail settings
WAGTAIL_SITE_NAME = "sandbox"
# Base URL to use when referring to full URLs within the Wagtail admin backend -
# e.g. in notification emails. Don't include '/admin' or a trailing slash
BASE_URL = 'http://example.com'
INTERNAL_IPS = ['127.0.0.1']

View File

View File

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block body_class %}template-404{% endblock %}
{% block content %}
<h1>Page not found</h1>
<h2>Sorry, this page could not be found.</h2>
{% endblock %}

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Internal server error</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h1>Internal server error</h1>
<h2>Sorry, there seems to be an error. Please try again soon.</h2>
</body>
</html>

View File

@ -0,0 +1,44 @@
{% load static wagtailuserbar %}
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
{% block title %}
{% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }}{% endif %}
{% endblock %}
{% block title_suffix %}
{% with self.get_site.site_name as site_name %}
{% if site_name %}- {{ site_name }}{% endif %}
{% endwith %}
{% endblock %}
</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{# Global stylesheets #}
<link rel="stylesheet" type="text/css" href="{% static 'css/sandbox.css' %}">
{% block extra_css %}
{# Override this in templates to add extra stylesheets #}
{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
{% wagtailuserbar %}
{% block content %}{% endblock %}
{# Global javascript #}
<script type="text/javascript" src="{% static 'js/sandbox.js' %}"></script>
{% block extra_js %}
{# Override this in templates to add extra javascript #}
{% endblock %}
</body>
</html>

42
sandbox/sandbox/urls.py Normal file
View File

@ -0,0 +1,42 @@
from __future__ import absolute_import, unicode_literals
import debug_toolbar
from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from sandbox.apps.search import views as search_views
urlpatterns = [
url(r'^django-admin/', admin.site.urls),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
url(r'^search/$', search_views.search, name='search'),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's page serving mechanism. This should be the last pattern in
# the list:
url(r'', include(wagtail_urls)),
# Alternatively, if you want Wagtail pages to be served from a subpath
# of your site, rather than the site root:
# url(r'^pages/', include(wagtail_urls)),
]
if settings.DEBUG:
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
# Serve static and media files from development server
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

18
sandbox/sandbox/wsgi.py Normal file
View File

@ -0,0 +1,18 @@
"""
WSGI config for sandbox project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""
from __future__ import absolute_import, unicode_literals
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings")
application = get_wsgi_application()

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,8 +1,30 @@
[bumpversion]
current_version = 0.15.2
commit = true
tag = true
tag_name = {new_version}
[tool:pytest]
DJANGO_SETTINGS_MODULE = tests.sandbox.settings
norecursedirs = .tox .git
DJANGO_SETTINGS_MODULE = tests.settings
minversion = 3.0
strict = true
django_find_project = false
testpaths = tests
python_paths = .
[flake8]
ignore=E731
exclude=
src/**/migrations/*.py
ignore = E731
max-line-length = 120
exclude =
src/**/migrations/*.py
[wheel]
universal = 1
[coverage]
include = src/**/
omit = src/**/migrations/*.py
[bumpversion:file:setup.py]
[bumpversion:file:docs/conf.py]

View File

@ -1,50 +1,66 @@
import re
from setuptools import find_packages, setup
install_requires = [
'django-polymorphic==1.0.2',
'wagtail>=1.7',
'wagtail>=2.0',
'user-agents>=1.1.0',
'wagtailfontawesome>=1.1.3',
'pycountry',
]
tests_require = [
'pytest==3.0.4',
'pytest-cov==2.4.0',
'pytest-django==3.0.0',
'pytest-sugar==0.7.1',
'factory_boy==2.8.1',
'flake8-blind-except',
'flake8-debugger',
'flake8-isort',
'flake8',
'freezegun==0.3.8',
'factory_boy==2.7.0',
'pytest-cov==2.5.1',
'pytest-django==4.1.0',
'pytest-pythonpath==0.7.2',
'pytest-sugar==0.9.1',
'pytest==6.1.2',
'wagtail_factories==1.1.0',
'pytest-mock==1.6.3',
]
docs_require = [
'sphinx>=1.7.6',
'sphinx_rtd_theme>=0.4.0',
]
with open('README.rst') as fh:
long_description = re.sub(
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
setup(
name='wagtail-personalisation',
version='0.1.0',
version='0.15.2',
description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV',
author_email='b.besemer@labdigital.nl',
url='http://labdigital.nl',
author='Lab Digital BV and others',
author_email='opensource@labdigital.nl',
url='https://labdigital.nl/',
install_requires=install_requires,
tests_require=tests_require,
extras_require={
'docs': docs_require,
'test': tests_require,
},
packages=find_packages('src'),
package_dir={'': 'src'},
include_package_data=True,
license='BSD',
long_description=open('README.rst').read(),
license='MIT',
long_description=long_description,
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 2.0',
'Topic :: Internet :: WWW/HTTP :: Site Management',
],
)

View File

@ -1,34 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import admin
from personalisation import models
class TimeRuleAdminInline(admin.TabularInline):
"""Inline the Time Rule into the administration interface for segments"""
model = models.TimeRule
extra = 0
class ReferralRuleAdminInline(admin.TabularInline):
"""Inline the Referral Rule into the
administration interface for segments"""
model = models.ReferralRule
extra = 0
class VisitCountRuleAdminInline(admin.TabularInline):
"""Inline the Visit Count Rule into the
administration interface for segments"""
model = models.VisitCountRule
extra = 0
class SegmentAdmin(admin.ModelAdmin):
"""Add the inlines to the Segment admin interface"""
inlines = (TimeRuleAdminInline,
ReferralRuleAdminInline, VisitCountRuleAdminInline)
admin.site.register(models.Segment, SegmentAdmin)

View File

@ -1,15 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from personalisation import views
app_name = 'segment'
urlpatterns = [
url(r'^segment/(?P<segment_id>[0-9]+)/enable/$', views.enable,
name='enable'),
url(r'^segment/(?P<segment_id>[0-9]+)/disable/$', views.disable,
name='disable'),
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
views.copy_page_view, name='copy_page')
]

View File

@ -1,333 +0,0 @@
from __future__ import absolute_import, unicode_literals
import re
from datetime import datetime
from django.db import models
from django.db.models.signals import pre_save
from django.template.defaultfilters import slugify
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail.utils.decorators import cached_classmethod
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, ObjectList,
PageChooserPanel, TabbedInterface)
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
from wagtail.wagtailcore.models import Page
@python_2_unicode_compatible
class AbstractBaseRule(models.Model):
"""Base for creating rules to segment users with"""
segment = ParentalKey(
'personalisation.Segment',
related_name="%(app_label)s_%(class)s_related",
related_query_name="%(app_label)s_%(class)ss"
)
def test_user(self):
"""Test if the user matches this rule"""
return True
def __str__(self):
return "Abstract segmentation rule"
class Meta:
abstract = True
@python_2_unicode_compatible
class TimeRule(AbstractBaseRule):
"""Time rule to segment users based on a start and end time"""
start_time = models.TimeField(_("Starting time"))
end_time = models.TimeField(_("Ending time"))
panels = [
FieldRowPanel([
FieldPanel('start_time'),
FieldPanel('end_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
def __str__(self):
return 'Time Rule'
@python_2_unicode_compatible
class ReferralRule(AbstractBaseRule):
"""Referral rule to segment users based on a regex test"""
regex_string = models.TextField(
_("Regex string to match the referer with"))
panels = [
FieldPanel('regex_string'),
]
def __init__(self, *args, **kwargs):
super(ReferralRule, self).__init__(*args, **kwargs)
def test_user(self, request):
pattern = re.compile(self.regex_string)
if 'HTTP_REFERER' in request.META:
referer = request.META['HTTP_REFERER']
if pattern.search(referer):
return True
return False
def __str__(self):
return 'Referral Rule'
@python_2_unicode_compatible
class VisitCountRule(AbstractBaseRule):
"""Visit count rule to segment users based on amount of visits"""
OPERATOR_CHOICES = (
('more_than', _("More than")),
('less_than', _("Less than")),
('equal_to', _("Equal to")),
)
operator = models.CharField(max_length=20,
choices=OPERATOR_CHOICES, default="more_than")
count = models.PositiveSmallIntegerField(default=0, null=True)
counted_page = models.ForeignKey(
'wagtailcore.Page',
null=False,
blank=False,
on_delete=models.CASCADE,
related_name='+',
)
panels = [
PageChooserPanel('counted_page'),
FieldRowPanel([
FieldPanel('operator'),
FieldPanel('count'),
]),
]
def __init__(self, *args, **kwargs):
super(VisitCountRule, self).__init__(*args, **kwargs)
def test_user(self, request):
operator = self.operator
segment_count = self.count
def get_visit_count(request):
"""Search through the sessions to get the page visit count
corresponding to the request."""
for page in request.session['visit_count']:
if page['path'] == request.path:
return page['count']
visit_count = get_visit_count(request)
if visit_count and operator == "more_than":
if visit_count > segment_count:
return True
elif visit_count and operator == "less_than":
if visit_count < segment_count:
return True
elif visit_count and operator == "equal_to":
if visit_count == segment_count:
return True
return False
def __str__(self):
return 'Visit count Rule'
@python_2_unicode_compatible
class QueryRule(AbstractBaseRule):
"""Query rule to segment users based on matching queries"""
parameter = models.SlugField(_("The query parameter to search for"),
max_length=20)
value = models.SlugField(_("The value of the parameter to match"),
max_length=20)
panels = [
FieldPanel('parameter'),
FieldPanel('value'),
]
def __init__(self, *args, **kwargs):
super(QueryRule, self).__init__(*args, **kwargs)
def test_user(self, request):
parameter = self.parameter
value = self.value
req_value = request.GET.get(parameter, '')
if req_value == value:
return True
return False
def __str__(self):
return 'Query Rule'
@python_2_unicode_compatible
class Segment(ClusterableModel):
"""Model for a new segment"""
name = models.CharField(max_length=255)
create_date = models.DateTimeField(auto_now_add=True)
edit_date = models.DateTimeField(auto_now=True)
enable_date = models.DateTimeField(null=True, editable=False)
disable_date = models.DateTimeField(null=True, editable=False)
visit_count = models.PositiveIntegerField(default=0, editable=False)
STATUS_CHOICES = (
('enabled', 'Enabled'),
('disabled', 'Disabled'),
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES,
default="enabled")
persistent = models.BooleanField(
default=False, help_text=_("Should the segment persist between visits?"))
panels = [
MultiFieldPanel([
FieldPanel('name', classname="title"),
FieldRowPanel([
FieldPanel('status'),
FieldPanel('persistent'),
]),
], heading="Segment"),
MultiFieldPanel([
InlinePanel(
"{}_related".format(rule._meta.db_table),
label=rule.__str__,
min_num=0,
max_num=1,
) for rule in AbstractBaseRule.__subclasses__()
], heading="Rules"),
]
def __str__(self):
return self.name
def encoded_name(self):
"""Returns a string with a slug for the segment"""
return slugify(self.name.lower())
def check_status_change(sender, instance, *args, **kwargs):
"""Check if the status has changed. Alter dates accordingly."""
try:
original_status = sender.objects.get(pk=instance.id).status
except sender.DoesNotExist:
original_status = ""
if original_status != instance.status:
if instance.status == "enabled":
instance.enable_date = timezone.now()
instance.visit_count = 0
return instance
if instance.status == "disabled":
instance.disable_date = timezone.now()
pre_save.connect(check_status_change, sender=Segment)
class AdminPersonalisablePageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super(AdminPersonalisablePageForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
page = super(AdminPersonalisablePageForm, self).save(commit=False)
if page.segment:
segment = page.segment
slug = "{}-{}".format(page.slug, segment.encoded_name())
title = "{} ({})".format(page.title, segment.name)
update_attrs = {
'title': title,
'slug': slug,
'segment': segment,
'live': False,
'canonical_page': page,
'is_segmented': True,
}
if page.is_segmented:
slug = "{}-{}".format(
page.canonical_page.slug, segment.encoded_name())
title = "{} ({})".format(
page.canonical_page.title, segment.name)
page.slug = slug
page.title = title
page.save()
return page
else:
new_page = page.copy(
update_attrs=update_attrs, copy_revisions=False)
return new_page
return page
class PersonalisablePage(Page):
canonical_page = models.ForeignKey(
'self', related_name='variations', on_delete=models.SET_NULL,
blank=True, null=True
)
segment = models.ForeignKey(
Segment, related_name='segments', on_delete=models.PROTECT,
blank=True, null=True
)
is_segmented = models.BooleanField(default=False)
variation_panels = [
MultiFieldPanel([
FieldPanel('segment'),
PageChooserPanel('canonical_page', page_type=None),
])
]
base_form_class = AdminPersonalisablePageForm
def __str__(self):
return "{}".format(self.title)
@cached_property
def has_variations(self):
return self.variations.exists()
@cached_property
def is_canonical(self):
return not self.canonical_page and self.has_variations
@cached_classmethod
def get_edit_handler(cls):
tabs = []
if cls.content_panels:
tabs.append(ObjectList(cls.content_panels, heading=_("Content")))
if cls.variation_panels:
tabs.append(ObjectList(cls.variation_panels, heading=_("Variations")))
if cls.promote_panels:
tabs.append(ObjectList(cls.promote_panels, heading=_("Promote")))
if cls.settings_panels:
tabs.append(ObjectList(cls.settings_panels, heading=_("Settings"),
classname='settings'))
edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
return edit_handler.bind_to_model(cls)
PersonalisablePage.get_edit_handler = get_edit_handler

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,117 +0,0 @@
{% extends "modeladmin/index.html" %}
{% load i18n l10n staticfiles modeladmin_tags personalisation_filters %}
{% block content_main %}
<div>
<div class="row">
{% block content_cols %}
{% 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" onclick="location.href = 'edit/{{ segment.pk }}'">
<h2>{{ segment }}</h2>
<div class="inspect_container">
<ul class="inspect segment_stats">
<li class="visit_stat">
{% trans "This segmented has been visited" %}
<span>{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
</li>
<li class="days_stat">
{% trans "This segment has been active for" %}
<span>{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
</li>
</ul>
<ul class="inspect segment_rules">
{% for rule in segment.personalisation_timerule_related.all %}
<li class="time_rule">
These users visit between
<span>{{ rule.start_time }} and {{ rule.end_time }}</span>
</li>
{% endfor %}
{% for rule in segment.personalisation_referralrule_related.all %}
<li class="referral_rule">
These visits originate from
<pre>{{ rule.regex_string }}</pre>
</li>
{% endfor %}
{% for rule in segment.personalisation_visitcountrule_related.all %}
<li class="visit_count_rule">
These users visited {{ rule.counted_page }}
<span>{{ rule.get_operator_display }} {{ rule.count }} times</span>
</li>
{% endfor %}
{% for rule in segment.personalisation_queryrule_related.all %}
<li class="query_rule">
These users used a url with the query
<pre>?{{ rule.parameter }}={{ rule.value }}</pre>
</li>
{% endfor %}
<li class="persistent_state {{ segment.persistent|yesno:"persistent,fleeting" }}">
{% trans "The persistence of this segment is" %}
{% if segment.persistent %}
<span title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
{% else %}
<span title="{% trans "This segment is reevaluated on every visit" %}">{% trans "Fleeting" %}</span>
{% endif %}
</li>
</ul>
</div>
{% if user_can_create %}
<ul class="block_actions">
{% if segment.status == "disabled" %}
<li><a href="{% url 'segment:enable' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
{% elif segment.status == "enabled" %}
<li><a href="{% url 'segment:disable' 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>
{% endblock %}
</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 %}
</ul>
</div>
{% endif %}
{% endblock %}
{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
from django.template import Library
from django.utils import timezone
register = Library()
@register.filter(name='days_since')
def active_days(enable_date, disable_date):
"""Returns the number of days the segment has been active"""
if enable_date is not None:
if disable_date is None or disable_date <= enable_date:
# There is no disable date, or it is not relevant.
delta = timezone.now() - enable_date
return delta.days
if disable_date > enable_date:
# There is a disable date and it is relevant.
delta = disable_date - enable_date
return delta.days
return 0

View File

@ -1,5 +0,0 @@
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

View File

@ -1,46 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, reverse
from personalisation.models import PersonalisablePage, Segment
def enable(request, segment_id):
"""Enable the selected segment"""
segment = get_object_or_404(Segment, pk=segment_id)
segment.status = 'enabled'
segment.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
def disable(request, segment_id):
"""Disable the selected segment"""
segment = get_object_or_404(Segment, pk=segment_id)
segment.status = 'disabled'
segment.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
def copy_page_view(request, page_id, segment_id):
"""Copy page with selected segment"""
segment = get_object_or_404(Segment, pk=segment_id)
page = get_object_or_404(PersonalisablePage, pk=page_id)
slug = "{}-{}".format(page.slug, segment.encoded_name())
title = "{} ({})".format(page.title, segment.name)
update_attrs = {
'title': title,
'slug': slug,
'segment': segment,
'live': False,
'canonical_page': page,
'is_segmented': True,
}
new_page = page.copy(update_attrs=update_attrs, copy_revisions=False)
edit_url = reverse('wagtailadmin_pages:edit', args=[new_page.id])
return HttpResponseRedirect(edit_url)

View File

@ -1,228 +0,0 @@
from __future__ import absolute_import, unicode_literals
import logging
import time
from django.conf.urls import include, url
from django.shortcuts import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.wagtailadmin.widgets import (
Button, ButtonWithDropdownFromHook)
from wagtail.wagtailcore import hooks
from personalisation import admin_urls
from personalisation.models import (AbstractBaseRule, PersonalisablePage,
Segment)
from personalisation.utils import impersonate_other_page
logger = logging.getLogger()
@hooks.register('register_admin_urls')
def register_admin_urls():
"""Adds the administration urls for the personalisation apps."""
return [
url(r'^personalisation/', include(
admin_urls,
app_name='personalisation',
namespace='personalisation')),
]
class SegmentModelAdmin(ModelAdmin):
"""The base model for the Segments administration interface."""
model = Segment
menu_icon = 'group'
add_to_settings_menu = False
list_display = ('status', 'name', 'create_date', 'edit_date')
index_view_extra_css = ['personalisation/segment/index.css']
form_view_extra_css = ['personalisation/segment/form.css']
modeladmin_register(SegmentModelAdmin)
@hooks.register('before_serve_page')
def set_visit_count(page, request, serve_args, serve_kwargs):
if 'visit_count' not in request.session:
request.session['visit_count'] = []
# Update the page visit count
def create_new_counter(page, request):
"""Create a new counter dict and place it in session storage."""
countdict = {
"slug": page.slug,
"id": page.pk,
"path": request.path,
"count": 1,
}
request.session['visit_count'].append(countdict)
if len(request.session['visit_count']) > 0:
for index, counter in enumerate(request.session['visit_count']):
if counter['id'] == page.pk:
# Counter already exists. Increase the count value by 1.
newcount = counter['count'] + 1
request.session['visit_count'][index]['count'] = newcount
request.session.modified = True
else:
# Counter doesn't exist.
# Create a new counter with count value 1.
create_new_counter(page, request)
else:
# No counters exist. Create a new counter with count value 1.
create_new_counter(page, request)
@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)
@hooks.register('before_serve_page')
def serve_variation(page, request, serve_args, serve_kwargs):
user_segments = []
for segment in request.session['segments']:
try:
user_segment = Segment.objects.get(pk=segment['id'],
status='enabled')
except Segment.DoesNotExist:
user_segment = None
if user_segment:
user_segments.append(user_segment)
if len(user_segments) > 0:
variations = _check_for_variations(user_segments, page)
if variations:
variation = variations[0]
impersonate_other_page(variation, page)
return variation.serve(request, *serve_args, **serve_kwargs)
def _check_for_variations(segments, page):
for segment in segments:
page_class = page.__class__
if not any(item == PersonalisablePage for item in page_class.__bases__):
page_class = PersonalisablePage
variation = page_class.objects.filter(
canonical_page=page, segment=segment)
if variation:
return variation
return None
@hooks.register('register_page_listing_buttons')
def page_listing_variant_buttons(page, page_perms, is_parent=False):
personalisable_page = PersonalisablePage.objects.filter(pk=page.pk)
segments = Segment.objects.all()
if personalisable_page and len(segments) > 0 and not (any(item.segment for item in personalisable_page)):
yield ButtonWithDropdownFromHook(
_('Variants'),
hook_name='register_page_listing_variant_buttons',
page=page,
page_perms=page_perms,
is_parent=is_parent,
attrs={'target': '_blank', 'title': _('Create a new variant')}, priority=100)
@hooks.register('register_page_listing_variant_buttons')
def page_listing_more_buttons(page, page_perms, is_parent=False):
segments = Segment.objects.all()
available_segments = [item for item in segments if not PersonalisablePage.objects.filter(segment=item, pk=page.pk)]
for segment in available_segments:
yield Button(segment.name,
reverse('segment:copy_page', args=[page.id, segment.id]),
attrs={"title": _('Create this variant')})
class SegmentSummaryPanel(object):
order = 500
def render(self):
segment_count = Segment.objects.count()
target_url = reverse('personalisation_segment_modeladmin_index')
title = _("Segments")
return mark_safe("""
<li class="icon icon-group">
<a href="{}"><span>{}</span>{}</a>
</li>""".format(target_url, segment_count, title))
@hooks.register('construct_homepage_summary_items')
def add_segment_summary_panel(request, summary_items):
return summary_items.append(SegmentSummaryPanel())

View File

@ -0,0 +1 @@
default_app_config = 'wagtail_personalisation.config.WagtailPersonalisationConfig'

View File

@ -0,0 +1,236 @@
from django.conf import settings
from django.db.models import F
from django.utils.module_loading import import_string
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import create_segment_dictionary
class BaseSegmentsAdapter:
"""Base segments adapter."""
def __init__(self, request):
"""Prepare the request session for segment storage.
:param request: The http request
:type request: django.http.HttpRequest
"""
self.request = request
def setup(self):
"""Prepare the adapter for segment storage."""
def get_segments(self):
"""Return the segments stored in the adapter storage."""
def get_segment_by_id(self):
"""Return a single segment stored in the adapter storage."""
def add(self):
"""Add a new segment to the adapter storage."""
def refresh(self):
"""Refresh the segments stored in the adapter storage."""
def _test_rules(self, rules, request, match_any=False):
"""Tests the provided rules to see if the request still belongs
to a segment.
:param rules: The rules to test for
:type rules: list of wagtail_personalisation.rules
:param request: The http request
:type request: django.http.HttpRequest
:param match_any: Whether all rules need to match, or any
:type match_any: bool
:returns: A boolean indicating the segment matches the request
:rtype: bool
"""
if not rules:
return False
if match_any:
return any(rule.test_user(request) for rule in rules)
return all(rule.test_user(request) for rule in rules)
class Meta:
abstract = True
class SessionSegmentsAdapter(BaseSegmentsAdapter):
"""Segment adapter that uses Django's session backend."""
def __init__(self, request):
super(SessionSegmentsAdapter, self).__init__(request)
self.request.session.setdefault('segments', [])
self._segment_cache = None
def _segments(self, ids=None):
if not ids:
ids = []
segments = (
Segment.objects
.enabled()
.filter(persistent=True)
.filter(pk__in=ids)
)
return segments
def get_segments(self, key="segments"):
"""Return the persistent segments stored in the request session.
:param key: The key under which the segments are stored
:type key: String
:returns: The segments in the request session
:rtype: list of wagtail_personalisation.models.Segment or empty list
"""
if key == "segments" and self._segment_cache is not None:
return self._segment_cache
if key not in self.request.session:
return []
raw_segments = self.request.session[key]
segment_ids = [segment['id'] for segment in raw_segments]
segments = self._segments(ids=segment_ids)
result = list(segments)
if key == "segments":
self._segment_cache = result
return result
def set_segments(self, segments, key="segments"):
"""Set the currently active segments
:param segments: The segments to set for the current request
:type segments: list of wagtail_personalisation.models.Segment
:param key: The key under which to store the segments. Optional
:type key: String
"""
cache_segments = []
serialized_segments = []
segment_ids = set()
for segment in segments:
serialized = create_segment_dictionary(segment)
if serialized['id'] in segment_ids:
continue
cache_segments.append(segment)
serialized_segments.append(serialized)
segment_ids.add(segment.pk)
self.request.session[key] = serialized_segments
if key == "segments":
self._segment_cache = cache_segments
def get_segment_by_id(self, segment_id):
"""Find and return a single segment from the request session.
:param segment_id: The primary key of the segment
:type segment_id: int
:returns: The matching segment
:rtype: wagtail_personalisation.models.Segment or None
"""
segments = self._segments(ids=[segment_id])
if segments.exists():
return segments.get()
def add_page_visit(self, page):
"""Mark the page as visited by the user"""
visit_count = self.request.session.setdefault('visit_count', [])
page_visits = [visit for visit in visit_count if visit['id'] == page.pk]
if page_visits:
for page_visit in page_visits:
page_visit['count'] += 1
page_visit['path'] = page.url_path if page else self.request.path
self.request.session.modified = True
else:
visit_count.append({
'slug': page.slug,
'id': page.pk,
'path': page.url_path if page else self.request.path,
'count': 1,
})
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
visit_count = self.request.session.setdefault('visit_count', [])
for visit in visit_count:
if visit['path'] == path:
return visit['count']
return 0
def update_visit_count(self):
"""Update the visit count for all segments in the request session."""
segments = self.request.session['segments']
segment_pks = [s['id'] for s in segments]
# Update counts
(Segment.objects
.enabled()
.filter(pk__in=segment_pks)
.update(visit_count=F('visit_count') + 1))
def refresh(self):
"""Retrieve the request session segments and verify whether or not they
still apply to the requesting visitor.
"""
enabled_segments = Segment.objects.enabled()
rule_models = AbstractBaseRule.get_descendant_models()
current_segments = self.get_segments()
excluded_segments = self.get_segments("excluded_segments")
current_segments = list(
set(current_segments) - set(excluded_segments)
)
# 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():
additional_segments.append(segment)
elif any((
segment.excluded_users.filter(id=self.request.user.id).exists(),
segment in excluded_segments
)):
continue
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.randomise_into_segment():
if segment.is_static and not segment.is_full:
if self.request.user.is_authenticated:
segment.static_users.add(self.request.user)
additional_segments.append(segment)
elif result:
if segment.is_static and self.request.user.is_authenticated:
segment.excluded_users.add(self.request.user)
else:
excluded_segments += [segment]
self.set_segments(current_segments + additional_segments)
self.set_segments(excluded_segments, "excluded_segments")
self.update_visit_count()
SEGMENT_ADAPTER_CLASS = import_string(getattr(
settings,
'PERSONALISATION_SEGMENTS_ADAPTER',
'wagtail_personalisation.adapters.SessionSegmentsAdapter'))
def get_segment_adapter(request):
"""Return the Segment Adapter for the given request"""
if not hasattr(request, 'segment_adapter'):
request.segment_adapter = SEGMENT_ADAPTER_CLASS(request)
return request.segment_adapter

View File

@ -0,0 +1,44 @@
from django.contrib import admin
from wagtail_personalisation import models, rules
class UserIsLoggedInRuleAdminInline(admin.TabularInline):
"""Inline the UserIsLoggedIn Rule into the
administration interface for segments.
"""
model = rules.UserIsLoggedInRule
class TimeRuleAdminInline(admin.TabularInline):
"""Inline the Time Rule into the
administration interface for segments.
"""
model = rules.TimeRule
class ReferralRuleAdminInline(admin.TabularInline):
"""Inline the Referral Rule into the
administration interface for segments.
"""
model = rules.ReferralRule
class VisitCountRuleAdminInline(admin.TabularInline):
"""Inline the Visit Count Rule into the
administration interface for segments.
"""
model = rules.VisitCountRule
class SegmentAdmin(admin.ModelAdmin):
"""Add the inline models to the Segment admin interface."""
inlines = (UserIsLoggedInRuleAdminInline, TimeRuleAdminInline,
ReferralRuleAdminInline, VisitCountRuleAdminInline)
admin.site.register(models.Segment, SegmentAdmin)

View File

@ -0,0 +1,16 @@
from django.conf.urls import url
from wagtail_personalisation import views
app_name = 'segment'
urlpatterns = [
url(r'^segment/(?P<segment_id>[0-9]+)/toggle/$',
views.toggle, name='toggle'),
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
views.copy_page_view, name='copy_page'),
url(r'^segment/toggle_segment_view/$',
views.toggle_segment_view, name='toggle_segment_view'),
url(r'^segment/users/(?P<segment_id>[0-9]+)$',
views.segment_user_data, name='segment_user_data'),
]

View File

@ -0,0 +1,52 @@
from django.utils.translation import ugettext_lazy as _
from wagtail.core import blocks
from wagtail_personalisation.adapters import get_segment_adapter
from wagtail_personalisation.models import Segment
def list_segment_choices():
yield -1, ("Show 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."""
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 render(self, value, context=None):
"""Only render this content block for users in this segment.
:param value: The value from the block
:type value: dict
:param context: The context containing the request
:type context: dict
:returns: The provided block if matched, otherwise an empty string
:rtype: blocks.StructBlock or empty str
"""
request = context['request']
adapter = get_segment_adapter(request)
user_segments = adapter.get_segments()
try:
segment_id = int(value['segment'])
except (ValueError, TypeError):
return ''
if segment_id > 0:
for segment in user_segments:
if segment.id == segment_id:
return super(PersonalisedStructBlock, self).render(
value, context)
if segment_id == -1:
return super(PersonalisedStructBlock, self).render(
value, context)
return ''

View File

@ -0,0 +1,13 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class WagtailPersonalisationConfig(AppConfig):
label = 'wagtail_personalisation'
name = 'wagtail_personalisation'
verbose_name = _('Wagtail Personalisation')
def ready(self):
from wagtail_personalisation import receivers
receivers.register()

View File

@ -0,0 +1,136 @@
from datetime import datetime
import functools
from importlib import import_module
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.templatetags.static import static
from django.test.client import RequestFactory
from django.utils.translation import ugettext_lazy as _
from wagtail.admin.forms import WagtailAdminModelForm
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@functools.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 count_matching_users(self, rules, match_any):
""" Calculates how many users match the given static rules
"""
count = 0
static_rules = [rule for rule in rules if rule.static]
if not static_rules:
return count
User = get_user_model()
users = User.objects.filter(is_active=True, is_staff=False)
for user in users.iterator():
if match_any:
if any(rule.test_user(None, user) for rule in static_rules):
count += 1
elif all(rule.test_user(None, user) for rule in static_rules):
count += 1
return count
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
if is_new and self.instance.is_static and not self.instance.all_rules_static:
rules = [
form.instance for formset in self.formsets.values()
for form in formset
if form not in formset.deleted_forms
]
self.instance.matched_users_count = self.count_matching_users(
rules, self.instance.match_any)
self.instance.matched_count_updated_at = datetime.now()
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 = []
users_to_exclude = []
User = get_user_model()
users = User.objects.filter(is_active=True, is_staff=False)
matched_count = 0
for user in users.iterator():
request.user = user
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
if passes:
matched_count += 1
if instance.count == 0 or len(users_to_add) < instance.count:
if instance.randomise_into_segment():
users_to_add.append(user)
else:
users_to_exclude.append(user)
instance.matched_users_count = matched_count
instance.matched_count_updated_at = datetime.now()
instance.static_users.add(*users_to_add)
instance.excluded_users.add(*users_to_exclude)
return instance
@property
def media(self):
media = super(SegmentAdminForm, self).media
media.add_js(
[static('js/segment_form_control.js')]
)
return media

View File

@ -0,0 +1,352 @@
# Wagtail Personalisation english translation strings.
# Copyright (C) 2017 Lab Digital B.V.
# This file is distributed under the same license as the wagtail_personalisation package.
# Boris Besemer <b.besemer@labdigital.nl>, 2017.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: wagtail_personalisation 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-31 09:30-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: blocks.py:20
msgid "Personalisation segment"
msgstr ""
#: blocks.py:21
msgid "Only show this content block for users in this segment"
msgstr ""
#: config.py:8
msgid "Wagtail Personalisation"
msgstr ""
#: models.py:26
msgid "Enabled"
msgstr ""
#: models.py:27
msgid "Disabled"
msgstr ""
#: models.py:39
msgid "Should the segment persist between visits?"
msgstr ""
#: models.py:42
msgid "Should the segment match all the rules or just one of them?"
msgstr ""
#: models.py:60
msgid "Rules"
msgstr ""
#: models.py:167
msgid "Content"
msgstr ""
#: models.py:169
msgid "Variations"
msgstr ""
#: models.py:171
msgid "Promote"
msgstr ""
#: models.py:173
msgid "Settings"
msgstr ""
#: rules.py:29 rules.py:48
msgid "Abstract segmentation rule"
msgstr ""
#: rules.py:63
msgid "Starting time"
msgstr ""
#: rules.py:64
msgid "Ending time"
msgstr ""
#: rules.py:74
msgid "Time Rule"
msgstr ""
#: rules.py:85
msgid "These users visit between"
msgstr ""
#: rules.py:86
msgid "{} and {}"
msgstr ""
#: rules.py:103
msgid "Monday"
msgstr ""
#: rules.py:104
msgid "Tuesday"
msgstr ""
#: rules.py:105
msgid "Wednesday"
msgstr ""
#: rules.py:106
msgid "Thursday"
msgstr ""
#: rules.py:107
msgid "Friday"
msgstr ""
#: rules.py:108
msgid "Saturday"
msgstr ""
#: rules.py:109
msgid "Sunday"
msgstr ""
#: rules.py:122
msgid "Day Rule"
msgstr ""
#: rules.py:146
msgid "These users visit on"
msgstr ""
#: rules.py:162
msgid "Regular expression to match the referrer"
msgstr ""
#: rules.py:169
msgid "Referral Rule"
msgstr ""
#: rules.py:182
msgid "These visits originate from"
msgstr ""
#: rules.py:183 rules.py:366
msgid "{}"
msgstr ""
#: rules.py:202
msgid "More than"
msgstr ""
#: rules.py:203
msgid "Less than"
msgstr ""
#: rules.py:204
msgid "Equal to"
msgstr ""
#: rules.py:247
msgid "Visit count Rule"
msgstr ""
#: rules.py:251
msgid "These users visited {}"
msgstr ""
#: rules.py:254
msgid "{} {} times"
msgstr ""
#: rules.py:271
msgid "The query parameter to search for"
msgstr ""
#: rules.py:273
msgid "The value of the parameter to match"
msgstr ""
#: rules.py:282
msgid "Query Rule"
msgstr ""
#: rules.py:293
msgid "These users used a URL with the query"
msgstr ""
#: rules.py:294
msgid "?{}={}"
msgstr ""
#: rules.py:312
msgid "Mobile phone"
msgstr ""
#: rules.py:313
msgid "Tablet"
msgstr ""
#: rules.py:314
msgid "Desktop"
msgstr ""
#: rules.py:323
msgid "Device Rule"
msgstr ""
#: rules.py:354
msgid "Logged in Rule"
msgstr ""
#: rules.py:360
msgid "Logged in"
msgstr ""
#: rules.py:362
msgid "Not logged in"
msgstr ""
#: rules.py:365
msgid "These visitors are"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/base.html:28
msgid "Switch view"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:14
#: templates/modeladmin/wagtail_personalisation/segment/index.html:14
msgid "Filter"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:30
msgid "This segment has been visited"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:31
msgid "time"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:34
msgid "This segment has been active for"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:35
msgid "day"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:43
msgid "The visitor must match"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:45
msgid "Any rule"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:47
msgid "All rules"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:52
msgid "The persistence of this segment is"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:54
msgid "This segment persists in between visits"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:54
msgid "Persistent"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:56
msgid "This segment is re-evaluated on every visit"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:56
msgid "Fleeting"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:76
msgid "Enable this segment"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:78
msgid "Disable this segment"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:80
msgid "Configure this segment"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:88
#, python-format
msgid ""
"\n"
" <a class=\"block suggestion\" href="
"\"%(url)s\">\n"
" <span class=\"suggestive_text\">Add "
"a new %(name)s</span>\n"
" </a>\n"
" "
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:101
#: templates/modeladmin/wagtail_personalisation/segment/index.html:45
#, python-format
msgid "Page %(current_page)s of %(num_pages)s."
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/index.html:24
#, python-format
msgid ""
"No %(name)s have been created yet. One of the following must be created "
"before you can add any %(name)s:"
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/index.html:29
#, python-format
msgid "No %(name)s have been created yet."
msgstr ""
#: templates/modeladmin/wagtail_personalisation/segment/index.html:31
#, python-format
msgid ""
"\n"
" Why not <a href=\"%(url)s\">add "
"one</a>?\n"
" "
msgstr ""
#: views.py:60
#, python-brace-format
msgid "{visits} visits"
msgstr ""
#: views.py:63
#, python-brace-format
msgid "{days} days"
msgstr ""
#: wagtail_hooks.py:121
msgid "Variants"
msgstr ""
#: wagtail_hooks.py:126
msgid "Create a new variant"
msgstr ""
#: wagtail_hooks.py:146
msgid "Create this variant"
msgstr ""
#: wagtail_hooks.py:159
msgid "Segments"
msgstr ""

View File

@ -12,7 +12,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0030_index_on_pagerevision_created_at'),
('wagtailcore', '0001_initial'),
]
operations = [
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
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')),
('is_segmented', models.BooleanField(default=False)),
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variations', to='personalisation.PersonalisablePage')),
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='wagtail_personalisation.PersonalisablePage')),
],
options={
'abstract': False,
@ -60,7 +60,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.TimeField(verbose_name='Starting time')),
('end_time', models.TimeField(verbose_name='Ending time')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_timerule_related', related_query_name='personalisation_timerules', to='personalisation.Segment')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_timerule_related', related_query_name='wagtail_personalisation_timerules', to='wagtail_personalisation.Segment')),
],
options={
'abstract': False,
@ -73,7 +73,7 @@ class Migration(migrations.Migration):
('operator', models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='ht', max_length=20)),
('count', models.PositiveSmallIntegerField(default=0, null=True)),
('counted_page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_visitcountrule_related', related_query_name='personalisation_visitcountrules', to='personalisation.Segment')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_visitcountrule_related', related_query_name='wagtail_personalisation_visitcountrules', to='wagtail_personalisation.Segment')),
],
options={
'abstract': False,
@ -82,11 +82,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='referralrule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_referralrule_related', related_query_name='personalisation_referralrules', to='personalisation.Segment'),
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_referralrule_related', related_query_name='wagtail_personalisation_referralrules', to='wagtail_personalisation.Segment'),
),
migrations.AddField(
model_name='personalisablepage',
name='segment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='segments', to='personalisation.Segment'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='segments', to='wagtail_personalisation.Segment'),
),
]

Some files were not shown because too many files have changed in this diff Show More