Permalink
Browse files

Adding in a lot of cool tests and making the async GA code optional.

How to use one or the other is explained in the docs.  Thanks to
Cliff Dyer for the awesome updates and tests!

Merge remote branch 'jcdyer/master'

Conflicts:
	google_analytics/models.py
	google_analytics/templates/google_analytics/analytics_template.html
  • Loading branch information...
2 parents ef5d509 + c8bc881 commit d28bca4de2dc2a955138d36f2ae7ea86917e4867 Clint Ecker committed Dec 4, 2009
View
@@ -1,2 +1,3 @@
*.pyc
-build
+build
+*.swp
View
@@ -0,0 +1,9 @@
+The primary author of thie app is:
+
+ * Clint Ecker <me@clintecker.com>
+
+There are a number of other, awesome contributors:
+
+ * Kevin Fricovsky <http://github.com/howiworkdaily>
+ * J. Cliff Dyer <http://github.com/jcdyer>
+ * Chris Adams <http://github.com/acdha>
View
@@ -12,6 +12,7 @@ be able to manage these Google analytics accounts from the Django admin page.
I also added a mode of operation that excludes the admin interface altogether
(you can just use the template tag)
+
## Two modes of operation ##
### Administering and associating codes with Django `Sites` framework ###
@@ -31,6 +32,81 @@ I also added a mode of operation that excludes the admin interface altogether
2. In your base template, usually a `base.html`, insert this tag at the very top: `{% load analytics %}`
3. In the same template, insert the following code right before the closing body tag: `{% analytics "UA-xxxxxx-x" %}` the `UA-xxxxxx-x` is a unique Google Analytics code for you domain when you sign up for a new account.
+
+## Asynchronous Tracking ##
+
+Google's recent asynchronous tracking API is also supported, as of v0.2. To use it,
+simply call the templatetag `{% analytics_async %}` in the document head instead
+of calling `{% analytics %}` just before the end of the body. You can also use
+`{% analytics_async "UA-xxxxxx-x" %}` as with the old templatetags.
+
+This is being added as an option rather than a replacement for two reasons:
+
+1. Google recommends the Asynchronous Tracking snippet goes in the <head> tag, while
+ the old snippet goes at the end of the <body> tag, so as not to disrupt page loading.
+ Therefore it is not a drop in replacement in your template
+2. The new snippet is reported to [break sites that have a comment before the head tag](http://www.stevesouders.com/blog/2009/12/01/google-analytics-goes-async/#comment-1171).
+ Adding the asynchronous tracking to existing code would cause backwards
+ incompatiblity.
+
+## Supporting other tracking methods ##
+
+Sometimes, the built-in code snippets are not sufficient for what you want to
+do with Google Analytics. You might need to use different access methods,
+or to support more complex Google Analytics functionality. Fortunately, using
+different code snippets is dead easy, and there are two ways to do it.
+
+
+### Overriding the analytics template ###
+
+The easiest way is to override the `'google_analytics/analytics_template.html'`
+template in a template directory that gets loaded before the one in the
+`google_analytics` app.
+
+
+### Registering a new analytics tag ###
+
+You may want to keep the existing snippets around, while adding a new method.
+Perhaps some of your pages need one snippet, but other pages need a different
+one. In this case all you have to do is register a new tag in your tag
+library using `do_get_analytics` like so:
+
+ from django import template
+ from google_analytics.templatetags import analytics
+
+ register = template.Library()
+ register.tag('my_analytics', analytics.do_get_analytics)
+
+Then create a template at `'google_analytics/%(tag_name)s_template.html'`.
+In this case the template name would be
+`'google_analytics/my_analytics_template.html'`. Pass the variable
+`{{ analytics_code }}` to the template wherever you need it.
+
+The new tag will have all the same properties as the default tag, supporting
+site-based analytics codes, as well as explicitly defined codes.
+
+The best way to do this is to create a tiny app just for this purpose, so
+you don't have to modify the code in `google_analytics`. Just put the above
+code in `[app_name]/templatetags/[tag_library_name].py`. Then put your
+template in `[app_name]/templates/google_analytics/[template_name]`. If your
+app is named `my_analytics_app`, your tag library is named `more_analytics`,
+and your tag is registered as `my_analytics`, the resulting app will have a
+directory structure like this:
+
+ my_analytics_app/
+ +-- templatetags/
+ | +-- __init__.py
+ | \-- more_analytics.py
+ \-- templates/
+ \-- google_analytics/
+ \-- my_analytics_template.html
+
+Finally, add `'my_analytics_app'` to `INSTALLED_APPS` in your `settings.py` file. Your new tag is
+ready to go. To use the tag, put `{% load more_analytics %}` at the head of
+your template. You can now access the `{% my_analytics %}` tag the same way
+you would use `{% analytics %}`.
+
+
## License ##
The MIT License
@@ -0,0 +1,18 @@
+[
+ {
+ "pk": 1,
+ "model": "sites.site",
+ "fields": {
+ "domain": "example.com",
+ "name": "example.com"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "google_analytics.analytics",
+ "fields": {
+ "analytics_code": "UA-777777-3",
+ "site": 1
+ }
+ }
+]
@@ -0,0 +1,13 @@
+<script type="text/javascript">
+var _gaq = _gaq || [];
+_gaq.push(['_setAccount', '{{ analytics_code }}']);
+_gaq.push(['_trackPageview']);
+(function() {
+ var ga=document.createElement('script');
+ ga.src=('https:'==document.location.protocol ?
+ 'https://ssl' : 'http://www')+'.google-analytics.com/ga.js';
+ ga.setAttribute('async','true');
+ // Avoid problems if the document doesn't actually contain a HEAD element:
+ var container = document.getElementsByTagName("head")[0] || document.body;
+ container.appendChild(ga);
+</script>
@@ -1,15 +1,9 @@
<script type="text/javascript">
-var _gaq = _gaq || [];
-_gaq.push(['_setAccount', '{{ analytics_code }}']);
-_gaq.push(['_trackPageview']);
-
-(function() {
-var ga = document.createElement('script');
-ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
-ga.setAttribute('async', 'true');
-
-// Avoid problems if the document doesn't actually contain a HEAD element:
-var container = document.getElementsByTagName("head")[0] || document.body;
-container.appendChild(ga);
-})();
-</script>
+var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+</script>
+<script type="text/javascript">
+var pageTracker = _gat._getTracker("{{ analytics_code }}");
+pageTracker._initData();
+pageTracker._trackPageview();
+</script>
@@ -0,0 +1 @@
+Tracking code: {{ analytics_code }}
@@ -9,11 +9,16 @@
Analytics = models.get_model('googleanalytics', 'analytics')
def do_get_analytics(parser, token):
- try:
+ contents = token.split_contents()
+ tag_name = contents[0]
+ template_name = 'google_analytics/%s_template.html' % tag_name
+ if len(contents) == 2:
# split_contents() knows not to split quoted strings.
- tag_name, code = token.split_contents()
- except ValueError:
+ code = contents[1]
+ elif len(contents) == 1:
code = None
+ else:
+ raise template.TemplateSyntaxError, "%r cannot take more than one argument" % tag_name
if not code:
current_site = Site.objects.get_current()
@@ -22,12 +27,14 @@ def do_get_analytics(parser, token):
raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name
code = code[1:-1]
current_site = None
- return AnalyticsNode(current_site, code)
+
+ return AnalyticsNode(current_site, code, template_name)
class AnalyticsNode(template.Node):
- def __init__(self, site=None, code=None):
+ def __init__(self, site=None, code=None, template_name='google_analytics/analytics_template.html'):
self.site = site
self.code = code
+ self.template_name = template_name
def render(self, context):
content = ''
@@ -43,7 +50,7 @@ def render(self, context):
return ''
if code.strip() != '':
- t = loader.get_template('google_analytics/analytics_template.html')
+ t = loader.get_template(self.template_name)
c = Context({
'analytics_code': code,
})
@@ -52,3 +59,4 @@ def render(self, context):
return ''
register.tag('analytics', do_get_analytics)
+register.tag('analytics_async', do_get_analytics)
@@ -0,0 +1 @@
+from test_templatetags import *
@@ -0,0 +1,121 @@
+from django.test import TestCase
+
+from django import template
+from django.contrib.sites.models import Site
+from google_analytics.models import Analytics
+from google_analytics.templatetags import analytics
+
+code7 = 'UA-777777-3' # for fixture-based codes
+code9 = 'UA-999999-1' # for explicit codes
+
+class ParserTest(TestCase):
+ """Test parsing of template tokens"""
+
+ fixtures = ['analytics_test']
+
+ def setUp(self):
+ self.parser = "unused"
+ #################################
+ # Pathological case - Do not test
+ #self.token_null = template.Token(template.TOKEN_BLOCK, "")
+ #################################
+
+ self.token_noarg = template.Token(template.TOKEN_BLOCK, "test")
+ self.token_onearg = template.Token(template.TOKEN_BLOCK, "test '%s'" % code9)
+ self.token_twoarg = template.Token(template.TOKEN_BLOCK, "test '%s' '%s'" % (code9, code7))
+ self.site = Site.objects.get_current()
+
+ def test_basic_return(self):
+ node = analytics.do_get_analytics(self.parser, self.token_noarg)
+ self.assertTrue(isinstance(node, template.Node))
+
+ def _test_null_node_template(self):
+ node = analytics.do_get_analytics(self.parser, self.token_null)
+ self.assertEqual(node.template_name, 'google_analytics/_template.html')
+
+ def test_noarg_node_template(self):
+ node = analytics.do_get_analytics(self.parser, self.token_noarg)
+ self.assertEqual(node.template_name, 'google_analytics/test_template.html')
+
+ def test_onearg_node_template(self):
+ node = analytics.do_get_analytics(self.parser, self.token_onearg)
+ self.assertEqual(node.template_name, 'google_analytics/test_template.html')
+
+ def test_twoarg_node_exception(self):
+ self.assertRaises(template.TemplateSyntaxError, analytics.do_get_analytics, self.parser, self.token_twoarg)
+
+ def test_noarg_site(self):
+ """If no access code is provided, the site will be set to the currently active site"""
+ node = analytics.do_get_analytics(self.parser, self.token_noarg)
+ self.assertEqual(node.site, self.site)
+
+ def test_onearg_site(self):
+ """If an access code is provided, the site will not be set"""
+ node = analytics.do_get_analytics(self.parser, self.token_onearg)
+ self.assertEqual(node.site, None)
+
+
+class NodeTest(TestCase):
+ """Test set-up and rendering of AnalyticsNodes"""
+
+ fixtures = ['analytics_test']
+
+ def setUp(self):
+ self.site = Site.objects.get_current()
+ self.node_noarg = analytics.AnalyticsNode()
+ self.node_code = analytics.AnalyticsNode(code=code9)
+ self.node_explicit_template = analytics.AnalyticsNode(code=code9, template_name='google_analytics/test_template.html')
+ self.node_site = analytics.AnalyticsNode(site=self.site, template_name='google_analytics/test_template.html')
+ self.node_code_and_site = analytics.AnalyticsNode(site=self.site, code=code9, template_name='google_analytics/test_template.html')
+
+ def test_fixture(self):
+ """Fixtures have been loaded"""
+ self.assertNotEqual(Analytics.objects.count(), 0)
+
+ def test_default_template_name(self):
+ self.assertEqual(
+ self.node_code.template_name,
+ 'google_analytics/analytics_template.html'
+ )
+
+ def test_explicit_code_name(self):
+ self.assertEqual(self.node_code.code, code9)
+ self.assertTrue(code9 in self.node_code.render(template.Context()))
+
+ def test_noarg_code_name(self):
+ """If the node is constructed with no code and no site, it will return
+ an empty string"""
+ self.assertEqual(self.node_noarg.code, None)
+ self.assertEqual(self.node_noarg.render(template.Context()), "")
+
+ def test_explicit_template_name(self):
+ self.assertEqual(
+ self.node_explicit_template.template_name,
+ 'google_analytics/test_template.html'
+ )
+ self.assertEqual(
+ self.node_explicit_template.render(template.Context()).strip(),
+ 'Tracking code: %s' % code9
+ )
+
+ def test_defined_site(self):
+ self.assertEqual(self.node_site.site, self.site)
+ self.assertEqual(self.node_site.code, None)
+ self.assertEqual(
+ self.node_site.render(template.Context()).strip(),
+ 'Tracking code: %s' % code7
+ )
+
+ def test_site_overrides_explicit_code(self):
+ """If both code and site are set, the site code will override the
+ explicitly set code. This is contrary to how the tag works, but
+ the parser never passes this combination of arguments."""
+
+ self.assertEqual(self.node_code_and_site.code, code9)
+ self.assertEqual(self.node_code_and_site.site, self.site)
+ self.assertEqual(
+ self.node_code_and_site.render(template.Context()).strip(),
+ 'Tracking code: %s' % code7
+ )
+
+
View
@@ -1,7 +1,7 @@
from distutils.core import setup
setup(name='google_analytics',
- version='0.1',
+ version='0.2',
description='A simple Django application to integrate Google Analytics into your projects',
author='Clint Ecker',
author_email='me@clintecker.com',
@@ -15,4 +15,4 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Utilities'],
- )
+ )

0 comments on commit d28bca4

Please sign in to comment.