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.forms import MultiplePatchForm
32 from patchwork.models import Bundle, Project, BundlePatch, UserProfile, \
33 PatchChangeNotification, EmailOptout, EmailConfirmation
35 def get_patch_ids(d, prefix = 'patch_id'):
38 for (k, v) in d.items():
54 'state': 'state__ordering',
55 'submitter': 'submitter__name',
56 'delegate': 'delegate__username',
58 default_order = ('date', True)
60 def __init__(self, str = None, editable = False):
62 self.editable = editable
63 (self.order, self.reversed) = self.default_order
68 if str is None or str == '':
76 if str not in self.order_map.keys():
80 self.reversed = reversed
91 def reversed_name(self):
95 return '-' + self.order
98 q = self.order_map[self.order]
104 # if we're using a non-default order, add the default as a secondary
105 # ordering. We reverse the default if the primary is reversed.
106 (default_name, default_reverse) = self.default_order
107 if self.order != default_name:
108 q = self.order_map[default_name]
109 if self.reversed ^ default_reverse:
113 return qs.order_by(*orders)
115 bundle_actions = ['create', 'add', 'remove']
116 def set_bundle(user, project, action, data, patches, context):
119 if action == 'create':
120 bundle_name = data['bundle_name'].strip()
121 if '/' in bundle_name:
122 return ['Bundle names can\'t contain slashes']
125 return ['No bundle name was specified']
127 if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0:
128 return ['You already have a bundle called "%s"' % bundle_name]
130 bundle = Bundle(owner = user, project = project,
133 context.add_message("Bundle %s created" % bundle.name)
136 bundle = get_object_or_404(Bundle, id = data['bundle_id'])
138 elif action =='remove':
139 bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
142 return ['no such bundle']
144 for patch in patches:
145 if action == 'create' or action == 'add':
146 bundlepatch_count = BundlePatch.objects.filter(bundle = bundle,
147 patch = patch).count()
148 if bundlepatch_count == 0:
149 bundle.append_patch(patch)
150 context.add_message("Patch '%s' added to bundle %s" % \
151 (patch.name, bundle.name))
153 context.add_message("Patch '%s' already in bundle %s" % \
154 (patch.name, bundle.name))
156 elif action == 'remove':
158 bp = BundlePatch.objects.get(bundle = bundle, patch = patch)
160 context.add_message("Patch '%s' removed from bundle %s\n" % \
161 (patch.name, bundle.name))
169 def send_notifications():
170 date_limit = datetime.datetime.now() - \
171 datetime.timedelta(minutes =
172 settings.NOTIFICATION_DELAY_MINUTES)
174 # This gets funky: we want to filter out any notifications that should
175 # be grouped with other notifications that aren't ready to go out yet. To
176 # do that, we join back onto PatchChangeNotification (PCN -> Patch ->
177 # Person -> Patch -> max(PCN.last_modified)), filtering out any maxima
178 # that are with the date_limit.
179 qs = PatchChangeNotification.objects \
180 .annotate(m = Max('patch__submitter__patch__patchchangenotification'
181 '__last_modified')) \
182 .filter(m__lt = date_limit)
184 groups = itertools.groupby(qs.order_by('patch__submitter'),
185 lambda n: n.patch.submitter)
189 for (recipient, notifications) in groups:
190 notifications = list(notifications)
191 projects = set([ n.patch.project.linkname for n in notifications ])
193 def delete_notifications():
194 pks = [ n.pk for n in notifications ]
195 PatchChangeNotification.objects.filter(pk__in = pks).delete()
197 if EmailOptout.is_optout(recipient.email):
198 delete_notifications()
202 'site': Site.objects.get_current(),
204 'notifications': notifications,
205 'projects': projects,
208 subject = render_to_string(
209 'patchwork/patch-change-notification-subject.text',
211 content = render_to_string('patchwork/patch-change-notification.mail',
214 message = EmailMessage(subject = subject, body = content,
215 from_email = settings.NOTIFICATION_FROM_EMAIL,
216 to = [recipient.email],
217 headers = {'Precedence': 'bulk'})
222 errors.append((recipient, ex))
225 delete_notifications()
230 # expire any pending confirmations
231 q = (Q(date__lt = datetime.datetime.now() - EmailConfirmation.validity) |
233 EmailConfirmation.objects.filter(q).delete()
235 # expire inactive users with no pending confirmation
236 pending_confs = EmailConfirmation.objects.values('user')
237 users = User.objects.filter(
239 last_login = F('date_joined')
241 id__in = pending_confs