Customer Care. Twitter OAuth and template fixes.

This commit is contained in:
Alex Buchanan 2010-09-24 14:33:14 -07:00
Родитель cc2c1143c5
Коммит 1ad3f3567f
16 изменённых файлов: 338 добавлений и 199 удалений

Просмотреть файл

@ -8,7 +8,7 @@ from sumo.models import ModelBase
class Tweet(ModelBase):
"""An entry on twitter."""
tweet_id = models.BigIntegerField()
tweet_id = models.BigIntegerField(unique=True)
raw_json = models.TextField()
locale = models.CharField(max_length=20, db_index=True)
created = models.DateTimeField(default=datetime.now, db_index=True)

Просмотреть файл

@ -5,46 +5,41 @@
{% block breadcrumbs %}{% endblock %}
{% block content_area %}
<div class="feature-contents">
<h2>Join our <br />Army of Awesome</h2>
<h3>Love Firefox and have a few moments to help? Help other Firefox users on Twitter. Good things will come to those who tweet!</h3>
</div>
<div class="feature-contents">
<h2>Join our <br />Army of Awesome</h2>
<h3>Love Firefox and have a few moments to help? Help other Firefox users on Twitter. Good things will come to those who tweet!</h3>
</div>
<div id="speach-bubbles">
<ol>
<li class="choose">Choose a tweet below</li>
<li class="signin">Sign in with Twitter</li>
<li class="respond">Respond to the tweet!</li>
</ol>
<br style="clear:both; height: 1px" />
</div>
<div id="speach-bubbles">
<ol>
<li class="choose">Choose a tweet below</li>
<li class="signin">Sign in with Twitter</li>
<li class="respond">Respond to the tweet!</li>
</ol>
</div>
<div id="tweetcontainer">
<div class="tweets-header">
<img src="{{ MEDIA_URL }}/img/customercare/twitter-icon.png" /><h2 class="showhide_heading" id="Where_to_ask_your_question">Choose a tweet to help</h2>
{% if authed %}
<a href="?twitter_delete_auth=1" id="twitter-logout">Log out of Twitter</a>
{% endif %}
</div>
<br style="clear:both; height: 1px" />
<ul id="tweets">
{% for tweet in tweets %}
<li class="tweet">
<li class="tweet" data-reply_to="{{ tweet.reply_to }}">
<a href="http://twitter.com/{{ tweet.user }}" class="avatar"><img src="{{ tweet.profile_img }}" /></a>
<span><span class="twittername">{{ tweet.user }}</span><span class="time">{{ tweet.date|utctimesince }}</span>
<span class="text">{{ tweet.text }}</span>
<span class="twittername">{{ tweet.user }}</span><span class="time">{{ tweet.date|utctimesince }}</span>
<p class="text">{{ tweet.text }}</p>
</li>
{% endfor %}
</ul>
</div>
<div id="reply-modal">
{% include 'customercare/reply_modal.html' %}
</div>
{% include 'customercare/reply_modal.html' %}
<div id="twitter-modal">
<h2>Sign in with your Twitter account</h2>
<p>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.</p>
<a href="{{ url('customercare.twitter_auth') }}">Sign in</a>
<a href="#" class="cancel">Cancel</a>
</div>
{% include 'customercare/twitter_modal.html' %}
{% endblock %}

Просмотреть файл

@ -1,47 +1,51 @@
<div id="reply-container">
<div id="initial-tweet">
<div id="reply-modal">
<div id="reply-container">
<div id="initial-tweet">
<a href="" class="avatar"><img src="" /></a>
<span class="box">
<img src="{{ MEDIA_URL }}img/customercare/initial-tweet-arrow.png" alt="" id="arrow" />
<a href="" class="twittername"></a>
<span class="text"></span>
<img src="{{ MEDIA_URL }}img/customercare/initial-tweet-arrow.png" alt="" id="arrow" />
<a href="" class="twittername"></a>
<span class="text"></span>
</span>
</div>
<div id="replies">
<h4>What is your reply about?</h4>
<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 }} #fxhelp</span>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<div class="hrbreak"></div>
<div id="replies">
<h4>What is your reply about?</h4>
<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 }} #fxhelp</span>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<div id="reply">
<h4>Get personal</h4>
<div class="hrbreak"></div>
<div id="reply">
<h4>Get personal</h4>
<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 class="reply-message" placeholder="Tweak it and make it your own. Personalized messages go a long way in helping others."></textarea>
</div>
<span class="submit-message">Your message was sent!</span>
<input type="submit" value="Submit" name="" id="submit" class="submitButton" title="Submit">
</div>
<span id="submit-message">Your message was sent!</span>
<input type="hidden" name="reply_to" id="reply_to">
<input type="submit" value="Submit" name="submit" id="submit" class="submitButton" title="Submit">
</form>
</div>
</div>
</div>
</div>
</div>

