Commits

uniqx committed 21a59e6 Merge

mörtsch

  • Participants
  • Parent commits 84afdb4, 65459dc

Comments (0)

Files changed (120)

 ^.+\.pyc$
 ^.+\.pyo$
 
+^.*~$
 ^.*\/\..+\.swp
 ^.*\/\..+\.swo
 

File freieit/settings.py

     'django.contrib.flatpages',
     'taggit',
     'haystack',
+    'registration',
+    'invitation',
     'freieit',
 )
 
         'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
     },
 }
+
+EMAIL_HOST = 'localhost'
+EMAIL_PORT = 2525
+EMAIL_USE_TLS = True
+DEFAULT_FROM_EMAIL = "freie.it <freie-it@logic.at>"
+
+INVITE_MODE = True
+ACCOUNT_INVITATION_DAYS = 14
+ACCOUNT_ACTIVATION_DAYS = 14
+INVITATIONS_PER_USER = 999

File freieit/templates/base.html

           <ul>
             <li>
               {% if user.is_authenticated %}
-                <a href="{% url freieit.views.login.do_logout %}">logout {{ user.username }}</a>
+                <a href="{% url invitation.views.invite %}">Invite an expert</a> /
+                <a href="{% url logout %}">logout {{ user.username }}</a>
               {% else %}
