Foursquare authentication #195

Merged
merged 32 commits into from Jun 5, 2012

Conversation

Projects
None yet
2 participants
Contributor

mihanovak commented May 26, 2012

Foursquare authentication. We will need authentication for issue #152.

@mitar mitar commented on an outdated diff May 27, 2012

piplmesh/account/backends.py
@@ -101,3 +101,42 @@ def authenticate(self, twitter_token=None, request=None):
user.twitter_token_secret = twitter_token.secret
user.save()
return user
+
+class FoursquareBackend(MongoEngineBackend):
+ """
+ Foursquare backend for authentication.
+ """
+
+ def authenticate(self, foursquare_token=None, request=None):
+
@mitar

mitar May 27, 2012

Owner

No empty line.

@mitar mitar and 1 other commented on an outdated diff May 27, 2012

piplmesh/account/backends.py
@@ -101,3 +101,42 @@ def authenticate(self, twitter_token=None, request=None):
user.twitter_token_secret = twitter_token.secret
user.save()
return user
+
+class FoursquareBackend(MongoEngineBackend):
+ """
+ Foursquare backend for authentication.
+ """
+
+ def authenticate(self, foursquare_token=None, request=None):
+
+ args = {
+ 'client_id': settings.FOURSQUARE_CLIENT_ID,
@mitar

mitar May 27, 2012

Owner

Please order field similar to how they are in Facebook backend.

@mihanovak

mihanovak May 28, 2012

Contributor

Done.

@mitar mitar and 1 other commented on an outdated diff May 27, 2012

piplmesh/account/backends.py
+ }
+
+ # Retrieve access token
+ url = urllib.urlopen('https://foursquare.com/oauth2/access_token?%s' % urllib.urlencode(args)).read()
+ response = json.loads(url)
+ access_token = response.get('access_token')
+
+ # Retrieve user's public profile information
+ data = urllib.urlopen('https://api.foursquare.com/v2/users/self?oauth_token=%s' % access_token)
+ foursquare_data = json.load(data)
+ foursquare_data = foursquare_data.get('response').get('user')
+
+ user, created = self.user_class.objects.get_or_create(
+ foursquare_id=foursquare_data.get('id'),
+ defaults={
+ 'username': foursquare_data.get('firstName') + foursquare_data.get('lastName'),
@mitar

mitar May 27, 2012

Owner

There is nothing better for username?

@mihanovak

mihanovak May 27, 2012

Contributor

Maybe id?

@mitar

mitar May 27, 2012

Owner

And what it contains?

@mihanovak

mihanovak May 27, 2012

Contributor

It is only eight digit number. Here is example of response:

response: {
      user: {
            id: "28727544"
            firstName: "FirstName"
            lastName: "LastName"
            relationship: "self"
            photo: "https://foursquare.com/img/blank_boy.png" 
      }

      ....

      contact: {
            email: "example@example.com"
      }

      ....

}
@mitar

mitar May 27, 2012

Owner

OK. Then do concatenation of first and last name.

@mitar mitar commented on an outdated diff May 27, 2012

piplmesh/account/models.py
@@ -43,6 +43,9 @@ class User(auth.User):
twitter_token_key = mongoengine.StringField(max_length=150)
twitter_token_secret = mongoengine.StringField(max_length=150)
+ foursquare_id = mongoengine.IntField()
@mitar

mitar May 27, 2012

Owner

I suggest you use string field. You never know what we will get as ID. ;-)

@mitar mitar and 1 other commented on an outdated diff May 27, 2012

piplmesh/account/templatetags/foursquare.py
@@ -0,0 +1,12 @@
+import urllib, json
+
+from django import template
+from django.conf import settings
+
+register = template.Library()
+
+@register.simple_tag
+def foursquare_picture(foursquare_token):
+ data = urllib.urlopen('https://api.foursquare.com/v2/users/self?oauth_token=%s' % foursquare_token)
@mitar

mitar May 27, 2012

Owner

For every template tag rendering we will be doing a request to foursquare.com?

@mihanovak

mihanovak May 28, 2012

Contributor

Corrected.

@mitar mitar and 1 other commented on an outdated diff May 27, 2012

piplmesh/account/views.py
+class FoursquareCallbackView(generic_views.RedirectView):
+ """
+ Authentication callback. Redirects user to LOGIN_REDIRECT_URL.
+ """
+
+ permanent = False
+ # TODO: Redirect users to the page they initially came from
+ url = settings.FOURSQUARE_LOGIN_REDIRECT
+
+ def get(self, request, *args, **kwargs):
+ if 'code' in request.GET:
+ user = auth.authenticate(foursquare_token=request.GET['code'], request=request)
+ auth.login(request, user)
+ return super(FoursquareCallbackView, self).get(request, *args, **kwargs)
+ else:
+ return super(FoursquareCallbackView, self).get(request, *args, **kwargs)
@mitar

mitar May 27, 2012

Owner

Add TODO about error message displayed to the user.

@mihanovak

mihanovak May 28, 2012

Contributor

Added.

@mitar mitar commented on an outdated diff May 27, 2012

piplmesh/settings.py
@@ -271,6 +272,11 @@
TWITTER_CONSUMER_SECRET = 'Dv80Q51jx8FWDInmZCGZs8AKDnRwAdrS0lxgZA4NWs'
TWITTER_LOGIN_REDIRECT = '/'
+#Foursquare settings
@mitar

mitar May 27, 2012

Owner

Space after #.

Owner

mitar commented May 28, 2012

Please update with latest from main repository.

mihanovak added some commits May 28, 2012

@mihanovak mihanovak Merge branch 'master' into foursquare_auth
Conflicts:
	piplmesh/frontend/templates/header.html
	piplmesh/urls.py
c1766a7
@mihanovak mihanovak Changed comment. bc6531c
Contributor

mihanovak commented May 28, 2012

Updated from main repository.

@mitar mitar and 1 other commented on an outdated diff May 29, 2012

piplmesh/account/backends.py
+ 'client_id': settings.FOURSQUARE_CLIENT_ID,
+ 'client_secret': settings.FOURSQUARE_CLIENT_SECRET,
+ 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')),
+ 'code': foursquare_token,
+ 'grant_type': 'authorization_code',
+ }
+
+ # Retrieve access token
+ url = urllib.urlopen('https://foursquare.com/oauth2/access_token?%s' % urllib.urlencode(args)).read()
+ response = json.loads(url)
+ access_token = response.get('access_token')
+
+ # Retrieve user's public profile information
+ data = urllib.urlopen('https://api.foursquare.com/v2/users/self?oauth_token=%s' % access_token)
+ foursquare_data = json.load(data)
+ foursquare_data = foursquare_data.get('response').get('user')
@mitar