Просмотреть файл

@ -0,0 +1,6 @@
<div id="twitter-modal" data-authed="{{ authed }}">
<h2>Sign in with your Twitter account</h2>
<p>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.</p>
<a href="#" class="cancel">Cancel</a>
<a href="?twitter_auth_request=1" class="signin">Sign in</a>
</div>

Просмотреть файл

@ -1,6 +1,6 @@
from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('customercare.views',
url(r'/twitter_auth', 'twitter_auth', name="customercare.twitter_auth"),
url(r'/twitter_post', 'twitter_post', name="customercare.twitter_post"),
url(r'', 'landing', name='customercare.landing'),
)

Просмотреть файл

@ -2,55 +2,24 @@ from datetime import datetime
from email.Utils import parsedate
import json
import logging
from uuid import uuid4
from django import http
from django.conf import settings
from django.core.cache import cache
from django.views.decorators.csrf import csrf_exempt
import jingo
import tweepy
from .models import CannedCategory, Tweet
import twitter
log = logging.getLogger('k')
log = logging.getLogger('custcare')
token_cache_prefix = 'custcare_token_'
key_prefix = token_cache_prefix + 'key_'
secret_prefix = token_cache_prefix + 'secret_'
# cookie names are duplicated in js/cusomtercare.js
access_cookie_name = 'custcare_twitter_access_id'
redirect_cookie_name = 'custcare_twitter_redirect_flag'
def auth_factory(request):
return tweepy.OAuthHandler(settings.TWITTER_CONSUMER_KEY,
settings.TWITTER_CONSUMER_SECRET,
'https://{0}/{1}/customercare/'.format(
request.get_host(), request.locale))
def set_access_cookie(resp, id):
resp.set_cookie(redirect_cookie_name, '1', httponly=True)
resp.set_cookie(access_cookie_name, id, secure=True)
def set_tokens(id, key, secret):
cache.set(key_prefix + id, key)
cache.set(secret_prefix + id, secret)
def get_tokens(id):
key = cache.get(key_prefix + id)
secret = cache.get(secret_prefix + id)
return key, secret
@twitter.auth_wanted
def landing(request):
"""Customer Care Landing page."""
twitter = request.twitter
canned_responses = CannedCategory.objects.all()
tweets = []
for tweet in Tweet.objects.filter(locale='en')[:10]:
@ -61,64 +30,26 @@ def landing(request):
'profile_img': data['profile_image_url'],
'user': data['from_user'],
'text': tweet,
'reply_to': tweet.tweet_id,
'date': date,
})
resp = jingo.render(request, 'customercare/landing.html', {
'canned_responses': canned_responses,
'tweets': tweets,
'now': datetime.utcnow(),
'authed': twitter.authed,
})
# TODO HTTP redirect flag checking?
if request.COOKIES.get(redirect_cookie_name):
return http.HttpResponseRedirect('https://{0}/{1}'.format(
request.get_host(), request.get_full_path()))
# if GET[oauth_verifier] exists, we're handling an OAuth login
verifier = request.GET.get('oauth_verifier')
if verifier:
auth = auth_factory(request)
request_key = request.COOKIES.get('request_token_key')
request_secret = request.COOKIES.get('request_token_secret')
if request_key and request_secret:
resp.delete_cookie('request_token_key')
resp.delete_cookie('request_token_secret')
auth.set_request_token(request_key, request_secret)
try:
auth.get_access_token(verifier)
except tweepy.TweepError:
log.warning('Tweepy Error with verifier token')
pass
else:
access_id = uuid4().hex
set_access_cookie(resp, access_id)
set_tokens(access_id, auth.access_token.key, auth.access_token.secret)
return resp
@csrf_exempt
@twitter.auth_required
def twitter_post(request):
# access_id = request.COOKIES.get(access_cookie_name)
# if access_id:
# key, secret = get_tokens(access_id)
# authed = True
# resp.write('key: %s sec: %s' % (key, secret))
# set_access_cookie(resp, access_id)
pass
def twitter_auth(request):
auth = auth_factory(request)
try:
redirect_url = auth.get_authorization_url()
except tweepy.TweepError:
log.warning('Tweepy error while getting authorization url')
return http.HttpReponseServerError()
resp = http.HttpResponseRedirect(redirect_url)
resp.set_cookie('request_token_key', auth.request_token.key, max_age=3600, secure=True)
resp.set_cookie('request_token_secret', auth.request_token.secret, max_age=3600, secure=True)
return resp
# FIXME ensure post length is under twitter limit
# do this in JS too
tweet = request.POST.get('tweet')
reply_to = request.POST.get('reply_to')
# TODO remove debug line
request.twitter.api.update_status(tweet, '25684040574')
return http.HttpResponse()