-                <a href="{% url freieit.views.login.show %}">Login</a> /
-                <a href="{% url freieit.views.register.show %}">registrieren</a></li>
+                <a href="{% url login %}">Login</a>
+                {# / <a href="/accounts/register">registrieren</a></li> #}
               {% endif %}
             <li><a href="/terms_of_business">Geschäftsbedingungen</a></li>
             <li><a href="/become_expert">freie.it Expert_in werden</a></li>

File freieit/templates/home.html

+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+<section>
+
+  <header>
+    <h2>Home</h2>
+  </header>
+
+  Startseitendummy
+
+</section>
+
+{% endblock content %}
+
+
+

File freieit/templates/login_form.html

       Passwort: <input type="password" name="password"><br />
       <input type="submit" value="anmelden">
     </form>
-    Falls Sie noch kein Benutzerkonto haben können sie sich <a href="{% url freieit.views.register.show %}">hier registieren</a>.
+    Falls Sie noch kein Benutzerkonto haben können sie sich <a href="/accounts/register">hier registieren</a>.
   </div>
 
 </section>

File freieit/templates/registration/activate.html

Empty file added.

File freieit/templates/registration/activation_complete.html

+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+  <h1>{% trans "Activation complete!" %}</h1>
+{% endblock %}

File freieit/templates/registration/activation_email.txt

+{% load i18n %}
+{% trans "Just one more step to activate your account on" %} {{ site }}.
+
+{% trans "Go to" %}
+http://{{site.domain}}{% url registration_activate activation_key=activation_key %}
+{% trans "to activate!" %}
+
+{% blocktrans with site.name as sitename %}All the best,
+
+The {{ sitename }} Team{% endblocktrans %}

File freieit/templates/registration/activation_email_subject.txt

+Activate your {{ site }} account

File freieit/templates/registration/registration_complete.html

+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+  <h1>{% trans "Registration confirmed!" %}</h1>
+  {% trans "Check your mails for activation link." %}
+{% endblock %}

File freieit/templates/registration/registration_form.html

+{% extends "base.html" %}
+
+{% block title %}Create an account{% endblock %}
+
+{% block content %}
+  <h1>Create an account</h1>
+
+  <form action="" method="post">
+      {% csrf_token %}
+      {{ form.as_p }}
+      <input type="submit" value="Create the account">
+  </form>
+{% endblock %}

File freieit/urls.py

 from django.conf import settings
 from django.conf.urls import patterns, include, url
+from invitation.views import register
+from django.contrib.auth.views import login, logout
+from registration.forms import RegistrationFormTermsOfService
 
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
 from django.contrib import admin
 admin.autodiscover()
 
-urlpatterns = patterns('',
-  # Examples:
-  # url(r'^$', 'freieit.views.home', name='home'),
-  # url(r'^freieit/', include('freieit.foo.urls')),
+if getattr(settings, 'INVITE_MODE', False):
+    urlpatterns = patterns('',
+        url(r'^accounts/register/$',    register,
+                                            {   
+                                                'form_class': RegistrationFormTermsOfService,
+                                                'backend': 'invitation.backends.InvitationBackend',
+                                            },  
+                                            name='registration_register'),
+    )   
+else:
+    urlpatterns = patterns('',
+        url(r'^accounts/register/$',    register,
+                                            {   
+                                                'form_class': RegistrationFormTermsOfService,
+                                                'backend': 'registration.backends.default.DefaultBackend',
+                                            },  
+                                            name='registration_register'),
+    )
 
+urlpatterns += patterns('',
+    url(r'^accounts/',              include('invitation.urls')),
+    url(r'^accounts/',              include('registration.urls')),
+    url(r'^admin/',                 include(admin.site.urls)),
+    # Uncomment the admin/doc line below to enable admin documentation:
+    url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+)
+
+urlpatterns += patterns('',
   (r'^search/', include('haystack.urls')),
 
-  #url(r'^', 'freieit.views.'),
-  #url(r'^$',                            'freieit.views.experts.show'),
+  url(r'^$', 'freieit.views.home.show'),
   url(r'^login$',                       'freieit.views.login.show'),
-  url(r'^logout$',                      'freieit.views.login.do_logout'),
-  url(r'^register$',                    'freieit.views.register.show'),
+  url(r'^login$',                       login, name='login'),
+  url(r'^accounts/login/$',             login),
+  url(r'^logout$',                      logout, name='logout'),
+  url(r'^accounts/logout/$',            logout),
+  #url(r'^register$',                    'freieit.views.register.show'),
   url(r'^profile$',                     'freieit.views.profile.show'),
   url(r'^expert/(?P<expert>[\w_\-]+)$', 'freieit.views.expert.show'),
   url(r'^experts/(?P<page_num>\d+)?$',  'freieit.views.experts.show'),
   url(r'^map$',                         'freieit.views.map.show'),
   url(r'^map/rss.xml$',                 'freieit.views.map.rss'),
-  # FIXME merge next 2 regex
-  #url(r'^tag/(?P<tag>\w+)',                   'freieit.views.tag.show'),
   url(r'^tag/(?P<tag>[\w_\-]+)/(?P<page_num>\d+)?', 'freieit.views.tag.show'),
-
-  # Uncomment the admin/doc line below to enable admin documentation:
-  url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
-
-  # Uncomment the next line to enable the admin:
-  url(r'^admin/', include(admin.site.urls)),
 )
 
 if settings.DEBUG:

File freieit/views/home.py

+from django.http import HttpResponse
+from django.shortcuts import render_to_response
+from django.contrib.auth import authenticate, login, logout
+
+from django.core.context_processors import csrf
+
+def show(request):
+  return render_to_response('home.html')

File freieit/views/register.py

 
 
 def show(request):
-  return render_to_response('register.html')
+    return render_to_response('register.html')
 
-
+def invite(request):
+    return render_to_response('invite.html')

File invitation/AUTHORS

+David Larlet <http://david.larlet.fr/>
+James Bennett <http://www.b-list.org/>
+Flo Ledermann <http://floledermann.com/>
+Brent O'Connor <http://epicserve.com/>

File invitation/LICENSE

+Copyright (c) 2008, see AUTHORS
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of the author nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File invitation/__init__.py

Empty file added.

File invitation/admin.py

+from django.contrib import admin
+from invitation.models import InvitationKey, InvitationUser
+
+class InvitationKeyAdmin(admin.ModelAdmin):
+    list_display = ('__unicode__', 'from_user', 'date_invited', 'key_expired')
+
+class InvitationUserAdmin(admin.ModelAdmin):
+    list_display = ('inviter', 'invitations_remaining')
+
+admin.site.register(InvitationKey, InvitationKeyAdmin)
+admin.site.register(InvitationUser, InvitationUserAdmin)

File invitation/backends.py

+from registration.backends.default import DefaultBackend
+from invitation.models import InvitationKey
+
+class InvitationBackend(DefaultBackend):
+
+    def post_registration_redirect(self, request, user, *args, **kwargs):
+        """
+        Return the name of the URL to redirect to after successful
+        user registration.
+
+        """
+        invitation_key = request.REQUEST.get('invitation_key')
+        key = InvitationKey.objects.get_key(invitation_key)
+        if key:
+            key.mark_used(user)
+
+        return ('registration_complete', (), {})

File invitation/forms.py

+from django import forms
+
+class InvitationKeyForm(forms.Form):
+    email = forms.EmailField()
+    

File invitation/locale/de/LC_MESSAGES/django.mo

Binary file added.

File invitation/locale/de/LC_MESSAGES/django.po

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-01-31 17:44+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: .\models.py:67
+msgid "invitation key"
+msgstr "Einladungscode"
+
+#: .\models.py:68
+msgid "date invited"
+msgstr "Einladungsdatum"
+
+#: .\templates\invitation\base.html.py:6
+msgid "Untitled Page"
+msgstr ""
+
+#: .\templates\invitation\invitation_complete.html.py:6
+msgid "Invitation sent"
+msgstr "Die Einladung wurde verschickt!"
+
+#: .\templates\invitation\invitation_complete.html.py:8
+msgid "Your friend will receive an email with your invitation soon!"
+msgstr ""
+"Der/die Empfänger/in wird in Kürze eine Email mit deiner Einladung erhalten."
+
+#: .\templates\invitation\invitation_email.txt.py:2
+#, python-format
+msgid "You have been invited by %(username)s to join %(sitename)s!"
+msgstr "Du hast eine Einladung von %(username)s zu %(sitename)s bekommen!"
+
+#: .\templates\invitation\invitation_email.txt.py:4
+msgid "Go to"
+msgstr "Klick auf"
+
+#: .\templates\invitation\invitation_email.txt.py:6
+msgid "to join!"
+msgstr "um mitzumachen!"
+
+#: .\templates\invitation\invitation_email.txt.py:8
+#, python-format
+msgid ""
+"All the best,\n"
+"\n"
+"The %(sitename)s Team"
+msgstr ""
+"Alles Gute,\n"
+"\n"
+"Das %(sitename)s Team"
+
+#: .\templates\invitation\invitation_email_subject.txt.py:2
+#, python-format
+msgid "Invitation from %(username)s to join %(sitename)s"
+msgstr "Einladung von %(username)s zu %(sitename)s"
+
+#: .\templates\invitation\invitation_form.html.py:6
+msgid "Invite a friend"
+msgstr "Leute einladen"
+
+#: .\templates\invitation\invitation_form.html.py:13
+msgid "Send Invitation"
+msgstr "Einladung abschicken"
+
+#: .\templates\invitation\invited.html.py:6
+msgid "You have been invited!"
+msgstr "Du wurdest eingeladen!"
+
+#: .\templates\invitation\invited.html.py:8
+msgid "proceed to registration"
+msgstr "Weiter zur Registrierung"

File invitation/management/__init__.py

Empty file added.

File invitation/management/commands/__init__.py

Empty file added.

File invitation/management/commands/cleanupinvitation.py

+"""
+A management command which deletes expired keys (e.g.,
+keys which were never activated) from the database.
+
+Calls ``InvitationKey.objects.delete_expired_keys()``, which
+contains the actual logic for determining which keys are deleted.
+
+"""
+
+from django.core.management.base import NoArgsCommand
+
+from invitation.models import InvitationKey
+
+
+class Command(NoArgsCommand):
+    help = "Delete expired invitations' keys from the database"
+
+    def handle_noargs(self, **options):
+        InvitationKey.objects.delete_expired_keys()

File invitation/models.py

+import os
+import random
+import datetime
+from django.db import models
+from django.conf import settings
+from django.utils.http import int_to_base36
+from django.utils.hashcompat import sha_constructor
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+from django.contrib.sites.models import Site
+
+from registration.models import SHA1_RE
+
+class InvitationKeyManager(models.Manager):
+    def get_key(self, invitation_key):
+        """
+        Return InvitationKey, or None if it doesn't (or shouldn't) exist.
+        """
+        # Don't bother hitting database if invitation_key doesn't match pattern.
+        if not SHA1_RE.search(invitation_key):
+            return None
+        
+        try:
+            key = self.get(key=invitation_key)
+        except self.model.DoesNotExist:
+            return None
+        
+        return key
+        
+    def is_key_valid(self, invitation_key):
+        """
+        Check if an ``InvitationKey`` is valid or not, returning a boolean,
+        ``True`` if the key is valid.
+        """
+        invitation_key = self.get_key(invitation_key)
+        return invitation_key and invitation_key.is_usable()
+
+    def create_invitation(self, user):
+        """
+        Create an ``InvitationKey`` and returns it.
+        
+        The key for the ``InvitationKey`` will be a SHA1 hash, generated 
+        from a combination of the ``User``'s username and a random salt.
+        """
+        salt = sha_constructor(str(random.random())).hexdigest()[:5]
+        key = sha_constructor("%s%s%s" % (datetime.datetime.now(), salt, user.username)).hexdigest()
+        return self.create(from_user=user, key=key)
+
+    def remaining_invitations_for_user(self, user):
+        """
+        Return the number of remaining invitations for a given ``User``.
+        """
+        invitation_user, created = InvitationUser.objects.get_or_create(
+            inviter=user,
+            defaults={'invitations_remaining': settings.INVITATIONS_PER_USER})
+        return invitation_user.invitations_remaining
+
+    def delete_expired_keys(self):
+        for key in self.all():
+            if key.key_expired():
+                key.delete()
+
+
+class InvitationKey(models.Model):
+    key = models.CharField(_('invitation key'), max_length=40)
+    date_invited = models.DateTimeField(_('date invited'), 
+                                        default=datetime.datetime.now)
+    from_user = models.ForeignKey(User, 
+                                  related_name='invitations_sent')
+    registrant = models.ForeignKey(User, null=True, blank=True, 
+                                  related_name='invitations_used')
+    
+    objects = InvitationKeyManager()
+    
+    def __unicode__(self):
+        return u"Invitation from %s on %s" % (self.from_user.username, self.date_invited)
+    
+    def is_usable(self):
+        """
+        Return whether this key is still valid for registering a new user.        
+        """
+        return self.registrant is None and not self.key_expired()
+    
+    def key_expired(self):
+        """
+        Determine whether this ``InvitationKey`` has expired, returning 
+        a boolean -- ``True`` if the key has expired.
+        
+        The date the key has been created is incremented by the number of days 
+        specified in the setting ``ACCOUNT_INVITATION_DAYS`` (which should be 
+        the number of days after invite during which a user is allowed to
+        create their account); if the result is less than or equal to the 
+        current date, the key has expired and this method returns ``True``.
+        
+        """
+        expiration_date = datetime.timedelta(days=settings.ACCOUNT_INVITATION_DAYS)
+        return self.date_invited + expiration_date <= datetime.datetime.now()
+    key_expired.boolean = True
+    
+    def mark_used(self, registrant):
+        """
+        Note that this key has been used to register a new user.
+        """
+        self.registrant = registrant
+        self.save()
+        
+    def send_to(self, email):
+        """
+        Send an invitation email to ``email``.
+        """
+        current_site = Site.objects.get_current()
+        
+        subject = render_to_string('invitation/invitation_email_subject.txt',
+                                   { 'site': current_site, 
+                                     'invitation_key': self })
+        # Email subject *must not* contain newlines
+        subject = ''.join(subject.splitlines())
+        
+        message = render_to_string('invitation/invitation_email.txt',
+                                   { 'invitation_key': self,
+                                     'expiration_days': settings.ACCOUNT_INVITATION_DAYS,
+                                     'site': current_site })
+        
+        send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email])
+
+        
+class InvitationUser(models.Model):
+    inviter = models.ForeignKey(User, unique=True)
+    invitations_remaining = models.IntegerField()
+
+    def __unicode__(self):
+        return u"InvitationUser for %s" % self.inviter.username
+
+    
+def user_post_save(sender, instance, created, **kwargs):
+    """Create InvitationUser for user when User is created."""
+    if created:
+        invitation_user = InvitationUser()
+        invitation_user.inviter = instance
+        invitation_user.invitations_remaining = settings.INVITATIONS_PER_USER
+        invitation_user.save()
+
+models.signals.post_save.connect(user_post_save, sender=User)
+
+def invitation_key_post_save(sender, instance, created, **kwargs):
+    """Decrement invitations_remaining when InvitationKey is created."""
+    if created:
+        invitation_user = InvitationUser.objects.get(inviter=instance.from_user)
+        remaining = invitation_user.invitations_remaining
+        invitation_user.invitations_remaining = remaining-1
+        invitation_user.save()
+
+models.signals.post_save.connect(invitation_key_post_save, sender=InvitationKey)
+

