=== modified file 'dashboard_app/extension.py'
--- dashboard_app/extension.py	2012-09-03 00:09:09 +0000
+++ dashboard_app/extension.py	2012-10-01 08:08:51 +0000
@@ -45,8 +45,8 @@
             Menu("Tests", reverse("dashboard_app.views.test_list")),
             Menu("Data Views", reverse("dashboard_app.views.data_view_list")),
             Menu("Reports", reverse("dashboard_app.views.report_list")),
-            Menu("Image Reports", reverse("dashboard_app.views.image_report_list")),
-            Menu("[BETA] Filters", reverse("dashboard_app.views.filters_list")),
+            Menu("Image Reports", reverse("dashboard_app.views.images.image_report_list")),
+            Menu("[BETA] Filters", reverse("dashboard_app.views.filters.views.filters_list")),
             ]
         return menu
 

=== modified file 'dashboard_app/models.py'
--- dashboard_app/models.py	2012-09-25 23:31:26 +0000
+++ dashboard_app/models.py	2012-10-01 08:08:51 +0000
@@ -1444,7 +1444,7 @@
 
     @models.permalink
     def get_absolute_url(self):
-        return ("dashboard_app.views.image_report_detail", (), dict(name=self.name))
+        return ("dashboard_app.views.images.image_report_detail", (), dict(name=self.name))
 
 
 class ImageSet(models.Model):
@@ -1878,7 +1878,7 @@
     @models.permalink
     def get_absolute_url(self):
         return (
-            "dashboard_app.views.filter_detail",
+            "dashboard_app.views.filters.views.filter_detail",
             [self.owner.username, self.name])
 
 

=== modified file 'dashboard_app/templates/dashboard_app/filter_detail.html'
--- dashboard_app/templates/dashboard_app/filter_detail.html	2012-09-03 00:09:09 +0000
+++ dashboard_app/templates/dashboard_app/filter_detail.html	2012-10-01 03:34:07 +0000
@@ -17,11 +17,11 @@
 
 {% if subscription %}
 <p>
-  <a href="{% url dashboard_app.views.filter_subscribe username=filter.owner.username name=filter.name %}">Manage</a> your subscription to this filter.
+  <a href="{% url dashboard_app.views.filters.views.filter_subscribe username=filter.owner.username name=filter.name %}">Manage</a> your subscription to this filter.
 </p>
 {% else %}
 <p>
-  <a href="{% url dashboard_app.views.filter_subscribe username=filter.owner.username name=filter.name %}">Subscribe</a> to this filter.
+  <a href="{% url dashboard_app.views.filters.views.filter_subscribe username=filter.owner.username name=filter.name %}">Subscribe</a> to this filter.
 </p>
 {% endif %}
 

=== modified file 'dashboard_app/templates/dashboard_app/filters_list.html'
--- dashboard_app/templates/dashboard_app/filters_list.html	2012-09-03 00:09:09 +0000
+++ dashboard_app/templates/dashboard_app/filters_list.html	2012-10-01 03:34:07 +0000
@@ -16,7 +16,7 @@
 {% render_table user_filters_table %}
 
 <p>
-  <a href="{% url dashboard_app.views.filter_add %}">Add new filter…</a>
+  <a href="{% url dashboard_app.views.filters.views.filter_add %}">Add new filter…</a>
 </p>
 
 {% else %}

=== modified file 'dashboard_app/templates/dashboard_app/image-report.html'
--- dashboard_app/templates/dashboard_app/image-report.html	2012-09-25 23:31:26 +0000
+++ dashboard_app/templates/dashboard_app/image-report.html	2012-10-01 08:08:51 +0000
@@ -83,7 +83,7 @@
 </table>
 
 <form method="POST"
-      action="{% url dashboard_app.views.link_bug_to_testrun %}"
+      action="{% url dashboard_app.views.images.link_bug_to_testrun %}"
       id="add-bug-dialog" style="display: none">
   {% csrf_token %}
   <input type="hidden" name="back" value="{{ request.path }}"/>
@@ -95,7 +95,7 @@
 </form>
 
 <form method="POST"
-      action="{% url dashboard_app.views.unlink_bug_and_testrun %}"
+      action="{% url dashboard_app.views.images.unlink_bug_and_testrun %}"
       id="go-to-bug-dialog" style="display: none">
   {% csrf_token %}
   <input type="hidden" name="back" value="{{ request.path }}"/>

=== modified file 'dashboard_app/urls.py'
--- dashboard_app/urls.py	2012-08-23 22:50:32 +0000
+++ dashboard_app/urls.py	2012-10-01 08:08:51 +0000
@@ -35,17 +35,17 @@
     url(r'^reports/(?P<name>[a-zA-Z0-9-_]+)/$', 'report_detail'),
     url(r'^tests/$', 'test_list'),
     url(r'^tests/(?P<test_id>[^/]+)/$', 'test_detail'),
