summaryrefslogtreecommitdiff
path: root/todolists
diff options
context:
space:
mode:
Diffstat (limited to 'todolists')
-rw-r--r--todolists/admin.py15
-rw-r--r--todolists/migrations/0001_initial.py61
-rw-r--r--todolists/migrations/__init__.py0
-rw-r--r--todolists/models.py99
-rw-r--r--todolists/templatetags/__init__.py0
-rw-r--r--todolists/templatetags/todolists.py19
-rw-r--r--todolists/urls.py26
-rw-r--r--todolists/utils.py57
-rw-r--r--todolists/views.py327
9 files changed, 476 insertions, 128 deletions
diff --git a/todolists/admin.py b/todolists/admin.py
new file mode 100644
index 00000000..246a8bca
--- /dev/null
+++ b/todolists/admin.py
@@ -0,0 +1,15 @@
+from django.contrib import admin
+
+from .models import Todolist
+
+
+class TodolistAdmin(admin.ModelAdmin):
+ list_display = ('name', 'creator', 'created', 'description')
+ list_filter = ('created', 'creator')
+ search_fields = ('name', 'description')
+ date_hierarchy = 'created'
+
+
+admin.site.register(Todolist, TodolistAdmin)
+
+# vim: set ts=4 sw=4 et:
diff --git a/todolists/migrations/0001_initial.py b/todolists/migrations/0001_initial.py
new file mode 100644
index 00000000..4ffbf838
--- /dev/null
+++ b/todolists/migrations/0001_initial.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.db.models.deletion
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('main', '0001_initial'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Todolist',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('slug', models.SlugField(unique=True, max_length=255)),
+ ('old_id', models.IntegerField(unique=True, null=True)),
+ ('name', models.CharField(max_length=255)),
+ ('description', models.TextField()),
+ ('created', models.DateTimeField(db_index=True)),
+ ('last_modified', models.DateTimeField(editable=False)),
+ ('raw', models.TextField(blank=True)),
+ ('creator', models.ForeignKey(related_name=b'created_todolists', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'get_latest_by': 'created',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TodolistPackage',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('pkgname', models.CharField(max_length=255)),
+ ('pkgbase', models.CharField(max_length=255)),
+ ('created', models.DateTimeField(editable=False)),
+ ('last_modified', models.DateTimeField(editable=False)),
+ ('removed', models.DateTimeField(null=True, blank=True)),
+ ('status', models.SmallIntegerField(default=0, choices=[(0, b'Incomplete'), (1, b'Complete'), (2, b'In-progress')])),
+ ('comments', models.TextField(null=True, blank=True)),
+ ('arch', models.ForeignKey(to='main.Arch')),
+ ('pkg', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to='main.Package', null=True)),
+ ('repo', models.ForeignKey(to='main.Repo')),
+ ('todolist', models.ForeignKey(to='todolists.Todolist')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
+ ],
+ options={
+ 'get_latest_by': 'created',
+ },
+ bases=(models.Model,),
+ ),
+ migrations.AlterUniqueTogether(
+ name='todolistpackage',
+ unique_together=set([('todolist', 'pkgname', 'arch')]),
+ ),
+ ]
diff --git a/todolists/migrations/__init__.py b/todolists/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/todolists/migrations/__init__.py
diff --git a/todolists/models.py b/todolists/models.py
new file mode 100644
index 00000000..92ca5839
--- /dev/null
+++ b/todolists/models.py
@@ -0,0 +1,99 @@
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.db import models
+from django.db.models import Q
+from django.db.models.signals import pre_save
+
+from main.models import Arch, Repo, Package
+from main.utils import set_created_field
+
+
+class TodolistManager(models.Manager):
+ def incomplete(self):
+ not_done = ((Q(todolistpackage__status=TodolistPackage.INCOMPLETE) |
+ Q(todolistpackage__status=TodolistPackage.IN_PROGRESS)) &
+ Q(todolistpackage__removed__isnull=True))
+ return self.order_by().filter(not_done).distinct()
+
+
+class Todolist(models.Model):
+ slug = models.SlugField(max_length=255, unique=True)
+ old_id = models.IntegerField(null=True, unique=True)
+ name = models.CharField(max_length=255)
+ description = models.TextField()
+ creator = models.ForeignKey(User, on_delete=models.PROTECT,
+ related_name="created_todolists")
+ created = models.DateTimeField(db_index=True)
+ last_modified = models.DateTimeField(editable=False)
+ raw = models.TextField(blank=True)
+
+ objects = TodolistManager()
+
+ class Meta:
+ get_latest_by = 'created'
+
+ def __unicode__(self):
+ return self.name
+
+ @property
+ def stripped_description(self):
+ return self.description.strip()
+
+ def get_absolute_url(self):
+ return '/todo/%s/' % self.slug
+
+ def get_full_url(self, proto='https'):
+ '''get a URL suitable for things like email including the domain'''
+ domain = Site.objects.get_current().domain
+ return '%s://%s%s' % (proto, domain, self.get_absolute_url())
+
+ def packages(self):
+ if not hasattr(self, '_packages'):
+ self._packages = self.todolistpackage_set.filter(
+ removed__isnull=True).select_related(
+ 'pkg', 'repo', 'arch', 'user__username').order_by(
+ 'pkgname', 'arch')
+ return self._packages
+
+
+class TodolistPackage(models.Model):
+ INCOMPLETE = 0
+ COMPLETE = 1
+ IN_PROGRESS = 2
+ STATUS_CHOICES = (
+ (INCOMPLETE, 'Incomplete'),
+ (COMPLETE, 'Complete'),
+ (IN_PROGRESS, 'In-progress'),
+ )
+
+ todolist = models.ForeignKey(Todolist)
+ pkg = models.ForeignKey(Package, null=True, on_delete=models.SET_NULL)
+ pkgname = models.CharField(max_length=255)
+ pkgbase = models.CharField(max_length=255)
+ arch = models.ForeignKey(Arch)
+ repo = models.ForeignKey(Repo)
+ created = models.DateTimeField(editable=False)
+ last_modified = models.DateTimeField(editable=False)
+ removed = models.DateTimeField(null=True, blank=True)
+ status = models.SmallIntegerField(default=INCOMPLETE,
+ choices=STATUS_CHOICES)
+ user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
+ comments = models.TextField(null=True, blank=True)
+
+ class Meta:
+ unique_together = (('todolist', 'pkgname', 'arch'),)
+ get_latest_by = 'created'
+
+ def __unicode__(self):
+ return self.pkgname
+
+ def status_css_class(self):
+ return self.get_status_display().lower().replace('-', '')
+
+
+pre_save.connect(set_created_field, sender=Todolist,
+ dispatch_uid="todolists.models")
+pre_save.connect(set_created_field, sender=TodolistPackage,
+ dispatch_uid="todolists.models")
+
+# vim: set ts=4 sw=4 et:
diff --git a/todolists/templatetags/__init__.py b/todolists/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/todolists/templatetags/__init__.py
diff --git a/todolists/templatetags/todolists.py b/todolists/templatetags/todolists.py
new file mode 100644
index 00000000..5f31dc1f
--- /dev/null
+++ b/todolists/templatetags/todolists.py
@@ -0,0 +1,19 @@
+from django import template
+
+register = template.Library()
+
+
+def pkg_absolute_url(repo, arch, pkgname):
+ return '/packages/%s/%s/%s/' % (repo.name.lower(), arch.name, pkgname)
+
+
+@register.simple_tag
+def todopkg_details_link(todopkg):
+ pkg = todopkg.pkg
+ if not pkg:
+ return todopkg.pkgname
+ link = '<a href="%s" title="View package details for %s">%s</a>'
+ url = pkg_absolute_url(todopkg.repo, todopkg.arch, pkg.pkgname)
+ return link % (url, pkg.pkgname, pkg.pkgname)
+
+# vim: set ts=4 sw=4 et:
diff --git a/todolists/urls.py b/todolists/urls.py
new file mode 100644
index 00000000..ed065f50
--- /dev/null
+++ b/todolists/urls.py
@@ -0,0 +1,26 @@
+from django.conf.urls import patterns
+from django.contrib.auth.decorators import permission_required
+
+from .views import (view_redirect, view, add, edit, flag,
+ list_pkgbases, DeleteTodolist, TodolistListView)
+
+urlpatterns = patterns('',
+ (r'^$', TodolistListView.as_view(), {}, 'todolist-list'),
+
+ # old todolists URLs, permanent redirect view so we don't break all links
+ (r'^(?P<old_id>\d+)/$', view_redirect),
+
+ (r'^add/$',
+ permission_required('todolists.add_todolist')(add)),
+ (r'^(?P<slug>[-\w]+)/$', view),
+ (r'^(?P<slug>[-\w]+)/edit/$',
+ permission_required('todolists.change_todolist')(edit)),
+ (r'^(?P<slug>[-\w]+)/delete/$',
+ permission_required('todolists.delete_todolist')(DeleteTodolist.as_view())),
+ (r'^(?P<slug>[-\w]+)/flag/(?P<pkg_id>\d+)/$',
+ permission_required('todolists.change_todolistpackage')(flag)),
+ (r'^(?P<slug>[-\w]+)/pkgbases/(?P<svn_root>[a-z]+)/$',
+ list_pkgbases),
+)
+
+# vim: set ts=4 sw=4 et:
diff --git a/todolists/utils.py b/todolists/utils.py
new file mode 100644
index 00000000..e04c2e5e
--- /dev/null
+++ b/todolists/utils.py
@@ -0,0 +1,57 @@
+from django.db import connections, router
+
+from .models import Todolist, TodolistPackage
+from packages.models import Package
+
+
+def todo_counts():
+ sql = """
+SELECT todolist_id, count(*), SUM(CASE WHEN status = %s THEN 1 ELSE 0 END)
+ FROM todolists_todolistpackage
+ WHERE removed IS NULL
+ GROUP BY todolist_id
+ """
+ database = router.db_for_write(TodolistPackage)
+ connection = connections[database]
+ cursor = connection.cursor()
+ cursor.execute(sql, [TodolistPackage.COMPLETE])
+ results = cursor.fetchall()
+ return {row[0]: (row[1], row[2]) for row in results}
+
+
+def get_annotated_todolists(incomplete_only=False):
+ lists = Todolist.objects.all().defer('raw').select_related(
+ 'creator').order_by('-created')
+ lookup = todo_counts()
+
+ # tag each list with package counts
+ for todolist in lists:
+ counts = lookup.get(todolist.id, (0, 0))
+ todolist.pkg_count = counts[0]
+ todolist.complete_count = counts[1]
+ todolist.incomplete_count = counts[0] - counts[1]
+
+ if incomplete_only:
+ lists = [l for l in lists if l.incomplete_count > 0]
+
+ return lists
+
+
+def attach_staging(packages, list_id):
+ '''Look for any staging version of the packages provided and attach them
+ to the 'staging' attribute on each package if found.'''
+ pkgnames = TodolistPackage.objects.filter(
+ todolist_id=list_id).values('pkgname')
+ staging_pkgs = Package.objects.normal().filter(repo__staging=True,
+ pkgname__in=pkgnames)
+ # now build a lookup dict to attach to the correct package
+ lookup = {(p.pkgname, p.arch): p for p in staging_pkgs}
+
+ annotated = []
+ for package in packages:
+ in_staging = lookup.get((package.pkgname, package.arch), None)
+ package.staging = in_staging
+
+ return annotated
+
+# vim: set ts=4 sw=4 et:
diff --git a/todolists/views.py b/todolists/views.py
index 25186243..a0b56e25 100644
--- a/todolists/views.py
+++ b/todolists/views.py
@@ -1,169 +1,240 @@
-from django import forms
+import json
+from operator import attrgetter
+from django import forms
from django.http import HttpResponse
+from django.conf import settings
from django.core.mail import send_mail
-from django.shortcuts import get_object_or_404, redirect
-from django.contrib.auth.decorators import login_required, permission_required
-from django.db.models import Count
+from django.shortcuts import (get_list_or_404, get_object_or_404,
+ redirect, render)
+from django.db import transaction
from django.views.decorators.cache import never_cache
-from django.views.generic.create_update import delete_object
-from django.views.generic.simple import direct_to_template
+from django.views.generic import DeleteView, ListView
from django.template import Context, loader
-from django.utils import simplejson
+from django.utils.timezone import now
-from main.models import Todolist, TodolistPkg, Package
+from main.models import Package, Repo
+from main.utils import find_unique_slug
+from packages.utils import attach_maintainers
+from .models import Todolist, TodolistPackage
+from .utils import get_annotated_todolists, attach_staging
-class TodoListForm(forms.Form):
- name = forms.CharField(max_length=255,
- widget=forms.TextInput(attrs={'size': '30'}))
- description = forms.CharField(required=False,
- widget=forms.Textarea(attrs={'rows': '4', 'cols': '60'}))
- packages = forms.CharField(required=False,
+
+class TodoListForm(forms.ModelForm):
+ raw = forms.CharField(label='Packages', required=False,
help_text='(one per line)',
widget=forms.Textarea(attrs={'rows': '20', 'cols': '60'}))
- def clean_packages(self):
- package_names = [s.strip() for s in
- self.cleaned_data['packages'].split("\n")]
- package_names = set(package_names)
- packages = Package.objects.filter(
- pkgname__in=package_names).exclude(
- repo__testing=True).order_by('arch')
- return packages
+ def package_names(self):
+ return {s.strip() for s in self.cleaned_data['raw'].split("\n")}
+
+ def packages(self):
+ return Package.objects.normal().filter(
+ pkgname__in=self.package_names(),
+ repo__testing=False, repo__staging=False).order_by('arch')
+
+ class Meta:
+ model = Todolist
+ fields = ('name', 'description', 'raw')
-@login_required
@never_cache
-def flag(request, listid, pkgid):
- list = get_object_or_404(Todolist, id=listid)
- pkg = get_object_or_404(TodolistPkg, id=pkgid)
- pkg.complete = not pkg.complete
- pkg.save()
+def flag(request, slug, pkg_id):
+ todolist = get_object_or_404(Todolist, slug=slug)
+ tlpkg = get_object_or_404(TodolistPackage, id=pkg_id, removed__isnull=True)
+ # TODO: none of this; require absolute value on submit
+ if tlpkg.status == TodolistPackage.INCOMPLETE:
+ tlpkg.status = TodolistPackage.COMPLETE
+ else:
+ tlpkg.status = TodolistPackage.INCOMPLETE
+ tlpkg.user = request.user
+ tlpkg.save(update_fields=('status', 'user', 'last_modified'))
if request.is_ajax():
- return HttpResponse(
- simplejson.dumps({'complete': pkg.complete}),
- mimetype='application/json')
- return redirect(list)
+ data = {
+ 'status': tlpkg.get_status_display(),
+ 'css_class': tlpkg.status_css_class(),
+ }
+ return HttpResponse(json.dumps(data), content_type='application/json')
+ return redirect(todolist)
+
+
+def view_redirect(request, old_id):
+ todolist = get_object_or_404(Todolist, old_id=old_id)
+ return redirect(todolist, permanent=True)
+
+
+def view(request, slug):
+ todolist = get_object_or_404(Todolist, slug=slug)
+ svn_roots = Repo.objects.values_list(
+ 'svn_root', flat=True).order_by().distinct()
+ # we don't hold onto the result, but the objects are the same here,
+ # so accessing maintainers in the template is now cheap
+ attach_maintainers(todolist.packages())
+ attach_staging(todolist.packages(), todolist.pk)
+ arches = {tp.arch for tp in todolist.packages()}
+ repos = {tp.repo for tp in todolist.packages()}
+ context = {
+ 'list': todolist,
+ 'svn_roots': svn_roots,
+ 'arches': sorted(arches),
+ 'repos': sorted(repos),
+ }
+ return render(request, 'todolists/view.html', context)
+
+
+def list_pkgbases(request, slug, svn_root):
+ '''Used to make bulk moves of packages a lot easier.'''
+ todolist = get_object_or_404(Todolist, slug=slug)
+ repos = get_list_or_404(Repo, svn_root=svn_root)
+ pkgbases = TodolistPackage.objects.values_list(
+ 'pkgbase', flat=True).filter(
+ todolist=todolist, repo__in=repos, removed__isnull=True).order_by(
+ 'pkgbase').distinct()
+ return HttpResponse('\n'.join(pkgbases), content_type='text/plain')
+
+
+class TodolistListView(ListView):
+ context_object_name = "lists"
+ template_name = "todolists/list.html"
+ paginate_by = 50
+
+ def get_queryset(self):
+ return get_annotated_todolists()
-@login_required
-@never_cache
-def view(request, listid):
- list = get_object_or_404(Todolist, id=listid)
- return direct_to_template(request, 'todolists/view.html', {'list': list})
-@login_required
-@never_cache
-def list(request):
- lists = Todolist.objects.select_related('creator').annotate(
- pkg_count=Count('todolistpkg')).order_by('-date_added')
- incomplete = Todolist.objects.filter(todolistpkg__complete=False).annotate(
- Count('todolistpkg')).values_list('id', 'todolistpkg__count')
-
- # tag each list with an incomplete package count
- lookup = {}
- for k, v in incomplete:
- lookup[k] = v
- for l in lists:
- l.incomplete_count = lookup.get(l.id, 0)
-
- return direct_to_template(request, 'todolists/list.html', {'lists': lists})
-
-@permission_required('main.add_todolist')
@never_cache
def add(request):
if request.POST:
form = TodoListForm(request.POST)
if form.is_valid():
- todo = Todolist.objects.create(
- creator = request.user,
- name = form.cleaned_data['name'],
- description = form.cleaned_data['description'])
-
- for pkg in form.cleaned_data['packages']:
- tpkg = TodolistPkg.objects.create(list = todo, pkg = pkg)
- send_todolist_email(tpkg)
-
- return redirect('/todo/')
+ new_packages = create_todolist_packages(form, creator=request.user)
+ send_todolist_emails(form.instance, new_packages)
+ return redirect(form.instance)
else:
form = TodoListForm()
page_dict = {
'title': 'Add Todo List',
+ 'description': '',
'form': form,
'submit_text': 'Create List'
- }
- return direct_to_template(request, 'general_form.html', page_dict)
+ }
+ return render(request, 'general_form.html', page_dict)
-@permission_required('main.change_todolist')
+
+# TODO: this calls for transaction management and async emailing
@never_cache
-def edit(request, list_id):
- todo_list = get_object_or_404(Todolist, id=list_id)
+def edit(request, slug):
+ todo_list = get_object_or_404(Todolist, slug=slug)
if request.POST:
- form = TodoListForm(request.POST)
+ form = TodoListForm(request.POST, instance=todo_list)
if form.is_valid():
- todo_list.name = form.cleaned_data['name']
- todo_list.description = form.cleaned_data['description']
- todo_list.save()
-
- packages = [p.pkg for p in todo_list.packages]
-
- # first delete any packages not in the new list
- for p in todo_list.packages:
- if p.pkg not in form.cleaned_data['packages']:
- p.delete()
-
- # now add any packages not in the old list
- for pkg in form.cleaned_data['packages']:
- if pkg not in packages:
- tpkg = TodolistPkg.objects.create(
- list = todo_list, pkg = pkg)
- send_todolist_email(tpkg)
-
+ new_packages = create_todolist_packages(form)
+ send_todolist_emails(todo_list, new_packages)
return redirect(todo_list)
else:
- form = TodoListForm(initial={
- 'name': todo_list.name,
- 'description': todo_list.description,
- 'packages': todo_list.package_names,
- })
+ form = TodoListForm(instance=todo_list,
+ initial={'packages': todo_list.raw})
+
page_dict = {
'title': 'Edit Todo List: %s' % todo_list.name,
+ 'description': '',
'form': form,
'submit_text': 'Save List'
- }
- return direct_to_template(request, 'general_form.html', page_dict)
-
-@permission_required('main.delete_todolist')
-@never_cache
-def delete_todolist(request, object_id):
- return delete_object(request, object_id=object_id, model=Todolist,
- template_name="todolists/todolist_confirm_delete.html",
- post_delete_redirect='/todo/')
-
-def send_todolist_email(todo):
- '''Sends an e-mail to the maintainer of a package notifying them that the
- package has been added to a todo list'''
- maints = todo.pkg.maintainers
- if not maints:
- return
-
- page_dict = {
- 'pkg': todo.pkg,
- 'todolist': todo.list,
- 'weburl': todo.pkg.get_full_url()
}
- t = loader.get_template('todolists/email_notification.txt')
- c = Context(page_dict)
- send_mail('arch: Package [%s] added to Todolist' % todo.pkg.pkgname,
- t.render(c),
- 'Arch Website Notification <nobody@archlinux.org>',
- [m.email for m in maints],
- fail_silently=True)
-
-def public_list(request):
- todo_lists = Todolist.objects.incomplete()
- return direct_to_template(request, "todolists/public_list.html",
- {"todo_lists": todo_lists})
-
+ return render(request, 'general_form.html', page_dict)
+
+
+class DeleteTodolist(DeleteView):
+ model = Todolist
+ # model in main == assumes name 'main/todolist_confirm_delete.html'
+ template_name = 'todolists/todolist_confirm_delete.html'
+ success_url = '/todo/'
+
+
+@transaction.atomic
+def create_todolist_packages(form, creator=None):
+ package_names = form.package_names()
+ packages = form.packages()
+ timestamp = now()
+ if creator:
+ # todo list is new, populate creator and slug fields
+ todolist = form.save(commit=False)
+ todolist.creator = creator
+ todolist.slug = find_unique_slug(Todolist, todolist.name)
+ todolist.save()
+ else:
+ # todo list already existed
+ form.save()
+ todolist = form.instance
+
+ # first mark removed any packages not in the new list
+ to_remove = set()
+ for todo_pkg in todolist.packages():
+ if todo_pkg.pkg and todo_pkg.pkg not in packages:
+ to_remove.add(todo_pkg.pk)
+ elif todo_pkg.pkgname not in package_names:
+ to_remove.add(todo_pkg.pk)
+
+ TodolistPackage.objects.filter(
+ pk__in=to_remove).update(removed=timestamp)
+
+ # Add (or mark unremoved) any packages in the new packages list
+ todo_pkgs = []
+ for package in packages:
+ # ensure get_or_create uses the fields in our unique constraint
+ defaults = {
+ 'pkg': package,
+ 'pkgbase': package.pkgbase,
+ 'repo': package.repo,
+ }
+ todo_pkg, created = TodolistPackage.objects.get_or_create(
+ todolist=todolist,
+ pkgname=package.pkgname,
+ arch=package.arch,
+ defaults=defaults)
+ if created:
+ todo_pkgs.append(todo_pkg)
+ else:
+ save = False
+ if todo_pkg.removed is not None:
+ todo_pkg.removed = None
+ save = True
+ if todo_pkg.pkg != package:
+ todo_pkg.pkg = package
+ save = True
+ if save:
+ todo_pkg.save()
+
+ return todo_pkgs
+
+
+def send_todolist_emails(todo_list, new_packages):
+ '''Sends emails to package maintainers notifying them that packages have
+ been added to a todo list.'''
+ # start by flipping the incoming list on its head: we want a list of
+ # involved maintainers and the packages they need to be notified about.
+ orphan_packages = []
+ maint_packages = {}
+ for todo_package in new_packages:
+ maints = todo_package.pkg.maintainers.values_list('email', flat=True)
+ if not maints:
+ orphan_packages.append(todo_package)
+ else:
+ for maint in maints:
+ maint_packages.setdefault(maint, []).append(todo_package)
+
+ for maint, packages in maint_packages.iteritems():
+ packages = sorted(packages, key=attrgetter('pkgname', 'arch'))
+ ctx = Context({
+ 'todo_packages': packages,
+ 'todolist': todo_list,
+ })
+ template = loader.get_template('todolists/email_notification.txt')
+ send_mail('Packages added to todo list \'%s\'' % todo_list.name,
+ template.render(ctx),
+ settings.BRANDING_EMAIL,
+ [maint],
+ fail_silently=True)
# vim: set ts=4 sw=4 et: