# Path: patchwork/models.py
# class Patch(models.Model):
#     project = models.ForeignKey(Project, on_delete=models.CASCADE)
#     msgid = models.CharField(max_length=255)
#     name = models.CharField(max_length=255)
#     date = models.DateTimeField(default=datetime.datetime.now)
#     last_updated = models.DateTimeField(auto_now=True)
#     submitter = models.ForeignKey(Person, on_delete=models.CASCADE)
#     delegate = models.ForeignKey(User, blank=True, null=True,
#                                  on_delete=models.CASCADE)
#     state = models.ForeignKey(State, null=True, on_delete=models.CASCADE)
#     archived = models.BooleanField(default=False)
#     headers = models.TextField(blank=True)
#     content = models.TextField(null=True, blank=True)
#     pull_url = models.CharField(max_length=255, null=True, blank=True)
#     commit_ref = models.CharField(max_length=255, null=True, blank=True)
#     hash = HashField(null=True, blank=True)
#     tags = models.ManyToManyField(Tag, through=PatchTag)
# 
#     objects = PatchManager()
# 
#     def commit_message(self):
#         """Retrieves the commit message"""
#         return Comment.objects.filter(patch=self, msgid=self.msgid)
# 
#     def answers(self):
#         """Retrieves the answers (ie all comments but the commit message)"""
#         return Comment.objects.filter(Q(patch=self) & ~Q(msgid=self.msgid))
# 
#     def comments(self):
#         """Retrieves all comments of this patch ie. the commit message and the
#            answers"""
#         return Comment.objects.filter(patch=self)
# 
#     def series(self):
#         try:
#             rev = SeriesRevisionPatch.objects.filter(patch=self)[0].revision
#             return rev.series
#         except Exception:
#             return None
# 
#     def _set_tag(self, tag, count):
#         if count == 0:
#             self.patchtag_set.filter(tag=tag).delete()
#             return
#         (patchtag, _) = PatchTag.objects.get_or_create(patch=self, tag=tag)
#         if patchtag.count != count:
#             patchtag.count = count
#             patchtag.save()
# 
#     def refresh_tag_counts(self):
#         tags = self.project.tags
#         counter = Counter()
#         for comment in self.comment_set.all():
#             counter = counter + extract_tags(comment.content, tags)
# 
#         for tag in tags:
#             self._set_tag(tag, counter[tag])
# 
#     def save(self):
#         if not hasattr(self, 'state') or not self.state:
#             self.state = get_default_initial_patch_state()
# 
#         if self.hash is None and self.content is not None:
#             self.hash = hash_patch(self.content).hexdigest()
# 
#         super(Patch, self).save()
# 
#     def filename(self):
#         return filename(self.name, '.patch')
# 
#     def human_name(self):
#         return self.name
# 
#     @models.permalink
#     def get_absolute_url(self):
#         return ('patch', (), {'patch_id': self.id})
# 
#     def __str__(self):
#         return self.name
# 
#     class Meta:
#         verbose_name_plural = 'Patches'
#         ordering = ['date']
#         unique_together = [('msgid', 'project')]
# 
# class State(models.Model):
#     name = models.CharField(max_length=100)
#     ordering = models.IntegerField(unique=True)
#     action_required = models.BooleanField(default=True)
# 
#     @classmethod
#     def from_string(cls, name):
#         return State.objects.get(name__iexact=name)
# 
#     def __str__(self):
#         return self.name
# 
#     class Meta:
#         ordering = ['ordering']
# 
# class PatchChangeNotification(models.Model):
#     patch = models.OneToOneField(Patch, primary_key=True,
#                                  on_delete=models.CASCADE)
#     last_modified = models.DateTimeField(default=datetime.datetime.now)
#     orig_state = models.ForeignKey(State, on_delete=models.CASCADE)
# 
# class EmailOptout(models.Model):
#     email = models.CharField(max_length=200, primary_key=True)
# 
#     @classmethod
#     def is_optout(cls, email):
#         email = email.lower().strip()
#         return cls.objects.filter(email=email).count() > 0
# 
#     def __str__(self):
#         return self.email


