forked from tschellenbach/Django-facebook
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathutils.py
689 lines (548 loc) · 20.2 KB
/
utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
from django.utils import six
from functools import wraps
try:
# using compatible_datetime instead of datetime only
# not to override the original datetime package
from django.utils import timezone as compatible_datetime
except ImportError:
from datetime import datetime as compatible_datetime
from datetime import datetime
from django.http import QueryDict, HttpResponse, HttpResponseRedirect
from django.conf import settings
import django.contrib.auth
from django.db import models, transaction
import logging
import re
from django_facebook import settings as facebook_settings
from django.utils.encoding import iri_to_uri
from django.template.loader import render_to_string
import gc
logger = logging.getLogger(__name__)
class NOTHING:
pass
'''
TODO, write an abstraction class for reading and writing users/profile models
'''
def get_profile_model():
'''
Get the profile model if present otherwise return None
'''
model = None
profile_string = getattr(settings, 'AUTH_PROFILE_MODULE', None)
if profile_string:
app_label, model_label = profile_string.split('.')
model = models.get_model(app_label, model_label)
return model
def get_user_model():
'''
For Django < 1.5 backward compatibility
'''
if hasattr(django.contrib.auth, 'get_user_model'):
return django.contrib.auth.get_user_model()
else:
return django.contrib.auth.models.User
def get_model_for_attribute(attribute):
if is_profile_attribute(attribute):
model = get_profile_model()
else:
model = get_user_model()
return model
def is_profile_attribute(attribute):
profile_model = get_profile_model()
profile_fields = []
if profile_model:
profile_fields = [f.name for f in profile_model._meta.fields]
return attribute in profile_fields
def is_user_attribute(attribute):
user_model = get_user_model()
user_fields = [f.name for f in user_model._meta.fields]
return attribute in user_fields
def get_instance_for_attribute(user, profile, attribute):
profile_fields = []
if profile:
profile_fields = [f.name for f in profile._meta.fields]
user_fields = [f.name for f in user._meta.fields]
is_profile_field = lambda f: f in profile_fields and hasattr(profile, f)
is_user_field = lambda f: f in user_fields and hasattr(user, f)
instance = None
if is_profile_field(attribute):
instance = profile
elif is_user_field(attribute):
instance = user
return instance
def get_user_attribute(user, profile, attribute, default=NOTHING):
profile_fields = []
if profile:
profile_fields = [f.name for f in profile._meta.fields]
user_fields = [f.name for f in user._meta.fields]
is_profile_field = lambda f: f in profile_fields and hasattr(profile, f)
is_user_field = lambda f: f in user_fields and hasattr(user, f)
if is_profile_field(attribute):
value = getattr(profile, attribute)
elif is_user_field(attribute):
value = getattr(user, attribute)
elif default is not NOTHING:
value = default
else:
raise AttributeError(
'user or profile didnt have attribute %s' % attribute)
return value
def update_user_attributes(user, profile, attributes_dict, save=False):
'''
Write the attributes either to the user or profile instance
'''
profile_fields = []
if profile:
profile_fields = [f.name for f in profile._meta.fields]
user_fields = [f.name for f in user._meta.fields]
is_profile_field = lambda f: f in profile_fields and hasattr(profile, f)
is_user_field = lambda f: f in user_fields and hasattr(user, f)
for f, value in attributes_dict.items():
if is_profile_field(f):
setattr(profile, f, value)
profile._fb_is_dirty = True
elif is_user_field(f):
setattr(user, f, value)
user._fb_is_dirty = True
else:
logger.info('skipping update of field %s', f)
if save:
if getattr(user, '_fb_is_dirty', False):
user.save()
if profile and getattr(profile, '_fb_is_dirty', False):
profile.save()
def try_get_profile(user):
try:
p = user.get_profile()
except:
p = None
return p
def hash_key(key):
import hashlib
hashed = hashlib.md5(key).hexdigest()
return hashed
def parse_signed_request(signed_request_string):
'''
Just here for your convenience, actual logic is in the
FacebookAuthorization class
'''
from open_facebook.api import FacebookAuthorization
signed_request = FacebookAuthorization.parse_signed_data(
signed_request_string)
return signed_request
def get_url_field():
'''
This should be compatible with both django 1.3, 1.4 and 1.5
In 1.5 the verify_exists argument is removed and always False
'''
from django.forms import URLField
field = URLField()
try:
field = URLField(verify_exists=False)
except TypeError as e:
pass
return field
def clear_persistent_graph_cache(request):
'''
Clears the caches for the graph cache
'''
request.facebook = None
request.session.delete('graph')
if request.user.is_authenticated():
profile = request.user.get_profile()
profile.clear_access_token()
def has_permissions(graph, scope_list):
from open_facebook import exceptions as open_facebook_exceptions
permissions_granted = False
try:
if graph:
permissions_granted = graph.has_permissions(scope_list)
except open_facebook_exceptions.OAuthException as e:
pass
return permissions_granted
def queryset_iterator(queryset, chunksize=1000, getfunc=getattr):
'''''
Iterate over a Django Queryset ordered by the primary key
This method loads a maximum of chunksize (default: 1000) rows in it's
memory at the same time while django normally would load all rows in it's
memory. Using the iterator() method only causes it to not preload all the
classes.
Note that the implementation of the iterator does not support ordered query sets.
'''
pk = 0
try:
'''In the case of an empty list, return'''
last_pk = getfunc(queryset.order_by('-pk')[0], 'pk')
except IndexError:
return
queryset = queryset.order_by('pk')
while pk < last_pk:
for row in queryset.filter(pk__gt=pk)[:chunksize].iterator():
pk = getfunc(row, 'pk')
yield row
gc.collect()
def get_oauth_url(scope, redirect_uri, extra_params=None):
'''
Returns the oAuth URL for the given scope and redirect_uri
'''
scope = parse_scope(scope)
query_dict = QueryDict('', True)
query_dict['scope'] = ','.join(scope)
query_dict['client_id'] = facebook_settings.FACEBOOK_APP_ID
query_dict['redirect_uri'] = redirect_uri
oauth_url = 'https://www.facebook.com/dialog/oauth?'
oauth_url += query_dict.urlencode()
return oauth_url
class ScriptRedirect(HttpResponse):
'''
Redirect for Facebook Canvas pages
'''
def __init__(self, redirect_to, show_body=True):
self.redirect_to = redirect_to
self.location = iri_to_uri(redirect_to)
context = dict(location=self.location,
show_body=show_body)
js_redirect = render_to_string(
'django_facebook/canvas_redirect.html', context)
super(ScriptRedirect, self).__init__(js_redirect)
def response_redirect(redirect_url, script_redirect=False):
'''
Abstract away canvas redirects
'''
if script_redirect:
return ScriptRedirect(redirect_url)
return HttpResponseRedirect(redirect_url)
def error_next_redirect(request, default='/', additional_params=None, next_key=None, redirect_url=None, canvas=False):
'''
Short cut for an error next redirect
'''
if not next_key:
next_key = ['error_next', 'next']
redirect = next_redirect(
request, default, additional_params, next_key, redirect_url, canvas)
return redirect
def next_redirect(request, default='/', additional_params=None,
next_key='next', redirect_url=None, canvas=False):
if facebook_settings.FACEBOOK_DEBUG_REDIRECTS:
return HttpResponse(
'<html><head></head><body><div>Debugging</div></body></html>')
from django.http import HttpResponseRedirect
if not isinstance(next_key, (list, tuple)):
next_key = [next_key]
# get the redirect url
if not redirect_url:
for key in next_key:
redirect_url = request.REQUEST.get(key)
if redirect_url:
break
if not redirect_url:
redirect_url = default
if additional_params:
query_params = QueryDict('', True)
query_params.update(additional_params)
seperator = '&' if '?' in redirect_url else '?'
redirect_url += seperator + query_params.urlencode()
if canvas:
return ScriptRedirect(redirect_url)
return HttpResponseRedirect(redirect_url)
@transaction.atomic
def mass_get_or_create(model_class, base_queryset, id_field, default_dict,
global_defaults):
'''
Updates the data by inserting all not found records
Doesnt delete records if not in the new data
example usage
>>> model_class = ListItem #the class for which you are doing the insert
>>> base_query_set = ListItem.objects.filter(user=request.user, list=1) #query for retrieving currently stored items
>>> id_field = 'user_id' #the id field on which to check
>>> default_dict = {'12': dict(comment='my_new_item'), '13': dict(comment='super')} #list of default values for inserts
>>> global_defaults = dict(user=request.user, list_id=1) #global defaults
'''
current_instances = list(base_queryset)
current_ids = set(
[unicode(getattr(c, id_field)) for c in current_instances])
given_ids = map(unicode, default_dict.keys())
# both ends of the comparison are in unicode ensuring the not in works
new_ids = [g for g in given_ids if g not in current_ids]
prepared_models = []
for new_id in new_ids:
defaults = default_dict[new_id]
defaults[id_field] = new_id
defaults.update(global_defaults)
model_instance = model_class(
**defaults
)
prepared_models.append(model_instance)
# efficiently create these objects all at once
# django 1.4 only
if hasattr(model_class.objects, 'bulk_create'):
model_class.objects.bulk_create(prepared_models)
else:
[m.save() for m in prepared_models]
inserted_model_instances = prepared_models
# returns a list of existing and new items
return current_instances, inserted_model_instances
def get_form_class(backend, request):
'''
Will use registration form in the following order:
1. User configured RegistrationForm
2. backend.get_form_class(request) from django-registration 0.8
3. RegistrationFormUniqueEmail from django-registration < 0.8
'''
form_class = None
# try the setting
form_class_string = facebook_settings.FACEBOOK_REGISTRATION_FORM
if form_class_string:
try:
form_class = get_class_from_string(form_class_string, None)
except ImportError:
# Shouldn't this fail -- if you set it, it must be correct?
pass
if not form_class:
backend = backend or get_registration_backend()
if backend:
form_class = backend.get_form_class(request)
assert form_class, 'we couldnt find a form class, so we cant go on like this'
return form_class
def get_registration_backend():
'''
Ensures compatability with the new and old version of django registration
'''
backend = None
backend_class = None
registration_backend_string = getattr(
facebook_settings, 'FACEBOOK_REGISTRATION_BACKEND', None)
if registration_backend_string:
backend_class = get_class_from_string(registration_backend_string)
# instantiate
if backend_class:
backend = backend_class()
return backend
def get_django_registration_version():
'''
Returns new, old or None depending on the version of django registration
Old works with forms
New works with backends
'''
try:
# support for the newer implementation
from registration.backends import get_backend
version = 'new'
except ImportError:
version = 'old'
try:
import registration
except ImportError as e:
version = None
return version
def parse_scope(scope):
'''
Turns
'email,user_about_me'
or
('email','user_about_me')
into a nice consistent
['email','user_about_me']
'''
assert scope, 'scope is required'
if isinstance(scope, six.string_types):
scope_list = scope.split(',')
elif isinstance(scope, (list, tuple)):
scope_list = list(scope)
else:
raise ValueError('unrecognized type for scope %r' % scope)
return scope_list
def simplify_class_decorator(class_decorator):
'''
Makes the decorator syntax uniform
Regardless if you call the decorator like
**Decorator examples**::
@decorator
or
@decorator()
or
@decorator(staff=True)
Complexity, Python's class based decorators are weird to say the least:
http://www.artima.com/weblogs/viewpost.jsp?thread=240845
This function makes sure that your decorator class always gets called with
**Methods called**::
__init__(fn, *option_args, *option_kwargs)
__call__()
return a function which accepts the *args and *kwargs intended
for fn
'''
# this makes sure the resulting decorator shows up as
# function FacebookRequired instead of outer
@wraps(class_decorator)
def outer(fn=None, *decorator_args, **decorator_kwargs):
# wraps isn't needed, the decorator should do the wrapping :)
# @wraps(fn, assigned=available_attrs(fn))
def actual_decorator(fn):
instance = class_decorator(fn, *decorator_args, **decorator_kwargs)
_wrapped_view = instance.__call__()
return _wrapped_view
if fn is not None:
wrapped_view = actual_decorator(fn)
else:
wrapped_view = actual_decorator
return wrapped_view
return outer
def to_int(input, default=0, exception=(ValueError, TypeError), regexp=None):
'''Convert the given input to an integer or return default
When trying to convert the exceptions given in the exception parameter
are automatically catched and the default will be returned.
The regexp parameter allows for a regular expression to find the digits
in a string.
When True it will automatically match any digit in the string.
When a (regexp) object (has a search method) is given, that will be used.
WHen a string is given, re.compile will be run over it first
The last group of the regexp will be used as value
'''
if regexp is True:
regexp = re.compile('(\d+)')
elif isinstance(regexp, six.string_types):
regexp = re.compile(regexp)
elif hasattr(regexp, 'search'):
pass
elif regexp is not None:
raise(TypeError, 'unknown argument for regexp parameter')
try:
if regexp:
match = regexp.search(input)
if match:
input = match.groups()[-1]
return int(input)
except exception:
return default
def to_bool(input, default=False):
'''
Take a request value and turn it into a bool
Never raises errors
'''
if input is None:
value = default
else:
int_value = to_int(input, default=None)
if int_value is None:
value = default
else:
value = bool(int_value)
return value
def remove_query_param(url, key):
p = re.compile('%s=[^=&]*&' % key, re.VERBOSE)
url = p.sub('', url)
p = re.compile('%s=[^=&]*' % key, re.VERBOSE)
url = p.sub('', url)
return url
def replace_query_param(url, key, value):
p = re.compile('%s=[^=&]*' % key, re.VERBOSE)
return p.sub('%s=%s' % (key, value), url)
DROP_QUERY_PARAMS = ['code', 'signed_request', 'state']
def cleanup_oauth_url(redirect_uri):
'''
We have to maintain order with respect to the
queryparams which is a bit of a pain
TODO: Very hacky will subclass QueryDict to SortedQueryDict at some point
And use a decent sort function
'''
if '?' in redirect_uri:
redirect_base, redirect_query = redirect_uri.split('?', 1)
query_dict_items = QueryDict(redirect_query).items()
else:
query_dict_items = QueryDict('', True)
# filtered_query_items = [(k, v) for k, v in query_dict_items
# if k.lower() not in DROP_QUERY_PARAMS]
# new_query_dict = QueryDict('', True)
# new_query_dict.update(dict(filtered_query_items))
excluded_query_items = [(k, v) for k, v in query_dict_items
if k.lower() in DROP_QUERY_PARAMS]
for k, v in excluded_query_items:
redirect_uri = remove_query_param(redirect_uri, k)
redirect_uri = redirect_uri.strip('?')
redirect_uri = redirect_uri.strip('&')
return redirect_uri
def replication_safe(f):
'''
Usually views which do a POST will require the next page to be
read from the master database. (To prevent issues with replication lag).
However certain views like login do not have this issue.
They do a post, but don't modify data which you'll show on subsequent pages.
This decorators marks these views as safe.
This ensures requests on the next page are allowed to use the slave db
'''
from functools import wraps
@wraps(f)
def wrapper(request, *args, **kwargs):
request.replication_safe = True
response = f(request, *args, **kwargs)
return response
return wrapper
def get_class_from_string(path, default=None):
"""
Return the class specified by the string.
IE: django.contrib.auth.models.User
Will return the user class or cause an ImportError
"""
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
i = path.rfind('.')
module, attr = path[:i], path[i + 1:]
mod = import_module(module)
try:
return getattr(mod, attr)
except AttributeError:
if default:
return default
else:
raise ImportError(
'Cannot import name {} (from {})'.format(attr, mod))
def parse_like_datetime(dt):
return datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S+0000")
def get_default_mapping():
from django_facebook.api import FacebookUserConverter
DEFAULT_FACEBOOK_CLASS_MAPPING = {
'user_conversion': FacebookUserConverter
}
return DEFAULT_FACEBOOK_CLASS_MAPPING
def get_class_mapping():
mapping = facebook_settings.FACEBOOK_CLASS_MAPPING
if mapping is None:
mapping = get_default_mapping()
return mapping
def get_class_for(purpose):
'''
Usage:
conversion_class = get_class_for('user_conversion')
'''
mapping = get_class_mapping()
class_ = mapping[purpose]
if isinstance(class_, six.string_types):
class_ = get_class_from_string(class_)
return class_
def get_instance_for(purpose, *args, **kwargs):
'''
**Usage**::
conversion_instance = get_instance_for(
'facebook_user_conversion', user=user)
'''
class_ = get_class_for(purpose)
instance = class_(*args, **kwargs)
return instance
def get_migration_data():
'''
Support for Django custom user models
See this blog post for inspiration
http://kevindias.com/writing/django-custom-user-models-south-and-reusable-apps/
https://github.com/stephenmcd/mezzanine/blob/master/mezzanine/core/migrations/0005_auto__chg_field_sitepermission_user__del_unique_sitepermission_user.py
'''
try:
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name)
user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name)
return User, user_orm_label, user_model_label