-    url(r'^filters/$', 'filters_list'),
-    url(r'^filters/\+add$', 'filter_add'),
-    url(r'^filters/\+add-preview-json$', 'filter_preview_json'),
-    url(r'^filters/\+add-cases-for-test-json$', 'filter_add_cases_for_test_json'),
-    url(r'^filters/\+attribute-name-completion-json$', 'filter_attr_name_completion_json'),
-    url(r'^filters/\+attribute-value-completion-json$', 'filter_attr_value_completion_json'),
-    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)$', 'filter_detail'),
-    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/json$', 'filter_json'),
-    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'filter_edit'),
-    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+subscribe$', 'filter_subscribe'),
-    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+delete$', 'filter_delete'),
+    url(r'^filters/$', 'filters.views.filters_list'),
+    url(r'^filters/\+add$', 'filters.views.filter_add'),
+    url(r'^filters/\+add-preview-json$', 'filters.views.filter_preview_json'),
+    url(r'^filters/\+add-cases-for-test-json$', 'filters.views.filter_add_cases_for_test_json'),
+    url(r'^filters/\+attribute-name-completion-json$', 'filters.views.filter_attr_name_completion_json'),
+    url(r'^filters/\+attribute-value-completion-json$', 'filters.views.filter_attr_value_completion_json'),
+    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)$', 'filters.views.filter_detail'),
+    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/json$', 'filters.views.filter_json'),
+    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+edit$', 'filters.views.filter_edit'),
+    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+subscribe$', 'filters.views.filter_subscribe'),
+    url(r'^filters/~(?P<username>[a-zA-Z0-9-_]+)/(?P<name>[a-zA-Z0-9-_]+)/\+delete$', 'filters.views.filter_delete'),
     url(r'^xml-rpc/$', linaro_django_xmlrpc.views.handler, 
         name='dashboard_app.views.dashboard_xml_rpc_handler',
         kwargs={
@@ -81,8 +81,8 @@
     url(r'^efforts/(?P<pk>[0-9]+)/$', 'testing_effort_detail'),
     url(r'^efforts/(?P<pk>[0-9]+)/update/$', 'testing_effort_update'),
     url(r'^efforts/(?P<project_identifier>[a-z0-9-]+)/\+new/$', 'testing_effort_create'),
-    url(r'^image-reports/$', 'image_report_list'),
-    url(r'^image-reports/(?P<name>[A-Za-z0-9_-]+)$', 'image_report_detail'),
-    url(r'^api/link-bug-to-testrun', 'link_bug_to_testrun'),
-    url(r'^api/unlink-bug-and-testrun', 'unlink_bug_and_testrun'),
+    url(r'^image-reports/$', 'images.image_report_list'),
+    url(r'^image-reports/(?P<name>[A-Za-z0-9_-]+)$', 'images.image_report_detail'),
+    url(r'^api/link-bug-to-testrun', 'images.link_bug_to_testrun'),
+    url(r'^api/unlink-bug-and-testrun', 'images.unlink_bug_and_testrun'),
 )

=== added directory 'dashboard_app/views'
=== renamed file 'dashboard_app/views.py' => 'dashboard_app/views/__init__.py'
--- dashboard_app/views.py	2012-09-25 23:49:47 +0000
+++ dashboard_app/views/__init__.py	2012-10-01 08:08:51 +0000
@@ -20,29 +20,18 @@
 Views for the Dashboard application
 """
 
-import operator
 import re
 import json
 
-from django.conf import settings
-from django.contrib.admin.widgets import FilteredSelectMultiple
 from django.contrib.auth.decorators import login_required
-from django.contrib.contenttypes.models import ContentType
 from django.contrib.sites.models import Site
-from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.urlresolvers import reverse
 from django.db.models.manager import Manager
 from django.db.models.query import QuerySet
-from django import forms
-from django.forms.formsets import BaseFormSet, formset_factory
-from django.forms.widgets import Select
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import render_to_response, redirect, get_object_or_404
 from django.template import RequestContext, loader
-from django.template import Template, Context
-from django.utils.html import escape
 from django.utils.safestring import mark_safe
-from django.views.decorators.http import require_POST
 from django.views.generic.list_detail import object_list, object_detail
 
 from django_tables2 import Attrs, Column, TemplateColumn
@@ -60,17 +49,10 @@
     BundleStream,
     DataReport,
     DataView,
-    Image,
-    ImageSet,
-    LaunchpadBug,
-    NamedAttribute,
     Tag,
     Test,
-    TestCase,
     TestResult,
     TestRun,
-    TestRunFilter,
-    TestRunFilterSubscription,
     TestingEffort,
 )
 
@@ -448,619 +430,6 @@
     searchable_columns = ['test_case__test_case_id']
 
 
-class UserFiltersTable(DataTablesTable):
-
-    name = TemplateColumn('''
-    <a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
-    ''')
-
-    bundle_streams = TemplateColumn('''
-    {% for r in record.bundle_streams.all %}
-        {{r.pathname}} <br />
-    {% endfor %}
-    ''')
-
-    build_number_attribute = Column()
-    def render_build_number_attribute(self, value):
-        if not value:
-            return ''
-        return value
-
-    attributes = TemplateColumn('''
-    {% for a in record.attributes.all %}
-    {{ a }}  <br />
-    {% endfor %}
-    ''')
-
-    test = TemplateColumn('''
-      <table style="border-collapse: collapse">
-        <tbody>
-          {% for test in record.tests.all %}
-          <tr>
-            <td>
-              {{ test.test }}
-            </td>
-            <td>
-              {% for test_case in test.all_case_names %}
-              {{ test_case }}
-              {% empty %}
-              <i>any</i>
-              {% endfor %}
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-    ''')
-
-    subscription = Column()
-    def render_subscription(self, record):
-        try:
-            sub = TestRunFilterSubscription.objects.get(
-                user=self.user, filter=record)
-        except TestRunFilterSubscription.DoesNotExist:
-            return "None"
-        else:
-            return sub.get_level_display()
-
-    public = Column()
-
-    def get_queryset(self, user):
-        return TestRunFilter.objects.filter(owner=user)
-
-
-class PublicFiltersTable(UserFiltersTable):
-
-    name = TemplateColumn('''
-    <a href="{{ record.get_absolute_url }}">~{{ record.owner.username }}/{{ record.name }}</a>
-    ''')
-
-    def __init__(self, *args, **kw):
-        super(PublicFiltersTable, self).__init__(*args, **kw)
-        del self.base_columns['public']
-
-    def get_queryset(self):
-        return TestRunFilter.objects.filter(public=True)
-
-
-@BreadCrumb("Filters and Subscriptions", parent=index)
-def filters_list(request):
-    public_filters_table = PublicFiltersTable("public-filters", None)
-    if request.user.is_authenticated():
-        public_filters_table.user = request.user
-        user_filters_table = UserFiltersTable("user-filters", None, params=(request.user,))
-        user_filters_table.user = request.user
-    else:
-        user_filters_table = None
-        del public_filters_table.base_columns['subscription']
-
-    return render_to_response(
-        'dashboard_app/filters_list.html', {
-            'user_filters_table': user_filters_table,
-            'public_filters_table': public_filters_table,
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                filters_list),
-        }, RequestContext(request)
-    )
-
-
-class TestRunColumn(Column):
-    def render(self, record):
-        # This column is only rendered if we don't really expect
-        # record.test_runs to be very long...
-        links = []
-        trs = [tr for tr in record.test_runs if tr.test.test_id == self.verbose_name]
-        for tr in trs:
-            text = '%s / %s' % (tr.denormalization.count_pass, tr.denormalization.count_all())
-            links.append('<a href="%s">%s</a>' % (tr.get_absolute_url(), text))
-        return mark_safe('&nbsp;'.join(links))
-
-
-class SpecificCaseColumn(Column):
-    def __init__(self, verbose_name, test_case_id):
-        super(SpecificCaseColumn, self).__init__(verbose_name)
-        self.test_case_id = test_case_id
-    def render(self, record):
-        r = []
-        for result in record.specific_results:
-            if result.test_case_id != self.test_case_id:
-                continue
-            if result.result == result.RESULT_PASS and result.units:
-                s = '%s %s' % (result.measurement, result.units)
-            else:
-                s = result.RESULT_MAP[result.result]
-            r.append('<a href="' + result.get_absolute_url() + '">'+s+'</a>')
-        return mark_safe(', '.join(r))
-
-
-class BundleColumn(Column):
-    def render(self, record):
-        return mark_safe('<a href="' + record.bundle.get_absolute_url() + '">' + escape(record.bundle.content_filename) + '</a>')
-
-
-class FilterTable(DataTablesTable):
-    def __init__(self, *args, **kwargs):
-        kwargs['template'] = 'dashboard_app/filter_results_table.html'
-        super(FilterTable, self).__init__(*args, **kwargs)
-        match_maker = self.data.queryset
-        self.base_columns['tag'].verbose_name = match_maker.key_name
-        bundle_stream_col = self.base_columns.pop('bundle_stream')
-        bundle_col = self.base_columns.pop('bundle')
-        tag_col = self.base_columns.pop('tag')
-        self.complex_header = False
-        if match_maker.filter_data['tests']:
-            del self.base_columns['passes']
-            del self.base_columns['total']
-            for i, t in enumerate(reversed(match_maker.filter_data['tests'])):
-                if len(t.all_case_names()) == 0:
-                    col = TestRunColumn(mark_safe(t.test.test_id))
-                    self.base_columns.insert(0, 'test_run_%s' % i, col)
-                elif len(t.all_case_names()) == 1:
-                    n = t.test.test_id + ':' + t.all_case_names()[0]
-                    col = SpecificCaseColumn(mark_safe(n), t.all_case_ids()[0])
-                    self.base_columns.insert(0, 'test_run_%s_case' % i, col)
-                else:
-                    col0 = SpecificCaseColumn(mark_safe(t.all_case_names()[0]), t.all_case_ids()[0])
-                    col0.in_group = True
-                    col0.first_in_group = True
-                    col0.group_length = len(t.all_case_names())
-                    col0.group_name = mark_safe(t.test.test_id)
-                    self.complex_header = True
-                    self.base_columns.insert(0, 'test_run_%s_case_%s' % (i, 0), col0)
-                    for j, n in enumerate(t.all_case_names()[1:], 1):
-                        col = SpecificCaseColumn(mark_safe(n), t.all_case_ids()[j])
-                        col.in_group = True
-                        col.first_in_group = False
-                        self.base_columns.insert(j, 'test_run_%s_case_%s' % (i, j), col)
-        else:
-            self.base_columns.insert(0, 'bundle', bundle_col)
-        if len(match_maker.filter_data['bundle_streams']) > 1:
-            self.base_columns.insert(0, 'bundle_stream', bundle_stream_col)
-        self.base_columns.insert(0, 'tag', tag_col)
-
-    tag = Column()
-
-    def render_bundle_stream(self, record):
-        bundle_streams = set(tr.bundle.bundle_stream for tr in record.test_runs)
-        links = []
-        for bs in sorted(bundle_streams, key=operator.attrgetter('pathname')):
-            links.append('<a href="%s">%s</a>' % (
-                bs.get_absolute_url(), escape(bs.pathname)))
-        return mark_safe('<br />'.join(links))
-    bundle_stream = Column(mark_safe("Bundle Stream(s)"))
-
-    def render_bundle(self, record):
-        bundles = set(tr.bundle for tr in record.test_runs)
-        links = []
-        for b in sorted(bundles, key=operator.attrgetter('uploaded_on')):
-            links.append('<a href="%s">%s</a>' % (
-                b.get_absolute_url(), escape(b.content_filename)))
-        return mark_safe('<br />'.join(links))
-    bundle = Column(mark_safe("Bundle(s)"))
-
-    passes = Column(accessor='pass_count')
-    total = Column(accessor='result_count')
-
-    def get_queryset(self, user, filter):
-        return filter.get_test_runs(user)
-
-    datatable_opts = {
-        "sPaginationType": "full_numbers",
-        "iDisplayLength": 25,
-        "bSort": False,
-        }
-
-
-def filter_json(request, username, name):
-    filter = TestRunFilter.objects.get(owner__username=username, name=name)
-    return FilterTable.json(request, params=(request.user, filter))
-
-
-class FilterPreviewTable(FilterTable):
-    def get_queryset(self, user, form):
-        return form.get_test_runs(user)
-
-    datatable_opts = FilterTable.datatable_opts.copy()
-    datatable_opts.update({
-        "iDisplayLength": 10,
-        })
-
-
-def filter_preview_json(request):
-    try:
-        filter = TestRunFilter.objects.get(owner=request.user, name=request.GET['name'])
-    except TestRunFilter.DoesNotExist:
-        filter = None
-    form = TestRunFilterForm(request.user, request.GET, instance=filter)
-    if not form.is_valid():
-        raise ValidationError(str(form.errors))
-    return FilterPreviewTable.json(request, params=(request.user, form))
-
-
-@BreadCrumb("Filter ~{username}/{name}", parent=filters_list, needs=['username', 'name'])
-def filter_detail(request, username, name):
-    filter = TestRunFilter.objects.get(owner__username=username, name=name)
-    if not filter.public and filter.owner != request.user:
-        raise PermissionDenied()
-    if not request.user.is_authenticated():
-        subscription = None
-    else:
-        try:
-            subscription = TestRunFilterSubscription.objects.get(
-                user=request.user, filter=filter)
-        except TestRunFilterSubscription.DoesNotExist:
-            subscription = None
-    return render_to_response(
-        'dashboard_app/filter_detail.html', {
-            'filter': filter,
-            'subscription': subscription,
-            'filter_table': FilterTable(
-                "filter-table",
-                reverse(filter_json, kwargs=dict(username=username, name=name)),
-                params=(request.user, filter)),
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                filter_detail, name=name, username=username),
-        }, RequestContext(request)
-    )
-
-
-class TestRunFilterSubscriptionForm(forms.ModelForm):
-    class Meta:
-        model = TestRunFilterSubscription
-        fields = ('level',)
-    def __init__(self, filter, user, *args, **kwargs):
-        super(TestRunFilterSubscriptionForm, self).__init__(*args, **kwargs)
-        self.instance.filter = filter
-        self.instance.user = user
-
-
-@BreadCrumb("Manage Subscription", parent=filter_detail, needs=['name', 'username'])
-@login_required
-def filter_subscribe(request, username, name):
-    filter = TestRunFilter.objects.get(owner__username=username, name=name)
-    if not filter.public and filter.owner != request.user:
-        raise PermissionDenied()
-    try:
-        subscription = TestRunFilterSubscription.objects.get(
-            user=request.user, filter=filter)
-    except TestRunFilterSubscription.DoesNotExist:
-        subscription = None
-    if request.method == "POST":
-        form = TestRunFilterSubscriptionForm(
-            filter, request.user, request.POST, instance=subscription)
-        if form.is_valid():
-            if 'unsubscribe' in request.POST:
-                subscription.delete()
-            else:
-                form.save()
-            return HttpResponseRedirect(filter.get_absolute_url())
-    else:
-        form = TestRunFilterSubscriptionForm(
-            filter, request.user, instance=subscription)
-    return render_to_response(
-        'dashboard_app/filter_subscribe.html', {
-            'filter': filter,
-            'form': form,
-            'subscription': subscription,
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                filter_subscribe, name=name, username=username),
-        }, RequestContext(request)
-    )
-
-
-test_run_filter_head = '''
-<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/filter-edit.css" />
-<script type="text/javascript" src="{% url admin:jsi18n %}"></script>
-<script type="text/javascript">
-var django = {};
-django.jQuery = $;
-var test_case_url = "{% url dashboard_app.views.filter_add_cases_for_test_json %}?test=";
-var attr_name_completion_url = "{% url dashboard_app.views.filter_attr_name_completion_json %}";
-var attr_value_completion_url = "{% url dashboard_app.views.filter_attr_value_completion_json %}";
-</script>
-<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/jquery.formset.js"></script>
-<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/filter-edit.js"></script>
-'''
-
-
-class AttributesForm(forms.Form):
-    name = forms.CharField(max_length=1024)
-    value = forms.CharField(max_length=1024)
-
-AttributesFormSet = formset_factory(AttributesForm, extra=0)
-
-
-
-class TruncatingSelect(Select):
-
-    def render_option(self, selected_choices, option_value, option_label):
-        if len(option_label) > 50:
-            option_label = option_label[:50] + '...'
-        return super(TruncatingSelect, self).render_option(
-            selected_choices, option_value, option_label)
-
-
-class TRFTestCaseForm(forms.Form):
-
-    test_case = forms.ModelChoiceField(
-        queryset=TestCase.objects.none(), widget=TruncatingSelect, empty_label=None)
-
-
-class BaseTRFTestCaseFormSet(BaseFormSet):
-
-    def __init__(self, *args, **kw):
-        self._queryset = kw.pop('queryset')
-        super(BaseTRFTestCaseFormSet, self).__init__(*args, **kw)
-
-    def add_fields(self, form, index):
-        super(BaseTRFTestCaseFormSet, self).add_fields(form, index)
-        if self._queryset is not None:
-            form.fields['test_case'].queryset = self._queryset
-
-
-TRFTestCaseFormSet = formset_factory(
-    TRFTestCaseForm, extra=0, formset=BaseTRFTestCaseFormSet)
-
-
-class TRFTestForm(forms.Form):
-
-    def __init__(self, *args, **kw):
-        super(TRFTestForm, self).__init__(*args, **kw)
-        kw['initial'] = kw.get('initial', {}).get('test_cases', None)
-        kw.pop('empty_permitted', None)
-        kw['queryset'] = None
-        v = self['test'].value()
-        if v:
-            test = self.fields['test'].to_python(v)
-            queryset = TestCase.objects.filter(test=test).order_by('test_case_id')
-            kw['queryset'] = queryset
-        self.test_case_formset = TRFTestCaseFormSet(*args, **kw)
-
-    def is_valid(self):
-        return super(TRFTestForm, self).is_valid() and \
-               self.test_case_formset.is_valid()
-
-    def full_clean(self):
-        super(TRFTestForm, self).full_clean()
-        self.test_case_formset.full_clean()
-
-    test = forms.ModelChoiceField(
-        queryset=Test.objects.order_by('test_id'), required=True)
-
-
-class BaseTRFTestsFormSet(BaseFormSet):
-
-    def is_valid(self):
-        if not super(BaseTRFTestsFormSet, self).is_valid():
-            return False
-        for form in self.forms:
-            if not form.is_valid():
-                return False
-        return True
-
-
-TRFTestsFormSet = formset_factory(
-    TRFTestForm, extra=0, formset=BaseTRFTestsFormSet)
-
-
-class FakeTRFTest(object):
-    def __init__(self, form):
-        self.test = form.cleaned_data['test']
-        self.test_id = self.test.id
-        self._case_ids = []
-        self._case_names = []
-        for tc_form in form.test_case_formset:
-            self._case_ids.append(tc_form.cleaned_data['test_case'].id)
-            self._case_names.append(tc_form.cleaned_data['test_case'].test_case_id)
-
-    def all_case_ids(self):
-        return self._case_ids
-
-    def all_case_names(self):
-        return self._case_names
-
-
-class TestRunFilterForm(forms.ModelForm):
-    class Meta:
-        model = TestRunFilter
-        exclude = ('owner',)
-        widgets = {
-            'bundle_streams': FilteredSelectMultiple("Bundle Streams", False),
-            }
-
-    @property
-    def media(self):
-        super_media = str(super(TestRunFilterForm, self).media)
-        return mark_safe(Template(test_run_filter_head).render(
-            Context({'STATIC_URL': settings.STATIC_URL})
-            )) + super_media
-
-    def validate_name(self, value):
-        self.instance.name = value
-        try:
-            self.instance.validate_unique()
-        except ValidationError, e:
-            if e.message_dict.values() == [[
-                u'Test run filter with this Owner and Name already exists.']]:
-                raise ValidationError("You already have a filter with this name")
-            else:
-                raise
-
-    def save(self, commit=True, **kwargs):
-        instance = super(TestRunFilterForm, self).save(commit=commit, **kwargs)
-        if commit:
-            instance.attributes.all().delete()
-            for a in self.attributes_formset.cleaned_data:
-                instance.attributes.create(name=a['name'], value=a['value'])
-            instance.tests.all().delete()
-            for i, test_form in enumerate(self.tests_formset.forms):
-                trf_test = instance.tests.create(
-                    test=test_form.cleaned_data['test'], index=i)
-                for j, test_case_form in enumerate(test_form.test_case_formset.forms):
-                    trf_test.cases.create(
-                        test_case=test_case_form.cleaned_data['test_case'], index=j)
-        return instance
-
-    def is_valid(self):
-        return super(TestRunFilterForm, self).is_valid() and \
-               self.attributes_formset.is_valid() and \
-               self.tests_formset.is_valid()
-
-    def full_clean(self):
-        super(TestRunFilterForm, self).full_clean()
-        self.attributes_formset.full_clean()
-        self.tests_formset.full_clean()
-
-    @property
-    def summary_data(self):
-        data = self.cleaned_data.copy()
-        tests = []
-        for form in self.tests_formset.forms:
-            tests.append(FakeTRFTest(form))
-        data['attributes'] = [
-            (d['name'], d['value']) for d in self.attributes_formset.cleaned_data]
-        data['tests'] = tests
-        return data
-
-    def __init__(self, user, *args, **kwargs):
-        super(TestRunFilterForm, self).__init__(*args, **kwargs)
-        self.instance.owner = user
-        kwargs.pop('instance', None)
-
-        attr_set_args = kwargs.copy()
-        if self.instance.pk:
-            initial = []
-            for attr in self.instance.attributes.all():
-                initial.append({
-                    'name': attr.name,
-                    'value': attr.value,
-                    })
-            attr_set_args['initial'] = initial
-        attr_set_args['prefix'] = 'attributes'
-        self.attributes_formset = AttributesFormSet(*args, **attr_set_args)
-
-        tests_set_args = kwargs.copy()
-        if self.instance.pk:
-            initial = []
-            for test in self.instance.tests.all().order_by('index').prefetch_related('cases'):
-                initial.append({
-                    'test': test.test,
-                    'test_cases': [{'test_case': unicode(tc.test_case.id)} for tc in test.cases.all().order_by('index')],
-                    })
-            tests_set_args['initial'] = initial
-        tests_set_args['prefix'] = 'tests'
-        self.tests_formset = TRFTestsFormSet(*args, **tests_set_args)
-
-        self.fields['bundle_streams'].queryset = \
-            BundleStream.objects.accessible_by_principal(user).order_by('pathname')
-        self.fields['name'].validators.append(self.validate_name)
-
-    def get_test_runs(self, user):
-        assert self.is_valid(), self.errors
-        filter = self.save(commit=False)
-        tests = []
-        for form in self.tests_formset.forms:
-            tests.append(FakeTRFTest(form))
-        return filter.get_test_runs_impl(
-            user, self.cleaned_data['bundle_streams'], self.summary_data['attributes'], tests)
-
-
-def filter_form(request, bread_crumb_trail, instance=None):
-    if request.method == 'POST':
-        form = TestRunFilterForm(request.user, request.POST, instance=instance)
-
-        if form.is_valid():
-            if 'save' in request.POST:
-                filter = form.save()
-                return HttpResponseRedirect(filter.get_absolute_url())
-            else:
-                c = request.POST.copy()
-                c.pop('csrfmiddlewaretoken', None)
-                return render_to_response(
-                    'dashboard_app/filter_preview.html', {
-                        'bread_crumb_trail': bread_crumb_trail,
-                        'form': form,
-                        'table': FilterPreviewTable(
-                            'filter-preview',
-                            reverse(filter_preview_json) + '?' + c.urlencode(),
-                            params=(request.user, form)),
-                    }, RequestContext(request))
-    else:
-        form = TestRunFilterForm(request.user, instance=instance)
-
-    return render_to_response(
-        'dashboard_app/filter_add.html', {
-            'bread_crumb_trail': bread_crumb_trail,
-            'form': form,
-        }, RequestContext(request))
-
-
-@BreadCrumb("Add new filter", parent=filters_list)
-def filter_add(request):
-    return filter_form(
-        request,
-        BreadCrumbTrail.leading_to(filter_add))
-
-
-@BreadCrumb("Edit", parent=filter_detail, needs=['name', 'username'])
-def filter_edit(request, username, name):
-    if request.user.username != username:
-        raise PermissionDenied()
-    filter = TestRunFilter.objects.get(owner=request.user, name=name)
-    return filter_form(
-        request,
-        BreadCrumbTrail.leading_to(filter_edit, name=name, username=username),
-        instance=filter)
-
-
-@BreadCrumb("Delete", parent=filter_detail, needs=['name', 'username'])
-def filter_delete(request, username, name):
-    if request.user.username != username:
-        raise PermissionDenied()
-    filter = TestRunFilter.objects.get(owner=request.user, name=name)
-    if request.method == "POST":
-        if 'yes' in request.POST:
-            filter.delete()
-            return HttpResponseRedirect(reverse(filters_list))
-        else:
-            return HttpResponseRedirect(filter.get_absolute_url())
-    return render_to_response(
-        'dashboard_app/filter_delete.html', {
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(filter_delete, name=name, username=username),
-            'filter': filter,
-        }, RequestContext(request))
-
-
-def filter_add_cases_for_test_json(request):
-    test = Test.objects.get(test_id=request.GET['test'])
-    result = TestCase.objects.filter(test=test).order_by('test_case_id').values('test_case_id', 'id')
-    return HttpResponse(
-        json.dumps(list(result)),
-        mimetype='application/json')
-
-
-def filter_attr_name_completion_json(request):
-    term = request.GET['term']
-    content_type_id = ContentType.objects.get_for_model(TestRun).id
-    result = NamedAttribute.objects.filter(
-        name__startswith=term, content_type_id=content_type_id
-        ).distinct().order_by('name').values_list('name', flat=True)
-    return HttpResponse(
-        json.dumps(list(result)),
-        mimetype='application/json')
-
-
-def filter_attr_value_completion_json(request):
-    name = request.GET['name']
-    term = request.GET['term']
-    content_type_id = ContentType.objects.get_for_model(TestRun).id
-    result = NamedAttribute.objects.filter(
-        name=name, content_type_id=content_type_id, value__startswith=term
-        ).distinct().order_by('value').values_list('value', flat=True)
-    return HttpResponse(
-        json.dumps(list(result)),
-        mimetype='application/json')
-
 
 def test_run_detail_test_json(request, pathname, content_sha1, analyzer_assigned_uuid):
     test_run = get_restricted_object_or_404(
@@ -1481,124 +850,3 @@
             pk=effort.pk)
     })
     return HttpResponse(t.render(c))
-
-
-@BreadCrumb("Image Reports", parent=index)
-def image_report_list(request):
-    imagesets = ImageSet.objects.filter()
-    imagesets_data = []
-    for imageset in imagesets:
-        images_data = []
-        for image in imageset.images.all():
-            # Migration hack: Image.filter cannot be auto populated, so ignore
-            # images that have not been migrated to filters for now.
-            if image.filter:
-                image_data = {
-                    'name': image.name,
-                    'bundle_count': image.filter.get_test_runs(request.user).count(),
-                    'link': image.name,
-                    }
-                images_data.append(image_data)
-        images_data.sort(key=lambda d:d['name'])
-        imageset_data = {
-            'name': imageset.name,
-            'images': images_data,
-            }
-        imagesets_data.append(imageset_data)
-    imagesets_data.sort(key=lambda d:d['name'])
-    return render_to_response(
-        "dashboard_app/image-reports.html", {
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(image_report_list),
-            'imagesets': imagesets_data,
-        }, RequestContext(request))
-
-
-@BreadCrumb("{name}", parent=image_report_list, needs=['name'])
-def image_report_detail(request, name):
-
-    image = Image.objects.get(name=name)
-    matches = image.filter.get_test_runs(request.user, prefetch_related=['launchpad_bugs'])[:50]
-
-    build_number_to_cols = {}
-
-    test_run_names = set()
-
-    for match in matches:
-        for test_run in match.test_runs:
-            name = test_run.test.test_id
-            denorm = test_run.denormalization
-            if denorm.count_pass == denorm.count_all():
-                cls = 'present pass'
-            else:
-                    cls = 'present fail'
-            bug_ids = sorted([b.bug_id for b in test_run.launchpad_bugs.all()])
-            test_run_data = dict(
-                present=True,
-                cls=cls,
-                uuid=test_run.analyzer_assigned_uuid,
-                passes=denorm.count_pass,
-                total=denorm.count_all(),
-                link=test_run.get_permalink(),
-                bug_ids=bug_ids,
-                )
-            if match.tag not in build_number_to_cols:
-                # This assumes 1 bundle per match...
-                build_number_to_cols[match.tag] = {
-                    'test_runs': {},
-                    'number': match.tag,
-                    'date': test_run.bundle.uploaded_on,
-                    'link': test_run.bundle.get_absolute_url(),
-                    }
-            build_number_to_cols[match.tag]['test_runs'][name] = test_run_data
-            if name != 'lava':
-                test_run_names.add(name)
-
-
-    test_run_names = sorted(test_run_names)
-    test_run_names.insert(0, 'lava')
-
-    cols = [c for n, c in sorted(build_number_to_cols.items())]
-
-    table_data = []
-
-    for test_run_name in test_run_names:
-        row_data = []
-        for col in cols:
-            test_run_data = col['test_runs'].get(test_run_name)
-            if not test_run_data:
-                test_run_data = dict(
-                    present=False,
-                    cls='missing',
-                    )
-            row_data.append(test_run_data)
-        table_data.append(row_data)
-
-    return render_to_response(
-        "dashboard_app/image-report.html", {
-            'bread_crumb_trail': BreadCrumbTrail.leading_to(
-                image_report_detail, name=image.name),
-            'image': image,
-            'cols': cols,
-            'table_data': table_data,
-            'test_run_names': test_run_names,
-        }, RequestContext(request))
-
-
-@require_POST
-def link_bug_to_testrun(request):
-    testrun = get_object_or_404(TestRun, analyzer_assigned_uuid=request.POST['uuid'])
-    bug_id = request.POST['bug']
-    lpbug = LaunchpadBug.objects.get_or_create(bug_id=int(bug_id))[0]
-    testrun.launchpad_bugs.add(lpbug)
-    testrun.save()
-    return HttpResponseRedirect(request.POST['back'])
-
-
-@require_POST
-def unlink_bug_and_testrun(request):
-    testrun = get_object_or_404(TestRun, analyzer_assigned_uuid=request.POST['uuid'])
-    bug_id = request.POST['bug']
-    lpbug = LaunchpadBug.objects.get_or_create(bug_id=int(bug_id))[0]
-    testrun.launchpad_bugs.remove(lpbug)
-    testrun.save()
-    return HttpResponseRedirect(request.POST['back'])

=== added directory 'dashboard_app/views/filters'
=== added file 'dashboard_app/views/filters/__init__.py'
--- dashboard_app/views/filters/__init__.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/views/filters/__init__.py	2012-10-01 03:34:07 +0000
@@ -0,0 +1,17 @@
+# Copyright (C) 2010-2011 Linaro Limited
+#
+# Author: Michael Hudson-Doyle <michael.hudson@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control.  If not, see <http://www.gnu.org/licenses/>.

=== added file 'dashboard_app/views/filters/forms.py'
--- dashboard_app/views/filters/forms.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/views/filters/forms.py	2012-10-01 03:34:07 +0000
@@ -0,0 +1,261 @@
+# Copyright (C) 2010-2011 Linaro Limited
+#
+# Author: Michael Hudson-Doyle <michael.hudson@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control.  If not, see <http://www.gnu.org/licenses/>.
+
+from django.conf import settings
+from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.core.exceptions import ValidationError
+from django import forms
+from django.forms.formsets import BaseFormSet, formset_factory
+from django.forms.widgets import Select
+from django.template import Template, Context
+from django.utils.safestring import mark_safe
+
+from dashboard_app.models import (
+    BundleStream,
+    Test,
+    TestCase,
+    TestRunFilter,
+    TestRunFilterSubscription,
+)
+
+class TestRunFilterSubscriptionForm(forms.ModelForm):
+    class Meta:
+        model = TestRunFilterSubscription
+        fields = ('level',)
+    def __init__(self, filter, user, *args, **kwargs):
+        super(TestRunFilterSubscriptionForm, self).__init__(*args, **kwargs)
+        self.instance.filter = filter
+        self.instance.user = user
+
+
+
+test_run_filter_head = '''
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}dashboard_app/css/filter-edit.css" />
+<script type="text/javascript" src="{% url admin:jsi18n %}"></script>
+<script type="text/javascript">
+var django = {};
+django.jQuery = $;
+var test_case_url = "{% url dashboard_app.views.filter_add_cases_for_test_json %}?test=";
+var attr_name_completion_url = "{% url dashboard_app.views.filter_attr_name_completion_json %}";
+var attr_value_completion_url = "{% url dashboard_app.views.filter_attr_value_completion_json %}";
+</script>
+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/jquery.formset.js"></script>
+<script type="text/javascript" src="{{ STATIC_URL }}dashboard_app/js/filter-edit.js"></script>
+'''
+
+
+class AttributesForm(forms.Form):
+    name = forms.CharField(max_length=1024)
+    value = forms.CharField(max_length=1024)
+
+AttributesFormSet = formset_factory(AttributesForm, extra=0)
+
+
+class TruncatingSelect(Select):
+
+    def render_option(self, selected_choices, option_value, option_label):
+        if len(option_label) > 50:
+            option_label = option_label[:50] + '...'
+        return super(TruncatingSelect, self).render_option(
+            selected_choices, option_value, option_label)
+
+
+class TRFTestCaseForm(forms.Form):
+
+    test_case = forms.ModelChoiceField(
+        queryset=TestCase.objects.none(), widget=TruncatingSelect, empty_label=None)
+
+
+class BaseTRFTestCaseFormSet(BaseFormSet):
+
+    def __init__(self, *args, **kw):
+        self._queryset = kw.pop('queryset')
+        super(BaseTRFTestCaseFormSet, self).__init__(*args, **kw)
+
+    def add_fields(self, form, index):
+        super(BaseTRFTestCaseFormSet, self).add_fields(form, index)
+        if self._queryset is not None:
+            form.fields['test_case'].queryset = self._queryset
+
+
+TRFTestCaseFormSet = formset_factory(
+    TRFTestCaseForm, extra=0, formset=BaseTRFTestCaseFormSet)
+
+
+class TRFTestForm(forms.Form):
+
+    def __init__(self, *args, **kw):
+        super(TRFTestForm, self).__init__(*args, **kw)
+        kw['initial'] = kw.get('initial', {}).get('test_cases', None)
+        kw.pop('empty_permitted', None)
+        kw['queryset'] = None
+        v = self['test'].value()
+        if v:
+            test = self.fields['test'].to_python(v)
+            queryset = TestCase.objects.filter(test=test).order_by('test_case_id')
+            kw['queryset'] = queryset
+        self.test_case_formset = TRFTestCaseFormSet(*args, **kw)
+
+    def is_valid(self):
+        return super(TRFTestForm, self).is_valid() and \
+               self.test_case_formset.is_valid()
+
+    def full_clean(self):
+        super(TRFTestForm, self).full_clean()
+        self.test_case_formset.full_clean()
+
+    test = forms.ModelChoiceField(
+        queryset=Test.objects.order_by('test_id'), required=True)
+
+
+class BaseTRFTestsFormSet(BaseFormSet):
+
+    def is_valid(self):
+        if not super(BaseTRFTestsFormSet, self).is_valid():
+            return False
+        for form in self.forms:
+            if not form.is_valid():
+                return False
+        return True
+
+
+TRFTestsFormSet = formset_factory(
+    TRFTestForm, extra=0, formset=BaseTRFTestsFormSet)
+
+
+class FakeTRFTest(object):
+    def __init__(self, form):
+        self.test = form.cleaned_data['test']
+        self.test_id = self.test.id
+        self._case_ids = []
+        self._case_names = []
+        for tc_form in form.test_case_formset:
+            self._case_ids.append(tc_form.cleaned_data['test_case'].id)
+            self._case_names.append(tc_form.cleaned_data['test_case'].test_case_id)
+
+    def all_case_ids(self):
+        return self._case_ids
+
+    def all_case_names(self):
+        return self._case_names
+
+
+class TestRunFilterForm(forms.ModelForm):
+    class Meta:
+        model = TestRunFilter
+        exclude = ('owner',)
+        widgets = {
+            'bundle_streams': FilteredSelectMultiple("Bundle Streams", False),
+            }
+
+    @property
+    def media(self):
+        super_media = str(super(TestRunFilterForm, self).media)
+        return mark_safe(Template(test_run_filter_head).render(
+            Context({'STATIC_URL': settings.STATIC_URL})
+            )) + super_media
+
+    def validate_name(self, value):
+        self.instance.name = value
+        try:
+            self.instance.validate_unique()
+        except ValidationError, e:
+            if e.message_dict.values() == [[
+                u'Test run filter with this Owner and Name already exists.']]:
+                raise ValidationError("You already have a filter with this name")
+            else:
+                raise
+
+    def save(self, commit=True, **kwargs):
+        instance = super(TestRunFilterForm, self).save(commit=commit, **kwargs)
+        if commit:
+            instance.attributes.all().delete()
+            for a in self.attributes_formset.cleaned_data:
+                instance.attributes.create(name=a['name'], value=a['value'])
+            instance.tests.all().delete()
+            for i, test_form in enumerate(self.tests_formset.forms):
+                trf_test = instance.tests.create(
+                    test=test_form.cleaned_data['test'], index=i)
+                for j, test_case_form in enumerate(test_form.test_case_formset.forms):
+                    trf_test.cases.create(
+                        test_case=test_case_form.cleaned_data['test_case'], index=j)
+        return instance
+
+    def is_valid(self):
+        return super(TestRunFilterForm, self).is_valid() and \
+               self.attributes_formset.is_valid() and \
+               self.tests_formset.is_valid()
+
+    def full_clean(self):
+        super(TestRunFilterForm, self).full_clean()
+        self.attributes_formset.full_clean()
+        self.tests_formset.full_clean()
+
+    @property
+    def summary_data(self):
+        data = self.cleaned_data.copy()
+        tests = []
+        for form in self.tests_formset.forms:
+            tests.append(FakeTRFTest(form))
+        data['attributes'] = [
+            (d['name'], d['value']) for d in self.attributes_formset.cleaned_data]
+        data['tests'] = tests
+        return data
+
+    def __init__(self, user, *args, **kwargs):
+        super(TestRunFilterForm, self).__init__(*args, **kwargs)
+        self.instance.owner = user
+        kwargs.pop('instance', None)
+
+        attr_set_args = kwargs.copy()
+        if self.instance.pk:
+            initial = []
+            for attr in self.instance.attributes.all():
+                initial.append({
+                    'name': attr.name,
+                    'value': attr.value,
+                    })
+            attr_set_args['initial'] = initial
+        attr_set_args['prefix'] = 'attributes'
+        self.attributes_formset = AttributesFormSet(*args, **attr_set_args)
+
+        tests_set_args = kwargs.copy()
+        if self.instance.pk:
+            initial = []
+            for test in self.instance.tests.all().order_by('index').prefetch_related('cases'):
+                initial.append({
+                    'test': test.test,
+                    'test_cases': [{'test_case': unicode(tc.test_case.id)} for tc in test.cases.all().order_by('index')],
+                    })
+            tests_set_args['initial'] = initial
+        tests_set_args['prefix'] = 'tests'
+        self.tests_formset = TRFTestsFormSet(*args, **tests_set_args)
+
+        self.fields['bundle_streams'].queryset = \
+            BundleStream.objects.accessible_by_principal(user).order_by('pathname')
+        self.fields['name'].validators.append(self.validate_name)
+
+    def get_test_runs(self, user):
+        assert self.is_valid(), self.errors
+        filter = self.save(commit=False)
+        tests = []
+        for form in self.tests_formset.forms:
+            tests.append(FakeTRFTest(form))
+        return filter.get_test_runs_impl(
+            user, self.cleaned_data['bundle_streams'], self.summary_data['attributes'], tests)
+

=== added file 'dashboard_app/views/filters/tables.py'
--- dashboard_app/views/filters/tables.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/views/filters/tables.py	2012-10-01 03:34:07 +0000
@@ -0,0 +1,223 @@
+# Copyright (C) 2010-2011 Linaro Limited
+#
+# Author: Michael Hudson-Doyle <michael.hudson@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control.  If not, see <http://www.gnu.org/licenses/>.
+
+import operator
+
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+
+from django_tables2 import Column, TemplateColumn
+
+from lava.utils.data_tables.tables import DataTablesTable
+
+from dashboard_app.models import (
+    TestRunFilter,
+    TestRunFilterSubscription,
+    )
+
+class UserFiltersTable(DataTablesTable):
+
+    name = TemplateColumn('''
+    <a href="{{ record.get_absolute_url }}">{{ record.name }}</a>
+    ''')
+
+    bundle_streams = TemplateColumn('''
+    {% for r in record.bundle_streams.all %}
+        {{r.pathname}} <br />
+    {% endfor %}
+    ''')
+
+    build_number_attribute = Column()
+    def render_build_number_attribute(self, value):
+        if not value:
+            return ''
+        return value
+
+    attributes = TemplateColumn('''
+    {% for a in record.attributes.all %}
+    {{ a }}  <br />
+    {% endfor %}
+    ''')
+
+    test = TemplateColumn('''
+      <table style="border-collapse: collapse">
+        <tbody>
+          {% for test in record.tests.all %}
+          <tr>
+            <td>
+              {{ test.test }}
+            </td>
+            <td>
+              {% for test_case in test.all_case_names %}
+              {{ test_case }}
+              {% empty %}
+              <i>any</i>
+              {% endfor %}
+            </td>
+          </tr>
+          {% endfor %}
+        </tbody>
+      </table>
+    ''')
+
+    subscription = Column()
+    def render_subscription(self, record):
+        try:
+            sub = TestRunFilterSubscription.objects.get(
+                user=self.user, filter=record)
+        except TestRunFilterSubscription.DoesNotExist:
+            return "None"
+        else:
+            return sub.get_level_display()
+
+    public = Column()
+
+    def get_queryset(self, user):
+        return TestRunFilter.objects.filter(owner=user)
+
+
+class PublicFiltersTable(UserFiltersTable):
+
+    name = TemplateColumn('''
+    <a href="{{ record.get_absolute_url }}">~{{ record.owner.username }}/{{ record.name }}</a>
+    ''')
+
+    def __init__(self, *args, **kw):
+        super(PublicFiltersTable, self).__init__(*args, **kw)
+        del self.base_columns['public']
+
+    def get_queryset(self):
+        return TestRunFilter.objects.filter(public=True)
+
+
+
+class TestRunColumn(Column):
+    def render(self, record):
+        # This column is only rendered if we don't really expect
+        # record.test_runs to be very long...
+        links = []
+        trs = [tr for tr in record.test_runs if tr.test.test_id == self.verbose_name]
+        for tr in trs:
+            text = '%s / %s' % (tr.denormalization.count_pass, tr.denormalization.count_all())
+            links.append('<a href="%s">%s</a>' % (tr.get_absolute_url(), text))
+        return mark_safe('&nbsp;'.join(links))
+
+
+class SpecificCaseColumn(Column):
+    def __init__(self, verbose_name, test_case_id):
+        super(SpecificCaseColumn, self).__init__(verbose_name)
+        self.test_case_id = test_case_id
+    def render(self, record):
+        r = []
+        for result in record.specific_results:
+            if result.test_case_id != self.test_case_id:
+                continue
+            if result.result == result.RESULT_PASS and result.units:
+                s = '%s %s' % (result.measurement, result.units)
+            else:
+                s = result.RESULT_MAP[result.result]
+            r.append('<a href="' + result.get_absolute_url() + '">'+s+'</a>')
+        return mark_safe(', '.join(r))
+
+
+class BundleColumn(Column):
+    def render(self, record):
+        return mark_safe('<a href="' + record.bundle.get_absolute_url() + '">' + escape(record.bundle.content_filename) + '</a>')
+
+
+class FilterTable(DataTablesTable):
+    def __init__(self, *args, **kwargs):
+        kwargs['template'] = 'dashboard_app/filter_results_table.html'
+        super(FilterTable, self).__init__(*args, **kwargs)
+        match_maker = self.data.queryset
+        self.base_columns['tag'].verbose_name = match_maker.key_name
+        bundle_stream_col = self.base_columns.pop('bundle_stream')
+        bundle_col = self.base_columns.pop('bundle')
+        tag_col = self.base_columns.pop('tag')
+        self.complex_header = False
+        if match_maker.filter_data['tests']:
+            del self.base_columns['passes']
+            del self.base_columns['total']
+            for i, t in enumerate(reversed(match_maker.filter_data['tests'])):
+                if len(t.all_case_names()) == 0:
+                    col = TestRunColumn(mark_safe(t.test.test_id))
+                    self.base_columns.insert(0, 'test_run_%s' % i, col)
+                elif len(t.all_case_names()) == 1:
+                    n = t.test.test_id + ':' + t.all_case_names()[0]
+                    col = SpecificCaseColumn(mark_safe(n), t.all_case_ids()[0])
+                    self.base_columns.insert(0, 'test_run_%s_case' % i, col)
+                else:
+                    col0 = SpecificCaseColumn(mark_safe(t.all_case_names()[0]), t.all_case_ids()[0])
+                    col0.in_group = True
+                    col0.first_in_group = True
+                    col0.group_length = len(t.all_case_names())
+                    col0.group_name = mark_safe(t.test.test_id)
+                    self.complex_header = True
+                    self.base_columns.insert(0, 'test_run_%s_case_%s' % (i, 0), col0)
+                    for j, n in enumerate(t.all_case_names()[1:], 1):
+                        col = SpecificCaseColumn(mark_safe(n), t.all_case_ids()[j])
+                        col.in_group = True
+                        col.first_in_group = False
+                        self.base_columns.insert(j, 'test_run_%s_case_%s' % (i, j), col)
+        else:
+            self.base_columns.insert(0, 'bundle', bundle_col)
+        if len(match_maker.filter_data['bundle_streams']) > 1:
+            self.base_columns.insert(0, 'bundle_stream', bundle_stream_col)
+        self.base_columns.insert(0, 'tag', tag_col)
+
+    tag = Column()
+
+    def render_bundle_stream(self, record):
+        bundle_streams = set(tr.bundle.bundle_stream for tr in record.test_runs)
+        links = []
+        for bs in sorted(bundle_streams, key=operator.attrgetter('pathname')):
+            links.append('<a href="%s">%s</a>' % (
+                bs.get_absolute_url(), escape(bs.pathname)))
+        return mark_safe('<br />'.join(links))
+    bundle_stream = Column(mark_safe("Bundle Stream(s)"))
+
+    def render_bundle(self, record):
+        bundles = set(tr.bundle for tr in record.test_runs)
+        links = []
+        for b in sorted(bundles, key=operator.attrgetter('uploaded_on')):
+            links.append('<a href="%s">%s</a>' % (
+                b.get_absolute_url(), escape(b.content_filename)))
+        return mark_safe('<br />'.join(links))
+    bundle = Column(mark_safe("Bundle(s)"))
+
+    passes = Column(accessor='pass_count')
+    total = Column(accessor='result_count')
+
+    def get_queryset(self, user, filter):
+        return filter.get_test_runs(user)
+
+    datatable_opts = {
+        "sPaginationType": "full_numbers",
+        "iDisplayLength": 25,
+        "bSort": False,
+        }
+
+
+class FilterPreviewTable(FilterTable):
+    def get_queryset(self, user, form):
+        return form.get_test_runs(user)
+
+    datatable_opts = FilterTable.datatable_opts.copy()
+    datatable_opts.update({
+        "iDisplayLength": 10,
+        })

=== added file 'dashboard_app/views/filters/views.py'
--- dashboard_app/views/filters/views.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/views/filters/views.py	2012-10-01 03:34:07 +0000
@@ -0,0 +1,249 @@
+# Copyright (C) 2010-2011 Linaro Limited
+#
+# Author: Michael Hudson-Doyle <michael.hudson@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+from django.contrib.auth.decorators import login_required
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from lava_server.bread_crumbs import (
+    BreadCrumb,
+    BreadCrumbTrail,
+)
+
+from dashboard_app.models import (
+    NamedAttribute,
+    Test,
+    TestCase,
+    TestRun,
+    TestRunFilter,
+    TestRunFilterSubscription,
+    )
+from dashboard_app.views import index
+from dashboard_app.views.filters.forms import (
+    TestRunFilterForm,
+    TestRunFilterSubscriptionForm,
+    )
+from dashboard_app.views.filters.tables import (
+    FilterTable,
+    FilterPreviewTable,
+    PublicFiltersTable,
+    UserFiltersTable,
+    )
+
+
+@BreadCrumb("Filters and Subscriptions", parent=index)
+def filters_list(request):
+    public_filters_table = PublicFiltersTable("public-filters", None)
+    if request.user.is_authenticated():
+        public_filters_table.user = request.user
+        user_filters_table = UserFiltersTable("user-filters", None, params=(request.user,))
+        user_filters_table.user = request.user
+    else:
+        user_filters_table = None
+        del public_filters_table.base_columns['subscription']
+
+    return render_to_response(
+        'dashboard_app/filters_list.html', {
+            'user_filters_table': user_filters_table,
+            'public_filters_table': public_filters_table,
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                filters_list),
+        }, RequestContext(request)
+    )
+
+
+def filter_json(request, username, name):
+    filter = TestRunFilter.objects.get(owner__username=username, name=name)
+    return FilterTable.json(request, params=(request.user, filter))
+
+
+
+def filter_preview_json(request):
+    try:
+        filter = TestRunFilter.objects.get(owner=request.user, name=request.GET['name'])
+    except TestRunFilter.DoesNotExist:
+        filter = None
+    form = TestRunFilterForm(request.user, request.GET, instance=filter)
+    if not form.is_valid():
+        raise ValidationError(str(form.errors))
+    return FilterPreviewTable.json(request, params=(request.user, form))
+
+
+@BreadCrumb("Filter ~{username}/{name}", parent=filters_list, needs=['username', 'name'])
+def filter_detail(request, username, name):
+    filter = TestRunFilter.objects.get(owner__username=username, name=name)
+    if not filter.public and filter.owner != request.user:
+        raise PermissionDenied()
+    if not request.user.is_authenticated():
+        subscription = None
+    else:
+        try:
+            subscription = TestRunFilterSubscription.objects.get(
+                user=request.user, filter=filter)
+        except TestRunFilterSubscription.DoesNotExist:
+            subscription = None
+    return render_to_response(
+        'dashboard_app/filter_detail.html', {
+            'filter': filter,
+            'subscription': subscription,
+            'filter_table': FilterTable(
+                "filter-table",
+                reverse(filter_json, kwargs=dict(username=username, name=name)),
+                params=(request.user, filter)),
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                filter_detail, name=name, username=username),
+        }, RequestContext(request)
+    )
+
+
+@BreadCrumb("Manage Subscription", parent=filter_detail, needs=['name', 'username'])
+@login_required
+def filter_subscribe(request, username, name):
+    filter = TestRunFilter.objects.get(owner__username=username, name=name)
+    if not filter.public and filter.owner != request.user:
+        raise PermissionDenied()
+    try:
+        subscription = TestRunFilterSubscription.objects.get(
+            user=request.user, filter=filter)
+    except TestRunFilterSubscription.DoesNotExist:
+        subscription = None
+    if request.method == "POST":
+        form = TestRunFilterSubscriptionForm(
+            filter, request.user, request.POST, instance=subscription)
+        if form.is_valid():
+            if 'unsubscribe' in request.POST:
+                subscription.delete()
+            else:
+                form.save()
+            return HttpResponseRedirect(filter.get_absolute_url())
+    else:
+        form = TestRunFilterSubscriptionForm(
+            filter, request.user, instance=subscription)
+    return render_to_response(
+        'dashboard_app/filter_subscribe.html', {
+            'filter': filter,
+            'form': form,
+            'subscription': subscription,
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                filter_subscribe, name=name, username=username),
+        }, RequestContext(request)
+    )
+
+
+def filter_form(request, bread_crumb_trail, instance=None):
+    if request.method == 'POST':
+        form = TestRunFilterForm(request.user, request.POST, instance=instance)
+
+        if form.is_valid():
+            if 'save' in request.POST:
+                filter = form.save()
+                return HttpResponseRedirect(filter.get_absolute_url())
+            else:
+                c = request.POST.copy()
+                c.pop('csrfmiddlewaretoken', None)
+                return render_to_response(
+                    'dashboard_app/filter_preview.html', {
+                        'bread_crumb_trail': bread_crumb_trail,
+                        'form': form,
+                        'table': FilterPreviewTable(
+                            'filter-preview',
+                            reverse(filter_preview_json) + '?' + c.urlencode(),
+                            params=(request.user, form)),
+                    }, RequestContext(request))
+    else:
+        form = TestRunFilterForm(request.user, instance=instance)
+
+    return render_to_response(
+        'dashboard_app/filter_add.html', {
+            'bread_crumb_trail': bread_crumb_trail,
+            'form': form,
+        }, RequestContext(request))
+
+
+@BreadCrumb("Add new filter", parent=filters_list)
+def filter_add(request):
+    return filter_form(
+        request,
+        BreadCrumbTrail.leading_to(filter_add))
+
+
+@BreadCrumb("Edit", parent=filter_detail, needs=['name', 'username'])
+def filter_edit(request, username, name):
+    if request.user.username != username:
+        raise PermissionDenied()
+    filter = TestRunFilter.objects.get(owner=request.user, name=name)
+    return filter_form(
+        request,
+        BreadCrumbTrail.leading_to(filter_edit, name=name, username=username),
+        instance=filter)
+
+
+@BreadCrumb("Delete", parent=filter_detail, needs=['name', 'username'])
+def filter_delete(request, username, name):
+    if request.user.username != username:
+        raise PermissionDenied()
+    filter = TestRunFilter.objects.get(owner=request.user, name=name)
+    if request.method == "POST":
+        if 'yes' in request.POST:
+            filter.delete()
+            return HttpResponseRedirect(reverse(filters_list))
+        else:
+            return HttpResponseRedirect(filter.get_absolute_url())
+    return render_to_response(
+        'dashboard_app/filter_delete.html', {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(filter_delete, name=name, username=username),
+            'filter': filter,
+        }, RequestContext(request))
+
+
+def filter_add_cases_for_test_json(request):
+    test = Test.objects.get(test_id=request.GET['test'])
+    result = TestCase.objects.filter(test=test).order_by('test_case_id').values('test_case_id', 'id')
+    return HttpResponse(
+        json.dumps(list(result)),
+        mimetype='application/json')
+
+
+def filter_attr_name_completion_json(request):
+    term = request.GET['term']
+    content_type_id = ContentType.objects.get_for_model(TestRun).id
+    result = NamedAttribute.objects.filter(
+        name__startswith=term, content_type_id=content_type_id
+        ).distinct().order_by('name').values_list('name', flat=True)
+    return HttpResponse(
+        json.dumps(list(result)),
+        mimetype='application/json')
+
+
+def filter_attr_value_completion_json(request):
+    name = request.GET['name']
+    term = request.GET['term']
+    content_type_id = ContentType.objects.get_for_model(TestRun).id
+    result = NamedAttribute.objects.filter(
+        name=name, content_type_id=content_type_id, value__startswith=term
+        ).distinct().order_by('value').values_list('value', flat=True)
+    return HttpResponse(
+        json.dumps(list(result)),
+        mimetype='application/json')
+

=== added file 'dashboard_app/views/images.py'
--- dashboard_app/views/images.py	1970-01-01 00:00:00 +0000
+++ dashboard_app/views/images.py	2012-10-01 08:08:51 +0000
@@ -0,0 +1,157 @@
+# Copyright (C) 2010-2012 Linaro Limited
+#
+# Author: Michael Hudson-Doyle <michael.hudson@linaro.org>
+#
+# This file is part of Launch Control.
+#
+# Launch Control is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License version 3
+# as published by the Free Software Foundation
+#
+# Launch Control is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Launch Control.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404, render_to_response
+from django.template import RequestContext
+from django.views.decorators.http import require_POST
+
+from lava_server.bread_crumbs import (
+    BreadCrumb,
+    BreadCrumbTrail,
+)
+
+from dashboard_app.models import (
+    LaunchpadBug,
+    Image,
+    ImageSet,
+    TestRun,
+)
+from dashboard_app.views import index
+
+
+@BreadCrumb("Image Reports", parent=index)
+def image_report_list(request):
+    imagesets = ImageSet.objects.filter()
+    imagesets_data = []
+    for imageset in imagesets:
+        images_data = []
+        for image in imageset.images.all():
+            # Migration hack: Image.filter cannot be auto populated, so ignore
+            # images that have not been migrated to filters for now.
+            if image.filter:
+                image_data = {
+                    'name': image.name,
+                    'bundle_count': image.filter.get_test_runs(request.user).count(),
+                    'link': image.name,
+                    }
+                images_data.append(image_data)
+        images_data.sort(key=lambda d:d['name'])
+        imageset_data = {
+            'name': imageset.name,
+            'images': images_data,
+            }
+        imagesets_data.append(imageset_data)
+    imagesets_data.sort(key=lambda d:d['name'])
+    return render_to_response(
+        "dashboard_app/image-reports.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(image_report_list),
+            'imagesets': imagesets_data,
+        }, RequestContext(request))
+
+
+@BreadCrumb("{name}", parent=image_report_list, needs=['name'])
+def image_report_detail(request, name):
+
+    image = Image.objects.get(name=name)
+    matches = image.filter.get_test_runs(request.user, prefetch_related=['launchpad_bugs'])[:50]
+
+    build_number_to_cols = {}
+
+    test_run_names = set()
+
+    for match in matches:
+        for test_run in match.test_runs:
+            name = test_run.test.test_id
+            denorm = test_run.denormalization
+            if denorm.count_pass == denorm.count_all():
+                cls = 'present pass'
+            else:
+                    cls = 'present fail'
+            bug_ids = sorted([b.bug_id for b in test_run.launchpad_bugs.all()])
+            test_run_data = dict(
+                present=True,
+                cls=cls,
+                uuid=test_run.analyzer_assigned_uuid,
+                passes=denorm.count_pass,
+                total=denorm.count_all(),
+                link=test_run.get_permalink(),
+                bug_ids=bug_ids,
+                )
+            if match.tag not in build_number_to_cols:
+                # This assumes 1 bundle per match...
+                build_number_to_cols[match.tag] = {
+                    'test_runs': {},
+                    'number': match.tag,
+                    'date': test_run.bundle.uploaded_on,
+                    'link': test_run.bundle.get_absolute_url(),
+                    }
+            build_number_to_cols[match.tag]['test_runs'][name] = test_run_data
+            if name != 'lava':
+                test_run_names.add(name)
+
+
+    test_run_names = sorted(test_run_names)
+    test_run_names.insert(0, 'lava')
+
+    cols = [c for n, c in sorted(build_number_to_cols.items())]
+
+    table_data = []
+
+    for test_run_name in test_run_names:
+        row_data = []
+        for col in cols:
+            test_run_data = col['test_runs'].get(test_run_name)
+            if not test_run_data:
+                test_run_data = dict(
+                    present=False,
+                    cls='missing',
+                    )
+            row_data.append(test_run_data)
+        table_data.append(row_data)
+
+    return render_to_response(
+        "dashboard_app/image-report.html", {
+            'bread_crumb_trail': BreadCrumbTrail.leading_to(
+                image_report_detail, name=image.name),
+            'image': image,
+            'cols': cols,
+            'table_data': table_data,
+            'test_run_names': test_run_names,
+        }, RequestContext(request))
+
+
+@require_POST
+def link_bug_to_testrun(request):
+    testrun = get_object_or_404(TestRun, analyzer_assigned_uuid=request.POST['uuid'])
+    bug_id = request.POST['bug']
+    lpbug = LaunchpadBug.objects.get_or_create(bug_id=int(bug_id))[0]
+    testrun.launchpad_bugs.add(lpbug)
+    testrun.save()
+    return HttpResponseRedirect(request.POST['back'])
+
+
+@require_POST
+def unlink_bug_and_testrun(request):
+    testrun = get_object_or_404(TestRun, analyzer_assigned_uuid=request.POST['uuid'])
+    bug_id = request.POST['bug']
+    lpbug = LaunchpadBug.objects.get_or_create(bug_id=int(bug_id))[0]
+    testrun.launchpad_bugs.remove(lpbug)
+    testrun.save()
+    return HttpResponseRedirect(request.POST['back'])

