8 |
{{ report.project.title }}
9 |
10 |
{{ report.timespan }} report
11 |
12 |
This report was last run on {{ report.last_run|localtime }}
13 |
14 |
« Back to project
15 |
16 | {% for query_result in report.query_results.all %}
17 |
18 |
{{ query_result.query_name }}{% if query_result.query.is_comparable %} (compare){% endif %}
19 |
20 |
{{ query_result.query.description }}
21 |
22 | {% for metric in query_result.metrics.all %}
23 |
24 |
25 | {{ metric.display_name }}
26 | {{ metric.name }}
27 |
28 |
29 |
30 |
31 | | Dimension |
32 | Value |
33 | Percent of total |
34 |
35 |
36 |
37 | {% for dimension in metric.dimensions.all %}
38 | {% include '_report_dimension.html' with dimension=dimension %}
39 | {% endfor %}
40 |
41 |
42 |
43 | {% endfor %}
44 |
45 | {% if query_result.sampled %}
46 |
Query results based on a sample of {{ query_result.sample_percent|floatformat:"1" }}% of sessions.
47 | {% endif %}
48 |
49 |
50 | {% endfor %}
51 |
52 | {% endblock %}
53 |
--------------------------------------------------------------------------------
/carebot/settings.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import app_config
4 |
5 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
6 | import os
7 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
8 |
9 | SECRET_KEY = '=_1*x-_9+d_5xn#$cx%yap+@y-#13%1=1$lay5@c#^f%-u2nj-'
10 | DEBUG = app_config.DEBUG
11 | TEMPLATE_DEBUG = app_config.DEBUG
12 | ALLOWED_HOSTS = []
13 |
14 | INSTALLED_APPS = (
15 | 'grappelli',
16 | 'django.contrib.admin',
17 | 'django.contrib.auth',
18 | 'django.contrib.contenttypes',
19 | 'django.contrib.sessions',
20 | 'django.contrib.messages',
21 | 'django.contrib.staticfiles',
22 | 'django.contrib.humanize',
23 | 'reports'
24 | )
25 |
26 | MIDDLEWARE_CLASSES = (
27 | 'django.contrib.sessions.middleware.SessionMiddleware',
28 | 'django.middleware.common.CommonMiddleware',
29 | 'django.middleware.csrf.CsrfViewMiddleware',
30 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
31 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
32 | 'django.contrib.messages.middleware.MessageMiddleware',
33 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
34 | )
35 |
36 | TEMPLATE_CONTEXT_PROCESSORS = (
37 | 'django.core.context_processors.request',
38 | 'django.contrib.auth.context_processors.auth'
39 | )
40 |
41 | ROOT_URLCONF = 'carebot.urls'
42 | WSGI_APPLICATION = 'carebot.wsgi.application'
43 |
44 | secrets = app_config.get_secrets()
45 |
46 | DATABASES = {
47 | 'default': {
48 | 'ENGINE': 'django.db.backends.postgresql_psycopg2',
49 | 'NAME': app_config.PROJECT_SLUG,
50 | 'USER': secrets.get('POSTGRES_USER') or app_config.PROJECT_SLUG,
51 | 'PASSWORD': secrets.get('POSTGRES_PASSWORD') or None,
52 | 'HOST': secrets.get('POSTGRES_HOST') or 'localhost',
53 | 'PORT': secrets.get('POSTGRES_PORT') or '5432'
54 | }
55 | }
56 |
57 | LANGUAGE_CODE = 'en-us'
58 | TIME_ZONE = 'America/New_York'
59 | USE_I18N = True
60 | USE_L10N = True
61 | USE_TZ = True
62 |
63 | STATIC_URL = '/%s/static/' % app_config.PROJECT_SLUG
64 | STATIC_ROOT = 'static'
65 |
66 | GRAPPELLI_ADMIN_TITLE = 'carebot'
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Copyright 2014 NPR. All rights reserved. No part of these materials may be reproduced, modified, stored in a retrieval system, or retransmitted, in any form or by any means, electronic, mechanical or otherwise, without prior written permission from NPR.
2 |
3 | (Want to use this code? Send an email to nprapps@npr.org!)
4 |
5 |
6 | carebot
7 | ========================
8 |
9 | * [What is this?](#what-is-this)
10 | * [Assumptions](#assumptions)
11 | * [Bootstrap the project](#bootstrap-the-project)
12 | * [Hide project secrets](#hide-project-secrets)
13 | * [Run the project](#run-the-project)
14 | * [Deploy to EC2](#deploy-to-ec2)
15 | * [Run a remote fab command](#run-a-remote-fab-command)
16 |
17 | What is this?
18 | -------------
19 |
20 | Carebot cares about us so much it automatically reports out, summarizes and sends us our analytics.
21 |
22 | For documentation of the metrics and queries used see the [reports](https://github.com/nprapps/reports#google-metrics-we-care-about) repo.
23 |
24 | Assumptions
25 | -----------
26 |
27 | The following things are assumed to be true in this documentation.
28 |
29 | * You are running OSX.
30 | * You are using Python 2.7. (Probably the version that came OSX.)
31 | * You have [virtualenv](https://pypi.python.org/pypi/virtualenv) and [virtualenvwrapper](https://pypi.python.org/pypi/virtualenvwrapper) installed and working.
32 | * You have NPR's AWS credentials stored as environment variables locally.
33 |
34 | For more details on the technology stack used with the app-template, see our [development environment blog post](http://blog.apps.npr.org/2013/06/06/how-to-setup-a-developers-environment.html).
35 |
36 | Bootstrap the project
37 | ---------------------
38 |
39 | ```
40 | cd carebot
41 | mkvirtualenv carebot
42 | pip install -r requirements.txt
43 | fab data.local_reset_db data.bootstrap_db
44 | python manage.py collectstatic
45 | ```
46 |
47 | **Problems installing requirements?** You may need to run the pip command as ``ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future pip install -r requirements.txt`` to work around an issue with OSX.
48 |
49 | Hide project secrets
50 | --------------------
51 |
52 | Project secrets should **never** be stored in ``app_config.py`` or anywhere else in the repository. They will be leaked to the client if you do. Instead, always store passwords, keys, etc. in environment variables and document that they are needed here in the README.
53 |
54 | Run the project
55 | ---------------
56 |
57 | ```
58 | workon $PROJECT_SLUG
59 | fab public_app
60 | ```
61 |
62 | Visit [localhost:8000](http://localhost:8000) in your browser.
63 |
64 | Deploy to EC2
65 | -------------
66 |
67 | One time setup:
68 |
69 | ```
70 | fab staging master servers.setup
71 | fab staging master data.server_reset_db
72 | fab staging master servers.fabcast:data.bootstrap_db
73 | ```
74 |
75 | Routine deployment:
76 |
77 | ```
78 | fab staging master deploy
79 | ```
80 |
81 | Run a remote fab command
82 | -------------------------
83 |
84 | Sometimes it makes sense to run a fabric command on the server, for instance, when you need to render using a production database. You can do this with the `fabcast` fabric command. For example:
85 |
86 | ```
87 | fab staging master servers.fabcast:cron_jobs.run_reports
88 | ```
89 |
90 | If any of the commands you run themselves require executing on the server, the server will SSH into itself to run them.
91 |
--------------------------------------------------------------------------------
/fabfile/data.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Commands that update or process the application data.
5 | """
6 | import csv
7 | from datetime import datetime
8 | from glob import glob
9 | import os
10 | import yaml
11 |
12 | from django.utils.text import slugify
13 | from fabric.api import local, settings, run, sudo, task
14 |
15 | import app_config
16 | import servers
17 | from reports.models import Project, Query, Tag
18 |
19 | SERVER_POSTGRES_CMD = 'export PGPASSWORD=$carebot_POSTGRES_PASSWORD && %s --username=$carebot_POSTGRES_USER --host=$carebot_POSTGRES_HOST --port=$carebot_POSTGRES_PORT'
20 |
21 | @task
22 | def server_reset_db():
23 | """
24 | Reset the database on a server.
25 | """
26 | with settings(warn_only=True):
27 | services = ['uwsgi']
28 | for service in services:
29 | service_name = servers._get_installed_service_name(service)
30 | sudo('service %s stop' % service_name)
31 |
32 | run(SERVER_POSTGRES_CMD % ('dropdb %s' % app_config.PROJECT_SLUG))
33 | run(SERVER_POSTGRES_CMD % ('createdb %s' % app_config.PROJECT_SLUG))
34 |
35 | for service in services:
36 | service_name = servers._get_installed_service_name(service)
37 | sudo('service %s start' % service_name)
38 |
39 | @task
40 | def migrate_db():
41 | local('python manage.py migrate')
42 |
43 | @task
44 | def local_reset_db():
45 | secrets = app_config.get_secrets()
46 |
47 | with settings(warn_only=True):
48 | local('dropdb %s' % app_config.PROJECT_SLUG)
49 | local('echo "CREATE USER %s WITH PASSWORD \'%s\';" | psql' % (app_config.PROJECT_SLUG, secrets['POSTGRES_PASSWORD']))
50 |
51 | local('createdb -O %s %s' % (app_config.PROJECT_SLUG, app_config.PROJECT_SLUG))
52 |
53 | @task
54 | def bootstrap_db():
55 | local('python manage.py migrate')
56 | local('python manage.py loaddata data/test_user.json')
57 |
58 | for yaml_path in glob('data/queries/*.yaml'):
59 | path, filename = os.path.split(yaml_path)
60 | slug, ext = os.path.splitext(filename)
61 |
62 | with open(yaml_path, 'r') as f:
63 | data = yaml.load(f)
64 |
65 | q = Query(
66 | name=data['name'],
67 | description=data.get('description', ''),
68 | slug=slug,
69 | )
70 |
71 | del data['name']
72 |
73 | if 'description' in data:
74 | q.description = data['description']
75 | del data['description']
76 |
77 | q.clan_yaml = yaml.dump(data, indent=4)
78 |
79 | q.save()
80 |
81 | with open('data/projects.csv') as f:
82 | rows = csv.DictReader(f)
83 |
84 | for row in rows:
85 | p = Project.objects.create(
86 | title=row['title'],
87 | slug=slugify(unicode(row['title'])),
88 | property_id=row['property_id'],
89 | domain=row['domain'],
90 | prefix=row['prefix'],
91 | start_date=datetime.strptime(row['start_date'], '%Y-%m-%d').date()
92 | )
93 |
94 | for tag in row['tags'].split(','):
95 | obj, created = Tag.objects.get_or_create(slug=tag)
96 | p.tags.add(obj)
97 |
98 | @task
99 | def rerun(slug):
100 | """
101 | Force a project to rerun all its reports.
102 | """
103 | project = Project.objects.get(slug=slug)
104 |
105 | project.run_reports(overwrite=True)
106 | project.social.refresh()
107 |
--------------------------------------------------------------------------------
/fabfile/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from fabric.api import local, require, settings, task
4 | from fabric.state import env
5 | from termcolor import colored
6 |
7 | from fabric.contrib import django
8 | django.project('carebot')
9 |
10 | import django
11 | django.setup()
12 |
13 | import app_config
14 |
15 | # Other fabfiles
16 | import data
17 | import utils
18 |
19 | if app_config.DEPLOY_TO_SERVERS:
20 | import servers
21 |
22 | if app_config.DEPLOY_CRONTAB:
23 | import cron_jobs
24 |
25 | """
26 | Base configuration
27 | """
28 | env.user = app_config.SERVER_USER
29 | env.forward_agent = True
30 |
31 | env.hosts = []
32 | env.settings = None
33 |
34 | """
35 | Environments
36 |
37 | Changing environment requires a full-stack test.
38 | """
39 | @task
40 | def production():
41 | """
42 | Run as though on production.
43 | """
44 | env.settings = 'production'
45 | app_config.configure_targets(env.settings)
46 | env.hosts = app_config.SERVERS
47 |
48 | @task
49 | def staging():
50 | """
51 | Run as though on staging.
52 | """
53 | env.settings = 'staging'
54 | app_config.configure_targets(env.settings)
55 | env.hosts = app_config.SERVERS
56 |
57 | """
58 | Branches
59 |
60 | Changing branches requires deploying that branch to a host.
61 | """
62 | @task
63 | def stable():
64 | """
65 | Work on stable branch.
66 | """
67 | env.branch = 'stable'
68 |
69 | @task
70 | def master():
71 | """
72 | Work on development branch.
73 | """
74 | env.branch = 'master'
75 |
76 | @task
77 | def branch(branch_name):
78 | """
79 | Work on any specified branch.
80 | """
81 | env.branch = branch_name
82 |
83 | """
84 | Running the app
85 | """
86 | @task
87 | def public_app(port='8000'):
88 | """
89 | Serve public_app.py.
90 | """
91 | local('gunicorn -b 0.0.0.0:%s --timeout 3600 --debug --reload --error-logfile - carebot.wsgi:application' % port)
92 |
93 | """
94 | Deployment
95 |
96 | Changes to deployment requires a full-stack test.
97 | """
98 | @task
99 | def deploy(remote='origin'):
100 | """
101 | Deploy the latest app to our servers.
102 | """
103 | require('settings', provided_by=[production, staging])
104 |
105 | if app_config.DEPLOY_TO_SERVERS:
106 | require('branch', provided_by=[stable, master, branch])
107 |
108 | if (app_config.DEPLOYMENT_TARGET == 'production' and env.branch != 'stable'):
109 | utils.confirm(
110 | colored("You are trying to deploy the '%s' branch to production.\nYou should really only deploy a stable branch.\nDo you know what you're doing?" % env.branch, "red")
111 | )
112 |
113 | servers.checkout_latest(remote)
114 |
115 | if app_config.DEPLOY_CRONTAB:
116 | servers.install_crontab()
117 |
118 | if app_config.DEPLOY_SERVICES:
119 | servers.deploy_confs()
120 |
121 | """
122 | Destruction
123 |
124 | Changes to destruction require setup/deploy to a test host in order to test.
125 | """
126 | @task
127 | def shiva_the_destroyer():
128 | """
129 | Deletes the app.
130 | """
131 | require('settings', provided_by=[production, staging])
132 |
133 | utils.confirm(
134 | colored("You are about to destroy everything deployed to %s for this project.\nDo you know what you're doing?')" % app_config.DEPLOYMENT_TARGET, "red")
135 | )
136 |
137 | with settings(warn_only=True):
138 | if app_config.DEPLOY_TO_SERVERS:
139 | servers.delete_project()
140 |
141 | if app_config.DEPLOY_CRONTAB:
142 | servers.uninstall_crontab()
143 |
144 | if app_config.DEPLOY_SERVICES:
145 | servers.nuke_confs()
146 |
147 |
--------------------------------------------------------------------------------
/reports/admin.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from django.contrib import admin
4 | from grappelli.forms import GrappelliSortableHiddenMixin
5 |
6 | from reports import models
7 |
8 | class QueryAdmin(admin.ModelAdmin):
9 | """
10 | Admin for the Query model.
11 | """
12 | list_display = ('name', 'is_comparable', 'description')
13 |
14 | fieldsets = (
15 | (None, {
16 | 'fields': ('name', 'slug', 'description', 'is_comparable')
17 | }),
18 | (None, {
19 | 'fields': ('clan_yaml',),
20 | 'classes': ('monospace',)
21 | })
22 | )
23 |
24 | prepopulated_fields = { 'slug': ('name',) }
25 |
26 | class TagInline(admin.TabularInline):
27 | model = models.Project.tags.through
28 | extra = 2
29 |
30 | class ProjectQueryInline(GrappelliSortableHiddenMixin, admin.TabularInline):
31 | """
32 | Admin for the ProjectQuery M2M inline.
33 | """
34 | model = models.ProjectQuery
35 | extra = 3
36 | sortable_field_name = 'order'
37 |
38 | class ProjectAdmin(admin.ModelAdmin):
39 | """
40 | Admin for the Project model.
41 | """
42 | fields = ('title', 'slug', 'property_id', 'domain', 'prefix', 'start_date')
43 | prepopulated_fields = { 'slug': ('title',) }
44 |
45 | list_display = ('title', 'tag_list', 'property_id', 'domain', 'prefix', 'start_date', 'view_reports')
46 | list_display_links = ('title',)
47 | list_filter = ('property_id', 'domain')
48 | search_fields = ('title',)
49 |
50 | def change_view(self, *args, **kwargs):
51 | """
52 | Change view, with inlines.
53 | """
54 | self.inlines = (TagInline, ProjectQueryInline,)
55 |
56 | return super(ProjectAdmin, self).change_view(*args, **kwargs)
57 |
58 | def add_view(self, *args, **kwargs):
59 | """
60 | Add view, without inlines.
61 | """
62 | self.inlines = (TagInline,)
63 |
64 | return super(ProjectAdmin, self).add_view(*args, **kwargs)
65 |
66 | def tag_list(self, model):
67 | return model.tag_list()
68 |
69 | tag_list.short_description = 'Tags'
70 |
71 | def view_reports(self, model):
72 | return '
7 |
Query Comparison
8 |
9 |
« Back to project index
10 |
11 |
52 |
53 | {% if query %}
54 |
Configuration
55 |
56 |
57 |
58 |
59 | | Query |
60 | {{ query.name }} |
61 |
62 |
63 | | Unit |
64 | {{ unit }} |
65 |
66 |
67 | | Timespan |
68 | {% if ndays %}{{ ndays }}-day{{ ndays|pluralize:"s"}}{% else %}all-time{% endif %} |
69 |
70 |
71 | | Tagged |
72 | {% if tag %}{{ tag.slug }}{% else %}(any){% endif %} |
73 |
74 |
75 |
76 |
77 |
{{ query.description }}
78 |
79 | {% for metric_names, projects in results.items %}
80 |
81 | {{ metric_names.1 }}
82 | {{ metric_names.0 }}
83 |
84 |
85 |
86 |
87 |
88 | | Project |
89 | {% for dimension_name in metric_dimensions|keyvalue:metric_names.0 %}
90 | {{ dimension_name }} |
91 | {% endfor %}
92 |
93 |
94 |
95 | {% for project_name, project_dimensions in projects.items %}
96 |
97 | | {{ project_name }} |
98 | {% for dimension_name in metric_dimensions|keyvalue:metric_names.0 %}
99 | {% include '_compare_dimension.html' %}
100 | {% endfor %}
101 |
102 | {% endfor %}
103 |
104 |
105 | {% endfor %}
106 | {% endif %}
107 |
108 |
109 |
112 | {% endblock %}
113 |
--------------------------------------------------------------------------------
/data/projects.csv:
--------------------------------------------------------------------------------
1 | title,tags,property_id,domain,prefix,start_date,default_queries
2 | Demolished: The End of Chicago's Public Housing,"lookatthis,slides",53470309,apps.npr.org,/lookatthis/posts/publichousing/,2014-12-23,complex
3 | Naughty or Nice,"app,audio",53470309,apps.npr.org,/naughty-or-nice/,2014-12-21,complex
4 | Songs We Love 2014,"app,music,audio",53470309,apps.npr.org,/best-songs-2014/,2014-12-10,complex
5 | Book Concierge 2014,"app,books",53470309,apps.npr.org,/best-books-2014/,2014-12-03,complex
6 | A Photo I Love: Erin Mystkowski,"lookatthis,audio",53470309,apps.npr.org,/lookatthis/posts/mystkowski-loves/,2014-11-18,complex
7 | Election Party 2014,"app,politics,live,tv",53470309,elections.npr.org,/,2014-11-04,complex
8 | Talking While Female,"seamus,video",53470309,www.npr.org,/blogs/health/2014/10/24/357584372/video-what-women-get-flak-for-when-they-talk,2014-10-24,complex
9 | This Is Color,"lookatthis,slides",53470309,apps.npr.org,/lookatthis/posts/colors/,2014-10-23,complex
10 | Plastic Rebirth,"lookatthis,slides",53470309,apps.npr.org,/lookatthis/posts/plastic/,2014-10-09,complex
11 | What Do Homeless Veteran's Look Like?,"lookatthis,slides",53470309,apps.npr.org,/lookatthis/posts/veterans/,2014-09-17,complex
12 | The End of Neighborhood Schools,"app,scroll",53470309,apps.npr.org,/the-end-of-neighborhood-schools/,2014-09-02,complex
13 | Behind the Civil Rights Act,app,53470309,apps.npr.org,/behind-the-civil-rights-act/,2014-07-02,complex
14 | "The Best Commencement Speeches, Ever",app,53470309,apps.npr.org,/commencement/,2014-05-19,simple
15 | 12 Weeks To A 6-Figure Job,"seamus,graphic",53470309,www.npr.org,/blogs/ed/2014/12/20/370954988/twelve-weeks-to-a-six-figure-job,2014-12-20,simple
16 | Buzkashi,"app,scroll",53470309,apps.npr.org,/buzkashi/,2014-05-04,simple
17 | Borderland,"app,slides",53470309,apps.npr.org,/borderland/,2014-04-03,simple
18 | Grave Science,"app,scroll",53470309,apps.npr.org,/grave-science/,2014-03-05,simple
19 | Wolves At The Door,"app,scroll",53470309,apps.npr.org,/wolves/,2014-02-03,simple
20 | Oil Boom,"seamus,gif",53470309,www.npr.org,/2014/01/29/266757131/welcome-to-oil-country-a-modern-day-gold-rush-in-north-dakota,2014-01-29,simple
21 | In Memoriam 2013,"app,music",53470309,apps.npr.org,/music-memoriam-2013/,2013-12-19,simple
22 | Your Questions About the Affordable Care Act,"app,faq",53470309,apps.npr.org,/affordable-care-act-questions/,2013-12-18,simple
23 | Book Concierge 2013,"app,books",53470309,apps.npr.org,/best-books-2013/,2013-12-04,simple
24 | Planet Money Makes A T-Shirt,"app,video,ugc",53470309,apps.npr.org,/tshirt/,2013-11-25,simple
25 | Lobbying Missuori,"app,station,politics,evergreen",53470309,www.lobbyingmissouri.org,/,2013-11-04,simple
26 | Playgrounds For Everyone,"app,evergreen",53470309,apps.npr.org,/playgrounds/,2013-08-27,simple
27 | Okkervil River,"app,music,audio",53470309,apps.npr.org,/okkervil-river/,2013-07-15,simple
28 | Zoom In On Oklahoma Tornado Damage,"app,breaking",53470309,apps.npr.org,/moore-oklahoma-tornado-damage/,2013-05-23,simple
29 | Deals For Developers,"app,politics,station",53470309,apps.npr.org,/deals-for-developers-wamu/,2013-05-20,simple
30 | Previously On Arrested Development,app,53470309,apps.npr.org,/arrested-development/,2013-05-17,simple
31 | Teenage Diaries,app,53470309,apps.npr.org,/teenage-diaries/,2013-05-10,simple
32 | She Works,"app,tumblr,ugc",53470309,she-works.tumblr.com,/,2013-05-06,simple
33 | Cook Your Cupboard,"app,tumblr,ugc",53470309,cookyourcupboard.tumblr.com,/,2013-04-02,simple
34 | Buried In Grain,"app,scroll",53470309,apps.npr.org,/buried-in-grain/,2013-03-24,simple
35 | Unfit For Work,"app,scroll",53470309,apps.npr.org,/unfit-for-work/,2013-03-22,simple
36 | Dinnertime Confessional,"app,tumblr",53470309,dinnertimeconfessional.tumblr.com,/,2013-02-26,simple
37 | Oscar Night Live Coverage,"app,live",53470309,apps.npr.org,/oscars-2013/,2013-02-22,simple
38 | Inauguration Live,"app,live",53470309,apps.npr.org,/inauguration/,2013-01-18,simple
39 | Sotomayor: From The Bronx to The Bench,"app,audio,slides",53470309,apps.npr.org,/sotomayor-family-photos/,2013-01-12,simple
40 | Dear Mr. President,"app,tumblr,ugc",53470309,inauguration2013.tumblr.com,/,2013-01-09,simple
41 | Bob Boilen's Wristbands,"app,music",53470309,apps.npr.org,/bob-boilens-wristbands-2012/,2012-12-31,simple
42 | In Memoriam 2012,"app,audio,slides",53470309,apps.npr.org,/music-memoriam-2012/,2012-12-24,simple
43 | Election Night 2012,"app,politics,live",53470309,elections2012.npr.org,/,2012-11-06,simple
44 | Election 2012: Early Voting,"app,politics",53470309,apps.npr.org,/early-voting-2012/,2012-09-25,simple
45 | Democratic National Convention,"app,live",53470309,apps.npr.org,/2012-democratic-national-convention/,2012-09-02,simple
46 | Fire Forecast,"app,evergreen",53470309,apps.npr.org,/fire-forecast/,2012-08-23,simple
47 |
--------------------------------------------------------------------------------
/reports/views.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from collections import OrderedDict
4 |
5 | from django.shortcuts import render
6 | from django.template.defaulttags import register
7 |
8 | import app_config
9 | from reports import models
10 |
11 | @register.filter
12 | def keyvalue(dict, key):
13 | return dict[key]
14 |
15 | def index(request):
16 | """
17 | Project index.
18 | """
19 | projects = models.Project.objects.all()
20 |
21 | context = {
22 | 'projects': projects
23 | }
24 |
25 | return render(request, 'index.html', context)
26 |
27 | def project(request, slug):
28 | """
29 | Project report index.
30 | """
31 | obj = models.Project.objects.get(slug=slug)
32 |
33 | all_shares = []
34 | all_shares_per_session = []
35 | socials = models.Social.objects.all()
36 |
37 | for social in socials:
38 | total = social.total()
39 |
40 | if total:
41 | all_shares.append(total)
42 | try:
43 | all_shares_per_session.append(float(total) / (float(social.project.all_time_report.sessions) / 1000))
44 | except ZeroDivisionError:
45 | all_shares_per_session.append('undefined')
46 | except TypeError:
47 | all_shares_per_session.append('undefined')
48 |
49 | try:
50 | shares_per_session = float(obj.social.total()) / (float(obj.all_time_report.sessions) / 1000)
51 | except ZeroDivisionError:
52 | shares_per_session = 'undefined'
53 | except TypeError:
54 | shares_per_session = 'undefined'
55 |
56 | context = {
57 | 'project': obj,
58 | 'reports': obj.reports.exclude(last_run__isnull=True),
59 | 'all_shares': all_shares,
60 | 'all_shares_per_session': all_shares_per_session,
61 | 'shares_per_session': shares_per_session
62 | }
63 |
64 | return render(request, 'project.html', context)
65 |
66 | def report(request, slug, ndays=None):
67 | """
68 | Generate a project report.
69 | """
70 | if ndays == 'all-time':
71 | ndays = None
72 |
73 | obj = models.Report.objects.get(
74 | project__slug=slug,
75 | ndays=ndays
76 | )
77 |
78 | context = {
79 | 'report': obj
80 | }
81 |
82 | return render(request, 'report.html', context)
83 |
84 | def compare_query(request):
85 | """
86 | Compare results of a query.
87 | """
88 | context= {
89 | 'queries': models.Query.objects.filter(is_comparable=True),
90 | 'report_ndays': app_config.DEFAULT_REPORT_NDAYS,
91 | 'tags': models.Tag.objects.all()
92 | }
93 |
94 | query_slug = request.GET.get('query', 'totals')
95 | ndays = request.GET.get('ndays', None)
96 | context['unit'] = request.GET.get('unit', 'count')
97 | tag_slug = request.GET.get('tag', None)
98 |
99 | if ndays == 'None':
100 | ndays = None
101 |
102 | context['query'] = models.Query.objects.get(slug=query_slug)
103 |
104 | query_results = models.QueryResult.objects.filter(
105 | query=context['query'],
106 | )
107 |
108 | if ndays:
109 | context['ndays'] = int(ndays)
110 | query_results = query_results.filter(report_ndays=context['ndays'])
111 | else:
112 | context['ndays'] = ndays
113 | query_results = query_results.filter(report_ndays__isnull=True)
114 |
115 | if tag_slug:
116 | context['tag'] = models.Tag.objects.get(slug=tag_slug)
117 | query_results = query_results.filter(
118 | report__project__tags=context['tag']
119 | )
120 |
121 | metric_dimensions = OrderedDict()
122 | results = OrderedDict()
123 |
124 | # Build comparison table
125 | for qr in query_results:
126 | project_title = qr.project_title
127 |
128 | for metric in qr.metrics.all():
129 | m = (metric.name, metric.display_name)
130 |
131 | if m not in results:
132 | results[m] = OrderedDict()
133 |
134 | if metric.name not in metric_dimensions:
135 | if metric.name != 'total':
136 | metric_dimensions[metric.name] = []
137 |
138 | if project_title not in results[m]:
139 | results[m][project_title] = {}
140 |
141 | for dimension in metric.dimensions.all():
142 | if dimension.name not in metric_dimensions[metric.name]:
143 | if dimension.name != 'total':
144 | metric_dimensions[metric.name].append(dimension.name)
145 |
146 | if dimension.name not in results[m][project_title]:
147 | results[m][project_title][dimension.name] = dimension
148 |
149 | for metric_name in metric_dimensions:
150 | metric_dimensions[metric_name].append('total')
151 |
152 | context.update({
153 | 'metric_dimensions': metric_dimensions,
154 | 'results': results
155 | })
156 |
157 | return render(request, 'compare_query.html', context)
158 |
--------------------------------------------------------------------------------
/app_config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Project-wide application configuration.
5 |
6 | DO NOT STORE SECRETS, PASSWORDS, ETC. IN THIS FILE.
7 | They will be exposed to users. Use environment variables instead.
8 | See get_secrets() below for a fast way to access them.
9 | """
10 |
11 | import os
12 |
13 | """
14 | NAMES
15 | """
16 | # Project name to be used in urls
17 | # Use dashes, not underscores!
18 | PROJECT_SLUG = 'carebot'
19 |
20 | # Project name to be used in file paths
21 | PROJECT_FILENAME = 'carebot'
22 |
23 | # The name of the repository containing the source
24 | REPOSITORY_NAME = 'carebot'
25 | GITHUB_USERNAME = 'nprapps'
26 | REPOSITORY_URL = 'git@github.com:%s/%s.git' % (GITHUB_USERNAME, REPOSITORY_NAME)
27 | REPOSITORY_ALT_URL = None # 'git@bitbucket.org:nprapps/%s.git' % REPOSITORY_NAME'
28 |
29 | # Project name used for assets rig
30 | # Should stay the same, even if PROJECT_SLUG changes
31 | ASSETS_SLUG = 'carebot'
32 |
33 | """
34 | DEPLOYMENT
35 | """
36 | PRODUCTION_SERVERS = ['cron.nprapps.org']
37 | STAGING_SERVERS = ['cron-staging.nprapps.org']
38 |
39 | # Should code be deployed to the web/cron servers?
40 | DEPLOY_TO_SERVERS = True
41 |
42 | SERVER_USER = 'ubuntu'
43 | SERVER_PYTHON = 'python2.7'
44 | SERVER_PROJECT_PATH = '/home/%s/apps/%s' % (SERVER_USER, PROJECT_FILENAME)
45 | SERVER_REPOSITORY_PATH = '%s/repository' % SERVER_PROJECT_PATH
46 | SERVER_VIRTUALENV_PATH = '%s/virtualenv' % SERVER_PROJECT_PATH
47 |
48 | # Should the crontab file be installed on the servers?
49 | # If True, DEPLOY_TO_SERVERS must also be True
50 | DEPLOY_CRONTAB = True
51 |
52 | # Should the service configurations be installed on the servers?
53 | # If True, DEPLOY_TO_SERVERS must also be True
54 | DEPLOY_SERVICES = True
55 |
56 | UWSGI_SOCKET_PATH = '/tmp/%s.uwsgi.sock' % PROJECT_FILENAME
57 |
58 | # Services are the server-side services we want to enable and configure.
59 | # A three-tuple following this format:
60 | # (service name, service deployment path, service config file extension)
61 | SERVER_SERVICES = [
62 | ('app', SERVER_REPOSITORY_PATH, 'ini'),
63 | ('uwsgi', '/etc/init', 'conf'),
64 | ('nginx', '/etc/nginx/locations-enabled', 'conf'),
65 | ]
66 |
67 | # These variables will be set at runtime. See configure_targets() below
68 | SERVERS = []
69 | SERVER_BASE_URL = None
70 | SERVER_LOG_PATH = None
71 | DEBUG = True
72 |
73 | SES_REGION = 'us-east-1'
74 |
75 | PROJECT_TYPES = [
76 | ('app', 'App'),
77 | ('seamus-graphic', 'Seamus Graphic'),
78 | ('lookatthis-post', 'Look At This Post')
79 | ]
80 |
81 | DEFAULT_QUERIES = [
82 | 'totals',
83 | 'sessions-by-device-category',
84 | 'sessions-by-browser',
85 | 'sessions-by-referring-domain',
86 | 'sessions-by-referring-social-network',
87 | 'performance',
88 | 'time-spent-on-page-by-device-category'
89 | ]
90 |
91 | DEFAULT_EVENT_QUERIES = [
92 | 'share-discuss-panel-opened',
93 | 'tweet-link-clicked',
94 | 'facebook-share-link-clicked',
95 | 'email-link-clicked',
96 | 'summary-copied'
97 | ]
98 |
99 | DEFAULT_REPORT_NDAYS = [
100 | 1,
101 | 7,
102 | 30
103 | ]
104 |
105 | EMAIL_SEND_ADDRESS = 'nprapps@npr.org'
106 | EMAIL_NOTIFY_ADDRESS = 'nprapps@npr.org'
107 |
108 | """
109 | Utilities
110 | """
111 | def get_secrets():
112 | """
113 | A method for accessing our secrets.
114 | """
115 | secrets = [
116 | 'POSTGRES_USER',
117 | 'POSTGRES_PASSWORD',
118 | 'POSTGRES_HOST',
119 | 'POSTGRES_PORT',
120 | 'SHAREDCOUNT_API_KEY',
121 | 'GECKOBOARD_API_KEY'
122 | ]
123 |
124 | secrets_dict = {}
125 |
126 | for secret in secrets:
127 | name = '%s_%s' % (PROJECT_FILENAME, secret)
128 | secrets_dict[secret] = os.environ.get(name, None)
129 |
130 | return secrets_dict
131 |
132 | def configure_targets(deployment_target):
133 | """
134 | Configure deployment targets. Abstracted so this can be
135 | overriden for rendering before deployment.
136 | """
137 | global SERVERS
138 | global SERVER_BASE_URL
139 | global SERVER_LOG_PATH
140 | global DEBUG
141 | global DEPLOYMENT_TARGET
142 | global DISQUS_SHORTNAME
143 |
144 | if deployment_target == 'production':
145 | SERVERS = PRODUCTION_SERVERS
146 | SERVER_BASE_URL = 'http://%s/%s' % (SERVERS[0], PROJECT_SLUG)
147 | SERVER_LOG_PATH = '/var/log/%s' % PROJECT_FILENAME
148 | DEBUG = False
149 | elif deployment_target == 'staging':
150 | SERVERS = STAGING_SERVERS
151 | SERVER_BASE_URL = 'http://%s/%s' % (SERVERS[0], PROJECT_SLUG)
152 | SERVER_LOG_PATH = '/var/log/%s' % PROJECT_FILENAME
153 | DEBUG = True
154 | else:
155 | SERVERS = []
156 | SERVER_BASE_URL = 'http://127.0.0.1:8001/%s' % PROJECT_SLUG
157 | SERVER_LOG_PATH = '/tmp'
158 | DEBUG = True
159 |
160 | DEPLOYMENT_TARGET = deployment_target
161 |
162 | """
163 | Run automated configuration
164 | """
165 | DEPLOYMENT_TARGET = os.environ.get('DEPLOYMENT_TARGET', None)
166 |
167 | configure_targets(DEPLOYMENT_TARGET)
168 |
--------------------------------------------------------------------------------
/data/test_user.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "fields": {
4 | "codename": "add_logentry",
5 | "name": "Can add log entry",
6 | "content_type": 1
7 | },
8 | "model": "auth.permission",
9 | "pk": 1
10 | },
11 | {
12 | "fields": {
13 | "codename": "change_logentry",
14 | "name": "Can change log entry",
15 | "content_type": 1
16 | },
17 | "model": "auth.permission",
18 | "pk": 2
19 | },
20 | {
21 | "fields": {
22 | "codename": "delete_logentry",
23 | "name": "Can delete log entry",
24 | "content_type": 1
25 | },
26 | "model": "auth.permission",
27 | "pk": 3
28 | },
29 | {
30 | "fields": {
31 | "codename": "add_permission",
32 | "name": "Can add permission",
33 | "content_type": 2
34 | },
35 | "model": "auth.permission",
36 | "pk": 4
37 | },
38 | {
39 | "fields": {
40 | "codename": "change_permission",
41 | "name": "Can change permission",
42 | "content_type": 2
43 | },
44 | "model": "auth.permission",
45 | "pk": 5
46 | },
47 | {
48 | "fields": {
49 | "codename": "delete_permission",
50 | "name": "Can delete permission",
51 | "content_type": 2
52 | },
53 | "model": "auth.permission",
54 | "pk": 6
55 | },
56 | {
57 | "fields": {
58 | "codename": "add_group",
59 | "name": "Can add group",
60 | "content_type": 3
61 | },
62 | "model": "auth.permission",
63 | "pk": 7
64 | },
65 | {
66 | "fields": {
67 | "codename": "change_group",
68 | "name": "Can change group",
69 | "content_type": 3
70 | },
71 | "model": "auth.permission",
72 | "pk": 8
73 | },
74 | {
75 | "fields": {
76 | "codename": "delete_group",
77 | "name": "Can delete group",
78 | "content_type": 3
79 | },
80 | "model": "auth.permission",
81 | "pk": 9
82 | },
83 | {
84 | "fields": {
85 | "codename": "add_user",
86 | "name": "Can add user",
87 | "content_type": 4
88 | },
89 | "model": "auth.permission",
90 | "pk": 10
91 | },
92 | {
93 | "fields": {
94 | "codename": "change_user",
95 | "name": "Can change user",
96 | "content_type": 4
97 | },
98 | "model": "auth.permission",
99 | "pk": 11
100 | },
101 | {
102 | "fields": {
103 | "codename": "delete_user",
104 | "name": "Can delete user",
105 | "content_type": 4
106 | },
107 | "model": "auth.permission",
108 | "pk": 12
109 | },
110 | {
111 | "fields": {
112 | "codename": "add_contenttype",
113 | "name": "Can add content type",
114 | "content_type": 5
115 | },
116 | "model": "auth.permission",
117 | "pk": 13
118 | },
119 | {
120 | "fields": {
121 | "codename": "change_contenttype",
122 | "name": "Can change content type",
123 | "content_type": 5
124 | },
125 | "model": "auth.permission",
126 | "pk": 14
127 | },
128 | {
129 | "fields": {
130 | "codename": "delete_contenttype",
131 | "name": "Can delete content type",
132 | "content_type": 5
133 | },
134 | "model": "auth.permission",
135 | "pk": 15
136 | },
137 | {
138 | "fields": {
139 | "codename": "add_session",
140 | "name": "Can add session",
141 | "content_type": 6
142 | },
143 | "model": "auth.permission",
144 | "pk": 16
145 | },
146 | {
147 | "fields": {
148 | "codename": "change_session",
149 | "name": "Can change session",
150 | "content_type": 6
151 | },
152 | "model": "auth.permission",
153 | "pk": 17
154 | },
155 | {
156 | "fields": {
157 | "codename": "delete_session",
158 | "name": "Can delete session",
159 | "content_type": 6
160 | },
161 | "model": "auth.permission",
162 | "pk": 18
163 | },
164 | {
165 | "fields": {
166 | "codename": "add_query",
167 | "name": "Can add query",
168 | "content_type": 7
169 | },
170 | "model": "auth.permission",
171 | "pk": 19
172 | },
173 | {
174 | "fields": {
175 | "codename": "change_query",
176 | "name": "Can change query",
177 | "content_type": 7
178 | },
179 | "model": "auth.permission",
180 | "pk": 20
181 | },
182 | {
183 | "fields": {
184 | "codename": "delete_query",
185 | "name": "Can delete query",
186 | "content_type": 7
187 | },
188 | "model": "auth.permission",
189 | "pk": 21
190 | },
191 | {
192 | "fields": {
193 | "codename": "add_project",
194 | "name": "Can add project",
195 | "content_type": 8
196 | },
197 | "model": "auth.permission",
198 | "pk": 22
199 | },
200 | {
201 | "fields": {
202 | "codename": "change_project",
203 | "name": "Can change project",
204 | "content_type": 8
205 | },
206 | "model": "auth.permission",
207 | "pk": 23
208 | },
209 | {
210 | "fields": {
211 | "codename": "delete_project",
212 | "name": "Can delete project",
213 | "content_type": 8
214 | },
215 | "model": "auth.permission",
216 | "pk": 24
217 | },
218 | {
219 | "fields": {
220 | "username": "test",
221 | "first_name": "",
222 | "last_name": "",
223 | "is_active": true,
224 | "is_superuser": true,
225 | "is_staff": true,
226 | "last_login": "2014-12-19T19:29:06.150Z",
227 | "groups": [],
228 | "user_permissions": [],
229 | "password": "pbkdf2_sha256$12000$Bz0EdooRXhBp$gys9qlRjqkkaM8k6EeBJcJ0JMh1OiDY6eVdlMsrPvXU=",
230 | "email": "test@test.com",
231 | "date_joined": "2014-12-19T19:29:06.150Z"
232 | },
233 | "model": "auth.user",
234 | "pk": 1
235 | },
236 | {
237 | "fields": {
238 | "expire_date": "2015-01-02T19:27:45.865Z",
239 | "session_data": "YTMyNWNjNjNhY2NiYTQ2YTNiYWE3NzYxNzI0MTVlOTA4Yjc1NDAzNzp7fQ=="
240 | },
241 | "model": "sessions.session",
242 | "pk": "wffjvq6bl0o9n2fikjighdigqosoa8g8"
243 | }
244 | ]
245 |
--------------------------------------------------------------------------------
/fabfile/cron_jobs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Cron jobs
5 | """
6 | from datetime import date
7 | import json
8 |
9 | import boto
10 | import boto.ses
11 | from django.utils import timezone
12 | from fabric.api import local, require, task
13 | import requests
14 |
15 | import app_config
16 | from reports.models import Project, Report
17 | from render_utils import render_to_string
18 |
19 | @task
20 | def test():
21 | """
22 | Example cron task. Note we use "local" instead of "run"
23 | because this will run on the server.
24 | """
25 | require('settings', provided_by=['production', 'staging'])
26 |
27 | local('echo $DEPLOYMENT_TARGET > /tmp/cron_test.txt')
28 |
29 | @task
30 | def run_reports(overwrite='false'):
31 | """
32 | Run project reports.
33 | """
34 | overwrite = (overwrite == 'true')
35 |
36 | print 'Starting at %s' % timezone.now()
37 |
38 | updated_reports = []
39 |
40 | for project in Project.objects.all():
41 | updated_reports.extend(project.run_reports(overwrite=overwrite))
42 | project.social.refresh()
43 |
44 | if updated_reports:
45 | print 'Sending notification email'
46 |
47 | email_body = render_to_string(
48 | 'email.txt',
49 | {
50 | 'reports': updated_reports
51 | },
52 | '/tmp/email.txt'
53 | )
54 |
55 | if app_config.DEPLOYMENT_TARGET:
56 | ses = boto.ses.connect_to_region(
57 | app_config.SES_REGION
58 | )
59 |
60 | ses.send_email(
61 | app_config.EMAIL_SEND_ADDRESS,
62 | 'Carebot cares!',
63 | email_body,
64 | [app_config.EMAIL_NOTIFY_ADDRESS]
65 | )
66 |
67 | GECKOBOARD_WIDGETS = {
68 | 'projects': [{
69 | 'title': '123621-8996005e-6ad7-4c99-8d71-326e14377926',
70 | 'date': '77517-ffadabe0-7363-0132-9f06-22000b490a2f',
71 | 'sessions': '123621-96934a20-e4d1-4241-b4f2-eb194397b799',
72 | 'social': '77517-6d790ab0-7333-0132-9ead-22000b490a2f',
73 | 'devices': '123621-7004cb03-40fc-4391-8792-d84a5c020043'
74 | }, {
75 | 'title': '77517-9da9ad80-7332-0132-9eab-22000b490a2f',
76 | 'date': '77517-3e2547e0-7364-0132-ef2d-22000b5e86d6',
77 | 'sessions': '77517-d0351800-7332-0132-9eac-22000b490a2f',
78 | 'social': '77517-a547fda0-7333-0132-df92-22000b51936c',
79 | 'devices': '77517-457cf730-7338-0132-eefa-22000b5e86d6'
80 | }, {
81 | 'title': '77517-1c6c2d80-7336-0132-9ec6-22000b490a2f',
82 | 'date': '77517-41d93a50-7364-0132-dfd5-22000b51936c',
83 | 'sessions': '77517-1e591bc0-7336-0132-eef8-22000b5e86d6',
84 | 'social': '77517-2020a130-7336-0132-7329-22000b5391df',
85 | 'devices': '77517-4862e280-7338-0132-df9d-22000b51936c'
86 | }],
87 | 'sessions_leaderboard': '123621-0528aa9d-a700-43d0-ae59-f6ce5cf42984'
88 | }
89 |
90 | @task
91 | def update_geckoboard():
92 | top = Project.objects.all()[:3]
93 |
94 | for i, project in enumerate(top):
95 | widgets = GECKOBOARD_WIDGETS['projects'][i]
96 | all_time_report = project.all_time_report
97 |
98 | _geckoboard_text(
99 | widgets['title'],
100 | '
11 |
{{ project.title }}
12 |
13 |
« Back to project list
14 |
15 |
Configuration (edit)
16 |
17 |
18 |
19 |
20 | | Google Analytics Property ID |
21 | {{ project.property_id }} |
22 |
23 |
24 | | Domain |
25 | {{ project.domain }} |
26 |
27 |
28 | | Prefix |
29 | {{ project.prefix }} |
30 |
31 |
32 | | Launch date |
33 | {{ project.start_date }} |
34 |
35 |
36 | | Tags |
37 | {% include "_tags.html" %} |
38 |
39 |
40 |
41 |
42 |
All-time sharing activity
43 |
44 |
45 |
46 |
47 | | Interaction |
48 | Count |
49 | Per 1,000 Sessions |
50 |
51 |
52 |
53 |
54 | | Facebook shares |
55 | {{ project.social.facebook_shares|intcomma }} |
56 | {% social_per_1000_sessions project "facebook_shares" %} |
57 |
58 |
59 |
60 | | Twitter |
61 | {{ project.social.twitter|intcomma }} |
62 | {% social_per_1000_sessions project "twitter" %} |
63 |
64 |
65 |
66 | | Google+ |
67 | {{ project.social.google|intcomma }} |
68 | {% social_per_1000_sessions project "google" %} |
69 |
70 |
71 |
72 | | Pinterest |
73 | {{ project.social.pinterest|intcomma }} |
74 | {% social_per_1000_sessions project "pinterest" %} |
75 |
76 |
77 |
78 | | LinkedIn |
79 | {{ project.social.linkedin|intcomma }} |
80 | {% social_per_1000_sessions project "linkedin" %} |
81 |
82 |
83 |
84 | | StumbleUpon |
85 | {{ project.social.stumbleupon|intcomma }} |
86 | {% social_per_1000_sessions project "stumbleupon" %} |
87 |
88 |
89 |
90 | | Total |
91 | {{ project.social.total|intcomma }} |
92 | {% social_per_1000_sessions project "total" %} |
93 |
94 |
95 |
96 |
97 |
98 |
Shares across all projects
99 |
100 |
101 |
102 |
103 |
Shares per 1,000 sessions across all projects
104 |
105 |
106 |
107 |
All-time Facebook likes/comments
108 |
109 |
110 |
111 | | Interaction |
112 | Count |
113 | Per Facebook Share |
114 |
115 |
116 |
117 |
118 | | Facebook likes |
119 | {{ project.social.facebook_likes|intcomma }} |
120 | {% social_per_1000_sessions project "facebook_likes" %} |
121 |
122 |
123 | | Facebook comments |
124 | {{ project.social.facebook_comments|intcomma }} |
125 | {% social_per_1000_sessions project "facebook_comments" %} |
126 |
127 |
128 |
129 |
130 |
Last updated: {{ project.social.last_update|localtime }}
131 |
132 |
Reports
133 |
134 |
135 |
136 |
137 | | Report timespan |
138 | Run date |
139 | Sessions |
140 |
141 |
142 |
143 | {% for report in reports %}
144 |
145 | | {{ report.timespan }} |
146 | {{ report.last_run|localtime }} |
147 | {{ report.sessions|intcomma }} |
148 |
149 | {% endfor %}
150 |
151 |
152 |
153 |
154 |
155 |
240 | {% endblock %}
241 |
--------------------------------------------------------------------------------
/reports/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='DimensionResult',
15 | fields=[
16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
17 | ('order', models.PositiveIntegerField()),
18 | ('name', models.CharField(max_length=128)),
19 | ('_value', models.CharField(max_length=128)),
20 | ('percent_of_total', models.FloatField(null=True)),
21 | ('project_title', models.CharField(max_length=128)),
22 | ('report_ndays', models.PositiveIntegerField(null=True)),
23 | ('query_name', models.CharField(max_length=128)),
24 | ('metric_name', models.CharField(max_length=128)),
25 | ('metric_data_type', models.CharField(max_length=30)),
26 | ],
27 | options={
28 | 'ordering': ('metric', 'order'),
29 | },
30 | bases=(models.Model,),
31 | ),
32 | migrations.CreateModel(
33 | name='MetricResult',
34 | fields=[
35 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
36 | ('order', models.PositiveIntegerField()),
37 | ('name', models.CharField(max_length=128)),
38 | ('data_type', models.CharField(max_length=30)),
39 | ('project_title', models.CharField(max_length=128)),
40 | ('report_ndays', models.PositiveIntegerField(null=True)),
41 | ('query_name', models.CharField(max_length=128)),
42 | ],
43 | options={
44 | 'ordering': ('query_result', 'order'),
45 | },
46 | bases=(models.Model,),
47 | ),
48 | migrations.CreateModel(
49 | name='Project',
50 | fields=[
51 | ('slug', models.SlugField(max_length=128, serialize=False, primary_key=True)),
52 | ('title', models.CharField(max_length=128)),
53 | ('property_id', models.CharField(default=b'53470309', max_length=10)),
54 | ('domain', models.CharField(default=b'apps.npr.org', max_length=128)),
55 | ('prefix', models.CharField(max_length=128)),
56 | ('start_date', models.DateField()),
57 | ],
58 | options={
59 | 'ordering': ('-start_date',),
60 | },
61 | bases=(models.Model,),
62 | ),
63 | migrations.CreateModel(
64 | name='ProjectQuery',
65 | fields=[
66 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
67 | ('order', models.PositiveIntegerField()),
68 | ],
69 | options={
70 | 'ordering': ('order',),
71 | },
72 | bases=(models.Model,),
73 | ),
74 | migrations.CreateModel(
75 | name='Query',
76 | fields=[
77 | ('slug', models.SlugField(max_length=128, serialize=False, primary_key=True)),
78 | ('name', models.CharField(max_length=128)),
79 | ('description', models.CharField(default=b'', max_length=256)),
80 | ('clan_yaml', models.TextField()),
81 | ],
82 | options={
83 | 'ordering': ('name',),
84 | 'verbose_name_plural': 'queries',
85 | },
86 | bases=(models.Model,),
87 | ),
88 | migrations.CreateModel(
89 | name='QueryResult',
90 | fields=[
91 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
92 | ('order', models.PositiveIntegerField()),
93 | ('sampled', models.BooleanField(default=False)),
94 | ('sample_size', models.PositiveIntegerField(default=0)),
95 | ('sample_space', models.PositiveIntegerField(default=0)),
96 | ('sample_percent', models.FloatField(default=100)),
97 | ('project_title', models.CharField(max_length=128)),
98 | ('report_ndays', models.PositiveIntegerField(null=True)),
99 | ('query_name', models.CharField(max_length=128)),
100 | ('query', models.ForeignKey(related_name='query_results', to='reports.Query')),
101 | ],
102 | options={
103 | 'ordering': ('report', 'order'),
104 | },
105 | bases=(models.Model,),
106 | ),
107 | migrations.CreateModel(
108 | name='Report',
109 | fields=[
110 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
111 | ('ndays', models.PositiveIntegerField(null=True)),
112 | ('results_json', models.TextField()),
113 | ('last_run', models.DateTimeField(null=True)),
114 | ('pageviews', models.PositiveIntegerField(null=True)),
115 | ('unique_pageviews', models.PositiveIntegerField(null=True)),
116 | ('users', models.PositiveIntegerField(null=True)),
117 | ('sessions', models.PositiveIntegerField(null=True)),
118 | ],
119 | options={
120 | 'ordering': ('project__start_date', 'ndays'),
121 | },
122 | bases=(models.Model,),
123 | ),
124 | migrations.CreateModel(
125 | name='Social',
126 | fields=[
127 | ('project', models.OneToOneField(primary_key=True, serialize=False, to='reports.Project')),
128 | ('facebook_likes', models.PositiveIntegerField(default=0)),
129 | ('facebook_shares', models.PositiveIntegerField(default=0)),
130 | ('facebook_comments', models.PositiveIntegerField(default=0)),
131 | ('twitter', models.PositiveIntegerField(default=0)),
132 | ('google', models.PositiveIntegerField(default=0)),
133 | ('pinterest', models.PositiveIntegerField(default=0)),
134 | ('linkedin', models.PositiveIntegerField(default=0)),
135 | ('stumbleupon', models.PositiveIntegerField(default=0)),
136 | ('last_update', models.DateTimeField(null=True)),
137 | ],
138 | options={
139 | 'ordering': ('-project__start_date',),
140 | 'verbose_name': 'social count',
141 | 'verbose_name_plural': 'social counts',
142 | },
143 | bases=(models.Model,),
144 | ),
145 | migrations.CreateModel(
146 | name='Tag',
147 | fields=[
148 | ('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
149 | ],
150 | options={
151 | },
152 | bases=(models.Model,),
153 | ),
154 | migrations.AddField(
155 | model_name='report',
156 | name='project',
157 | field=models.ForeignKey(related_name='reports', to='reports.Project'),
158 | preserve_default=True,
159 | ),
160 | migrations.AddField(
161 | model_name='queryresult',
162 | name='report',
163 | field=models.ForeignKey(related_name='query_results', to='reports.Report'),
164 | preserve_default=True,
165 | ),
166 | migrations.AddField(
167 | model_name='projectquery',
168 | name='project',
169 | field=models.ForeignKey(related_name='project_queries', to='reports.Project'),
170 | preserve_default=True,
171 | ),
172 | migrations.AddField(
173 | model_name='projectquery',
174 | name='query',
175 | field=models.ForeignKey(related_name='project_queries', to='reports.Query'),
176 | preserve_default=True,
177 | ),
178 | migrations.AddField(
179 | model_name='project',
180 | name='queries',
181 | field=models.ManyToManyField(to='reports.Query', through='reports.ProjectQuery'),
182 | preserve_default=True,
183 | ),
184 | migrations.AddField(
185 | model_name='project',
186 | name='tags',
187 | field=models.ManyToManyField(to='reports.Tag'),
188 | preserve_default=True,
189 | ),
190 | migrations.AddField(
191 | model_name='metricresult',
192 | name='query_result',
193 | field=models.ForeignKey(related_name='metrics', to='reports.QueryResult'),
194 | preserve_default=True,
195 | ),
196 | migrations.AddField(
197 | model_name='dimensionresult',
198 | name='metric',
199 | field=models.ForeignKey(related_name='dimensions', to='reports.MetricResult', null=True),
200 | preserve_default=True,
201 | ),
202 | ]
203 |
--------------------------------------------------------------------------------
/fabfile/servers.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """
4 | Commands work with servers. (Hiss, boo.)
5 | """
6 |
7 | import copy
8 |
9 | from fabric.api import local, put, settings, require, run, sudo, task
10 | from fabric.state import env
11 | from jinja2 import Template
12 |
13 | import app_config
14 |
15 | """
16 | Setup
17 | """
18 |
19 | @task
20 | def setup():
21 | """
22 | Setup servers for deployment.
23 |
24 | This does not setup services or push to S3. Run deploy() next.
25 | """
26 | require('settings', provided_by=['production', 'staging'])
27 | require('branch', provided_by=['stable', 'master', 'branch'])
28 |
29 | if not app_config.DEPLOY_TO_SERVERS:
30 | print 'You must set DEPLOY_TO_SERVERS = True in your app_config.py before setting up the servers.'
31 |
32 | return
33 |
34 | create_directories()
35 | create_virtualenv()
36 | clone_repo()
37 | checkout_latest()
38 | install_requirements()
39 | setup_logs()
40 |
41 | def create_directories():
42 | """
43 | Create server directories.
44 | """
45 | require('settings', provided_by=['production', 'staging'])
46 |
47 | run('mkdir -p %(SERVER_PROJECT_PATH)s' % app_config.__dict__)
48 | run('mkdir -p /var/www/uploads/%(PROJECT_FILENAME)s' % app_config.__dict__)
49 |
50 | def create_virtualenv():
51 | """
52 | Setup a server virtualenv.
53 | """
54 | require('settings', provided_by=['production', 'staging'])
55 |
56 | run('virtualenv -p %(SERVER_PYTHON)s %(SERVER_VIRTUALENV_PATH)s' % app_config.__dict__)
57 | run('source %(SERVER_VIRTUALENV_PATH)s/bin/activate' % app_config.__dict__)
58 |
59 | def clone_repo():
60 | """
61 | Clone the source repository.
62 | """
63 | require('settings', provided_by=['production', 'staging'])
64 |
65 | run('git clone %(REPOSITORY_URL)s %(SERVER_REPOSITORY_PATH)s' % app_config.__dict__)
66 |
67 | if app_config.REPOSITORY_ALT_URL:
68 | run('git remote add bitbucket %(REPOSITORY_ALT_URL)s' % app_config.__dict__)
69 |
70 | @task
71 | def checkout_latest(remote='origin'):
72 | """
73 | Checkout the latest source.
74 | """
75 | require('settings', provided_by=['production', 'staging'])
76 | require('branch', provided_by=['stable', 'master', 'branch'])
77 |
78 | run('cd %s; git fetch %s' % (app_config.SERVER_REPOSITORY_PATH, remote))
79 | run('cd %s; git checkout %s; git pull %s %s' % (app_config.SERVER_REPOSITORY_PATH, env.branch, remote, env.branch))
80 |
81 | @task
82 | def install_requirements():
83 | """
84 | Install the latest requirements.
85 | """
86 | require('settings', provided_by=['production', 'staging'])
87 |
88 | run('%(SERVER_VIRTUALENV_PATH)s/bin/pip install -U -r %(SERVER_REPOSITORY_PATH)s/requirements.txt' % app_config.__dict__)
89 |
90 | @task
91 | def setup_logs():
92 | """
93 | Create log directories.
94 | """
95 | require('settings', provided_by=['production', 'staging'])
96 |
97 | sudo('mkdir %(SERVER_LOG_PATH)s' % app_config.__dict__)
98 | sudo('chown ubuntu:ubuntu %(SERVER_LOG_PATH)s' % app_config.__dict__)
99 |
100 | @task
101 | def install_crontab():
102 | """
103 | Install cron jobs script into cron.d.
104 | """
105 | require('settings', provided_by=['production', 'staging'])
106 |
107 | sudo('cp %(SERVER_REPOSITORY_PATH)s/crontab /etc/cron.d/%(PROJECT_FILENAME)s' % app_config.__dict__)
108 |
109 | @task
110 | def uninstall_crontab():
111 | """
112 | Remove a previously install cron jobs script from cron.d
113 | """
114 | require('settings', provided_by=['production', 'staging'])
115 |
116 | sudo('rm /etc/cron.d/%(PROJECT_FILENAME)s' % app_config.__dict__)
117 |
118 | def delete_project():
119 | """
120 | Remove the project directory. Invoked by shiva.
121 | """
122 | run('rm -rf %(SERVER_PROJECT_PATH)s' % app_config.__dict__)
123 |
124 | """
125 | Configuration
126 | """
127 |
128 | def _get_template_conf_path(service, extension):
129 | """
130 | Derive the path for a conf template file.
131 | """
132 | return 'confs/%s.%s' % (service, extension)
133 |
134 | def _get_rendered_conf_path(service, extension):
135 | """
136 | Derive the rendered path for a conf file.
137 | """
138 | return 'confs/rendered/%s.%s.%s' % (app_config.PROJECT_FILENAME, service, extension)
139 |
140 | def _get_installed_conf_path(service, remote_path, extension):
141 | """
142 | Derive the installed path for a conf file.
143 | """
144 | return '%s/%s.%s.%s' % (remote_path, app_config.PROJECT_FILENAME, service, extension)
145 |
146 | def _get_installed_service_name(service):
147 | """
148 | Derive the init service name for an installed service.
149 | """
150 | return '%s.%s' % (app_config.PROJECT_FILENAME, service)
151 |
152 | @task
153 | def render_confs():
154 | """
155 | Renders server configurations.
156 | """
157 | require('settings', provided_by=['production', 'staging'])
158 |
159 | with settings(warn_only=True):
160 | local('mkdir confs/rendered')
161 |
162 | # Copy the app_config so that when we load the secrets they don't
163 | # get exposed to other management commands
164 | context = copy.copy(app_config.__dict__)
165 | context.update(app_config.get_secrets())
166 |
167 | for service, remote_path, extension in app_config.SERVER_SERVICES:
168 | template_path = _get_template_conf_path(service, extension)
169 | rendered_path = _get_rendered_conf_path(service, extension)
170 |
171 | with open(template_path, 'r') as read_template:
172 |
173 | with open(rendered_path, 'wb') as write_template:
174 | payload = Template(read_template.read())
175 | write_template.write(payload.render(**context))
176 |
177 | @task
178 | def deploy_confs():
179 | """
180 | Deploys rendered server configurations to the specified server.
181 | This will reload nginx and the appropriate uwsgi config.
182 | """
183 | require('settings', provided_by=['production', 'staging'])
184 |
185 | render_confs()
186 |
187 | with settings(warn_only=True):
188 | for service, remote_path, extension in app_config.SERVER_SERVICES:
189 | rendered_path = _get_rendered_conf_path(service, extension)
190 | installed_path = _get_installed_conf_path(service, remote_path, extension)
191 |
192 | a = local('md5 -q %s' % rendered_path, capture=True)
193 | b = run('md5sum %s' % installed_path).split()[0]
194 |
195 | if a != b:
196 | print 'Updating %s' % installed_path
197 | put(rendered_path, installed_path, use_sudo=True)
198 |
199 | if service == 'nginx':
200 | sudo('service nginx reload')
201 | elif service == 'uwsgi':
202 | service_name = _get_installed_service_name(service)
203 | sudo('initctl reload-configuration')
204 | sudo('service %s restart' % service_name)
205 | elif service == 'app':
206 | run('touch %s' % app_config.UWSGI_SOCKET_PATH)
207 | sudo('chmod 644 %s' % app_config.UWSGI_SOCKET_PATH)
208 | sudo('chown www-data:www-data %s' % app_config.UWSGI_SOCKET_PATH)
209 | else:
210 | print '%s has not changed' % rendered_path
211 |
212 | @task
213 | def reload_service(service):
214 | require('settings', provided_by=['production', 'staging'])
215 |
216 | if service == 'nginx':
217 | sudo('service nginx reload')
218 | elif service == 'uwsgi':
219 | service_name = _get_installed_service_name(service)
220 | sudo('initctl reload-configuration')
221 | sudo('service %s restart' % service_name)
222 | elif service == 'app':
223 | run('touch %s' % app_config.UWSGI_SOCKET_PATH)
224 | sudo('chmod 644 %s' % app_config.UWSGI_SOCKET_PATH)
225 | sudo('chown www-data:www-data %s' % app_config.UWSGI_SOCKET_PATH)
226 |
227 | @task
228 | def nuke_confs():
229 | """
230 | DESTROYS rendered server configurations from the specified server.
231 | This will reload nginx and stop the uwsgi config.
232 | """
233 | require('settings', provided_by=['production', 'staging'])
234 |
235 | for service, remote_path, extension in app_config.SERVER_SERVICES:
236 | with settings(warn_only=True):
237 | installed_path = _get_installed_conf_path(service, remote_path, extension)
238 |
239 | sudo('rm -f %s' % installed_path)
240 |
241 | if service == 'nginx':
242 | sudo('service nginx reload')
243 | elif service == 'uwsgi':
244 | service_name = _get_installed_service_name(service)
245 | sudo('service %s stop' % service_name)
246 | sudo('initctl reload-configuration')
247 | elif service == 'app':
248 | sudo('rm %s' % app_config.UWSGI_SOCKET_PATH)
249 |
250 | """
251 | Django
252 | """
253 |
254 | @task
255 | def collectstatic():
256 | require('settings', provided_by=['production', 'staging'])
257 |
258 | run('cd %(SERVER_REPOSITORY_PATH)s; %(SERVER_VIRTUALENV_PATH)s/bin/python %(SERVER_REPOSITORY_PATH)s/manage.py collectstatic --noinput' % app_config.__dict__)
259 |
260 | """
261 | Fabcasting
262 | """
263 |
264 | @task
265 | def fabcast(command):
266 | """
267 | Actually run specified commands on the server specified
268 | by staging() or production().
269 | """
270 | require('settings', provided_by=['production', 'staging'])
271 |
272 | if not app_config.DEPLOY_TO_SERVERS:
273 | print 'You must set DEPLOY_TO_SERVERS = True in your app_config.py and setup a server before fabcasting.'
274 |
275 | run('cd %s && bash run_on_server.sh fab branch:%s $DEPLOYMENT_TARGET %s' % (app_config.SERVER_REPOSITORY_PATH, env.branch, command))
276 |
277 |
--------------------------------------------------------------------------------
/reports/static/reports/tablesort.min.js:
--------------------------------------------------------------------------------
1 | ;(function() {
2 | function Tablesort(el, options) {
3 | if (!el) throw new Error('Element not found');
4 | if (el.tagName !== 'TABLE') throw new Error('Element must be a table');
5 | this.init(el, options || {});
6 | }
7 |
8 | Tablesort.prototype = {
9 |
10 | init: function(el, options) {
11 | var that = this,
12 | firstRow;
13 | this.thead = false;
14 | this.options = options;
15 |
16 | if (el.rows && el.rows.length > 0) {
17 | if (el.tHead && el.tHead.rows.length > 0) {
18 | firstRow = el.tHead.rows[el.tHead.rows.length - 1];
19 | that.thead = true;
20 | } else {
21 | firstRow = el.rows[0];
22 | }
23 | }
24 |
25 | if (!firstRow) return;
26 |
27 | var onClick = function() {
28 | if (that.current && that.current !== this) {
29 | if (that.current.classList.contains(classSortUp)) {
30 | that.current.classList.remove(classSortUp);
31 | }
32 | else if (that.current.classList.contains(classSortDown)) {
33 | that.current.classList.remove(classSortDown);
34 | }
35 | }
36 |
37 | that.current = this;
38 | that.sortTable(this);
39 | };
40 |
41 | var defaultSort;
42 |
43 | // Assume first row is the header and attach a click handler to each.
44 | for (var i = 0; i < firstRow.cells.length; i++) {
45 | var cell = firstRow.cells[i];
46 | if (!cell.classList.contains('no-sort')) {
47 | cell.classList.add('sort-header');
48 | cell.addEventListener('click', onClick, false);
49 |
50 | if (cell.classList.contains('sort-default')) {
51 | defaultSort = cell;
52 | }
53 | }
54 | }
55 |
56 | if (defaultSort) {
57 | that.current = defaultSort;
58 | that.sortTable(defaultSort, true);
59 | }
60 | },
61 |
62 | getFirstDataRowIndex: function() {
63 | // If table does not have a