Completing Twitter/FB OAuth end to end. Now need to surface and find friends.

This commit is contained in:
Samuel Clay 2011-12-23 18:28:16 -08:00
parent c4d4eecc81
commit d9efea8f1a
11 changed files with 176 additions and 38 deletions

View file

@ -1,8 +1,7 @@
import datetime
import time
from django.shortcuts import render_to_response, get_object_or_404
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required
from django.template import RequestContext
from django.template.loader import render_to_string
from django.db import IntegrityError
from django.views.decorators.cache import never_cache
@ -40,13 +39,14 @@ from utils.story_functions import format_story_link_date__long
from utils.story_functions import bunch
from utils.story_functions import story_score
from utils import log as logging
from utils.view_functions import get_argument_or_404
from utils.view_functions import get_argument_or_404, render_to
from utils.ratelimit import ratelimit
from vendor.timezones.utilities import localtime_for_timezone
SINGLE_DAY = 60*60*24
@never_cache
@render_to('reader/feeds.xhtml')
def index(request):
if request.method == "POST":
if request.POST['submit'] == 'login':
@ -80,7 +80,7 @@ def index(request):
if start_import_from_google_reader:
del request.session['import_from_google_reader']
return render_to_response('reader/feeds.xhtml', {
return {
'user_profile' : hasattr(user, 'profile') and user.profile,
'login_form' : login_form,
'signup_form' : signup_form,
@ -96,7 +96,7 @@ def index(request):
'user_statistics' : user_statistics,
'feedbacks' : feedbacks,
'start_import_from_google_reader': start_import_from_google_reader,
}, context_instance=RequestContext(request))
}
@never_cache
def login(request):

View file

@ -12,6 +12,7 @@ from apps.rss_feeds.models import MStory
from apps.social.models import MSharedStory, MSocialServices
from utils import json_functions as json
from utils.user_functions import get_user, ajax_login_required
from utils.view_functions import render_to
from utils import log as logging
from utils import PyRSS2Gen as RSS
from vendor import facebook
@ -101,6 +102,7 @@ def friends(request):
return social_services.to_json()
@login_required
@render_to('social/social_connect.xhtml')
def twitter_connect(request):
twitter_consumer_key = settings.TWITTER_CONSUMER_KEY
twitter_consumer_secret = settings.TWITTER_CONSUMER_SECRET
@ -118,15 +120,15 @@ def twitter_connect(request):
api = tweepy.API(auth)
twitter_user = api.me()
except (tweepy.error.TweepError, IOError):
return json.json_response(request, dict(error="Twitter has returned an error. Try connecting again."))
return dict(error="Twitter has returned an error. Try connecting again.")
# Be sure that two people aren't using the same Twitter account.
existing_user = MSocialServices.objects.filter(twitter_uid=unicode(twitter_user.id))
if existing_user and existing_user[0].user_id != request.user.pk:
user = User.objects.get(pk=existing_user[0].user_id)
return json.json_response(request, dict(error=("Another user (%s, %s) has "
return dict(error=("Another user (%s, %s) has "
"already connected with those Twitter credentials."
% (user.username, user.email_address))))
% (user.username, user.email_address)))
social_services, _ = MSocialServices.objects.get_or_create(user_id=request.user.pk)
social_services.twitter_uid = unicode(twitter_user.id)
@ -134,15 +136,16 @@ def twitter_connect(request):
social_services.twitter_access_secret = access_token.secret
social_services.save()
social_services.sync_twitter_friends()
return json.json_response(request, dict(code=1))
return {}
else:
# Start the OAuth process
auth = tweepy.OAuthHandler(twitter_consumer_key, twitter_consumer_secret)
auth_url = auth.get_authorization_url()
return HttpResponseRedirect(auth_url)
return {'next': auth_url}
@login_required
@render_to('social/social_connect.xhtml')
def facebook_connect(request):
facebook_app_id = settings.FACEBOOK_APP_ID
facebook_secret = settings.FACEBOOK_SECRET
@ -164,7 +167,7 @@ def facebook_connect(request):
response = urlparse.parse_qs(response_text)
if "access_token" not in response:
return json.json_response(request, dict(error="Facebook has returned an error. Try connecting again."))
return dict(error="Facebook has returned an error. Try connecting again.")
access_token = response["access_token"][-1]
@ -177,22 +180,22 @@ def facebook_connect(request):
existing_user = MSocialServices.objects.filter(facebook_uid=uid)
if existing_user and existing_user[0].user_id != request.user.pk:
user = User.objects.get(pk=existing_user[0].user_id)
return json.json_response(request, dict(error=("Another user (%s, %s) has "
return dict(error=("Another user (%s, %s) has "
"already connected with those Facebook credentials."
% (user.username, user.email_address))))
% (user.username, user.email_address)))
social_services, _ = MSocialServices.objects.get_or_create(user_id=request.user.pk)
social_services.facebook_uid = uid
social_services.facebook_access_token = access_token
social_services.save()
social_services.sync_facebook_friends()
return json.json_response(request, dict(code=1))
return {}
elif request.REQUEST.get('error'):
pass
else:
# Start the OAuth process
url = "https://www.facebook.com/dialog/oauth?" + urllib.urlencode(args)
return HttpResponseRedirect(url)
return {'next': url}
@ajax_login_required
def twitter_disconnect(request):