File invitation/templates/invitation/base.html

+{% load i18n %}
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>{% block title %}{% trans "Untitled Page" %}{% endblock %}</title>
+</head>
+
+<body>
+{% block invitation_content %}{% endblock %}
+</body>
+</html>

File invitation/templates/invitation/invitation_complete.html

+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<h2>{% trans "Invitation sent" %}</h2>
+<p>
+{% trans "Your friend will receive an email with your invitation soon!" %}
+</p>
+
+
+{% endblock %}

File invitation/templates/invitation/invitation_email.txt

+{% load i18n %}
+{% blocktrans with site.name as sitename and invitation_key.from_user.username as username %}You have been invited by {{ username }} to join {{ sitename }}!{% endblocktrans %}
+
+{% trans "Go to" %}
+http://{{site.domain}}{% url invitation_invited invitation_key=invitation_key.key %}
+{% trans "to join!" %}
+
+{% blocktrans with site.name as sitename %}All the best,
+
+The {{ sitename }} Team{% endblocktrans %}

File invitation/templates/invitation/invitation_email_subject.txt

+{% load i18n %}
+{% blocktrans with site.name as sitename and invitation_key.from_user.username as username %}Invitation from {{ username }} to join {{ sitename }}{% endblocktrans %}

File invitation/templates/invitation/invitation_form.html

+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<h2>{% trans "Invite a friend" %}</h2>
+
+{% ifequal remaining_invitations 0 %}
+<p>{% trans "You currently have no invitations to send." %}</p>
+{% else %}
+
+<form action="{% url invitation_invite %}" method="POST">{% csrf_token %}
+<table>
+{{form.as_table}}
+</table>
+
+<input type="submit" value="{% trans "Send Invitation" %}" />
+</form>
+{% endifequal %}
+
+{% endblock %}

File invitation/templates/invitation/invited.html

+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<h2>{% trans "You have been invited!" %}</h2>
+
+<a href="{% url registration_register %}?invitation_key={{ invitation_key }}">-&gt; {% trans "proceed to registration" %}</a>
+
+{% endblock %}

File invitation/templates/invitation/wrong_invitation_key.html

+{% extends "base.html" %}
+
+{% block content %}
+
+<b>Invalid invitation key!</b><br/>
+Make sure you got a valid invitation of one of our members.
+
+{% endblock %}

File invitation/tests.py

