Compare commits

...

63 Commits

Author SHA1 Message Date
Sergey 24d9a7ff62
Merge pull request #72 from GSA/HTTPS-GOOGLE-COM
use HTTPS for www.google.com
2023-10-24 14:19:07 +03:00
Fuhu Xia 0c6df3f737 use HTTPS for www.google.com 2023-10-23 12:53:13 -04:00
Sergey 3402d747e7
Merge pull request #71 from nickumia-reisys/fix-actions
Fix Github Actions
2023-08-13 19:31:39 +03:00
Nicholas Kumia 9306edb3b2
lint: get action to pass
lint: add reference for called function
2023-08-11 17:20:30 -04:00
Nicholas Kumia 776a0260cc
update: python actions + version
update: python actions + version
2023-08-11 17:11:46 -04:00
Sergey 77f8567e21
Merge pull request #70 from ckan/Zharktas-patch-1
Fix download_handler config option in readme
2023-06-16 13:21:05 +03:00
Jari Voutilainen 5931ef08ff
Fix download_handler config option in readme 2023-06-15 10:38:05 +03:00
Sergey Motornyuk 84c1cf157e bump version 2023-06-14 20:29:43 +03:00
Sergey Motornyuk 78765390f6 chore(release): 2.3.0 2023-06-14 20:29:01 +03:00
Sergey Motornyuk 2cee663cf3 chore: add tests for tracking mode 2023-06-14 20:28:47 +03:00
Sergey Motornyuk acb7a81bc4 feat: track downloads via MeasurementProtocol 2023-06-14 20:23:38 +03:00
Sergey Motornyuk 5268985575 feat: add GTM tracking support 2023-06-14 18:24:48 +03:00
Sergey Motornyuk 2a74be2e27 chore(release): 2.2.3 2023-06-14 15:51:35 +03:00
Sergey Motornyuk b6b3f91936 chore: update version 2023-06-14 15:50:55 +03:00
Sergey Motornyuk cf75e577cb fix: report empty googleanalytics.id 2023-06-14 15:36:29 +03:00
Sergey Motornyuk 913908fc61 feat: add blocks to gtag snippet 2023-06-14 15:29:36 +03:00
Sergey Motornyuk ef084a2a60 chore(release): 2.2.2 2023-03-03 02:18:06 +02:00
Sergey Motornyuk 2a01b9e354 fix: add config declaration to manifest 2023-03-03 02:17:26 +02:00
Sergey Motornyuk 305371c794 Merge tag 'v2.2.1'
chore(release): 2.2.1
2023-01-17 21:20:31 +02:00
Sergey Motornyuk 56415355aa chore(release): 2.2.1 2023-01-17 21:19:48 +02:00
Sergey Motornyuk 924bc71a33 fix: adapt event_tracking.js to gtag` 2023-01-17 21:19:39 +02:00
Sergey Motornyuk e633655086 remove breakpoint 2022-12-03 17:54:48 +02:00
Sergey Motornyuk 486bb9fa1c Call skip_event before sending data to queue 2022-12-03 17:37:19 +02:00
Sergey Motornyuk d085a7d5c2 chore(release): 2.2.0 2022-12-03 17:29:47 +02:00
Sergey Motornyuk 9f1310af20 feat!: IGoogleAnalytics interface 2022-12-03 17:29:06 +02:00
Sergey Motornyuk 0055c3e063 Extract config 2022-09-09 17:47:05 +03:00
Sergey Motornyuk 8c6c173583 fix: API requests with version are not tracked 2022-08-25 15:51:51 +03:00
Sergey Motornyuk 48fb119c71 typo in mp params 2022-08-24 02:42:27 +03:00
Sergey Motornyuk 11968341d4 update pyproject.toml 2022-08-24 01:55:13 +03:00
Sergey Motornyuk 0748249e1f feat: experimental measurement protol reporting 2022-08-24 01:52:08 +03:00
Sergey Motornyuk 0257299b5a feat: use gtag for G-* IDs 2022-08-23 17:11:41 +03:00
Sergey Motornyuk 12e08b8cd8 CLI error on missing profile 2022-07-29 01:41:29 +03:00
Sergey c2c7b1ce9b
Merge pull request #67 from ckan/2.10-support
CKAN 2.10 support
2022-06-08 19:16:38 +03:00
pdelboca 7ba025c490 Remove 2.7 from test matrix 2022-06-08 11:46:59 +02:00
pdelboca 4455f9ab89 Update Readme 2022-05-30 13:03:01 +02:00
pdelboca 8a135677d4 Clean obsolete code 2022-05-30 12:52:37 +02:00
pdelboca 6882708017 Fix import 2022-05-30 12:41:36 +02:00
pdelboca dc5c5c5f2f Fix more pep8 2022-05-30 12:38:28 +02:00
pdelboca c4dd52fc56 Fix pep8 2022-05-30 12:37:09 +02:00
pdelboca 2bc1d0e0dd Fix pep8 2022-05-30 12:30:05 +02:00
pdelboca 1813413b33 Fix fixture for older versions 2022-05-30 12:26:50 +02:00
pdelboca df5a7d198f Remove broken method 2022-05-30 12:19:43 +02:00
pdelboca 259d9768e2 Fix dev requirements 2.0 2022-05-30 12:17:20 +02:00
pdelboca 1d41a85079 Fix pinned version 2022-05-30 12:12:59 +02:00
pdelboca 4e57fbaef2 Pin pytest in CKAN 2.0 2022-05-30 12:06:34 +02:00
pdelboca 589890ef74 Fix typos 2022-05-30 11:59:02 +02:00
pdelboca 97448b4887 Manage pytest requirements for CKAN versions 2022-05-30 11:42:27 +02:00
pdelboca 483d4d20ce Fix file folder 2022-05-30 11:30:20 +02:00
pdelboca f63b879b18 Restore dev-requirements.txt 2022-05-30 11:27:05 +02:00
pdelboca e223d3c71e Use CKAN Core dev-requirements 2022-05-30 11:22:21 +02:00
pdelboca bd0678b2d5 Add pytest-cov 2022-05-30 11:14:23 +02:00
pdelboca f678a70842 Use toolkit to access action 2022-05-30 10:12:52 +02:00
pdelboca 6d55c38b33 Install requirements on tests 2022-05-30 10:08:35 +02:00
Sergey Motornyuk 34171b12a1 Handle empty rows 2022-05-08 04:22:12 +03:00
Sergey Motornyuk ae59b35121 Fix auth functions 2022-05-08 02:03:28 +03:00
Sergey Motornyuk 885a05fea4 Optional filters 2022-05-07 02:07:23 +03:00
Sergey Motornyuk 67b599ce1a Add report action 2022-05-07 01:59:15 +03:00
Sergey Motornyuk 86ec2858a5 Add actions 2022-05-06 21:50:26 +03:00
Sergey Motornyuk 1d48695a93 Disable test coverage 2022-05-06 19:38:54 +03:00
Sergey Motornyuk dc75b4c472 Configure tests 2022-05-06 19:28:52 +03:00
Sergey Motornyuk 7f5c2c03d9 Add config declarations 2022-05-06 19:01:29 +03:00
Sergey Motornyuk b70b26f392 Prepare to v2.10 2022-05-06 17:29:58 +03:00
Sergey Motornyuk 4991ca7440 [#64] py2 compatible open 2022-01-05 13:10:08 +02:00
55 changed files with 1393 additions and 680 deletions

75
.github/workflows/test.yml vendored Normal file
View File

@ -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

3
.gitignore vendored
View File

@ -6,5 +6,6 @@ syntax: glob
*~
build/
dist/
.mypy_cache/
credentials.json
token.dat
token.dat

48
CHANGELOG.md Normal file
View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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);
}
}
});
}
});
}
};
};
});

View File

@ -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]

View File

@ -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

View File

@ -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
)
)

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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>
"""

View File

@ -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()

View File

@ -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

View File

@ -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", []),
}

View File

@ -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)

View File

@ -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,
],
}

View File

@ -0,0 +1 @@
Generic single-database configuration.

View File

@ -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

View File

@ -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()

View File

@ -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"}

View File

@ -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),
)

View File

@ -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)

View File

@ -0,0 +1,4 @@
from sqlalchemy.ext.declarative import declarative_base
import ckan.model.meta as meta
Base = declarative_base(metadata=meta.metadata)

View File

@ -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)

View File

@ -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)

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 -->

View File

@ -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' %}

View File

@ -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 %}

View File

@ -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")

View File

@ -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"]

View File

@ -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()

View File

@ -0,0 +1,7 @@
import pytest
@pytest.mark.usefixtures("clean_db")
def test_script(app):
resp = app.get("/")
assert "GoogleAnalyticsObject" in resp

View File

@ -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

View File

@ -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

View File

@ -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)

4
dev-requirements-2.9.txt Normal file
View File

@ -0,0 +1,4 @@
pytest<=6.0.2
pytest-ckan
pytest-factoryboy
pytest-cov

1
dev-requirements.txt Normal file
View File

@ -0,0 +1 @@
pytest-ckan

View File

@ -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

77
setup.cfg Normal file
View File

@ -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

View File

@ -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()

46
test.ini Normal file
View File

@ -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

View File

@ -1 +0,0 @@
<ns0:feed ns1:etag="W/&quot;CkQNRHo4fSp7I2A9WhZTEEw.&quot;" 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&gt;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&gt;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/&quot;CkQNRHo4fSp7I2A9WhZTEEw.&quot;" 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>

View File

@ -1,8 +0,0 @@
<ns0:feed ns1:etag="W/&quot;AkYHSXo5eCp7I2A9WhZSGUQ.&quot;" 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&amp;dimensions=ga:pagePath&amp;metrics=ga:newVisits,ga:uniquePageviews,ga:visitors,ga:visits&amp;filters=ga:pagePath%3D~%5E/downloads/&amp;start-date=2011-03-22&amp;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/&quot;C0EEQX47eSp7I2A9WhZSGUs.&quot;" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&amp;ga:pagePath=/downloads/http%3A%2F%2Fwww.annakarenina.com%2Findex.json&amp;filters=ga:pagePath%3D~%5E/downloads/&amp;start-date=2011-03-22&amp;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/&quot;C0EEQX47eSp7I2A9WhZSGUs.&quot;" 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&amp;sort=-ga%3AnewVisits&amp;end-date=2011-04-05&amp;start-date=2011-03-22&amp;metrics=ga%3Avisits%2Cga%3Avisitors%2Cga%3AnewVisits%2Cga%3AuniquePageviews&amp;ids=ga%3A42156377&amp;dimensions=ga%3ApagePath&amp;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>

View File

@ -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

View File

@ -1,9 +0,0 @@
<ns0:feed ns1:etag="W/&quot;AkYHRn87fip7I2A9WhZSGUQ.&quot;" 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&amp;dimensions=ga:pagePath&amp;metrics=ga:newVisits,ga:uniquePageviews,ga:visitors,ga:visits&amp;filters=ga:pagePath%3D~%5E/dataset/&amp;start-date=2011-03-22&amp;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/&quot;C0EEQX47eSp7I2A9WhZSGUs.&quot;" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&amp;ga:pagePath=/dataset/annakarenina&amp;filters=ga:pagePath%3D~%5E/dataset/&amp;start-date=2011-03-22&amp;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/&quot;C0EEQX47eSp7I2A9WhZSGUs.&quot;" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&amp;ga:pagePath=/dataset/annakarenina/invalid&amp;filters=ga:pagePath%3D~%5E/dataset/&amp;start-date=2011-03-22&amp;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/&quot;C0EEQX47eSp7I2A9WhZSGUs.&quot;" ns1:kind="analytics#datarow"><ns0:id>http://www.google.com/analytics/feeds/data?ids=ga:42156377&amp;ga:pagePath=/dataset/annakarenina-invalid&amp;filters=ga:pagePath%3D~%5E/dataset/&amp;start-date=2011-03-22&amp;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&amp;sort=-ga%3AnewVisits&amp;end-date=2011-04-05&amp;start-date=2011-03-22&amp;metrics=ga%3Avisits%2Cga%3Avisitors%2Cga%3AnewVisits%2Cga%3AuniquePageviews&amp;ids=ga%3A42156377&amp;dimensions=ga%3ApagePath&amp;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>

View File

@ -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