зеркало из 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 re
|
||||||
import rfc822
|
import rfc822
|
||||||
import urllib
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
|
|
||||||
import cronjobs
|
import cronjobs
|
||||||
|
import tweepy
|
||||||
|
|
||||||
from .models import Tweet
|
from customercare.models import Tweet
|
||||||
|
|
||||||
|
|
||||||
SEARCH_URL = 'http://search.twitter.com/search.json'
|
SEARCH_URL = 'http://search.twitter.com/search.json'
|
||||||
|
@ -118,3 +121,56 @@ def _filter_tweet(item):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return item
|
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>
|
<li class="respond">Respond to the tweet!</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tweetcontainer">
|
<div id="tweetcontainer">
|
||||||
<div class="tweets-header">
|
<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>
|
<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>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br style="clear:both; height: 1px" />
|
<br style="clear:both; height: 1px" />
|
||||||
|
|
||||||
<ul id="tweets">
|
<ul id="tweets">
|
||||||
|
@ -49,6 +49,61 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_sidebar %}
|
{% 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">
|
<div id="side-getinvolved">
|
||||||
<h3>Take it to the next level!</h3>
|
<h3>Take it to the next level!</h3>
|
||||||
<p>Want to go beyond 140 characters? Join the support community and help many more
|
<p>Want to go beyond 140 characters? Join the support community and help many more
|
||||||
|
|
|
@ -5,9 +5,11 @@ import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
from django.views.decorators.http import require_POST, require_GET
|
from django.views.decorators.http import require_POST, require_GET
|
||||||
|
|
||||||
|
from babel.numbers import format_number
|
||||||
from bleach import Bleach
|
from bleach import Bleach
|
||||||
import jingo
|
import jingo
|
||||||
import tweepy
|
import tweepy
|
||||||
|
@ -79,7 +81,40 @@ def landing(request):
|
||||||
|
|
||||||
canned_responses = CannedCategory.objects.all()
|
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', {
|
return jingo.render(request, 'customercare/landing.html', {
|
||||||
|
'activity_stats': activity_stats,
|
||||||
|
'contributor_stats': contributor_stats,
|
||||||
'canned_responses': canned_responses,
|
'canned_responses': canned_responses,
|
||||||
'tweets': _get_tweets(),
|
'tweets': _get_tweets(),
|
||||||
'authed': twitter.authed,
|
'authed': twitter.authed,
|
||||||
|
|
|
@ -67,13 +67,89 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#side-getinvolved {
|
#side-stats {
|
||||||
background: transparent url('../img/side-getinvolved-bg.png') no-repeat top left;
|
margin-top: 40px;
|
||||||
margin-top: 140px;
|
}
|
||||||
padding-top: 20px;
|
#side-stats .unavailable {
|
||||||
color: #999186;
|
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 {
|
#side-getinvolved h3 {
|
||||||
color: #1E4262;
|
color: #1E4262;
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.1 KiB |
|
@ -340,5 +340,25 @@
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}).bullseye();
|
}).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));
|
}(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
|
THUMBNAIL_PROGRESS_HEIGHT = 32 # height of the above image
|
||||||
VIDEO_MAX_FILESIZE = 16777216 # 16 megabytes, in bytes
|
VIDEO_MAX_FILESIZE = 16777216 # 16 megabytes, in bytes
|
||||||
|
|
||||||
# Customer Care tweet collection settings
|
# Customer Care settings
|
||||||
CC_MAX_TWEETS = 500 # Max. no. of tweets in DB
|
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_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_TWEET_ACTIVITY_URL = 'https://metrics.mozilla.com/stats/twitter/armyOfAwesomeKillRate.json' # Tweet activity stats
|
||||||
CC_SHOW_REPLIES = True
|
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_KEY = ''
|
||||||
TWITTER_CONSUMER_SECRET = ''
|
TWITTER_CONSUMER_SECRET = ''
|
||||||
|
|
||||||
|
|
||||||
NOTIFICATIONS_FROM_ADDRESS = 'notifications@support.mozilla.com'
|
NOTIFICATIONS_FROM_ADDRESS = 'notifications@support.mozilla.com'
|
||||||
|
|
||||||
# URL of the chat server.
|
# URL of the chat server.
|
||||||
|
|
Загрузка…
Ссылка в новой задаче