import datetime
from django.conf import settings
from django.core import mail
from django.test import TestCase
from patchwork.models import Patch, State, PatchChangeNotification, EmailOptout
from patchwork.tests.utils import defaults
from patchwork.utils import send_notifications
        self.assertIn(self.patch.get_absolute_url(), msg.body)

    def testNotificationEscaping(self):
        self.patch.name = 'Patch name with " character'
        self.patch.save()
        PatchChangeNotification(patch=self.patch,
                                orig_state=self.patch.state).save()
        self._expireNotifications()

        errors = send_notifications()
        self.assertEqual(errors, [])
        self.assertEqual(len(mail.outbox), 1)
        msg = mail.outbox[0]
        self.assertEqual(msg.to, [self.submitter.email])
        self.assertNotIn('&quot;', msg.body)

    def testNotificationOptout(self):
        """ensure opt-out addresses don't get notifications"""
        PatchChangeNotification(patch=self.patch,
                                orig_state=self.patch.state).save()
        self._expireNotifications()

        EmailOptout(email=self.submitter.email).save()

        errors = send_notifications()
        self.assertEqual(errors, [])
        self.assertEqual(len(mail.outbox), 0)

    def testNotificationMerge(self):
        patches = [self.patch,

# Path: patchwork/models.py
# class Patch(models.Model):
#     project = models.ForeignKey(Project, on_delete=models.CASCADE)
#     msgid = models.CharField(max_length=255)
#     name = models.CharField(max_length=255)
#     date = models.DateTimeField(default=datetime.datetime.now)
#     last_updated = models.DateTimeField(auto_now=True)
#     submitter = models.ForeignKey(Person, on_delete=models.CASCADE)
#     delegate = models.ForeignKey(User, blank=True, null=True,
#                                  on_delete=models.CASCADE)
#     state = models.ForeignKey(State, null=True, on_delete=models.CASCADE)
#     archived = models.BooleanField(default=False)
#     headers = models.TextField(blank=True)
#     content = models.TextField(null=True, blank=True)
#     pull_url = models.CharField(max_length=255, null=True, blank=True)
#     commit_ref = models.CharField(max_length=255, null=True, blank=True)
#     hash = HashField(null=True, blank=True)
#     tags = models.ManyToManyField(Tag, through=PatchTag)
# 
#     objects = PatchManager()
# 
#     def commit_message(self):
#         """Retrieves the commit message"""
#         return Comment.objects.filter(patch=self, msgid=self.msgid)
# 
#     def answers(self):
#         """Retrieves the answers (ie all comments but the commit message)"""
#         return Comment.objects.filter(Q(patch=self) & ~Q(msgid=self.msgid))
# 
#     def comments(self):
#         """Retrieves all comments of this patch ie. the commit message and the
#            answers"""
#         return Comment.objects.filter(patch=self)
# 
#     def series(self):
#         try:
#             rev = SeriesRevisionPatch.objects.filter(patch=self)[0].revision
#             return rev.series
#         except Exception:
#             return None
# 
#     def _set_tag(self, tag, count):
#         if count == 0:
#             self.patchtag_set.filter(tag=tag).delete()
#             return
#         (patchtag, _) = PatchTag.objects.get_or_create(patch=self, tag=tag)
#         if patchtag.count != count:
#             patchtag.count = count
#             patchtag.save()
# 
#     def refresh_tag_counts(self):
#         tags = self.project.tags
#         counter = Counter()
#         for comment in self.comment_set.all():
#             counter = counter + extract_tags(comment.content, tags)
# 
#         for tag in tags:
#             self._set_tag(tag, counter[tag])
# 
#     def save(self):
#         if not hasattr(self, 'state') or not self.state:
#             self.state = get_default_initial_patch_state()
# 
#         if self.hash is None and self.content is not None:
#             self.hash = hash_patch(self.content).hexdigest()
# 
#         super(Patch, self).save()
# 
#     def filename(self):
#         return filename(self.name, '.patch')
# 
#     def human_name(self):
#         return self.name
# 
#     @models.permalink
#     def get_absolute_url(self):
#         return ('patch', (), {'patch_id': self.id})
# 
#     def __str__(self):
#         return self.name
# 
#     class Meta:
#         verbose_name_plural = 'Patches'
#         ordering = ['date']
#         unique_together = [('msgid', 'project')]
# 
# class State(models.Model):
#     name = models.CharField(max_length=100)
#     ordering = models.IntegerField(unique=True)
#     action_required = models.BooleanField(default=True)
# 
#     @classmethod
#     def from_string(cls, name):
#         return State.objects.get(name__iexact=name)
# 
#     def __str__(self):
#         return self.name
# 
#     class Meta:
#         ordering = ['ordering']
# 
# class PatchChangeNotification(models.Model):
#     patch = models.OneToOneField(Patch, primary_key=True,
#                                  on_delete=models.CASCADE)
#     last_modified = models.DateTimeField(default=datetime.datetime.now)
#     orig_state = models.ForeignKey(State, on_delete=models.CASCADE)
# 
# class EmailOptout(models.Model):
#     email = models.CharField(max_length=200, primary_key=True)
# 
#     @classmethod
#     def is_optout(cls, email):
#         email = email.lower().strip()
#         return cls.objects.filter(email=email).count() > 0
# 
#     def __str__(self):
#         return self.email


# Path: patchwork/tests/test_notifications.py
import datetime
from django.conf import settings
from django.core import mail
from django.test import TestCase
from patchwork.models import Patch, State, PatchChangeNotification, EmailOptout
from patchwork.tests.utils import defaults
from patchwork.utils import send_notifications

        self.assertIn(self.patch.get_absolute_url(), msg.body)

    def testNotificationEscaping(self):
        self.patch.name = 'Patch name with " character'
        self.patch.save()
        PatchChangeNotification(patch=self.patch,
                                orig_state=self.patch.state).save()
        self._expireNotifications()

        errors = send_notifications()
        self.assertEqual(errors, [])
        self.assertEqual(len(mail.outbox), 1)
        msg = mail.outbox[0]
        self.assertEqual(msg.to, [self.submitter.email])
        self.assertNotIn('&quot;', msg.body)

    def testNotificationOptout(self):
        """ensure opt-out addresses don't get notifications"""
        PatchChangeNotification(patch=self.patch,
                                orig_state=self.patch.state).save()
        self._expireNotifications()

        EmailOptout(email=self.submitter.email).save()

        errors = send_notifications()
        self.assertEqual(errors, [])
        self.assertEqual(len(mail.outbox), 0)

    def testNotificationMerge(self):
        patches = [self.patch,

                   Patch(project=self.project, msgid='testpatch-2',