Skip to content

Commit 2cb74f4

Browse files
committed
Merge remote-tracking branch 'anka/master'
2 parents dab4ca3 + e378d4f commit 2cb74f4

File tree

18 files changed

+190
-6
lines changed

18 files changed

+190
-6
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Adam McKerlie
1111
Ahmet Emre Aladağ
1212
Aldiantoro Nugroho
1313
Alexander Gaevsky
14+
Anna Sirota
1415
Andrean Franc
1516
Andrey Balandin
1617
Andy Matthews

ChangeLog

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2016-03-11 Raymond Penners <[email protected]>
2+
3+
* Shopify is now a supported provider, thanks Anna Sirota!
4+
15
2016-03-10 Raymond Penners <[email protected]>
26

37
* In order to be secure by default, users are now blocked from

allauth/compat.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ def render_to_string(template_name, context=None, request=None, using=None):
1717
return _render_to_string(template_name, context, RequestContext(request))
1818

1919
try:
20-
from urllib.parse import parse_qsl, urlparse, urlunparse
20+
from urllib.parse import parse_qsl, parse_qs, urlparse, urlunparse
2121
except ImportError:
22-
from urlparse import parse_qsl, urlparse, urlunparse # noqa
22+
from urlparse import parse_qsl, parse_qs, urlparse, urlunparse # noqa
2323

2424
try:
2525
import importlib

allauth/socialaccount/providers/google/tests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def raise_for_status(self):
6868
reverse(self.provider.id + '_login'),
6969
dict(process='login'))
7070

