mirror of
https://github.com/jazzband/django-analytical.git
synced 2026-03-17 14:40:28 +00:00
Compare commits
482 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6f12719cc | ||
|
|
694fc9097a | ||
|
|
10102d1017 | ||
|
|
033d2dc02f | ||
|
|
4ea4605791 | ||
|
|
7253a3a048 | ||
|
|
a751ffa5e3 | ||
|
|
8b12e33e69 | ||
|
|
e0f95bc86b | ||
|
|
110d5bf361 | ||
|
|
0f099b8ab4 | ||
|
|
18b5a73bd3 | ||
|
|
df5b04b932 | ||
|
|
7704f8c56b | ||
|
|
25d08a8633 | ||
|
|
a246d62ae0 | ||
|
|
0d87a76da0 | ||
|
|
6850710413 | ||
|
|
4540999dc1 | ||
|
|
d3ddf1ac6a | ||
|
|
1dd5b2ac62 | ||
|
|
a112ec445f | ||
|
|
82fbbeb6b2 | ||
|
|
8d6419eec8 | ||
|
|
6e367cdd2d | ||
|
|
2163c77684 | ||
|
|
858a05307b | ||
|
|
19c43bef4a | ||
|
|
5ad082df48 | ||
|
|
47a5a4d6a9 | ||
|
|
f56a779b7a | ||
|
|
a82bde708a | ||
|
|
83e4361d2a | ||
|
|
25979fd0c2 | ||
|
|
96f1ba98b1 | ||
|
|
7e28ee31f1 | ||
|
|
e596e20183 | ||
|
|
248e04aef5 | ||
|
|
f487cb895c | ||
|
|
688524305f | ||
|
|
ba59f396df | ||
|
|
844a178c04 | ||
|
|
ce90933ab1 | ||
|
|
a8be4ea814 | ||
|
|
d82ca0aed5 | ||
|
|
52aa88f08a | ||
|
|
023702fa75 | ||
|
|
1d9a3b5626 | ||
|
|
2fd2279ecb | ||
|
|
a021e07e15 | ||
|
|
051d46ceda | ||
|
|
8d449868da | ||
|
|
44e642c34b | ||
|
|
41b8c243d4 | ||
|
|
5daa0c5329 | ||
|
|
e4399d63a1 | ||
|
|
0b162449c5 | ||
|
|
58aa95df34 | ||
|
|
aa29a051bd | ||
|
|
dbd7414b33 | ||
|
|
1923108e93 | ||
|
|
a615b954ae | ||
|
|
392cbba489 | ||
|
|
37009f2e08 | ||
|
|
f0e7ebe731 | ||
|
|
baaa1c4a8b | ||
|
|
896a19196f | ||
|
|
8cd75d9e60 | ||
|
|
a3fe981f76 | ||
|
|
514780aeb2 | ||
|
|
7f1358dcb6 | ||
|
|
5ea7d15868 | ||
|
|
47fa937910 | ||
|
|
bf471d7dfc | ||
|
|
a9d7d17ce6 | ||
|
|
ba8982be0a | ||
|
|
00bc123b88 | ||
|
|
4515fcd1be | ||
|
|
9163294603 | ||
|
|
3eb17007ad | ||
|
|
df7ed2d132 | ||
|
|
10d109eb6c | ||
|
|
146a96fca0 | ||
|
|
98e7e24e3e | ||
|
|
161983fad5 | ||
|
|
197cdbcfad | ||
|
|
7580bc1619 | ||
|
|
de2ed4bafc | ||
|
|
2322f7a0bf | ||
|
|
1ebc2a23ea | ||
|
|
b9320b41d9 | ||
|
|
e517bce3a6 | ||
|
|
ef40ba6ff5 | ||
|
|
ee956230ea | ||
|
|
d98e815493 | ||
|
|
1cdfb0ed0d | ||
|
|
3292b664ec | ||
|
|
ac2ebf375c | ||
|
|
7e68563849 | ||
|
|
788cab447e | ||
|
|
88197ca17e | ||
|
|
438b1408fa | ||
|
|
962af837af | ||
|
|
c10759d378 | ||
|
|
d9b21e96c6 | ||
|
|
77fdb51432 | ||
|
|
1ce265d2d9 | ||
|
|
d14d4727ee | ||
|
|
63adb0fbba | ||
|
|
0ea5d39155 | ||
|
|
756ac787e8 | ||
|
|
637805e003 | ||
|
|
5487fd677b | ||
|
|
724e4ddac4 | ||
|
|
c7f8dc21ad | ||
|
|
84e7dab3d2 | ||
|
|
b96bf0050a | ||
|
|
d5de14ebdc | ||
|
|
02b0768c97 | ||
|
|
ef0b19ccb2 | ||
|
|
3b1ab2bbed | ||
|
|
e5f8c199dc | ||
|
|
bfe92c716c | ||
|
|
367606c12c | ||
|
|
dc975aa2d3 | ||
|
|
29a95f0369 | ||
|
|
3680872619 | ||
|
|
4e874d88ae | ||
|
|
4a07cb35e4 | ||
|
|
c4934b4c96 | ||
|
|
81502c3d68 | ||
|
|
456ab03a7d | ||
|
|
005c00c272 | ||
|
|
cb02a33b9f | ||
|
|
b6ed31e034 | ||
|
|
fb4f1c1b40 | ||
|
|
1164f75969 | ||
|
|
ba89b80695 | ||
|
|
1ac44107df | ||
|
|
abde59822d | ||
|
|
a5ca5c7ae1 | ||
|
|
4f4de63759 | ||
|
|
deba4e0021 | ||
|
|
d2c782c1d5 | ||
|
|
32c45f2f0c | ||
|
|
e91615b754 | ||
|
|
d08da39fb1 | ||
|
|
3007bca61e | ||
|
|
de3c2d73d1 | ||
|
|
c2a11ce794 | ||
|
|
2d8cbd25bd | ||
|
|
736472587d | ||
|
|
11e0372017 | ||
|
|
bf3a1aebde | ||
|
|
d7fd927b2c | ||
|
|
d33d380dd4 | ||
|
|
771848b428 | ||
|
|
30e9ddc7cf | ||
|
|
b837a20ed4 | ||
|
|
d76c72e2a5 | ||
|
|
f8c4f6bf47 | ||
|
|
1b7429c3e1 | ||
|
|
4b4f26f54e | ||
|
|
47cf9aac3e | ||
|
|
128c535550 | ||
|
|
c34d429f57 | ||
|
|
0a549c8ee6 | ||
|
|
926b46dc03 | ||
|
|
8f4a62ac6d | ||
|
|
7b3c107758 | ||
|
|
ac66302ddc | ||
|
|
3653b388fd | ||
|
|
dfc8baf2ee | ||
|
|
a9d0befa46 | ||
|
|
4c72289ac8 | ||
|
|
7bc1467374 | ||
|
|
2e9329dbb8 | ||
|
|
be7b530f7e | ||
|
|
ef44d0ccf1 | ||
|
|
9738f31688 | ||
|
|
4e8644fbdc | ||
|
|
347df54502 | ||
|
|
77e1d6f95a | ||
|
|
6f71ddf04f | ||
|
|
5f0b1128f2 | ||
|
|
2ea2f824e8 | ||
|
|
be564dd2b8 | ||
|
|
621d207944 | ||
|
|
969377b39a | ||
|
|
a7246fbe44 | ||
|
|
70b2ff9081 | ||
|
|
d19e1bfdb7 | ||
|
|
cd9394467e | ||
|
|
2ee99a656b | ||
|
|
4157c55e26 | ||
|
|
4ada8ff81a | ||
|
|
c7b14bdf5c | ||
|
|
bec45403ee | ||
|
|
9df1182877 | ||
|
|
22403f9a03 | ||
|
|
add3baff74 | ||
|
|
e1d060398c | ||
|
|
eaa9610cca | ||
|
|
8785a55a5a | ||
|
|
0b38497524 | ||
|
|
67ee8bdb50 | ||
|
|
7da975c08c | ||
|
|
7d96f8e618 | ||
|
|
e8e2d84158 | ||
|
|
6f1db73026 | ||
|
|
7ef1c9fd31 | ||
|
|
6eeb809ea2 | ||
|
|
212394a30d | ||
|
|
d9b482b8c7 | ||
|
|
5a82e9b446 | ||
|
|
4221a1a3ed | ||
|
|
1cd95cd77b | ||
|
|
9cf5c4995e | ||
|
|
d579ec2be0 | ||
|
|
2f22762010 | ||
|
|
f869323aeb | ||
|
|
71e8538451 | ||
|
|
1cb458fe7c | ||
|
|
eadd182fd0 | ||
|
|
fb24d003f2 | ||
|
|
109de4c1d7 | ||
|
|
0da24961a6 | ||
|
|
826725f2ce | ||
|
|
31f9dbf3ad | ||
|
|
8b232d06cb | ||
|
|
cb3accc26b | ||
|
|
6de1b138f9 | ||
|
|
3c118b2f37 | ||
|
|
20e6a38163 | ||
|
|
fa034b0b10 | ||
|
|
b9953ba629 | ||
|
|
4ceb9aa2f1 | ||
|
|
b2cf42d138 | ||
|
|
7a6f24173b | ||
|
|
bf96a8fe50 | ||
|
|
849c551d55 | ||
|
|
83312e01f9 | ||
|
|
0a4a9b7220 | ||
|
|
66f4aea54a | ||
|
|
6f35fbb774 | ||
|
|
7847d22f95 | ||
|
|
709b8edcf4 | ||
|
|
f832ec6179 | ||
|
|
14c1772641 | ||
|
|
a1d3bbc09f | ||
|
|
9e6f7c8723 | ||
|
|
56bcb46bb2 | ||
|
|
54b7dfce2c | ||
|
|
ea6b49f354 | ||
|
|
e5b0d6a262 | ||
|
|
f35511b33e | ||
|
|
f04ba7d125 | ||
|
|
7243f34243 | ||
|
|
1bc53c3cc5 | ||
|
|
c05e294250 | ||
|
|
740cf0f11d | ||
|
|
5e9e205e78 | ||
|
|
6e18be8771 | ||
|
|
50bebf3181 | ||
|
|
3aba89dce8 | ||
|
|
586b164084 | ||
|
|
1eff2f4881 | ||
|
|
25f45ec19a | ||
|
|
735d86d16f | ||
|
|
de1d29f3c9 | ||
|
|
f7a55746b4 | ||
|
|
9fe81545fd | ||
|
|
f84872a0ab | ||
|
|
cf9c581de1 | ||
|
|
7fcadd3000 | ||
|
|
2510333c5d | ||
|
|
d067769947 | ||
|
|
21328da7ba | ||
|
|
330949ecb7 | ||
|
|
9144118f80 | ||
|
|
f8b820c1db | ||
|
|
d7fcdb0346 | ||
|
|
d42e6d348b | ||
|
|
b833f37f95 | ||
|
|
87be940822 | ||
|
|
e2f25d43cd | ||
|
|
5af731ab9d | ||
|
|
25236d524a | ||
|
|
4273525668 | ||
|
|
0725045330 | ||
|
|
29a18306bb | ||
|
|
7f039cce3f | ||
|
|
5a336088f6 | ||
|
|
8742050a0c | ||
|
|
8a16cea356 | ||
|
|
bdd1a171f2 | ||
|
|
ecb4da9ffb | ||
|
|
032a157c6f | ||
|
|
f6ac6d61ee | ||
|
|
0182cc7abb | ||
|
|
2625271654 | ||
|
|
8a95cd0acf | ||
|
|
1909663966 | ||
|
|
2a5015fed2 | ||
|
|
d8c5852abb | ||
|
|
aabe85b925 | ||
|
|
6411469886 | ||
|
|
0889583e81 | ||
|
|
6ccc5327f3 | ||
|
|
c6de68c037 | ||
|
|
b30503be93 | ||
|
|
3e93cc5c4b | ||
|
|
e2ad1ceced | ||
|
|
87bb09c66e | ||
|
|
cfdc330d25 | ||
|
|
11507a9a3f | ||
|
|
da1dd33e0e | ||
|
|
a6c91d19d4 | ||
|
|
903ad4eb04 | ||
|
|
f1fe8ca740 | ||
|
|
f27d946ccf | ||
|
|
8af3181752 | ||
|
|
89f51ab594 | ||
|
|
d84dc23bd4 | ||
|
|
56705cc3e6 | ||
|
|
15a5b1881d | ||
|
|
fff72f7598 | ||
|
|
a9c9598e55 | ||
|
|
bde02a3adf | ||
|
|
c6f24decb5 | ||
|
|
3f22d2881f | ||
|
|
8838b97167 | ||
|
|
f391a1d27c | ||
|
|
8fd4361bfe | ||
|
|
b002debe67 | ||
|
|
88a7cb625f | ||
|
|
99ce2f5fcb | ||
|
|
9792c6c531 | ||
|
|
7c02c9364c | ||
|
|
b968f12c4e | ||
|
|
4fc1f74319 | ||
|
|
f2f0f34076 | ||
|
|
001d44b10f | ||
|
|
8103e5d94a | ||
|
|
58d83fe5aa | ||
|
|
fa3cdf108c | ||
|
|
e46804810f | ||
|
|
79051b28a7 | ||
|
|
d99146da0b | ||
|
|
e44141be80 | ||
|
|
72b17165cb | ||
|
|
adb184adfd | ||
|
|
aab1cfc508 | ||
|
|
e5f88b543f | ||
|
|
5855a1693a | ||
|
|
ff25a20ab7 | ||
|
|
67f29fdb2b | ||
|
|
dcf51bc6ca | ||
|
|
dc031d9e01 | ||
|
|
b8aa694d07 | ||
|
|
871a6efd1b | ||
|
|
d1b28ef809 | ||
|
|
31b06af0ce | ||
|
|
c5c886d5bf | ||
|
|
54d922496d | ||
|
|
fe608af67c | ||
|
|
abb8e4cbbf | ||
|
|
447ea49f98 | ||
|
|
6f65f836d0 | ||
|
|
e1bfe4c40a | ||
|
|
b8f1203bca | ||
|
|
6992f170de | ||
|
|
3f31081113 | ||
|
|
51a08742c5 | ||
|
|
3e417d0357 | ||
|
|
c929410aef | ||
|
|
38b9cc70e6 | ||
|
|
7ccd84d1d3 | ||
|
|
33af2c6184 | ||
|
|
186587a506 | ||
|
|
3c1206db11 | ||
|
|
228244cf94 | ||
|
|
b383b178bf | ||
|
|
940a4315b3 | ||
|
|
cf80f86b18 | ||
|
|
969fbea602 | ||
|
|
7ac9b38ee5 | ||
|
|
bf0da79846 | ||
|
|
6cf24c4709 | ||
|
|
e49da10e01 | ||
|
|
60ba045b75 | ||
|
|
c28f509ff9 | ||
|
|
8770c37b33 | ||
|
|
d21847d1e2 | ||
|
|
1ebe4f676b | ||
|
|
8561069582 | ||
|
|
6105127d93 | ||
|
|
60c2d03c50 | ||
|
|
dc13c2a89f | ||
|
|
f24f03c3f8 | ||
|
|
170f8babad | ||
|
|
af4f5ab675 | ||
|
|
12a24c0d17 | ||
|
|
f70b3e6132 | ||
|
|
54a4c67a07 | ||
|
|
cef2956edb | ||
|
|
7751777b4e | ||
|
|
559cd3677f | ||
|
|
0ba4bc4456 | ||
|
|
da01567365 | ||
|
|
2b323bdb72 | ||
|
|
a94f0d8bb0 | ||
|
|
6ebdb3de98 | ||
|
|
d76a0981b8 | ||
|
|
6763d3e06b | ||
|
|
90ff353de2 | ||
|
|
ba2c13c57c | ||
|
|
064dd5b681 | ||
|
|
010612b115 | ||
|
|
afcda9e8bd | ||
|
|
b83f1b5c1a | ||
|
|
e781f3ed93 | ||
|
|
e1983ad8a9 | ||
|
|
d24802b358 | ||
|
|
63020a2062 | ||
|
|
be591d66f1 | ||
|
|
02d5664705 | ||
|
|
6343b103f3 | ||
|
|
1b647249e6 | ||
|
|
2a13008ffc | ||
|
|
440d078d86 | ||
|
|
11719a99f6 | ||
|
|
8e3c6ef34d | ||
|
|
b5b4716db8 | ||
|
|
5dacedf9a4 | ||
|
|
9fb770953d | ||
|
|
a631acb591 | ||
|
|
fe12e97fba | ||
|
|
480b92c326 | ||
|
|
e1242f37d1 | ||
|
|
c2f9713a6c | ||
|
|
341b57f683 | ||
|
|
7870252ba2 | ||
|
|
f8cf9d39a7 | ||
|
|
782c8d90cf | ||
|
|
cb71b7ca0a | ||
|
|
a57fd27164 | ||
|
|
f11a35e4ed | ||
|
|
a88d7272a6 | ||
|
|
f4f99f6d6d | ||
|
|
2fd6dc1f77 | ||
|
|
92d6183481 | ||
|
|
b18c659206 | ||
|
|
0754b5fd47 | ||
|
|
9bef77b3b4 | ||
|
|
2d5f8dd220 | ||
|
|
a663463198 | ||
|
|
8cdd68011e | ||
|
|
d89b9f56af | ||
|
|
60f32daf07 | ||
|
|
652a084532 | ||
|
|
3eb0ba07f9 | ||
|
|
9031022553 | ||
|
|
d12bb6de2d | ||
|
|
f96df3d8c6 | ||
|
|
5a906c3cfc | ||
|
|
1c9e478cc9 | ||
|
|
2786f28664 | ||
|
|
9b83ef1c3e | ||
|
|
5a1cceced4 | ||
|
|
2e872396b9 | ||
|
|
d38d30c846 | ||
|
|
255dd4f387 | ||
|
|
361c2e861f | ||
|
|
7b2c45d9e9 | ||
|
|
2285a0ab76 | ||
|
|
fb98371a01 | ||
|
|
8e115b3c65 | ||
|
|
3c4f165eb7 | ||
|
|
3f345342a3 | ||
|
|
10c48b645f | ||
|
|
a00d36d29e |
133 changed files with 7570 additions and 2289 deletions
35
.github/workflows/check.yml
vendored
Normal file
35
.github/workflows/check.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
name: Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
env:
|
||||
- lint
|
||||
- format
|
||||
- audit
|
||||
- package
|
||||
- docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install prerequisites
|
||||
run: python -m pip install tox
|
||||
|
||||
- name: Run ${{ matrix.env }}
|
||||
run: tox -e ${{ matrix.env }}
|
||||
44
.github/workflows/release.yml
vendored
Normal file
44
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: '1'
|
||||
PY_COLORS: '1'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'jazzband/django-analytical'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
**/pyproject.toml
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install tox
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
tox -e package
|
||||
|
||||
- name: Upload packages to Jazzband
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: jazzband
|
||||
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
|
||||
repository_url: https://jazzband.co/projects/django-analytical/upload
|
||||
58
.github/workflows/test.yml
vendored
Normal file
58
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: '1'
|
||||
PY_COLORS: '1'
|
||||
|
||||
jobs:
|
||||
python-django:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
python-version:
|
||||
- '3.10'
|
||||
- '3.11'
|
||||
- '3.12'
|
||||
- '3.13'
|
||||
django-version:
|
||||
- '4.2'
|
||||
- '5.1'
|
||||
- '5.2'
|
||||
include:
|
||||
- { python-version: '3.9', django-version: '4.2' }
|
||||
exclude:
|
||||
- { python-version: '3.13', django-version: '4.2' }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
**/pyproject.toml
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install tox tox-gh-actions
|
||||
|
||||
- name: Tox tests (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }})
|
||||
run: tox
|
||||
env:
|
||||
DJANGO: ${{ matrix.django-version }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
name: Python ${{ matrix.python-version }}
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
|
|
@ -1,11 +1,23 @@
|
|||
/.*
|
||||
!/.gitignore
|
||||
|
||||
/build
|
||||
/dist
|
||||
/MANIFEST
|
||||
|
||||
/docs/_templates/layout.html
|
||||
.*.sw?
|
||||
/*.geany
|
||||
/.idea
|
||||
/.tox
|
||||
/.vscode
|
||||
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
/.coverage
|
||||
/coverage.xml
|
||||
/tests/*-report.json
|
||||
/tests/*-report.xml
|
||||
|
||||
/build
|
||||
/dist
|
||||
/docs/_build
|
||||
/docs/_templates/layout.html
|
||||
/MANIFEST
|
||||
*.egg-info
|
||||
|
||||
/requirements.txt
|
||||
/uv.lock
|
||||
|
|
|
|||
1
.pre-commit-config.yaml
Normal file
1
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1 @@
|
|||
repos: []
|
||||
18
.readthedocs.yaml
Normal file
18
.readthedocs.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Read the Docs configuration file
|
||||
# https://docs.readthedocs.io/en/stable/config-file/v2.html
|
||||
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# We recommend specifying your dependencies to enable reproducible builds:
|
||||
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
# python:
|
||||
# install:
|
||||
# - requirements: docs/requirements.txt
|
||||
18
AUTHORS.rst
18
AUTHORS.rst
|
|
@ -1,18 +0,0 @@
|
|||
The django-analytical package was written by `Joost Cassee`_, with
|
||||
contributions from `Eric Davis`_, `Paul Oswald`_, `Uros Trebec`_ and
|
||||
others.
|
||||
|
||||
Included Javascript code snippets for integration of the analytics
|
||||
services were written by the respective service providers.
|
||||
|
||||
The application was inspired by and uses ideas from Analytical_, Joshua
|
||||
Krall's all-purpose analytics front-end for Rails.
|
||||
|
||||
The work on Crazy Egg was made possible by `Bateau Knowledge`_.
|
||||
|
||||
.. _`Joost Cassee`: mailto:joost@cassee.net
|
||||
.. _`Eric Davis`: https://github.com/edavis
|
||||
.. _`Paul Oswald`: https://github.com/poswald
|
||||
.. _`Uros Trebec`: https://github.com/failedguidedog
|
||||
.. _Analytical: https://github.com/jkrall/analytical
|
||||
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
|
||||
165
CHANGELOG.rst
165
CHANGELOG.rst
|
|
@ -1,3 +1,162 @@
|
|||
(unreleased)
|
||||
------------
|
||||
* Fix GA gtag user_id setup and add support for custom dimensions (Erick Massip)
|
||||
* Change spelling of "JavaScript" across all files in docstrings and docs
|
||||
(Peter Bittner)
|
||||
* Ask site visitors for consent when using Matomo (Julian Haluska & Ronard Luna)
|
||||
|
||||
Version 3.2.0
|
||||
-------------
|
||||
* Remove deprecated Piwik integration. Use Matomo instead! (Peter Bittner)
|
||||
* Migrate packaging from setup.py to pyproject.toml with Ruff for linting
|
||||
and formatting (Peter Bittner)
|
||||
* Remove obsolete type attribute in script tags for JavaScript (Peter Bittner)
|
||||
* Drop the end-of-life Python 3.8 as required by changed semantics of the
|
||||
license metadata field (Peter Bittner)
|
||||
* Remove AUTHORS file to avoid confusion; this is now metadata maintained
|
||||
in pyproject.toml (Peter Bittner)
|
||||
* Add more configuration options for Woopra (Peter Bittner)
|
||||
|
||||
Version 3.1.0
|
||||
-------------
|
||||
* Rename default branch from master to main (Peter Bittner, Jannis Leidel)
|
||||
* Modernize packaging setup, add pyproject.toml (Peter Bittner)
|
||||
* Integrate isort, reorganize imports (David Smith)
|
||||
* Refactor test suite from Python unit tests to Pytest (David Smith)
|
||||
* Add Heap integration (Garrett Coakley)
|
||||
* Drop Django 3.1, cover Django 4.0 and Python 3.10 in test suite (David Smith)
|
||||
|
||||
Version 3.0.0
|
||||
-------------
|
||||
* Add support for Lucky Orange (Peter Bittner)
|
||||
* Add missing instructions in Installation chapter of the docs (Peter Bittner)
|
||||
* Migrate test setup to Pytest (David Smith, Peter Bittner, Pi Delport)
|
||||
* Support Django 3.1 and Python 3.9, drop Django 1.11 and Python 2.7/3.5 (David Smith)
|
||||
* Migrate from Travis CI to GitHub Actions (Jannis Leidel)
|
||||
* Update accepted patterns (regex) for Google Analytics GTag (Taha Rushain)
|
||||
* Scope Piwik warning to use of Piwik (Hugo Barrera)
|
||||
* Add ``user_id`` to Google Analytics GTag (Sean Wallace)
|
||||
|
||||
Version 2.6.0
|
||||
-------------
|
||||
* Support Django 3.0 and Python 3.8, drop Django 2.1
|
||||
* Add support for Google Analytics Tag Manager (Marc Bourqui)
|
||||
* Add Matomo, the renamed version of Piwik (Scott Karlin)
|
||||
* Move Joost's project over to the Jazzband
|
||||
|
||||
Version 2.5.0
|
||||
-------------
|
||||
* Add support for Google analytics.js (Marc Bourqui)
|
||||
* Add support for Intercom HMAC identity verification (Pi Delport)
|
||||
* Add support for Hotjar (Pi Delport)
|
||||
* Make sure _trackPageview happens before other settings in Google Analytics
|
||||
(Diederik van der Boor)
|
||||
|
||||
Version 2.4.0
|
||||
-------------
|
||||
* Support Django 2.0 (Matthäus G. Chajdas)
|
||||
|
||||
Version 2.3.0
|
||||
-------------
|
||||
* Add Facebook Pixel support (Pi Delport)
|
||||
* Add Python 3.6 and Django 1.10 & 1.11 tests (Pi Delport)
|
||||
* Drop Python 3.2 support
|
||||
|
||||
Version 2.2.2
|
||||
-------------
|
||||
* Allow port in Piwik domain path. (Alex Ramsay)
|
||||
|
||||
Version 2.2.1
|
||||
-------------
|
||||
* Fix a bug with the extra Google Analytics variables also pushing the `_gat.`
|
||||
flag onto the configuration array.
|
||||
|
||||
Version 2.2.0
|
||||
-------------
|
||||
* Update Woopra JavaScript snippet (Aleck Landgraf)
|
||||
|
||||
Version 2.1.0
|
||||
-------------
|
||||
* Support Rating\@mail.ru (Nikolay Korotkiy)
|
||||
* Support Yandex.Metrica (Nikolay Korotkiy)
|
||||
* Add support for extra Google Analytics variables (Steve Schwarz)
|
||||
* Remove support for Reinvigorate (service shut down)
|
||||
|
||||
Version 2.0.0
|
||||
-------------
|
||||
* Support Django 1.9, drop support for Django < 1.7 (Hugo Osvaldo Barrera)
|
||||
* Support custom user models with an alternative username field (Brad Pitcher)
|
||||
|
||||
Version 1.0.0
|
||||
-------------
|
||||
* Add Piwik user variables support (Alexandre Pocquet)
|
||||
|
||||
Version 0.22.0
|
||||
--------------
|
||||
* Mark package as Python 3 compatible (Martín Gaitán)
|
||||
* Fix Clickmap tracker id regular expression
|
||||
* Test with Django 1.8
|
||||
|
||||
Version 0.21.0
|
||||
--------------
|
||||
* Added compatibility with Python 3 (Eric Amador)
|
||||
|
||||
Version 0.20.0
|
||||
--------------
|
||||
* Support Django 1.7 (Craig Bruce)
|
||||
* Update Mixpanel identity code (Martín Gaitán)
|
||||
* Identify authenticated users in Uservoice (Martín Gaitán)
|
||||
* Add full name and email to Olark (Scott Adams)
|
||||
|
||||
Version 0.19.0
|
||||
--------------
|
||||
* Add Piwik integration (Peter Bittner)
|
||||
|
||||
Version 0.18.0
|
||||
--------------
|
||||
* Update HubSpot code (Craig Bruce)
|
||||
|
||||
Version 0.17.1
|
||||
--------------
|
||||
* Fix typo in Intercom.io support (Steven Skoczen)
|
||||
|
||||
Version 0.17.0
|
||||
--------------
|
||||
* Update UserVoice support (Martín Gaitán)
|
||||
* Add support for Intercom.io (Steven Skoczen)
|
||||
|
||||
Version 0.16.0
|
||||
--------------
|
||||
* Add support for GA Display Advertising features (Max Arnold)
|
||||
|
||||
Version 0.15.0
|
||||
--------------
|
||||
* Add IP anonymization setting to GA tracking pixel (Tinnet Coronam)
|
||||
* Include Django 1.5 in tox.ini (Tinnet Coronam)
|
||||
* Add Clickmap integration (Philippe O. Wagner)
|
||||
|
||||
Version 0.14.0
|
||||
--------------
|
||||
* Update mixpanel integration to latest code (Simon Ye)
|
||||
|
||||
Version 0.13.0
|
||||
--------------
|
||||
* Add support for the KISSmetrics alias feature (Sandra Mau)
|
||||
* Update testing code for Django 1.4 (Pi Delport)
|
||||
|
||||
Version 0.12.0
|
||||
--------------
|
||||
* Add support for the UserVoice service.
|
||||
|
||||
Version 0.11.3
|
||||
--------------
|
||||
* Added support for Gaug.es (Steven Skoczen)
|
||||
|
||||
Version 0.11.2
|
||||
--------------
|
||||
* Fix Spring Metrics custom variables.
|
||||
* Update Spring Metrics documentation.
|
||||
|
||||
Version 0.11.1
|
||||
--------------
|
||||
* Fix Woopra for anonymous users (Steven Skoczen).
|
||||
|
|
@ -6,7 +165,7 @@ Version 0.11.0
|
|||
--------------
|
||||
* Added support for the Spring Metrics service.
|
||||
* Allow sending events and properties to KISSmetrics (Paul Oswald).
|
||||
* Add support for the Site Speed report in Google Analytics (Uros
|
||||
* Add support for the Site Speed report in Google Analytics (Uros
|
||||
Trebec).
|
||||
|
||||
Version 0.10.0
|
||||
|
|
@ -26,7 +185,7 @@ Version 0.9.1
|
|||
Version 0.9.0
|
||||
-------------
|
||||
* Updated Clicky tracking code to support multiple site ids.
|
||||
* Fixed Chartbeat auto-domain bug when the Sites framework is not used
|
||||
* Fixed Chartbeat auto-domain bug when the Sites framework is not used
|
||||
(Eric Davis).
|
||||
* Improved testing code (Eric Davis).
|
||||
|
||||
|
|
@ -59,7 +218,7 @@ Version 0.5.0
|
|||
-------------
|
||||
* Split off Geckoboard support into django-geckoboard_.
|
||||
|
||||
.. _django-geckoboard: http://pypi.python.org/pypi/django-geckoboard
|
||||
.. _django-geckoboard: https://pypi.org/project/django-geckoboard
|
||||
|
||||
Version 0.4.0
|
||||
-------------
|
||||
|
|
|
|||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Code of Conduct
|
||||
|
||||
As contributors and maintainers of the Jazzband projects, and in the interest of
|
||||
fostering an open and welcoming community, we pledge to respect all people who
|
||||
contribute through reporting issues, posting feature requests, updating documentation,
|
||||
submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in the Jazzband a harassment-free experience
|
||||
for everyone, regardless of the level of experience, gender, gender identity and
|
||||
expression, sexual orientation, disability, personal appearance, body size, race,
|
||||
ethnicity, age, religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery
|
||||
- Personal attacks
|
||||
- Trolling or insulting/derogatory comments
|
||||
- Public or private harassment
|
||||
- Publishing other's private information, such as physical or electronic addresses,
|
||||
without explicit permission
|
||||
- Other unethical or unprofessional conduct
|
||||
|
||||
The Jazzband roadies have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are not
|
||||
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
|
||||
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
By adopting this Code of Conduct, the roadies commit themselves to fairly and
|
||||
consistently applying these principles to every aspect of managing the jazzband
|
||||
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
|
||||
removed from the Jazzband roadies.
|
||||
|
||||
This code of conduct applies both within project spaces and in public spaces when an
|
||||
individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
|
||||
investigated and will result in a response that is deemed necessary and appropriate to
|
||||
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
|
||||
reporter of an incident.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
|
||||
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
|
||||
|
||||
[homepage]: https://contributor-covenant.org
|
||||
[version]: https://contributor-covenant.org/version/1/3/0/
|
||||
5
CONTRIBUTING.rst
Normal file
5
CONTRIBUTING.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.. image:: https://jazzband.co/static/img/jazzband.svg
|
||||
:target: https://jazzband.co/
|
||||
:alt: Jazzband
|
||||
|
||||
This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree to abide by the `Contributor Code of Conduct <https://jazzband.co/about/conduct>`_ and follow the `guidelines <https://jazzband.co/about/guidelines>`_.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (C) 2011 Joost Cassee and others
|
||||
Copyright (C) 2011-2019 Joost Cassee and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
include LICENSE.txt *.rst
|
||||
recursive-include docs *.rst *.py
|
||||
recursive-include tests *.py
|
||||
|
|
|
|||
210
README.rst
210
README.rst
|
|
@ -1,69 +1,141 @@
|
|||
django-analytical
|
||||
=================
|
||||
|
||||
The django-analytical application integrates analytics services into a
|
||||
Django_ project.
|
||||
|
||||
Using an analytics service with a Django project means adding Javascript
|
||||
tracking code to the project templates. Of course, every service has
|
||||
its own specific installation instructions. Furthermore, you need to
|
||||
include your unique identifiers, which then end up in the templates.
|
||||
Not very nice.
|
||||
|
||||
This application hides the details of the different analytics services
|
||||
behind a generic interface, and keeps personal information and
|
||||
configuration out of the templates. Its goal is to make the basic
|
||||
set-up very simple, while allowing advanced users to customize tracking.
|
||||
Each service is set up as recommended by the services themselves, using
|
||||
an asynchronous version of the Javascript code if possible.
|
||||
|
||||
Currently supported services:
|
||||
|
||||
* `Chartbeat`_ traffic analysis
|
||||
* `Clicky`_ traffic analysis
|
||||
* `Crazy Egg`_ visual click tracking
|
||||
* `Google Analytics`_ traffic analysis
|
||||
* `GoSquared`_ traffic monitoring
|
||||
* `HubSpot`_ inbound marketing
|
||||
* `KISSinsights`_ feedback surveys
|
||||
* `KISSmetrics`_ funnel analysis
|
||||
* `Mixpanel`_ event tracking
|
||||
* `Olark`_ visitor chat
|
||||
* `Optimizely`_ A/B testing
|
||||
* `Performable`_ web analytics and landing pages
|
||||
* `Reinvigorate`_ visitor tracking
|
||||
* `SnapEngage`_ live chat
|
||||
* `Spring Metrics`_ conversion tracking
|
||||
* `Woopra`_ web analytics
|
||||
|
||||
The documentation can be found in the ``docs`` directory or `read
|
||||
online`_. The source code and issue tracker are generously `hosted by
|
||||
GitHub`_.
|
||||
|
||||
If you want to help out with the development of django-analytical, by
|
||||
posting detailed bug reports, proposing new features or other analytics
|
||||
services to support, or suggesting documentation improvements, use the
|
||||
`issue tracker`_. If you want to get your hands dirty, great! Clone
|
||||
the repository, make changes and send a pull request. Please do create
|
||||
an issue to discuss your plans.
|
||||
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
.. _Chartbeat: http://www.chartbeat.com/
|
||||
.. _Clicky: http://getclicky.com/
|
||||
.. _`Crazy Egg`: http://www.crazyegg.com/
|
||||
.. _`Google Analytics`: http://www.google.com/analytics/
|
||||
.. _GoSquared: http://www.gosquared.com/
|
||||
.. _HubSpot: http://www.hubspot.com/
|
||||
.. _KISSinsights: http://www.kissinsights.com/
|
||||
.. _KISSmetrics: http://www.kissmetrics.com/
|
||||
.. _Mixpanel: http://www.mixpanel.com/
|
||||
.. _Olark: http://www.olark.com/
|
||||
.. _Optimizely: http://www.optimizely.com/
|
||||
.. _Performable: http://www.performable.com/
|
||||
.. _Reinvigorate: http://www.reinvigorate.com/
|
||||
.. _SnapEngage: http://www.snapengage.com/
|
||||
.. _`Spring Metrics`: http://www.springmetrics.com/
|
||||
.. _Woopra: http://www.woopra.com/
|
||||
.. _`read online`: http://packages.python.org/django-analytical/
|
||||
.. _`hosted by GitHub`: http://github.com/jcassee/django-analytical
|
||||
.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
|
||||
django-analytical |latest-version|
|
||||
==================================
|
||||
|
||||
|build-status| |coverage| |python-support| |license| |jazzband|
|
||||
|
||||
The django-analytical application integrates analytics services into a
|
||||
Django_ project.
|
||||
|
||||
.. start docs include
|
||||
|
||||
Using an analytics service with a Django project means adding JavaScript
|
||||
tracking code to the project templates. Of course, every service has
|
||||
its own specific installation instructions. Furthermore, you need to
|
||||
include your unique identifiers, which then end up in the templates.
|
||||
Not very nice.
|
||||
|
||||
This application hides the details of the different analytics services
|
||||
behind a generic interface, and keeps personal information and
|
||||
configuration out of the templates. Its goal is to make the basic
|
||||
set-up very simple, while allowing advanced users to customize tracking.
|
||||
Each service is set up as recommended by the services themselves, using
|
||||
an asynchronous version of the JavaScript code if possible.
|
||||
|
||||
.. end docs include
|
||||
|
||||
.. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg
|
||||
:alt: Latest version on PyPI
|
||||
:target: https://pypi.org/project/django-analytical/
|
||||
.. |build-status| image:: https://github.com/jazzband/django-analytical/workflows/Test/badge.svg
|
||||
:target: https://github.com/jazzband/django-analytical/actions
|
||||
:alt: GitHub Actions
|
||||
.. |coverage| image:: https://codecov.io/gh/jazzband/django-analytical/branch/main/graph/badge.svg
|
||||
:alt: Test coverage
|
||||
:target: https://codecov.io/gh/jazzband/django-analytical
|
||||
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg
|
||||
:target: https://pypi.org/project/django-analytical/
|
||||
:alt: Python versions
|
||||
.. |license| image:: https://img.shields.io/pypi/l/django-analytical.svg
|
||||
:alt: Software license
|
||||
:target: https://github.com/jazzband/django-analytical/blob/main/LICENSE.txt
|
||||
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
|
||||
:alt: Jazzband
|
||||
:target: https://jazzband.co/projects/django-analytical
|
||||
.. _`Django`: http://www.djangoproject.com/
|
||||
|
||||
Currently Supported Services
|
||||
----------------------------
|
||||
|
||||
* `Chartbeat`_ traffic analysis
|
||||
* `Clickmap`_ visual click tracking
|
||||
* `Clicky`_ traffic analysis
|
||||
* `Crazy Egg`_ visual click tracking
|
||||
* `Facebook Pixel`_ advertising analytics
|
||||
* `Gaug.es`_ real time web analytics
|
||||
* `Google Analytics`_ traffic analysis
|
||||
* `GoSquared`_ traffic monitoring
|
||||
* `Heap`_ analytics and events tracking
|
||||
* `Hotjar`_ analytics and user feedback
|
||||
* `HubSpot`_ inbound marketing
|
||||
* `Intercom`_ live chat and support
|
||||
* `KISSinsights`_ feedback surveys
|
||||
* `KISSmetrics`_ funnel analysis
|
||||
* `Lucky Orange`_ analytics and user feedback
|
||||
* `Mixpanel`_ event tracking
|
||||
* `Olark`_ visitor chat
|
||||
* `Optimizely`_ A/B testing
|
||||
* `Performable`_ web analytics and landing pages
|
||||
* `Matomo (formerly Piwik)`_ open source web analytics
|
||||
* `Rating\@Mail.ru`_ web analytics
|
||||
* `SnapEngage`_ live chat
|
||||
* `Spring Metrics`_ conversion tracking
|
||||
* `UserVoice`_ user feedback and helpdesk
|
||||
* `Woopra`_ web analytics
|
||||
* `Yandex.Metrica`_ web analytics
|
||||
|
||||
.. _`Chartbeat`: http://www.chartbeat.com/
|
||||
.. _`Clickmap`: http://clickmap.ch/
|
||||
.. _`Clicky`: http://getclicky.com/
|
||||
.. _`Crazy Egg`: http://www.crazyegg.com/
|
||||
.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
|
||||
.. _`Gaug.es`: http://get.gaug.es/
|
||||
.. _`Google Analytics`: http://www.google.com/analytics/
|
||||
.. _`GoSquared`: http://www.gosquared.com/
|
||||
.. _`Heap`: https://heapanalytics.com/
|
||||
.. _`Hotjar`: https://www.hotjar.com/
|
||||
.. _`HubSpot`: http://www.hubspot.com/
|
||||
.. _`Intercom`: http://www.intercom.io/
|
||||
.. _`KISSinsights`: http://www.kissinsights.com/
|
||||
.. _`KISSmetrics`: http://www.kissmetrics.com/
|
||||
.. _`Lucky Orange`: http://www.luckyorange.com/
|
||||
.. _`Mixpanel`: http://www.mixpanel.com/
|
||||
.. _`Olark`: http://www.olark.com/
|
||||
.. _`Optimizely`: http://www.optimizely.com/
|
||||
.. _`Performable`: http://www.performable.com/
|
||||
.. _`Matomo (formerly Piwik)`: https://matomo.org
|
||||
.. _`Rating\@Mail.ru`: http://top.mail.ru/
|
||||
.. _`SnapEngage`: http://www.snapengage.com/
|
||||
.. _`Spring Metrics`: http://www.springmetrics.com/
|
||||
.. _`UserVoice`: http://www.uservoice.com/
|
||||
.. _`Woopra`: http://www.woopra.com/
|
||||
.. _`Yandex.Metrica`: http://metrica.yandex.com
|
||||
|
||||
Documentation and Support
|
||||
-------------------------
|
||||
|
||||
The documentation can be found in the ``docs`` directory or `read
|
||||
online`_. The source code and issue tracker are generously `hosted by
|
||||
GitHub`_.
|
||||
|
||||
.. _`read online`: https://django-analytical.readthedocs.io/
|
||||
.. _`hosted by GitHub`: https://github.com/jazzband/django-analytical
|
||||
.. _`Gitter chat room`: https://gitter.im/jazzband/django-analytical
|
||||
|
||||
How To Contribute
|
||||
-----------------
|
||||
|
||||
.. start contribute include
|
||||
|
||||
If you want to help out with the development of django-analytical, by
|
||||
posting detailed bug reports, proposing new features or other analytics
|
||||
services to support, or suggesting documentation improvements, use the
|
||||
`issue tracker`_. If you want to get your hands dirty, great! Clone
|
||||
the repository, make changes and place a `pull request`_. Creating an
|
||||
issue to discuss your plans is useful.
|
||||
|
||||
At the end, don't forget to add yourself to the `list of authors`_ and
|
||||
update the `changelog`_ with a short description of your contribution.
|
||||
We want you to stand out from the crowd as an open source superstar! ✦
|
||||
|
||||
This is a `Jazzband`_ project. By contributing you agree to abide by the
|
||||
`Contributor Code of Conduct`_ and follow the `guidelines`_.
|
||||
|
||||
.. _`issue tracker`: https://github.com/jazzband/django-analytical/issues
|
||||
.. _`pull request`: https://github.com/jazzband/django-analytical/pulls
|
||||
.. _`list of authors`: https://github.com/jazzband/django-analytical/blob/main/pyproject.toml
|
||||
.. _`changelog`: https://github.com/jazzband/django-analytical/blob/main/CHANGELOG.rst
|
||||
.. _`Jazzband`: https://jazzband.co
|
||||
.. _`Contributor Code of Conduct`: https://jazzband.co/about/conduct
|
||||
.. _`guidelines`: https://jazzband.co/about/guidelines
|
||||
|
||||
.. end contribute include
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
"""
|
||||
Analytics service integration for Django
|
||||
========================================
|
||||
|
||||
The django-analytical application integrates analytics services into a
|
||||
Django_ project. See the ``docs`` directory for more information.
|
||||
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
Analytics service integration for Django projects.
|
||||
"""
|
||||
|
||||
__author__ = "Joost Cassee"
|
||||
__email__ = "joost@cassee.net"
|
||||
__version__ = "0.11.1"
|
||||
__copyright__ = "Copyright (C) 2011 Joost Cassee and others"
|
||||
__license__ = "MIT License"
|
||||
__version__ = '3.2.0'
|
||||
|
|
|
|||
|
|
@ -2,38 +2,47 @@
|
|||
Analytical template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from importlib import import_module
|
||||
|
||||
from django import template
|
||||
from django.template import Node, TemplateSyntaxError
|
||||
from django.utils.importlib import import_module
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
TAG_LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom']
|
||||
TAG_POSITIONS = ['first', None, 'last']
|
||||
TAG_MODULES = [
|
||||
'analytical.chartbeat',
|
||||
'analytical.clickmap',
|
||||
'analytical.clicky',
|
||||
'analytical.crazy_egg',
|
||||
'analytical.facebook_pixel',
|
||||
'analytical.gauges',
|
||||
'analytical.google_analytics',
|
||||
'analytical.google_analytics_js',
|
||||
'analytical.google_analytics_gtag',
|
||||
'analytical.gosquared',
|
||||
'analytical.heap',
|
||||
'analytical.hotjar',
|
||||
'analytical.hubspot',
|
||||
'analytical.intercom',
|
||||
'analytical.kiss_insights',
|
||||
'analytical.kiss_metrics',
|
||||
'analytical.luckyorange',
|
||||
'analytical.matomo',
|
||||
'analytical.mixpanel',
|
||||
'analytical.olark',
|
||||
'analytical.optimizely',
|
||||
'analytical.performable',
|
||||
'analytical.reinvigorate',
|
||||
'analytical.rating_mailru',
|
||||
'analytical.snapengage',
|
||||
'analytical.spring_metrics',
|
||||
'analytical.uservoice',
|
||||
'analytical.woopra',
|
||||
'analytical.yandex_metrica',
|
||||
]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
register = template.Library()
|
||||
|
||||
|
|
@ -44,8 +53,10 @@ def _location_tag(location):
|
|||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0])
|
||||
return AnalyticalNode(location)
|
||||
|
||||
return analytical_tag
|
||||
|
||||
|
||||
for loc in TAG_LOCATIONS:
|
||||
register.tag('analytical_%s' % loc, _location_tag(loc))
|
||||
|
||||
|
|
@ -55,27 +66,31 @@ class AnalyticalNode(Node):
|
|||
self.nodes = [node_cls() for node_cls in template_nodes[location]]
|
||||
|
||||
def render(self, context):
|
||||
return "".join([node.render(context) for node in self.nodes])
|
||||
return ''.join([node.render(context) for node in self.nodes])
|
||||
|
||||
|
||||
def _load_template_nodes():
|
||||
template_nodes = dict((l, dict((p, []) for p in TAG_POSITIONS))
|
||||
for l in TAG_LOCATIONS)
|
||||
template_nodes = {loc: {pos: [] for pos in TAG_POSITIONS} for loc in TAG_LOCATIONS}
|
||||
|
||||
def add_node_cls(location, node, position=None):
|
||||
template_nodes[location][position].append(node)
|
||||
|
||||
for path in TAG_MODULES:
|
||||
module = _import_tag_module(path)
|
||||
try:
|
||||
module.contribute_to_analytical(add_node_cls)
|
||||
except AnalyticalException, e:
|
||||
except AnalyticalException as e:
|
||||
logger.debug("not loading tags from '%s': %s", path, e)
|
||||
for location in TAG_LOCATIONS:
|
||||
template_nodes[location] = sum((template_nodes[location][p]
|
||||
for p in TAG_POSITIONS), [])
|
||||
template_nodes[location] = sum(
|
||||
(template_nodes[location][p] for p in TAG_POSITIONS), []
|
||||
)
|
||||
return template_nodes
|
||||
|
||||
|
||||
def _import_tag_module(path):
|
||||
app_name, lib_name = path.rsplit('.', 1)
|
||||
return import_module("%s.templatetags.%s" % (app_name, lib_name))
|
||||
return import_module('%s.templatetags.%s' % (app_name, lib_name))
|
||||
|
||||
|
||||
template_nodes = _load_template_nodes()
|
||||
|
|
|
|||
|
|
@ -2,23 +2,19 @@
|
|||
Chartbeat template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, get_required_setting
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
USER_ID_RE = re.compile(r'^\d+$')
|
||||
INIT_CODE = """<script type="text/javascript">var _sf_startpt=(new Date()).getTime()</script>"""
|
||||
INIT_CODE = """<script>var _sf_startpt=(new Date()).getTime()</script>"""
|
||||
SETUP_CODE = """
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var _sf_async_config=%(config)s;
|
||||
(function(){
|
||||
function loadChartbeat() {
|
||||
|
|
@ -36,7 +32,7 @@ SETUP_CODE = """
|
|||
loadChartbeat : function() { oldonload(); loadChartbeat(); };
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
""" # noqa
|
||||
DOMAIN_CONTEXT_KEY = 'chartbeat_domain'
|
||||
|
||||
|
||||
|
|
@ -48,17 +44,18 @@ def chartbeat_top(parser, token):
|
|||
"""
|
||||
Top Chartbeat template tag.
|
||||
|
||||
Render the top Javascript code for Chartbeat.
|
||||
Render the top JavaScript code for Chartbeat.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return ChartbeatTopNode()
|
||||
|
||||
|
||||
class ChartbeatTopNode(Node):
|
||||
def render(self, context):
|
||||
if is_internal_ip(context):
|
||||
return disable_html(INIT_CODE, "Chartbeat")
|
||||
return disable_html(INIT_CODE, 'Chartbeat')
|
||||
return INIT_CODE
|
||||
|
||||
|
||||
|
|
@ -67,7 +64,7 @@ def chartbeat_bottom(parser, token):
|
|||
"""
|
||||
Bottom Chartbeat template tag.
|
||||
|
||||
Render the bottom Javascript code for Chartbeat. You must supply
|
||||
Render the bottom JavaScript code for Chartbeat. You must supply
|
||||
your Chartbeat User ID (as a string) in the ``CHARTBEAT_USER_ID``
|
||||
setting.
|
||||
"""
|
||||
|
|
@ -76,17 +73,19 @@ def chartbeat_bottom(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return ChartbeatBottomNode()
|
||||
|
||||
|
||||
class ChartbeatBottomNode(Node):
|
||||
def __init__(self):
|
||||
self.user_id = get_required_setting('CHARTBEAT_USER_ID', USER_ID_RE,
|
||||
"must be (a string containing) a number")
|
||||
self.user_id = get_required_setting(
|
||||
'CHARTBEAT_USER_ID', USER_ID_RE, 'must be (a string containing) a number'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
config = {'uid': self.user_id}
|
||||
domain = _get_domain(context)
|
||||
if domain is not None:
|
||||
config['domain'] = domain
|
||||
html = SETUP_CODE % {'config': simplejson.dumps(config)}
|
||||
html = SETUP_CODE % {'config': json.dumps(config, sort_keys=True)}
|
||||
if is_internal_ip(context, 'CHARTBEAT'):
|
||||
html = disable_html(html, 'Chartbeat')
|
||||
return html
|
||||
|
|
@ -107,7 +106,9 @@ def _get_domain(context):
|
|||
if 'django.contrib.sites' not in settings.INSTALLED_APPS:
|
||||
return
|
||||
elif getattr(settings, 'CHARTBEAT_AUTO_DOMAIN', True):
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
try:
|
||||
return Site.objects.get_current().domain
|
||||
except (ImproperlyConfigured, Site.DoesNotExist): #pylint: disable=E1101
|
||||
except (ImproperlyConfigured, Site.DoesNotExist): # pylint: disable=E1101
|
||||
return
|
||||
|
|
|
|||
60
analytical/templatetags/clickmap.py
Normal file
60
analytical/templatetags/clickmap.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Clickmap template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
CLICKMAP_TRACKER_ID_RE = re.compile(r'^\w+$')
|
||||
TRACKING_CODE = """
|
||||
<script>
|
||||
var clickmapConfig = {tracker: '%(tracker_id)s', version:'2'};
|
||||
window.clickmapAsyncInit = function(){ __clickmap.init(clickmapConfig); };
|
||||
(function() { var _cmf = document.createElement('script'); _cmf.async = true;
|
||||
_cmf.src = document.location.protocol + '//www.clickmap.ch/tracker.js?t=';
|
||||
_cmf.src += clickmapConfig.tracker; _cmf.id += 'clickmap_tracker';
|
||||
_cmf.src += '&v='+clickmapConfig.version+'&now='+(new Date().getTime());
|
||||
if (document.getElementById('clickmap_tracker')==null) {
|
||||
document.getElementsByTagName('head')[0].appendChild(_cmf); }}());
|
||||
</script>
|
||||
"""
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def clickmap(parser, token):
|
||||
"""
|
||||
Clickmap tracker template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your clickmap tracker ID (as a string) in the ``CLICKMAP_TRACKER_ID``
|
||||
setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return ClickmapNode()
|
||||
|
||||
|
||||
class ClickmapNode(Node):
|
||||
def __init__(self):
|
||||
self.tracker_id = get_required_setting(
|
||||
'CLICKMAP_TRACKER_ID',
|
||||
CLICKMAP_TRACKER_ID_RE,
|
||||
'must be an alphanumeric string',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = TRACKING_CODE % {'tracker_id': self.tracker_id}
|
||||
if is_internal_ip(context, 'CLICKMAP'):
|
||||
html = disable_html(html, 'Clickmap')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
ClickmapNode() # ensure properly configured
|
||||
add_node('body_bottom', ClickmapNode)
|
||||
|
|
@ -2,20 +2,21 @@
|
|||
Clicky template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import get_identity, is_internal_ip, disable_html, \
|
||||
get_required_setting
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
SITE_ID_RE = re.compile(r'^\d+$')
|
||||
TRACKING_CODE = """
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var clicky = { log: function(){ return; }, goal: function(){ return; }};
|
||||
var clicky_site_ids = clicky_site_ids || [];
|
||||
clicky_site_ids.push(%(site_id)s);
|
||||
|
|
@ -29,8 +30,7 @@ TRACKING_CODE = """
|
|||
})();
|
||||
</script>
|
||||
<noscript><p><img alt="Clicky" width="1" height="1" src="//in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
|
||||
"""
|
||||
|
||||
""" # noqa
|
||||
|
||||
register = Library()
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ def clicky(parser, token):
|
|||
"""
|
||||
Clicky tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your Clicky Site ID (as a string) in the ``CLICKY_SITE_ID``
|
||||
setting.
|
||||
"""
|
||||
|
|
@ -49,10 +49,12 @@ def clicky(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return ClickyNode()
|
||||
|
||||
|
||||
class ClickyNode(Node):
|
||||
def __init__(self):
|
||||
self.site_id = get_required_setting('CLICKY_SITE_ID', SITE_ID_RE,
|
||||
"must be a (string containing) a number")
|
||||
self.site_id = get_required_setting(
|
||||
'CLICKY_SITE_ID', SITE_ID_RE, 'must be a (string containing) a number'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
custom = {}
|
||||
|
|
@ -65,8 +67,10 @@ class ClickyNode(Node):
|
|||
if identity is not None:
|
||||
custom.setdefault('session', {})['username'] = identity
|
||||
|
||||
html = TRACKING_CODE % {'site_id': self.site_id,
|
||||
'custom': simplejson.dumps(custom)}
|
||||
html = TRACKING_CODE % {
|
||||
'site_id': self.site_id,
|
||||
'custom': json.dumps(custom, sort_keys=True),
|
||||
}
|
||||
if is_internal_ip(context, 'CLICKY'):
|
||||
html = disable_html(html, 'Clicky')
|
||||
return html
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
Crazy Egg template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, get_required_setting
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
|
||||
SETUP_CODE = """<script type="text/javascript" src="//dnn506yrbagrg.cloudfront.net/pages/scripts/%(account_nr_1)s/%(account_nr_2)s.js"></script>"""
|
||||
SETUP_CODE = '<script src="{placeholder_url}"></script>'.format(
|
||||
placeholder_url='//dnn506yrbagrg.cloudfront.net/pages/scripts/'
|
||||
'%(account_nr_1)s/%(account_nr_2)s.js'
|
||||
)
|
||||
USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');"
|
||||
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ def crazy_egg(parser, token):
|
|||
"""
|
||||
Crazy Egg tracking template tag.
|
||||
|
||||
Renders Javascript code to track page clicks. You must supply
|
||||
Renders JavaScript code to track page clicks. You must supply
|
||||
your Crazy Egg account number (as a string) in the
|
||||
``CRAZY_EGG_ACCOUNT_NUMBER`` setting.
|
||||
"""
|
||||
|
|
@ -33,21 +33,32 @@ def crazy_egg(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return CrazyEggNode()
|
||||
|
||||
|
||||
class CrazyEggNode(Node):
|
||||
def __init__(self):
|
||||
self.account_nr = get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER',
|
||||
ACCOUNT_NUMBER_RE, "must be (a string containing) a number")
|
||||
self.account_nr = get_required_setting(
|
||||
'CRAZY_EGG_ACCOUNT_NUMBER',
|
||||
ACCOUNT_NUMBER_RE,
|
||||
'must be (a string containing) a number',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = SETUP_CODE % {'account_nr_1': self.account_nr[:4],
|
||||
'account_nr_2': self.account_nr[4:]}
|
||||
html = SETUP_CODE % {
|
||||
'account_nr_1': self.account_nr[:4],
|
||||
'account_nr_2': self.account_nr[4:],
|
||||
}
|
||||
values = (context.get('crazy_egg_var%d' % i) for i in range(1, 6))
|
||||
vars = [(i, v) for i, v in enumerate(values, 1) if v is not None]
|
||||
if vars:
|
||||
js = " ".join(USERVAR_CODE % {'varnr': varnr, 'value': value}
|
||||
for (varnr, value) in vars)
|
||||
html = '%s\n<script type="text/javascript">%s</script>' \
|
||||
% (html, js)
|
||||
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
|
||||
if params:
|
||||
js = ' '.join(
|
||||
USERVAR_CODE
|
||||
% {
|
||||
'varnr': varnr,
|
||||
'value': value,
|
||||
}
|
||||
for (varnr, value) in params
|
||||
)
|
||||
html = '%s\n<script>%s</script>' % (html, js)
|
||||
if is_internal_ip(context, 'CRAZY_EGG'):
|
||||
html = disable_html(html, 'Crazy Egg')
|
||||
return html
|
||||
|
|
|
|||
96
analytical/templatetags/facebook_pixel.py
Normal file
96
analytical/templatetags/facebook_pixel.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""
|
||||
Facebook Pixel template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
FACEBOOK_PIXEL_HEAD_CODE = """\
|
||||
<script>
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', '%(FACEBOOK_PIXEL_ID)s');
|
||||
fbq('track', 'PageView');
|
||||
</script>
|
||||
"""
|
||||
|
||||
FACEBOOK_PIXEL_BODY_CODE = """\
|
||||
<noscript><img height="1" width="1" style="display:none"
|
||||
src="https://www.facebook.com/tr?id=%(FACEBOOK_PIXEL_ID)s&ev=PageView&noscript=1"
|
||||
/></noscript>
|
||||
"""
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
def _validate_no_args(token):
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
|
||||
|
||||
@register.tag
|
||||
def facebook_pixel_head(parser, token):
|
||||
"""
|
||||
Facebook Pixel head template tag.
|
||||
"""
|
||||
_validate_no_args(token)
|
||||
return FacebookPixelHeadNode()
|
||||
|
||||
|
||||
@register.tag
|
||||
def facebook_pixel_body(parser, token):
|
||||
"""
|
||||
Facebook Pixel body template tag.
|
||||
"""
|
||||
_validate_no_args(token)
|
||||
return FacebookPixelBodyNode()
|
||||
|
||||
|
||||
class _FacebookPixelNode(Node):
|
||||
"""
|
||||
Base class: override and provide code_template.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.pixel_id = get_required_setting(
|
||||
'FACEBOOK_PIXEL_ID',
|
||||
re.compile(r'^\d+$'),
|
||||
'must be (a string containing) a number',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = self.code_template % {'FACEBOOK_PIXEL_ID': self.pixel_id}
|
||||
if is_internal_ip(context, 'FACEBOOK_PIXEL'):
|
||||
return disable_html(html, 'Facebook Pixel')
|
||||
else:
|
||||
return html
|
||||
|
||||
@property
|
||||
def code_template(self):
|
||||
raise NotImplementedError # pragma: no cover
|
||||
|
||||
|
||||
class FacebookPixelHeadNode(_FacebookPixelNode):
|
||||
code_template = FACEBOOK_PIXEL_HEAD_CODE
|
||||
|
||||
|
||||
class FacebookPixelBodyNode(_FacebookPixelNode):
|
||||
code_template = FACEBOOK_PIXEL_BODY_CODE
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
# ensure properly configured
|
||||
FacebookPixelHeadNode()
|
||||
FacebookPixelBodyNode()
|
||||
add_node('head_bottom', FacebookPixelHeadNode)
|
||||
add_node('body_bottom', FacebookPixelBodyNode)
|
||||
61
analytical/templatetags/gauges.py
Normal file
61
analytical/templatetags/gauges.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""
|
||||
Gaug.es template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
SITE_ID_RE = re.compile(r'[\da-f]+$')
|
||||
TRACKING_CODE = """
|
||||
<script>
|
||||
var _gauges = _gauges || [];
|
||||
(function() {
|
||||
var t = document.createElement('script');
|
||||
t.type = 'text/javascript';
|
||||
t.async = true;
|
||||
t.id = 'gauges-tracker';
|
||||
t.setAttribute('data-site-id', '%(site_id)s');
|
||||
t.src = '//secure.gaug.es/track.js';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def gauges(parser, token):
|
||||
"""
|
||||
Gaug.es template tag.
|
||||
|
||||
Renders JavaScript code to gaug.es testing. You must supply
|
||||
your Site ID account number in the ``GAUGES_SITE_ID``
|
||||
setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return GaugesNode()
|
||||
|
||||
|
||||
class GaugesNode(Node):
|
||||
def __init__(self):
|
||||
self.site_id = get_required_setting(
|
||||
'GAUGES_SITE_ID', SITE_ID_RE, "must be a string looking like 'XXXXXXX'"
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = TRACKING_CODE % {'site_id': self.site_id}
|
||||
if is_internal_ip(context, 'GAUGES'):
|
||||
html = disable_html(html, 'Gauges')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
GaugesNode()
|
||||
add_node('head_bottom', GaugesNode)
|
||||
|
|
@ -1,24 +1,22 @@
|
|||
"""
|
||||
Google Analytics template tags and filters.
|
||||
|
||||
DEPRECATED
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import decimal
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, \
|
||||
get_required_setting, get_domain, AnalyticalException
|
||||
|
||||
def enumerate(sequence, start=0):
|
||||
"""Copy of the Python 2.6 `enumerate` builtin for compatibility."""
|
||||
n = start
|
||||
for elem in sequence:
|
||||
yield n, elem
|
||||
n += 1
|
||||
|
||||
from analytical.utils import (
|
||||
AnalyticalException,
|
||||
disable_html,
|
||||
get_domain,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
TRACK_SINGLE_DOMAIN = 1
|
||||
TRACK_MULTIPLE_SUBDOMAINS = 2
|
||||
|
|
@ -30,15 +28,14 @@ SCOPE_PAGE = 3
|
|||
|
||||
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
|
||||
SETUP_CODE = """
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', '%(property_id)s']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
%(commands)s
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
ga.src = ('https:' == document.location.protocol ? %(source_scheme)s) + %(source_url)s;
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
|
|
@ -46,20 +43,35 @@ SETUP_CODE = """
|
|||
"""
|
||||
DOMAIN_CODE = "_gaq.push(['_setDomainName', '%s']);"
|
||||
NO_ALLOW_HASH_CODE = "_gaq.push(['_setAllowHash', false]);"
|
||||
TRACK_PAGE_VIEW = "_gaq.push(['_trackPageview']);"
|
||||
ALLOW_LINKER_CODE = "_gaq.push(['_setAllowLinker', true]);"
|
||||
CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)s, '%(name)s', " \
|
||||
"'%(value)s', %(scope)s]);"
|
||||
CUSTOM_VAR_CODE = (
|
||||
"_gaq.push(['_setCustomVar', %(index)s, '%(name)s', '%(value)s', %(scope)s]);"
|
||||
)
|
||||
SITE_SPEED_CODE = "_gaq.push(['_trackPageLoadTime']);"
|
||||
ANONYMIZE_IP_CODE = "_gaq.push(['_gat._anonymizeIp']);"
|
||||
SAMPLE_RATE_CODE = "_gaq.push(['_setSampleRate', '%s']);"
|
||||
SITE_SPEED_SAMPLE_RATE_CODE = "_gaq.push(['_setSiteSpeedSampleRate', '%s']);"
|
||||
SESSION_COOKIE_TIMEOUT_CODE = "_gaq.push(['_setSessionCookieTimeout', '%s']);"
|
||||
VISITOR_COOKIE_TIMEOUT_CODE = "_gaq.push(['_setVisitorCookieTimeout', '%s']);"
|
||||
DEFAULT_SOURCE = ("'https://ssl' : 'http://www'", "'.google-analytics.com/ga.js'")
|
||||
DISPLAY_ADVERTISING_SOURCE = (
|
||||
"'https://' : 'http://'",
|
||||
"'stats.g.doubleclick.net/dc.js'",
|
||||
)
|
||||
|
||||
ZEROPLACES = decimal.Decimal('0')
|
||||
TWOPLACES = decimal.Decimal('0.01')
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def google_analytics(parser, token):
|
||||
"""
|
||||
Google Analytics tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your website property ID (as a string) in the
|
||||
``GOOGLE_ANALYTICS_PROPERTY_ID`` setting.
|
||||
"""
|
||||
|
|
@ -68,33 +80,47 @@ def google_analytics(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return GoogleAnalyticsNode()
|
||||
|
||||
|
||||
class GoogleAnalyticsNode(Node):
|
||||
def __init__(self):
|
||||
self.property_id = get_required_setting(
|
||||
'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE,
|
||||
"must be a string looking like 'UA-XXXXXX-Y'")
|
||||
'GOOGLE_ANALYTICS_PROPERTY_ID',
|
||||
PROPERTY_ID_RE,
|
||||
"must be a string looking like 'UA-XXXXXX-Y'",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
commands = self._get_domain_commands(context)
|
||||
commands.extend(self._get_custom_var_commands(context))
|
||||
commands.extend(self._get_other_commands(context))
|
||||
html = SETUP_CODE % {'property_id': self.property_id,
|
||||
'commands': " ".join(commands)}
|
||||
commands.append(TRACK_PAGE_VIEW)
|
||||
if getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False):
|
||||
source = DISPLAY_ADVERTISING_SOURCE
|
||||
else:
|
||||
source = DEFAULT_SOURCE
|
||||
html = SETUP_CODE % {
|
||||
'property_id': self.property_id,
|
||||
'commands': ' '.join(commands),
|
||||
'source_scheme': source[0],
|
||||
'source_url': source[1],
|
||||
}
|
||||
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
|
||||
html = disable_html(html, 'Google Analytics')
|
||||
return html
|
||||
|
||||
def _get_domain_commands(self, context):
|
||||
commands = []
|
||||
tracking_type = getattr(settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE',
|
||||
TRACK_SINGLE_DOMAIN)
|
||||
tracking_type = getattr(
|
||||
settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE', TRACK_SINGLE_DOMAIN
|
||||
)
|
||||
if tracking_type == TRACK_SINGLE_DOMAIN:
|
||||
pass
|
||||
else:
|
||||
domain = get_domain(context, 'google_analytics')
|
||||
if domain is None:
|
||||
raise AnalyticalException("tracking multiple domains with"
|
||||
" Google Analytics requires a domain name")
|
||||
raise AnalyticalException(
|
||||
'tracking multiple domains with Google Analytics requires a domain name'
|
||||
)
|
||||
commands.append(DOMAIN_CODE % domain)
|
||||
commands.append(NO_ALLOW_HASH_CODE)
|
||||
if tracking_type == TRACK_MULTIPLE_DOMAINS:
|
||||
|
|
@ -102,26 +128,79 @@ class GoogleAnalyticsNode(Node):
|
|||
return commands
|
||||
|
||||
def _get_custom_var_commands(self, context):
|
||||
values = (context.get('google_analytics_var%s' % i)
|
||||
for i in range(1, 6))
|
||||
vars = [(i, v) for i, v in enumerate(values, 1) if v is not None]
|
||||
values = (context.get('google_analytics_var%s' % i) for i in range(1, 6))
|
||||
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
|
||||
commands = []
|
||||
for index, var in vars:
|
||||
for index, var in params:
|
||||
name = var[0]
|
||||
value = var[1]
|
||||
try:
|
||||
scope = var[2]
|
||||
except IndexError:
|
||||
scope = SCOPE_PAGE
|
||||
commands.append(CUSTOM_VAR_CODE % locals())
|
||||
commands.append(
|
||||
CUSTOM_VAR_CODE
|
||||
% {
|
||||
'index': index,
|
||||
'name': name,
|
||||
'value': value,
|
||||
'scope': scope,
|
||||
}
|
||||
)
|
||||
return commands
|
||||
|
||||
def _get_other_commands(self, context):
|
||||
commands = []
|
||||
if getattr(settings, 'GOOGLE_ANALYTICS_SITE_SPEED', False):
|
||||
commands.append(SITE_SPEED_CODE)
|
||||
|
||||
if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False):
|
||||
commands.append(ANONYMIZE_IP_CODE)
|
||||
|
||||
sampleRate = getattr(settings, 'GOOGLE_ANALYTICS_SAMPLE_RATE', False)
|
||||
if sampleRate is not False:
|
||||
value = decimal.Decimal(sampleRate)
|
||||
if not 0 <= value <= 100:
|
||||
raise AnalyticalException(
|
||||
"'GOOGLE_ANALYTICS_SAMPLE_RATE' must be >= 0 and <= 100"
|
||||
)
|
||||
commands.append(SAMPLE_RATE_CODE % value.quantize(TWOPLACES))
|
||||
|
||||
siteSpeedSampleRate = getattr(
|
||||
settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False
|
||||
)
|
||||
if siteSpeedSampleRate is not False:
|
||||
value = decimal.Decimal(siteSpeedSampleRate)
|
||||
if not 0 <= value <= 100:
|
||||
raise AnalyticalException(
|
||||
"'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE' must be >= 0 and <= 100"
|
||||
)
|
||||
commands.append(SITE_SPEED_SAMPLE_RATE_CODE % value.quantize(TWOPLACES))
|
||||
|
||||
sessionCookieTimeout = getattr(
|
||||
settings, 'GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT', False
|
||||
)
|
||||
if sessionCookieTimeout is not False:
|
||||
value = decimal.Decimal(sessionCookieTimeout)
|
||||
if value < 0:
|
||||
raise AnalyticalException(
|
||||
"'GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT' must be >= 0"
|
||||
)
|
||||
commands.append(SESSION_COOKIE_TIMEOUT_CODE % value.quantize(ZEROPLACES))
|
||||
|
||||
visitorCookieTimeout = getattr(
|
||||
settings, 'GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT', False
|
||||
)
|
||||
if visitorCookieTimeout is not False:
|
||||
value = decimal.Decimal(visitorCookieTimeout)
|
||||
if value < 0:
|
||||
raise AnalyticalException(
|
||||
"'GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT' must be >= 0"
|
||||
)
|
||||
commands.append(VISITOR_COOKIE_TIMEOUT_CODE % value.quantize(ZEROPLACES))
|
||||
return commands
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
GoogleAnalyticsNode() # ensure properly configured
|
||||
add_node('head_bottom', GoogleAnalyticsNode)
|
||||
|
|
|
|||
78
analytical/templatetags/google_analytics_gtag.py
Normal file
78
analytical/templatetags/google_analytics_gtag.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""
|
||||
Google Analytics template tags and filters, using the new gtag.js library.
|
||||
https://developers.google.com/tag-platform/gtagjs/reference
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
PROPERTY_ID_RE = re.compile(
|
||||
r'^UA-\d+-\d+$|^G-[a-zA-Z0-9]+$|^AW-[a-zA-Z0-9]+$|^DC-[a-zA-Z0-9]+$'
|
||||
)
|
||||
SETUP_CODE = """
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={property_id}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){{dataLayer.push(arguments);}}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '{property_id}', {custom_dimensions});
|
||||
</script>
|
||||
"""
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def google_analytics_gtag(parser, token):
|
||||
"""
|
||||
Google Analytics tracking template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your website property ID (as a string) in the
|
||||
``GOOGLE_ANALYTICS_GTAG_PROPERTY_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return GoogleAnalyticsGTagNode()
|
||||
|
||||
|
||||
class GoogleAnalyticsGTagNode(Node):
|
||||
def __init__(self):
|
||||
self.property_id = get_required_setting(
|
||||
'GOOGLE_ANALYTICS_GTAG_PROPERTY_ID',
|
||||
PROPERTY_ID_RE,
|
||||
"""must be a string looking like one of these patterns
|
||||
('UA-XXXXXX-Y' , 'AW-XXXXXXXXXX',
|
||||
'G-XXXXXXXX', 'DC-XXXXXXXX')""",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
custom_dimensions = context.get('google_analytics_custom_dimensions', {})
|
||||
|
||||
identity = get_identity(context, prefix='google_analytics_gtag')
|
||||
if identity is not None:
|
||||
custom_dimensions['user_id'] = identity
|
||||
|
||||
html = SETUP_CODE.format(
|
||||
property_id=self.property_id,
|
||||
custom_dimensions=json.dumps(custom_dimensions),
|
||||
)
|
||||
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
|
||||
html = disable_html(html, 'Google Analytics')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
GoogleAnalyticsGTagNode() # ensure properly configured
|
||||
add_node('head_top', GoogleAnalyticsGTagNode)
|
||||
176
analytical/templatetags/google_analytics_js.py
Normal file
176
analytical/templatetags/google_analytics_js.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""
|
||||
Google Analytics template tags and filters, using the new analytics.js library.
|
||||
"""
|
||||
|
||||
import decimal
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import (
|
||||
AnalyticalException,
|
||||
disable_html,
|
||||
get_domain,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
TRACK_SINGLE_DOMAIN = 1
|
||||
TRACK_MULTIPLE_SUBDOMAINS = 2
|
||||
TRACK_MULTIPLE_DOMAINS = 3
|
||||
|
||||
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
|
||||
SETUP_CODE = """
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){{i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){{
|
||||
(i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
}})(window,document,'script','{js_source}','ga');
|
||||
|
||||
ga('create', '{property_id}', 'auto', {create_fields});
|
||||
{commands}ga('send', 'pageview');
|
||||
</script>
|
||||
"""
|
||||
REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');\n"
|
||||
CUSTOM_VAR_CODE = "ga('set', '{name}', {value});\n"
|
||||
ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);\n"
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def google_analytics_js(parser, token):
|
||||
"""
|
||||
Google Analytics tracking template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your website property ID (as a string) in the
|
||||
``GOOGLE_ANALYTICS_JS_PROPERTY_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return GoogleAnalyticsJsNode()
|
||||
|
||||
|
||||
class GoogleAnalyticsJsNode(Node):
|
||||
def __init__(self):
|
||||
self.property_id = get_required_setting(
|
||||
'GOOGLE_ANALYTICS_JS_PROPERTY_ID',
|
||||
PROPERTY_ID_RE,
|
||||
"must be a string looking like 'UA-XXXXXX-Y'",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
import json
|
||||
|
||||
create_fields = self._get_domain_fields(context)
|
||||
create_fields.update(self._get_other_create_fields(context))
|
||||
commands = self._get_custom_var_commands(context)
|
||||
commands.extend(self._get_other_commands(context))
|
||||
display_features = getattr(
|
||||
settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False
|
||||
)
|
||||
if display_features:
|
||||
commands.insert(0, REQUIRE_DISPLAY_FEATURES)
|
||||
|
||||
js_source = getattr(
|
||||
settings,
|
||||
'GOOGLE_ANALYTICS_JS_SOURCE',
|
||||
'https://www.google-analytics.com/analytics.js',
|
||||
)
|
||||
|
||||
html = SETUP_CODE.format(
|
||||
property_id=self.property_id,
|
||||
create_fields=json.dumps(create_fields),
|
||||
commands=''.join(commands),
|
||||
js_source=js_source,
|
||||
)
|
||||
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
|
||||
html = disable_html(html, 'Google Analytics')
|
||||
return html
|
||||
|
||||
def _get_domain_fields(self, context):
|
||||
domain_fields = {}
|
||||
tracking_type = getattr(
|
||||
settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE', TRACK_SINGLE_DOMAIN
|
||||
)
|
||||
if tracking_type == TRACK_SINGLE_DOMAIN:
|
||||
pass
|
||||
else:
|
||||
domain = get_domain(context, 'google_analytics')
|
||||
if domain is None:
|
||||
raise AnalyticalException(
|
||||
'tracking multiple domains with Google Analytics requires a domain name'
|
||||
)
|
||||
domain_fields['legacyCookieDomain'] = domain
|
||||
if tracking_type == TRACK_MULTIPLE_DOMAINS:
|
||||
domain_fields['allowLinker'] = True
|
||||
return domain_fields
|
||||
|
||||
def _get_other_create_fields(self, context):
|
||||
other_fields = {}
|
||||
|
||||
site_speed_sample_rate = getattr(
|
||||
settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False
|
||||
)
|
||||
if site_speed_sample_rate is not False:
|
||||
value = int(decimal.Decimal(site_speed_sample_rate))
|
||||
if not 0 <= value <= 100:
|
||||
raise AnalyticalException(
|
||||
"'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE' must be >= 0 and <= 100"
|
||||
)
|
||||
other_fields['siteSpeedSampleRate'] = value
|
||||
|
||||
sample_rate = getattr(settings, 'GOOGLE_ANALYTICS_SAMPLE_RATE', False)
|
||||
if sample_rate is not False:
|
||||
value = int(decimal.Decimal(sample_rate))
|
||||
if not 0 <= value <= 100:
|
||||
raise AnalyticalException(
|
||||
"'GOOGLE_ANALYTICS_SAMPLE_RATE' must be >= 0 and <= 100"
|
||||
)
|
||||
other_fields['sampleRate'] = value
|
||||
|
||||
cookie_expires = getattr(settings, 'GOOGLE_ANALYTICS_COOKIE_EXPIRATION', False)
|
||||
if cookie_expires is not False:
|
||||
value = int(decimal.Decimal(cookie_expires))
|
||||
if value < 0:
|
||||
raise AnalyticalException(
|
||||
"'GOOGLE_ANALYTICS_COOKIE_EXPIRATION' must be >= 0"
|
||||
)
|
||||
other_fields['cookieExpires'] = value
|
||||
|
||||
return other_fields
|
||||
|
||||
def _get_custom_var_commands(self, context):
|
||||
values = (context.get('google_analytics_var%s' % i) for i in range(1, 6))
|
||||
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
|
||||
commands = []
|
||||
for _, var in params:
|
||||
name = var[0]
|
||||
value = var[1]
|
||||
try:
|
||||
float(value)
|
||||
except ValueError:
|
||||
value = f"'{value}'"
|
||||
commands.append(
|
||||
CUSTOM_VAR_CODE.format(
|
||||
name=name,
|
||||
value=value,
|
||||
)
|
||||
)
|
||||
return commands
|
||||
|
||||
def _get_other_commands(self, context):
|
||||
commands = []
|
||||
|
||||
if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False):
|
||||
commands.append(ANONYMIZE_IP_CODE)
|
||||
|
||||
return commands
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
GoogleAnalyticsJsNode() # ensure properly configured
|
||||
add_node('head_bottom', GoogleAnalyticsJsNode)
|
||||
|
|
@ -2,21 +2,20 @@
|
|||
GoSquared template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import get_identity, get_user_from_context, \
|
||||
is_internal_ip, disable_html, get_required_setting
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
TOKEN_RE = re.compile(r'^\S+-\S+-\S+$')
|
||||
TRACKING_CODE = """
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var GoSquared={};
|
||||
%(config)s
|
||||
(function(w){
|
||||
|
|
@ -28,7 +27,7 @@ TRACKING_CODE = """
|
|||
w.addEventListener?w.addEventListener("load",gs,false):w.attachEvent("onload",gs);
|
||||
})(window);
|
||||
</script>
|
||||
"""
|
||||
""" # noqa
|
||||
TOKEN_CODE = 'GoSquared.acct = "%s";'
|
||||
IDENTIFY_CODE = 'GoSquared.UserName = "%s";'
|
||||
|
||||
|
|
@ -41,7 +40,7 @@ def gosquared(parser, token):
|
|||
"""
|
||||
GoSquared tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your GoSquared site token in the ``GOSQUARED_SITE_TOKEN`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
|
|
@ -49,10 +48,14 @@ def gosquared(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return GoSquaredNode()
|
||||
|
||||
|
||||
class GoSquaredNode(Node):
|
||||
def __init__(self):
|
||||
self.site_token = get_required_setting('GOSQUARED_SITE_TOKEN', TOKEN_RE,
|
||||
"must be a string looking like XXX-XXXXXX-X")
|
||||
self.site_token = get_required_setting(
|
||||
'GOSQUARED_SITE_TOKEN',
|
||||
TOKEN_RE,
|
||||
'must be a string looking like XXX-XXXXXX-X',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
configs = [TOKEN_CODE % self.site_token]
|
||||
|
|
|
|||
57
analytical/templatetags/heap.py
Normal file
57
analytical/templatetags/heap.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Heap template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
HEAP_TRACKER_ID_RE = re.compile(r'^\d+$')
|
||||
TRACKING_CODE = """
|
||||
<script>
|
||||
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
|
||||
heap.load("%(tracker_id)s");
|
||||
</script>
|
||||
|
||||
""" # noqa
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
def _validate_no_args(token):
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
|
||||
|
||||
@register.tag
|
||||
def heap(parser, token):
|
||||
"""
|
||||
Heap tracker template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your heap tracker ID (as a string) in the ``HEAP_TRACKER_ID``
|
||||
setting.
|
||||
"""
|
||||
_validate_no_args(token)
|
||||
return HeapNode()
|
||||
|
||||
|
||||
class HeapNode(Node):
|
||||
def __init__(self):
|
||||
self.tracker_id = get_required_setting(
|
||||
'HEAP_TRACKER_ID', HEAP_TRACKER_ID_RE, 'must be an numeric string'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = TRACKING_CODE % {'tracker_id': self.tracker_id}
|
||||
if is_internal_ip(context, 'HEAP'):
|
||||
html = disable_html(html, 'Heap')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
HeapNode() # ensure properly configured
|
||||
add_node('head_bottom', HeapNode)
|
||||
62
analytical/templatetags/hotjar.py
Normal file
62
analytical/templatetags/hotjar.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""
|
||||
Hotjar template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
HOTJAR_TRACKING_CODE = """\
|
||||
<script>
|
||||
(function(h,o,t,j,a,r){
|
||||
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
|
||||
h._hjSettings={hjid:%(HOTJAR_SITE_ID)s,hjsv:6};
|
||||
a=o.getElementsByTagName('head')[0];
|
||||
r=o.createElement('script');r.async=1;
|
||||
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
|
||||
a.appendChild(r);
|
||||
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
def _validate_no_args(token):
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
|
||||
|
||||
@register.tag
|
||||
def hotjar(parser, token):
|
||||
"""
|
||||
Hotjar template tag.
|
||||
"""
|
||||
_validate_no_args(token)
|
||||
return HotjarNode()
|
||||
|
||||
|
||||
class HotjarNode(Node):
|
||||
def __init__(self):
|
||||
self.site_id = get_required_setting(
|
||||
'HOTJAR_SITE_ID',
|
||||
re.compile(r'^\d+$'),
|
||||
'must be (a string containing) a number',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = HOTJAR_TRACKING_CODE % {'HOTJAR_SITE_ID': self.site_id}
|
||||
if is_internal_ip(context, 'HOTJAR'):
|
||||
return disable_html(html, 'Hotjar')
|
||||
else:
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
# ensure properly configured
|
||||
HotjarNode()
|
||||
add_node('head_bottom', HotjarNode)
|
||||
|
|
@ -2,26 +2,25 @@
|
|||
HubSpot template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, get_required_setting
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
PORTAL_ID_RE = re.compile(r'^\d+$')
|
||||
DOMAIN_RE = re.compile(r'^[\w.-]+$')
|
||||
TRACKING_CODE = """
|
||||
<script type="text/javascript" language="javascript">
|
||||
var hs_portalid = %(portal_id)s;
|
||||
var hs_salog_version = "2.00";
|
||||
var hs_ppa = "%(domain)s";
|
||||
document.write(unescape("%%3Cscript src='" + document.location.protocol + "//" + hs_ppa + "/salog.js.aspx' type='text/javascript'%%3E%%3C/script%%3E"));
|
||||
</script>
|
||||
"""
|
||||
|
||||
<!-- Start of Async HubSpot Analytics Code -->
|
||||
<script>
|
||||
(function(d,s,i,r) {
|
||||
if (d.getElementById(i)){return;}
|
||||
var n=d.createElement(s),e=d.getElementsByTagName(s)[0];
|
||||
n.id=i;n.src='//js.hs-analytics.net/analytics/'+(Math.ceil(new Date()/r)*r)+'/%(portal_id)s.js';
|
||||
e.parentNode.insertBefore(n, e);
|
||||
})(document,"script","hs-analytics",300000);
|
||||
</script>
|
||||
<!-- End of Async HubSpot Analytics Code -->
|
||||
""" # noqa
|
||||
|
||||
register = Library()
|
||||
|
||||
|
|
@ -31,25 +30,23 @@ def hubspot(parser, token):
|
|||
"""
|
||||
HubSpot tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
your portal ID (as a string) in the ``HUBSPOT_PORTAL_ID`` setting,
|
||||
and the website domain in ``HUBSPOT_DOMAIN``.
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your portal ID (as a string) in the ``HUBSPOT_PORTAL_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return HubSpotNode()
|
||||
|
||||
|
||||
class HubSpotNode(Node):
|
||||
def __init__(self):
|
||||
self.portal_id = get_required_setting('HUBSPOT_PORTAL_ID',
|
||||
PORTAL_ID_RE, "must be a (string containing a) number")
|
||||
self.domain = get_required_setting('HUBSPOT_DOMAIN',
|
||||
DOMAIN_RE, "must be an internet domain name")
|
||||
self.portal_id = get_required_setting(
|
||||
'HUBSPOT_PORTAL_ID', PORTAL_ID_RE, 'must be a (string containing a) number'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = TRACKING_CODE % {'portal_id': self.portal_id,
|
||||
'domain': self.domain}
|
||||
html = TRACKING_CODE % {'portal_id': self.portal_id}
|
||||
if is_internal_ip(context, 'HUBSPOT'):
|
||||
html = disable_html(html, 'HubSpot')
|
||||
return html
|
||||
|
|
|
|||
132
analytical/templatetags/intercom.py
Normal file
132
analytical/templatetags/intercom.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
intercom.io template tags and filters.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
get_user_from_context,
|
||||
get_user_is_authenticated,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
APP_ID_RE = re.compile(r'[\da-z]+$')
|
||||
TRACKING_CODE = """
|
||||
<script id="IntercomSettingsScriptTag">
|
||||
window.intercomSettings = %(settings_json)s;
|
||||
</script>
|
||||
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
|
||||
""" # noqa
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
def _hashable_bytes(data):
|
||||
"""
|
||||
Coerce strings to hashable bytes.
|
||||
"""
|
||||
if isinstance(data, bytes):
|
||||
return data
|
||||
elif isinstance(data, str):
|
||||
return data.encode('ascii') # Fail on anything non-ASCII.
|
||||
else:
|
||||
raise TypeError(data)
|
||||
|
||||
|
||||
def intercom_user_hash(data):
|
||||
"""
|
||||
Return a SHA-256 HMAC `user_hash` as expected by Intercom, if configured.
|
||||
|
||||
Return None if the `INTERCOM_HMAC_SECRET_KEY` setting is not configured.
|
||||
"""
|
||||
if getattr(settings, 'INTERCOM_HMAC_SECRET_KEY', None):
|
||||
return hmac.new(
|
||||
key=_hashable_bytes(settings.INTERCOM_HMAC_SECRET_KEY),
|
||||
msg=_hashable_bytes(data),
|
||||
digestmod=hashlib.sha256,
|
||||
).hexdigest()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@register.tag
|
||||
def intercom(parser, token):
|
||||
"""
|
||||
Intercom.io template tag.
|
||||
|
||||
Renders JavaScript code to intercom.io testing. You must supply
|
||||
your APP ID account number in the ``INTERCOM_APP_ID``
|
||||
setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return IntercomNode()
|
||||
|
||||
|
||||
class IntercomNode(Node):
|
||||
def __init__(self):
|
||||
self.app_id = get_required_setting(
|
||||
'INTERCOM_APP_ID', APP_ID_RE, "must be a string looking like 'XXXXXXX'"
|
||||
)
|
||||
|
||||
def _identify(self, user):
|
||||
name = user.get_full_name()
|
||||
if not name:
|
||||
name = user.username
|
||||
return name
|
||||
|
||||
def _get_custom_attrs(self, context):
|
||||
params = {}
|
||||
for dict_ in context:
|
||||
for var, val in dict_.items():
|
||||
if var.startswith('intercom_'):
|
||||
params[var[9:]] = val
|
||||
|
||||
user = get_user_from_context(context)
|
||||
if user is not None and get_user_is_authenticated(user):
|
||||
if 'name' not in params:
|
||||
params['name'] = get_identity(context, 'intercom', self._identify, user)
|
||||
if 'email' not in params and user.email:
|
||||
params['email'] = user.email
|
||||
|
||||
params.setdefault('user_id', user.pk)
|
||||
|
||||
params['created_at'] = int(user.date_joined.timestamp())
|
||||
else:
|
||||
params['created_at'] = None
|
||||
|
||||
# Generate a user_hash HMAC to verify the user's identity, if configured.
|
||||
# (If both user_id and email are present, the user_id field takes precedence.)
|
||||
# See:
|
||||
# https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
|
||||
user_hash_data = params.get('user_id', params.get('email'))
|
||||
if user_hash_data:
|
||||
user_hash = intercom_user_hash(str(user_hash_data))
|
||||
if user_hash is not None:
|
||||
params.setdefault('user_hash', user_hash)
|
||||
|
||||
return params
|
||||
|
||||
def render(self, context):
|
||||
params = self._get_custom_attrs(context)
|
||||
params['app_id'] = self.app_id
|
||||
html = TRACKING_CODE % {'settings_json': json.dumps(params, sort_keys=True)}
|
||||
|
||||
if is_internal_ip(context, 'INTERCOM'):
|
||||
html = disable_html(html, 'Intercom')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
IntercomNode()
|
||||
add_node('body_bottom', IntercomNode)
|
||||
|
|
@ -2,21 +2,18 @@
|
|||
KISSinsights template tags.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import get_identity, get_required_setting
|
||||
|
||||
|
||||
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
|
||||
SITE_CODE_RE = re.compile(r'^[\w]+$')
|
||||
SETUP_CODE = """
|
||||
<script type="text/javascript">var _kiq = _kiq || []; %(commands)s</script>
|
||||
<script type="text/javascript" src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script>
|
||||
"""
|
||||
<script>var _kiq = _kiq || []; %(commands)s</script>
|
||||
<script src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script>
|
||||
""" # noqa
|
||||
IDENTIFY_CODE = "_kiq.push(['identify', '%s']);"
|
||||
SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);"
|
||||
SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey'
|
||||
|
|
@ -30,7 +27,7 @@ def kiss_insights(parser, token):
|
|||
"""
|
||||
KISSinsights set-up template tag.
|
||||
|
||||
Renders Javascript code to set-up surveys. You must supply
|
||||
Renders JavaScript code to set-up surveys. You must supply
|
||||
your account number and site code in the
|
||||
``KISS_INSIGHTS_ACCOUNT_NUMBER`` and ``KISS_INSIGHTS_SITE_CODE``
|
||||
settings.
|
||||
|
|
@ -40,13 +37,19 @@ def kiss_insights(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return KissInsightsNode()
|
||||
|
||||
|
||||
class KissInsightsNode(Node):
|
||||
def __init__(self):
|
||||
self.account_number = get_required_setting(
|
||||
'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
|
||||
"must be (a string containing) a number")
|
||||
self.site_code = get_required_setting('KISS_INSIGHTS_SITE_CODE',
|
||||
SITE_CODE_RE, "must be a string containing three characters")
|
||||
'KISS_INSIGHTS_ACCOUNT_NUMBER',
|
||||
ACCOUNT_NUMBER_RE,
|
||||
'must be (a string containing) a number',
|
||||
)
|
||||
self.site_code = get_required_setting(
|
||||
'KISS_INSIGHTS_SITE_CODE',
|
||||
SITE_CODE_RE,
|
||||
'must be a string containing three characters',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
commands = []
|
||||
|
|
@ -54,12 +57,14 @@ class KissInsightsNode(Node):
|
|||
if identity is not None:
|
||||
commands.append(IDENTIFY_CODE % identity)
|
||||
try:
|
||||
commands.append(SHOW_SURVEY_CODE
|
||||
% context[SHOW_SURVEY_CONTEXT_KEY])
|
||||
commands.append(SHOW_SURVEY_CODE % context[SHOW_SURVEY_CONTEXT_KEY])
|
||||
except KeyError:
|
||||
pass
|
||||
html = SETUP_CODE % {'account_number': self.account_number,
|
||||
'site_code': self.site_code, 'commands': " ".join(commands)}
|
||||
html = SETUP_CODE % {
|
||||
'account_number': self.account_number,
|
||||
'site_code': self.site_code,
|
||||
'commands': ' '.join(commands),
|
||||
}
|
||||
return html
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,20 +2,21 @@
|
|||
KISSmetrics template tags.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, get_identity, \
|
||||
get_required_setting
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
API_KEY_RE = re.compile(r'^[0-9a-f]{40}$')
|
||||
TRACKING_CODE = """
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var _kmq = _kmq || [];
|
||||
%(commands)s
|
||||
function _kms(u){
|
||||
|
|
@ -35,9 +36,11 @@ TRACKING_CODE = """
|
|||
IDENTIFY_CODE = "_kmq.push(['identify', '%s']);"
|
||||
EVENT_CODE = "_kmq.push(['record', '%(name)s', %(properties)s]);"
|
||||
PROPERTY_CODE = "_kmq.push(['set', %(properties)s]);"
|
||||
ALIAS_CODE = "_kmq.push(['alias', '%s', '%s']);"
|
||||
|
||||
EVENT_CONTEXT_KEY = 'kiss_metrics_event'
|
||||
PROPERTY_CONTEXT_KEY = 'kiss_metrics_properties'
|
||||
ALIAS_CONTEXT_KEY = 'kiss_metrics_alias'
|
||||
|
||||
register = Library()
|
||||
|
||||
|
|
@ -47,7 +50,7 @@ def kiss_metrics(parser, token):
|
|||
"""
|
||||
KISSinsights tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your KISSmetrics API key in the ``KISS_METRICS_API_KEY``
|
||||
setting.
|
||||
"""
|
||||
|
|
@ -56,31 +59,51 @@ def kiss_metrics(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return KissMetricsNode()
|
||||
|
||||
|
||||
class KissMetricsNode(Node):
|
||||
def __init__(self):
|
||||
self.api_key = get_required_setting('KISS_METRICS_API_KEY',
|
||||
API_KEY_RE,
|
||||
"must be a string containing a 40-digit hexadecimal number")
|
||||
self.api_key = get_required_setting(
|
||||
'KISS_METRICS_API_KEY',
|
||||
API_KEY_RE,
|
||||
'must be a string containing a 40-digit hexadecimal number',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
commands = []
|
||||
identity = get_identity(context, 'kiss_metrics')
|
||||
if identity is not None:
|
||||
commands.append(IDENTIFY_CODE % identity)
|
||||
try:
|
||||
properties = context[ALIAS_CONTEXT_KEY]
|
||||
key, value = properties.popitem()
|
||||
commands.append(ALIAS_CODE % (key, value))
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
name, properties = context[EVENT_CONTEXT_KEY]
|
||||
commands.append(EVENT_CODE % {'name': name,
|
||||
'properties': simplejson.dumps(properties)})
|
||||
commands.append(
|
||||
EVENT_CODE
|
||||
% {
|
||||
'name': name,
|
||||
'properties': json.dumps(properties, sort_keys=True),
|
||||
}
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
properties = context[PROPERTY_CONTEXT_KEY]
|
||||
commands.append(PROPERTY_CODE % {
|
||||
'properties': simplejson.dumps(properties)})
|
||||
commands.append(
|
||||
PROPERTY_CODE
|
||||
% {
|
||||
'properties': json.dumps(properties, sort_keys=True),
|
||||
}
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
html = TRACKING_CODE % {'api_key': self.api_key,
|
||||
'commands': " ".join(commands)}
|
||||
html = TRACKING_CODE % {
|
||||
'api_key': self.api_key,
|
||||
'commands': ' '.join(commands),
|
||||
}
|
||||
if is_internal_ip(context, 'KISS_METRICS'):
|
||||
html = disable_html(html, 'KISSmetrics')
|
||||
return html
|
||||
|
|
|
|||
60
analytical/templatetags/luckyorange.py
Normal file
60
analytical/templatetags/luckyorange.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Lucky Orange template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
LUCKYORANGE_TRACKING_CODE = """\
|
||||
<script type='text/javascript'>
|
||||
window.__lo_site_id = %(LUCKYORANGE_SITE_ID)s;
|
||||
(function() {
|
||||
var wa = document.createElement('script'); wa.type = 'text/javascript'; wa.async = true;
|
||||
wa.src = 'https://d10lpsik1i8c69.cloudfront.net/w.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(wa, s);
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
def _validate_no_args(token):
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
|
||||
|
||||
@register.tag
|
||||
def luckyorange(parser, token):
|
||||
"""
|
||||
Lucky Orange template tag.
|
||||
"""
|
||||
_validate_no_args(token)
|
||||
return LuckyOrangeNode()
|
||||
|
||||
|
||||
class LuckyOrangeNode(Node):
|
||||
def __init__(self):
|
||||
self.site_id = get_required_setting(
|
||||
'LUCKYORANGE_SITE_ID',
|
||||
re.compile(r'^\d+$'),
|
||||
'must be (a string containing) a number',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = LUCKYORANGE_TRACKING_CODE % {'LUCKYORANGE_SITE_ID': self.site_id}
|
||||
if is_internal_ip(context, 'LUCKYORANGE'):
|
||||
return disable_html(html, 'Lucky Orange')
|
||||
else:
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
# ensure properly configured
|
||||
LuckyOrangeNode()
|
||||
add_node('head_bottom', LuckyOrangeNode)
|
||||
152
analytical/templatetags/matomo.py
Normal file
152
analytical/templatetags/matomo.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
"""
|
||||
Matomo template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from itertools import chain
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
# domain name (characters separated by a dot), optional port, optional URI path, no slash
|
||||
DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$')
|
||||
|
||||
# numeric ID
|
||||
SITEID_RE = re.compile(r'^\d+$')
|
||||
|
||||
TRACKING_CODE = """
|
||||
<script>
|
||||
var _paq = window._paq || [];
|
||||
%(variables)s
|
||||
%(commands)s
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//%(url)s/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', %(siteid)s]);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<noscript><p><img src="//%(url)s/matomo.php?idsite=%(siteid)s" style="border:0;" alt="" /></p></noscript>
|
||||
""" # noqa
|
||||
|
||||
VARIABLE_CODE = (
|
||||
'_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa
|
||||
)
|
||||
IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);'
|
||||
DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);"
|
||||
|
||||
GIVE_CONSENT_CLASS = 'matomo_give_consent'
|
||||
REMOVE_CONSENT_CLASS = 'matomo_remove_consent'
|
||||
ASK_FOR_CONSENT_CODE = """
|
||||
_paq.push(['requireConsent']);
|
||||
|
||||
var elements = document.getElementsByClassName("{}");
|
||||
for (var i = 0; i < elements.length; i++) {{
|
||||
elements[i].addEventListener("click",
|
||||
function () {{
|
||||
_paq.push(["forgetConsentGiven"]);
|
||||
}}
|
||||
);
|
||||
}}
|
||||
|
||||
var elements = document.getElementsByClassName("{}");
|
||||
for (var i = 0; i < elements.length; i++) {{
|
||||
elements[i].addEventListener("click",
|
||||
function () {{
|
||||
_paq.push(["rememberConsentGiven"]);
|
||||
}}
|
||||
);
|
||||
}}
|
||||
""".format(REMOVE_CONSENT_CLASS, GIVE_CONSENT_CLASS)
|
||||
|
||||
DEFAULT_SCOPE = 'page'
|
||||
|
||||
MatomoVar = namedtuple('MatomoVar', ('index', 'name', 'value', 'scope'))
|
||||
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def matomo(parser, token):
|
||||
"""
|
||||
Matomo tracking template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your Matomo domain (plus optional URI path), and tracked site ID
|
||||
in the ``MATOMO_DOMAIN_PATH`` and the ``MATOMO_SITE_ID`` setting.
|
||||
|
||||
Custom variables can be passed in the ``matomo_vars`` context
|
||||
variable. It is an iterable of custom variables as tuples like:
|
||||
``(index, name, value[, scope])`` where scope may be ``'page'``
|
||||
(default) or ``'visit'``. Index should be an integer and the
|
||||
other parameters should be strings.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return MatomoNode()
|
||||
|
||||
|
||||
class MatomoNode(Node):
|
||||
def __init__(self):
|
||||
self.domain_path = get_required_setting(
|
||||
'MATOMO_DOMAIN_PATH',
|
||||
DOMAINPATH_RE,
|
||||
'must be a domain name, optionally followed '
|
||||
'by an URI path, no trailing slash (e.g. '
|
||||
'matomo.example.com or my.matomo.server/path)',
|
||||
)
|
||||
self.site_id = get_required_setting(
|
||||
'MATOMO_SITE_ID', SITEID_RE, 'must be a (string containing a) number'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
custom_variables = context.get('matomo_vars', ())
|
||||
|
||||
complete_variables = (
|
||||
var if len(var) >= 4 else var + (DEFAULT_SCOPE,) for var in custom_variables
|
||||
)
|
||||
|
||||
variables_code = (
|
||||
VARIABLE_CODE % MatomoVar(*var)._asdict() for var in complete_variables
|
||||
)
|
||||
|
||||
commands = []
|
||||
if getattr(settings, 'MATOMO_DISABLE_COOKIES', False):
|
||||
commands.append(DISABLE_COOKIES_CODE)
|
||||
|
||||
if getattr(settings, 'MATOMO_ASK_FOR_CONSENT', False):
|
||||
commands.append(ASK_FOR_CONSENT_CODE)
|
||||
|
||||
userid = get_identity(context, 'matomo')
|
||||
if userid is not None:
|
||||
variables_code = chain(
|
||||
variables_code, (IDENTITY_CODE % {'userid': userid},)
|
||||
)
|
||||
|
||||
html = TRACKING_CODE % {
|
||||
'url': self.domain_path,
|
||||
'siteid': self.site_id,
|
||||
'variables': '\n '.join(variables_code),
|
||||
'commands': '\n '.join(commands),
|
||||
}
|
||||
if is_internal_ip(context, 'MATOMO'):
|
||||
html = disable_html(html, 'Matomo')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
MatomoNode() # ensure properly configured
|
||||
add_node('body_bottom', MatomoNode)
|
||||
|
|
@ -2,28 +2,31 @@
|
|||
Mixpanel template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, get_identity, \
|
||||
get_required_setting
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
MIXPANEL_API_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$')
|
||||
TRACKING_CODE = """
|
||||
<script type="text/javascript">
|
||||
var mpq = [];
|
||||
mpq.push(['init', '%(token)s']);
|
||||
%(commands)s
|
||||
(function(){var b,a,e,d,c;b=document.createElement("script");b.type="text/javascript";b.async=true;b.src=(document.location.protocol==="https:"?"https:":"http:")+"//api.mixpanel.com/site_media/js/api/mixpanel.js";a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(b,a);e=function(f){return function(){mpq.push([f].concat(Array.prototype.slice.call(arguments,0)))}};d=["init","track","track_links","track_forms","register","register_once","identify","name_tag","set_config"];for(c=0;c<d.length;c++){mpq[d[c]]=e(d[c])}})();
|
||||
<script>(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"===e.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f);b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==
|
||||
typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,
|
||||
e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
|
||||
mixpanel.init('%(token)s');
|
||||
%(commands)s
|
||||
</script>
|
||||
"""
|
||||
IDENTIFY_CODE = "mpq.push(['identify', '%s']);"
|
||||
EVENT_CODE = "mpq.push(['track', '%(name)s', %(properties)s]);"
|
||||
""" # noqa
|
||||
IDENTIFY_CODE = "mixpanel.identify('%s');"
|
||||
IDENTIFY_PROPERTIES = 'mixpanel.people.set(%s);'
|
||||
EVENT_CODE = "mixpanel.track('%(name)s', %(properties)s);"
|
||||
EVENT_CONTEXT_KEY = 'mixpanel_event'
|
||||
|
||||
register = Library()
|
||||
|
|
@ -34,7 +37,7 @@ def mixpanel(parser, token):
|
|||
"""
|
||||
Mixpanel tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your Mixpanel token in the ``MIXPANEL_API_TOKEN`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
|
|
@ -42,28 +45,46 @@ def mixpanel(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return MixpanelNode()
|
||||
|
||||
|
||||
class MixpanelNode(Node):
|
||||
def __init__(self):
|
||||
self.token = get_required_setting(
|
||||
'MIXPANEL_API_TOKEN', MIXPANEL_API_TOKEN_RE,
|
||||
"must be a string containing a 32-digit hexadecimal number")
|
||||
self._token = get_required_setting(
|
||||
'MIXPANEL_API_TOKEN',
|
||||
MIXPANEL_API_TOKEN_RE,
|
||||
'must be a string containing a 32-digit hexadecimal number',
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
commands = []
|
||||
identity = get_identity(context, 'mixpanel')
|
||||
if identity is not None:
|
||||
commands.append(IDENTIFY_CODE % identity)
|
||||
if isinstance(identity, dict):
|
||||
commands.append(
|
||||
IDENTIFY_CODE % identity.get('id', identity.get('username'))
|
||||
)
|
||||
commands.append(
|
||||
IDENTIFY_PROPERTIES % json.dumps(identity, sort_keys=True)
|
||||
)
|
||||
else:
|
||||
commands.append(IDENTIFY_CODE % identity)
|
||||
try:
|
||||
name, properties = context[EVENT_CONTEXT_KEY]
|
||||
commands.append(EVENT_CODE % {'name': name,
|
||||
'properties': simplejson.dumps(properties)})
|
||||
commands.append(
|
||||
EVENT_CODE
|
||||
% {
|
||||
'name': name,
|
||||
'properties': json.dumps(properties, sort_keys=True),
|
||||
}
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
html = TRACKING_CODE % {'token': self.token,
|
||||
'commands': " ".join(commands)}
|
||||
html = TRACKING_CODE % {
|
||||
'token': self._token,
|
||||
'commands': ' '.join(commands),
|
||||
}
|
||||
if is_internal_ip(context, 'MIXPANEL'):
|
||||
html = disable_html(html, 'Mixpanel')
|
||||
return html
|
||||
return mark_safe(html)
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
|
|
|
|||
|
|
@ -2,36 +2,52 @@
|
|||
Olark template tags.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import get_identity, get_required_setting
|
||||
|
||||
|
||||
SITE_ID_RE = re.compile(r'^\d+-\d+-\d+-\d+$')
|
||||
SETUP_CODE = """
|
||||
<script type='text/javascript'>
|
||||
/*{literal}<![CDATA[*/ window.olark||(function(k){var g=window,j=document,a=g.location.protocol=="https:"?"https:":"http:",i=k.name,b="load",h="addEventListener";(function(){g[i]=function(){(c.s=c.s||[]).push(arguments)};var c=g[i]._={},f=k.methods.length;while(f--){(function(l){g[i][l]=function(){g[i]("call",l,arguments)}})(k.methods[f])}c.l=k.loader;c.i=arguments.callee;c.p={0:+new Date};c.P=function(l){c.p[l]=new Date-c.p[0]};function e(){c.P(b);g[i](b)}g[h]?g[h](b,e,false):g.attachEvent("on"+b,e);c.P(1);var d=j.createElement("script"),m=document.getElementsByTagName("script")[0];d.type="text/javascript";d.async=true;d.src=a+"//"+c.l;m.parentNode.insertBefore(d,m);c.P(2)})()})({loader:(function(a){return "static.olark.com/jsclient/loader1.js?ts="+(a?a[1]:(+new Date))})(document.cookie.match(/olarkld=([0-9]+)/)),name:"olark",methods:["configure","extend","declare","identify"]}); olark.identify('%(site_id)s');/*]]>{/literal}*/
|
||||
%(extra_code)s
|
||||
</script>
|
||||
"""
|
||||
""" # noqa
|
||||
NICKNAME_CODE = "olark('api.chat.updateVisitorNickname', {snippet: '%s'});"
|
||||
NICKNAME_CONTEXT_KEY = 'olark_nickname'
|
||||
FULLNAME_CODE = "olark('api.visitor.updateFullName', {{fullName: '{0}'}});"
|
||||
FULLNAME_CONTEXT_KEY = 'olark_fullname'
|
||||
EMAIL_CODE = "olark('api.visitor.updateEmailAddress', {{emailAddress: '{0}'}});"
|
||||
EMAIL_CONTEXT_KEY = 'olark_email'
|
||||
STATUS_CODE = "olark('api.chat.updateVisitorStatus', {snippet: %s});"
|
||||
STATUS_CONTEXT_KEY = 'olark_status'
|
||||
MESSAGE_CODE = "olark.configure('locale.%(key)s', \"%(msg)s\");"
|
||||
MESSAGE_KEYS = set(["welcome_title", "chatting_title", "unavailable_title",
|
||||
"busy_title", "away_message", "loading_title", "welcome_message",
|
||||
"busy_message", "chat_input_text", "name_input_text",
|
||||
"email_input_text", "offline_note_message", "send_button_text",
|
||||
"offline_note_thankyou_text", "offline_note_error_text",
|
||||
"offline_note_sending_text", "operator_is_typing_text",
|
||||
"operator_has_stopped_typing_text", "introduction_error_text",
|
||||
"introduction_messages", "introduction_submit_button_text"])
|
||||
MESSAGE_CODE = 'olark.configure(\'locale.%(key)s\', "%(msg)s");'
|
||||
MESSAGE_KEYS = {
|
||||
'welcome_title',
|
||||
'chatting_title',
|
||||
'unavailable_title',
|
||||
'busy_title',
|
||||
'away_message',
|
||||
'loading_title',
|
||||
'welcome_message',
|
||||
'busy_message',
|
||||
'chat_input_text',
|
||||
'name_input_text',
|
||||
'email_input_text',
|
||||
'offline_note_message',
|
||||
'send_button_text',
|
||||
'offline_note_thankyou_text',
|
||||
'offline_note_error_text',
|
||||
'offline_note_sending_text',
|
||||
'operator_is_typing_text',
|
||||
'operator_has_stopped_typing_text',
|
||||
'introduction_error_text',
|
||||
'introduction_messages',
|
||||
'introduction_submit_button_text',
|
||||
}
|
||||
|
||||
register = Library()
|
||||
|
||||
|
|
@ -41,7 +57,7 @@ def olark(parser, token):
|
|||
"""
|
||||
Olark set-up template tag.
|
||||
|
||||
Renders Javascript code to set-up Olark chat. You must supply
|
||||
Renders JavaScript code to set-up Olark chat. You must supply
|
||||
your site ID in the ``OLARK_SITE_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
|
|
@ -49,10 +65,14 @@ def olark(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return OlarkNode()
|
||||
|
||||
|
||||
class OlarkNode(Node):
|
||||
def __init__(self):
|
||||
self.site_id = get_required_setting('OLARK_SITE_ID', SITE_ID_RE,
|
||||
"must be a string looking like 'XXXX-XXX-XX-XXXX'")
|
||||
self.site_id = get_required_setting(
|
||||
'OLARK_SITE_ID',
|
||||
SITE_ID_RE,
|
||||
"must be a string looking like 'XXXX-XXX-XX-XXXX'",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
extra_code = []
|
||||
|
|
@ -63,19 +83,30 @@ class OlarkNode(Node):
|
|||
if identity is not None:
|
||||
extra_code.append(NICKNAME_CODE % identity)
|
||||
try:
|
||||
extra_code.append(STATUS_CODE %
|
||||
simplejson.dumps(context[STATUS_CONTEXT_KEY]))
|
||||
extra_code.append(FULLNAME_CODE.format(context[FULLNAME_CONTEXT_KEY]))
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
extra_code.append(EMAIL_CODE.format(context[EMAIL_CONTEXT_KEY]))
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
extra_code.append(
|
||||
STATUS_CODE % json.dumps(context[STATUS_CONTEXT_KEY], sort_keys=True)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
extra_code.extend(self._get_configuration(context))
|
||||
html = SETUP_CODE % {'site_id': self.site_id,
|
||||
'extra_code': " ".join(extra_code)}
|
||||
html = SETUP_CODE % {
|
||||
'site_id': self.site_id,
|
||||
'extra_code': ' '.join(extra_code),
|
||||
}
|
||||
return html
|
||||
|
||||
def _get_nickname(self, user):
|
||||
name = user.get_full_name()
|
||||
if name:
|
||||
return "%s (%s)" % (name, user.username)
|
||||
return '%s (%s)' % (name, user.username)
|
||||
else:
|
||||
return user.username
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@
|
|||
Optimizely template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, get_required_setting
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
|
||||
SETUP_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>"""
|
||||
|
|
@ -23,7 +20,7 @@ def optimizely(parser, token):
|
|||
"""
|
||||
Optimizely template tag.
|
||||
|
||||
Renders Javascript code to set-up A/B testing. You must supply
|
||||
Renders JavaScript code to set-up A/B testing. You must supply
|
||||
your Optimizely account number in the ``OPTIMIZELY_ACCOUNT_NUMBER``
|
||||
setting.
|
||||
"""
|
||||
|
|
@ -32,11 +29,14 @@ def optimizely(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return OptimizelyNode()
|
||||
|
||||
|
||||
class OptimizelyNode(Node):
|
||||
def __init__(self):
|
||||
self.account_number = get_required_setting(
|
||||
'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
|
||||
"must be a string looking like 'XXXXXXX'")
|
||||
'OPTIMIZELY_ACCOUNT_NUMBER',
|
||||
ACCOUNT_NUMBER_RE,
|
||||
"must be a string looking like 'XXXXXXX'",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = SETUP_CODE % {'account_number': self.account_number}
|
||||
|
|
|
|||
|
|
@ -2,36 +2,38 @@
|
|||
Performable template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from analytical.utils import is_internal_ip, disable_html, get_identity, \
|
||||
get_required_setting
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
API_KEY_RE = re.compile(r'^\w+$')
|
||||
SETUP_CODE = """
|
||||
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/pax/%(api_key)s.js" type="text/javascript"></script>
|
||||
"""
|
||||
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/pax/%(api_key)s.js"></script>
|
||||
""" # noqa
|
||||
IDENTIFY_CODE = """
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var _paq = _paq || [];
|
||||
_paq.push(["identify", {identity: "%s"}]);
|
||||
</script>
|
||||
"""
|
||||
EMBED_CODE = """
|
||||
<script type="text/javascript" src="//d1nu2rn22elx8m.cloudfront.net/performable/embed/page.js"></script>
|
||||
<script type="text/javascript">
|
||||
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/embed/page.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
var $f = new PerformableEmbed();
|
||||
$f.initialize({'host': '%(hostname)s', 'page': '%(page_id)s'});
|
||||
$f.write();
|
||||
})()
|
||||
</script>
|
||||
"""
|
||||
""" # noqa
|
||||
|
||||
register = Library()
|
||||
|
||||
|
|
@ -41,7 +43,7 @@ def performable(parser, token):
|
|||
"""
|
||||
Performable template tag.
|
||||
|
||||
Renders Javascript code to set-up Performable tracking. You must
|
||||
Renders JavaScript code to set-up Performable tracking. You must
|
||||
supply your Performable API key in the ``PERFORMABLE_API_KEY``
|
||||
setting.
|
||||
"""
|
||||
|
|
@ -50,16 +52,18 @@ def performable(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return PerformableNode()
|
||||
|
||||
|
||||
class PerformableNode(Node):
|
||||
def __init__(self):
|
||||
self.api_key = get_required_setting('PERFORMABLE_API_KEY', API_KEY_RE,
|
||||
"must be a string looking like 'XXXXX'")
|
||||
self.api_key = get_required_setting(
|
||||
'PERFORMABLE_API_KEY', API_KEY_RE, "must be a string looking like 'XXXXX'"
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = SETUP_CODE % {'api_key': self.api_key}
|
||||
identity = get_identity(context, 'performable')
|
||||
if identity is not None:
|
||||
html = "%s%s" % (IDENTIFY_CODE % identity, html)
|
||||
html = '%s%s' % (IDENTIFY_CODE % identity, html)
|
||||
if is_internal_ip(context, 'PERFORMABLE'):
|
||||
html = disable_html(html, 'Performable')
|
||||
return html
|
||||
|
|
@ -70,7 +74,13 @@ def performable_embed(hostname, page_id):
|
|||
"""
|
||||
Include a Performable landing page.
|
||||
"""
|
||||
return EMBED_CODE % {'hostname': hostname, 'page_id': page_id}
|
||||
return mark_safe(
|
||||
EMBED_CODE
|
||||
% {
|
||||
'hostname': hostname,
|
||||
'page_id': page_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
|
|
|
|||
67
analytical/templatetags/rating_mailru.py
Normal file
67
analytical/templatetags/rating_mailru.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""
|
||||
Rating@Mail.ru template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
COUNTER_ID_RE = re.compile(r'^\d{7}$')
|
||||
COUNTER_CODE = """
|
||||
<script>
|
||||
var _tmr = window._tmr || (window._tmr = []);
|
||||
_tmr.push({id: "%(counter_id)s", type: "pageView", start: (new Date()).getTime()});
|
||||
(function (d, w, id) {
|
||||
if (d.getElementById(id)) return;
|
||||
var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
|
||||
ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js";
|
||||
var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};
|
||||
if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
|
||||
})(document, window, "topmailru-code");
|
||||
</script>
|
||||
<noscript><div style="position:absolute;left:-10000px;">
|
||||
<img src="//top-fwz1.mail.ru/counter?id=%(counter_id)s;js=na" style="border:0;" height="1" width="1" alt="Rating@Mail.ru" />
|
||||
</div></noscript>
|
||||
""" # noqa
|
||||
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def rating_mailru(parser, token):
|
||||
"""
|
||||
Rating@Mail.ru counter template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your website counter ID (as a string) in the
|
||||
``RATING_MAILRU_COUNTER_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return RatingMailruNode()
|
||||
|
||||
|
||||
class RatingMailruNode(Node):
|
||||
def __init__(self):
|
||||
self.counter_id = get_required_setting(
|
||||
'RATING_MAILRU_COUNTER_ID',
|
||||
COUNTER_ID_RE,
|
||||
"must be (a string containing) a number'",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
html = COUNTER_CODE % {
|
||||
'counter_id': self.counter_id,
|
||||
}
|
||||
if is_internal_ip(context, 'RATING_MAILRU_METRICA'):
|
||||
html = disable_html(html, 'Rating@Mail.ru')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
RatingMailruNode() # ensure properly configured
|
||||
add_node('head_bottom', RatingMailruNode)
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
"""
|
||||
Reinvigorate template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import get_identity, is_internal_ip, disable_html, \
|
||||
get_required_setting
|
||||
|
||||
|
||||
TRACKING_ID_RE = re.compile(r'^[\w\d]+-[\w\d]+$')
|
||||
TRACKING_CODE = """
|
||||
<script type="text/javascript">
|
||||
document.write(unescape("%%3Cscript src='" + (("https:" == document.location.protocol) ? "https://ssl-" : "http://") + "include.reinvigorate.net/re_.js' type='text/javascript'%%3E%%3C/script%%3E"));
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
try {
|
||||
%(tags)s
|
||||
reinvigorate.track("%(tracking_id)s");
|
||||
} catch(err) {}
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def reinvigorate(parser, token):
|
||||
"""
|
||||
Reinvigorate tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
your Reinvigorate tracking ID (as a string) in the
|
||||
``REINVIGORATE_TRACKING_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return ReinvigorateNode()
|
||||
|
||||
class ReinvigorateNode(Node):
|
||||
def __init__(self):
|
||||
self.tracking_id = get_required_setting('REINVIGORATE_TRACKING_ID',
|
||||
TRACKING_ID_RE,
|
||||
"must be a string looking like XXXXX-XXXXXXXXXX")
|
||||
|
||||
def render(self, context):
|
||||
re_vars = {}
|
||||
for dict_ in context:
|
||||
for var, val in dict_.items():
|
||||
if var.startswith('reinvigorate_'):
|
||||
re_vars[var[13:]] = val
|
||||
if 'name' not in re_vars:
|
||||
identity = get_identity(context, 'reinvigorate',
|
||||
lambda u: u.get_full_name())
|
||||
if identity is not None:
|
||||
re_vars['name'] = identity
|
||||
if 'context' not in re_vars:
|
||||
email = get_identity(context, 'reinvigorate', lambda u: u.email)
|
||||
if email is not None:
|
||||
re_vars['context'] = email
|
||||
tags = " ".join("var re_%s_tag = %s;" % (tag, simplejson.dumps(value))
|
||||
for tag, value in re_vars.items())
|
||||
|
||||
html = TRACKING_CODE % {'tracking_id': self.tracking_id,
|
||||
'tags': tags}
|
||||
if is_internal_ip(context, 'REINVIGORATE'):
|
||||
html = disable_html(html, 'Reinvigorate')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
ReinvigorateNode() # ensure properly configured
|
||||
add_node('body_bottom', ReinvigorateNode)
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
SnapEngage template tags.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -12,7 +10,6 @@ from django.utils import translation
|
|||
|
||||
from analytical.utils import get_identity, get_required_setting
|
||||
|
||||
|
||||
BUTTON_LOCATION_LEFT = 0
|
||||
BUTTON_LOCATION_RIGHT = 1
|
||||
BUTTON_LOCATION_TOP = 2
|
||||
|
|
@ -27,17 +24,21 @@ FORM_POSITION_TOP_RIGHT = 'tr'
|
|||
FORM_POSITION_BOTTOM_LEFT = 'bl'
|
||||
FORM_POSITION_BOTTOM_RIGHT = 'br'
|
||||
|
||||
WIDGET_ID_RE = re.compile(r'^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$')
|
||||
WIDGET_ID_RE = re.compile(
|
||||
r'^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$'
|
||||
)
|
||||
SETUP_CODE = """
|
||||
<script type="text/javascript">
|
||||
document.write(unescape("%%3Cscript src='" + ((document.location.protocol=="https:")?"https://snapabug.appspot.com":"http://www.snapengage.com") + "/snapabug.js' type='text/javascript'%%3E%%3C/script%%3E"));</script><script type="text/javascript">
|
||||
<script>
|
||||
document.write(unescape("%%3Cscript src='" + ((document.location.protocol=="https:")?"https://snapabug.appspot.com":"http://www.snapengage.com") + "/snapabug.js' type='text/javascript'%%3E%%3C/script%%3E"));</script><script>
|
||||
%(settings_code)s
|
||||
</script>
|
||||
"""
|
||||
""" # noqa
|
||||
DOMAIN_CODE = 'SnapABug.setDomain("%s");'
|
||||
SECURE_CONNECTION_CODE = 'SnapABug.setSecureConnexion();'
|
||||
INIT_CODE = 'SnapABug.init("%s");'
|
||||
ADDBUTTON_CODE = 'SnapABug.addButton("%(id)s","%(location)s","%(offset)s"%(dynamic_tail)s);'
|
||||
ADDBUTTON_CODE = (
|
||||
'SnapABug.addButton("%(id)s","%(location)s","%(offset)s"%(dynamic_tail)s);'
|
||||
)
|
||||
SETBUTTON_CODE = 'SnapABug.setButton("%s");'
|
||||
SETEMAIL_CODE = 'SnapABug.setUserEmail("%s"%s);'
|
||||
SETLOCALE_CODE = 'SnapABug.setLocale("%s");'
|
||||
|
|
@ -58,7 +59,7 @@ def snapengage(parser, token):
|
|||
"""
|
||||
SnapEngage set-up template tag.
|
||||
|
||||
Renders Javascript code to set-up SnapEngage chat. You must supply
|
||||
Renders JavaScript code to set-up SnapEngage chat. You must supply
|
||||
your widget ID in the ``SNAPENGAGE_WIDGET_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
|
|
@ -66,23 +67,28 @@ def snapengage(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return SnapEngageNode()
|
||||
|
||||
|
||||
class SnapEngageNode(Node):
|
||||
def __init__(self):
|
||||
self.widget_id = get_required_setting('SNAPENGAGE_WIDGET_ID',
|
||||
WIDGET_ID_RE, "must be a string looking like this: "
|
||||
"'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'")
|
||||
self.widget_id = get_required_setting(
|
||||
'SNAPENGAGE_WIDGET_ID',
|
||||
WIDGET_ID_RE,
|
||||
"must be a string looking like this: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
settings_code = []
|
||||
|
||||
domain = self._get_setting(context, 'snapengage_domain',
|
||||
'SNAPENGAGE_DOMAIN')
|
||||
domain = self._get_setting(context, 'snapengage_domain', 'SNAPENGAGE_DOMAIN')
|
||||
if domain is not None:
|
||||
settings_code.append(DOMAIN_CODE % domain)
|
||||
|
||||
secure_connection = self._get_setting(context,
|
||||
'snapengage_secure_connection', 'SNAPENGAGE_SECURE_CONNECTION',
|
||||
False)
|
||||
secure_connection = self._get_setting(
|
||||
context,
|
||||
'snapengage_secure_connection',
|
||||
'SNAPENGAGE_SECURE_CONNECTION',
|
||||
False,
|
||||
)
|
||||
if secure_connection:
|
||||
settings_code.append(SECURE_CONNECTION_CODE)
|
||||
|
||||
|
|
@ -90,80 +96,101 @@ class SnapEngageNode(Node):
|
|||
if email is None:
|
||||
email = get_identity(context, 'snapengage', lambda u: u.email)
|
||||
if email is not None:
|
||||
if self._get_setting(context, 'snapengage_readonly_email',
|
||||
'SNAPENGAGE_READONLY_EMAIL', False):
|
||||
if self._get_setting(
|
||||
context, 'snapengage_readonly_email', 'SNAPENGAGE_READONLY_EMAIL', False
|
||||
):
|
||||
readonly_tail = ',true'
|
||||
else:
|
||||
readonly_tail = ''
|
||||
settings_code.append(SETEMAIL_CODE % (email, readonly_tail))
|
||||
|
||||
locale = self._get_setting(context, 'snapengage_locale',
|
||||
'SNAPENGAGE_LOCALE')
|
||||
locale = self._get_setting(context, 'snapengage_locale', 'SNAPENGAGE_LOCALE')
|
||||
if locale is None:
|
||||
locale = translation.to_locale(translation.get_language())
|
||||
settings_code.append(SETLOCALE_CODE % locale)
|
||||
|
||||
form_position = self._get_setting(context,
|
||||
'snapengage_form_position', 'SNAPENGAGE_FORM_POSITION')
|
||||
form_position = self._get_setting(
|
||||
context, 'snapengage_form_position', 'SNAPENGAGE_FORM_POSITION'
|
||||
)
|
||||
if form_position is not None:
|
||||
settings_code.append(FORM_POSITION_CODE % form_position)
|
||||
|
||||
form_top_position = self._get_setting(context,
|
||||
'snapengage_form_top_position', 'SNAPENGAGE_FORM_TOP_POSITION')
|
||||
form_top_position = self._get_setting(
|
||||
context, 'snapengage_form_top_position', 'SNAPENGAGE_FORM_TOP_POSITION'
|
||||
)
|
||||
if form_top_position is not None:
|
||||
settings_code.append(FORM_TOP_POSITION_CODE % form_top_position)
|
||||
|
||||
show_offline = self._get_setting(context, 'snapengage_show_offline',
|
||||
'SNAPENGAGE_SHOW_OFFLINE', True)
|
||||
show_offline = self._get_setting(
|
||||
context, 'snapengage_show_offline', 'SNAPENGAGE_SHOW_OFFLINE', True
|
||||
)
|
||||
if not show_offline:
|
||||
settings_code.append(DISABLE_OFFLINE_CODE)
|
||||
|
||||
screenshots = self._get_setting(context, 'snapengage_screenshots',
|
||||
'SNAPENGAGE_SCREENSHOTS', True)
|
||||
screenshots = self._get_setting(
|
||||
context, 'snapengage_screenshots', 'SNAPENGAGE_SCREENSHOTS', True
|
||||
)
|
||||
if not screenshots:
|
||||
settings_code.append(DISABLE_SCREENSHOT_CODE)
|
||||
|
||||
offline_screenshots = self._get_setting(context,
|
||||
offline_screenshots = self._get_setting(
|
||||
context,
|
||||
'snapengage_offline_screenshots',
|
||||
'SNAPENGAGE_OFFLINE_SCREENSHOTS', True)
|
||||
'SNAPENGAGE_OFFLINE_SCREENSHOTS',
|
||||
True,
|
||||
)
|
||||
if not offline_screenshots:
|
||||
settings_code.append(DISABLE_OFFLINE_SCREENSHOT_CODE)
|
||||
|
||||
if not context.get('snapengage_proactive_chat', True):
|
||||
settings_code.append(DISABLE_PROACTIVE_CHAT_CODE)
|
||||
|
||||
sounds = self._get_setting(context, 'snapengage_sounds',
|
||||
'SNAPENGAGE_SOUNDS', True)
|
||||
sounds = self._get_setting(
|
||||
context, 'snapengage_sounds', 'SNAPENGAGE_SOUNDS', True
|
||||
)
|
||||
if not sounds:
|
||||
settings_code.append(DISABLE_SOUNDS_CODE)
|
||||
|
||||
button_effect = self._get_setting(context, 'snapengage_button_effect',
|
||||
'SNAPENGAGE_BUTTON_EFFECT')
|
||||
button_effect = self._get_setting(
|
||||
context, 'snapengage_button_effect', 'SNAPENGAGE_BUTTON_EFFECT'
|
||||
)
|
||||
if button_effect is not None:
|
||||
settings_code.append(BUTTONEFFECT_CODE % button_effect)
|
||||
|
||||
button = self._get_setting(context, 'snapengage_button',
|
||||
'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT)
|
||||
button = self._get_setting(
|
||||
context, 'snapengage_button', 'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT
|
||||
)
|
||||
if button == BUTTON_STYLE_NONE:
|
||||
settings_code.append(INIT_CODE % self.widget_id)
|
||||
else:
|
||||
if not isinstance(button, int):
|
||||
# Assume button as a URL to a custom image
|
||||
settings_code.append(SETBUTTON_CODE % button)
|
||||
button_location = self._get_setting(context,
|
||||
'snapengage_button_location', 'SNAPENGAGE_BUTTON_LOCATION',
|
||||
BUTTON_LOCATION_LEFT)
|
||||
button_offset = self._get_setting(context,
|
||||
'snapengage_button_location_offset',
|
||||
'SNAPENGAGE_BUTTON_LOCATION_OFFSET', '55%')
|
||||
settings_code.append(ADDBUTTON_CODE % {
|
||||
'id': self.widget_id,
|
||||
'location': button_location,
|
||||
'offset': button_offset,
|
||||
'dynamic_tail': ',true' if (button == BUTTON_STYLE_LIVE) else '',
|
||||
})
|
||||
html = SETUP_CODE % {'widget_id': self.widget_id,
|
||||
'settings_code': " ".join(settings_code)}
|
||||
button_location = self._get_setting(
|
||||
context,
|
||||
'snapengage_button_location',
|
||||
'SNAPENGAGE_BUTTON_LOCATION',
|
||||
BUTTON_LOCATION_LEFT,
|
||||
)
|
||||
button_offset = self._get_setting(
|
||||
context,
|
||||
'snapengage_button_location_offset',
|
||||
'SNAPENGAGE_BUTTON_LOCATION_OFFSET',
|
||||
'55%',
|
||||
)
|
||||
settings_code.append(
|
||||
ADDBUTTON_CODE
|
||||
% {
|
||||
'id': self.widget_id,
|
||||
'location': button_location,
|
||||
'offset': button_offset,
|
||||
'dynamic_tail': ',true' if (button == BUTTON_STYLE_LIVE) else '',
|
||||
}
|
||||
)
|
||||
html = SETUP_CODE % {
|
||||
'widget_id': self.widget_id,
|
||||
'settings_code': ' '.join(settings_code),
|
||||
}
|
||||
return html
|
||||
|
||||
def _get_setting(self, context, context_key, setting=None, default=None):
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
Spring Metrics template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import get_identity, is_internal_ip, disable_html, \
|
||||
get_required_setting
|
||||
|
||||
from analytical.utils import (
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
TRACKING_ID_RE = re.compile(r'^[0-9a-f]+$')
|
||||
TRACKING_CODE = """
|
||||
|
|
@ -30,8 +30,7 @@ TRACKING_CODE = """
|
|||
)();
|
||||
%(custom_commands)s
|
||||
</script>
|
||||
"""
|
||||
|
||||
""" # noqa
|
||||
|
||||
register = Library()
|
||||
|
||||
|
|
@ -41,7 +40,7 @@ def spring_metrics(parser, token):
|
|||
"""
|
||||
Spring Metrics tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your Spring Metrics Tracking ID in the
|
||||
``SPRING_METRICS_TRACKING_ID`` setting.
|
||||
"""
|
||||
|
|
@ -50,10 +49,12 @@ def spring_metrics(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return SpringMetricsNode()
|
||||
|
||||
|
||||
class SpringMetricsNode(Node):
|
||||
def __init__(self):
|
||||
self.tracking_id = get_required_setting('SPRING_METRICS_TRACKING_ID',
|
||||
TRACKING_ID_RE, "must be a hexadecimal string")
|
||||
self.tracking_id = get_required_setting(
|
||||
'SPRING_METRICS_TRACKING_ID', TRACKING_ID_RE, 'must be a hexadecimal string'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
custom = {}
|
||||
|
|
@ -62,25 +63,28 @@ class SpringMetricsNode(Node):
|
|||
if var.startswith('spring_metrics_'):
|
||||
custom[var[15:]] = val
|
||||
if 'email' not in custom:
|
||||
identity = get_identity(context, 'spring_metrics',
|
||||
lambda u: u.email)
|
||||
identity = get_identity(context, 'spring_metrics', lambda u: u.email)
|
||||
if identity is not None:
|
||||
custom['email'] = identity
|
||||
|
||||
html = TRACKING_CODE % {'tracking_id': self.tracking_id,
|
||||
'custom_commands': self._generate_custom_javascript(custom)}
|
||||
html = TRACKING_CODE % {
|
||||
'tracking_id': self.tracking_id,
|
||||
'custom_commands': self._generate_custom_javascript(custom),
|
||||
}
|
||||
if is_internal_ip(context, 'SPRING_METRICS'):
|
||||
html = disable_html(html, 'Spring Metrics')
|
||||
return html
|
||||
|
||||
def _generate_custom_javascript(self, vars):
|
||||
def _generate_custom_javascript(self, params):
|
||||
commands = []
|
||||
convert = vars.pop('convert', None)
|
||||
convert = params.pop('convert', None)
|
||||
if convert is not None:
|
||||
commands.append("_springMetq.push(['convert', '%s'])" % convert)
|
||||
commands.extend("_springMetq.push(['setdata', {'%s': '%s'}]);"
|
||||
% (var, val) for var, val in vars.items())
|
||||
return " ".join(commands)
|
||||
commands.extend(
|
||||
"_springMetq.push(['setdata', {'%s': '%s'}]);" % (var, val)
|
||||
for var, val in params.items()
|
||||
)
|
||||
return ' '.join(commands)
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
|
|
|
|||
90
analytical/templatetags/uservoice.py
Normal file
90
analytical/templatetags/uservoice.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""
|
||||
UserVoice template tags.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import get_identity, get_required_setting
|
||||
|
||||
WIDGET_KEY_RE = re.compile(r'^[a-zA-Z0-9]*$')
|
||||
TRACKING_CODE = """
|
||||
<script>
|
||||
|
||||
UserVoice=window.UserVoice||[];(function(){
|
||||
var uv=document.createElement('script');uv.type='text/javascript';
|
||||
uv.async=true;uv.src='//widget.uservoice.com/%(widget_key)s.js';
|
||||
var s=document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(uv,s)})();
|
||||
|
||||
UserVoice.push(['set', %(options)s]);
|
||||
%(trigger)s
|
||||
%(identity)s
|
||||
</script>
|
||||
"""
|
||||
IDENTITY = """UserVoice.push(['identify', %(options)s]);"""
|
||||
TRIGGER = "UserVoice.push(['addTrigger', {}]);"
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def uservoice(parser, token):
|
||||
"""
|
||||
UserVoice tracking template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your UserVoice Widget Key in the ``USERVOICE_WIDGET_KEY``
|
||||
setting or the ``uservoice_widget_key`` template context variable.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return UserVoiceNode()
|
||||
|
||||
|
||||
class UserVoiceNode(Node):
|
||||
def __init__(self):
|
||||
self.default_widget_key = get_required_setting(
|
||||
'USERVOICE_WIDGET_KEY', WIDGET_KEY_RE, 'must be an alphanumeric string'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
widget_key = context.get('uservoice_widget_key')
|
||||
if not widget_key:
|
||||
widget_key = self.default_widget_key
|
||||
if not widget_key:
|
||||
return ''
|
||||
# default
|
||||
options = {}
|
||||
options.update(getattr(settings, 'USERVOICE_WIDGET_OPTIONS', {}))
|
||||
options.update(context.get('uservoice_widget_options', {}))
|
||||
|
||||
identity = get_identity(context, 'uservoice', self._identify)
|
||||
if identity:
|
||||
identity = IDENTITY % {'options': json.dumps(identity, sort_keys=True)}
|
||||
|
||||
trigger = context.get(
|
||||
'uservoice_add_trigger', getattr(settings, 'USERVOICE_ADD_TRIGGER', True)
|
||||
)
|
||||
|
||||
html = TRACKING_CODE % {
|
||||
'widget_key': widget_key,
|
||||
'options': json.dumps(options, sort_keys=True),
|
||||
'trigger': TRIGGER if trigger else '',
|
||||
'identity': identity if identity else '',
|
||||
}
|
||||
return html
|
||||
|
||||
def _identify(self, user):
|
||||
name = user.get_full_name()
|
||||
if not name:
|
||||
name = user.username
|
||||
return {'name': name, 'email': user.email}
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
UserVoiceNode() # ensure properly configured
|
||||
add_node('body_bottom', UserVoiceNode)
|
||||
|
|
@ -2,34 +2,34 @@
|
|||
Woopra template tags and filters.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import re
|
||||
from contextlib import suppress
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.utils import simplejson
|
||||
|
||||
from analytical.utils import get_identity, get_user_from_context, \
|
||||
is_internal_ip, disable_html, get_required_setting
|
||||
|
||||
from analytical.utils import (
|
||||
AnalyticalException,
|
||||
disable_html,
|
||||
get_identity,
|
||||
get_required_setting,
|
||||
get_user_from_context,
|
||||
get_user_is_authenticated,
|
||||
is_internal_ip,
|
||||
)
|
||||
|
||||
DOMAIN_RE = re.compile(r'^\S+$')
|
||||
TRACKING_CODE = """
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
var woo_settings = %(settings)s;
|
||||
var woo_visitor = %(visitor)s;
|
||||
(function(){
|
||||
var wsc=document.createElement('script');
|
||||
wsc.type='text/javascript';
|
||||
wsc.src=document.location.protocol+'//static.woopra.com/js/woopra.js';
|
||||
wsc.async=true;
|
||||
var ssc = document.getElementsByTagName('script')[0];
|
||||
ssc.parentNode.insertBefore(wsc, ssc);
|
||||
})();
|
||||
!function(){var a,b,c,d=window,e=document,f=arguments,g="script",h=["config","track","trackForm","trackClick","identify","visit","push","call"],i=function(){var a,b=this,c=function(a){b[a]=function(){return b._e.push([a].concat(Array.prototype.slice.call(arguments,0))),b}};for(b._e=[],a=0;a<h.length;a++)c(h[a])};for(d.__woo=d.__woo||{},a=0;a<f.length;a++)d.__woo[f[a]]=d[f[a]]=d[f[a]]||new i;b=e.createElement(g),b.async=1,b.src="//static.woopra.com/js/w.js",c=e.getElementsByTagName(g)[0],c.parentNode.insertBefore(b,c)}("woopra");
|
||||
woopra.config(woo_settings);
|
||||
woopra.identify(woo_visitor);
|
||||
woopra.track();
|
||||
</script>
|
||||
"""
|
||||
|
||||
""" # noqa
|
||||
|
||||
register = Library()
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ def woopra(parser, token):
|
|||
"""
|
||||
Woopra tracking template tag.
|
||||
|
||||
Renders Javascript code to track page visits. You must supply
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your Woopra domain in the ``WOOPRA_DOMAIN`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
|
|
@ -47,45 +47,78 @@ def woopra(parser, token):
|
|||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return WoopraNode()
|
||||
|
||||
|
||||
class WoopraNode(Node):
|
||||
def __init__(self):
|
||||
self.domain = get_required_setting('WOOPRA_DOMAIN', DOMAIN_RE,
|
||||
"must be a domain name")
|
||||
self.domain = get_required_setting(
|
||||
'WOOPRA_DOMAIN', DOMAIN_RE, 'must be a domain name'
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
settings = self._get_settings(context)
|
||||
visitor = self._get_visitor(context)
|
||||
|
||||
html = TRACKING_CODE % {
|
||||
'settings': simplejson.dumps(settings),
|
||||
'visitor': simplejson.dumps(visitor),
|
||||
'settings': json.dumps(settings, sort_keys=True),
|
||||
'visitor': json.dumps(visitor, sort_keys=True),
|
||||
}
|
||||
if is_internal_ip(context, 'WOOPRA'):
|
||||
html = disable_html(html, 'Woopra')
|
||||
return html
|
||||
|
||||
def _get_settings(self, context):
|
||||
vars = {'domain': self.domain}
|
||||
try:
|
||||
vars['idle_timeout'] = str(settings.WOOPRA_IDLE_TIMEOUT)
|
||||
except AttributeError:
|
||||
pass
|
||||
return vars
|
||||
variables = {'domain': self.domain}
|
||||
woopra_int_settings = {
|
||||
'idle_timeout': 'WOOPRA_IDLE_TIMEOUT',
|
||||
}
|
||||
woopra_str_settings = {
|
||||
'cookie_name': 'WOOPRA_COOKIE_NAME',
|
||||
'cookie_domain': 'WOOPRA_COOKIE_DOMAIN',
|
||||
'cookie_path': 'WOOPRA_COOKIE_PATH',
|
||||
'cookie_expire': 'WOOPRA_COOKIE_EXPIRE',
|
||||
}
|
||||
woopra_bool_settings = {
|
||||
'click_tracking': 'WOOPRA_CLICK_TRACKING',
|
||||
'download_tracking': 'WOOPRA_DOWNLOAD_TRACKING',
|
||||
'outgoing_tracking': 'WOOPRA_OUTGOING_TRACKING',
|
||||
'outgoing_ignore_subdomain': 'WOOPRA_OUTGOING_IGNORE_SUBDOMAIN',
|
||||
'ignore_query_url': 'WOOPRA_IGNORE_QUERY_URL',
|
||||
'hide_campaign': 'WOOPRA_HIDE_CAMPAIGN',
|
||||
}
|
||||
|
||||
for key, name in woopra_int_settings.items():
|
||||
with suppress(AttributeError):
|
||||
variables[key] = getattr(settings, name)
|
||||
if type(variables[key]) is not int:
|
||||
raise AnalyticalException(f'{name} must be an int value')
|
||||
|
||||
for key, name in woopra_str_settings.items():
|
||||
with suppress(AttributeError):
|
||||
variables[key] = getattr(settings, name)
|
||||
if type(variables[key]) is not str:
|
||||
raise AnalyticalException(f'{name} must be a string value')
|
||||
|
||||
for key, name in woopra_bool_settings.items():
|
||||
with suppress(AttributeError):
|
||||
variables[key] = getattr(settings, name)
|
||||
if type(variables[key]) is not bool:
|
||||
raise AnalyticalException(f'{name} must be a boolean value')
|
||||
|
||||
return variables
|
||||
|
||||
def _get_visitor(self, context):
|
||||
vars = {}
|
||||
params = {}
|
||||
for dict_ in context:
|
||||
for var, val in dict_.items():
|
||||
if var.startswith('woopra_'):
|
||||
vars[var[7:]] = val
|
||||
if 'name' not in vars and 'email' not in vars:
|
||||
params[var[7:]] = val
|
||||
if 'name' not in params and 'email' not in params:
|
||||
user = get_user_from_context(context)
|
||||
if user is not None and user.is_authenticated():
|
||||
vars['name'] = get_identity(context, 'woopra',
|
||||
self._identify, user)
|
||||
if user is not None and get_user_is_authenticated(user):
|
||||
params['name'] = get_identity(context, 'woopra', self._identify, user)
|
||||
if user.email:
|
||||
vars['email'] = user.email
|
||||
return vars
|
||||
params['email'] = user.email
|
||||
return params
|
||||
|
||||
def _identify(self, user):
|
||||
name = user.get_full_name()
|
||||
|
|
|
|||
91
analytical/templatetags/yandex_metrica.py
Normal file
91
analytical/templatetags/yandex_metrica.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""
|
||||
Yandex.Metrica template tags and filters.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.template import Library, Node, TemplateSyntaxError
|
||||
|
||||
from analytical.utils import disable_html, get_required_setting, is_internal_ip
|
||||
|
||||
COUNTER_ID_RE = re.compile(r'^\d{8}$')
|
||||
COUNTER_CODE = """
|
||||
<script>
|
||||
(function (d, w, c) {
|
||||
(w[c] = w[c] || []).push(function() {
|
||||
try {
|
||||
w.yaCounter%(counter_id)s = new Ya.Metrika(%(options)s);
|
||||
} catch(e) { }
|
||||
});
|
||||
|
||||
var n = d.getElementsByTagName("script")[0],
|
||||
s = d.createElement("script"),
|
||||
f = function () { n.parentNode.insertBefore(s, n); };
|
||||
s.type = "text/javascript";
|
||||
s.async = true;
|
||||
s.src = "https://mc.yandex.ru/metrika/watch.js";
|
||||
|
||||
if (w.opera == "[object Opera]") {
|
||||
d.addEventListener("DOMContentLoaded", f, false);
|
||||
} else { f(); }
|
||||
})(document, window, "yandex_metrika_callbacks");
|
||||
</script>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/%(counter_id)s" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
""" # noqa
|
||||
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.tag
|
||||
def yandex_metrica(parser, token):
|
||||
"""
|
||||
Yandex.Metrica counter template tag.
|
||||
|
||||
Renders JavaScript code to track page visits. You must supply
|
||||
your website counter ID (as a string) in the
|
||||
``YANDEX_METRICA_COUNTER_ID`` setting.
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) > 1:
|
||||
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
|
||||
return YandexMetricaNode()
|
||||
|
||||
|
||||
class YandexMetricaNode(Node):
|
||||
def __init__(self):
|
||||
self.counter_id = get_required_setting(
|
||||
'YANDEX_METRICA_COUNTER_ID',
|
||||
COUNTER_ID_RE,
|
||||
"must be (a string containing) a number'",
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
options = {
|
||||
'id': int(self.counter_id),
|
||||
'clickmap': True,
|
||||
'trackLinks': True,
|
||||
'accurateTrackBounce': True,
|
||||
}
|
||||
if getattr(settings, 'YANDEX_METRICA_WEBVISOR', False):
|
||||
options['webvisor'] = True
|
||||
if getattr(settings, 'YANDEX_METRICA_TRACKHASH', False):
|
||||
options['trackHash'] = True
|
||||
if getattr(settings, 'YANDEX_METRICA_NOINDEX', False):
|
||||
options['ut'] = 'noindex'
|
||||
if getattr(settings, 'YANDEX_METRICA_ECOMMERCE', False):
|
||||
options['ecommerce'] = 'dataLayer'
|
||||
html = COUNTER_CODE % {
|
||||
'counter_id': self.counter_id,
|
||||
'options': json.dumps(options),
|
||||
}
|
||||
if is_internal_ip(context, 'YANDEX_METRICA'):
|
||||
html = disable_html(html, 'Yandex.Metrica')
|
||||
return html
|
||||
|
||||
|
||||
def contribute_to_analytical(add_node):
|
||||
YandexMetricaNode() # ensure properly configured
|
||||
add_node('head_bottom', YandexMetricaNode)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
"""
|
||||
Tests for django-analytical.
|
||||
"""
|
||||
|
||||
from analytical.tests.test_tag_analytical import *
|
||||
from analytical.tests.test_tag_chartbeat import *
|
||||
from analytical.tests.test_tag_clicky import *
|
||||
from analytical.tests.test_tag_crazy_egg import *
|
||||
from analytical.tests.test_tag_google_analytics import *
|
||||
from analytical.tests.test_tag_gosquared import *
|
||||
from analytical.tests.test_tag_hubspot import *
|
||||
from analytical.tests.test_tag_kiss_insights import *
|
||||
from analytical.tests.test_tag_kiss_metrics import *
|
||||
from analytical.tests.test_tag_mixpanel import *
|
||||
from analytical.tests.test_tag_olark import *
|
||||
from analytical.tests.test_tag_optimizely import *
|
||||
from analytical.tests.test_tag_performable import *
|
||||
from analytical.tests.test_tag_reinvigorate import *
|
||||
from analytical.tests.test_tag_snapengage import *
|
||||
from analytical.tests.test_tag_spring_metrics import *
|
||||
from analytical.tests.test_tag_woopra import *
|
||||
from analytical.tests.test_utils import *
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""
|
||||
django-analytical testing settings.
|
||||
"""
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'analytical',
|
||||
]
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
"""
|
||||
Tests for the Chartbeat template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
from django.test import TestCase
|
||||
|
||||
from analytical.templatetags.chartbeat import ChartbeatTopNode, \
|
||||
ChartbeatBottomNode
|
||||
from analytical.tests.utils import TagTestCase, with_apps, without_apps, \
|
||||
override_settings, SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@without_apps('django.contrib.sites')
|
||||
@override_settings(CHARTBEAT_USER_ID='12345')
|
||||
class ChartbeatTagTestCaseNoSites(TestCase):
|
||||
def test_rendering_setup_no_site(self):
|
||||
r = ChartbeatBottomNode().render(Context())
|
||||
self.assertTrue('var _sf_async_config={"uid": "12345"};' in r, r)
|
||||
|
||||
|
||||
@with_apps('django.contrib.sites')
|
||||
@override_settings(CHARTBEAT_USER_ID='12345')
|
||||
class ChartbeatTagTestCaseWithSites(TestCase):
|
||||
def setUp(self):
|
||||
from django.core.management import call_command
|
||||
from django.db.models import loading
|
||||
loading.cache.loaded = False
|
||||
call_command("syncdb", verbosity=0)
|
||||
|
||||
def test_rendering_setup_site(self):
|
||||
site = Site.objects.create(domain="test.com", name="test")
|
||||
with override_settings(SITE_ID=site.id):
|
||||
r = ChartbeatBottomNode().render(Context())
|
||||
self.assertTrue(re.search(
|
||||
'var _sf_async_config={.*"uid": "12345".*};', r), r)
|
||||
self.assertTrue(re.search(
|
||||
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
|
||||
|
||||
@override_settings(CHARTBEAT_AUTO_DOMAIN=False)
|
||||
def test_auto_domain_false(self):
|
||||
"""
|
||||
Even if 'django.contrib.sites' is in INSTALLED_APPS, if
|
||||
CHARTBEAT_AUTO_DOMAIN is False, ensure there is no 'domain'
|
||||
in _sf_async_config.
|
||||
"""
|
||||
r = ChartbeatBottomNode().render(Context())
|
||||
self.assertTrue('var _sf_async_config={"uid": "12345"};' in r, r)
|
||||
|
||||
|
||||
@override_settings(CHARTBEAT_USER_ID='12345')
|
||||
class ChartbeatTagTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``chartbeat`` template tag.
|
||||
"""
|
||||
|
||||
def test_top_tag(self):
|
||||
r = self.render_tag('chartbeat', 'chartbeat_top',
|
||||
{'chartbeat_domain': "test.com"})
|
||||
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
|
||||
|
||||
def test_bottom_tag(self):
|
||||
r = self.render_tag('chartbeat', 'chartbeat_bottom',
|
||||
{'chartbeat_domain': "test.com"})
|
||||
self.assertTrue(re.search(
|
||||
'var _sf_async_config={.*"uid": "12345".*};', r), r)
|
||||
self.assertTrue(re.search(
|
||||
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
|
||||
|
||||
def test_top_node(self):
|
||||
r = ChartbeatTopNode().render(
|
||||
Context({'chartbeat_domain': "test.com"}))
|
||||
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
|
||||
|
||||
def test_bottom_node(self):
|
||||
r = ChartbeatBottomNode().render(
|
||||
Context({'chartbeat_domain': "test.com"}))
|
||||
self.assertTrue(re.search(
|
||||
'var _sf_async_config={.*"uid": "12345".*};', r), r)
|
||||
self.assertTrue(re.search(
|
||||
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
|
||||
|
||||
@override_settings(CHARTBEAT_USER_ID=SETTING_DELETED)
|
||||
def test_no_user_id(self):
|
||||
self.assertRaises(AnalyticalException, ChartbeatBottomNode)
|
||||
|
||||
@override_settings(CHARTBEAT_USER_ID='123abc')
|
||||
def test_wrong_user_id(self):
|
||||
self.assertRaises(AnalyticalException, ChartbeatBottomNode)
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
r = ChartbeatBottomNode().render(context)
|
||||
self.assertTrue(r.startswith(
|
||||
'<!-- Chartbeat disabled on internal IP address'), r)
|
||||
self.assertTrue(r.endswith('-->'), r)
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
"""
|
||||
Tests for the Google Analytics template tags and filters.
|
||||
"""
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
|
||||
from analytical.templatetags.google_analytics import GoogleAnalyticsNode, \
|
||||
TRACK_SINGLE_DOMAIN, TRACK_MULTIPLE_DOMAINS, TRACK_MULTIPLE_SUBDOMAINS,\
|
||||
SCOPE_VISITOR, SCOPE_SESSION, SCOPE_PAGE
|
||||
from analytical.tests.utils import TestCase, TagTestCase, override_settings, \
|
||||
without_apps, SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7',
|
||||
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN)
|
||||
class GoogleAnalyticsTagTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``google_analytics`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('google_analytics', 'google_analytics')
|
||||
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
|
||||
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = GoogleAnalyticsNode().render(Context())
|
||||
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
|
||||
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
|
||||
|
||||
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID=SETTING_DELETED)
|
||||
def test_no_property_id(self):
|
||||
self.assertRaises(AnalyticalException, GoogleAnalyticsNode)
|
||||
|
||||
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='wrong')
|
||||
def test_wrong_property_id(self):
|
||||
self.assertRaises(AnalyticalException, GoogleAnalyticsNode)
|
||||
|
||||
@override_settings(
|
||||
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_SUBDOMAINS,
|
||||
GOOGLE_ANALYTICS_DOMAIN='example.com')
|
||||
def test_track_multiple_subdomains(self):
|
||||
r = GoogleAnalyticsNode().render(Context())
|
||||
self.assertTrue("_gaq.push(['_setDomainName', 'example.com']);" in r, r)
|
||||
self.assertTrue("_gaq.push(['_setAllowHash', false]);" in r, r)
|
||||
|
||||
@override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
|
||||
GOOGLE_ANALYTICS_DOMAIN='example.com')
|
||||
def test_track_multiple_domains(self):
|
||||
r = GoogleAnalyticsNode().render(Context())
|
||||
self.assertTrue("_gaq.push(['_setDomainName', 'example.com']);" in r, r)
|
||||
self.assertTrue("_gaq.push(['_setAllowHash', false]);" in r, r)
|
||||
self.assertTrue("_gaq.push(['_setAllowLinker', true]);" in r, r)
|
||||
|
||||
def test_custom_vars(self):
|
||||
context = Context({
|
||||
'google_analytics_var1': ('test1', 'foo'),
|
||||
'google_analytics_var2': ('test2', 'bar', SCOPE_VISITOR),
|
||||
'google_analytics_var4': ('test4', 'baz', SCOPE_SESSION),
|
||||
'google_analytics_var5': ('test5', 'qux', SCOPE_PAGE),
|
||||
})
|
||||
r = GoogleAnalyticsNode().render(context)
|
||||
self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 3]);"
|
||||
in r, r)
|
||||
self.assertTrue("_gaq.push(['_setCustomVar', 2, 'test2', 'bar', 1]);"
|
||||
in r, r)
|
||||
self.assertTrue("_gaq.push(['_setCustomVar', 4, 'test4', 'baz', 2]);"
|
||||
in r, r)
|
||||
self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test5', 'qux', 3]);"
|
||||
in r, r)
|
||||
|
||||
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED=True)
|
||||
def test_track_page_load_time(self):
|
||||
r = GoogleAnalyticsNode().render(Context())
|
||||
self.assertTrue("_gaq.push(['_trackPageLoadTime']);" in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
r = GoogleAnalyticsNode().render(context)
|
||||
self.assertTrue(r.startswith(
|
||||
'<!-- Google Analytics disabled on internal IP address'), r)
|
||||
self.assertTrue(r.endswith('-->'), r)
|
||||
|
||||
|
||||
@without_apps('django.contrib.sites')
|
||||
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7',
|
||||
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
|
||||
GOOGLE_ANALYTICS_DOMAIN=SETTING_DELETED,
|
||||
ANALYTICAL_DOMAIN=SETTING_DELETED)
|
||||
class NoDomainTestCase(TestCase):
|
||||
def test_exception_without_domain(self):
|
||||
context = Context()
|
||||
self.assertRaises(AnalyticalException, GoogleAnalyticsNode().render,
|
||||
context)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"""
|
||||
Tests for the HubSpot template tags and filters.
|
||||
"""
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
|
||||
from analytical.templatetags.hubspot import HubSpotNode
|
||||
from analytical.tests.utils import TagTestCase, override_settings, SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@override_settings(HUBSPOT_PORTAL_ID='1234', HUBSPOT_DOMAIN='example.com')
|
||||
class HubSpotTagTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``hubspot`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('hubspot', 'hubspot')
|
||||
self.assertTrue('var hs_portalid = 1234;' in r, r)
|
||||
self.assertTrue('var hs_ppa = "example.com";' in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = HubSpotNode().render(Context())
|
||||
self.assertTrue('var hs_portalid = 1234;' in r, r)
|
||||
self.assertTrue('var hs_ppa = "example.com";' in r, r)
|
||||
|
||||
@override_settings(HUBSPOT_PORTAL_ID=SETTING_DELETED)
|
||||
def test_no_portal_id(self):
|
||||
self.assertRaises(AnalyticalException, HubSpotNode)
|
||||
|
||||
@override_settings(HUBSPOT_PORTAL_ID='wrong')
|
||||
def test_wrong_portal_id(self):
|
||||
self.assertRaises(AnalyticalException, HubSpotNode)
|
||||
|
||||
@override_settings(HUBSPOT_DOMAIN=SETTING_DELETED)
|
||||
def test_no_domain(self):
|
||||
self.assertRaises(AnalyticalException, HubSpotNode)
|
||||
|
||||
@override_settings(HUBSPOT_DOMAIN='wrong domain')
|
||||
def test_wrong_domain(self):
|
||||
self.assertRaises(AnalyticalException, HubSpotNode)
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
r = HubSpotNode().render(context)
|
||||
self.assertTrue(r.startswith(
|
||||
'<!-- HubSpot disabled on internal IP address'), r)
|
||||
self.assertTrue(r.endswith('-->'), r)
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
"""
|
||||
Tests for the KISSmetrics tags and filters.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
|
||||
from analytical.templatetags.kiss_metrics import KissMetricsNode
|
||||
from analytical.tests.utils import TagTestCase, override_settings, SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
|
||||
'01234567')
|
||||
class KissMetricsTagTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``kiss_metrics`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('kiss_metrics', 'kiss_metrics')
|
||||
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
|
||||
"6789abcdef01234567.1.js" in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = KissMetricsNode().render(Context())
|
||||
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
|
||||
"6789abcdef01234567.1.js" in r, r)
|
||||
|
||||
@override_settings(KISS_METRICS_API_KEY=SETTING_DELETED)
|
||||
def test_no_api_key(self):
|
||||
self.assertRaises(AnalyticalException, KissMetricsNode)
|
||||
|
||||
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
|
||||
'0123456')
|
||||
def test_api_key_too_short(self):
|
||||
self.assertRaises(AnalyticalException, KissMetricsNode)
|
||||
|
||||
@override_settings(KISS_METRICS_API_KEY='0123456789abcdef0123456789abcdef'
|
||||
'012345678')
|
||||
def test_api_key_too_long(self):
|
||||
self.assertRaises(AnalyticalException, KissMetricsNode)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify(self):
|
||||
r = KissMetricsNode().render(Context({'user': User(username='test')}))
|
||||
self.assertTrue("_kmq.push(['identify', 'test']);" in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_anonymous_user(self):
|
||||
r = KissMetricsNode().render(Context({'user': AnonymousUser()}))
|
||||
self.assertFalse("_kmq.push(['identify', " in r, r)
|
||||
|
||||
def test_event(self):
|
||||
r = KissMetricsNode().render(Context({'kiss_metrics_event':
|
||||
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
|
||||
self.assertTrue("_kmq.push(['record', 'test_event', "
|
||||
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
|
||||
|
||||
def test_property(self):
|
||||
r = KissMetricsNode().render(Context({'kiss_metrics_properties':
|
||||
{'prop1': 'val1', 'prop2': 'val2'}}))
|
||||
self.assertTrue("_kmq.push(['set', "
|
||||
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
r = KissMetricsNode().render(context)
|
||||
self.assertTrue(r.startswith(
|
||||
'<!-- KISSmetrics disabled on internal IP address'), r)
|
||||
self.assertTrue(r.endswith('-->'), r)
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
"""
|
||||
Tests for the Olark template tags and filters.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.template import Context
|
||||
|
||||
from analytical.templatetags.olark import OlarkNode
|
||||
from analytical.tests.utils import TagTestCase, override_settings, SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@override_settings(OLARK_SITE_ID='1234-567-89-0123')
|
||||
class OlarkTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``olark`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('olark', 'olark')
|
||||
self.assertTrue("olark.identify('1234-567-89-0123');" in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = OlarkNode().render(Context())
|
||||
self.assertTrue("olark.identify('1234-567-89-0123');" in r, r)
|
||||
|
||||
@override_settings(OLARK_SITE_ID=SETTING_DELETED)
|
||||
def test_no_site_id(self):
|
||||
self.assertRaises(AnalyticalException, OlarkNode)
|
||||
|
||||
@override_settings(OLARK_SITE_ID='1234-567-8901234')
|
||||
def test_wrong_site_id(self):
|
||||
self.assertRaises(AnalyticalException, OlarkNode)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify(self):
|
||||
r = OlarkNode().render(Context({'user':
|
||||
User(username='test', first_name='Test', last_name='User')}))
|
||||
self.assertTrue("olark('api.chat.updateVisitorNickname', "
|
||||
"{snippet: 'Test User (test)'});" in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_anonymous_user(self):
|
||||
r = OlarkNode().render(Context({'user': AnonymousUser()}))
|
||||
self.assertFalse("olark('api.chat.updateVisitorNickname', " in r, r)
|
||||
|
||||
def test_nickname(self):
|
||||
r = OlarkNode().render(Context({'olark_nickname': 'testnick'}))
|
||||
self.assertTrue("olark('api.chat.updateVisitorNickname', "
|
||||
"{snippet: 'testnick'});" in r, r)
|
||||
|
||||
def test_status_string(self):
|
||||
r = OlarkNode().render(Context({'olark_status': 'teststatus'}))
|
||||
self.assertTrue("olark('api.chat.updateVisitorStatus', "
|
||||
'{snippet: "teststatus"});' in r, r)
|
||||
|
||||
def test_status_string_list(self):
|
||||
r = OlarkNode().render(Context({'olark_status':
|
||||
['teststatus1', 'teststatus2']}))
|
||||
self.assertTrue("olark('api.chat.updateVisitorStatus', "
|
||||
'{snippet: ["teststatus1", "teststatus2"]});' in r, r)
|
||||
|
||||
def test_messages(self):
|
||||
messages = [
|
||||
"welcome_title",
|
||||
"chatting_title",
|
||||
"unavailable_title",
|
||||
"busy_title",
|
||||
"away_message",
|
||||
"loading_title",
|
||||
"welcome_message",
|
||||
"busy_message",
|
||||
"chat_input_text",
|
||||
"name_input_text",
|
||||
"email_input_text",
|
||||
"offline_note_message",
|
||||
"send_button_text",
|
||||
"offline_note_thankyou_text",
|
||||
"offline_note_error_text",
|
||||
"offline_note_sending_text",
|
||||
"operator_is_typing_text",
|
||||
"operator_has_stopped_typing_text",
|
||||
"introduction_error_text",
|
||||
"introduction_messages",
|
||||
"introduction_submit_button_text",
|
||||
]
|
||||
vars = dict(('olark_%s' % m, m) for m in messages)
|
||||
r = OlarkNode().render(Context(vars))
|
||||
for m in messages:
|
||||
self.assertTrue("olark.configure('locale.%s', \"%s\");" % (m, m)
|
||||
in r, r)
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
"""
|
||||
Tests for the Reinvigorate template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
|
||||
from analytical.templatetags.reinvigorate import ReinvigorateNode
|
||||
from analytical.tests.utils import TagTestCase, override_settings, \
|
||||
SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@override_settings(REINVIGORATE_TRACKING_ID='12345-abcdefghij')
|
||||
class ReinvigorateTagTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``reinvigorate`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('reinvigorate', 'reinvigorate')
|
||||
self.assertTrue('reinvigorate.track("12345-abcdefghij");' in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = ReinvigorateNode().render(Context({}))
|
||||
self.assertTrue('reinvigorate.track("12345-abcdefghij");' in r, r)
|
||||
|
||||
@override_settings(REINVIGORATE_TRACKING_ID=SETTING_DELETED)
|
||||
def test_no_tracking_id(self):
|
||||
self.assertRaises(AnalyticalException, ReinvigorateNode)
|
||||
|
||||
@override_settings(REINVIGORATE_TRACKING_ID='123abc')
|
||||
def test_wrong_tracking_id(self):
|
||||
self.assertRaises(AnalyticalException, ReinvigorateNode)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify(self):
|
||||
r = ReinvigorateNode().render(Context({'user':
|
||||
User(username='test', first_name='Test', last_name='User',
|
||||
email='test@example.com')}))
|
||||
self.assertTrue('var re_name_tag = "Test User";' in r, r)
|
||||
self.assertTrue('var re_context_tag = "test@example.com";' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_anonymous_user(self):
|
||||
r = ReinvigorateNode().render(Context({'user': AnonymousUser()}))
|
||||
self.assertFalse('var re_name_tag = ' in r, r)
|
||||
self.assertFalse('var re_context_tag = ' in r, r)
|
||||
|
||||
def test_tags(self):
|
||||
r = ReinvigorateNode().render(Context({'reinvigorate_var1': 'val1',
|
||||
'reinvigorate_var2': 2}))
|
||||
self.assertTrue(re.search('var re_var1_tag = "val1";', r), r)
|
||||
self.assertTrue(re.search('var re_var2_tag = 2;', r), r)
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
r = ReinvigorateNode().render(context)
|
||||
self.assertTrue(r.startswith(
|
||||
'<!-- Reinvigorate disabled on internal IP address'), r)
|
||||
self.assertTrue(r.endswith('-->'), r)
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
"""
|
||||
Tests for the SnapEngage template tags and filters.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.template import Context
|
||||
from django.utils import translation
|
||||
|
||||
from analytical.templatetags.snapengage import SnapEngageNode, \
|
||||
BUTTON_STYLE_LIVE, BUTTON_STYLE_DEFAULT, BUTTON_STYLE_NONE, \
|
||||
BUTTON_LOCATION_LEFT, BUTTON_LOCATION_RIGHT, BUTTON_LOCATION_TOP, \
|
||||
BUTTON_LOCATION_BOTTOM, FORM_POSITION_TOP_LEFT
|
||||
from analytical.tests.utils import TagTestCase, override_settings, \
|
||||
SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
WIDGET_ID = 'ec329c69-0bf0-4db8-9b77-3f8150fb977e'
|
||||
|
||||
|
||||
@override_settings(
|
||||
SNAPENGAGE_WIDGET_ID=WIDGET_ID,
|
||||
SNAPENGAGE_BUTTON=BUTTON_STYLE_DEFAULT,
|
||||
SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_LEFT,
|
||||
SNAPENGAGE_BUTTON_OFFSET="55%",
|
||||
)
|
||||
class SnapEngageTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``snapengage`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('snapengage', 'snapengage')
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"55%");' in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"55%");' in r, r)
|
||||
|
||||
@override_settings(SNAPENGAGE_WIDGET_ID=SETTING_DELETED)
|
||||
def test_no_site_id(self):
|
||||
self.assertRaises(AnalyticalException, SnapEngageNode)
|
||||
|
||||
@override_settings(SNAPENGAGE_WIDGET_ID='abc')
|
||||
def test_wrong_site_id(self):
|
||||
self.assertRaises(AnalyticalException, SnapEngageNode)
|
||||
|
||||
def test_no_button(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_button': BUTTON_STYLE_NONE}))
|
||||
self.assertTrue('SnapABug.init("ec329c69-0bf0-4db8-9b77-3f8150fb977e")'
|
||||
in r, r)
|
||||
with override_settings(SNAPENGAGE_BUTTON=BUTTON_STYLE_NONE):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.init("ec329c69-0bf0-4db8-9b77-3f8150fb977e")' in r, r)
|
||||
|
||||
def test_live_button(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_button': BUTTON_STYLE_LIVE}))
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"55%",true);' in r, r)
|
||||
with override_settings(SNAPENGAGE_BUTTON=BUTTON_STYLE_LIVE):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"55%",true);' in r, r)
|
||||
|
||||
def test_custom_button(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_button': "http://www.example.com/button.png"}))
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"55%");' in r, r)
|
||||
self.assertTrue(
|
||||
'SnapABug.setButton("http://www.example.com/button.png");' in r, r)
|
||||
with override_settings(
|
||||
SNAPENGAGE_BUTTON="http://www.example.com/button.png"):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"55%");' in r, r)
|
||||
self.assertTrue(
|
||||
'SnapABug.setButton("http://www.example.com/button.png");' in r,
|
||||
r)
|
||||
|
||||
def test_button_location_right(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_button_location': BUTTON_LOCATION_RIGHT}))
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","1",'
|
||||
'"55%");' in r, r)
|
||||
with override_settings(
|
||||
SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_RIGHT):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","1",'
|
||||
'"55%");' in r, r)
|
||||
|
||||
def test_button_location_top(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_button_location': BUTTON_LOCATION_TOP}))
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","2",'
|
||||
'"55%");' in r, r)
|
||||
with override_settings(SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_TOP):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","2",'
|
||||
'"55%");' in r, r)
|
||||
|
||||
def test_button_location_bottom(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_button_location': BUTTON_LOCATION_BOTTOM}))
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","3",'
|
||||
'"55%");' in r, r)
|
||||
with override_settings(
|
||||
SNAPENGAGE_BUTTON_LOCATION=BUTTON_LOCATION_BOTTOM):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","3",'
|
||||
'"55%");' in r, r)
|
||||
|
||||
def test_button_offset(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_button_location_offset': "30%"}))
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"30%");' in r, r)
|
||||
with override_settings(SNAPENGAGE_BUTTON_LOCATION_OFFSET="30%"):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue(
|
||||
'SnapABug.addButton("ec329c69-0bf0-4db8-9b77-3f8150fb977e","0",'
|
||||
'"30%");' in r, r)
|
||||
|
||||
def test_button_effect(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_button_effect': "-4px"}))
|
||||
self.assertTrue('SnapABug.setButtonEffect("-4px");' in r, r)
|
||||
with override_settings(SNAPENGAGE_BUTTON_EFFECT="-4px"):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.setButtonEffect("-4px");' in r, r)
|
||||
|
||||
def test_form_position(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_form_position': FORM_POSITION_TOP_LEFT}))
|
||||
self.assertTrue('SnapABug.setChatFormPosition("tl");' in r, r)
|
||||
with override_settings(SNAPENGAGE_FORM_POSITION=FORM_POSITION_TOP_LEFT):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.setChatFormPosition("tl");' in r, r)
|
||||
|
||||
def test_form_top_position(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_form_top_position': 40}))
|
||||
self.assertTrue('SnapABug.setFormTopPosition(40);' in r, r)
|
||||
with override_settings(SNAPENGAGE_FORM_TOP_POSITION=40):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.setFormTopPosition(40);' in r, r)
|
||||
|
||||
def test_domain(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_domain': "example.com"}))
|
||||
self.assertTrue('SnapABug.setDomain("example.com");' in r, r)
|
||||
with override_settings(SNAPENGAGE_DOMAIN="example.com"):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.setDomain("example.com");' in r, r)
|
||||
|
||||
def test_secure_connection(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_secure_connection': True}))
|
||||
self.assertTrue('SnapABug.setSecureConnexion();' in r, r)
|
||||
with override_settings(SNAPENGAGE_SECURE_CONNECTION=True):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.setSecureConnexion();' in r, r)
|
||||
|
||||
def test_show_offline(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_show_offline': False}))
|
||||
self.assertTrue('SnapABug.allowOffline(false);' in r, r)
|
||||
with override_settings(SNAPENGAGE_SHOW_OFFLINE=False):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.allowOffline(false);' in r, r)
|
||||
|
||||
def test_proactive_chat(self):
|
||||
r = SnapEngageNode().render(Context({
|
||||
'snapengage_proactive_chat': False}))
|
||||
self.assertTrue('SnapABug.allowProactiveChat(false);' in r, r)
|
||||
|
||||
def test_screenshot(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_screenshots': False}))
|
||||
self.assertTrue('SnapABug.allowScreenshot(false);' in r, r)
|
||||
with override_settings(SNAPENGAGE_SCREENSHOTS=False):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.allowScreenshot(false);' in r, r)
|
||||
|
||||
def test_offline_screenshots(self):
|
||||
r = SnapEngageNode().render(Context(
|
||||
{'snapengage_offline_screenshots': False}))
|
||||
self.assertTrue('SnapABug.showScreenshotOption(false);' in r, r)
|
||||
with override_settings(SNAPENGAGE_OFFLINE_SCREENSHOTS=False):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.showScreenshotOption(false);' in r, r)
|
||||
|
||||
def test_sounds(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_sounds': False}))
|
||||
self.assertTrue('SnapABug.allowChatSound(false);' in r, r)
|
||||
with override_settings(SNAPENGAGE_SOUNDS=False):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.allowChatSound(false);' in r, r)
|
||||
|
||||
@override_settings(SNAPENGAGE_READONLY_EMAIL=False)
|
||||
def test_email(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_email':
|
||||
'test@example.com'}))
|
||||
self.assertTrue('SnapABug.setUserEmail("test@example.com");' in r, r)
|
||||
|
||||
def test_email_readonly(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_email':
|
||||
'test@example.com', 'snapengage_readonly_email': True}))
|
||||
self.assertTrue('SnapABug.setUserEmail("test@example.com",true);' in r,
|
||||
r)
|
||||
with override_settings(SNAPENGAGE_READONLY_EMAIL=True):
|
||||
r = SnapEngageNode().render(Context({'snapengage_email':
|
||||
'test@example.com'}))
|
||||
self.assertTrue('SnapABug.setUserEmail("test@example.com",true);'
|
||||
in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify(self):
|
||||
r = SnapEngageNode().render(Context({'user':
|
||||
User(username='test', email='test@example.com')}))
|
||||
self.assertTrue('SnapABug.setUserEmail("test@example.com");' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_anonymous_user(self):
|
||||
r = SnapEngageNode().render(Context({'user': AnonymousUser()}))
|
||||
self.assertFalse('SnapABug.setUserEmail(' in r, r)
|
||||
|
||||
def test_language(self):
|
||||
r = SnapEngageNode().render(Context({'snapengage_locale': 'fr'}))
|
||||
self.assertTrue('SnapABug.setLocale("fr");' in r, r)
|
||||
with override_settings(SNAPENGAGE_LOCALE='fr'):
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.setLocale("fr");' in r, r)
|
||||
|
||||
def test_automatic_language(self):
|
||||
real_get_language = translation.get_language
|
||||
try:
|
||||
translation.get_language = lambda: 'fr-ca'
|
||||
r = SnapEngageNode().render(Context())
|
||||
self.assertTrue('SnapABug.setLocale("fr_CA");' in r, r)
|
||||
finally:
|
||||
translation.get_language = real_get_language
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
"""
|
||||
Tests for the Spring Metrics template tags and filters.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
|
||||
from analytical.templatetags.spring_metrics import SpringMetricsNode
|
||||
from analytical.tests.utils import TagTestCase, override_settings, \
|
||||
SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@override_settings(SPRING_METRICS_TRACKING_ID='12345678')
|
||||
class SpringMetricsTagTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``spring_metrics`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('spring_metrics', 'spring_metrics')
|
||||
self.assertTrue("_springMetq.push(['id', '12345678']);" in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = SpringMetricsNode().render(Context({}))
|
||||
self.assertTrue("_springMetq.push(['id', '12345678']);" in r, r)
|
||||
|
||||
@override_settings(SPRING_METRICS_TRACKING_ID=SETTING_DELETED)
|
||||
def test_no_site_id(self):
|
||||
self.assertRaises(AnalyticalException, SpringMetricsNode)
|
||||
|
||||
@override_settings(SPRING_METRICS_TRACKING_ID='123xyz')
|
||||
def test_wrong_site_id(self):
|
||||
self.assertRaises(AnalyticalException, SpringMetricsNode)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify(self):
|
||||
r = SpringMetricsNode().render(Context({'user':
|
||||
User(email='test@test.com')}))
|
||||
self.assertTrue("_springMetq.push(['setdata', "
|
||||
"{'email': 'test@test.com'}]);" in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_anonymous_user(self):
|
||||
r = SpringMetricsNode().render(Context({'user': AnonymousUser()}))
|
||||
self.assertFalse("_springMetq.push(['setdata', {'email':" in r, r)
|
||||
|
||||
def test_custom(self):
|
||||
r = SpringMetricsNode().render(Context({'spring_metrics_var1': 'val1',
|
||||
'spring_metrics_var2': 'val2'}))
|
||||
self.assertTrue("_springMetq.push(['setdata', {'var1': 'val1'}]);" in r,
|
||||
r)
|
||||
self.assertTrue("_springMetq.push(['setdata', {'var2': 'val2'}]);" in r,
|
||||
r)
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
r = SpringMetricsNode().render(context)
|
||||
self.assertTrue(r.startswith(
|
||||
'<!-- Spring Metrics disabled on internal IP address'), r)
|
||||
self.assertTrue(r.endswith('-->'), r)
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
"""
|
||||
Tests for the Woopra template tags and filters.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
|
||||
from analytical.templatetags.woopra import WoopraNode
|
||||
from analytical.tests.utils import TagTestCase, override_settings, \
|
||||
SETTING_DELETED
|
||||
from analytical.utils import AnalyticalException
|
||||
|
||||
|
||||
@override_settings(WOOPRA_DOMAIN='example.com')
|
||||
class WoopraTagTestCase(TagTestCase):
|
||||
"""
|
||||
Tests for the ``woopra`` template tag.
|
||||
"""
|
||||
|
||||
def test_tag(self):
|
||||
r = self.render_tag('woopra', 'woopra')
|
||||
self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
|
||||
|
||||
def test_node(self):
|
||||
r = WoopraNode().render(Context({}))
|
||||
self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
|
||||
|
||||
@override_settings(WOOPRA_DOMAIN=SETTING_DELETED)
|
||||
def test_no_domain(self):
|
||||
self.assertRaises(AnalyticalException, WoopraNode)
|
||||
|
||||
@override_settings(WOOPRA_DOMAIN='this is not a domain')
|
||||
def test_wrong_domain(self):
|
||||
self.assertRaises(AnalyticalException, WoopraNode)
|
||||
|
||||
@override_settings(WOOPRA_IDLE_TIMEOUT=1234)
|
||||
def test_idle_timeout(self):
|
||||
r = WoopraNode().render(Context({}))
|
||||
self.assertTrue('var woo_settings = {"domain": "example.com", '
|
||||
'"idle_timeout": "1234"};' in r, r)
|
||||
|
||||
def test_custom(self):
|
||||
r = WoopraNode().render(Context({'woopra_var1': 'val1',
|
||||
'woopra_var2': 'val2'}))
|
||||
self.assertTrue('var woo_visitor = {"var1": "val1", "var2": "val2"};'
|
||||
in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_name_and_email(self):
|
||||
r = WoopraNode().render(Context({'user': User(username='test',
|
||||
first_name='Firstname', last_name='Lastname',
|
||||
email="test@example.com")}))
|
||||
self.assertTrue('var woo_visitor = {"name": "Firstname Lastname", '
|
||||
'"email": "test@example.com"};' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_username_no_email(self):
|
||||
r = WoopraNode().render(Context({'user': User(username='test')}))
|
||||
self.assertTrue('var woo_visitor = {"name": "test"};' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_no_identify_when_explicit_name(self):
|
||||
r = WoopraNode().render(Context({'woopra_name': 'explicit',
|
||||
'user': User(username='implicit')}))
|
||||
self.assertTrue('var woo_visitor = {"name": "explicit"};' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_no_identify_when_explicit_email(self):
|
||||
r = WoopraNode().render(Context({'woopra_email': 'explicit',
|
||||
'user': User(username='implicit')}))
|
||||
self.assertTrue('var woo_visitor = {"email": "explicit"};' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
|
||||
def test_identify_anonymous_user(self):
|
||||
r = WoopraNode().render(Context({'user': AnonymousUser()}))
|
||||
self.assertTrue('var woo_visitor = {};' in r, r)
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
r = WoopraNode().render(context)
|
||||
self.assertTrue(r.startswith(
|
||||
'<!-- Woopra disabled on internal IP address'), r)
|
||||
self.assertTrue(r.endswith('-->'), r)
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
"""
|
||||
Tests for the analytical.utils module.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context
|
||||
|
||||
from analytical.utils import (
|
||||
get_domain, is_internal_ip, get_required_setting, AnalyticalException)
|
||||
from analytical.tests.utils import (
|
||||
TestCase, override_settings, with_apps, SETTING_DELETED)
|
||||
|
||||
|
||||
class SettingDeletedTestCase(TestCase):
|
||||
@override_settings(USER_ID=SETTING_DELETED)
|
||||
def test_deleted_setting_raises_exception(self):
|
||||
self.assertRaises(AttributeError, getattr, settings, "USER_ID")
|
||||
|
||||
@override_settings(USER_ID=1)
|
||||
def test_only_disable_within_context_manager(self):
|
||||
"""
|
||||
Make sure deleted settings returns once the block exits.
|
||||
"""
|
||||
self.assertEqual(settings.USER_ID, 1)
|
||||
|
||||
with override_settings(USER_ID=SETTING_DELETED):
|
||||
self.assertRaises(AttributeError, getattr, settings, "USER_ID")
|
||||
|
||||
self.assertEqual(settings.USER_ID, 1)
|
||||
|
||||
@override_settings(USER_ID=SETTING_DELETED)
|
||||
def test_get_required_setting(self):
|
||||
"""
|
||||
Make sure using get_required_setting fails in the right place.
|
||||
"""
|
||||
# only available in python >= 2.7
|
||||
if hasattr(self, 'assertRaisesRegexp'):
|
||||
with self.assertRaisesRegexp(AnalyticalException, "^USER_ID setting: not found$"):
|
||||
user_id = get_required_setting("USER_ID", "\d+", "invalid USER_ID")
|
||||
else:
|
||||
self.assertRaises(AnalyticalException,
|
||||
get_required_setting, "USER_ID", "\d+", "invalid USER_ID")
|
||||
|
||||
@override_settings(ANALYTICAL_DOMAIN="example.org")
|
||||
class GetDomainTestCase(TestCase):
|
||||
def test_get_service_domain_from_context(self):
|
||||
context = Context({'test_domain': 'example.com'})
|
||||
self.assertEqual(get_domain(context, 'test'), 'example.com')
|
||||
|
||||
def test_get_analytical_domain_from_context(self):
|
||||
context = Context({'analytical_domain': 'example.com'})
|
||||
self.assertEqual(get_domain(context, 'test'), 'example.com')
|
||||
|
||||
@override_settings(TEST_DOMAIN="example.net")
|
||||
def test_get_service_domain_from_settings(self):
|
||||
context = Context()
|
||||
self.assertEqual(get_domain(context, 'test'), 'example.net')
|
||||
|
||||
def test_get_analytical_domain_from_settings(self):
|
||||
context = Context()
|
||||
self.assertEqual(get_domain(context, 'test'), 'example.org')
|
||||
|
||||
|
||||
@with_apps('django.contrib.sites')
|
||||
@override_settings(TEST_DOMAIN=SETTING_DELETED,
|
||||
ANALYTICAL_DOMAIN=SETTING_DELETED)
|
||||
class GetDomainTestCaseWithSites(TestCase):
|
||||
def test_get_domain_from_site(self):
|
||||
site = Site.objects.create(domain="example.com", name="test")
|
||||
with override_settings(SITE_ID=site.id):
|
||||
context = Context()
|
||||
self.assertEqual(get_domain(context, 'test'), 'example.com')
|
||||
|
||||
|
||||
class InternalIpTestCase(TestCase):
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_no_internal_ip(self):
|
||||
context = Context()
|
||||
self.assertFalse(is_internal_ip(context))
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
self.assertTrue(is_internal_ip(context))
|
||||
|
||||
@override_settings(TEST_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_prefix_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
self.assertTrue(is_internal_ip(context, 'TEST'))
|
||||
|
||||
@override_settings(INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip_fallback(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
self.assertTrue(is_internal_ip(context))
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_internal_ip_forwarded_for(self):
|
||||
req = HttpRequest()
|
||||
req.META['HTTP_X_FORWARDED_FOR'] = '1.1.1.1'
|
||||
context = Context({'request': req})
|
||||
self.assertTrue(is_internal_ip(context))
|
||||
|
||||
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
|
||||
def test_render_different_internal_ip(self):
|
||||
req = HttpRequest()
|
||||
req.META['REMOTE_ADDR'] = '2.2.2.2'
|
||||
context = Context({'request': req})
|
||||
self.assertFalse(is_internal_ip(context))
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
"""
|
||||
Testing utilities.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import copy
|
||||
|
||||
from django.conf import settings, UserSettingsHolder
|
||||
from django.core.management import call_command
|
||||
from django.db.models import loading
|
||||
from django.template import Template, Context, RequestContext
|
||||
from django.test.testcases import TestCase
|
||||
from django.utils.functional import wraps
|
||||
|
||||
|
||||
SETTING_DELETED = object()
|
||||
|
||||
|
||||
# Backported adapted from Django trunk (r16377)
|
||||
class override_settings(object):
|
||||
"""
|
||||
Temporarily override Django settings.
|
||||
|
||||
Can be used as either a decorator on test classes/functions or as
|
||||
a context manager inside test functions.
|
||||
|
||||
In either case it temporarily overrides django.conf.settings so
|
||||
that you can test how code acts when certain settings are set to
|
||||
certain values or deleted altogether with SETTING_DELETED.
|
||||
|
||||
>>> @override_settings(FOOBAR=42)
|
||||
>>> class TestBaz(TestCase):
|
||||
>>> # settings.FOOBAR == 42 for all tests
|
||||
>>>
|
||||
>>> @override_settings(FOOBAR=43)
|
||||
>>> def test_widget(self):
|
||||
>>> # settings.FOOBAR == 43 for just this test
|
||||
>>>
|
||||
>>> with override_settings(FOOBAR=44):
|
||||
>>> # settings.FOOBAR == 44 just inside this block
|
||||
>>> pass
|
||||
>>>
|
||||
>>> # settings.FOOBAR == 43 inside the test
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
self.options = kwargs
|
||||
self.wrapped = settings._wrapped
|
||||
|
||||
def __enter__(self):
|
||||
self.enable()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.disable()
|
||||
|
||||
def __call__(self, test_func):
|
||||
from django.test import TransactionTestCase
|
||||
if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase):
|
||||
# When decorating a class, we need to construct a new class
|
||||
# with the same name so that the test discovery tools can
|
||||
# get a useful name.
|
||||
def _pre_setup(innerself):
|
||||
self.enable()
|
||||
test_func._pre_setup(innerself)
|
||||
def _post_teardown(innerself):
|
||||
test_func._post_teardown(innerself)
|
||||
self.disable()
|
||||
inner = type(
|
||||
test_func.__name__,
|
||||
(test_func,),
|
||||
{
|
||||
'_pre_setup': _pre_setup,
|
||||
'_post_teardown': _post_teardown,
|
||||
'__module__': test_func.__module__,
|
||||
})
|
||||
else:
|
||||
@wraps(test_func)
|
||||
def inner(*args, **kwargs):
|
||||
with self:
|
||||
return test_func(*args, **kwargs)
|
||||
return inner
|
||||
|
||||
def enable(self):
|
||||
class OverrideSettingsHolder(UserSettingsHolder):
|
||||
def __getattr__(self, name):
|
||||
if name == "default_settings":
|
||||
return self.__dict__["default_settings"]
|
||||
return getattr(self.default_settings, name)
|
||||
|
||||
override = OverrideSettingsHolder(copy.copy(settings._wrapped))
|
||||
for key, new_value in self.options.items():
|
||||
if new_value is SETTING_DELETED:
|
||||
try:
|
||||
delattr(override.default_settings, key)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
setattr(override, key, new_value)
|
||||
settings._wrapped = override
|
||||
|
||||
def disable(self):
|
||||
settings._wrapped = self.wrapped
|
||||
|
||||
|
||||
def run_tests():
|
||||
"""
|
||||
Use the Django test runner to run the tests.
|
||||
|
||||
Sets the return code to the number of failed tests.
|
||||
"""
|
||||
import sys
|
||||
from django.test.simple import DjangoTestSuiteRunner
|
||||
runner = DjangoTestSuiteRunner()
|
||||
sys.exit(runner.run_tests(["analytical"]))
|
||||
|
||||
|
||||
def with_apps(*apps):
|
||||
"""
|
||||
Class decorator that makes sure the passed apps are present in
|
||||
INSTALLED_APPS.
|
||||
"""
|
||||
apps_set = set(settings.INSTALLED_APPS)
|
||||
apps_set.update(apps)
|
||||
return override_settings(INSTALLED_APPS=list(apps_set))
|
||||
|
||||
|
||||
def without_apps(*apps):
|
||||
"""
|
||||
Class decorator that makes sure the passed apps are not present in
|
||||
INSTALLED_APPS.
|
||||
"""
|
||||
apps_list = [a for a in settings.INSTALLED_APPS if a not in apps]
|
||||
return override_settings(INSTALLED_APPS=apps_list)
|
||||
|
||||
|
||||
class TagTestCase(TestCase):
|
||||
"""
|
||||
Tests for a template tag.
|
||||
|
||||
Adds support methods for testing template tags.
|
||||
"""
|
||||
|
||||
def render_tag(self, library, tag, vars=None, request=None):
|
||||
if vars is None:
|
||||
vars = {}
|
||||
t = Template("{%% load %s %%}{%% %s %%}" % (library, tag))
|
||||
if request is not None:
|
||||
context = RequestContext(request, vars)
|
||||
else:
|
||||
context = Context(vars)
|
||||
return t.render(context)
|
||||
|
|
@ -3,12 +3,9 @@ Utility function for django-analytical.
|
|||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
||||
HTML_COMMENT = "<!-- %(service)s disabled on internal IP " \
|
||||
"address\n%(html)s\n-->"
|
||||
HTML_COMMENT = '<!-- %(service)s disabled on internal IP address\n%(html)s\n-->'
|
||||
|
||||
|
||||
def get_required_setting(setting, value_re, invalid_msg):
|
||||
|
|
@ -21,11 +18,14 @@ def get_required_setting(setting, value_re, invalid_msg):
|
|||
try:
|
||||
value = getattr(settings, setting)
|
||||
except AttributeError:
|
||||
raise AnalyticalException("%s setting: not found" % setting)
|
||||
raise AnalyticalException('%s setting: not found' % setting)
|
||||
if not value:
|
||||
raise AnalyticalException('%s setting is not set' % setting)
|
||||
value = str(value)
|
||||
if not value_re.search(value):
|
||||
raise AnalyticalException("%s setting: %s: '%s'"
|
||||
% (setting, invalid_msg, value))
|
||||
raise AnalyticalException(
|
||||
"%s setting: %s: '%s'" % (setting, invalid_msg, value)
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -48,6 +48,19 @@ def get_user_from_context(context):
|
|||
return None
|
||||
|
||||
|
||||
def get_user_is_authenticated(user):
|
||||
"""Check if the user is authenticated.
|
||||
|
||||
This is a compatibility function needed to support both Django 1.x and 2.x;
|
||||
Django 2.x turns the function into a proper boolean so function calls will
|
||||
fail.
|
||||
"""
|
||||
if callable(user.is_authenticated):
|
||||
return user.is_authenticated()
|
||||
else:
|
||||
return user.is_authenticated
|
||||
|
||||
|
||||
def get_identity(context, prefix=None, identity_func=None, user=None):
|
||||
"""
|
||||
Get the identity of a logged in user from a template context.
|
||||
|
|
@ -70,11 +83,11 @@ def get_identity(context, prefix=None, identity_func=None, user=None):
|
|||
try:
|
||||
if user is None:
|
||||
user = get_user_from_context(context)
|
||||
if user.is_authenticated():
|
||||
if get_user_is_authenticated(user):
|
||||
if identity_func is not None:
|
||||
return identity_func(user)
|
||||
else:
|
||||
return user.username
|
||||
return user.get_username()
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
return None
|
||||
|
|
@ -98,6 +111,8 @@ def get_domain(context, prefix):
|
|||
domain = getattr(settings, 'ANALYTICAL_DOMAIN', None)
|
||||
if domain is None:
|
||||
if 'django.contrib.sites' in settings.INSTALLED_APPS:
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
try:
|
||||
domain = Site.objects.get_current().domain
|
||||
except (ImproperlyConfigured, Site.DoesNotExist):
|
||||
|
|
@ -121,15 +136,15 @@ def is_internal_ip(context, prefix=None):
|
|||
if not remote_ip:
|
||||
return False
|
||||
|
||||
internal_ips = ''
|
||||
internal_ips = None
|
||||
if prefix is not None:
|
||||
internal_ips = getattr(settings, '%s_INTERNAL_IPS' % prefix, '')
|
||||
if not internal_ips:
|
||||
internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', '')
|
||||
if not internal_ips:
|
||||
internal_ips = getattr(settings, 'INTERNAL_IPS', '')
|
||||
internal_ips = getattr(settings, '%s_INTERNAL_IPS' % prefix, None)
|
||||
if internal_ips is None:
|
||||
internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', None)
|
||||
if internal_ips is None:
|
||||
internal_ips = getattr(settings, 'INTERNAL_IPS', None)
|
||||
|
||||
return remote_ip in internal_ips
|
||||
return remote_ip in (internal_ips or [])
|
||||
except (KeyError, AttributeError):
|
||||
return False
|
||||
|
||||
|
|
@ -148,4 +163,5 @@ class AnalyticalException(Exception):
|
|||
Raised when an exception occurs in any django-analytical code that should
|
||||
be silenced in templates.
|
||||
"""
|
||||
|
||||
silent_variable_failure = True
|
||||
|
|
|
|||
|
|
@ -1,26 +1,21 @@
|
|||
def setup(app):
|
||||
app.add_crossref_type(
|
||||
directivename = "setting",
|
||||
rolename = "setting",
|
||||
indextemplate = "pair: %s; setting",
|
||||
directivename='setting',
|
||||
rolename='setting',
|
||||
indextemplate='pair: %s; setting',
|
||||
)
|
||||
app.add_crossref_type(
|
||||
directivename = "templatetag",
|
||||
rolename = "ttag",
|
||||
indextemplate = "pair: %s; template tag"
|
||||
directivename='templatetag',
|
||||
rolename='ttag',
|
||||
indextemplate='pair: %s; template tag',
|
||||
)
|
||||
app.add_crossref_type(
|
||||
directivename = "templatefilter",
|
||||
rolename = "tfilter",
|
||||
indextemplate = "pair: %s; template filter"
|
||||
directivename='templatefilter',
|
||||
rolename='tfilter',
|
||||
indextemplate='pair: %s; template filter',
|
||||
)
|
||||
app.add_crossref_type(
|
||||
directivename = "fieldlookup",
|
||||
rolename = "lookup",
|
||||
indextemplate = "pair: %s; field lookup type",
|
||||
)
|
||||
app.add_description_unit(
|
||||
directivename = "decorator",
|
||||
rolename = "dec",
|
||||
indextemplate = "pair: %s; function decorator",
|
||||
directivename='fieldlookup',
|
||||
rolename='lookup',
|
||||
indextemplate='pair: %s; field lookup type',
|
||||
)
|
||||
|
|
|
|||
34
docs/conf.py
34
docs/conf.py
|
|
@ -1,19 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing
|
||||
# directory.
|
||||
|
||||
import sys, os
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.abspath('.'), '_ext'))
|
||||
sys.path.append(os.path.dirname(os.path.abspath('.')))
|
||||
|
||||
import analytical
|
||||
import analytical # noqa
|
||||
|
||||
# -- General configuration --------------------------------------------------
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
project = u'django-analytical'
|
||||
copyright = u'2011, Joost Cassee <joost@cassee.net>'
|
||||
project = 'django-analytical'
|
||||
copyright = '2011, Joost Cassee <joost@cassee.net>'
|
||||
|
||||
release = analytical.__version__
|
||||
# The short X.Y version.
|
||||
|
|
@ -21,28 +21,32 @@ version = release.rsplit('.', 1)[0]
|
|||
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'local']
|
||||
templates_path = ['_templates']
|
||||
source_suffix = '.rst'
|
||||
source_suffix = {'.rst': 'restructuredtext'}
|
||||
master_doc = 'index'
|
||||
|
||||
add_function_parentheses = True
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
intersphinx_mapping = {
|
||||
'http://docs.python.org/2.6': None,
|
||||
'http://docs.djangoproject.com/en/1.3': 'http://docs.djangoproject.com/en/1.3/_objects/',
|
||||
'python': ('https://docs.python.org/3.13', None),
|
||||
'django': ('https://docs.djangoproject.com/en/stable', None),
|
||||
}
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
# -- Options for HTML output ------------------------------------------------
|
||||
|
||||
html_theme = 'default'
|
||||
html_static_path = ['_static']
|
||||
htmlhelp_basename = 'analyticaldoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
# -- Options for LaTeX output -----------------------------------------------
|
||||
|
||||
latex_documents = [
|
||||
('index', 'django-analytical.tex', u'Documentation for django-analytical',
|
||||
u'Joost Cassee', 'manual'),
|
||||
(
|
||||
'index',
|
||||
'django-analytical.tex',
|
||||
'Documentation for django-analytical',
|
||||
'Joost Cassee',
|
||||
'manual',
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ initialization code if the client IP address is detected as one from the
|
|||
:data:`ANALYTICAL_INTERNAL_IPS` setting. The default value for this
|
||||
setting is :data:`INTERNAL_IPS`.
|
||||
|
||||
Example::
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ANALYTICAL_INTERNAL_IPS = ['192.168.1.45', '192.168.1.57']
|
||||
|
||||
|
|
@ -45,7 +47,9 @@ logged in through the standard Django authentication system and the
|
|||
current user is accessible in the template context, the username can be
|
||||
passed to the analytics services that support identifying users. This
|
||||
feature is configured by the :data:`ANALYTICAL_AUTO_IDENTIFY` setting
|
||||
and is enabled by default. To disable::
|
||||
and is enabled by default. To disable:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ANALYTICAL_AUTO_IDENTIFY = False
|
||||
|
||||
|
|
@ -64,3 +68,52 @@ and is enabled by default. To disable::
|
|||
Alternatively, add one of the variables to the context yourself
|
||||
when you render the template.
|
||||
|
||||
Changing the identity
|
||||
*********************
|
||||
|
||||
If you want to override the identity of the logged-in user that the various
|
||||
providers send you can do it by setting the ``analytical_identity`` context
|
||||
variable in your view code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
context = RequestContext({'analytical_identity': user.uuid})
|
||||
return some_template.render(context)
|
||||
|
||||
or in the template:
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{% with analytical_identity=request.user.uuid|default:None %}
|
||||
{% analytical_head_top %}
|
||||
{% endwith %}
|
||||
|
||||
or by implementing a context processor, e.g.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# FILE: myproject/context_processors.py
|
||||
from django.conf import settings
|
||||
|
||||
def get_identity(request):
|
||||
return {
|
||||
'analytical_identity': 'some-value-here',
|
||||
}
|
||||
|
||||
# FILE: myproject/settings.py
|
||||
TEMPLATES = [
|
||||
{
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'myproject.context_processors.get_identity',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
That allows you as a developer to leave your view code untouched and
|
||||
make sure that the variable is injected for all templates.
|
||||
|
||||
If you want to change the identity only for specific provider use the
|
||||
``*_identity`` context variable, where the ``*`` prefix is the module name
|
||||
of the specific provider.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ version numbers. Patch-level increments indicate bug fixes, minor
|
|||
version increments indicate new functionality and major version
|
||||
increments indicate backwards incompatible changes.
|
||||
|
||||
Version 1.0.0 is the last to support Django < 1.7. Users of older Django
|
||||
versions should stick to 1.0.0, and are encouraged to upgrade their setups.
|
||||
Starting with 2.0.0, dropping support for obsolete Django versions is not
|
||||
considered to be a backward-incompatible change.
|
||||
|
||||
.. _`Semantic Versioning`: http://semver.org/
|
||||
|
||||
.. include:: ../CHANGELOG.rst
|
||||
|
|
@ -18,8 +23,21 @@ increments indicate backwards incompatible changes.
|
|||
Credits
|
||||
=======
|
||||
|
||||
.. include:: ../AUTHORS.rst
|
||||
The django-analytical package was originally written by `Joost Cassee`_
|
||||
and is now maintained by `Peter Bittner`_ and the `Jazzband community`_.
|
||||
All known contributors are listed as ``authors`` in the `project metadata`_.
|
||||
|
||||
Included JavaScript code snippets for integration of the analytics services
|
||||
were written by the respective service providers.
|
||||
|
||||
The application was inspired by and uses ideas from Analytical_, Joshua
|
||||
Krall's all-purpose analytics front-end for Rails.
|
||||
|
||||
.. _`Joost Cassee`: https://github.com/jcassee
|
||||
.. _`Peter Bittner`: https://github.com/bittner
|
||||
.. _`Jazzband community`: https://jazzband.co/
|
||||
.. _`project metadata`: https://github.com/jazzband/django-analytical/blob/main/pyproject.toml#L15-L60
|
||||
.. _`Analytical`: https://github.com/jkrall/analytical
|
||||
|
||||
.. _helping-out:
|
||||
|
||||
|
|
@ -27,4 +45,6 @@ Helping out
|
|||
===========
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-after: GitHub`_.
|
||||
:start-after: .. start contribute include
|
||||
:end-before: .. end contribute include
|
||||
|
||||
|
|
|
|||
|
|
@ -5,20 +5,20 @@ django-analytical
|
|||
The django-analytical application integrates analytics services into a
|
||||
Django_ project.
|
||||
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
.. _Django: https://www.djangoproject.com/
|
||||
|
||||
:Package: http://pypi.python.org/pypi/django-analytical/
|
||||
:Source: http://github.com/jcassee/django-analytical
|
||||
:Package: https://pypi.org/project/django-analytical/
|
||||
:Source: https://github.com/jazzband/django-analytical
|
||||
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-after: Django_ project.
|
||||
:end-before: Currently supported services:
|
||||
:start-after: .. start docs include
|
||||
:end-before: .. end docs include
|
||||
|
||||
To get a feel of how django-analytics works, check out the
|
||||
To get a feel of how django-analytical works, check out the
|
||||
:doc:`tutorial`.
|
||||
|
||||
|
||||
|
|
|
|||
142
docs/install.rst
142
docs/install.rst
|
|
@ -20,23 +20,29 @@ Installing the Python package
|
|||
|
||||
To install django-analytical the ``analytical`` package must be added to
|
||||
the Python path. You can install it directly from PyPI using
|
||||
``easy_install``::
|
||||
``easy_install``:
|
||||
|
||||
$ easy_install django-analytical
|
||||
.. code-block:: bash
|
||||
|
||||
$ easy_install django-analytical
|
||||
|
||||
You can also install directly from source. Download either the latest
|
||||
stable version from PyPI_ or any release from GitHub_, or use Git to
|
||||
get the development code::
|
||||
get the development code:
|
||||
|
||||
$ git clone https://github.com/jcassee/django-analytical.git
|
||||
.. code-block:: bash
|
||||
|
||||
.. _PyPI: http://pypi.python.org/pypi/django-analytical/
|
||||
.. _GitHub: http://github.com/jcassee/django-analytical
|
||||
$ git clone https://github.com/jazzband/django-analytical.git
|
||||
|
||||
Then install the package by running the setup script::
|
||||
.. _PyPI: https://pypi.org/project/django-analytical/
|
||||
.. _GitHub: http://github.com/jazzband/django-analytical
|
||||
|
||||
$ cd django-analytical
|
||||
$ python setup.py install
|
||||
Then install the package by running the setup script:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cd django-analytical
|
||||
$ python setup.py install
|
||||
|
||||
|
||||
.. _installing-the-application:
|
||||
|
|
@ -46,13 +52,15 @@ Installing the Django application
|
|||
|
||||
After you installed django-analytical, add the ``analytical`` Django
|
||||
application to the list of installed applications in the ``settings.py``
|
||||
file of your project::
|
||||
file of your project:
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'analytical',
|
||||
...
|
||||
]
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'analytical',
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
.. _adding-the-template-tags:
|
||||
|
|
@ -60,30 +68,32 @@ file of your project::
|
|||
Adding the template tags to the base template
|
||||
=============================================
|
||||
|
||||
Because every analytics service uses own specific Javascript code that
|
||||
Because every analytics service uses own specific JavaScript code that
|
||||
should be added to the top or bottom of either the head or body of the
|
||||
HTML page, django-analytical provides four general-purpose template tags
|
||||
that will render the code needed for the services you are using. Your
|
||||
base template should look like this::
|
||||
base template should look like this:
|
||||
|
||||
{% load analytical %}
|
||||
<!DOCTYPE ... >
|
||||
<html>
|
||||
<head>
|
||||
{% analytical_head_top %}
|
||||
.. code-block:: django
|
||||
|
||||
...
|
||||
{% load analytical %}
|
||||
<!DOCTYPE ... >
|
||||
<html>
|
||||
<head>
|
||||
{% analytical_head_top %}
|
||||
|
||||
{% analytical_head_bottom %}
|
||||
</head>
|
||||
<body>
|
||||
{% analytical_body_top %}
|
||||
...
|
||||
|
||||
...
|
||||
{% analytical_head_bottom %}
|
||||
</head>
|
||||
<body>
|
||||
{% analytical_body_top %}
|
||||
|
||||
{% analytical_body_bottom %}
|
||||
</body>
|
||||
</html>
|
||||
...
|
||||
|
||||
{% analytical_body_bottom %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Instead of using the generic tags, you can also just use tags specific
|
||||
for the analytics service(s) you are using. See :ref:`services` for
|
||||
|
|
@ -101,37 +111,70 @@ settings required to enable each service are listed here:
|
|||
|
||||
* :doc:`Chartbeat <services/chartbeat>`::
|
||||
|
||||
CHARTBEAT_USER_ID = '12345'
|
||||
CHARTBEAT_USER_ID = '12345'
|
||||
|
||||
* :doc:`Clickmap <services/clickmap>`::
|
||||
|
||||
CLICKMAP_TRACKER_CODE = '12345678....912'
|
||||
|
||||
* :doc:`Clicky <services/clicky>`::
|
||||
|
||||
CLICKY_SITE_ID = '12345678'
|
||||
CLICKY_SITE_ID = '12345678'
|
||||
|
||||
* :doc:`Crazy Egg <services/crazy_egg>`::
|
||||
|
||||
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
|
||||
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
|
||||
|
||||
* :doc:`Google Analytics <services/google_analytics>`::
|
||||
* :doc:`Facebook Pixel <services/facebook_pixel>`::
|
||||
|
||||
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
|
||||
FACEBOOK_PIXEL_ID = '1234567890'
|
||||
|
||||
* :doc:`Gaug.es <services/gauges>`::
|
||||
|
||||
GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef'
|
||||
|
||||
* :doc:`Google Analytics (legacy) <services/google_analytics>`::
|
||||
|
||||
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
|
||||
|
||||
* :doc:`Google Analytics (gtag.js) <services/google_analytics_gtag>`::
|
||||
|
||||
GOOGLE_ANALYTICS_GTAG_PROPERTY_ID = 'UA-1234567-8'
|
||||
|
||||
* :doc:`Google Analytics (analytics.js) <services/google_analytics_js>`::
|
||||
|
||||
GOOGLE_ANALYTICS_JS_PROPERTY_ID = 'UA-12345678-9'
|
||||
|
||||
* :doc:`HubSpot <services/hubspot>`::
|
||||
|
||||
HUBSPOT_PORTAL_ID = '1234'
|
||||
HUBSPOT_DOMAIN = 'somedomain.web101.hubspot.com'
|
||||
|
||||
* :doc:`Intercom <services/intercom>`::
|
||||
|
||||
INTERCOM_APP_ID = '0123456789abcdef0123456789abcdef01234567'
|
||||
|
||||
* :doc:`KISSinsights <services/kiss_insights>`::
|
||||
|
||||
KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
|
||||
KISS_INSIGHTS_SITE_CODE = 'abc'
|
||||
KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
|
||||
KISS_INSIGHTS_SITE_CODE = 'abc'
|
||||
|
||||
* :doc:`KISSmetrics <services/kiss_metrics>`::
|
||||
|
||||
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
|
||||
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
|
||||
|
||||
* :doc:`Lucky Orange <services/luckyorange>`::
|
||||
|
||||
LUCKYORANGE_SITE_ID = '123456'
|
||||
|
||||
* :doc:`Matomo (formerly Piwik) <services/matomo>`::
|
||||
|
||||
MATOMO_DOMAIN_PATH = 'your.matomo.server/optional/path'
|
||||
MATOMO_SITE_ID = '123'
|
||||
|
||||
* :doc:`Mixpanel <services/mixpanel>`::
|
||||
|
||||
MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
|
||||
MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
|
||||
|
||||
* :doc:`Olark <services/olark>`::
|
||||
|
||||
|
|
@ -139,23 +182,30 @@ settings required to enable each service are listed here:
|
|||
|
||||
* :doc:`Optimizely <services/optimizely>`::
|
||||
|
||||
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
|
||||
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
|
||||
|
||||
* :doc:`Performable <services/performable>`::
|
||||
|
||||
PERFORMABLE_API_KEY = '123abc'
|
||||
|
||||
* :doc:`Reinvigorate <services/reinvigorate>`::
|
||||
* :doc:`Rating\@Mail.ru <services/rating_mailru>`::
|
||||
|
||||
REINVIGORATE_TRACKING_ID = '12345-abcdefghij'
|
||||
RATING_MAILRU_COUNTER_ID = '1234567'
|
||||
|
||||
* :doc:`SnapEngage <services/snapengage>`::
|
||||
|
||||
SNAPENGAGE_WIDGET_ID = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
|
||||
|
||||
* :doc:`Woopra <services/woopra>`::
|
||||
|
||||
WOOPRA_DOMAIN = 'abcde.com'
|
||||
|
||||
* :doc:`Yandex.Metrica <services/yandex_metrica>`::
|
||||
|
||||
YANDEX_METRICA_COUNTER_ID = '12345678'
|
||||
|
||||
----
|
||||
|
||||
The django-analytics application is now set-up to track visitors. For
|
||||
information about further configuration and customization, see
|
||||
:doc:`features`.
|
||||
The django-analytical application is now set-up to track visitors. For
|
||||
information about identifying users, further configuration and
|
||||
customization, see :doc:`features`.
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ If you would like to have another analytics service supported by
|
|||
django-analytical, please create an issue on the project
|
||||
`issue tracker`_. See also :ref:`helping-out`.
|
||||
|
||||
.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
|
||||
.. _`issue tracker`: http://github.com/jazzband/django-analytical/issues
|
||||
|
||||
|
||||
Currently supported services:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
services/*
|
||||
services/*
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ contains a line that looks like this::
|
|||
Here, ``XXXXX`` is your User ID. Set :const:`CHARTBEAT_USER_ID` in the
|
||||
project :file:`settings.py` file::
|
||||
|
||||
CHARTBEAT_SITE_ID = 'XXXXX'
|
||||
CHARTBEAT_USER_ID = 'XXXXX'
|
||||
|
||||
If you do not set a User ID, the tracking code will not be rendered.
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ important information about detecting the visitor IP address.
|
|||
Setting the domain
|
||||
------------------
|
||||
|
||||
The Javascript tracking code can send the website domain to Chartbeat.
|
||||
The JavaScript tracking code can send the website domain to Chartbeat.
|
||||
If you use multiple subdomains this enables you to treat them as one
|
||||
website in Chartbeat. If your project uses the sites framework, the
|
||||
domain name of the current :class:`~django.contrib.sites.models.Site`
|
||||
|
|
|
|||
78
docs/services/clickmap.rst
Normal file
78
docs/services/clickmap.rst
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
==================================
|
||||
Clickmap -- visual click tracking
|
||||
==================================
|
||||
|
||||
`Clickmap`_ is a real-time heatmap tool to track mouse clicks and scroll paths of your website visitors.
|
||||
Gain intelligence about what's hot and what's not, and optimize your conversion with Clickmap.
|
||||
|
||||
.. _`Clickmap`: http://www.clickmap.ch/
|
||||
|
||||
|
||||
.. clickmap-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Clickmap integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Clickmap template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`clickmap-configuration`.
|
||||
|
||||
The Clickmap JavaScript code is inserted into templates using a template
|
||||
tag. Load the :mod:`clickmap` template tag library and insert the
|
||||
:ttag:`clickmap` tag. Because every page that you want to track must
|
||||
have the tag, it is useful to add it to your base template. Insert
|
||||
the tag at the bottom of the HTML body::
|
||||
|
||||
{% load clickmap %}
|
||||
...
|
||||
{% clickmap %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
.. _clickmap-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Clickmap integration, you must first set your
|
||||
Clickmap Tracker ID. If you don't have a Clickmap account yet,
|
||||
`sign up`_ to get your Tracker ID.
|
||||
|
||||
.. _`sign up`: http://www.clickmap.ch/
|
||||
|
||||
|
||||
.. _clickmap-tracker-id:
|
||||
|
||||
Setting the Tracker ID
|
||||
----------------------
|
||||
|
||||
Clickmap gives you a unique Tracker ID, and the :ttag:`clickmap`
|
||||
tag will include it in the rendered JavaScript code. You can find your
|
||||
Tracker ID clicking the link named "Tracker" in the dashboard
|
||||
of your Clickmap account. Set :const:`CLICKMAP_TRACKER_ID` in the project
|
||||
:file:`settings.py` file::
|
||||
|
||||
CLICKMAP_TRACKER_ID = 'XXXXXXXX'
|
||||
|
||||
If you do not set an Tracker ID, the tracking code will not be
|
||||
rendered.
|
||||
|
||||
|
||||
.. _clickmap-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` setting
|
||||
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
|
||||
commented out. See :ref:`identifying-visitors` for important information
|
||||
about detecting the visitor IP address.
|
||||
|
|
@ -53,7 +53,7 @@ Setting the Site ID
|
|||
-------------------
|
||||
|
||||
Every website you track with Clicky gets its own Site ID, and the
|
||||
:ttag:`clicky` tag will include it in the rendered Javascript code.
|
||||
:ttag:`clicky` tag will include it in the rendered JavaScript code.
|
||||
You can find the Site ID in the *Info* tab of the website *Preferences*
|
||||
page, in your Clicky account. Set :const:`CLICKY_SITE_ID` in the
|
||||
project :file:`settings.py` file::
|
||||
|
|
@ -84,7 +84,7 @@ Custom data
|
|||
|
||||
As described in the Clicky `customized tracking`_ documentation page,
|
||||
the data that is tracked by Clicky can be customized by setting the
|
||||
:data:`clicky_custom` Javascript variable before loading the tracking
|
||||
:data:`clicky_custom` JavaScript variable before loading the tracking
|
||||
code. Using template context variables, you can let the :ttag:`clicky`
|
||||
tag pass custom data to Clicky automatically. You can set the context
|
||||
variables in your view when you render a template containing the
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ Setting the account number
|
|||
--------------------------
|
||||
|
||||
Crazy Egg gives you a unique account number, and the :ttag:`crazy egg`
|
||||
tag will include it in the rendered Javascript code. You can find your
|
||||
tag will include it in the rendered JavaScript code. You can find your
|
||||
account number by clicking the link named "What's my code?" in the
|
||||
dashboard of your Crazy Egg account. Set
|
||||
:const:`CRAZY_EGG_ACCOUNT_NUMBER` in the project :file:`settings.py`
|
||||
|
|
|
|||
84
docs/services/facebook_pixel.rst
Normal file
84
docs/services/facebook_pixel.rst
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
=======================================
|
||||
Facebook Pixel -- advertising analytics
|
||||
=======================================
|
||||
|
||||
`Facebook Pixel`_ is Facebook's tool for conversion tracking, optimisation and remarketing.
|
||||
|
||||
.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
|
||||
|
||||
|
||||
.. facebook-pixel-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Facebook Pixel integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Facebook Pixel template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`facebook-pixel-configuration`.
|
||||
|
||||
The Facebook Pixel code is inserted into templates using template tags.
|
||||
Because every page that you want to track must have the tag,
|
||||
it is useful to add it to your base template.
|
||||
At the top of the template, load the :mod:`facebook_pixel` template tag library.
|
||||
Then insert the :ttag:`facebook_pixel_head` tag at the bottom of the head section,
|
||||
and optionally insert the :ttag:`facebook_pixel_body` tag at the bottom of the body section::
|
||||
|
||||
{% load facebook_pixel %}
|
||||
<html>
|
||||
<head>
|
||||
...
|
||||
{% facebook_pixel_head %}
|
||||
</head>
|
||||
<body>
|
||||
...
|
||||
{% facebook_pixel_body %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
.. note::
|
||||
The :ttag:`facebook_pixel_body` tag code will only be used for browsers with JavaScript disabled.
|
||||
It can be omitted if you don't need to support them.
|
||||
|
||||
|
||||
.. _facebook-pixel-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Facebook Pixel integration,
|
||||
you must first set your Pixel ID.
|
||||
|
||||
|
||||
.. _facebook-pixel-id:
|
||||
|
||||
Setting the Pixel ID
|
||||
--------------------
|
||||
|
||||
Each Facebook Adverts account you have can have a Pixel ID,
|
||||
and the :mod:`facebook_pixel` tags will include it in the rendered page.
|
||||
You can find the Pixel ID on the "Pixels" section of your Facebook Adverts account.
|
||||
Set :const:`FACEBOOK_PIXEL_ID` in the project :file:`settings.py` file::
|
||||
|
||||
FACEBOOK_PIXEL_ID = 'XXXXXXXXXX'
|
||||
|
||||
If you do not set a Pixel ID, the code will not be rendered.
|
||||
|
||||
|
||||
.. _facebook-pixel-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`FACEBOOK_PIXEL_INTERNAL_IPS`
|
||||
setting, the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
93
docs/services/gauges.rst
Normal file
93
docs/services/gauges.rst
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
=============================
|
||||
Gaug.es -- Real-time tracking
|
||||
=============================
|
||||
|
||||
Gaug.es_ is an easy way to implement real-time tracking for multiple
|
||||
websites.
|
||||
|
||||
.. _Gaug.es: http://www.gaug.es/
|
||||
|
||||
|
||||
.. gauges-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Gaug.es integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Gaug.es template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`gauges-configuration`.
|
||||
|
||||
The Gaug.es JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`gauges` template tag library and
|
||||
insert the :ttag:`gauges` tag. Because every page that you want to
|
||||
track must have the tag, it is useful to add it to your base template.
|
||||
Insert the tag at the top of the HTML head::
|
||||
|
||||
{% load gauges %}
|
||||
<html>
|
||||
<head>
|
||||
{% gauges %}
|
||||
...
|
||||
|
||||
|
||||
.. _gauges-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Gaug.es integration, you must first set your
|
||||
site id.
|
||||
|
||||
|
||||
.. _gauges-site-id:
|
||||
|
||||
Setting the site id
|
||||
--------------------------
|
||||
|
||||
Gaug.es gives you a unique site id, and the :ttag:`gauges`
|
||||
tag will include it in the rendered JavaScript code. You can find your
|
||||
site id by clicking the *Tracking Code* link when logged into
|
||||
the on the gaug.es website. A page will display containing
|
||||
HTML code looking like this::
|
||||
|
||||
<script>
|
||||
var _gauges = _gauges || [];
|
||||
(function() {
|
||||
var t = document.createElement('script');
|
||||
t.type = 'text/javascript';
|
||||
t.async = true;
|
||||
t.id = 'gauges-tracker';
|
||||
t.setAttribute('data-site-id', 'XXXXXXXXXXXXXXXXXXXXXXX');
|
||||
t.src = '//secure.gaug.es/track.js';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})();
|
||||
</script>
|
||||
|
||||
The code ``XXXXXXXXXXXXXXXXXXXXXXX`` is your site id. Set
|
||||
:const:`GAUGES_SITE_ID` in the project :file:`settings.py`
|
||||
file::
|
||||
|
||||
GAUGES_SITE_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
||||
If you do not set an site id, the JavaScript code will not be
|
||||
rendered.
|
||||
|
||||
|
||||
.. _gauges-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` setting
|
||||
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
|
||||
commented out. See :ref:`identifying-visitors` for important information
|
||||
about detecting the visitor IP address.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
====================================
|
||||
Google Analytics -- traffic analysis
|
||||
====================================
|
||||
==============================================
|
||||
Google Analytics (legacy) -- traffic analysis
|
||||
==============================================
|
||||
|
||||
`Google Analytics`_ is the well-known web analytics service from
|
||||
Google. The product is aimed more at marketers than webmasters or
|
||||
|
|
@ -15,7 +15,7 @@ features.
|
|||
Installation
|
||||
============
|
||||
|
||||
To start using the Google Analytics integration, you must have installed
|
||||
To start using the Google Analytics (legacy) integration, you must have installed
|
||||
the django-analytical package and have added the ``analytical``
|
||||
application to :const:`INSTALLED_APPS` in your project
|
||||
:file:`settings.py` file. See :doc:`../install` for details.
|
||||
|
|
@ -58,7 +58,7 @@ Setting the property ID
|
|||
|
||||
Every website you track with Google Analytics gets its own property ID,
|
||||
and the :ttag:`google_analytics` tag will include it in the rendered
|
||||
Javascript code. You can find the web property ID on the overview page
|
||||
JavaScript code. You can find the web property ID on the overview page
|
||||
of your account. Set :const:`GOOGLE_ANALYTICS_PROPERTY_ID` in the
|
||||
project :file:`settings.py` file::
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ Tracking multiple domains
|
|||
|
||||
The default code is suitable for tracking a single domain. If you track
|
||||
multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE`
|
||||
setting to one of the :const:`analytical.google_analytics.SCOPE_*`
|
||||
setting to one of the :const:`analytical.templatetags.google_analytics.TRACK_*`
|
||||
constants:
|
||||
|
||||
============================= ===== =============================================
|
||||
|
|
@ -87,7 +87,7 @@ Constant Value Description
|
|||
============================= ===== =============================================
|
||||
|
||||
As noted, the default tracking style is
|
||||
:const:`~analytical.google_analytics.TRACK_SINGLE_DOMAIN`.
|
||||
:const:`~analytical.templatetags.google_analytics.TRACK_SINGLE_DOMAIN`.
|
||||
|
||||
When you track multiple (sub)domains, django-analytical needs to know
|
||||
what domain name to pass to Google Analytics. If you use the contrib
|
||||
|
|
@ -98,6 +98,21 @@ either pass the domain to the template tag through the context variable
|
|||
or set it in the project :file:`settings.py` file using
|
||||
:const:`GOOGLE_ANALYTICS_DOMAIN` (fallback: :const:`ANALYTICAL_DOMAIN`).
|
||||
|
||||
Display Advertising
|
||||
-------------------
|
||||
|
||||
Display Advertising allows you to view Demographics and Interests reports,
|
||||
add Remarketing Lists and support DoubleClick Campain Manager integration.
|
||||
|
||||
You can enable `Display Advertising features`_ by setting the
|
||||
:const:`GOOGLE_ANALYTICS_DISPLAY_ADVERTISING` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_DISPLAY_ADVERTISING = True
|
||||
|
||||
By default, display advertising features are disabled.
|
||||
|
||||
.. _`Display Advertising features`: https://support.google.com/analytics/answer/3450482
|
||||
|
||||
|
||||
Tracking site speed
|
||||
-------------------
|
||||
|
|
@ -109,7 +124,7 @@ You can view page load times in the `Site Speed report`_ by setting the
|
|||
|
||||
By default, page load times are not tracked.
|
||||
|
||||
.. _`Site Speed report`: http://www.google.com/support/analyticshelp/bin/answer.py?answer=1205784&topic=1282106
|
||||
.. _`Site Speed report`: https://support.google.com/analytics/answer/1205784
|
||||
|
||||
|
||||
.. _google-analytics-internal-ips:
|
||||
|
|
@ -144,19 +159,19 @@ when your render a template containing the tracking code::
|
|||
|
||||
The value of the context variable is a tuple *(name, value, [scope])*.
|
||||
The scope parameter is one of the
|
||||
:const:`analytical.google_analytics.SCOPE_*` constants:
|
||||
:const:`analytical.templatetags.google_analytics.SCOPE_*` constants:
|
||||
|
||||
================= ====== =============================================
|
||||
Constant Value Description
|
||||
================= ====== =============================================
|
||||
``SCOPE_VISITOR`` 1 Distinguishes categories of visitors across
|
||||
multiple sessions.
|
||||
``SCOPE_SESSION`` 2 Ddistinguishes different visitor experiences
|
||||
``SCOPE_SESSION`` 2 Distinguishes different visitor experiences
|
||||
across sessions.
|
||||
``SCOPE_PAGE`` 3 Defines page-level activity.
|
||||
================= ====== =============================================
|
||||
|
||||
The default scope is :const:`~analytical.google_analytics.SCOPE_PAGE`.
|
||||
The default scope is :const:`~analytical.templatetags.google_analytics.SCOPE_PAGE`.
|
||||
|
||||
You may want to set custom variables in a context processor that you add
|
||||
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
|
||||
|
|
@ -172,3 +187,83 @@ Just remember that if you set the same context variable in the
|
|||
context processor, the latter clobbers the former.
|
||||
|
||||
.. _`custom variables`: http://code.google.com/apis/analytics/docs/tracking/gaTrackingCustomVariables.html
|
||||
|
||||
|
||||
.. _google-analytics-anonimyze-ips:
|
||||
|
||||
Anonymize IPs
|
||||
-------------
|
||||
|
||||
You can enable the `IP anonymization`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_ANONYMIZE_IP` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_ANONYMIZE_IP = True
|
||||
|
||||
This may be mandatory for deployments in countries that have a firm policies
|
||||
concerning data privacy (e.g. Germany).
|
||||
|
||||
By default, IPs are not anonymized.
|
||||
|
||||
.. _`IP anonymization`: https://support.google.com/analytics/bin/answer.py?hl=en&answer=2763052
|
||||
|
||||
|
||||
.. _google-analytics-sample-rate:
|
||||
|
||||
Sample Rate
|
||||
-----------
|
||||
|
||||
You can configure the `Sample Rate`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_SAMPLE_RATE` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_SAMPLE_RATE = 10
|
||||
|
||||
The value is a percentage and can be between 0 and 100 and can be a string or
|
||||
decimal value of with up to two decimal places.
|
||||
|
||||
.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsamplerate
|
||||
|
||||
|
||||
.. _google-analytics-site-speed-sample-rate:
|
||||
|
||||
Site Speed Sample Rate
|
||||
----------------------
|
||||
|
||||
You can configure the `Site Speed Sample Rate`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE = 10
|
||||
|
||||
The value is a percentage and can be between 0 and 100 and can be a string or
|
||||
decimal value of with up to two decimal places.
|
||||
|
||||
.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsitespeedsamplerate
|
||||
|
||||
|
||||
.. _google-analytics-session-cookie-timeout:
|
||||
|
||||
Session Cookie Timeout
|
||||
----------------------
|
||||
|
||||
You can configure the `Session Cookie Timeout`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT = 3600000
|
||||
|
||||
The value is the session cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
|
||||
|
||||
.. _`Session Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
|
||||
|
||||
|
||||
.. _google-analytics-visitor-cookie-timeout:
|
||||
|
||||
Visitor Cookie Timeout
|
||||
----------------------
|
||||
|
||||
You can configure the `Visitor Cookie Timeout`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT = 3600000
|
||||
|
||||
The value is the visitor cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
|
||||
|
||||
.. _`Visitor Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setvisitorcookietimeout
|
||||
|
|
|
|||
168
docs/services/google_analytics_gtag.rst
Normal file
168
docs/services/google_analytics_gtag.rst
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
===============================================
|
||||
Google Analytics (gtag.js) -- traffic analysis
|
||||
===============================================
|
||||
|
||||
`Google Analytics`_ is the well-known web analytics service from
|
||||
Google. The product is aimed more at marketers than webmasters or
|
||||
technologists, supporting integration with AdWords and other e-commence
|
||||
features. The global site tag (`gtag.js`_) is a JavaScript tagging
|
||||
framework and API that allows you to send event data to Google Analytics,
|
||||
Google Ads, and Google Marketing Platform.
|
||||
|
||||
.. _`Google Analytics`: http://www.google.com/analytics/
|
||||
.. _`gtag.js`: https://developers.google.com/analytics/devguides/collection/gtagjs/
|
||||
|
||||
|
||||
.. google-analytics-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Google Analytics integration, you must have installed
|
||||
the django-analytical package and have added the ``analytical``
|
||||
application to :const:`INSTALLED_APPS` in your project
|
||||
:file:`settings.py` file. See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Google Analytics template tag to your
|
||||
templates. This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`google-analytics-configuration-gtag`.
|
||||
|
||||
The Google Analytics tracking code is inserted into templates using a
|
||||
template tag. Load the :mod:`google_analytics_gtag` template tag library and
|
||||
insert the :ttag:`google_analytics_gtag` tag. Because every page that you
|
||||
want to track must have the tag, it is useful to add it to your base
|
||||
template. Insert the tag at the bottom of the HTML head::
|
||||
|
||||
{% load google_analytics_gtag %}
|
||||
<html>
|
||||
<head>
|
||||
{% google_analytics_gtag %}
|
||||
...
|
||||
</head>
|
||||
...
|
||||
|
||||
|
||||
.. _google-analytics-configuration-gtag:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Google Analytics integration, you must first set
|
||||
your website property ID. If you track multiple domains with the same
|
||||
code, you also need to set-up the domain. Finally, you can add custom
|
||||
segments for Google Analytics to track.
|
||||
|
||||
|
||||
.. _google-analytics-gtag-property-id:
|
||||
|
||||
Setting the property ID
|
||||
-----------------------
|
||||
|
||||
Every website you track with Google Analytics gets its own property ID,
|
||||
and the :ttag:`google_analytics_gtag` tag will include it in the rendered
|
||||
JavaScript code. You can find the web property ID on the overview page
|
||||
of your account. Set :const:`GOOGLE_ANALYTICS_GTAG_PROPERTY_ID` in the
|
||||
project :file:`settings.py` file::
|
||||
|
||||
GOOGLE_ANALYTICS_GTAG_PROPERTY_ID = 'UA-XXXXXX-X'
|
||||
|
||||
If you do not set a property ID, the tracking code will not be rendered.
|
||||
|
||||
Please note that the accepted Property IDs should be one of the following formats:
|
||||
|
||||
- 'UA-XXXXXX-Y'
|
||||
- 'AW-XXXXXXXXXX'
|
||||
- 'G-XXXXXXXX'
|
||||
- 'DC-XXXXXXXX'
|
||||
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`GOOGLE_ANALYTICS_INTERNAL_IPS`
|
||||
setting, the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
|
||||
.. _google-analytics-identify-user:
|
||||
|
||||
Identifying authenticated users
|
||||
-------------------------------
|
||||
|
||||
The username of an authenticated user is passed to Google Analytics
|
||||
automatically as the ``user_id``. See :ref:`identifying-visitors`.
|
||||
|
||||
According to `Google Analytics conditions`_ you should avoid
|
||||
sending Personally Identifiable Information.
|
||||
Using ``username`` as ``user_id`` might not be the best option.
|
||||
To avoid that, you can change the identity
|
||||
by setting ``google_analytics_gtag_identity`` (or ``analytical_identity`` to
|
||||
affect all providers) context variable:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
context = RequestContext({'google_analytics_gtag_identity': user.uuid})
|
||||
return some_template.render(context)
|
||||
|
||||
or in the template:
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{% with google_analytics_gtag_identity=request.user.uuid|default:None %}
|
||||
{% analytical_head_top %}
|
||||
{% endwith %}
|
||||
|
||||
.. _`Google Analytics conditions`: https://developers.google.com/analytics/solutions/crm-integration#user_id
|
||||
|
||||
.. _google-analytics-custom-dimensions:
|
||||
|
||||
Custom dimensions
|
||||
----------------
|
||||
|
||||
As described in the Google Analytics `custom dimensions`_ documentation
|
||||
page, you can define custom dimensions which are variables specific to your
|
||||
business needs. These variables can include both custom event parameters as
|
||||
well as customer user properties. Using the template context variable
|
||||
``google_analytics_custom_dimensions``, you can let the :ttag:`google_analytics_gtag`
|
||||
pass custom dimensions to Google Analytics automatically. The ``google_analytics_custom_dimensions``
|
||||
variable must be set to a dictionary where the keys are the dimension names
|
||||
and the values are the dimension values. You can set the context variable in your
|
||||
view when you render a template containing the tracking code::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
context = RequestContext({
|
||||
'google_analytics_custom_dimensions': {
|
||||
'gender': 'female',
|
||||
'country': 'US',
|
||||
'user_properties': {
|
||||
'age': 25
|
||||
}
|
||||
}
|
||||
})
|
||||
return some_template.render(context)
|
||||
|
||||
Note that the ``user_properties`` key is used to pass user properties to Google
|
||||
Analytics. It's not necessary to always use this key, but that'd be the way of
|
||||
sending user properties to Google Analytics automatically.
|
||||
|
||||
You may want to set custom dimensions in a context processor that you add
|
||||
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def google_analytics_segment_language(request):
|
||||
try:
|
||||
return {'google_analytics_custom_dimensions': {'language': request.LANGUAGE_CODE}}
|
||||
except AttributeError:
|
||||
return {}
|
||||
|
||||
Just remember that if you set the same context variable in the
|
||||
:class:`~django.template.context.RequestContext` constructor and in a
|
||||
context processor, the latter clobbers the former.
|
||||
|
||||
.. _`custom dimensions`: https://support.google.com/analytics/answer/10075209
|
||||
238
docs/services/google_analytics_js.rst
Normal file
238
docs/services/google_analytics_js.rst
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
====================================================
|
||||
Google Analytics (analytics.js) -- traffic analysis
|
||||
====================================================
|
||||
|
||||
`Google Analytics`_ is the well-known web analytics service from
|
||||
Google. The product is aimed more at marketers than webmasters or
|
||||
technologists, supporting integration with AdWords and other e-commence
|
||||
features. The `analytics.js`_ library (also known as "the Google
|
||||
Analytics tag") is a JavaScript library for measuring how users interact
|
||||
with your website.
|
||||
|
||||
.. _`Google Analytics`: http://www.google.com/analytics/
|
||||
.. _`analytics.js`: https://developers.google.com/analytics/devguides/collection/analyticsjs/
|
||||
|
||||
|
||||
.. google-analytics-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Google Analytics integration, you must have installed
|
||||
the django-analytical package and have added the ``analytical``
|
||||
application to :const:`INSTALLED_APPS` in your project
|
||||
:file:`settings.py` file. See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Google Analytics template tag to your
|
||||
templates. This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`google-analytics-configuration-js`.
|
||||
|
||||
The Google Analytics tracking code is inserted into templates using a
|
||||
template tag. Load the :mod:`google_analytics_js` template tag library and
|
||||
insert the :ttag:`google_analytics_js` tag. Because every page that you
|
||||
want to track must have the tag, it is useful to add it to your base
|
||||
template. Insert the tag at the bottom of the HTML head::
|
||||
|
||||
{% load google_analytics_js %}
|
||||
<html>
|
||||
<head>
|
||||
...
|
||||
{% google_analytics_js %}
|
||||
</head>
|
||||
...
|
||||
|
||||
|
||||
.. _google-analytics-configuration-js:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Google Analytics integration, you must first set
|
||||
your website property ID. If you track multiple domains with the same
|
||||
code, you also need to set-up the domain. Finally, you can add custom
|
||||
segments for Google Analytics to track.
|
||||
|
||||
|
||||
.. _google-analytics-js-property-id:
|
||||
|
||||
Setting the property ID
|
||||
-----------------------
|
||||
|
||||
Every website you track with Google Analytics gets its own property ID,
|
||||
and the :ttag:`google_analytics_js` tag will include it in the rendered
|
||||
JavaScript code. You can find the web property ID on the overview page
|
||||
of your account. Set :const:`GOOGLE_ANALYTICS_JS_PROPERTY_ID` in the
|
||||
project :file:`settings.py` file::
|
||||
|
||||
GOOGLE_ANALYTICS_JS_PROPERTY_ID = 'UA-XXXXXXXX-X'
|
||||
|
||||
If you do not set a property ID, the tracking code will not be rendered.
|
||||
|
||||
|
||||
Tracking multiple domains
|
||||
-------------------------
|
||||
|
||||
The default code is suitable for tracking a single domain. If you track
|
||||
multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE`
|
||||
setting to one of the :const:`analytical.templatetags.google_analytics_js.TRACK_*`
|
||||
constants:
|
||||
|
||||
============================= ===== =============================================
|
||||
Constant Value Description
|
||||
============================= ===== =============================================
|
||||
``TRACK_SINGLE_DOMAIN`` 1 Track one domain.
|
||||
``TRACK_MULTIPLE_SUBDOMAINS`` 2 Track multiple subdomains of the same top
|
||||
domain (e.g. `fr.example.com` and
|
||||
`nl.example.com`).
|
||||
``TRACK_MULTIPLE_DOMAINS`` 3 Track multiple top domains (e.g. `example.fr`
|
||||
and `example.nl`).
|
||||
============================= ===== =============================================
|
||||
|
||||
As noted, the default tracking style is
|
||||
:const:`~analytical.templatetags.google_analytics_js.TRACK_SINGLE_DOMAIN`.
|
||||
|
||||
When you track multiple (sub)domains, django-analytical needs to know
|
||||
what domain name to pass to Google Analytics. If you use the contrib
|
||||
sites app, the domain is automatically picked up from the current
|
||||
:const:`~django.contrib.sites.models.Site` instance. Otherwise, you may
|
||||
either pass the domain to the template tag through the context variable
|
||||
:const:`google_analytics_domain` (fallback: :const:`analytical_domain`)
|
||||
or set it in the project :file:`settings.py` file using
|
||||
:const:`GOOGLE_ANALYTICS_DOMAIN` (fallback: :const:`ANALYTICAL_DOMAIN`).
|
||||
|
||||
Display Advertising
|
||||
-------------------
|
||||
|
||||
Display Advertising allows you to view Demographics and Interests reports,
|
||||
add Remarketing Lists and support DoubleClick Campain Manager integration.
|
||||
|
||||
You can enable `Display Advertising features`_ by setting the
|
||||
:const:`GOOGLE_ANALYTICS_DISPLAY_ADVERTISING` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_DISPLAY_ADVERTISING = True
|
||||
|
||||
By default, display advertising features are disabled.
|
||||
|
||||
.. _`Display Advertising features`: https://support.google.com/analytics/answer/3450482
|
||||
|
||||
|
||||
.. _google-analytics-js-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`GOOGLE_ANALYTICS_INTERNAL_IPS`
|
||||
setting, the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
|
||||
|
||||
.. _google-analytics-js-custom-variables:
|
||||
|
||||
Custom variables
|
||||
----------------
|
||||
|
||||
As described in the Google Analytics `custom variables`_ documentation
|
||||
page, you can define custom segments. Using template context variables
|
||||
``google_analytics_var1`` through ``google_analytics_var5``, you can let
|
||||
the :ttag:`google_analytics_js` tag pass custom variables to Google
|
||||
Analytics automatically. You can set the context variables in your view
|
||||
when your render a template containing the tracking code::
|
||||
|
||||
context = RequestContext({'google_analytics_var1': ('gender', 'female'),
|
||||
'google_analytics_var2': ('visit', 1)})
|
||||
return some_template.render(context)
|
||||
|
||||
The value of the context variable is a tuple *(name, value)*.
|
||||
|
||||
You may want to set custom variables in a context processor that you add
|
||||
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
|
||||
|
||||
def google_analytics_segment_language(request):
|
||||
try:
|
||||
return {'google_analytics_var3': request.LANGUAGE_CODE}
|
||||
except AttributeError:
|
||||
return {}
|
||||
|
||||
Just remember that if you set the same context variable in the
|
||||
:class:`~django.template.context.RequestContext` constructor and in a
|
||||
context processor, the latter clobbers the former.
|
||||
|
||||
.. _`custom variables`: https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#custom-vars
|
||||
|
||||
|
||||
.. _google-analytics-js-anonimyze-ips:
|
||||
|
||||
Anonymize IPs
|
||||
-------------
|
||||
|
||||
You can enable the `IP anonymization`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_ANONYMIZE_IP` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_ANONYMIZE_IP = True
|
||||
|
||||
This may be mandatory for deployments in countries that have a firm policies
|
||||
concerning data privacy (e.g. Germany).
|
||||
|
||||
By default, IPs are not anonymized.
|
||||
|
||||
.. _`IP anonymization`: https://support.google.com/analytics/bin/answer.py?hl=en&answer=2763052
|
||||
|
||||
|
||||
.. _google-analytics-js-sample-rate:
|
||||
|
||||
Sample Rate
|
||||
-----------
|
||||
|
||||
You can configure the `Sample Rate`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_SAMPLE_RATE` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_SAMPLE_RATE = 10
|
||||
|
||||
The value is a percentage and can be between 0 and 100 and can be a string or
|
||||
integer value.
|
||||
|
||||
.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#sampleRate
|
||||
|
||||
|
||||
.. _google-analytics-js-site-speed-sample-rate:
|
||||
|
||||
Site Speed Sample Rate
|
||||
----------------------
|
||||
|
||||
You can configure the `Site Speed Sample Rate`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE = 10
|
||||
|
||||
The value is a percentage and can be between 0 and 100 and can be a string or
|
||||
integer value.
|
||||
|
||||
.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#siteSpeedSampleRate
|
||||
|
||||
|
||||
.. _google-analytics-cookie-expiration:
|
||||
|
||||
Cookie Expiration
|
||||
-----------------
|
||||
|
||||
You can configure the `Cookie Expiration`_ feature by setting the
|
||||
:const:`GOOGLE_ANALYTICS_COOKIE_EXPIRATION` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_COOKIE_EXPIRATION = 3600000
|
||||
|
||||
The value is the cookie expiration in seconds or 0 to delete the cookie when the browser is closed.
|
||||
|
||||
.. _`Cookie Expiration`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
|
||||
|
||||
Custom JavaScript Source
|
||||
------------------------
|
||||
|
||||
You can configure a custom URL for the javascript file by setting the
|
||||
:const:`GOOGLE_ANALYTICS_JS_SOURCE` configuration setting::
|
||||
|
||||
GOOGLE_ANALYTICS_JS_SOURCE = 'https://www.example.com/analytics.js'
|
||||
59
docs/services/heap.rst
Normal file
59
docs/services/heap.rst
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
=====================================
|
||||
Heap -- analytics and events tracking
|
||||
=====================================
|
||||
|
||||
`Heap`_ automatically captures all user interactions on your site, from the moment of installation forward.
|
||||
|
||||
.. _`Heap`: https://heap.io/
|
||||
|
||||
|
||||
.. heap-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Heap integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
.. _heap-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Heap integration, you must first get your
|
||||
Heap Tracker ID. If you don't have a Heap account yet,
|
||||
`sign up`_ to get your Tracker ID.
|
||||
|
||||
.. _`sign up`: https://heap.io/
|
||||
|
||||
|
||||
.. _heap-tracker-id:
|
||||
|
||||
Setting the Tracker ID
|
||||
----------------------
|
||||
|
||||
Heap gives you a unique ID. You can find this ID on the Projects page
|
||||
of your Heap account. Set :const:`HEAP_TRACKER_ID` in the project
|
||||
:file:`settings.py` file::
|
||||
|
||||
HEAP_TRACKER_ID = 'XXXXXXXX'
|
||||
|
||||
If you do not set an Tracker ID, the tracking code will not be
|
||||
rendered.
|
||||
|
||||
The tracking code will be added just before the closing head tag.
|
||||
|
||||
|
||||
.. _heap-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` setting
|
||||
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
|
||||
commented out. See :ref:`identifying-visitors` for important information
|
||||
about detecting the visitor IP address.
|
||||
73
docs/services/hotjar.rst
Normal file
73
docs/services/hotjar.rst
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
=====================================
|
||||
Hotjar -- analytics and user feedback
|
||||
=====================================
|
||||
|
||||
`Hotjar`_ is a website analytics and user feedback tool.
|
||||
|
||||
.. _`Hotjar`: https://www.hotjar.com/
|
||||
|
||||
|
||||
.. hotjar-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Hotjar integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Hotjar template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`hotjar-configuration`.
|
||||
|
||||
The Hotjar code is inserted into templates using template tags.
|
||||
Because every page that you want to track must have the tag,
|
||||
it is useful to add it to your base template.
|
||||
At the top of the template, load the :mod:`hotjar` template tag library.
|
||||
Then insert the :ttag:`hotjar` tag at the bottom of the head section::
|
||||
|
||||
{% load hotjar %}
|
||||
<html>
|
||||
<head>
|
||||
...
|
||||
{% hotjar %}
|
||||
</head>
|
||||
...
|
||||
</html>
|
||||
|
||||
|
||||
.. _hotjar-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Hotjar integration, you must first set your Site ID.
|
||||
|
||||
|
||||
.. _hotjar-id:
|
||||
|
||||
Setting the Hotjar Site ID
|
||||
--------------------------
|
||||
|
||||
You can find the Hotjar Site ID in the "Sites & Organizations" section of your Hotjar account.
|
||||
Set :const:`HOTJAR_SITE_ID` in the project :file:`settings.py` file::
|
||||
|
||||
HOTJAR_SITE_ID = 'XXXXXXXXX'
|
||||
|
||||
If you do not set a Hotjar Site ID, the code will not be rendered.
|
||||
|
||||
|
||||
.. _hotjar-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`HOTJAR_INTERNAL_IPS`
|
||||
setting, the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
|
|
@ -3,7 +3,7 @@ HubSpot -- inbound marketing
|
|||
============================
|
||||
|
||||
HubSpot_ helps you get found by customers. It provides tools for
|
||||
content creation, convertion and marketing analysis. HubSpot uses
|
||||
content creation, conversion and marketing analysis. HubSpot uses
|
||||
tracking on your website to measure effect of your marketing efforts.
|
||||
|
||||
.. _HubSpot: http://www.hubspot.com/
|
||||
|
|
@ -43,29 +43,28 @@ Configuration
|
|||
=============
|
||||
|
||||
Before you can use the HubSpot integration, you must first set your
|
||||
portal ID and domain.
|
||||
portal ID, also known as your Hub ID.
|
||||
|
||||
|
||||
.. _hubspot-portal-id:
|
||||
|
||||
Setting the portal ID and domain
|
||||
--------------------------------
|
||||
Setting the portal ID
|
||||
---------------------
|
||||
|
||||
Your HubSpot account has its own portal ID and primary domain name, and
|
||||
the :ttag:`hubspot` tag will include them in the rendered Javascript
|
||||
code. You can find the portal ID and domain by going to the *Domains*
|
||||
tab in your HubSpot account. The domain you need to use is listed as
|
||||
*Primary Domain* on that page, and the portal ID can be found in the
|
||||
footer. Set :const:`HUBSPOT_PORTAL_ID` and :const:`HUBSPOT_DOMAIN` in
|
||||
the project :file:`settings.py` file::
|
||||
Your HubSpot account has its own portal ID, the :ttag:`hubspot` tag
|
||||
will include them in the rendered JavaScript code. You can find the
|
||||
portal ID by accessing your dashboard. Alternatively, read this
|
||||
`Quick Answer page <http://help.hubspot.com/articles/KCS_Article/Where-can-I-find-my-HUB-ID>`_.
|
||||
Set :const:`HUBSPOT_PORTAL_ID` in the project :file:`settings.py` file::
|
||||
|
||||
HUBSPOT_PORTAL_ID = 'XXXX'
|
||||
HUBSPOT_DOMAIN = 'XXXXXXXX.web101.hubspot.com'
|
||||
|
||||
If you do not set the portal ID and domain, the tracking code will not
|
||||
be rendered.
|
||||
If you do not set the portal ID, the tracking code will not be rendered.
|
||||
|
||||
|
||||
.. deprecated:: 0.18.0
|
||||
`HUBSPOT_DOMAIN` is no longer required.
|
||||
|
||||
.. _hubspot-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
|
|
|
|||
166
docs/services/intercom.rst
Normal file
166
docs/services/intercom.rst
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
=================================
|
||||
Intercom.io -- Real-time tracking
|
||||
=================================
|
||||
|
||||
Intercom.io_ is an easy way to implement real-chat and individual
|
||||
support for a website
|
||||
|
||||
.. _Intercom.io: http://www.intercom.io/
|
||||
|
||||
|
||||
.. intercom-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Intercom.io integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Intercom.io template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`intercom-configuration`.
|
||||
|
||||
The Intercom.io JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`intercom` template tag library and
|
||||
insert the :ttag:`intercom` tag. Because every page that you want to
|
||||
track must have the tag, it is useful to add it to your base template.
|
||||
Insert the tag at the bottom of the HTML body::
|
||||
|
||||
{% load intercom %}
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<!-- Your page -->
|
||||
{% intercom %}
|
||||
</body>
|
||||
</html>
|
||||
...
|
||||
|
||||
|
||||
.. _intercom-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Intercom.io integration, you must first set your
|
||||
app id.
|
||||
|
||||
|
||||
.. _intercom-site-id:
|
||||
|
||||
Setting the app id
|
||||
--------------------------
|
||||
|
||||
Intercom.io gives you a unique app id, and the :ttag:`intercom`
|
||||
tag will include it in the rendered JavaScript code. You can find your
|
||||
app id by clicking the *Tracking Code* link when logged into
|
||||
the on the intercom.io website. A page will display containing
|
||||
HTML code looking like this::
|
||||
|
||||
<script id="IntercomSettingsScriptTag">
|
||||
window.intercomSettings = { name: "Jill Doe", email: "jill@example.com", created_at: 1234567890, app_id: "XXXXXXXXXXXXXXXXXXXXXXX" };
|
||||
</script>
|
||||
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
|
||||
|
||||
The code ``XXXXXXXXXXXXXXXXXXXXXXX`` is your app id. Set
|
||||
:const:`INTERCOM_APP_ID` in the project :file:`settings.py`
|
||||
file::
|
||||
|
||||
INTERCOM_APP_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
||||
If you do not set an app id, the JavaScript code will not be
|
||||
rendered.
|
||||
|
||||
|
||||
Custom data
|
||||
-----------
|
||||
|
||||
As described in the Intercom documentation on `custom visitor data`_,
|
||||
the data that is tracked by Intercom can be customized. Using template
|
||||
context variables, you can let the :ttag:`intercom` tag pass custom data
|
||||
to Intercom automatically. You can set the context variables in your view
|
||||
when your render a template containing the tracking code::
|
||||
|
||||
context = RequestContext({'intercom_cart_value': cart.total_price})
|
||||
return some_template.render(context)
|
||||
|
||||
For some data, it is annoying to do this for every view, so you may want
|
||||
to set variables in a context processor that you add to the
|
||||
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
|
||||
|
||||
from django.utils.hashcompat import md5_constructor as md5
|
||||
|
||||
GRAVATAR_URL = 'http://www.gravatar.com/avatar/'
|
||||
|
||||
def intercom_custom_data(request):
|
||||
try:
|
||||
email = request.user.email
|
||||
except AttributeError:
|
||||
return {}
|
||||
email_hash = md5(email).hexdigest()
|
||||
avatar_url = "%s%s" % (GRAVATAR_URL, email_hash)
|
||||
return {'intercom_avatar': avatar_url}
|
||||
|
||||
Just remember that if you set the same context variable in the
|
||||
:class:`~django.template.context.RequestContext` constructor and in a
|
||||
context processor, the latter clobbers the former.
|
||||
|
||||
Standard variables that will be displayed in the Intercom live visitor
|
||||
data are listed in the table below, but you can define any ``intercom_*``
|
||||
variable you like and have that detail passed from within the visitor
|
||||
live stream data when viewing Intercom.
|
||||
|
||||
==================== ===========================================
|
||||
Context variable Description
|
||||
==================== ===========================================
|
||||
``intercom_name`` The visitor's full name.
|
||||
-------------------- -------------------------------------------
|
||||
``intercom_email`` The visitor's email address.
|
||||
-------------------- -------------------------------------------
|
||||
``intercom_user_id`` The visitor's user id.
|
||||
-------------------- -------------------------------------------
|
||||
``created_at`` The date the visitor created an account
|
||||
==================== ===========================================
|
||||
|
||||
|
||||
.. _`custom visitor data`: https://www.intercom.com/help/configure-intercom-for-your-product-or-site/customize-intercom-to-be-about-your-users/send-custom-user-attributes-to-intercom
|
||||
|
||||
|
||||
Identifying authenticated users
|
||||
-------------------------------
|
||||
|
||||
If you have not set the ``intercom_name``, ``intercom_email``, or ``intercom_user_id`` variables
|
||||
explicitly, the username and email address of an authenticated user are
|
||||
passed to Intercom automatically. See :ref:`identifying-visitors`.
|
||||
|
||||
.. _intercom-internal-ips:
|
||||
|
||||
|
||||
Verifying identified users
|
||||
--------------------------
|
||||
|
||||
Intercom supports HMAC authentication of users identified by user ID or email, in order to prevent impersonation.
|
||||
For more information, see `Enable identity verification on your web product`_ in the Intercom documentation.
|
||||
|
||||
To enable this, configure your Intercom account's HMAC secret key::
|
||||
|
||||
INTERCOM_HMAC_SECRET_KEY = 'XXXXXXXXXXXXXXXXXXXXXXX'
|
||||
|
||||
(You can find this secret key under the "Identity verification" section of your Intercom account settings page.)
|
||||
|
||||
.. _`Enable identity verification on your web product`: https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
|
||||
|
||||
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` setting
|
||||
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
|
||||
commented out. See :ref:`identifying-visitors` for important information
|
||||
about detecting the visitor IP address.
|
||||
|
|
@ -54,10 +54,10 @@ Setting the account number and site code
|
|||
|
||||
In order to install the survey code, you need to set your KISSinsights
|
||||
account number and website code. The :ttag:`kiss_insights` tag will
|
||||
include it in the rendered Javascript code. You can find the account
|
||||
include it in the rendered JavaScript code. You can find the account
|
||||
number and website code by visiting the code installation page of the
|
||||
website you want to place the surveys on. You will see some HTML code
|
||||
with a Javascript tag with a ``src`` attribute containing
|
||||
with a JavaScript tag with a ``src`` attribute containing
|
||||
``//s3.amazonaws.com/ki.js/XXXXX/YYY.js``. Here ``XXXXX`` is the
|
||||
account number and ``YYY`` the website code. Set
|
||||
:const:`KISS_INSIGHTS_ACCOUNT_NUMBER` and
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ This step is only needed if you are not using the generic
|
|||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`kiss-metrics-configuration`.
|
||||
|
||||
The KISSmetrics Javascript code is inserted into templates using a
|
||||
The KISSmetrics JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`kiss_metrics` template tag library and
|
||||
insert the :ttag:`kiss_metrics` tag. Because every page that you want
|
||||
to track must have the tag, it is useful to add it to your base
|
||||
|
|
@ -53,7 +53,7 @@ Setting the API key
|
|||
|
||||
Every website you track events for with KISSmetrics gets its own API
|
||||
key, and the :ttag:`kiss_metrics` tag will include it in the rendered
|
||||
Javascript code. You can find the website API key by visiting the
|
||||
JavaScript code. You can find the website API key by visiting the
|
||||
website *Product center* on your KISSmetrics dashboard. Set
|
||||
:const:`KISS_METRICS_API_KEY` in the project :file:`settings.py` file::
|
||||
|
||||
|
|
@ -108,7 +108,29 @@ Just remember that if you set the same context variable in the
|
|||
:class:`~django.template.context.RequestContext` constructor and in a
|
||||
context processor, the latter clobbers the former.
|
||||
|
||||
.. _kiss-metrics-event:
|
||||
|
||||
.. _kiss-metrics-alias:
|
||||
|
||||
Alias
|
||||
-----
|
||||
|
||||
Alias is used to associate one identity with another.
|
||||
This most likely will occur if a user is not signed in yet,
|
||||
you assign them an anonymous identity and record activity for them
|
||||
and they later sign in and you get a named identity.
|
||||
|
||||
For example::
|
||||
|
||||
context = RequestContext({
|
||||
'kiss_metrics_alias': {'my_registered@email' : 'my_user_id'},
|
||||
})
|
||||
return some_template.render(context)
|
||||
|
||||
The output script tag will then include the corresponding properties as
|
||||
documented in the `KISSmetrics alias API`_ docs.
|
||||
|
||||
.. _`KISSmetrics alias API`: http://support.kissmetrics.com/apis/common-methods#alias
|
||||
|
||||
|
||||
Recording events
|
||||
----------------
|
||||
|
|
@ -122,7 +144,7 @@ For example::
|
|||
})
|
||||
return some_template.render(context)
|
||||
|
||||
The output script tag will then include the corresponding Javascript event as
|
||||
The output script tag will then include the corresponding JavaScript event as
|
||||
documented in the `KISSmetrics record API`_ docs.
|
||||
|
||||
|
||||
|
|
|
|||
75
docs/services/luckyorange.rst
Normal file
75
docs/services/luckyorange.rst
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
==================================================
|
||||
Lucky Orange -- All-in-one conversion optimization
|
||||
==================================================
|
||||
|
||||
`Lucky Orange`_ is a website analytics and user feedback tool.
|
||||
|
||||
.. _`Lucky Orange`: https://www.luckyorange.com/
|
||||
|
||||
|
||||
.. luckyorange-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Lucky Orange integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Lucky Orange template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`luckyorange-configuration`.
|
||||
|
||||
The Lucky Orange tracking code is inserted into templates using template
|
||||
tags. Because every page that you want to track must have the tag, it
|
||||
is useful to add it to your base template. At the top of the template,
|
||||
load the :mod:`luckyorange` template tag library. Then insert the
|
||||
:ttag:`luckyorange` tag at the bottom of the head section::
|
||||
|
||||
{% load luckyorange %}
|
||||
<html>
|
||||
<head>
|
||||
...
|
||||
{% luckyorange %}
|
||||
</head>
|
||||
...
|
||||
</html>
|
||||
|
||||
|
||||
.. _luckyorange-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Lucky Orange integration, you must first set your
|
||||
Site ID.
|
||||
|
||||
|
||||
.. _luckyorange-id:
|
||||
|
||||
Setting the Lucky Orange Site ID
|
||||
--------------------------------
|
||||
|
||||
You can find the Lucky Orange Site ID in the "Settings" of your Lucky
|
||||
Orange account, reachable via the gear icon on the top right corner.
|
||||
Set :const:`LUCKYORANGE_SITE_ID` in the project :file:`settings.py` file::
|
||||
|
||||
LUCKYORANGE_SITE_ID = 'XXXXXX'
|
||||
|
||||
If you do not set a Lucky Orange Site ID, the code will not be rendered.
|
||||
|
||||
|
||||
.. _luckyorange-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`LUCKYORANGE_INTERNAL_IPS`
|
||||
setting, the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
182
docs/services/matomo.rst
Normal file
182
docs/services/matomo.rst
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
====================================================
|
||||
Matomo (formerly Piwik) -- open source web analytics
|
||||
====================================================
|
||||
|
||||
Matomo_ is an open analytics platform currently used by individuals,
|
||||
companies and governments all over the world.
|
||||
|
||||
.. _Matomo: http://matomo.org/
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Matomo integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Matomo template tag to your templates. This
|
||||
step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`matomo-configuration`.
|
||||
|
||||
The Matomo tracking code is inserted into templates using a template
|
||||
tag. Load the :mod:`matomo` template tag library and insert the
|
||||
:ttag:`matomo` tag. Because every page that you want to track must
|
||||
have the tag, it is useful to add it to your base template. Insert
|
||||
the tag at the bottom of the HTML body as recommended by the
|
||||
`Matomo best practice for Integration Plugins`_::
|
||||
|
||||
{% load matomo %}
|
||||
...
|
||||
{% matomo %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
.. _`Matomo best practice for Integration Plugins`: http://matomo.org/integrate/how-to/
|
||||
|
||||
|
||||
|
||||
.. _matomo-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Matomo integration, you must first define
|
||||
domain name and optional URI path to your Matomo server, as well as
|
||||
the Matomo ID of the website you're tracking with your Matomo server,
|
||||
in your project settings.
|
||||
|
||||
|
||||
Setting the domain
|
||||
------------------
|
||||
|
||||
Your Django project needs to know where your Matomo server is located.
|
||||
Typically, you'll have Matomo installed on a subdomain of its own
|
||||
(e.g. ``matomo.example.com``), otherwise it runs in a subdirectory of
|
||||
a website of yours (e.g. ``www.example.com/matomo``). Set
|
||||
:const:`MATOMO_DOMAIN_PATH` in the project :file:`settings.py` file
|
||||
accordingly::
|
||||
|
||||
MATOMO_DOMAIN_PATH = 'matomo.example.com'
|
||||
|
||||
If you do not set a domain the tracking code will not be rendered.
|
||||
|
||||
|
||||
Setting the site ID
|
||||
-------------------
|
||||
|
||||
Your Matomo server can track several websites. Each website has its
|
||||
site ID (this is the ``idSite`` parameter in the query string of your
|
||||
browser's address bar when you visit the Matomo Dashboard). Set
|
||||
:const:`MATOMO_SITE_ID` in the project :file:`settings.py` file to
|
||||
the value corresponding to the website you're tracking::
|
||||
|
||||
MATOMO_SITE_ID = '4'
|
||||
|
||||
If you do not set the site ID the tracking code will not be rendered.
|
||||
|
||||
|
||||
.. _matomo-uservars:
|
||||
|
||||
User variables
|
||||
--------------
|
||||
|
||||
Matomo supports sending `custom variables`_ along with default statistics. If
|
||||
you want to set a custom variable, use the context variable ``matomo_vars`` when
|
||||
you render your template. It should be an iterable of custom variables
|
||||
represented by tuples like: ``(index, name, value[, scope])``, where scope may
|
||||
be ``'page'`` (default) or ``'visit'``. ``index`` should be an integer and the
|
||||
other parameters should be strings. ::
|
||||
|
||||
context = Context({
|
||||
'matomo_vars': [(1, 'foo', 'Sir Lancelot of Camelot'),
|
||||
(2, 'bar', 'To seek the Holy Grail', 'page'),
|
||||
(3, 'spam', 'Blue', 'visit')]
|
||||
})
|
||||
return some_template.render(context)
|
||||
|
||||
Matomo default settings allow up to 5 custom variables for both scope. Variable
|
||||
mapping between index and name must stay constant, or the latest name
|
||||
override the previous one.
|
||||
|
||||
If you use the same user variables in different views and its value can
|
||||
be computed from the HTTP request, you can also set them in a context
|
||||
processor that you add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list
|
||||
in :file:`settings.py`.
|
||||
|
||||
.. _`custom variables`: http://developer.matomo.org/guides/tracking-javascript-guide#custom-variables
|
||||
|
||||
|
||||
.. _matomo-user-tracking:
|
||||
|
||||
User tracking
|
||||
-------------
|
||||
|
||||
If you use the standard Django authentication system, you can allow Matomo to
|
||||
`track individual users`_ by setting the :data:`ANALYTICAL_AUTO_IDENTIFY`
|
||||
setting to :const:`True`. This is enabled by default. Matomo will identify
|
||||
users based on their ``username``.
|
||||
|
||||
If you disable this settings, or want to customize what user id to use, you can
|
||||
set the context variable ``analytical_identity`` (for global configuration) or
|
||||
``matomo_identity`` (for Matomo specific configuration). Setting one to
|
||||
:const:`None` will disable the user tracking feature::
|
||||
|
||||
# Matomo will identify this user as 'BDFL' if ANALYTICAL_AUTO_IDENTIFY is True or unset
|
||||
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
|
||||
|
||||
# Matomo will identify this user as 'Guido van Rossum'
|
||||
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
|
||||
context = Context({
|
||||
'matomo_identity': request.user.get_full_name()
|
||||
})
|
||||
|
||||
# Matomo will not identify this user (but will still collect statistics)
|
||||
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
|
||||
context = Context({
|
||||
'matomo_identity': None
|
||||
})
|
||||
|
||||
.. _`track individual users`: http://developer.matomo.org/guides/tracking-javascript-guide#user-id
|
||||
|
||||
Disabling cookies
|
||||
-----------------
|
||||
|
||||
If you want to `disable cookies`_, set :data:`MATOMO_DISABLE_COOKIES` to
|
||||
:const:`True`. This is disabled by default.
|
||||
|
||||
.. _`disable cookies`: https://matomo.org/faq/general/faq_157/
|
||||
|
||||
Ask for consent
|
||||
---------------
|
||||
|
||||
If you do not want to track visitors without permission, you can `ask for consent`_ first.
|
||||
To enable this, set :data:`MATOMO_ASK_FOR_CONSENT` to :const:`True`.
|
||||
By default, no consent for tracking is needed (i.e. :const:`False`).
|
||||
|
||||
To give and remove consent in your page, create DOM elements with the following classes:
|
||||
|
||||
`matomo_give_consent` - class name for element to click when visitors want to **give** consent
|
||||
`matomo_remove_consent` - class name for element to click when visitors want to **remove** consent
|
||||
|
||||
Examples::
|
||||
|
||||
# button to allow tracking
|
||||
<button class="matomo_give_consent">Track me!</button>
|
||||
|
||||
# button to remove tracking consent
|
||||
<button class="matomo_remove_consent">Don't track me anymore!</button>
|
||||
|
||||
.. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually, you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` (which
|
||||
takes the value of :const:`INTERNAL_IPS` by default) the tracking code
|
||||
is commented out. See :ref:`identifying-visitors` for important
|
||||
information about detecting the visitor IP address.
|
||||
|
|
@ -24,7 +24,7 @@ step is only needed if you are not using the generic
|
|||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`mixpanel-configuration`.
|
||||
|
||||
The Mixpanel Javascript code is inserted into templates using a
|
||||
The Mixpanel JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`mixpanel` template tag library and
|
||||
insert the :ttag:`mixpanel` tag. Because every page that you want
|
||||
to track must have the tag, it is useful to add it to your base
|
||||
|
|
@ -53,7 +53,7 @@ Setting the token
|
|||
-----------------
|
||||
|
||||
Every website you track events for with Mixpanel gets its own token,
|
||||
and the :ttag:`mixpanel` tag will include it in the rendered Javascript
|
||||
and the :ttag:`mixpanel` tag will include it in the rendered JavaScript
|
||||
code. You can find the project token on the Mixpanel *projects* page.
|
||||
Set :const:`MIXPANEL_API_TOKEN` in the project :file:`settings.py`
|
||||
file::
|
||||
|
|
@ -109,23 +109,44 @@ Just remember that if you set the same context variable in the
|
|||
:class:`~django.template.context.RequestContext` constructor and in a
|
||||
context processor, the latter clobbers the former.
|
||||
|
||||
Mixpanel can also receive properties for your identified user, using
|
||||
`mixpanel.people.set`_. If want to send extra properties, just set a
|
||||
dictionary instead of a string in the ``mixpanel_identity`` context
|
||||
variable. The key ``id`` or ``username`` will be used as the user unique
|
||||
id, and any other key-value pair will be passed as *people properties*.
|
||||
For example::
|
||||
|
||||
def identify(request):
|
||||
try:
|
||||
return {
|
||||
'mixpanel_identity': {
|
||||
'id': request.user.id,
|
||||
'last_login': str(request.user.last_login),
|
||||
'date_joined': str(request.user.date_joined),
|
||||
}
|
||||
}
|
||||
except AttributeError:
|
||||
return {}
|
||||
|
||||
|
||||
.. _`mixpanel.people.set`: https://mixpanel.com/help/reference/javascript-full-api-reference#mixpanel.people.set
|
||||
|
||||
|
||||
.. mixpanel-events:
|
||||
|
||||
Tracking events
|
||||
===============
|
||||
|
||||
The django-analytical app integrates the Mixpanel Javascript API in
|
||||
The django-analytical app integrates the Mixpanel JavaScript API in
|
||||
templates. To tracking events in views or other parts of Django, you
|
||||
can use Wes Winham's `django-celery`_ package.
|
||||
can use Wes Winham's `mixpanel-celery`_ package.
|
||||
|
||||
If you want to track an event in Javascript, use the asynchronous
|
||||
If you want to track an event in JavaScript, use the asynchronous
|
||||
notation, as described in the section titled
|
||||
`"Asynchronous Tracking with Javascript"`_ in the Mixpanel
|
||||
`"Asynchronous Tracking with JavaScript"`_ in the Mixpanel
|
||||
documentation. For example::
|
||||
|
||||
mpq.push(["track", "play-game", {"level": "12", "weapon": "sword", "character": "knight"}]);
|
||||
|
||||
.. _django-celery: http://github.com/winhamwr/mixpanel-celery
|
||||
.. _`"Asynchronous Tracking with Javascript"`: http://mixpanel.com/api/docs/guides/integration/js#async
|
||||
mixpanel.track("play-game", {"level": "12", "weapon": "sword", "character": "knight"});
|
||||
|
||||
.. _mixpanel-celery: http://github.com/winhamwr/mixpanel-celery
|
||||
.. _`"Asynchronous Tracking with JavaScript"`: http://mixpanel.com/api/docs/guides/integration/js#async
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ step is only needed if you are not using the generic
|
|||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`olark-configuration`.
|
||||
|
||||
The Olark Javascript code is inserted into templates using a template
|
||||
The Olark JavaScript code is inserted into templates using a template
|
||||
tag. Load the :mod:`olark` template tag library and insert the
|
||||
:ttag:`olark` tag. Because every page that you want to track
|
||||
must have the tag, it is useful to add it to your base template. Insert
|
||||
|
|
@ -52,7 +52,7 @@ Setting the site ID
|
|||
-------------------
|
||||
|
||||
In order to install the chat code, you need to set your Olark site ID.
|
||||
The :ttag:`olark` tag will include it in the rendered Javascript code.
|
||||
The :ttag:`olark` tag will include it in the rendered JavaScript code.
|
||||
You can find the site ID on `installation page`_ of you Olark account.
|
||||
Set :const:`OLARK_SITE_ID` in the project :file:`settings.py` file::
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ Just remember that if you set the same context variable in the
|
|||
:class:`~django.template.context.RequestContext` constructor and in a
|
||||
context processor, the latter clobbers the former.
|
||||
|
||||
See also `api.chat.updateVisitorNickname`_ in the Olark Javascript API
|
||||
See also `api.chat.updateVisitorNickname`_ in the Olark JavaScript API
|
||||
documentation.
|
||||
|
||||
.. _`api.chat.updateVisitorNickname`: http://www.olark.com/documentation/javascript/api.chat.updateVisitorNickname
|
||||
|
|
@ -113,7 +113,7 @@ and the :ttag:`olark` tag will pass them to Olark as status messages::
|
|||
]})
|
||||
return some_template.render(context)
|
||||
|
||||
See also `api.chat.updateVisitorStatus`_ in the Olark Javascript API
|
||||
See also `api.chat.updateVisitorStatus`_ in the Olark JavaScript API
|
||||
documentation.
|
||||
|
||||
.. _`api.chat.updateVisitorStatus`: http://www.olark.com/documentation/javascript/api.chat.updateVisitorStatus
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ This step is only needed if you are not using the generic
|
|||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`optimizely-configuration`.
|
||||
|
||||
The Optimizely Javascript code is inserted into templates using a
|
||||
The Optimizely JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`optimizely` template tag library and
|
||||
insert the :ttag:`optimizely` tag. Because every page that you want to
|
||||
track must have the tag, it is useful to add it to your base template.
|
||||
|
|
@ -53,7 +53,7 @@ Setting the account number
|
|||
--------------------------
|
||||
|
||||
Optimizely gives you a unique account number, and the :ttag:`optimizely`
|
||||
tag will include it in the rendered Javascript code. You can find your
|
||||
tag will include it in the rendered JavaScript code. You can find your
|
||||
account number by clicking the *Implementation* link in the top
|
||||
right-hand corner of the Optimizely website. A pop-up window will
|
||||
appear containing HTML code looking like this::
|
||||
|
|
@ -66,7 +66,7 @@ file::
|
|||
|
||||
OPTIMIZELY_ACCOUNT_NUMBER = 'XXXXXXX'
|
||||
|
||||
If you do not set an account number, the Javascript code will not be
|
||||
If you do not set an account number, the JavaScript code will not be
|
||||
rendered.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ This step is only needed if you are not using the generic
|
|||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`performable-configuration`.
|
||||
|
||||
The Performable Javascript code is inserted into templates using a
|
||||
The Performable JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`performable` template tag library and
|
||||
insert the :ttag:`performable` tag. Because every page that you want to
|
||||
track must have the tag, it is useful to add it to your base template.
|
||||
|
|
@ -53,14 +53,14 @@ Setting the API key
|
|||
-------------------
|
||||
|
||||
You Performable account has its own API key, which :ttag:`performable`
|
||||
tag will include it in the rendered Javascript code. You can find your
|
||||
tag will include it in the rendered JavaScript code. You can find your
|
||||
API key on the *Account Settings* page (click 'Account Settings' in the
|
||||
top right-hand corner of your Performable dashboard). Set
|
||||
:const:`PERFORMABLE_API_KEY` in the project :file:`settings.py` file::
|
||||
|
||||
PERFORMABLE_API_KEY = 'XXXXXX'
|
||||
|
||||
If you do not set an API key, the Javascript code will not be rendered.
|
||||
If you do not set an API key, the JavaScript code will not be rendered.
|
||||
|
||||
|
||||
.. _performable-identity-user:
|
||||
|
|
@ -116,7 +116,7 @@ Embedding a landing page
|
|||
========================
|
||||
|
||||
You can embed a Performable landing page in your Django website. The
|
||||
:ttag:`performable_embed` template tag adds the Javascript code to embed
|
||||
:ttag:`performable_embed` template tag adds the JavaScript code to embed
|
||||
the page. It takes two arguments: the hostname and the page ID::
|
||||
|
||||
{% performable_embed HOSTNAME PAGE_ID %}
|
||||
|
|
|
|||
73
docs/services/rating_mailru.rst
Normal file
73
docs/services/rating_mailru.rst
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
===================================
|
||||
Rating\@Mail.ru -- traffic analysis
|
||||
===================================
|
||||
|
||||
`Rating\@Mail.ru`_ is an analytics tool like as google analytics.
|
||||
|
||||
.. _`Rating\@Mail.ru`: http://top.mail.ru/
|
||||
|
||||
|
||||
.. rating-mailru-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Rating\@Mail.ru integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Rating\@Mail.ru template tag to your templates. This
|
||||
step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`rating-mailru-configuration`.
|
||||
|
||||
The Rating\@Mail.ru counter code is inserted into templates using a template
|
||||
tag. Load the :mod:`rating_mailru` template tag library and insert the
|
||||
:ttag:`rating_mailru` tag. Because every page that you want to track must
|
||||
have the tag, it is useful to add it to your base template. Insert
|
||||
the tag at the bottom of the HTML head::
|
||||
|
||||
{% load rating_mailru %}
|
||||
<html>
|
||||
<head>
|
||||
...
|
||||
{% rating_mailru %}
|
||||
</head>
|
||||
...
|
||||
|
||||
|
||||
.. _rating-mailru-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Rating\@Mail.ru integration, you must first set
|
||||
your website counter ID.
|
||||
|
||||
|
||||
.. _rating-mailru-counter-id:
|
||||
|
||||
Setting the counter ID
|
||||
----------------------
|
||||
|
||||
Every website you track with Rating\@Mail.ru gets its own counter ID,
|
||||
and the :ttag:`rating_mailru` tag will include it in the rendered
|
||||
JavaScript code. You can find the web counter ID on the overview page
|
||||
of your account. Set :const:`RATING_MAILRU_COUNTER_ID` in the
|
||||
project :file:`settings.py` file::
|
||||
|
||||
RATING_MAILRU_COUNTER_ID = '1234567'
|
||||
|
||||
If you do not set a counter ID, the counter code will not be rendered.
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`RATING_MAILRU_INTERNAL_IPS` setting,
|
||||
the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
================================
|
||||
Reinvigorate -- visitor tracking
|
||||
================================
|
||||
|
||||
Reinvigorate_ gives you real-time traffic analysis, visitor activity,
|
||||
search and referrer information and click heatmaps. A system tray /
|
||||
system status bar application for your desktop notifies you when
|
||||
interesting events occur.
|
||||
|
||||
.. _Reinvigorate: http://www.reinvigorate.com/
|
||||
|
||||
|
||||
.. reinvigorate-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Reinvigorate integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Reinvigorate template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`reinvigorate-configuration`.
|
||||
|
||||
The Reinvigorate tracking code is inserted into templates using a
|
||||
template tag. Load the :mod:`reinvigorate` template tag library and
|
||||
insert the :ttag:`reinvigorate` tag. Because every page that you want
|
||||
to track must have the tag, it is useful to add it to your base
|
||||
template. Insert the tag somewhere within the HTML body::
|
||||
|
||||
{% load reinvigorate %}
|
||||
...
|
||||
{% reinvigorate %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
.. _reinvigorate-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Reinvigorate integration, you must first set your
|
||||
tracking ID. You can also customize the data that Reinvigorate tracks.
|
||||
|
||||
|
||||
.. _reinvigorate-tracking-id:
|
||||
|
||||
Setting the tracking ID
|
||||
-----------------------
|
||||
|
||||
Every website you track with Reinvigorate gets a tracking ID, and the
|
||||
:ttag:`reinvigorate` tag will include it in the rendered Javascript
|
||||
code. You can find the tracking ID in the URL of your website report
|
||||
pages. The URL looks like this:
|
||||
|
||||
\https://report.reinvigorate.net/accounts/XXXXX-XXXXXXXXXX/
|
||||
|
||||
Here, ``XXXXX-XXXXXXXXXX`` is the tracking ID. Set
|
||||
:const:`REINVIGORATE_TRACKING_ID` in the project :file:`settings.py`
|
||||
file::
|
||||
|
||||
REINVIGORATE_TRACKING_ID = 'XXXXX-XXXXXXXXXX'
|
||||
|
||||
If you do not set a tracking ID, the tracking code will not be rendered.
|
||||
|
||||
|
||||
.. _reinvigorate-internal-ips:
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`REINVIGORATE_INTERNAL_IPS`
|
||||
setting, the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
|
||||
|
||||
.. _reinvigorate-tags:
|
||||
|
||||
Reinvigorate tags
|
||||
-----------------
|
||||
|
||||
As described in the Reinvigorate *NameTags* and *Snoop* pages,
|
||||
the data that is tracked by Reinvigorate can be customized by adding
|
||||
*tags* to the Javascript tracking code. (These should not be confused
|
||||
with Django template tags.) Using template context variables, you can
|
||||
let the :ttag:`reinvigorate` template tag pass reinvigorate tags to
|
||||
automatically. You can set the context variables in your view when your
|
||||
render a template containing the tracking code::
|
||||
|
||||
context = RequestContext({'reinvigorate_purchase': True,
|
||||
'reinvigorate_comment': 'Got discount'})
|
||||
return some_template.render(context)
|
||||
|
||||
If you have tags that are generated on every page, you may want to set
|
||||
them in a context processor that you add to the
|
||||
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
|
||||
|
||||
def reinvigorate_tags(request):
|
||||
try:
|
||||
return {'name': request.user.username}
|
||||
except AttributeError:
|
||||
return {}
|
||||
|
||||
Just remember that if you set the same context variable in the
|
||||
:class:`~django.template.context.RequestContext` constructor and in a
|
||||
context processor, the latter clobbers the former.
|
||||
|
||||
Here is a table with the most important tags. All tags listed on the
|
||||
Reinvigorate pages can be set by replacing ``re_XXX_tag`` with
|
||||
``reinvigorate_XXX``.
|
||||
|
||||
========================= =============================================
|
||||
Context variable Description
|
||||
========================= =============================================
|
||||
``reinvigorate_name`` The visitor name.
|
||||
------------------------- ---------------------------------------------
|
||||
``reinvigorate_context`` Some context information about the visitor,
|
||||
e.g. an e-mail address.
|
||||
------------------------- ---------------------------------------------
|
||||
``reinvigorate_purchase`` A boolean indicating whether the visitor has
|
||||
just made a purchase. Setting this variable
|
||||
triggers an event in the Snoop notification
|
||||
application.
|
||||
------------------------- ---------------------------------------------
|
||||
``reinvigorate_new_user`` A boolean indicating whether the visitor has
|
||||
just registered as a new user. Setting this
|
||||
variable triggers an event in the Snoop
|
||||
notification application.
|
||||
------------------------- ---------------------------------------------
|
||||
``reinvigorate_comment`` A comment, which is included in a Snoop
|
||||
event notification.
|
||||
========================= =============================================
|
||||
|
||||
|
||||
.. _reinvigorate-identify-user:
|
||||
|
||||
Identifying authenticated users
|
||||
-------------------------------
|
||||
|
||||
If you have not set the ``reinvigorate_name`` context variable
|
||||
explicitly, the full name of an authenticated user is passed to
|
||||
Reinvigorate automatically. Similarly, the e-mail address is passed
|
||||
automatically in the ``context`` tag. See :ref:`identifying-visitors`.
|
||||
|
||||
|
||||
----
|
||||
|
||||
Thanks go to Reinvigorate for their support with the development of this
|
||||
application.
|
||||
|
|
@ -24,7 +24,7 @@ This step is only needed if you are not using the generic
|
|||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`snapengage-configuration`.
|
||||
|
||||
The SnapEngage Javascript code is inserted into templates using a
|
||||
The SnapEngage JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`SnapEngage` template tag library and
|
||||
insert the :ttag:`SnapEngage` tag. Because every page that you want to
|
||||
track must have the tag, it is useful to add it to your base template.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Spring Metrics -- conversion tracking
|
||||
=====================================
|
||||
|
||||
`Spring Metrics`_ is a convesions analysis tool. It shows you the top
|
||||
`Spring Metrics`_ is a conversions analysis tool. It shows you the top
|
||||
converting sources, search keywords and landing pages. The real-time
|
||||
dashboard shows you how customers interact with your website and how
|
||||
to increase conversion.
|
||||
|
|
@ -53,7 +53,7 @@ Setting the Tracking ID
|
|||
|
||||
Every website you track with Spring Metrics gets its own Tracking ID,
|
||||
and the :ttag:`spring_metrics` tag will include it in the rendered
|
||||
Javascript code. You can find the Tracking ID in the `Site Settings`_
|
||||
JavaScript code. You can find the Tracking ID in the `Site Settings`_
|
||||
of your Spring Metrics account. Set :const:`SPRING_METRICS_TRACKING_ID`
|
||||
in the project :file:`settings.py` file::
|
||||
|
||||
|
|
|
|||
218
docs/services/uservoice.rst
Normal file
218
docs/services/uservoice.rst
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
..
|
||||
After updating this file, remember to upload to the UserVoice
|
||||
knowledge base.
|
||||
|
||||
=======================================
|
||||
UserVoice -- user feedback and helpdesk
|
||||
=======================================
|
||||
|
||||
UserVoice_ makes it simple for your customers to give, discuss, and vote
|
||||
for feedback. An unobtrusive feedback tab allows visitors to easily
|
||||
submit and discuss ideas without having to sign up for a new account.
|
||||
The best ideas are delivered to you based on customer votes.
|
||||
|
||||
.. _UserVoice: http://www.uservoice.com/
|
||||
|
||||
|
||||
.. _uservoice-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the UserVoice integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the UserVoice template tag to your templates.
|
||||
This step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`uservoice-configuration`.
|
||||
|
||||
The UserVoice JavaScript code is inserted into templates using a
|
||||
template tag. Load the :mod:`uservoice` template tag library and insert
|
||||
the :ttag:`uservoice` tag. Because every page that you want to have
|
||||
the feedback tab to appear on must have the tag, it is useful to add
|
||||
it to your base template. Insert the tag at the bottom of the HTML
|
||||
body::
|
||||
|
||||
{% load uservoice %}
|
||||
...
|
||||
{% uservoice %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
.. _uservoice-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the UserVoice integration, you must first set the
|
||||
widget key.
|
||||
|
||||
|
||||
Setting the widget key
|
||||
----------------------
|
||||
|
||||
In order to use the feedback widget, you need to configure which widget
|
||||
you want to show. You can find the widget keys in the *Channels* tab on
|
||||
your UserVoice *Settings* page. Under the *JavaScript Widget* heading,
|
||||
find the JavaScript embed code of the widget. The widget key is the
|
||||
alphanumerical string contained in the URL of the script imported by the
|
||||
embed code::
|
||||
|
||||
<script>
|
||||
|
||||
UserVoice=window.UserVoice||[];(function(){
|
||||
var uv=document.createElement('script');uv.type='text/javascript';
|
||||
uv.async=true;uv.src='//widget.uservoice.com/XXXXXXXXXXXXXXXXXXXX.js';
|
||||
var s=document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(uv,s)})();
|
||||
</script>
|
||||
|
||||
(The widget key is shown as ``XXXXXXXXXXXXXXXXXXXX``.)
|
||||
|
||||
The default widget
|
||||
..................
|
||||
|
||||
Often you will use the same widget throughout your website. The default
|
||||
widget key is configured by setting :const:`USERVOICE_WIDGET_KEY` in
|
||||
the project :file:`settings.py` file::
|
||||
|
||||
USERVOICE_WIDGET_KEY = 'XXXXXXXXXXXXXXXXXXXX'
|
||||
|
||||
If the setting is present but empty, no widget is shown by default. This
|
||||
is useful if you want to set a widget using a template context variable,
|
||||
as the setting must be present for the generic :ttag:`analytical.*` tags
|
||||
to work.
|
||||
|
||||
Widget options
|
||||
..............
|
||||
|
||||
You can set :const:`USERVOICE_WIDGET_OPTIONS` to customize your widget
|
||||
with UserVoice's options.
|
||||
|
||||
.. tip::
|
||||
|
||||
See the `JS SDK Overview <https://developer.uservoice.com/docs/widgets/overview/>`_ and the `reference <https://developer.uservoice.com/docs/widgets/options/>`_ for the details of available options.
|
||||
|
||||
For example, to override the default icon style with a tab and on the left,
|
||||
you could define:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
USERVOICE_WIDGET_OPTIONS = {"trigger_position": "left",
|
||||
"trigger_style": "tab"}
|
||||
|
||||
|
||||
|
||||
Per-view widget
|
||||
...............
|
||||
|
||||
The widget configuration can be overriden in a view using
|
||||
``uservoice_widget_options`` template context variable. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
context = RequestContext({'uservoice_widget_options': 'mode': 'satisfaction'})
|
||||
return some_template.render(context)
|
||||
|
||||
It's also possible to set a different widget key for a particular view
|
||||
with ``uservoice_widget_key``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
context = RequestContext({'uservoice_widget_key': 'XXXXXXXXXXXXXXXXXXXX'})
|
||||
return some_template.render(context)
|
||||
|
||||
These variable passed in the context overrides the default
|
||||
widget configuration.
|
||||
|
||||
|
||||
.. _uservoice-link:
|
||||
|
||||
Using a custom link
|
||||
-------------------
|
||||
|
||||
Instead of showing the default feedback icon or tab, you can make the UserVoice
|
||||
widget launch when a visitor clicks a link or when some other event
|
||||
occurs. As the `documentation describe <https://developer.uservoice.com/docs/widgets/methods/#custom-trigger>`_, simply add the ``data-uv-trigger`` HTML attribute to the element. For example::
|
||||
|
||||
<a href="mailto:questions@yoursite.com" data-uv-trigger>Contact us</a>
|
||||
|
||||
|
||||
In order to hidden the default trigger, you should disable it putting
|
||||
``uservoice_add_trigger`` to ``False``::
|
||||
|
||||
context = RequestContext({'uservoice_add_trigger': False})
|
||||
return your_template_with_custom_uservoice_link.render(context)
|
||||
|
||||
If you want to disable the automatic trigger globally, set in :file:`settings.py`::
|
||||
|
||||
USERVOICE_ADD_TRIGGER = False
|
||||
|
||||
|
||||
Setting the widget key in a context processor
|
||||
.............................................
|
||||
|
||||
You can also set the widget keys in a context processor that you add to
|
||||
the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`.
|
||||
For example, to show a specific widget to logged in users::
|
||||
|
||||
def uservoice_widget_key(request):
|
||||
try:
|
||||
if request.user.is_authenticated():
|
||||
return {'uservoice_widget_key': 'XXXXXXXXXXXXXXXXXXXX'}
|
||||
except AttributeError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
The widget key passed in the context variable overrides both the default
|
||||
and the per-view widget key.
|
||||
|
||||
Identifying users
|
||||
-----------------
|
||||
|
||||
If your websites identifies visitors, you can pass this information on
|
||||
to Uservoice. By default, the name and email of an authenticated user
|
||||
is passed to Uservoice automatically. See :ref:`identifying-visitors`.
|
||||
|
||||
You can also send the visitor identity yourself by adding either the
|
||||
``uservoice_identity`` or the ``analytical_identity`` variable to
|
||||
the template context. (If both are set, the former takes precedence.)
|
||||
This should be a dictionary with the desired user traits as its keys.
|
||||
Check the `documentation on identifying users`_ to see valid traits.
|
||||
For example::
|
||||
|
||||
context = RequestContext({'uservoice_identity': {'email': user_email,
|
||||
'name': username }})
|
||||
return some_template.render(context)
|
||||
|
||||
If you can derive the identity from the HTTP request, you can also use
|
||||
a context processor that you add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
|
||||
|
||||
def identify(request):
|
||||
try:
|
||||
return {'uservoice_identity': {
|
||||
email: request.user.username,
|
||||
name: request.user.get_full_name(),
|
||||
id: request.user.id,
|
||||
type: 'vip',
|
||||
account: {
|
||||
name: 'Acme, Co.',
|
||||
monthly_rate: 9.99,
|
||||
ltv: 1495.00,
|
||||
plan: 'Enhanced'
|
||||
}
|
||||
}
|
||||
}
|
||||
except AttributeError:
|
||||
return {}
|
||||
|
||||
.. _`documentation on identifying users`: https://developer.uservoice.com/docs/widgets/identify/
|
||||
|
||||
----
|
||||
|
||||
Thanks go to UserVoice for their support with the development of this
|
||||
application.
|
||||
|
|
@ -37,7 +37,7 @@ the tag at the bottom of the HTML head::
|
|||
</head>
|
||||
...
|
||||
|
||||
Because Javascript code is asynchronous, putting the tag in the head
|
||||
Because JavaScript code is asynchronous, putting the tag in the head
|
||||
section increases the chances that a page view is going to be tracked
|
||||
before the visitor leaves the page. See for details the `Asynchronous
|
||||
JavaScript Developer’s Guide`_ on the Woopra website.
|
||||
|
|
@ -94,6 +94,38 @@ a page reporting. So it’s important to keep the number reasonable in
|
|||
order to accurately make predictions.
|
||||
|
||||
|
||||
Cookie control
|
||||
--------------
|
||||
|
||||
Optional settings that influence the cookie behavior::
|
||||
|
||||
WOOPRA_COOKIE_NAME = "wooTracker"
|
||||
WOOPRA_COOKIE_DOMAIN = ".example.com"
|
||||
WOOPRA_COOKIE_PATH = "/"
|
||||
WOOPRA_COOKIE_EXPIRE = "Fri Jan 01 2027 15:00:00 GMT+0000"
|
||||
|
||||
See the `Woopra documentation`_ for more specific details.
|
||||
|
||||
|
||||
Tracking control
|
||||
----------------
|
||||
|
||||
Optional settings that influence the tracking as such::
|
||||
|
||||
WOOPRA_CLICK_TRACKING = False
|
||||
WOOPRA_DOWNLOAD_TRACKING = False
|
||||
WOOPRA_OUTGOING_TRACKING = False
|
||||
WOOPRA_OUTGOING_IGNORE_SUBDOMAIN = True
|
||||
WOOPRA_IGNORE_QUERY_URL = True
|
||||
WOOPRA_HIDE_CAMPAIGN = False
|
||||
|
||||
See the `Woopra documentation`_ for more specific details.
|
||||
|
||||
|
||||
.. _`Woopra documentation`:
|
||||
https://docs.woopra.com/reference/woopraconfig#configuring-your-tracker
|
||||
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
|
|
|
|||
84
docs/services/yandex_metrica.rst
Normal file
84
docs/services/yandex_metrica.rst
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
==================================
|
||||
Yandex.Metrica -- traffic analysis
|
||||
==================================
|
||||
|
||||
`Yandex.Metrica`_ is an analytics tool like as google analytics.
|
||||
|
||||
.. _`Yandex.Metrica`: http://metrica.yandex.com/
|
||||
|
||||
|
||||
.. yandex-metrica-installation:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
To start using the Yandex.Metrica integration, you must have installed the
|
||||
django-analytical package and have added the ``analytical`` application
|
||||
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
|
||||
See :doc:`../install` for details.
|
||||
|
||||
Next you need to add the Yandex.Metrica template tag to your templates. This
|
||||
step is only needed if you are not using the generic
|
||||
:ttag:`analytical.*` tags. If you are, skip to
|
||||
:ref:`yandex-metrica-configuration`.
|
||||
|
||||
The Yandex.Metrica counter code is inserted into templates using a template
|
||||
tag. Load the :mod:`yandex_metrica` template tag library and insert the
|
||||
:ttag:`yandex_metrica` tag. Because every page that you want to track must
|
||||
have the tag, it is useful to add it to your base template. Insert
|
||||
the tag at the bottom of the HTML head::
|
||||
|
||||
{% load yandex_metrica %}
|
||||
<html>
|
||||
<head>
|
||||
...
|
||||
{% yandex_metrica %}
|
||||
</head>
|
||||
...
|
||||
|
||||
|
||||
.. _yandex-metrica-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Yandex.Metrica integration, you must first set
|
||||
your website counter ID.
|
||||
|
||||
|
||||
.. _yandex-metrica-counter-id:
|
||||
|
||||
Setting the counter ID
|
||||
----------------------
|
||||
|
||||
Every website you track with Yandex.Metrica gets its own counter ID,
|
||||
and the :ttag:`yandex_metrica` tag will include it in the rendered
|
||||
JavaScript code. You can find the web counter ID on the overview page
|
||||
of your account. Set :const:`YANDEX_METRICA_COUNTER_ID` in the
|
||||
project :file:`settings.py` file::
|
||||
|
||||
YANDEX_METRICA_COUNTER_ID = '12345678'
|
||||
|
||||
If you do not set a counter ID, the counter code will not be rendered.
|
||||
|
||||
You can set additional options to tune your counter:
|
||||
|
||||
============================ ============= =============================================
|
||||
Constant Default Value Description
|
||||
============================ ============= =============================================
|
||||
``YANDEX_METRICA_WEBVISOR`` False Webvisor, scroll map, form analysis.
|
||||
``YANDEX_METRICA_TRACKHASH`` False Hash tracking in the browser address bar.
|
||||
``YANDEX_METRICA_NOINDEX`` False Stop automatic page indexing.
|
||||
``YANDEX_METRICA_ECOMMERCE`` False Dispatch ecommerce data to Metrica.
|
||||
============================ ============= =============================================
|
||||
|
||||
Internal IP addresses
|
||||
---------------------
|
||||
|
||||
Usually you do not want to track clicks from your development or
|
||||
internal IP addresses. By default, if the tags detect that the client
|
||||
comes from any address in the :const:`YANDEX_METRICA_INTERNAL_IPS` setting,
|
||||
the tracking code is commented out. It takes the value of
|
||||
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
|
||||
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
|
||||
important information about detecting the visitor IP address.
|
||||
|
|
@ -6,7 +6,6 @@ Here's a full list of all available settings, in alphabetical order, and
|
|||
their default values.
|
||||
|
||||
|
||||
|
||||
.. data:: ANALYTICAL_AUTO_IDENTIFY
|
||||
|
||||
Default: ``True``
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ Setting up basic tracking
|
|||
|
||||
To get started with django-analytical, the package must first be
|
||||
installed. You can download and install the latest stable package from
|
||||
the Python Package Index automatically by using ``easy_install``::
|
||||
the Python Package Index automatically by using ``easy_install``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ easy_install django-analytical
|
||||
|
||||
|
|
@ -37,7 +39,9 @@ For more ways to install django-analytical, see
|
|||
:ref:`installing-the-package`.
|
||||
|
||||
After you install django-analytical, you need to add it to the list of
|
||||
installed applications in the ``settings.py`` file of your project::
|
||||
installed applications in the ``settings.py`` file of your project:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
|
|
@ -46,7 +50,9 @@ installed applications in the ``settings.py`` file of your project::
|
|||
]
|
||||
|
||||
Then you have to add the general-purpose django-analytical template tags
|
||||
to your base template::
|
||||
to your base template:
|
||||
|
||||
.. code-block:: django
|
||||
|
||||
{% load analytical %}
|
||||
<!DOCTYPE ... >
|
||||
|
|
@ -69,7 +75,9 @@ to your base template::
|
|||
|
||||
Finally, you need to configure the Clicky Site ID and the Crazy Egg
|
||||
account number. Add the following to your project :file:`settings.py`
|
||||
file (replacing the ``x``'s with your own codes)::
|
||||
file (replacing the ``x``'s with your own codes):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
CLICKY_SITE_ID = 'xxxxxxxx'
|
||||
CRAZY_EGG_ACCOUNT_NUMBER = 'xxxxxxxx'
|
||||
|
|
@ -110,7 +118,9 @@ protocol version.
|
|||
|
||||
In order to filter on protocol version in Crazy Egg, you need to
|
||||
include the visitor IP protocol version in the Crazy Egg tracking code.
|
||||
The easiest way to do this is by using a context processor::
|
||||
The easiest way to do this is by using a context processor:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def track_ip_proto(request):
|
||||
addr = request.META.get('HTTP_X_FORWARDED_FOR', '')
|
||||
|
|
|
|||
127
pyproject.toml
Normal file
127
pyproject.toml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
[build-system]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["setuptools>=80"]
|
||||
|
||||
[project]
|
||||
name = "django-analytical"
|
||||
dynamic = ["version"]
|
||||
description = "Analytics service integration for Django projects"
|
||||
readme = "README.rst"
|
||||
license = "MIT"
|
||||
license-files = ["LICENSE.txt"]
|
||||
authors = [
|
||||
{name = "Joost Cassee", email = "joost@cassee.net"},
|
||||
{name = "Joshua Krall", email = "joshuakrall@pobox.com"},
|
||||
{name = "Aleck Landgraf", email = "aleck.landgraf@buildingenergy.com"},
|
||||
{name = "Alexandre Pocquet", email = "apocquet@lecko.fr"},
|
||||
{name = "Bateau Knowledge", email = "info@bateauknowledge.nl"},
|
||||
{name = "Bogdan Bodnar", email = "bogdanbodnar@mail.com"},
|
||||
{name = "Brad Pitcher", email = "bradpitcher@gmail.com"},
|
||||
{name = "Corentin Mercier", email = "corentin@mercier.link"},
|
||||
{name = "Craig Bruce", email = "craig@eyesopen.com"},
|
||||
{name = "Daniel Vitiello", email = "ezdismissal@gmail.com"},
|
||||
{name = "David Smith", email = "smithdc@gmail.com"},
|
||||
{name = "Diederik van der Boor", email = "vdboor@edoburu.nl"},
|
||||
{name = "Eric Amador", email = "eric.amador14@gmail.com"},
|
||||
{name = "Eric Davis", email = "eric@davislv.com"},
|
||||
{name = "Eric Wang", email = "gnawrice@gmail.com"},
|
||||
{name = "Erick Massip", email = "ericmassip1@gmail.com"},
|
||||
{name = "Garrett Coakley", email = "garrettc@users.noreply.github.com"},
|
||||
{name = "Garrett Robinson", email = "garrett.f.robinson@gmail.com"},
|
||||
{name = "GreenKahuna", email = "info@greenkahuna.com"},
|
||||
{name = "Hugo Osvaldo Barrera", email = "hugo@barrera.io"},
|
||||
{name = "Ian Ramsay", email = "ianalexr@yahoo.com"},
|
||||
{name = "Iván Raskovsky", email = "raskovsky+git@gmail.com"},
|
||||
{name = "James Paden", email = "james@xemion.com"},
|
||||
{name = "Jannis Leidel", email = "jannis@leidel.info"},
|
||||
{name = "Julien Grenier", email = "julien.grenier42@gmail.com"},
|
||||
{name = "Kevin Olbrich", email = "ko@sv01.de"},
|
||||
{name = "Marc Bourqui", email = "m.bourqui@edsi-tech.com"},
|
||||
{name = "Martey Dodoo", email = "martey@mobolic.com"},
|
||||
{name = "Martín Gaitán", email = "gaitan@gmail.com"},
|
||||
{name = "Matthäus G. Chajdas", email = "dev@anteru.net"},
|
||||
{name = "Max Arnold", email = "arnold.maxim@gmail.com"},
|
||||
{name = "Nikolay Korotkiy", email = "sikmir@gmail.com"},
|
||||
{name = "Paul Oswald", email = "pauloswald@gmail.com"},
|
||||
{name = "Peter Bittner", email = "django@bittner.it"},
|
||||
{name = "Petr Dlouhý", email = "petr.dlouhy@email.cz"},
|
||||
{name = "Philippe O. Wagner", email = "admin@arteria.ch"},
|
||||
{name = "Pi Delport", email = "pjdelport@gmail.com"},
|
||||
{name = "Sandra Mau", email = "sandra.mau@gmail.com"},
|
||||
{name = "Scott Adams", email = "scottadams80@gmail.com"},
|
||||
{name = "Scott Karlin", email = "gitlab@karlin-online.com"},
|
||||
{name = "Sean Wallace", email = "sean@lowpro.ca"},
|
||||
{name = "Sid Mitra", email = "sidmitra.del@gmail.com"},
|
||||
{name = "Simon Ye", email = "sye737@gmail.com"},
|
||||
{name = "Steve Schwarz", email = "steve@agilitynerd.com"},
|
||||
{name = "Steven Skoczen", email = "steven.skoczen@wk.com"},
|
||||
{name = "Tim Gates", email = "tim.gates@iress.com"},
|
||||
{name = "Tinnet Coronam", email = "tinnet@coronam.net"},
|
||||
{name = "Uros Trebec", email = "uros@trebec.org"},
|
||||
{name = "Walter Renner", email = "walter.renner@me.com"},
|
||||
{name = "Julian Haluska", email = "mail@julianhaluska.de"},
|
||||
{name = "Ronard Luna", email = "rlunag@proton.me"},
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Jazzband community", email = "jazzband-bot@users.noreply.github.com"},
|
||||
{name = "Peter Bittner", email = "django@bittner.it"},
|
||||
]
|
||||
keywords=[
|
||||
"django",
|
||||
"analytics",
|
||||
]
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 4.2",
|
||||
"Framework :: Django :: 5.1",
|
||||
"Framework :: Django :: 5.2",
|
||||
"Intended Audience :: Developers",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"django>=4.2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/jazzband/django-analytical"
|
||||
Documentation = "https://django-analytical.readthedocs.io/"
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
skip_covered = true
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["analytical"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--junitxml=tests/unittests-report.xml --color=yes --verbose"
|
||||
DJANGO_SETTINGS_MODULE = "tests.testproject.settings"
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "single"
|
||||
|
||||
[tool.ruff.lint.flake8-quotes]
|
||||
inline-quotes = "single"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"analytical",
|
||||
"analytical.templatetags",
|
||||
]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "analytical.__version__"}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
[build_sphinx]
|
||||
source-dir = docs
|
||||
build-dir = build/docs
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = build/docs/html
|
||||
73
setup.py
73
setup.py
|
|
@ -1,73 +0,0 @@
|
|||
from distutils.core import setup, Command
|
||||
import os
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'analytical.tests.settings'
|
||||
|
||||
cmdclass = {}
|
||||
|
||||
try:
|
||||
from sphinx.setup_command import BuildDoc
|
||||
cmdclass['build_sphinx'] = BuildDoc
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from sphinx_pypi_upload import UploadDoc
|
||||
cmdclass['upload_sphinx'] = UploadDoc
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class TestCommand(Command):
|
||||
description = "run package tests"
|
||||
user_options = []
|
||||
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
from analytical.tests.utils import run_tests
|
||||
run_tests()
|
||||
|
||||
cmdclass['test'] = TestCommand
|
||||
|
||||
|
||||
def read(fname):
|
||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||
|
||||
import analytical
|
||||
|
||||
setup(
|
||||
name = 'django-analytical',
|
||||
version = analytical.__version__,
|
||||
license = analytical.__license__,
|
||||
description = 'Analytics service integration for Django projects',
|
||||
long_description = read('README.rst'),
|
||||
author = analytical.__author__,
|
||||
author_email = analytical.__email__,
|
||||
packages = [
|
||||
'analytical',
|
||||
'analytical.templatetags',
|
||||
'analytical.tests',
|
||||
'analytical.tests.templatetags',
|
||||
],
|
||||
keywords = ['django', 'analytics'],
|
||||
classifiers = [
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
],
|
||||
platforms = ['any'],
|
||||
url = 'http://github.com/jcassee/django-analytical',
|
||||
download_url = 'http://github.com/jcassee/django-analytical/archives/master',
|
||||
cmdclass = cmdclass,
|
||||
)
|
||||
33
tests/testproject/settings.py
Normal file
33
tests/testproject/settings.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""
|
||||
django-analytical testing settings.
|
||||
"""
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.sites',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.auth',
|
||||
'analytical',
|
||||
]
|
||||
|
||||
SECRET_KEY = 'testing'
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
)
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
},
|
||||
]
|
||||
|
||||
USE_TZ = False
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue