diff options
Diffstat (limited to 'releng')
-rw-r--r-- | releng/__init__.py | 0 | ||||
-rw-r--r-- | releng/admin.py | 34 | ||||
-rw-r--r-- | releng/fixtures/architecture.json | 30 | ||||
-rw-r--r-- | releng/fixtures/bootloaders.json | 23 | ||||
-rw-r--r-- | releng/fixtures/boottype.json | 23 | ||||
-rw-r--r-- | releng/fixtures/clockchoices.json | 72 | ||||
-rw-r--r-- | releng/fixtures/filesystems.json | 23 | ||||
-rw-r--r-- | releng/fixtures/hardware.json | 44 | ||||
-rw-r--r-- | releng/fixtures/installtype.json | 30 | ||||
-rw-r--r-- | releng/fixtures/isotypes.json | 16 | ||||
-rw-r--r-- | releng/fixtures/modules.json | 86 | ||||
-rw-r--r-- | releng/fixtures/source.json | 23 | ||||
-rw-r--r-- | releng/management/__init__.py | 0 | ||||
-rw-r--r-- | releng/management/commands/__init__.py | 0 | ||||
-rw-r--r-- | releng/management/commands/syncisos.py | 61 | ||||
-rw-r--r-- | releng/migrations/0001_initial.py | 185 | ||||
-rw-r--r-- | releng/migrations/0002_release_last_modified.py | 22 | ||||
-rw-r--r-- | releng/migrations/0003_release_populate_last_modified.py | 21 | ||||
-rw-r--r-- | releng/migrations/__init__.py | 0 | ||||
-rw-r--r-- | releng/models.py | 193 | ||||
-rw-r--r-- | releng/urls.py | 30 | ||||
-rw-r--r-- | releng/views.py | 283 |
22 files changed, 1199 insertions, 0 deletions
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..9c93c4be --- /dev/null +++ b/releng/admin.py @@ -0,0 +1,34 @@ +from django.contrib import admin + +from .models import (Architecture, BootType, Bootloader, ClockChoice, + Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, + Test, Release) + +class IsoAdmin(admin.ModelAdmin): + list_display = ('name', 'created', 'active', 'removed') + list_filter = ('active', 'created') + date_hierarchy = 'created' + +class TestAdmin(admin.ModelAdmin): + list_display = ('user_name', 'user_email', 'created', 'ip_address', + 'iso', 'success') + list_filter = ('success', 'iso') + +class ReleaseAdmin(admin.ModelAdmin): + list_display = ('version', 'release_date', 'kernel_version', 'available', + 'created') + list_filter = ('available', 'release_date') + readonly_fields = ('created', 'last_modified') + + +SIMPLE_MODELS = (Architecture, BootType, Bootloader, ClockChoice, Filesystem, + HardwareType, InstallType, IsoType, Module, Source) + +for model in SIMPLE_MODELS: + admin.site.register(model) + +admin.site.register(Iso, IsoAdmin) +admin.site.register(Test, TestAdmin) +admin.site.register(Release, ReleaseAdmin) + +# 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..d2d4eb80 --- /dev/null +++ b/releng/fixtures/clockchoices.json @@ -0,0 +1,72 @@ +[ + { + "pk": 1, + "model": "releng.clockchoice", + "fields": { + "name": "default region/timezone, keep clock" + } + }, + { + "pk": 2, + "model": "releng.clockchoice", + "fields": { + "name": "default region/timezone, change clock manually (UTC)" + } + }, + { + "pk": 3, + "model": "releng.clockchoice", + "fields": { + "name": "default region/timezone, change clock with NTP (UTC)" + } + }, + { + "pk": 4, + "model": "releng.clockchoice", + "fields": { + "name": "default region/timezone, change clock manually (localtime)" + } + }, + { + "pk": 5, + "model": "releng.clockchoice", + "fields": { + "name": "default region/timezone, change clock with NTP (localtime)" + } + }, + { + "pk": 6, + "model": "releng.clockchoice", + "fields": { + "name": "update region/timezone, keep clock" + } + }, + { + "pk": 7, + "model": "releng.clockchoice", + "fields": { + "name": "update region/timezone, change clock manually (UTC)" + } + }, + { + "pk": 8, + "model": "releng.clockchoice", + "fields": { + "name": "update region/timezone, change clock with NTP (UTC)" + } + }, + { + "pk": 9, + "model": "releng.clockchoice", + "fields": { + "name": "update region/timezone, change clock manually (localtime)" + } + }, + { + "pk": 10, + "model": "releng.clockchoice", + "fields": { + "name": "update region/timezone, change clock with NTP (localtime)" + } + } +] 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..07d17f28 --- /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 (if special, 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..9d1950a5 --- /dev/null +++ b/releng/fixtures/source.json @@ -0,0 +1,23 @@ +[ + { + "pk": 1, + "model": "releng.source", + "fields": { + "name": "net install manual networking config (verify network, rc.conf, resolv.conf, mirrorlist)" + } + }, + { + "pk": 2, + "model": "releng.source", + "fields": { + "name": "net install dhcp (verify network, rc.conf, mirrorlist)" + } + }, + { + "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..f182cc33 --- /dev/null +++ b/releng/management/commands/syncisos.py @@ -0,0 +1,61 @@ +import re +import urllib +from HTMLParser import HTMLParser, HTMLParseError + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.utils.timezone import now + +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) is not None: + self.hyperlinks.append(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) + active_isos = parser.parse(settings.ISO_LIST_URL) + + 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(update_fields=('active', 'removed')) + # and then mark all other names as no longer active + 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/0001_initial.py b/releng/migrations/0001_initial.py new file mode 100644 index 00000000..b56f389d --- /dev/null +++ b/releng/migrations/0001_initial.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Architecture', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Bootloader', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='BootType', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='ClockChoice', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Filesystem', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='HardwareType', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='InstallType', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Iso', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=255)), + ('created', models.DateTimeField(editable=False)), + ('removed', models.DateTimeField(default=None, null=True, blank=True)), + ('active', models.BooleanField(default=True)), + ], + options={ + 'verbose_name': 'ISO', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='IsoType', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'verbose_name': 'ISO type', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Module', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Release', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('release_date', models.DateField(db_index=True)), + ('version', models.CharField(unique=True, max_length=50)), + ('kernel_version', models.CharField(max_length=50, blank=True)), + ('md5_sum', models.CharField(max_length=32, verbose_name=b'MD5 digest', blank=True)), + ('sha1_sum', models.CharField(max_length=40, verbose_name=b'SHA1 digest', blank=True)), + ('created', models.DateTimeField(editable=False)), + ('available', models.BooleanField(default=True)), + ('info', models.TextField(verbose_name=b'Public information', blank=True)), + ('torrent_data', models.TextField(help_text=b'base64-encoded torrent file', blank=True)), + ], + options={ + 'ordering': ('-release_date', '-version'), + 'get_latest_by': 'release_date', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Source', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Test', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('user_name', models.CharField(max_length=500)), + ('user_email', models.EmailField(max_length=75, verbose_name=b'email address')), + ('ip_address', models.GenericIPAddressField(verbose_name=b'IP address', unpack_ipv4=True)), + ('created', models.DateTimeField(editable=False)), + ('success', models.BooleanField(default=True)), + ('comments', models.TextField(null=True, blank=True)), + ('architecture', models.ForeignKey(to='releng.Architecture')), + ('boot_type', models.ForeignKey(to='releng.BootType')), + ('bootloader', models.ForeignKey(to='releng.Bootloader')), + ('clock_choice', models.ForeignKey(to='releng.ClockChoice')), + ('filesystem', models.ForeignKey(to='releng.Filesystem')), + ('hardware_type', models.ForeignKey(to='releng.HardwareType')), + ('install_type', models.ForeignKey(to='releng.InstallType')), + ('iso', models.ForeignKey(to='releng.Iso')), + ('iso_type', models.ForeignKey(to='releng.IsoType')), + ('modules', models.ManyToManyField(to='releng.Module', null=True, blank=True)), + ('rollback_filesystem', models.ForeignKey(related_name=b'rollback_test_set', blank=True, to='releng.Filesystem', null=True)), + ('rollback_modules', models.ManyToManyField(related_name=b'rollback_test_set', null=True, to='releng.Module', blank=True)), + ('source', models.ForeignKey(to='releng.Source')), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/releng/migrations/0002_release_last_modified.py b/releng/migrations/0002_release_last_modified.py new file mode 100644 index 00000000..58502452 --- /dev/null +++ b/releng/migrations/0002_release_last_modified.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('releng', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='release', + name='last_modified', + field=models.DateTimeField(default=datetime.datetime(2001, 1, 1, tzinfo=utc), editable=False), + preserve_default=False, + ), + ] diff --git a/releng/migrations/0003_release_populate_last_modified.py b/releng/migrations/0003_release_populate_last_modified.py new file mode 100644 index 00000000..ec7b6fda --- /dev/null +++ b/releng/migrations/0003_release_populate_last_modified.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + +def forwards(apps, schema_editor): + Release = apps.get_model('releng', 'Release') + Release.objects.update(last_modified=models.F('created')) + +def backwards(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('releng', '0002_release_last_modified'), + ] + + operations = [ + migrations.RunPython(forwards, backwards) + ] 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..a4af81ab --- /dev/null +++ b/releng/models.py @@ -0,0 +1,193 @@ +from base64 import b64decode +from bencode import bdecode, bencode +from datetime import datetime +import hashlib +from pytz import utc + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.db import models +from django.db.models.signals import pre_save +from django.utils.safestring import mark_safe + +from main.fields import PositiveBigIntegerField +from main.utils import set_created_field, parse_markdown + + +class IsoOption(models.Model): + name = models.CharField(max_length=200) + + def __unicode__(self): + return self.name + + class Meta: + abstract = True + + +class RollbackOption(IsoOption): + 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): + class Meta: + verbose_name = 'ISO type' + + +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('email address') + ip_address = models.GenericIPAddressField('IP address', unpack_ipv4=True) + 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(default=True) + comments = models.TextField(null=True, blank=True) + + +class Release(models.Model): + release_date = models.DateField(db_index=True) + version = models.CharField(max_length=50, unique=True) + kernel_version = models.CharField(max_length=50, blank=True) + md5_sum = models.CharField('MD5 digest', max_length=32, blank=True) + sha1_sum = models.CharField('SHA1 digest', max_length=40, blank=True) + created = models.DateTimeField(editable=False) + last_modified = models.DateTimeField(editable=False) + available = models.BooleanField(default=True) + info = models.TextField('Public information', blank=True) + torrent_data = models.TextField(blank=True, + help_text="base64-encoded torrent file") + + class Meta: + get_latest_by = 'release_date' + ordering = ('-release_date', '-version') + + def __unicode__(self): + return self.version + + def get_absolute_url(self): + return reverse('releng-release-detail', args=[self.version]) + + def dir_path(self): + return "iso/%s/" % self.version + + def iso_url(self): + return "iso/%s/%s-%s-dual.iso" % (self.version, settings.BRANDING_SLUG, self.version) + + def magnet_uri(self): + query = [ + ('dn', "%s-%s-dual.iso" % (settings.BRANDING_SLUG, self.version)), + ] + if settings.TORRENT_TRACKERS: + query.extend(('tr', uri) for uri in settings.TORRENT_TRACKERS) + metadata = self.torrent() + if metadata and 'info_hash' in metadata: + query.insert(0, ('xt', "urn:btih:%s" % metadata['info_hash'])) + return "magnet:?%s" % '&'.join(['%s=%s' % (k, v) for k, v in query]) + + def info_html(self): + return mark_safe(parse_markdown(self.info)) + + def torrent(self): + try: + data = b64decode(self.torrent_data.encode('utf-8')) + except TypeError: + return None + if not data: + return None + data = bdecode(data) + # transform the data into a template-friendly dict + info = data.get('info', {}) + metadata = { + 'comment': data.get('comment', None), + 'created_by': data.get('created by', None), + 'creation_date': None, + 'announce': data.get('announce', None), + 'file_name': info.get('name', None), + 'file_length': info.get('length', None), + 'piece_count': len(info.get('pieces', '')) / 20, + 'piece_length': info.get('piece length', None), + 'url_list': data.get('url-list', []), + 'info_hash': None, + } + if 'creation date' in data: + created= datetime.utcfromtimestamp(data['creation date']) + metadata['creation_date'] = created.replace(tzinfo=utc) + if info: + metadata['info_hash'] = hashlib.sha1(bencode(info)).hexdigest() + + return metadata + + +for model in (Iso, Test, Release): + pre_save.connect(set_created_field, sender=model, + 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..ca76eb25 --- /dev/null +++ b/releng/urls.py @@ -0,0 +1,30 @@ +from django.conf.urls import include, patterns + +from .views import ReleaseListView, ReleaseDetailView + +feedback_patterns = 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'), + (r'^iso/overview/$', 'iso_overview', {}, 'releng-iso-overview'), +) + +releases_patterns = patterns('releng.views', + (r'^$', + ReleaseListView.as_view(), {}, 'releng-release-list'), + (r'^json/$', + 'releases_json', {}, 'releng-release-list-json'), + (r'^(?P<version>[-.\w]+)/$', + ReleaseDetailView.as_view(), {}, 'releng-release-detail'), + (r'^(?P<version>[-.\w]+)/torrent/$', + 'release_torrent', {}, 'releng-release-torrent'), +) + +urlpatterns = patterns('', + (r'^feedback/', include(feedback_patterns)), + (r'^releases/', include(releases_patterns)), +) + +# vim: set ts=4 sw=4 et: diff --git a/releng/views.py b/releng/views.py new file mode 100644 index 00000000..0fb55b29 --- /dev/null +++ b/releng/views.py @@ -0,0 +1,283 @@ +from base64 import b64decode +import json + +from django import forms +from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder +from django.core.urlresolvers import reverse +from django.db.models import Count, Max +from django.http import Http404, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.views.generic import DetailView, ListView + +from .models import (Architecture, BootType, Bootloader, ClockChoice, + Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, + Test, Release) + + +def standard_field(model, empty_label=None, help_text=None, required=True): + return forms.ModelChoiceField(queryset=model.objects.all(), + widget=forms.RadioSelect(), empty_label=empty_label, + help_text=help_text, required=required) + + +class TestForm(forms.ModelForm): + iso = forms.ModelChoiceField(queryset=Iso.objects.filter( + active=True).order_by('-id')) + 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="Verify /etc/fstab, `df -hT` output and commands like " + "lvdisplay for special modules.") + modules = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), + widget=forms.CheckboxSelectMultiple(), required=False) + bootloader = standard_field(Bootloader, + help_text="Verify that the entries in the bootloader config " + "looks OK.") + rollback_filesystem = standard_field(Filesystem, + help_text="If you did a rollback followed by a new attempt to " + "setup your blockdevices/filesystems, select which option you " + "took here.", + empty_label="N/A (did not rollback)", required=False) + rollback_modules = forms.ModelMultipleChoiceField( + queryset=Module.objects.all(), + help_text="If you did a rollback followed by a new attempt to " + "setup your blockdevices/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 ran into problems please create a ticket on <a " + "href=\""+settings.BUGTRACKER_RELENG_URL+"\">the " + "bugtracker</a> (or check that one already exists) and link to " + "it 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() + form.save_m2m() + return redirect('releng-test-thanks') + else: + form = TestForm() + + context = {'form': form} + return render(request, 'releng/add.html', context) + + +def calculate_option_overview(field_name): + field = Test._meta.get_field(field_name) + model = field.rel.to + is_rollback = field_name.startswith('rollback_') + option = { + 'option': model, + 'field_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, + '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 } ... ] + # } + # ... + # ] + all_options = [] + fields = ['architecture', 'iso_type', 'boot_type', 'hardware_type', + 'install_type', 'source', 'clock_choice', 'filesystem', 'modules', + 'bootloader', 'rollback_filesystem', 'rollback_modules'] + 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, + } + return render(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.select_related() + context = { + 'iso_name': iso.name, + 'test_list': test_list + } + return render(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 + 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.select_related().order_by( + '-iso__name', '-pk') + context = { + 'option': option_model, + 'value': real_value, + 'value_id': value, + 'test_list': test_list + } + return render(request, 'releng/result_list.html', context) + + +def submit_test_thanks(request): + return render(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) + + # only show "useful" rows, currently active ISOs or those with results + isos = [iso for iso in isos if + iso.active is True or iso.successes > 0 or iso.failures > 0] + + context = { + 'isos': isos + } + return render(request, 'releng/iso_overview.html', context) + + +class ReleaseListView(ListView): + model = Release + + +class ReleaseDetailView(DetailView): + model = Release + slug_field = 'version' + slug_url_kwarg = 'version' + + +def release_torrent(request, version): + release = get_object_or_404(Release, version=version) + if not release.torrent_data: + raise Http404 + data = b64decode(release.torrent_data.encode('utf-8')) + response = HttpResponse(data, content_type='application/x-bittorrent') + # TODO: this is duplicated from Release.iso_url() + filename = '%s-%s-dual.iso.torrent' % (settings.BRANDING_SLUG, release.version) + response['Content-Disposition'] = 'attachment; filename=%s' % filename + return response + + +class ReleaseJSONEncoder(DjangoJSONEncoder): + release_attributes = ('release_date', 'version', 'kernel_version', + 'created', 'md5_sum', 'sha1_sum') + + def default(self, obj): + if isinstance(obj, Release): + data = {attr: getattr(obj, attr) or None + for attr in self.release_attributes} + data['available'] = obj.available + data['iso_url'] = '/' + obj.iso_url() + data['magnet_uri'] = obj.magnet_uri() + data['torrent_url'] = reverse('releng-release-torrent', args=[obj.version]) + data['info'] = obj.info_html() + torrent_data = obj.torrent() + if torrent_data: + torrent_data.pop('url_list', None) + data['torrent'] = torrent_data + return data + return super(ReleaseJSONEncoder, self).default(obj) + + +def releases_json(request): + releases = Release.objects.all() + try: + latest_version = Release.objects.filter(available=True).values_list( + 'version', flat=True).latest() + except Release.DoesNotExist: + latest_version = None + + data = { + 'version': 1, + 'releases': list(releases), + 'latest_version': latest_version, + } + to_json = json.dumps(data, ensure_ascii=False, cls=ReleaseJSONEncoder) + response = HttpResponse(to_json, content_type='application/json') + return response + +# vim: set ts=4 sw=4 et: |