From d53aef33b68372cd423aa29d1f29cf44e21b531a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Mat=C3=ADas=20Aguirre?= Date: Mon, 6 Feb 2012 15:16:28 -0200 Subject: [PATCH] Partial pipeline. Refs #90 --- README.rst | 35 +++++++++++++++++++++++ doc/pipeline.rst | 34 ++++++++++++++++++++++ example/app/pipeline.py | 14 +++++++++ example/app/views.py | 18 ++++++++++-- example/local_settings.py.template | 12 ++++++++ example/templates/form.html | 15 ++++++++++ example/urls.py | 3 +- social_auth/backends/__init__.py | 24 +++++++++++++--- social_auth/backends/google.py | 1 + social_auth/backends/pipeline/__init__.py | 1 + social_auth/backends/pipeline/misc.py | 30 +++++++++++++++++++ social_auth/urls.py | 21 ++++++++++---- social_auth/views.py | 18 +++++++++++- 13 files changed, 211 insertions(+), 15 deletions(-) create mode 100644 example/app/pipeline.py create mode 100644 example/templates/form.html create mode 100644 social_auth/backends/pipeline/misc.py diff --git a/README.rst b/README.rst index 226906b..a787846 100644 --- a/README.rst +++ b/README.rst @@ -374,6 +374,40 @@ If any function returns something else beside a ``dict`` or ``None``, the workflow will be cut and the value returned immediately, this is useful to return ``HttpReponse`` instances like ``HttpResponseRedirect``. +---------------- +Partial Pipeline +---------------- + +It's possible to cut the pipeline process to return to the user asking for more +data and resume the process later, to accomplish this add the entry +``social_auth.backends.pipeline.misc.save_status_to_session`` (or a similar +implementation) to the pipeline setting before any entry that returns an +``HttpResponse`` instance:: + + SOCIAL_AUTH_PIPELINE = ( + ... + social_auth.backends.pipeline.misc.save_status_to_session, + app.pipeline.redirect_to_basic_user_data_form + ... + ) + +When it's time to resume the process just redirect the user to +``/complete//`` view. By default the pipeline will be resumed in the +next entry after ``save_status_to_session`` but this can be modified by setting +the following setting to the import path of the pipeline entry to resume +processing:: + + SOCIAL_AUTH_PIPELINE_RESUME_ENTRY = + +``save_status_to_session`` saves needed data into user session, the key can be +defined by ``SOCIAL_AUTH_PARTIAL_PIPELINE_KEY`` which default value is +``partial_pipeline``:: + + SOCIAL_AUTH_PARTIAL_PIPELINE_KEY = 'partial_pipeline' + +Check the `example application`_ to check a basic usage. + + --------------- Deprecated bits --------------- @@ -899,3 +933,4 @@ Base work is copyrighted by: .. _Flickr OAuth: http://www.flickr.com/services/api/ .. _Flickr App Garden: http://www.flickr.com/services/apps/create/ .. _danielgtaylor: https://github.com/danielgtaylor +.. _example application: https://github.com/omab/django-social-auth/blob/master/example/local_settings.py.template#L21 diff --git a/doc/pipeline.rst b/doc/pipeline.rst index d8c9f94..13cf8c1 100644 --- a/doc/pipeline.rst +++ b/doc/pipeline.rst @@ -53,4 +53,38 @@ workflow will be cut and the value returned immediately, this is useful to return ``HttpReponse`` instances like ``HttpResponseRedirect``. +Partial Pipeline +---------------- + +It's possible to cut the pipeline process to return to the user asking for more +data and resume the process later, to accomplish this add the entry +``social_auth.backends.pipeline.misc.save_status_to_session`` (or a similar +implementation) to the pipeline setting before any entry that returns an +``HttpResponse`` instance:: + + SOCIAL_AUTH_PIPELINE = ( + ... + social_auth.backends.pipeline.misc.save_status_to_session, + app.pipeline.redirect_to_basic_user_data_form + ... + ) + +When it's time to resume the process just redirect the user to +``/complete//`` view. By default the pipeline will be resumed in the +next entry after ``save_status_to_session`` but this can be modified by setting +the following setting to the import path of the pipeline entry to resume +processing:: + + SOCIAL_AUTH_PIPELINE_RESUME_ENTRY = + +``save_status_to_session`` saves needed data into user session, the key can be +defined by ``SOCIAL_AUTH_PARTIAL_PIPELINE_KEY`` which default value is +``partial_pipeline``:: + + SOCIAL_AUTH_PARTIAL_PIPELINE_KEY = 'partial_pipeline' + +Check the `example application`_ to check a basic usage. + + .. _django-social-auth: https://github.com/omab/django-social-auth +.. _example application: https://github.com/omab/django-social-auth/blob/master/example/local_settings.py.template#L21 diff --git a/example/app/pipeline.py b/example/app/pipeline.py new file mode 100644 index 0000000..0d81b17 --- /dev/null +++ b/example/app/pipeline.py @@ -0,0 +1,14 @@ +from django.http import HttpResponseRedirect + + +def username(request, *args, **kwargs): + if kwargs.get('user'): + username = kwargs['user'].username + else: + username = request.session.get('saved_username') + return { 'username': username } + + +def redirect_to_form(*args, **kwargs): + if not kwargs['request'].session.get('saved_username') and kwargs.get('user') is None: + return HttpResponseRedirect('/form/') diff --git a/example/app/views.py b/example/app/views.py index 002e86c..ef457fc 100644 --- a/example/app/views.py +++ b/example/app/views.py @@ -2,10 +2,11 @@ from django.http import HttpResponseRedirect from django.contrib.auth import logout as auth_logout from django.contrib.auth.decorators import login_required from django.template import RequestContext -from django.shortcuts import render_to_response +from django.shortcuts import render_to_response, redirect from django.contrib.messages.api import get_messages from social_auth import __version__ as version +from social_auth.utils import setting def home(request): @@ -19,8 +20,10 @@ def home(request): @login_required def done(request): """Login complete view, displays user data""" - ctx = {'version': version, - 'last_login': request.session.get('social_auth_last_login_backend')} + ctx = { + 'version': version, + 'last_login': request.session.get('social_auth_last_login_backend') + } return render_to_response('done.html', ctx, RequestContext(request)) def error(request): @@ -34,3 +37,12 @@ def logout(request): """Logs out user""" auth_logout(request) return HttpResponseRedirect('/') + + +def form(request): + if request.method == 'POST' and request.POST.get('username'): + name = setting('SOCIAL_AUTH_PARTIAL_PIPELINE_KEY', 'partial_pipeline') + request.session['saved_username'] = request.POST['username'] + backend = request.session[name]['backend'] + return redirect('socialauth_complete', backend=backend) + return render_to_response('form.html', {}, RequestContext(request)) diff --git a/example/local_settings.py.template b/example/local_settings.py.template index 6e733b4..7ac2635 100644 --- a/example/local_settings.py.template +++ b/example/local_settings.py.template @@ -19,3 +19,15 @@ GITHUB_APP_ID = '' GITHUB_API_SECRET = '' FOURSQUARE_CONSUMER_KEY = '' FOURSQUARE_CONSUMER_SECRET = '' + +SOCIAL_AUTH_PIPELINE = ( + 'social_auth.backends.pipeline.social.social_auth_user', + 'social_auth.backends.pipeline.associate.associate_by_email', + 'social_auth.backends.pipeline.misc.save_status_to_session', + 'app.pipeline.redirect_to_form', + 'app.pipeline.username', + 'social_auth.backends.pipeline.user.create_user', + 'social_auth.backends.pipeline.social.associate_user', + 'social_auth.backends.pipeline.social.load_extra_data', + 'social_auth.backends.pipeline.user.update_user_details', +) diff --git a/example/templates/form.html b/example/templates/form.html new file mode 100644 index 0000000..fc6e814 --- /dev/null +++ b/example/templates/form.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block heading %}User basic form{% endblock %} + +{% block content %} +
+ {% csrf_token %} +

