david / django-invitation-backend (http://code.welldev.org/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.

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.)
David Montgomery / davidlmontgomery
14 months ago

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: