Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: mozilla/kuma
...
head fork: mozilla/kuma
  • 4 commits
  • 30 files changed
  • 0 commit comments
  • 2 contributors
Showing with 178 additions and 1,236 deletions.
  1. 0  apps/customercare/__init__.py
  2. +0 −36 apps/customercare/admin.py
  3. +0 −210 apps/customercare/cron.py
  4. +0 −123 apps/customercare/fixtures/tweets.json
  5. +0 −31 apps/customercare/helpers.py
  6. +0 −70 apps/customercare/models.py
  7. +0 −4 apps/customercare/templates/customercare/base.html
  8. +0 −148 apps/customercare/templates/customercare/landing.html
  9. +0 −72 apps/customercare/templates/customercare/reply_modal.html
  10. +0 −36 apps/customercare/templates/customercare/tweets.html
  11. +0 −16 apps/customercare/templates/customercare/twitter_modal.html
  12. +0 −48 apps/customercare/tests/__init__.py
  13. +0 −105 apps/customercare/tests/test_cron.py
  14. +0 −29 apps/customercare/tests/test_helpers.py
  15. +0 −56 apps/customercare/tests/test_templates.py
  16. +0 −30 apps/customercare/tests/test_views.py
  17. +0 −7 apps/customercare/urls.py
  18. +0 −188 apps/customercare/views.py
  19. +38 −0 puppet/files/etc/apache2/conf.d/mozilla-kuma-apache.conf
  20. +11 −0 puppet/files/etc/init/carbon-cache.conf
  21. +14 −0 puppet/files/etc/init/statsd.conf
  22. +5 −0 puppet/files/home/vagrant/statsd-config.js
  23. +10 −0 puppet/files/opt/graphite/conf/storage-schemas.conf
  24. +7 −0 puppet/files/tmp/graphite_reqs.txt
  25. +1 −1  puppet/files/vagrant/kumascript_settings_local.json
  26. +1 −4 puppet/files/vagrant/settings_local.py
  27. +3 −1 puppet/manifests/classes/dev-hacks.pp
  28. +84 −0 puppet/manifests/classes/statsd.pp
  29. +4 −1 puppet/manifests/dev-vagrant.pp
  30. +0 −20 settings.py