+ + +

+ + +
+{% endblock %} diff --git a/example/urls.py b/example/urls.py index bcede41..8344d27 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,7 +1,7 @@ from django.conf.urls.defaults import patterns, url, include from django.contrib import admin -from app.views import home, done, logout, error +from app.views import home, done, logout, error, form admin.autodiscover() @@ -11,6 +11,7 @@ urlpatterns = patterns('', url(r'^done/$', done, name='done'), url(r'^error/$', error, name='error'), url(r'^logout/$', logout, name='logout'), + url(r'^form/$', form, name='form'), url(r'^admin/', include(admin.site.urls)), url(r'', include('social_auth.urls')), ) diff --git a/social_auth/backends/__init__.py b/social_auth/backends/__init__.py index d026475..0adf4c9 100644 --- a/social_auth/backends/__init__.py +++ b/social_auth/backends/__init__.py @@ -98,11 +98,22 @@ class SocialAuthBackend(ModelBackend): return None response = kwargs.get('response') - details = self.get_user_details(response) - uid = self.get_user_id(details, response) - out = self.pipeline(PIPELINE, backend=self, uid=uid, - details=details, is_new=False, + + if 'pipeline_index' in kwargs: + details = kwargs.pop('details') + uid = kwargs.pop('uid') + is_new = kwargs.pop('is_new') + pipeline = PIPELINE[kwargs['pipeline_index']:] + else: + details = self.get_user_details(response) + uid = self.get_user_id(details, response) + is_new = False + pipeline = PIPELINE + + out = self.pipeline(pipeline, backend=self, uid=uid, + details=details, is_new=is_new, *args, **kwargs) + if not isinstance(out, dict): return out @@ -303,6 +314,11 @@ class BaseAuth(object): """Completes loging process, must return user instance""" raise NotImplementedError('Implement in subclass') + def continue_pipeline(self, *args, **kwargs): + """Continue previos halted pipeline""" + kwargs.update({ self.AUTH_BACKEND.name: True }) + return authenticate(*args, **kwargs) + def auth_extra_arguments(self): """Return extra argumens needed on auth process, setting is per bancked and defined by _AUTH_EXTRA_ARGUMENTS. diff --git a/social_auth/backends/google.py b/social_auth/backends/google.py index 92e4385..5786938 100644 --- a/social_auth/backends/google.py +++ b/social_auth/backends/google.py @@ -84,6 +84,7 @@ class GoogleBackend(OpenIDBackend): http://axschema.org/contact/email""" return details['email'] + # Auth classes class GoogleAuth(OpenIdAuth): """Google OpenID authentication""" diff --git a/social_auth/backends/pipeline/__init__.py b/social_auth/backends/pipeline/__init__.py index 6af8113..ae04cde 100644 --- a/social_auth/backends/pipeline/__init__.py +++ b/social_auth/backends/pipeline/__init__.py @@ -10,6 +10,7 @@ import warnings from django.conf import settings from social_auth.models import User +from social_auth.backends import get_backend, PIPELINE USERNAME = 'username' diff --git a/social_auth/backends/pipeline/misc.py b/social_auth/backends/pipeline/misc.py new file mode 100644 index 0000000..3b30deb --- /dev/null +++ b/social_auth/backends/pipeline/misc.py @@ -0,0 +1,30 @@ +from social_auth.backends import PIPELINE +from social_auth.utils import setting + + +PIPELINE_ENTRY = 'social_auth.backends.pipeline.misc.save_status_to_session' + + +def save_status_to_session(request, backend, details, response, uid, + *args, **kwargs): + """Saves current social-auth status to session.""" + next_entry = setting('SOCIAL_AUTH_PIPELINE_RESUME_ENTRY') + + try: + if next_entry: + idx = PIPELINE.index(next_entry) + else: + idx = PIPELINE.index(PIPELINE_ENTRY) + 1 + except ValueError: + idx = None + + name = setting('SOCIAL_AUTH_PARTIAL_PIPELINE_KEY', 'partial_pipeline') + request.session[name] = { + 'backend': backend.name, + 'uid': uid, + 'details': details, + 'response': response, + 'is_new': kwargs.get('is_new', True), + 'next_index': idx + } + request.session.modified = True diff --git a/social_auth/urls.py b/social_auth/urls.py index e1bb193..3d41d3c 100644 --- a/social_auth/urls.py +++ b/social_auth/urls.py @@ -6,12 +6,21 @@ from social_auth.views import auth, complete, associate, associate_complete, \ urlpatterns = patterns('', - url(r'^login/(?P[^/]+)/$', auth, name='socialauth_begin'), - url(r'^complete/(?P[^/]+)/$', complete, name='socialauth_complete'), - url(r'^associate/(?P[^/]+)/$', associate, name='socialauth_associate_begin'), + # authentication + url(r'^login/(?P[^/]+)/$', auth, + name='socialauth_begin'), + url(r'^complete/(?P[^/]+)/$', complete, + name='socialauth_complete'), + + # association + url(r'^associate/(?P[^/]+)/$', associate, + name='socialauth_associate_begin'), url(r'^associate/complete/(?P[^/]+)/$', associate_complete, name='socialauth_associate_complete'), - url(r'^disconnect/(?P[^/]+)/$', disconnect, name='socialauth_disconnect'), - url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/$', disconnect, - name='socialauth_disconnect_individual'), + + # disconnection + url(r'^disconnect/(?P[^/]+)/$', disconnect, + name='socialauth_disconnect'), + url(r'^disconnect/(?P[^/]+)/(?P[^/]+)/$', + disconnect, name='socialauth_disconnect_individual'), ) diff --git a/social_auth/views.py b/social_auth/views.py index 48a2446..46a9018 100644 --- a/social_auth/views.py +++ b/social_auth/views.py @@ -49,6 +49,8 @@ def dsa_view(redirect_name=None): try: return func(request, backend, *args, **kwargs) except Exception, e: # some error ocurred + if setting('DEBUG'): + raise backend_name = backend.AUTH_BACKEND.name logger.error(unicode(e), exc_info=True, @@ -190,4 +192,18 @@ def auth_complete(request, backend, user=None, *args, **kwargs): """Complete auth process. Return authenticated user or None.""" if user and not user.is_authenticated(): user = None - return backend.auth_complete(user=user, request=request, *args, **kwargs) + + name = setting('SOCIAL_AUTH_PARTIAL_PIPELINE_KEY', 'partial_pipeline') + if request.session.get(name): + data = request.session.pop(name) + request.session.modified = True + return backend.continue_pipeline(pipeline_index=data['next_index'], + user=user, + request=request, + uid=data['uid'], + details=data['details'], + is_new=data['is_new'], + response=data['response'], + *args, **kwargs) + else: + return backend.auth_complete(user=user, request=request, *args, **kwargs) -- 2.39.5