+"""
+Unit tests for django-invitation.
+
+These tests assume that you've completed all the prerequisites for
+getting django-invitation running in the default setup, to wit:
+
+1. You have ``invitation`` in your ``INSTALLED_APPS`` setting.
+
+2. You have created all of the templates mentioned in this
+   application's documentation.
+
+3. You have added the setting ``ACCOUNT_INVITATION_DAYS`` to your
+   settings file.
+
+4. You have URL patterns pointing to the invitation views.
+
+"""
+
+import datetime
+import sha
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core import mail
+from django.core import management
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+from invitation import forms
+from invitation.models import InvitationKey, InvitationUser
+
+class InvitationTestCase(TestCase):
+    """
+    Base class for the test cases.
+    
+    This sets up one user and two keys -- one expired, one not -- which are 
+    used to exercise various parts of the application.
+    
+    """
+    def setUp(self):
+        self.sample_user = User.objects.create_user(username='alice',
+                                                    password='secret',
+                                                    email='alice@example.com')
+        self.sample_key = InvitationKey.objects.create_invitation(user=self.sample_user)
+        self.expired_key = InvitationKey.objects.create_invitation(user=self.sample_user)
+        self.expired_key.date_invited -= datetime.timedelta(days=settings.ACCOUNT_INVITATION_DAYS + 1)
+        self.expired_key.save()
+        
+        self.sample_registration_data = {
+            'invitation_key': self.sample_key.key,
+            'username': 'new_user',
+            'email': 'newbie@example.com',
+            'password1': 'secret',
+            'password2': 'secret',
+            'tos': '1'}
+        
+
+    def assertRedirect(self, response, viewname):
+        """Assert that response has been redirected to ``viewname``."""
+        self.assertEqual(response.status_code, 302)
+        expected_location = 'http://testserver' + reverse(viewname)
+        self.assertEqual(response['Location'], expected_location)      
+
+
+class InvitationModelTests(InvitationTestCase):
+    """
+    Tests for the model-oriented functionality of django-invitation.
+    
+    """
+    def test_invitation_key_created(self):
+        """
+        Test that a ``InvitationKey`` is created for a new key.
+        
+        """
+        self.assertEqual(InvitationKey.objects.count(), 2)
+
+    def test_invitation_email(self):
+        """
+        Test that ``InvitationKey.send_to`` sends an invitation email.
+        
+        """
+        self.sample_key.send_to('bob@example.com')
+        self.assertEqual(len(mail.outbox), 1)
+
+    def test_key_expiration_condition(self):
+        """
+        Test that ``InvitationKey.key_expired()`` returns ``True`` for expired 
+        keys, and ``False`` otherwise.
+        
+        """
+        # Unexpired user returns False.
+        self.failIf(self.sample_key.key_expired())
+
+        # Expired user returns True.
+        self.failUnless(self.expired_key.key_expired())
+
+    def test_expired_user_deletion(self):
+        """
+        Test ``InvitationKey.objects.delete_expired_keys()``.
+        
+        Only keys whose expiration date has passed are deleted by 
+        delete_expired_keys.
+        
+        """
+        InvitationKey.objects.delete_expired_keys()
+        self.assertEqual(InvitationKey.objects.count(), 1)
+
+    def test_management_command(self):
+        """
+        Test that ``manage.py cleanupinvitation`` functions correctly.
+        
+        """
+        management.call_command('cleanupinvitation')
+        self.assertEqual(InvitationKey.objects.count(), 1)
+        
+    def test_invitations_remaining(self):
+        """Test InvitationUser calculates remaining invitations properly."""
+        remaining_invites = InvitationKey.objects.remaining_invitations_for_user
+
+        # New user starts with settings.INVITATIONS_PER_USER
+        user = User.objects.create_user(username='newbie',
+                                        password='secret',
+                                        email='newbie@example.com')
+        self.assertEqual(remaining_invites(user), settings.INVITATIONS_PER_USER)
+
+        # After using some, amount remaining is decreased
+        used = InvitationKey.objects.filter(from_user=self.sample_user).count()
+        expected_remaining = settings.INVITATIONS_PER_USER - used
+        remaining = remaining_invites(self.sample_user)
+        self.assertEqual(remaining, expected_remaining)
+        
+        # Using Invitationuser via Admin, remaining can be increased
+        invitation_user = InvitationUser.objects.get(inviter=self.sample_user)
+        new_remaining = 2*settings.INVITATIONS_PER_USER + 1
+        invitation_user.invitations_remaining = new_remaining
+        invitation_user.save()
+        remaining = remaining_invites(self.sample_user)
+        self.assertEqual(remaining, new_remaining)
+
+        # If no InvitationUser (for pre-existing/legacy User), one is created
+        old_sample_user = User.objects.create_user(username='lewis',
+                                                   password='secret',
+                                                   email='lewis@example.com')
+        old_sample_user.invitationuser_set.all().delete()
+        self.assertEqual(old_sample_user.invitationuser_set.count(), 0)
+        remaining = remaining_invites(old_sample_user)
+        self.assertEqual(remaining, settings.INVITATIONS_PER_USER)
+
+        
+class InvitationFormTests(InvitationTestCase):
+    """
+    Tests for the forms and custom validation logic included in
+    django-invitation.
+    
+    """
+    def test_invitation_form(self):
+        """
+        Test that ``InvitationKeyForm`` enforces email constraints.
+        
+        """
+        invalid_data_dicts = [
+            # Invalid email.
+            {
+            'data': { 'email': 'example.com' },
+            'error': ('email', [u"Enter a valid e-mail address."])
+            },
+            ]
+
+        for invalid_dict in invalid_data_dicts:
+            form = forms.InvitationKeyForm(data=invalid_dict['data'])
+            self.failIf(form.is_valid())
+            self.assertEqual(form.errors[invalid_dict['error'][0]], invalid_dict['error'][1])
+
+        form = forms.InvitationKeyForm(data={ 'email': 'foo@example.com' })
+        self.failUnless(form.is_valid())
+
+
+class InvitationViewTests(InvitationTestCase):
+    """
+    Tests for the views included in django-invitation.
+    
+    """
+    def test_invitation_view(self):
+        """
+        Test that the invitation view rejects invalid submissions,
+        and creates a new key and redirects after a valid submission.
+        
+        """
+        # You need to be logged in to send an invite.
+        response = self.client.login(username='alice', password='secret')
+        remaining_invitations = InvitationKey.objects.remaining_invitations_for_user(self.sample_user)
+        
+        # Invalid email data fails.
+        response = self.client.post(reverse('invitation_invite'),
+                                    data={ 'email': 'example.com' })
+        self.assertEqual(response.status_code, 200)
+        self.failUnless(response.context['form'])
+        self.failUnless(response.context['form'].errors)
+
+        # Valid email data succeeds.
+        response = self.client.post(reverse('invitation_invite'),
+                                    data={ 'email': 'foo@example.com' })
+        self.assertRedirect(response, 'invitation_complete')
+        self.assertEqual(InvitationKey.objects.count(), 3)
+        self.assertEqual(InvitationKey.objects.remaining_invitations_for_user(self.sample_user), remaining_invitations-1)
+        
+        # Once remaining invitations exhausted, you fail again.
+        while InvitationKey.objects.remaining_invitations_for_user(self.sample_user) > 0:
+            self.client.post(reverse('invitation_invite'),
+                             data={'email': 'foo@example.com'})
+        self.assertEqual(InvitationKey.objects.remaining_invitations_for_user(self.sample_user), 0)
+        response = self.client.post(reverse('invitation_invite'),
+                                    data={'email': 'foo@example.com'})
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.context['remaining_invitations'], 0)
+        self.failUnless(response.context['form'])
+    
+    def test_invited_view(self):
+        """
+        Test that the invited view invite the user from a valid
+        key and fails if the key is invalid or has expired.
+       
+        """
+        # Valid key puts use the invited template.
+        response = self.client.get(reverse('invitation_invited',
+                                           kwargs={ 'invitation_key': self.sample_key.key }))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'invitation/invited.html')
+
+        # Expired key use the wrong key template.
+        response = self.client.get(reverse('invitation_invited',
+                                           kwargs={ 'invitation_key': self.expired_key.key }))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'invitation/wrong_invitation_key.html')
+
+        # Invalid key use the wrong key template.
+        response = self.client.get(reverse('invitation_invited',
+                                           kwargs={ 'invitation_key': 'foo' }))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'invitation/wrong_invitation_key.html')
+
+        # Nonexistent key use the wrong key template.
+        response = self.client.get(reverse('invitation_invited',
+                                           kwargs={ 'invitation_key': sha.new('foo').hexdigest() }))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'invitation/wrong_invitation_key.html')
+
+    def test_register_view(self):
+        """
+        Test that after registration a key cannot be reused.
+        
+        """        
+        # The first use of the key to register a new user works.
+        registration_data = self.sample_registration_data.copy()
+        response = self.client.post(reverse('registration_register'), 
+                                    data=registration_data)
+        self.assertRedirect(response, 'registration_complete')
+        user = User.objects.get(username='new_user')
+        key = InvitationKey.objects.get_key(self.sample_key.key)
+        self.assertEqual(user, key.registrant)
+
+        # Trying to reuse the same key then fails.
+        registration_data['username'] = 'even_newer_user'
+        response = self.client.post(reverse('registration_register'), 
+                                    data=registration_data)
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 
+                                'invitation/wrong_invitation_key.html')
+        try:        
+            even_newer_user = User.objects.get(username='even_newer_user')
+            self.fail("Invitation already used - No user should be created.")
+        except User.DoesNotExist:
+            pass
+ 
+        
+class InviteModeOffTests(InvitationTestCase):
+    """
+    Tests for the case where INVITE_MODE is False.
+    
+    (The test cases other than this one generally assume that INVITE_MODE is 
+    True.)
+    
+    """
+    def setUp(self):
+        super(InviteModeOffTests, self).setUp()
+        self.saved_invite_mode = settings.INVITE_MODE
+        settings.INVITE_MODE = False
+
+    def tearDown(self):
+        settings.INVITE_MODE = self.saved_invite_mode
+        super(InviteModeOffTests, self).tearDown()
+       
+    def test_invited_view(self):
+        """
+        Test that the invited view redirects to registration_register.
+       
+        """
+        response = self.client.get(reverse('invitation_invited',
+                            kwargs={ 'invitation_key': self.sample_key.key }))
+        self.assertRedirect(response, 'registration_register')
+
+    def test_register_view(self):
+        """
+        Test register view.  
+        
+        With INVITE_MODE = FALSE, django-invitation just passes this view on to
+        django-registration's register.
+       
+        """
+        # get
+        response = self.client.get(reverse('registration_register'))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, 'registration/registration_form.html')
+        
+        # post
+        response = self.client.post(reverse('registration_register'), 
+                                    data=self.sample_registration_data)
+        self.assertRedirect(response, 'registration_complete')
+        

