7

Compare commits

...

415 Commits

Author SHA1 Message Date
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
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
173 changed files with 12299 additions and 1336 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"
}

10
.gitignore vendored
View File

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

31
.travis.yml Normal file
View File

@ -0,0 +1,31 @@
---
sudo: false
language: python
matrix:
include:
- python: 2.7
env: lint
- python: 2.7
env: TOXENV=py27-django111-wagtail113
install:
- pip install tox codecov
script:
- tox
after_success:
- tox -e coverage-report
- codecov
deploy:
provider: pypi
distributions: sdist bdist_wheel
user: praekelt.org
password:
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
on:
tags: true
condition: $TOXENV = py27-django111-wagtail113

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

62
CHANGES
View File

@ -1,3 +1,59 @@
0.1 (TBD)
====================
- Initial release
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)

10
CONTRIBUTORS.rst Normal file
View File

@ -0,0 +1,10 @@
Authors
=======
* Jasper Berghoef
* Boris Besemer
Contributors
============
* Michael van Tellingen
* Pim Vernooij
* Tomasz Knapik

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,4 +1,4 @@
.PHONY: all clean requirements develop test lint flake8 isort dist
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
all: clean requirements dist
@ -10,7 +10,9 @@ clean:
find . -name '*.egg-info' -delete
requirements:
pip install --upgrade -e .
pip install --upgrade -e .[docs,test]
install: develop
develop: clean requirements
@ -21,18 +23,32 @@ 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
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,75 @@
.. 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/LabD/wagtail-personalisation.svg?branch=master
:target: https://travis-ci.org/LabD/wagtail-personalisation
.. image:: http://codecov.io/github/LabD/wagtail-personalisation/coverage.svg?branch=master
:target: http://codecov.io/github/LabD/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
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:: logo.png
:scale: 50 %
:alt: Wagxperience
:align: center
.. image:: screenshot.png
Instructions
------------
Wagtail Personalisation requires Wagtail 1.9 or 1.10 and Django 1.9, 1.10 or 1.11.
To install the package with pip::
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',
# ...
]
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)

163
docs/conf.py Normal file
View File

@ -0,0 +1,163 @@
#!/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
# 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 = '2017, 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.11.2'
# The full version, including alpha/beta/rc tags.
release = '0.11.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 = {
'github_user': 'LabD',
'github_banner': True,
'github_repo': 'wagtail-personalisation',
'travis_button': True,
'codecov_button': True,
'analytics_id': 'UA-100203499-2',
}
# 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'),
]

123
docs/default_rules.rst Normal file
View File

@ -0,0 +1,123 @@
Included rules
==============
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'd 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``

32
docs/getting_started.rst Normal file
View File

@ -0,0 +1,32 @@
Getting started
===============
Installing Wagxperience
-----------------------
Installing the module
^^^^^^^^^^^^^^^^^^^^^
The Wagxperience app runs in the Wagtail CMS. You can find out more here_.
.. _here: http://docs.wagtail.io/en/latest/getting_started/tutorial.html
1. Install the module::
pip install wagtail-personalisation
2. Add the module and ``wagtail.contrib.modeladmin`` to your ``INSTALLED_APPS``::
INSTALLED_APPS = [
# ...
'wagtail.contrib.modeladmin',
'wagtail_personalisation',
# ...
]
3. Update your database::
python manage.py migrate
Continue reading: :doc:`implementation`

87
docs/implementation.rst Normal file
View File

@ -0,0 +1,87 @@
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::
from wagtail_personalisation.models import PersonalisablePage
class HomePage(PersonalisablePage):
subtitle = models.CharField(max_length=255)
body = RichTextField(blank=True, default='')
content_panels = PersonalisablePage.content_panels + [
FieldPanel('subtitle'),
FieldPanel('body'),
]
It's just as simple as extending a standard ``Page`` class.
Migrating an existing page to be personalisable
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Creating custom rules
---------------------
Rules consist of two important elements, the model's fields and the ``test_user`` function.
A very simple example of a rule would look something like this::
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from personalisation import AbstractBaseRule
class MyNewRule(AbstractBaseRule):
field = models.BooleanField(default=False)
panels = [
FieldPanel('field'),
]
def __init__(self, *args, **kwargs):
super(MyNewRule, self).__init__(*args, **kwargs)
def test_user(self, request):
return self.field
As you can see, the only real requirement is the ``test_user`` function that will either return
``True`` or ``False`` based on the model's fields and optionally the request object.
Below is the "Time rule" model included with the module, which offers more complex functionality::
@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'
Continue reading: :doc:`usage_guide`

