]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/utils.py
notifications: add project name to patch update notification
[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 query(self):
97         q = self.order_map[self.order]
98         if self.reversed:
99             q = '-' + q
100         return q
101
102 bundle_actions = ['create', 'add', 'remove']
103 def set_bundle(user, project, action, data, patches, context):
104     # set up the bundle
105     bundle = None
106     if action == 'create':
107         bundle_name = data['bundle_name'].strip()
108         if '/' in bundle_name:
109             return ['Bundle names can\'t contain slashes']
110
111         if not bundle_name:
112             return ['No bundle name was specified']
113
114         if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0:
115             return ['You already have a bundle called "%s"' % bundle_name]
116
117         bundle = Bundle(owner = user, project = project,
118                 name = bundle_name)
119         bundle.save()
120         context.add_message("Bundle %s created" % bundle.name)
121
122     elif action =='add':
123         bundle = get_object_or_404(Bundle, id = data['bundle_id'])
124
125     elif action =='remove':
126         bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
127
128     if not bundle:
129         return ['no such bundle']
130
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))
139             else:
140                 context.add_message("Patch '%s' already in bundle %s" % \
141                         (patch.name, bundle.name))
142
143         elif action == 'remove':
144             try:
145                 bp = BundlePatch.objects.get(bundle = bundle, patch = patch)
146                 bp.delete()
147                 context.add_message("Patch '%s' removed from bundle %s\n" % \
148                         (patch.name, bundle.name))
149             except Exception:
150                 pass
151
152     bundle.save()
153
154     return []
155
156 def send_notifications():
157     date_limit = datetime.datetime.now() - \
158                      datetime.timedelta(minutes =
159                                 settings.NOTIFICATION_DELAY_MINUTES)
160
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)
170
171     groups = itertools.groupby(qs.order_by('patch__submitter'),
172                                lambda n: n.patch.submitter)
173
174     errors = []
175
176     for (recipient, notifications) in groups:
177         notifications = list(notifications)
178         projects = set([ n.patch.project.linkname for n in notifications ])
179
180         def delete_notifications():
181             PatchChangeNotification.objects.filter(
182                                 pk__in = notifications).delete()
183
184         if EmailOptout.is_optout(recipient.email):
185             delete_notifications()
186             continue
187
188         context = {
189             'site': Site.objects.get_current(),
190             'person': recipient,
191             'notifications': notifications,
192             'projects': projects,
193         }
194
195         subject = render_to_string(
196                         'patchwork/patch-change-notification-subject.text',
197                         context).strip()
198         content = render_to_string('patchwork/patch-change-notification.mail',
199                                 context)
200
201         message = EmailMessage(subject = subject, body = content,
202                                from_email = settings.NOTIFICATION_FROM_EMAIL,
203                                to = [recipient.email],
204                                headers = {'Precedence': 'bulk'})
205
206         try:
207             message.send()
208         except ex:
209             errors.append((recipient, ex))
210             continue
211
212         delete_notifications()
213
214     return errors