X-Git-Url: http://git.ozlabs.org/?a=blobdiff_plain;f=apps%2Fpatchwork%2Futils.py;h=9ed9e41e35f60affafe0e791ed01d978a6c271ad;hb=f0ad2c6a249c0ee3a4b356e10033ea0041ecbea4;hp=5df6404dafeb594c37bc2a815b39c8d5b53d2d1f;hpb=e653db155cdb671d6ab1c52492fd37b9f80cb805;p=patchwork diff --git a/apps/patchwork/utils.py b/apps/patchwork/utils.py index 5df6404..9ed9e41 100644 --- a/apps/patchwork/utils.py +++ b/apps/patchwork/utils.py @@ -18,9 +18,19 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from patchwork.forms import MultiplePatchForm -from patchwork.models import Bundle, Project, BundlePatch, UserProfile +import itertools +import datetime from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.conf import settings +from django.core.mail import EmailMessage +from django.db.models import Max, Q, F +from django.db.utils import IntegrityError +from patchwork.forms import MultiplePatchForm +from patchwork.models import Bundle, Project, BundlePatch, UserProfile, \ + PatchChangeNotification, EmailOptout, EmailConfirmation def get_patch_ids(d, prefix = 'patch_id'): ids = [] @@ -50,12 +60,12 @@ class Order(object): def __init__(self, str = None, editable = False): self.reversed = False self.editable = editable + (self.order, self.reversed) = self.default_order if self.editable: return if str is None or str == '': - (self.order, self.reversed) = self.default_order return reversed = False @@ -64,7 +74,6 @@ class Order(object): reversed = True if str not in self.order_map.keys(): - (self.order, self.reversed) = self.default_order return self.order = str @@ -85,11 +94,23 @@ class Order(object): else: return '-' + self.order - def query(self): + def apply(self, qs): q = self.order_map[self.order] if self.reversed: q = '-' + q - return q + + orders = [q] + + # if we're using a non-default order, add the default as a secondary + # ordering. We reverse the default if the primary is reversed. + (default_name, default_reverse) = self.default_order + if self.order != default_name: + q = self.order_map[default_name] + if self.reversed ^ default_reverse: + q = '-' + q + orders.append(q) + + return qs.order_by(*orders) bundle_actions = ['create', 'add', 'remove'] def set_bundle(user, project, action, data, patches, context): @@ -97,9 +118,15 @@ def set_bundle(user, project, action, data, patches, context): bundle = None if action == 'create': bundle_name = data['bundle_name'].strip() + if '/' in bundle_name: + return ['Bundle names can\'t contain slashes'] + if not bundle_name: return ['No bundle name was specified'] + if Bundle.objects.filter(owner = user, name = bundle_name).count() > 0: + return ['You already have a bundle called "%s"' % bundle_name] + bundle = Bundle(owner = user, project = project, name = bundle_name) bundle.save() @@ -139,46 +166,83 @@ def set_bundle(user, project, action, data, patches, context): return [] +def send_notifications(): + date_limit = datetime.datetime.now() - \ + datetime.timedelta(minutes = + settings.NOTIFICATION_DELAY_MINUTES) -def set_patches(user, project, action, data, patches, context): - errors = [] - form = MultiplePatchForm(project = project, data = data) + # This gets funky: we want to filter out any notifications that should + # be grouped with other notifications that aren't ready to go out yet. To + # do that, we join back onto PatchChangeNotification (PCN -> Patch -> + # Person -> Patch -> max(PCN.last_modified)), filtering out any maxima + # that are with the date_limit. + qs = PatchChangeNotification.objects \ + .annotate(m = Max('patch__submitter__patch__patchchangenotification' + '__last_modified')) \ + .filter(m__lt = date_limit) - try: - project = Project.objects.get(id = data['project']) - except: - errors = ['No such project'] - return (errors, form) + groups = itertools.groupby(qs.order_by('patch__submitter'), + lambda n: n.patch.submitter) - str = '' + errors = [] - # this may be a bundle action, which doesn't modify a patch. in this - # case, don't require a valid form, or patch editing permissions - if action in bundle_actions: - errors = set_bundle(user, project, action, data, patches, context) - return (errors, form) + for (recipient, notifications) in groups: + notifications = list(notifications) + projects = set([ n.patch.project.linkname for n in notifications ]) - if not form.is_valid(): - errors = ['The submitted form data was invalid'] - return (errors, form) + def delete_notifications(): + pks = [ n.pk for n in notifications ] + PatchChangeNotification.objects.filter(pk__in = pks).delete() - for patch in patches: - if not patch.is_editable(user): - errors.append('You don\'t have permissions to edit the ' + \ - 'patch "%s"' \ - % patch.name) + if EmailOptout.is_optout(recipient.email): + delete_notifications() + continue + + context = { + 'site': Site.objects.get_current(), + 'person': recipient, + 'notifications': notifications, + 'projects': projects, + } + + subject = render_to_string( + 'patchwork/patch-change-notification-subject.text', + context).strip() + content = render_to_string('patchwork/patch-change-notification.mail', + context) + + message = EmailMessage(subject = subject, body = content, + from_email = settings.NOTIFICATION_FROM_EMAIL, + to = [recipient.email], + headers = {'Precedence': 'bulk'}) + + try: + message.send() + except ex: + errors.append((recipient, ex)) continue - if action == 'update': - form.save(patch) - str = 'updated' + delete_notifications() + + return errors + +def do_expiry(): + # expire any pending confirmations + q = (Q(date__lt = datetime.datetime.now() - EmailConfirmation.validity) | + Q(active = False)) + EmailConfirmation.objects.filter(q).delete() + + # expire inactive users with no pending confirmation + pending_confs = EmailConfirmation.objects.values('user') + users = User.objects.filter( + is_active = False, + last_login = F('date_joined') + ).exclude( + id__in = pending_confs + ) + + # delete users + users.delete() - if len(patches) > 0: - if len(patches) == 1: - str = 'patch ' + str - else: - str = 'patches ' + str - context.add_message(str) - return (errors, form)