24
docs/index.rst Normal file
View File

@ -0,0 +1,24 @@
.. 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!
==========================================
.. toctree::
:maxdepth: 2
:caption: Contents:
getting_started
implementation
usage_guide
default_rules
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

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

95
docs/usage_guide.rst Normal file
View File

@ -0,0 +1,95 @@
Usage guide
===========
Creating a segment
------------------
As soon as the installation is completed and configured, the module will be
visible in the Wagtail administrative area.
To create a segment, go to the "Segments" page and click on "Add a new segment".
On this page you will be presented with a form. Follow these steps to create a
new segment:
1. Enter a name for your segment.
2. (Optional) Select whether to match any or all defined rules.
``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.
3. (Optional) Set the segment persistence.
When persistence is enabled, your segment will stick to the visitor once
applied, even if the rules no longer match on the next visit.
4. Define your segment rules.
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.
5. Save your segment.
Click "save" to store your segment. It will be enabled by default,
unless otherwise defined.
Creating personalized content
-----------------------------
Once you've created a segment you can start serving these visitors with
personalised content. To do this, you can go one of two directions.
1. Create a copy of a page for your segment.
2. Create StreamField blocks only visible to your segment.
3. Create a template block only visible to your segment.
Method 1: Create a copy
^^^^^^^^^^^^^^^^^^^^^^^
To create a copy from a page for a specific Segment (which you can change to
your liking after copying it) simply go to the Explorer section and find the
page you'd wish to personalize.
You'll notice a new "Variants" dropdown button has appeared. Click the button
and select the segment you'd like to create personalized content for.
Once you've selected the segment, a copy of the page will be created with a
title that includes the segment. Don't worry, your visitors won't be able to
see this title.
You can change everything on this page you'd like. Visitors that are included in
your segment, will automatically see the new page you've created for them.
Method 2: Create a StreamField block
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Method 3: Create a template block
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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::
{% 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::
{% 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.

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: 30 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/LabD/wagtail-personalisation.git"
},
"author": "Lab Digital",
"license": "ISC",
"bugs": {
"url": "https://github.com/LabD/wagtail-personalisation/issues"
},
"homepage": "https://github.com/LabD/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>=1.11,<1.12
wagtail>=1.10,<1.11
django-debug-toolbar==1.8
-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.wagtailcore.fields
import wagtail_personalisation
class Migration(migrations.Migration):
dependencies = [
('home', '0002_create_homepage'),
]
operations = [
migrations.AddField(
model_name='homepage',
name='intro',
field=wagtail.wagtailcore.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.wagtailcore.fields.StreamField((('personalisable_paragraph', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
preserve_default=False,
),
]

View File

@ -0,0 +1,23 @@
from __future__ import absolute_import, unicode_literals
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.fields import RichTextField, StreamField
from wagtail.wagtailcore.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

@ -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.wagtailcore.models import Page
from wagtail.wagtailsearch.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,51 @@
from django.contrib.auth.models import (
AbstractBaseUser, PermissionsMixin, UserManager)
from django.core.mail import send_mail
from django.db import connections, models
from django.dispatch import receiver
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
class User(AbstractBaseUser, PermissionsMixin):
"""Cusomtized 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)

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

@ -0,0 +1,162 @@
"""
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
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/
# Application definition
INSTALLED_APPS = [
'wagtail.wagtailforms',
'wagtail.wagtailredirects',
'wagtail.wagtailembeds',
'wagtail.wagtailsites',
'wagtail.wagtailusers',
'wagtail.wagtailsnippets',
'wagtail.wagtaildocs',
'wagtail.wagtailimages',
'wagtail.wagtailsearch',
'wagtail.wagtailadmin',
'wagtail.wagtailcore',
'wagtail.contrib.modeladmin',
'wagtailfontawesome',
'modelcluster',
'taggit',
'debug_toolbar',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'wagtail_personalisation',
'sandbox.apps.home',
'sandbox.apps.search',
'sandbox.apps.user',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
]
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.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from sandbox.apps.search import views as search_views
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^cms/', 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,31 @@
[bumpversion]
current_version = 0.11.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:run]
omit =
src/**/migrations/*.py
[bumpversion:file:setup.py]
[bumpversion:file:docs/conf.py]

View File

@ -1,50 +1,69 @@
import re
from setuptools import find_packages, setup
install_requires = [
'django-polymorphic==1.0.2',
'wagtail>=1.7',
'wagtail>=1.10,<1.14',
'user-agents>=1.0.1',
'wagtailfontawesome>=1.0.6',
]
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',
'flake8-blind-except',
'flake8-debugger',
'flake8-imports',
'freezegun==0.3.8',
'factory_boy==2.7.0',
'pytest-cov==2.4.0',
'pytest-django==3.1.2',
'pytest-sugar==0.7.1',
'pytest-mock==1.6.3',
'pytest==3.1.0',
'wagtail_factories==0.3.0',
]
docs_require = [
'sphinx>=1.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',
description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV',
author_email='b.besemer@labdigital.nl',
url='http://labdigital.nl',
name='wagtail-personalisation-molo',
version='0.11.2',
description='A forked version of Wagtail add-on for showing personalized content',
author='Praekelt.org',
author_email='dev@praekeltfoundation.org',
url='https://github.com/praekeltfoundation/wagtail-personalisation/',
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',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'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,226 @@
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.db.models import F
from django.utils.module_loading import import_string
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import create_segment_dictionary
class BaseSegmentsAdapter(object):
"""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 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 = (
Segment.objects
.enabled()
.filter(persistent=True)
.in_bulk(segment_ids))
retval = [segments[pk] for pk in segment_ids if pk in segments]
if key == "segments":
self._segment_cache = retval
return retval
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
"""
for segment in self.get_segments():
if segment.pk == segment_id:
return segment
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")
# 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 (segment.excluded_users.filter(id=self.request.user.id).exists() or
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,46 @@
from __future__ import absolute_import, unicode_literals
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,18 @@
from __future__ import absolute_import, unicode_literals
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,44 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailcore import blocks
from wagtail_personalisation.adapters import get_segment_adapter
from wagtail_personalisation.models import Segment
def list_segment_choices():
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()
if value['segment']:
for segment in user_segments:
if segment.id == int(value['segment']):
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,138 @@
from __future__ import absolute_import, unicode_literals
from datetime import datetime
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.contrib.staticfiles.templatetags.staticfiles import static
from django.test.client import RequestFactory
from django.utils.lru_cache import lru_cache
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.forms import WagtailAdminModelForm
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@lru_cache(maxsize=1000)
def user_from_data(user_id):
User = get_user_model()
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
class SegmentAdminForm(WagtailAdminModelForm):
def 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'),
),
]

View File

@ -10,7 +10,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('personalisation', '0001_initial'),
('wagtail_personalisation', '0001_initial'),
]
operations = [
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('query_parameter', models.TextField(verbose_name='The query parameter to search for')),
('query_value', models.TextField(verbose_name='The value of the parameter to match')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_queryrule_related', related_query_name='personalisation_queryrules', to='personalisation.Segment')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_queryrule_related', related_query_name='wagtail_personalisation_queryrules', to='wagtail_personalisation.Segment')),
],
options={
'abstract': False,

View File

@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('personalisation', '0002_auto_20161205_1623'),
('wagtail_personalisation', '0002_auto_20161205_1623'),
]
operations = [

View File

@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('personalisation', '0003_auto_20161206_1005'),
('wagtail_personalisation', '0003_auto_20161206_1005'),
]
operations = [

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-11 12:15
from __future__ import unicode_literals
import django.db.models.deletion
import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0004_segment_persistent'),
]
operations = [
migrations.CreateModel(
name='UserIsLoggedInRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_logged_in', models.BooleanField(default=False)),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_userisloggedinrule_related', related_query_name='wagtail_personalisation_userisloggedinrules', to='wagtail_personalisation.Segment')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-22 13:23
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0005_userisloggedinrule'),
]
operations = [
migrations.AddField(
model_name='segment',
name='match_any',
field=models.BooleanField(default=False, help_text='Should the segment match all the rules or just one of them?'),
),
]

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-10 14:35
from __future__ import unicode_literals
import django.db.models.deletion
import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0006_segment_match_any'),
]
operations = [
migrations.CreateModel(
name='DayRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mon', models.BooleanField(default=False, verbose_name='Monday')),
('tue', models.BooleanField(default=False, verbose_name='Tuesday')),
('wed', models.BooleanField(default=False, verbose_name='Wednesday')),
('thu', models.BooleanField(default=False, verbose_name='Thursday')),
('fri', models.BooleanField(default=False, verbose_name='Friday')),
('sat', models.BooleanField(default=False, verbose_name='Saturday')),
('sun', models.BooleanField(default=False, verbose_name='Sunday')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_dayrule_related', related_query_name='wagtail_personalisation_dayrules', to='wagtail_personalisation.Segment')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-04-20 15:47
from __future__ import unicode_literals
import django.db.models.deletion
import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0007_dayrule'),
]
operations = [
migrations.CreateModel(
name='DeviceRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mobile', models.BooleanField(default=False, verbose_name='Mobile phone')),
('tablet', models.BooleanField(default=False, verbose_name='Tablet')),
('desktop', models.BooleanField(default=False, verbose_name='Desktop')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_devicerule_related', related_query_name='wagtail_personalisation_devicerules', to='wagtail_personalisation.Segment')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 04:28
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0008_devicerule'),
]
operations = [
migrations.RemoveField(
model_name='personalisablepage',
name='canonical_page',
),
migrations.RemoveField(
model_name='personalisablepage',
name='page_ptr',
),
migrations.RemoveField(
model_name='personalisablepage',
name='segment',
),
migrations.DeleteModel(
name='PersonalisablePage',
),
]

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 11:01
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0009_auto_20170531_0428'),
]
operations = [
migrations.AlterModelOptions(
name='dayrule',
options={'verbose_name': 'Day Rule'},
),
migrations.AlterModelOptions(
name='devicerule',
options={'verbose_name': 'Device Rule'},
),
migrations.AlterModelOptions(
name='queryrule',
options={'verbose_name': 'Query Rule'},
),
migrations.AlterModelOptions(
name='referralrule',
options={'verbose_name': 'Referral Rule'},
),
migrations.AlterModelOptions(
name='timerule',
options={'verbose_name': 'Time Rule'},
),
migrations.AlterModelOptions(
name='userisloggedinrule',
options={'verbose_name': 'Logged in Rule'},
),
migrations.AlterModelOptions(
name='visitcountrule',
options={'verbose_name': 'Visit count Rule'},
),
migrations.AlterField(
model_name='referralrule',
name='regex_string',
field=models.TextField(verbose_name='Regular expression to match the referrer'),
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 14:28
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0001_initial'),
('wagtail_personalisation', '0010_auto_20170531_1101'),
]
operations = [
migrations.CreateModel(
name='PersonalisablePageMetadata',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_segmented', models.BooleanField(default=False)),
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='personalisable_canonical_metadata', to='wagtailcore.Page')),
('segment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='page_metadata', to='wagtail_personalisation.Segment')),
('variant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-01 11:48
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0011_personalisablepagemetadata'),
]
operations = [
migrations.RemoveField(
model_name='personalisablepagemetadata',
name='is_segmented',
),
]

View File

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

View File

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

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-01-25 09:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0015_static_users'),
]
operations = [
migrations.AddField(
model_name='segment',
name='matched_count_updated_at',
field=models.DateTimeField(editable=False, null=True),
),
migrations.AddField(
model_name='segment',
name='matched_users_count',
field=models.PositiveIntegerField(default=0, editable=False),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.8 on 2018-01-31 16:12
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0016_auto_20180125_0918'),
]
operations = [
migrations.AddField(
model_name='segment',
name='randomisation_percent',
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='If this number is set each user matching the rules will have this percentage chance of being placed in the segment.', null=True, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]),
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.9 on 2018-02-09 08:28
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtail_personalisation', '0017_segment_randomisation_percent'),
]
operations = [
migrations.AddField(
model_name='segment',
name='excluded_users',
field=models.ManyToManyField(help_text='Users that matched the rules but were excluded from the segment for some reason e.g. randomisation', related_name='excluded_segments', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,305 @@
from __future__ import absolute_import, unicode_literals
import random
from django import forms
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models, transaction
from django.template.defaultfilters import slugify
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
from wagtail.wagtailcore.models import Page
from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import count_active_days
from .forms import SegmentAdminForm
class SegmentQuerySet(models.QuerySet):
def enabled(self):
return self.filter(status=self.model.STATUS_ENABLED)
@python_2_unicode_compatible
class Segment(ClusterableModel):
"""The segment model."""
STATUS_ENABLED = 'enabled'
STATUS_DISABLED = 'disabled'
STATUS_CHOICES = (
(STATUS_ENABLED, _('Enabled')),
(STATUS_DISABLED, _('Disabled')),
)
TYPE_DYNAMIC = 'dynamic'
TYPE_STATIC = 'static'
TYPE_CHOICES = (
(TYPE_DYNAMIC, _('Dynamic')),
(TYPE_STATIC, _('Static')),
)
name = models.CharField(max_length=255)
create_date = models.DateTimeField(auto_now_add=True)
edit_date = models.DateTimeField(auto_now=True)
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 = models.CharField(
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED)
persistent = models.BooleanField(
default=False, help_text=_("Should the segment persist between visits?"))
match_any = models.BooleanField(
default=False,
help_text=_("Should the segment match all the rules or just one of them?")
)
type = models.CharField(
max_length=20,
choices=TYPE_CHOICES,
default=TYPE_DYNAMIC,
help_text=mark_safe(_("""
</br></br><strong>Dynamic:</strong> Users in this segment will change
as more or less meet the rules specified in the segment.
</br><strong>Static:</strong> If the segment contains only static
compatible rules the segment will contain the members that pass
those rules when the segment is created. Mixed static segments or
those containing entirely non static compatible rules will be
populated using the count variable.
"""))
)
count = models.PositiveSmallIntegerField(
default=0,
help_text=_(
"If this number is set for a static segment users will be added to the "
"set until the number is reached. After this no more users will be added."
)
)
static_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
)
excluded_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
help_text=_("Users that matched the rules but were excluded from the "
"segment for some reason e.g. randomisation"),
related_name="excluded_segments"
)
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
randomisation_percent = models.PositiveSmallIntegerField(
null=True, blank=True, default=None,
help_text=_(
"If this number is set each user matching the rules will "
"have this percentage chance of being placed in the segment."
), validators=[
MaxValueValidator(100),
MinValueValidator(0)
])
objects = SegmentQuerySet.as_manager()
base_form_class = SegmentAdminForm
def __init__(self, *args, **kwargs):
Segment.panels = [
MultiFieldPanel([
FieldPanel('name', classname="title"),
FieldRowPanel([
FieldPanel('status'),
FieldPanel('persistent'),
]),
FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count', classname='count_field'),
FieldPanel('randomisation_percent', classname='percent_field'),
], heading="Segment"),
MultiFieldPanel([
InlinePanel(
"{}_related".format(rule_model._meta.db_table),
label='{}{}'.format(
rule_model._meta.verbose_name,
' ({})'.format(_('Static compatible')) if rule_model.static else ''
),
) for rule_model in AbstractBaseRule.__subclasses__()
], heading=_("Rules")),
]
super(Segment, self).__init__(*args, **kwargs)
def __str__(self):
return self.name
@property
def is_static(self):
return self.type == self.TYPE_STATIC
@classmethod
def all_static(cls, rules):
return all(rule.static for rule in rules)
@property
def all_rules_static(self):
rules = self.get_rules()
return rules and self.all_static(rules)
@property
def is_full(self):
return self.static_users.count() >= self.count
def encoded_name(self):
"""Return a string with a slug for the segment."""
return slugify(self.name.lower())
def get_active_days(self):
"""Return the amount of days the segment has been active."""
return count_active_days(self.enable_date, self.disable_date)
def get_used_pages(self):
"""Return the pages that have variants using this segment."""
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
return pages
def get_created_variants(self):
"""Return the variants using this segment."""
pages = Page.objects.filter(_personalisable_page_metadata__segment=self)
return pages
def get_rules(self):
"""Retrieve all rules in the segment."""
segment_rules = []
for rule_model in AbstractBaseRule.get_descendant_models():
segment_rules.extend(
rule_model._default_manager.filter(segment=self))
return segment_rules
def toggle(self, save=True):
self.status = (
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
else self.STATUS_DISABLED)
if save:
self.save()
def randomise_into_segment(self):
""" Returns True if randomisation_percent is not set or it generates
a random number less than the randomisation_percent
This is so there is some randomisation in which users are added to the
segment
"""
if self.randomisation_percent is None:
return True
if random.randint(1, 100) <= self.randomisation_percent:
return True
return False
class PersonalisablePageMetadata(ClusterableModel):
"""The personalisable page model. Allows creation of variants with linked
segments.
"""
canonical_page = models.ForeignKey(
Page, related_name='personalisable_canonical_metadata',
on_delete=models.SET_NULL,
blank=True, null=True
)
variant = models.OneToOneField(
Page, related_name='_personalisable_page_metadata')
segment = models.ForeignKey(
Segment, related_name='page_metadata', null=True, blank=True)
@cached_property
def has_variants(self):
"""Return a boolean indicating whether or not the personalisable page
has variants.
:returns: A boolean indicating whether or not the personalisable page
has variants.
:rtype: bool
"""
return self.variants_metadata.exists()
@cached_property
def variants_metadata(self):
return (
PersonalisablePageMetadata.objects
.filter(canonical_page_id=self.canonical_page_id)
.exclude(variant_id=self.variant_id)
.exclude(variant_id=self.canonical_page_id))
@cached_property
def is_canonical(self):
"""Return a boolean indicating whether or not the personalisable page
is a canonical page.
:returns: A boolean indicating whether or not the personalisable
page
is a canonical page.
:rtype: bool
"""
return self.canonical_page_id == self.variant_id
def copy_for_segment(self, segment):
page = self.canonical_page
slug = "{}-{}".format(page.slug, segment.encoded_name())
title = "{} ({})".format(page.title, segment.name)
update_attrs = {
'title': title,
'slug': slug,
'live': False,
}
with transaction.atomic():
new_page = self.canonical_page.copy(
update_attrs=update_attrs, copy_revisions=False)
PersonalisablePageMetadata.objects.create(
canonical_page=page,
variant=new_page,
segment=segment)
return new_page
def metadata_for_segments(self, segments):
return (
self.__class__.objects
.filter(
canonical_page_id=self.canonical_page_id,
segment__in=segments))
def get_unused_segments(self):
if self.is_canonical:
return (
Segment.objects
.exclude(page_metadata__canonical_page_id=self.canonical_page_id))
return Segment.objects.none()
class PersonalisablePageMixin(object):
"""The personalisable page model. Allows creation of variants with linked
segments.
"""
@cached_property
def personalisation_metadata(self):
try:
metadata = self._personalisable_page_metadata
except AttributeError:
metadata = PersonalisablePageMetadata.objects.create(
canonical_page=self, variant=self)
return metadata

View File

@ -0,0 +1,24 @@
from django.db.models.signals import pre_save
from django.utils import timezone
from wagtail_personalisation.models import Segment
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 == instance.STATUS_ENABLED:
instance.enable_date = timezone.now()
instance.visit_count = 0
return instance
if instance.status == instance.STATUS_DISABLED:
instance.disable_date = timezone.now()
def register():
pre_save.connect(check_status_change, sender=Segment)

View File

@ -0,0 +1,404 @@
from __future__ import absolute_import, unicode_literals
import re
from datetime import datetime
from importlib import import_module
from django.apps import apps
from django.conf import settings
from django.contrib.sessions.models import Session
from django.db import models
from django.template.defaultfilters import slugify
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.test.client import RequestFactory
from modelcluster.fields import ParentalKey
from user_agents import parse
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel, FieldRowPanel, PageChooserPanel)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@python_2_unicode_compatible
class AbstractBaseRule(models.Model):
"""Base for creating rules to segment users with."""
icon = 'fa-circle-o'
static = False
segment = ParentalKey(
'wagtail_personalisation.Segment',
related_name="%(app_label)s_%(class)s_related",
related_query_name="%(app_label)s_%(class)ss"
)
class Meta:
abstract = True
verbose_name = 'Abstract segmentation rule'
def __str__(self):
return force_text(self._meta.verbose_name)
def test_user(self):
"""Test if the user matches this rule."""
return True
def encoded_name(self):
"""Return a string with a slug for the rule."""
return slugify(force_text(self).lower())
def description(self):
"""Return a description explaining the functionality of the rule.
Used in the segmentation dashboard.
:returns: A dict containing a title and a value
:rtype: dict
"""
description = {
'title': _('Abstract segmentation rule'),
'value': '',
}
return description
@classmethod
def get_descendant_models(cls):
return [model for model in apps.get_models()
if issubclass(model, AbstractBaseRule)]
class TimeRule(AbstractBaseRule):
"""Time rule to segment users based on a start and end time.
Matches when the time a request is made falls between the
set start time and end time.
"""
icon = 'fa-clock-o'
start_time = models.TimeField(_("Starting time"))
end_time = models.TimeField(_("Ending time"))
panels = [
FieldRowPanel([
FieldPanel('start_time'),
FieldPanel('end_time'),
]),
]
class Meta:
verbose_name = _('Time Rule')
def test_user(self, request=None):
return self.start_time <= datetime.now().time() <= self.end_time
def description(self):
return {
'title': _('These users visit between'),
'value': _('{} and {}').format(
self.start_time.strftime("%H:%M"),
self.end_time.strftime("%H:%M")
),
}
class DayRule(AbstractBaseRule):
"""Day rule to segment users based on the day(s) of a visit.
Matches when the day a request is made matches with the days
set in the rule.
"""
icon = 'fa-calendar-check-o'
mon = models.BooleanField(_("Monday"), default=False)
tue = models.BooleanField(_("Tuesday"), default=False)
wed = models.BooleanField(_("Wednesday"), default=False)
thu = models.BooleanField(_("Thursday"), default=False)
fri = models.BooleanField(_("Friday"), default=False)
sat = models.BooleanField(_("Saturday"), default=False)
sun = models.BooleanField(_("Sunday"), default=False)
panels = [
FieldPanel('mon'),
FieldPanel('tue'),
FieldPanel('wed'),
FieldPanel('thu'),
FieldPanel('fri'),
FieldPanel('sat'),
FieldPanel('sun'),
]
class Meta:
verbose_name = _('Day Rule')
def test_user(self, request=None):
return [self.mon, self.tue, self.wed, self.thu,
self.fri, self.sat, self.sun][datetime.today().weekday()]
def description(self):
days = (
('mon', self.mon), ('tue', self.tue), ('wed', self.wed),
('thu', self.thu), ('fri', self.fri), ('sat', self.sat),
('sun', self.sun),
)
chosen_days = [day_name for day_name, chosen in days if chosen]
return {
'title': _('These users visit on'),
'value': ", ".join([day for day in chosen_days]).title(),
}
class ReferralRule(AbstractBaseRule):
"""Referral rule to segment users based on a regex test.
Matches when the referral header in a request matches with
the set regex test.
"""
icon = 'fa-globe'
regex_string = models.TextField(
_("Regular expression to match the referrer"))
panels = [
FieldPanel('regex_string'),
]
class Meta:
verbose_name = _('Referral Rule')
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 description(self):
return {
'title': _('These visits originate from'),
'value': self.regex_string,
'code': True
}
class VisitCountRule(AbstractBaseRule):
"""Visit count rule to segment users based on amount of visits to a
specified page.
Matches when the operator and count validate True
when visiting the set page.
"""
icon = 'fa-calculator'
static = True
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'),
]),
]
class Meta:
verbose_name = _('Visit count Rule')
def _get_user_session(self, user):
sessions = Session.objects.iterator()
for session in sessions:
session_data = session.get_decoded()
if session_data.get('_auth_user_id') == str(user.id):
return SessionStore(session_key=session.session_key)
return SessionStore()
def test_user(self, request, user=None):
# Local import for cyclic import
from wagtail_personalisation.adapters import (
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
if user:
# Create a fake request so we can use the adapter
request = RequestFactory().get('/')
request.user = user
# If we're using the session adapter check for an active session
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
request.session = self._get_user_session(user)
else:
request.session = SessionStore()
elif not request:
# Return false if we don't have a user or a request
return False
operator = self.operator
segment_count = self.count
adapter = get_segment_adapter(request)
visit_count = adapter.get_visit_count(self.counted_page)
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 description(self):
return {
'title': _('These users visited {}').format(
self.counted_page
),
'value': _('{} {} times').format(
self.get_operator_display(),
self.count
),
}
def get_column_header(self):
return "Visit count - %s" % self.counted_page
def get_user_info_string(self, user):
# Local import for cyclic import
from wagtail_personalisation.adapters import (
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
# Create a fake request so we can use the adapter
request = RequestFactory().get('/')
request.user = user
# If we're using the session adapter check for an active session
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
request.session = self._get_user_session(user)
else:
request.session = SessionStore()
adapter = get_segment_adapter(request)
visit_count = adapter.get_visit_count(self.counted_page)
return str(visit_count)
class QueryRule(AbstractBaseRule):
"""Query rule to segment users based on matching queries.
Matches when both the set parameter and value match with one
present in the request query.
"""
icon = 'fa-link'
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'),
]
class Meta:
verbose_name = _('Query Rule')
def test_user(self, request):
return request.GET.get(self.parameter, '') == self.value
def description(self):
return {
'title': _('These users used a URL with the query'),
'value': _('?{}={}').format(
self.parameter,
self.value
),
'code': True
}
class DeviceRule(AbstractBaseRule):
"""Device rule to segment users based on matching devices.
Matches when the set device type matches with the one present
in the request user agent headers.
"""
icon = 'fa-tablet'
mobile = models.BooleanField(_("Mobile phone"), default=False)
tablet = models.BooleanField(_("Tablet"), default=False)
desktop = models.BooleanField(_("Desktop"), default=False)
panels = [
FieldPanel('mobile'),
FieldPanel('tablet'),
FieldPanel('desktop'),
]
class Meta:
verbose_name = _('Device Rule')
def test_user(self, request=None):
ua_header = request.META['HTTP_USER_AGENT']
user_agent = parse(ua_header)
if user_agent.is_mobile:
return self.mobile
if user_agent.is_tablet:
return self.tablet
if user_agent.is_pc:
return self.desktop
return False
class UserIsLoggedInRule(AbstractBaseRule):
"""User is logged in rule to segment users based on their authentication
status.
Matches when the user is authenticated.
"""
icon = 'fa-user'
is_logged_in = models.BooleanField(default=False)
panels = [
FieldPanel('is_logged_in'),
]
class Meta:
verbose_name = _('Logged in Rule')
def test_user(self, request=None):
return request.user.is_authenticated() == self.is_logged_in
def description(self):
return {
'title': _('These visitors are'),
'value': _('Logged in') if self.is_logged_in else _('Not logged in'),
}

View File

@ -0,0 +1,2 @@
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block .inspect_container .inspect .stat_card{display:inline-block;margin-bottom:5px;margin-right:10px}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
/*# sourceMappingURL=dashboard.css.map*/

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