1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
4 # This file is part of the Patchwork package.
6 # Patchwork is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # Patchwork is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Patchwork; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 from django.shortcuts import get_object_or_404
24 from django.template.loader import render_to_string
25 from django.contrib.sites.models import Site
26 from django.conf import settings
27 from django.core.mail import EmailMessage
28 from django.db.models import Max
29 from django.db.utils import IntegrityError
30 from patchwork.forms import MultiplePatchForm
31 from patchwork.models import Bundle, Project, BundlePatch, UserProfile, \
32 PatchChangeNotification, EmailOptout
34 def get_patch_ids(d, prefix = 'patch_id'):
37 for (k, v) in d.items():
53 'state': 'state__ordering',
54 'submitter': 'submitter__name',
55 'delegate': 'delegate__username',
57 default_order = ('date', True)
59 def __init__(self, str = None, editable = False):
61 self.editable = editable
62 (self.order, self.reversed) = self.default_order
67 if str is None or str == '':
75 if str not in self.order_map.keys():
79 self.reversed = reversed
90 def reversed_name(self):
94 return '-' + self.order
97 q = self.order_map[self.order]
102 bundle_actions = ['create', 'add', 'remove']
103 def set_bundle(user, project, action, data, patches, context):
106 if action == 'create':
107 bundle_name = data['bundle_name'].strip()
108 if '/' in bundle_name:
109 return ['Bundle names can\'t contain slashes']
112 return ['No bundle name was specified']
114 if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0:
115 return ['You already have a bundle called "%s"' % bundle_name]
117 bundle = Bundle(owner = user, project = project,
120 context.add_message("Bundle %s created" % bundle.name)
123 bundle = get_object_or_404(Bundle, id = data['bundle_id'])
125 elif action =='remove':
126 bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
129 return ['no such bundle']
131 for patch in patches:
132 if action == 'create' or action == 'add':
133 bundlepatch_count = BundlePatch.objects.filter(bundle = bundle,
134 patch = patch).count()
135 if bundlepatch_count == 0:
136 bundle.append_patch(patch)
137 context.add_message("Patch '%s' added to bundle %s" % \
138 (patch.name, bundle.name))
140 context.add_message("Patch '%s' already in bundle %s" % \
141 (patch.name, bundle.name))
143 elif action == 'remove':
145 bp = BundlePatch.objects.get(bundle = bundle, patch = patch)
147 context.add_message("Patch '%s' removed from bundle %s\n" % \
148 (patch.name, bundle.name))
156 def send_notifications():
157 date_limit = datetime.datetime.now() - \
158 datetime.timedelta(minutes =
159 settings.NOTIFICATION_DELAY_MINUTES)
161 # This gets funky: we want to filter out any notifications that should
162 # be grouped with other notifications that aren't ready to go out yet. To
163 # do that, we join back onto PatchChangeNotification (PCN -> Patch ->
164 # Person -> Patch -> max(PCN.last_modified)), filtering out any maxima
165 # that are with the date_limit.
166 qs = PatchChangeNotification.objects \
167 .annotate(m = Max('patch__submitter__patch__patchchangenotification'
168 '__last_modified')) \
169 .filter(m__lt = date_limit)
171 groups = itertools.groupby(qs.order_by('patch__submitter'),
172 lambda n: n.patch.submitter)
176 for (recipient, notifications) in groups:
177 notifications = list(notifications)
179 def delete_notifications():
180 PatchChangeNotification.objects.filter(
181 pk__in = notifications).delete()
183 if EmailOptout.is_optout(recipient.email):
184 delete_notifications()
188 'site': Site.objects.get_current(),
190 'notifications': notifications,
192 subject = render_to_string(
193 'patchwork/patch-change-notification-subject.text',
195 content = render_to_string('patchwork/patch-change-notification.mail',
198 message = EmailMessage(subject = subject, body = content,
199 from_email = settings.NOTIFICATION_FROM_EMAIL,
200 to = [recipient.email],
201 headers = {'Precedence': 'bulk'})
206 errors.append((recipient, ex))
209 delete_notifications()