Skip to content
683 lines (549 sloc) 22.7 KB
from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
except ImportError:
from django.contrib.contenttypes.generic import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.base import ModelBase
from django_facebook import model_managers, settings as facebook_settings
from open_facebook.utils import json, camel_to_underscore
from datetime import timedelta
from django_facebook.utils import compatible_datetime as datetime, \
get_model_for_attribute, get_user_attribute, get_instance_for_attribute, \
try_get_profile, update_user_attributes
from django_facebook.utils import get_user_model, get_profile
from open_facebook.exceptions import OAuthException
import logging
import os
logger = logging.getLogger(__name__)
def get_user_model_setting():
from django.conf import settings
default = 'auth.User'
user_model_setting = getattr(settings, 'AUTH_USER_MODEL', default)
return user_model_setting
def validate_settings():
Checks our Facebook and Django settings and looks for common errors
from django.conf import settings
from django_facebook import settings as facebook_settings
if facebook_settings.FACEBOOK_SKIP_VALIDATE:
# check for required settings
if not facebook_settings.FACEBOOK_APP_ID:
logger.warn('Warning FACEBOOK_APP_ID isnt specified')
if not facebook_settings.FACEBOOK_APP_SECRET:
logger.warn('Warning FACEBOOK_APP_SECRET isnt specified')
# warn on things which will cause bad performance
if facebook_settings.FACEBOOK_STORE_LIKES or facebook_settings.FACEBOOK_STORE_FRIENDS:
if not facebook_settings.FACEBOOK_CELERY_STORE:
msg = '''Storing friends or likes without using Celery will significantly slow down your login
# make sure the context processors are present
required = ['django_facebook.context_processors.facebook',
context_processors = settings.TEMPLATE_CONTEXT_PROCESSORS
for context_processor in required:
if context_processor not in context_processors:
'Required context processor %s wasnt found', context_processor)
required = 'django_facebook.auth_backends.FacebookBackend'
if required not in backends:
logger.warn('Required auth backend %s wasnt found', required)
if facebook_settings.FACEBOOK_PROFILE_IMAGE_PATH:
PROFILE_IMAGE_PATH = os.path.join('images', 'facebook_profiles/%Y/%m/%d')
The user has not connected their profile with Facebook
The user has connected their profile with Facebook, but isn't
setup for Facebook sharing
- sharing is either disabled
- or we have no valid access token
The user is connected to Facebook and sharing is enabled
class BaseFacebookModel(models.Model):
Abstract class to add to your profile or user model.
NOTE: If you don't use this this abstract class, make sure you copy/paste
the fields in.
about_me = models.TextField(blank=True, null=True)
facebook_id = models.BigIntegerField(blank=True, unique=True, null=True)
access_token = models.TextField(
blank=True, help_text='Facebook token for offline access', null=True)
facebook_name = models.CharField(max_length=255, blank=True, null=True)
facebook_profile_url = models.TextField(blank=True, null=True)
website_url = models.TextField(blank=True, null=True)
blog_url = models.TextField(blank=True, null=True)
date_of_birth = models.DateField(blank=True, null=True)
gender = models.CharField(max_length=1, choices=(
('m', 'Male'), ('f', 'Female')), blank=True, null=True)
raw_data = models.TextField(blank=True, null=True)
# the field which controls if we are sharing to facebook
facebook_open_graph = models.NullBooleanField(
help_text='Determines if this user want to share via open graph')
# set to true if we require a new access token
new_token_required = models.BooleanField(default=False,
help_text='Set to true if the access token is outdated or lacks permissions')
def open_graph_new_token_required(self):
Shows if we need to (re)authenticate the user for open graph sharing
reauthentication = False
if self.facebook_open_graph and self.new_token_required:
reauthentication = True
elif self.facebook_open_graph is None:
reauthentication = True
return reauthentication
def __str__(self):
return self.get_user().username
class Meta:
abstract = True
def refresh(self):
Get the latest version of this object from the db
return self.__class__.objects.get(
def get_user(self):
Since this mixin can be used both for profile and user models
if hasattr(self, 'user'):
user = self.user
user = self
return user
def get_user_id(self):
Since this mixin can be used both for profile and user_id models
if hasattr(self, 'user_id'):
user_id = self.user_id
user_id =
return user_id
def facebook_og_state(self):
if not self.facebook_id:
elif self.access_token and self.facebook_open_graph:
return state
def likes(self):
likes = FacebookLike.objects.filter(user_id=self.get_user_id())
return likes
def friends(self):
friends = FacebookUser.objects.filter(user_id=self.get_user_id())
return friends
def disconnect_facebook(self):
self.access_token = None
self.new_token_required = False
self.facebook_id = None
def clear_access_token(self):
self.access_token = None
self.new_token_required = False
def update_access_token(self, new_value):
Updates the access token
# updates to 123 and sets new_token_required to False
:param new_value:
The new value for access_token
self.access_token = new_value
self.new_token_required = False
def extend_access_token(self):
We can extend the token only once per day
Normal short lived tokens last 1-2 hours
Long lived tokens (given by extending) last 60 days
The token can be extended multiple times, supposedly on every visit
''''extending access token for user %s', self.get_user())
results = None
if facebook_settings.FACEBOOK_CELERY_TOKEN_EXTEND:
from django_facebook import tasks
tasks.extend_access_token.delay(self, self.access_token)
results = self._extend_access_token(self.access_token)
return results
def _extend_access_token(self, access_token):
from open_facebook.api import FacebookAuthorization
results = FacebookAuthorization.extend_access_token(access_token)
access_token = results['access_token']
old_token = self.access_token
token_changed = access_token != old_token
message = 'a new' if token_changed else 'the same'
log_format = 'Facebook provided %s token, which expires at %s'
expires_delta = timedelta(days=60), message, expires_delta)
if token_changed:'Saving the new access token')
from django_facebook.signals import facebook_token_extend_finished
sender=get_user_model(), user=self.get_user(), profile=self,
token_changed=token_changed, old_token=old_token
return results
def get_offline_graph(self):
Returns a open facebook graph client based on the access token stored
in the user's profile
from open_facebook.api import OpenFacebook
if self.access_token:
graph = OpenFacebook(access_token=self.access_token)
graph.current_user_id = self.facebook_id
return graph
BaseFacebookProfileModel = BaseFacebookModel
class FacebookModel(BaseFacebookModel):
the image field really destroys the subclassability of an abstract model
you always need to customize the upload settings and storage settings
thats why we stick it in a separate class
override the BaseFacebookProfile if you want to change the image
image = models.ImageField(blank=True, null=True,
upload_to=PROFILE_IMAGE_PATH, max_length=255)
def profile_or_self(self):
user_or_profile_model = get_model_for_attribute('facebook_id')
user_model = get_user_model()
if user_or_profile_model == user_model:
return self
return get_profile(self)
class Meta:
abstract = True
# better name for the mixin now that it can also be used for user models
FacebookProfileModel = FacebookModel
class FacebookUser(models.Model):
Model for storing a users friends
# in order to be able to easily move these to an another db,
# use a user_id and no foreign key
user_id = models.IntegerField()
facebook_id = models.BigIntegerField()
name = models.TextField(blank=True, null=True)
gender = models.CharField(choices=(
('F', 'female'), ('M', 'male')), blank=True, null=True, max_length=1)
objects = model_managers.FacebookUserManager()
class Meta:
unique_together = ['user_id', 'facebook_id']
def __str__(self):
return u'Facebook user %s' %
class FacebookLike(models.Model):
Model for storing all of a users fb likes
# in order to be able to easily move these to an another db,
# use a user_id and no foreign key
user_id = models.IntegerField()
facebook_id = models.BigIntegerField()
name = models.TextField(blank=True, null=True)
category = models.TextField(blank=True, null=True)
created_time = models.DateTimeField(blank=True, null=True)
class Meta:
unique_together = ['user_id', 'facebook_id']
class FacebookProfile(FacebookProfileModel):
Not abstract version of the facebook profile model
Use this by setting
AUTH_PROFILE_MODULE = 'django_facebook.FacebookProfile'
user = models.OneToOneField(get_user_model_setting())
if getattr(settings, 'AUTH_USER_MODEL', None) == 'django_facebook.FacebookCustomUser':
from django.contrib.auth.models import AbstractUser, UserManager
class FacebookCustomUser(AbstractUser, FacebookModel):
The django 1.5 approach to adding the facebook related fields
objects = UserManager()
# add any customizations you like
state = models.CharField(max_length=255, blank=True, null=True)
except ImportError as e:'Couldnt setup FacebookUser, got error %s', e)
class BaseModelMetaclass(ModelBase):
Cleaning up the table naming conventions
def __new__(cls, name, bases, attrs):
super_new = ModelBase.__new__(cls, name, bases, attrs)
module_name = camel_to_underscore(name)
app_label = super_new.__module__.split('.')[-2]
db_table = '%s_%s' % (app_label, module_name)
django_default = '%s_%s' % (app_label, name.lower())
if not getattr(super_new._meta, 'proxy', False):
db_table_is_default = django_default == super_new._meta.db_table
# Don't overwrite when people customize the db_table
if db_table_is_default:
super_new._meta.db_table = db_table
return super_new
class BaseModel(models.Model):
Stores the fields common to all incentive models
__metaclass__ = BaseModelMetaclass
def __str__(self):
Looks at some common ORM naming standards and tries to display those before
default to the django default
attributes = ['name', 'title', 'slug']
name = None
for a in attributes:
if hasattr(self, a):
name = getattr(self, a)
if not name:
name = repr(self.__class__)
return name
class Meta:
abstract = True
class CreatedAtAbstractBase(BaseModel):
Stores the fields common to all incentive models
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
# determine if we should clean this model
auto_clean = False
def save(self, *args, **kwargs):
Allow for auto clean support
if self.auto_clean:
saved =, *args, **kwargs)
return saved
def __str__(self):
Looks at some common ORM naming standards and tries to display those before
default to the django default
attributes = ['name', 'title', 'slug']
name = None
for a in attributes:
if hasattr(self, a):
name = getattr(self, a)
if not name:
name = repr(self.__class__)
return name
def __repr__(self):
return '<%s[%s]>' % (self.__class__.__name__,
class Meta:
abstract = True
class OpenGraphShare(BaseModel):
Object for tracking all shares to Facebook
Used for statistics and evaluating how things are going
I recommend running this in a task
**Example usage**::
from user.models import OpenGraphShare
user = UserObject
url = ''
kwargs = dict(list=url)
share = OpenGraphShare.objects.create(
user = user,
result = share.send()
**Advanced usage**::
share.update(message='Hello world')
Using this model has the advantage that it allows us to
- remove open graph shares (since we store the Facebook id)
- retry open graph shares, which is handy in case of
- updated access tokens (retry all shares from this user in the last facebook_settings.FACEBOOK_OG_SHARE_RETRY_DAYS)
- Facebook outages (Facebook often has minor interruptions, retry in 15m, for max facebook_settings.FACEBOOK_OG_SHARE_RETRIES)
objects = model_managers.OpenGraphShareManager()
user = models.ForeignKey(get_user_model_setting())
# domain stores
action_domain = models.CharField(max_length=255)
facebook_user_id = models.BigIntegerField()
# what we are sharing, dict and object
share_dict = models.TextField(blank=True, null=True)
content_type = models.ForeignKey(ContentType, blank=True, null=True)
object_id = models.PositiveIntegerField(blank=True, null=True)
content_object = GenericForeignKey('content_type', 'object_id')
# completion data
error_message = models.TextField(blank=True, null=True)
last_attempt = models.DateTimeField(
blank=True, null=True, auto_now_add=True)
retry_count = models.IntegerField(blank=True, null=True)
# only written if we actually succeed
share_id = models.CharField(blank=True, null=True, max_length=255)
completed_at = models.DateTimeField(blank=True, null=True)
# tracking removals
removed_at = models.DateTimeField(blank=True, null=True)
# updated at and created at, last one needs an index
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
db_table = facebook_settings.FACEBOOK_OG_SHARE_DB_TABLE
def save(self, *args, **kwargs):
if self.user and not self.facebook_user_id:
profile = get_profile(self.user)
self.facebook_user_id = get_user_attribute(
self.user, profile, 'facebook_id')
return, *args, **kwargs)
def send(self, graph=None, shared_explicitly=False):
result = None
# update the last attempt
self.last_attempt =
# see if the graph is enabled
profile = try_get_profile(self.user)
user_or_profile = get_instance_for_attribute(
self.user, profile, 'access_token')
graph = graph or user_or_profile.get_offline_graph()
user_enabled = shared_explicitly or \
(user_or_profile.facebook_open_graph and self.facebook_user_id)
# start sharing
if graph and user_enabled:
graph_location = '%s/%s' % (
self.facebook_user_id, self.action_domain)
share_dict = self.get_share_dict()
from open_facebook.exceptions import OpenFacebookException
result = graph.set(graph_location, **share_dict)
share_id = result.get('id')
if not share_id:
error_message = 'No id in Facebook response, found %s for url %s with data %s' % (
result, graph_location, share_dict)
raise OpenFacebookException(error_message)
self.share_id = share_id
self.error_message = None
self.completed_at =
except OpenFacebookException as e:
'Open graph share failed, writing message %s' % str(e))
self.error_message = repr(e)
# maybe we need a new access token
new_token_required = self.exception_requires_new_token(
e, graph)
# verify that the token didnt change in the mean time
user_or_profile = user_or_profile.__class__.objects.get(
token_changed = graph.access_token != user_or_profile.access_token'new token required is %s and token_changed is %s',
new_token_required, token_changed)
if new_token_required and not token_changed:
'a new token is required, setting the flag on the user or profile')
# time to ask the user for a new token
update_user_attributes(self.user, profile, dict(
new_token_required=True), save=True)
elif not graph:
self.error_message = 'no graph available'
elif not user_enabled:
self.error_message = 'user not enabled'
return result
def exception_requires_new_token(self, e, graph):
Determines if the exceptions is something which requires us to
ask for a new token. Examples are:
Error validating access token: Session has expired at unix time
1350669826. The current unix time is 1369657666.
(#200) Requires extended permission: publish_actions (error code 200)
new_token = False
if isinstance(e, OAuthException):
new_token = True
# if we have publish actions than our token is ok
# we get in this flow if Facebook mistakenly marks exceptions
# as oAuthExceptions
publish_actions = graph.has_permissions(['publish_actions'])
if publish_actions:
new_token = False
return new_token
def update(self, data, graph=None):
Update the share with the given data
result = None
profile = get_profile(self.user)
graph = graph or profile.get_offline_graph()
# update the share dict so a retry will do the right thing
# just in case we fail the first time
shared = self.update_share_dict(data)
# broadcast the change to facebook
if self.share_id:
result = graph.set(self.share_id, **shared)
return result
def remove(self, graph=None):
if not self.share_id:
raise ValueError('Can only delete shares which have an id')
# see if the graph is enabled
profile = get_profile(self.user)
graph = graph or profile.get_offline_graph()
response = None
if graph:
response = graph.delete(self.share_id)
self.removed_at =
return response
def retry(self, graph=None, reset_retries=False):
if self.completed_at:
raise ValueError('You can\'t retry completed shares')
if reset_retries:
self.retry_count = 0
# handle the case where self.retry_count = None
self.retry_count = self.retry_count + 1 if self.retry_count else 1
# actually retry now
result = self.send(graph=graph)
return result
def set_share_dict(self, share_dict):
share_dict_string = json.dumps(share_dict)
self.share_dict = share_dict_string
def get_share_dict(self):
share_dict_string = self.share_dict
share_dict = json.loads(share_dict_string)
return share_dict
def update_share_dict(self, share_dict):
old_share_dict = self.get_share_dict()
return old_share_dict
Something went wrong with that request. Please try again.