Skip to content

Commit

Permalink
huge
Browse files Browse the repository at this point in the history
  • Loading branch information
vinta committed Jul 22, 2016
1 parent 57e75b7 commit dd07d06
Show file tree
Hide file tree
Showing 20 changed files with 176 additions and 192 deletions.
11 changes: 2 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,16 @@ python:
- "3.5"

env:
- DJANGO_VERSION=1.4.22
- DJANGO_VERSION=1.5.12
- DJANGO_VERSION=1.6.11
- DJANGO_VERSION=1.7.11
- DJANGO_VERSION=1.8.9
- DJANGO_VERSION=1.9.2
- DJANGO_VERSION=1.8.14
- DJANGO_VERSION=1.9.8

matrix:
exclude:
- python: "2.6"
env: DJANGO_VERSION=1.7.11
- python: "3.3"
env: DJANGO_VERSION=1.4.22
- python: "3.4"
env: DJANGO_VERSION=1.4.22
- python: "3.5"
env: DJANGO_VERSION=1.4.22

install:
- pip install -r requirements_test.txt
Expand Down
7 changes: 1 addition & 6 deletions email_confirm_la/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@
import django


__all__ = ['GenericForeignKey', 'update_fields']
__all__ = ['GenericForeignKey', ]


if django.VERSION >= (1, 7):
from django.contrib.contenttypes.fields import GenericForeignKey
else:
from django.contrib.contenttypes.generic import GenericForeignKey

if django.VERSION >= (1, 5):
update_fields = lambda instance, fields: instance.save(update_fields=fields)
else:
update_fields = lambda instance, fields: instance.save()
34 changes: 12 additions & 22 deletions email_confirm_la/conf.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
# coding: utf-8

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

from appconf import AppConf

class DefaultConfigs(object):

class ECLAAppConf(AppConf):
EMAIL_BACKEND = settings.EMAIL_BACKEND
HTTP_PROTOCOL = 'http'
DOMAIN = ''
CONFIRM_EXPIRE_SEC = 60 * 60 * 24 * 1 # 1 day
CONFIRM_URL_REVERSE_NAME = 'confirm_email'
EMAIL_CONFIRM_LA_HTTP_PROTOCOL = 'http'
EMAIL_CONFIRM_LA_DOMAIN = 'example.com'
EMAIL_CONFIRM_LA_CONFIRM_EXPIRE_SEC = 60 * 60 * 24 * 1 # 1 day
EMAIL_CONFIRM_LA_CONFIRM_URL_REVERSE_NAME = 'email_confirm_la:confirm_email'

# TODO: EMAIL UNIQUE OR NOT
# unique_together = (('content_type', 'email_field_name', 'email'), )

class Meta:
prefix = 'email_confirm_la'
class Configs(object):

def configure_domain(self, value):
from django.contrib.sites.models import Site
def __getattr__(self, name):
default_setting = getattr(DefaultConfigs, name)
setting = getattr(settings, name, default_setting)

if not value:
try:
current_site = Site.objects.get_current()
except ImproperlyConfigured:
raise ImproperlyConfigured("You need to provide `EMAIL_CONFIRM_LA_DOMAIN` in settings if you don't use the sites framework.")
else:
value = current_site.domain
return setting

return value

configs = Configs()
2 changes: 1 addition & 1 deletion email_confirm_la/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# coding: utf-8


class EmailConfirmationExpired(Exception):
class ExpiredError(Exception):
pass
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from django.core.management.base import BaseCommand
from django.utils import timezone

from email_confirm_la.conf import settings
from email_confirm_la.conf import configs
from email_confirm_la.models import EmailConfirmation


class Command(BaseCommand):

def handle(self, **options):
expiration_time = timezone.now() - datetime.timedelta(seconds=settings.EMAIL_CONFIRM_LA_CONFIRM_EXPIRE_SEC)
EmailConfirmation.objects.filter(send_at__lt=expiration_time).iterator().delete()
expiration_time = timezone.now() - datetime.timedelta(seconds=configs.EMAIL_CONFIRM_LA_CONFIRM_EXPIRE_SEC)
EmailConfirmation.objects.filter(send_at__lt=expiration_time).delete()
6 changes: 3 additions & 3 deletions email_confirm_la/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-02-28 22:17
# Generated by Django 1.9.8 on 2016-07-22 08:04
from __future__ import unicode_literals

