[Python-modules-commits] [python-social-auth] 03/07: Import python-social-auth_0.2.21+dfsg.orig.tar.gz

Wolfgang Borgert debacle at moszumanska.debian.org
Tue Jan 10 00:41:32 UTC 2017


This is an automated email from the git hooks/post-receive script.

debacle pushed a commit to branch master
in repository python-social-auth.

commit 02d4d7708972d1bf78a5ae590cfca8bdd6533293
Author: W. Martin Borgert <debacle at debian.org>
Date:   Mon Jan 9 23:42:41 2017 +0000

    Import python-social-auth_0.2.21+dfsg.orig.tar.gz
---
 CHANGELOG.md                                       |  94 +++++++++-
 README.rst                                         |   9 +-
 docs/backends/battlenet.rst                        |  10 +-
 docs/backends/edmodo.rst                           |  22 +++
 docs/backends/facebook.rst                         |   2 +-
 docs/backends/index.rst                            |   5 +
 docs/backends/line.rst                             |   7 +
 docs/backends/sketchfab.rst                        |  17 ++
 docs/backends/twitter.rst                          |   5 +
 docs/backends/untappd.rst                          |  28 +++
 docs/backends/upwork.rst                           |  28 +++
 docs/backends/vk.rst                               |   2 +-
 docs/backends/yammer.rst                           |   3 +-
 docs/intro.rst                                     |   2 +
 docs/pipeline.rst                                  |  59 +++---
 docs/thanks.rst                                    |   2 +
 docs/use_cases.rst                                 |   5 -
 .../cherrypy_example/local_settings.py.template    |   1 +
 examples/django_example/example/settings.py        |   2 +
 examples/django_me_example/example/settings.py     |   2 +
 examples/flask_example/__init__.py                 |   7 +-
 examples/flask_example/models/user.py              |   2 +-
 examples/flask_example/routes/main.py              |   2 +-
 examples/flask_example/settings.py                 |   1 +
 examples/flask_me_example/__init__.py              |   6 +-
 examples/flask_me_example/models/user.py           |   2 +-
 examples/flask_me_example/routes/main.py           |   2 +-
 examples/flask_me_example/settings.py              |   1 +
 .../__init__.py                                    |  32 ++--
 examples/flask_peewee_example/manage.py            |  26 +++
 examples/flask_peewee_example/models/__init__.py   |   4 +
 examples/flask_peewee_example/models/user.py       |  24 +++
 examples/flask_peewee_example/requirements.txt     |   7 +
 examples/flask_peewee_example/routes/__init__.py   |   2 +
 .../routes/main.py                                 |   0
 .../settings.py                                    |   2 +-
 examples/flask_peewee_example/templates/base.html  |  14 ++
 examples/flask_peewee_example/templates/done.html  |  24 +++
 examples/flask_peewee_example/templates/home.html  |  85 +++++++++
 examples/pyramid_example/example/settings.py       |   1 +
 examples/tornado_example/settings.py               |   1 +
 examples/webpy_example/app.py                      |   1 +
 site/css/site.css                                  |   4 +
 site/docs                                          |   1 +
 site/img/glyphicons-halflings-white.png            | Bin 0 -> 8777 bytes
 site/img/glyphicons-halflings.png                  | Bin 0 -> 12799 bytes
 site/index.html                                    | 104 +++++++++++
 social/__init__.py                                 |   2 +-
 social/actions.py                                  |  17 +-
 social/apps/django_app/default/admin.py            |  16 +-
 social/apps/django_app/default/config.py           |   1 +
 social/apps/django_app/default/fields.py           |   9 +-
 .../django_app/default/migrations/0001_initial.py  |   2 +
 .../default/migrations/0002_add_related_name.py    |  11 +-
 .../migrations/0003_alter_email_max_length.py      |   4 +-
 .../default/migrations/0004_auto_20160423_0400.py  |   3 +-
 .../default/migrations/0005_auto_20160727_2333.py  |  19 ++
 social/apps/django_app/default/models.py           |   3 +
 social/apps/django_app/tests.py                    |   1 +
 social/apps/flask_app/peewee/__init__.py           |   0
 social/apps/flask_app/peewee/models.py             |  48 +++++
 social/apps/flask_app/routes.py                    |   2 +-
 social/apps/pyramid_app/models.py                  |   2 +
 social/apps/pyramid_app/views.py                   |   2 +-
 social/backends/battlenet.py                       |   2 +-
 social/backends/coding.py                          |  48 +++++
 social/backends/coursera.py                        |   4 +
 social/backends/dribbble.py                        |   2 +-
 social/backends/edmodo.py                          |  34 ++++
 social/backends/facebook.py                        |   8 +-
 social/backends/line.py                            |  91 ++++++++++
 social/backends/oauth.py                           |  10 ++
 social/backends/qiita.py                           |   2 +-
 social/backends/reddit.py                          |   6 +-
 social/backends/sketchfab.py                       |  39 ++++
 social/backends/uber.py                            |  16 +-
 social/backends/untappd.py                         | 110 ++++++++++++
 social/backends/upwork.py                          |  39 ++++
 social/backends/weixin.py                          |  78 +++++++-
 social/pipeline/social_auth.py                     |   5 +-
 social/pipeline/user.py                            |   4 +-
 social/storage/django_orm.py                       |  29 ++-
 social/storage/peewee_orm.py                       | 199 +++++++++++++++++++++
 social/storage/sqlalchemy_orm.py                   |   8 +-
 social/strategies/base.py                          |   2 +
 social/strategies/pyramid_strategy.py              |   8 +-
 social/tests/backends/test_edmodo.py               |  44 +++++
 social/tests/backends/test_facebook.py             |   2 +-
 social/tests/backends/test_sketchfab.py            |  26 +++
 social/tests/backends/test_twitter.py              | 129 +++++++++++++
 social/tests/backends/test_upwork.py               |  53 ++++++
 social/tests/test_utils.py                         |  52 +++++-
 social/utils.py                                    |  27 ++-
 93 files changed, 1742 insertions(+), 137 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f65d2a8..c16a9aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,94 @@
 # Change Log
 
