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)
178 projects = set([ n.patch.project.linkname for n in notifications ])
180 def delete_notifications():
181 PatchChangeNotification.objects.filter(
182 pk__in = notifications).delete()
184 if EmailOptout.is_optout(recipient.email):
185 delete_notifications()
189 'site': Site.objects.get_current(),
191 'notifications': notifications,
192 'projects': projects,
195 subject = render_to_string(
196 'patchwork/patch-change-notification-subject.text',
198 content = render_to_string('patchwork/patch-change-notification.mail',
201 message = EmailMessage(subject = subject, body = content,
202 from_email = settings.NOTIFICATION_FROM_EMAIL,
203 to = [recipient.email],
204 headers = {'Precedence': 'bulk'})
209 errors.append((recipient, ex))
212 delete_notifications()