summaryrefslogtreecommitdiff
path: root/app/lttr/models.py
diff options
context:
space:
mode:
authorlxf <sng@luxagraf.net>2022-05-14 16:38:07 -0400
committerlxf <sng@luxagraf.net>2022-05-14 16:38:07 -0400
commitbb3973ffb714c932e9ec6dd6a751228dc71fe1d3 (patch)
tree6fa32f9392ad2ec32271349b86a4c1388fd6ba95 /app/lttr/models.py
initial commit
Diffstat (limited to 'app/lttr/models.py')
-rw-r--r--app/lttr/models.py401
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)
+