View file

@ -279,6 +279,7 @@
float: right;
position: relative;
bottom: -1px;
margin-right: 16px;
}
.NB-modal .NB-modal-tab {
@ -311,7 +312,6 @@
margin-top: 0;
clear: both;
border-top: 1px solid #A0A0A0;
padding: 12px 0 0;
}
.NB-modal .NB-tab.NB-active {
@ -319,7 +319,7 @@
}
.NB-modal .NB-modal-section {
padding: 12px 0;
padding: 24px 0;
border-bottom: 1px solid #A0A0A0;
}

View file

@ -4564,7 +4564,7 @@ background: transparent;
background: transparent url('../img/icons/silk/email.png') no-repeat 0 0;
}
.NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-twitter {
background: transparent url('../img/reader/twitter_icon_2.png') no-repeat 0 0;
background: transparent url('../img/reader/twitter_icon.png') no-repeat 0 0;
}
.NB-menu-manage .NB-menu-manage-story-thirdparty .NB-menu-manage-thirdparty-facebook {
background: transparent url('../img/reader/facebook_icon.png') no-repeat 0 0;
@ -4839,7 +4839,7 @@ background: transparent;
/* ===================== */
.NB-modal-email .NB-modal-loading {
margin: 6px 8px 0;
margin: 6px 8px 0;;
}
.NB-modal-email label {
float: left;
@ -6355,9 +6355,12 @@ background: transparent;
/* = Friends Modal = */
/* ================= */
.NB-modal-friends .NB-modal-loading {
float: none;
margin: 4px 8px 0 0;
.NB-modal-friends .NB-tab {
min-height: 80px;
}
.NB-modal-friends .NB-modal-tabs .NB-modal-loading {
margin: 6px 8px 0 0;
float: left;
}
.NB-modal-friends .NB-friends-services {
overflow: hidden;
@ -6366,7 +6369,7 @@ background: transparent;
float: left;
text-align: center;
font-weight: bold;
margin-right: 12px;
margin-right: 0px;
}
.NB-modal-friends .NB-friends-service-title {
float: left;
@ -6378,10 +6381,13 @@ background: transparent;
width: auto;
margin-top: -2px;
}
.NB-modal-friends .NB-friends-service-connect img {
vertical-align: bottom;
margin: 0 4px 0 0;
}
.NB-modal-friends .NB-friends-autofollow {
width: auto;
margin-left: 20px;
float: left;
float: right;
font-size: 12px;
position: relative;
margin-top: -4px;
@ -6396,4 +6402,14 @@ background: transparent;
margin-left: 20px;
display: block;
text-align: left;
cursor: pointer;
}
.NB-modal-friends .NB-tab .NB-ghost {
margin: 0 auto;
padding: 24px 0;
color: #A0A0A0;
font-size: 14px;
text-align: center;
font-weight: bold;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 B

After

Width:  |  Height:  |  Size: 698 B

View file

@ -3527,7 +3527,7 @@
},
open_friends_modal: function(score) {
NEWSBLUR.friends = new NEWSBLUR.ReaderFriends();
NEWSBLUR.reader_friends = new NEWSBLUR.ReaderFriends();
},
open_recommend_modal: function(feed_id) {

View file

@ -36,26 +36,27 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, {
$.make('div', { className: 'NB-tab NB-tab-findfriends NB-active' }, [
$.make('div', { className: 'NB-modal-section NB-friends-services'})
]),
$.make('div', { className: 'NB-tab NB-tab-following' }, [
'List of followings'
]),
$.make('div', { className: 'NB-tab NB-tab-followers' }, [
'List of followers'
])
$.make('div', { className: 'NB-tab NB-tab-following' }),
$.make('div', { className: 'NB-tab NB-tab-followers' })
]);
},
fetch_friends: function() {
this.model.fetch_friends(_.bind(this.make_friends, this));
$('.NB-modal-loading', this.$modal).addClass('NB-active');
this.model.fetch_friends(_.bind(function(data) {
this.make_friends(data);
this.make_followers(data);
this.make_following(data);
}, this));
},
make_friends: function(data) {
console.log(["data", data]);
$('.NB-modal-loading', this.$modal).removeClass('NB-active');
var $services = $('.NB-friends-services', this.$modal).empty();
_.each(['twitter', 'facebook'], function(service) {
var $service;
console.log(["data", data, service, data.services[service][service+'_uid']]);
if (data.services[service][service+'_uid']) {
$service = $.make('div', { className: 'NB-friends-service NB-connected NB-friends-service-'+service}, [
$.make('div', { className: 'NB-friends-service-title' }, _.capitalize(service)),
@ -64,7 +65,10 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, {
} else {
$service = $.make('div', { className: 'NB-friends-service NB-friends-service-'+service}, [
$.make('div', { className: 'NB-friends-service-title' }, _.capitalize(service)),
$.make('div', { className: 'NB-friends-service-connect NB-modal-submit-button NB-modal-submit-green' }, 'Connect to ' + _.capitalize(service))
$.make('div', { className: 'NB-friends-service-connect NB-modal-submit-button NB-modal-submit-green' }, [
$.make('img', { src: NEWSBLUR.Globals.MEDIA_URL + '/img/reader/' + service + '_icon.png' }),
'Connect to ' + _.capitalize(service)
])
]);
}
$services.append($service);
@ -79,6 +83,21 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, {
])
]);
$services.append($autofollow);
this.resize();
},
make_followers: function(data) {
if (!data.followers || !data.followers.length) {
var $ghost = $.make('div', { className: 'NB-ghost NB-modal-section' }, 'Nobody has yet subscribed to your shared stories.');
$('.NB-tab-followers', this.$modal).empty().append($ghost);
}
},
make_following: function(data) {
if (!data.following || !data.following.length) {
var $ghost = $.make('div', { className: 'NB-ghost NB-modal-section' }, 'You are not yet subscribing to anybody\'s shared stories.');
$('.NB-tab-following', this.$modal).empty().append($ghost);
}
},
open_modal: function(callback) {
@ -130,12 +149,27 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, {
$tabs.filter('.NB-tab-'+newtab).addClass('NB-active');
},
connect: function(service) {
var options = "location=0,status=0,width=800,height=500";
var url = "/social/" + service + "_connect";
this.connect_window = window.open(url, '_blank', options);
},
disconnect: function(service) {
var $service = $('.NB-friends-service-'+service, this.$modal);
$('.NB-friends-service-connect', $service).text('Disconnecting...');
this.model.disconnect_social_service(service, _.bind(this.make_friends, this));
},
post_connect: function(data) {
if (data.error) {
var $error = $.make('div', { className: 'DV-error' }, data.error);
$('.NB-friends-services', this.$modal).append($error);
} else {
this.fetch_friends();
}
},
// ===========
// = Actions =
// ===========

View file

@ -31,7 +31,7 @@
<p style="line-height:20px;">Stay up to date and in touch with me, yr. developer, in a few different ways:</p>
<p style="line-height: 20px;">
<ul style="list-style: none;">
<li style="line-height:22px;"><a href="http://twitter.com/samuelclay/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/twitter_icon_2.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow @samuelclay on Twitter</a>.</li>
<li style="line-height:22px;"><a href="http://twitter.com/samuelclay/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/twitter_icon.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow @samuelclay on Twitter</a>.</li>
<li style="line-height:22px;"><a href="http://twitter.com/newsblur/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/twitter.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow @newsblur on Twitter</a>.</li>
<li style="line-height:22px;"><a href="http://github.com/samuelclay/" style="text-decoration:none"><img src="http://www.newsblur.com/media/img/reader/github_icon.png" style="width:16px;height:16px;vertical-align:top;padding-top:3px;"> Follow the constantly evolving source code on GitHub</a>.</li>
</ul>

View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<title>NewsBlur - Connecting...</title>
<head>
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
font-weight: bold;
color: #A0A0A0;
font-style: italic;
}
@-webkit-keyframes spin{
from {-webkit-transform: rotate(0deg)}
to {-webkit-transform: rotate(360deg)}
}
.NB-loader {
margin: 72px auto;
width: 256px;
height: 256px;
display: block;
-webkit-animation: spin 3.4s infinite linear;
}
</style>
<script type="text/javascript" charset="utf-8">
var next = "{{ next|safe }}";
if (next) {
console.log(["next", next]);
setTimeout(function() {
window.location.href = next;
}, 1000);
} else if (window.opener && window.opener.NEWSBLUR) {
window.opener.NEWSBLUR.reader_friends.post_connect({
{% if error %}
'error': "{{ error }}",
{% endif %}
});
window.close();
} else {
window.location.href = "/";
}
</script>
</head>
<body>
<div style="text-align: center;">
<img class="NB-loader" src="{{ MEDIA_URL }}img/logo_512.png">
<div>Hold on just a smidgen...</div>
<div>If I was a bird I'd be a pigeon.</div>
</div>
</body>
</html>

View file

@ -1,8 +1,34 @@
from django.http import Http404
from django.template import RequestContext
from django.shortcuts import render_to_response
def get_argument_or_404(request, param, method='REQUEST'):
try:
return getattr(request, method)[param]
except KeyError:
raise Http404
raise Http404
def render_to(template):
"""
Decorator for Django views that sends returned dict to render_to_response function
with given template and RequestContext as context instance.
If view doesn't return dict then decorator simply returns output.
Additionally view can return two-tuple, which must contain dict as first
element and string with template name as second. This string will
override template name, given as parameter
Parameters:
- template: template name to use
"""
def renderer(func):
def wrapper(request, *args, **kw):
output = func(request, *args, **kw)
if isinstance(output, (list, tuple)):
return render_to_response(output[1], output[0], RequestContext(request))
elif isinstance(output, dict):
return render_to_response(template, output, RequestContext(request))
return output
return wrapper
return renderer