diff options
Diffstat (limited to 'app/lttr/models.py')
-rw-r--r-- | app/lttr/models.py | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/app/lttr/models.py b/app/lttr/models.py new file mode 100644 index 0000000..5fed036 --- /dev/null +++ b/app/lttr/models.py @@ -0,0 +1,401 @@ +import datetime + +from django.dispatch import receiver +from django.contrib.gis.db import models +from django.db.models.signals import post_save +from django.contrib.sites.models import Site +from django.template.loader import select_template +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone +from django.utils.text import slugify +from django.urls import reverse +from django.core.mail import send_mail +from django.conf import settings +from django.utils.crypto import get_random_string +from django.core.mail import EmailMultiAlternatives + +from django.template import Context, Template + +from bs4 import BeautifulSoup + +from taggit.managers import TaggableManager + +from utils.util import render_images, parse_video, markdown_to_html +from taxonomy.models import TaggedItems +from media.models import LuxImage, LuxImageSize +from posts.models import Post + + +# Possible actions that user can perform +ACTIONS = ('subscribe', 'unsubscribe', 'update') + + +def markdown_to_emailhtml(base_html): + soup = BeautifulSoup(base_html, "lxml") + for p in soup.find_all('p'): + p.attrs['style']="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-weight:normal;margin-bottom:1.4em;font-size:17px;line-height:1.5;hyphens:auto;color:#222222;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif !important;" + for i in soup.find_all('img'): + i.attrs['width']="720" + i.attrs['style']="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;display:inline;margin-bottom:0;width:100% !important;max-width:100% !important;height:auto !important;max-height:auto !important;" + for h in soup.find_all('hr'): + h.attrs['style']="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;width:20%;margin-top:40px;margin-bottom:40px;margin-right:0px;margin-left:0px;border-width:0;border-top-width:1px;border-top-style:solid;border-top-color:#ddd;" + return str(soup)[12:-14] + + +def make_activation_code(): + """ Generate a unique activation code. """ + + # Use Django's crypto get_random_string() instead of rolling our own. + return get_random_string(length=40) + + +class Newsletter(models.Model): + """ A model for Newletters. Might I one day have two? I might. """ + title = models.CharField(max_length=250) + slug = models.SlugField(db_index=True, unique=True) + intro = models.TextField(blank=True, null=True) + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("lttr:detail", kwargs={"slug": self.slug}) + + def subscribe_url(self): + return reverse('lttr:newsletter_subscribe_request', kwargs={'newsletter_slug': self.slug}) + + def unsubscribe_url(self): + return reverse('newsletter_unsubscribe_request', kwargs={'newsletter_slug': self.slug}) + + def update_url(self): + return reverse('newsletter_update_request', kwargs={'newsletter_slug': self.slug}) + + def archive_url(self): + return reverse('newsletter_archive', kwargs={'newsletter_slug': self.slug}) + + def get_subscriptions(self): + return Subscriber.objects.filter(newsletter=self, subscribed=True) + + def get_template_plain(self): + return 'lttr/emails/%s_plain_text_email.txt' % self.slug + + def get_template_html(self): + return 'lttr/emails/%s_html_email.html' % self.slug + + def get_templates(self, action): + """ + Return a subject, text, HTML tuple with e-mail templates for + a particular action. Returns a tuple with subject, text and e-mail + template. + """ + + assert action in ACTIONS + ('message', ), 'Unknown action: %s' % action + + # Common substitutions for filenames + tpl_subst = { + 'action': action, + 'newsletter': self.slug + } + + # Common root path for all the templates + tpl_root = 'lttr/message/' + + subject_template = select_template([ + tpl_root + '%(newsletter)s/%(action)s_subject.txt' % tpl_subst, + tpl_root + '%(action)s_subject.txt' % tpl_subst, + ]) + + text_template = select_template([ + tpl_root + '%(newsletter)s/%(action)s.txt' % tpl_subst, + tpl_root + '%(action)s.txt' % tpl_subst, + ]) + + html_template = select_template([ + tpl_root + '%(newsletter)s/%(action)s.html' % tpl_subst, + tpl_root + '%(action)s.html' % tpl_subst, + ]) + + return (subject_template, text_template, html_template) + + def get_sender(self): + return 'Scott Gilbertson <sng@luxagraf.net>' + + @classmethod + def get_default(cls): + try: + return cls.objects.all()[0].pk + except IndexError: + return None + + + +class NewsletterMailing(models.Model): + """ A model for Newletter Mailings, the things actually sent out """ + newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE) + post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.SET_NULL) + title = models.CharField(max_length=250, blank=True) + subtitle = models.CharField(max_length=250, null=True, blank=True) + body_html = models.TextField(blank=True) + body_email_html = models.TextField(blank=True) + body_markdown = models.TextField(blank=True) + pub_date = models.DateTimeField(blank=True) + featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True) + + class Meta: + ordering = ('-title', '-pub_date') + + def __str__(self): + return self.title + + def email_encode(self): + return self.body_markdown + + def save(self, *args, **kwargs): + created = self.pk is None + if created and self.post: + self.title = self.post.title + self.subtitle = self.post.subtitle + self.body_markdown = self.post.body_markdown + self.pub_date = self.post.pub_date + self.featured_image = self.post.featured_image + self.issue = self.post.issue + if not created: + md = render_images(self.body_markdown) + self.body_html = markdown_to_html(md) + self.body_email_html = markdown_to_emailhtml(self.body_html) + self.date_created = timezone.now() + self.issue = self.post.issue + if created and not self.featured_image: + self.featured_image = LuxImage.objects.latest() + super(NewsletterMailing, self).save() + + +class Subscriber(models.Model): + """ A model for Newletter Subscriber """ + email_field = models.EmailField(db_column='email', db_index=True, blank=True, null=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True) + date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False) + date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False) + newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE) + activation_code = models.CharField(max_length=40, default=make_activation_code) + subscribed = models.BooleanField(default=False, db_index=True) + subscribe_date = models.DateTimeField(null=True, blank=True) + unsubscribed = models.BooleanField(default=False, db_index=True) + unsubscribe_date = models.DateTimeField(null=True, blank=True) + + def __str__(self): + if self.user: + return self.user.username + return self.email_field + + def get_name(self): + if self.user: + return self.user.get_full_name() + + def get_email(self): + if self.user: + return self.user.email + return self.email_field + + def set_email(self, email): + if not self.user: + self.email_field = email + email = property(get_email, set_email) + + def update(self, action): + """ + Update subscription according to requested action: + subscribe/unsubscribe/update/, then save the changes. + """ + + assert action in ('subscribe', 'update', 'unsubscribe') + + # If a new subscription or update, make sure it is subscribed + # Else, unsubscribe + if action == 'subscribe' or action == 'update': + self.subscribed = True + else: + self.unsubscribed = True + + # This triggers the subscribe() and/or unsubscribe() methods, taking + # care of stuff like maintaining the (un)subscribe date. + self.save() + + def _subscribe(self): + """ + Internal helper method for managing subscription state + during subscription. + """ + + self.subscribe_date = datetime.datetime.now() + self.subscribed = True + self.unsubscribed = False + + def _unsubscribe(self): + """ + Internal helper method for managing subscription state + during unsubscription. + """ + self.subscribed = False + self.unsubscribed = True + self.unsubscribe_date = datetime.datetime.now() + + def save(self, *args, **kwargs): + """ + Perform some basic validation and state maintenance of Subscription. + TODO: Move this code to a more suitable place (i.e. `clean()`) and + cleanup the code. Refer to comment below and + https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.clean + """ + + # This is a lame way to find out if we have changed but using Django + # API internals is bad practice. This is necessary to discriminate + # from a state where we have never been subscribed but is mostly for + # backward compatibility. It might be very useful to make this just + # one attribute 'subscribe' later. In this case unsubscribed can be + # replaced by a method property. + + if self.pk: + assert(Subscriber.objects.filter(pk=self.pk).count() == 1) + + subscription = Subscriber.objects.get(pk=self.pk) + old_subscribed = subscription.subscribed + old_unsubscribed = subscription.unsubscribed + + # If we are subscribed now and we used not to be so, subscribe. + # If we user to be unsubscribed but are not so anymore, subscribe. + if ((self.subscribed and not old_subscribed) or + (old_unsubscribed and not self.unsubscribed)): + self._subscribe() + + assert not self.unsubscribed + assert self.subscribed + + # If we are unsubcribed now and we used not to be so, unsubscribe. + # If we used to be subscribed but are not subscribed anymore, + # unsubscribe. + elif ((self.unsubscribed and not old_unsubscribed) or + (old_subscribed and not self.subscribed)): + self._unsubscribe() + + assert not self.subscribed + assert self.unsubscribed + else: + if self.subscribed: + self._subscribe() + elif self.unsubscribed: + self._unsubscribe() + + super(Subscriber, self).save(*args, **kwargs) + + def get_recipient(self): + return get_address(self.name, self.email) + + def send_activation_email(self, action): + assert action in ACTIONS, 'Unknown action: %s' % action + + (subject_template, text_template, html_template) = \ + self.newsletter.get_templates(action) + + variable_dict = { + 'subscription': self, + 'site': Site.objects.get_current(), + 'newsletter': self.newsletter, + 'date': self.subscribe_date, + 'STATIC_URL': settings.STATIC_URL, + 'MEDIA_URL': settings.MEDIA_URL + } + + subject = subject_template.render(variable_dict).strip() + text = text_template.render(variable_dict) + + message = EmailMultiAlternatives( + subject, text, + from_email=self.newsletter.get_sender(), + to=[self.email] + ) + + if html_template: + message.attach_alternative( + html_template.render(variable_dict), "text/html" + ) + + message.send() + + def subscribe_activate_url(self): + return reverse('lttr:newsletter_activate', kwargs={ + 'slug': self.newsletter.slug, + 'activation_code': self.activation_code + }) + + def unsubscribe_activate_url(self): + return reverse('lttr:newsletter_unsubscribe', kwargs={ + 'slug': self.newsletter.slug, + 'activation_code': self.activation_code + }) + + def update_activate_url(self): + return reverse('lttr:newsletter_update_activate', kwargs={ + 'slug': self.newsletter.slug, + 'action': 'update', + 'activation_code': self.activation_code + }) + + +def get_address(name, email): + if name: + return u'%s <%s>' % (name, email) + else: + return u'%s' % email + + +class StatusType(models.IntegerChoices): + INIT = 0, ('Initialized') + SENT = 1, ('Sent') + ERROR = 2, ('Error') + + +class MailingStatus(models.Model): + newsletter_mailing = models.ForeignKey(NewsletterMailing, on_delete=models.CASCADE, verbose_name=_('newsletter')) + subscriber = models.ForeignKey(Subscriber, on_delete=models.CASCADE, verbose_name=_('subscriber')) + status = models.IntegerField(choices=StatusType.choices, null=True) + creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) + + def newsletter(self): + return self.newsletter_mailing.newsletter + + def __str__(self): + return '%s : %s : %s' % (self.newsletter_mailing, + self.subscriber, + self.get_status_display()) + + class Meta: + ordering = ('-creation_date',) + verbose_name = _('subscriber mailing status') + verbose_name_plural = _('subscriber mailing statuses') + + +''' +from lttr.mailer import SendShit +mailing = NewsletterMailing.objects.get(pk=1) +newsletter = Newsletter.objects.get(pk=3) +n = SendShit(newsletter, mailing, 1) +n.send_mailings() +''' + + +def send_notification_email(newsletter, message, instance): + recipient_list = ['sng@luxagraf.net',] + subject = _('[%(site)s] New Subscriber to "%(object)s"') % { + 'site': Site.objects.get_current().name, + 'object': newsletter, + } + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, recipient_list, fail_silently=True) + + +@receiver(post_save, sender=Subscriber) +def post_save_events(sender, update_fields, created, instance, **kwargs): + if instance.subscribed: + message = "%s has signed up for %s." %(instance.email_field, instance.newsletter) + send_notification_email(instance.newsletter, message, instance) + |