+class Contact(models.Model):
+ email = models.EmailField(unique=True)
+ level = models.PositiveSmallIntegerField(
+ choices=[
+ (Level.COLD, _('Cold')),
+ (Level.TRIED, _('Would-be donor')),
+ (Level.SINGLE, _('One-time donor')),
+ (Level.RECURRING, _('Recurring donor')),
+ (Level.OPT_OUT, _('Opt out')),
+ ])
+ since = models.DateTimeField()
+ expires_at = models.DateTimeField(null=True, blank=True)
+ key = models.CharField(max_length=64, blank=True)
+
+ class Meta:
+ verbose_name = _('contact')
+ verbose_name_plural = _('contacts')
+
+ def save(self, *args, **kwargs):
+ if not self.key:
+ self.key = get_random_hash(self.email)
+ super().save(*args, **kwargs)
+
+ @property
+ def is_annoyed(self):
+ cutoff = now() - timedelta(settings.MESSAGING_MIN_DAYS)
+ return self.emailsent_set.filter(timestamp__gte=cutoff).exists()
+
+ def get_optout_url(self):
+ return reverse('messaging_optout', args=[self.key])
+
+ @classmethod
+ def update(cls, email, level, since, expires_at=None):
+ obj, created = cls.objects.get_or_create(email=email, defaults={
+ "level": level,
+ "since": since,
+ "expires_at": expires_at
+ })
+ if not created:
+ obj.ascend(level, since, expires_at)
+
+ def ascend(self, level, since=None, expires_at=None):
+ if since is None:
+ since = now()
+ if level < self.level:
+ return
+ if level == self.level:
+ self.since = min(since, self.since)
+
+ if expires_at and self.expires_at:
+ self.expires_at = max(expires_at, self.expires_at)
+ else:
+ self.expires_at = expires_at
+ else:
+ self.level = level
+ self.since = since
+ self.expires_at = expires_at
+ self.save()
+