# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-from patchwork.forms import MultiplePatchForm
-from patchwork.models import Bundle, Project, State
+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.shortcuts import render_to_response, get_object_or_404
+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 = []
class Order(object):
order_map = {
- 'date': 'date',
- 'name': 'name',
- 'state': 'state__ordering',
- 'submitter': 'submitter__name'
+ 'date': 'date',
+ 'name': 'name',
+ 'state': 'state__ordering',
+ 'submitter': 'submitter__name',
+ 'delegate': 'delegate__username',
}
- default_order = 'date'
+ default_order = ('date', True)
- def __init__(self, str = None):
+ 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.default_order
return
reversed = False
reversed = True
if str not in self.order_map.keys():
- self.order = self.default_order
return
self.order = str
else:
return '-' + self.order
- def query(self):
+ def apply(self, qs):
q = self.order_map[self.order]
if self.reversed:
q = '-' + q
- return q
+
+ qs = qs.order_by(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
+ qs = qs.order_by(q)
+
+ return qs
bundle_actions = ['create', 'add', 'remove']
def set_bundle(user, project, action, data, patches, context):
# set up the bundle
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 = data['bundle_name'])
+ name = bundle_name)
bundle.save()
- str = 'added to new bundle "%s"' % bundle.name
- auth_required = False
+ context.add_message("Bundle %s created" % bundle.name)
elif action =='add':
bundle = get_object_or_404(Bundle, id = data['bundle_id'])
- str = 'added to bundle "%s"' % bundle.name
- auth_required = False
elif action =='remove':
bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
- str = 'removed from bundle "%s"' % bundle.name
- auth_required = False
if not bundle:
return ['no such bundle']
for patch in patches:
if action == 'create' or action == 'add':
- bundle.patches.add(patch)
+ bundlepatch_count = BundlePatch.objects.filter(bundle = bundle,
+ patch = patch).count()
+ if bundlepatch_count == 0:
+ bundle.append_patch(patch)
+ context.add_message("Patch '%s' added to bundle %s" % \
+ (patch.name, bundle.name))
+ else:
+ context.add_message("Patch '%s' already in bundle %s" % \
+ (patch.name, bundle.name))
elif action == 'remove':
- bundle.patches.remove(patch)
-
- if len(patches) > 0:
- if len(patches) == 1:
- str = 'patch ' + str
- else:
- str = 'patches ' + str
- context.add_message(str)
+ try:
+ bp = BundlePatch.objects.get(bundle = bundle, patch = patch)
+ bp.delete()
+ context.add_message("Patch '%s' removed from bundle %s\n" % \
+ (patch.name, bundle.name))
+ except Exception:
+ pass
bundle.save()
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 = []
- print "action: ", action
+ for (recipient, notifications) in groups:
+ notifications = list(notifications)
+ projects = set([ n.patch.project.linkname for n in notifications ])
- # 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)
+ def delete_notifications():
+ PatchChangeNotification.objects.filter(
+ pk__in = notifications).delete()
- if not form.is_valid():
- errors = ['The submitted form data was invalid']
- return (errors, form)
+ if EmailOptout.is_optout(recipient.email):
+ delete_notifications()
+ continue
- for patch in patches:
- if not patch.is_editable(user):
- errors.append('You don\'t have permissions to edit the ' + \
- 'patch "%s"' \
- % patch.name)
+ 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()
- elif action == 'ack':
- pass
+ return errors
- elif action == 'archive':
- patch.archived = True
- patch.save()
- str = 'archived'
+def do_expiry():
+ # expire any pending confirmations
+ q = (Q(date__lt = datetime.datetime.now() - EmailConfirmation.validity) |
+ Q(active = False))
+ EmailConfirmation.objects.filter(q).delete()
- elif action == 'unarchive':
- patch.archived = True
- patch.save()
- str = 'un-archived'
+ # 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
+ )
- elif action == 'delete':
- patch.delete()
- str = 'un-archived'
+ # 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)