david / django-invitation-backend (http://code.welldev.org/django-invitation/)
fork of django-invitation
A fork of django-invitation to use the new django-registration-backends branch. See http://bitbucket.org/ubernostrum/django-registr... for the reasons.
Clone this repository (size: 52.2 KB): HTTPS / SSH
$ hg clone http://code.welldev.org/django-invitation-backend
| commit 21: | b2977a099938 |
| parent 20: | e5a394941120 |
| branch: | default |
Stores registrant with InvitationKey, and uses it to prevent key reuse.
This is a fix for issue 3:
http://bitbucket.org/david/django-invitation/issue/3/expire-used-invitations
A key is usable if it is not expired (as before), and if also no one
has registered using the key.
This change adds the column 'registrant' to InvitationKey, and so
requires changing your database schema. The column can be NULL, so
it's a pretty simple addition.
(I chose to use 'registrant' rather than 'to_user' (which would parallel
'from_user') because technically we don't know who the invitation key was
sent to. All we know is that this particular user used the key to register.)
The implementation uses profile_callback, which more tightly couples
django-invitation to version 0.7 of django-registration. (It already
had this requirement based on the function signatures, but now we're
actually using the extra profile_callback argument.)
Changed (Δ1.5 KB):
raw changeset »
invitation/models.py (34 lines added, 11 lines removed)
invitation/tests.py (3 lines added, 1 lines removed)
invitation/views.py (20 lines added, 0 lines removed)
Up to file-list invitation/models.py:
| … | … | @@ -14,21 +14,28 @@ from django.contrib.sites.models import |
14 |
14 |
from registration.models import SHA1_RE |
15 |
15 |
|
16 |
16 |
class InvitationKeyManager(models.Manager): |
17 |
def get_key(self, invitation_key): |
|
18 |
""" |
|
19 |
Return InvitationKey, or None if it doesn't (or shouldn't) exist. |
|
20 |
""" |
|
21 |
# Don't bother hitting database if invitation_key doesn't match pattern. |
|
22 |
if not SHA1_RE.search(invitation_key): |
|
23 |
return None |
|
24 |
||
25 |
try: |
|
26 |
key = self.get(key=invitation_key) |
|
27 |
except self.model.DoesNotExist: |
|
28 |
return None |
|
29 |
||
30 |
return key |
|
31 |
||
17 |
32 |
def is_key_valid(self, invitation_key): |
18 |
33 |
""" |
19 |
34 |
Check if an ``InvitationKey`` is valid or not, returning a boolean, |
20 |
35 |
``True`` if the key is valid. |
21 |
36 |
""" |
22 |
# Make sure the key we're trying conforms to the pattern of a |
|
23 |
# SHA1 hash; if it doesn't, no point trying to look it up in |
|
24 |
# the database. |
|
25 |
if SHA1_RE.search(invitation_key): |
|
26 |
try: |
|
27 |
invitation_key = self.get(key=invitation_key) |
|
28 |
except self.model.DoesNotExist: |
|
29 |
return False |
|
30 |
return not invitation_key.key_expired() |
|
31 |
return False |
|
37 |
invitation_key = self.get_key(invitation_key) |
|
38 |
return invitation_key and invitation_key.is_usable() |
|
32 |
39 |
|
33 |
40 |
def create_invitation(self, user): |
34 |
41 |
""" |
| … | … | @@ -58,13 +65,22 @@ class InvitationKey(models.Model): |
58 |
65 |
key = models.CharField(_('invitation key'), max_length=40) |
59 |
66 |
date_invited = models.DateTimeField(_('date invited'), |
60 |
67 |
default=datetime.datetime.now) |
61 |
from_user = models.ForeignKey(User |
|
68 |
from_user = models.ForeignKey(User, |
|
69 |
related_name='invitations_sent') |
|
70 |
registrant = models.ForeignKey(User, null=True, blank=True, |
|
71 |
related_name='invitations_used') |
|
62 |
72 |
|
63 |
73 |
objects = InvitationKeyManager() |
64 |
74 |
|
65 |
75 |
def __unicode__(self): |
66 |
76 |
return u"Invitation from %s on %s" % (self.from_user.username, self.date_invited) |
67 |
77 |
|
78 |
def is_usable(self): |
|
79 |
""" |
|
80 |
Return whether this key is still valid for registering a new user. |
|
81 |
""" |
|
82 |
return self.registrant is None and not self.key_expired() |
|
83 |
||
68 |
84 |
def key_expired(self): |
69 |
85 |
""" |
70 |
86 |
Determine whether this ``InvitationKey`` has expired, returning |
| … | … | @@ -81,6 +97,13 @@ class InvitationKey(models.Model): |
81 |
97 |
return self.date_invited + expiration_date <= datetime.datetime.now() |
82 |
98 |
key_expired.boolean = True |
83 |
99 |
|
100 |
def mark_used(self, registrant): |
|
101 |
""" |
|
102 |
Note that this key has been used to register a new user. |
|
103 |
""" |
|
104 |
self.registrant = registrant |
|
105 |
self.save() |
|
106 |
||
84 |
107 |
def send_to(self, email): |
85 |
108 |
""" |
86 |
109 |
Send an invitation email to ``email``. |
Up to file-list invitation/tests.py:
| … | … | @@ -217,7 +217,9 @@ class InvitationViewTests(InvitationTest |
217 |
217 |
redirect_location = response._headers['location'][1] |
218 |
218 |
self.assertTrue(redirect_location.endswith( |
219 |
219 |
reverse('registration_complete'))) |
220 |
user = User.objects.get(username='new_user') |
|
220 |
user = User.objects.get(username='new_user') |
|
221 |
key = InvitationKey.objects.get_key(self.sample_key.key) |
|
222 |
self.assertEqual(user, key.registrant) |
|
221 |
223 |
|
222 |
224 |
# Trying to reuse the same key then fails. |
223 |
225 |
registration_data['username'] = 'even_newer_user' |
Up to file-list invitation/views.py:
| … | … | @@ -15,6 +15,24 @@ remaining_invitations_for_user = Invitat |
15 |
15 |
|
16 |
16 |
# TODO: move the authorization control to a dedicated decorator |
17 |
17 |
|
18 |
class InvitationUsedCallback(object): |
|
19 |
""" |
|
20 |
Callable to mark InvitationKey as used, by way of profile_callback. |
|
21 |
""" |
|
22 |
def __init__(self, invitation_key, profile_callback): |
|
23 |
self.invitation_key = invitation_key |
|
24 |
self.profile_callback = profile_callback |
|
25 |
||
26 |
def __call__(self, user): |
|
27 |
"""Mark the key used, and then call the real callback (if any).""" |
|
28 |
key = InvitationKey.objects.get_key(self.invitation_key) |
|
29 |
if key: |
|
30 |
key.mark_used(user) |
|
31 |
||
32 |
if self.profile_callback: |
|
33 |
self.profile_callback(user) |
|
34 |
||
35 |
||
18 |
36 |
def invited(request, invitation_key=None, extra_context=None): |
19 |
37 |
if 'INVITE_MODE' in settings.get_all_members() and settings.INVITE_MODE: |
20 |
38 |
if invitation_key and is_key_valid(invitation_key): |
| … | … | @@ -37,6 +55,8 @@ def register(request, success_url=None, |
37 |
55 |
if is_key_valid(request.REQUEST['invitation_key']): |
38 |
56 |
invitation_key = request.REQUEST['invitation_key'] |
39 |
57 |
extra_context.update({'invitation_key': invitation_key}) |
58 |
profile_callback = InvitationUsedCallback(invitation_key, |
|
59 |
profile_callback) |
|
40 |
60 |
return registration_register(request, success_url, form_class, |
41 |
61 |
profile_callback, template_name, extra_context) |
42 |
62 |
else: |
