Skip to content
This repository has been archived by the owner on Feb 22, 2024. It is now read-only.

Commit

Permalink
Merge branch 'develop' into alexef-develop
Browse files Browse the repository at this point in the history
  • Loading branch information
eriktaubeneck committed Jul 10, 2014
2 parents 65b1007 + 933c401 commit da68c7c
Show file tree
Hide file tree
Showing 20 changed files with 247 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ language: python
python:
- "2.6"
- "2.7"
- "pypy"

install:
- pip install . --quiet --use-mirrors
Expand All @@ -22,4 +21,5 @@ script: nosetests

branches:
only:
- master
- develop
22 changes: 21 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ Flask-Social Changelog

Here you can see the full list of changes between each Flask-Social release.

Version 1.6.2
-------------

Released November 13 2013

- Changed provider.get_connection_values() to also return Full Name (if avaliable)
- Fixed bug in the connect_callback() fuction to redirect upon OAuth failure


Version 1.6.1
-------------

Released July 25 2013

- Changed google provider to use oauth2 service instead of plus service
- Fixed MongoEngine datastore to support multiple versions
- Fixed bug when a user denies access from OAuth provider
- Fixed foursquare provider image_url bug


Version 1.6.0
-------------

Expand Down Expand Up @@ -63,4 +83,4 @@ Version 0.1.0

Released March 2012

- Initial release
- Initial release
12 changes: 10 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
Flask-Social
============

.. image:: https://secure.travis-ci.org/mattupstate/flask-social.png?branch=develop
:Master:

.. image:: https://secure.travis-ci.org/mattupstate/flask-social.png?branch=master
:target: https://secure.travis-ci.org/mattupstate/flask-social?branch=master

:Develop:

.. image:: https://secure.travis-ci.org/mattupstate/flask-social.png?branch=develop
:target: https://secure.travis-ci.org/mattupstate/flask-social?branch=develop

Oauth provider login and APIs for use with
`Flask-Security <http://packages.python.org/Flask-Security/>`_
Expand All @@ -14,4 +22,4 @@ Resources
- `Code <http://github.com/mattupstate/flask-social/>`_
- `Development Version
<http://github.com/mattupstate/flask-rq/zipball/develop#egg=Flask-Social-dev>`_
- `Example Application <http://flask-social-example.herokuapp.com/>`_
- `Example Application <http://flask-social-example.herokuapp.com/>`_
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
# built documents.
#
# The short X.Y version.
version = '1.6.0'
version = '1.6.2'
# The full version, including alpha/beta/rc tags.
release = version

Expand Down Expand Up @@ -307,4 +307,4 @@

pygments_style = 'tango'
html_theme = 'default'
html_theme_options = {}
html_theme_options = {}
60 changes: 50 additions & 10 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ Then install your datastore requirement.

**MongoEngine**::

$ pip install https://github.com/sbook/flask-mongoengine/tarball/master
$ pip install flask-mongoengine

Then install your provider API libraries.

**Facebook**::

$ pip install http://github.com/pythonforfacebook/facebook-sdk/tarball/master
$ pip install facebook-sdk

**Twitter**::

Expand Down Expand Up @@ -111,7 +111,7 @@ to configure your application with your provider's application values

**Google**::

