+
+You will be shown a page like the one in the django admin
+that shows you which entries would be deleted on a deauthorization callback.
\ No newline at end of file
diff --git a/facebook/feincms/middleware.py b/facebook/feincms/middleware.py
index 4c32dc3..e0dab12 100644
--- a/facebook/feincms/middleware.py
+++ b/facebook/feincms/middleware.py
@@ -9,6 +9,9 @@ class PreventForeignApp(object):
associated page """
def process_request(self, request):
+ if 'deauthorize' in request.path:
+ return None
+
if 'facebook' in request.session and 'signed_request' in request.session['facebook']:
facebook_page = get_page_from_request(request)
signed_request = request.session['facebook']['signed_request']
diff --git a/facebook/feincms/views.py b/facebook/feincms/views.py
index dc288ae..bd8fafe 100644
--- a/facebook/feincms/views.py
+++ b/facebook/feincms/views.py
@@ -1,4 +1,5 @@
from django.conf import settings
+from django.http import HttpResponse
from django.shortcuts import redirect
from feincms.module.page.models import Page
@@ -9,8 +10,9 @@ def redirect_to_slug(request):
try:
facebook_page = request.session['facebook']['signed_request']['page']
- except e:
- return '' % e
+ except KeyError as e:
+ return HttpResponse('' % e)
+
page = Page.objects.from_request(request, best_match=True)
if facebook_page['admin'] and facebook_page['liked']:
@@ -32,4 +34,4 @@ def redirect_to_slug(request):
try:
return redirect(page.get_children().filter(slug='unliked')[0])
except IndexError:
- return ''
\ No newline at end of file
+ return HttpResponse('')
\ No newline at end of file
diff --git a/facebook/graph.py b/facebook/graph.py
index cc862d7..25a046b 100644
--- a/facebook/graph.py
+++ b/facebook/graph.py
@@ -84,9 +84,19 @@ def get_objects(self, ids, **args):
return self.request("", args)
def get_connections(self, id, connection_name, **args):
- """Fetchs the connections for given object."""
+ """Fetches the connections for given object."""
return self.request(id + "/" + connection_name, args)
+ def batch_request(self, batch):
+ """ Combines multiple requests into one.
+ Batch must be a List of Dicts in the format:
+ [{"method": "GET", "relative_url": "me"},
+ {"method": "GET", "relative_url": "me/friends?limit=50"}]
+ It returns a list of response dicts:
+ http://developers.facebook.com/docs/reference/api/batch/
+ """
+ return self.request('', None, {'batch': json.dumps(batch)})
+
def put_object(self, parent_object, connection_name, **data):
"""Writes the given object to the graph, connected to the given parent.
diff --git a/facebook/middleware.py b/facebook/middleware.py
index e8e5bbd..6f67389 100644
--- a/facebook/middleware.py
+++ b/facebook/middleware.py
@@ -31,6 +31,10 @@ def process_request(self, request):
logger.debug('Request Method = %s\n, AccessToken=%s' % (request.method, fb.access_token))
+ if 'deauthorize' in request.path:
+ logger.debug('deauthorize call, SignedRequestMiddleware returning...')
+ return None
+
if 'feincms' in settings.INSTALLED_APPS:
# if feincms is installed, try to get the application from the page
from facebook.feincms.utils import get_application_from_request
diff --git a/facebook/modules/base.py b/facebook/modules/base.py
index 54ce763..3d86648 100644
--- a/facebook/modules/base.py
+++ b/facebook/modules/base.py
@@ -120,6 +120,9 @@ def to_django(self, response, graph=None, save_related=True):
obj.get_from_facebook(graph=graph, save=True)
elif created and not save_related and 'name' in val:
obj._name = val['name']
+ elif not created and 'name' in val:
+ # make sure name is defined:
+ obj._name = val['name']
elif isinstance(fieldclass, models.DateField):
# Check for Birthday:
setattr(self, field, datetime.strptime(val, "%m/%d/%Y").date())
@@ -133,8 +136,8 @@ def to_django(self, response, graph=None, save_related=True):
setattr(self, '_%s_id' % prop, val['id'])
- def save_from_facebook(self, response, update_slug=False, save_related=True):
- self.to_django(response, save_related)
+ def save_from_facebook(self, response, update_slug=False, save_related=True, graph=None):
+ self.to_django(response, graph=graph, save_related=save_related)
if update_slug or not self.slug:
self.generate_slug()
self.save()
diff --git a/facebook/modules/connections/milestone/__init__.py b/facebook/modules/connections/milestone/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/facebook/modules/connections/milestone/models.py b/facebook/modules/connections/milestone/models.py
new file mode 100644
index 0000000..689cd36
--- /dev/null
+++ b/facebook/modules/connections/milestone/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from facebook.modules.base import Base
+
+class Milestone(Base):
+ id = models.BigIntegerField(primary_key=True)
+ # from is a Generic FK to User or Page.
+ _from = generic.GenericForeignKey('__profile_type', '__profile_id')
+ __profile_id = models.BigIntegerField()
+ __profile_type = models.ForeignKey(ContentType)
+ _created_time = models.DateTimeField(blank=True, null=True)
+ _updated_time = models.DateTimeField(blank=True, null=True)
+ _start_time = models.DateTimeField(blank=True, null=True)
+ _end_time = models.DateTimeField(blank=True, null=True)
+ _title = models.CharField(max_lenght=255, blank=True, null=True)
+ _description = models.TextField()
+
+ class Meta:
+ ordering = ['-_start_time']
+ get_latest_by = '_start_time'
+ verbose_name = _('Milestone')
+ verbose_name_plural = _('Milestones')
+
+ def __unicode__(self):
+ return self.title
\ No newline at end of file
diff --git a/facebook/modules/connections/post/admin.py b/facebook/modules/connections/post/admin.py
index 317b31d..cdd351c 100644
--- a/facebook/modules/connections/post/admin.py
+++ b/facebook/modules/connections/post/admin.py
@@ -1,5 +1,8 @@
+from django.contrib.admin.options import InlineModelAdmin
from facebook.modules.base import AdminBase
+from django.contrib.contenttypes.models import ContentType
+
class PostAdmin(AdminBase):
def picture_link(self, obj):
return ' ' % (obj._picture)
@@ -17,3 +20,4 @@ def icon_link(self, obj):
'_properties', '_actions', '_privacy', '_likes', '_comments', '_targeting')
date_hierarchy = '_updated_time'
list_filter = ('_type',)
+
diff --git a/facebook/modules/connections/post/models.py b/facebook/modules/connections/post/models.py
index 027c72c..2add096 100644
--- a/facebook/modules/connections/post/models.py
+++ b/facebook/modules/connections/post/models.py
@@ -1,5 +1,7 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
from facebook.fields import JSONField
from facebook.modules.base import Base
@@ -55,7 +57,7 @@ class Facebook:
def __unicode__(self):
- return u'%s, %s %s' % (self.id, self._message[:50], self._picture)
+ return u'%s, %s' % (self.id, self._message[:50])
# Deal with changes from Facebook
@property
@@ -70,6 +72,8 @@ def _subject(self):
# Note has no type attribute.
def guess_type(self):
+ if self._type:
+ return self._type
if self._subject:
self._type = 'note'
elif self._story and self._picture:
@@ -126,9 +130,26 @@ def status(self):
class Post(PostBase):
+
class Meta(PostBase.Meta):
app_label = 'facebook'
verbose_name = _('Post')
verbose_name_plural = _('Posts')
abstract = False
+
+class PagePost(models.Model):
+ """ This is a generic intermediate model to attach posts to Pages or Profiles
+ """
+ post = models.ForeignKey(Post)
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.BigIntegerField()
+ to = generic.GenericForeignKey()
+ created = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ app_label = 'facebook'
+ unique_together = (('post', 'content_type', 'object_id'),)
+
+ def __unicode__(self):
+ return u'%s to %s' % (self.post, self.to)
diff --git a/facebook/modules/profile/models.py b/facebook/modules/profile/models.py
index 12cc8cc..56a1c28 100644
--- a/facebook/modules/profile/models.py
+++ b/facebook/modules/profile/models.py
@@ -1,7 +1,8 @@
+from django.contrib.contenttypes import generic
from django.db import models
from django.template.defaultfilters import slugify
from django.utils.translation import ugettext_lazy as _
-from django import forms
+from django.conf import settings
from facebook.modules.base import Base, AdminBase
@@ -18,6 +19,12 @@ class Profile(Base):
_pic_large = models.URLField(max_length=500, blank=True, null=True, verify_exists=False, editable=False)
_pic_crop = models.URLField(max_length=500, blank=True, null=True, verify_exists=False, editable=False)
+ # get all posts for the selected profile
+ # posts = [p.post for p in page.post_throughs.select_related('post').all()]
+ if 'facebook.modules.connections.post' in settings.INSTALLED_APPS:
+ post_throughs = generic.GenericRelation('PagePost',
+ related_name="%(app_label)s_%(class)s_related")
+
class Meta(Base.Meta):
abstract = True
app_label = 'facebook'
@@ -45,4 +52,4 @@ class ProfileAdmin(AdminBase):
def pic_img(self, obj):
return ' ' % obj._picture if obj._picture else ''
pic_img.allow_tags = True
- pic_img.short_description = _('Picture')
\ No newline at end of file
+ pic_img.short_description = _('Picture')
diff --git a/facebook/modules/profile/page/admin.py b/facebook/modules/profile/page/admin.py
index c95d1c8..6b6d5b4 100644
--- a/facebook/modules/profile/page/admin.py
+++ b/facebook/modules/profile/page/admin.py
@@ -13,7 +13,6 @@
from .models import Page
-
class PageAdmin(ProfileAdmin):
def has_access(self, obj):
if obj.updated + datetime.timedelta(days=60) < datetime.datetime.now():
@@ -23,7 +22,30 @@ def has_access(self, obj):
has_access.short_description = _('Access Token')
has_access.boolean = True
- list_display = ('id', 'profile_link', 'slug', '_name', 'pic_img', '_likes', 'has_access')
+ def token_expires_in(self, obj):
+ if not obj._access_token_expires:
+ return ''
+ expires_in = (obj._access_token_expires - datetime.datetime.now()).days
+ if expires_in > 10:
+ return _('%s days' % expires_in)
+ elif expires_in < 0:
+ return _('expired ')
+ else:
+ return _('%s days ' % expires_in)
+
+ token_expires_in.short_description = _('expires in')
+ token_expires_in.allow_tags = True
+
+ def insight_link(self, obj):
+ if '?' in obj.facebook_link:
+ return u'%s ' % (obj.facebook_link, obj._name)
+ else:
+ return u'%s ' % (obj.facebook_link, obj._name)
+ insight_link.allow_tags = True
+ insight_link.short_description = _('Name')
+
+
+ list_display = ('id', 'profile_link', 'slug', 'insight_link', 'pic_img', '_likes', 'has_access', 'token_expires_in')
readonly_fields = ('_name', '_picture', '_likes', '_graph', '_link', '_location', '_phone',
'_checkins', '_website', '_talking_about_count','_username', '_category')
actions = ['get_page_access_token']
@@ -34,14 +56,17 @@ def get_page_access_token(self, request, queryset):
app_dict = get_app_dict(default_post_app)
token_exchange = do_exchange_token(app_dict, graph.access_token)
logger.debug('exchanged token: %s' % token_exchange)
+ token_expires_in = datetime.timedelta(minutes=60)
if 'access_token' in token_exchange:
graph.access_token = token_exchange['access_token']
+ token_expires_in = datetime.timedelta(days=60)
try:
response = graph.request('me/accounts/')
except GraphAPIError as e:
self.message_user(request, 'There was an error: %s' % e.message )
return False
#logger.debug(response)
+ token_expires_in = datetime.datetime.now() + token_expires_in
if response and response.get('data', False):
data = response['data']
message = {'count': 0, 'message': u''}
@@ -51,7 +76,8 @@ def get_page_access_token(self, request, queryset):
for page in queryset:
if accounts.get(page._id, None):
if accounts[page._id].get('access_token', False):
- queryset.filter(id=page._id).update(_access_token=accounts[page._id]['access_token'])
+ queryset.filter(id=page._id).update(_access_token=accounts[page._id]['access_token'],
+ _access_token_expires=token_expires_in)
message['message'] = u'%sSet access token for page %s\n' % (message['message'], page._name)
else:
message['message'] = u'%sDid not get access token for page %s\n' % (message['message'], page._name)
@@ -63,5 +89,4 @@ def get_page_access_token(self, request, queryset):
get_page_access_token.short_description = _('Get an access token for the selected page(s)')
-
admin.site.register(Page, PageAdmin)
\ No newline at end of file
diff --git a/facebook/modules/profile/page/models.py b/facebook/modules/profile/page/models.py
index 777f820..2423a72 100644
--- a/facebook/modules/profile/page/models.py
+++ b/facebook/modules/profile/page/models.py
@@ -13,6 +13,7 @@ class PageBase(Profile):
# Cached Facebook Graph fields for db lookup
_likes = models.IntegerField(blank=True, null=True, help_text=_('Cached fancount of the page'))
_access_token = models.CharField(max_length=255, blank=True, null=True)
+ _access_token_expires = models.DateTimeField(blank=True, null=True)
_category = models.CharField(_('category'), max_length=255, blank=True, null=True)
_location = JSONField(_('location'), blank=True, null=True)
_phone = models.CharField(_('phone'), max_length=255, blank=True, null=True)
diff --git a/facebook/modules/profile/user/models.py b/facebook/modules/profile/user/models.py
index a7662b4..3bfb4ba 100644
--- a/facebook/modules/profile/user/models.py
+++ b/facebook/modules/profile/user/models.py
@@ -21,6 +21,8 @@ class UserBase(Profile):
_location = models.CharField(max_length=70, blank=True, null=True)
_gender = models.CharField(max_length=10, blank=True, null=True)
_locale = models.CharField(max_length=6, blank=True, null=True)
+ _timezone = models.IntegerField(blank=True, null=True)
+ _verified = models.BooleanField(blank=True)
friends = models.ManyToManyField('self')
diff --git a/facebook/templates/admin/facebook/change_form.html b/facebook/templates/admin/facebook/change_form.html
index 22fa7f4..bff4fb8 100644
--- a/facebook/templates/admin/facebook/change_form.html
+++ b/facebook/templates/admin/facebook/change_form.html
@@ -14,7 +14,7 @@
{{ app }}
{% endfor %}
Application: {% fb_first_app %}
-
+
App Access Token: {{ graph.access_token }}, via {{ graph.via }}
User Access Token: None
diff --git a/facebook/templates/admin/facebook/change_list.html b/facebook/templates/admin/facebook/change_list.html
index e11b532..a70a990 100644
--- a/facebook/templates/admin/facebook/change_list.html
+++ b/facebook/templates/admin/facebook/change_list.html
@@ -9,7 +9,7 @@
Make sure your page has an access token if you would like to post a message in the future. (Select 'get access token' from the menu above.)
The access token is only for the application {% fb_first_app %}.
-
+
App Access Token: {% access_token request %}
User Access Token: None
diff --git a/facebook/urls.py b/facebook/urls.py
index ce1d1ce..2d6e0cf 100644
--- a/facebook/urls.py
+++ b/facebook/urls.py
@@ -10,7 +10,7 @@
urlpatterns = patterns('',
- url(r'^deauthorize/$', 'facebook.views.deauthorize_and_delete', name='deauthorize'),
+ url(r'^deauthorize/(?:(?P[A-Za-z0-9_-]+)/)?$', 'facebook.views.deauthorize_and_delete', name='deauthorize'),
url(r'^fql/$', 'facebook.views.fql_console', name="fql_console"),
url(r'^log_error/$', 'facebook.views.log_error', name="log_error"),
url(r'^channel.html$', 'facebook.views.channel', name='channel'),
diff --git a/facebook/views.py b/facebook/views.py
index f01c23a..46c86a9 100644
--- a/facebook/views.py
+++ b/facebook/views.py
@@ -1,15 +1,17 @@
import sys, logging, urllib2
from datetime import datetime, timedelta
-import urllib
-import urlparse
from django.conf import settings
+from django.contrib import admin
+from django.contrib.admin.util import get_deleted_objects
+from django.db import router
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden,\
Http404
from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import render_to_response, get_object_or_404, render
from django.template import loader, RequestContext
from django.views.decorators.http import require_POST
+from django.contrib.admin import helpers
from facebook.utils import validate_redirect, do_exchange_token
from facebook.graph import get_graph
@@ -17,6 +19,8 @@
from facebook.session import get_session
from facebook.modules.profile.application.utils import get_app_dict
from facebook.modules.profile.user.models import User
+from facebook.modules.profile.user.admin import UserAdmin
+from django.utils.translation import ugettext_lazy as _
logger = logging.getLogger(__name__)
@@ -94,14 +98,50 @@ def channel(request):
# Deauthorize callback, signed request: {u'issued_at': 1305126336, u'user_id': u'xxxx', u'user': {u'locale': u'de_DE', u'country': u'ch'}, u'algorithm': u'HMAC-SHA256'}
@csrf_exempt
-def deauthorize_and_delete(request):
+def deauthorize_and_delete(request, app_name=None):
""" Deletes a user on a deauthorize callback. """
if request.method == 'GET':
- raise Http404
+ if request.user.is_superuser:
+ application = get_app_dict(app_name)
+ # Preview callback
+ if 'userid' in request.GET:
+ queryset = User.objects.filter(id=int(request.GET.get('userid'), 0))
+ opts = User._meta
+ modeladmin = UserAdmin(User, admin.site)
+ using = router.db_for_write(User)
+ # Populate deletable_objects, a data structure of all related objects that
+ # will also be deleted.
+ deletable_objects, perms_needed, protected = get_deleted_objects(
+ queryset, opts, request.user, modeladmin.admin_site, using)
+
+ context = {
+ "title": _("Preview deauthorization callback"),
+ "objects_name": 'User',
+ "deletable_objects": [deletable_objects],
+ 'queryset': queryset,
+ "perms_lacking": perms_needed,
+ "protected": protected,
+ "opts": opts,
+ "root_path": modeladmin.admin_site.root_path,
+ "app_label": 'facebook',
+ 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
+ }
+
+ return render(request, [
+ "admin/facebook/delete_selected_confirmation.html",
+ "admin/delete_selected_confirmation.html"
+ ], context)
+ else:
+ return HttpResponse(u"""Deauthorize callback for app %s
+ with id %s called with GET. Call with userid= as
+ parameter to preview cascade.""" % (app_name, application['ID']))
+ else:
+ raise Http404
if 'signed_request' in request.POST:
- application = get_app_dict()
- parsed_request = parseSignedRequest(request.REQUEST['signed_request'], application['SECRET'])
- user = get_object_or_404(User, id=parsed_request['user_id'])
+ application = get_app_dict(app_name)
+ parsed_request = parseSignedRequest(request.POST.get('signed_request'), application['SECRET'])
+ user = get_object_or_404(User, id=int(parsed_request.get('user_id')))
+
if settings.DEBUG == False:
user.delete()
logger.info('Deleting User: %s' % user)
@@ -139,8 +179,8 @@ def internal_redirect(request):
return HttpResponseForbidden('The next= paramater is not an allowed redirect url.')
-""" Allows to register client-side errors. """
def log_error(request):
+ """ Allows to register client-side errors. """
if not request.is_ajax() or not request.method == 'POST':
raise Http404
logger.error(request.POST.get('message'))