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]
103 # if we're using a non-default order, add the default as a secondary
104 # ordering. We reverse the default if the primary is reversed.
105 (default_name, default_reverse) = self.default_order
106 if self.order != default_name:
107 q = self.order_map[default_name]
108 if self.reversed ^ default_reverse:
114 bundle_actions = ['create', 'add', 'remove']
115 def set_bundle(user, project, action, data, patches, context):
118 if action == 'create':
119 bundle_name = data['bundle_name'].strip()
120 if '/' in bundle_name:
121 return ['Bundle names can\'t contain slashes']
124 return ['No bundle name was specified']
126 if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0:
127 return ['You already have a bundle called "%s"' % bundle_name]
129 bundle = Bundle(owner = user, project = project,
132 context.add_message("Bundle %s created" % bundle.name)
135 bundle = get_object_or_404(Bundle, id = data['bundle_id'])
137 elif action =='remove':
138 bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
141 return ['no such bundle']
143 for patch in patches:
144 if action == 'create' or action == 'add':
145 bundlepatch_count = BundlePatch.objects.filter(bundle = bundle,
146 patch = patch).count()
147 if bundlepatch_count == 0:
148 bundle.append_patch(patch)
149 context.add_message("Patch '%s' added to bundle %s" % \
150 (patch.name, bundle.name))
152 context.add_message("Patch '%s' already in bundle %s" % \
153 (patch.name, bundle.name))
155 elif action == 'remove':
157 bp = BundlePatch.objects.get(bundle = bundle, patch = patch)
159 context.add_message("Patch '%s' removed from bundle %s\n" % \
160 (patch.name, bundle.name))
168 def send_notifications():
169 date_limit = datetime.datetime.now() - \
170 datetime.timedelta(minutes =
171 settings.NOTIFICATION_DELAY_MINUTES)
173 # This gets funky: we want to filter out any notifications that should
174 # be grouped with other notifications that aren't ready to go out yet. To
175 # do that, we join back onto PatchChangeNotification (PCN -> Patch ->
176 # Person -> Patch -> max(PCN.last_modified)), filtering out any maxima
177 # that are with the date_limit.
178 qs = PatchChangeNotification.objects \
179 .annotate(m = Max('patch__submitter__patch__patchchangenotification'
180 '__last_modified')) \
181 .filter(m__lt = date_limit)
183 groups = itertools.groupby(qs.order_by('patch__submitter'),
184 lambda n: n.patch.submitter)
188 for (recipient, notifications) in groups:
189 notifications = list(notifications)
190 projects = set([ n.patch.project.linkname for n in notifications ])
192 def delete_notifications():
193 PatchChangeNotification.objects.filter(
194 pk__in = notifications).delete()
196 if EmailOptout.is_optout(recipient.email):
197 delete_notifications()
201 'site': Site.objects.get_current(),
203 'notifications': notifications,
204 'projects': projects,
207 subject = render_to_string(
208 'patchwork/patch-change-notification-subject.text',
210 content = render_to_string('patchwork/patch-change-notification.mail',
213 message = EmailMessage(subject = subject, body = content,
214 from_email = settings.NOTIFICATION_FROM_EMAIL,
215 to = [recipient.email],
216 headers = {'Precedence': 'bulk'})
221 errors.append((recipient, ex))
224 delete_notifications()