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