89
apps/twitter/__init__.py Normal file
Просмотреть файл

@ -0,0 +1,89 @@
import logging
from uuid import uuid4
from django import http
from django.core.cache import cache
import tweepy
log = logging.getLogger('k')
PREFIX = 'custcare_'
ACCESS_NAME = PREFIX + 'access'
REDIRECT_NAME = PREFIX + 'redirect'
REQUEST_KEY_NAME = PREFIX + 'request_key'
REQUEST_SECRET_NAME = PREFIX + 'request_secret'
MAX_AGE = 3600
def ssl_url(request):
return 'https://{0}{1}'.format(request.get_host(), request.get_full_path())
# Twitter sessions are SSL only, so redirect to SSL if needed
def auth_wanted(view_func):
def wrapper(request, *args, **kwargs):
if request.COOKIES.get(REDIRECT_NAME) and not request.is_secure():
return http.HttpResponseRedirect(ssl_url(request))
return view_func(request, *args, **kwargs)
return wrapper
# returns a HttpResponseBadRequest in not authed
def auth_required(view_func):
def wrapper(request, *args, **kwargs):
if not request.twitter.authed:
return http.HttpResponseBadRequest()
return view_func(request, *args, **kwargs)
return wrapper
class Session(object):
id = None
key = None
secret = None
@property
def cachekey_key(self):
return '{0}_key_{1}'.format(ACCESS_NAME, self.id)
@property
def cachekey_secret(self):
return '{0}_secret_{1}'.format(ACCESS_NAME, self.id)
@property
def authed(self):
return bool(self.id and self.key and self.secret)
@classmethod
def factory(cls, key=None, secret=None):
s = cls()
s.id = uuid4().hex
s.key = key
s.secret = secret
return s
@classmethod
def from_request(cls, request):
s = cls()
s.id = request.COOKIES.get(ACCESS_NAME)
s.key = cache.get(s.cachekey_key)
s.secret = cache.get(s.cachekey_secret)
return s
def delete(self, response):
response.delete_cookie(REDIRECT_NAME)
response.delete_cookie(ACCESS_NAME)
cache.delete(self.cachekey_key)
cache.delete(self.cachekey_secret)
self.id = None
self.key = None
self.secret = None
def save(self, response):
cache.set(self.cachekey_key, self.key, MAX_AGE)
cache.set(self.cachekey_secret, self.secret, MAX_AGE)
response.set_cookie(REDIRECT_NAME, '1', max_age=MAX_AGE)
response.set_cookie(ACCESS_NAME, self.id, max_age=MAX_AGE, secure=True)

Просмотреть файл

