Compare commits
63 Commits
Author | SHA1 | Date |
---|---|---|
Sergey | 24d9a7ff62 | |
Fuhu Xia | 0c6df3f737 | |
Sergey | 3402d747e7 | |
Nicholas Kumia | 9306edb3b2 | |
Nicholas Kumia | 776a0260cc | |
Sergey | 77f8567e21 | |
Jari Voutilainen | 5931ef08ff | |
Sergey Motornyuk | 84c1cf157e | |
Sergey Motornyuk | 78765390f6 | |
Sergey Motornyuk | 2cee663cf3 | |
Sergey Motornyuk | acb7a81bc4 | |
Sergey Motornyuk | 5268985575 | |
Sergey Motornyuk | 2a74be2e27 | |
Sergey Motornyuk | b6b3f91936 | |
Sergey Motornyuk | cf75e577cb | |
Sergey Motornyuk | 913908fc61 | |
Sergey Motornyuk | ef084a2a60 | |
Sergey Motornyuk | 2a01b9e354 | |
Sergey Motornyuk | 305371c794 | |
Sergey Motornyuk | 56415355aa | |
Sergey Motornyuk | 924bc71a33 | |
Sergey Motornyuk | e633655086 | |
Sergey Motornyuk | 486bb9fa1c | |
Sergey Motornyuk | d085a7d5c2 | |
Sergey Motornyuk | 9f1310af20 | |
Sergey Motornyuk | 0055c3e063 | |
Sergey Motornyuk | 8c6c173583 | |
Sergey Motornyuk | 48fb119c71 | |
Sergey Motornyuk | 11968341d4 | |
Sergey Motornyuk | 0748249e1f | |
Sergey Motornyuk | 0257299b5a | |
Sergey Motornyuk | 12e08b8cd8 | |
Sergey | c2c7b1ce9b | |
pdelboca | 7ba025c490 | |
pdelboca | 4455f9ab89 | |
pdelboca | 8a135677d4 | |
pdelboca | 6882708017 | |
pdelboca | dc5c5c5f2f | |
pdelboca | c4dd52fc56 | |
pdelboca | 2bc1d0e0dd | |
pdelboca | 1813413b33 | |
pdelboca | df5a7d198f | |
pdelboca | 259d9768e2 | |
pdelboca | 1d41a85079 | |
pdelboca | 4e57fbaef2 | |
pdelboca | 589890ef74 | |
pdelboca | 97448b4887 | |
pdelboca | 483d4d20ce | |
pdelboca | f63b879b18 | |
pdelboca | e223d3c71e | |
pdelboca | bd0678b2d5 | |
pdelboca | f678a70842 | |
pdelboca | 6d55c38b33 | |
Sergey Motornyuk | 34171b12a1 | |
Sergey Motornyuk | ae59b35121 | |
Sergey Motornyuk | 885a05fea4 | |
Sergey Motornyuk | 67b599ce1a | |
Sergey Motornyuk | 86ec2858a5 | |
Sergey Motornyuk | 1d48695a93 | |
Sergey Motornyuk | dc75b4c472 | |
Sergey Motornyuk | 7f5c2c03d9 | |
Sergey Motornyuk | b70b26f392 | |
Sergey Motornyuk | 4991ca7440 |
|
@ -0,0 +1,75 @@
|
|||
name: Tests
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install requirements
|
||||
run: pip install flake8 pycodestyle
|
||||
- name: Check syntax
|
||||
run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics --exclude ckan
|
||||
- name: Run flake8
|
||||
run: flake8 . --count --max-line-length=127 --statistics --exclude ckan
|
||||
|
||||
test:
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
ckan-version: ["2.10", 2.9, 2.9-py2, 2.8]
|
||||
fail-fast: false
|
||||
|
||||
name: CKAN ${{ matrix.ckan-version }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: openknowledge/ckan-dev:${{ matrix.ckan-version }}
|
||||
services:
|
||||
solr:
|
||||
image: ckan/ckan-solr:${{ matrix.ckan-version }}
|
||||
postgres:
|
||||
image: ckan/ckan-postgres-dev:${{ matrix.ckan-version }}
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
redis:
|
||||
image: redis:3
|
||||
env:
|
||||
CKAN_SQLALCHEMY_URL: postgresql://ckan_default:pass@postgres/ckan_test
|
||||
CKAN_DATASTORE_WRITE_URL: postgresql://datastore_write:pass@postgres/datastore_test
|
||||
CKAN_DATASTORE_READ_URL: postgresql://datastore_read:pass@postgres/datastore_test
|
||||
CKAN_SOLR_URL: http://solr:8983/solr/ckan
|
||||
CKAN_REDIS_URL: redis://redis:6379/1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install requirements
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
# Replace default path to CKAN core config file with the one on the container
|
||||
sed -i -e 's/use = config:.*/use = config:\/srv\/app\/src\/ckan\/test-core.ini/' test.ini
|
||||
- name: Setup extension (CKAN 2.10)
|
||||
if: ${{ matrix.ckan-version == '2.10' }}
|
||||
run: |
|
||||
pip install -r dev-requirements.txt
|
||||
ckan -c test.ini db init
|
||||
ckan -c test.ini db upgrade -p googleanalytics
|
||||
- name: Setup extension (CKAN == 2.9)
|
||||
if: ${{ matrix.ckan-version == '2.9' || matrix.ckan-version == '2.9-py2' }}
|
||||
run: |
|
||||
pip install -r dev-requirements-2.9.txt
|
||||
ckan -c test.ini db init
|
||||
ckan -c test.ini db upgrade -p googleanalytics
|
||||
- name: Setup extension (CKAN < 2.9)
|
||||
if: ${{ matrix.ckan-version == '2.8' }}
|
||||
run: |
|
||||
pip install -r dev-requirements-2.9.txt
|
||||
paster --plugin=ckan db init -c test.ini
|
||||
paster --plugin=ckanext-googleanalytics initdb -c test.ini
|
||||
- name: Run tests
|
||||
run: pytest --ckan-ini=test.ini --cov=ckanext.googleanalytics --disable-warnings ckanext/googleanalytics/tests
|
|
@ -6,5 +6,6 @@ syntax: glob
|
|||
*~
|
||||
build/
|
||||
dist/
|
||||
.mypy_cache/
|
||||
credentials.json
|
||||
token.dat
|
||||
token.dat
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [2.3.0](https://github.com/ckan/ckanext-googleanalytics/compare/v2.2.3...v2.3.0) (2023-06-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add GTM tracking support ([5268985](https://github.com/ckan/ckanext-googleanalytics/commit/52689855758ad1368bd8ae8a785443761ba0abe4))
|
||||
* track downloads via MeasurementProtocol ([acb7a81](https://github.com/ckan/ckanext-googleanalytics/commit/acb7a81bc43f7c0eef94f78a2a605dfb8a8db20b))
|
||||
|
||||
### [2.2.3](https://github.com/ckan/ckanext-googleanalytics/compare/v2.2.2...v2.2.3) (2023-06-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add blocks to gtag snippet ([913908f](https://github.com/ckan/ckanext-googleanalytics/commit/913908fc611f59be80322238953edcc84e3b0f06))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* report empty googleanalytics.id ([cf75e57](https://github.com/ckan/ckanext-googleanalytics/commit/cf75e577cb70fc9c5b0be270c07fb68376646c46))
|
||||
|
||||
### [2.2.2](https://github.com/ckan/ckanext-googleanalytics/compare/v2.2.1...v2.2.2) (2023-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add config declaration to manifest ([2a01b9e](https://github.com/ckan/ckanext-googleanalytics/commit/2a01b9e35460bf15e3b812efc49af6e2712c88b5))
|
||||
|
||||
### [2.2.1](https://github.com/ckan/ckanext-googleanalytics/compare/v2.2.0...v2.2.1) (2023-01-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adapt event_tracking.js to gtag` ([924bc71](https://github.com/ckan/ckanext-googleanalytics/commit/924bc71a33dce4e846df9ce77f1ba1036f9dc788))
|
||||
|
||||
## [2.2.0](https://github.com/ckan/ckanext-googleanalytics/compare/v2.1.1...v2.2.0) (2022-12-03)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* IGoogleAnalytics interface
|
||||
|
||||
### Features
|
||||
|
||||
* IGoogleAnalytics interface ([9f1310a](https://github.com/ckan/ckanext-googleanalytics/commit/9f1310af20b9dd0bf8eab43021bda89a0a2f7705))
|
|
@ -1,4 +1,4 @@
|
|||
include README.rst
|
||||
include LICENSE.txt
|
||||
include requirements.txt
|
||||
recursive-include ckanext/googleanalytics *.html *.js *.json *.css *.yml *.mo
|
||||
recursive-include ckanext/googleanalytics *.html *.js *.json *.css *.yml *.mo *.yaml
|
||||
|
|
23
README.md
23
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Status:** Production
|
||||
|
||||
**CKAN Version:** >= 2.7
|
||||
**CKAN Version:** >= 2.8
|
||||
|
||||
A CKAN extension that both sends tracking data to Google Analytics and
|
||||
retrieves statistics from Google Analytics and inserts them into CKAN pages.
|
||||
|
@ -57,7 +57,6 @@ retrieves statistics from Google Analytics and inserts them into CKAN pages.
|
|||
|
||||
googleanalytics_resource_prefix = /downloads/
|
||||
googleanalytics.domain = auto
|
||||
googleanalytics.track_events = false
|
||||
googleanalytics.fields = {}
|
||||
googleanalytics.enable_user_id = false
|
||||
googleanalytics.download_handler = ckan.views.resource:download
|
||||
|
@ -76,11 +75,6 @@ retrieves statistics from Google Analytics and inserts them into CKAN pages.
|
|||
<http://code.google.com/apis/analytics/docs/gaJS/gaJSApiDomainDirectory.html#_gat.GA_Tracker_._setDomainName>`_
|
||||
for more info.
|
||||
|
||||
If ``track_events`` is set, Google Analytics event tracking will be
|
||||
enabled. *CKAN 1.x only.* *Note that event tracking for resource downloads
|
||||
is always enabled,* ``track_events`` *enables event tracking for other
|
||||
pages as well.*
|
||||
|
||||
``fields`` allows you to specify various options when creating the tracker. See `Google's documentation <https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference>`.
|
||||
|
||||
If ``enable_user_id`` is set to ``true``, then logged in users will be tracked into the Google Analytics' dashboard.
|
||||
|
@ -94,7 +88,7 @@ retrieves statistics from Google Analytics and inserts them into CKAN pages.
|
|||
function must be called instead of `ckan.views.resource:download`
|
||||
via `ckanext.googleanalytics.download_handler` config variable. For ckanext-cloudstorage you can use:
|
||||
|
||||
ckanext.googleanalytics.download_handler = ckanext.cloudstorage.views:download
|
||||
googleanalytics.download_handler = ckanext.cloudstorage.views:download
|
||||
|
||||
# Domain Linking
|
||||
|
||||
|
@ -112,14 +106,7 @@ See `Googles' documentation<https://support.google.com/analytics/answer/1034342?
|
|||
set up the required database tables (of course, altering the
|
||||
``--config`` option to point to your site config file):
|
||||
|
||||
paster initdb --config=../ckan/development.ini
|
||||
|
||||
2. Optionally, add::
|
||||
|
||||
googleanalytics.show_downloads = true
|
||||
|
||||
to your CKAN ini file. If ``show_downloads`` is set, a download count for
|
||||
resources will be displayed on individual package pages.
|
||||
ckan -c ../ckan/ckan.ini initdb
|
||||
|
||||
3. Follow the steps in the *Authorization* section below.
|
||||
|
||||
|
@ -173,9 +160,9 @@ Before ckanext-googleanalytics can retrieve statistics from Google Analytics, yo
|
|||
|
||||
There are some very high-level functional tests that you can run using::
|
||||
|
||||
(pyenv)~/pyenv/src/ckan$ nosetests --ckan ../ckanext-googleanalytics/tests/
|
||||
$ pip install -r dev-requirements.txt
|
||||
$ pytest --ckan-ini=test.ini
|
||||
|
||||
(note -- that's run from the CKAN software root, not the extension root)
|
||||
|
||||
## Future
|
||||
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
// Add Google Analytics Event Tracking to resource download links.
|
||||
ckan.module("google-analytics", function(jQuery, _) {
|
||||
"use strict";
|
||||
return {
|
||||
options: {
|
||||
googleanalytics_resource_prefix: ""
|
||||
},
|
||||
initialize: function() {
|
||||
jQuery("a.resource-url-analytics").on("click", function() {
|
||||
var resource_url = encodeURIComponent(jQuery(this).prop("href"));
|
||||
if (resource_url) {
|
||||
ga("send", "event", "Resource", "Download", resource_url);
|
||||
"use strict";
|
||||
|
||||
return {
|
||||
options: {
|
||||
googleanalytics_resource_prefix: ""
|
||||
},
|
||||
initialize: function() {
|
||||
jQuery("a.resource-url-analytics").on("click", function() {
|
||||
|
||||
var resource_url = encodeURIComponent(jQuery(this).prop("href"));
|
||||
if (resource_url) {
|
||||
if (typeof ga === "undefined") {
|
||||
gtag('event', "Download", {
|
||||
'event_category': "Resource",
|
||||
'event_label': resource_url,
|
||||
});
|
||||
} else {
|
||||
ga("send", "event", "Resource", "Download", resource_url);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
|
|
|
@ -7,15 +7,12 @@ import re
|
|||
import logging
|
||||
import click
|
||||
import ckan.model as model
|
||||
|
||||
from . import dbutil
|
||||
|
||||
import ckan.plugins.toolkit as tk
|
||||
|
||||
from . import dbutil, config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
PACKAGE_URL = "/dataset/" # XXX get from routes...
|
||||
DEFAULT_RESOURCE_URL_TAG = "/downloads/"
|
||||
DEFAULT_RECENT_VIEW_DAYS = 14
|
||||
|
||||
RESOURCE_URL_REGEX = re.compile("/dataset/[a-z0-9-_]+/resource/([a-z0-9-_]+)")
|
||||
DATASET_EDIT_REGEX = re.compile("/dataset/edit/([a-z0-9-_]+)")
|
||||
|
@ -53,33 +50,21 @@ def load(credentials, start_date):
|
|||
except TypeError as e:
|
||||
raise Exception("Unable to create a service: {0}".format(e))
|
||||
profile_id = get_profile_id(service)
|
||||
|
||||
if not profile_id:
|
||||
tk.error_shout("Unknown Profile ID. `googleanalytics.profile_id` or `googleanalytics.account` must be specified")
|
||||
raise click.Abort()
|
||||
if start_date:
|
||||
bulk_import(service, profile_id, start_date)
|
||||
else:
|
||||
query = "ga:pagePath=~%s,ga:pagePath=~%s" % (
|
||||
PACKAGE_URL,
|
||||
_resource_url_tag(),
|
||||
config.prefix(),
|
||||
)
|
||||
packages_data = get_ga_data(service, profile_id, query_filter=query)
|
||||
save_ga_data(packages_data)
|
||||
log.info("Saved %s records from google" % len(packages_data))
|
||||
|
||||
|
||||
def _resource_url_tag():
|
||||
return tk.config.get(
|
||||
"googleanalytics_resource_prefix", DEFAULT_RESOURCE_URL_TAG
|
||||
)
|
||||
|
||||
|
||||
def _recent_view_days():
|
||||
return tk.asint(
|
||||
tk.config.get(
|
||||
"googleanalytics.recent_view_days", DEFAULT_RECENT_VIEW_DAYS
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# xxx #
|
||||
###############################################################################
|
||||
|
@ -121,7 +106,8 @@ def internal_save(packages_data, summary_date):
|
|||
SET package_id = COALESCE(
|
||||
(SELECT id FROM package p WHERE t.url = %s || p.name)
|
||||
,'~~not~found~~')
|
||||
WHERE t.package_id = '~~not~found~~' AND tracking_type = 'page';"""
|
||||
WHERE t.package_id = '~~not~found~~'
|
||||
AND tracking_type = 'page';"""
|
||||
engine.execute(sql, "%sedit/" % PACKAGE_URL)
|
||||
|
||||
# update summary totals for resources
|
||||
|
@ -136,10 +122,11 @@ def internal_save(packages_data, summary_date):
|
|||
SELECT sum(count)
|
||||
FROM tracking_summary t2
|
||||
WHERE t1.url = t2.url
|
||||
AND t2.tracking_date <= t1.tracking_date AND t2.tracking_date >= t1.tracking_date - %s
|
||||
AND t2.tracking_date <= t1.tracking_date
|
||||
AND t2.tracking_date >= t1.tracking_date - %s
|
||||
) + t1.count
|
||||
WHERE t1.running_total = 0 AND tracking_type = 'resource';"""
|
||||
engine.execute(sql, _recent_view_days())
|
||||
engine.execute(sql, config.recent_view_days())
|
||||
|
||||
# update summary totals for pages
|
||||
sql = """UPDATE tracking_summary t1
|
||||
|
@ -153,12 +140,13 @@ def internal_save(packages_data, summary_date):
|
|||
SELECT sum(count)
|
||||
FROM tracking_summary t2
|
||||
WHERE t1.package_id = t2.package_id
|
||||
AND t2.tracking_date <= t1.tracking_date AND t2.tracking_date >= t1.tracking_date - %s
|
||||
AND t2.tracking_date <= t1.tracking_date
|
||||
AND t2.tracking_date >= t1.tracking_date - %s
|
||||
) + t1.count
|
||||
WHERE t1.running_total = 0 AND tracking_type = 'page'
|
||||
AND t1.package_id IS NOT NULL
|
||||
AND t1.package_id != '~~not~found~~';"""
|
||||
engine.execute(sql, _recent_view_days())
|
||||
engine.execute(sql, config.recent_view_days())
|
||||
|
||||
|
||||
def bulk_import(service, profile_id, start_date=None):
|
||||
|
@ -209,7 +197,7 @@ def get_ga_data_new(service, profile_id, start_date=None, end_date=None):
|
|||
packages = {}
|
||||
query = "ga:pagePath=~%s,ga:pagePath=~%s" % (
|
||||
PACKAGE_URL,
|
||||
_resource_url_tag(),
|
||||
config.prefix(),
|
||||
)
|
||||
metrics = "ga:uniquePageviews"
|
||||
sort = "-ga:uniquePageviews"
|
||||
|
@ -259,7 +247,7 @@ def save_ga_data(packages_data):
|
|||
ever = visits.get("ever", 0)
|
||||
matches = RESOURCE_URL_REGEX.match(identifier)
|
||||
if matches:
|
||||
resource_url = identifier[len(_resource_url_tag()) :]
|
||||
resource_url = identifier[len(config.prefix()):]
|
||||
resource = (
|
||||
model.Session.query(model.Resource)
|
||||
.autoflush(True)
|
||||
|
@ -272,7 +260,7 @@ def save_ga_data(packages_data):
|
|||
dbutil.update_resource_visits(resource.id, recently, ever)
|
||||
log.info("Updated %s with %s visits" % (resource.id, visits))
|
||||
else:
|
||||
package_name = identifier[len(PACKAGE_URL) :]
|
||||
package_name = identifier[len(PACKAGE_URL):]
|
||||
if "/" in package_name:
|
||||
log.warning("%s not a valid package name" % package_name)
|
||||
continue
|
||||
|
@ -331,7 +319,7 @@ def get_ga_data(service, profile_id, query_filter):
|
|||
{'identifier': {'recent':3, 'ever':6}}
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
recent_date = now - datetime.timedelta(_recent_view_days())
|
||||
recent_date = now - datetime.timedelta(config.recent_view_days())
|
||||
recent_date = recent_date.strftime("%Y-%m-%d")
|
||||
floor_date = datetime.date(2005, 1, 1)
|
||||
packages = {}
|
||||
|
@ -353,8 +341,8 @@ def get_ga_data(service, profile_id, query_filter):
|
|||
package = "/" + "/".join(package.split("/")[2:])
|
||||
|
||||
count = result[1]
|
||||
# Make sure we add the different representations of the same
|
||||
# dataset /mysite.com & /www.mysite.com ...
|
||||
# Make sure we add the different representations of the
|
||||
# same dataset /mysite.com & /www.mysite.com ...
|
||||
val = 0
|
||||
if package in packages and date_name in packages[package]:
|
||||
val += packages[package][date_name]
|
||||
|
|
|
@ -9,14 +9,11 @@ import time
|
|||
from pylons import config as pylonsconfig
|
||||
from ckan.lib.cli import CkanCommand
|
||||
import ckan.model as model
|
||||
from ckan.plugins.toolkit import asint
|
||||
|
||||
from . import dbutil
|
||||
from . import dbutil, config
|
||||
|
||||
log = logging.getLogger("ckanext.googleanalytics")
|
||||
PACKAGE_URL = "/dataset/" # XXX get from routes...
|
||||
DEFAULT_RESOURCE_URL_TAG = "/downloads/"
|
||||
DEFAULT_RECENT_VIEW_DAYS = 14
|
||||
|
||||
RESOURCE_URL_REGEX = re.compile("/dataset/[a-z0-9-_]+/resource/([a-z0-9-_]+)")
|
||||
DATASET_EDIT_REGEX = re.compile("/dataset/edit/([a-z0-9-_]+)")
|
||||
|
@ -61,14 +58,8 @@ class LoadAnalytics(CkanCommand):
|
|||
self._load_config()
|
||||
self.CONFIG = pylonsconfig
|
||||
|
||||
self.resource_url_tag = self.CONFIG.get(
|
||||
"googleanalytics_resource_prefix", DEFAULT_RESOURCE_URL_TAG
|
||||
)
|
||||
self.recent_view_days = asint(
|
||||
self.CONFIG.get(
|
||||
"googleanalytics.recent_view_days", DEFAULT_RECENT_VIEW_DAYS
|
||||
)
|
||||
)
|
||||
self.resource_url_tag = config.prefix()
|
||||
self.recent_view_days = config.recent_view_days()
|
||||
|
||||
# funny dance we need to do to make sure we've got a
|
||||
# configured session
|
||||
|
@ -112,7 +103,8 @@ class LoadAnalytics(CkanCommand):
|
|||
SET package_id = COALESCE(
|
||||
(SELECT id FROM package p WHERE t.url = %s || p.name)
|
||||
,'~~not~found~~')
|
||||
WHERE t.package_id = '~~not~found~~' AND tracking_type = 'page';"""
|
||||
WHERE t.package_id = '~~not~found~~'
|
||||
AND tracking_type = 'page';"""
|
||||
engine.execute(sql, "%sedit/" % PACKAGE_URL)
|
||||
|
||||
# update summary totals for resources
|
||||
|
@ -127,7 +119,8 @@ class LoadAnalytics(CkanCommand):
|
|||
SELECT sum(count)
|
||||
FROM tracking_summary t2
|
||||
WHERE t1.url = t2.url
|
||||
AND t2.tracking_date <= t1.tracking_date AND t2.tracking_date >= t1.tracking_date - %s
|
||||
AND t2.tracking_date <= t1.tracking_date
|
||||
AND t2.tracking_date >= t1.tracking_date - %s
|
||||
) + t1.count
|
||||
WHERE t1.running_total = 0 AND tracking_type = 'resource';"""
|
||||
engine.execute(sql, self.recent_view_days)
|
||||
|
@ -144,7 +137,8 @@ class LoadAnalytics(CkanCommand):
|
|||
SELECT sum(count)
|
||||
FROM tracking_summary t2
|
||||
WHERE t1.package_id = t2.package_id
|
||||
AND t2.tracking_date <= t1.tracking_date AND t2.tracking_date >= t1.tracking_date - %s
|
||||
AND t2.tracking_date <= t1.tracking_date
|
||||
AND t2.tracking_date >= t1.tracking_date - %s
|
||||
) + t1.count
|
||||
WHERE t1.running_total = 0 AND tracking_type = 'page'
|
||||
AND t1.package_id IS NOT NULL
|
||||
|
@ -274,7 +268,7 @@ class LoadAnalytics(CkanCommand):
|
|||
ever = visits.get("ever", 0)
|
||||
matches = RESOURCE_URL_REGEX.match(identifier)
|
||||
if matches:
|
||||
resource_url = identifier[len(self.resource_url_tag) :]
|
||||
resource_url = identifier[len(self.resource_url_tag):]
|
||||
resource = (
|
||||
model.Session.query(model.Resource)
|
||||
.autoflush(True)
|
||||
|
@ -287,7 +281,7 @@ class LoadAnalytics(CkanCommand):
|
|||
dbutil.update_resource_visits(resource.id, recently, ever)
|
||||
log.info("Updated %s with %s visits" % (resource.id, visits))
|
||||
else:
|
||||
package_name = identifier[len(PACKAGE_URL) :]
|
||||
package_name = identifier[len(PACKAGE_URL):]
|
||||
if "/" in package_name:
|
||||
log.warning("%s not a valid package name" % package_name)
|
||||
continue
|
||||
|
@ -369,8 +363,8 @@ class LoadAnalytics(CkanCommand):
|
|||
package = "/" + "/".join(package.split("/")[2:])
|
||||
|
||||
count = result[1]
|
||||
# Make sure we add the different representations of the same
|
||||
# dataset /mysite.com & /www.mysite.com ...
|
||||
# Make sure we add the different representations of the
|
||||
# same dataset /mysite.com & /www.mysite.com ...
|
||||
val = 0
|
||||
if (
|
||||
package in packages
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import ast
|
||||
import logging
|
||||
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
import ckan.plugins.toolkit as tk
|
||||
|
||||
CONFIG_TRACKING_ID = "googleanalytics.id"
|
||||
CONFIG_HANDLER_PATH = "googleanalytics.download_handler"
|
||||
CONFIG_TRACKING_MODE = "googleanalytics.tracking_mode"
|
||||
|
||||
DEFAULT_RESOURCE_URL_TAG = "/downloads/"
|
||||
DEFAULT_RECENT_VIEW_DAYS = 14
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def tracking_id():
|
||||
# type: () -> str
|
||||
return tk.config["googleanalytics.id"]
|
||||
|
||||
|
||||
def download_handler():
|
||||
handler_path = tk.config.get(CONFIG_HANDLER_PATH)
|
||||
if handler_path:
|
||||
handler = import_string(handler_path, silent=True)
|
||||
else:
|
||||
handler = None
|
||||
log.warning(("Missing {} config option.").format(CONFIG_HANDLER_PATH))
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def tracking_mode():
|
||||
# type: () -> Literal["ga", "gtag", "gtm"]
|
||||
type_ = tk.config.get(CONFIG_TRACKING_MODE)
|
||||
if type_:
|
||||
return type_
|
||||
|
||||
id_ = tracking_id()
|
||||
|
||||
if id_.startswith("UA-"):
|
||||
return "ga"
|
||||
|
||||
if id_.startswith("G-"):
|
||||
return "gtag"
|
||||
|
||||
if id_.startswith("GTM-"):
|
||||
return "gtm"
|
||||
|
||||
return "ga"
|
||||
|
||||
|
||||
def measurement_id():
|
||||
# type: () -> str
|
||||
"""Set the MeasurementID for tracking API actions. By default,
|
||||
`googleanalytics.id` is used.
|
||||
|
||||
Use this option during migration from Universal Analytics, to track API
|
||||
requests using Measurement Protocol, while tracking browser event using
|
||||
Universal Analytics.
|
||||
|
||||
Requires `googleanalytics.measurement_protocol.client_id`.
|
||||
"""
|
||||
return tk.config.get("googleanalytics.measurement_protocol.id") or tracking_id()
|
||||
|
||||
|
||||
def measurement_protocol_client_id():
|
||||
# type: () -> str | None
|
||||
return tk.config.get("googleanalytics.measurement_protocol.client_id")
|
||||
|
||||
|
||||
def measurement_protocol_client_secret():
|
||||
return tk.config.get("googleanalytics.measurement_protocol.client_secret")
|
||||
|
||||
|
||||
def measurement_protocol_track_downloads():
|
||||
# type: () -> bool
|
||||
return tk.asbool(
|
||||
tk.config.get("googleanalytics.measurement_protocol.track_downloads")
|
||||
)
|
||||
|
||||
|
||||
def account():
|
||||
return tk.config.get("googleanalytics.account")
|
||||
|
||||
|
||||
def profile_id():
|
||||
return tk.config.get("googleanalytics.profile_id")
|
||||
|
||||
|
||||
def credentials():
|
||||
return tk.config.get("googleanalytics.credentials.path")
|
||||
|
||||
|
||||
def domain():
|
||||
return tk.config.get("googleanalytics.domain", "auto")
|
||||
|
||||
|
||||
def fields():
|
||||
fields = ast.literal_eval(tk.config.get("googleanalytics.fields", "{}"))
|
||||
|
||||
if linked_domains():
|
||||
fields["allowLinker"] = "true"
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def linked_domains():
|
||||
googleanalytics_linked_domains = tk.config.get(
|
||||
"googleanalytics.linked_domains", ""
|
||||
)
|
||||
return [x.strip() for x in googleanalytics_linked_domains.split(",") if x]
|
||||
|
||||
|
||||
def enable_user_id():
|
||||
return tk.asbool(tk.config.get("googleanalytics.enable_user_id", False))
|
||||
|
||||
|
||||
def prefix():
|
||||
return tk.config.get(
|
||||
"googleanalytics_resource_prefix", DEFAULT_RESOURCE_URL_TAG
|
||||
)
|
||||
|
||||
|
||||
def recent_view_days():
|
||||
return tk.asint(
|
||||
tk.config.get(
|
||||
"googleanalytics.recent_view_days", DEFAULT_RECENT_VIEW_DAYS
|
||||
)
|
||||
)
|
|
@ -0,0 +1,58 @@
|
|||
version: 1
|
||||
groups:
|
||||
- annotation: GoogleAnalytics settings
|
||||
options:
|
||||
- key: googleanalytics.id
|
||||
required: true
|
||||
placeholder: UA-000000000-1
|
||||
validators: not_empty
|
||||
description: |
|
||||
Google tag ID(`G-*`) for Google Analytics 4, the Tracking ID(`UA-*`)
|
||||
for Universal Analytics, or container ID(`GTM-*`) for Google Tag
|
||||
Manager.
|
||||
|
||||
- key: googleanalytics.download_handler
|
||||
default: ckan.views.resource:download
|
||||
description: |
|
||||
Import path of the CKAN view that handles resource downloads. It can
|
||||
be used in combination with plugins that replace download
|
||||
view(`ckanext-cloudstorage`, `ckanext-s3filestore`).
|
||||
|
||||
- key: googleanalytics.tracking_mode
|
||||
experimental: true
|
||||
example: gtag
|
||||
description: |
|
||||
Defines the type of code snippet embedded into the page. Can be set
|
||||
to `ga`(enables `googleanalytics.js`), `gtag`(enables `gtag.js`), or
|
||||
`gtm`(enables Google Tag Manager). The plugin will check
|
||||
`googleanalytics.id` and set appropriate value depending on the
|
||||
prefix: `G-` enables `gtag`, `UA-` enables `ga` mode, and `GTM-`
|
||||
enables Google Tag Manager.
|
||||
|
||||
Use this option only if you know better which snippet you need. In
|
||||
future, after dropping UniversalAnalytics, this option may be
|
||||
removed.
|
||||
|
||||
- key: googleanalytics.account
|
||||
|
||||
- key: googleanalytics.profile_id
|
||||
|
||||
- key: googleanalytics.domain
|
||||
default: auto
|
||||
|
||||
- key: googleanalytics.credentials.path
|
||||
|
||||
- key: googleanalytics.fields
|
||||
default: "{}"
|
||||
|
||||
- key: googleanalytics.linked_domains
|
||||
default: ""
|
||||
|
||||
- key: googleanalytics.enable_user_id
|
||||
type: bool
|
||||
|
||||
- key: googleanalytics_resource_prefix
|
||||
default: "/downloads/"
|
||||
|
||||
- key: googleanalytics.recent_view_days
|
||||
default: 14
|
|
@ -4,24 +4,15 @@ import logging
|
|||
from ckan.lib.base import BaseController, c, render, request
|
||||
from . import dbutil
|
||||
|
||||
import ckan.logic as logic
|
||||
import hashlib
|
||||
from . import plugin
|
||||
from pylons import config
|
||||
|
||||
from paste.util.multidict import MultiDict
|
||||
|
||||
from ckan.controllers.api import ApiController
|
||||
|
||||
from ckan.exceptions import CkanVersionException
|
||||
import ckan.plugins.toolkit as tk
|
||||
|
||||
try:
|
||||
tk.requires_ckan_version("2.9")
|
||||
except CkanVersionException:
|
||||
pass
|
||||
else:
|
||||
from builtins import str
|
||||
from ckanext.googleanalytics import config
|
||||
|
||||
|
||||
log = logging.getLogger("ckanext.googleanalytics")
|
||||
|
@ -39,25 +30,24 @@ class GAApiController(ApiController):
|
|||
def _post_analytics(
|
||||
self, user, request_obj_type, request_function, request_id
|
||||
):
|
||||
if config.get("googleanalytics.id"):
|
||||
data_dict = {
|
||||
"v": 1,
|
||||
"tid": config.get("googleanalytics.id"),
|
||||
"cid": hashlib.md5(user).hexdigest(),
|
||||
# customer id should be obfuscated
|
||||
"t": "event",
|
||||
"dh": c.environ["HTTP_HOST"],
|
||||
"dp": c.environ["PATH_INFO"],
|
||||
"dr": c.environ.get("HTTP_REFERER", ""),
|
||||
"ec": "CKAN API Request",
|
||||
"ea": request_obj_type + request_function,
|
||||
"el": request_id,
|
||||
}
|
||||
plugin.GoogleAnalyticsPlugin.analytics_queue.put(data_dict)
|
||||
data_dict = {
|
||||
"v": 1,
|
||||
"tid": config.tracking_id(),
|
||||
"cid": hashlib.md5(user).hexdigest(),
|
||||
# customer id should be obfuscated
|
||||
"t": "event",
|
||||
"dh": c.environ["HTTP_HOST"],
|
||||
"dp": c.environ["PATH_INFO"],
|
||||
"dr": c.environ.get("HTTP_REFERER", ""),
|
||||
"ec": "CKAN API Request",
|
||||
"ea": request_obj_type + request_function,
|
||||
"el": request_id,
|
||||
}
|
||||
plugin.GoogleAnalyticsPlugin.analytics_queue.put(data_dict)
|
||||
|
||||
def action(self, logic_function, ver=None):
|
||||
try:
|
||||
function = logic.get_action(logic_function)
|
||||
function = tk.get_action(logic_function)
|
||||
side_effect_free = getattr(function, "side_effect_free", False)
|
||||
request_data = self._get_request_data(
|
||||
try_url_params=side_effect_free
|
||||
|
|
|
@ -4,22 +4,19 @@ from sqlalchemy import func
|
|||
|
||||
import ckan.model as model
|
||||
|
||||
# from ckan.model.authz import PSEUDO_USER__VISITOR
|
||||
from ckan.lib.base import *
|
||||
|
||||
cached_tables = {}
|
||||
|
||||
|
||||
def init_tables():
|
||||
metadata = MetaData()
|
||||
package_stats = Table(
|
||||
Table(
|
||||
"package_stats",
|
||||
metadata,
|
||||
Column("package_id", String(60), primary_key=True),
|
||||
Column("visits_recently", Integer),
|
||||
Column("visits_ever", Integer),
|
||||
)
|
||||
resource_stats = Table(
|
||||
Table(
|
||||
"resource_stats",
|
||||
metadata,
|
||||
Column("resource_id", String(60), primary_key=True),
|
||||
|
@ -81,36 +78,6 @@ def get_resource_visits_for_url(url):
|
|||
return count and count[0] or ""
|
||||
|
||||
|
||||
""" get_top_packages is broken, and needs to be rewritten to work with
|
||||
CKAN 2.*. This is because ckan.authz has been removed in CKAN 2.*
|
||||
|
||||
See commit ffa86c010d5d25fa1881c6b915e48f3b44657612
|
||||
"""
|
||||
|
||||
|
||||
def get_top_packages(limit=20):
|
||||
items = []
|
||||
# caveat emptor: the query below will not filter out private
|
||||
# or deleted datasets (TODO)
|
||||
q = model.Session.query(model.Package)
|
||||
connection = model.Session.connection()
|
||||
package_stats = get_table("package_stats")
|
||||
s = select(
|
||||
[
|
||||
package_stats.c.package_id,
|
||||
package_stats.c.visits_recently,
|
||||
package_stats.c.visits_ever,
|
||||
]
|
||||
).order_by(package_stats.c.visits_recently.desc())
|
||||
res = connection.execute(s).fetchmany(limit)
|
||||
for package_id, recent, ever in res:
|
||||
item = q.filter(text("package.id = '%s'" % package_id))
|
||||
if not item.count():
|
||||
continue
|
||||
items.append((item.first(), recent, ever))
|
||||
return items
|
||||
|
||||
|
||||
def get_top_resources(limit=20):
|
||||
items = []
|
||||
connection = model.Session.connection()
|
||||
|
|
|
@ -1,70 +1,45 @@
|
|||
import httplib2
|
||||
from apiclient.discovery import build
|
||||
from oauth2client.service_account import ServiceAccountCredentials
|
||||
|
||||
from ckan.exceptions import CkanVersionException
|
||||
import ckan.plugins.toolkit as tk
|
||||
|
||||
try:
|
||||
tk.requires_ckan_version("2.9")
|
||||
except CkanVersionException:
|
||||
from pylons import config
|
||||
else:
|
||||
config = tk.config
|
||||
|
||||
|
||||
def _prepare_credentials(credentials_filename):
|
||||
"""
|
||||
Either returns the user's oauth credentials or uses the credentials
|
||||
file to generate a token (by forcing the user to login in the browser)
|
||||
"""
|
||||
scope = ["https://www.googleapis.com/auth/analytics.readonly"]
|
||||
credentials = ServiceAccountCredentials.from_json_keyfile_name(
|
||||
credentials_filename, scopes=scope
|
||||
)
|
||||
return credentials
|
||||
from ckanext.googleanalytics import config
|
||||
|
||||
|
||||
def init_service(credentials_file):
|
||||
"""
|
||||
Given a file containing the user's oauth token (and another with
|
||||
credentials in case we need to generate the token) will return a
|
||||
service object representing the analytics API.
|
||||
"""
|
||||
http = httplib2.Http()
|
||||
"""Get a service that communicates to a Google API."""
|
||||
scope = ["https://www.googleapis.com/auth/analytics.readonly"]
|
||||
credentials = ServiceAccountCredentials.from_json_keyfile_name(
|
||||
credentials_file, scopes=scope
|
||||
)
|
||||
|
||||
credentials = _prepare_credentials(credentials_file)
|
||||
http = credentials.authorize(http) # authorize the http object
|
||||
|
||||
return build("analytics", "v3", http=http)
|
||||
return build("analytics", "v3", credentials=credentials)
|
||||
|
||||
|
||||
def get_profile_id(service):
|
||||
"""
|
||||
"""Get static profile ID or fetch one from the service.
|
||||
|
||||
Get the profile ID for this user and the service specified by the
|
||||
'googleanalytics.id' configuration option. This function iterates
|
||||
over all of the accounts available to the user who invoked the
|
||||
service to find one where the account name matches (in case the
|
||||
user has several).
|
||||
|
||||
If not user configured, the first account is used
|
||||
"""
|
||||
|
||||
profile_id = config.profile_id()
|
||||
if profile_id:
|
||||
return profile_id
|
||||
|
||||
accounts = service.management().accounts().list().execute()
|
||||
|
||||
if not accounts.get("items"):
|
||||
return None
|
||||
accountName = config.get("googleanalytics.account")
|
||||
webPropertyId = config.get("googleanalytics.id")
|
||||
accountName = config.account()
|
||||
webPropertyId = config.tracking_id()
|
||||
for acc in accounts.get("items"):
|
||||
if acc.get("name") == accountName:
|
||||
if not accountName or acc.get("name") == accountName:
|
||||
accountId = acc.get("id")
|
||||
|
||||
# TODO: check, whether next line is doing something useful.
|
||||
webproperties = (
|
||||
service.management()
|
||||
.webproperties()
|
||||
.list(accountId=accountId)
|
||||
.execute()
|
||||
)
|
||||
|
||||
profiles = (
|
||||
service.management()
|
||||
.profiles()
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
header_code = """
|
||||
<script type="text/javascript">
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', '%s']);
|
||||
_gaq.push(['_setDomainName', '%s']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
(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';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
footer_code = """
|
||||
<script type="text/javascript" src="%s"></script>
|
||||
"""
|
||||
|
||||
download_style = """
|
||||
<style type="text/css">
|
||||
span.downloads-count {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
"""
|
|
@ -0,0 +1,50 @@
|
|||
|
||||
import ckan.plugins.toolkit as tk
|
||||
from ckanext.googleanalytics import config
|
||||
|
||||
|
||||
def get_helpers():
|
||||
return {
|
||||
"googleanalytics_header": googleanalytics_header,
|
||||
"googleanalytics_id": googleanalytics_id,
|
||||
"googleanalytics_resource_prefix": googleanalytics_resource_prefix,
|
||||
"googleanalytics_tracking_mode": googleanalytics_tracking_mode,
|
||||
}
|
||||
|
||||
|
||||
def googleanalytics_resource_prefix():
|
||||
|
||||
return config.prefix()
|
||||
|
||||
|
||||
def googleanalytics_header():
|
||||
"""Render the googleanalytics_header snippet for CKAN 2.0 templates.
|
||||
|
||||
This is a template helper function that renders the
|
||||
googleanalytics_header jinja snippet. To be called from the jinja
|
||||
templates in this extension, see ITemplateHelpers.
|
||||
|
||||
"""
|
||||
|
||||
fields = config.fields()
|
||||
|
||||
if config.enable_user_id() and tk.c.user:
|
||||
fields["userId"] = str(tk.c.userobj.id)
|
||||
|
||||
data = {
|
||||
"googleanalytics_id": config.tracking_id(),
|
||||
"googleanalytics_domain": config.domain(),
|
||||
"googleanalytics_fields": str(fields),
|
||||
"googleanalytics_linked_domains": config.linked_domains(),
|
||||
}
|
||||
return tk.render_snippet(
|
||||
"googleanalytics/snippets/googleanalytics_header.html", data
|
||||
)
|
||||
|
||||
|
||||
def googleanalytics_tracking_mode():
|
||||
return config.tracking_mode()
|
||||
|
||||
|
||||
def googleanalytics_id():
|
||||
return config.tracking_id()
|
|
@ -0,0 +1,8 @@
|
|||
from ckan.plugins import Interface
|
||||
|
||||
|
||||
class IGoogleAnalytics(Interface):
|
||||
def googleanalytics_skip_event(self, data):
|
||||
"""Decide if sending data to GA must be skipped.
|
||||
"""
|
||||
return False
|
|
@ -0,0 +1,95 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import ckan.plugins.toolkit as tk
|
||||
from ckan.logic import validate
|
||||
|
||||
from . import schema
|
||||
from .. import config
|
||||
from ..model import PackageStats, ResourceStats
|
||||
from ..ga_auth import init_service, get_profile_id
|
||||
|
||||
|
||||
def get_actions():
|
||||
return dict(
|
||||
googleanalytics_package_stats_show=package_stats_show,
|
||||
googleanalytics_resource_stats_show=resource_stats_show,
|
||||
googleanalytics_event_report=event_report,
|
||||
)
|
||||
|
||||
|
||||
@validate(schema.package_stats_show)
|
||||
@tk.side_effect_free
|
||||
def package_stats_show(context, data_dict):
|
||||
tk.check_access("googleanalytics_package_stats_show", context, data_dict)
|
||||
rec = (
|
||||
context["session"]
|
||||
.query(PackageStats)
|
||||
.filter(PackageStats.package_id == data_dict["id"])
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if not rec:
|
||||
raise tk.ObjectNotFound()
|
||||
|
||||
return rec.for_json(context)
|
||||
|
||||
|
||||
@validate(schema.resource_stats_show)
|
||||
@tk.side_effect_free
|
||||
def resource_stats_show(context, data_dict):
|
||||
tk.check_access("googleanalytics_resource_stats_show", context, data_dict)
|
||||
rec = (
|
||||
context["session"]
|
||||
.query(ResourceStats)
|
||||
.filter(ResourceStats.resource_id == data_dict["id"])
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if not rec:
|
||||
raise tk.ObjectNotFound()
|
||||
|
||||
return rec.for_json(context)
|
||||
|
||||
|
||||
@validate(schema.event_report)
|
||||
@tk.side_effect_free
|
||||
def event_report(context, data_dict):
|
||||
tk.check_access("sysadmin", context, data_dict)
|
||||
|
||||
se = init_service(config.credentials())
|
||||
filters = []
|
||||
if "action" in data_dict:
|
||||
filters.append(
|
||||
"ga:eventAction=={action}".format(action=data_dict["action"])
|
||||
)
|
||||
|
||||
if "category" in data_dict:
|
||||
filters.append(
|
||||
"ga:eventCategory=={category}".format(
|
||||
category=data_dict["category"]
|
||||
)
|
||||
)
|
||||
|
||||
if "label" in data_dict:
|
||||
filters.append(
|
||||
"ga:eventLabel=={label}".format(label=data_dict["label"])
|
||||
)
|
||||
|
||||
report = (
|
||||
se.data()
|
||||
.ga()
|
||||
.get(
|
||||
ids="ga:{id}".format(id=get_profile_id(se)),
|
||||
dimensions=",".join(data_dict["dimensions"]),
|
||||
metrics=",".join(data_dict["metrics"]),
|
||||
start_date=data_dict["start_date"].date().isoformat(),
|
||||
end_date=data_dict["end_date"].date().isoformat(),
|
||||
filters=";".join(filters) or None,
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
return {
|
||||
"headers": [h["name"] for h in report["columnHeaders"]],
|
||||
"rows": report.get("rows", []),
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from ckan.authz import is_authorized
|
||||
|
||||
|
||||
def get_auth():
|
||||
return dict(
|
||||
googleanalytics_package_stats_show=package_stats_show,
|
||||
googleanalytics_resource_stats_show=resource_stats_show,
|
||||
)
|
||||
|
||||
|
||||
def package_stats_show(context, data_dict):
|
||||
return is_authorized("package_show", context, data_dict)
|
||||
|
||||
|
||||
def resource_stats_show(context, data_dict):
|
||||
return is_authorized("resource_show", context, data_dict)
|
|
@ -0,0 +1,32 @@
|
|||
from ckan.logic.schema import validator_args
|
||||
|
||||
|
||||
@validator_args
|
||||
def package_stats_show(not_empty):
|
||||
return {"id": [not_empty]}
|
||||
|
||||
|
||||
@validator_args
|
||||
def resource_stats_show(not_empty):
|
||||
return {"id": [not_empty]}
|
||||
|
||||
|
||||
@validator_args
|
||||
def event_report(
|
||||
not_empty, isodate, json_list_or_string, default, ignore_empty
|
||||
):
|
||||
return {
|
||||
"start_date": [not_empty, isodate],
|
||||
"end_date": [not_empty, isodate],
|
||||
"category": [ignore_empty],
|
||||
"action": [ignore_empty],
|
||||
"label": [ignore_empty],
|
||||
"dimensions": [
|
||||
default("ga:eventCategory,ga:eventAction,ga:eventLabel"),
|
||||
json_list_or_string,
|
||||
],
|
||||
"metrics": [
|
||||
default("ga:totalEvents,ga:uniqueEvents,ga:eventValue"),
|
||||
json_list_or_string,
|
||||
],
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
|
@ -0,0 +1,74 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = %(here)s
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# timezone to use when rendering the date
|
||||
# within the migration file as well as the filename.
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to /home/sergey/Projects/core/ckanext-googleanalytics/ckanext/googleanalytics/migration/googleanalytics/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat /home/sergey/Projects/core/ckanext-googleanalytics/ckanext/googleanalytics/migration/googleanalytics/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
|
@ -0,0 +1,84 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
import os
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
name = os.path.basename(os.path.dirname(__file__))
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
|
||||
url = config.get_main_option(u"sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
version_table=u"{}_alembic_version".format(name),
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix=u"sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
version_table=u"{}_alembic_version".format(name),
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
|
@ -0,0 +1,24 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,50 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: b74febeb899b
|
||||
Revises:
|
||||
Create Date: 2022-05-06 17:46:09.398679
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b74febeb899b"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
conn = op.get_bind()
|
||||
inspector = Inspector.from_engine(conn)
|
||||
tables = inspector.get_table_names()
|
||||
if "package_stats" not in tables:
|
||||
_create_package_stats()
|
||||
|
||||
if "resource_stats" not in tables:
|
||||
_create_resource_stats()
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table("resource_stats")
|
||||
op.drop_table("package_stats")
|
||||
|
||||
|
||||
def _create_package_stats():
|
||||
op.create_table(
|
||||
"package_stats",
|
||||
sa.Column("package_id", sa.String(60), primary_key=True),
|
||||
sa.Column("visits_recently", sa.Integer),
|
||||
sa.Column("visits_ever", sa.Integer),
|
||||
)
|
||||
|
||||
|
||||
def _create_resource_stats():
|
||||
op.create_table(
|
||||
"resource_stats",
|
||||
sa.Column("resource_id", sa.String(60), primary_key=True),
|
||||
sa.Column("visits_recently", sa.Integer),
|
||||
sa.Column("visits_ever", sa.Integer),
|
||||
)
|
|
@ -0,0 +1,29 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from sqlalchemy import Column, String, Integer
|
||||
|
||||
from ckan.lib.dictization import table_dictize
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class PackageStats(Base):
|
||||
__tablename__ = "package_stats"
|
||||
|
||||
package_id = Column(String(60), primary_key=True)
|
||||
visits_recently = Column(Integer)
|
||||
visits_ever = Column(Integer)
|
||||
|
||||
def for_json(self, context):
|
||||
return table_dictize(self, context)
|
||||
|
||||
|
||||
class ResourceStats(Base):
|
||||
__tablename__ = "resource_stats"
|
||||
|
||||
resource_id = Column(String(60), primary_key=True)
|
||||
visits_recently = Column(Integer)
|
||||
visits_ever = Column(Integer)
|
||||
|
||||
def for_json(self, context):
|
||||
return table_dictize(self, context)
|
|
@ -0,0 +1,4 @@
|
|||
from sqlalchemy.ext.declarative import declarative_base
|
||||
import ckan.model.meta as meta
|
||||
|
||||
Base = declarative_base(metadata=meta.metadata)
|
|
@ -1,21 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
from six.moves.urllib.parse import urlencode
|
||||
import ast
|
||||
import logging
|
||||
import threading
|
||||
|
||||
|
||||
import requests
|
||||
|
||||
import ckan.lib.helpers as h
|
||||
import ckan.plugins as p
|
||||
import ckan.plugins.toolkit as tk
|
||||
|
||||
from ckan.exceptions import CkanVersionException
|
||||
from ckan.exceptions import CkanConfigurationException, CkanVersionException
|
||||
|
||||
DEFAULT_RESOURCE_URL_TAG = "/downloads/"
|
||||
from .. import helpers, utils
|
||||
from ..logic import action, auth
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,7 +22,7 @@ else:
|
|||
from ckanext.googleanalytics.plugin.flask_plugin import GAMixinPlugin
|
||||
|
||||
|
||||
class GoogleAnalyticsException(Exception):
|
||||
class GoogleAnalyticsException(CkanConfigurationException):
|
||||
pass
|
||||
|
||||
|
||||
|
@ -41,115 +36,44 @@ class AnalyticsPostThread(threading.Thread):
|
|||
def run(self):
|
||||
while True:
|
||||
# grabs host from queue
|
||||
data_dict = self.queue.get()
|
||||
|
||||
data = urlencode(data_dict)
|
||||
log.debug("Sending API event to Google Analytics: " + data)
|
||||
# send analytics
|
||||
res = requests.post(
|
||||
"http://www.google-analytics.com/collect",
|
||||
data,
|
||||
timeout=10,
|
||||
)
|
||||
data = self.queue.get()
|
||||
utils.send_event(data)
|
||||
# signals to queue job is done
|
||||
self.queue.task_done()
|
||||
|
||||
|
||||
class GoogleAnalyticsPlugin(GAMixinPlugin, p.SingletonPlugin):
|
||||
|
||||
p.implements(p.IConfigurable, inherit=True)
|
||||
p.implements(p.IConfigurer, inherit=True)
|
||||
p.implements(p.ITemplateHelpers)
|
||||
p.implements(p.IActions)
|
||||
p.implements(p.IAuthFunctions)
|
||||
|
||||
def get_auth_functions(self):
|
||||
return auth.get_auth()
|
||||
|
||||
def get_actions(self):
|
||||
return action.get_actions()
|
||||
|
||||
def configure(self, config):
|
||||
"""Load config settings for this extension from config file.
|
||||
|
||||
See IConfigurable.
|
||||
|
||||
"""
|
||||
if "googleanalytics.id" not in config:
|
||||
msg = "Missing googleanalytics.id in config"
|
||||
raise GoogleAnalyticsException(msg)
|
||||
self.googleanalytics_id = config["googleanalytics.id"]
|
||||
self.googleanalytics_domain = config.get(
|
||||
"googleanalytics.domain", "auto"
|
||||
)
|
||||
self.googleanalytics_fields = ast.literal_eval(
|
||||
config.get("googleanalytics.fields", "{}")
|
||||
)
|
||||
|
||||
googleanalytics_linked_domains = config.get(
|
||||
"googleanalytics.linked_domains", ""
|
||||
)
|
||||
self.googleanalytics_linked_domains = [
|
||||
x.strip() for x in googleanalytics_linked_domains.split(",") if x
|
||||
]
|
||||
|
||||
if self.googleanalytics_linked_domains:
|
||||
self.googleanalytics_fields["allowLinker"] = "true"
|
||||
|
||||
# If resource_prefix is not in config file then write the default value
|
||||
# to the config dict, otherwise templates seem to get 'true' when they
|
||||
# try to read resource_prefix from config.
|
||||
if "googleanalytics_resource_prefix" not in config:
|
||||
config[
|
||||
"googleanalytics_resource_prefix"
|
||||
] = DEFAULT_RESOURCE_URL_TAG
|
||||
self.googleanalytics_resource_prefix = config[
|
||||
"googleanalytics_resource_prefix"
|
||||
]
|
||||
|
||||
self.show_downloads = tk.asbool(
|
||||
config.get("googleanalytics.show_downloads", True)
|
||||
)
|
||||
self.track_events = tk.asbool(
|
||||
config.get("googleanalytics.track_events", False)
|
||||
)
|
||||
self.enable_user_id = tk.asbool(
|
||||
config.get("googleanalytics.enable_user_id", False)
|
||||
)
|
||||
|
||||
p.toolkit.add_resource("../assets", "ckanext-googleanalytics")
|
||||
|
||||
# spawn a pool of 5 threads, and pass them queue instance
|
||||
for i in range(5):
|
||||
for _i in range(5):
|
||||
t = AnalyticsPostThread(self.analytics_queue)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
def update_config(self, config):
|
||||
"""Change the CKAN (Pylons) environment configuration.
|
||||
tk.add_template_directory(config, "../templates")
|
||||
tk.add_resource("../assets", "ckanext-googleanalytics")
|
||||
|
||||
See IConfigurer.
|
||||
|
||||
"""
|
||||
p.toolkit.add_template_directory(config, "../templates")
|
||||
if not config.get("googleanalytics.id"):
|
||||
msg = "Missing or empty googleanalytics.id in config"
|
||||
raise GoogleAnalyticsException(msg)
|
||||
|
||||
def get_helpers(self):
|
||||
"""Return the CKAN 2.0 template helper functions this plugin provides.
|
||||
return helpers.get_helpers()
|
||||
|
||||
See ITemplateHelpers.
|
||||
|
||||
"""
|
||||
return {"googleanalytics_header": self.googleanalytics_header}
|
||||
|
||||
def googleanalytics_header(self):
|
||||
"""Render the googleanalytics_header snippet for CKAN 2.0 templates.
|
||||
|
||||
This is a template helper function that renders the
|
||||
googleanalytics_header jinja snippet. To be called from the jinja
|
||||
templates in this extension, see ITemplateHelpers.
|
||||
|
||||
"""
|
||||
|
||||
if self.enable_user_id and tk.c.user:
|
||||
self.googleanalytics_fields["userId"] = str(tk.c.userobj.id)
|
||||
|
||||
data = {
|
||||
"googleanalytics_id": self.googleanalytics_id,
|
||||
"googleanalytics_domain": self.googleanalytics_domain,
|
||||
"googleanalytics_fields": str(self.googleanalytics_fields),
|
||||
"googleanalytics_linked_domains": self.googleanalytics_linked_domains,
|
||||
}
|
||||
return p.toolkit.render_snippet(
|
||||
"googleanalytics/snippets/googleanalytics_header.html", data
|
||||
)
|
||||
if tk.check_ckan_version("2.10"):
|
||||
tk.blanket.config_declarations(GoogleAnalyticsPlugin)
|
||||
|
|
|
@ -6,9 +6,9 @@ import importlib
|
|||
|
||||
import ckan.plugins as plugins
|
||||
import ckan.plugins.toolkit as tk
|
||||
from ckanext.googleanalytics import config
|
||||
|
||||
from ckan.controllers.package import PackageController
|
||||
from pylons import config
|
||||
from routes.mapper import SubMapper
|
||||
|
||||
|
||||
|
@ -147,19 +147,17 @@ def wrap_resource_download(func):
|
|||
def _post_analytics(
|
||||
user, event_type, request_obj_type, request_function, request_id
|
||||
):
|
||||
|
||||
if config.get("googleanalytics.id"):
|
||||
data_dict = {
|
||||
"v": 1,
|
||||
"tid": config.get("googleanalytics.id"),
|
||||
"cid": hashlib.md5(tk.c.user).hexdigest(),
|
||||
# customer id should be obfuscated
|
||||
"t": "event",
|
||||
"dh": tk.c.environ["HTTP_HOST"],
|
||||
"dp": tk.c.environ["PATH_INFO"],
|
||||
"dr": tk.c.environ.get("HTTP_REFERER", ""),
|
||||
"ec": event_type,
|
||||
"ea": request_obj_type + request_function,
|
||||
"el": request_id,
|
||||
}
|
||||
GAMixinPlugin.analytics_queue.put(data_dict)
|
||||
data_dict = {
|
||||
"v": 1,
|
||||
"tid": config.tracking_id(),
|
||||
"cid": hashlib.md5(tk.c.user).hexdigest(),
|
||||
# customer id should be obfuscated
|
||||
"t": "event",
|
||||
"dh": tk.c.environ["HTTP_HOST"],
|
||||
"dp": tk.c.environ["PATH_INFO"],
|
||||
"dr": tk.c.environ.get("HTTP_REFERER", ""),
|
||||
"ec": event_type,
|
||||
"ea": request_obj_type + request_function,
|
||||
"el": request_id,
|
||||
}
|
||||
GAMixinPlugin.analytics_queue.put(data_dict)
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{% set type = 'asset' if h.ckan_version().split('.')[1] | int >= 9 else 'resource' %}
|
||||
{% include 'googleanalytics/snippets/event_tracking_' ~ type ~ '.html' %}
|
||||
<div class="js-hide" data-module="google-analytics"
|
||||
data-module-googleanalytics_resource_prefix="{{ g.googleanalytics_resource_prefix }}">
|
||||
data-module-googleanalytics_resource_prefix="{{ h.googleanalytics_resource_prefix() }}">
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<script type="text/javascript">
|
||||
(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','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{googleanalytics_id}}', '{{googleanalytics_domain}}', {{googleanalytics_fields|safe}});
|
||||
{% if googleanalytics_linked_domains %}
|
||||
ga('require', 'linker');
|
||||
ga('linker:autoLink', [{% for domain in googleanalytics_linked_domains %}'{{domain}}'{% if not loop.last %},{% endif %}{% endfor %}] );
|
||||
{% endif %}
|
||||
ga('set', 'anonymizeIp', true);
|
||||
ga('send', 'pageview');
|
||||
</script>
|
|
@ -0,0 +1,26 @@
|
|||
{% block main %}
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{googleanalytics_id}}"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
|
||||
{% block setup %}
|
||||
gtag('set', 'linker');
|
||||
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '{{googleanalytics_id}}', {
|
||||
anonymize_ip: true,
|
||||
linker: {
|
||||
domains: {{ googleanalytics_linked_domains|tojson }}
|
||||
}
|
||||
});
|
||||
{% endblock setup %}
|
||||
|
||||
{% block extra %}
|
||||
{% endblock extra %}
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','{{googleanalytics_id}}');</script>
|
||||
<!-- End Google Tag Manager -->
|
|
@ -1,14 +1 @@
|
|||
<script type="text/javascript">
|
||||
(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','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{googleanalytics_id}}', '{{googleanalytics_domain}}', {{googleanalytics_fields|safe}});
|
||||
{% if googleanalytics_linked_domains %}
|
||||
ga('require', 'linker');
|
||||
ga('linker:autoLink', [{% for domain in googleanalytics_linked_domains %}'{{domain}}'{% if not loop.last %},{% endif %}{% endfor %}] );
|
||||
{% endif %}
|
||||
ga('set', 'anonymizeIp', true);
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{% include 'googleanalytics/snippets/_' ~ h.googleanalytics_tracking_mode() ~ '.html' %}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{% ckan_extends %}
|
||||
|
||||
{% block page %}
|
||||
{% block googleanalytics_body_script %}
|
||||
{% if h.googleanalytics_tracking_mode() == "gtm" %}
|
||||
{% with id = h.googleanalytics_id() %}
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ id }}"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{{ super() }}
|
||||
{% endblock page %}
|
|
@ -0,0 +1,46 @@
|
|||
import pytest
|
||||
import factory
|
||||
|
||||
from factory.alchemy import SQLAlchemyModelFactory
|
||||
from pytest_factoryboy import register
|
||||
from ckan.plugins import toolkit
|
||||
|
||||
import ckan.model as model
|
||||
|
||||
from ckanext.googleanalytics.model import PackageStats, ResourceStats
|
||||
|
||||
|
||||
if toolkit.check_ckan_version("2.9"):
|
||||
@pytest.fixture()
|
||||
def clean_db(reset_db, migrate_db_for):
|
||||
reset_db()
|
||||
migrate_db_for("googleanalytics")
|
||||
else:
|
||||
from ckanext.googleanalytics.dbutil import init_tables
|
||||
|
||||
@pytest.fixture()
|
||||
def clean_db(reset_db):
|
||||
reset_db()
|
||||
init_tables()
|
||||
|
||||
|
||||
@register
|
||||
class PackageStatsFactory(SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
sqlalchemy_session = model.Session
|
||||
model = PackageStats
|
||||
|
||||
package_id = factory.Faker("uuid4")
|
||||
visits_recently = factory.Faker("pyint")
|
||||
visits_ever = factory.Faker("pyint")
|
||||
|
||||
|
||||
@register
|
||||
class ResourceStatsFactory(SQLAlchemyModelFactory):
|
||||
class Meta:
|
||||
sqlalchemy_session = model.Session
|
||||
model = ResourceStats
|
||||
|
||||
resource_id = factory.Faker("uuid4")
|
||||
visits_recently = factory.Faker("pyint")
|
||||
visits_ever = factory.Faker("pyint")
|
|
@ -0,0 +1,44 @@
|
|||
import pytest
|
||||
|
||||
from ckan.tests.helpers import call_action
|
||||
import ckan.plugins.toolkit as tk
|
||||
import ckan.model as model
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
||||
class TestPackageStatsShow:
|
||||
|
||||
def test_existing(self, package_stats):
|
||||
with pytest.raises(tk.ObjectNotFound):
|
||||
call_action(
|
||||
"googleanalytics_package_stats_show",
|
||||
id=package_stats.package_id
|
||||
)
|
||||
model.Session.commit()
|
||||
|
||||
rec = call_action(
|
||||
"googleanalytics_package_stats_show", id=package_stats.package_id
|
||||
)
|
||||
|
||||
assert rec["visits_recently"] == rec["visits_recently"]
|
||||
assert rec["visits_ever"] == rec["visits_ever"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("with_plugins", "clean_db")
|
||||
class TestResourceStatsShow:
|
||||
|
||||
def test_existing(self, resource_stats):
|
||||
with pytest.raises(tk.ObjectNotFound):
|
||||
call_action(
|
||||
"googleanalytics_resource_stats_show",
|
||||
id=resource_stats.resource_id
|
||||
)
|
||||
model.Session.commit()
|
||||
|
||||
rec = call_action(
|
||||
"googleanalytics_resource_stats_show",
|
||||
id=resource_stats.resource_id
|
||||
)
|
||||
|
||||
assert rec["visits_recently"] == rec["visits_recently"]
|
||||
assert rec["visits_ever"] == rec["visits_ever"]
|
|
@ -0,0 +1,14 @@
|
|||
import pytest
|
||||
|
||||
from ckanext.googleanalytics import config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("tracking_id", "mode"), [
|
||||
("UA-123", "ga"),
|
||||
("G-123", "gtag"),
|
||||
("GTM-123", "gtm"),
|
||||
("HELLO-123", "ga"),
|
||||
])
|
||||
def test_tracking_mode(tracking_id, mode, monkeypatch, ckan_config):
|
||||
monkeypatch.setitem(ckan_config, config.CONFIG_TRACKING_ID, tracking_id)
|
||||
assert mode == config.tracking_mode()
|
|
@ -0,0 +1,7 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("clean_db")
|
||||
def test_script(app):
|
||||
resp = app.get("/")
|
||||
assert "GoogleAnalyticsObject" in resp
|
|
@ -0,0 +1,26 @@
|
|||
import pytest
|
||||
|
||||
import ckan.plugins.toolkit as tk
|
||||
from ckanext.googleanalytics import config
|
||||
|
||||
|
||||
def _render_header(mode, tracking_id):
|
||||
return tk.render_snippet("googleanalytics/snippets/_{}.html".format(mode), {
|
||||
"googleanalytics_id": tracking_id,
|
||||
"googleanalytics_domain": config.domain(),
|
||||
"googleanalytics_fields": config.fields(),
|
||||
"googleanalytics_linked_domains": config.linked_domains()
|
||||
})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("with_plugins", "with_request_context")
|
||||
class TestCodeSnippets:
|
||||
@pytest.mark.parametrize("mode", ["ga", "gtag", "gtm"])
|
||||
@pytest.mark.parametrize("tracking_id", ["UA-123", "G-123", "GTM-123"])
|
||||
def test_tracking_(self, mode, tracking_id, app, ckan_config, monkeypatch):
|
||||
snippet = tk.h.googleanalytics_header()
|
||||
monkeypatch.setitem(ckan_config, config.CONFIG_TRACKING_ID, tracking_id)
|
||||
monkeypatch.setitem(ckan_config, config.CONFIG_TRACKING_MODE, mode)
|
||||
snippet = _render_header(mode, tracking_id)
|
||||
resp = app.get("/about")
|
||||
assert snippet in resp.body
|
|
@ -0,0 +1,95 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from six.moves.urllib.parse import urlencode
|
||||
from ckanext.googleanalytics import config
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
EVENT_API = "CKAN API Request"
|
||||
EVENT_DOWNLOAD = "CKAN Resource Download Request"
|
||||
|
||||
|
||||
def send_event(data):
|
||||
if isinstance(data, MeasurementProtocolData):
|
||||
if data["event"] == EVENT_API:
|
||||
return _mp_api_handler({
|
||||
"action": data["object"],
|
||||
"payload": data["payload"],
|
||||
})
|
||||
|
||||
if data["event"] == EVENT_DOWNLOAD:
|
||||
return _mp_download_handler({"payload": {
|
||||
"resource_id": data["id"],
|
||||
}})
|
||||
|
||||
log.warning("Only API and Download events supported by Measurement Protocol at the moment")
|
||||
return
|
||||
|
||||
return _ga_handler(data)
|
||||
|
||||
|
||||
class SafeJSONEncoder(json.JSONEncoder):
|
||||
def default(self, _):
|
||||
return None
|
||||
|
||||
|
||||
def _mp_api_handler(data_dict):
|
||||
log.debug(
|
||||
"Sending API event to Google Analytics using the Measurement Protocol: %s",
|
||||
data_dict
|
||||
)
|
||||
_mp_event({
|
||||
"name": data_dict["action"],
|
||||
"params": data_dict["payload"]
|
||||
})
|
||||
|
||||
|
||||
def _mp_download_handler(data_dict):
|
||||
log.debug(
|
||||
"Sending Downlaod event to Google Analytics using the Measurement Protocol: %s",
|
||||
data_dict
|
||||
)
|
||||
_mp_event({
|
||||
"name": "file_download",
|
||||
"params": data_dict["payload"],
|
||||
})
|
||||
|
||||
|
||||
def _mp_event(event):
|
||||
resp = requests.post(
|
||||
"https://www.google-analytics.com/mp/collect",
|
||||
params={
|
||||
"api_secret": config.measurement_protocol_client_secret(),
|
||||
"measurement_id": config.measurement_id()
|
||||
},
|
||||
data=json.dumps({
|
||||
"client_id": config.measurement_protocol_client_id(),
|
||||
"non_personalized_ads": False,
|
||||
"events": [event]
|
||||
}, cls=SafeJSONEncoder)
|
||||
)
|
||||
|
||||
if resp.status_code >= 300:
|
||||
log.error("Cannot post event: %s", resp)
|
||||
|
||||
|
||||
def _ga_handler(data_dict):
|
||||
data = urlencode(data_dict)
|
||||
log.debug("Sending API event to Google Analytics: %s", data)
|
||||
|
||||
requests.post(
|
||||
"https://www.google-analytics.com/collect",
|
||||
data,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
class UniversalAnalyticsData(dict):
|
||||
pass
|
||||
|
||||
|
||||
class MeasurementProtocolData(dict):
|
||||
pass
|
|
@ -5,16 +5,15 @@ import logging
|
|||
import six
|
||||
|
||||
from flask import Blueprint
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
import ckan.logic as logic
|
||||
import ckan.plugins.toolkit as tk
|
||||
import ckan.views.api as api
|
||||
import ckan.views.resource as resource
|
||||
|
||||
from ckan.common import g
|
||||
from ckan.plugins import PluginImplementations
|
||||
|
||||
CONFIG_HANDLER_PATH = "googleanalytics.download_handler"
|
||||
from ckanext.googleanalytics import utils, config, interfaces
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
ga = Blueprint("google_analytics", "google_analytics")
|
||||
|
@ -22,7 +21,7 @@ ga = Blueprint("google_analytics", "google_analytics")
|
|||
|
||||
def action(logic_function, ver=api.API_MAX_VERSION):
|
||||
try:
|
||||
function = logic.get_action(logic_function)
|
||||
function = tk.get_action(logic_function)
|
||||
side_effect_free = getattr(function, "side_effect_free", False)
|
||||
request_data = api._get_request_data(try_url_params=side_effect_free)
|
||||
if isinstance(request_data, dict):
|
||||
|
@ -31,7 +30,7 @@ def action(logic_function, ver=api.API_MAX_VERSION):
|
|||
id = request_data["q"]
|
||||
if "query" in request_data:
|
||||
id = request_data[u"query"]
|
||||
_post_analytics(g.user, "CKAN API Request", logic_function, "", id)
|
||||
_post_analytics(g.user, utils.EVENT_API, logic_function, "", id, request_data)
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
pass
|
||||
|
@ -45,7 +44,7 @@ ga.add_url_rule(
|
|||
view_func=action,
|
||||
)
|
||||
ga.add_url_rule(
|
||||
u"/<int(min=3, max={0}):ver>/action/<logic_function>".format(
|
||||
"/api/<int(min=3, max={0}):ver>/action/<logic_function>".format(
|
||||
api.API_MAX_VERSION
|
||||
),
|
||||
methods=["GET", "POST"],
|
||||
|
@ -54,18 +53,13 @@ ga.add_url_rule(
|
|||
|
||||
|
||||
def download(id, resource_id, filename=None, package_type="dataset"):
|
||||
handler_path = tk.config.get(CONFIG_HANDLER_PATH)
|
||||
if handler_path:
|
||||
handler = import_string(handler_path, silent=True)
|
||||
else:
|
||||
handler = None
|
||||
log.warning(("Missing {} config option.").format(CONFIG_HANDLER_PATH))
|
||||
handler = config.download_handler()
|
||||
if not handler:
|
||||
log.debug("Use default CKAN callback for resource.download")
|
||||
handler = resource.download
|
||||
_post_analytics(
|
||||
g.user,
|
||||
"CKAN Resource Download Request",
|
||||
utils.EVENT_DOWNLOAD,
|
||||
"Resource",
|
||||
"Download",
|
||||
resource_id,
|
||||
|
@ -88,23 +82,44 @@ ga.add_url_rule(
|
|||
|
||||
|
||||
def _post_analytics(
|
||||
user, event_type, request_obj_type, request_function, request_id
|
||||
user, event_type,
|
||||
request_obj_type, request_function,
|
||||
request_id, request_payload=None
|
||||
):
|
||||
|
||||
from ckanext.googleanalytics.plugin import GoogleAnalyticsPlugin
|
||||
|
||||
if tk.config.get("googleanalytics.id"):
|
||||
data_dict = {
|
||||
"v": 1,
|
||||
"tid": tk.config.get("googleanalytics.id"),
|
||||
"cid": hashlib.md5(six.ensure_binary(tk.c.user)).hexdigest(),
|
||||
# customer id should be obfuscated
|
||||
"t": "event",
|
||||
"dh": tk.request.environ["HTTP_HOST"],
|
||||
"dp": tk.request.environ["PATH_INFO"],
|
||||
"dr": tk.request.environ.get("HTTP_REFERER", ""),
|
||||
"ec": event_type,
|
||||
"ea": request_obj_type + request_function,
|
||||
"el": request_id,
|
||||
}
|
||||
if config.tracking_id():
|
||||
mp_client_id = config.measurement_protocol_client_id()
|
||||
if mp_client_id and (
|
||||
event_type == utils.EVENT_API
|
||||
or (event_type == utils.EVENT_DOWNLOAD and config.measurement_protocol_track_downloads())
|
||||
):
|
||||
data_dict = utils.MeasurementProtocolData({
|
||||
"event": event_type,
|
||||
"object": request_obj_type,
|
||||
"function": request_function,
|
||||
"id": request_id,
|
||||
"payload": request_payload,
|
||||
})
|
||||
|
||||
else:
|
||||
data_dict = utils.UniversalAnalyticsData({
|
||||
"v": 1,
|
||||
"tid": config.tracking_id(),
|
||||
"cid": hashlib.md5(six.ensure_binary(tk.c.user)).hexdigest(),
|
||||
# customer id should be obfuscated
|
||||
"t": "event",
|
||||
"dh": tk.request.environ["HTTP_HOST"],
|
||||
"dp": tk.request.environ["PATH_INFO"],
|
||||
"dr": tk.request.environ.get("HTTP_REFERER", ""),
|
||||
"ec": event_type,
|
||||
"ea": request_obj_type + request_function,
|
||||
"el": request_id,
|
||||
})
|
||||
|
||||
for p in PluginImplementations(interfaces.IGoogleAnalytics):
|
||||
if p.googleanalytics_skip_event(data_dict):
|
||||
return
|
||||
|
||||
GoogleAnalyticsPlugin.analytics_queue.put(data_dict)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
pytest<=6.0.2
|
||||
pytest-ckan
|
||||
pytest-factoryboy
|
||||
pytest-cov
|
|
@ -0,0 +1 @@
|
|||
pytest-ckan
|
|
@ -1,63 +1,4 @@
|
|||
[tool.black]
|
||||
line-length = 79
|
||||
preview = true
|
||||
|
||||
[tool.towncrier]
|
||||
issue_format = ""
|
||||
directory = "changes"
|
||||
package = "ckanext.googleanalytics"
|
||||
package_dir = "ckanext"
|
||||
filename = "CHANGELOG.rst"
|
||||
name = "ckanext-googleanalytics"
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.8"
|
||||
include = ["ckanext/googleanalytics"]
|
||||
exclude = [
|
||||
"**/test*",
|
||||
"**/migration",
|
||||
]
|
||||
ignore = [
|
||||
"ckan"
|
||||
]
|
||||
strict = []
|
||||
|
||||
strictParameterNoneValue = true # type must be Optional if default value is None
|
||||
|
||||
reportFunctionMemberAccess = true # non-standard member accesses for functions
|
||||
reportMissingImports = true
|
||||
reportMissingModuleSource = true
|
||||
reportMissingTypeStubs = false
|
||||
reportImportCycles = false
|
||||
reportUnusedImport = false
|
||||
reportUnusedClass = true
|
||||
reportUnusedFunction = true
|
||||
reportUnusedVariable = false
|
||||
reportDuplicateImport = true
|
||||
reportOptionalSubscript = true
|
||||
reportOptionalMemberAccess = true
|
||||
reportOptionalCall = true
|
||||
reportOptionalIterable = true
|
||||
reportOptionalContextManager = true
|
||||
reportOptionalOperand = true
|
||||
reportTypedDictNotRequiredAccess = false # We are using Context in a way that conflicts with this check
|
||||
reportConstantRedefinition = false
|
||||
reportIncompatibleMethodOverride = false
|
||||
reportIncompatibleVariableOverride = true
|
||||
reportOverlappingOverload = true
|
||||
reportUntypedFunctionDecorator = false
|
||||
reportUnknownParameterType = false # it creates a lot of noise
|
||||
reportUnknownArgumentType = false
|
||||
reportUnknownLambdaType = false
|
||||
reportMissingTypeArgument = true
|
||||
reportInvalidTypeVarUse = true
|
||||
reportCallInDefaultInitializer = true
|
||||
reportUnknownVariableType = false
|
||||
reportUntypedBaseClass = false # ignore it because we are relying on untyped CKAN
|
||||
reportUnnecessaryIsInstance = true
|
||||
reportUnnecessaryCast = true
|
||||
reportUnnecessaryComparison = true
|
||||
reportAssertAlwaysTrue = true
|
||||
reportSelfClsParameterName = true
|
||||
reportUnusedCallResult = false # allow function calls for side-effect only (like logic.check_acces)
|
||||
|
||||
useLibraryCodeForTypes = true
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
[metadata]
|
||||
name = ckanext-googleanalytics
|
||||
version = 2.3.0
|
||||
description = Add GA tracking and reporting to CKAN instance
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/ckan/ckanext-googleanalytics
|
||||
author = Seb Bacon
|
||||
author_email = seb.bacon@gmail.com
|
||||
license = AGPL
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
keywords =
|
||||
CKAN
|
||||
|
||||
[options]
|
||||
# python_requires = >= 3.7
|
||||
install_requires =
|
||||
ckantoolkit
|
||||
google-api-python-client
|
||||
|
||||
packages = find:
|
||||
namespace_packages = ckanext
|
||||
include_package_data = True
|
||||
|
||||
[options.extras_require]
|
||||
requirements =
|
||||
gdata>=2.0.0
|
||||
google-api-python-client>=1.6.1, <1.7.0
|
||||
pyOpenSSL>=16.2.0
|
||||
rsa>=3.1.4, <=4.0
|
||||
|
||||
[options.entry_points]
|
||||
ckan.plugins =
|
||||
googleanalytics = ckanext.googleanalytics.plugin:GoogleAnalyticsPlugin
|
||||
|
||||
paste.paster_command =
|
||||
loadanalytics = ckanext.googleanalytics.commands:LoadAnalytics
|
||||
initdb = ckanext.googleanalytics.commands:InitDB
|
||||
|
||||
babel.extractors =
|
||||
ckan = ckan.lib.extract:extract_ckan
|
||||
[extract_messages]
|
||||
keywords = translate isPlural
|
||||
add_comments = TRANSLATORS:
|
||||
output_file = ckanext/googleanalytics/i18n/ckanext-googleanalytics.pot
|
||||
width = 80
|
||||
|
||||
[init_catalog]
|
||||
domain = ckanext-googleanalytics
|
||||
input_file = ckanext/googleanalytics/i18n/ckanext-googleanalytics.pot
|
||||
output_dir = ckanext/googleanalytics/i18n
|
||||
|
||||
[update_catalog]
|
||||
domain = ckanext-googleanalytics
|
||||
input_file = ckanext/googleanalytics/i18n/ckanext-googleanalytics.pot
|
||||
output_dir = ckanext/googleanalytics/i18n
|
||||
previous = true
|
||||
|
||||
[compile_catalog]
|
||||
domain = ckanext-googleanalytics
|
||||
directory = ckanext/googleanalytics/i18n
|
||||
statistics = true
|
||||
|
||||
[tool:pytest]
|
||||
filterwarnings =
|
||||
ignore::sqlalchemy.exc.SADeprecationWarning
|
||||
ignore::sqlalchemy.exc.SAWarning
|
||||
ignore::DeprecationWarning
|
||||
|
||||
addopts = --ckan-ini test.ini
|
47
setup.py
47
setup.py
|
@ -1,46 +1,3 @@
|
|||
import os
|
||||
from setuptools import setup, find_packages
|
||||
HERE = os.path.dirname(__file__)
|
||||
from setuptools import setup
|
||||
|
||||
version = "2.0.7"
|
||||
|
||||
extras_require = {}
|
||||
_extras_groups = [
|
||||
('requirements', 'requirements.txt'),
|
||||
]
|
||||
for group, filepath in _extras_groups:
|
||||
with open(os.path.join(HERE, filepath), 'r') as f:
|
||||
extras_require[group] = f.readlines()
|
||||
|
||||
# Get the long description from the relevant file
|
||||
with open(os.path.join(HERE, 'README.md'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name="ckanext-googleanalytics",
|
||||
version=version,
|
||||
description="Add GA tracking and reporting to CKAN instance",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
keywords="",
|
||||
author="Seb Bacon",
|
||||
author_email="seb.bacon@gmail.com",
|
||||
url="",
|
||||
license="",
|
||||
packages=find_packages(exclude=["ez_setup", "examples", "tests"]),
|
||||
namespace_packages=["ckanext", "ckanext.googleanalytics"],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=[],
|
||||
extras_require=extras_require,
|
||||
entry_points="""
|
||||
[ckan.plugins]
|
||||
# Add plugins here, eg
|
||||
googleanalytics=ckanext.googleanalytics.plugin:GoogleAnalyticsPlugin
|
||||
|
||||
[paste.paster_command]
|
||||
loadanalytics = ckanext.googleanalytics.commands:LoadAnalytics
|
||||
initdb = ckanext.googleanalytics.commands:InitDB
|
||||
""",
|
||||
)
|
||||
setup()
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
[DEFAULT]
|
||||
debug = false
|
||||
smtp_server = localhost
|
||||
error_email_from = ckan@localhost
|
||||
|
||||
[app:main]
|
||||
use = config:../ckan/test-core.ini
|
||||
|
||||
# Insert any custom config settings to be used when running your extension's
|
||||
# tests here. These will override the one defined in CKAN core's test-core.ini
|
||||
ckan.plugins = googleanalytics
|
||||
|
||||
googleanalytics.id = UA-000000000-1
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root, ckan, sqlalchemy
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
|
||||
[logger_ckan]
|
||||
qualname = ckan
|
||||
handlers =
|
||||
level = INFO
|
||||
|
||||
[logger_sqlalchemy]
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
level = WARN
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s
|
|
@ -1 +0,0 @@
|
|||
<ns0:feed ns1:etag="W/"CkQNRHo4fSp7I2A9WhZTEEw."" ns1:kind="analytics#accounts" xmlns:ns0="http://www.w3.org/2005/Atom" xmlns:ns1="http://schemas.google.com/g/2005"><ns0:updated>2011-03-13T01:59:55.435-08:00</ns0:updated><ns0:id>http://www.google.com/analytics/feeds/accounts/seb.bacon@okfn.org</ns0:id><ns0:generator version="1.0">Google Analytics</ns0:generator><ns0:author><ns0:name>Google Analytics</ns0:name></ns0:author><ns0:link href="https://www.google.com/analytics/feeds/accounts/default?max-results=300" rel="self" type="application/atom+xml" /><ns2:segment id="gaid::-1" name="All Visits" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition> </ns2:definition></ns2:segment><ns2:segment id="gaid::-2" name="New Visitors" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:visitorType==New Visitor</ns2:definition></ns2:segment><ns2:segment id="gaid::-3" name="Returning Visitors" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:visitorType==Returning Visitor</ns2:definition></ns2:segment><ns2:segment id="gaid::-4" name="Paid Search Traffic" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:medium==cpa,ga:medium==cpc,ga:medium==cpm,ga:medium==cpp,ga:medium==cpv,ga:medium==ppc</ns2:definition></ns2:segment><ns2:segment id="gaid::-5" name="Non-paid Search Traffic" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:medium==organic</ns2:definition></ns2:segment><ns2:segment id="gaid::-6" name="Search Traffic" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:medium==cpa,ga:medium==cpc,ga:medium==cpm,ga:medium==cpp,ga:medium==cpv,ga:medium==organic,ga:medium==ppc</ns2:definition></ns2:segment><ns2:segment id="gaid::-7" name="Direct Traffic" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:medium==(none)</ns2:definition></ns2:segment><ns2:segment id="gaid::-8" name="Referral Traffic" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:medium==referral</ns2:definition></ns2:segment><ns2:segment id="gaid::-9" name="Visits with Conversions" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:goalCompletionsAll>0</ns2:definition></ns2:segment><ns2:segment id="gaid::-10" name="Visits with Transactions" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:transactions>0</ns2:definition></ns2:segment><ns2:segment id="gaid::-11" name="Mobile Traffic" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:isMobile==Yes</ns2:definition></ns2:segment><ns2:segment id="gaid::-12" name="Non-bounce Visits" xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:definition>ga:bounces==0</ns2:definition></ns2:segment><ns2:startIndex xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">1</ns2:startIndex><ns2:totalResults xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">1</ns2:totalResults><ns2:itemsPerPage xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">300</ns2:itemsPerPage><ns0:entry ns1:etag="W/"CkQNRHo4fSp7I2A9WhZTEEw."" ns1:kind="analytics#account"><ns0:id>http://www.google.com/analytics/feeds/accounts/ga:42156377</ns0:id><ns2:tableId xmlns:ns2="http://schemas.google.com/analytics/2009">ga:42156377</ns2:tableId><ns0:updated>2011-03-13T01:59:55.435-08:00</ns0:updated><ns2:property name="ga:accountId" value="21313878" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:property name="ga:accountName" value="borf" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:property name="ga:profileId" value="42156377" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:property name="ga:webPropertyId" value="UA-borf-1" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:property name="ga:currency" value="USD" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:property name="ga:timezone" value="Europe/London" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:title>borf</ns0:title><ns0:link href="http://www.google.com/analytics" rel="alternate" type="text/html" /></ns0:entry><ns0:title>Profile list for seb.bacon@okfn.org</ns0:title></ns0:feed>
|
|
@ -1,8 +0,0 @@
|
|||
<ns0:feed ns1:etag="W/"AkYHSXo5eCp7I2A9WhZSGUQ."" ns1:kind="analytics#data" xmlns:ns0="http://www.w3.org/2005/Atom" xmlns:ns1="http://schemas.google.com/g/2005"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&dimensions=ga:pagePath&metrics=ga:newVisits,ga:uniquePageviews,ga:visitors,ga:visits&filters=ga:pagePath%3D~%5E/downloads/&start-date=2011-03-22&end-date=2011-04-05</ns0:id><ns2:dataSource xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:tableName>datagm.staging.ckan.net/</ns2:tableName><ns2:tableId>ga:42156377</ns2:tableId><ns2:property name="ga:profileId" value="42156377" /><ns2:property name="ga:webPropertyId" value="UA-21313878-1" /><ns2:property name="ga:accountName" value="http://datagm.staging.ckan.net/" /></ns2:dataSource><ns2:aggregates xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:metric confidenceInterval="0.0" name="ga:visits" type="integer" value="2" /><ns2:metric confidenceInterval="0.0" name="ga:visitors" type="integer" value="157" /><ns2:metric confidenceInterval="0.0" name="ga:newVisits" type="integer" value="0" /><ns2:metric confidenceInterval="0.0" name="ga:uniquePageviews" type="integer" value="164" /></ns2:aggregates><ns0:updated>2011-04-05T03:08:58.420-07:00</ns0:updated><ns2:containsSampledData xmlns:ns2="http://schemas.google.com/analytics/2009">false</ns2:containsSampledData>
|
||||
|
||||
<ns0:entry ns1:etag="W/"C0EEQX47eSp7I2A9WhZSGUs."" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&ga:pagePath=/downloads/http%3A%2F%2Fwww.annakarenina.com%2Findex.json&filters=ga:pagePath%3D~%5E/downloads/&start-date=2011-03-22&end-date=2011-04-05</ns0:id><ns0:updated>2011-04-04T17:00:00.001-07:00</ns0:updated><ns2:dimension name="ga:pagePath" value="/downloads/http://www.annakarenina.com/index.json" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:title>ga:pagePath=/downloads/http%3A%2F%2Fwww.annakarenina.com%2Findex.json</ns0:title><ns2:metric confidenceInterval="0.0" name="ga:visits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:visitors" type="integer" value="4" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:newVisits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:uniquePageviews" type="integer" value="4" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:link href="http://www.google.com/analytics" rel="alternate" type="text/html" /></ns0:entry>
|
||||
|
||||
|
||||
<ns0:entry ns1:etag="W/"C0EEQX47eSp7I2A9WhZSGUs."" ns1:kind="analytics#datarow"><ns0:id>missingthing</ns0:id><ns0:updated>2011-04-04T17:00:00.001-07:00</ns0:updated><ns2:dimension name="ga:pagePath" value="/downloads/missingthing" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:title>ga:pagePath=/downloads/missingthing</ns0:title><ns2:metric confidenceInterval="0.0" name="ga:visits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:visitors" type="integer" value="3" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:newVisits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:uniquePageviews" type="integer" value="4" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:link href="http://www.google.com/analytics" rel="alternate" type="text/html" /></ns0:entry>
|
||||
|
||||
<ns0:generator version="1.0">Google Analytics</ns0:generator><ns2:startIndex xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">1</ns2:startIndex><ns0:title>Google Analytics Data for Profile 42156377</ns0:title><ns0:author><ns0:name>Google Analytics</ns0:name></ns0:author><ns0:link href="https://www.google.com/analytics/feeds/data?max-results=10000&sort=-ga%3AnewVisits&end-date=2011-04-05&start-date=2011-03-22&metrics=ga%3Avisits%2Cga%3Avisitors%2Cga%3AnewVisits%2Cga%3AuniquePageviews&ids=ga%3A42156377&dimensions=ga%3ApagePath&filters=ga%3ApagePath%3D%7E%5E%2Fdownloads%2F" rel="self" type="application/atom+xml" /><ns2:endDate xmlns:ns2="http://schemas.google.com/analytics/2009">2011-04-05</ns2:endDate><ns2:totalResults xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">43</ns2:totalResults><ns2:startDate xmlns:ns2="http://schemas.google.com/analytics/2009">2011-03-22</ns2:startDate><ns2:itemsPerPage xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">10000</ns2:itemsPerPage></ns0:feed>
|
|
@ -1,61 +0,0 @@
|
|||
import os
|
||||
import BaseHTTPServer
|
||||
import threading
|
||||
|
||||
here_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class MockHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if "feeds/accounts/default" in self.path:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
fixture = os.path.join(here_dir, "accountsfixture.xml")
|
||||
content = open(fixture, "r").read()
|
||||
elif "analytics/feeds/data" in self.path:
|
||||
if "dataset" in self.path:
|
||||
fixture = os.path.join(here_dir, "packagefixture.xml")
|
||||
elif "download" in self.path:
|
||||
fixture = os.path.join(here_dir, "downloadfixture.xml")
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
content = open(fixture, "r").read()
|
||||
else:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
content = "empty"
|
||||
self.wfile.write(content)
|
||||
|
||||
def do_POST(self):
|
||||
if "ClientLogin" in self.path:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
content = "Auth=blah"
|
||||
else:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
content = "empty"
|
||||
self.wfile.write(content)
|
||||
|
||||
def do_QUIT(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.server.stop = True
|
||||
|
||||
|
||||
class ReusableServer(BaseHTTPServer.HTTPServer):
|
||||
allow_reuse_address = 1
|
||||
|
||||
def serve_til_quit(self):
|
||||
self.stop = False
|
||||
while not self.stop:
|
||||
self.handle_request()
|
||||
|
||||
|
||||
def runmockserver():
|
||||
server_address = ("localhost", 6969)
|
||||
httpd = ReusableServer(server_address, MockHandler)
|
||||
httpd_thread = threading.Thread(target=httpd.serve_til_quit)
|
||||
httpd_thread.setDaemon(True)
|
||||
httpd_thread.start()
|
||||
return httpd_thread
|
|
@ -1,9 +0,0 @@
|
|||
<ns0:feed ns1:etag="W/"AkYHRn87fip7I2A9WhZSGUQ."" ns1:kind="analytics#data" xmlns:ns0="http://www.w3.org/2005/Atom" xmlns:ns1="http://schemas.google.com/g/2005"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&dimensions=ga:pagePath&metrics=ga:newVisits,ga:uniquePageviews,ga:visitors,ga:visits&filters=ga:pagePath%3D~%5E/dataset/&start-date=2011-03-22&end-date=2011-04-05</ns0:id><ns2:dataSource xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:tableName>datagm.staging.ckan.net/</ns2:tableName><ns2:tableId>ga:42156377</ns2:tableId><ns2:property name="ga:profileId" value="42156377" /><ns2:property name="ga:webPropertyId" value="UA-21313878-1" /><ns2:property name="ga:accountName" value="http://datagm.staging.ckan.net/" /></ns2:dataSource><ns2:aggregates xmlns:ns2="http://schemas.google.com/analytics/2009"><ns2:metric confidenceInterval="0.0" name="ga:visits" type="integer" value="122" /><ns2:metric confidenceInterval="0.0" name="ga:visitors" type="integer" value="526" /><ns2:metric confidenceInterval="0.0" name="ga:newVisits" type="integer" value="83" /><ns2:metric confidenceInterval="0.0" name="ga:uniquePageviews" type="integer" value="606" /></ns2:aggregates><ns0:updated>2011-04-05T03:08:57.106-07:00</ns0:updated><ns2:containsSampledData xmlns:ns2="http://schemas.google.com/analytics/2009">false</ns2:containsSampledData>
|
||||
|
||||
<ns0:entry ns1:etag="W/"C0EEQX47eSp7I2A9WhZSGUs."" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&ga:pagePath=/dataset/annakarenina&filters=ga:pagePath%3D~%5E/dataset/&start-date=2011-03-22&end-date=2011-04-05</ns0:id><ns0:updated>2011-04-04T17:00:00.001-07:00</ns0:updated><ns2:dimension name="ga:pagePath" value="/dataset/annakarenina" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:title>ga:pagePath=/dataset/annakarenina</ns0:title><ns2:metric confidenceInterval="0.0" name="ga:visits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:visitors" type="integer" value="2" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:newVisits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:uniquePageviews" type="integer" value="2" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:link href="http://www.google.com/analytics" rel="alternate" type="text/html" /></ns0:entry>
|
||||
|
||||
<ns0:entry ns1:etag="W/"C0EEQX47eSp7I2A9WhZSGUs."" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&ga:pagePath=/dataset/annakarenina/invalid&filters=ga:pagePath%3D~%5E/dataset/&start-date=2011-03-22&end-date=2011-04-05</ns0:id><ns0:updated>2011-04-04T17:00:00.001-07:00</ns0:updated><ns2:dimension name="ga:pagePath" value="/dataset/annakarenina/invalid" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:title>ga:pagePath=/dataset/annakarenina/invalid</ns0:title><ns2:metric confidenceInterval="0.0" name="ga:visits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:visitors" type="integer" value="2" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:newVisits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:uniquePageviews" type="integer" value="2" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:link href="http://www.google.com/analytics" rel="alternate" type="text/html" /></ns0:entry>
|
||||
|
||||
<ns0:entry ns1:etag="W/"C0EEQX47eSp7I2A9WhZSGUs."" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&ga:pagePath=/dataset/annakarenina-invalid&filters=ga:pagePath%3D~%5E/dataset/&start-date=2011-03-22&end-date=2011-04-05</ns0:id><ns0:updated>2011-04-04T17:00:00.001-07:00</ns0:updated><ns2:dimension name="ga:pagePath" value="/dataset/annakarenina-invalid" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:title>ga:pagePath=/dataset/annakarenina-invalid</ns0:title><ns2:metric confidenceInterval="0.0" name="ga:visits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:visitors" type="integer" value="2" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:newVisits" type="integer" value="0" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns2:metric confidenceInterval="0.0" name="ga:uniquePageviews" type="integer" value="2" xmlns:ns2="http://schemas.google.com/analytics/2009" /><ns0:link href="http://www.google.com/analytics" rel="alternate" type="text/html" /></ns0:entry>
|
||||
|
||||
<ns0:generator version="1.0">Google Analytics</ns0:generator><ns2:startIndex xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">1</ns2:startIndex><ns0:title>Google Analytics Data for Profile 42156377</ns0:title><ns0:author><ns0:name>Google Analytics</ns0:name></ns0:author><ns0:link href="https://www.google.com/analytics/feeds/data?max-results=10000&sort=-ga%3AnewVisits&end-date=2011-04-05&start-date=2011-03-22&metrics=ga%3Avisits%2Cga%3Avisitors%2Cga%3AnewVisits%2Cga%3AuniquePageviews&ids=ga%3A42156377&dimensions=ga%3ApagePath&filters=ga%3ApagePath%3D%7E%5E%2Fdataset%2F" rel="self" type="application/atom+xml" /><ns2:endDate xmlns:ns2="http://schemas.google.com/analytics/2009">2011-04-05</ns2:endDate><ns2:totalResults xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">152</ns2:totalResults><ns2:startDate xmlns:ns2="http://schemas.google.com/analytics/2009">2011-03-22</ns2:startDate><ns2:itemsPerPage xmlns:ns2="http://a9.com/-/spec/opensearchrss/1.0/">10000</ns2:itemsPerPage></ns0:feed>
|
|
@ -1,122 +0,0 @@
|
|||
import httplib
|
||||
from unittest import TestCase
|
||||
|
||||
from ckan.config.middleware import make_app
|
||||
from paste.deploy import appconfig
|
||||
import paste.fixture
|
||||
from ckan.tests import conf_dir, url_for, CreateTestData
|
||||
|
||||
from mockgoogleanalytics import runmockserver
|
||||
from ckanext.googleanalytics.commands import LoadAnalytics
|
||||
from ckanext.googleanalytics.commands import InitDB
|
||||
from ckanext.googleanalytics import dbutil
|
||||
import ckanext.googleanalytics.gasnippet as gasnippet
|
||||
|
||||
|
||||
class MockClient(httplib.HTTPConnection):
|
||||
def request(self, http_request):
|
||||
filters = http_request.uri.query.get("filters")
|
||||
path = http_request.uri.path
|
||||
if filters:
|
||||
if "dataset" in filters:
|
||||
path += "/dataset"
|
||||
else:
|
||||
path += "/download"
|
||||
httplib.HTTPConnection.request(self, http_request.method, path)
|
||||
resp = self.getresponse()
|
||||
return resp
|
||||
|
||||
|
||||
class TestConfig(TestCase):
|
||||
def test_config(self):
|
||||
config = appconfig("config:test.ini", relative_to=conf_dir)
|
||||
config.local_conf["ckan.plugins"] = "googleanalytics"
|
||||
config.local_conf["googleanalytics.id"] = ""
|
||||
command = LoadAnalytics("loadanalytics")
|
||||
command.CONFIG = config.local_conf
|
||||
self.assertRaises(Exception, command.run, [])
|
||||
|
||||
|
||||
class TestLoadCommand(TestCase):
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
InitDB("initdb").run([]) # set up database tables
|
||||
|
||||
config = appconfig("config:test.ini", relative_to=conf_dir)
|
||||
config.local_conf["ckan.plugins"] = "googleanalytics"
|
||||
config.local_conf["googleanalytics.username"] = "borf"
|
||||
config.local_conf["googleanalytics.password"] = "borf"
|
||||
config.local_conf["googleanalytics.id"] = "UA-borf-1"
|
||||
config.local_conf["googleanalytics.show_downloads"] = "true"
|
||||
cls.config = config.local_conf
|
||||
wsgiapp = make_app(config.global_conf, **config.local_conf)
|
||||
env = {
|
||||
"HTTP_ACCEPT": (
|
||||
"text/html;q=0.9,text/plain;" "q=0.8,image/png,*/*;q=0.5"
|
||||
)
|
||||
}
|
||||
cls.app = paste.fixture.TestApp(wsgiapp, extra_environ=env)
|
||||
CreateTestData.create()
|
||||
runmockserver()
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
CreateTestData.delete()
|
||||
conn = httplib.HTTPConnection("localhost:%d" % 6969)
|
||||
conn.request("QUIT", "/")
|
||||
conn.getresponse()
|
||||
|
||||
def test_analytics_snippet(self):
|
||||
response = self.app.get(url_for(controller="tag", action="index"))
|
||||
code = gasnippet.header_code % (
|
||||
self.config["googleanalytics.id"],
|
||||
"auto",
|
||||
)
|
||||
assert code in response.body
|
||||
|
||||
def test_top_packages(self):
|
||||
command = LoadAnalytics("loadanalytics")
|
||||
command.TEST_HOST = MockClient("localhost", 6969)
|
||||
command.CONFIG = self.config
|
||||
command.run([])
|
||||
packages = dbutil.get_top_packages()
|
||||
resources = dbutil.get_top_resources()
|
||||
self.assertEquals(packages[0][1], 2)
|
||||
self.assertEquals(resources[0][1], 4)
|
||||
|
||||
def test_download_count_inserted(self):
|
||||
command = LoadAnalytics("loadanalytics")
|
||||
command.TEST_HOST = MockClient("localhost", 6969)
|
||||
command.CONFIG = self.config
|
||||
command.run([])
|
||||
response = self.app.get(
|
||||
url_for(controller="package", action="read", id="annakarenina")
|
||||
)
|
||||
assert "[downloaded 4 times]" in response.body
|
||||
|
||||
def test_js_inserted_resource_view(self):
|
||||
from nose import SkipTest
|
||||
|
||||
raise SkipTest("Test won't work until CKAN 1.5.2")
|
||||
|
||||
from ckan.logic.action import get
|
||||
from ckan import model
|
||||
|
||||
context = {"model": model, "ignore_auth": True}
|
||||
data = {"id": "annakarenina"}
|
||||
pkg = get.package_show(context, data)
|
||||
resource_id = pkg["resources"][0]["id"]
|
||||
|
||||
command = LoadAnalytics("loadanalytics")
|
||||
command.TEST_HOST = MockClient("localhost", 6969)
|
||||
command.CONFIG = self.config
|
||||
command.run([])
|
||||
response = self.app.get(
|
||||
url_for(
|
||||
controller="package",
|
||||
action="resource_read",
|
||||
id="annakarenina",
|
||||
resource_id=resource_id,
|
||||
)
|
||||
)
|
||||
assert 'onclick="javascript: _gaq.push(' in response.body
|
Loading…
Reference in New Issue