from django.db import migrations, models
Expand All @@ -20,7 +20,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('email_field_name', models.CharField(max_length=32, verbose_name='Email field name')),
('email_field_name', models.CharField(default='email', max_length=32, verbose_name='Email field name')),
('email', models.EmailField(max_length=254, verbose_name='Email')),
('confirmation_key', models.CharField(max_length=64, unique=True, verbose_name='Confirmation_key')),
('send_at', models.DateTimeField(blank=True, db_index=True, null=True)),
Expand All @@ -33,6 +33,6 @@ class Migration(migrations.Migration):
),
migrations.AlterUniqueTogether(
name='emailconfirmation',
unique_together=set([('content_type', 'email_field_name', 'email')]),
unique_together=set([('content_type', 'email_field_name', 'object_id', 'email')]),
),
]
123 changes: 68 additions & 55 deletions email_confirm_la/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,25 @@

import datetime

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.mail import EmailMessage
from django.core.mail import get_connection
from django.core.urlresolvers import reverse
from django.db import models
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import pgettext_lazy as _

from email_confirm_la import signals
from email_confirm_la import utils
from email_confirm_la.conf import configs
from email_confirm_la.compat import GenericForeignKey
from email_confirm_la.compat import update_fields
from email_confirm_la.conf import settings
from email_confirm_la.exceptions import EmailConfirmationExpired
from email_confirm_la.exceptions import ExpiredError
from email_confirm_la.utils import generate_random_token


class EmailConfirmationManager(models.Manager):

def set_email_for_object(self, email, content_object, email_field_name='email', skip_verify=False, template_context=None):
def verify_email_for_object(self, email, content_object, email_field_name='email'):
"""
Add an email for `content_object` and send a confirmation mail by default.
Expand All @@ -41,13 +40,26 @@ def set_email_for_object(self, email, content_object, email_field_name='email',
confirmation.content_object = content_object
confirmation.email_field_name = email_field_name
confirmation.email = email
confirmation.confirmation_key = utils.generate_random_token([str(content_type.id), str(content_object.id), email, ])
confirmation.confirmation_key = generate_random_token([str(content_type.id), str(content_object.id), str(email_field_name), email, ])
confirmation.save()

if skip_verify:
confirmation.confirm(ignore_expire=True)
confirmation.send()

return confirmation

def get_unverified_email_for_object(self, content_object, email_field_name='email'):
try:
confirmation = EmailConfirmation.objects.get_for_object(content_object, email_field_name)
except EmailConfirmation.DoesNotExist:
unverified_email = None
else:
confirmation.send(template_context)
unverified_email = confirmation.email

return unverified_email

def get_for_object(self, content_object, email_field_name='email'):
content_type = ContentType.objects.get_for_model(content_object)
confirmation = EmailConfirmation.objects.get(content_type=content_type, object_id=content_object.id, email_field_name=email_field_name)

return confirmation

Expand All @@ -57,9 +69,9 @@ def get_queryset_for_object(self, content_object, email_field_name='email'):

return queryset

def get_for_object(self, content_object, email_field_name='email'):
content_type = ContentType.objects.get_for_model(content_object)
confirmation = EmailConfirmation.objects.get(content_type=content_type, object_id=content_object.id, email_field_name=email_field_name)
def get_for_email(self, email, content_object_model, email_field_name='email'):
content_type = ContentType.objects.get_for_model(content_object_model)
confirmation = EmailConfirmation.objects.get(content_type=content_type, email_field_name=email_field_name, email=email)

return confirmation

Expand All @@ -69,95 +81,96 @@ class EmailConfirmation(models.Model):
Once an email is confirmed, it will be delete from this model. In other words, there are only unconfirmed emails in the database.
"""

ExpiredError = ExpiredError

content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
email_field_name = models.CharField(verbose_name=_('ec_la', 'Email field name'), max_length=16, default='email')
email_field_name = models.CharField(verbose_name=_('ec_la', 'Email field name'), max_length=32, default='email')
email = models.EmailField(verbose_name=_('ec_la', 'Email'))
confirmation_key = models.CharField(verbose_name=_('ec_la', 'Confirmation_key'), max_length=64, unique=True)
send_at = models.DateTimeField(null=True, blank=True, db_index=True)
# confirmed_at = models.DateTimeField(null=True, blank=True)

objects = EmailConfirmationManager()

class Meta:
verbose_name = _('ec_la', 'Email confirmation')
verbose_name_plural = _('ec_la', 'Email confirmation')
# unique_together = (('content_type', 'email_field_name', 'object_id', 'email'), )
unique_together = (('content_type', 'email_field_name', 'email'), )
unique_together = (('content_type', 'email_field_name', 'object_id', 'email'), )

def __repr__(self):
return '<EmailConfirmation {0}>'.format(self.email)