@ -0,0 +1,79 @@
import logging
from django import http
from django.conf import settings
from . import *
import tweepy
log = logging.getLogger('k')
class SessionMiddleware(object):
def process_request(self, request):
if getattr(request, 'twitter', False):
return
request.twitter = Session.from_request(request)
auth = tweepy.OAuthHandler(settings.TWITTER_CONSUMER_KEY,
settings.TWITTER_CONSUMER_SECRET,
ssl_url(request))
if request.GET.get('twitter_delete_auth'):
request.twitter = Session.factory()
elif request.twitter.authed:
auth.set_access_token(request.twitter.key, request.twitter.secret)
request.twitter.api = tweepy.API(auth)
else:
verifier = request.GET.get('oauth_verifier')
if verifier:
# We are completing an OAuth login
request_key = request.COOKIES.get(REQUEST_KEY_NAME)
request_secret = request.COOKIES.get(REQUEST_SECRET_NAME)
if request_key and request_secret:
auth.set_request_token(request_key, request_secret)
try:
auth.get_access_token(verifier)
except tweepy.TweepError:
log.warning('Tweepy Error with verifier token')
pass
else:
request.twitter = Session.factory(
auth.access_token.key, auth.access_token.secret)
elif request.GET.get('twitter_auth_request'):
# We are requesting Twitter auth
try:
redirect_url = auth.get_authorization_url()
except tweepy.TweepError:
log.warning('Tweepy error while getting authorization url')
else:
response = http.HttpResponseRedirect(redirect_url)
response.set_cookie(REQUEST_KEY_NAME, auth.request_token.key,
max_age=MAX_AGE, secure=True)
response.set_cookie(REQUEST_SECRET_NAME, auth.request_token.secret,
max_age=MAX_AGE, secure=True)
return response
def process_response(self, request, response):
if getattr(request, 'twitter', False):
if request.GET.get('twitter_delete_auth'):
request.twitter.delete(response)
if request.twitter.authed:
response.delete_cookie(REQUEST_KEY_NAME)
response.delete_cookie(REQUEST_SECRET_NAME)
request.twitter.save(response)
return response

0
apps/twitter/models.py Normal file
Просмотреть файл

Просмотреть файл

@ -26,6 +26,7 @@ body {
}
#speach-bubbles {
padding: 60px 0 15px 25px;
float: left;
}
#speach-bubbles li {
list-style-type: none;
@ -46,7 +47,8 @@ body {
#tweetcontainer {
border: 1px solid #f5f5f5;
padding: 2px;
clear: both;
float: left;
clear: left;
}
#tweetcontainer h2 {
margin: 0px;
@ -93,24 +95,21 @@ body {
width: 48px;
height: 48px;
}
#tweets li span {
display: block;
}
#tweets li span {
#tweets li .twittername {
color: #307fc1;
text-decoration: none;
font-weight: bold;
font-size: 13px;
line-height: 1.4em;
}
#tweets li span .time {
#tweets li .time {
float: right;
font-weight: normal;
font-style: italic;
font-size: 12px;
color: #afaba3;
}
#tweets li span .text {
#tweets li .text {
font-weight: normal;
font-size: 14px;
color: #69645b;
@ -281,7 +280,7 @@ body {
display: inline;
}
#reply .submit-message {
#submit-message {
display: none;
background: url('../img/customercare/reply-check.png') right top no-repeat;
font-weight: bold;

Просмотреть файл

@ -67,4 +67,18 @@
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-getinvolved h3 {
color: #1E4262;
font-size: 150%;
}
#side-getinvolved p {
padding-top: 10px;
font-size: 115%;
}

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 4.8 KiB

После

Ширина:  |  Высота:  |  Размер: 4.8 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 4.2 KiB

После

Ширина:  |  Высота:  |  Размер: 4.2 KiB

Просмотреть файл

