]> git.ozlabs.org Git - patchwork/blobdiff - apps/patchwork/utils.py
notifications: add project name to patch update notification
[patchwork] / apps / patchwork / utils.py
index 05e66e26eb0c72bd9b7e0db0378fc9e5d012a1cb..f8fee3fcaaa1c8d0db9e5d50b4989ac75ae9ef3c 100644 (file)
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
-from patchwork.forms import MultiplePatchForm
-from patchwork.models import Bundle, Project, State, UserProfile
+import itertools
+import datetime
+from django.shortcuts import get_object_or_404
+from django.template.loader import render_to_string
+from django.contrib.sites.models import Site
 from django.conf import settings
-from django.shortcuts import render_to_response, get_object_or_404
+from django.core.mail import EmailMessage
+from django.db.models import Max
+from django.db.utils import IntegrityError
+from patchwork.forms import MultiplePatchForm
+from patchwork.models import Bundle, Project, BundlePatch, UserProfile, \
+        PatchChangeNotification, EmailOptout
 
 def get_patch_ids(d, prefix = 'patch_id'):
     ids = []
@@ -46,13 +54,17 @@ class Order(object):
         'submitter':    'submitter__name',
         'delegate':     'delegate__username',
     }
-    default_order = 'date'
+    default_order = ('date', True)
 
-    def __init__(self, str = None):
+    def __init__(self, str = None, editable = False):
         self.reversed = False
+        self.editable = editable
+        (self.order, self.reversed) = self.default_order
+
+        if self.editable:
+            return
 
         if str is None or str == '':
-            self.order = self.default_order
             return
 
         reversed = False
@@ -61,7 +73,6 @@ class Order(object):
             reversed = True
 
         if str not in self.order_map.keys():
-            self.order = self.default_order
             return
 
         self.order = str
@@ -93,105 +104,111 @@ def set_bundle(user, project, action, data, patches, context):
     # set up the bundle
     bundle = None
     if action == 'create':
+        bundle_name = data['bundle_name'].strip()
+        if '/' in bundle_name:
+            return ['Bundle names can\'t contain slashes']
+
+        if not bundle_name:
+            return ['No bundle name was specified']
+
+        if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0:
+            return ['You already have a bundle called "%s"' % bundle_name]
+
         bundle = Bundle(owner = user, project = project,
-                name = data['bundle_name'])
+                name = bundle_name)
         bundle.save()
-        str = 'added to new bundle "%s"' % bundle.name
-        auth_required = False
+        context.add_message("Bundle %s created" % bundle.name)
 
     elif action =='add':
         bundle = get_object_or_404(Bundle, id = data['bundle_id'])
-        str = 'added to bundle "%s"' % bundle.name
-        auth_required = False
 
     elif action =='remove':
         bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
-        str = 'removed from bundle "%s"' % bundle.name
-        auth_required = False
 
     if not bundle:
         return ['no such bundle']
 
     for patch in patches:
         if action == 'create' or action == 'add':
-            bundle.patches.add(patch)
+            bundlepatch_count = BundlePatch.objects.filter(bundle = bundle,
+                        patch = patch).count()
+            if bundlepatch_count == 0:
+                bundle.append_patch(patch)
+                context.add_message("Patch '%s' added to bundle %s" % \
+                        (patch.name, bundle.name))
+            else:
+                context.add_message("Patch '%s' already in bundle %s" % \
+                        (patch.name, bundle.name))
 
         elif action == 'remove':
-            bundle.patches.remove(patch)
-
-    if len(patches) > 0:
-        if len(patches) == 1:
-            str = 'patch ' + str
-        else:
-            str = 'patches ' + str
-        context.add_message(str)
+            try:
+                bp = BundlePatch.objects.get(bundle = bundle, patch = patch)
+                bp.delete()
+                context.add_message("Patch '%s' removed from bundle %s\n" % \
+                        (patch.name, bundle.name))
+            except Exception:
+                pass
 
     bundle.save()
 
     return []
 
+def send_notifications():
+    date_limit = datetime.datetime.now() - \
+                     datetime.timedelta(minutes =
+                                settings.NOTIFICATION_DELAY_MINUTES)
 
-def set_patches(user, project, action, data, patches, context):
-    errors = []
-    form = MultiplePatchForm(project = project, data = data)
+    # This gets funky: we want to filter out any notifications that should
+    # be grouped with other notifications that aren't ready to go out yet. To
+    # do that, we join back onto PatchChangeNotification (PCN -> Patch ->
+    # Person -> Patch -> max(PCN.last_modified)), filtering out any maxima
+    # that are with the date_limit.
+    qs = PatchChangeNotification.objects \
+            .annotate(m = Max('patch__submitter__patch__patchchangenotification'
+                        '__last_modified')) \
+                .filter(m__lt = date_limit)
 
-    try:
-        project = Project.objects.get(id = data['project'])
-    except:
-        errors = ['No such project']
-        return (errors, form)
+    groups = itertools.groupby(qs.order_by('patch__submitter'),
+                               lambda n: n.patch.submitter)
 
-    str = ''
+    errors = []
 
-    # this may be a bundle action, which doesn't modify a patch. in this
-    # case, don't require a valid form, or patch editing permissions
-    if action in bundle_actions:
-        errors = set_bundle(user, project, action, data, patches, context)
-        return (errors, form)
+    for (recipient, notifications) in groups:
+        notifications = list(notifications)
+        projects = set([ n.patch.project.linkname for n in notifications ])
 
-    if not form.is_valid():
-        errors = ['The submitted form data was invalid']
-        return (errors, form)
+        def delete_notifications():
+            PatchChangeNotification.objects.filter(
+                                pk__in = notifications).delete()
 
-    for patch in patches:
-        if not patch.is_editable(user):
-            errors.append('You don\'t have permissions to edit the ' + \
-                    'patch "%s"' \
-                    % patch.name)
+        if EmailOptout.is_optout(recipient.email):
+            delete_notifications()
             continue
 
-        if action == 'update':
-            form.save(patch)
-            str = 'updated'
-
-        elif action == 'ack':
-            pass
-
-        elif action == 'archive':
-            patch.archived = True
-            patch.save()
-            str = 'archived'
-
-        elif action == 'unarchive':
-            patch.archived = True
-            patch.save()
-            str = 'un-archived'
-
-        elif action == 'delete':
-            patch.delete()
-            str = 'un-archived'
-
-
-    if len(patches) > 0:
-        if len(patches) == 1:
-            str = 'patch ' + str
-        else:
-            str = 'patches ' + str
-        context.add_message(str)
-
-    return (errors, form)
+        context = {
+            'site': Site.objects.get_current(),
+            'person': recipient,
+            'notifications': notifications,
+            'projects': projects,
+        }
+
+        subject = render_to_string(
+                        'patchwork/patch-change-notification-subject.text',
+                        context).strip()
+        content = render_to_string('patchwork/patch-change-notification.mail',
+                                context)
+
+        message = EmailMessage(subject = subject, body = content,
+                               from_email = settings.NOTIFICATION_FROM_EMAIL,
+                               to = [recipient.email],
+                               headers = {'Precedence': 'bulk'})
+
+        try:
+            message.send()
+        except ex:
+            errors.append((recipient, ex))
+            continue
 
-def userprofile_register_callback(user):
-    profile = UserProfile(user = user)
-    profile.save()
+        delete_notifications()
 
+    return errors