Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

models.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import os
  4. from collections import namedtuple
  5. from uuid import uuid4
  6. from django.core.mail import EmailMessage, EmailMultiAlternatives
  7. from django.db import models
  8. from django.template import Context, Template
  9. from django.utils.encoding import python_2_unicode_compatible
  10. from django.utils.translation import pgettext_lazy
  11. from django.utils.translation import ugettext_lazy as _
  12. from django.utils import timezone
  13. from jsonfield import JSONField
  14. from post_office import cache
  15. from post_office.fields import CommaSeparatedEmailField
  16. from .compat import text_type, smart_text
  17. from .connections import connections
  18. from .settings import context_field_class, get_log_level
  19. from .validators import validate_email_with_name, validate_template_syntax
  20. PRIORITY = namedtuple('PRIORITY', 'low medium high now')._make(range(4))
  21. STATUS = namedtuple('STATUS', 'sent failed queued')._make(range(3))
  22. @python_2_unicode_compatible
  23. class Email(models.Model):
  24. """
  25. A model to hold email information.
  26. """
  27. PRIORITY_CHOICES = [(PRIORITY.low, _("low")), (PRIORITY.medium, _("medium")),
  28. (PRIORITY.high, _("high")), (PRIORITY.now, _("now"))]
  29. STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed")),
  30. (STATUS.queued, _("queued"))]
  31. from_email = models.CharField(_("Email From"), max_length=254,
  32. validators=[validate_email_with_name])
  33. to = CommaSeparatedEmailField(_("Email To"))
  34. cc = CommaSeparatedEmailField(_("Cc"))
  35. bcc = CommaSeparatedEmailField(_("Bcc"))
  36. subject = models.CharField(_("Subject"), max_length=989, blank=True)
  37. message = models.TextField(_("Message"), blank=True)
  38. html_message = models.TextField(_("HTML Message"), blank=True)
  39. """
  40. Emails with 'queued' status will get processed by ``send_queued`` command.
  41. Status field will then be set to ``failed`` or ``sent`` depending on
  42. whether it's successfully delivered.
  43. """
  44. status = models.PositiveSmallIntegerField(
  45. _("Status"),
  46. choices=STATUS_CHOICES, db_index=True,
  47. blank=True, null=True)
  48. priority = models.PositiveSmallIntegerField(_("Priority"),
  49. choices=PRIORITY_CHOICES,
  50. blank=True, null=True)
  51. created = models.DateTimeField(auto_now_add=True, db_index=True)
  52. last_updated = models.DateTimeField(db_index=True, auto_now=True)
  53. scheduled_time = models.DateTimeField(_('The scheduled sending time'),
  54. blank=True, null=True, db_index=True)
  55. headers = JSONField(_('Headers'), blank=True, null=True)
  56. template = models.ForeignKey('post_office.EmailTemplate', blank=True,
  57. null=True, verbose_name=_('Email template'),
  58. on_delete=models.CASCADE)
  59. context = context_field_class(_('Context'), blank=True, null=True)
  60. backend_alias = models.CharField(_('Backend alias'), blank=True, default='',
  61. max_length=64)
  62. class Meta:
  63. app_label = 'post_office'
  64. verbose_name = pgettext_lazy("Email address", "Email")
  65. verbose_name_plural = pgettext_lazy("Email addresses", "Emails")
  66. def __init__(self, *args, **kwargs):
  67. super(Email, self).__init__(*args, **kwargs)
  68. self._cached_email_message = None
  69. def __str__(self):
  70. return u'%s' % self.to
  71. def email_message(self):
  72. """
  73. Returns Django EmailMessage object for sending.
  74. """
  75. if self._cached_email_message:
  76. return self._cached_email_message
  77. return self.prepare_email_message()
  78. def prepare_email_message(self):
  79. """
  80. Returns a django ``EmailMessage`` or ``EmailMultiAlternatives`` object,
  81. depending on whether html_message is empty.
  82. """
  83. subject = smart_text(self.subject)
  84. if self.template is not None:
  85. _context = Context(self.context)
  86. subject = Template(self.template.subject).render(_context)
  87. message = Template(self.template.content).render(_context)
  88. html_message = Template(self.template.html_content).render(_context)
  89. else:
  90. subject = self.subject
  91. message = self.message
  92. html_message = self.html_message
  93. connection = connections[self.backend_alias or 'default']
  94. if html_message:
  95. msg = EmailMultiAlternatives(
  96. subject=subject, body=message, from_email=self.from_email,
  97. to=self.to, bcc=self.bcc, cc=self.cc,
  98. headers=self.headers, connection=connection)
  99. msg.attach_alternative(html_message, "text/html")
  100. else:
  101. msg = EmailMessage(
  102. subject=subject, body=message, from_email=self.from_email,
  103. to=self.to, bcc=self.bcc, cc=self.cc,
  104. headers=self.headers, connection=connection)
  105. for attachment in self.attachments.all():
  106. msg.attach(attachment.name, attachment.file.read(), mimetype=attachment.mimetype or None)
  107. attachment.file.close()
  108. self._cached_email_message = msg
  109. return msg
  110. def dispatch(self, log_level=None,
  111. disconnect_after_delivery=True, commit=True):
  112. """
  113. Sends email and log the result.
  114. """
  115. try:
  116. self.email_message().send()
  117. status = STATUS.sent
  118. message = ''
  119. exception_type = ''
  120. except Exception as e:
  121. status = STATUS.failed
  122. message = str(e)
  123. exception_type = type(e).__name__
  124. # If run in a bulk sending mode, reraise and let the outer
  125. # layer handle the exception
  126. if not commit:
  127. raise
  128. if commit:
  129. self.status = status
  130. self.save(update_fields=['status'])
  131. if log_level is None:
  132. log_level = get_log_level()
  133. # If log level is 0, log nothing, 1 logs only sending failures
  134. # and 2 means log both successes and failures
  135. if log_level == 1:
  136. if status == STATUS.failed:
  137. self.logs.create(status=status, message=message,
  138. exception_type=exception_type)
  139. elif log_level == 2:
  140. self.logs.create(status=status, message=message,
  141. exception_type=exception_type)
  142. return status
  143. def save(self, *args, **kwargs):
  144. self.full_clean()
  145. return super(Email, self).save(*args, **kwargs)
  146. @python_2_unicode_compatible
  147. class Log(models.Model):
  148. """
  149. A model to record sending email sending activities.
  150. """
  151. STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed"))]
  152. email = models.ForeignKey(Email, editable=False, related_name='logs',
  153. verbose_name=_('Email address'), on_delete=models.CASCADE)
  154. date = models.DateTimeField(auto_now_add=True)
  155. status = models.PositiveSmallIntegerField(_('Status'), choices=STATUS_CHOICES)
  156. exception_type = models.CharField(_('Exception type'), max_length=255, blank=True)
  157. message = models.TextField(_('Message'))
  158. class Meta:
  159. app_label = 'post_office'
  160. verbose_name = _("Log")
  161. verbose_name_plural = _("Logs")
  162. def __str__(self):
  163. return text_type(self.date)
  164. class EmailTemplateManager(models.Manager):
  165. def get_by_natural_key(self, name, language, default_template):
  166. return self.get(name=name, language=language, default_template=default_template)
  167. @python_2_unicode_compatible
  168. class EmailTemplate(models.Model):
  169. """
  170. Model to hold template information from db
  171. """
  172. name = models.CharField(_('Name'), max_length=255, help_text=_("e.g: 'welcome_email'"))
  173. description = models.TextField(_('Description'), blank=True,
  174. help_text=_("Description of this template."))
  175. created = models.DateTimeField(auto_now_add=True)
  176. last_updated = models.DateTimeField(auto_now=True)
  177. subject = models.CharField(max_length=255, blank=True,
  178. verbose_name=_("Subject"), validators=[validate_template_syntax])
  179. content = models.TextField(blank=True,
  180. verbose_name=_("Content"), validators=[validate_template_syntax])
  181. html_content = models.TextField(blank=True,
  182. verbose_name=_("HTML content"), validators=[validate_template_syntax])
  183. language = models.CharField(max_length=12,
  184. verbose_name=_("Language"),
  185. help_text=_("Render template in alternative language"),
  186. default='', blank=True)
  187. default_template = models.ForeignKey('self', related_name='translated_templates',
  188. null=True, default=None, verbose_name=_('Default template'), on_delete=models.CASCADE)
  189. objects = EmailTemplateManager()
  190. class Meta:
  191. app_label = 'post_office'
  192. unique_together = ('name', 'language', 'default_template')
  193. verbose_name = _("Email Template")
  194. verbose_name_plural = _("Email Templates")
  195. ordering = ['name']
  196. def __str__(self):
  197. return u'%s %s' % (self.name, self.language)
  198. def natural_key(self):
  199. return (self.name, self.language, self.default_template)
  200. def save(self, *args, **kwargs):
  201. # If template is a translation, use default template's name
  202. if self.default_template and not self.name:
  203. self.name = self.default_template.name
  204. template = super(EmailTemplate, self).save(*args, **kwargs)
  205. cache.delete(self.name)
  206. return template
  207. def get_upload_path(instance, filename):
  208. """Overriding to store the original filename"""
  209. if not instance.name:
  210. instance.name = filename # set original filename
  211. date = timezone.now().date()
  212. filename = '{name}.{ext}'.format(name=uuid4().hex,
  213. ext=filename.split('.')[-1])
  214. return os.path.join('post_office_attachments', str(date.year),
  215. str(date.month), str(date.day), filename)
  216. @python_2_unicode_compatible
  217. class Attachment(models.Model):
  218. """
  219. A model describing an email attachment.
  220. """
  221. file = models.FileField(_('File'), upload_to=get_upload_path)
  222. name = models.CharField(_('Name'), max_length=255, help_text=_("The original filename"))
  223. emails = models.ManyToManyField(Email, related_name='attachments',
  224. verbose_name=_('Email addresses'))
  225. mimetype = models.CharField(max_length=255, default='', blank=True)
  226. class Meta:
  227. app_label = 'post_office'
  228. verbose_name = _("Attachment")
  229. verbose_name_plural = _("Attachments")
  230. def __str__(self):
  231. return self.name