From c0bf9e20660cfae7ea8994472555bba23398b598 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 09:19:48 -0500 Subject: Remove custom utc_now() function, use django.utils.timezone.now() This was around from the time when we handled timezones sanely and Django did not; now that we are on 1.4 we no longer need our own code to handle this. Signed-off-by: Dan McGee --- releng/management/commands/syncisos.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'releng') diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py index 62f005ff..223c771b 100644 --- a/releng/management/commands/syncisos.py +++ b/releng/management/commands/syncisos.py @@ -4,8 +4,8 @@ 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 main.utils import utc_now from releng.models import Iso @@ -54,9 +54,8 @@ class Command(BaseCommand): existing.active = True existing.removed = None existing.save() - now = utc_now() # 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) + active=False, removed=now()) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From 76c37ce3acc7a4af0271c7535d4a33042f7749b5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 24 Jul 2012 09:35:55 -0500 Subject: Replace deprecated direct_to_template() with render() shortcut Now that Django actually provides a concise way to use a RequestContext object without instantiating it, we can use that rather than the old function-based generic view that worked well to do the same. Additionally, these function-based generic views will be gone in Django 1.5, so might as well make the move now. Signed-off-by: Dan McGee --- releng/views.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'releng') diff --git a/releng/views.py b/releng/views.py index fc81d410..a1bc3b81 100644 --- a/releng/views.py +++ b/releng/views.py @@ -2,8 +2,7 @@ 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 +from django.shortcuts import get_object_or_404, redirect, render from .models import (Architecture, BootType, Bootloader, ClockChoice, Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, @@ -73,7 +72,7 @@ def submit_test_result(request): form = TestForm() context = {'form': form} - return direct_to_template(request, 'releng/add.html', context) + return render(request, 'releng/add.html', context) def calculate_option_overview(field_name): field = Test._meta.get_field(field_name) @@ -145,7 +144,7 @@ def test_results_overview(request): 'options': all_options, 'iso_url': settings.ISO_LIST_URL, } - return direct_to_template(request, 'releng/results.html', context) + return render(request, 'releng/results.html', context) def test_results_iso(request, iso_id): iso = get_object_or_404(Iso, pk=iso_id) @@ -154,7 +153,7 @@ def test_results_iso(request, iso_id): 'iso_name': iso.name, 'test_list': test_list } - return direct_to_template(request, 'releng/result_list.html', context) + 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(): @@ -170,10 +169,10 @@ def test_results_for(request, option, value): 'value_id': value, 'test_list': test_list } - return direct_to_template(request, 'releng/result_list.html', context) + return render(request, 'releng/result_list.html', context) def submit_test_thanks(request): - return direct_to_template(request, "releng/thanks.html", None) + return render(request, "releng/thanks.html", None) def iso_overview(request): isos = Iso.objects.all().order_by('-pk') @@ -192,6 +191,6 @@ def iso_overview(request): context = { 'isos': isos } - return direct_to_template(request, 'releng/iso_overview.html', context) + return render(request, 'releng/iso_overview.html', context) # vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From bc5dab41a82ff980c51dd4e408983e32886721bb Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 13 Aug 2012 10:11:20 -0500 Subject: releng views code cleanup Signed-off-by: Dan McGee --- releng/views.py | 54 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 17 deletions(-) (limited to 'releng') diff --git a/releng/views.py b/releng/views.py index a1bc3b81..67b3cb4a 100644 --- a/releng/views.py +++ b/releng/views.py @@ -8,11 +8,13 @@ from .models import (Architecture, BootType, Bootloader, ClockChoice, Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, Test) + 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')) @@ -24,29 +26,34 @@ class TestForm(forms.ModelForm): source = standard_field(Source) clock_choice = standard_field(ClockChoice) filesystem = standard_field(Filesystem, - help_text="Verify /etc/fstab, `df -hT` output and commands like " \ + help_text="Verify /etc/fstab, `df -hT` output and commands like " "lvdisplay for special modules.") modules = forms.ModelMultipleChoiceField(queryset=Module.objects.all(), - help_text="", widget=forms.CheckboxSelectMultiple(), required=False) + widget=forms.CheckboxSelectMultiple(), required=False) bootloader = standard_field(Bootloader, - help_text="Verify that the entries in the bootloader config looks OK.") + 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.", + 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.", + 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 the " \ - "bugtracker (or check that one already exists) and link to " \ + help_text="Only check this if everything went fine. " + "If you ran into problems please create a ticket on the " + "bugtracker (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) + widget=forms.TextInput(attrs={'style': 'display:none;'}), + required=False) class Meta: model = Test @@ -59,6 +66,7 @@ class TestForm(forms.ModelForm): "modules": forms.CheckboxSelectMultiple(), } + def submit_test_result(request): if request.POST: form = TestForm(request.POST) @@ -74,6 +82,7 @@ def submit_test_result(request): 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 @@ -108,6 +117,7 @@ def calculate_option_overview(field_name): 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 @@ -128,13 +138,19 @@ def options_fetch_iso(options): return options + def test_results_overview(request): # data structure produced: - # [ { option, name, is_rollback, values: [ { value, success, failure } ... ] } ... ] + # [ { + # option, name, is_rollback, + # values: [ { value, success, failure } ... ] + # } + # ... + # ] all_options = [] - fields = [ 'architecture', 'iso_type', 'boot_type', 'hardware_type', + fields = ['architecture', 'iso_type', 'boot_type', 'hardware_type', 'install_type', 'source', 'clock_choice', 'filesystem', 'modules', - 'bootloader', 'rollback_filesystem', 'rollback_modules' ] + 'bootloader', 'rollback_filesystem', 'rollback_modules'] for field in fields: all_options.append(calculate_option_overview(field)) @@ -146,6 +162,7 @@ def test_results_overview(request): } 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() @@ -155,6 +172,7 @@ def test_results_iso(request, iso_id): } 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 @@ -171,9 +189,11 @@ def test_results_for(request, option, value): } 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( @@ -186,7 +206,7 @@ def iso_overview(request): # only show "useful" rows, currently active ISOs or those with results isos = [iso for iso in isos if - iso.active == True or iso.successes > 0 or iso.failures > 0] + iso.active is True or iso.successes > 0 or iso.failures > 0] context = { 'isos': isos -- cgit v1.2.3-2-g168b From 2e9094630b5117575f899f068046c6e1021387a1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 15 Aug 2012 08:25:00 -0500 Subject: PEP8 spacing in releng models file Signed-off-by: Dan McGee --- releng/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index 56187766..98454e95 100644 --- a/releng/models.py +++ b/releng/models.py @@ -4,6 +4,7 @@ from django.db.models.signals import pre_save from main.utils import set_created_field + class IsoOption(models.Model): name = models.CharField(max_length=200) @@ -13,10 +14,12 @@ class IsoOption(models.Model): 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) @@ -32,37 +35,48 @@ class Iso(models.Model): 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() -- cgit v1.2.3-2-g168b From 79b0ff49bd3319133502243ded4506fc2d56cd9e Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 4 Sep 2012 08:37:09 -0500 Subject: Use GenericIPAddressField for releng test IP address field We were already using this on package flag requests, and we can support IPv6 addresses here as well with minimal hassle. Signed-off-by: Dan McGee --- .../0003_auto__chg_field_test_ip_address.py | 99 ++++++++++++++++++++++ releng/models.py | 6 +- 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 releng/migrations/0003_auto__chg_field_test_ip_address.py (limited to 'releng') diff --git a/releng/migrations/0003_auto__chg_field_test_ip_address.py b/releng/migrations/0003_auto__chg_field_test_ip_address.py new file mode 100644 index 00000000..78297f14 --- /dev/null +++ b/releng/migrations/0003_auto__chg_field_test_ip_address.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.alter_column('releng_test', 'ip_address', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39)) + + def backwards(self, orm): + db.alter_column('releng_test', 'ip_address', self.gf('django.db.models.fields.IPAddressField')(max_length=15)) + + 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.GenericIPAddressField', [], {'max_length': '39'}), + '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 98454e95..d602e9e5 100644 --- a/releng/models.py +++ b/releng/models.py @@ -79,8 +79,10 @@ class Bootloader(IsoOption): class Test(models.Model): user_name = models.CharField(max_length=500) - user_email = models.EmailField() - ip_address = models.IPAddressField() + user_email = models.EmailField('email address') + # Great work, Django... https://code.djangoproject.com/ticket/18212 + ip_address = models.GenericIPAddressField(verbose_name='IP address', + unpack_ipv4=True) created = models.DateTimeField(editable=False) iso = models.ForeignKey(Iso) -- cgit v1.2.3-2-g168b From df208fb0bd3f8e5783a6e41f411e7aa80179b2b6 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 1 Nov 2012 09:20:38 -0500 Subject: Signal attachment cleanup Signed-off-by: Dan McGee --- releng/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index d602e9e5..bd178add 100644 --- a/releng/models.py +++ b/releng/models.py @@ -104,9 +104,9 @@ class Test(models.Model): success = models.BooleanField() comments = models.TextField(null=True, blank=True) -pre_save.connect(set_created_field, sender=Iso, - dispatch_uid="releng.models") -pre_save.connect(set_created_field, sender=Test, - dispatch_uid="releng.models") + +for model in (Iso, Test): + pre_save.connect(set_created_field, sender=model, + dispatch_uid="releng.models") # vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From f7331a0eca351300685ebee494e810d8c82c35b1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 20 Nov 2012 19:16:25 -0600 Subject: Add Release model to releng This should prevent the need for monthly template updates from Pierre and Thomas; best to just let them enter the data themselves and have it show up on the website. Signed-off-by: Dan McGee --- releng/admin.py | 23 +++--- releng/migrations/0004_auto__add_release.py | 121 ++++++++++++++++++++++++++++ releng/models.py | 19 ++++- 3 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 releng/migrations/0004_auto__add_release.py (limited to 'releng') diff --git a/releng/admin.py b/releng/admin.py index 42755002..c7e6396e 100644 --- a/releng/admin.py +++ b/releng/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from .models import (Architecture, BootType, Bootloader, ClockChoice, Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, - Test) + Test, Release) class IsoAdmin(admin.ModelAdmin): list_display = ('name', 'created', 'active', 'removed') @@ -14,19 +14,20 @@ class TestAdmin(admin.ModelAdmin): 'iso', 'success') list_filter = ('success', 'iso') +class ReleaseAdmin(admin.ModelAdmin): + list_display = ('version', 'release_date', 'kernel_version', 'available', + 'created') + list_filter = ('available', 'release_date') -admin.site.register(Architecture) -admin.site.register(BootType) -admin.site.register(Bootloader) -admin.site.register(ClockChoice) -admin.site.register(Filesystem) -admin.site.register(HardwareType) -admin.site.register(InstallType) -admin.site.register(IsoType) -admin.site.register(Module) -admin.site.register(Source) + +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/migrations/0004_auto__add_release.py b/releng/migrations/0004_auto__add_release.py new file mode 100644 index 00000000..fe4acea5 --- /dev/null +++ b/releng/migrations/0004_auto__add_release.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_table('releng_release', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('release_date', self.gf('django.db.models.fields.DateField')(db_index=True)), + ('version', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('kernel_version', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)), + ('torrent_infohash', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('available', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('info', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('releng', ['Release']) + + def backwards(self, orm): + db.delete_table('releng_release') + + + 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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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 bd178add..2f9a0785 100644 --- a/releng/models.py +++ b/releng/models.py @@ -105,7 +105,24 @@ class Test(models.Model): comments = models.TextField(null=True, blank=True) -for model in (Iso, Test): +class Release(models.Model): + release_date = models.DateField(db_index=True) + version = models.CharField(max_length=50) + kernel_version = models.CharField(max_length=50, blank=True) + torrent_infohash = models.CharField(max_length=64, blank=True) + created = models.DateTimeField(editable=False) + available = models.BooleanField(default=True) + info = models.TextField('Public information', blank=True) + + class Meta: + get_latest_by = 'release_date' + ordering = ('-release_date', '-version') + + def __unicode__(self): + return self.version + + +for model in (Iso, Test, Release): pre_save.connect(set_created_field, sender=model, dispatch_uid="releng.models") -- cgit v1.2.3-2-g168b From 402487b007e206b013ecbf8b3017dc1231f4bbbc Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 20 Nov 2012 19:33:49 -0600 Subject: Move some logic out of the templates to the Release model This includes magnet URI generation, ISO paths, etc. Signed-off-by: Dan McGee --- releng/models.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index 2f9a0785..c591bc0f 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,3 +1,5 @@ +from urllib import urlencode + from django.core.urlresolvers import reverse from django.db import models from django.db.models.signals import pre_save @@ -121,6 +123,22 @@ class Release(models.Model): def __unicode__(self): return self.version + def dir_path(self): + return "iso/%s/" % self.version + + def iso_url(self): + return "iso/%s/archlinux-%s-dual.iso" % (self.version, self.version) + + def magnet_uri(self): + query = { + 'dn': "archlinux-%s-dual.iso" % self.version, + 'tr': ("udp://tracker.archlinux.org:6969", + "http://tracker.archlinux.org:6969/announce"), + } + if self.torrent_infohash: + query['xt'] = "urn:btih:%s" % self.torrent_infohash + return "magnet:?%s" % urlencode(query, doseq=True) + for model in (Iso, Test, Release): pre_save.connect(set_created_field, sender=model, -- cgit v1.2.3-2-g168b From c81a9271b8bbc03418442c01d50a4c4945999e71 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 21 Nov 2012 00:10:12 -0500 Subject: Show release notes on downloads page Signed-off-by: Dan McGee --- releng/models.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index c591bc0f..b3ab1a31 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,8 +1,10 @@ +import markdown from urllib import urlencode 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.utils import set_created_field @@ -139,6 +141,10 @@ class Release(models.Model): query['xt'] = "urn:btih:%s" % self.torrent_infohash return "magnet:?%s" % urlencode(query, doseq=True) + def info_html(self): + return mark_safe(markdown.markdown( + self.info, safe_mode=True, enable_attributes=False)) + for model in (Iso, Test, Release): pre_save.connect(set_created_field, sender=model, -- cgit v1.2.3-2-g168b From a732d3cebcb8ff3170502b13d01ba90ac8efe26f Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 21 Nov 2012 00:54:20 -0500 Subject: Fix new magnet link generation Apparently clients don't like urlencoded values in the magnet link, so %3A isn't treated the same as ':'. Signed-off-by: Dan McGee --- releng/models.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index b3ab1a31..a22e01d6 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,5 +1,4 @@ import markdown -from urllib import urlencode from django.core.urlresolvers import reverse from django.db import models @@ -132,14 +131,14 @@ class Release(models.Model): return "iso/%s/archlinux-%s-dual.iso" % (self.version, self.version) def magnet_uri(self): - query = { - 'dn': "archlinux-%s-dual.iso" % self.version, - 'tr': ("udp://tracker.archlinux.org:6969", - "http://tracker.archlinux.org:6969/announce"), - } + query = [ + ('dn', "archlinux-%s-dual.iso" % self.version), + ('tr', "udp://tracker.archlinux.org:6969"), + ('tr', "http://tracker.archlinux.org:6969/announce"), + ] if self.torrent_infohash: - query['xt'] = "urn:btih:%s" % self.torrent_infohash - return "magnet:?%s" % urlencode(query, doseq=True) + query.insert(0, ('xt', "urn:btih:%s" % self.torrent_infohash)) + return "magnet:?%s" % '&'.join(['%s=%s' % (k, v) for k, v in query]) def info_html(self): return mark_safe(markdown.markdown( -- cgit v1.2.3-2-g168b From 8cfaa39a0464f7ee35af76473d3e73ae587a6cb8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 10:55:58 -0600 Subject: Add more metadata to releng Release model Add a file_size field which we will use in the RSS feed, and also add a field for future storage of the torrent data itself. Signed-off-by: Dan McGee --- ...se_file_size__add_field_release_torrent_data.py | 127 +++++++++++++++++++++ releng/models.py | 3 + 2 files changed, 130 insertions(+) create mode 100644 releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py (limited to 'releng') diff --git a/releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py b/releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py new file mode 100644 index 00000000..96e0727c --- /dev/null +++ b/releng/migrations/0005_auto__add_field_release_file_size__add_field_release_torrent_data.py @@ -0,0 +1,127 @@ +# -*- coding: 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 field 'Release.file_size' + db.add_column('releng_release', 'file_size', + self.gf('main.fields.PositiveBigIntegerField')(null=True, blank=True), + keep_default=False) + + # Adding field 'Release.torrent_data' + db.add_column('releng_release', 'torrent_data', + self.gf('django.db.models.fields.TextField')(default='', blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Release.file_size' + db.delete_column('releng_release', 'file_size') + + # Deleting field 'Release.torrent_data' + db.delete_column('releng_release', 'torrent_data') + + + 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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'file_size': ('main.fields.PositiveBigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'torrent_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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'] \ No newline at end of file diff --git a/releng/models.py b/releng/models.py index a22e01d6..9f091371 100644 --- a/releng/models.py +++ b/releng/models.py @@ -5,6 +5,7 @@ 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 @@ -113,9 +114,11 @@ class Release(models.Model): version = models.CharField(max_length=50) kernel_version = models.CharField(max_length=50, blank=True) torrent_infohash = models.CharField(max_length=64, blank=True) + file_size = PositiveBigIntegerField(null=True, blank=True) created = models.DateTimeField(editable=False) available = models.BooleanField(default=True) info = models.TextField('Public information', blank=True) + torrent_data = models.TextField(blank=True) class Meta: get_latest_by = 'release_date' -- cgit v1.2.3-2-g168b From 1a7e5d22f1c4e948c624d26b4d8c1ed30189acfe Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 12:06:47 -0600 Subject: Add basic release list and details views Signed-off-by: Dan McGee --- releng/models.py | 3 +++ releng/urls.py | 6 ++++++ releng/views.py | 13 ++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index 9f091371..8bc54def 100644 --- a/releng/models.py +++ b/releng/models.py @@ -127,6 +127,9 @@ class Release(models.Model): 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 diff --git a/releng/urls.py b/releng/urls.py index 8d1b8f24..8413d318 100644 --- a/releng/urls.py +++ b/releng/urls.py @@ -1,5 +1,7 @@ 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'), @@ -11,5 +13,9 @@ feedback_patterns = patterns('releng.views', urlpatterns = patterns('', (r'^feedback/', include(feedback_patterns)), + (r'^releases/$', + ReleaseListView.as_view(), {}, 'releng-release-list'), + (r'^releases/(?P[-.\w]+)/$', + ReleaseDetailView.as_view(), {}, 'releng-release-detail'), ) # vim: set ts=4 sw=4 et: diff --git a/releng/views.py b/releng/views.py index 67b3cb4a..6c49275f 100644 --- a/releng/views.py +++ b/releng/views.py @@ -3,10 +3,11 @@ 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, render +from django.views.generic import DetailView, ListView from .models import (Architecture, BootType, Bootloader, ClockChoice, Filesystem, HardwareType, InstallType, Iso, IsoType, Module, Source, - Test) + Test, Release) def standard_field(model, empty_label=None, help_text=None, required=True): @@ -213,4 +214,14 @@ def iso_overview(request): } 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' + # vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From 71c0c7453a5bc28b6e6576fe4f1351139f33ade5 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 13:08:06 -0600 Subject: Implement torrent data parsing and extraction via bencode This allows uploading of the actual torrent file itself into the webapp and then pulling the relevant pieces of information out of it. Signed-off-by: Dan McGee --- releng/models.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index 8bc54def..c7d26966 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,4 +1,9 @@ +from base64 import b64decode +from bencode import bdecode, bencode +from datetime import datetime +import hashlib import markdown +from pytz import utc from django.core.urlresolvers import reverse from django.db import models @@ -150,6 +155,34 @@ class Release(models.Model): return mark_safe(markdown.markdown( self.info, safe_mode=True, enable_attributes=False)) + def torrent(self): + try: + data = b64decode(self.torrent_data) + except TypeError: + 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, -- cgit v1.2.3-2-g168b From 4d52242f4b5a20a591b5bd44cc0dc12f15a9c92c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 17:19:27 -0600 Subject: Mark release version string as unique It should be unique anyway, but it is especially important now that we are using it in URL patterns for lookup. Signed-off-by: Dan McGee --- .../0006_auto__add_unique_release_version.py | 117 +++++++++++++++++++++ releng/models.py | 2 +- 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 releng/migrations/0006_auto__add_unique_release_version.py (limited to 'releng') diff --git a/releng/migrations/0006_auto__add_unique_release_version.py b/releng/migrations/0006_auto__add_unique_release_version.py new file mode 100644 index 00000000..cb834870 --- /dev/null +++ b/releng/migrations/0006_auto__add_unique_release_version.py @@ -0,0 +1,117 @@ +# -*- coding: 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 unique constraint on 'Release', fields ['version'] + db.create_unique('releng_release', ['version']) + + + def backwards(self, orm): + # Removing unique constraint on 'Release', fields ['version'] + db.delete_unique('releng_release', ['version']) + + + 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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'file_size': ('main.fields.PositiveBigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'torrent_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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'] \ No newline at end of file diff --git a/releng/models.py b/releng/models.py index c7d26966..a0dd57aa 100644 --- a/releng/models.py +++ b/releng/models.py @@ -116,7 +116,7 @@ class Test(models.Model): class Release(models.Model): release_date = models.DateField(db_index=True) - version = models.CharField(max_length=50) + version = models.CharField(max_length=50, unique=True) kernel_version = models.CharField(max_length=50, blank=True) torrent_infohash = models.CharField(max_length=64, blank=True) file_size = PositiveBigIntegerField(null=True, blank=True) -- cgit v1.2.3-2-g168b From b642c93aff6bd22013615ae8b51b7a02763e261c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 19 Jan 2013 17:38:54 -0600 Subject: Add a view to download the torrent available for a given release Signed-off-by: Dan McGee --- releng/urls.py | 15 +++++++++++---- releng/views.py | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) (limited to 'releng') diff --git a/releng/urls.py b/releng/urls.py index 8413d318..76c36345 100644 --- a/releng/urls.py +++ b/releng/urls.py @@ -11,11 +11,18 @@ feedback_patterns = patterns('releng.views', (r'^iso/overview/$', 'iso_overview', {}, 'releng-iso-overview'), ) -urlpatterns = patterns('', - (r'^feedback/', include(feedback_patterns)), - (r'^releases/$', +releases_patterns = patterns('releng.views', + (r'^$', ReleaseListView.as_view(), {}, 'releng-release-list'), - (r'^releases/(?P[-.\w]+)/$', + (r'^(?P[-.\w]+)/$', ReleaseDetailView.as_view(), {}, 'releng-release-detail'), + (r'^(?P[-.\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 index 6c49275f..ad4b07d1 100644 --- a/releng/views.py +++ b/releng/views.py @@ -1,7 +1,9 @@ +from base64 import b64decode + from django import forms from django.conf import settings from django.db.models import Count, Max -from django.http import Http404 +from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.views.generic import DetailView, ListView @@ -224,4 +226,16 @@ class ReleaseDetailView(DetailView): 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) + response = HttpResponse(data, content_type='application/x-bittorrent') + # TODO: this is duplicated from Release.iso_url() + filename = 'archlinux-%s-dual.iso.torrent' % release.version + response['Content-Disposition'] = 'attachment; filename=%s' % filename + return response + # vim: set ts=4 sw=4 et: -- cgit v1.2.3-2-g168b From 1a55c0f0bc8359f85f6c23d69cdf3ca05a4d25a2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 13:50:33 -0700 Subject: Don't error on empty torrent data Signed-off-by: Dan McGee --- releng/models.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index a0dd57aa..19040905 100644 --- a/releng/models.py +++ b/releng/models.py @@ -160,6 +160,8 @@ class Release(models.Model): data = b64decode(self.torrent_data) except TypeError: return None + if not data: + return None data = bdecode(data) # transform the data into a template-friendly dict info = data.get('info', {}) -- cgit v1.2.3-2-g168b From 7d4a8b9adf353d7adce4c3c22101e774092eb4de Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 14:06:41 -0700 Subject: Add MD5 and SHA1 fields for releases Signed-off-by: Dan McGee --- ...dd_field_release_md5__add_field_release_sha1.py | 122 +++++++++++++++++++++ releng/models.py | 4 +- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py (limited to 'releng') diff --git a/releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py b/releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py new file mode 100644 index 00000000..f76be3d7 --- /dev/null +++ b/releng/migrations/0007_auto__add_field_release_md5__add_field_release_sha1.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +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_release', 'md5_sum', + self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True), + keep_default=False) + db.add_column('releng_release', 'sha1_sum', + self.gf('django.db.models.fields.CharField')(default='', max_length=40, blank=True), + keep_default=False) + db.alter_column('releng_release', 'torrent_infohash', self.gf('django.db.models.fields.CharField')(max_length=40)) + + def backwards(self, orm): + db.delete_column('releng_release', 'md5_sum') + db.delete_column('releng_release', 'sha1_sum') + db.alter_column('releng_release', 'torrent_infohash', self.gf('django.db.models.fields.CharField')(max_length=64)) + + 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.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'file_size': ('main.fields.PositiveBigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'kernel_version': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'md5_sum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'release_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}), + 'sha1_sum': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'torrent_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'torrent_infohash': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), + 'version': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}) + }, + '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.GenericIPAddressField', [], {'max_length': '39'}), + '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 19040905..dd034e7f 100644 --- a/releng/models.py +++ b/releng/models.py @@ -118,7 +118,9 @@ 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) - torrent_infohash = models.CharField(max_length=64, blank=True) + torrent_infohash = models.CharField(max_length=40, blank=True) + md5_sum = models.CharField('MD5 digest', max_length=32, blank=True) + sha1_sum = models.CharField('SHA1 digest', max_length=40, blank=True) file_size = PositiveBigIntegerField(null=True, blank=True) created = models.DateTimeField(editable=False) available = models.BooleanField(default=True) -- cgit v1.2.3-2-g168b From bc539b6ed174fed1545aabaa4ceb7a7f925cbbed Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Mon, 28 Jan 2013 14:13:53 -0700 Subject: Extract torrent trackers into a settings variable This allows them to be overridden and changed in a central location, like we do with the SVN URL, PXE boot URL, etc. Signed-off-by: Dan McGee --- releng/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'releng') diff --git a/releng/models.py b/releng/models.py index dd034e7f..b95f7d52 100644 --- a/releng/models.py +++ b/releng/models.py @@ -5,6 +5,7 @@ import hashlib import markdown 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 @@ -146,9 +147,9 @@ class Release(models.Model): def magnet_uri(self): query = [ ('dn', "archlinux-%s-dual.iso" % self.version), - ('tr', "udp://tracker.archlinux.org:6969"), - ('tr', "http://tracker.archlinux.org:6969/announce"), ] + if settings.TORRENT_TRACKERS: + query.extend(('tr', uri) for uri in settings.TORRENT_TRACKERS) if self.torrent_infohash: query.insert(0, ('xt', "urn:btih:%s" % self.torrent_infohash)) return "magnet:?%s" % '&'.join(['%s=%s' % (k, v) for k, v in query]) -- cgit v1.2.3-2-g168b From 5566d43a7734f6bb2f48d5d511351da12ddc5cc1 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 9 Feb 2013 16:43:40 -0600 Subject: Use 'update_fields' model.save() kwarg This was added in Django 1.5 and allows saving only a subset of a model's fields. It makes sense in a few cases to utilize it. Signed-off-by: Dan McGee --- releng/management/commands/syncisos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'releng') diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py index 223c771b..c9f61964 100644 --- a/releng/management/commands/syncisos.py +++ b/releng/management/commands/syncisos.py @@ -53,7 +53,7 @@ class Command(BaseCommand): if not existing.active: existing.active = True existing.removed = None - existing.save() + 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()) -- cgit v1.2.3-2-g168b From b7b24740640e24883cd17fd683e1d465fbb343f8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 16 Apr 2013 22:12:01 -0500 Subject: Various minor code cleanups and fixes Most of these were suggested by PyCharm, and include everything from little syntax issues and other bad smells to dead or bad code. Signed-off-by: Dan McGee --- releng/management/commands/syncisos.py | 2 +- releng/models.py | 2 +- releng/views.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'releng') diff --git a/releng/management/commands/syncisos.py b/releng/management/commands/syncisos.py index c9f61964..f182cc33 100644 --- a/releng/management/commands/syncisos.py +++ b/releng/management/commands/syncisos.py @@ -20,7 +20,7 @@ class IsoListParser(HTMLParser): if tag == 'a': for name, value in attrs: if name == "href": - if value != '../' and self.url_re.search(value) != None: + if value != '../' and self.url_re.search(value) is not None: self.hyperlinks.append(value[:-1]) def parse(self, url): diff --git a/releng/models.py b/releng/models.py index b95f7d52..5ee2f325 100644 --- a/releng/models.py +++ b/releng/models.py @@ -160,7 +160,7 @@ class Release(models.Model): def torrent(self): try: - data = b64decode(self.torrent_data) + data = b64decode(self.torrent_data.encode('utf-8')) except TypeError: return None if not data: diff --git a/releng/views.py b/releng/views.py index ad4b07d1..b1c76a4a 100644 --- a/releng/views.py +++ b/releng/views.py @@ -231,7 +231,7 @@ 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) + 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 = 'archlinux-%s-dual.iso.torrent' % release.version -- cgit v1.2.3-2-g168b