From d9efea8f1a3090626bbea7d2ef1b73c834e29b62 Mon Sep 17 00:00:00 2001 From: Samuel Clay Date: Fri, 23 Dec 2011 18:28:16 -0800 Subject: [PATCH] Completing Twitter/FB OAuth end to end. Now need to surface and find friends. --- apps/reader/views.py | 10 ++--- apps/social/views.py | 23 +++++----- media/css/modals.css | 4 +- media/css/reader.css | 32 ++++++++++---- media/img/reader/twitter_icon.png | Bin 698 -> 474 bytes media/img/reader/twitter_icon_2.png | Bin 474 -> 698 bytes media/js/newsblur/reader.js | 2 +- media/js/newsblur/reader_friends.js | 52 +++++++++++++++++++---- templates/mail/email_base.xhtml | 2 +- templates/social/social_connect.xhtml | 59 ++++++++++++++++++++++++++ utils/view_functions.py | 30 ++++++++++++- 11 files changed, 176 insertions(+), 38 deletions(-) create mode 100644 templates/social/social_connect.xhtml diff --git a/apps/reader/views.py b/apps/reader/views.py index 29e708107..b88627280 100644 --- a/apps/reader/views.py +++ b/apps/reader/views.py @@ -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): diff --git a/apps/social/views.py b/apps/social/views.py index a31897d23..f84fd033c 100644 --- a/apps/social/views.py +++ b/apps/social/views.py @@ -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): diff --git a/media/css/modals.css b/media/css/modals.css index 5918c535a..4c6d9fd65 100644 --- a/media/css/modals.css +++ b/media/css/modals.css @@ -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; } diff --git a/media/css/reader.css b/media/css/reader.css index 083251739..8d95131d7 100644 --- a/media/css/reader.css +++ b/media/css/reader.css @@ -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; } \ No newline at end of file diff --git a/media/img/reader/twitter_icon.png b/media/img/reader/twitter_icon.png index e5f44408debbda9a0e6d3b124f1a4363add65268..dd4e4d518d22d12e92d36d805f1fb95187590a5a 100644 GIT binary patch delta 448 zcmV;x0YCn_1=<6UB!2{RLP=Bz2nYy#2xN!=000SaNLh0L01EH`01EH{Laa2H0000L zbVXQnLvm$dbZKvHAXI5>WdJiTF*PqSF-#@kf&c&j9CSrkbW?9;ba!ELWdK2BZ(?O2 zMrm?ocW-iQb09-gGnm#!0{{R4Gf6~2R5*>DlD$epK@f$%&3~>)f_+L+5FtVeL9ny) z5n^TGbBN$iW8T5i%Gw98uuu>|B!VG=L4=5+5OXCr+1;_oHQ>E#Fj<&pIP>i}JF5V8 z?p}lm&OxcD)OM{k(J(-*@r3b{P6JIZbS#d7` zRIeT=6!HMPi^8D-4g$CJ6KGKjx6Nk&9@`NMrNv)55y991px*8>H#1G8JWr)O=tVtC zHMJG*g`D%AbR9Q8Xmnz%$gm`*GP*w_MBZ`K82tx+961;t0Q~oePaHVs&{~f*-1&^? qcH_ZbW)DxqSvMBIT1#tpQ+)%{EWjhpgA2R>0000p delta 674 zcmV;T0$u&u1G)u}B!3xnMObuGZ)S9NVRB^vL1b@YWgtmyVP|DhWnpA_ami&o00079 zNklUIYC&8O)3H<%8I}yg`;L&Mz_%@XxN1`vO$9= z$R-g6K@*cqL9HWswDYs~_pa6RL{+)*=y5$37KT~q*<>GRGJj{$CvB9`Qcp6Pv(bIB zyAyHa!Coy_-OI}*nu^qf%tRBds~jd_M1W@a>-fm=CQSD-UCM>ukB-PvQA88flvyf0 zx~ilFqY>#$Oz!2@6lQy)Q$uvV`{tG(e!T742N!*G{i?K(wUt3+BW5+ay_RgYS{;W; zSp;o#Ki%51-hVys=Bmc%TBu8SI4HPCA|=4RSplOdx-lxe*(l;wKIb_9wR#uze^ zYC5(Rp5-td%;^QG6%|aY)22wRnlHcj+NdTXL=Nx1bIFGvUk0tHU>aip04G*@n^i8H zI{${0TFq#F{bS#?Af*zvW0*#fLD!8;i}hN|q&1mk5r1jK2~p+xdlyU0nL= z^DH7%q}6gw3>kd>=>;@2h5P$Y?B_-lG>4;1SwVCqsiv*Eij#WGzQ6O(x!vbQ6jgFs zA_=O((L7AsQ6LZz)@RqoreopTn|IBd2U;M)>lYT@zWOT2=t?sk<#0Hs-ik_B&Y$t~ z!&%SC*?(7FQ;{-f6)Sh2KJsSHLN^<>Ri?vj`fsywdbJX*y!zsS#i_O59xNQs1$tXD zx~rF8q7GzmaB$@Cc$jvUliPo9UED1@s}uHHxOno|`&X7K2&s*bt=xb1)Duhp9v%5} zZ}!}35>@51U+&c2(=Eu>)ksqVh5`{GGLYchnHz<#-ntb31Bn_=ro2~2<^TWy07*qo IM6N<$f~Jc|ZvX%Q diff --git a/media/img/reader/twitter_icon_2.png b/media/img/reader/twitter_icon_2.png index dd4e4d518d22d12e92d36d805f1fb95187590a5a..e5f44408debbda9a0e6d3b124f1a4363add65268 100644 GIT binary patch delta 674 zcmV;T0$u&u1G)u}B!3xnMObuGZ)S9NVRB^vL1b@YWgtmyVP|DhWnpA_ami&o00079 zNklUIYC&8O)3H<%8I}yg`;L&Mz_%@XxN1`vO$9= z$R-g6K@*cqL9HWswDYs~_pa6RL{+)*=y5$37KT~q*<>GRGJj{$CvB9`Qcp6Pv(bIB zyAyHa!Coy_-OI}*nu^qf%tRBds~jd_M1W@a>-fm=CQSD-UCM>ukB-PvQA88flvyf0 zx~ilFqY>#$Oz!2@6lQy)Q$uvV`{tG(e!T742N!*G{i?K(wUt3+BW5+ay_RgYS{;W; zSp;o#Ki%51-hVys=Bmc%TBu8SI4HPCA|=4RSplOdx-lxe*(l;wKIb_9wR#uze^ zYC5(Rp5-td%;^QG6%|aY)22wRnlHcj+NdTXL=Nx1bIFGvUk0tHU>aip04G*@n^i8H zI{${0TFq#F{bS#?Af*zvW0*#fLD!8;i}hN|q&1mk5r1jK2~p+xdlyU0nL= z^DH7%q}6gw3>kd>=>;@2h5P$Y?B_-lG>4;1SwVCqsiv*Eij#WGzQ6O(x!vbQ6jgFs zA_=O((L7AsQ6LZz)@RqoreopTn|IBd2U;M)>lYT@zWOT2=t?sk<#0Hs-ik_B&Y$t~ z!&%SC*?(7FQ;{-f6)Sh2KJsSHLN^<>Ri?vj`fsywdbJX*y!zsS#i_O59xNQs1$tXD zx~rF8q7GzmaB$@Cc$jvUliPo9UED1@s}uHHxOno|`&X7K2&s*bt=xb1)Duhp9v%5} zZ}!}35>@51U+&c2(=Eu>)ksqVh5`{GGLYchnHz<#-ntb31Bn_=ro2~2<^TWy07*qo IM6N<$f~Jc|ZvX%Q delta 448 zcmV;x0YCn_1=<6UB!2{RLP=Bz2nYy#2xN!=000SaNLh0L01EH`01EH{Laa2H0000L zbVXQnLvm$dbZKvHAXI5>WdJiTF*PqSF-#@kf&c&j9CSrkbW?9;ba!ELWdK2BZ(?O2 zMrm?ocW-iQb09-gGnm#!0{{R4Gf6~2R5*>DlD$epK@f$%&3~>)f_+L+5FtVeL9ny) z5n^TGbBN$iW8T5i%Gw98uuu>|B!VG=L4=5+5OXCr+1;_oHQ>E#Fj<&pIP>i}JF5V8 z?p}lm&OxcD)OM{k(J(-*@r3b{P6JIZbS#d7` zRIeT=6!HMPi^8D-4g$CJ6KGKjx6Nk&9@`NMrNv)55y991px*8>H#1G8JWr)O=tVtC zHMJG*g`D%AbR9Q8Xmnz%$gm`*GP*w_MBZ`K82tx+961;t0Q~oePaHVs&{~f*-1&^? qcH_ZbW)DxqSvMBIT1#tpQ+)%{EWjhpgA2R>0000p diff --git a/media/js/newsblur/reader.js b/media/js/newsblur/reader.js index c33acb50d..6ae107ca7 100644 --- a/media/js/newsblur/reader.js +++ b/media/js/newsblur/reader.js @@ -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) { diff --git a/media/js/newsblur/reader_friends.js b/media/js/newsblur/reader_friends.js index 68f613a05..c0d06f959 100644 --- a/media/js/newsblur/reader_friends.js +++ b/media/js/newsblur/reader_friends.js @@ -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 = // =========== diff --git a/templates/mail/email_base.xhtml b/templates/mail/email_base.xhtml index 7ba89693d..98e217fb9 100644 --- a/templates/mail/email_base.xhtml +++ b/templates/mail/email_base.xhtml @@ -31,7 +31,7 @@

Stay up to date and in touch with me, yr. developer, in a few different ways:

diff --git a/templates/social/social_connect.xhtml b/templates/social/social_connect.xhtml new file mode 100644 index 000000000..ff1bcd3ea --- /dev/null +++ b/templates/social/social_connect.xhtml @@ -0,0 +1,59 @@ + + + +NewsBlur - Connecting... + + + + + + + + +
+ + + +
Hold on just a smidgen...
+
If I was a bird I'd be a pigeon.
+ +
+ + + diff --git a/utils/view_functions.py b/utils/view_functions.py index edd4029a1..01cb1aaf6 100644 --- a/utils/view_functions.py +++ b/utils/view_functions.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file