diff options
Diffstat (limited to 'releng')
-rw-r--r-- | releng/admin.py | 4 | ||||
-rw-r--r-- | releng/management/commands/syncisos.py | 16 | ||||
-rw-r--r-- | releng/migrations/0002_auto__add_field_iso_removed.py | 99 | ||||
-rw-r--r-- | releng/models.py | 37 | ||||
-rw-r--r-- | releng/urls.py | 1 | ||||
-rw-r--r-- | releng/views.py | 71 |
6 files changed, 185 insertions, 43 deletions
diff --git a/releng/admin.py b/releng/admin.py index be5e211f..e1411b84 100644 --- a/releng/admin.py +++ b/releng/admin.py @@ -5,8 +5,8 @@ from .models import (Architecture, BootType, Bootloader, ClockChoice, Test) class IsoAdmin(admin.ModelAdmin): - list_display = ('name', 'created', 'active') - list_filter = ('active',) + list_display = ('name', 'created', 'active', 'removed') + list_filter = ('active', 'created') class TestAdmin(admin.ModelAdmin): list_display = ('user_name', 'user_email', 'created', 'ip_address', diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py index ba174131..270c6c34 100644 --- a/releng/management/commands/syncisos.py +++ b/releng/management/commands/syncisos.py @@ -1,3 +1,4 @@ +from datetime import datetime import re import urllib from HTMLParser import HTMLParser, HTMLParseError @@ -33,19 +34,28 @@ class IsoListParser(HTMLParser): raise CommandError('Couldn\'t parse "%s"' % url) class Command(BaseCommand): - help = 'Gets new isos from %s' % settings.ISO_LIST_URL + help = 'Gets new ISOs from %s' % settings.ISO_LIST_URL def handle(self, *args, **options): parser = IsoListParser() isonames = Iso.objects.values_list('name', flat=True) active_isos = parser.parse(settings.ISO_LIST_URL) - # create any names that don't already exist for iso in active_isos: + # create any names that don't already exist if iso not in isonames: new = Iso(name=iso, active=True) new.save() + # update those that do if they were marked inactive + else: + existing = Iso.objects.get(name=iso) + if not existing.active: + existing.active = True + existing.removed = None + existing.save() + now = datetime.utcnow() # and then mark all other names as no longer active - Iso.objects.exclude(name__in=active_isos).update(active=False) + Iso.objects.filter(active=True).exclude(name__in=active_isos).update( + active=False, removed=now) # vim: set ts=4 sw=4 et: diff --git a/releng/migrations/0002_auto__add_field_iso_removed.py b/releng/migrations/0002_auto__add_field_iso_removed.py new file mode 100644 index 00000000..d5cd09c8 --- /dev/null +++ b/releng/migrations/0002_auto__add_field_iso_removed.py @@ -0,0 +1,99 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.add_column('releng_iso', 'removed', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True), keep_default=False) + + def backwards(self, orm): + db.delete_column('releng_iso', 'removed') + + models = { + 'releng.architecture': { + 'Meta': {'object_name': 'Architecture'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.bootloader': { + 'Meta': {'object_name': 'Bootloader'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.boottype': { + 'Meta': {'object_name': 'BootType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.clockchoice': { + 'Meta': {'object_name': 'ClockChoice'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.filesystem': { + 'Meta': {'object_name': 'Filesystem'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.hardwaretype': { + 'Meta': {'object_name': 'HardwareType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.installtype': { + 'Meta': {'object_name': 'InstallType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.iso': { + 'Meta': {'object_name': 'Iso'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'removed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}) + }, + 'releng.isotype': { + 'Meta': {'object_name': 'IsoType'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.module': { + 'Meta': {'object_name': 'Module'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.source': { + 'Meta': {'object_name': 'Source'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + 'releng.test': { + 'Meta': {'object_name': 'Test'}, + 'architecture': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Architecture']"}), + 'boot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.BootType']"}), + 'bootloader': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Bootloader']"}), + 'clock_choice': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.ClockChoice']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'filesystem': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Filesystem']"}), + 'hardware_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.HardwareType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'install_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.InstallType']"}), + 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}), + 'iso': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Iso']"}), + 'iso_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.IsoType']"}), + 'modules': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['releng.Module']", 'null': 'True', 'blank': 'True'}), + 'rollback_filesystem': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'rollback_test_set'", 'null': 'True', 'to': "orm['releng.Filesystem']"}), + 'rollback_modules': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rollback_test_set'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['releng.Module']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['releng.Source']"}), + 'success': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '500'}) + } + } + + complete_apps = ['releng'] diff --git a/releng/models.py b/releng/models.py index 5510db6a..56187766 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,3 +1,4 @@ +from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import pre_save @@ -9,52 +10,34 @@ class IsoOption(models.Model): def __unicode__(self): return self.name - def get_test_result(self, success): - try: - return self.test_set.filter(success=success).select_related( - 'iso').latest('iso__id').iso - except Test.DoesNotExist: - return None - - def get_last_success(self): - return self.get_test_result(True) - - def get_last_failure(self): - return self.get_test_result(False) - class Meta: abstract = True class RollbackOption(IsoOption): - def get_rollback_test_result(self, success): - try: - return self.rollback_test_set.filter(success=success).select_related( - 'iso').latest('iso__id').iso - except Test.DoesNotExist: - return None - - def get_last_rollback_success(self): - return self.get_rollback_test_result(True) - - def get_last_rollback_failure(self): - return self.get_rollback_test_result(False) - class Meta: abstract = True class Iso(models.Model): name = models.CharField(max_length=255) created = models.DateTimeField(editable=False) + removed = models.DateTimeField(null=True, blank=True, default=None) active = models.BooleanField(default=True) + def get_absolute_url(self): + return reverse('releng-results-iso', args=[self.pk]) + def __unicode__(self): return self.name + class Meta: + verbose_name = 'ISO' + class Architecture(IsoOption): pass class IsoType(IsoOption): - pass + class Meta: + verbose_name = 'ISO type' class BootType(IsoOption): pass diff --git a/releng/urls.py b/releng/urls.py index 4a125dff..239ad02b 100644 --- a/releng/urls.py +++ b/releng/urls.py @@ -6,6 +6,7 @@ feedback_patterns = patterns('releng.views', (r'^thanks/$', 'submit_test_thanks', {}, 'releng-test-thanks'), (r'^iso/(?P<iso_id>\d+)/$', 'test_results_iso', {}, 'releng-results-iso'), (r'^(?P<option>.+)/(?P<value>\d+)/$','test_results_for', {}, 'releng-results-for'), + (r'^iso/overview/$', 'iso_overview', {}, 'releng-iso-overview'), ) urlpatterns = patterns('', diff --git a/releng/views.py b/releng/views.py index 1d4a0b5e..2b3d0936 100644 --- a/releng/views.py +++ b/releng/views.py @@ -1,5 +1,6 @@ from django import forms from django.conf import settings +from django.db.models import Count, Max from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.views.generic.simple import direct_to_template @@ -80,22 +81,53 @@ def calculate_option_overview(field_name): is_rollback = field_name.startswith('rollback_') option = { 'option': model, - 'name': field_name, + 'name': model._meta.verbose_name, 'is_rollback': is_rollback, 'values': [] } + if not is_rollback: + successes = dict(model.objects.values_list('pk').filter( + test__success=True).annotate(latest=Max('test__iso__id'))) + failures = dict(model.objects.values_list('pk').filter( + test__success=False).annotate(latest=Max('test__iso__id'))) + else: + successes = dict(model.objects.values_list('pk').filter( + rollback_test_set__success=True).annotate( + latest=Max('rollback_test_set__iso__id'))) + failures = dict(model.objects.values_list('pk').filter( + rollback_test_set__success=False).annotate( + latest=Max('rollback_test_set__iso__id'))) + for value in model.objects.all(): - data = { 'value': value } - if is_rollback: - data['success'] = value.get_last_rollback_success() - data['failure'] = value.get_last_rollback_failure() - else: - data['success'] = value.get_last_success() - data['failure'] = value.get_last_failure() + data = { + 'value': value, + 'success': successes.get(value.pk), + 'failure': failures.get(value.pk), + } option['values'].append(data) return option +def options_fetch_iso(options): + '''Replaces the Iso PK with a full Iso model object in a list of options + used on the overview page. We do it this way to only have to query the Iso + table once rather than once per option.''' + # collect all necessary Iso PKs + all_pks = set() + for option in options: + all_pks.update(v['success'] for v in option['values']) + all_pks.update(v['failure'] for v in option['values']) + + all_pks.discard(None) + all_isos = Iso.objects.in_bulk(all_pks) + + for option in options: + for value in option['values']: + value['success'] = all_isos.get(value['success']) + value['failure'] = all_isos.get(value['failure']) + + return options + def test_results_overview(request): # data structure produced: # [ { option, name, is_rollback, values: [ { value, success, failure } ... ] } ... ] @@ -106,6 +138,8 @@ def test_results_overview(request): for field in fields: all_options.append(calculate_option_overview(field)) + all_options = options_fetch_iso(all_options) + context = { 'options': all_options, 'iso_url': settings.ISO_LIST_URL, @@ -114,7 +148,7 @@ def test_results_overview(request): def test_results_iso(request, iso_id): iso = get_object_or_404(Iso, pk=iso_id) - test_list = iso.test_set.all() + test_list = iso.test_set.select_related() context = { 'iso_name': iso.name, 'test_list': test_list @@ -125,10 +159,12 @@ def test_results_for(request, option, value): if option not in Test._meta.get_all_field_names(): raise Http404 option_model = getattr(Test, option).field.rel.to + option_model.verbose_name = option_model._meta.verbose_name real_value = get_object_or_404(option_model, pk=value) - test_list = real_value.test_set.order_by('-iso__name', '-pk') + test_list = real_value.test_set.select_related().order_by( + '-iso__name', '-pk') context = { - 'option': option, + 'option': option_model, 'value': real_value, 'value_id': value, 'test_list': test_list @@ -138,4 +174,17 @@ def test_results_for(request, option, value): def submit_test_thanks(request): return direct_to_template(request, "releng/thanks.html", None) +def iso_overview(request): + isos = Iso.objects.all().order_by('-pk') + successes = dict(Iso.objects.values_list('pk').filter(test__success=True).annotate(ct=Count('test'))) + failures = dict(Iso.objects.values_list('pk').filter(test__success=False).annotate(ct=Count('test'))) + for iso in isos: + iso.successes = successes.get(iso.pk, 0) + iso.failures = failures.get(iso.pk, 0) + + context = { + 'isos': isos + } + return direct_to_template(request, 'releng/iso_overview.html', context) + # vim: set ts=4 sw=4 et: |