diff options
29 files changed, 1047 insertions, 1 deletions
diff --git a/media/archweb.css b/media/archweb.css index 4b0b9e89..504c8270 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -260,6 +260,11 @@ ul.admin-actions li { display: inline; padding-left: 1.5em; } #dev-signoffs .signoff-no { color: red; } #dev-signoffs .signed-username { color: #888; margin-left: 0.5em; } +/* iso testing feedback form */ +#releng-feedback label { width: auto; display: inline; font-weight: normal; } +#releng-feedback ul { padding-left: 1em; } +#releng-feedback li { list-style: none; } + /* highlight current website in the navbar */ #archnavbar.anb-home ul li#anb-home a { color: white !important; } #archnavbar.anb-packages ul li#anb-packages a { color: white !important; } diff --git a/releng/__init__.py b/releng/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/releng/__init__.py diff --git a/releng/admin.py b/releng/admin.py new file mode 100644 index 00000000..10acaa98 --- /dev/null +++ b/releng/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from .models import (Architecture, BootType, Bootloader, ClockChoice, + Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source) + +admin.site.register(Iso) +admin.site.register(Architecture) +admin.site.register(IsoType) +admin.site.register(BootType) +admin.site.register(HardwareType) +admin.site.register(InstallType) +admin.site.register(Source) +admin.site.register(ClockChoice) +admin.site.register(Filesystem) +admin.site.register(Module) +admin.site.register(Bootloader) + +# vim: set ts=4 sw=4 et: diff --git a/releng/fixtures/architecture.json b/releng/fixtures/architecture.json new file mode 100644 index 00000000..0bf9b8bf --- /dev/null +++ b/releng/fixtures/architecture.json @@ -0,0 +1,30 @@ +[ + { + "pk": 1, + "model": "releng.architecture", + "fields": { + "name": "dual, option i686" + } + }, + { + "pk": 2, + "model": "releng.architecture", + "fields": { + "name": "dual, option x86_64" + } + }, + { + "pk": 3, + "model": "releng.architecture", + "fields": { + "name": "i686" + } + }, + { + "pk": 4, + "model": "releng.architecture", + "fields": { + "name": "x86_64" + } + } +] diff --git a/releng/fixtures/bootloaders.json b/releng/fixtures/bootloaders.json new file mode 100644 index 00000000..bee02f2b --- /dev/null +++ b/releng/fixtures/bootloaders.json @@ -0,0 +1,23 @@ +[ + { + "pk": 1, + "model": "releng.bootloader", + "fields": { + "name": "grub" + } + }, + { + "pk": 2, + "model": "releng.bootloader", + "fields": { + "name": "syslinux" + } + }, + { + "pk": 3, + "model": "releng.bootloader", + "fields": { + "name": "other/manual" + } + } +] diff --git a/releng/fixtures/boottype.json b/releng/fixtures/boottype.json new file mode 100644 index 00000000..ed4636eb --- /dev/null +++ b/releng/fixtures/boottype.json @@ -0,0 +1,23 @@ +[ + { + "pk": 1, + "model": "releng.boottype", + "fields": { + "name": "optical" + } + }, + { + "pk": 2, + "model": "releng.boottype", + "fields": { + "name": "usb" + } + }, + { + "pk": 3, + "model": "releng.boottype", + "fields": { + "name": "pxe" + } + } +] diff --git a/releng/fixtures/clockchoices.json b/releng/fixtures/clockchoices.json new file mode 100644 index 00000000..f328801a --- /dev/null +++ b/releng/fixtures/clockchoices.json @@ -0,0 +1,23 @@ +[ + { + "pk": 1, + "model": "releng.clockchoice", + "fields": { + "name": "unchanged" + } + }, + { + "pk": 2, + "model": "releng.clockchoice", + "fields": { + "name": "configured manually" + } + }, + { + "pk": 3, + "model": "releng.clockchoice", + "fields": { + "name": "NTP" + } + } +] diff --git a/releng/fixtures/filesystems.json b/releng/fixtures/filesystems.json new file mode 100644 index 00000000..208f5c73 --- /dev/null +++ b/releng/fixtures/filesystems.json @@ -0,0 +1,23 @@ +[ + { + "pk": 1, + "model": "releng.filesystem", + "fields": { + "name": "autoprepare" + } + }, + { + "pk": 2, + "model": "releng.filesystem", + "fields": { + "name": "manual" + } + }, + { + "pk": 3, + "model": "releng.filesystem", + "fields": { + "name": "from config file" + } + } +] diff --git a/releng/fixtures/hardware.json b/releng/fixtures/hardware.json new file mode 100644 index 00000000..a2bb9ec0 --- /dev/null +++ b/releng/fixtures/hardware.json @@ -0,0 +1,44 @@ +[ + { + "pk": 1, + "model": "releng.hardwaretype", + "fields": { + "name": "virtualbox" + } + }, + { + "pk": 2, + "model": "releng.hardwaretype", + "fields": { + "name": "qemu" + } + }, + { + "pk": 3, + "model": "releng.hardwaretype", + "fields": { + "name": "intel i686" + } + }, + { + "pk": 4, + "model": "releng.hardwaretype", + "fields": { + "name": "intel x86_64" + } + }, + { + "pk": 5, + "model": "releng.hardwaretype", + "fields": { + "name": "amd i686" + } + }, + { + "pk": 6, + "model": "releng.hardwaretype", + "fields": { + "name": "amd x86_64" + } + } +] diff --git a/releng/fixtures/installtype.json b/releng/fixtures/installtype.json new file mode 100644 index 00000000..7fa21fc1 --- /dev/null +++ b/releng/fixtures/installtype.json @@ -0,0 +1,30 @@ +[ + { + "pk": 1, + "model": "releng.installtype", + "fields": { + "name": "interactive install" + } + }, + { + "pk": 2, + "model": "releng.installtype", + "fields": { + "name": "automatic install generic example" + } + }, + { + "pk": 3, + "model": "releng.installtype", + "fields": { + "name": "automatic install fancy example" + } + }, + { + "pk": 4, + "model": "releng.installtype", + "fields": { + "name": "automatic install custom config (specify in comments)" + } + } +] diff --git a/releng/fixtures/isotypes.json b/releng/fixtures/isotypes.json new file mode 100644 index 00000000..a529b181 --- /dev/null +++ b/releng/fixtures/isotypes.json @@ -0,0 +1,16 @@ +[ + { + "pk": 1, + "model": "releng.isotype", + "fields": { + "name": "core" + } + }, + { + "pk": 2, + "model": "releng.isotype", + "fields": { + "name": "net" + } + } +] diff --git a/releng/fixtures/modules.json b/releng/fixtures/modules.json new file mode 100644 index 00000000..9cdf1a8d --- /dev/null +++ b/releng/fixtures/modules.json @@ -0,0 +1,86 @@ +[ + { + "pk": 1, + "model": "releng.module", + "fields": { + "name": "lvm2" + } + }, + { + "pk": 2, + "model": "releng.module", + "fields": { + "name": "dm_crypt" + } + }, + { + "pk": 3, + "model": "releng.module", + "fields": { + "name": "softraid" + } + }, + { + "pk": 4, + "model": "releng.module", + "fields": { + "name": "nilfs2" + } + }, + { + "pk": 5, + "model": "releng.module", + "fields": { + "name": "btrfs" + } + }, + { + "pk": 6, + "model": "releng.module", + "fields": { + "name": "ext2" + } + }, + { + "pk": 7, + "model": "releng.module", + "fields": { + "name": "ext3" + } + }, + { + "pk": 8, + "model": "releng.module", + "fields": { + "name": "ext4" + } + }, + { + "pk": 9, + "model": "releng.module", + "fields": { + "name": "swap" + } + }, + { + "pk": 10, + "model": "releng.module", + "fields": { + "name": "xfs" + } + }, + { + "pk": 11, + "model": "releng.module", + "fields": { + "name": "jfs" + } + }, + { + "pk": 12, + "model": "releng.module", + "fields": { + "name": "reiserFS" + } + } +] diff --git a/releng/fixtures/source.json b/releng/fixtures/source.json new file mode 100644 index 00000000..2622f4c1 --- /dev/null +++ b/releng/fixtures/source.json @@ -0,0 +1,23 @@ +[ + { + "pk": 1, + "model": "releng.source", + "fields": { + "name": "net install manual networking config (Check that it works + rc.conf, resolv.conf, mirrorlist)" + } + }, + { + "pk": 2, + "model": "releng.source", + "fields": { + "name": "net install dhcp (Check that it works + rc.conf)" + } + }, + { + "pk": 3, + "model": "releng.source", + "fields": { + "name": "core" + } + } +] diff --git a/releng/management/__init__.py b/releng/management/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/releng/management/__init__.py diff --git a/releng/management/commands/__init__.py b/releng/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/releng/management/commands/__init__.py diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py new file mode 100644 index 00000000..247b01cd --- /dev/null +++ b/releng/management/commands/syncisos.py @@ -0,0 +1,48 @@ +import re +import urllib +from HTMLParser import HTMLParser, HTMLParseError + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from releng.models import Iso + +class IsoListParser(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + + self.hyperlinks = [] + self.url_re = re.compile('(?!\.{2})/$') + + def handle_starttag(self, tag, attrs): + if tag == 'a': + for name, value in attrs: + if name == "href": + if value != '../' and self.url_re.search(value) != None: + self.hyperlinks.append(value[:len(value)-1]) + + def parse(self, url): + try: + remote_file = urllib.urlopen(url) + data = remote_file.read() + remote_file.close() + self.feed(data) + self.close() + return self.hyperlinks + except HTMLParseError: + raise CommandError('Couldn\'t parse "%s"' % url) + +class Command(BaseCommand): + 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) + new_isos = parser.parse(settings.ISO_LIST_URL) + + for iso in new_isos: + if iso not in isonames: + new = Iso(name=iso) + new.save() + +# vim: set ts=4 sw=4 et: diff --git a/releng/migrations/0001_initial.py b/releng/migrations/0001_initial.py new file mode 100644 index 00000000..91fab8b7 --- /dev/null +++ b/releng/migrations/0001_initial.py @@ -0,0 +1,258 @@ +# 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): + + # Adding model 'Iso' + db.create_table('releng_iso', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal('releng', ['Iso']) + + # Adding model 'Architecture' + db.create_table('releng_architecture', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['Architecture']) + + # Adding model 'IsoType' + db.create_table('releng_isotype', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['IsoType']) + + # Adding model 'BootType' + db.create_table('releng_boottype', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['BootType']) + + # Adding model 'HardwareType' + db.create_table('releng_hardwaretype', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['HardwareType']) + + # Adding model 'InstallType' + db.create_table('releng_installtype', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['InstallType']) + + # Adding model 'Source' + db.create_table('releng_source', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['Source']) + + # Adding model 'ClockChoice' + db.create_table('releng_clockchoice', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['ClockChoice']) + + # Adding model 'Filesystem' + db.create_table('releng_filesystem', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['Filesystem']) + + # Adding model 'Module' + db.create_table('releng_module', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['Module']) + + # Adding model 'Bootloader' + db.create_table('releng_bootloader', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=200)), + )) + db.send_create_signal('releng', ['Bootloader']) + + # Adding model 'Test' + db.create_table('releng_test', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user_name', self.gf('django.db.models.fields.CharField')(max_length=500)), + ('user_email', self.gf('django.db.models.fields.EmailField')(max_length=75)), + ('ip_address', self.gf('django.db.models.fields.IPAddressField')(max_length=15)), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('iso', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Iso'])), + ('architecture', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Architecture'])), + ('iso_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.IsoType'])), + ('boot_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.BootType'])), + ('hardware_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.HardwareType'])), + ('install_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.InstallType'])), + ('source', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Source'])), + ('clock_choice', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.ClockChoice'])), + ('filesystem', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Filesystem'])), + ('bootloader', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['releng.Bootloader'])), + ('rollback_filesystem', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='rollback_test_set', null=True, to=orm['releng.Filesystem'])), + ('success', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('comments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('releng', ['Test']) + + # Adding M2M table for field modules on 'Test' + db.create_table('releng_test_modules', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('test', models.ForeignKey(orm['releng.test'], null=False)), + ('module', models.ForeignKey(orm['releng.module'], null=False)) + )) + db.create_unique('releng_test_modules', ['test_id', 'module_id']) + + # Adding M2M table for field rollback_modules on 'Test' + db.create_table('releng_test_rollback_modules', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('test', models.ForeignKey(orm['releng.test'], null=False)), + ('module', models.ForeignKey(orm['releng.module'], null=False)) + )) + db.create_unique('releng_test_rollback_modules', ['test_id', 'module_id']) + + + def backwards(self, orm): + + # Deleting model 'Iso' + db.delete_table('releng_iso') + + # Deleting model 'Architecture' + db.delete_table('releng_architecture') + + # Deleting model 'IsoType' + db.delete_table('releng_isotype') + + # Deleting model 'BootType' + db.delete_table('releng_boottype') + + # Deleting model 'HardwareType' + db.delete_table('releng_hardwaretype') + + # Deleting model 'InstallType' + db.delete_table('releng_installtype') + + # Deleting model 'Source' + db.delete_table('releng_source') + + # Deleting model 'ClockChoice' + db.delete_table('releng_clockchoice') + + # Deleting model 'Filesystem' + db.delete_table('releng_filesystem') + + # Deleting model 'Module' + db.delete_table('releng_module') + + # Deleting model 'Bootloader' + db.delete_table('releng_bootloader') + + # Deleting model 'Test' + db.delete_table('releng_test') + + # Removing M2M table for field modules on 'Test' + db.delete_table('releng_test_modules') + + # Removing M2M table for field rollback_modules on 'Test' + db.delete_table('releng_test_rollback_modules') + + + 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'}) + }, + '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/migrations/__init__.py b/releng/migrations/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/releng/migrations/__init__.py diff --git a/releng/models.py b/releng/models.py new file mode 100644 index 00000000..dc297d5b --- /dev/null +++ b/releng/models.py @@ -0,0 +1,121 @@ +from datetime import datetime + +from django.db import models +from django.db.models import Max + +class IsoOption(models.Model): + name = models.CharField(max_length=200) + + 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) + active = models.BooleanField(default=True) + + def __unicode__(self): + return self.name + +class Architecture(IsoOption): + pass + +class IsoType(IsoOption): + pass + +class BootType(IsoOption): + pass + +class HardwareType(IsoOption): + pass + +class InstallType(IsoOption): + pass + +class Source(IsoOption): + pass + +class ClockChoice(IsoOption): + pass + +class Filesystem(RollbackOption): + pass + +class Module(RollbackOption): + pass + +class Bootloader(IsoOption): + pass + +class Test(models.Model): + user_name = models.CharField(max_length=500) + user_email = models.EmailField() + ip_address = models.IPAddressField() + created = models.DateTimeField(editable=False) + + iso = models.ForeignKey(Iso) + architecture = models.ForeignKey(Architecture) + iso_type = models.ForeignKey(IsoType) + boot_type = models.ForeignKey(BootType) + hardware_type = models.ForeignKey(HardwareType) + install_type = models.ForeignKey(InstallType) + source = models.ForeignKey(Source) + clock_choice = models.ForeignKey(ClockChoice) + filesystem = models.ForeignKey(Filesystem) + modules = models.ManyToManyField(Module, null=True, blank=True) + bootloader = models.ForeignKey(Bootloader) + rollback_filesystem = models.ForeignKey(Filesystem, + related_name="rollback_test_set", null=True, blank=True) + rollback_modules = models.ManyToManyField(Module, + related_name="rollback_test_set", null=True, blank=True) + + success = models.BooleanField() + comments = models.TextField(null=True, blank=True) + +def set_created_field(sender, **kwargs): + # We use this same callback for both Isos and Tests + obj = kwargs['instance'] + if not obj.created: + obj.created = datetime.utcnow() + +from django.db.models.signals import pre_save + +pre_save.connect(set_created_field, sender=Iso, + dispatch_uid="releng.models") +pre_save.connect(set_created_field, sender=Test, + dispatch_uid="releng.models") + +# vim: set ts=4 sw=4 et: diff --git a/releng/urls.py b/releng/urls.py new file mode 100644 index 00000000..f76a9593 --- /dev/null +++ b/releng/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import patterns + +urlpatterns = patterns('releng.views', + (r'^$', 'test_results_overview', {}, 'releng-test-overview'), + (r'^submit/$', 'submit_test_result', {}, 'releng-test-submit'), + (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'), +) + +# vim: set ts=4 sw=4 et: diff --git a/releng/views.py b/releng/views.py new file mode 100644 index 00000000..5cffb2f5 --- /dev/null +++ b/releng/views.py @@ -0,0 +1,132 @@ +from django import forms +from django.conf import settings +from django.http import Http404 +from django.shortcuts import get_object_or_404, redirect +from django.views.generic.simple import direct_to_template + +from .models import (Architecture, BootType, Bootloader, ClockChoice, + Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, + Test) + +def standard_field(model, help_text=None): + return forms.ModelChoiceField(queryset=model.objects.all(), + widget=forms.RadioSelect(), empty_label=None, help_text=help_text) + +class TestForm(forms.ModelForm): + iso = forms.ModelChoiceField(queryset=Iso.objects.filter(active=True)) + architecture = standard_field(Architecture) + iso_type = standard_field(IsoType) + boot_type = standard_field(BootType) + hardware_type = standard_field(HardwareType) + install_type = standard_field(InstallType) + source = standard_field(Source) + clock_choice = standard_field(ClockChoice) + filesystem = standard_field(Filesystem, + help_text="Check the installed system, including fstab.") + modules = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), + help_text="", widget=forms.CheckboxSelectMultiple(), required=False) + bootloader = standard_field(Bootloader) + rollback_filesystem = forms.ModelChoiceField(queryset=Filesystem.objects.all(), + help_text="If you did a rollback followed by a new attempt to setup " \ + "your lockdevices/filesystems, select which option you took here.", + widget=forms.RadioSelect(), required=False) + rollback_modules = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), + help_text="If you did a rollback followed b a new attempt to setup " \ + "your lockdevices/filesystems, select which option you took here.", + widget=forms.CheckboxSelectMultiple(), required=False) + success = forms.BooleanField(help_text="Only check this if everything went fine. " \ + "If you you ran into any errors please specify them in the " \ + "comments.", required=False) + website = forms.CharField(label='', + widget=forms.TextInput(attrs={'style': 'display:none;'}), required=False) + + class Meta: + model = Test + fields = ("user_name", "user_email", "iso", "architecture", + "iso_type", "boot_type", "hardware_type", + "install_type", "source", "clock_choice", "filesystem", + "modules", "bootloader", "rollback_filesystem", + "rollback_modules", "success", "comments") + widgets = { + "modules": forms.CheckboxSelectMultiple(), + } + +def submit_test_result(request): + if request.POST: + form = TestForm(request.POST) + if form.is_valid() and request.POST['website'] == '': + test = form.save(commit=False) + test.ip_address = request.META.get("REMOTE_ADDR", None) + test.save() + return redirect('releng-test-thanks') + else: + form = TestForm() + + context = {'form': form} + return direct_to_template(request, 'releng/add.html', context) + +def calculate_option_overview(model, is_rollback=False): + option = { + 'option': model, + 'name': model._meta.verbose_name, + 'is_rollback': is_rollback, + 'values': [] + } + 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() + option['values'].append(data) + + return option + +def test_results_overview(request): + # data structure produced: + # [ { option, name, is_rollback, values: [ { value, success, failure } ... ] } ... ] + all_options = [] + models = [ Architecture, IsoType, BootType, HardwareType, InstallType, + Source, ClockChoice, Filesystem, Module, Bootloader ] + for model in models: + all_options.append(calculate_option_overview(model)) + # now handle rollback options + for model in [ Filesystem, Module ]: + all_options.append(calculate_option_overview(model, True)) + + print all_options + context = { + 'options': all_options, + 'iso_url': settings.ISO_LIST_URL, + } + return direct_to_template(request, 'releng/results.html', context) + +def test_results_iso(request, iso_id): + iso = get_object_or_404(Iso, pk=iso_id) + test_list = iso.test_set.all() + context = { + 'iso_name': iso.name, + 'test_list': test_list + } + return direct_to_template(request, 'releng/result_list.html', context) + +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 + real_value = get_object_or_404(option_model, pk=value) + test_list = real_value.test_set.order_by("iso__name", "pk") + context = { + 'option': option, + 'value': real_value, + 'value_id': value, + 'test_list': test_list + } + return direct_to_template(request, 'releng/result_list.html', context) + +def submit_test_thanks(request): + return direct_to_template(request, "releng/thanks.html", None) + +# vim: set ts=4 sw=4 et: diff --git a/settings.py b/settings.py index 1d26d9eb..db1c93d3 100644 --- a/settings.py +++ b/settings.py @@ -104,6 +104,7 @@ INSTALLED_APPS = ( 'devel', 'public', 'south', # database migration support + 'releng', ) ## Import local settings @@ -123,4 +124,7 @@ if DEBUG_TOOLBAR: INSTALLED_APPS = list(INSTALLED_APPS) + [ 'debug_toolbar' ] +# URL to fetch a current list of available ISOs +ISO_LIST_URL = 'http://releng.archlinux.org/isos/' + # vim: set ts=4 sw=4 et: diff --git a/templates/public/index.html b/templates/public/index.html index 132412f6..5b79a1fe 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -119,6 +119,8 @@ title="Arch communities in your native language">International Communities</a></li> <li><a href="https://wiki.archlinux.org/index.php/Related_Projects" title="Projects that are in some way related to Arch Linux">Related Projects</a></li> + <li><a href="{% url releng-test-overview %}" + title="Releng Testbuild Feedback">Releng Testbuild Feedback</a></li> </ul> <h4>Support</h4> diff --git a/templates/releng/add.html b/templates/releng/add.html new file mode 100644 index 00000000..3678532d --- /dev/null +++ b/templates/releng/add.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block title %}Arch Linux - Test Result Entry{% endblock %} + +{% block content %} +<div class="box"> + <h2>Arch Releng Testbuild Feedback Entry</h2> + + <p>This page allows you to submit feedback after testing and using a + release engineering install ISO. If you do not currently have feedback to + submit, you may want to take a look at the current + <a href="{% url releng-test-overview %}">results page</a>.</p> + + <div id="releng-feedback"> <form action="" method="post">{% csrf_token %} + {{ form.as_p }} + <input type="submit" value="Submit" /> + </form> + </div> +</div> +{% endblock %} diff --git a/templates/releng/result_list.html b/templates/releng/result_list.html new file mode 100644 index 00000000..b3ae025b --- /dev/null +++ b/templates/releng/result_list.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block content %} +<div class="box"> + <h2>Results for: + {% if option %}{{ option|title }}: {{ value }}{% endif %} + {{ iso_name|default:"" }} + </h2> + + <p><a href="{% url releng-test-overview %}">Go back to testing results</a></p> + + <table id="releng-result" class="results"> + <thead> + <tr> + <th>Iso</th> + <th>Submitted By</th> + <th>Date Submitted</th> + <th>Success</th> + </tr> + </thead> + <tbody> + {% for test in test_list %} + <tr> + <td>{{ test.iso.name }}</td> + <td>{{ test.user_name }}</td> + <td>{{ test.created|date }}</td> + <td>{{ test.success|yesno }}</td> + </tr> + {% endfor %} + </tbody> + </table> +</div> +{% load cdn %}{% jquery %} +<script type="text/javascript" src="/media/jquery.tablesorter.min.js"></script> +<script type="text/javascript" src="/media/archweb.js"></script> +<script type="text/javascript"> +$(document).ready(function() { + $(".results:not(:has(tbody tr.empty))").tablesorter({widgets: ['zebra']}); +}); +</script> +{% endblock %} diff --git a/templates/releng/result_section.html b/templates/releng/result_section.html new file mode 100644 index 00000000..52f03339 --- /dev/null +++ b/templates/releng/result_section.html @@ -0,0 +1,26 @@ +<tr> + <td><h3>{% if option.is_rollback %}Rollback: {% endif %}{{ option.name|title }}</h3></td> +</tr> +{% for item in option.values %} +<tr> + <td> + <a href="{% url releng-results-for option.name|lower item.value.pk %}"> + {{ item.value.name|lower }} + </a> + </td> + <td> + {% if item.success %} + <a href="{% url releng-results-iso item.success.pk %}"> + {{ item.success.name }} + </a> + {% else %}Never succeeded{% endif %} + </td> + <td> + {% if item.failure %} + <a href="{% url releng-results-iso item.failure.pk %}"> + {{ item.failure.name }} + </a> + {% else %}Never failed{% endif %} + </td> +</tr> +{% endfor %} diff --git a/templates/releng/results.html b/templates/releng/results.html new file mode 100644 index 00000000..4aca1f10 --- /dev/null +++ b/templates/releng/results.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block title %}Arch Linux - Release Engineering Testbuild Results{% endblock %} + +{% block content %} +<div class="box"> + <h2>Release Engineering Testbuild Results</h2> + + <p>This is a overview screen showing a test results matrix of release + engineering produced ISOs. Various options and configurations are shown + with last success and last failure results, if known. To help improve ISO + quality, you are encouraged to <a href="{% url releng-test-submit %}">give feedback</a> + if you have tested and used any ISOs. Both successful and failed results + are encouraged and welcome.</p> + + <p>All ISOs referenced on this page are available from + <a href="{{ iso_url }}">{{ iso_url }}</a>.</p> + + <table> + {% for option in options %} + {% include "releng/result_section.html" %} + {% endfor %} + </table> +</div> +{% endblock %} diff --git a/templates/releng/thanks.html b/templates/releng/thanks.html new file mode 100644 index 00000000..b261426d --- /dev/null +++ b/templates/releng/thanks.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Arch Linux - Feedback - Thanks!{% endblock %} + +{% block content %} +<div class="box"> + <h2>Thanks!</h2> + <p>Thank you for taking the time to give us this information! + Your results have been succesfully added to our database.</p> + <p>You can now <a href="{% url releng-test-overview %}">go back to the results</a>, + or <a href="{% url releng-test-submit %}">give more feedback</a>.</p> +</div> +{% endblock %} @@ -73,6 +73,7 @@ urlpatterns += patterns('', (r'^mirrors/', include('mirrors.urls')), (r'^news/', include('news.urls')), (r'^packages/', include('packages.urls')), + (r'^releng/', include('releng.urls')), (r'^todo/', include('todolists.urls')), (r'^opensearch/packages/$', 'packages.views.opensearch', {}, 'opensearch-packages'), @@ -81,7 +82,7 @@ urlpatterns += patterns('', if settings.DEBUG == True: urlpatterns += patterns('', - (r'^media/(.*)$', 'django.views.static.serve', + (r'^media/(.*)$', 'django.views.static.serve', {'document_root': os.path.join(settings.DEPLOY_PATH, 'media')})) # vim: set ts=4 sw=4 et: |