File invitation/urls.py

+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+
+from registration.forms import RegistrationFormTermsOfService
+from invitation.views import invite, invited, register
+
+urlpatterns = patterns('',
+    url(r'^invite/complete/$',
+                direct_to_template,
+                {'template': 'invitation/invitation_complete.html'},
+                name='invitation_complete'),
+    url(r'^invite/$',
+                invite,
+                name='invitation_invite'),
+    url(r'^invited/(?P<invitation_key>\w+)/$', 
+                invited,
+                name='invitation_invited'),
+    url(r'^register/$',
+                register,
+                { 'backend': 'registration.backends.default.DefaultBackend' },
+                name='registration_register'),
+)

File invitation/views.py

+from django.conf import settings
+from django.views.generic.simple import direct_to_template
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.contrib.auth.decorators import login_required
+
+from registration.views import register as registration_register
+from registration.forms import RegistrationForm
+from registration.backends import default as registration_backend
+
+from invitation.models import InvitationKey
+from invitation.forms import InvitationKeyForm
+from invitation.backends import InvitationBackend
+
+is_key_valid = InvitationKey.objects.is_key_valid
+remaining_invitations_for_user = InvitationKey.objects.remaining_invitations_for_user
+
+def invited(request, invitation_key=None, extra_context=None):
+    if getattr(settings, 'INVITE_MODE', False):
+        if invitation_key and is_key_valid(invitation_key):
+            template_name = 'invitation/invited.html'
+        else:
+            template_name = 'invitation/wrong_invitation_key.html'
+        extra_context = extra_context is not None and extra_context.copy() or {}
+        extra_context.update({'invitation_key': invitation_key})
+        return direct_to_template(request, template_name, extra_context)
+    else:
+        return HttpResponseRedirect(reverse('registration_register'))
+
+def register(request, backend, success_url=None,
+            form_class=RegistrationForm,
+            disallowed_url='registration_disallowed',
+            post_registration_redirect=None,
+            template_name='registration/registration_form.html',
+            wrong_template_name='invitation/wrong_invitation_key.html',
+            extra_context=None):
+    extra_context = extra_context is not None and extra_context.copy() or {}
+    if getattr(settings, 'INVITE_MODE', False):
+        invitation_key = request.REQUEST.get('invitation_key', False)
+        if invitation_key:
+            extra_context.update({'invitation_key': invitation_key})
+            if is_key_valid(invitation_key):
+                return registration_register(request, backend, success_url,
+                                            form_class, disallowed_url,
+                                            template_name, extra_context)
+            else:
+                extra_context.update({'invalid_key': True})
+        else:
+            extra_context.update({'no_key': True})
+        return direct_to_template(request, wrong_template_name, extra_context)
+    else:
+        return registration_register(request, backend, success_url, form_class,
+                                     disallowed_url, template_name, extra_context)
+
+def invite(request, success_url=None,
+            form_class=InvitationKeyForm,
+            template_name='invitation/invitation_form.html',
+            extra_context=None):
+    extra_context = extra_context is not None and extra_context.copy() or {}
+    remaining_invitations = remaining_invitations_for_user(request.user)
+    if request.method == 'POST':
+        form = form_class(data=request.POST, files=request.FILES)
+        if remaining_invitations > 0 and form.is_valid():
+            invitation = InvitationKey.objects.create_invitation(request.user)
+            invitation.send_to(form.cleaned_data["email"])
+            # success_url needs to be dynamically generated here; setting a
+            # a default value using reverse() will cause circular-import
+            # problems with the default URLConf for this application, which
+            # imports this file.
+            return HttpResponseRedirect(success_url or reverse('invitation_complete'))
+    else:
+        form = form_class()
+    extra_context.update({
+            'form': form,
+            'remaining_invitations': remaining_invitations,
+        })
+    return direct_to_template(request, template_name, extra_context)
+invite = login_required(invite)

File registration/AUTHORS