SOCIAL_GOOGLE = {
app.config['SOCIAL_GOOGLE'] = {
'consumer_key': 'xxxx',
'consumer_secret': 'xxxx'
}
Expand All @@ -127,15 +127,16 @@ index::

# ... create the app ...

app.config['SECURITY_POST_LOGIN'] = '/profile'
app.config['SECURITY_POST_LOGIN_VIEW'] = '/profile'

db = SQLAlchemy(app)

# ... define user and role models ...

class Connection(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('user')
full_name = db.Column(db.String(255))
provider_id = db.Column(db.String(255))
provider_user_id = db.Column(db.String(255))
access_token = db.Column(db.String(255))
Expand All @@ -149,6 +150,15 @@ index::
Security(app, SQLAlchemyUserDatastore(db, User, Role))
Social(app, SQLAlchemyConnectionDatastore(db, Connection))

If you do not want the same OAuth account to be connected to more than one user account,
add the following to the Connection database model:

__table_args__ = (db.UniqueConstraint('provider_id', 'provider_user_id', name='_providerid_userid_uc'), {})

This will ensure that every row in the Connection table has a unique provider_id and
provider_user_id pair. This means that any given Twitter account can only be connected
once.


Connecting to Providers
-----------------------
Expand All @@ -168,15 +178,17 @@ add a mechanism on the profile page to do so. First the view method::

You should notice the mechanism for retreiving the current user's connection
with each service provider. If a connection is not found, the value will be
`None`. Now lets take a look at the profile template::
`None`.

Now lets take a look at the profile template::

{% macro show_provider_button(provider_id, display_name, conn) %}
{% if conn %}
<form action="{{ url_for('flask_social.remove_connection', provider_id=conn.provider_id, provider_user_id=conn.provider_user_id) }}" method="DELETE">
<form action="{{ url_for('social.remove_connection', provider_id=conn.provider_id, provider_user_id=conn.provider_user_id) }}?__METHOD_OVERRIDE__=DELETE" method="POST">
<input type="submit" value="Disconnect {{ display_name }}" />
</form>
{% else %}
<form action="{{ url_for('flask_social.connect', provider_id=provider_id) }}" method="POST">
<form action="{{ url_for('social.connect', provider_id=provider_id) }}" method="POST">
<input type="submit" value="Connect {{ display_name }}" />
</form>
{% endif %}
Expand All @@ -195,13 +207,41 @@ connect button will initiate the OAuth flow with the given provider, allowing
the user to authorize the application and return a token and/or secret to be
used when configuring an API instance.

However, notice that the first form for removing social connections uses HTTP method
tunneling, since HTML forms only support POST and GET. We tunnel by passing a query
string parameter called __METHOD_OVERRIDE__ and set its value to DELETE. Ideally, we
can handle this via a piece of middleware::

from werkzeug import url_decode

# Taken from https://github.com/mattupstate/flask-social-example/blob/master/app/middleware.py
class MethodRewriteMiddleware(object):
''' Middleware that will allow the passing of METHOD_OVERRIDE to a url
for HTTP verbs that cannot be done via <form>, like DELETE. '''
def __init__(self, app):
self.app = app

def __call__(self, environ, start_response):
if 'METHOD_OVERRIDE' in environ.get('QUERY_STRING', ''):
args = url_decode(environ['QUERY_STRING'])
method = args.get('__METHOD_OVERRIDE__')
if method:
method = method.encode('ascii', 'replace')
environ['REQUEST_METHOD'] = method
return self.app(environ, start_response)

app.wsgi_app = MethodRewriteMiddleware(app.wsgi_app)

The middleware will now look for "METHOD_OVERRIDE" in the query string and if it's
found, update the REQUEST_METHOD environment variable.

Logging In
----------

If a user has a connection established to a service provider then it is possible
for them to login via the provider. A login form would look like the following::

<form action="{{ url_for('flask_security.authenticate') }}" method="POST" name="login_form">
<form action="{{ url_for('security.authenticate') }}" method="POST" name="login_form">
{{ form.hidden_tag() }}
{{ form.username.label }} {{ form.username }}<br/>
{{ form.password.label }} {{ form.password }}<br/>
Expand All @@ -210,7 +250,7 @@ for them to login via the provider. A login form would look like the following::
</form>

{% macro social_login(provider_id, display_name) %}
<form action="{{ url_for('flask_social.login', provider_id=provider_id) }}" method="POST">
<form action="{{ url_for('social.login', provider_id=provider_id) }}" method="POST">
<input type="submit" value="Login with {{ display_name }}" />
</form>
{% endmacro %}
Expand Down
2 changes: 1 addition & 1 deletion flask_social/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
:license: MIT, see LICENSE for more details.
"""

__version__ = '1.6.0'
__version__ = '1.6.2'

from .core import Social
from .datastore import SQLAlchemyConnectionDatastore, \
Expand Down
2 changes: 1 addition & 1 deletion flask_social/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def init_app(self, app, datastore=None):
providers = dict()

for key, config in app.config.items():
if not key.startswith('SOCIAL_') or key in default_config:
if not key.startswith('SOCIAL_') or config is None or key in default_config:
continue

suffix = key.lower().replace('social_', '')
Expand Down
5 changes: 4 additions & 1 deletion flask_social/datastore.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def __init__(self, db, connection_model):
ConnectionDatastore.__init__(self, connection_model)

def _query(self, **kwargs):
from mongoengine.queryset import Q, QCombination
try:
from mongoengine.queryset import Q, QCombination
except ImportError:
from mongoengine.queryset.visitor import Q, QCombination
queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items())
query = QCombination(QCombination.AND, queries)
return self.connection_model.objects(query)
Expand Down
3 changes: 2 additions & 1 deletion flask_social/providers/facebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ def get_connection_values(response, **kwargs):
provider_user_id=profile['id'],
access_token=access_token,
secret=None,
display_name=profile.get('username', profile.get('name')),
display_name=profile.get('username', None),
full_name = profile.get('name', None),
profile_url=profile_url,
image_url=image_url,
email=profile.get('email', '')
Expand Down
5 changes: 4 additions & 1 deletion flask_social/providers/foursquare.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from __future__ import absolute_import

import foursquare
import urlparse

config = {
'id': 'foursquare',
Expand Down Expand Up @@ -52,14 +53,16 @@ def get_connection_values(response, **kwargs):
api = foursquare.Foursquare(access_token=access_token)
user = api.users()['user']
profile_url = 'http://www.foursquare.com/user/' + user['id']
image_url = '%s%s' % (user['photo']['prefix'], user['photo']['suffix'][1:])
image_url = urlparse.urljoin(user['photo']['prefix'],
user['photo']['suffix'])

return dict(
provider_id=config['id'],
provider_user_id=user['id'],
access_token=access_token,
secret=None,
display_name=profile_url.split('/')[-1:][0],
full_name = '%s %s' % (user['firstName'], user['lastName']),
profile_url=profile_url,
image_url=image_url
)
33 changes: 15 additions & 18 deletions flask_social/providers/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,24 @@
},
'request_token_params': {
'response_type': 'code',
'scope': 'https://www.googleapis.com/auth/plus.me'
'scope': 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.me'
#add ' https://www.googleapis.com/auth/userinfo.email' to scope to also get email
}
}

def _get_api(credentials):
http = httplib2.Http()
http = credentials.authorize(http)
api = googleapi.build('oauth2', 'v2', http=http)
return api


def get_api(connection, **kwargs):
credentials = googleoauth.AccessTokenCredentials(
access_token=getattr(connection, 'access_token'),
user_agent=''
)

http = httplib2.Http()
http = credentials.authorize(http)
return googleapi.build('plus', 'v1', http=http)
return _get_api(credentials)


def get_provider_user_id(response, **kwargs):
Expand All @@ -52,11 +56,7 @@ def get_provider_user_id(response, **kwargs):
access_token=response['access_token'],
user_agent=''
)

http = httplib2.Http()
http = credentials.authorize(http)
api = googleapi.build('plus', 'v1', http=http)
profile = api.people().get(userId='me').execute()
profile = _get_api(credentials).userinfo().get().execute()
return profile['id']
return None

Expand All @@ -72,17 +72,14 @@ def get_connection_values(response, **kwargs):
user_agent=''
)

http = httplib2.Http()
http = credentials.authorize(http)
api = googleapi.build('plus', 'v1', http=http)
profile = api.people().get(userId='me').execute()

profile = _get_api(credentials).userinfo().get().execute()
return dict(
provider_id=config['id'],
provider_user_id=profile['id'],
access_token=access_token,
secret=None,
display_name=profile['displayName'],
profile_url=profile['url'],
image_url=profile['image']['url']
display_name=profile['name'],
full_name=profile['name'],
profile_url=profile.get('link'),
image_url=profile.get('picture')
)
Loading

0 comments on commit da68c7c

Please sign in to comment.