View
0  apps/customercare/__init__.py
No changes.
View
36 apps/customercare/admin.py
@@ -1,36 +0,0 @@
-from django.contrib import admin
-
-from .models import Tweet, CannedCategory, CannedResponse, CategoryMembership
-
-
-class TweetAdmin(admin.ModelAdmin):
- date_hierarchy = 'created'
- list_display = ('tweet_id', '__unicode__', 'created', 'locale')
- list_filter = ('locale',)
- search_fields = ('raw_json',)
-admin.site.register(Tweet, TweetAdmin)
-
-
-class MembershipInline(admin.StackedInline):
- """Inline to show response/category relationships."""
- extra = 1
- model = CategoryMembership
- raw_id_fields = ('response',)
-
-
-class CategoryAdmin(admin.ModelAdmin):
- list_display = ('title', 'locale', 'weight', 'response_count')
- inlines = (MembershipInline,)
-
- def response_count(self, obj):
- return obj.responses.count()
-admin.site.register(CannedCategory, CategoryAdmin)
-
-
-class ResponseAdmin(admin.ModelAdmin):
- list_display = ('title', 'locale', 'category_count')
- inlines = (MembershipInline,)
-
- def category_count(self, obj):
- return obj.categories.count()
-admin.site.register(CannedResponse, ResponseAdmin)
View
210 apps/customercare/cron.py
@@ -1,210 +0,0 @@
-import calendar
-from datetime import datetime, timedelta
-import json
-import logging
-import re
-import rfc822
-import time
-import urllib
-import urllib2
-
-from django.conf import settings
-from django.core.cache import cache
-from django.db.utils import IntegrityError
-from django.utils.encoding import smart_str
-
-import cronjobs
-from multidb.pinning import pin_this_thread
-import tweepy
-
-from customercare.models import Tweet
-
-
-SEARCH_URL = 'http://search.twitter.com/search.json'
-
-LINK_REGEX = re.compile('https?\:', re.IGNORECASE)
-MENTION_REGEX = re.compile('(^|\W)@')
-RT_REGEX = re.compile('^rt\W', re.IGNORECASE)
-
-log = logging.getLogger('k.twitter')
-
-
-@cronjobs.register
-def collect_tweets():
- """Collect new tweets about Firefox."""
- search_options = {
- 'q': 'firefox',
- 'rpp': settings.CC_TWEETS_PERPAGE, # Items per page.
- 'result_type': 'recent', # Retrieve tweets by date.
- }
-
- # If we already have some tweets, collect nothing older than what we have.
- try:
- latest_tweet = Tweet.objects.latest()
- except Tweet.DoesNotExist:
- log.debug('No existing tweets. Retrieving %d tweets from search.' % (
- settings.CC_TWEETS_PERPAGE))
- else:
- search_options['since_id'] = latest_tweet.tweet_id
- log.debug('Retrieving tweets with id >= %s' % latest_tweet.tweet_id)
-
- # Retrieve Tweets
- try:
- raw_data = json.load(urllib.urlopen('%s?%s' % (
- SEARCH_URL, urllib.urlencode(search_options))))
- except Exception, e:
- log.warning('Twitter request failed: %s' % e)
- return
-
- if not ('results' in raw_data and raw_data['results']):
- log.info('Twitter returned 0 results.')
- return
-
- # Drop tweets into DB
- for item in raw_data['results']:
- log.debug('Handling tweet %d: %s...' % (item['id'],
- smart_str(item['text'][:50])))
- # Apply filters to tweet before saving
- item = _filter_tweet(item)
- if not item:
- continue
-
- created_date = datetime.utcfromtimestamp(calendar.timegm(
- rfc822.parsedate(item['created_at'])))
-
- item_lang = item.get('iso_language_code', 'en')
- tweet = Tweet(tweet_id=item['id'], raw_json=json.dumps(item),
- locale=item_lang, created=created_date)
- try:
- tweet.save()
- except IntegrityError:
- continue
- else:
- log.debug('Tweet %d saved.' % item['id'])
-
-
-@cronjobs.register
-def purge_tweets():
- """Periodically purge old tweets for each locale.
-
- This does a lot of DELETEs on master, so it shouldn't run too frequently.
- Probably once every hour or more.
-
- """
- # Pin to master
- pin_this_thread()
-
- # Build list of tweets to delete, by id.
- for locale in settings.SUMO_LANGUAGES:
- locale = settings.LOCALES[locale].iso639_1
- # Some locales don't have an iso639_1 code, too bad for them.
- if not locale:
- continue
- oldest = _get_oldest_tweet(locale, settings.CC_MAX_TWEETS)
- if oldest:
- log.debug('Truncating tweet list: Removing tweets older than %s, '
- 'for [%s].' % (oldest.created, locale))
- Tweet.objects.filter(locale=locale,
- created__lte=oldest.created).delete()
-
-
-def _get_oldest_tweet(locale, n=0):
- """Returns the nth oldest tweet per locale, defaults to newest."""
- try:
- return Tweet.objects.filter(locale=locale).order_by(
- '-created')[n]
- except IndexError:
- return None
-
-
-def _filter_tweet(item):
- """
- Apply some filters to an incoming tweet.
-
- May modify tweet. If None is returned, tweet will be discarded.
- Used to exclude replies and such from incoming tweets.
- """
- # No replies, no mentions
- if item['to_user_id'] or MENTION_REGEX.search(item['text']):
- log.debug('Tweet %d discarded (reply).' % item['id'])
- return None
-
- # No retweets
- if RT_REGEX.search(item['text']) or item['text'].find('(via ') > -1:
- log.debug('Tweet %d discarded (retweet).' % item['id'])
- return None
-
- # No links
- if LINK_REGEX.search(item['text']):
- log.debug('Tweet %d discarded (link).' % item['id'])
- return None
-
- # Exclude filtered users
- if item['from_user'] in settings.CC_IGNORE_USERS:
- log.debug('Tweet %d discarded (user %s).' % (
- item['id'], item['from_user']))
- return None
-
- return item
-
-
-@cronjobs.register
-def get_customercare_stats():
- """
- Fetch Customer Care stats from Mozilla Metrics.
-
- Example Activity Stats data:
- {"resultset": [["Yesterday",1234,123,0.0154],
- ["Last Week",12345,1234,0.0240], ...]
- "metadata": [...]}
-
- Example Top Contributor data:
- {"resultset": [[1,"Overall","John Doe","johndoe",840],
- [2,"Overall","Jane Doe","janedoe",435], ...],
- "metadata": [...]}
- """
-
- stats_sources = {
- settings.CC_TWEET_ACTIVITY_URL: settings.CC_TWEET_ACTIVITY_CACHE_KEY,
- settings.CC_TOP_CONTRIB_URL: settings.CC_TOP_CONTRIB_CACHE_KEY,
- }
- for url, cache_key in stats_sources.items():
- log.debug('Updating %s from %s' % (cache_key, url))
- try:
- json_resource = urllib2.urlopen(url)
- json_data = json.load(json_resource)
- if not json_data['resultset']:
- raise KeyError('Result set was empty.')
- except Exception, e:
- log.error('Error updating %s: %s' % (cache_key, e))
- continue
-
- # Make sure the file is not outdated.
- headers = json_resource.info()
- lastmod = datetime.fromtimestamp(time.mktime(
- rfc822.parsedate(headers['Last-Modified'])))
- if ((datetime.now() - lastmod) > timedelta(
- seconds=settings.CC_STATS_WARNING)):
- log.warning('Resource %s is outdated. Last update: %s' % (
- cache_key, lastmod))
-
- # Grab top contributors' avatar URLs from the public twitter API.
- if cache_key == settings.CC_TOP_CONTRIB_CACHE_KEY:
- twitter = tweepy.API()
- avatars = {}
- for contrib in json_data['resultset']:
- username = contrib[3]
-
- if avatars.get(username):
- continue
-
- try:
- user = twitter.get_user(username)
- except tweepy.TweepError, e:
- log.warning('Error grabbing avatar of user %s: %s' % (
- username, e))
- else:
- avatars[username] = user.profile_image_url
- json_data['avatars'] = avatars
-
- cache.set(cache_key, json_data, settings.CC_STATS_CACHE_TIMEOUT)
View
123 apps/customercare/fixtures/tweets.json
@@ -1,123 +0,0 @@
-[
- {
- "pk": 30,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25308717656,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Kudos to Brizzly - the application is much less buggy lately. No more double tweet boxes. No more extra UI garbage on my Firefox window.\", \"created_at\": \"Thu, 23 Sep 2010 13:52:40 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/876200349/tool-belt_Logo_normal.jpg\", \"source\": \"<a href="http://www.brizzly.com" rel="nofollow">Brizzly</a>\", \"from_user\": \"rossgk\", \"from_user_id\": 150857, \"to_user_id\": null, \"geo\": null, \"id\": 25308717656, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:52:40"
- }
- },
- {
- "pk": 23,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25308845620,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Firefox\\u3000chrome\\u306b\\u4e57\\u308a\\u63db\\u3048\\u308b\\u304b\\u601d\\u6848\\u4e2d #firefox\", \"created_at\": \"Thu, 23 Sep 2010 13:54:12 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/1041604705/188292_8256_normal.jpg\", \"source\": \"<a href="http://www.echofon.com/" rel="nofollow">Echofon</a>\", \"from_user\": \"ikd_fine\", \"from_user_id\": 129866568, \"to_user_id\": null, \"geo\": null, \"id\": 25308845620, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:54:12"
- }
- },
- {
- "pk": 21,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25308851981,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"We're noticing a lot of #Flash Errors in #Firefox in the last few days. Including the AS3 Scroll Bars on my site. Too many updates FTL.\", \"created_at\": \"Thu, 23 Sep 2010 13:54:17 +0000\", \"profile_image_url\": \"http://a3.twimg.com/profile_images/1037516415/IA_100x100_avtr_normal.png\", \"source\": \"<a href="http://twitter.com/">web</a>\", \"from_user\": \"runtime_iA\", \"from_user_id\": 130011759, \"to_user_id\": null, \"geo\": null, \"id\": 25308851981, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:54:17"
- }
- },
- {
- "pk": 19,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25308865789,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Ok so it looks good in chrome, firefox and safari. \\nIt even seems to look ok in IE. (163 visitors to MDN using IE last month)\", \"created_at\": \"Thu, 23 Sep 2010 13:54:27 +0000\", \"profile_image_url\": \"http://a0.twimg.com/profile_images/533310872/twitterscotty_normal.jpg\", \"source\": \"<a href="http://kiwi-app.net" rel="nofollow">kiwi</a>\", \"from_user\": \"macdevnet\", \"from_user_id\": 28772, \"to_user_id\": null, \"geo\": null, \"id\": 25308865789, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:54:27"
- }
- },
- {
- "pk": 15,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25308906635,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"I'm still partial to Firefox, but IE9 Beta is FAST!\", \"created_at\": \"Thu, 23 Sep 2010 13:54:56 +0000\", \"profile_image_url\": \"http://a2.twimg.com/profile_images/1120347326/cole_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"Kid_Zer0\", \"from_user_id\": 130442244, \"to_user_id\": null, \"geo\": null, \"id\": 25308906635, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:54:56"
- }
- },
- {
- "pk": 14,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25308913992,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Hey guys...am thinking of switching from IE to Firefox. yay or nay?\", \"created_at\": \"Thu, 23 Sep 2010 13:55:02 +0000\", \"profile_image_url\": \"http://a2.twimg.com/profile_images/151180126/Lucid_normal.jpg\", \"source\": \"<a href="http://twitter.com/">web</a>\", \"from_user\": \"LucidLilith\", \"from_user_id\": 1884444, \"to_user_id\": null, \"geo\": null, \"id\": 25308913992, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:55:02"
- }
- },
- {
- "pk": 11,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25309157145,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Just changed my firefox theme for the first time in months and months. I like my new one :)\", \"created_at\": \"Thu, 23 Sep 2010 13:57:58 +0000\", \"profile_image_url\": \"http://a3.twimg.com/profile_images/1093204039/Me_and_claw_normal.jpg\", \"source\": \"<a href="http://twitter.com/">web</a>\", \"from_user\": \"jamesgrant17\", \"from_user_id\": 4403196, \"to_user_id\": null, \"geo\": null, \"id\": 25309157145, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:57:58"
- }
- },
- {
- "pk": 101,
- "model": "customercare.tweet",
- "fields": {
- "locale": "ro",
- "tweet_id": 25309168529,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Un tweet n romana #Firefox\", \"created_at\": \"Thu, 23 Sep 2010 13:58:06 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/1117809237/cool_cat_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"__jimcasey__\", \"from_user_id\": 142651388, \"to_user_id\": null, \"geo\": null, \"id\": 25309168521, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:58:06"
- }
- },
- {
- "pk": 102,
- "model": "customercare.tweet",
- "fields": {
- "locale": "ro",
- "tweet_id": 25309168528,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Inca un tweet in Romana pentru Firefox!\", \"created_at\": \"Thu, 23 Sep 2010 13:54:56 +0000\", \"profile_image_url\": \"http://a2.twimg.com/profile_images/1120347326/cole_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"Kid_Zer0\", \"from_user_id\": 130442244, \"to_user_id\": null, \"geo\": null, \"id\": 25308906635, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-24 14:58:06"
- }
- },
- {
- "pk": 10,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25309168521,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"Looks like with #Firefox "Tabs on top" & "Hide Menubar" add-ons you can get same amount of browser space that you have in Chrome. Yay!\", \"created_at\": \"Thu, 23 Sep 2010 13:58:06 +0000\", \"profile_image_url\": \"http://a1.twimg.com/profile_images/1117809237/cool_cat_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"__jimcasey__\", \"from_user_id\": 142651388, \"to_user_id\": null, \"geo\": null, \"id\": 25309168521, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 13:58:06"
- }
- },
- {
- "pk": 4,
- "model": "customercare.tweet",
- "fields": {
- "locale": "en",
- "tweet_id": 25309381333,
- "raw_json": "{\"iso_language_code\": \"en\", \"text\": \"On this day in 2002, first public version of Mozilla Firefox ('Phoenix 0.1') is released. Jose Canseco becomes first member of 40-40 club.\", \"created_at\": \"Thu, 23 Sep 2010 14:00:35 +0000\", \"profile_image_url\": \"http://a3.twimg.com/profile_images/407391555/643797910_fgRus-S_normal.jpg\", \"source\": \"<a href="http://www.tweetdeck.com" rel="nofollow">TweetDeck</a>\", \"from_user\": \"jasonboche\", \"from_user_id\": 1644275, \"to_user_id\": null, \"geo\": null, \"id\": 25309381333, \"metadata\": {\"result_type\": \"recent\"}}",
- "reply_to": null,
- "created": "2010-09-23 14:00:35"
- }
- }
-]
View
31 apps/customercare/helpers.py
@@ -1,31 +0,0 @@
-from datetime import datetime
-
-from django.conf import settings
-from django.template import defaultfilters
-
-from jingo import register
-import pytz
-
-
-@register.filter
-def utctimesince(time):
- return defaultfilters.timesince(time, datetime.utcnow())
-
-
-def _append_tz(t):
- tz = pytz.timezone(settings.TIME_ZONE)
- return tz.localize(t)
-
-
-@register.filter
-def isotime(t):
- """Date/Time format according to ISO 8601"""
- if not hasattr(t, 'tzinfo'):
- return
- return _append_tz(t).astimezone(pytz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
-
-
-@register.filter
-def round_percent(num):
- """Return a customercare-format percentage from a number."""
- return round(num, 1) if num < 10 else int(round(num, 0))
View
70 apps/customercare/models.py
@@ -1,70 +0,0 @@
-from datetime import datetime
-import json
-
-from django.db import models
-
-from sumo.models import ModelBase, LocaleField
-
-
-class Tweet(ModelBase):
- """An entry on twitter."""
- tweet_id = models.BigIntegerField(unique=True)
- raw_json = models.TextField()
- # This is different from our usual locale, so not using LocaleField.
- locale = models.CharField(max_length=20, db_index=True)
- created = models.DateTimeField(default=datetime.now, db_index=True)
- reply_to = models.BigIntegerField(blank=True, null=True, default=None,
- db_index=True)
-
- class Meta:
- get_latest_by = 'created'
- ordering = ('-created',)
-
- def __unicode__(self):
- tweet = json.loads(self.raw_json)
- return tweet['text']
-
-
-class CannedCategory(ModelBase):
- """Category for canned responses."""
- title = models.CharField(max_length=255)
- weight = models.IntegerField(
- default=0, db_index=True,
- help_text='Heavier items sink, lighter ones bubble up.')
- locale = LocaleField(db_index=True)
-
- class Meta:
- ordering = ('locale', 'weight', 'title')
- unique_together = ('title', 'locale')
- verbose_name_plural = 'Canned categories'
-
- def __unicode__(self):
- return u'[%s] %s' % (self.locale, self.title)
-
-
-class CannedResponse(ModelBase):
- """Canned response to tweets."""
- title = models.CharField(max_length=255)
- response = models.CharField(max_length=140)
- categories = models.ManyToManyField(
- CannedCategory, related_name='responses', through='CategoryMembership')
- locale = LocaleField(db_index=True)
-
- class Meta:
- ordering = ('locale', 'title')
- unique_together = ('title', 'locale')
-
- def __unicode__(self):
- return u'[%s] %s' % (self.locale, self.title)
-
-
-class CategoryMembership(ModelBase):
- """Mapping table for canned responses <-> categories."""
- category = models.ForeignKey(CannedCategory, blank=True)
- response = models.ForeignKey(CannedResponse, blank=True)
- weight = models.IntegerField(
- default=0, db_index=True,
- help_text='Heavier items sink, lighter ones bubble up.')
-
- def __unicode__(self):
- return '%s in %s' % (self.response.title, self.category.title)
View
4 apps/customercare/templates/customercare/base.html
@@ -1,4 +0,0 @@
-{# vim: set ts=2 et sts=2 sw=2: #}
-{% extends "base.html" %}
-{% set styles = ('customercare', 'jqueryui/jqueryui') %}
-{% set scripts = ('customercare', 'libs/jqueryui') %}
View
148 apps/customercare/templates/customercare/landing.html
@@ -1,148 +0,0 @@
-{# vim: set ts=2 et sts=2 sw=2: #}
-{% extends "customercare/base.html" %}
-{% from 'includes/common_macros.html' import search_box %}
-{% set title = _('Join our Army of Awesome') %}
-{% set hide_plugin_check = True %}
-
-{% block breadcrumbs %}{% endblock %}
-
-{% block content %}
-<article class="main">
- <div class="feature-contents">
- <h1>{{ _('Join our <mark>Army of Awesome</mark>')|safe }}</h1>
- <h2>
- {% trans %}
- Love Firefox and have a few moments to help?<br />
- Help other Firefox users on Twitter.
- Good things will come to those who tweet!
- {% endtrans %}
- </h2>
- </div>
-
- <div id="speech-bubbles">
- <ol>
- <li class="choose">{{ _('Choose a <mark>tweet below</mark>')|safe }}</li>
- <li class="signin">{{ _('Sign in with <mark>Twitter</mark>')|safe }}</li>
- <li class="respond">{{ _('Respond to <mark>the tweet!</mark>')|safe }}</li>
- </ol>
- </div>
-
- <div id="tweetcontainer">
- <div class="tweets-header">
- <img id="twitter-icon" src="{{ MEDIA_URL }}img/customercare/twitter-icon.png" /><h2 class="showhide_heading" id="Where_to_ask_your_question">{{ _('Choose a tweet to help') }}</h2>
- <a id="refresh-tweets" href="{{ url('customercare.more_tweets') }}" class="header-button">{{ _('Refresh') }}<img id="refresh-busy" src="{{ MEDIA_URL }}img/customercare/spinner.gif"/></a>
- {% if authed %}
- <form id="twitter-logout" action="" method="post">
- {{ csrf() }}
- <input type="hidden" name="twitter_delete_auth" value="1">
- <input type="submit" class="header-button" value="{{ _('Sign out') }}">
- </form>
- {% else %}
- <a id="signin-button" href="#" class="header-button">{{ _('Sign in') }}</a>
- {% endif %}
- </div>
-
- <br style="clear:both; height: 1px" />
-
- <div id="tweets-wrap">
- {% if not tweets %}
- <div class="warning-box">
- {% trans language=settings.LOCALES[request.locale].native %}
- We couldn't find any recent tweets for {{ language }} at this time.
- Please check again later or view tweets for other languages
- by using the language selector at the bottom of the page.
- {% endtrans %}
- </div>
- {% endif %}
- <ul id="tweets">
- {% include 'customercare/tweets.html' %}
- </ul>
- </div>
-
- <div id="infinite-scroll">
- <img id="scroll-busy" src="{{ MEDIA_URL }}img/customercare/spinner.gif"/>
- </div>
- </div>
-
- {% include 'customercare/reply_modal.html' %}
-
- {% include 'customercare/twitter_modal.html' %}
-</article>
-{% endblock %}
-
-{% block side_promos %}
- <section id="side-stats">
- <h1>{{ _('Our army has responded to:') }}</h1>
- {% if not activity_stats %}
- <p class="unavailable">{{ _('Recent stats not available.') }}</p>
- {% else %}
- <div class="bubble">
- <div class="perc">
- {{ _('<mark>{percent}%</mark> of tweets')|fe(percent=activity_stats[0][1]['perc']|round_percent) }}
- </div>
- <div class="numbers">
- <div class="replies">
- {{ _('<mark>{num_replies}</mark> replies')|fe(num_replies=activity_stats[0][1]['replies']) }}
- </div>
- <div class="separator">/</div>
- <div class="tweets">
- {{ _('<mark>{num_requests}</mark> tweets')|fe(num_requests=activity_stats[0][1]['requests']) }}
- </div>
- </div>
-
- <select>
- {% for act in activity_stats %}
- <option value="{{ loop.index0 }}"
- data-perc ="{{ act[1]['perc']|round_percent }}"
- data-replies="{{ act[1]['replies'] }}"
- data-requests="{{ act[1]['requests'] }}">
- {% if act[0] == 'Yesterday' %}
- {{ _('Yesterday', 'army_stats') }}
- {% elif act[0] == 'Last Week'%}
- {{ _('Last Week', 'army_stats') }}
- {% elif act[0] == 'Last Month'%}
- {{ _('Last Month', 'army_stats') }}
- {% elif act[0] == 'Overall'%}
- {{ _('Last Overall', 'army_stats') }}
- {% else %}
- {{ act[0] }}
- {% endif %}
- </option>
- {% endfor %}
- </select>
- </div>
- <div class="speech"></div>
- {% endif %}
-
- <div class="contribs">
- {% if contributor_stats %}
- {% for act in activity_stats %}
- {% set period = act[0] %}
- <div class="contributors period{{ loop.index0 }}"
- data-period="{{ period }}">
- {% for contrib in contributor_stats.get(period, []) %}
- <a href="http://twitter.com/{{ contrib['username'] }}" target="_blank">
- <img src="{{ contrib['avatar'] }}" alt="{{ contrib['username'] }}"
- title="{{ contrib['name']}}: {{ contrib['count'] }} replies"
- class="avatar" />
- </a>
- {% endfor %}
- </div>
- {% endfor %}
- {% endif %}
- </div>
- </section>
- <section id="next-level">
- <h1>{{ _('Take it to the <mark>next level!</mark>')|safe }}</h1>
- <div class="img">
- <img src="{{ settings.MEDIA_URL }}img/promo.sumo.png" alt="" />
- </div>
- <p>
- {% trans join_url=url('wiki.document', 'superheroes-wanted') %}
- Want to go beyond 140 characters?
- <a href="{{ join_url }}">Join the support community</a>
- and help many more Firefox users.
- {% endtrans %}
- </p>
- </section>
-{% endblock %}
View
72 apps/customercare/templates/customercare/reply_modal.html
@@ -1,72 +0,0 @@
-{# vim: set ts=2 et sts=2 sw=2: #}
-<div id="reply-modal" class="kbox" data-modal="true" title="{{ _('Reply') }}">
- <div id="reply-container">
-
- <div id="initial-tweet">
- <a href="" class="avatar" target="_blank"><img src="" /></a>
- <span class="box">
- <img src="{{ MEDIA_URL }}img/customercare/initial-tweet-arrow.png" alt="" id="arrow" />
- <a href="" class="twittername" target="_blank"></a>
- <a href="" class="permalink" target="_blank">
- <time datetime=""></time>
- </a>
- <div class="text"></div>
- </span>
- </div>
-
- <div id="replies">
- <h4>{{ _('What is your reply about?') }}</h4>
- {% if not canned_responses %}
- <span id="no-responses">
- {% trans signpost_help_url='https://wiki.mozilla.org/Army_of_Awesome/Signposts', contact_email='atopal@mozilla.com'|public_email %}
- We have created <a href="{{ signpost_help_url }}">signpost messages</a>
- for easily replying to top Firefox topics on Twitter.
- If you would like to have messages for your language appear
- here, contact Kadir Topal for more info: {{ contact_email }}
- {% endtrans %}
- </span>
- {% else %}
- <div id="accordion">
- {% for resp in canned_responses %}
- <h3><a href="#">{{ resp.title }}</a></h3>
- <div>
- <ul class="topics">
- {% for topic in resp.responses.all() %}
- <li>
- <a class="reply-topic" href="#">{{ topic.title }}</a>
- <span class="snippet">{{ topic.response }}</span>
- </li>
- {% endfor %}
- </ul>
- </div>
- {% endfor %}
- </div>
- {% endif %}
-
- <div class="hrbreak"></div>
-
- <div id="reply">
- <h4>{{ _('Get personal') }}</h4>
- <p class="desc">
- {% trans %}
- Tweak it and make it your own.
- Personalized messages go a long way in helping others.
- {% endtrans %}
- </p>
- <div class="container">
- <div class="character-counter">140</div>
- <form action="{{ url('customercare.twitter_post') }}" method="POST">
- <div class="inner-container">
- <img src="{{ MEDIA_URL }}img/customercare/reply-arrow.png" alt="" id="reply-arrow" />
- <textarea id="reply-message"></textarea>
- </div>
-
- <span id="submit-message">{{ _('Your message was sent!') }}</span>
- <span id="error-message"></span>
- <a href="#submit" id="submit" class="submitButton" title="{{ _('Submit') }}">{{ _('Submit') }} <img id="submit-busy" src="{{ MEDIA_URL }}img/customercare/spinner.gif"/></a>
- </form>
- </div>
- </div>
- </div>
- </div>
-</div>
View
36 apps/customercare/templates/customercare/tweets.html
@@ -1,36 +0,0 @@
-{# vim: set ts=2 et sts=2 sw=2: #}
-{% for tweet in tweets -%}
- <li class="tweet{% if tweet.reply_to %} reply{% endif %}" data-tweet-id="{{ tweet.id }}" id="tweet-{{ tweet.id }}">
- <div class="tweet-contents{{ loop.cycle('', ' alt') }}">
- <a href="http://twitter.com/{{ tweet.user }}" class="avatar" target="_blank">
- <img src="{{ tweet.profile_img }}" />
- <span class="twittername">{{ tweet.user }}</span>
- </a>
- <a href="http://twitter.com/{{ tweet.user }}/status/{{ tweet.id }}" class="permalink" target="_blank">
- <time datetime="{{ tweet.date|isotime }}">{{ tweet.date|utctimesince }} ago</time>
- </a>
- {% if tweet.replies %}
- <a href="#" class="reply_count" data-count="{{ tweet.reply_count }}">
- {% if tweet.reply_count > 1 %}
- {{ ngettext('{0} and {1} other replied', '{0} and {1} others replied',
- tweet.reply_count - 1)|f(tweet.replies[-1].user, tweet.reply_count - 1) }}
- {% else %}
- {{ _('{0} replied')|f(tweet.replies[0].user) }}
- {% endif %}
- </a>
- {% elif settings.CC_SHOW_REPLIES %}
- <span class="reply_count">{{ _('Reply now') }}</span>
- {% endif %}
- <p class="text">{{ tweet.text|safe }}</p>
- </div>
- <div id="replies_{{ tweet.id }}" class="replies" data-tweet-id="{{ tweet.id }}">
- <ul>
- {% if tweet.replies %}
- {% with tweets = tweet.replies %}
- {% include 'customercare/tweets.html' %}
- {% endwith %}
- {% endif %}
- </ul>
- </div>
- </li>
-{% endfor %}
View
16 apps/customercare/templates/customercare/twitter_modal.html
@@ -1,16 +0,0 @@
-{# vim: set ts=2 et sts=2 sw=2: #}
-<div id="twitter-modal" class="kbox" title="{{ _('Sign in with your Twitter account') }}" data-modal="true" data-authed="{{ authed }}" data-twitter-user="{{ twitter_user }}">
- <div class="inner-container">
- <p>
- {% trans %}
- Before you join the Army of Awesome, you need to log in so you
- can respond to tweets.
- You will now be redirected to Twitter to log in.
- {% endtrans %}
- </p>
- <div>
- <a href="#" class="cancel kbox-cancel">{{ _('Cancel') }}</a>
- <a href="?twitter_auth_request=1" class="signin">{{ _('Sign in') }}</a>
- </div>
- </div>
-</div>
View
48 apps/customercare/tests/__init__.py
@@ -1,48 +0,0 @@
-from datetime import datetime
-import random
-
-from django.conf import settings
-
-from customercare import models
-
-
-def cc_category(save=True, **kwargs):
- """Return a canned category."""
- responses = kwargs.pop('responses', [])
- save = save or responses # Adding responses forces save.
- defaults = {'title': str(datetime.now()),
- 'weight': random.choice(range(50)),
- 'locale': settings.LANGUAGE_CODE}
- defaults.update(kwargs)
-
- category = models.CannedCategory(**defaults)
- if save:
- category.save()
- # Add responses to this category.
- for response, weight in responses:
- models.CategoryMembership.objects.create(
- category=category, response=response, weight=weight)
-
- return category
-
-
-def cc_response(save=True, **kwargs):
- """Return a canned response."""
- categories = kwargs.pop('categories', [])
- save = save or categories # Adding categories forces save.
-
- defaults = {'title': str(datetime.now()),
- 'response': 'Test response (%s).' % random.choice(range(50)),
- 'locale': settings.LANGUAGE_CODE}
- defaults.update(kwargs)
-
- response = models.CannedResponse(**defaults)
- if save:
- response.save()
- # Add categories to this response.
- for category, weight in categories:
- weight = random.choice(range(50))
- models.CategoryMembership.objects.create(
- category=category, response=response, weight=weight)
-
- return response
View
105 apps/customercare/tests/test_cron.py
@@ -1,105 +0,0 @@
-import copy
-
-from django.conf import settings
-
-from mock import patch_object
-from nose.tools import eq_
-
-from customercare.cron import _filter_tweet, _get_oldest_tweet, purge_tweets
-from customercare.models import Tweet
-from sumo.tests import TestCase
-
-
-class TwitterCronTestCase(TestCase):
- tweet_template = {
- "profile_image_url": (
- "http://a3.twimg.com/profile_images/688562959/"
- "jspeis_gmail.com_852af0c8__1__normal.jpg"),
- "created_at": "Mon, 25 Oct 2010 18:12:20 +0000",
- "from_user": "jspeis",
- "metadata": {
- "result_type": "recent",
- },
- "to_user_id": None,
- "text": "giving the Firefox 4 beta a whirl",
- "id": 28713868836,
- "from_user_id": 2385258,
- "geo": None,
- "iso_language_code": "en",
- "source": "&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"
- }
-
- def setUp(self):
- self.tweet = copy.deepcopy(self.tweet_template)
-
- def test_unfiltered(self):
- """Do not filter tweets without a reason."""
- eq_(self.tweet, _filter_tweet(self.tweet))
-
- def test_mentions(self):
- """Filter out mentions."""
- self.tweet['text'] = 'Hey @someone!'
- assert _filter_tweet(self.tweet) is None
-
- def test_replies(self):
- self.tweet['to_user_id'] = 12345
- self.tweet['text'] = '@someone Hello!'
- assert _filter_tweet(self.tweet) is None
-
- def test_retweets(self):
- """No retweets or 'via'"""
- self.tweet['text'] = 'RT @someone: Firefox is awesome'
- assert _filter_tweet(self.tweet) is None
-
- self.tweet['text'] = 'Firefox is awesome (via @someone)'
- assert _filter_tweet(self.tweet) is None
-
- def test_links(self):
- """Filter out tweets with links."""
- self.tweet['text'] = 'Just watching: http://youtube.com/12345 Fun!'
- assert _filter_tweet(self.tweet) is None
-
- def test_fx4status(self):
- """Ensure fx4status tweets are filtered out."""
- self.tweet['from_user'] = 'fx4status'
- assert _filter_tweet(self.tweet) is None
-
-
-class GetOldestTweetTestCase(TestCase):
- fixtures = ['tweets.json']
-
- def test_get_oldest_tweet_exists(self):
- eq_(11, _get_oldest_tweet('en', 2).pk)
- eq_(4, _get_oldest_tweet('en', 0).pk)
- eq_(21, _get_oldest_tweet('en', 6).pk)
-
- def test_get_oldest_tweet_offset_too_big(self):
- eq_(None, _get_oldest_tweet('en', 100))
-
- def test_get_oldest_tweet_none_exist(self):
- eq_(None, _get_oldest_tweet('fr', 0))
- eq_(None, _get_oldest_tweet('fr', 1))
- eq_(None, _get_oldest_tweet('fr', 20))
-
-
-class PurgeTweetsTestCase(TestCase):
- """Tweets are deleted for each locale."""
- fixtures = ['tweets.json']
-
- @patch_object(settings._wrapped, 'CC_MAX_TWEETS', 1)
- def test_purge_tweets_two_locales(self):
- purge_tweets()
- eq_(1, Tweet.objects.filter(locale='en').count())
- eq_(1, Tweet.objects.filter(locale='ro').count())
-
- @patch_object(settings._wrapped, 'CC_MAX_TWEETS', 3)
- def test_purge_tweets_one_locale(self):
- purge_tweets()
- eq_(3, Tweet.objects.filter(locale='en').count())
- # Does not touch Romanian tweets.
- eq_(2, Tweet.objects.filter(locale='ro').count())
-
- @patch_object(settings._wrapped, 'CC_MAX_TWEETS', 0)
- def test_purge_all_tweets(self):
- purge_tweets()
- eq_(0, Tweet.objects.count())
View
29 apps/customercare/tests/test_helpers.py
@@ -1,29 +0,0 @@
-from datetime import datetime
-
-from nose.tools import eq_
-
-from customercare.helpers import isotime, round_percent
-from sumo.tests import TestCase
-
-
-def test_isotime():
- """Test isotime helper."""
- time = datetime(2009, 12, 25, 10, 11, 12)
- eq_(isotime(time), '2009-12-25T18:11:12Z')
-
- assert isotime(None) is None
-
-
-class RoundPercentTests(TestCase):
- """Tests for round_percent."""
- def test_high_percent_int(self):
- eq_('90', str(round_percent(90)))
-
- def test_high_percent_float(self):
- eq_('90', str(round_percent(90.3456)))
-
- def test_low_percent_int(self):
- eq_('6.0', str(round_percent(6)))
-
- def test_low_percent_float(self):
- eq_('6.3', str(round_percent(6.299)))
View
56 apps/customercare/tests/test_templates.py
@@ -1,56 +0,0 @@
-from nose.tools import eq_
-from pyquery import PyQuery as pq
-
-from customercare.tests import cc_category, cc_response
-from sumo.urlresolvers import reverse
-from sumo.tests import TestCase
-
-
-class CannedResponsesTestCase(TestCase):
- """Canned responses tests."""
-
- def test_empty_canned_responses(self):
- """When no canned responses are available, fall back to a message."""
- r = self.client.get(reverse('customercare.landing'), follow=True)
- eq_(200, r.status_code)
- doc = pq(r.content)
- assert doc('#no-responses'), 'Fallback message is not showing up.'
- assert doc('#no-responses .email'), 'Must haz contact email.'
-
- def test_list_canned_responses(self):
- """Listing canned responses works as expected."""
- c1 = cc_category(weight=0)
- c2 = cc_category(weight=20)
- c3 = cc_category(locale='fr')
- r1 = cc_response(categories=[(c1, 0)])
- r2 = cc_response(categories=[(c1, 1), (c2, 0)])
- r3 = cc_response(categories=[(c1, 2), (c2, 1), (c3, 0)])
- r4 = cc_response(categories=[(c3, 1)])
-
- r = self.client.get(reverse('customercare.landing'), follow=True)
- eq_(200, r.status_code)
- doc = pq(r.content)
- responses_plain = doc('#accordion').text()
- assert r1.title in responses_plain
- assert r4.title not in responses_plain
- assert r3.title in responses_plain
- # Ordering works for categories.
- assert responses_plain.find(c1.title) < responses_plain.find(c2.title)
- # And for responses within a category.
- assert responses_plain.find(r1.title) < responses_plain.find(r2.title)
- assert responses_plain.find(r2.title) < responses_plain.find(r3.title)
-
- # Listing 5 responses: r1 x 3, r2 x 2, r3 x 1
- eq_(5, len(doc('#accordion a.reply-topic')))
-
-
-class TweetListTestCase(TestCase):
- """Tests for the list of tweets."""
-
- def test_fallback_message(self):
- """Fallback message when there are no tweets."""
- r = self.client.get(reverse('customercare.landing'), follow=True)
- eq_(200, r.status_code)
- doc = pq(r.content)
- assert doc('#tweets-wrap .warning-box'), (
- 'Fallback message is not showing up.')
View
30 apps/customercare/tests/test_views.py
@@ -1,30 +0,0 @@
-from nose.tools import eq_
-
-from customercare.views import _get_tweets
-from sumo.tests import TestCase
-
-
-class TweetListTestCase(TestCase):
- """Tests for the customer care tweet list."""
-
- fixtures = ['tweets.json']
-
- def test_limit(self):
- """Do not return more than LIMIT tweets."""
- tweets = _get_tweets(limit=2)
- eq_(len(tweets), 2)
-
- def test_max_id(self):
- """Ensure max_id offset works."""
- tweets_1 = _get_tweets()
- assert tweets_1
-
- # Select max_id from the first list
- max_id = tweets_1[3]['id']
- tweets_2 = _get_tweets(max_id=max_id)
- assert tweets_2
-
- # Make sure this id is not in the result, and all tweets are older than
- # max_id.
- for tweet in tweets_2:
- assert tweet['id'] < max_id
View
7 apps/customercare/urls.py
@@ -1,7 +0,0 @@
-from django.conf.urls.defaults import patterns, url
-
-urlpatterns = patterns('customercare.views',
- url(r'/more_tweets', 'more_tweets', name="customercare.more_tweets"),
- url(r'/twitter_post', 'twitter_post', name="customercare.twitter_post"),
- url(r'', 'landing', name='customercare.landing'),
-)
View
188 apps/customercare/views.py
@@ -1,188 +0,0 @@
-import calendar
-from datetime import datetime
-from email.utils import parsedate, formatdate
-import json
-import logging
-
-from django.conf import settings
-from django.core.cache import cache
-from django.http import HttpResponseBadRequest
-from django.views.decorators.http import require_POST, require_GET
-
-from babel.numbers import format_number
-import bleach
-import jingo
-from tower import ugettext as _
-import tweepy
-
-from .models import CannedCategory, Tweet
-import twitter
-
-
-log = logging.getLogger('k.customercare')
-
-MAX_TWEETS = 20
-
-
-def _tweet_for_template(tweet):
- """Return the dict needed for tweets.html to render a tweet + replies."""
- data = json.loads(tweet.raw_json)
-
- parsed_date = parsedate(data['created_at'])
- date = datetime(*parsed_date[0:6])
-
- # Recursively fetch replies.
- if settings.CC_SHOW_REPLIES:
- replies = _get_tweets(limit=0, reply_to=tweet.tweet_id)
- else:
- replies = None
-
- return {'profile_img': bleach.clean(data['profile_image_url']),
- 'user': bleach.clean(data['from_user']),
- 'text': bleach.clean(data['text']),
- 'id': int(tweet.tweet_id),
- 'date': date,
- 'reply_count': len(replies) if replies else 0,
- 'replies': replies,
- 'reply_to': tweet.reply_to}
-
-
-def _get_tweets(locale=settings.LANGUAGE_CODE,
- limit=MAX_TWEETS, max_id=None, reply_to=None):
- """
- Fetch a list of tweets.
-
- limit is the maximum number of tweets returned.
- max_id will only return tweets with the status ids less than the given id.
- """
- locale = settings.LOCALES[locale].iso639_1
- q = Tweet.objects.filter(locale=locale, reply_to=reply_to)
- if max_id:
- q = q.filter(tweet_id__lt=max_id)
- if limit:
- q = q[:limit]
-
- return [_tweet_for_template(tweet) for tweet in q]
-
-
-@require_GET
-def more_tweets(request):
- """AJAX view returning a list of tweets."""
- max_id = request.GET.get('max_id')
- return jingo.render(request, 'customercare/tweets.html',
- {'tweets': _get_tweets(locale=request.locale,
- max_id=max_id)})
-
-
-@require_GET
-@twitter.auth_wanted
-def landing(request):
- """Customer Care Landing page."""
-
- twitter = request.twitter
-
- canned_responses = CannedCategory.objects.filter(locale=request.locale)
-
- # Stats. See customercare.cron.get_customercare_stats.
- activity = cache.get(settings.CC_TWEET_ACTIVITY_CACHE_KEY)
- if activity:
- activity_stats = []
- for act in activity['resultset']:
- activity_stats.append((act[0], {
- 'requests': format_number(act[1], locale='en_US'),
- 'replies': format_number(act[2], locale='en_US'),
- 'perc': act[3] * 100,
- }))
- else:
- activity_stats = None
-
- contributors = cache.get(settings.CC_TOP_CONTRIB_CACHE_KEY)
- if contributors:
- contributor_stats = {}
- for contrib in contributors['resultset']:
- # Create one list per time period
- period = contrib[1]
- if not contributor_stats.get(period):
- contributor_stats[period] = []
- elif len(contributor_stats[period]) == 16:
- # Show a max. of 16 people.
- continue
-
- contributor_stats[period].append({
- 'name': contrib[2],
- 'username': contrib[3],
- 'count': contrib[4],
- 'avatar': contributors['avatars'].get(contrib[3]),
- })
- else:
- contributor_stats = None
-
- return jingo.render(request, 'customercare/landing.html', {
- 'activity_stats': activity_stats,
- 'contributor_stats': contributor_stats,
- 'canned_responses': canned_responses,
- 'tweets': _get_tweets(locale=request.locale),
- 'authed': twitter.authed,
- 'twitter_user': (twitter.api.auth.get_username() if
- twitter.authed else None),
- })
-
-
-@require_POST
-@twitter.auth_required
-def twitter_post(request):
- """Post a tweet, and return a rendering of it (and any replies)."""
-
- try:
- reply_to = int(request.POST.get('reply_to', ''))
- except ValueError:
- # L10n: the tweet needs to be a reply to another tweet.
- return HttpResponseBadRequest(_('Reply-to is empty'))
-
- content = request.POST.get('content', '')
- if len(content) == 0:
- # L10n: the tweet has no content.
- return HttpResponseBadRequest(_('Message is empty'))
-
- if len(content) > 140:
- return HttpResponseBadRequest(_('Message is too long'))
-
- try:
- result = request.twitter.api.update_status(content, reply_to)
- except tweepy.TweepError, e:
- # L10n: {message} is an error coming from our twitter api library
- return HttpResponseBadRequest(
- _('An error occured: {message}').format(message=e))
-
- # Store reply in database.
-
- # If tweepy's status models actually implemented a dictionary, it would
- # be too boring.
- status = dict(result.__dict__)
- author = dict(result.author.__dict__)
-
- # Raw JSON blob data
- raw_tweet_data = {
- 'id': status['id'],
- 'text': status['text'],
- 'created_at': formatdate(calendar.timegm(
- status['created_at'].timetuple())),
- 'iso_language_code': author['lang'],
- 'from_user_id': author['id'],
- 'from_user': author['screen_name'],
- 'profile_image_url': author['profile_image_url'],
- }
- # Tweet metadata
- tweet_model_data = {
- 'tweet_id': status['id'],
- 'raw_json': json.dumps(raw_tweet_data),
- 'locale': author['lang'],
- 'created': status['created_at'],
- 'reply_to': reply_to,
- }
- tweet = Tweet(**tweet_model_data)
- tweet.save()
-
- # We could optimize by not encoding and then decoding JSON.
- return jingo.render(request, 'customercare/tweets.html',
- {'tweets': [_tweet_for_template(tweet)]})
View
38 puppet/files/etc/apache2/conf.d/mozilla-kuma-apache.conf
@@ -1,3 +1,5 @@
+Listen 8080
+
WSGISocketPrefix /var/run/wsgi
<VirtualHost *:80>
@@ -65,3 +67,39 @@ WSGISocketPrefix /var/run/wsgi
RewriteRule ^/media/uploads/(.*)$ https://developer.mozilla.org/media/uploads/$1 [P,L]
</VirtualHost>
+
+<VirtualHost *:8080>
+ ServerName developer-local.allizom.org
+
+ DocumentRoot "/opt/graphite/webapp"
+ ErrorLog /opt/graphite/storage/log/webapp/error.log
+ CustomLog /opt/graphite/storage/log/webapp/access.log common
+
+ # Set up graphite via mod_wsgi
+ WSGIDaemonProcess graphite processes=5 threads=5 display-name='%{GROUP}' inactivity-timeout=120
+ WSGIProcessGroup graphite
+ WSGIApplicationGroup %{GLOBAL}
+ WSGIImportScript /opt/graphite/conf/graphite.wsgi process-group=graphite application-group=%{GLOBAL}
+ WSGIScriptAlias / /opt/graphite/conf/graphite.wsgi
+
+ Alias /content/ /opt/graphite/webapp/content/
+ <Location "/content/">
+ SetHandler None
+ </Location>
+
+ # XXX In order for the django admin site media to work you
+ # must change @DJANGO_ROOT@ to be the path to your django
+ # installation, which is probably something like:
+ # /usr/lib/python2.6/site-packages/django
+ Alias /media/ "/usr/local/lib/python2.7/dist-packages/django/contrib/admin/media/"
+ <Location "/media/">
+ SetHandler None
+ </Location>
+
+ # The graphite.wsgi file has to be accessible by apache. It won't
+ # be visible to clients because of the DocumentRoot though.
+ <Directory /opt/graphite/conf/>
+ Order deny,allow
+ Allow from all
+ </Directory>
+</VirtualHost>
View
11 puppet/files/etc/init/carbon-cache.conf
@@ -0,0 +1,11 @@
+description "Graphite Carbon Cache Daemon"
+
+start on runlevel [23]
+stop on shutdown
+
+expect daemon
+respawn
+
+pre-start exec rm -f /opt/graphite/storage/carbon-cache-a.pid
+
+exec /opt/graphite/bin/carbon-cache.py start
View
14 puppet/files/etc/init/statsd.conf
@@ -0,0 +1,14 @@
+# cat /etc/init/statsd.conf
+description "statsd"
+author "lorchard"
+
+start on startup
+stop on shutdown
+
+expect daemon
+respawn
+
+script
+ export HOME="/home/vagrant"
+ exec sudo -u vagrant node /home/vagrant/node_modules/.bin/statsd /home/vagrant/statsd-config.js
+end script
View
5 puppet/files/home/vagrant/statsd-config.js
@@ -0,0 +1,5 @@
+{
+ graphitePort: 2003,
+ graphiteHost: "127.0.0.1",
+ port: 8125
+}
View
10 puppet/files/opt/graphite/conf/storage-schemas.conf
@@ -0,0 +1,10 @@
+# Schema definitions for Whisper files. Entries are scanned in order,
+# and first match wins. This file is scanned for changes every 60 seconds.
+#
+# [name]
+# pattern = regex
+# retentions = timePerPoint:timeToStore, timePerPoint:timeToStore, ...
+[stats]
+priority = 110
+pattern = ^stats\..*
+retentions = 10s:6h,1m:7d,10m:1y
View
7 puppet/files/tmp/graphite_reqs.txt
@@ -0,0 +1,7 @@
+django==1.3
+python-memcached
+django-tagging
+twisted
+whisper==0.9.9
+carbon==0.9.9
+graphite-web==0.9.9
View
2  puppet/files/vagrant/kumascript_settings_local.json
@@ -7,7 +7,7 @@
}
},
"statsd": {
- "enabled": false,
+ "enabled": true,
"host": "127.0.0.1",
"port": 8125
},
View
5 puppet/files/vagrant/settings_local.py
@@ -30,7 +30,7 @@
#EMAIL_FILE_PATH = '/home/vagrant/logs/kuma-email.log'
# Uncomment to enable a real celery queue
-CELERY_ALWAYS_EAGER = False
+#CELERY_ALWAYS_EAGER = False
INSTALLED_APPS = INSTALLED_APPS + (
"django_extensions",
@@ -59,7 +59,6 @@
'debug_toolbar.panels.sql.SQLDebugPanel',
'debug_toolbar.panels.signals.SignalDebugPanel',
'debug_toolbar.panels.logger.LoggingPanel',
- 'django_statsd.panel.StatsdPanel',
)
DEVSERVER_MODULES = (
@@ -130,8 +129,6 @@
KUMASCRIPT_URL_TEMPLATE = 'http://localhost:9080/docs/{path}'
-STATSD_CLIENT = 'django_statsd.clients.toolbar'
-
ATTACHMENT_HOST = 'mdn-local.mozillademos.org'
ES_DISABLED = False
View
4 puppet/manifests/classes/dev-hacks.pp
@@ -11,11 +11,13 @@
class dev_hacks {
exec { 'locale-gen':
- command => "/usr/sbin/locale-gen en_US.utf8"
+ command => "/usr/sbin/locale-gen en_US.utf8",
+ unless => '/bin/grep -q "en_US.utf8" /etc/default/locale'
}
exec { 'update-locale':
command => "/usr/sbin/update-locale LC_ALL='en_US.utf8'",
+ unless => '/bin/grep -q "en_US.utf8" /etc/default/locale',
require => Exec['locale-gen']
}
View
84 puppet/manifests/classes/statsd.pp
@@ -0,0 +1,84 @@
+# see also: http://www.kinvey.com/blog/89/how-to-set-up-metric-collection-using-graphite-and-statsd-on-ubuntu-1204-lts
+class statsd {
+ exec { 'statsd-install':
+ cwd => '/home/vagrant',
+ user => 'vagrant',
+ # TODO: This revision works, but try to see what statsd runs in mozilla infra
+ command => '/usr/bin/npm install git://github.com/etsy/statsd.git#922e9e58c57ae4e61268cbd6925c112f0e4e468c',
+ creates => '/home/vagrant/node_modules/statsd',
+ require => [Package["nodejs"], Package["npm"]]
+ }
+ file { '/home/vagrant/statsd-config.js':
+ source => '/vagrant/puppet/files/home/vagrant/statsd-config.js',
+ owner => "vagrant", group => "vagrant", mode => 0664,
+ require => Exec['statsd-install']
+ }
+ package {
+ ['sqlite3', 'libcairo2', 'libcairo2-dev', 'python-cairo', 'pkg-config']:
+ ensure => present;
+ }
+ exec { 'graphite-install':
+ cwd => '/tmp',
+ timeout => 1200, # Too long, but this can take awhile
+ command => '/usr/bin/pip install --download-cache=/vagrant/puppet/cache/pip -r /vagrant/puppet/files/tmp/graphite_reqs.txt',
+ creates => '/opt/graphite/webapp/graphite/manage.py';
+ }
+ file { '/opt/graphite/conf/carbon.conf':
+ source => '/opt/graphite/conf/carbon.conf.example',
+ owner => "root", group => "www-data", mode => 0664,
+ require => Exec['graphite-install']
+ }
+ file { '/opt/graphite/webapp/graphite/local_settings.py':
+ source => '/opt/graphite/webapp/graphite/local_settings.py.example',
+ owner => "root", group => "www-data", mode => 0664,
+ require => Exec['graphite-install']
+ }
+ file { '/opt/graphite/conf/graphite.wsgi':
+ source => '/opt/graphite/conf/graphite.wsgi.example',
+ owner => "root", group => "www-data", mode => 0664,
+ require => Exec['graphite-install']
+ }
+ file { '/opt/graphite/storage/log/webapp':
+ ensure => directory,
+ owner => "www-data", group => "www-data", mode => 0775,
+ require => Exec['graphite-install']
+ }
+ file { '/opt/graphite/conf/storage-schemas.conf':
+ source => '/vagrant/puppet/files/opt/graphite/conf/storage-schemas.conf',
+ owner => "root", group => "www-data", mode => 0664,
+ require => Exec['graphite-install']
+ }
+ file { '/etc/init/statsd.conf':
+ source => '/vagrant/puppet/files/etc/init/statsd.conf',
+ owner => "root", group => "www-data", mode => 0775,
+ require => Exec['statsd-install']
+ }
+ file { '/etc/init/carbon-cache.conf':
+ source => '/vagrant/puppet/files/etc/init/carbon-cache.conf',
+ owner => "root", group => "www-data", mode => 0775,
+ require => Exec['graphite-install']
+ }
+ exec { 'graphite-syncdb':
+ cwd => '/opt/graphite/webapp/graphite',
+ command => '/usr/bin/python manage.py syncdb --noinput',
+ creates => '/opt/graphite/storage/graphite.db',
+ require => Exec['graphite-install']
+ }
+ exec { 'graphite-superuser':
+ cwd => '/opt/graphite/webapp/graphite',
+ command => '/usr/bin/python manage.py loaddata /vagrant/puppet/files/tmp/graphite_auth.json',
+ unless => "/usr/bin/sqlite3 /opt/graphite/storage/graphite.db 'select * from auth_user' | /bin/grep -q admin",
+ require => Exec['graphite-syncdb']
+ }
+ service { 'carbon-cache':
+ ensure => running,
+ enable => true,
+ require => File['/etc/init/carbon-cache.conf']
+ }
+ service { 'statsd':
+ ensure => running,
+ enable => true,
+ require => File['/etc/init/statsd.conf']
+ }
+
+}
View
5 puppet/manifests/dev-vagrant.pp
@@ -17,7 +17,8 @@
tools: before => Stage[basics];
basics: before => Stage[langs];
langs: before => Stage[vendors];
- vendors: before => Stage[main];
+ vendors: before => Stage[extras];
+ extras: before => Stage[main];
vendors_post: require => Stage[main];
# Stage[main]
hacks_post: require => Stage[vendors_post];
@@ -39,6 +40,8 @@
nodejs: stage => langs;
python: stage => langs;
+ statsd: stage => extras;
+
site_config: stage => main;
dev_hacks_post: stage => hacks_post;
}
View
20 settings.py
@@ -502,7 +502,6 @@ def JINJA_CONFIG():
'messages': [
('vendor/**', 'ignore'),
('apps/access/**', 'ignore'),
- ('apps/customercare/**', 'ignore'),
('apps/dashboards/**', 'ignore'),
('apps/flagit/**', 'ignore'),
('apps/forums/**', 'ignore'),
@@ -680,12 +679,6 @@ def JINJA_CONFIG():
'js/libs/DataTables-1.9.4/media/js/jquery.dataTables.js',
'js/libs/DataTables-1.9.4/extras/Scroller/media/js/dataTables.scroller.js',
),
- 'customercare': (
- 'js/libs/jquery.NobleCount.js',
- 'js/libs/jquery.cookie.js',
- 'js/libs/jquery.bullseye-1.0.min.js',
- 'js/users.js',
- ),
'users': (
'js/users.js',
),
@@ -837,19 +830,6 @@ def read_only_mode(env):
# Do not change this without also deleting all wiki documents:
WIKI_DEFAULT_LANGUAGE = LANGUAGE_CODE
-# Customer Care settings
-CC_MAX_TWEETS = 500 # Max. no. of tweets in DB
-CC_TWEETS_PERPAGE = 100 # How many tweets to collect in one go. Max: 100.
-CC_SHOW_REPLIES = True # Show replies to tweets?
-
-CC_TWEET_ACTIVITY_URL = 'https://metrics.mozilla.com/stats/twitter/armyOfAwesomeKillRate.json' # Tweet activity stats
-CC_TOP_CONTRIB_URL = 'https://metrics.mozilla.com/stats/twitter/armyOfAwesomeTopSoldiers.json' # Top contributor stats
-CC_TWEET_ACTIVITY_CACHE_KEY = 'sumo-cc-tweet-stats'
-CC_TOP_CONTRIB_CACHE_KEY = 'sumo-cc-top-contrib-stats'
-CC_STATS_CACHE_TIMEOUT = 24 * 60 * 60 # 24 hours
-CC_STATS_WARNING = 30 * 60 * 60 # Warn if JSON data is older than 30 hours
-CC_IGNORE_USERS = ['fx4status'] # User names whose tweets to ignore.
-
TWITTER_CONSUMER_KEY = ''
TWITTER_CONSUMER_SECRET = ''

No commit comments for this range

Something went wrong with that request. Please try again.