71-
adapter = GoogleOAuth2Adapter()
71+
adapter = GoogleOAuth2Adapter(request)
7272
app = adapter.get_provider().get_app(request)
7373
token = SocialToken(token='some_token')
7474
response_with_401 = LessMockedResponse(

allauth/socialaccount/providers/oauth/views.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
class OAuthAdapter(object):
1616

17+
def __init__(self, request):
18+
self.request = request
19+
1720
def complete_login(self, request, app):
1821
"""
1922
Returns a SocialLogin instance
@@ -30,7 +33,7 @@ def adapter_view(cls, adapter):
3033
def view(request, *args, **kwargs):
3134
self = cls()
3235
self.request = request
33-
self.adapter = adapter()
36+
self.adapter = adapter(request)
3437
return self.dispatch(request, *args, **kwargs)
3538
return view
3639

allauth/socialaccount/providers/oauth2/views.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.http import HttpResponseRedirect
88
from django.utils import timezone
99

10+
from allauth.exceptions import ImmediateHttpResponse
1011
from allauth.utils import build_absolute_uri
1112
from allauth.account import app_settings
1213
from allauth.socialaccount.helpers import render_authentication_error
@@ -29,6 +30,9 @@ class OAuth2Adapter(object):
2930
basic_auth = False
3031
headers = None
3132

33+
def __init__(self, request):
34+
self.request = request
35+
3236
def get_provider(self):
3337
return providers.registry.by_id(self.provider_id)
3438

@@ -54,8 +58,11 @@ def adapter_view(cls, adapter):
5458
def view(request, *args, **kwargs):
5559
self = cls()
5660
self.request = request
57-
self.adapter = adapter()
58-
return self.dispatch(request, *args, **kwargs)
61+
self.adapter = adapter(request)
62+
try:
63+
return self.dispatch(request, *args, **kwargs)
64+
except ImmediateHttpResponse as e:
65+
return e.response
5966
return view
6067

6168
def get_client(self, request, app):

allauth/socialaccount/providers/shopify/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your models here.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from allauth.socialaccount import providers
2+
from allauth.socialaccount.providers.base import ProviderAccount
3+
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
4+
5+
6+
class ShopifyAccount(ProviderAccount):
7+
pass
8+
9+
10+
class ShopifyProvider(OAuth2Provider):
11+
id = 'shopify'
12+
name = 'Shopify'
13+
package = 'allauth.socialaccount.providers.shopify'
14+
account_class = ShopifyAccount
15+
16+
def get_auth_params(self, request, action):
17+
ret = super(ShopifyProvider, self).get_auth_params(request, action)
18+
shop = request.GET.get('shop', None)
19+
if shop:
20+
ret.update({'shop': shop})
21+
return ret
22+
23+
def get_default_scope(self):
24+
return ['read_orders', 'read_products']
25+
26+
def extract_uid(self, data):
27+
return str(data['shop']['id'])
28+
29+
def extract_common_fields(self, data):
30+
# See: https://docs.shopify.com/api/shop
31+
# User is only available with Shopify Plus, email is the only
32+
# common field
33+
return dict(email=data['shop']['email'])
34+
35+
providers.registry.register(ShopifyProvider)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from django.core.urlresolvers import reverse
2+
3+
from allauth.socialaccount.tests import create_oauth2_tests
4+
from allauth.tests import MockedResponse, mocked_response
5+
from allauth.socialaccount.providers import registry
6+
from allauth.compat import parse_qs, urlparse
7+
8+
from .provider import ShopifyProvider
9+
10+
11+
class ShopifyTests(create_oauth2_tests(registry.by_id(ShopifyProvider.id))):
12+
def login(self, resp_mock, process='login', with_refresh_token=True):
13+
resp = self.client.get(reverse(self.provider.id + '_login'),
14+
{'process': process, 'shop': 'test'})
15+
p = urlparse(resp['location'])
16+
q = parse_qs(p.query)
17+
complete_url = reverse(self.provider.id+'_callback')
18+
self.assertGreater(q['redirect_uri'][0]
19+
.find(complete_url), 0)
20+
response_json = self \
21+
.get_login_response_json(with_refresh_token=with_refresh_token)
22+
with mocked_response(
23+
MockedResponse(
24+
200,
25+
response_json,
26+
{'content-type': 'application/json'}),
27+
resp_mock):
28+
resp = self.client.get(complete_url,
29+
{'code': 'test',
30+
'state': q['state'][0],
31+
'shop': 'test',
32+
})
33+
return resp
34+
35+
def get_mocked_response(self):
36+
return MockedResponse(200, """
37+
{
38+
"shop": {
39+
"id": "1234566",
40+
"name": "Test Shop",
41+
"email": "[email protected]"
42+
}
43+
}
44+
""")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
2+
from .provider import ShopifyProvider
3+
4+
urlpatterns = default_urlpatterns(ShopifyProvider)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import re
2+
import requests
3+
4+
from django.http import HttpResponseBadRequest
5+
6+
from allauth.exceptions import ImmediateHttpResponse
7+
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
8+
OAuth2LoginView,
9+
OAuth2CallbackView)
10+
from .provider import ShopifyProvider
11+
12+
13+
class ShopifyOAuth2Adapter(OAuth2Adapter):
14+
provider_id = ShopifyProvider.id
15+
supports_state = False
16+
scope_delimiter = ','
17+
18+
def _shop_domain(self):
19+
shop = self.request.GET.get('shop', '')
20+
if '.' not in shop:
21+
shop = '{}.myshopify.com'.format(shop)
22+
# Ensure the provided hostname parameter is a valid hostname,
23+
# ends with myshopify.com, and does not contain characters
24+
# other than letters (a-z), numbers (0-9), dots, and hyphens.
25+
if not re.match(r'^[a-z0-9-]+\.myshopify\.com$', shop):
26+
raise ImmediateHttpResponse(HttpResponseBadRequest(
27+
'Invalid `shop` parameter'))
28+
return shop
29+
30+
def _shop_url(self, path):
31+
shop = self._shop_domain()
32+
return 'https://{}{}'.format(shop, path)
33+
34+
@property
35+
def access_token_url(self):
36+
return self._shop_url('/admin/oauth/access_token')
37+
38+
@property
39+
def authorize_url(self):
40+
return self._shop_url('/admin/oauth/authorize')
41+
42+
@property
43+
def profile_url(self):
44+
return self._shop_url('/admin/shop.json')
45+
46+
def complete_login(self, request, app, token, **kwargs):
47+
headers = {
48+
'X-Shopify-Access-Token': '{token}'.format(token=token.token)}
49+
response = requests.get(
50+
self.profile_url,
51+
headers=headers)
52+
extra_data = response.json()
53+
return self.get_provider().sociallogin_from_response(
54+
request, extra_data)
55+
56+
57+
oauth2_login = OAuth2LoginView.adapter_view(ShopifyOAuth2Adapter)
58+
oauth2_callback = OAuth2CallbackView.adapter_view(ShopifyOAuth2Adapter)

allauth/templates/shopify/base.html

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{% extends "socialaccount/base.html" %}

allauth/templates/shopify/login.html

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{% extends "shopify/base.html" %}
2+
3+
{% load i18n socialaccount %}
4+
5+
{% block head_title %}Shopify Sign In{% endblock %}
6+
7+
{% block content %}
8+
9+
<h1>{% trans 'Shopify Sign In' %}</h1>
10+
11+
12+
<form class="shopify_login" method="post" action="{% provider_login_url "shopify" %}">
13+
{% csrf_token %}
14+
<label for="auth_param_shop">Enter your Shopify store name:
15+
<input type="text" placeholder="your-store" name="shop" id="auth_param_shop">
16+
<span>.myshopify.com</span>
17+
</label>
18+
<button type="submit">Sign In</button>
19+
</form>
20+
21+
{% endblock %}

docs/installation.rst

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ settings.py (Important - Please note 'django.contrib.sites' is required as INSTA
8181
'allauth.socialaccount.providers.persona',
8282
'allauth.socialaccount.providers.pinterest',
8383
'allauth.socialaccount.providers.reddit',
84+
'allauth.socialaccount.providers.shopify',
8485
'allauth.socialaccount.providers.soundcloud',
8586
'allauth.socialaccount.providers.spotify',
8687
'allauth.socialaccount.providers.stackexchange',

docs/overview.rst

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ Supported Providers
8989

9090
- Reddit (OAuth2)
9191

92+
- Shopify (OAuth2)
93+
9294
- SoundCloud (OAuth2)
9395

9496
- Spotify (OAuth2)

example/example/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
'allauth.socialaccount.providers.openid',
149149
'allauth.socialaccount.providers.persona',
150150
'allauth.socialaccount.providers.reddit',
151+
'allauth.socialaccount.providers.shopify',
151152
'allauth.socialaccount.providers.soundcloud',
152153
'allauth.socialaccount.providers.stackexchange',
153154
'allauth.socialaccount.providers.twitch',

test_settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
'allauth.socialaccount.providers.pinterest',
102102
'allauth.socialaccount.providers.reddit',
103103
'allauth.socialaccount.providers.robinhood',
104+
'allauth.socialaccount.providers.shopify',
104105
'allauth.socialaccount.providers.soundcloud',
105106
'allauth.socialaccount.providers.spotify',
106107
'allauth.socialaccount.providers.stackexchange',

0 commit comments

Comments
 (0)