Facebook Login + User Groups + Profiles & Folders

So excuse the title, but for lack of a better way to describe everything together, I'll explain a little more in depth.

So for a project I'm currently working on, the customer has requested a Facebook login for its users, things get complicated because they also want a user profile page and a folder or storage area to upload images which can be tagged and displayed throughout the site based on some prerequisites...

I suppose the question here is how to best implement this idea, yes yes, that's pretty broad I know... I've already taken a look at the cs.auth.facebook package, but I find it lacks any flexibility in controlling the API requests without modifying the actual code. The default userschema is too minimalist for my needs, I tried creating a custom userschema.xml so as to define some custom fields and parameters, but it's still not quite what I need. I suppose it's more tailored to a default site user rather than a custom one..?

This leaves me with Products.membrane which if I'm honest, I've yet to test fully, but looks like what I may need.

So following on from this, what would be your approach to creating a custom user with a Facebook login and storing some files that user may upload. The following requirements should be met.

  • Facebook login
    • Custom fields to store user defined profile data
    • A user profile page with overview
    • A user gallery

Just some hints or pointers would be useful! Thanks peeps!

You can use event subscribers to get the user information that you need when the user logs into your site. There are two interfaces that describe events for which you can subscribe and get the information you need from Facebook:

Products.PlonePAS.interfaces.events.IUserInitialLoginInEvent
Products.PluggableAuthService.interfaces.events.IUserLoggedInEvent

In some of our sites we have done so to update user data when using cs.auth.facebook.

Thanks for the fast reply, that will definitely be useful... But I'm still trying to figure out the best way to create a profile page then, or what the best way of doing so would be. Take for example this forum, when you navigate to /users/erral for example, I can see your profile. Assuming you're logged in, you can also add some content or change some field data. So how would you go about this with the Facebook login..?

I guess you need to create some folder users and in there return a dynamic view-name from the username? Could you/someone please elaborate a little on this..?

Thanks!

In Plone you can create Member folders, by default in /Members/erral, but the path can be configured, and even the content-type of that folder can be configured.

You can go to yoursite.com/@security-controlpanel and enable creating member folders. The content-type of the folder can be configured here: yoursite.com/portal_membership/manage_mapRoles

Perhaps the use of Products.membrane could solve also your use-case but I have never used it, neither with plain Plone nor with facebook users.

You can also check if pas.plugins.authomatic (which provides additional user-validation sources) works for you.

So I did as you said and everything is running smoothly, managed to setup a user folder environment with a custom Content-Type to handle any extra profile fields I'd like to store. So thanks for that!
My next question comes back around to the cs.auth.facebook plug-in...

At the moment it only requests the following:

userId = profile.get('id').encode("utf-8")
name = profile.get('name').encode("utf-8")
email = profile.get('email', '').encode("utf-8")
username = profile.get('username', '').encode("utf-8")
location = profile.get('location', {}).get('name', '').encode("utf-8")

However, reading through the API docs shows that there's a slew of other fields I can use. Do you know if there's some way of requesting extra data with the plug-in, without having to modify the code..? Just wondering, in case you've had some experience using it..?

Thanks for the help, really!

When we developed the plugin, we decided to get the minimal information possible from Facebook to be able to identify the user and let event subscribers or other views to get extra information. Moreover at that time some extra permissions were required to get some other information from API, so we let it as it is.

At this moment we get that information you say and also the profile photo using login event subscribers.

You may also want to have a look at pas.plugins.authomatic for the login part with facebook using OAuth2.

OK, I think I'll use a combination of both plug-ins, maybe I can integrate a sync button or something... @erral - I was wondering why the other FB Graph fields weren't being returned, so I guess you need to request them separately... So for this maybe I'll use the pas.plugins.authomatic plug-in.

I have just 1 final question to wrap this up, really great help you guys! The profile_image saved by cs.auth.facebook, when calling the MemberData object for the user, using the getProperty('portrait') method, returns some image binary... Is there a way of displaying this in the template or do I need to convert and save the binary as a NamedBlobImage object..?

Edit:

I also tried using getPersonalPortrait with api.portal.get_tool('portal_membership') accompanied by the current user ID, but this just returns the defaultPortrait.png image...

If you just save the portrait data in the member data, you need to use the portrait url:

http://yoursite.com/portal_memberdata/portraits/erral

If you are saving it in the member folder as a field it will be already available as a field, so you can render it using the standard @@images view and plone.app.imaging.

Did as above, everything is working exactly how I planned :smiley:
Thanks guys for all the help!

Just for reference, I wanted to request more data from Facebook, so I created a little method to call in the event handler.

from cs.auth.facebook.login import FACEBOOK_ACCESS_TOKEN_URL
from plone import api

import json
import urllib


def get_auth(context=None, user=None, fields=None):
    """Make blind profile request using login ID from authentication"""
    profile_url = 'https://graph.facebook.com/{0}'.format(user)
    profile_fields = ','.join(fields)
    # App defaults
    fb_app_id = api.portal.get_registry_record(
        'cs.auth.facebook.controlpanel.IFacebookloginSettings.fb_app_id'
    )
    fb_app_secret = api.portal.get_registry_record(
        'cs.auth.facebook.controlpanel.IFacebookloginSettings.fb_app_secret'
    )
    # Request defaults
    args = {'client_id': fb_app_id,
            'client_secret': fb_app_secret,
            'grant_type': 'client_credentials',
            'redirect_uri': context.absolute_url()}
    # Request token
    response = json.loads(urllib.urlopen(
        '{0}?{1}'.format(FACEBOOK_ACCESS_TOKEN_URL, urllib.urlencode(args))
    ).read())
    token = str(response["access_token"])
    # Request fields
    request = json.load(urllib.urlopen(
        '{0}?fields={1}&{2}'.format(profile_url,
                                    profile_fields,
                                    urllib.urlencode({'access_token': token}))
    ))
    return request

Then in the event I ran...

session = self.object.REQUEST.SESSION
portal = api.portal.get()
current_user = api.user.get_current()
...
# Checks if user is of type FacebookUser
...
available_fields = dict()
fields = [
    'location',
    'address',
    'locale',
    'first_name',
    'last_name',
    'about',
    'email',
]
values = get_auth(context=portal, user=current_user.id, fields=fields)
for field in fields:
    if field in values:
        available_fields[field] = values[field]
session.set('fb_data', available_fields)

From there I redirect to the member folder and assign the fields with any available session data.
There's probably a better way to do this, but it seems to work so... For now, mission accomplished.