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 datetime
import time 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.contrib.auth.decorators import login_required
from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.db import IntegrityError from django.db import IntegrityError
from django.views.decorators.cache import never_cache 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 bunch
from utils.story_functions import story_score from utils.story_functions import story_score
from utils import log as logging 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 utils.ratelimit import ratelimit
from vendor.timezones.utilities import localtime_for_timezone from vendor.timezones.utilities import localtime_for_timezone
SINGLE_DAY = 60*60*24 SINGLE_DAY = 60*60*24
@never_cache @never_cache
@render_to('reader/feeds.xhtml')
def index(request): def index(request):
if request.method == "POST": if request.method == "POST":
if request.POST['submit'] == 'login': if request.POST['submit'] == 'login':
@ -80,7 +80,7 @@ def index(request):
if start_import_from_google_reader: if start_import_from_google_reader:
del request.session['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, 'user_profile' : hasattr(user, 'profile') and user.profile,
'login_form' : login_form, 'login_form' : login_form,
'signup_form' : signup_form, 'signup_form' : signup_form,
@ -96,7 +96,7 @@ def index(request):
'user_statistics' : user_statistics, 'user_statistics' : user_statistics,
'feedbacks' : feedbacks, 'feedbacks' : feedbacks,
'start_import_from_google_reader': start_import_from_google_reader, 'start_import_from_google_reader': start_import_from_google_reader,
}, context_instance=RequestContext(request)) }
@never_cache @never_cache
def login(request): def login(request):

View file

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

View file

@ -279,6 +279,7 @@
float: right; float: right;
position: relative; position: relative;
bottom: -1px; bottom: -1px;
margin-right: 16px;
} }
.NB-modal .NB-modal-tab { .NB-modal .NB-modal-tab {
@ -311,7 +312,6 @@
margin-top: 0; margin-top: 0;
clear: both; clear: both;
border-top: 1px solid #A0A0A0; border-top: 1px solid #A0A0A0;
padding: 12px 0 0;
} }
.NB-modal .NB-tab.NB-active { .NB-modal .NB-tab.NB-active {
@ -319,7 +319,7 @@
} }
.NB-modal .NB-modal-section { .NB-modal .NB-modal-section {
padding: 12px 0; padding: 24px 0;
border-bottom: 1px solid #A0A0A0; 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; 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 { .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 { .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; background: transparent url('../img/reader/facebook_icon.png') no-repeat 0 0;
@ -4839,7 +4839,7 @@ background: transparent;
/* ===================== */ /* ===================== */
.NB-modal-email .NB-modal-loading { .NB-modal-email .NB-modal-loading {
margin: 6px 8px 0; margin: 6px 8px 0;;
} }
.NB-modal-email label { .NB-modal-email label {
float: left; float: left;
@ -6355,9 +6355,12 @@ background: transparent;
/* = Friends Modal = */ /* = Friends Modal = */
/* ================= */ /* ================= */
.NB-modal-friends .NB-modal-loading { .NB-modal-friends .NB-tab {
float: none; min-height: 80px;
margin: 4px 8px 0 0; }
.NB-modal-friends .NB-modal-tabs .NB-modal-loading {
margin: 6px 8px 0 0;
float: left;
} }
.NB-modal-friends .NB-friends-services { .NB-modal-friends .NB-friends-services {
overflow: hidden; overflow: hidden;
@ -6366,7 +6369,7 @@ background: transparent;
float: left; float: left;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
margin-right: 12px; margin-right: 0px;
} }
.NB-modal-friends .NB-friends-service-title { .NB-modal-friends .NB-friends-service-title {
float: left; float: left;
@ -6378,10 +6381,13 @@ background: transparent;
width: auto; width: auto;
margin-top: -2px; 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 { .NB-modal-friends .NB-friends-autofollow {
width: auto; width: auto;
margin-left: 20px; float: right;
float: left;
font-size: 12px; font-size: 12px;
position: relative; position: relative;
margin-top: -4px; margin-top: -4px;
@ -6396,4 +6402,14 @@ background: transparent;
margin-left: 20px; margin-left: 20px;
display: block; display: block;
text-align: left; 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) { open_friends_modal: function(score) {
NEWSBLUR.friends = new NEWSBLUR.ReaderFriends(); NEWSBLUR.reader_friends = new NEWSBLUR.ReaderFriends();
}, },
open_recommend_modal: function(feed_id) { 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-tab NB-tab-findfriends NB-active' }, [
$.make('div', { className: 'NB-modal-section NB-friends-services'}) $.make('div', { className: 'NB-modal-section NB-friends-services'})
]), ]),
$.make('div', { className: 'NB-tab NB-tab-following' }, [ $.make('div', { className: 'NB-tab NB-tab-following' }),
'List of followings' $.make('div', { className: 'NB-tab NB-tab-followers' })
]),
$.make('div', { className: 'NB-tab NB-tab-followers' }, [
'List of followers'
])
]); ]);
}, },
fetch_friends: function() { 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) { make_friends: function(data) {
console.log(["data", data]); console.log(["data", data]);
$('.NB-modal-loading', this.$modal).removeClass('NB-active');
var $services = $('.NB-friends-services', this.$modal).empty(); var $services = $('.NB-friends-services', this.$modal).empty();
_.each(['twitter', 'facebook'], function(service) { _.each(['twitter', 'facebook'], function(service) {
var $service; var $service;
console.log(["data", data, service, data.services[service][service+'_uid']]);
if (data.services[service][service+'_uid']) { if (data.services[service][service+'_uid']) {
$service = $.make('div', { className: 'NB-friends-service NB-connected NB-friends-service-'+service}, [ $service = $.make('div', { className: 'NB-friends-service NB-connected NB-friends-service-'+service}, [
$.make('div', { className: 'NB-friends-service-title' }, _.capitalize(service)), $.make('div', { className: 'NB-friends-service-title' }, _.capitalize(service)),
@ -64,7 +65,10 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, {
} else { } else {
$service = $.make('div', { className: 'NB-friends-service NB-friends-service-'+service}, [ $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-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); $services.append($service);
@ -79,6 +83,21 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, {
]) ])
]); ]);
$services.append($autofollow); $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) { open_modal: function(callback) {
@ -130,12 +149,27 @@ _.extend(NEWSBLUR.ReaderFriends.prototype, {
$tabs.filter('.NB-tab-'+newtab).addClass('NB-active'); $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) { disconnect: function(service) {
var $service = $('.NB-friends-service-'+service, this.$modal); var $service = $('.NB-friends-service-'+service, this.$modal);
$('.NB-friends-service-connect', $service).text('Disconnecting...'); $('.NB-friends-service-connect', $service).text('Disconnecting...');
this.model.disconnect_social_service(service, _.bind(this.make_friends, this)); 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 = // = 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;">Stay up to date and in touch with me, yr. developer, in a few different ways:</p>
<p style="line-height: 20px;"> <p style="line-height: 20px;">
<ul style="list-style: none;"> <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://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> <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> </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.http import Http404
from django.template import RequestContext
from django.shortcuts import render_to_response
def get_argument_or_404(request, param, method='REQUEST'): def get_argument_or_404(request, param, method='REQUEST'):
try: try:
return getattr(request, method)[param] return getattr(request, method)[param]
except KeyError: 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