mitar May 29, 2012

Owner

Create new variable foursquare_user for this data.

Can you also please describe in the comment what all fields are in foursquare_user and what they mean? Like for each field in its line. So that later on other developers can know what is provided if it will be needed.

@mitar

mitar May 29, 2012

Owner

But here is OK if it blows. We want this data in there. So use foursquare_data['response']['user'].

@mihanovak

mihanovak May 30, 2012

Contributor

Done.

@mitar mitar and 1 other commented on an outdated diff May 29, 2012

piplmesh/account/backends.py
+ url = urllib.urlopen('https://foursquare.com/oauth2/access_token?%s' % urllib.urlencode(args)).read()
+ response = json.loads(url)
+ access_token = response.get('access_token')
+
+ # Retrieve user's public profile information
+ data = urllib.urlopen('https://api.foursquare.com/v2/users/self?oauth_token=%s' % access_token)
+ foursquare_data = json.load(data)
+ foursquare_data = foursquare_data.get('response').get('user')
+
+ user, created = self.user_class.objects.get_or_create(
+ foursquare_id=foursquare_data.get('id'),
+ defaults={
+ 'username': foursquare_data.get('firstName') + foursquare_data.get('lastName'),
+ 'first_name': foursquare_data.get('firstName'),
+ 'last_name': foursquare_data.get('lastName'),
+ 'email': foursquare_data.get('contact').get('email'),
@mitar

mitar May 29, 2012

Owner

This could blow. Use:

foursquare_data.get('contact', {}).get('email')

None value, which is default, does not have get method. Same elsewhere.

@mihanovak

mihanovak May 30, 2012

Contributor

Done.

@mitar mitar and 1 other commented on an outdated diff May 29, 2012

piplmesh/account/views.py
+ 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')),
+ }
+ return "https://foursquare.com/oauth2/authenticate?%(args)s" % {'args': urllib.urlencode(args)}
+
+class FoursquareCallbackView(generic_views.RedirectView):
+ """
+ Authentication callback. Redirects user to LOGIN_REDIRECT_URL.
+ """
+
+ permanent = False
+ # TODO: Redirect users to the page they initially came from
+ url = settings.FOURSQUARE_LOGIN_REDIRECT
+
+ def get(self, request, *args, **kwargs):
+ if 'code' in request.GET:
+ user = auth.authenticate(foursquare_token=request.GET['code'], request=request)
@mitar

mitar May 29, 2012

Owner

Please add assert that user is authenticated. See TwitterCallbackView.

@mihanovak

mihanovak May 30, 2012

Contributor

Done.

Owner

mitar commented May 29, 2012

Otherwise looks good. Merge again with latest from the main repository. You will see there are some changes to how user images are done.

Owner

mitar commented Jun 1, 2012

Merge please with latest from main repository, again. (Yes, development is fast this days.)

Is there anything else still pending or should I review the pull request otherwise?

@mihanovak mihanovak commented on an outdated diff Jun 1, 2012

piplmesh/account/views.py
+ }
+ return "https://foursquare.com/oauth2/authenticate?%s" % urllib.urlencode(args)
+
+class FoursquareCallbackView(generic_views.RedirectView):
+ """
+ Authentication callback. Redirects user to LOGIN_REDIRECT_URL.
+ """
+
+ permanent = False
+ # TODO: Redirect users to the page they initially came from
+ url = settings.FOURSQUARE_LOGIN_REDIRECT
+
+ def get(self, request, *args, **kwargs):
+ if 'code' in request.GET:
+ user = auth.authenticate(foursquare_token=request.GET['code'], request=request)
+ assert user.is_authenticated()
@mihanovak

