├── .gitignore
├── AUTHORS.txt
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── invitation
├── __init__.py
├── admin.py
├── app_settings.py
├── forms.py
├── locale
│ └── tr
│ │ └── LC_MESSAGES
│ │ └── django.po
├── management
│ └── __init__.py
├── models.py
├── signals.py
├── templates
│ └── admin
│ │ └── invitation
│ │ └── invitationstats
│ │ ├── _reward_link.html
│ │ └── change_list.html
├── templatetags
│ ├── __init__.py
│ └── invitation_tags.py
├── tests
│ ├── __init__.py
│ ├── invite_only_urls.py
│ ├── invite_optional_urls.py
│ ├── models.py
│ ├── templates
│ │ ├── invitation
│ │ │ ├── invitation_complete.html
│ │ │ ├── invitation_email.txt
│ │ │ ├── invitation_email_subject.txt
│ │ │ ├── invitation_form.html
│ │ │ ├── invitation_home.html
│ │ │ ├── invitation_registered.html
│ │ │ ├── invitation_unavailable.html
│ │ │ ├── invite_only.html
│ │ │ └── wrong_invitation_key.html
│ │ └── registration
│ │ │ ├── registration_complete.html
│ │ │ ├── registration_form.html
│ │ │ └── registration_register.html
│ ├── urls.py
│ ├── utils.py
│ └── views.py
├── urls.py
└── views.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[oc]
2 | *.mo
3 | *.db
4 | cache/
5 | *.kdevelop
6 | *.kdevses
7 | *.egg-info
8 | MANIFEST
9 |
--------------------------------------------------------------------------------
/AUTHORS.txt:
--------------------------------------------------------------------------------
1 | Atamert Ölçgen
2 | lhon
3 | Ales Zabala Alava (Shagi)
4 | Brad Montgomery
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010, Atamert Ölçgen (http://www.muhuk.com)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of django-inviting nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include LICENSE.txt
3 | recursive-include invitation/locale/*/LC_MESSAGES *.po *.mo
4 | recursive-include invitation/templates *.html
5 | recursive-include invitation/tests/templates *.html
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 |
2 | .. attention:: This package is no longer maintained.
3 |
4 |
5 | Built on top of ``django-registration``, **django-inviting** handles registration through invitations.
6 |
7 |
8 | Features
9 | ========
10 |
11 | - Invitations can be optional or required to be registered.
12 | - Admin integration
13 | - Adding available invitations with custom performance and rewarding
14 | algorithms. (for invite only mode)
15 |
16 |
17 | Installation
18 | ============
19 |
20 | This application depends on ``django-registration``.
21 |
22 | #. Add ``"django-inviting"`` directory to your Python path.
23 | #. Add ``"invitation"`` to your ``INSTALLED_APPS`` tuple found in
24 | your settings file.
25 | #. Include ``"invitation.urls"`` to your URLconf.
26 |
27 |
28 | Testing & Example
29 | =================
30 |
31 | TODO
32 |
33 |
34 | Usage
35 | =====
36 |
37 | You can configure ``django-inviting`` app's behaviour with the following
38 | settings:
39 |
40 | :INVITATION_INVITE_ONLY:
41 | Set this to True if you want registration to be only possible via
42 | invitations. Default value is ``False``.
43 |
44 | :INVITATION_EXPIRE_DAYS:
45 | How many days before an invitation is expired. Default value is ``15``.
46 |
47 | :INVITATION_INITIAL_INVITATIONS:
48 | How many invitations are available to new users. If
49 | ``INVITATION_INVITE_ONLY`` is ``False`` this setting
50 | has no effect. Default value is ``10``.
51 |
52 | :INVITATION_PERFORMANCE_FUNC:
53 | A method that takes an ``InvitationStats`` instance and returns a
54 | ``float`` between ``0.0`` and ``1.0``. You can supply a custom
55 | performance method by reference or by import path as a string.
56 | Default value is ``None``. If a custom performance function is not
57 | supplied one of the default performance functions in ``invitation.models``
58 | will be used according to ``INVITATION_INVITE_ONLY`` value.
59 |
60 | :INVITATION_REWARD_THRESHOLD:
61 | A ``float`` that determines which users are rewarded. Default value
62 | is ``0.75``.
63 |
64 |
65 | See Also
66 | ========
67 |
68 | - `django-invitation `_
69 | - `django-invite `_
70 |
--------------------------------------------------------------------------------
/invitation/__init__.py:
--------------------------------------------------------------------------------
1 | __author__ = u'Atamert \xd6l\xe7gen'
2 | __copyright__ = u'Copyright 2010, \xd6l\xe7gen Bili\u015fim'
3 | __credits__ = [u'Atamert \xd6l\xe7gen']
4 |
5 |
6 | __license__ = 'BSD'
7 | __version__ = '0.6.1'
8 | __maintainer__ = u'Atamert \xd6l\xe7gen'
9 | __email__ = 'muhuk@muhuk.com'
10 | __status__ = 'Production'
11 |
--------------------------------------------------------------------------------
/invitation/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from models import Invitation, InvitationStats
3 |
4 |
5 | class InvitationAdmin(admin.ModelAdmin):
6 | list_display = ('user', 'email', 'expiration_date')
7 | admin.site.register(Invitation, InvitationAdmin)
8 |
9 |
10 | class InvitationStatsAdmin(admin.ModelAdmin):
11 | list_display = ('user', 'available', 'sent', 'accepted', 'performance')
12 |
13 | def performance(self, obj):
14 | return '%0.2f' % obj.performance
15 | admin.site.register(InvitationStats, InvitationStatsAdmin)
16 |
--------------------------------------------------------------------------------
/invitation/app_settings.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.core.exceptions import ImproperlyConfigured
3 | from django.utils.importlib import import_module
4 |
5 |
6 | def get_performance_func(settings):
7 | performance_func = getattr(settings, 'INVITATION_PERFORMANCE_FUNC', None)
8 | if isinstance(performance_func, (str, unicode)):
9 | module_name, func_name = performance_func.rsplit('.', 1)
10 | try:
11 | performance_func = getattr(import_module(module_name), func_name)
12 | except ImportError:
13 | raise ImproperlyConfigured('Can\'t import performance function ' \
14 | '`%s` from `%s`' % (func_name,
15 | module_name))
16 | if performance_func and not callable(performance_func):
17 | raise ImproperlyConfigured('INVITATION_PERFORMANCE_FUNC must be a ' \
18 | 'callable or an import path string ' \
19 | 'pointing to a callable.')
20 |
21 |
22 | INVITE_ONLY = getattr(settings, 'INVITATION_INVITE_ONLY', False)
23 | EXPIRE_DAYS = getattr(settings, 'INVITATION_EXPIRE_DAYS', 15)
24 | INITIAL_INVITATIONS = getattr(settings, 'INVITATION_INITIAL_INVITATIONS', 10)
25 | REWARD_THRESHOLD = getattr(settings, 'INVITATION_REWARD_THRESHOLD', 0.75)
26 | PERFORMANCE_FUNC = get_performance_func(settings)
27 |
--------------------------------------------------------------------------------
/invitation/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib.auth.models import User
3 | from registration.forms import RegistrationForm
4 |
5 |
6 | def save_user(form_instance):
7 | """
8 | Create a new **active** user from form data.
9 |
10 | This method is intended to replace the ``save`` of
11 | ``django-registration``s ``RegistrationForm``. Required form fields
12 | are ``username``, ``email`` and ``password1``.
13 | """
14 | username = form_instance.cleaned_data['username']
15 | email = form_instance.cleaned_data['email']
16 | password = form_instance.cleaned_data['password1']
17 | new_user = User.objects.create_user(username, email, password)
18 | new_user.save()
19 | return new_user
20 |
21 |
22 | class InvitationForm(forms.Form):
23 | email = forms.EmailField()
24 |
25 |
26 | class RegistrationFormInvitation(RegistrationForm):
27 | """
28 | Subclass of ``registration.RegistrationForm`` that create an **active**
29 | user.
30 |
31 | Since registration is (supposedly) done via invitation, no further
32 | activation is required. For this reason ``email`` field always return
33 | the value of ``email`` argument given the constructor.
34 | """
35 | def __init__(self, email, *args, **kwargs):
36 | super(RegistrationFormInvitation, self).__init__(*args, **kwargs)
37 | self._make_email_immutable(email)
38 |
39 | def _make_email_immutable(self, email):
40 | self._email = self.initial['email'] = email
41 | if 'email' in self.data:
42 | self.data = self.data.copy()
43 | self.data['email'] = email
44 | self.fields['email'].widget.attrs.update({'readonly': True})
45 |
46 | def clean_email(self):
47 | return self._email
48 |
49 | save = save_user
50 |
--------------------------------------------------------------------------------
/invitation/locale/tr/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: django-inviting\n"
9 | "Report-Msgid-Bugs-To: \n"
10 | "POT-Creation-Date: 2010-02-25 13:36+0200\n"
11 | "PO-Revision-Date: 2009-10-13 17:47+0200\n"
12 | "Last-Translator: Atamert Ölçgen \n"
13 | "Language-Team: muhuk \n"
14 | "MIME-Version: 1.0\n"
15 | "Content-Type: text/plain; charset=UTF-8\n"
16 | "Content-Transfer-Encoding: 8bit\n"
17 | "X-Poedit-Language: Turkish\n"
18 |
19 | #: models.py:70
20 | msgid "e-mail"
21 | msgstr "e-posta"
22 |
23 | #: models.py:71
24 | msgid "invitation key"
25 | msgstr "davetiye kodu"
26 |
27 | #: models.py:72
28 | msgid "date invited"
29 | msgstr "davet edildilği tarih"
30 |
31 | #: models.py:78
32 | msgid "invitation"
33 | msgstr "davetiye"
34 |
35 | #: models.py:79
36 | msgid "invitations"
37 | msgstr "davetiyeler"
38 |
39 | #: models.py:83
40 | #, python-format
41 | msgid "%(username)s invited %(email)s on %(date)s"
42 | msgstr "%(username)s, %(email)s'i %(date)s tarihinde davet etti"
43 |
44 | #: models.py:102
45 | msgid "expiration date"
46 | msgstr "son kullanma tarihi"
47 |
48 | #: models.py:160
49 | msgid "available invitations"
50 | msgstr "kullanılabilir davetiyeler"
51 |
52 | #: models.py:162
53 | msgid "invitations sent"
54 | msgstr "gönderilmiş davetiyeler"
55 |
56 | #: models.py:163
57 | msgid "invitations accepted"
58 | msgstr "kabul edilmiş davetiyeler"
59 |
60 | #: models.py:168
61 | msgid "invitation stats"
62 | msgstr "davetiye istatistikleri"
63 |
64 | #: models.py:172
65 | #, python-format
66 | msgid "invitation stats for %(username)s"
67 | msgstr "%(username)s için davetiye istatistikleri"
68 |
69 | #: views.py:79
70 | #, python-format
71 | msgid "%(users)s users are given a total of %(invitations)s invitations."
72 | msgstr "%(users)s kullanıcıya toplam %(invitations)s davetiye verildi."
73 |
74 | #: views.py:84
75 | msgid "No user has performance above threshold, no invitations awarded."
76 | msgstr ""
77 | "Belirlenen performans eşiğinin üstünde kullanıcı olmadığı "
78 | "için hiç davetiye tanımlanmadı."
79 |
80 | #: templates/admin/invitation/invitationstats/_reward_link.html:3
81 | msgid "Reward users with good performance"
82 | msgstr "Aktif kullanıcılara davetiye tanımla"
83 |
84 | #: templates/admin/invitation/invitationstats/change_list.html:10
85 | #, python-format
86 | msgid "Add %(name)s"
87 | msgstr "%(name)s ekle"
88 |
--------------------------------------------------------------------------------
/invitation/management/__init__.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_syncdb
2 | from django.contrib.auth.models import User
3 | from invitation import models
4 |
5 |
6 | def create_stats_for_existing_users(sender, **kwargs):
7 | """
8 | Create `InvitationStats` objects for all users after a `sycndb`
9 |
10 | """
11 | count = 0
12 | for user in User.objects.filter(invitation_stats__isnull=True):
13 | models.InvitationStats.objects.create(user=user)
14 | count += 1
15 | if count > 0:
16 | print "Created InvitationStats for %s existing Users" % count
17 |
18 |
19 | post_syncdb.connect(create_stats_for_existing_users, sender=models)
20 |
--------------------------------------------------------------------------------
/invitation/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import random
3 | from django.db import models
4 | from django.core.mail import send_mail
5 | from django.conf import settings
6 | from django.template.loader import render_to_string
7 | from django.utils.translation import ugettext_lazy as _
8 | from django.utils.hashcompat import sha_constructor
9 | from django.contrib.auth.models import User
10 | from django.contrib.sites.models import Site, RequestSite
11 | import app_settings
12 | import signals
13 |
14 |
15 | def performance_calculator_invite_only(invitation_stats):
16 | """Calculate a performance score between ``0.0`` and ``1.0``.
17 | """
18 | if app_settings.INVITE_ONLY:
19 | total = invitation_stats.available + invitation_stats.sent
20 | try:
21 | send_ratio = float(invitation_stats.sent) / total
22 | except ZeroDivisionError:
23 | send_ratio = 0.0
24 | accept_ratio = performance_calculator_invite_optional(invitation_stats)
25 | return min((send_ratio + accept_ratio) * 0.6, 1.0)
26 |
27 |
28 | def performance_calculator_invite_optional(invitation_stats):
29 | try:
30 | accept_ratio = float(invitation_stats.accepted) / invitation_stats.sent
31 | return min(accept_ratio, 1.0)
32 | except ZeroDivisionError:
33 | return 0.0
34 |
35 |
36 | DEFAULT_PERFORMANCE_CALCULATORS = {
37 | True: performance_calculator_invite_only,
38 | False: performance_calculator_invite_optional,
39 | }
40 |
41 |
42 | class InvitationError(Exception):
43 | pass
44 |
45 |
46 | class InvitationManager(models.Manager):
47 | def invite(self, user, email):
48 | """
49 | Get or create an invitation for ``email`` from ``user``.
50 |
51 | This method doesn't an send email. You need to call ``send_email()``
52 | method on returned ``Invitation`` instance.
53 | """
54 | invitation = None
55 | try:
56 | # It is possible that there is more than one invitation fitting
57 | # the criteria. Normally this means some older invitations are
58 | # expired or an email is invited consequtively.
59 | invitation = self.filter(user=user, email=email)[0]
60 | if not invitation.is_valid():
61 | invitation = None
62 | except (Invitation.DoesNotExist, IndexError):
63 | pass
64 | if invitation is None:
65 | user.invitation_stats.use()
66 | key = '%s%0.16f%s%s' % (settings.SECRET_KEY,
67 | random.random(),
68 | user.email,
69 | email)
70 | key = sha_constructor(key).hexdigest()
71 | invitation = self.create(user=user, email=email, key=key)
72 | return invitation
73 | invite.alters_data = True
74 |
75 | def find(self, invitation_key):
76 | """
77 | Find a valid invitation for the given key or raise
78 | ``Invitation.DoesNotExist``.
79 |
80 | This function always returns a valid invitation. If an invitation is
81 | found but not valid it will be automatically deleted.
82 | """
83 | try:
84 | invitation = self.filter(key=invitation_key)[0]
85 | except IndexError:
86 | raise Invitation.DoesNotExist
87 | if not invitation.is_valid():
88 | invitation.delete()
89 | raise Invitation.DoesNotExist
90 | return invitation
91 |
92 | def valid(self):
93 | """Filter valid invitations.
94 | """
95 | expiration = datetime.datetime.now() - datetime.timedelta(
96 | app_settings.EXPIRE_DAYS)
97 | return self.get_query_set().filter(date_invited__gte=expiration)
98 |
99 | def invalid(self):
100 | """Filter invalid invitation.
101 | """
102 | expiration = datetime.datetime.now() - datetime.timedelta(
103 | app_settings.EXPIRE_DAYS)
104 | return self.get_query_set().filter(date_invited__le=expiration)
105 |
106 |
107 | class Invitation(models.Model):
108 | user = models.ForeignKey(User, related_name='invitations')
109 | email = models.EmailField(_(u'e-mail'))
110 | key = models.CharField(_(u'invitation key'), max_length=40, unique=True)
111 | date_invited = models.DateTimeField(_(u'date invited'),
112 | default=datetime.datetime.now)
113 |
114 | objects = InvitationManager()
115 |
116 | class Meta:
117 | verbose_name = _(u'invitation')
118 | verbose_name_plural = _(u'invitations')
119 | ordering = ('-date_invited',)
120 |
121 | def __unicode__(self):
122 | return _('%(username)s invited %(email)s on %(date)s') % {
123 | 'username': self.user.username,
124 | 'email': self.email,
125 | 'date': str(self.date_invited.date()),
126 | }
127 |
128 | @models.permalink
129 | def get_absolute_url(self):
130 | return ('invitation_register', (), {'invitation_key': self.key})
131 |
132 | @property
133 | def _expires_at(self):
134 | return self.date_invited + datetime.timedelta(app_settings.EXPIRE_DAYS)
135 |
136 | def is_valid(self):
137 | """
138 | Return ``True`` if the invitation is still valid, ``False`` otherwise.
139 | """
140 | return datetime.datetime.now() < self._expires_at
141 |
142 | def expiration_date(self):
143 | """Return a ``datetime.date()`` object representing expiration date.
144 | """
145 | return self._expires_at.date()
146 | expiration_date.short_description = _(u'expiration date')
147 | expiration_date.admin_order_field = 'date_invited'
148 |
149 | def send_email(self, email=None, site=None, request=None):
150 | """
151 | Send invitation email.
152 |
153 | Both ``email`` and ``site`` parameters are optional. If not supplied
154 | instance's ``email`` field and current site will be used.
155 |
156 | **Templates:**
157 |
158 | :invitation/invitation_email_subject.txt:
159 | Template used to render the email subject.
160 |
161 | **Context:**
162 |
163 | :invitation: ``Invitation`` instance ``send_email`` is called on.
164 | :site: ``Site`` instance to be used.
165 |
166 | :invitation/invitation_email.txt:
167 | Template used to render the email body.
168 |
169 | **Context:**
170 |
171 | :invitation: ``Invitation`` instance ``send_email`` is called on.
172 | :expiration_days: ``INVITATION_EXPIRE_DAYS`` setting.
173 | :site: ``Site`` instance to be used.
174 |
175 | **Signals:**
176 |
177 | ``invitation.signals.invitation_sent`` is sent on completion.
178 | """
179 | email = email or self.email
180 | if site is None:
181 | if Site._meta.installed:
182 | site = Site.objects.get_current()
183 | elif request is not None:
184 | site = RequestSite(request)
185 | subject = render_to_string('invitation/invitation_email_subject.txt',
186 | {'invitation': self, 'site': site})
187 | # Email subject *must not* contain newlines
188 | subject = ''.join(subject.splitlines())
189 | message = render_to_string('invitation/invitation_email.txt', {
190 | 'invitation': self,
191 | 'expiration_days': app_settings.EXPIRE_DAYS,
192 | 'site': site
193 | })
194 | send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email])
195 | signals.invitation_sent.send(sender=self)
196 |
197 | def mark_accepted(self, new_user):
198 | """
199 | Update sender's invitation statistics and delete self.
200 |
201 | ``invitation.signals.invitation_accepted`` is sent just before the
202 | instance is deleted.
203 | """
204 | self.user.invitation_stats.mark_accepted()
205 | signals.invitation_accepted.send(sender=self,
206 | inviting_user=self.user,
207 | new_user=new_user)
208 | self.delete()
209 | mark_accepted.alters_data = True
210 |
211 |
212 | class InvitationStatsManager(models.Manager):
213 | def give_invitations(self, user=None, count=None):
214 | rewarded_users = 0
215 | invitations_given = 0
216 | if not isinstance(count, int) and not callable(count):
217 | raise TypeError('Count must be int or callable.')
218 | if user is None:
219 | qs = self.get_query_set()
220 | else:
221 | qs = self.filter(user=user)
222 | for instance in qs:
223 | if callable(count):
224 | c = count(instance.user)
225 | else:
226 | c = count
227 | if c:
228 | instance.add_available(c)
229 | rewarded_users += 1
230 | invitations_given += c
231 | return rewarded_users, invitations_given
232 |
233 | def reward(self, user=None, reward_count=app_settings.INITIAL_INVITATIONS):
234 | def count(user):
235 | if user.invitation_stats.performance >= \
236 | app_settings.REWARD_THRESHOLD:
237 | return reward_count
238 | return 0
239 | return self.give_invitations(user, count)
240 |
241 |
242 | class InvitationStats(models.Model):
243 | """Store invitation statistics for ``user``.
244 | """
245 | user = models.OneToOneField(User,
246 | related_name='invitation_stats')
247 | available = models.IntegerField(_(u'available invitations'),
248 | default=app_settings.INITIAL_INVITATIONS)
249 | sent = models.IntegerField(_(u'invitations sent'), default=0)
250 | accepted = models.IntegerField(_(u'invitations accepted'), default=0)
251 |
252 | objects = InvitationStatsManager()
253 |
254 | class Meta:
255 | verbose_name = verbose_name_plural = _(u'invitation stats')
256 | ordering = ('-user',)
257 |
258 | def __unicode__(self):
259 | return _(u'invitation stats for %(username)s') % {
260 | 'username': self.user.username}
261 |
262 | @property
263 | def performance(self):
264 | if app_settings.PERFORMANCE_FUNC:
265 | return app_settings.PERFORMANCE_FUNC(self)
266 | return DEFAULT_PERFORMANCE_CALCULATORS[app_settings.INVITE_ONLY](self)
267 |
268 | def add_available(self, count=1):
269 | """
270 | Add usable invitations.
271 |
272 | **Optional arguments:**
273 |
274 | :count:
275 | Number of invitations to add. Default is ``1``.
276 |
277 | ``invitation.signals.invitation_added`` is sent at the end.
278 | """
279 | self.available = models.F('available') + count
280 | self.save()
281 | signals.invitation_added.send(sender=self, user=self.user, count=count)
282 | add_available.alters_data = True
283 |
284 | def use(self, count=1):
285 | """
286 | Mark invitations used.
287 |
288 | Raises ``InvitationError`` if ``INVITATION_INVITE_ONLY`` is True or
289 | ``count`` is more than available invitations.
290 |
291 | **Optional arguments:**
292 |
293 | :count:
294 | Number of invitations to mark used. Default is ``1``.
295 | """
296 | if app_settings.INVITE_ONLY:
297 | if self.available - count >= 0:
298 | self.available = models.F('available') - count
299 | else:
300 | raise InvitationError('No available invitations.')
301 | self.sent = models.F('sent') + count
302 | self.save()
303 | use.alters_data = True
304 |
305 | def mark_accepted(self, count=1):
306 | """
307 | Mark invitations accepted.
308 |
309 | Raises ``InvitationError`` if more invitations than possible is
310 | being accepted.
311 |
312 | **Optional arguments:**
313 |
314 | :count:
315 | Optional. Number of invitations to mark accepted. Default is ``1``.
316 | """
317 | if self.accepted + count > self.sent:
318 | raise InvitationError('There can\'t be more accepted ' \
319 | 'invitations than sent invitations.')
320 | self.accepted = models.F('accepted') + count
321 | self.save()
322 | mark_accepted.alters_data = True
323 |
324 |
325 | def create_stats(sender, instance, created, raw, **kwargs):
326 | if created and not raw:
327 | InvitationStats.objects.create(user=instance)
328 | models.signals.post_save.connect(create_stats,
329 | sender=User,
330 | dispatch_uid='invitation.models.create_stats')
331 |
--------------------------------------------------------------------------------
/invitation/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import Signal
2 |
3 |
4 | invitation_added = Signal(providing_args=['user', 'count'])
5 |
6 | invitation_sent = Signal()
7 |
8 | invitation_accepted = Signal(providing_args=['inviting_user', 'new_user'])
9 |
--------------------------------------------------------------------------------
/invitation/templates/admin/invitation/invitationstats/_reward_link.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if INVITE_ONLY %}
3 | {% blocktrans %}Reward users with good performance{% endblocktrans %}
4 | {% endif %}
5 |
--------------------------------------------------------------------------------
/invitation/templates/admin/invitation/invitationstats/change_list.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_list.html" %}
2 | {% load i18n invitation_tags %}
3 |
4 | {% block object-tools %}
5 | {% if has_add_permission %}
6 |
14 | {% endif %}
15 | {% endblock %}
--------------------------------------------------------------------------------
/invitation/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 |
--------------------------------------------------------------------------------
/invitation/templatetags/invitation_tags.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from invitation.app_settings import INVITE_ONLY
3 |
4 |
5 | register = template.Library()
6 |
7 |
8 | @register.inclusion_tag('admin/invitation/invitationstats/_reward_link.html')
9 | def admin_reward_link():
10 | """
11 | Adds a reward action if INVITE_ONLY is ``True``.
12 |
13 | Usage::
14 |
15 | {% admin_reward_link %}
16 | """
17 | return {'INVITE_ONLY': INVITE_ONLY}
18 |
--------------------------------------------------------------------------------
/invitation/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from views import InviteOnlyModeTestCase
2 | from views import InviteOptionalModeTestCase
3 | from models import InvitationTestCase
4 | from models import InvitationStatsInviteOnlyTestCase
5 | from models import InvitationStatsInviteOptionalTestCase
6 |
--------------------------------------------------------------------------------
/invitation/tests/invite_only_urls.py:
--------------------------------------------------------------------------------
1 | from django.utils.importlib import import_module
2 | from invitation import app_settings
3 |
4 |
5 | app_settings.INVITE_ONLY = True
6 | reload(import_module('invitation.urls'))
7 | reload(import_module('invitation.tests.urls'))
8 | from invitation.tests.urls import urlpatterns
9 |
--------------------------------------------------------------------------------
/invitation/tests/invite_optional_urls.py:
--------------------------------------------------------------------------------
1 | from django.utils.importlib import import_module
2 | from invitation import app_settings
3 |
4 |
5 | app_settings.INVITE_ONLY = False
6 | reload(import_module('invitation.urls'))
7 | reload(import_module('invitation.tests.urls'))
8 | from invitation.tests.urls import urlpatterns
9 |
--------------------------------------------------------------------------------
/invitation/tests/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from django.core import mail
3 | from django.contrib.auth.models import User
4 | from utils import BaseTestCase
5 | from invitation import app_settings
6 | from invitation.models import InvitationError, Invitation, InvitationStats
7 | from invitation.models import performance_calculator_invite_only
8 | from invitation.models import performance_calculator_invite_optional
9 |
10 |
11 | EXPIRE_DAYS = app_settings.EXPIRE_DAYS
12 | INITIAL_INVITATIONS = app_settings.INITIAL_INVITATIONS
13 |
14 |
15 | class InvitationTestCase(BaseTestCase):
16 | def setUp(self):
17 | super(InvitationTestCase, self).setUp()
18 | user = self.user()
19 | user.invitation_stats.use()
20 | self.invitation = Invitation.objects.create(user=user,
21 | email=u'test@example.com',
22 | key=u'F' * 40)
23 |
24 | def make_invalid(self, invitation=None):
25 | invitation = invitation or self.invitation
26 | invitation.date_invited = datetime.datetime.now() - \
27 | datetime.timedelta(EXPIRE_DAYS + 10)
28 | invitation.save()
29 | return invitation
30 |
31 | def test_send_email(self):
32 | self.invitation.send_email()
33 | self.assertEqual(len(mail.outbox), 1)
34 | self.assertEqual(mail.outbox[0].recipients()[0], u'test@example.com')
35 | self.invitation.send_email(u'other@email.org')
36 | self.assertEqual(len(mail.outbox), 2)
37 | self.assertEqual(mail.outbox[1].recipients()[0], u'other@email.org')
38 |
39 | def test_mark_accepted(self):
40 | new_user = User.objects.create_user('test', 'test@example.com', 'test')
41 | pk = self.invitation.pk
42 | self.invitation.mark_accepted(new_user)
43 | self.assertRaises(Invitation.DoesNotExist,
44 | Invitation.objects.get, pk=pk)
45 |
46 | def test_invite(self):
47 | self.user().invitation_stats.add_available(10)
48 | Invitation.objects.all().delete()
49 | invitation = Invitation.objects.invite(self.user(), 'test@example.com')
50 | self.assertEqual(invitation.user, self.user())
51 | self.assertEqual(invitation.email, 'test@example.com')
52 | self.assertEqual(len(invitation.key), 40)
53 | self.assertEqual(invitation.is_valid(), True)
54 | self.assertEqual(type(invitation.expiration_date()), datetime.date)
55 | # Test if existing valid record is returned
56 | # when we try with the same credentials
57 | self.assertEqual(Invitation.objects.invite(self.user(),
58 | 'test@example.com'), invitation)
59 | # Try with an invalid invitation
60 | invitation = self.make_invalid(invitation)
61 | new_invitation = Invitation.objects.invite(self.user(),
62 | 'test@example.com')
63 | self.assertEqual(new_invitation.is_valid(), True)
64 | self.assertNotEqual(new_invitation, invitation)
65 |
66 | def test_find(self):
67 | self.assertEqual(Invitation.objects.find(self.invitation.key),
68 | self.invitation)
69 | invitation = self.make_invalid()
70 | self.assertEqual(invitation.is_valid(), False)
71 | self.assertRaises(Invitation.DoesNotExist,
72 | Invitation.objects.find, invitation.key)
73 | self.assertEqual(Invitation.objects.all().count(), 0)
74 | self.assertRaises(Invitation.DoesNotExist,
75 | Invitation.objects.find, '')
76 |
77 |
78 | class InvitationStatsBaseTestCase(BaseTestCase):
79 | def stats(self, user=None):
80 | user = user or self.user()
81 | return (user.invitation_stats.available,
82 | user.invitation_stats.sent,
83 | user.invitation_stats.accepted)
84 |
85 | class MockInvitationStats(object):
86 | def __init__(self, available, sent, accepted):
87 | self.available = available
88 | self.sent = sent
89 | self.accepted = accepted
90 |
91 |
92 | class InvitationStatsInviteOnlyTestCase(InvitationStatsBaseTestCase):
93 | def setUp(self):
94 | super(InvitationStatsInviteOnlyTestCase, self).setUp()
95 | app_settings.INVITE_ONLY = True
96 |
97 | def test_default_performance_func(self):
98 | self.assertAlmostEqual(performance_calculator_invite_only(
99 | self.MockInvitationStats(5, 5, 1)), 0.42)
100 | self.assertAlmostEqual(performance_calculator_invite_only(
101 | self.MockInvitationStats(0, 10, 10)), 1.0)
102 | self.assertAlmostEqual(performance_calculator_invite_only(
103 | self.MockInvitationStats(10, 0, 0)), 0.0)
104 |
105 | def test_add_available(self):
106 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS, 0, 0))
107 | self.user().invitation_stats.add_available()
108 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 1, 0, 0))
109 | self.user().invitation_stats.add_available(10)
110 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 11, 0, 0))
111 |
112 | def test_use(self):
113 | self.user().invitation_stats.add_available(10)
114 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 10, 0, 0))
115 | self.user().invitation_stats.use()
116 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 9, 1, 0))
117 | self.user().invitation_stats.use(5)
118 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 4, 6, 0))
119 | self.assertRaises(InvitationError,
120 | self.user().invitation_stats.use,
121 | INITIAL_INVITATIONS + 5)
122 |
123 | def test_mark_accepted(self):
124 | if INITIAL_INVITATIONS < 10:
125 | i = 10
126 | self.user().invitation_stats.add_available(10-INITIAL_INVITATIONS)
127 | else:
128 | i = INITIAL_INVITATIONS
129 | self.user().invitation_stats.use(i)
130 | self.user().invitation_stats.mark_accepted()
131 | self.assertEqual(self.stats(), (0, i, 1))
132 | self.user().invitation_stats.mark_accepted(5)
133 | self.assertEqual(self.stats(), (0, i, 6))
134 | self.assertRaises(InvitationError,
135 | self.user().invitation_stats.mark_accepted, i)
136 |
137 | def test_give_invitations(self):
138 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS, 0, 0))
139 | InvitationStats.objects.give_invitations(count=3)
140 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 3, 0, 0))
141 | InvitationStats.objects.give_invitations(self.user(), count=3)
142 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 6, 0, 0))
143 | InvitationStats.objects.give_invitations(self.user(),
144 | count=lambda u: 4)
145 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS + 10, 0, 0))
146 |
147 | def test_reward(self):
148 | self.assertAlmostEqual(self.user().invitation_stats.performance, 0.0)
149 | InvitationStats.objects.reward()
150 | self.assertEqual(self.user().invitation_stats.available,
151 | INITIAL_INVITATIONS)
152 | self.user().invitation_stats.use(INITIAL_INVITATIONS)
153 | self.user().invitation_stats.mark_accepted(INITIAL_INVITATIONS)
154 | InvitationStats.objects.reward()
155 | invitation_stats = self.user().invitation_stats
156 | self.assertEqual(invitation_stats.performance > 0.5, True)
157 | self.assertEqual(invitation_stats.available, INITIAL_INVITATIONS)
158 |
159 |
160 | class InvitationStatsInviteOptionalTestCase(InvitationStatsBaseTestCase):
161 | def setUp(self):
162 | super(InvitationStatsInviteOptionalTestCase, self).setUp()
163 | app_settings.INVITE_ONLY = False
164 |
165 | def test_default_performance_func(self):
166 | self.assertAlmostEqual(performance_calculator_invite_optional(
167 | self.MockInvitationStats(5, 5, 1)), 0.2)
168 | self.assertAlmostEqual(performance_calculator_invite_optional(
169 | self.MockInvitationStats(20, 5, 1)), 0.2)
170 | self.assertAlmostEqual(performance_calculator_invite_optional(
171 | self.MockInvitationStats(0, 5, 1)), 0.2)
172 | self.assertAlmostEqual(performance_calculator_invite_optional(
173 | self.MockInvitationStats(0, 10, 10)), 1.0)
174 | self.assertAlmostEqual(performance_calculator_invite_optional(
175 | self.MockInvitationStats(10, 0, 0)), 0.0)
176 |
177 | def test_use(self):
178 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS, 0, 0))
179 | self.user().invitation_stats.use()
180 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS, 1, 0))
181 | self.user().invitation_stats.use(5)
182 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS, 6, 0))
183 | self.user().invitation_stats.use(INITIAL_INVITATIONS + 5)
184 | self.assertEqual(self.stats(), (INITIAL_INVITATIONS,
185 | INITIAL_INVITATIONS + 11,
186 | 0))
187 |
188 | def test_mark_accepted(self):
189 | if INITIAL_INVITATIONS < 10:
190 | i = 10
191 | self.user().invitation_stats.add_available(10-INITIAL_INVITATIONS)
192 | else:
193 | i = INITIAL_INVITATIONS
194 | self.user().invitation_stats.use(i)
195 | self.user().invitation_stats.mark_accepted()
196 | self.assertEqual(self.stats(), (i, i, 1))
197 | self.user().invitation_stats.mark_accepted(5)
198 | self.assertEqual(self.stats(), (i, i, 6))
199 | self.assertRaises(InvitationError,
200 | self.user().invitation_stats.mark_accepted, i)
201 | self.user().invitation_stats.mark_accepted(4)
202 | self.assertEqual(self.stats(), (i, i, 10))
203 |
204 | def test_reward(self):
205 | self.assertAlmostEqual(self.user().invitation_stats.performance, 0.0)
206 | InvitationStats.objects.reward()
207 | self.assertEqual(self.user().invitation_stats.available,
208 | INITIAL_INVITATIONS)
209 | self.user().invitation_stats.use(INITIAL_INVITATIONS)
210 | self.user().invitation_stats.mark_accepted(INITIAL_INVITATIONS)
211 | InvitationStats.objects.reward()
212 | invitation_stats = self.user().invitation_stats
213 | self.assertEqual(
214 | invitation_stats.performance > app_settings.REWARD_THRESHOLD, True)
215 | self.assertEqual(invitation_stats.available, INITIAL_INVITATIONS * 2)
216 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invitation_complete.html:
--------------------------------------------------------------------------------
1 | Invitation Complete
2 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invitation_email.txt:
--------------------------------------------------------------------------------
1 | Invitation Email
2 |
3 | http://{{ site.domain }}{{ invitation.get_absolute_url }}
4 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invitation_email_subject.txt:
--------------------------------------------------------------------------------
1 | Invitation
2 | Subject
3 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invitation_form.html:
--------------------------------------------------------------------------------
1 | Invitation Form
2 |
3 | {{ form }}
4 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invitation_home.html:
--------------------------------------------------------------------------------
1 | Invitation Home
2 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invitation_registered.html:
--------------------------------------------------------------------------------
1 | Invitation Home
2 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invitation_unavailable.html:
--------------------------------------------------------------------------------
1 | Invitation Unavailable
2 |
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/invite_only.html:
--------------------------------------------------------------------------------
1 | Invite Only
--------------------------------------------------------------------------------
/invitation/tests/templates/invitation/wrong_invitation_key.html:
--------------------------------------------------------------------------------
1 | Wrong Invitation Key
--------------------------------------------------------------------------------
/invitation/tests/templates/registration/registration_complete.html:
--------------------------------------------------------------------------------
1 | Registration Complete
2 |
--------------------------------------------------------------------------------
/invitation/tests/templates/registration/registration_form.html:
--------------------------------------------------------------------------------
1 | Registration Form
2 |
3 | {{ form }}
4 |
--------------------------------------------------------------------------------
/invitation/tests/templates/registration/registration_register.html:
--------------------------------------------------------------------------------
1 | Registration Register
2 |
--------------------------------------------------------------------------------
/invitation/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 | import invitation.urls as invitation_urls
3 |
4 |
5 | urlpatterns = invitation_urls.urlpatterns + patterns('',
6 | url(r'^register/$',
7 | 'django.views.generic.simple.direct_to_template',
8 | {'template': 'registration/registration_register.html'},
9 | name='registration_register'),
10 | url(r'^register/complete/$',
11 | 'django.views.generic.simple.direct_to_template',
12 | {'template': 'registration/registration_complete.html'},
13 | name='registration_complete'),
14 | )
15 |
--------------------------------------------------------------------------------
/invitation/tests/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.conf import settings
3 | from django.test import TestCase
4 | from django.contrib.auth.models import User
5 |
6 |
7 | class BaseTestCase(TestCase):
8 | def setUp(self):
9 | super(BaseTestCase, self).setUp()
10 | settings.TEMPLATE_DIRS = (
11 | os.path.join(os.path.dirname(__file__), 'templates'),
12 | )
13 | User.objects.create_user('testuser',
14 | 'testuser@example.com',
15 | 'testuser')
16 |
17 | def user(self):
18 | return User.objects.get(username='testuser')
19 |
--------------------------------------------------------------------------------
/invitation/tests/views.py:
--------------------------------------------------------------------------------
1 | from django.core.urlresolvers import reverse
2 | from django.core import mail
3 | from django.contrib.auth.models import User
4 | from utils import BaseTestCase
5 | from invitation.models import Invitation
6 |
7 |
8 | class InviteOnlyModeTestCase(BaseTestCase):
9 | urls = 'invitation.tests.invite_only_urls'
10 |
11 | def test_invation_mode(self):
12 | # Normal registration view should redirect
13 | response = self.client.get(reverse('registration_register'))
14 | self.assertRedirects(response, reverse('invitation_invite_only'))
15 | # But registration after invitation view should work
16 | response = self.client.get(reverse('invitation_register',
17 | args=('A' * 40,)))
18 | self.assertEqual(response.status_code, 200)
19 |
20 | def test_invitation(self):
21 | available = self.user().invitation_stats.available
22 | self.client.login(username='testuser', password='testuser')
23 | response = self.client.post(reverse('invitation_invite'),
24 | {'email': 'friend@example.com'})
25 | self.assertRedirects(response, reverse('invitation_complete'))
26 | self.assertEqual(self.user().invitation_stats.available, available-1)
27 | # Delete previously created invitation and
28 | # set available invitations count to 0.
29 | Invitation.objects.all().delete()
30 | invitation_stats = self.user().invitation_stats
31 | invitation_stats.available = 0
32 | invitation_stats.save()
33 | del(invitation_stats)
34 | response = self.client.post(reverse('invitation_invite'),
35 | {'email': 'friend@example.com'})
36 | self.assertRedirects(response, reverse('invitation_unavailable'))
37 |
38 | def test_registration(self):
39 | # Make sure error message is shown in
40 | # case of an invalid invitation key
41 | response = self.client.get(reverse('invitation_register',
42 | args=('A' * 40,)))
43 | self.assertEqual(response.status_code, 200)
44 | self.assertTemplateUsed(response,
45 | 'invitation/wrong_invitation_key.html')
46 | # Registration with an invitation
47 | invitation = Invitation.objects.invite(self.user(),
48 | 'friend@example.com')
49 | register_url = reverse('invitation_register', args=(invitation.key,))
50 | response = self.client.get(register_url)
51 | self.assertEqual(response.status_code, 200)
52 | self.assertTemplateUsed(response,
53 | 'registration/registration_form.html')
54 | self.assertContains(response, invitation.email)
55 | # We are posting a different email than the
56 | # invitation.email but the form should just
57 | # ignore it and register with invitation.email
58 | response = self.client.post(register_url,
59 | {'username': u'friend',
60 | 'email': u'noone@example.com',
61 | 'password1': u'friend',
62 | 'password2': u'friend'})
63 | self.assertRedirects(response, reverse('invitation_registered'))
64 | self.assertEqual(len(mail.outbox), 0) # No confirmation email
65 | self.assertEqual(self.user().invitation_stats.accepted, 1)
66 | new_user = User.objects.get(username='friend')
67 | self.assertEqual(new_user.is_active, True)
68 | self.assertRaises(Invitation.DoesNotExist,
69 | Invitation.objects.get,
70 | user=self.user(),
71 | email='friend@example.com')
72 |
73 |
74 | class InviteOptionalModeTestCase(BaseTestCase):
75 | urls = 'invitation.tests.invite_optional_urls'
76 |
77 | def test_invation_mode(self):
78 | # Normal registration view should work
79 | response = self.client.get(reverse('registration_register'))
80 | self.assertEqual(response.status_code, 200)
81 | self.assertTemplateUsed(response,
82 | 'registration/registration_register.html')
83 | # So as registration after invitation view
84 | response = self.client.get(reverse('invitation_register',
85 | args=('A' * 40,)))
86 | self.assertEqual(response.status_code, 200)
87 |
88 | def test_invitation(self):
89 | self.client.login(username='testuser', password='testuser')
90 | response = self.client.get(reverse('invitation_invite'))
91 | self.assertEqual(response.status_code, 200)
92 | response = self.client.post(reverse('invitation_invite'),
93 | {'email': 'friend@example.com'})
94 | self.assertRedirects(response, reverse('invitation_complete'))
95 | invitation_query = Invitation.objects.filter(user=self.user(),
96 | email='friend@example.com')
97 | self.assertEqual(invitation_query.count(), 1)
98 | self.assertEqual(len(mail.outbox), 1)
99 | self.assertEqual(self.user().invitation_stats.sent, 1)
100 |
--------------------------------------------------------------------------------
/invitation/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 | from django.views.generic.simple import direct_to_template
3 | from django.contrib.auth.decorators import login_required
4 | from app_settings import INVITE_ONLY
5 |
6 |
7 | login_required_direct_to_template = login_required(direct_to_template)
8 |
9 |
10 | urlpatterns = patterns('',
11 | url(r'^invitation/$',
12 | login_required_direct_to_template,
13 | {'template': 'invitation/invitation_home.html'},
14 | name='invitation_home'),
15 | url(r'^invitation/invite/$',
16 | 'invitation.views.invite',
17 | name='invitation_invite'),
18 | url(r'^invitation/invite/complete/$',
19 | login_required_direct_to_template,
20 | {'template': 'invitation/invitation_complete.html'},
21 | name='invitation_complete'),
22 | url(r'^invitation/invite/unavailable/$',
23 | login_required_direct_to_template,
24 | {'template': 'invitation/invitation_unavailable.html'},
25 | name='invitation_unavailable'),
26 | url(r'^invitation/accept/complete/$',
27 | direct_to_template,
28 | {'template': 'invitation/invitation_registered.html'},
29 | name='invitation_registered'),
30 | url(r'^invitation/accept/(?P\w+)/$',
31 | 'invitation.views.register',
32 | name='invitation_register'),
33 | )
34 |
35 |
36 | if INVITE_ONLY:
37 | urlpatterns += patterns('',
38 | url(r'^register/$',
39 | 'django.views.generic.simple.redirect_to',
40 | {'url': '../invitation/invite_only/', 'permanent': False},
41 | name='registration_register'),
42 | url(r'^invitation/invite_only/$',
43 | direct_to_template,
44 | {'template': 'invitation/invite_only.html'},
45 | name='invitation_invite_only'),
46 | url(r'^invitation/reward/$',
47 | 'invitation.views.reward',
48 | name='invitation_reward'),
49 | )
50 |
--------------------------------------------------------------------------------
/invitation/views.py:
--------------------------------------------------------------------------------
1 | from django.core.urlresolvers import reverse
2 | from django.http import HttpResponseRedirect
3 | from django.template import RequestContext
4 | from django.shortcuts import render_to_response
5 | from django.utils.translation import ugettext
6 | from django.contrib.auth.decorators import login_required
7 | from django.contrib.admin.views.decorators import staff_member_required
8 | from models import InvitationError, Invitation, InvitationStats
9 | from forms import InvitationForm, RegistrationFormInvitation
10 | from registration.signals import user_registered
11 |
12 |
13 | def apply_extra_context(context, extra_context=None):
14 | if extra_context is None:
15 | extra_context = {}
16 | for key, value in extra_context.items():
17 | context[key] = callable(value) and value() or value
18 | return context
19 |
20 |
21 | @login_required
22 | def invite(request, success_url=None,
23 | form_class=InvitationForm,
24 | template_name='invitation/invitation_form.html',
25 | extra_context=None):
26 | """
27 | Create an invitation and send invitation email.
28 |
29 | Send invitation email and then redirect to success URL if the
30 | invitation form is valid. Redirect named URL ``invitation_unavailable``
31 | on InvitationError. Render invitation form template otherwise.
32 |
33 | **Required arguments:**
34 |
35 | None.
36 |
37 | **Optional arguments:**
38 |
39 | :success_url:
40 | The URL to redirect to on successful registration. Default value is
41 | ``None``, ``invitation_complete`` will be resolved in this case.
42 |
43 | :form_class:
44 | A form class to use for invitation. Takes ``request.user`` as first
45 | argument to its constructor. Must have an ``email`` field. Custom
46 | validation can be implemented here.
47 |
48 | :template_name:
49 | A custom template to use. Default value is
50 | ``invitation/invitation_form.html``.
51 |
52 | :extra_context:
53 | A dictionary of variables to add to the template context. Any
54 | callable object in this dictionary will be called to produce
55 | the end result which appears in the context.
56 |
57 | **Template:**
58 |
59 | ``invitation/invitation_form.html`` or ``template_name`` keyword
60 | argument.
61 |
62 | **Context:**
63 |
64 | A ``RequestContext`` instance is used rendering the template. Context,
65 | in addition to ``extra_context``, contains:
66 |
67 | :form:
68 | The invitation form.
69 | """
70 | if request.method == 'POST':
71 | form = form_class(request.POST, request.FILES)
72 | if form.is_valid():
73 | try:
74 | invitation = Invitation.objects.invite(
75 | request.user, form.cleaned_data["email"])
76 | except InvitationError:
77 | return HttpResponseRedirect(reverse('invitation_unavailable'))
78 | invitation.send_email(request=request)
79 | return HttpResponseRedirect(success_url or \
80 | reverse('invitation_complete'))
81 | else:
82 | form = form_class()
83 | context = apply_extra_context(RequestContext(request), extra_context)
84 | return render_to_response(template_name,
85 | {'form': form},
86 | context_instance=context)
87 |
88 |
89 | def register(request,
90 | invitation_key,
91 | wrong_key_template='invitation/wrong_invitation_key.html',
92 | redirect_to_if_authenticated='/',
93 | success_url=None,
94 | form_class=RegistrationFormInvitation,
95 | template_name='registration/registration_form.html',
96 | extra_context=None):
97 | """
98 | Allow a new user to register via invitation.
99 |
100 | Send invitation email and then redirect to success URL if the
101 | invitation form is valid. Redirect named URL ``invitation_unavailable``
102 | on InvitationError. Render invitation form template otherwise. Sends
103 | registration.signals.user_registered after creating the user.
104 |
105 | **Required arguments:**
106 |
107 | :invitation_key:
108 | An invitation key in the form of ``[\da-e]{40}``
109 |
110 | **Optional arguments:**
111 |
112 | :wrong_key_template:
113 | Template to be used when an invalid invitation key is supplied.
114 | Default value is ``invitation/wrong_invitation_key.html``.
115 |
116 | :redirect_to_if_authenticated:
117 | URL to be redirected when an authenticated user calls this view.
118 | Defaults value is ``/``
119 |
120 | :success_url:
121 | The URL to redirect to on successful registration. Default value is
122 | ``None``, ``invitation_registered`` will be resolved in this case.
123 |
124 | :form_class:
125 | A form class to use for registration. Takes the invited email as first
126 | argument to its constructor.
127 |
128 | :template_name:
129 | A custom template to use. Default value is
130 | ``registration/registration_form.html``.
131 |
132 | :extra_context:
133 | A dictionary of variables to add to the template context. Any
134 | callable object in this dictionary will be called to produce
135 | the end result which appears in the context.
136 |
137 | **Templates:**
138 |
139 | ``invitation/invitation_form.html`` or ``template_name`` keyword
140 | argument as the *main template*.
141 |
142 | ``invitation/wrong_invitation_key.html`` or ``wrong_key_template`` keyword
143 | argument as the *wrong key template*.
144 |
145 | **Context:**
146 |
147 | ``RequestContext`` instances are used rendering both templates. Context,
148 | in addition to ``extra_context``, contains:
149 |
150 | For wrong key template
151 | :invitation_key: supplied invitation key
152 |
153 | For main template
154 | :form:
155 | The registration form.
156 | """
157 | if request.user.is_authenticated():
158 | return HttpResponseRedirect(redirect_to_if_authenticated)
159 | try:
160 | invitation = Invitation.objects.find(invitation_key)
161 | except Invitation.DoesNotExist:
162 | context = apply_extra_context(RequestContext(request), extra_context)
163 | return render_to_response(wrong_key_template,
164 | {'invitation_key': invitation_key},
165 | context_instance=context)
166 | if request.method == 'POST':
167 | form = form_class(invitation.email, request.POST, request.FILES)
168 | if form.is_valid():
169 | new_user = form.save()
170 | invitation.mark_accepted(new_user)
171 | user_registered.send(sender="invitation",
172 | user=new_user,
173 | request=request)
174 | return HttpResponseRedirect(success_url or \
175 | reverse('invitation_registered'))
176 | else:
177 | form = form_class(invitation.email)
178 | context = apply_extra_context(RequestContext(request), extra_context)
179 | return render_to_response(template_name,
180 | {'form': form},
181 | context_instance=context)
182 |
183 |
184 | @staff_member_required
185 | def reward(request):
186 | """
187 | Add invitations to users with high invitation performance and redirect
188 | refferring page.
189 | """
190 | rewarded_users, invitations_given = InvitationStats.objects.reward()
191 | if rewarded_users:
192 | message = ugettext(u'%(users)s users are given a total of ' \
193 | u'%(invitations)s invitations.') % {
194 | 'users': rewarded_users,
195 | 'invitations': invitations_given}
196 | else:
197 | message = ugettext(u'No user has performance above ' \
198 | u'threshold, no invitations awarded.')
199 | request.user.message_set.create(message=message)
200 | return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
201 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from distutils.core import setup
4 | from invitation import __version__, __maintainer__, __email__
5 |
6 |
7 | def compile_translations():
8 | try:
9 | from django.core.management.commands.compilemessages \
10 | import compile_messages
11 | except ImportError:
12 | return None
13 | curdir = os.getcwdu()
14 | os.chdir(os.path.join(os.path.dirname(__file__), 'invitation'))
15 | try:
16 | compile_messages(stderr=sys.stderr)
17 | except TypeError:
18 | # compile_messages doesn't accept stderr parameter prior to 1.2.4
19 | compile_messages()
20 | os.chdir(curdir)
21 | compile_translations()
22 |
23 |
24 | license_text = open('LICENSE.txt').read()
25 | long_description = open('README.rst').read()
26 |
27 |
28 | setup(
29 | name = 'django-inviting',
30 | version = __version__,
31 | url = 'http://github.com/muhuk/django-inviting',
32 | author = __maintainer__,
33 | author_email = __email__,
34 | license = license_text,
35 | packages = ['invitation',
36 | 'invitation.tests',
37 | 'invitation.templatetags'],
38 | package_data= {
39 | 'invitation': ['templates/admin/invitation/invitationstats/*',
40 | 'tests/templates/invitations/*',
41 | 'tests/templates/registration/*',
42 | 'locale/*/LC_MESSAGES/django.*']
43 | },
44 | data_files=[('', ['LICENSE.txt',
45 | 'README.rst'])],
46 | description = 'Registration through invitations',
47 | long_description=long_description,
48 | classifiers = ['Development Status :: 5 - Production/Stable',
49 | 'Environment :: Web Environment',
50 | 'Framework :: Django',
51 | 'Intended Audience :: Developers',
52 | 'License :: OSI Approved :: BSD License',
53 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content']
54 | )
55 |
--------------------------------------------------------------------------------