]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/utils.py
37b85ce1f503396c3363d2ad3f59c70315471134
[patchwork] / apps / 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.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
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         qs = qs.order_by(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             qs = qs.order_by(q)
111
112         return qs
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             PatchChangeNotification.objects.filter(
194                                 pk__in = notifications).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