mihanovak Jun 1, 2012

Contributor

I don't know why I get assertion error if I login with foursquare. Any hint? :)

Traceback:

Environment:

Request Method: GET
Request URL: http://127.0.0.1:8000/foursquare/callback/?code=ZCN4PDJ3AAS5QK0IMFIBL0TEXLT0WQHDFXBTK5YPESVUGC2F

Django Version: 1.4
Python Version: 2.7.2
Installed Applications:
('piplmesh.account',
 'piplmesh.api',
 'piplmesh.frontend',
 'piplmesh.nodes',
 'piplmesh.utils',
 'django.contrib.messages',
 'django.contrib.sessions',
 'django.contrib.staticfiles',
 'pushserver',
 'djcelery',
 'tastypie',
 'tastypie_mongoengine')
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'piplmesh.account.middleware.LazyUserMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'piplmesh.account.middleware.UserBasedLocaleMiddleware',
 'piplmesh.frontend.middleware.NodesMiddleware')


Traceback:
File "C:\env_piplmesh\lib\site-packages\django\core\handlers\base.py" in get_response
  111.                         response = callback(request, *callback_args, **callback_kwargs)
File "C:\env_piplmesh\lib\site-packages\django\views\generic\base.py" in view
  48.             return self.dispatch(request, *args, **kwargs)