@ -1,56 +1,68 @@
var has_twitter_access = false;
// cookie names are duplicated in apps/customercare/views.py
if ($.cookie('custcare_twitter_access_id'))
has_twitter_access = true;
$(document).ready(function() {
$('.reply-message').NobleCount('.character-counter');
$('.reply-message').NobleCount('.character-counter');
$('.reply-message').autoPlaceholderText();
$('.reply-message').autoPlaceholderText();
$('#accordion').accordion({
'icons': false,
'autoHeight': false,
});
$('#accordion').accordion({
'icons': false,
'autoHeight': false,
});
$('.tweet').click(function() {
var twitter_modal = $('#twitter-modal');
if (twitter_modal.attr('data-authed') == 'False') {
twitter_modal.dialog({
'modal': 'true',
'position': 'top',
});
twitter_modal.find('.cancel').click(function(e) {
twitter_modal.dialog('close');
e.preventDefault();
return false;
});
return;
}
var reply_to = $(this).attr('data-reply_to')
var avatar_href = $(this).find('.avatar').attr('href');
var avatar_img = $(this).find('.avatar img').attr('src');
var twittername = $(this).find('.twittername').text();
var text = $(this).find('.text').text();
var modal = $('#reply-modal');
modal.find('#reply_to').val(reply_to);
modal.find('.avatar').attr('href', avatar_href);
modal.find('.avatar img').attr('src', avatar_img);
modal.find('.twittername').text(twittername);
modal.find('.text').text(text);
modal.dialog({
'modal': true,
'position': 'top',
'width': 500,
});
});
$('.reply-topic').click(function(e) {
snippet = $(this).next('.snippet').text();
$('.reply-message').val(snippet);
$('.reply-message').trigger('keydown');
$('.tweet').click(function() {
if (!has_twitter_access) {
$('#twitter-modal').dialog({
'modal': 'true',
'position': 'top',
});
$('#twitter-modal .cancel').click(function(e) {
$('#twitter-modal').dialog('close');
e.preventDefault();
return false;
});
return;
}
var avatar_href = $(this).find('.avatar').attr('href');
var avatar_img = $(this).find('.avatar img').attr('src');
var twittername = $(this).find('.twittername').text();
var text = $(this).find('.text').text();
var modal = $('#reply-modal');
modal.find('.avatar').attr('href', avatar_href);
modal.find('.avatar img').attr('src', avatar_img);
modal.find('.twittername').text(twittername);
modal.find('.text').text(text);
modal.dialog({
'modal': true,
'position': 'top',
'width': 500,
});
});
$('.reply-topic').click(function(e) {
snippet = $(this).next('.snippet').text();
$('.reply-message').val(snippet);
$('.reply-message').trigger('keydown');
e.preventDefault();
return false;
});
$('#reply-modal #submit').click(function(e) {
var action = $('#reply-modal form').attr('action');
var tweet = $('.reply-message').val();
var reply_to = $('#reply_to').val();
$.post(
action,
{ 'tweet': tweet, 'reply_to': reply_to },
function() {
$('#submit-message').show();
}
);
e.preventDefault();
return false;
});
});

Просмотреть файл

@ -142,6 +142,8 @@ MIDDLEWARE_CLASSES = (
# TODO: Replace with Kitsune auth.
'sumo.middleware.TikiCookieMiddleware',
'twitter.middleware.SessionMiddleware',
)
# Auth
@ -189,6 +191,7 @@ INSTALLED_APPS = (
'wiki',
'gallery',
'customercare',
'twitter',
)
# Extra apps for testing
@ -330,7 +333,6 @@ MINIFY_BUNDLES = {
'customercare': (
'js/libs/jqueryui.min.js',
'js/libs/jquery.NobleCount.js',
'js/libs/jquery.cookie.js',
'js/customercare.js',
),
},

Просмотреть файл

@ -14,4 +14,12 @@
<li><a href="{{ settings.LOGIN_URL }}">{{ _('Log In') }}</a></li>
{% endif %}
</ul>
<div id="side-getinvolved">
<h3>Want to get involved?</h3>
<p>Did you know that most of the content on Firefox Support was written by volunteers?</p>
<p><a href="http://support.mozilla.com/en-US/kb/Providing+Forum+Support">Find out how to contribute</a>,<br>or <a href="">log in</a>.</p>
<p><a href="/"><img src="{{ MEDIA_URL }}img/sumo-logo.png" class="sumo-logo"></a></p>
</div>
{% endblock %}