]> git.ozlabs.org Git - patchwork/blob - patchwork/utils.py
parsemail: Don't catch all exceptions when a Project isn't found
[patchwork] / patchwork / utils.py
1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
3 #
4 # This file is part of the Patchwork package.
5 #
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.
10 #
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.
15 #
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
19
20
21 import itertools
22 import datetime
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
33
34 def get_patch_ids(d, prefix = 'patch_id'):
35     ids = []
36
37     for (k, v) in d.items():
38         a = k.split(':')
39         if len(a) != 2:
40             continue
41         if a[0] != prefix:
42             continue
43         if not v:
44             continue
45         ids.append(a[1])
46
47     return ids
48
49 class Order(object):
50     order_map = {
51         'date':         'date',
52         'name':         'name',
53         'state':        'state__ordering',
54         'submitter':    'submitter__name',
55         'delegate':     'delegate__username',
56     }
57     default_order = ('date', True)
58
59     def __init__(self, str = None, editable = False):
60         self.reversed = False
61         self.editable = editable
62         (self.order, self.reversed) = self.default_order
63
64         if self.editable:
65             return
66
67         if str is None or str == '':
68             return
69
70         reversed = False
71         if str[0] == '-':
72             str = str[1:]
73             reversed = True
74
75         if str not in self.order_map.keys():
76             return
77
78         self.order = str
79         self.reversed = reversed
80
81     def __str__(self):
82         str = self.order
83         if self.reversed:
84             str = '-' + str
85         return str
86
87     def name(self):
88         return self.order
89
90     def reversed_name(self):
91         if self.reversed:
92             return self.order
93         else:
94             return '-' + self.order
95
96     def apply(self, qs):
97         q = self.order_map[self.order]
98         if self.reversed:
99             q = '-' + q
100
101         orders = [q]
102
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:
109                 q = '-' + q
110             orders.append(q)
111
112         return qs.order_by(*orders)
113
114 bundle_actions = ['create', 'add', 'remove']
115 def set_bundle(user, project, action, data, patches, context):
116     # set up the bundle
117     bundle = None
118     if action == 'create':
119         bundle_name = data['bundle_name'].strip()
120         if '/' in bundle_name:
121             return ['Bundle names can\'t contain slashes']
122
123         if not bundle_name:
124             return ['No bundle name was specified']
125
126         if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0:
127             return ['You already have a bundle called "%s"' % bundle_name]
128
129         bundle = Bundle(owner = user, project = project,
130                 name = bundle_name)
131         bundle.save()
132         context.add_message("Bundle %s created" % bundle.name)
133
134     elif action =='add':
135         bundle = get_object_or_404(Bundle, id = data['bundle_id'])
136
137     elif action =='remove':
138         bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
139
140     if not bundle:
141         return ['no such bundle']
142
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))
151             else:
152                 context.add_message("Patch '%s' already in bundle %s" % \
153                         (patch.name, bundle.name))
154
155         elif action == 'remove':
156             try:
157                 bp = BundlePatch.objects.get(bundle = bundle, patch = patch)
158                 bp.delete()
159                 context.add_message("Patch '%s' removed from bundle %s\n" % \
160                         (patch.name, bundle.name))
161             except Exception:
162                 pass
163
164     bundle.save()
165
166     return []
167
168 def send_notifications():
169     date_limit = datetime.datetime.now() - \
170                      datetime.timedelta(minutes =
171                                 settings.NOTIFICATION_DELAY_MINUTES)
172
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)
182
183     groups = itertools.groupby(qs.order_by('patch__submitter'),
184                                lambda n: n.patch.submitter)
185
186     errors = []
187
188     for (recipient, notifications) in groups:
189         notifications = list(notifications)
190         projects = set([ n.patch.project.linkname for n in notifications ])
191
192         def delete_notifications():
193             pks = [ n.pk for n in notifications ]
194             PatchChangeNotification.objects.filter(pk__in = pks).delete()
195
196         if EmailOptout.is_optout(recipient.email):
197             delete_notifications()
198             continue
199
200         context = {
201             'site': Site.objects.get_current(),
202             'person': recipient,
203             'notifications': notifications,
204             'projects': projects,
205         }
206
207         subject = render_to_string(
208                         'patchwork/patch-change-notification-subject.text',
209                         context).strip()
210         content = render_to_string('patchwork/patch-change-notification.mail',
211                                 context)
212
213         message = EmailMessage(subject = subject, body = content,
214                                from_email = settings.NOTIFICATION_FROM_EMAIL,
215                                to = [recipient.email],
216                                headers = {'Precedence': 'bulk'})
217
218         try:
219             message.send()
220         except ex:
221             errors.append((recipient, ex))
222             continue
223
224         delete_notifications()
225
226     return errors
227
228 def do_expiry():
229     # expire any pending confirmations
230     q = (Q(date__lt = datetime.datetime.now() - EmailConfirmation.validity) |
231             Q(active = False))
232     EmailConfirmation.objects.filter(q).delete()
233
234     # expire inactive users with no pending confirmation
235     pending_confs = EmailConfirmation.objects.values('user')
236     users = User.objects.filter(
237                 is_active = False,
238                 last_login = F('date_joined')
239             ).exclude(
240                 id__in = pending_confs
241             )
242
243     # delete users
244     users.delete()
245
246
247