зеркало из https://github.com/mozilla/kitsune.git
Merge branch 'cc-tweet-summary-612952'
This commit is contained in:
Коммит
9effabdf32
|
@ -5,14 +5,17 @@ import logging
|
|||
import re
|
||||
import rfc822
|
||||
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
|
||||
import tweepy
|
||||
|
||||
from .models import Tweet
|
||||
from customercare.models import Tweet
|
||||
|
||||
|
||||
SEARCH_URL = 'http://search.twitter.com/search.json'
|
||||
|
@ -118,3 +121,56 @@ def _filter_tweet(item):
|
|||
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_data = json.load(urllib2.urlopen(url))
|
||||
json_data['resultset'] = ''
|
||||
if not json_data['resultset']:
|
||||
raise KeyError('Result set was empty.')
|
||||
except Exception, e:
|
||||
log.error('Error updating %s: %s' % (cache_key, e))
|
||||
continue
|
||||
|
||||
# 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)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<li class="respond">Respond to the tweet!</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>
|
||||
|
@ -31,7 +31,7 @@
|
|||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<br style="clear:both; height: 1px" />
|
||||
|
||||
<ul id="tweets">
|
||||
|
@ -49,6 +49,61 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block extra_sidebar %}
|
||||
<div id="side-stats">
|
||||
<h3>Our army has responded to:</h3>
|
||||
{% if not activity_stats %}
|
||||
<p class="unavailable">Recent stats not available.</p>
|
||||
{% else %}
|
||||
<div class="bubble">
|
||||
<div class="perc">
|
||||
<span class="data">{{ activity_stats[0][1]['perc'] }}</span>%
|
||||
<span class="label">of tweets</span>
|
||||
</div>
|
||||
<div class="numbers">
|
||||
<div class="replies">
|
||||
<span class="data">{{ activity_stats[0][1]['replies'] }}</span>
|
||||
<span class="label">replies</span>
|
||||
</div>
|
||||
/
|
||||
<div class="tweets">
|
||||
<span class="data">{{ activity_stats[0][1]['requests'] }}</span>
|
||||
<span class="label">tweets</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select>
|
||||
{% for act in activity_stats %}
|
||||
<option value="{{ loop.index0 }}"
|
||||
data-perc ="{{ act[1]['perc'] }}"
|
||||
data-replies="{{ act[1]['replies'] }}"
|
||||
data-requests="{{ act[1]['requests'] }}">
|
||||
{{ act[0] }}
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div id="side-getinvolved">
|
||||
<h3>Take it to the next level!</h3>
|
||||
<p>Want to go beyond 140 characters? Join the support community and help many more
|
||||
|
|
|
@ -5,9 +5,11 @@ import logging
|
|||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
|
||||
from babel.numbers import format_number
|
||||
from bleach import Bleach
|
||||
import jingo
|
||||
import tweepy
|
||||
|
@ -79,7 +81,40 @@ def landing(request):
|
|||
|
||||
canned_responses = CannedCategory.objects.all()
|
||||
|
||||
# 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': int(round(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] = []
|
||||
|
||||
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(),
|
||||
'authed': twitter.authed,
|
||||
|
|
|
@ -67,13 +67,89 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#side-getinvolved {
|
||||
background: transparent url('../img/side-getinvolved-bg.png') no-repeat top left;
|
||||
margin-top: 140px;
|
||||
padding-top: 20px;
|
||||
color: #999186;
|
||||
#side-stats {
|
||||
margin-top: 40px;
|
||||
}
|
||||
#side-stats .unavailable {
|
||||
font-size: 1.2em;
|
||||
font-style: italic;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#side-stats .bubble {
|
||||
background-color: #E6F0F9;
|
||||
-webkit-border-radius: 50px;
|
||||
-moz-border-radius: 50px;
|
||||
border-radius: 50px;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
#side-stats .speech {
|
||||
background: transparent url(../img/customercare/bubble.png) no-repeat 50px bottom;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
#side-stats .label {
|
||||
color: #00639B;
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
font-size: .75em;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-variant: small-caps;
|
||||
}
|
||||
#side-stats .perc {
|
||||
font-size: 2.5em;
|
||||
font-style: italic;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
#side-stats .numbers {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
#side-stats .numbers .replies,
|
||||
#side-stats .numbers .tweets {
|
||||
text-align: center;
|
||||
width: 45%;
|
||||
}
|
||||
#side-stats .numbers .replies {
|
||||
float: left;
|
||||
}
|
||||
#side-stats .numbers .tweets {
|
||||
float: right;
|
||||
}
|
||||
#side-stats .numbers .data {
|
||||
display: block;
|
||||
margin-bottom: -.3em;
|
||||
}
|
||||
#side-stats select {
|
||||
font-family: Arial,Helvetica,sans-serif;
|
||||
margin: 10px 0 20px;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
}
|
||||
#side-stats .contribs {
|
||||
clear: both;
|
||||
margin: 10px 20px 0;
|
||||
width: 160px;
|
||||
}
|
||||
#side-stats .avatar {
|
||||
margin: 2px;
|
||||
width: 32px;
|
||||
}
|
||||
#side-stats .contribs .contributors {
|
||||
display: none;
|
||||
}
|
||||
#side-stats .contribs .contributors:first-child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#side-getinvolved {
|
||||
background: transparent url('../img/side-getinvolved-bg.png') no-repeat top left;
|
||||
padding-top: 20px;
|
||||
color: #999186;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
#side-stats h3,
|
||||
#side-getinvolved h3 {
|
||||
color: #1E4262;
|
||||
font-size: 150%;
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.1 KiB |
|
@ -340,5 +340,25 @@
|
|||
}
|
||||
);
|
||||
}).bullseye();
|
||||
|
||||
/* Statistics */
|
||||
$('#side-stats select').change(function(e) {
|
||||
var $this = $(this),
|
||||
option = $this.children('option[value=' + $this.val() + ']'),
|
||||
bubble = $('#side-stats .bubble')
|
||||
contribs = $('#side-stats .contribs');
|
||||
// Update numbers
|
||||
bubble.find('.perc .data').text(option.attr('data-perc'));
|
||||
bubble.find('.replies .data').text(option.attr('data-replies'));
|
||||
bubble.find('.tweets .data').text(option.attr('data-requests'));
|
||||
|
||||
// Update contributors
|
||||
contribs.find('.contributors:visible').fadeOut('fast', function() {
|
||||
contribs.find('.contributors.period' + $this.val()).fadeIn('fast');
|
||||
});
|
||||
|
||||
$this.blur();
|
||||
e.preventDefault();
|
||||
}).val('0');
|
||||
});
|
||||
}(jQuery));
|
||||
|
|
15
settings.py
15
settings.py
|
@ -535,16 +535,21 @@ THUMBNAIL_PROGRESS_WIDTH = 32 # width of the above image
|
|||
THUMBNAIL_PROGRESS_HEIGHT = 32 # height of the above image
|
||||
VIDEO_MAX_FILESIZE = 16777216 # 16 megabytes, in bytes
|
||||
|
||||
# Customer Care tweet collection 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.
|
||||
# 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?
|
||||
|
||||
# Show replies to tweets?
|
||||
CC_SHOW_REPLIES = True
|
||||
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
|
||||
|
||||
TWITTER_CONSUMER_KEY = ''
|
||||
TWITTER_CONSUMER_SECRET = ''
|
||||
|
||||
|
||||
NOTIFICATIONS_FROM_ADDRESS = 'notifications@support.mozilla.com'
|
||||
|
||||
# URL of the chat server.
|
||||
|
|
Загрузка…
Ссылка в новой задаче