-## [v0.2.17](https://github.com/omab/python-social-auth/tree/v0.2.17) (2016-04-20)
+## [Unreleased](https://github.com/omab/python-social-auth/tree/HEAD)
+
+[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.21...HEAD)
+
+## [v0.2.21](https://github.com/omab/python-social-auth/tree/v0.2.21) (2016-08-15)
+
+**Closed issues:**
+
+- Django Migrations Broken [\#991](https://github.com/omab/python-social-auth/issues/991)
+
+**Merged pull requests:**
+
+- Fixed Django Migrations [\#993](https://github.com/omab/python-social-auth/pull/993) ([clintonb](https://github.com/clintonb))
+- Rewrited pipeline.rst [\#992](https://github.com/omab/python-social-auth/pull/992) ([an0o0nym](https://github.com/an0o0nym))
+- fix typo "Piepeline" -\> "Pipeline" [\#990](https://github.com/omab/python-social-auth/pull/990) ([das-g](https://github.com/das-g))
+- Fixed Django \< 1.8 broken compatibility [\#986](https://github.com/omab/python-social-auth/pull/986) ([seroy](https://github.com/seroy))
+
+## [v0.2.20](https://github.com/omab/python-social-auth/tree/v0.2.20) (2016-08-12)
+[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.19...v0.2.20)
+
+**Closed issues:**
 
+- On production /complete/facebook just times out with a Gateway Timeout [\#972](https://github.com/omab/python-social-auth/issues/972)
+- Support namespace via : [\#971](https://github.com/omab/python-social-auth/issues/971)
+- Django Association model missing index [\#967](https://github.com/omab/python-social-auth/issues/967)
+- VK auth using access token failed. Unable to retrieve email address. [\#943](https://github.com/omab/python-social-auth/issues/943)
+- ImportError: No module named django\_app  [\#935](https://github.com/omab/python-social-auth/issues/935)
+- ImportError: No module named 'example.local\_settings' with pyramid\_example [\#919](https://github.com/omab/python-social-auth/issues/919)
+- "'User' object is not callable." issue. [\#895](https://github.com/omab/python-social-auth/issues/895)
+- Support for the peewee ORM in storage. [\#877](https://github.com/omab/python-social-auth/issues/877)
+- Meetup.com OAuth2 [\#677](https://github.com/omab/python-social-auth/issues/677)
+
+**Merged pull requests:**
+
+- fix comment word [\#983](https://github.com/omab/python-social-auth/pull/983) ([alexpantyukhin](https://github.com/alexpantyukhin))
+- Added exception handling for user creation race condition in Django [\#975](https://github.com/omab/python-social-auth/pull/975) ([carsongee](https://github.com/carsongee))
+- Update facebook api version to v2.7 [\#973](https://github.com/omab/python-social-auth/pull/973) ([c-bata](https://github.com/c-bata))
+- Added index to Django Association model [\#969](https://github.com/omab/python-social-auth/pull/969) ([clintonb](https://github.com/clintonb))
+- Corrected migration dependency [\#968](https://github.com/omab/python-social-auth/pull/968) ([clintonb](https://github.com/clintonb))
+- Removed dep method get\_all\_field\_names method from Django 1.8+ [\#966](https://github.com/omab/python-social-auth/pull/966) ([zsiddique](https://github.com/zsiddique))
+- Multiple hosts in redirect sanitaion. [\#965](https://github.com/omab/python-social-auth/pull/965) ([moorchegue](https://github.com/moorchegue))
+- "else" scenario in Pyramid html func was causing an exception every time. [\#964](https://github.com/omab/python-social-auth/pull/964) ([moorchegue](https://github.com/moorchegue))
+- Allow POST requests for auth method so OpenID forms could use it that way [\#963](https://github.com/omab/python-social-auth/pull/963) ([moorchegue](https://github.com/moorchegue))
+- Add redirect\_uri to yammer docs [\#960](https://github.com/omab/python-social-auth/pull/960) ([m3brown](https://github.com/m3brown))
+- Fix for flask/SQLAlchemy: commit on save \(but not when using Pyramid\) [\#957](https://github.com/omab/python-social-auth/pull/957) ([aoghina](https://github.com/aoghina))
+- Switch from flask.ext.login to flask\_login [\#951](https://github.com/omab/python-social-auth/pull/951) ([EdwardBetts](https://github.com/EdwardBetts))
+- username max\_length can be None [\#950](https://github.com/omab/python-social-auth/pull/950) ([EdwardBetts](https://github.com/EdwardBetts))
+- Upgrade facebook backend api to latest version \(v2.6\) [\#941](https://github.com/omab/python-social-auth/pull/941) ([stphivos](https://github.com/stphivos))
+- Line support added [\#937](https://github.com/omab/python-social-auth/pull/937) ([polyn0m](https://github.com/polyn0m))
+- django migration should respect SOCIAL\_AUTH\_USER\_MODEL setting [\#936](https://github.com/omab/python-social-auth/pull/936) ([max-arnold](https://github.com/max-arnold))
+- fix first and last name recovery [\#934](https://github.com/omab/python-social-auth/pull/934) ([PhilipGarnero](https://github.com/PhilipGarnero))
+- fixes empty uid in coursera backend [\#933](https://github.com/omab/python-social-auth/pull/933) ([CrowbarKZ](https://github.com/CrowbarKZ))
+- add support peewee for flask \#877 [\#932](https://github.com/omab/python-social-auth/pull/932) ([alexpantyukhin](https://github.com/alexpantyukhin))
+- Fixed typo [\#928](https://github.com/omab/python-social-auth/pull/928) ([arogachev](https://github.com/arogachev))
+- Fix mixed-content error of loading http over https scheme after disconnection from social account [\#924](https://github.com/omab/python-social-auth/pull/924) ([andela-kerinoso](https://github.com/andela-kerinoso))
+- Add back-end for Edmodo [\#921](https://github.com/omab/python-social-auth/pull/921) ([browniebroke](https://github.com/browniebroke))
+- Add Django AppConfig Label of "social\_auth" for migrations [\#916](https://github.com/omab/python-social-auth/pull/916) ([cclay](https://github.com/cclay))
+- Update vk.rst [\#907](https://github.com/omab/python-social-auth/pull/907) ([slushkovsky](https://github.com/slushkovsky))
+- VULNERABILITY - BaseStrategy.validate\_email\(\) doesn't actually check email address [\#900](https://github.com/omab/python-social-auth/pull/900) ([scottp-dpaw](https://github.com/scottp-dpaw))
+- Removed broken link in use cases docs fixing \#860 [\#886](https://github.com/omab/python-social-auth/pull/886) ([RobinStephenson](https://github.com/RobinStephenson))
+- Fixes bug where partial pipelines from abandoned login attempts will be resumed … [\#882](https://github.com/omab/python-social-auth/pull/882) ([SeanHayes](https://github.com/SeanHayes))
+- Revise battlenet endpoint to return account ID and battletag [\#799](https://github.com/omab/python-social-auth/pull/799) ([ckcollab](https://github.com/ckcollab))
+- Fixed 401 client redirect error for reddit backend [\#772](https://github.com/omab/python-social-auth/pull/772) ([opaqe](https://github.com/opaqe))
+
+## [v0.2.19](https://github.com/omab/python-social-auth/tree/v0.2.19) (2016-04-29)
+[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.18...v0.2.19)
+
+**Closed issues:**
+
+- \[Flask\] Not Logged in After Redirect [\#913](https://github.com/omab/python-social-auth/issues/913)
+- Django: type\(social\_user.extra\_data\) == unicode [\#898](https://github.com/omab/python-social-auth/issues/898)
+- Email is empty in login with Facebook [\#889](https://github.com/omab/python-social-auth/issues/889)
+
+**Merged pull requests:**
+
+- Storing token\_type in extra\_data field when using OAuth 2.0 [\#912](https://github.com/omab/python-social-auth/pull/912) ([clintonb](https://github.com/clintonb))
+- Updates to OpenIdConnectAuth [\#911](https://github.com/omab/python-social-auth/pull/911) ([clintonb](https://github.com/clintonb))
+- Corrected default value of JSONField [\#908](https://github.com/omab/python-social-auth/pull/908) ([clintonb](https://github.com/clintonb))
+
+## [v0.2.18](https://github.com/omab/python-social-auth/tree/v0.2.18) (2016-04-20)
+[Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.17...v0.2.18)
+
+## [v0.2.17](https://github.com/omab/python-social-auth/tree/v0.2.17) (2016-04-20)
 [Full Changelog](https://github.com/omab/python-social-auth/compare/v0.2.16...v0.2.17)
 
 **Merged pull requests:**
 
+- ADDED: upwork backend [\#904](https://github.com/omab/python-social-auth/pull/904) ([shepilov-vladislav](https://github.com/shepilov-vladislav))
+- Add Sketchfab OAuth2 backend [\#901](https://github.com/omab/python-social-auth/pull/901) ([sylvinus](https://github.com/sylvinus))
 - django 1.8+ compat to ensure to\_python is always called when accessing result from db.. [\#897](https://github.com/omab/python-social-auth/pull/897) ([sbussetti](https://github.com/sbussetti))
 
 ## [v0.2.16](https://github.com/omab/python-social-auth/tree/v0.2.16) (2016-04-13)
@@ -29,6 +112,9 @@
 
 **Merged pull requests:**
 
+- Add weixin public number oauth backend. [\#899](https://github.com/omab/python-social-auth/pull/899) ([duoduo369](https://github.com/duoduo369))
+- Add support for Untappd as an OAuth v2 backend [\#894](https://github.com/omab/python-social-auth/pull/894) ([svvitale](https://github.com/svvitale))
+- add coding oauth [\#892](https://github.com/omab/python-social-auth/pull/892) ([joway](https://github.com/joway))
 - Add a backend for Classlink. [\#890](https://github.com/omab/python-social-auth/pull/890) ([antinescience](https://github.com/antinescience))
 - Pass response to AuthCancel exception [\#883](https://github.com/omab/python-social-auth/pull/883) ([st4lk](https://github.com/st4lk))
 - modifed wrong key names in pocket.py [\#878](https://github.com/omab/python-social-auth/pull/878) ([EunJung-Seo](https://github.com/EunJung-Seo))
@@ -40,6 +126,7 @@
 - Add some tests for Spotify backend + add a backend for Deezer music service [\#845](https://github.com/omab/python-social-auth/pull/845) ([khamaileon](https://github.com/khamaileon))
 - \[Fix\] update odnoklasniki docs to new domain ok [\#836](https://github.com/omab/python-social-auth/pull/836) ([vanadium23](https://github.com/vanadium23))
 - add github enterprise docs on how to specify the API URL [\#834](https://github.com/omab/python-social-auth/pull/834) ([iserko](https://github.com/iserko))
+- Added optional 'include\_email' query param for Twitter backend. [\#829](https://github.com/omab/python-social-auth/pull/829) ([halfstrik](https://github.com/halfstrik))
 - Fix ImportError: cannot import name ‘urlencode’ in Python3 [\#828](https://github.com/omab/python-social-auth/pull/828) ([mishbahr](https://github.com/mishbahr))
 - Fix wrong evaluation of boolean kwargs [\#824](https://github.com/omab/python-social-auth/pull/824) ([falknes](https://github.com/falknes))
 - SAML: raise AuthMissingParameter if idp param missing [\#821](https://github.com/omab/python-social-auth/pull/821) ([omarkhan](https://github.com/omarkhan))
@@ -68,6 +155,7 @@
 
 - Add support for Drip Email Marketing Site [\#810](https://github.com/omab/python-social-auth/pull/810) ([buddylindsey](https://github.com/buddylindsey))
 - Fix Django 1.10 deprecation warnings [\#806](https://github.com/omab/python-social-auth/pull/806) ([yprez](https://github.com/yprez))
+- bugs in social\_user and associate\_by\_email return values [\#800](https://github.com/omab/python-social-auth/pull/800) ([falcon1kr](https://github.com/falcon1kr))
 - Changed instagram backend to new authorization routes [\#797](https://github.com/omab/python-social-auth/pull/797) ([clybob](https://github.com/clybob))
 - Update settings.rst [\#793](https://github.com/omab/python-social-auth/pull/793) ([skolsuper](https://github.com/skolsuper))
 - Add naver.com OAuth2 backend [\#789](https://github.com/omab/python-social-auth/pull/789) ([se0kjun](https://github.com/se0kjun))
@@ -410,7 +498,7 @@
 - Pull Request for \#501 [\#502](https://github.com/omab/python-social-auth/pull/502) ([cdeblois](https://github.com/cdeblois))
 - Add support for Launchpad OpenId [\#500](https://github.com/omab/python-social-auth/pull/500) ([ianw](https://github.com/ianw))
 - Jawbone authentification fix [\#498](https://github.com/omab/python-social-auth/pull/498) ([rivf](https://github.com/rivf))
-- Coursera backend [\#496](https://github.com/omab/python-social-auth/pull/496) ([dreame4](https://github.com/dreame4))
+- Coursera backend [\#496](https://github.com/omab/python-social-auth/pull/496) ([adambabik](https://github.com/adambabik))
 - Added nonce unique constraint [\#491](https://github.com/omab/python-social-auth/pull/491) ([candlejack297](https://github.com/candlejack297))
 - Store Spotify's refresh\_token. [\#482](https://github.com/omab/python-social-auth/pull/482) ([ctbarna](https://github.com/ctbarna))
 - Slack improvements [\#479](https://github.com/omab/python-social-auth/pull/479) ([gorillamania](https://github.com/gorillamania))
@@ -927,7 +1015,7 @@
 **Merged pull requests:**
 
 - Fix OpenId auth with Flask 0.10 [\#16](https://github.com/omab/python-social-auth/pull/16) ([Flyflo](https://github.com/Flyflo))
-- Add CodersClan button [\#13](https://github.com/omab/python-social-auth/pull/13) ([Orchestrator81](https://github.com/Orchestrator81))
+- Add CodersClan button [\#13](https://github.com/omab/python-social-auth/pull/13) ([DrorCohenCC](https://github.com/DrorCohenCC))
 - Added a default to response in FacebookOAuth.do\_auth [\#12](https://github.com/omab/python-social-auth/pull/12) ([san-mate](https://github.com/san-mate))
 - Bug fix of FacebookAppOAuth2 [\#11](https://github.com/omab/python-social-auth/pull/11) ([san-mate](https://github.com/san-mate))
 
diff --git a/README.rst b/README.rst
index ec4e25a..f2b0516 100644
--- a/README.rst
+++ b/README.rst
@@ -14,9 +14,6 @@ for more frameworks and ORMs.
 .. image:: https://badge.fury.io/py/python-social-auth.png
    :target: http://badge.fury.io/py/python-social-auth
 
-.. image:: https://pypip.in/d/python-social-auth/badge.png
-   :target: https://crate.io/packages/python-social-auth?version=latest
-
 .. image:: https://readthedocs.org/projects/python-social-auth/badge/?version=latest
    :target: https://readthedocs.org/projects/python-social-auth/?badge=latest
    :alt: Documentation Status
@@ -84,6 +81,7 @@ or current ones extended):
     * Kakao_ OAuth2 https://developer.kakao.com
     * `Khan Academy`_ OAuth1
     * Launchpad_ OpenId
+    * Line_ OAuth2
     * Linkedin_ OAuth1
     * Live_ OAuth2
     * Livejournal_ OpenId
@@ -108,6 +106,7 @@ or current ones extended):
     * Readability_ OAuth1
     * Reddit_ OAuth2 https://github.com/reddit/reddit/wiki/OAuth2
     * Shopify_ OAuth2
+    * Sketchfab_ OAuth2
     * Skyrock_ OAuth1
     * Soundcloud_ OAuth2
     * Stackoverflow_ OAuth2
@@ -123,6 +122,7 @@ or current ones extended):
     * Twilio_ Auth
     * Twitter_ OAuth1
     * Uber_ OAuth2
+    * Untappd_ OAuth2
     * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications
     * Weibo_ OAuth2
     * Withings_ OAuth1
@@ -263,6 +263,7 @@ check `django-social-auth LICENSE`_ for details:
 .. _Instagram: https://instagram.com
 .. _Itembase: https://www.itembase.com
 .. _LaunchPad: https://help.launchpad.net/YourAccount/OpenID
+.. _Line: https://line.me/
 .. _Linkedin: https://www.linkedin.com
 .. _Live: https://live.com
 .. _Livejournal: http://livejournal.com
@@ -278,6 +279,7 @@ check `django-social-auth LICENSE`_ for details:
 .. _Pocket: http://getpocket.com
 .. _Podio: https://podio.com
 .. _Shopify: http://shopify.com
+.. _Sketchfab: https://sketchfab.com/developers/oauth
 .. _Skyrock: https://skyrock.com
 .. _Soundcloud: https://soundcloud.com
 .. _Stocktwits: https://stocktwits.com
@@ -325,3 +327,4 @@ check `django-social-auth LICENSE`_ for details:
 .. _PixelPin: http://pixelpin.co.uk
 .. _Zotero: http://www.zotero.org/
 .. _Pinterest: https://www.pinterest.com
+.. _Untappd: https://untappd.com/
diff --git a/docs/backends/battlenet.rst b/docs/backends/battlenet.rst
index 65a1ac0..db6a842 100644
--- a/docs/backends/battlenet.rst
+++ b/docs/backends/battlenet.rst
@@ -19,12 +19,16 @@ enable ``python-social-auth`` support follow this steps:
         ...
     )
 
-Note: The API returns an accountId which will be used as identifier for the
-user.  If you want to allow the user to choose a username from his own
+Note: If you want to allow the user to choose a username from his own
 characters, some further steps are required, see the use cases part of the
-documentation.
+documentation. To get the account id and battletag use the user_data function, as
+`account id is no longer passed inherently`_.
+
+Another note: If you get a 500 response "Internal Server Error" the API now requires `https on callback endpoints`_.
 
 Further documentation at `Developer Guide`_.
 
 .. _Battlenet Developer Portal: https://dev.battle.net/
 .. _Developer Guide: https://dev.battle.net/docs/read/oauth
+.. _https on callback endpoints: http://us.battle.net/en/forum/topic/17085510584
+.. _account id is no longer passed inherently: http://us.battle.net/en/forum/topic/18300183303
diff --git a/docs/backends/edmodo.rst b/docs/backends/edmodo.rst
new file mode 100644
index 0000000..095c456
--- /dev/null
+++ b/docs/backends/edmodo.rst
@@ -0,0 +1,22 @@
+Edmodo
+======
+
+Edmodo supports OAuth 2.
+
+- Register a new application at `Edmodo Connect API`_, and follow the
+  instructions below.
+- Add the Edmodo OAuth2 backend to your settings page::
+
+    SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
+        ...
+        'social.backends.edmodo.EdmodoOAuth2',
+        ...
+    )
+
+- Fill ``App Key``, ``App Secret`` and ``App Scope`` values in the settings::
+
+      SOCIAL_AUTH_EDMODO_OAUTH2_KEY = ''
+      SOCIAL_AUTH_EDMODO_OAUTH2_SECRET = ''
+      SOCIAL_AUTH_EDMODO_SCOPE = ['basic']
+
+.. _Edmodo Connect API: https://developers.edmodo.com/edmodo-connect/edmodo-connect-overview-getting-started/
diff --git a/docs/backends/facebook.rst b/docs/backends/facebook.rst
index cf91365..894070b 100644
--- a/docs/backends/facebook.rst
+++ b/docs/backends/facebook.rst
@@ -18,7 +18,7 @@ development resources`_:
       SOCIAL_AUTH_FACEBOOK_SECRET = ''
 
 - Define ``SOCIAL_AUTH_FACEBOOK_SCOPE`` to get extra permissions
-  from facebook. Email is not sent by deafault, to get it, you must request the
+  from facebook. Email is not sent by default, to get it, you must request the
   ``email`` permission::
 
      SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
diff --git a/docs/backends/index.rst b/docs/backends/index.rst
index 7e3a235..37c29bb 100644
--- a/docs/backends/index.rst
+++ b/docs/backends/index.rst
@@ -71,6 +71,7 @@ Social backends
    dribbble
    drip
    dropbox
+   edmodo
    eveonline
    evernote
    facebook
@@ -89,6 +90,7 @@ Social backends
    khanacademy
    lastfm
    launchpad
+   line
    linkedin
    livejournal
    live
@@ -120,6 +122,7 @@ Social backends
    runkeeper
    salesforce
    shopify
+   sketchfab
    skyrock
    slack
    soundcloud
@@ -139,6 +142,8 @@ Social backends
    twitch
    twitter
    uber
+   untappd
+   upwork
    vend
    vimeo
    vk
diff --git a/docs/backends/line.rst b/docs/backends/line.rst
new file mode 100644
index 0000000..f63c611
--- /dev/null
+++ b/docs/backends/line.rst
@@ -0,0 +1,7 @@
+Line.me
+=======
+
+Fill App Id and Secret in your project settings::
+
+	SOCIAL_AUTH_LINE_KEY = '...'
+	SOCIAL_AUTH_LINE_SECRET = '...'
diff --git a/docs/backends/sketchfab.rst b/docs/backends/sketchfab.rst
new file mode 100644
index 0000000..c69a388
--- /dev/null
+++ b/docs/backends/sketchfab.rst
@@ -0,0 +1,17 @@
+Sketchfab
+=========
+
+Sketchfab uses OAuth 2 for authentication.
+
+To use:
+
+- Follow the steps at `Sketchfab Oauth`_, and ask for an
+  ``Authorization code`` grant type.
+
+- Fill the ``Client id/key`` and ``Client Secret`` values you received
+  in your django settings::
+
+      SOCIAL_AUTH_SKETCHFAB_KEY   = ''
+      SOCIAL_AUTH_SKETCHFAB_SECRET = ''
+
+.. _Sketchfab Oauth: https://sketchfab.com/developers/oauth
diff --git a/docs/backends/twitter.rst b/docs/backends/twitter.rst
index e40c413..64e885c 100644
--- a/docs/backends/twitter.rst
+++ b/docs/backends/twitter.rst
@@ -20,6 +20,10 @@ To enable Twitter these two keys are needed. Further documentation at
   Client type instead of the Browser. Almost any dummy value will work if
   you plan some test.
 
+- You can request user's Email address (consult `Twitter verify
+  credentials`_), the parameter is sent automatically, but the
+  applicaton needs to be whitelisted in order to get a valid value.
+
 Twitter usually fails with a 401 error when trying to call the request-token
 URL, this is usually caused by server datetime errors (check miscellaneous
 section). Installing ``ntp`` and syncing the server date with some pool does
@@ -27,3 +31,4 @@ the trick.
 
 .. _Twitter development resources: http://dev.twitter.com/pages/auth
 .. _Twitter App Creation: http://twitter.com/apps/new
+.. _Twitter verify credentials: https://dev.twitter.com/rest/reference/get/account/verify_credentials
diff --git a/docs/backends/untappd.rst b/docs/backends/untappd.rst
new file mode 100644
index 0000000..0b41a12
--- /dev/null
+++ b/docs/backends/untappd.rst
@@ -0,0 +1,28 @@
+Untappd
+=======
+
+Untappd uses OAuth v2 for Authentication, check the `official docs`_.
+
+- Create an app by filling out the form here: `Add App`_
+
+- Apps are approved on a one-by-one basis, so you'll need to wait a
+  few days to get your client ID and secret.
+
+- Fill ``Client ID`` and ``Client Secret`` values in the settings::
+
+        SOCIAL_AUTH_UNTAPPD_KEY = '<App UID>'
+        SOCIAL_AUTH_UNTAPPD_SECRET = '<App secret>'
+
+- Add the backend to the ``AUTHENTICATION_BACKENDS`` setting::
+
+        AUTHENTICATION_BACKENDS = (
+            ...
+            'social.backends.untappd.UntappdOAuth2',
+            ...
+        )
+
+- Then you can start using ``{% url social:begin 'untappd' %}`` in
+  your templates
+
+.. _official docs: https://untappd.com/api/docs
+.. _Add App: https://untappd.com/api/register?register=new
diff --git a/docs/backends/upwork.rst b/docs/backends/upwork.rst
new file mode 100644
index 0000000..59b5990
--- /dev/null
+++ b/docs/backends/upwork.rst
@@ -0,0 +1,28 @@
+Upwork
+======
+
+Upwork supports only OAuth 1.
+
+- Register a new application at `Upwork Developers`_.
+
+OAuth1
+------
+
+Add the Upwork OAuth backend to your settings page::
+
+    SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
+        ...
+        'social.backends.upwork.UpworkOAuth',
+        ...
+    )
+
+- Fill ``App Key`` and ``App Secret`` values in the settings::
+
+      SOCIAL_AUTH_UPWORK_KEY = ''
+      SOCIAL_AUTH_UPWORK_SECRET = ''
+
+
+**Note:** For more information please go to `Upwork API Reference`_.
+
+.. _Upwork Developers: https://www.upwork.com/services/api/apply
+.. _Upwork API Reference: https://developers.upwork.com/?lang=python
diff --git a/docs/backends/vk.rst b/docs/backends/vk.rst
index 2b5deb5..a9195c7 100644
--- a/docs/backends/vk.rst
+++ b/docs/backends/vk.rst
@@ -15,7 +15,7 @@ VK.com uses OAuth2 for Authentication.
       SOCIAL_AUTH_VK_OAUTH2_KEY = ''
       SOCIAL_AUTH_VK_OAUTH2_SECRET = ''
 
-- Add ``'social.backends.vk.VKOAuth2'`` into your ``AUTHENTICATION_BACKENDS``.
+- Add ``'social.backends.vk.VKOAuth2'`` into your ``SOCIAL_AUTH_AUTHENTICATION_BACKENDS``.
 
 - Then you can start using ``/login/vk-oauth2`` in your link href.
 
diff --git a/docs/backends/yammer.rst b/docs/backends/yammer.rst
index df6104d..86f4c74 100644
--- a/docs/backends/yammer.rst
+++ b/docs/backends/yammer.rst
@@ -10,7 +10,8 @@ Production Mode
 In order to enable the backend, follow:
 
 
-- Register an application at `Client Applications`_
+- Register an application at `Client Applications`_,
+  set the ``Redirect URI`` to ``http://<your hostname>/complete/yammer/``
 
 - Fill **Client Key** and **Client Secret** settings::
 
diff --git a/docs/intro.rst b/docs/intro.rst
index 12a45cb..af3e37c 100644
--- a/docs/intro.rst
+++ b/docs/intro.rst
@@ -83,6 +83,7 @@ or extend current one):
     * Twilio_ Auth
     * Twitch_ OAuth2
     * Twitter_ OAuth1
+    * Upwork_ OAuth1
     * Vimeo_ OAuth1
     * VK.com_ OpenAPI, OAuth2 and OAuth2 for Applications
     * Weibo_ OAuth2
@@ -179,3 +180,4 @@ section.
 .. _Webpy: https://github.com/omab/python-social-auth/tree/master/social/apps/webpy_app
 .. _Tornado: http://www.tornadoweb.org/
 .. _Authentication Pipeline: pipeline.html
+.. _Upwork: https://www.upwork.com
diff --git a/docs/pipeline.rst b/docs/pipeline.rst
index bd65660..b3efc48 100644
--- a/docs/pipeline.rst
+++ b/docs/pipeline.rst
@@ -12,7 +12,7 @@ in the parameters to avoid errors for unexpected arguments.
 
 Each pipeline entry can return a ``dict`` or ``None``, any other type of return
 value is treated as a response instance and returned directly to the client,
-check *Partial Piepeline* below for details.
+check *Partial Pipeline* below for details.
 
 If a ``dict`` is returned, the value in the set will be merged into the
 ``kwargs`` argument for the next pipeline entry, ``None`` is taken as if ``{}``
@@ -63,7 +63,7 @@ The default pipeline is composed by::
         # Create a user account if we haven't found one yet.
         'social.pipeline.user.create_user',
 
-        # Create the record that associated the social account with this user.
+        # Create the record that associates the social account with the user.
         'social.pipeline.social_auth.associate_user',
 
         # Populate the extra_data field in the social record with the values
@@ -109,8 +109,9 @@ Each pipeline function will receive the following parameters:
     * ``is_new`` flag (initialized as ``False``)
     * Any arguments passed to ``auth_complete`` backend method, default views
       pass these arguments:
-      - current logged in user (if it's logged in, otherwise ``None``)
-      - current request
+      
+      * current logged in user (if it's logged in, otherwise ``None``)
+      * current request
 
 
 Disconnection Pipeline
@@ -199,9 +200,9 @@ three fields:
 ``verified = True / False``
     Flag marking if the email was verified or not.
 
-You should use the code in this instance the build the link for email
-validation which should go to ``/complete/email?verification_code=<code here>``, if using
-Django you can do it with::
+You should use the code in this instance to build the link for email
+validation which should go to ``/complete/email?verification_code=<code here>``. If you are using
+Django, you can do it with::
 
     from django.core.urlresolvers import reverse
     url = strategy.build_absolute_uri(
@@ -226,23 +227,26 @@ Or individually by defining the setting per backend basis like
 Extending the Pipeline
 ======================
 
-The main purpose of the pipeline (either creation or deletion pipelines), is to
-allow extensibility for developers, you can jump in the middle of it, do
-changes to the data, create other models instances, ask users for data, or even
-halt the whole process.
+The main purpose of the pipeline (either creation or deletion pipelines) is to
+allow extensibility for developers. You can jump in the middle of it, do
+changes to the data, create other models instances, ask users for extra data,
+or even halt the whole process.
 
 Extending the pipeline implies:
 
     1. Writing a function
-    2. Locate it in a accessible path (accessible in the way that it can be
-       imported)
-    3. Override the default pipeline definition with one that includes your
-       function.
-
-Writing the function is quite simple. Depending on the place you locate it will
-determine the arguments it will receive, for example, adding your function
-after ``social.pipeline.user.create_user`` ensures that you get the user
-instance (created or already existent) instead of a ``None`` value.
+    2. Locating the function in an accessible path 
+       (accessible in the way that it can be imported)
+    3. Overriding the default pipeline definition with one that includes
+       newly created function.
+
+The part of writing the function is quite simple. However please be careful
+when placing your function in the pipeline definition, because order
+does matter in this case! Ordering of functions in ``SOCIAL_AUTH_PIPELINE``
+will determine the value of arguments that each function will receive. 
+For example, adding your function after ``social.pipeline.user.create_user``
+ensures that your function will get the user instance (created or already existent)
+instead of a ``None`` value.
 
 The pipeline functions will get quite a lot of arguments, ranging from the
 backend in use, different model instances, server requests and provider
@@ -285,7 +289,7 @@ other APIs endpoints to retrieve even more details about the user, store them
 on some other place, etc.
 
 Here's an example of a simple pipeline function that will create a ``Profile``
-class related to the current user, this profile will store some simple details
+class instance, related to the current user. This profile will store some simple details
 returned by the provider (``Facebook`` in this example). The usual Facebook
 ``response`` looks like this::
 
@@ -319,9 +323,9 @@ the timezone in our ``Profile`` model::
             profile.timezone = response.get('timezone')
             profile.save()
 
-Now all that's needed is to tell ``python-social-auth`` to use this function in
-the pipeline, since it needs the user instance, it needs to be put after
-``create_user`` function::
+Now all that's needed is to tell ``python-social-auth`` to use our function in
+the pipeline. Since the function uses user instance, we need to put it after
+``social.pipeline.user.create_user``::
 
     SOCIAL_AUTH_PIPELINE = (
         'social.pipeline.social_auth.social_details',
@@ -336,10 +340,9 @@ the pipeline, since it needs the user instance, it needs to be put after
         'social.pipeline.user.user_details',
     )
 
-If the return value of the function is a ``dict``, the values will be merged
-into the next pipeline function parameters, so, for instance, if you want the
-``profile`` instance to be available to the next function, all that it needs to
-do is return ``{'profile': profile}``.
+So far the function we created returns ``None``, which is taken as if ``{}`` was returned.
+If you want the ``profile`` object to be available to the next function in the
+pipeline, all you need to do is return ``{'profile': profile}``.
 
 .. _python-social-auth: https://github.com/omab/python-social-auth
 .. _example applications: https://github.com/omab/python-social-auth/tree/master/examples
diff --git a/docs/thanks.rst b/docs/thanks.rst
index ca4a40e..46d7a20 100644
--- a/docs/thanks.rst
+++ b/docs/thanks.rst
@@ -110,6 +110,7 @@ let me know and I'll update the list):
   * vbsteven_
   * sbassi_
   * aspcanada_
+  * browniebroke_
 
 
 .. _python-social-auth: https://github.com/omab/python-social-auth
@@ -215,3 +216,4 @@ let me know and I'll update the list):
 .. _vbsteven: https://github.com/vbsteven
 .. _sbassi: https://github.com/sbassi
 .. _aspcanada: https://github.com/aspcanada
+.. _browniebroke: https://github.com/browniebroke
diff --git a/docs/use_cases.rst b/docs/use_cases.rst
index 3ac5856..4f7c926 100644
--- a/docs/use_cases.rst
+++ b/docs/use_cases.rst
@@ -143,9 +143,6 @@ will be done by AJAX. It doesn't return the user information, but that's
 something that can be extended and filled to suit the project where it's going
 to be used.
 
-This topic is well addressed in `A Rest API using Django and authentication
-with OAuth2 AND third parties!`_ wrote by `Félix Descôteaux`_.
-
 
 Multiple scopes per provider
 ----------------------------
@@ -313,5 +310,3 @@ Set this pipeline after ``social_user``::
 
 .. _python-social-auth: https://github.com/omab/python-social-auth
 .. _People API endpoint: https://developers.google.com/+/api/latest/people/list
-.. _Félix Descôteaux: https://twitter.com/FelixDescoteaux
-.. _A Rest API using Django and authentication with OAuth2 AND third parties!: http://httplambda.com/a-rest-api-with-django-and-oauthw-authentication/
diff --git a/examples/cherrypy_example/local_settings.py.template b/examples/cherrypy_example/local_settings.py.template
index 98c8f8f..7209aca 100644
--- a/examples/cherrypy_example/local_settings.py.template
+++ b/examples/cherrypy_example/local_settings.py.template
@@ -39,6 +39,7 @@ SOCIAL_SETTINGS = {
         'social.backends.podio.PodioOAuth2',
         'social.backends.reddit.RedditOAuth2',
         'social.backends.wunderlist.WunderlistOAuth2',
+        'social.backends.upwork.UpworkOAuth',
     ),
     'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': '',
     'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': ''
diff --git a/examples/django_example/example/settings.py b/examples/django_example/example/settings.py
index 62f2b5b..9077b5a 100644
--- a/examples/django_example/example/settings.py
+++ b/examples/django_example/example/settings.py
@@ -174,6 +174,7 @@ AUTHENTICATION_BACKENDS = (
     'social.backends.readability.ReadabilityOAuth',
     'social.backends.reddit.RedditOAuth2',
     'social.backends.runkeeper.RunKeeperOAuth2',
+    'social.backends.sketchfab.SketchfabOAuth2',
     'social.backends.skyrock.SkyrockOAuth',
     'social.backends.soundcloud.SoundcloudOAuth2',
     'social.backends.spotify.SpotifyOAuth2',
@@ -203,6 +204,7 @@ AUTHENTICATION_BACKENDS = (
     'social.backends.email.EmailAuth',
     'social.backends.username.UsernameAuth',
     'django.contrib.auth.backends.ModelBackend',
+    'social.backends.upwork.UpworkOAuth',
 )
 
 AUTH_USER_MODEL = 'app.CustomUser'
diff --git a/examples/django_me_example/example/settings.py b/examples/django_me_example/example/settings.py
index 7dcdfad..88783b3 100644
--- a/examples/django_me_example/example/settings.py
+++ b/examples/django_me_example/example/settings.py
@@ -173,6 +173,7 @@ AUTHENTICATION_BACKENDS = (
     'social.backends.yammer.YammerOAuth2',
     'social.backends.stackoverflow.StackoverflowOAuth2',
     'social.backends.readability.ReadabilityOAuth',
+    'social.backends.sketchfab.SketchfabOAuth2',
     'social.backends.skyrock.SkyrockOAuth',
     'social.backends.tumblr.TumblrOAuth',
     'social.backends.reddit.RedditOAuth2',
@@ -182,6 +183,7 @@ AUTHENTICATION_BACKENDS = (
     'social.backends.email.EmailAuth',
     'social.backends.username.UsernameAuth',
     'social.backends.wunderlist.WunderlistOAuth2',
+    'social.backends.upwork.UpworkOAuth',
     'mongoengine.django.auth.MongoEngineBackend',
     'django.contrib.auth.backends.ModelBackend',
 )
diff --git a/examples/flask_example/__init__.py b/examples/flask_example/__init__.py
old mode 100644
new mode 100755
index d24fb82..07c1080
--- a/examples/flask_example/__init__.py
+++ b/examples/flask_example/__init__.py
@@ -4,7 +4,7 @@ from sqlalchemy import create_engine
 from sqlalchemy.orm import scoped_session, sessionmaker
 
 from flask import Flask, g
-from flask.ext import login
+from flask_login import LoginManager, current_user
 
 sys.path.append('../..')
 
@@ -29,7 +29,7 @@ db_session = scoped_session(Session)
 app.register_blueprint(social_auth)
 init_social(app, db_session)
 
-login_manager = login.LoginManager()
+login_manager = LoginManager()
 login_manager.login_view = 'main'
 login_manager.login_message = ''
 login_manager.init_app(app)
@@ -48,7 +48,8 @@ def load_user(userid):
 
 @app.before_request
 def global_user():
-    g.user = login.current_user
+    # evaluate proxy value
+    g.user = current_user._get_current_object()
 
 
 @app.teardown_appcontext
diff --git a/examples/flask_example/models/user.py b/examples/flask_example/models/user.py
index 7cb7058..08bcee8 100644
--- a/examples/flask_example/models/user.py
+++ b/examples/flask_example/models/user.py
@@ -1,7 +1,7 @@
 from sqlalchemy import Column, String, Integer, Boolean
 from sqlalchemy.ext.declarative import declarative_base
 
-from flask.ext.login import UserMixin
+from flask_login import UserMixin
 
 from flask_example import db_session
 
diff --git a/examples/flask_example/routes/main.py b/examples/flask_example/routes/main.py
index 5e0dd9c..fd65261 100644
--- a/examples/flask_example/routes/main.py
+++ b/examples/flask_example/routes/main.py
@@ -1,5 +1,5 @@
 from flask import render_template, redirect
-from flask.ext.login import login_required, logout_user
+from flask_login import login_required, logout_user
 
 from flask_example import app
 
diff --git a/examples/flask_example/settings.py b/examples/flask_example/settings.py
index 0abaa91..6b5324c 100644
--- a/examples/flask_example/settings.py
+++ b/examples/flask_example/settings.py
@@ -52,4 +52,5 @@ SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
     'social.backends.reddit.RedditOAuth2',
     'social.backends.mineid.MineIDOAuth2',
     'social.backends.wunderlist.WunderlistOAuth2',
+    'social.backends.upwork.UpworkOAuth',
 )
diff --git a/examples/flask_me_example/__init__.py b/examples/flask_me_example/__init__.py
index f838a20..9f5aaac 100644
--- a/examples/flask_me_example/__init__.py
+++ b/examples/flask_me_example/__init__.py
@@ -1,7 +1,7 @@
 import sys
 
 from flask import Flask, g
-from flask.ext import login
+from flask_login import LoginManager, current_user
 from flask.ext.mongoengine import MongoEngine
 
 sys.path.append('../..')
@@ -26,7 +26,7 @@ db = MongoEngine(app)
 app.register_blueprint(social_auth)
 init_social(app, db)
 
-login_manager = login.LoginManager()
+login_manager = LoginManager()
 login_manager.login_view = 'main'
 login_manager.login_message = ''
 login_manager.init_app(app)
@@ -45,7 +45,7 @@ def load_user(userid):
 
 @app.before_request
 def global_user():
-    g.user = login.current_user
+    g.user = current_user
 
 
 @app.context_processor
diff --git a/examples/flask_me_example/models/user.py b/examples/flask_me_example/models/user.py
index 035b527..d351fed 100644
--- a/examples/flask_me_example/models/user.py
+++ b/examples/flask_me_example/models/user.py
@@ -1,6 +1,6 @@
 from mongoengine import StringField, EmailField, BooleanField
 
-from flask.ext.login import UserMixin
+from flask_login import UserMixin
 
 from flask_me_example import db
 
diff --git a/examples/flask_me_example/routes/main.py b/examples/flask_me_example/routes/main.py
index 127986e..d2cfd82 100644
--- a/examples/flask_me_example/routes/main.py
+++ b/examples/flask_me_example/routes/main.py
@@ -1,5 +1,5 @@
 from flask import render_template, redirect
-from flask.ext.login import login_required, logout_user
+from flask_login import login_required, logout_user
 
 from flask_me_example import app
 
diff --git a/examples/flask_me_example/settings.py b/examples/flask_me_example/settings.py
index e4f2338..cf4ed21 100644
--- a/examples/flask_me_example/settings.py
+++ b/examples/flask_me_example/settings.py
@@ -58,4 +58,5 @@ SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
     'social.backends.reddit.RedditOAuth2',
     'social.backends.mineid.MineIDOAuth2',
     'social.backends.wunderlist.WunderlistOAuth2',
+    'social.backends.upwork.UpworkOAuth',
 )
diff --git a/examples/flask_example/__init__.py b/examples/flask_peewee_example/__init__.py
similarity index 58%
copy from examples/flask_example/__init__.py
copy to examples/flask_peewee_example/__init__.py
index d24fb82..4c2543f 100644
--- a/examples/flask_example/__init__.py
+++ b/examples/flask_peewee_example/__init__.py
@@ -1,8 +1,5 @@
 import sys
 
-from sqlalchemy import create_engine
-from sqlalchemy.orm import scoped_session, sessionmaker
-
 from flask import Flask, g
 from flask.ext import login
 
@@ -10,7 +7,8 @@ sys.path.append('../..')
 
 from social.apps.flask_app.routes import social_auth
 from social.apps.flask_app.template_filters import backends
-from social.apps.flask_app.default.models import init_social
+from social.apps.flask_app.peewee.models import *
+from peewee import *
 
 # App
 app = Flask(__name__)
@@ -21,13 +19,14 @@ try:
 except ImportError:
     pass
 
+from models.user import database_proxy, User
+
 # DB
-engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI'])
-Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
-db_session = scoped_session(Session)
+database = SqliteDatabase('test.db')
+database_proxy.initialize(database)
 
 app.register_blueprint(social_auth)
-init_social(app, db_session)
+init_social(app, database)
 
 login_manager = login.LoginManager()
 login_manager.login_view = 'main'
@@ -41,24 +40,15 @@ from flask_example import routes
 @login_manager.user_loader
 def load_user(userid):
     try:
-        return models.user.User.query.get(int(userid))
-    except (TypeError, ValueError):
+        us = User.get(User.id == userid)
+        return us
+    except User.DoesNotExist:
         pass
 
 
 @app.before_request
 def global_user():
-    g.user = login.current_user
-
-
- at app.teardown_appcontext
-def commit_on_success(error=None):
-    if error is None:
-        db_session.commit()
-    else:
-        db_session.rollback()
-
-    db_session.remove()
+    g.user = login.current_user._get_current_object()
 
 
 @app.context_processor
diff --git a/examples/flask_peewee_example/manage.py b/examples/flask_peewee_example/manage.py
new file mode 100644
index 0000000..9270586
--- /dev/null
+++ b/examples/flask_peewee_example/manage.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+import sys
+
+from flask.ext.script import Server, Manager, Shell
+
+sys.path.append('..')
+
+from flask_example import app, database
+
+
+manager = Manager(app)
+manager.add_command('runserver', Server())
+manager.add_command('shell', Shell(make_context=lambda: {
+    'app': app
+}))
+
+
+ at manager.command
+def syncdb():
+    from flask_example.models.user import User
+    from social.apps.flask_app.peewee.models import FlaskStorage
+
+    database.create_tables([User, FlaskStorage.user, FlaskStorage.nonce, FlaskStorage.association, FlaskStorage.code])
+
+if __name__ == '__main__':
+    manager.run()
diff --git a/examples/flask_peewee_example/models/__init__.py b/examples/flask_peewee_example/models/__init__.py
new file mode 100644
index 0000000..2253824
--- /dev/null
+++ b/examples/flask_peewee_example/models/__init__.py
@@ -0,0 +1,4 @@
+from flask_example.models import user
+from social.apps.flask_app.peewee import models
+# create a peewee database instance -- our models will use this database to
+# persist information
diff --git a/examples/flask_peewee_example/models/user.py b/examples/flask_peewee_example/models/user.py
new file mode 100644
index 0000000..a46f559
--- /dev/null
+++ b/examples/flask_peewee_example/models/user.py
@@ -0,0 +1,24 @@
+from peewee import *
+from datetime import datetime
+from flask.ext.login import UserMixin
+
+database_proxy = Proxy()
+
+
+# model definitions -- the standard "pattern" is to define a base model class
+# that specifies which database to use.  then, any subclasses will automatically
+# use the correct storage.
+class BaseModel(Model):
+    class Meta:
+        database = database_proxy
+
+# the user model specifies its fields (or columns) declaratively, like django
+class User(BaseModel, UserMixin):
+    username = CharField(unique=True)
+    password = CharField(null=True)
+    email = CharField(null=True)
+    active = BooleanField(default=True)
+    join_date = DateTimeField(default=datetime.now)
+
+    class Meta:
+        order_by = ('username',)
diff --git a/examples/flask_peewee_example/requirements.txt b/examples/flask_peewee_example/requirements.txt
new file mode 100644
index 0000000..e52656b
--- /dev/null
+++ b/examples/flask_peewee_example/requirements.txt
@@ -0,0 +1,7 @@
+Peewee
+Flask
+Flask-Login
+Flask-Script
+Werkzeug
+pysqlite
+Jinja2
diff --git a/examples/flask_peewee_example/routes/__init__.py b/examples/flask_peewee_example/routes/__init__.py
new file mode 100644
index 0000000..d3586e8
--- /dev/null
+++ b/examples/flask_peewee_example/routes/__init__.py
@@ -0,0 +1,2 @@
+from flask_example.routes import main
+from social.apps.flask_app import routes
diff --git a/examples/flask_example/routes/main.py b/examples/flask_peewee_example/routes/main.py
similarity index 100%
copy from examples/flask_example/routes/main.py
copy to examples/flask_peewee_example/routes/main.py
diff --git a/examples/flask_example/settings.py b/examples/flask_peewee_example/settings.py
similarity index 96%
copy from examples/flask_example/settings.py
copy to examples/flask_peewee_example/settings.py
index 0abaa91..419c575 100644
--- a/examples/flask_example/settings.py
+++ b/examples/flask_peewee_example/settings.py
@@ -3,10 +3,10 @@ from os.path import dirname, abspath
 SECRET_KEY = 'random-secret-key'
 SESSION_COOKIE_NAME = 'psa_session'
 DEBUG = True
-SQLALCHEMY_DATABASE_URI = 'sqlite:////%s/test.db' % dirname(abspath(__file__))
 DEBUG_TB_INTERCEPT_REDIRECTS = False
 SESSION_PROTECTION = 'strong'
 
+SOCIAL_AUTH_STORAGE = 'social.apps.flask_app.peewee.models.FlaskStorage'
 SOCIAL_AUTH_LOGIN_URL = '/'
 SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/done/'
 SOCIAL_AUTH_USER_MODEL = 'flask_example.models.user.User'
diff --git a/examples/flask_peewee_example/templates/base.html b/examples/flask_peewee_example/templates/base.html
new file mode 100644
index 0000000..86db504
--- /dev/null
+++ b/examples/flask_peewee_example/templates/base.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Social</title>
+    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet" media="screen">
+  </head>
+  <body>
+    {% block content %}{% endblock %}
+    {% block scripts %}{% endblock %}
+
+    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
+    <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js" type="text/javascript"></script>
+  </body>
+</html>
diff --git a/examples/flask_peewee_example/templates/done.html b/examples/flask_peewee_example/templates/done.html
new file mode 100644
index 0000000..ccabf53
--- /dev/null
+++ b/examples/flask_peewee_example/templates/done.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+
+{% block content %}
+<p>You are logged in as {{ user.username }}!</p>
+
+<p>Associated:</p>
+{% for assoc in backends.associated %}
+  <div>
+    {{ assoc.provider }}
+    <form method="post" action="{{ url_for("social.disconnect", backend=assoc.provider, association_id=assoc.id) }}">
+      <button>Disconnect</button>
+    </form>
+  </div>
+{% endfor %}
+
+<p>Associate:</p>
+<ul>
+  {% for name in backends.not_associated %}
+    <li>
+      <a href="{{ url_for("social.auth", backend=name) }}">{{ name }}</a>
+    </li>
+  {% endfor %}
+</ul>
+{% endblock %}
diff --git a/examples/flask_peewee_example/templates/home.html b/examples/flask_peewee_example/templates/home.html
new file mode 100644
index 0000000..1c7f9bc
--- /dev/null
+++ b/examples/flask_peewee_example/templates/home.html
@@ -0,0 +1,85 @@
+{% extends "base.html" %}
+
+{% block content %}
+<a href="{{ url_for("social.auth", backend="google-oauth2") }}">Google OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="google-oauth") }}">Google OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="google") }}">Google OpenId</a> <br />
+<a href="{{ url_for("social.auth", backend="twitter") }}">Twitter OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="yahoo") }}">Yahoo OpenId</a> <br />
+<a href="{{ url_for("social.auth", backend="yahoo-oauth") }}">Yahoo OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="stripe") }}">Stripe OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="facebook") }}">Facebook OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="facebook-app") }}">Facebook App</a> <br />
+<a href="{{ url_for("social.auth", backend="angel") }}">Angel OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="behance") }}">Behance OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="bitbucket") }}">Bitbucket OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="box") }}">Box OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="linkedin") }}">LinkedIn OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="github") }}">Github OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="foursquare") }}">Foursquare OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="instagram") }}">Instagram OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="live") }}">Live OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="vk-oauth2") }}">VK.com OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="dailymotion") }}">Dailymotion OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="disqus") }}">Disqus OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="dropbox") }}">Dropbox OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="evernote-sandbox") }}">Evernote OAuth (sandbox mode)</a> <br />
+<a href="{{ url_for("social.auth", backend="fitbit") }}">Fitbit OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="flickr") }}">Flickr OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="soundcloud") }}">Soundcloud OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="thisismyjam") }}">ThisIsMyJam OAuth1</a> <br />
+<a href="{{ url_for("social.auth", backend="stocktwits") }}">Stocktwits OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="tripit") }}">Tripit OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="clef") }}">Clef OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="twilio") }}">Twilio</a> <br />
+<a href="{{ url_for("social.auth", backend="xing") }}">Xing OAuth</a> <br />
+<a href="{{ url_for("social.auth", backend="yandex-oauth2") }}">Yandex OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="podio") }}">Podio OAuth2</a> <br />
+<a href="{{ url_for("social.auth", backend="mineid") }}">MineID OAuth2</a> <br />
+
+<form action="{{ url_for("social.auth", backend="openid") }}" method="post">
+  <div>
+    <label for="openid_identifier">OpenId provider</label>
+    <input id="openid_identifier" type="text" value="" name="openid_identifier" />
+    <input type="submit" value="Login" />
+  </div>
+</form>
+
+<form action="{{ url_for("social.auth", backend="livejournal") }}" method="post">
+  <div>
+    <label for="openid_lj_identifier">LiveJournal ID</label>
+    <input id="openid_lj_identifier" type="text" value="" name="openid_lj_user" />
+    <input type="submit" value="Login" />
+  </div>
+</form>
+
+<form method="post" action="{{ url_for("social.complete", backend="persona") }}">
+  <input type="hidden" name="assertion" value="" />
+  <a rel="nofollow" id="persona" href="#">Persona</a>
+</form>
+{% endblock %}
+
+{% block scripts %}
+<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js" type="text/javascript"></script>
+<script src="https://login.persona.org/include.js" type="text/javascript"></script>
+<script type="text/javascript">
+$(function () {
+    $('#persona').on('click', function (e) {
+        e.preventDefault();
+        var self = $(this);
+
+        navigator.id.get(function (assertion) {
+            if (assertion) {
+                self.parent('form')
+                        .find('input[type=hidden]')
+                        .attr('value', assertion)
+                        .end()
+                    .submit();
+            } else {
+                alert('Some error occurred');
+            }
+        });
+    });
+});
+</script>
+{% endblock %}
diff --git a/examples/pyramid_example/example/settings.py b/examples/pyramid_example/example/settings.py
index 35a69ad..05e2d9e 100644
--- a/examples/pyramid_example/example/settings.py
+++ b/examples/pyramid_example/example/settings.py
@@ -46,6 +46,7 @@ SOCIAL_AUTH_SETTINGS = {
         'social.backends.reddit.RedditOAuth2',
         'social.backends.mineid.MineIDOAuth2',
         'social.backends.wunderlist.WunderlistOAuth2',
+        'social.backends.upwork.UpworkOAuth',
     )
 }
 
diff --git a/examples/tornado_example/settings.py b/examples/tornado_example/settings.py
index ff6b146..81ae2eb 100644
--- a/examples/tornado_example/settings.py
+++ b/examples/tornado_example/settings.py
@@ -45,6 +45,7 @@ SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
     'social.backends.reddit.RedditOAuth2',
     'social.backends.mineid.MineIDOAuth2',
     'social.backends.wunderlist.WunderlistOAuth2',
+    'social.backends.upwork.UpworkOAuth',
 )
 
 from local_settings import *
diff --git a/examples/webpy_example/app.py b/examples/webpy_example/app.py
index 4109fac..2224d93 100644
--- a/examples/webpy_example/app.py
+++ b/examples/webpy_example/app.py
@@ -57,6 +57,7 @@ web.config[setting_name('AUTHENTICATION_BACKENDS')] = (
     'social.backends.podio.PodioOAuth2',
     'social.backends.mineid.MineIDOAuth2',
     'social.backends.wunderlist.WunderlistOAuth2',
+    'social.backends.upwork.UpworkOAuth',
 )
 web.config[setting_name('LOGIN_REDIRECT_URL')] = '/done/'
 
diff --git a/site/css/site.css b/site/css/site.css
new file mode 100644
index 0000000..0292b3b
--- /dev/null
+++ b/site/css/site.css
@@ -0,0 +1,4 @@
+body {
+  padding-top: 60px;
+  padding-bottom: 40px;
+}
diff --git a/site/docs b/site/docs
new file mode 120000
index 0000000..16bfa41
--- /dev/null
+++ b/site/docs
@@ -0,0 +1 @@
+../docs/_build/
\ No newline at end of file
diff --git a/site/img/glyphicons-halflings-white.png b/site/img/glyphicons-halflings-white.png
new file mode 100644
index 0000000..3bf6484
Binary files /dev/null and b/site/img/glyphicons-halflings-white.png differ
diff --git a/site/img/glyphicons-halflings.png b/site/img/glyphicons-halflings.png
new file mode 100644
index 0000000..a996999
Binary files /dev/null and b/site/img/glyphicons-halflings.png differ
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..230537b
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Python Social Auth</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+    <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
+    <link href="css/bootstrap-responsive.min.css" rel="stylesheet" media="screen">
+    <link href="css/site.css" rel="stylesheet" media="screen">
+  </head>
+  <body>
+    <div class="navbar navbar-inverse navbar-fixed-top">
+      <div class="navbar-inner">
+        <div class="container">
+          <a class="brand" href="#">Python Social Auth</a>
+
+          <div class="nav-collapse collapse">
+            <ul class="nav">
+              <li class="active"><a href="#">Home</a></li>
+              <li><a href="docs/index.html">Documentation</a></li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="container">
+      <div class="hero-unit">
+        <h1>Python Social Auth</h1>
+        <p>
+          Python Social Auth is an easy to setup social authentication/registration
+          mechanism with support for several frameworks and auth providers.
+        </p>
+
+        <p>
+          Crafted using base code from django-social-auth, implements a common interface
+          to define new authentication providers from third parties. And to bring support
+          for more frameworks and ORMs.
+        </p>
+        <p><a href="docs/index.html" class="btn btn-primary btn-large">Learn more »</a></p>
+      </div>
+
+      <div class="row">
+        <div class="span6">
+          <h2>Frameworks</h2>
+          <p>
+            The lib supports a few frameworks at the moment with <a href="http://djangoproject.com/">Django</a>,
+            <a href="http://flask.pocoo.org/">Flask</a>, <a href="http://www.pylonsproject.org/projects/pyramid/about">Pyramid</a>,
+            <a href="http://webpy.org/">Webpy</a>, <a href="http://www.cherrypy.org/">CherryPy</a> and
+            <a href="http://www.tornadoweb.org/">Tornado</a> and more to come. The <a href="docs/strategies.html">frameworks API</a>
+            should ease the implementation to increase the number of frameworks supported.
+          </p>
+          <p><a class="btn" href="docs/strategies.html">View details »</a></p>
+        </div>
+
+        <div class="span6">
+          <h2>Authentication Providers</h2>
+          <p>
+            Ported from <a href="https://github.com/omab/django-social-auth">django-social-auth</a>, the application
+            brings plenty of authentication providers, many from popular services like <a href="docs/backends/google.html">Google</a>,
+            <a href="docs/backends/facebook.html">Facebook</a>, <a href="docs/backends/twitter.html">Twitter</a> and
+            <a href="docs/backends/github.html">Github</a>. The <a href="docs/backends/implementation.html">backends API</a>
+            have some implementation details on how to implement your own backends.
+          </p>
+          <p><a class="btn" href="docs/backends/index.html">View details »</a></p>
+        </div>
+      </div>
+
+      <div class="row">
+        <div class="span6">
+          <h2>ORMs</h2>
+          <p>
+            There are <a href="http://wiki.python.org/moin/HigherLevelDatabaseProgramming">multiple ORM python libraries</a> around,
+            some frameworks has their own built-in version too. <a href="https://github.com/omab/python-social-auth">python-social-auth</a>
+            tries to support the different interfaces available, at the moment <a href="http://www.sqlalchemy.org/">SQLAlchemy</a>,
+            <a href="https://docs.djangoproject.com/en/dev/topics/db/">Django ORM</a> and <a href="http://mongoengine.org/">Mongoengine</a>
+            are supported, but with the <a href="docs/storage.html">Storage API</a> it should be easy to add more support.
+          </p>
+          <p><a class="btn" href="docs/storage.html">View details »</a></p>
+        </div>
+
+        <div class="span6">
+          <h2>Development and Contact</h2>
+          <p>
+            The code is available on <a href="https://github.com/omab/python-social-auth">Github</a>, report any
+            <a href="https://github.com/omab/python-social-auth/issues">issue</a> if you find any. Pull requests are
+            always welcome. There's a <a href="https://groups.google.com/forum/?fromgroups#!forum/python-social-auth">mailing list</a>
+            and IRC channel <code>#python-social-auth</code> on Freenode network.
+          </p>
+          <p><a class="btn" href="https://github.com/omab/python-social-auth">View details »</a></p>
+        </div>
+      </div>
+
+      <hr />
+
+      <footer>
+        <p>© Matías Aguirre 2012</p>
+      </footer>
+    </div>
+
+    <script src="http://code.jquery.com/jquery.js"></script>
+    <script src="js/bootstrap.min.js"></script>
+  </body>
+</html>
diff --git a/social/__init__.py b/social/__init__.py
index 895ff9a..8766fd5 100644
--- a/social/__init__.py
+++ b/social/__init__.py
@@ -2,6 +2,6 @@
 python-social-auth application, allows OpenId or OAuth user
 registration/authentication just adding a few configurations.
 """
-version = (0, 2, 19)
+version = (0, 2, 21)
 extra = ''
 __version__ = '.'.join(map(str, version)) + extra
diff --git a/social/actions.py b/social/actions.py
index 4b005cf..af61bd7 100644
--- a/social/actions.py
+++ b/social/actions.py
@@ -19,8 +19,9 @@ def do_auth(backend, redirect_name='next'):
         # Check and sanitize a user-defined GET/POST next field value
         redirect_uri = data[redirect_name]
         if backend.setting('SANITIZE_REDIRECTS', True):
-            redirect_uri = sanitize_redirect(backend.strategy.request_host(),
-                                             redirect_uri)
+            allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \
+                            [backend.strategy.request_host()]
+            redirect_uri = sanitize_redirect(allowed_hosts, redirect_uri)
         backend.strategy.session_set(
             redirect_name,
             redirect_uri or backend.setting('LOGIN_REDIRECT_URL')
@@ -91,7 +92,9 @@ def do_complete(backend, login, user=None, redirect_name='next',
                '{0}={1}'.format(redirect_name, redirect_value)
 
     if backend.setting('SANITIZE_REDIRECTS', True):
-        url = sanitize_redirect(backend.strategy.request_host(), url) or \
+        allowed_hosts = backend.setting('ALLOWED_REDIRECT_HOSTS', []) + \
+                        [backend.strategy.request_host()]
+        url = sanitize_redirect(allowed_hosts, url) or \
               backend.setting('LOGIN_REDIRECT_URL')
     return backend.strategy.redirect(url)
 
@@ -110,8 +113,10 @@ def do_disconnect(backend, user, association_id=None, redirect_name='next',
 
     if isinstance(response, dict):
         response = backend.strategy.redirect(
-            backend.strategy.request_data().get(redirect_name, '') or
-            backend.setting('DISCONNECT_REDIRECT_URL') or
-            backend.setting('LOGIN_REDIRECT_URL')
+            backend.strategy.absolute_uri(
+                backend.strategy.request_data().get(redirect_name, '') or
+                backend.setting('DISCONNECT_REDIRECT_URL') or
+                backend.setting('LOGIN_REDIRECT_URL')
+            )
         )
     return response
diff --git a/social/apps/django_app/default/admin.py b/social/apps/django_app/default/admin.py
index de7802b..5cee753 100644
--- a/social/apps/django_app/default/admin.py
+++ b/social/apps/django_app/default/admin.py
@@ -1,4 +1,6 @@
 """Admin settings"""
+from itertools import chain
+
 from django.conf import settings
 from django.contrib import admin
 
@@ -24,11 +26,23 @@ class UserSocialAuthOption(admin.ModelAdmin):
                        hasattr(_User, 'username') and 'username' or \
                        None
             fieldnames = ('first_name', 'last_name', 'email', username)
-            all_names = _User._meta.get_all_field_names()
+            all_names = self._get_all_field_names(_User._meta)
             search_fields = [name for name in fieldnames
                                 if name and name in all_names]
         return ['user__' + name for name in search_fields]
 
+    @staticmethod
+    def _get_all_field_names(model):
+        names = chain.from_iterable(
+            (field.name, field.attname)
+                if hasattr(field, 'attname') else (field.name,)
+            for field in model.get_fields()
+            # For complete backwards compatibility, you may want to exclude
+            # GenericForeignKey from the results.
+            if not (field.many_to_one and field.related_model is None)
+        )
+        return list(set(names))
+
 
 class NonceOption(admin.ModelAdmin):
     """Nonce options"""
diff --git a/social/apps/django_app/default/config.py b/social/apps/django_app/default/config.py
index 745e24d..a2c44b7 100644
--- a/social/apps/django_app/default/config.py
+++ b/social/apps/django_app/default/config.py
@@ -3,6 +3,7 @@ from django.apps import AppConfig
 
 class PythonSocialAuthConfig(AppConfig):
     name = 'social.apps.django_app.default'
+    label = 'social_auth'
     verbose_name = 'Python Social Auth'
 
     def ready(self):
diff --git a/social/apps/django_app/default/fields.py b/social/apps/django_app/default/fields.py
index 8593386..ab47fba 100644
--- a/social/apps/django_app/default/fields.py
+++ b/social/apps/django_app/default/fields.py
@@ -1,5 +1,6 @@
 import json
 import six
+import functools
 
 from django.core.exceptions import ValidationError
 from django.db import models
@@ -10,8 +11,14 @@ try:
 except ImportError:
     from django.utils.encoding import smart_text
 
+try:
+    from django.db.models import SubfieldBase
+    field_class = functools.partial(six.with_metaclass, SubfieldBase)
+except ImportError:
+    field_class = functools.partial(six.with_metaclass, type)
+
 
-class JSONField(models.TextField):
+class JSONField(field_class(models.TextField)):
     """Simple JSON field that stores python structures as JSON strings
     on database.
     """
diff --git a/social/apps/django_app/default/migrations/0001_initial.py b/social/apps/django_app/default/migrations/0001_initial.py
index 9917f67..6766d92 100644
--- a/social/apps/django_app/default/migrations/0001_initial.py
+++ b/social/apps/django_app/default/migrations/0001_initial.py
@@ -23,6 +23,8 @@ ASSOCIATION_HANDLE_LENGTH = getattr(
 
 
 class Migration(migrations.Migration):
+    replaces = [('default', '0001_initial')]
+
     dependencies = [
         migrations.swappable_dependency(USER_MODEL),
     ]
diff --git a/social/apps/django_app/default/migrations/0002_add_related_name.py b/social/apps/django_app/default/migrations/0002_add_related_name.py
index 8e39f15..8a791ea 100644
--- a/social/apps/django_app/default/migrations/0002_add_related_name.py
+++ b/social/apps/django_app/default/migrations/0002_add_related_name.py
@@ -4,17 +4,24 @@ from __future__ import unicode_literals
 from django.db import models, migrations
 from django.conf import settings
 
+from social.utils import setting_name
+
+USER_MODEL = getattr(settings, setting_name('USER_MODEL'), None) or \
+             getattr(settings, 'AUTH_USER_MODEL', None) or \
+             'auth.User'
+
 
 class Migration(migrations.Migration):
+    replaces = [('default', '0002_add_related_name')]
 
     dependencies = [
-        ('default', '0001_initial'),
+        ('social_auth', '0001_initial'),
     ]
 
     operations = [
         migrations.AlterField(
             model_name='usersocialauth',
             name='user',
-            field=models.ForeignKey(related_name='social_auth', to=settings.AUTH_USER_MODEL)
+            field=models.ForeignKey(related_name='social_auth', to=USER_MODEL)
         ),
     ]
diff --git a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py
index 882c311..3557d70 100644
--- a/social/apps/django_app/default/migrations/0003_alter_email_max_length.py
+++ b/social/apps/django_app/default/migrations/0003_alter_email_max_length.py
@@ -10,8 +10,10 @@ EMAIL_LENGTH = getattr(settings, setting_name('EMAIL_LENGTH'), 254)
 
 
 class Migration(migrations.Migration):
+    replaces = [('default', '0003_alter_email_max_length')]
+
     dependencies = [
-        ('default', '0002_add_related_name'),
+        ('social_auth', '0002_add_related_name'),
     ]
 
     operations = [
diff --git a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py
index 668bf0e..82648bc 100644
--- a/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py
+++ b/social/apps/django_app/default/migrations/0004_auto_20160423_0400.py
@@ -6,9 +6,10 @@ import social.apps.django_app.default.fields
 
 
 class Migration(migrations.Migration):
+    replaces = [('default', '0004_auto_20160423_0400')]
 
     dependencies = [
-        ('default', '0003_alter_email_max_length'),
+        ('social_auth', '0003_alter_email_max_length'),
     ]
 
     operations = [
diff --git a/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py b/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py
new file mode 100644
index 0000000..3df56ef
--- /dev/null
+++ b/social/apps/django_app/default/migrations/0005_auto_20160727_2333.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2016-07-28 02:33
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('social_auth', '0004_auto_20160423_0400'),
+    ]
+
+    operations = [
+        migrations.AlterUniqueTogether(
+            name='association',
+            unique_together=set([('server_url', 'handle')]),
+        ),
+    ]
diff --git a/social/apps/django_app/default/models.py b/social/apps/django_app/default/models.py
index 9f18529..047da91 100644
--- a/social/apps/django_app/default/models.py
+++ b/social/apps/django_app/default/models.py
@@ -96,6 +96,9 @@ class Association(models.Model, DjangoAssociationMixin):
 
     class Meta:
         db_table = 'social_auth_association'
+        unique_together = (
+            ('server_url', 'handle',)
+        )
 
 
 class Code(models.Model, DjangoCodeMixin):
diff --git a/social/apps/django_app/tests.py b/social/apps/django_app/tests.py
index 022d7fc..1dec95e 100644
--- a/social/apps/django_app/tests.py
+++ b/social/apps/django_app/tests.py
@@ -31,6 +31,7 @@ from social.tests.backends.test_mixcloud import *
 from social.tests.backends.test_podio import *
 from social.tests.backends.test_readability import *
 from social.tests.backends.test_reddit import *
+from social.tests.backends.test_sketchfab import *
 from social.tests.backends.test_skyrock import *
 from social.tests.backends.test_soundcloud import *
 from social.tests.backends.test_stackoverflow import *
diff --git a/social/apps/flask_app/peewee/__init__.py b/social/apps/flask_app/peewee/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/social/apps/flask_app/peewee/models.py b/social/apps/flask_app/peewee/models.py
new file mode 100644
index 0000000..497e9a5
--- /dev/null
+++ b/social/apps/flask_app/peewee/models.py
@@ -0,0 +1,48 @@
+"""Flask Peewee ORM models for Social Auth"""
+from peewee import Model, ForeignKeyField, Proxy
+
+from social.utils import setting_name, module_member
+from social.storage.peewee_orm import PeeweeUserMixin, \
+                                      PeeweeAssociationMixin, \
+                                      PeeweeNonceMixin, \
+                                      PeeweeCodeMixin, \
+                                      BasePeeweeStorage, \
+                                      database_proxy
+
+
+class FlaskStorage(BasePeeweeStorage):
+    user = None
+    nonce = None
+    association = None
+    code = None
+
+
+def init_social(app, db):
+    User = module_member(app.config[setting_name('USER_MODEL')])
+
+    database_proxy.initialize(db)
+
+    class UserSocialAuth(PeeweeUserMixin):
+        """Social Auth association model"""
+        user = ForeignKeyField(User, related_name='social_auth')
+
+        @classmethod
+        def user_model(cls):
+            return User
+
+    class Nonce(PeeweeNonceMixin):
+        """One use numbers"""
+        pass
+
+    class Association(PeeweeAssociationMixin):
+        """OpenId account association"""
+        pass
+
+    class Code(PeeweeCodeMixin):
+        pass
+
+    # Set the references in the storage class
+    FlaskStorage.user = UserSocialAuth
+    FlaskStorage.nonce = Nonce
+    FlaskStorage.association = Association
+    FlaskStorage.code = Code
diff --git a/social/apps/flask_app/routes.py b/social/apps/flask_app/routes.py
index 6c1eec7..b3b1406 100644
--- a/social/apps/flask_app/routes.py
+++ b/social/apps/flask_app/routes.py
@@ -1,5 +1,5 @@
 from flask import g, Blueprint, request
-from flask.ext.login import login_required, login_user
+from flask_login import login_required, login_user
 
 from social.actions import do_auth, do_complete, do_disconnect
 from social.apps.flask_app.utils import psa
diff --git a/social/apps/pyramid_app/models.py b/social/apps/pyramid_app/models.py
index 2d70e06..5752f3f 100644
--- a/social/apps/pyramid_app/models.py
+++ b/social/apps/pyramid_app/models.py
@@ -24,6 +24,8 @@ def init_social(config, Base, session):
     app_session = session
 
     class _AppSession(object):
+        COMMIT_SESSION = False
+
         @classmethod
         def _session(cls):
             return app_session
diff --git a/social/apps/pyramid_app/views.py b/social/apps/pyramid_app/views.py
index 38ee4fa..7587ff9 100644
--- a/social/apps/pyramid_app/views.py
+++ b/social/apps/pyramid_app/views.py
@@ -5,7 +5,7 @@ from social.actions import do_auth, do_complete, do_disconnect
 from social.apps.pyramid_app.utils import psa, login_required
 
 
- at view_config(route_name='social.auth', request_method='GET')
+ at view_config(route_name='social.auth', request_method=('GET', 'POST'))
 @psa('social.complete')
 def auth(request):
     return do_auth(request.backend, redirect_name='next')
diff --git a/social/backends/battlenet.py b/social/backends/battlenet.py
index ecf77a1..eefd2fa 100644
--- a/social/backends/battlenet.py
+++ b/social/backends/battlenet.py
@@ -45,6 +45,6 @@ class BattleNetOAuth2(BaseOAuth2):
     def user_data(self, access_token, *args, **kwargs):
         """ Loads user data from service """
         return self.get_json(
-            'https://eu.api.battle.net/account/user/battletag',
+            'https://eu.api.battle.net/account/user',
             params={'access_token': access_token}
         )
diff --git a/social/backends/coding.py b/social/backends/coding.py
new file mode 100644
index 0000000..3b4295e
--- /dev/null
+++ b/social/backends/coding.py
@@ -0,0 +1,48 @@
+"""
+Coding OAuth2 backend, docs at:
+"""
+from six.moves.urllib.parse import urljoin
+
+from social.backends.oauth import BaseOAuth2
+
+
+class CodingOAuth2(BaseOAuth2):
+    """Coding OAuth authentication backend"""
+
+    name = 'coding'
+    API_URL = 'https://coding.net/api/'
+    AUTHORIZATION_URL = 'https://coding.net/oauth_authorize.html'
+    ACCESS_TOKEN_URL = 'https://coding.net/api/oauth/access_token'
+    ACCESS_TOKEN_METHOD = 'POST'
+    SCOPE_SEPARATOR = ','
+    DEFAULT_SCOPE = ['user']
+    REDIRECT_STATE = False
+
+    def api_url(self):
+        return self.API_URL
+
+    def get_user_details(self, response):
+        """Return user details from Github account"""
+        fullname, first_name, last_name = self.get_user_names(
+            response.get('name')
+        )
+        return {'username': response.get('name'),
+                'email': response.get('email') or '',
+                'fullname': fullname,
+                'first_name': first_name,
+                'last_name': last_name}
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from service"""
+        data = self._user_data(access_token)
+        if data.get('code') != 0:
+            # 获取失败
+            pass
+        return data.get('data')
+
+    def _user_data(self, access_token, path=None):
+        url = urljoin(
+            self.api_url(),
+            'account/current_user{0}'.format(path or '')
+        )
+        return self.get_json(url, params={'access_token': access_token})
diff --git a/social/backends/coursera.py b/social/backends/coursera.py
index 9507edb..ed32721 100644
--- a/social/backends/coursera.py
+++ b/social/backends/coursera.py
@@ -28,6 +28,10 @@ class CourseraOAuth2(BaseOAuth2):
         """Return user details from Coursera account"""
         return {'username': self._get_username_from_response(response)}
 
+    def get_user_id(self, details, response):
+        """Return a username prepared in get_user_details as uid"""
+        return details.get(self.ID_KEY)
+
     def user_data(self, access_token, *args, **kwargs):
         """Load user data from the service"""
         return self.get_json(
diff --git a/social/backends/dribbble.py b/social/backends/dribbble.py
index e1c3eee..ab6d0e5 100644
--- a/social/backends/dribbble.py
+++ b/social/backends/dribbble.py
@@ -58,5 +58,5 @@ class DribbbleOAuth2(BaseOAuth2):
         return self.get_json(
             'https://api.dribbble.com/v1/user',
             headers={
-                'Authorization': ' Bearer {0}'.format(access_token)
+                'Authorization': 'Bearer {0}'.format(access_token)
             })
diff --git a/social/backends/edmodo.py b/social/backends/edmodo.py
new file mode 100644
index 0000000..cc73589
--- /dev/null
+++ b/social/backends/edmodo.py
@@ -0,0 +1,34 @@
+"""
+Edmodo OAuth2 Sign-in backend, docs at:
+    http://psa.matiasaguirre.net/docs/backends/edmodo.html
+"""
+from social.backends.oauth import BaseOAuth2
+
+
+class EdmodoOAuth2(BaseOAuth2):
+    """Edmodo OAuth2"""
+    name = 'edmodo'
+    AUTHORIZATION_URL = 'https://api.edmodo.com/oauth/authorize'
+    ACCESS_TOKEN_URL = 'https://api.edmodo.com/oauth/token'
+    ACCESS_TOKEN_METHOD = 'POST'
+
+    def get_user_details(self, response):
+        """Return user details from Edmodo account"""
+        fullname, first_name, last_name = self.get_user_names(
+            first_name=response.get('first_name'),
+            last_name=response.get('last_name')
+        )
+        return {
+            'username': response.get('username'),
+            'email': response.get('email'),
+            'fullname': fullname,
+            'first_name': first_name,
+            'last_name': last_name
+        }
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from Edmodo"""
+        return self.get_json(
+            'https://api.edmodo.com/users/me',
+            params={'access_token': access_token}
+        )
diff --git a/social/backends/facebook.py b/social/backends/facebook.py
index f76b502..f904556 100644
--- a/social/backends/facebook.py
+++ b/social/backends/facebook.py
@@ -19,11 +19,11 @@ class FacebookOAuth2(BaseOAuth2):
     name = 'facebook'
     RESPONSE_TYPE = None
     SCOPE_SEPARATOR = ','
-    AUTHORIZATION_URL = 'https://www.facebook.com/v2.3/dialog/oauth'
-    ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.3/oauth/access_token'
-    REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.3/{uid}/permissions'
+    AUTHORIZATION_URL = 'https://www.facebook.com/v2.7/dialog/oauth'
+    ACCESS_TOKEN_URL = 'https://graph.facebook.com/v2.7/oauth/access_token'
+    REVOKE_TOKEN_URL = 'https://graph.facebook.com/v2.7/{uid}/permissions'
     REVOKE_TOKEN_METHOD = 'DELETE'
-    USER_DATA_URL = 'https://graph.facebook.com/v2.3/me'
+    USER_DATA_URL = 'https://graph.facebook.com/v2.7/me'
     EXTRA_DATA = [
         ('id', 'id'),
         ('expires', 'expires')
diff --git a/social/backends/line.py b/social/backends/line.py
new file mode 100644
index 0000000..ba8136e
--- /dev/null
+++ b/social/backends/line.py
@@ -0,0 +1,91 @@
+# vim:fileencoding=utf-8
+import requests
+import json
+
+from social.backends.oauth import BaseOAuth2
+from social.exceptions import AuthFailed
+from social.utils import handle_http_errors
+
+
+class LineOAuth2(BaseOAuth2):
+    name = 'line'
+    AUTHORIZATION_URL = 'https://access.line.me/dialog/oauth/weblogin'
+    ACCESS_TOKEN_URL = 'https://api.line.me/v1/oauth/accessToken'
+    BASE_API_URL = 'https://api.line.me'
+    USER_INFO_URL = BASE_API_URL + '/v1/profile'
+    ACCESS_TOKEN_METHOD = 'POST'
+    STATE_PARAMETER = True
+    REDIRECT_STATE = True
+    ID_KEY = 'mid'
+    EXTRA_DATA = [
+        ('mid', 'id'),
+        ('expire', 'expire'),
+        ('refreshToken', 'refresh_token')
+    ]
+
+    def auth_params(self, state=None):
+        client_id, client_secret = self.get_key_and_secret()
+        return {
+            'client_id': client_id,
+            'redirect_uri': self.get_redirect_uri(),
+            'response_type': self.RESPONSE_TYPE
+        }
+
+    def process_error(self, data):
+        error_code = data.get('errorCode') or \
+                     data.get('statusCode') or \
+                     data.get('error')
+        error_message = data.get('errorMessage') or \
+                        data.get('statusMessage') or \
+                        data.get('error_desciption')
+        if error_code is not None or error_message is not None:
+            raise AuthFailed(self, error_message or error_code)
+
+    @handle_http_errors
+    def auth_complete(self, *args, **kwargs):
+        """Completes login process, must return user instance"""
+        client_id, client_secret = self.get_key_and_secret()
+        code = self.data.get('code')
+
+        self.process_error(self.data)
+
+        try:
+            response = self.request_access_token(
+                self.access_token_url(),
+                method=self.ACCESS_TOKEN_METHOD,
+                params={
+                    'requestToken': code,
+                    'channelSecret': client_secret
+                }
+            )
+            self.process_error(response)
+
+            return self.do_auth(response['accessToken'], response=response,
+                                *args, **kwargs)
+        except requests.HTTPError as err:
+            self.process_error(json.loads(err.response.content))
+
+    def get_user_details(self, response):
+        response.update({
+            'fullname': response.get('displayName'),
+            'picture_url': response.get('pictureUrl')
+        })
+        return response
+
+    def get_user_id(self, details, response):
+        """Return a unique ID for the current user, by default from server response."""
+        return response.get(self.ID_KEY)
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from service"""
+        try:
+            response = self.get_json(
+                self.USER_INFO_URL,
+                headers={
+                    "Authorization": "Bearer {}".format(access_token)
+                }
+            )
+            self.process_error(response)
+            return response
+        except requests.HTTPError as err:
+            self.process_error(err.response.json())
diff --git a/social/backends/oauth.py b/social/backends/oauth.py
index b2210ec..3182e52 100644
--- a/social/backends/oauth.py
+++ b/social/backends/oauth.py
@@ -357,6 +357,16 @@ class BaseOAuth2(OAuthAuth):
         return {'Content-Type': 'application/x-www-form-urlencoded',
                 'Accept': 'application/json'}
 
+    def extra_data(self, user, uid, response, details=None, *args, **kwargs):
+        """Return access_token, token_type, and extra defined names to store in
+            extra_data field"""
+        data = super(BaseOAuth2, self).extra_data(user, uid, response,
+                                                  details=details,
+                                                  *args, **kwargs)
+        data['token_type'] = response.get('token_type') or \
+                             kwargs.get('token_type')
+        return data
+
     def request_access_token(self, *args, **kwargs):
         return self.get_json(*args, **kwargs)
 
diff --git a/social/backends/qiita.py b/social/backends/qiita.py
index 52b947a..92bcbd3 100644
--- a/social/backends/qiita.py
+++ b/social/backends/qiita.py
@@ -62,5 +62,5 @@ class QiitaOAuth2(BaseOAuth2):
         return self.get_json(
             'https://qiita.com/api/v2/authenticated_user',
            headers={
-                'Authorization': ' Bearer {0}'.format(access_token)
+                'Authorization': 'Bearer {0}'.format(access_token)
             })
diff --git a/social/backends/reddit.py b/social/backends/reddit.py
index 62ac42c..712bdfb 100644
--- a/social/backends/reddit.py
+++ b/social/backends/reddit.py
@@ -42,9 +42,9 @@ class RedditOAuth2(BaseOAuth2):
 
     def auth_headers(self):
         return {
-            'Authorization': 'Basic {0}'.format(base64.urlsafe_b64encode(
-                ('{0}:{1}'.format(*self.get_key_and_secret()).encode())
-            ))
+            'Authorization': b'Basic ' + base64.urlsafe_b64encode(
+                '{0}:{1}'.format(*self.get_key_and_secret()).encode()
+            )
         }
 
     def refresh_token_params(self, token, redirect_uri=None, *args, **kwargs):
diff --git a/social/backends/sketchfab.py b/social/backends/sketchfab.py
new file mode 100644
index 0000000..cb19ef5
--- /dev/null
+++ b/social/backends/sketchfab.py
@@ -0,0 +1,39 @@
+"""
+Sketchfab OAuth2 backend, docs at:
+    http://psa.matiasaguirre.net/docs/backends/sketchfab.html
+    https://sketchfab.com/developers/oauth
+"""
+from social.backends.oauth import BaseOAuth2
+
+
+class SketchfabOAuth2(BaseOAuth2):
+    name = 'sketchfab'
+    ID_KEY = 'uid'
+    AUTHORIZATION_URL = 'https://sketchfab.com/oauth2/authorize/'
+    ACCESS_TOKEN_URL = 'https://sketchfab.com/oauth2/token/'
+    ACCESS_TOKEN_METHOD = 'POST'
+    REDIRECT_STATE = False
+    REQUIRES_EMAIL_VALIDATION = False
+    EXTRA_DATA = [
+        ('username', 'username'),
+        ('apiToken', 'apiToken')
+    ]
+
+    def get_user_details(self, response):
+        """Return user details from Sketchfab account"""
+        user_data = response
+        email = user_data.get('email', '')
+        username = user_data['username']
+        name = user_data.get('displayName', '')
+        fullname, first_name, last_name = self.get_user_names(name)
+        return {'username': username,
+                'fullname': fullname,
+                'first_name': first_name,
+                'last_name': last_name,
+                'email': email}
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from service"""
+        return self.get_json('https://sketchfab.com/v2/users/me', headers={
+            'Authorization': 'Bearer {0}'.format(access_token)
+        })
diff --git a/social/backends/uber.py b/social/backends/uber.py
index ef6c381..6b1463b 100644
--- a/social/backends/uber.py
+++ b/social/backends/uber.py
@@ -19,7 +19,11 @@ class UberOAuth2(BaseOAuth2):
     def get_user_details(self, response):
         """Return user details from Uber account"""
         email = response.get('email', '')
-        fullname, first_name, last_name = self.get_user_names()
+        fullname, first_name, last_name = self.get_user_names(
+            '',
+            response.get('first_name', ''),
+            response.get('last_name', '')
+        )
         return {'username': email,
                 'email': email,
                 'fullname': fullname,
@@ -28,12 +32,8 @@ class UberOAuth2(BaseOAuth2):
 
     def user_data(self, access_token, *args, **kwargs):
         """Loads user data from service"""
-        client_id, client_secret = self.get_key_and_secret()
         response = kwargs.pop('response')
-
         return self.get_json('https://api.uber.com/v1/me', headers={
-                                    'Authorization': '{0} {1}'.format(
-                                        response.get('token_type'), access_token
-                                    )
-                                }
-                            )
+            'Authorization': '{0} {1}'.format(response.get('token_type'),
+                                              access_token)
+        })
diff --git a/social/backends/untappd.py b/social/backends/untappd.py
new file mode 100644
index 0000000..7d241ad
--- /dev/null
+++ b/social/backends/untappd.py
@@ -0,0 +1,110 @@
+import requests
+
+from social.backends.oauth import BaseOAuth2
+from social.exceptions import AuthFailed
+from social.utils import handle_http_errors
+
+
+class UntappdOAuth2(BaseOAuth2):
+    """Untappd OAuth2 authentication backend"""
+    name = 'untappd'
+    AUTHORIZATION_URL = 'https://untappd.com/oauth/authenticate/'
+    ACCESS_TOKEN_URL = 'https://untappd.com/oauth/authorize/'
+    BASE_API_URL = 'https://api.untappd.com'
+    USER_INFO_URL = BASE_API_URL + '/v4/user/info/'
+    ACCESS_TOKEN_METHOD = 'GET'
+    STATE_PARAMETER = False
+    REDIRECT_STATE = False
+    EXTRA_DATA = [
+        ('id', 'id'),
+        ('bio', 'bio'),
+        ('date_joined', 'date_joined'),
+        ('location', 'location'),
+        ('url', 'url'),
+        ('user_avatar', 'user_avatar'),
+        ('user_avatar_hd', 'user_avatar_hd'),
+        ('user_cover_photo', 'user_cover_photo')
+    ]
+
+    def auth_params(self, state=None):
+        client_id, client_secret = self.get_key_and_secret()
+        params = {
+            'client_id': client_id,
+            'redirect_url': self.get_redirect_uri(),
+            'response_type': self.RESPONSE_TYPE
+        }
+        return params
+
+    def process_error(self, data):
+        """
+        All errors from Untappd are contained in the 'meta' key of the response.
+        """
+        response_code = data.get('meta', {}).get('http_code')
+        if response_code is not None and response_code != requests.codes.ok:
+            raise AuthFailed(self, data['meta']['error_detail'])
+
+    @handle_http_errors
+    def auth_complete(self, *args, **kwargs):
+        """Completes login process, must return user instance"""
+        client_id, client_secret = self.get_key_and_secret()
+        code = self.data.get('code')
+
+        self.process_error(self.data)
+
+        # Untapped sends the access token request with URL parameters,
+        # not a body
+        response = self.request_access_token(
+            self.access_token_url(),
+            method=self.ACCESS_TOKEN_METHOD,
+            params={
+                'response_type': 'code',
+                'code': code,
+                'client_id': client_id,
+                'client_secret': client_secret,
+                'redirect_url': self.get_redirect_uri()
+            }
+        )
+
+        self.process_error(response)
+
+        # Both the access_token and the rest of the response are
+        # buried in the 'response' key
+        return self.do_auth(
+            response['response']['access_token'],
+            response=response['response'],
+            *args, **kwargs
+        )
+
+    def get_user_details(self, response):
+        """Return user details from an Untappd account"""
+        # Start with the user data as it was returned
+        user_data = response['user']
+
+        # Make a few updates to match expected key names
+        user_data.update({
+            'username': user_data.get('user_name'),
+            'email': user_data.get('settings', {}).get('email_address', ''),
+            'first_name': user_data.get('first_name'),
+            'last_name': user_data.get('last_name'),
+            'fullname': user_data.get('first_name') + ' ' +
+                        user_data.get('last_name')
+        })
+        return user_data
+
+    def get_user_id(self, details, response):
+        """
+        Return a unique ID for the current user, by default from
+        server response.
+        """
+        return response['user'].get(self.ID_KEY)
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from service"""
+        response = self.get_json(self.USER_INFO_URL, params={
+            'access_token': access_token,
+            'compact': 'true'
+        })
+        self.process_error(response)
+
+        # The response data is buried in the 'response' key
+        return response['response']
diff --git a/social/backends/upwork.py b/social/backends/upwork.py
new file mode 100644
index 0000000..64f4deb
--- /dev/null
+++ b/social/backends/upwork.py
@@ -0,0 +1,39 @@
+"""
+Upwork OAuth1 backend
+"""
+from social.backends.oauth import BaseOAuth1
+
+
+class UpworkOAuth(BaseOAuth1):
+    """Upwork OAuth authentication backend"""
+    name = 'upwork'
+    ID_KEY = 'id'
+    AUTHORIZATION_URL = 'https://www.upwork.com/services/api/auth'
+    REQUEST_TOKEN_URL = 'https://www.upwork.com/api/auth/v1/oauth/token/request'
+    REQUEST_TOKEN_METHOD = 'POST'
+    ACCESS_TOKEN_URL = 'https://www.upwork.com/api/auth/v1/oauth/token/access'
+    ACCESS_TOKEN_METHOD = 'POST'
+    REDIRECT_URI_PARAMETER_NAME = 'oauth_callback'
+
+    def get_user_details(self, response):
+        """Return user details from Upwork account"""
+        info = response.get('info', {})
+        auth_user = response.get('auth_user', {})
+        first_name = auth_user.get('first_name')
+        last_name = auth_user.get('last_name')
+        fullname = '{} {}'.format(first_name, last_name)
+        profile_url = info.get('profile_url', '')
+        username = profile_url.rsplit('/')[-1].replace('~', '')
+        return {
+            'username': username,
+            'fullname': fullname,
+            'first_name': first_name,
+            'last_name': last_name
+        }
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from service"""
+        return self.get_json(
+            'https://www.upwork.com/api/auth/v1/info.json',
+            auth=self.oauth_auth(access_token)
+        )
diff --git a/social/backends/weixin.py b/social/backends/weixin.py
index 1b5c2d5..279a030 100644
--- a/social/backends/weixin.py
+++ b/social/backends/weixin.py
@@ -3,6 +3,7 @@
 """
 Weixin OAuth2 backend
 """
+import urllib
 from requests import HTTPError
 
 from social.backends.oauth import BaseOAuth2
@@ -43,7 +44,9 @@ class WeixinOAuth2(BaseOAuth2):
         nickname = data.get('nickname')
         if nickname:
             # weixin api has some encode bug, here need handle
-            data['nickname'] = nickname.encode('raw_unicode_escape').decode('utf-8')
+            data['nickname'] = nickname.encode(
+                'raw_unicode_escape'
+            ).decode('utf-8')
         return data
 
     def auth_params(self, state=None):
@@ -99,3 +102,76 @@ class WeixinOAuth2(BaseOAuth2):
         self.process_error(response)
         return self.do_auth(response['access_token'], response=response,
                             *args, **kwargs)
+
+
+class WeixinOAuth2APP(WeixinOAuth2):
+    """
+    Weixin OAuth authentication backend
+
+    Can't use in web, only in weixin app
+    """
+    name = 'weixinapp'
+    ID_KEY = 'openid'
+    AUTHORIZATION_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize'
+    ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token'
+    ACCESS_TOKEN_METHOD = 'POST'
+    REDIRECT_STATE = False
+
+    def auth_url(self):
+        if self.STATE_PARAMETER or self.REDIRECT_STATE:
+            # Store state in session for further request validation. The state
+            # value is passed as state parameter (as specified in OAuth2 spec),
+            # but also added to redirect, that way we can still verify the
+            # request if the provider doesn't implement the state parameter.
+            # Reuse token if any.
+            name = self.name + '_state'
+            state = self.strategy.session_get(name)
+            if state is None:
+                state = self.state_token()
+                self.strategy.session_set(name, state)
+        else:
+            state = None
+
+        params = self.auth_params(state)
+        params.update(self.get_scope_argument())
+        params.update(self.auth_extra_arguments())
+        params = urllib.urlencode(sorted(params.items()))
+        return '{}#wechat_redirect'.format(
+            self.AUTHORIZATION_URL + '?' + params
+        )
+
+    def auth_complete_params(self, state=None):
+        appid, secret = self.get_key_and_secret()
+        return {
+            'grant_type': 'authorization_code',  # request auth code
+            'code': self.data.get('code', ''),  # server response code
+            'appid': appid,
+            'secret': secret,
+        }
+
+    def validate_state(self):
+        return None
+
+    def auth_complete(self, *args, **kwargs):
+        """Completes loging process, must return user instance"""
+        self.process_error(self.data)
+        try:
+            response = self.request_access_token(
+                self.ACCESS_TOKEN_URL,
+                data=self.auth_complete_params(self.validate_state()),
+                headers=self.auth_headers(),
+                method=self.ACCESS_TOKEN_METHOD
+            )
+        except HTTPError as err:
+            if err.response.status_code == 400:
+                raise AuthCanceled(self)
+            else:
+                raise
+        except KeyError:
+            raise AuthUnknownError(self)
+
+        if 'errcode' in response:
+            raise AuthCanceled(self)
+        self.process_error(response)
+        return self.do_auth(response['access_token'], response=response,
+                            *args, **kwargs)
diff --git a/social/pipeline/social_auth.py b/social/pipeline/social_auth.py
index 87895ec..b181ba5 100644
--- a/social/pipeline/social_auth.py
+++ b/social/pipeline/social_auth.py
@@ -27,7 +27,7 @@ def social_user(backend, uid, user=None, *args, **kwargs):
     return {'social': social,
             'user': user,
             'is_new': user is None,
-            'new_association': False}
+            'new_association': social is None}
 
 
 def associate_user(backend, uid, user=None, social=None, *args, **kwargs):
@@ -76,7 +76,8 @@ def associate_by_email(backend, details, user=None, *args, **kwargs):
                 'The given email address is associated with another account'
             )
         else:
-            return {'user': users[0]}
+            return {'user': users[0],
+                    'is_new': False}
 
 
 def load_extra_data(backend, details, response, uid, user, *args, **kwargs):
diff --git a/social/pipeline/user.py b/social/pipeline/user.py
index a46fc3e..e5f8d65 100644
--- a/social/pipeline/user.py
+++ b/social/pipeline/user.py
@@ -39,7 +39,9 @@ def get_username(strategy, details, user=None, *args, **kwargs):
         else:
             username = uuid4().hex
 
-        short_username = username[:max_length - uuid_length]
+        short_username = (username[:max_length - uuid_length]
+                          if max_length is not None
+                          else username)
         final_username = slug_func(clean_func(username[:max_length]))
 
         # Generate a unique username for current user using username
diff --git a/social/storage/django_orm.py b/social/storage/django_orm.py
index 8d9e672..e331656 100644
--- a/social/storage/django_orm.py
+++ b/social/storage/django_orm.py
@@ -1,6 +1,9 @@
 """Django ORM models for Social Auth"""
 import base64
 import six
+import sys
+from django.db import transaction
+from django.db.utils import IntegrityError
 
 from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \
                                 CodeMixin, BaseStorage
@@ -57,7 +60,20 @@ class DjangoUserMixin(UserMixin):
         username_field = cls.username_field()
         if 'username' in kwargs and username_field not in kwargs:
             kwargs[username_field] = kwargs.pop('username')
-        return cls.user_model().objects.create_user(*args, **kwargs)
+        try:
+            user = cls.user_model().objects.create_user(*args, **kwargs)
+        except IntegrityError:
+            # User might have been created on a different thread, try and find them.
+            # If we don't, re-raise the IntegrityError.
+            exc_info = sys.exc_info()
+            # If email comes in as None it won't get found in the get
+            if kwargs.get('email', True) is None:
+                kwargs['email'] = ''
+            try:
+                user = cls.user_model().objects.get(*args, **kwargs)
+            except cls.user_model().DoesNotExist:
+                six.reraise(*exc_info)
+        return user
 
     @classmethod
     def get_user(cls, pk=None, **kwargs):
@@ -96,7 +112,16 @@ class DjangoUserMixin(UserMixin):
     def create_social_auth(cls, user, uid, provider):
         if not isinstance(uid, six.string_types):
             uid = str(uid)
-        return cls.objects.create(user=user, uid=uid, provider=provider)
+        if hasattr(transaction, 'atomic'):
+            # In Django versions that have an "atomic" transaction decorator / context
+            # manager, there's a transaction wrapped around this call.
+            # If the create fails below due to an IntegrityError, ensure that the transaction
+            # stays undamaged by wrapping the create in an atomic.
+            with transaction.atomic():
+                social_auth = cls.objects.create(user=user, uid=uid, provider=provider)
+        else:
+            social_auth = cls.objects.create(user=user, uid=uid, provider=provider)
+        return social_auth
 
 
 class DjangoNonceMixin(NonceMixin):
diff --git a/social/storage/peewee_orm.py b/social/storage/peewee_orm.py
new file mode 100644
index 0000000..60b5da0
--- /dev/null
+++ b/social/storage/peewee_orm.py
@@ -0,0 +1,199 @@
+import six
+import base64
+
+from peewee import CharField, Model, Proxy, IntegrityError
+from playhouse.kv import JSONField
+
+from social.storage.base import UserMixin, AssociationMixin, NonceMixin, \
+                                CodeMixin, BaseStorage
+
+
+def get_query_by_dict_param(cls, params):
+    query = True
+
+    for field_name, value in params.iteritems():
+        query_item = cls._meta.fields[field_name] == value
+        query = query & query_item
+        return query
+
+
+database_proxy = Proxy()
+
+
+class BaseModel(Model):
+    class Meta:
+        database = database_proxy
+
+
+class PeeweeUserMixin(UserMixin, BaseModel):
+    provider = CharField()
+    extra_data = JSONField(null=True)
+    uid = CharField()
+    user = None
+
+    @classmethod
+    def changed(cls, user):
+        user.save()
+
+    def set_extra_data(self, extra_data=None):
+        if super(PeeweeUserMixin, self).set_extra_data(extra_data):
+            self.save()
+
+    @classmethod
+    def username_max_length(cls):
+        username_field = cls.username_field()
+        field = getattr(cls.user_model(), username_field)
+        return field.max_length
+
+    @classmethod
+    def username_field(cls):
+        return getattr(cls.user_model(), 'USERNAME_FIELD', 'username')
+
+    @classmethod
+    def allowed_to_disconnect(cls, user, backend_name, association_id=None):
+        if association_id is not None:
+            query = cls.select().where(cls.id != association_id)
+        else:
+            query = cls.select().where(cls.provider != backend_name)
+        query = query.where(cls.user == user)
+
+        if hasattr(user, 'has_usable_password'):
+            valid_password = user.has_usable_password()
+        else:
+            valid_password = True
+        return valid_password or query.count() > 0
+
+    @classmethod
+    def disconnect(cls, entry):
+        entry.delete_instance()
+
+    @classmethod
+    def user_exists(cls, *args, **kwargs):
+        """
+        Return True/False if a User instance exists with the given arguments.
+        """
+        user_model = cls.user_model()
+        query = get_query_by_dict_param(user_model, kwargs)
+        return user_model.select().where(query).count() > 0
+
+    @classmethod
+    def get_username(cls, user):
+        return getattr(user, cls.username_field(), None)
+
+    @classmethod
+    def create_user(cls, *args, **kwargs):
+        username_field = cls.username_field()
+        if 'username' in kwargs and username_field not in kwargs:
+            kwargs[username_field] = kwargs.pop('username')
+        return cls.user_model().create(*args, **kwargs)
+
+    @classmethod
+    def get_user(cls, pk, **kwargs):
+        if pk:
+            kwargs = {'id': pk}
+        try:
+            return cls.user_model().select().get(
+                get_query_by_dict_param(cls.user_model(), kwargs)
+            )
+        except cls.user_model().DoesNotExist:
+            return None
+
+    @classmethod
+    def get_users_by_email(cls, email):
+        user_model = cls.user_model()
+        return user_model.select().where(user_model.email == email)
+
+    @classmethod
+    def get_social_auth(cls, provider, uid):
+        if not isinstance(uid, six.string_types):
+            uid = str(uid)
+        try:
+            return cls.select().where(
+                cls.provider == provider, cls.uid == uid
+            ).get()
+        except cls.DoesNotExist:
+            return None
+
+    @classmethod
+    def get_social_auth_for_user(cls, user, provider=None, id=None):
+        query = cls.select().where(cls.user == user)
+        if provider:
+            query = query.where(cls.provider == provider)
+        if id:
+            query = query.where(cls.id == id)
+        return list(query)
+
+    @classmethod
+    def create_social_auth(cls, user, uid, provider):
+        if not isinstance(uid, six.string_types):
+            uid = str(uid)
+        return cls.create(user=user, uid=uid, provider=provider)
+
+
+class PeeweeNonceMixin(NonceMixin, BaseModel):
+    server_url = CharField()
+    timestamp = CharField()
+    salt = CharField()
+
+    @classmethod
+    def use(cls, server_url, timestamp, salt):
+        return cls.select().get_or_create(cls.server_url == server_url,
+                                          cls.timestamp == timestamp,
+                                          cls.salt == salt)
+
+
+class PeeweeAssociationMixin(AssociationMixin, BaseModel):
+    server_url = CharField()
+    handle = CharField()
+    secret = CharField()  # base64 encoded
+    issued = CharField()
+    lifetime = CharField()
+    assoc_type = CharField()
+
+    @classmethod
+    def store(cls, server_url, association):
+        try:
+            assoc = cls.select().get(cls.server_url == server_url,
+                                     cls.handle == association.handle)
+        except cls.DoesNotExist:
+            assoc = cls(server_url=server_url,
+                        handle=association.handle)
+
+        assoc.secret = base64.encodestring(association.secret)
+        assoc.issued = association.issued
+        assoc.lifetime = association.lifetime
+        assoc.assoc_type = association.assoc_type
+        assoc.save()
+
+    @classmethod
+    def get(cls, *args, **kwargs):
+        query = get_query_by_dict_param(cls, kwargs)
+        return cls.select().where(query)
+
+    @classmethod
+    def remove(cls, ids_to_delete):
+        cls.select().where(cls.id << ids_to_delete).delete()
+
+
+class PeeweeCodeMixin(CodeMixin, BaseModel):
+    email = CharField()
+    code = CharField()  # base64 encoded
+    issued = CharField()
+
+    @classmethod
+    def get_code(cls, code):
+        try:
+            return cls.select().get(cls.code == code)
+        except cls.DoesNotExist:
+            return None
+
+
+class BasePeeweeStorage(BaseStorage):
+    user = PeeweeUserMixin
+    nonce = PeeweeNonceMixin
+    association = PeeweeAssociationMixin
+    code = PeeweeCodeMixin
+
+    @classmethod
+    def is_integrity_error(cls, exception):
+        return exception.__class__ is IntegrityError
diff --git a/social/storage/sqlalchemy_orm.py b/social/storage/sqlalchemy_orm.py
index 621bf4c..e48ef64 100644
--- a/social/storage/sqlalchemy_orm.py
+++ b/social/storage/sqlalchemy_orm.py
@@ -28,6 +28,8 @@ class JSONType(PickleType):
 
 
 class SQLAlchemyMixin(object):
+    COMMIT_SESSION = True
+
     @classmethod
     def _session(cls):
         raise NotImplementedError('Implement in subclass')
@@ -43,7 +45,11 @@ class SQLAlchemyMixin(object):
     @classmethod
     def _save_instance(cls, instance):
         cls._session().add(instance)
-        cls._flush()
+        if cls.COMMIT_SESSION:
+            cls._session().commit()
+            cls._session().flush()
+        else:
+            cls._flush()
         return instance
 
     @classmethod
diff --git a/social/strategies/base.py b/social/strategies/base.py
index ce66af0..22f24e1 100644
--- a/social/strategies/base.py
+++ b/social/strategies/base.py
@@ -131,6 +131,8 @@ class BaseStrategy(object):
         verification_code = self.storage.code.get_code(code)
         if not verification_code or verification_code.code != code:
             return False
+        elif verification_code.email != email:
+            return False
         else:
             verification_code.verify()
             return True
diff --git a/social/strategies/pyramid_strategy.py b/social/strategies/pyramid_strategy.py
index 6ac4157..9761a42 100644
--- a/social/strategies/pyramid_strategy.py
+++ b/social/strategies/pyramid_strategy.py
@@ -38,13 +38,7 @@ class PyramidStrategy(BaseStrategy):
 
     def html(self, content):
         """Return HTTP response with given content"""
-        response = getattr(self.request, 'response', None)
-        if response is None:
-            response = Response(body=content)
-        else:
-            response = self.request.response
-            response.body = content
-        return response
+        return Response(body=content)
 
     def request_data(self, merge=True):
         """Return current request data (POST or GET)"""
diff --git a/social/tests/backends/test_edmodo.py b/social/tests/backends/test_edmodo.py
new file mode 100644
index 0000000..ff3914c
--- /dev/null
+++ b/social/tests/backends/test_edmodo.py
@@ -0,0 +1,44 @@
+import json
+
+from social.tests.backends.oauth import OAuth2Test
+
+
+class EdmodoOAuth2Test(OAuth2Test):
+    backend_path = 'social.backends.edmodo.EdmodoOAuth2'
+    user_data_url = 'https://api.edmodo.com/users/me'
+    expected_username = 'foobar12345'
+    access_token_body = json.dumps({
+        'access_token': 'foobar',
+        'token_type': 'bearer'
+    })
+    user_data_body = json.dumps({
+        'username': 'foobar12345',
+        'coppa_verified': False,
+        'first_name': 'Foo',
+        'last_name': 'Bar',
+        'premium': False,
+        'verified_institution_member': False,
+        'url': 'https://api.edmodo.com/users/12345',
+        'type': 'teacher',
+        'time_zone': None,
+        'end_level': None,
+        'start_level': None,
+        'locale': 'en',
+        'subjects': None,
+        'utc_offset': None,
+        'email': 'foo.bar at example.com',
+        'gender': None,
+        'about': None,
+        'user_title': None,
+        'id': 12345,
+        'avatars': {
+            'small': 'https://api.edmodo.com/users/12345/avatar?type=small&u=5a15xug93m53mi4ey3ck4fvkq',
+            'large': 'https://api.edmodo.com/users/12345/avatar?type=large&u=5a15xug93m53mi4ey3ck4fvkq'
+        }
+    })
+
+    def test_login(self):
+        self.do_login()
+
+    def test_partial_pipeline(self):
+        self.do_partial_pipeline()
diff --git a/social/tests/backends/test_facebook.py b/social/tests/backends/test_facebook.py
index 166d753..81146d9 100644
--- a/social/tests/backends/test_facebook.py
+++ b/social/tests/backends/test_facebook.py
@@ -7,7 +7,7 @@ from social.tests.backends.oauth import OAuth2Test
 
 class FacebookOAuth2Test(OAuth2Test):
     backend_path = 'social.backends.facebook.FacebookOAuth2'
-    user_data_url = 'https://graph.facebook.com/v2.3/me'
+    user_data_url = 'https://graph.facebook.com/v2.7/me'
     expected_username = 'foobar'
     access_token_body = json.dumps({
         'access_token': 'foobar',
diff --git a/social/tests/backends/test_sketchfab.py b/social/tests/backends/test_sketchfab.py
new file mode 100644
index 0000000..ada632f
--- /dev/null
+++ b/social/tests/backends/test_sketchfab.py
@@ -0,0 +1,26 @@
+import json
+
+from social.tests.backends.oauth import OAuth2Test
+
+
+class SketchfabOAuth2Test(OAuth2Test):
+    backend_path = 'social.backends.sketchfab.SketchfabOAuth2'
+    user_data_url = 'https://sketchfab.com/v2/users/me'
+    expected_username = 'foobar'
+    access_token_body = json.dumps({
+        'access_token': 'foobar',
+        'token_type': 'bearer'
+    })
+    user_data_body = json.dumps({
+        'uid': '42',
+        'email': 'foo at bar.com',
+        'displayName': 'foo bar',
+        'username': 'foobar',
+        'apiToken': 'XXX'
+    })
+
+    def test_login(self):
+        self.do_login()
+
+    def test_partial_pipeline(self):
+        self.do_partial_pipeline()
diff --git a/social/tests/backends/test_twitter.py b/social/tests/backends/test_twitter.py
index 4300237..fc2af14 100644
--- a/social/tests/backends/test_twitter.py
+++ b/social/tests/backends/test_twitter.py
@@ -129,3 +129,132 @@ class TwitterOAuth1Test(OAuth1Test):
 
     def test_partial_pipeline(self):
         self.do_partial_pipeline()
+
+
+class TwitterOAuth1IncludeEmailTest(OAuth1Test):
+    backend_path = 'social.backends.twitter.TwitterOAuth'
+    user_data_url = 'https://api.twitter.com/1.1/account/' \
+                        'verify_credentials.json?include_email=true'
+    expected_username = 'foobar'
+    access_token_body = json.dumps({
+        'access_token': 'foobar',
+        'token_type': 'bearer'
+    })
+    request_token_body = urlencode({
+        'oauth_token_secret': 'foobar-secret',
+        'oauth_token': 'foobar',
+        'oauth_callback_confirmed': 'true'
+    })
+    user_data_body = json.dumps({
+        'follow_request_sent': False,
+        'profile_use_background_image': True,
+        'id': 10101010,
+        'description': 'Foo bar baz qux',
+        'verified': False,
+        'entities': {
+            'description': {
+                'urls': []
+            }
+        },
+        'profile_image_url_https': 'https://twimg0-a.akamaihd.net/'
+                                   'profile_images/532018826/'
+                                   'n587119531_1939735_9305_normal.jpg',
+        'profile_sidebar_fill_color': '252429',
+        'profile_text_color': '666666',
+        'followers_count': 77,
+        'profile_sidebar_border_color': '181A1E',
+        'location': 'Fooland',
+        'default_profile_image': False,
+        'listed_count': 4,
+        'status': {
+            'favorited': False,
+            'contributors': None,
+            'retweeted_status': {
+                'favorited': False,
+                'contributors': None,
+                'truncated': False,
+                'source': 'web',
+                'text': '"Foo foo foo foo',
+                'created_at': 'Fri Dec 21 18:12:00 +0000 2012',
+                'retweeted': True,
+                'in_reply_to_status_id': None,
+                'coordinates': None,
+                'id': 101010101010101010,
+                'entities': {
+                    'user_mentions': [],
+                    'hashtags': [],
+                    'urls': []
+                },
+                'in_reply_to_status_id_str': None,
+                'place': None,
+                'id_str': '101010101010101010',
+                'in_reply_to_screen_name': None,
+                'retweet_count': 8,
+                'geo': None,
+                'in_reply_to_user_id_str': None,
+                'in_reply_to_user_id': None
+            },
+            'truncated': False,
+            'source': 'web',
+            'text': 'RT @foo: "Foo foo foo foo',
+            'created_at': 'Fri Dec 21 18:24:10 +0000 2012',
+            'retweeted': True,
+            'in_reply_to_status_id': None,
+            'coordinates': None,
+            'id': 101010101010101010,
+            'entities': {
+                'user_mentions': [{
+                    'indices': [3, 10],
+                    'id': 10101010,
+                    'screen_name': 'foo',
+                    'id_str': '10101010',
+                    'name': 'Foo'
+                }],
+                'hashtags': [],
+                'urls': []
+            },
+            'in_reply_to_status_id_str': None,
+            'place': None,
+            'id_str': '101010101010101010',
+            'in_reply_to_screen_name': None,
+            'retweet_count': 8,
+            'geo': None,
+            'in_reply_to_user_id_str': None,
+            'in_reply_to_user_id': None
+        },
+        'utc_offset': -10800,
+        'statuses_count': 191,
+        'profile_background_color': '1A1B1F',
+        'friends_count': 151,
+        'profile_background_image_url_https': 'https://twimg0-a.akamaihd.net/'
+                                              'images/themes/theme9/bg.gif',
+        'profile_link_color': '2FC2EF',
+        'profile_image_url': 'http://a0.twimg.com/profile_images/532018826/'
+                             'n587119531_1939735_9305_normal.jpg',
+        'is_translator': False,
+        'geo_enabled': False,
+        'id_str': '74313638',
+        'profile_background_image_url': 'http://a0.twimg.com/images/themes/'
+                                        'theme9/bg.gif',
+        'screen_name': 'foobar',
+        'lang': 'en',
+        'profile_background_tile': False,
+        'favourites_count': 2,
+        'name': 'Foo',
+        'notifications': False,
+        'url': None,
+        'created_at': 'Tue Sep 15 00:26:17 +0000 2009',
+        'contributors_enabled': False,
+        'time_zone': 'Buenos Aires',
+        'protected': False,
+        'default_profile': False,
+        'following': False,
+        'email': 'foo at bar.bas'
+    })
+
+    def test_login(self):
+        user = self.do_login()
+        self.assertEquals(user.email, 'foo at bar.bas')
+
+    def test_partial_pipeline(self):
+        self.do_partial_pipeline()
diff --git a/social/tests/backends/test_upwork.py b/social/tests/backends/test_upwork.py
new file mode 100644
index 0000000..8eb6d8d
--- /dev/null
+++ b/social/tests/backends/test_upwork.py
@@ -0,0 +1,53 @@
+import json
+
+from social.p3 import urlencode
+from social.tests.backends.oauth import OAuth1Test
+
+
+class UpworkOAuth1Test(OAuth1Test):
+    backend_path = 'social.backends.upwork.UpworkOAuth'
+    user_data_url = 'https://www.upwork.com/api/auth/v1/info.json'
+    expected_username = '10101010'
+    access_token_body = json.dumps({
+        'access_token': 'foobar',
+        'token_type': 'bearer'
+    })
+    request_token_body = urlencode({
+        'oauth_token_secret': 'foobar-secret',
+        'oauth_token': 'foobar',
+        'oauth_callback_confirmed': 'true'
+    })
+    user_data_body = json.dumps({
+        'info': {
+            'portrait_32_img': '',
+            'capacity': {
+                'buyer': 'no',
+                'affiliate_manager': 'no',
+                'provider': 'yes'
+            },
+            'company_url': '',
+            'has_agency': '1',
+            'portrait_50_img': '',
+            'portrait_100_img': '',
+            'location': {
+                'city': 'New York',
+                'state': '',
+                'country': 'USA'
+            },
+            'ref': '9755314',
+            'profile_url': 'https://www.upwork.com/users/~10101010'
+        },
+        'auth_user': {
+            'timezone': 'USA/New York',
+            'first_name': 'Foo',
+            'last_name': 'Bar',
+            'timezone_offset': '10000'
+        },
+        'server_time': '1111111111'
+    })
+
+    def test_login(self):
+        self.do_login()
+
+    def test_partial_pipeline(self):
+        self.do_partial_pipeline()
diff --git a/social/tests/test_utils.py b/social/tests/test_utils.py
index 7bd8db0..4a13ba3 100644
--- a/social/tests/test_utils.py
+++ b/social/tests/test_utils.py
@@ -13,31 +13,41 @@ PY3 = sys.version_info[0] == 3
 
 class SanitizeRedirectTest(unittest.TestCase):
     def test_none_redirect(self):
-        self.assertEqual(sanitize_redirect('myapp.com', None), None)
+        self.assertEqual(sanitize_redirect(['myapp.com'], None), None)
 
     def test_empty_redirect(self):
-        self.assertEqual(sanitize_redirect('myapp.com', ''), None)
+        self.assertEqual(sanitize_redirect(['myapp.com'], ''), None)
 
     def test_dict_redirect(self):
-        self.assertEqual(sanitize_redirect('myapp.com', {}), None)
+        self.assertEqual(sanitize_redirect(['myapp.com'], {}), None)
 
     def test_invalid_redirect(self):
-        self.assertEqual(sanitize_redirect('myapp.com', {'foo': 'bar'}), None)
+        self.assertEqual(sanitize_redirect(['myapp.com'], {'foo': 'bar'}), None)
 
     def test_wrong_path_redirect(self):
         self.assertEqual(
-            sanitize_redirect('myapp.com', 'http://notmyapp.com/path/'),
+            sanitize_redirect(['myapp.com'], 'http://notmyapp.com/path/'),
             None
         )
 
     def test_valid_absolute_redirect(self):
         self.assertEqual(
-            sanitize_redirect('myapp.com', 'http://myapp.com/path/'),
+            sanitize_redirect(['myapp.com'], 'http://myapp.com/path/'),
             'http://myapp.com/path/'
         )
 
     def test_valid_relative_redirect(self):
-        self.assertEqual(sanitize_redirect('myapp.com', '/path/'), '/path/')
+        self.assertEqual(sanitize_redirect(['myapp.com'], '/path/'), '/path/')
+
+    def test_multiple_hosts(self):
+        allowed_hosts = ['myapp1.com', 'myapp2.com']
+        for host in allowed_hosts:
+            url = 'http://{}/path/'.format(host)
+            self.assertEqual(sanitize_redirect(allowed_hosts, url), url)
+
+    def test_multiple_hosts_wrong_host(self):
+        self.assertEqual(sanitize_redirect(
+            ['myapp1.com', 'myapp2.com'], 'http://notmyapp.com/path/'), None)
 
 
 class UserIsAuthenticatedTest(unittest.TestCase):
@@ -121,6 +131,30 @@ class BuildAbsoluteURITest(unittest.TestCase):
 
 
 class PartialPipelineData(unittest.TestCase):
+    def test_returns_partial_when_uid_and_email_do_match(self):
+        email = 'foo at example.com'
+        backend = self._backend({'uid': email})
+        backend.strategy.request_data.return_value = {
+            backend.ID_KEY: email
+        }
+        key, val = ('foo', 'bar')
+        _, xkwargs = partial_pipeline_data(backend, None,
+                                           *(), **dict([(key, val)]))
+        self.assertTrue(key in xkwargs)
+        self.assertEqual(xkwargs[key], val)
+        self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0)
+
+    def test_clean_pipeline_when_uid_does_not_match(self):
+        backend = self._backend({'uid': 'foo at example.com'})
+        backend.strategy.request_data.return_value = {
+            backend.ID_KEY: 'bar at example.com'
+        }
+        key, val = ('foo', 'bar')
+        ret = partial_pipeline_data(backend, None,
+                                           *(), **dict([(key, val)]))
+        self.assertIsNone(ret)
+        self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 1)
+
     def test_kwargs_included_in_result(self):
         backend = self._backend()
         key, val = ('foo', 'bar')
@@ -128,6 +162,7 @@ class PartialPipelineData(unittest.TestCase):
                                            *(), **dict([(key, val)]))
         self.assertTrue(key in xkwargs)
         self.assertEqual(xkwargs[key], val)
+        self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0)
 
     def test_update_user(self):
         user = object()
@@ -135,15 +170,18 @@ class PartialPipelineData(unittest.TestCase):
         _, xkwargs = partial_pipeline_data(backend, user)
         self.assertTrue('user' in xkwargs)
         self.assertEqual(xkwargs['user'], user)
+        self.assertEqual(backend.strategy.clean_partial_pipeline.call_count, 0)
 
     def _backend(self, session_kwargs=None):
         strategy = Mock()
         strategy.request = None
+        strategy.request_data.return_value = {}
         strategy.session_get.return_value = object()
         strategy.partial_from_session.return_value = \
             (0, 'mock-backend', [], session_kwargs or {})
 
         backend = Mock()
+        backend.ID_KEY = 'email'
         backend.name = 'mock-backend'
         backend.strategy = strategy
         return backend
diff --git a/social/utils.py b/social/utils.py
index 0b5a507..f405375 100644
--- a/social/utils.py
+++ b/social/utils.py
@@ -81,21 +81,21 @@ def setting_name(*names):
     return to_setting_name(*((SETTING_PREFIX,) + names))
 
 
-def sanitize_redirect(host, redirect_to):
+def sanitize_redirect(hosts, redirect_to):
     """
-    Given the hostname and an untrusted URL to redirect to,
+    Given a list of hostnames and an untrusted URL to redirect to,
     this method tests it to make sure it isn't garbage/harmful
     and returns it, else returns None, similar as how's it done
     on django.contrib.auth.views.
     """
     if redirect_to:
         try:
-            # Don't redirect to a different host
-            netloc = urlparse(redirect_to)[1] or host
+            # Don't redirect to a host that's not in the list
+            netloc = urlparse(redirect_to)[1] or hosts[0]
         except (TypeError, AttributeError):
             pass
         else:
-            if netloc == host:
+            if netloc in hosts:
                 return redirect_to
 
 
@@ -166,7 +166,24 @@ def partial_pipeline_data(backend, user=None, *args, **kwargs):
     if partial:
         idx, backend_name, xargs, xkwargs = \
             backend.strategy.partial_from_session(partial)
+
+        partial_matches_request = False
+
         if backend_name == backend.name:
+            partial_matches_request = True
+
+            req_data = backend.strategy.request_data()
+            # Normally when resuming a pipeline, request_data will be empty. We
+            # only need to check for a uid match if new data was provided (i.e.
+            # if current request specifies the ID_KEY).
+            if backend.ID_KEY in req_data:
+                id_from_partial = xkwargs.get('uid')
+                id_from_request = req_data.get(backend.ID_KEY)
+
+                if id_from_partial != id_from_request:
+                    partial_matches_request = False
+
+        if partial_matches_request:
             kwargs.setdefault('pipeline_index', idx)
             if user:  # don't update user if it's None
                 kwargs.setdefault('user', user)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-social-auth.git



More information about the Python-modules-commits mailing list