+The primary author of django-registration is James Bennett
+<james@b-list.org>, who may be found online at
+<http://www.b-list.org/>.
+
+
+Others who have contributed to the application:
+
+* Samuel Adam (French translation)
+* Jannis Leidel (German translation)
+* Rapahel Hertzog (helpful suggestions on packaging and distribution)
+* Panos Laganakos (Greek translation)
+* Ilya Filippov and Andy Mikhailenko (Russian translation)
+* Jarek Zgoda (Polish translation)
+* Meir Kriheli (Hebrew translation)
+* Italo Maia (Brazilian Portuguese translation)
+* Shinya Okano (Japanese translation)
+* A. Al-Ibrahim (Arabic translation)
+* Ernesto Rico Schmidt (Spanish translation)
+* Vladislav Mitov (Bulgarian translation)
+* Leonardo Manuel Rocha (Argentinean Spanish translation)
+* Emil Stenström (Swedish translation)
+* Liang Feng (Chinese translations)
+* Nebojsa Djordjevic (Serbian translation)
+* Nicola Larosa (Italian translation)
+* Joost Cassee (Dutch translation)
+* Björn Kristinsson (Icelandic translation)
+* Rune Bromer (Danish translation)
+* Domen Kožar (Slovenian translation)
+* Young Gyu Park (Korean translation)
+* Flavio Curella (updated Italian translation)
+* Jon Lønne (Bokmål translation)
+* Carles Barrobés (Catalan translation)
+* Nuno Mariz (Portuguese translation)
+* Patrick Samson (French translation)
+* Recep Kirmizi (Turkish translation)
+* Mohsen Mansouryar (Persian translation)

File registration/LICENSE

+Copyright (c) 2007-2012, James Bennett
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of the author nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File registration/__init__.py

+VERSION = (0, 8, 0, 'final', 0)
+
+def get_version(version=None):
+    """Derives a PEP386-compliant version number from VERSION."""
+    if version is None:
+        version = VERSION
+    assert len(version) == 5
+    assert version[3] in ('alpha', 'beta', 'rc', 'final')
+
+    # Now build the two parts of the version number:
+    # main = X.Y[.Z]
+    # sub = .devN - for pre-alpha releases
+    #     | {a|b|c}N - for alpha, beta and rc releases
+
+    parts = 2 if version[2] == 0 else 3
+    main = '.'.join(str(x) for x in version[:parts])
+
+    sub = ''
+    if version[3] == 'alpha' and version[4] == 0:
+        # At the toplevel, this would cause an import loop.
+        from django.utils.version import get_svn_revision
+        svn_revision = get_svn_revision()[4:]
+        if svn_revision != 'unknown':
+            sub = '.dev%s' % svn_revision
+
+    elif version[3] != 'final':
+        mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
+        sub = mapping[version[3]] + str(version[4])
+
+    return main + sub

File registration/admin.py

+from django.contrib import admin
+from django.contrib.sites.models import RequestSite
+from django.contrib.sites.models import Site
+from django.utils.translation import ugettext_lazy as _
+
+from registration.models import RegistrationProfile
+
+
+class RegistrationAdmin(admin.ModelAdmin):
+    actions = ['activate_users', 'resend_activation_email']
+    list_display = ('user', 'activation_key_expired')
+    raw_id_fields = ['user']
+    search_fields = ('user__username', 'user__first_name', 'user__last_name')
+
+    def activate_users(self, request, queryset):
+        """
+        Activates the selected users, if they are not alrady
+        activated.
+        
+        """
+        for profile in queryset:
+            RegistrationProfile.objects.activate_user(profile.activation_key)
+    activate_users.short_description = _("Activate users")
+
+    def resend_activation_email(self, request, queryset):
+        """
+        Re-sends activation emails for the selected users.
+
+        Note that this will *only* send activation emails for users
+        who are eligible to activate; emails will not be sent to users
+        whose activation keys have expired or who have already
+        activated.
+        
+        """
+        if Site._meta.installed:
+            site = Site.objects.get_current()
+        else:
+            site = RequestSite(request)
+
+        for profile in queryset:
+            if not profile.activation_key_expired():
+                profile.send_activation_email(site)
+    resend_activation_email.short_description = _("Re-send activation emails")
+
+
+admin.site.register(RegistrationProfile, RegistrationAdmin)

File registration/auth_urls.py

+"""
+URL patterns for the views included in ``django.contrib.auth``.
+
+Including these URLs (via the ``include()`` directive) will set up the
+following patterns based at whatever URL prefix they are included
+under:
+
+* User login at ``login/``.
+
+* User logout at ``logout/``.
+
+* The two-step password change at ``password/change/`` and
+  ``password/change/done/``.
+
+* The four-step password reset at ``password/reset/``,
+  ``password/reset/confirm/``, ``password/reset/complete/`` and
+  ``password/reset/done/``.
+
+The default registration backend already has an ``include()`` for
+these URLs, so under the default setup it is not necessary to manually
+include these views. Other backends may or may not include them;
+consult a specific backend's documentation for details.
+
+"""
+
+from django.conf.urls.defaults import *
+
+from django.contrib.auth import views as auth_views
+
+
+urlpatterns = patterns('',
+                       url(r'^login/$',
+                           auth_views.login,
+                           {'template_name': 'registration/login.html'},
+                           name='auth_login'),
+                       url(r'^logout/$',
+                           auth_views.logout,
+                           {'template_name': 'registration/logout.html'},
+                           name='auth_logout'),
+                       url(r'^password/change/$',
+                           auth_views.password_change,
+                           name='auth_password_change'),
+                       url(r'^password/change/done/$',
+                           auth_views.password_change_done,
+                           name='auth_password_change_done'),
+                       url(r'^password/reset/$',
+                           auth_views.password_reset,
+                           name='auth_password_reset'),
+                       url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
+                           auth_views.password_reset_confirm,
+                           name='auth_password_reset_confirm'),
+                       url(r'^password/reset/complete/$',
+                           auth_views.password_reset_complete,
+                           name='auth_password_reset_complete'),
+                       url(r'^password/reset/done/$',
+                           auth_views.password_reset_done,
+                           name='auth_password_reset_done'),
+)

File registration/backends/__init__.py

+from django.core.exceptions import ImproperlyConfigured
+
+
+# Python 2.7 has an importlib with import_module; for older Pythons,
+# Django's bundled copy provides it.
+try: # pragma: no cover
+    from importlib import import_module # pragma: no cover
+except ImportError: # pragma: no cover
+    from django.utils.importlib import import_module # pragma: no cover
+
+def get_backend(path):
+    """
+    Return an instance of a registration backend, given the dotted
+    Python import path (as a string) to the backend class.
+
+    If the backend cannot be located (e.g., because no such module
+    exists, or because the module does not contain a class of the
+    appropriate name), ``django.core.exceptions.ImproperlyConfigured``
+    is raised.
+    
+    """
+    i = path.rfind('.')
+    module, attr = path[:i], path[i+1:]
+    try:
+        mod = import_module(module)
+    except ImportError, e:
+        raise ImproperlyConfigured('Error loading registration backend %s: "%s"' % (module, e))
+    try:
+        backend_class = getattr(mod, attr)
+    except AttributeError:
+        raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
+    return backend_class()

File registration/backends/default/__init__.py

+from django.conf import settings
+from django.contrib.sites.models import RequestSite
+from django.contrib.sites.models import Site
+
+from registration import signals
+from registration.forms import RegistrationForm
+from registration.models import RegistrationProfile
+
+
+class DefaultBackend(object):
+    """
+    A registration backend which follows a simple workflow:
+
+    1. User signs up, inactive account is created.
+
+    2. Email is sent to user with activation link.
+
+    3. User clicks activation link, account is now active.
+
+    Using this backend requires that
+
+    * ``registration`` be listed in the ``INSTALLED_APPS`` setting
+      (since this backend makes use of models defined in this
+      application).
+
+    * The setting ``ACCOUNT_ACTIVATION_DAYS`` be supplied, specifying
+      (as an integer) the number of days from registration during
+      which a user may activate their account (after that period
+      expires, activation will be disallowed).
+
+    * The creation of the templates
+      ``registration/activation_email_subject.txt`` and
+      ``registration/activation_email.txt``, which will be used for
+      the activation email. See the notes for this backends
+      ``register`` method for details regarding these templates.
+
+    Additionally, registration can be temporarily closed by adding the
+    setting ``REGISTRATION_OPEN`` and setting it to
+    ``False``. Omitting this setting, or setting it to ``True``, will
+    be interpreted as meaning that registration is currently open and
+    permitted.
+
+    Internally, this is accomplished via storing an activation key in
+    an instance of ``registration.models.RegistrationProfile``. See
+    that model and its custom manager for full documentation of its
+    fields and supported operations.
+    
+    """
+    def register(self, request, **kwargs):
+        """
+        Given a username, email address and password, register a new
+        user account, which will initially be inactive.
+
+        Along with the new ``User`` object, a new
+        ``registration.models.RegistrationProfile`` will be created,
+        tied to that ``User``, containing the activation key which
+        will be used for this account.
+
+        An email will be sent to the supplied email address; this
+        email should contain an activation link. The email will be
+        rendered using two templates. See the documentation for
+        ``RegistrationProfile.send_activation_email()`` for
+        information about these templates and the contexts provided to
+        them.
+
+        After the ``User`` and ``RegistrationProfile`` are created and
+        the activation email is sent, the signal
+        ``registration.signals.user_registered`` will be sent, with
+        the new ``User`` as the keyword argument ``user`` and the
+        class of this backend as the sender.
+
+        """
+        username, email, password = kwargs['username'], kwargs['email'], kwargs['password1']
+        if Site._meta.installed:
+            site = Site.objects.get_current()
+        else:
+            site = RequestSite(request)
+        new_user = RegistrationProfile.objects.create_inactive_user(username, email,
+                                                                    password, site)
+        signals.user_registered.send(sender=self.__class__,
+                                     user=new_user,
+                                     request=request)
+        return new_user
+
+    def activate(self, request, activation_key):
+        """
+        Given an an activation key, look up and activate the user
+        account corresponding to that key (if possible).
+
+        After successful activation, the signal
+        ``registration.signals.user_activated`` will be sent, with the
+        newly activated ``User`` as the keyword argument ``user`` and
+        the class of this backend as the sender.
+        
+        """
+        activated = RegistrationProfile.objects.activate_user(activation_key)
+        if activated:
+            signals.user_activated.send(sender=self.__class__,
+                                        user=activated,
+                                        request=request)
+        return activated
+
+    def registration_allowed(self, request):
+        """
+        Indicate whether account registration is currently permitted,
+        based on the value of the setting ``REGISTRATION_OPEN``. This
+        is determined as follows:
+
+        * If ``REGISTRATION_OPEN`` is not specified in settings, or is
+          set to ``True``, registration is permitted.
+
+        * If ``REGISTRATION_OPEN`` is both specified and set to
+          ``False``, registration is not permitted.
+        
+        """
+        return getattr(settings, 'REGISTRATION_OPEN', True)
+
+    def get_form_class(self, request):
+        """
+        Return the default form class used for user registration.
+        
+        """
+        return RegistrationForm
+
+    def post_registration_redirect(self, request, user):
+        """
+        Return the name of the URL to redirect to after successful
+        user registration.
+        
+        """
+        return ('registration_complete', (), {})
+
+    def post_activation_redirect(self, request, user):
+        """
+        Return the name of the URL to redirect to after successful
+        account activation.
+        
+        """
+        return ('registration_activation_complete', (), {})

File registration/backends/default/urls.py

+"""
+URLconf for registration and activation, using django-registration's
+default backend.
+
+If the default behavior of these views is acceptable to you, simply
+use a line like this in your root URLconf to set up the default URLs
+for registration::
+
+    (r'^accounts/', include('registration.backends.default.urls')),
+
+This will also automatically set up the views in
+``django.contrib.auth`` at sensible default locations.
+
+If you'd like to customize the behavior (e.g., by passing extra
+arguments to the various views) or split up the URLs, feel free to set
+up your own URL patterns for these views instead.
+
+"""
+
+
+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+
+from registration.views import activate
+from registration.views import register
+
+
+urlpatterns = patterns('',
+                       url(r'^activate/complete/$',
+                           direct_to_template,
+                           {'template': 'registration/activation_complete.html'},
+                           name='registration_activation_complete'),
+                       # Activation keys get matched by \w+ instead of the more specific
+                       # [a-fA-F0-9]{40} because a bad activation key should still get to the view;
+                       # that way it can return a sensible "invalid key" message instead of a
+                       # confusing 404.
+                       url(r'^activate/(?P<activation_key>\w+)/$',
+                           activate,
+                           {'backend': 'registration.backends.default.DefaultBackend'},
+                           name='registration_activate'),
+                       url(r'^register/$',
+                           register,
+                           {'backend': 'registration.backends.default.DefaultBackend'},
+                           name='registration_register'),
+                       url(r'^register/complete/$',
+                           direct_to_template,
+                           {'template': 'registration/registration_complete.html'},
+                           name='registration_complete'),
+                       url(r'^register/closed/$',
+                           direct_to_template,
+                           {'template': 'registration/registration_closed.html'},
+                           name='registration_disallowed'),
+                       (r'', include('registration.auth_urls')),
+                       )

File registration/backends/simple/__init__.py

+from django.conf import settings
+from django.contrib.auth import authenticate
+from django.contrib.auth import login
+from django.contrib.auth.models import User
+
+from registration import signals
+from registration.forms import RegistrationForm
+
+
+class SimpleBackend(object):
+    """
+    A registration backend which implements the simplest possible
+    workflow: a user supplies a username, email address and password
+    (the bare minimum for a useful account), and is immediately signed
+    up and logged in.
+    
+    """
+    def register(self, request, **kwargs):
+        """
+        Create and immediately log in a new user.
+        
+        """
+        username, email, password = kwargs['username'], kwargs['email'], kwargs['password1']
+        User.objects.create_user(username, email, password)
+        
+        # authenticate() always has to be called before login(), and
+        # will return the user we just created.
+        new_user = authenticate(username=username, password=password)
+        login(request, new_user)
+        signals.user_registered.send(sender=self.__class__,
+                                     user=new_user,
+                                     request=request)
+        return new_user
+
+    def activate(self, **kwargs):
+        raise NotImplementedError
+
+    def registration_allowed(self, request):
+        """
+        Indicate whether account registration is currently permitted,
+        based on the value of the setting ``REGISTRATION_OPEN``. This
+        is determined as follows:
+
+        * If ``REGISTRATION_OPEN`` is not specified in settings, or is
+          set to ``True``, registration is permitted.
+
+        * If ``REGISTRATION_OPEN`` is both specified and set to
+          ``False``, registration is not permitted.
+        
+        """
+        return getattr(settings, 'REGISTRATION_OPEN', True)
+
+    def get_form_class(self, request):
+        return RegistrationForm
+
+    def post_registration_redirect(self, request, user):
+        """
+        After registration, redirect to the user's account page.
+        
+        """
+        return (user.get_absolute_url(), (), {})
+
+    def post_activation_redirect(self, request, user):
+        raise NotImplementedError