def __str__(self):
return 'Confirmation for %s' % self.email
return 'Confirmation for {0}'.format(self.email)

def __unicode__(self):
return 'Confirmation for %s' % self.email

def get_confirmation_url(self, full=True, request=None):
url_reverse_name = settings.EMAIL_CONFIRM_LA_CONFIRM_URL_REVERSE_NAME
url = reverse(url_reverse_name, kwargs={'confirmation_key': self.confirmation_key})

if full:
# TODO: use request.build_absolute_uri()
final_url = '%s://%s%s' % (
settings.EMAIL_CONFIRM_LA_HTTP_PROTOCOL,
settings.EMAIL_CONFIRM_LA_DOMAIN,
url,
)
else:
final_url = url

return final_url
return 'Confirmation for {0}'.format(self.email)

def send(self, template_context=None):
default_template_context = {
'email': self.email,
'confirmation_key': self.confirmation_key,
'confirmation_url': self.get_confirmation_url(),
'content_object': self.content_object,
}
if isinstance(template_context, dict):
template_context = dict(default_template_context.items() + template_context.items()) # merge dictionaries
else:
template_context = default_template_context

subject = render_to_string('email_confirm_la/email/email_confirmation_subject.txt', template_context)
subject = ''.join(subject.splitlines()) # remove unnecessary line breaks
subject = ''.join(subject.splitlines()).strip() # remove unnecessary line breaks
body = render_to_string('email_confirm_la/email/email_confirmation_message.html', template_context)

message = EmailMessage(subject, body, settings.DEFAULT_FROM_EMAIL, [self.email, ])
message.content_subtype = 'html'
message.send()

# connection = get_connection(settings.EMAIL_CONFIRM_LA_EMAIL_BACKEND)
# connection.send_messages([message, ])

self.send_at = timezone.now()
update_fields(self, fields=('send_at', ))
self.save(update_fields=('send_at', ))

signals.post_email_confirmation_send.send(
sender=self.__class__,
confirmation=self,
)

@property
def is_expired(self):
if not self.send_at:
return False

expiration_time = self.send_at + datetime.timedelta(seconds=settings.EMAIL_CONFIRM_LA_CONFIRM_EXPIRE_SEC)
def get_confirmation_url(self, full=True):
url_reverse_name = configs.EMAIL_CONFIRM_LA_CONFIRM_URL_REVERSE_NAME
url = reverse(url_reverse_name, kwargs={'confirmation_key': self.confirmation_key})
if full:
confirmation_url = '{0}://{1}{2}'.format(configs.EMAIL_CONFIRM_LA_HTTP_PROTOCOL, configs.EMAIL_CONFIRM_LA_DOMAIN, url)
else:
confirmation_url = url

return expiration_time <= timezone.now()
return confirmation_url

def confirm(self, ignore_expire=False):
if not ignore_expire and self.is_expired:
raise EmailConfirmationExpired()
def confirm(self, ignore_expiration=False, save_to_content_object=True):
if not ignore_expiration and self.is_expired:
raise ExpiredError()

content_object = self.content_object
setattr(content_object, self.email_field_name, self.email)
update_fields(content_object, fields=(self.email_field_name, ))
if save_to_content_object:
setattr(self.content_object, self.email_field_name, self.email)
self.content_object.save(update_fields=(self.email_field_name, ))

signals.post_email_confirmation_confirm.send(
sender=self.__class__,
confirmation=self,
save_to_content_object=save_to_content_object,
)

# delete all confirmations for the same email
EmailConfirmation.objects.filter(content_type=self.content_type, email_field_name=self.email_field_name, email=self.email).delete()
def clean(self):
"""
delete all confirmations for the same content_object and the same field
"""

EmailConfirmation.objects.filter(content_type=self.content_type, object_id=self.object_id, email_field_name=self.email_field_name).delete()

@property
def is_expired(self):
if not self.send_at:
return False

expiration_time = self.send_at + datetime.timedelta(seconds=configs.EMAIL_CONFIRM_LA_CONFIRM_EXPIRE_SEC)

return expiration_time <= timezone.now()
6 changes: 3 additions & 3 deletions email_confirm_la/signals.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# coding: utf-8

import django.dispatch
from django.dispatch import Signal


post_email_confirmation_send = django.dispatch.Signal(providing_args=['confirmation', ])
post_email_confirmation_confirm = django.dispatch.Signal(providing_args=['confirmation', ])
post_email_confirmation_send = Signal(providing_args=['confirmation', ])
post_email_confirmation_confirm = Signal(providing_args=['confirmation', 'save_to_content_object'])
Loading

0 comments on commit dd07d06

Please sign in to comment.