File "C:\env_piplmesh\lib\site-packages\django\views\generic\base.py" in dispatch
  69.         return handler(request, *args, **kwargs)
File "D:\PiplMesh\PiplMesh2\piplmesh\account\views.py" in get
  120.             assert user.is_authenticated()

Exception Type: AssertionError at /foursquare/callback/
Exception Value: 
@mihanovak

mihanovak Jun 1, 2012

Contributor

Solved! I forgot to update is_authenticated in models.py.

Contributor

mihanovak commented Jun 1, 2012

That should be it.

@mitar mitar and 1 other commented on an outdated diff Jun 1, 2012

piplmesh/account/backends.py
+ firstName - A user's first name.
+ lastName - A user's last name.
+ homeCity - User's home city.
+ photo - URL of a profile picture for this user.
+ gender - A user's gender: male, female, or none.
+ relationship - (Optional) The relationship of the acting user (me) to this user (them).
+
+ Present in user details:
+ type, contact, pings, badges, checkins, mayorships, tips, todos, photos, friends, followers,
+ requests, pageInfo.
+ """
+
+ user, created = self.user_class.objects.get_or_create(
+ foursquare_id=foursquare_user.get('id'),
+ defaults={
+ 'username': foursquare_user.get('firstName', {}) + foursquare_user.get('lastName', {}),
@mitar

mitar Jun 1, 2012

Owner

Why {} default? It should be ''. Empty string.

@mihanovak

mihanovak Jun 1, 2012

Contributor

Changed to ''.

@mitar mitar commented on an outdated diff Jun 1, 2012

piplmesh/account/backends.py
+ lastName - A user's last name.
+ homeCity - User's home city.
+ photo - URL of a profile picture for this user.
+ gender - A user's gender: male, female, or none.
+ relationship - (Optional) The relationship of the acting user (me) to this user (them).
+
+ Present in user details:
+ type, contact, pings, badges, checkins, mayorships, tips, todos, photos, friends, followers,
+ requests, pageInfo.
+ """
+
+ user, created = self.user_class.objects.get_or_create(
+ foursquare_id=foursquare_user.get('id'),
+ defaults={
+ 'username': foursquare_user.get('firstName', {}) + foursquare_user.get('lastName', {}),
+ 'first_name': foursquare_user.get('firstName', {}),

@mitar mitar commented on an outdated diff Jun 1, 2012

piplmesh/account/backends.py
+ photo - URL of a profile picture for this user.
+ gender - A user's gender: male, female, or none.
+ relationship - (Optional) The relationship of the acting user (me) to this user (them).
+
+ Present in user details:
+ type, contact, pings, badges, checkins, mayorships, tips, todos, photos, friends, followers,
+ requests, pageInfo.
+ """
+
+ user, created = self.user_class.objects.get_or_create(
+ foursquare_id=foursquare_user.get('id'),
+ defaults={
+ 'username': foursquare_user.get('firstName', {}) + foursquare_user.get('lastName', {}),
+ 'first_name': foursquare_user.get('firstName', {}),
+ 'last_name': foursquare_user.get('lastName', {}),
+ 'email': foursquare_user.get('contact', {}).get('email'),
@mitar

mitar Jun 1, 2012

Owner

But here is OK. Because you want for second get to operate on dict.

@mitar mitar and 1 other commented on an outdated diff Jun 1, 2012

piplmesh/account/backends.py
+ gender - A user's gender: male, female, or none.
+ relationship - (Optional) The relationship of the acting user (me) to this user (them).
+
+ Present in user details:
+ type, contact, pings, badges, checkins, mayorships, tips, todos, photos, friends, followers,
+ requests, pageInfo.
+ """
+
+ user, created = self.user_class.objects.get_or_create(
+ foursquare_id=foursquare_user.get('id'),
+ defaults={
+ 'username': foursquare_user.get('firstName', {}) + foursquare_user.get('lastName', {}),
+ 'first_name': foursquare_user.get('firstName', {}),
+ 'last_name': foursquare_user.get('lastName', {}),
+ 'email': foursquare_user.get('contact', {}).get('email'),
+ 'gender': foursquare_user.get('gender', {}),
@mitar

mitar Jun 1, 2012

Owner

Probably no default is needed. Or check how we configure gender elsewhere.

@mihanovak

mihanovak Jun 1, 2012

Contributor

Done.

@mitar mitar and 1 other commented on an outdated diff Jun 1, 2012

piplmesh/account/views.py
@@ -90,6 +90,41 @@ def get(self, request, *args, **kwargs):
# TODO: Use information provided from twitter as to why the login was not successful
return super(TwitterCallbackView, self).get(request, *args, **kwargs)
+class FoursquareLoginView(generic_views.RedirectView):
+ """
+ This view authenticates the user via Foursquare.
+ """
+
+ permanent = False
+
+ def get_redirect_url(self, **kwargs):
+ args = {
+ 'client_id': settings.FOURSQUARE_CLIENT_ID,
+ 'response_type': 'code',
+ 'redirect_uri': self.request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')),
+ }
+ return "https://foursquare.com/oauth2/authenticate?%s" % urllib.urlencode(args)
@mitar

mitar Jun 1, 2012

Owner

Constant string.

@mihanovak

mihanovak Jun 1, 2012

Contributor

Done.

@mitar mitar and 1 other commented on an outdated diff Jun 1, 2012

piplmesh/account/backends.py
+ 'client_secret': settings.FOURSQUARE_CLIENT_SECRET,
+ 'redirect_uri': request.build_absolute_uri(urlresolvers.reverse('foursquare_callback')),
+ 'code': foursquare_token,
+ 'grant_type': 'authorization_code',
+ }
+
+ # Retrieve access token
+ url = urllib.urlopen('https://foursquare.com/oauth2/access_token?%s' % urllib.urlencode(args)).read()
+ response = json.loads(url)
+ access_token = response.get('access_token')
+
+ # Retrieve user's public profile information
+ data = urllib.urlopen('https://api.foursquare.com/v2/users/self?%s' % urllib.urlencode({'oauth_token': access_token}))
+ foursquare_data = json.load(data)
+ foursquare_user = foursquare_data['response']['user']
+ """
@mitar

mitar Jun 1, 2012

Owner

Move all this to main docstring documentation for this backend. You can also create some initial description (like, "Foursquare user fields are:").

@mihanovak

mihanovak Jun 1, 2012

Contributor

Done.

Owner

mitar commented Jun 1, 2012

Otherwise looks good.

Contributor

mihanovak commented Jun 3, 2012

Something else to fix?

Owner

mitar commented Jun 4, 2012

Update from main repository and do your authentication similar to how others are done now.

Contributor

mihanovak commented Jun 4, 2012

Updated from main repository, corrected authentication and fixed one bug.

@mitar mitar and 1 other commented on an outdated diff Jun 4, 2012

piplmesh/account/backends.py
@@ -197,6 +197,54 @@ def authenticate(self, google_access_token, request):
return user
+class FoursquareBackend(MongoEngineBackend):
+ """
+ Foursquare authentication.
+
+ Foursquare user fields are:
+ id - A unique identifier for this user.
+ firstName - A user's first name.
+ lastName - A user's last name.
+ homeCity - User's home city.
+ photo - URL of a profile picture for this user.
+ gender - A user's gender: male, female, or none.
+ relationship - (Optional) The relationship of the acting user (me) to this user (them).
+
+ Fields present in user details:
@mitar

mitar Jun 4, 2012

Owner

What are user details?

@mihanovak

mihanovak Jun 4, 2012

Contributor

Corrected description.

@mitar mitar and 1 other commented on an outdated diff Jun 4, 2012

piplmesh/account/backends.py
+ lastName - A user's last name.
+ homeCity - User's home city.
+ photo - URL of a profile picture for this user.
+ gender - A user's gender: male, female, or none.
+ relationship - (Optional) The relationship of the acting user (me) to this user (them).
+
+ Fields present in user details:
+ type, contact, pings, badges, checkins, mayorships, tips, todos, photos, friends, followers,
+ requests, pageInfo.
+ """
+
+ def authenticate(self, foursquare_access_token, request):
+ # Retrieve user's profile information
+ # TODO: Handle error, what if request was denied?
+ foursquare_user_data = json.load(urllib.urlopen('https://api.foursquare.com/v2/users/self?%s' % urllib.urlencode({'oauth_token': foursquare_access_token})))
+ foursquare_profile_data = foursquare_user_data['response']['user']
@mitar

mitar Jun 4, 2012

Owner

What is difference between user data and profile data?

@mihanovak

mihanovak Jun 4, 2012

Contributor

In user data are all data including this:

meta: {
    code: 200
}
notifications: [
    {
        type: "notificationTray"
        item: {
            unreadCount: 0
        }
    }
]

I think that we don't need this data?

@mitar

mitar Jun 4, 2012

Owner

Then combine this into one line. ;-)

@mihanovak

mihanovak Jun 4, 2012

Contributor

Corrected :).

@mitar mitar commented on the diff Jun 4, 2012

piplmesh/account/models.py
elif self.google_profile_data:
- return self.google_profile_data.picture
+ return self.google_profile_data.get('picture')
@mitar

mitar Jun 4, 2012

Owner

Hm, so you are saying that google_profile_data is a dict and not an object? OK. Then we have to fix this all around. ;-)

@mihanovak

mihanovak Jun 4, 2012

Contributor

Yes. Before the change I got this error:

Exception Type: AttributeError at /
Exception Value: 'BaseDict' object has no attribute 'picture'

@mitar mitar commented on the diff Jun 4, 2012

piplmesh/account/backends.py
+ photo - URL of a profile picture for this user.
+ gender - A user's gender: male, female, or none.
+ relationship - (Optional) The relationship of the acting user (me) to this user (them).
+ type - One of page, celebrity, or user. Users can establish following relationships with celebrities.
+ contact - An object containing none, some, or all of twitter, facebook, email, and phone. Both are strings.
+ pings - (Optional) Pings from this user.
+ badges - Contains the count of badges for this user. May eventually contain some selected badges
+ checkins - Contains the count of checkins by this user. May contain the most recent checkin as an array.
+ mayorships - Contains the count of mayorships for this user and an items array that for now is empty.
+ tips - Contains the count of tips from this user. May contain an array of selected tips as items.
+ todos - Contains the count of todos this user has. May contain an array of selected todos as items.
+ photos - Contains the count of photos this user has. May contain an array of selected photos as items.
+ friends - Contains count of friends for this user and groups of users who are friends.
+ followers - Contains count of followers for this user, if they are a page or celebrity
+ requests - Contains count of pending friend requests for this user.
+ pageInfo - Contains a detailed page, if they are a page.
@mitar

mitar Jun 4, 2012

Owner

So all those are available in foursquare_profile_data?

@mihanovak

mihanovak Jun 5, 2012

Contributor

Yes.

@mitar mitar commented on the diff Jun 4, 2012

piplmesh/account/backends.py
@@ -197,6 +197,62 @@ def authenticate(self, google_access_token, request):
return user
+class FoursquareBackend(MongoEngineBackend):
+ """
+ Foursquare authentication.
+
+ Foursquare user fields are:
@mitar

mitar Jun 4, 2012

Owner

Foursquare profile data fields are:

Please use the same way of listing (with :) as other backends do.

mitar merged commit 755e3ba into wlanslovenija:master Jun 5, 2012

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment