- {% endfor %}
- {% include "pagination.html" %}
-
-
-{% endblock %}
diff --git a/profiles/urls.py b/profiles/urls.py
index ebe8835..c71a83c 100644
--- a/profiles/urls.py
+++ b/profiles/urls.py
@@ -1,7 +1,6 @@
from django.conf.urls import patterns, include, url
from .views import *
-from .feeds import UserRecentSnippetsRss, UserRecentSnippetsAtom
urlpatterns = patterns('',
url(r'^$', MyProfileView.as_view(), name='my_profile'),
@@ -13,11 +12,4 @@
url(r'^(?P[\w.@+-]+)/project/new/$', ProjectCreateView.as_view(), name='project_create'),
url(r'^(?P[\w.@+-]+)/project/(?P\d+)/edit/$', ProjectUpdateView.as_view(), name='project_update'),
url(r'^(?P[\w.@+-]+)/project/(?P\d+)/delete/$', ProjectDeleteView.as_view(), name='project_delete'),
- url(r'^(?P[\w.@+-]+)/snippets/$', UserSnippetsView.as_view(), name='user_snippets'),
- url(r'^(?P[\w.@+-]+)/snippets/rss/$', UserRecentSnippetsRss(), name='user_snippet_rss'),
- url(r'^(?P[\w.@+-]+)/snippets/atom/$', UserRecentSnippetsAtom(), name='user_snippet_atom'),
- url(r'^(?P[\w.@+-]+)/snippets/new/$', SnippetCreateView.as_view(), name='snippet_create'),
- url(r'^(?P[\w.@+-]+)/snippets/(?P\d+)/$', SnippetDetailView.as_view(), name='snippet_detail'),
- url(r'^(?P[\w.@+-]+)/snippets/(?P\d+)/edit/$', SnippetUpdateView.as_view(), name='snippet_update'),
- url(r'^(?P[\w.@+-]+)/snippets/(?P\d+)/delete/$', SnippetDeleteView.as_view(), name='snippet_delete'),
)
diff --git a/profiles/views.py b/profiles/views.py
index a09e91d..f82a0c7 100644
--- a/profiles/views.py
+++ b/profiles/views.py
@@ -7,10 +7,10 @@
from braces.views import SetHeadlineMixin
from django.contrib.auth.models import User
-from .models import UserProfile, Snippet, SavedResource, TopicFollow, Project
+from .models import UserProfile, SavedResource, TopicFollow, Project
from resources.models import Resource
-from .forms import UserUpdateForm, UserProfileUpdateForm, SnippetCreateForm, SnippetUpdateForm, ProjectCreateForm, ProjectUpdateForm
+from .forms import UserUpdateForm, UserProfileUpdateForm, ProjectCreateForm, ProjectUpdateForm
def user_redirect_view(request, username):
user = get_object_or_404(User, username=username)
@@ -111,84 +111,6 @@ def get_success_url(self):
return reverse_lazy('user_projects', kwargs={'username':self.request.user.username})
-class UserSnippetsView(SetHeadlineMixin, UserInfoMixin, ListView):
- context_object_name = 'snippets'
- template_name = 'profiles/user_snippets.html'
- paginate_by = 12
-
- def get_queryset(self):
- user = get_object_or_404(User, username=self.kwargs['username'])
- self.headline = unicode(user.username) + ' Wall'
- return Snippet.objects.filter(user=user).filter(show=True)
-
-#TODO first implement a view_snippet permission in models then add a Permission required mixin here
-class UserHiddenSnippetsView(SetHeadlineMixin, ListView):
- context_object_name = 'snippets'
- template_name = 'profiles/user_snippets.html'
- paginate_by = 12
-
- def get_queryset(self):
- user = get_object_or_404(User, username=self.kwargs['username'])
- return Snippet.objects.filter(user=user).filter(show=False)
-
- def get_context_data(self,**kwargs):
- context = super(UserHiddenSnippetsView, self).get_context_data(**kwargs)
- user = get_object_or_404(User, username=self.kwargs['username'])
- context['userinfo'] = user
- return context
-
-
-class SnippetDetailView(SetHeadlineMixin, DetailView):
- model = Snippet
- context_object_name = 'snippet'
- template_name = 'profiles/snippet_detail.html'
-
- def get_object(self):
- snippet = get_object_or_404(Snippet, pk=self.kwargs['pk'])
- if snippet.show:
- self.headline = unicode(snippet.title) + """ | """ + unicode(self.request.user.username) + """ Wall"""
- return snippet
- else:
- raise Http404
-
-
-class SnippetCreateView(LoginRequiredMixin, SetHeadlineMixin, CreateView):
- model = Snippet
- form_class = SnippetCreateForm
- headline = 'Create New Snippet'
-
- def form_valid(self, form):
- user = get_object_or_404(User, username =self.request.user.username)
- form.instance.user = user
- return super(SnippetCreateView, self).form_valid(form)
-
- def get_success_url(self):
- messages.success(self.request, 'New Snippet have been created')
- return reverse_lazy('user_snippets', kwargs={'username':self.request.user.username})
-
-
-class SnippetUpdateView(LoginRequiredMixin, PermissionRequiredMixin, SetHeadlineMixin, UpdateView):
- model = Snippet
- form_class = SnippetUpdateForm
- permission_required = 'profiles.change_snippet'
- headline = 'Edit Snippet'
- return_403 = True
-
- def get_success_url(self):
- messages.success(self.request, 'Your changes have been saved')
- return reverse_lazy('user_snippets', kwargs={'username':self.request.user.username})
-
-
-class SnippetDeleteView(PermissionRequiredMixin, DeleteView):
- permission_required = 'profiles.delete_snippet'
- return_403 = True
- model = Snippet
-
- def get_success_url(self):
- messages.success(self.request, 'Your snippet have been deleted')
- return reverse_lazy('user_snippets', kwargs={'username':self.request.user.username})
-
-
class UserUpdateView(LoginRequiredMixin, PermissionRequiredMixin, SetHeadlineMixin, UpdateView):
form_class = UserUpdateForm
model = User
diff --git a/registration/__init__.py b/registration/__init__.py
new file mode 100644
index 0000000..6e1954a
--- /dev/null
+++ b/registration/__init__.py
@@ -0,0 +1,22 @@
+VERSION = (1, 0, 0, 'final', 0)
+
+
+def get_version():
+ "Returns a PEP 386-compliant version number from 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] != 'final':
+ mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
+ sub = mapping[VERSION[3]] + str(VERSION[4])
+
+ return str(main + sub)
diff --git a/registration/admin.py b/registration/admin.py
new file mode 100644
index 0000000..6541f6a
--- /dev/null
+++ b/registration/admin.py
@@ -0,0 +1,46 @@
+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)
diff --git a/registration/auth_urls.py b/registration/auth_urls.py
new file mode 100644
index 0000000..8515ee1
--- /dev/null
+++ b/registration/auth_urls.py
@@ -0,0 +1,60 @@
+"""
+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 import include
+from django.conf.urls import patterns
+from django.conf.urls import url
+
+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[0-9A-Za-z]+)-(?P.+)/$',
+ 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'),
+)
diff --git a/registration/backends/__init__.py b/registration/backends/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/registration/backends/default/__init__.py b/registration/backends/default/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/registration/backends/default/urls.py b/registration/backends/default/urls.py
new file mode 100644
index 0000000..c1251b8
--- /dev/null
+++ b/registration/backends/default/urls.py
@@ -0,0 +1,50 @@
+"""
+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 registration behavior, feel free to set up
+your own URL patterns for these views instead.
+
+"""
+
+
+from django.conf.urls import patterns
+from django.conf.urls import include
+from django.conf.urls import url
+from django.views.generic.base import TemplateView
+
+from registration.backends.default.views import ActivationView
+from registration.backends.default.views import RegistrationView
+
+
+urlpatterns = patterns('',
+ url(r'^activate/complete/$',
+ TemplateView.as_view(template_name='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\w+)/$',
+ ActivationView.as_view(),
+ name='registration_activate'),
+ url(r'^register/$',
+ RegistrationView.as_view(),
+ name='registration_register'),
+ url(r'^register/complete/$',
+ TemplateView.as_view(template_name='registration/registration_complete.html'),
+ name='registration_complete'),
+# url(r'^register/closed/$',
+# TemplateView.as_view(template_name='registration/registration_closed.html'),
+# name='registration_disallowed'),
+ (r'', include('registration.auth_urls')),
+ )
diff --git a/registration/backends/default/views.py b/registration/backends/default/views.py
new file mode 100644
index 0000000..3cd8563
--- /dev/null
+++ b/registration/backends/default/views.py
@@ -0,0 +1,134 @@
+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 RegistrationFormUniqueEmail
+from registration.models import RegistrationProfile
+from registration.views import ActivationView as BaseActivationView
+from registration.views import RegistrationView as BaseRegistrationView
+
+
+class RegistrationView(BaseRegistrationView):
+ """
+ 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.
+
+ """
+
+ form_class = RegistrationFormUniqueEmail
+
+ def register(self, request, **cleaned_data):
+ """
+ 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 = cleaned_data['username'], cleaned_data['email'], cleaned_data['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 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_success_url(self, request, user):
+ """
+ Return the name of the URL to redirect to after successful
+ user registration.
+
+ """
+ return ('registration_complete', (), {})
+
+
+class ActivationView(BaseActivationView):
+ 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_user = RegistrationProfile.objects.activate_user(activation_key)
+ if activated_user:
+ signals.user_activated.send(sender=self.__class__,
+ user=activated_user,
+ request=request)
+ return activated_user
+
+ def get_success_url(self, request, user):
+ return ('registration_activation_complete', (), {})
diff --git a/registration/backends/simple/__init__.py b/registration/backends/simple/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/registration/backends/simple/urls.py b/registration/backends/simple/urls.py
new file mode 100644
index 0000000..0be6ee2
--- /dev/null
+++ b/registration/backends/simple/urls.py
@@ -0,0 +1,36 @@
+"""
+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 registration behavior, feel free to set up
+your own URL patterns for these views instead.
+
+"""
+
+
+from django.conf.urls import include
+from django.conf.urls import patterns
+from django.conf.urls import url
+from django.views.generic.base import TemplateView
+
+from registration.backends.simple.views import RegistrationView
+
+
+urlpatterns = patterns('',
+ url(r'^register/$',
+ RegistrationView.as_view(),
+ name='registration_register'),
+ url(r'^register/closed/$',
+ TemplateView.as_view(template_name='registration/registration_closed.html'),
+ name='registration_disallowed'),
+ (r'', include('registration.auth_urls')),
+ )
diff --git a/registration/backends/simple/views.py b/registration/backends/simple/views.py
new file mode 100644
index 0000000..0c85172
--- /dev/null
+++ b/registration/backends/simple/views.py
@@ -0,0 +1,45 @@
+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.views import RegistrationView as BaseRegistrationView
+
+
+class RegistrationView(BaseRegistrationView):
+ """
+ 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, **cleaned_data):
+ username, email, password = cleaned_data['username'], cleaned_data['email'], cleaned_data['password1']
+ User.objects.create_user(username, email, password)
+
+ 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 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_success_url(self, request, user):
+ return (user.get_absolute_url(), (), {})
diff --git a/registration/forms.py b/registration/forms.py
new file mode 100644
index 0000000..de1e92b
--- /dev/null
+++ b/registration/forms.py
@@ -0,0 +1,120 @@
+"""
+Forms and validation code for user registration.
+
+Note that all of these forms assume Django's bundle default ``User``
+model; since it's not possible for a form to anticipate in advance the
+needs of custom user models, you will need to write your own forms if
+you're using a custom model.
+
+"""
+
+
+from django.contrib.auth.models import User
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+
+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.
+
+ """
+ required_css_class = 'required'
+
+ username = forms.RegexField(regex=r'^[\w.@+-]+$',
+ max_length=30,
+ label=_("Username"),
+ error_messages={'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.")})
+ email = forms.EmailField(label=_("E-mail"))
+ password1 = forms.CharField(widget=forms.PasswordInput,
+ label=_("Password"))
+ password2 = forms.CharField(widget=forms.PasswordInput,
+ 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,
+ 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']
diff --git a/registration/management/__init__.py b/registration/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/registration/management/commands/__init__.py b/registration/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/registration/management/commands/cleanupregistration.py b/registration/management/commands/cleanupregistration.py
new file mode 100644
index 0000000..abec5ae
--- /dev/null
+++ b/registration/management/commands/cleanupregistration.py
@@ -0,0 +1,19 @@
+"""
+A management command which deletes expired accounts (e.g.,
+accounts which signed up but never activated) from the database.
+
+Calls ``RegistrationProfile.objects.delete_expired_users()``, which
+contains the actual logic for determining which accounts are deleted.
+
+"""
+
+from django.core.management.base import NoArgsCommand
+
+from registration.models import RegistrationProfile
+
+
+class Command(NoArgsCommand):
+ help = "Delete expired user registrations from the database"
+
+ def handle_noargs(self, **options):
+ RegistrationProfile.objects.delete_expired_users()
diff --git a/registration/models.py b/registration/models.py
new file mode 100644
index 0000000..2148749
--- /dev/null
+++ b/registration/models.py
@@ -0,0 +1,271 @@
+import datetime
+import hashlib
+import random
+import re
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.db import models
+from django.db import transaction
+from django.template.loader import render_to_string
+from django.utils.translation import ugettext_lazy as _
+
+try:
+ from django.contrib.auth import get_user_model
+ User = get_user_model()
+except ImportError:
+ from django.contrib.auth.models import User
+
+try:
+ from django.utils.timezone import now as datetime_now
+except ImportError:
+ datetime_now = datetime.datetime.now
+
+
+SHA1_RE = re.compile('^[a-f0-9]{40}$')
+
+
+class RegistrationManager(models.Manager):
+ """
+ Custom manager for the ``RegistrationProfile`` model.
+
+ The methods defined here provide shortcuts for account creation
+ and activation (including generation and emailing of activation
+ keys), and for cleaning out expired inactive accounts.
+
+ """
+ def activate_user(self, activation_key):
+ """
+ Validate an activation key and activate the corresponding
+ ``User`` if valid.
+
+ If the key is valid and has not expired, return the ``User``
+ after activating.
+
+ If the key is not valid or has expired, return ``False``.
+
+ If the key is valid but the ``User`` is already active,
+ return ``False``.
+
+ To prevent reactivation of an account which has been
+ deactivated by site administrators, the activation key is
+ reset to the string constant ``RegistrationProfile.ACTIVATED``
+ after successful activation.
+
+ """
+ # Make sure the key we're trying conforms to the pattern of a
+ # SHA1 hash; if it doesn't, no point trying to look it up in
+ # the database.
+ if SHA1_RE.search(activation_key):
+ try:
+ profile = self.get(activation_key=activation_key)
+ except self.model.DoesNotExist:
+ return False
+ if not profile.activation_key_expired():
+ user = profile.user
+ user.is_active = True
+ user.save()
+ profile.activation_key = self.model.ACTIVATED
+ profile.save()
+ return user
+ return False
+
+ def create_inactive_user(self, username, email, password,
+ site, send_email=True):
+ """
+ Create a new, inactive ``User``, generate a
+ ``RegistrationProfile`` and email its activation key to the
+ ``User``, returning the new ``User``.
+
+ By default, an activation email will be sent to the new
+ user. To disable this, pass ``send_email=False``.
+
+ """
+ new_user = User.objects.create_user(username, email, password)
+ new_user.is_active = False
+ new_user.save()
+
+ registration_profile = self.create_profile(new_user)
+
+ if send_email:
+ registration_profile.send_activation_email(site)
+
+ return new_user
+ create_inactive_user = transaction.commit_on_success(create_inactive_user)
+
+ def create_profile(self, user):
+ """
+ Create a ``RegistrationProfile`` for a given
+ ``User``, and return the ``RegistrationProfile``.
+
+ The activation key for the ``RegistrationProfile`` will be a
+ SHA1 hash, generated from a combination of the ``User``'s
+ username and a random salt.
+
+ """
+ salt = hashlib.sha1(str(random.random())).hexdigest()[:5]
+ username = user.username
+ if isinstance(username, unicode):
+ username = username.encode('utf-8')
+ activation_key = hashlib.sha1(salt+username).hexdigest()
+ return self.create(user=user,
+ activation_key=activation_key)
+
+ def delete_expired_users(self):
+ """
+ Remove expired instances of ``RegistrationProfile`` and their
+ associated ``User``s.
+
+ Accounts to be deleted are identified by searching for
+ instances of ``RegistrationProfile`` with expired activation
+ keys, and then checking to see if their associated ``User``
+ instances have the field ``is_active`` set to ``False``; any
+ ``User`` who is both inactive and has an expired activation
+ key will be deleted.
+
+ It is recommended that this method be executed regularly as
+ part of your routine site maintenance; this application
+ provides a custom management command which will call this
+ method, accessible as ``manage.py cleanupregistration``.
+
+ Regularly clearing out accounts which have never been
+ activated serves two useful purposes:
+
+ 1. It alleviates the ocasional need to reset a
+ ``RegistrationProfile`` and/or re-send an activation email
+ when a user does not receive or does not act upon the
+ initial activation email; since the account will be
+ deleted, the user will be able to simply re-register and
+ receive a new activation key.
+
+ 2. It prevents the possibility of a malicious user registering
+ one or more accounts and never activating them (thus
+ denying the use of those usernames to anyone else); since
+ those accounts will be deleted, the usernames will become
+ available for use again.
+
+ If you have a troublesome ``User`` and wish to disable their
+ account while keeping it in the database, simply delete the
+ associated ``RegistrationProfile``; an inactive ``User`` which
+ does not have an associated ``RegistrationProfile`` will not
+ be deleted.
+
+ """
+ for profile in self.all():
+ try:
+ if profile.activation_key_expired():
+ user = profile.user
+ if not user.is_active:
+ user.delete()
+ profile.delete()
+ except User.DoesNotExist:
+ profile.delete()
+
+class RegistrationProfile(models.Model):
+ """
+ A simple profile which stores an activation key for use during
+ user account registration.
+
+ Generally, you will not want to interact directly with instances
+ of this model; the provided manager includes methods
+ for creating and activating new accounts, as well as for cleaning
+ out accounts which have never been activated.
+
+ While it is possible to use this model as the value of the
+ ``AUTH_PROFILE_MODULE`` setting, it's not recommended that you do
+ so. This model's sole purpose is to store data temporarily during
+ account registration and activation.
+
+ """
+ ACTIVATED = u"ALREADY_ACTIVATED"
+
+ user = models.ForeignKey(User, unique=True, verbose_name=_('user'))
+ activation_key = models.CharField(_('activation key'), max_length=40)
+
+ objects = RegistrationManager()
+
+ class Meta:
+ verbose_name = _('registration profile')
+ verbose_name_plural = _('registration profiles')
+
+ def __unicode__(self):
+ return u"Registration information for %s" % self.user
+
+ def activation_key_expired(self):
+ """
+ Determine whether this ``RegistrationProfile``'s activation
+ key has expired, returning a boolean -- ``True`` if the key
+ has expired.
+
+ Key expiration is determined by a two-step process:
+
+ 1. If the user has already activated, the key will have been
+ reset to the string constant ``ACTIVATED``. Re-activating
+ is not permitted, and so this method returns ``True`` in
+ this case.
+
+ 2. Otherwise, the date the user signed up is incremented by
+ the number of days specified in the setting
+ ``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of
+ days after signup during which a user is allowed to
+ activate 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_ACTIVATION_DAYS)
+ return self.activation_key == self.ACTIVATED or \
+ (self.user.date_joined + expiration_date <= datetime_now())
+ activation_key_expired.boolean = True
+
+ def send_activation_email(self, site):
+ """
+ Send an activation email to the user associated with this
+ ``RegistrationProfile``.
+
+ The activation email will make use of two templates:
+
+ ``registration/activation_email_subject.txt``
+ This template will be used for the subject line of the
+ email. Because it is used as the subject line of an email,
+ this template's output **must** be only a single line of
+ text; output longer than one line will be forcibly joined
+ into only a single line.
+
+ ``registration/activation_email.txt``
+ This template will be used for the body of the email.
+
+ These templates will each receive the following context
+ variables:
+
+ ``activation_key``
+ The activation key for the new account.
+
+ ``expiration_days``
+ The number of days remaining during which the account may
+ be activated.
+
+ ``site``
+ An object representing the site on which the user
+ registered; depending on whether ``django.contrib.sites``
+ is installed, this may be an instance of either
+ ``django.contrib.sites.models.Site`` (if the sites
+ application is installed) or
+ ``django.contrib.sites.models.RequestSite`` (if
+ not). Consult the documentation for the Django sites
+ framework for details regarding these objects' interfaces.
+
+ """
+ ctx_dict = {'activation_key': self.activation_key,
+ 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
+ 'site': site}
+ subject = render_to_string('registration/activation_email_subject.txt',
+ ctx_dict)
+ # Email subject *must not* contain newlines
+ subject = ''.join(subject.splitlines())
+
+ message = render_to_string('registration/activation_email.txt',
+ ctx_dict)
+
+ self.user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
+
diff --git a/registration/signals.py b/registration/signals.py
new file mode 100644
index 0000000..343e3a5
--- /dev/null
+++ b/registration/signals.py
@@ -0,0 +1,8 @@
+from django.dispatch import Signal
+
+
+# A new user has registered.
+user_registered = Signal(providing_args=["user", "request"])
+
+# A user has activated his or her account.
+user_activated = Signal(providing_args=["user", "request"])
diff --git a/registration/tests/__init__.py b/registration/tests/__init__.py
new file mode 100644
index 0000000..82202ca
--- /dev/null
+++ b/registration/tests/__init__.py
@@ -0,0 +1,4 @@
+from registration.tests.default_backend import *
+from registration.tests.forms import *
+from registration.tests.models import *
+from registration.tests.simple_backend import *
diff --git a/registration/tests/default_backend.py b/registration/tests/default_backend.py
new file mode 100644
index 0000000..cdf1b0a
--- /dev/null
+++ b/registration/tests/default_backend.py
@@ -0,0 +1,198 @@
+import datetime
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core import mail
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+from registration import signals
+from registration.admin import RegistrationAdmin
+from registration.forms import RegistrationForm
+from registration.backends.default.views import RegistrationView
+from registration.models import RegistrationProfile
+
+
+class DefaultBackendViewTests(TestCase):
+ """
+ Test the default registration backend.
+
+ Running these tests successfully will require two templates to be
+ created for the sending of activation emails; details on these
+ templates and their contexts may be found in the documentation for
+ the default backend.
+
+ """
+ urls = 'registration.backends.default.urls'
+
+ def setUp(self):
+ """
+ Create an instance of the default backend for use in testing,
+ and set ``ACCOUNT_ACTIVATION_DAYS`` if it's not set already.
+
+ """
+ self.old_activation = getattr(settings, 'ACCOUNT_ACTIVATION_DAYS', None)
+ if self.old_activation is None:
+ settings.ACCOUNT_ACTIVATION_DAYS = 7 # pragma: no cover
+
+ def tearDown(self):
+ """
+ Yank ``ACCOUNT_ACTIVATION_DAYS`` back out if it wasn't
+ originally set.
+
+ """
+ if self.old_activation is None:
+ settings.ACCOUNT_ACTIVATION_DAYS = self.old_activation # pragma: no cover
+
+ def test_allow(self):
+ """
+ The setting ``REGISTRATION_OPEN`` appropriately controls
+ whether registration is permitted.
+
+ """
+ old_allowed = getattr(settings, 'REGISTRATION_OPEN', True)
+ settings.REGISTRATION_OPEN = True
+
+ resp = self.client.get(reverse('registration_register'))
+ self.assertEqual(200, resp.status_code)
+
+ settings.REGISTRATION_OPEN = False
+
+ # Now all attempts to hit the register view should redirect to
+ # the 'registration is closed' message.
+ resp = self.client.get(reverse('registration_register'))
+ self.assertRedirects(resp, reverse('registration_disallowed'))
+
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'})
+ self.assertRedirects(resp, reverse('registration_disallowed'))
+
+ settings.REGISTRATION_OPEN = old_allowed
+
+ def test_registration_get(self):
+ """
+ HTTP ``GET`` to the registration view uses the appropriate
+ template and populates a registration form into the context.
+
+ """
+ resp = self.client.get(reverse('registration_register'))
+ self.assertEqual(200, resp.status_code)
+ self.assertTemplateUsed(resp,
+ 'registration/registration_form.html')
+ self.failUnless(isinstance(resp.context['form'],
+ RegistrationForm))
+
+ def test_registration(self):
+ """
+ Registration creates a new inactive account and a new profile
+ with activation key, populates the correct account data and
+ sends an activation email.
+
+ """
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'})
+ self.assertRedirects(resp, reverse('registration_complete'))
+
+ new_user = User.objects.get(username='bob')
+
+ self.failUnless(new_user.check_password('secret'))
+ self.assertEqual(new_user.email, 'bob@example.com')
+
+ # New user must not be active.
+ self.failIf(new_user.is_active)
+
+ # A registration profile was created, and an activation email
+ # was sent.
+ self.assertEqual(RegistrationProfile.objects.count(), 1)
+ self.assertEqual(len(mail.outbox), 1)
+
+ def test_registration_no_sites(self):
+ """
+ Registration still functions properly when
+ ``django.contrib.sites`` is not installed; the fallback will
+ be a ``RequestSite`` instance.
+
+ """
+ Site._meta.installed = False
+
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'})
+ self.assertEqual(302, resp.status_code)
+
+ new_user = User.objects.get(username='bob')
+
+ self.failUnless(new_user.check_password('secret'))
+ self.assertEqual(new_user.email, 'bob@example.com')
+
+ self.failIf(new_user.is_active)
+
+ self.assertEqual(RegistrationProfile.objects.count(), 1)
+ self.assertEqual(len(mail.outbox), 1)
+
+ Site._meta.installed = True
+
+ def test_registration_failure(self):
+ """
+ Registering with invalid data fails.
+
+ """
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'notsecret'})
+ self.assertEqual(200, resp.status_code)
+ self.failIf(resp.context['form'].is_valid())
+ self.assertEqual(0, len(mail.outbox))
+
+ def test_activation(self):
+ """
+ Activation of an account functions properly.
+
+ """
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'})
+
+ profile = RegistrationProfile.objects.get(user__username='bob')
+
+ resp = self.client.get(reverse('registration_activate',
+ args=(),
+ kwargs={'activation_key': profile.activation_key}))
+ self.assertRedirects(resp, reverse('registration_activation_complete'))
+
+ def test_activation_expired(self):
+ """
+ An expired account can't be activated.
+
+ """
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'})
+
+ profile = RegistrationProfile.objects.get(user__username='bob')
+ user = profile.user
+ user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
+ user.save()
+
+ resp = self.client.get(reverse('registration_activate',
+ args=(),
+ kwargs={'activation_key': profile.activation_key}))
+
+ self.assertEqual(200, resp.status_code)
+ self.assertTemplateUsed(resp, 'registration/activate.html')
+ self.failIf('activation_key' in resp.context)
diff --git a/registration/tests/forms.py b/registration/tests/forms.py
new file mode 100644
index 0000000..dbb1f06
--- /dev/null
+++ b/registration/tests/forms.py
@@ -0,0 +1,119 @@
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from registration import forms
+
+
+class RegistrationFormTests(TestCase):
+ """
+ Test the default registration forms.
+
+ """
+ def test_registration_form(self):
+ """
+ Test that ``RegistrationForm`` enforces username constraints
+ and matching passwords.
+
+ """
+ # Create a user so we can verify that duplicate usernames aren't
+ # permitted.
+ User.objects.create_user('alice', 'alice@example.com', 'secret')
+
+ invalid_data_dicts = [
+ # Non-alphanumeric username.
+ {'data': {'username': 'foo/bar',
+ 'email': 'foo@example.com',
+ 'password1': 'foo',
+ 'password2': 'foo'},
+ 'error': ('username', [u"This value may contain only letters, numbers and @/./+/-/_ characters."])},
+ # Already-existing username.
+ {'data': {'username': 'alice',
+ 'email': 'alice@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'},
+ 'error': ('username', [u"A user with that username already exists."])},
+ # Mismatched passwords.
+ {'data': {'username': 'foo',
+ 'email': 'foo@example.com',
+ 'password1': 'foo',
+ 'password2': 'bar'},
+ 'error': ('__all__', [u"The two password fields didn't match."])},
+ ]
+
+ for invalid_dict in invalid_data_dicts:
+ form = forms.RegistrationForm(data=invalid_dict['data'])
+ self.failIf(form.is_valid())
+ self.assertEqual(form.errors[invalid_dict['error'][0]],
+ invalid_dict['error'][1])
+
+ form = forms.RegistrationForm(data={'username': 'foo',
+ 'email': 'foo@example.com',
+ 'password1': 'foo',
+ 'password2': 'foo'})
+ self.failUnless(form.is_valid())
+
+ def test_registration_form_tos(self):
+ """
+ Test that ``RegistrationFormTermsOfService`` requires
+ agreement to the terms of service.
+
+ """
+ form = forms.RegistrationFormTermsOfService(data={'username': 'foo',
+ 'email': 'foo@example.com',
+ 'password1': 'foo',
+ 'password2': 'foo'})
+ self.failIf(form.is_valid())
+ self.assertEqual(form.errors['tos'],
+ [u"You must agree to the terms to register"])
+
+ form = forms.RegistrationFormTermsOfService(data={'username': 'foo',
+ 'email': 'foo@example.com',
+ 'password1': 'foo',
+ 'password2': 'foo',
+ 'tos': 'on'})
+ self.failUnless(form.is_valid())
+
+ def test_registration_form_unique_email(self):
+ """
+ Test that ``RegistrationFormUniqueEmail`` validates uniqueness
+ of email addresses.
+
+ """
+ # Create a user so we can verify that duplicate addresses
+ # aren't permitted.
+ User.objects.create_user('alice', 'alice@example.com', 'secret')
+
+ form = forms.RegistrationFormUniqueEmail(data={'username': 'foo',
+ 'email': 'alice@example.com',
+ 'password1': 'foo',
+ 'password2': 'foo'})
+ self.failIf(form.is_valid())
+ self.assertEqual(form.errors['email'],
+ [u"This email address is already in use. Please supply a different email address."])
+
+ form = forms.RegistrationFormUniqueEmail(data={'username': 'foo',
+ 'email': 'foo@example.com',
+ 'password1': 'foo',
+ 'password2': 'foo'})
+ self.failUnless(form.is_valid())
+
+ def test_registration_form_no_free_email(self):
+ """
+ Test that ``RegistrationFormNoFreeEmail`` disallows
+ registration with free email addresses.
+
+ """
+ base_data = {'username': 'foo',
+ 'password1': 'foo',
+ 'password2': 'foo'}
+ for domain in forms.RegistrationFormNoFreeEmail.bad_domains:
+ invalid_data = base_data.copy()
+ invalid_data['email'] = u"foo@%s" % domain
+ form = forms.RegistrationFormNoFreeEmail(data=invalid_data)
+ self.failIf(form.is_valid())
+ self.assertEqual(form.errors['email'],
+ [u"Registration using free email addresses is prohibited. Please supply a different email address."])
+
+ base_data['email'] = 'foo@example.com'
+ form = forms.RegistrationFormNoFreeEmail(data=base_data)
+ self.failUnless(form.is_valid())
diff --git a/registration/tests/models.py b/registration/tests/models.py
new file mode 100644
index 0000000..f763cb2
--- /dev/null
+++ b/registration/tests/models.py
@@ -0,0 +1,225 @@
+import datetime
+import re
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core import mail
+from django.core import management
+from django.test import TestCase
+from django.utils.hashcompat import sha_constructor
+
+from registration.models import RegistrationProfile
+
+
+class RegistrationModelTests(TestCase):
+ """
+ Test the model and manager used in the default backend.
+
+ """
+ user_info = {'username': 'alice',
+ 'password': 'swordfish',
+ 'email': 'alice@example.com'}
+
+ def setUp(self):
+ self.old_activation = getattr(settings, 'ACCOUNT_ACTIVATION_DAYS', None)
+ settings.ACCOUNT_ACTIVATION_DAYS = 7
+
+ def tearDown(self):
+ settings.ACCOUNT_ACTIVATION_DAYS = self.old_activation
+
+ def test_profile_creation(self):
+ """
+ Creating a registration profile for a user populates the
+ profile with the correct user and a SHA1 hash to use as
+ activation key.
+
+ """
+ new_user = User.objects.create_user(**self.user_info)
+ profile = RegistrationProfile.objects.create_profile(new_user)
+
+ self.assertEqual(RegistrationProfile.objects.count(), 1)
+ self.assertEqual(profile.user.id, new_user.id)
+ self.failUnless(re.match('^[a-f0-9]{40}$', profile.activation_key))
+ self.assertEqual(unicode(profile),
+ "Registration information for alice")
+
+ def test_activation_email(self):
+ """
+ ``RegistrationProfile.send_activation_email`` sends an
+ email.
+
+ """
+ new_user = User.objects.create_user(**self.user_info)
+ profile = RegistrationProfile.objects.create_profile(new_user)
+ profile.send_activation_email(Site.objects.get_current())
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].to, [self.user_info['email']])
+
+ def test_user_creation(self):
+ """
+ Creating a new user populates the correct data, and sets the
+ user's account inactive.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ self.assertEqual(new_user.username, 'alice')
+ self.assertEqual(new_user.email, 'alice@example.com')
+ self.failUnless(new_user.check_password('swordfish'))
+ self.failIf(new_user.is_active)
+
+ def test_user_creation_email(self):
+ """
+ By default, creating a new user sends an activation email.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ self.assertEqual(len(mail.outbox), 1)
+
+ def test_user_creation_no_email(self):
+ """
+ Passing ``send_email=False`` when creating a new user will not
+ send an activation email.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ send_email=False,
+ **self.user_info)
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_unexpired_account(self):
+ """
+ ``RegistrationProfile.activation_key_expired()`` is ``False``
+ within the activation window.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ profile = RegistrationProfile.objects.get(user=new_user)
+ self.failIf(profile.activation_key_expired())
+
+ def test_expired_account(self):
+ """
+ ``RegistrationProfile.activation_key_expired()`` is ``True``
+ outside the activation window.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ new_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1)
+ new_user.save()
+ profile = RegistrationProfile.objects.get(user=new_user)
+ self.failUnless(profile.activation_key_expired())
+
+ def test_valid_activation(self):
+ """
+ Activating a user within the permitted window makes the
+ account active, and resets the activation key.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ profile = RegistrationProfile.objects.get(user=new_user)
+ activated = RegistrationProfile.objects.activate_user(profile.activation_key)
+
+ self.failUnless(isinstance(activated, User))
+ self.assertEqual(activated.id, new_user.id)
+ self.failUnless(activated.is_active)
+
+ profile = RegistrationProfile.objects.get(user=new_user)
+ self.assertEqual(profile.activation_key, RegistrationProfile.ACTIVATED)
+
+ def test_expired_activation(self):
+ """
+ Attempting to activate outside the permitted window does not
+ activate the account.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ new_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1)
+ new_user.save()
+
+ profile = RegistrationProfile.objects.get(user=new_user)
+ activated = RegistrationProfile.objects.activate_user(profile.activation_key)
+
+ self.failIf(isinstance(activated, User))
+ self.failIf(activated)
+
+ new_user = User.objects.get(username='alice')
+ self.failIf(new_user.is_active)
+
+ profile = RegistrationProfile.objects.get(user=new_user)
+ self.assertNotEqual(profile.activation_key, RegistrationProfile.ACTIVATED)
+
+ def test_activation_invalid_key(self):
+ """
+ Attempting to activate with a key which is not a SHA1 hash
+ fails.
+
+ """
+ self.failIf(RegistrationProfile.objects.activate_user('foo'))
+
+ def test_activation_already_activated(self):
+ """
+ Attempting to re-activate an already-activated account fails.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ profile = RegistrationProfile.objects.get(user=new_user)
+ RegistrationProfile.objects.activate_user(profile.activation_key)
+
+ profile = RegistrationProfile.objects.get(user=new_user)
+ self.failIf(RegistrationProfile.objects.activate_user(profile.activation_key))
+
+ def test_activation_nonexistent_key(self):
+ """
+ Attempting to activate with a non-existent key (i.e., one not
+ associated with any account) fails.
+
+ """
+ # Due to the way activation keys are constructed during
+ # registration, this will never be a valid key.
+ invalid_key = sha_constructor('foo').hexdigest()
+ self.failIf(RegistrationProfile.objects.activate_user(invalid_key))
+
+ def test_expired_user_deletion(self):
+ """
+ ``RegistrationProfile.objects.delete_expired_users()`` only
+ deletes inactive users whose activation window has expired.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ expired_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ username='bob',
+ password='secret',
+ email='bob@example.com')
+ expired_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1)
+ expired_user.save()
+
+ RegistrationProfile.objects.delete_expired_users()
+ self.assertEqual(RegistrationProfile.objects.count(), 1)
+ self.assertRaises(User.DoesNotExist, User.objects.get, username='bob')
+
+ def test_management_command(self):
+ """
+ The ``cleanupregistration`` management command properly
+ deletes expired accounts.
+
+ """
+ new_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ **self.user_info)
+ expired_user = RegistrationProfile.objects.create_inactive_user(site=Site.objects.get_current(),
+ username='bob',
+ password='secret',
+ email='bob@example.com')
+ expired_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1)
+ expired_user.save()
+
+ management.call_command('cleanupregistration')
+ self.assertEqual(RegistrationProfile.objects.count(), 1)
+ self.assertRaises(User.DoesNotExist, User.objects.get, username='bob')
diff --git a/registration/tests/simple_backend.py b/registration/tests/simple_backend.py
new file mode 100644
index 0000000..fe61079
--- /dev/null
+++ b/registration/tests/simple_backend.py
@@ -0,0 +1,89 @@
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+
+from registration.forms import RegistrationForm
+
+
+class SimpleBackendViewTests(TestCase):
+ urls = 'registration.backends.simple.urls'
+
+ def test_allow(self):
+ """
+ The setting ``REGISTRATION_OPEN`` appropriately controls
+ whether registration is permitted.
+
+ """
+ old_allowed = getattr(settings, 'REGISTRATION_OPEN', True)
+ settings.REGISTRATION_OPEN = True
+
+ resp = self.client.get(reverse('registration_register'))
+ self.assertEqual(200, resp.status_code)
+
+ settings.REGISTRATION_OPEN = False
+
+ # Now all attempts to hit the register view should redirect to
+ # the 'registration is closed' message.
+ resp = self.client.get(reverse('registration_register'))
+ self.assertRedirects(resp, reverse('registration_disallowed'))
+
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'})
+ self.assertRedirects(resp, reverse('registration_disallowed'))
+
+ settings.REGISTRATION_OPEN = old_allowed
+
+ def test_registration_get(self):
+ """
+ HTTP ``GET`` to the registration view uses the appropriate
+ template and populates a registration form into the context.
+
+ """
+ resp = self.client.get(reverse('registration_register'))
+ self.assertEqual(200, resp.status_code)
+ self.assertTemplateUsed(resp,
+ 'registration/registration_form.html')
+ self.failUnless(isinstance(resp.context['form'],
+ RegistrationForm))
+
+ def test_registration(self):
+ """
+ Registration creates a new account and logs the user in.
+
+ """
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'secret'})
+
+ new_user = User.objects.get(username='bob')
+ self.assertEqual(302, resp.status_code)
+ self.failUnless(new_user.get_absolute_url() in resp['Location'])
+
+ self.failUnless(new_user.check_password('secret'))
+ self.assertEqual(new_user.email, 'bob@example.com')
+
+ # New user must be active.
+ self.failUnless(new_user.is_active)
+
+ # New user must be logged in.
+ resp = self.client.get(reverse('registration_register'))
+ self.failUnless(resp.context['user'].is_authenticated())
+
+ def test_registration_failure(self):
+ """
+ Registering with invalid data fails.
+
+ """
+ resp = self.client.post(reverse('registration_register'),
+ data={'username': 'bob',
+ 'email': 'bob@example.com',
+ 'password1': 'secret',
+ 'password2': 'notsecret'})
+ self.assertEqual(200, resp.status_code)
+ self.failIf(resp.context['form'].is_valid())
diff --git a/registration/tests/urls.py b/registration/tests/urls.py
new file mode 100644
index 0000000..02ef609
--- /dev/null
+++ b/registration/tests/urls.py
@@ -0,0 +1,82 @@
+"""
+URLs used in the unit tests for django-registration.
+
+You should not attempt to use these URLs in any sort of real or
+development environment; instead, use
+``registration/backends/default/urls.py``. This URLconf includes those
+URLs, and also adds several additional URLs which serve no purpose
+other than to test that optional keyword arguments are properly
+handled.
+
+"""
+
+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('',
+ # Test the 'activate' view with custom template
+ # name.
+ url(r'^activate-with-template-name/(?P\w+)/$',
+ activate,
+ {'template_name': 'registration/test_template_name.html',
+ 'backend': 'registration.backends.default.DefaultBackend'},
+ name='registration_test_activate_template_name'),
+ # Test the 'activate' view with
+ # extra_context_argument.
+ url(r'^activate-extra-context/(?P\w+)/$',
+ activate,
+ {'extra_context': {'foo': 'bar', 'callable': lambda: 'called'},
+ 'backend': 'registration.backends.default.DefaultBackend'},
+ name='registration_test_activate_extra_context'),
+ # Test the 'activate' view with success_url argument.
+ url(r'^activate-with-success-url/(?P\w+)/$',
+ activate,
+ {'success_url': 'registration_test_custom_success_url',
+ 'backend': 'registration.backends.default.DefaultBackend'},
+ name='registration_test_activate_success_url'),
+ # Test the 'register' view with custom template
+ # name.
+ url(r'^register-with-template-name/$',
+ register,
+ {'template_name': 'registration/test_template_name.html',
+ 'backend': 'registration.backends.default.DefaultBackend'},
+ name='registration_test_register_template_name'),
+ # Test the'register' view with extra_context
+ # argument.
+ url(r'^register-extra-context/$',
+ register,
+ {'extra_context': {'foo': 'bar', 'callable': lambda: 'called'},
+ 'backend': 'registration.backends.default.DefaultBackend'},
+ name='registration_test_register_extra_context'),
+ # Test the 'register' view with custom URL for
+ # closed registration.
+ url(r'^register-with-disallowed-url/$',
+ register,
+ {'disallowed_url': 'registration_test_custom_disallowed',
+ 'backend': 'registration.backends.default.DefaultBackend'},
+ name='registration_test_register_disallowed_url'),
+ # Set up a pattern which will correspond to the
+ # custom 'disallowed_url' above.
+ url(r'^custom-disallowed/$',
+ direct_to_template,
+ {'template': 'registration/registration_closed.html'},
+ name='registration_test_custom_disallowed'),
+ # Test the 'register' view with custom redirect
+ # on successful registration.
+ url(r'^register-with-success_url/$',
+ register,
+ {'success_url': 'registration_test_custom_success_url',
+ 'backend': 'registration.backends.default.DefaultBackend'},
+ name='registration_test_register_success_url'
+ ),
+ # Pattern for custom redirect set above.
+ url(r'^custom-success/$',
+ direct_to_template,
+ {'template': 'registration/test_template_name.html'},
+ name='registration_test_custom_success_url'),
+ (r'', include('registration.backends.default.urls')),
+ )
diff --git a/registration/urls.py b/registration/urls.py
new file mode 100644
index 0000000..8579f75
--- /dev/null
+++ b/registration/urls.py
@@ -0,0 +1,15 @@
+"""
+Backwards-compatible URLconf for existing django-registration
+installs; this allows the standard ``include('registration.urls')`` to
+continue working, but that usage is deprecated and will be removed for
+django-registration 1.0. For new installs, use
+``include('registration.backends.default.urls')``.
+
+"""
+
+import warnings
+
+warnings.warn("include('registration.urls') is deprecated; use include('registration.backends.default.urls') instead.",
+ DeprecationWarning)
+
+from registration.backends.default.urls import *
diff --git a/registration/views.py b/registration/views.py
new file mode 100644
index 0000000..77aeae1
--- /dev/null
+++ b/registration/views.py
@@ -0,0 +1,142 @@
+"""
+Views which allow users to create and activate accounts.
+
+"""
+
+from django.shortcuts import redirect
+from django.views.generic.base import TemplateView
+from django.views.generic.edit import FormView
+
+from registration import signals
+from registration.forms import RegistrationForm
+
+
+class _RequestPassingFormView(FormView):
+ """
+ A version of FormView which passes extra arguments to certain
+ methods, notably passing the HTTP request nearly everywhere, to
+ enable finer-grained processing.
+
+ """
+ def get(self, request, *args, **kwargs):
+ # Pass request to get_form_class and get_form for per-request
+ # form control.
+ form_class = self.get_form_class(request)
+ form = self.get_form(form_class)
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def post(self, request, *args, **kwargs):
+ # Pass request to get_form_class and get_form for per-request
+ # form control.
+ form_class = self.get_form_class(request)
+ form = self.get_form(form_class)
+ if form.is_valid():
+ # Pass request to form_valid.
+ return self.form_valid(request, form)
+ else:
+ return self.form_invalid(form)
+
+ def get_form_class(self, request=None):
+ return super(_RequestPassingFormView, self).get_form_class()
+
+ def get_form_kwargs(self, request=None, form_class=None):
+ return super(_RequestPassingFormView, self).get_form_kwargs()
+
+ def get_initial(self, request=None):
+ return super(_RequestPassingFormView, self).get_initial()
+
+ def get_success_url(self, request=None, user=None):
+ # We need to be able to use the request and the new user when
+ # constructing success_url.
+ return super(_RequestPassingFormView, self).get_success_url()
+
+ def form_valid(self, form, request=None):
+ return super(_RequestPassingFormView, self).form_valid(form)
+
+ def form_invalid(self, form, request=None):
+ return super(_RequestPassingFormView, self).form_invalid(form)
+
+
+class RegistrationView(_RequestPassingFormView):
+ """
+ Base class for user registration views.
+
+ """
+ disallowed_url = 'registration_disallowed'
+ form_class = RegistrationForm
+ http_method_names = ['get', 'post', 'head', 'options', 'trace']
+ success_url = None
+ template_name = 'registration/registration_form.html'
+
+ def dispatch(self, request, *args, **kwargs):
+ """
+ Check that user signup is allowed before even bothering to
+ dispatch or do other processing.
+
+ """
+ if not self.registration_allowed(request):
+ return redirect(self.disallowed_url)
+ return super(RegistrationView, self).dispatch(request, *args, **kwargs)
+
+ def form_valid(self, request, form):
+ new_user = self.register(request, **form.cleaned_data)
+ success_url = self.get_success_url(request, new_user)
+
+ # success_url may be a simple string, or a tuple providing the
+ # full argument set for redirect(). Attempting to unpack it
+ # tells us which one it is.
+ try:
+ to, args, kwargs = success_url
+ return redirect(to, *args, **kwargs)
+ except ValueError:
+ return redirect(success_url)
+
+ def registration_allowed(self, request):
+ """
+ Override this to enable/disable user registration, either
+ globally or on a per-request basis.
+
+ """
+ return True
+
+ def register(self, request, **cleaned_data):
+ """
+ Implement user-registration logic here. Access to both the
+ request and the full cleaned_data of the registration form is
+ available here.
+
+ """
+ raise NotImplementedError
+
+
+class ActivationView(TemplateView):
+ """
+ Base class for user activation views.
+
+ """
+ http_method_names = ['get']
+ template_name = 'registration/activate.html'
+
+ def get(self, request, *args, **kwargs):
+ activated_user = self.activate(request, *args, **kwargs)
+ if activated_user:
+ signals.user_activated.send(sender=self.__class__,
+ user=activated_user,
+ request=request)
+ success_url = self.get_success_url(request, activated_user)
+ try:
+ to, args, kwargs = success_url
+ return redirect(to, *args, **kwargs)
+ except ValueError:
+ return redirect(success_url)
+ return super(ActivationView, self).get(request, *args, **kwargs)
+
+ def activate(self, request, *args, **kwargs):
+ """
+ Implement account-activation logic here.
+
+ """
+ raise NotImplementedError
+
+ def get_success_url(self, request, user):
+ raise NotImplementedError
diff --git a/requirements.txt b/requirements.txt
index efc215b..56693b6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,21 +1,24 @@
-Django==1.4.5
+Django==1.5.4
+Markdown==2.3.1
MySQL-python==1.2.4
PIL==1.1.7
-South==0.7.6
+South==0.8.2
anyjson==0.3.3
argparse==1.2.1
-distribute==0.6.36
-django-braces==1.0.0
+distribute==0.7.3
+django-braces==1.2.2
django-disqus==0.4.1
-django-extensions==1.0.3
-django-guardian
+django-extensions==1.2.0
+django-guardian==1.1.1
+django-haystack==2.1.0
django-ratings==0.3.7
-django-registration==0.8
-django-widget-tweaks==1.1.2
-gunicorn==0.17.2
+django-registration==1.0
+django-widget-tweaks==1.3
+gunicorn==18.0
+ipython==1.0.0
+pyelasticsearch==0.6
python-dateutil==2.1
+requests==2.0.0
+simplejson==3.3.1
+six==1.4.1
wsgiref==0.1.2
-Markdown==2.3.1
-django-haystack==2.0.0
-pyelasticsearch==0.5
-requests==1.2.3
diff --git a/templates/500.html b/templates/500.html
index 212ab09..ef92bee 100644
--- a/templates/500.html
+++ b/templates/500.html
@@ -1,6 +1,80 @@
-{% extends "base.html" %}
-{% block head_title %}Server Error(500 error){% endblock %}
-{% block content %}
-
Server error(500)
-
Sorry, our server resulted some error. We have informed our administrators. Till then try going to Home!
We have actually made this website for us. We want to learn new things related to Computer Science. But what happens everytime is we waste much of our time in finding good resources to learn from.
-
This website aims to become a public library cum recommendation hub for learning. You want to learn programming or any computer science topic. Search it in topics and you are good to go.
- We collect content across the web and submit it here. There is no crawler doing this. We hand picked these resources from our bookmarks and recommendations given by many people.
-
-
- Then some initial rating is calculated and listed in the Resources Section under the appropriate category and topic. You can view resources according to their popularity and recent-ness.
-
-
- Also each Topic has its own page explaining what it is and how it is used in real world. This gives only beginners a view about the topic and they can decide if they want to learn it or not. We are working on making this crowdsourced so that experts in the field can make it even better.
-
-
- We are also working on forum(also evaluating hosted option) so that learners can ask questions easily. Even silly questions that get tagged unappropriate in Stack Overflow are welcome. For more Check out Guidelines Section to get a knowledge of functioning and our expectation from you.
-
-
-
I am a programmer
-
Share your knowledge, add resources which you think can help any person wanting to learn programming. If you are familier with Django, you can help us develop this project.
Thanks {{ account }}, activation complete! You may now Login using the username and password you set at registration.
+
Thanks {{ account }}, activation complete! You may now Login using the username and password you set at registration.
{% else %}
Oops – it seems that your activation key is invalid. Please check the url again.
{% endif %}
diff --git a/templates/registration/activation_complete.html b/templates/registration/activation_complete.html
index e3887cb..c2f1bfa 100644
--- a/templates/registration/activation_complete.html
+++ b/templates/registration/activation_complete.html
@@ -1,7 +1,7 @@
{% extends "registration/registration_base.html" %}
{% block head_title %}Activation complete{% endblock %}
{% block content %}
-Thanks, activation complete! You may now login using the username and password you set at registration.
+Thanks, activation complete! You may now login using the username and password you set at registration.
{% endblock %}
diff --git a/templates/registration/activation_email.txt b/templates/registration/activation_email.txt
index adebd39..0968534 100644
--- a/templates/registration/activation_email.txt
+++ b/templates/registration/activation_email.txt
@@ -1,4 +1,4 @@
-Welcome to Codesters
+Welcome to {{ site.name }}
You (or someone pretending to be you) have asked to register an account at
{{ site.name }}. If this wasn't you, please ignore this email
@@ -7,7 +7,10 @@ and your address will be removed from our records.
To activate this account, please click the following link within the next
{{ expiration_days }} days:
-http://{{site.domain}}{% url registration_activate activation_key %}
+http://{{site.domain}}{% url 'registration_activate' activation_key %}
+
+Thank you for your interest in {{ site.name }}. If you need any help or have
+a suggestion, please email us at admin@{{ site.domain }}.
Sincerely,
{{ site.name }} Management
diff --git a/templates/registration/login.html b/templates/registration/login.html
index 9bbe772..8915f26 100644
--- a/templates/registration/login.html
+++ b/templates/registration/login.html
@@ -3,7 +3,7 @@
{% block content %}
diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html
index 3686f81..b5de5de 100644
--- a/templates/registration/password_reset_complete.html
+++ b/templates/registration/password_reset_complete.html
@@ -1,5 +1,5 @@
{% extends "registration/registration_base.html" %}
{% block head_title %}Password reset complete{% endblock %}
{% block content %}
-Your password has been reset! You may now log in.
+Your password has been reset! You may now Log In.
{% endblock %}
diff --git a/templates/registration/password_reset_email.html b/templates/registration/password_reset_email.html
index d6b7c0c..62977c2 100644
--- a/templates/registration/password_reset_email.html
+++ b/templates/registration/password_reset_email.html
@@ -7,10 +7,10 @@
To reset your password, please click the following link, or copy and paste it
into your web browser:
-{{ protocol }}://{{ domain }}{% url auth_password_reset_confirm uid token %}
+{{ protocol }}://{{ domain }}{% url 'auth_password_reset_confirm' uid token %}
Your username, in case you've forgotten: {{ user.username }}
Best regards,
-{{ site_name }} Management
\ No newline at end of file
+{{ site_name }} Management
diff --git a/templates/registration/registration_form.html b/templates/registration/registration_form.html
index 6411c27..7a2ce6e 100644
--- a/templates/registration/registration_form.html
+++ b/templates/registration/registration_form.html
@@ -1,5 +1,4 @@
{% extends "registration/registration_base.html" %}
-{% load url from future %}
{% block head_title %}Register for an account{% endblock %}
{% block content %}
diff --git a/resources/templates/resources/base.html b/templates/resources/base.html
similarity index 100%
rename from resources/templates/resources/base.html
rename to templates/resources/base.html
diff --git a/resources/templates/resources/rating.html b/templates/resources/rating.html
similarity index 97%
rename from resources/templates/resources/rating.html
rename to templates/resources/rating.html
index dbd8396..1fb7eb7 100644
--- a/resources/templates/resources/rating.html
+++ b/templates/resources/rating.html
@@ -1,4 +1,3 @@
-{% load url from future %}
{% load ratings %}
{% rating_by_user user on resource.rating as vote %}
{% if vote > 0 %}
diff --git a/resources/templates/resources/resource_detail.html b/templates/resources/resource_detail.html
similarity index 98%
rename from resources/templates/resources/resource_detail.html
rename to templates/resources/resource_detail.html
index 0cfaffd..9623af8 100644
--- a/resources/templates/resources/resource_detail.html
+++ b/templates/resources/resource_detail.html
@@ -1,9 +1,8 @@
{% extends "base.html" %}
-{% load url from future %}
{% load age issaved from codesters %}
{% load disqus_tags %}
-{% load markup %}
+{% load markdown_deux_tags %}
{% block meta_description %}{{ resource.help_text }} - {{ resource.resource_type }} submitted by {{ resource.created_by }}{% endblock %}
diff --git a/resources/templates/resources/resource_form.html b/templates/resources/resource_form.html
similarity index 94%
rename from resources/templates/resources/resource_form.html
rename to templates/resources/resource_form.html
index 89e7453..d241562 100644
--- a/resources/templates/resources/resource_form.html
+++ b/templates/resources/resource_form.html
@@ -1,5 +1,4 @@
{% extends "resources/base.html" %}
-{% load url from future %}
{% block resource_content %}
Back
diff --git a/resources/templates/resources/resource_home.html b/templates/resources/resource_home.html
similarity index 98%
rename from resources/templates/resources/resource_home.html
rename to templates/resources/resource_home.html
index 8acecd5..d3eb5dd 100644
--- a/resources/templates/resources/resource_home.html
+++ b/templates/resources/resource_home.html
@@ -1,5 +1,4 @@
{% extends "resources/base.html" %}
-{% load url from future %}
{% block head_title %}Resources - Select a topic to start learning{% endblock %}
diff --git a/resources/templates/resources/resource_list.html b/templates/resources/resource_list.html
similarity index 97%
rename from resources/templates/resources/resource_list.html
rename to templates/resources/resource_list.html
index 8e4c37f..65b18d6 100644
--- a/resources/templates/resources/resource_list.html
+++ b/templates/resources/resource_list.html
@@ -1,5 +1,4 @@
{% extends "resources/base.html" %}
-{% load url from future %}
{% load check_active from codesters %}
{% block resource_content %}
diff --git a/resources/templates/resources/resource_type_nav.html b/templates/resources/resource_type_nav.html
similarity index 98%
rename from resources/templates/resources/resource_type_nav.html
rename to templates/resources/resource_type_nav.html
index 1e78eec..9f63e01 100644
--- a/resources/templates/resources/resource_type_nav.html
+++ b/templates/resources/resource_type_nav.html
@@ -1,4 +1,3 @@
-{% load url from future %}
{% load check_active check_active_button from codesters %}
diff --git a/resources/templates/resources/single_resource_snippet.html b/templates/resources/single_resource_snippet.html
similarity index 98%
rename from resources/templates/resources/single_resource_snippet.html
rename to templates/resources/single_resource_snippet.html
index f23c641..2201929 100644
--- a/resources/templates/resources/single_resource_snippet.html
+++ b/templates/resources/single_resource_snippet.html
@@ -1,4 +1,3 @@
-{% load url from future %}
{% load age website issaved from codesters %}
diff --git a/resources/templates/resources/topic_form.html b/templates/resources/topic_form.html
similarity index 100%
rename from resources/templates/resources/topic_form.html
rename to templates/resources/topic_form.html
diff --git a/resources/templates/resources/topic_home.html b/templates/resources/topic_home.html
similarity index 98%
rename from resources/templates/resources/topic_home.html
rename to templates/resources/topic_home.html
index aca5d01..88a3cf4 100644
--- a/resources/templates/resources/topic_home.html
+++ b/templates/resources/topic_home.html
@@ -1,7 +1,6 @@
{% extends "resources/base.html" %}
-{% load url from future %}
{% load isfollow from codesters %}
-{% load markup %}
+{% load markdown_deux_tags %}
{% block meta_description %}{{ current_topic.description|slice:":200" }}{% endblock %}
diff --git a/resources/templates/resources/topics.html b/templates/resources/topics.html
similarity index 95%
rename from resources/templates/resources/topics.html
rename to templates/resources/topics.html
index 53d8abe..9d32be9 100644
--- a/resources/templates/resources/topics.html
+++ b/templates/resources/topics.html
@@ -1,5 +1,4 @@
{% load active from codesters %}
-{% load url from future %}
Add New Resource
{% if user.is_staff %}
diff --git a/templates/search/base.html b/templates/search/base.html
index 26d97b3..dc4fb23 100644
--- a/templates/search/base.html
+++ b/templates/search/base.html
@@ -1,5 +1,4 @@
{% extends "bare_base.html" %}
-{% load url from future %}
{% block head_title %}Search{% endblock head_title %}
{% block body %}
diff --git a/templates/snippets/coming_soon_snippet.html b/templates/snippets/coming_soon_snippet.html
index 80f6c81..c21bc35 100644
--- a/templates/snippets/coming_soon_snippet.html
+++ b/templates/snippets/coming_soon_snippet.html
@@ -1,4 +1,3 @@
-{% load url from future %}
Coming Soon
We are actively developing this website. Soon you will be able to see this content. Till then browse other features...