File registration/backends/simple/urls.py

+"""
+URLconf for registration and activation, using django-registration's
+one-step backend.
+
+If the default behavior of these views is acceptable to you, simply
+use a line like this in your root URLconf to set up the default URLs
+for registration::
+
+    (r'^accounts/', include('registration.backends.simple.urls')),
+
+This will also automatically set up the views in
+``django.contrib.auth`` at sensible default locations.
+
+If you'd like to customize the behavior (e.g., by passing extra
+arguments to the various views) or split up the URLs, feel free to set
+up your own URL patterns for these views instead.
+
+"""
+
+
+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+
+from registration.views import activate
+from registration.views import register
+
+
+urlpatterns = patterns('',
+                       url(r'^register/$',
+                           register,
+                           {'backend': 'registration.backends.simple.SimpleBackend'},
+                           name='registration_register'),
+                       url(r'^register/closed/$',
+                           direct_to_template,
+                           {'template': 'registration/registration_closed.html'},
+                           name='registration_disallowed'),
+                       (r'', include('registration.auth_urls')),
+                       )

File registration/forms.py

+"""
+Forms and validation code for user registration.
+
+"""
+
+
+from django.contrib.auth.models import User
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+
+# I put this on all required fields, because it's easier to pick up
+# on them with CSS or JavaScript if they have a class of "required"
+# in the HTML. Your mileage may vary. If/when Django ticket #3515
+# lands in trunk, this will no longer be necessary.
+attrs_dict = {'class': 'required'}
+
+
+class RegistrationForm(forms.Form):
+    """
+    Form for registering a new user account.
+    
+    Validates that the requested username is not already in use, and
+    requires the password to be entered twice to catch typos.
+    
+    Subclasses should feel free to add any additional validation they
+    need, but should avoid defining a ``save()`` method -- the actual
+    saving of collected user data is delegated to the active
+    registration backend.
+    
+    """
+    username = forms.RegexField(regex=r'^[\w.@+-]+$',
+                                max_length=30,
+                                widget=forms.TextInput(attrs=attrs_dict),
+                                label=_("Username"),
+                                error_messages={'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.")})
+    email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict,
+                                                               maxlength=75)),
+                             label=_("E-mail"))
+    password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False),
+                                label=_("Password"))
+    password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False),
+                                label=_("Password (again)"))
+    
+    def clean_username(self):
+        """
+        Validate that the username is alphanumeric and is not already
+        in use.
+        
+        """
+        existing = User.objects.filter(username__iexact=self.cleaned_data['username'])
+        if existing.exists():
+            raise forms.ValidationError(_("A user with that username already exists."))
+        else:
+            return self.cleaned_data['username']
+
+    def clean(self):
+        """
+        Verifiy that the values entered into the two password fields
+        match. Note that an error here will end up in
+        ``non_field_errors()`` because it doesn't apply to a single
+        field.
+        
+        """
+        if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
+            if self.cleaned_data['password1'] != self.cleaned_data['password2']:
+                raise forms.ValidationError(_("The two password fields didn't match."))
+        return self.cleaned_data
+
+
+class RegistrationFormTermsOfService(RegistrationForm):
+    """
+    Subclass of ``RegistrationForm`` which adds a required checkbox
+    for agreeing to a site's Terms of Service.
+    
+    """
+    tos = forms.BooleanField(widget=forms.CheckboxInput(attrs=attrs_dict),
+                             label=_(u'I have read and agree to the Terms of Service'),
+                             error_messages={'required': _("You must agree to the terms to register")})
+
+
+class RegistrationFormUniqueEmail(RegistrationForm):
+    """
+    Subclass of ``RegistrationForm`` which enforces uniqueness of
+    email addresses.
+    
+    """
+    def clean_email(self):
+        """
+        Validate that the supplied email address is unique for the
+        site.
+        
+        """
+        if User.objects.filter(email__iexact=self.cleaned_data['email']):
+            raise forms.ValidationError(_("This email address is already in use. Please supply a different email address."))
+        return self.cleaned_data['email']
+
+
+class RegistrationFormNoFreeEmail(RegistrationForm):
+    """
+    Subclass of ``RegistrationForm`` which disallows registration with
+    email addresses from popular free webmail services; moderately
+    useful for preventing automated spam registrations.
+    
+    To change the list of banned domains, subclass this form and
+    override the attribute ``bad_domains``.
+    
+    """
+    bad_domains = ['aim.com', 'aol.com', 'email.com', 'gmail.com',
+                   'googlemail.com', 'hotmail.com', 'hushmail.com',
+                   'msn.com', 'mail.ru', 'mailinator.com', 'live.com',
+                   'yahoo.com']
+    
+    def clean_email(self):
+        """
+        Check the supplied email address against a list of known free
+        webmail domains.
+        
+        """
+        email_domain = self.cleaned_data['email'].split('@')[1]
+        if email_domain in self.bad_domains:
+            raise forms.ValidationError(_("Registration using free email addresses is prohibited. Please supply a different email address."))
+        return self.cleaned_data['email']

File registration/locale/ar/LC_MESSAGES/django.mo

Binary file added.

File registration/locale/ar/LC_MESSAGES/django.po

+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-09-19 19:30-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:38
+msgid "username"
+msgstr "اسم المستخدم"
+
+#: forms.py:41
+msgid "email address"
+msgstr "عنوان البريد الالكتروني"
+
+#: forms.py:43
+msgid "password"
+msgstr "كلمة المرور"
+
+#: forms.py:45
+msgid "password (again)"
+msgstr "تأكيد كلمة المرور"
+
+#: forms.py:54
+msgid "Usernames can only contain letters, numbers and underscores"
+msgstr "يمكن أن يحتوي اسم المستخدم على احرف، ارقام وشرطات سطرية فقط"
+
+#: forms.py:59
+msgid "This username is already taken. Please choose another."
+msgstr "اسم المستخدم مسجل مسبقا. يرجى اختيار اسم اخر."
+
+#: forms.py:68
+msgid "You must type the same password each time"
+msgstr "يجب ادخال كلمة المرور مطابقة كل مرة"
+
+#: forms.py:96
+msgid "I have read and agree to the Terms of Service"
+msgstr "أقر بقراءة والموافقة على شروط الخدمة"
+