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.auth.models import User
26 from django.contrib.sites.models import Site
27 from django.conf import settings
28 from django.core.mail import EmailMessage
29 from django.db.models import Max, Q, F
30 from django.db.utils import IntegrityError
31 from patchwork.models import Bundle, Project, BundlePatch, UserProfile, \
32 PatchChangeNotification, EmailOptout, EmailConfirmation
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:
112 return qs.order_by(*orders)
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 pks = [ n.pk for n in notifications ]
194 PatchChangeNotification.objects.filter(pk__in = pks).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()
229 # expire any pending confirmations
230 q = (Q(date__lt = datetime.datetime.now() - EmailConfirmation.validity) |
232 EmailConfirmation.objects.filter(q).delete()
234 # expire inactive users with no pending confirmation
235 pending_confs = EmailConfirmation.objects.values('user')
236 users = User.objects.filter(
238 last_login = F('date_joined')
240 id__in = pending_confs