diff --git a/confirmation/CHANGELOG.txt b/confirmation/CHANGELOG.txt new file mode 100644 index 0000000000..f6f0ab56f9 --- /dev/null +++ b/confirmation/CHANGELOG.txt @@ -0,0 +1,3 @@ +============================= +Django Confirmation Changelog +============================= diff --git a/confirmation/INSTALL.txt b/confirmation/INSTALL.txt new file mode 100644 index 0000000000..a2283d56a7 --- /dev/null +++ b/confirmation/INSTALL.txt @@ -0,0 +1,12 @@ +To install django-confirmation, run the following command inside this +directory: + + python setup.py install + +You can also just copy the included ``confirmation`` directory somewhere +on your Python path, or symlink to it from somewhere on your Python path; +this is useful if you're working from a Subversion checkout. + +This application requires Python 2.3 or later, and Django 1.0 or later. +You can obtain Python from http://www.python.org/ and Django from +http://www.djangoproject.com/. diff --git a/confirmation/LICENSE.txt b/confirmation/LICENSE.txt new file mode 100644 index 0000000000..73aaa11858 --- /dev/null +++ b/confirmation/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright (c) 2008, Jarek Zgoda +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. diff --git a/confirmation/README.txt b/confirmation/README.txt new file mode 100644 index 0000000000..4b6eb8c9d9 --- /dev/null +++ b/confirmation/README.txt @@ -0,0 +1,10 @@ +=================== +Django Confirmation +=================== + +This is a generic object confirmation system for Django applications. + +For installation instructions, see the file "INSTALL.txt" in this +directory; for instructions on how to use this application, and on +what it provides, see the file "overview.txt" in the "docs/" +directory. diff --git a/confirmation/__init__.py b/confirmation/__init__.py new file mode 100644 index 0000000000..0d3ce817c6 --- /dev/null +++ b/confirmation/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, dis- +# tribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the fol- +# lowing conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- +# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +VERSION = (0, 9, 'pre') diff --git a/confirmation/admin.py b/confirmation/admin.py new file mode 100644 index 0000000000..2b5f6ba5b6 --- /dev/null +++ b/confirmation/admin.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +__revision__ = '$Id: admin.py 3 2008-11-18 07:33:52Z jarek.zgoda $' + + +from django.contrib import admin + +from confirmation.models import Confirmation + + +admin.site.register(Confirmation) \ No newline at end of file diff --git a/confirmation/management/__init__.py b/confirmation/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/confirmation/management/commands/__init__.py b/confirmation/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/confirmation/management/commands/cleanupconfirmation.py b/confirmation/management/commands/cleanupconfirmation.py new file mode 100644 index 0000000000..7a1b97c4ed --- /dev/null +++ b/confirmation/management/commands/cleanupconfirmation.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +__revision__ = '$Id: cleanupconfirmation.py 5 2008-11-18 09:10:12Z jarek.zgoda $' + + +from django.core.management.base import NoArgsCommand + +from confirmation.models import Confirmation + + +class Command(NoArgsCommand): + help = 'Delete expired confirmations from database' + + def handle_noargs(self, **options): + Confirmation.objects.delete_expired_confirmations() diff --git a/confirmation/models.py b/confirmation/models.py new file mode 100644 index 0000000000..daa1cd1af6 --- /dev/null +++ b/confirmation/models.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +__revision__ = '$Id: models.py 28 2009-10-22 15:03:02Z jarek.zgoda $' + +import os +import re +import datetime +from hashlib import sha1 + +from django.db import models +from django.core.urlresolvers import reverse +from django.core.mail import send_mail +from django.conf import settings +from django.template import loader, Context +from django.contrib.sites.models import Site +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.utils.translation import ugettext_lazy as _ + +from confirmation.util import get_status_field + +try: + import mailer + send_mail = mailer.send_mail +except ImportError: + # no mailer app present, stick with default + pass + + +SHA1_RE = re.compile('^[a-f0-9]{40}$') + + +class ConfirmationManager(models.Manager): + + def confirm(self, confirmation_key): + if SHA1_RE.search(confirmation_key): + try: + confirmation = self.get(confirmation_key=confirmation_key) + except self.model.DoesNotExist: + return False + if not confirmation.key_expired(): + obj = confirmation.content_object + status_field = get_status_field(obj._meta.app_label, obj._meta.module_name) + setattr(obj, status_field, getattr(settings, 'STATUS_ACTIVE', 1)) + obj.save() + return obj + return False + + def send_confirmation(self, obj, email_address): + confirmation_key = sha1(str(os.urandom(12)) + str(email_address)).hexdigest() + current_site = Site.objects.get_current() + activate_url = u'http://%s%s' % (current_site.domain, + reverse('confirmation.views.confirm', kwargs={'confirmation_key': confirmation_key})) + context = Context({ + 'activate_url': activate_url, + 'current_site': current_site, + 'confirmation_key': confirmation_key, + 'target': obj, + 'days': getattr(settings, 'EMAIL_CONFIRMATION_DAYS', 10), + }) + templates = [ + 'confirmation/%s_confirmation_email_subject.txt' % obj._meta.module_name, + 'confirmation/confirmation_email_subject.txt', + ] + template = loader.select_template(templates) + subject = template.render(context).strip().replace(u'\n', u' ') # no newlines, please + templates = [ + 'confirmation/%s_confirmation_email_body.txt' % obj._meta.module_name, + 'confirmation/confirmation_email_body.txt', + ] + template = loader.select_template(templates) + body = template.render(context) + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [email_address]) + return self.create(content_object=obj, date_sent=datetime.datetime.now(), confirmation_key=confirmation_key) + + def delete_expired_confirmations(self): + for confirmation in self.all(): + if confirmation.key_expired(): + confirmation.delete() + + +class Confirmation(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + date_sent = models.DateTimeField(_('sent')) + confirmation_key = models.CharField(_('activation key'), max_length=40) + + objects = ConfirmationManager() + + class Meta: + verbose_name = _('confirmation email') + verbose_name_plural = _('confirmation emails') + + def __unicode__(self): + return _('confirmation email for %s') % self.content_object + + def key_expired(self): + expiration_date = self.date_sent + datetime.timedelta(days=getattr(settings, 'EMAIL_CONFIRMATION_DAYS', 10)) + return expiration_date <= datetime.datetime.now() + key_expired.boolean = True diff --git a/confirmation/settings.py b/confirmation/settings.py new file mode 100644 index 0000000000..8544123bce --- /dev/null +++ b/confirmation/settings.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +__revision__ = '$Id: settings.py 12 2008-11-23 19:38:52Z jarek.zgoda $' + +EMAIL_CONFIRMATION_DAYS = 10 + +STATUS_ACTIVE = 1 + +STATUS_FIELDS = { +} diff --git a/confirmation/setup.py b/confirmation/setup.py new file mode 100644 index 0000000000..642ff97e30 --- /dev/null +++ b/confirmation/setup.py @@ -0,0 +1,35 @@ +from setuptools import setup, find_packages + +# Dynamically calculate the version based on confirmation.VERSION. +version_tuple = __import__('confirmation').VERSION +if version_tuple[2] is not None: + version = "%d.%d_%s" % version_tuple +else: + version = "%d.%d" % version_tuple[:2] + + +setup( + name = 'django-confirmation', + version = version, + description = 'Generic object confirmation for Django', + author = 'Jarek Zgoda', + author_email = 'jarek.zgoda@gmail.com', + url = 'http://code.google.com/p/django-confirmation/', + license = 'New BSD License', + packages = find_packages(), + classifiers = [ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Utilities', + ], + zip_safe = False, + install_requires = [ + 'django>=1.0', + ], +) + diff --git a/confirmation/urls.py b/confirmation/urls.py new file mode 100644 index 0000000000..12fefa6fe3 --- /dev/null +++ b/confirmation/urls.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +__revision__ = '$Id: urls.py 3 2008-11-18 07:33:52Z jarek.zgoda $' + + +from django.conf.urls.defaults import * + +from confirmation.views import confirm + + +urlpatterns = patterns('', + (r'^(?P\w+)/$', confirm), +) \ No newline at end of file diff --git a/confirmation/util.py b/confirmation/util.py new file mode 100644 index 0000000000..7d544b98fd --- /dev/null +++ b/confirmation/util.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +__revision__ = '$Id: util.py 3 2008-11-18 07:33:52Z jarek.zgoda $' + +from django.conf import settings + +def get_status_field(app_label, model_name): + model = '%s.%s' % (app_label, model_name) + mapping = getattr(settings, 'STATUS_FIELDS', {}) + return mapping.get(model, 'status') \ No newline at end of file diff --git a/confirmation/views.py b/confirmation/views.py new file mode 100644 index 0000000000..0bf2cfccda --- /dev/null +++ b/confirmation/views.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2008, Jarek Zgoda + +__revision__ = '$Id: views.py 21 2008-12-05 09:21:03Z jarek.zgoda $' + + +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.conf import settings + +from confirmation.models import Confirmation + + +def confirm(request, confirmation_key): + confirmation_key = confirmation_key.lower() + obj = Confirmation.objects.confirm(confirmation_key) + confirmed = True + if not obj: + # confirmation failed + confirmed = False + try: + # try to get the object we was supposed to confirm + obj = Confirmation.objects.get(confirmation_key=confirmation_key) + except Confirmation.DoesNotExist: + pass + ctx = { + 'object': obj, + 'confirmed': confirmed, + 'days': getattr(settings, 'EMAIL_CONFIRMATION_DAYS', 10), + } + templates = [ + 'confirmation/confirm.html', + ] + if obj: + # if we have an object, we can use specific template + templates.insert(0, 'confirmation/confirm_%s.html' % obj._meta.module_name) + return render_to_response(templates, ctx, + context_instance=RequestContext(request))