diff options
author | Luke Shumaker <LukeShu@sbcglobal.net> | 2014-05-25 19:35:23 -0400 |
---|---|---|
committer | Luke Shumaker <LukeShu@sbcglobal.net> | 2014-05-25 19:35:23 -0400 |
commit | 824bd93116c4c0a48d7112ae0bceb46e70851df5 (patch) | |
tree | 21285571c08e286891b8e7740c543941798fe778 | |
parent | 7ccaa7be630e50ce40182d168d41c2f333005443 (diff) | |
parent | 1a1fd4a3d6aa7f6b2338f7751f5f97a30a059a04 (diff) |
Merge branch 'archweb-generic' into master-nomake
Conflicts:
templates/public/download.html
31 files changed, 664 insertions, 192 deletions
diff --git a/devel/models.py b/devel/models.py index 44bbc66e..5c4d4fe7 100644 --- a/devel/models.py +++ b/devel/models.py @@ -4,7 +4,7 @@ import pytz from django.db import models from django.db.models.signals import pre_save from django.contrib.auth.models import User -from django_countries import CountryField +from django_countries.fields import CountryField from .fields import PGPKeyField from main.utils import make_choice, set_created_field diff --git a/devel/reports.py b/devel/reports.py new file mode 100644 index 00000000..ca1a3321 --- /dev/null +++ b/devel/reports.py @@ -0,0 +1,198 @@ +from datetime import timedelta +import pytz + +from django.db.models import F +from django.template.defaultfilters import filesizeformat +from django.utils.timezone import now + +from .models import DeveloperKey, UserProfile +from main.models import PackageFile +from packages.models import PackageRelation, Depend + +class DeveloperReport(object): + def __init__(self, slug, name, desc, packages_func, + names=None, attrs=None, personal=True): + self.slug = slug + self.name = name + self.description = desc + self.packages = packages_func + self.names = names + self.attrs = attrs + self.personal = personal + + +def old(packages, username): + cutoff = now() - timedelta(days=365 * 2) + return packages.filter( + build_date__lt=cutoff).order_by('build_date') + + +def outofdate(packages, username): + cutoff = now() - timedelta(days=30) + return packages.filter( + flag_date__lt=cutoff).order_by('flag_date') + + +def big(packages, username): + cutoff = 50 * 1024 * 1024 + packages = packages.filter( + compressed_size__gte=cutoff).order_by('-compressed_size') + # Format the compressed and installed sizes with MB/GB/etc suffixes + for package in packages: + package.compressed_size_pretty = filesizeformat( + package.compressed_size) + package.installed_size_pretty = filesizeformat( + package.installed_size) + return packages + + +def badcompression(packages, username): + cutoff = 0.90 * F('installed_size') + packages = packages.filter(compressed_size__gt=0, installed_size__gt=0, + compressed_size__gte=cutoff).order_by('-compressed_size') + + # Format the compressed and installed sizes with MB/GB/etc suffixes + for package in packages: + package.compressed_size_pretty = filesizeformat( + package.compressed_size) + package.installed_size_pretty = filesizeformat( + package.installed_size) + ratio = package.compressed_size / float(package.installed_size) + package.ratio = '%.3f' % ratio + package.compress_type = package.filename.split('.')[-1] + + return packages + + +def uncompressed_man(packages, username): + # checking for all '.0'...'.9' + '.n' extensions + bad_files = PackageFile.objects.filter(is_directory=False, + directory__contains='/man/', + filename__regex=r'\.[0-9n]').exclude( + filename__endswith='.gz').exclude( + filename__endswith='.xz').exclude( + filename__endswith='.bz2').exclude( + filename__endswith='.html') + if username: + pkg_ids = set(packages.values_list('id', flat=True)) + bad_files = bad_files.filter(pkg__in=pkg_ids) + bad_files = bad_files.values_list( + 'pkg_id', flat=True).order_by().distinct() + return packages.filter(id__in=set(bad_files)) + + +def uncompressed_info(packages, username): + # we don't worry about looking for '*.info-1', etc., given that an + # uncompressed root page probably exists in the package anyway + bad_files = PackageFile.objects.filter(is_directory=False, + directory__endswith='/info/', filename__endswith='.info') + if username: + pkg_ids = set(packages.values_list('id', flat=True)) + bad_files = bad_files.filter(pkg__in=pkg_ids) + bad_files = bad_files.values_list( + 'pkg_id', flat=True).order_by().distinct() + return packages.filter(id__in=set(bad_files)) + + +def unneeded_orphans(packages, username): + owned = PackageRelation.objects.all().values('pkgbase') + required = Depend.objects.all().values('name') + # The two separate calls to exclude is required to do the right thing + return packages.exclude(pkgbase__in=owned).exclude( + pkgname__in=required) + + +def mismatched_signature(packages, username): + filtered = [] + packages = packages.select_related( + 'arch', 'repo', 'packager').filter(signature_bytes__isnull=False) + known_keys = DeveloperKey.objects.select_related( + 'owner').filter(owner__isnull=False) + known_keys = {dk.key: dk for dk in known_keys} + for package in packages: + bad = False + sig = package.signature + dev_key = known_keys.get(sig.key_id, None) + if dev_key: + package.sig_by = dev_key.owner + if dev_key.owner_id != package.packager_id: + bad = True + else: + package.sig_by = sig.key_id + bad = True + + if bad: + filtered.append(package) + return filtered + + +def signature_time(packages, username): + cutoff = timedelta(hours=24) + filtered = [] + packages = packages.select_related( + 'arch', 'repo', 'packager').filter(signature_bytes__isnull=False) + for package in packages: + sig = package.signature + sig_date = sig.creation_time.replace(tzinfo=pytz.utc) + package.sig_date = sig_date.date() + if sig_date > package.build_date + cutoff: + filtered.append(package) + + return filtered + + +REPORT_OLD = DeveloperReport('old', 'Old', + 'Packages last built more than two years ago', old) + +REPORT_OUTOFDATE = DeveloperReport('long-out-of-date', 'Long Out-of-date', + 'Packages marked out-of-date more than 30 days ago', outofdate) + +REPORT_BIG = DeveloperReport('big', 'Big', + 'Packages with compressed size > 50 MiB', big, + ['Compressed Size', 'Installed Size'], + ['compressed_size_pretty', 'installed_size_pretty']) + +REPORT_BADCOMPRESS = DeveloperReport('badcompression', 'Bad Compression', + 'Packages with a compression ratio of less than 10%', badcompression, + ['Compressed Size', 'Installed Size', 'Ratio', 'Type'], + ['compressed_size_pretty', 'installed_size_pretty','ratio', 'compress_type']) + + +REPORT_MAN = DeveloperReport('uncompressed-man', 'Uncompressed Manpages', + 'Packages with uncompressed manpages', uncompressed_man) + +REPORT_INFO = DeveloperReport('uncompressed-info', 'Uncompressed Info Pages', + 'Packages with uncompressed info pages', uncompressed_info) + +REPORT_ORPHANS = DeveloperReport('unneeded-orphans', 'Unneeded Orphans', + 'Packages that have no maintainer and are not required by any ' + + 'other package in any repository', unneeded_orphans, + personal=False) + +REPORT_SIGNATURE = DeveloperReport('mismatched-signature', + 'Mismatched Signatures', + 'Packages where the signing key is unknown or signer != packager', + mismatched_signature, + ['Signed By', 'Packager'], + ['sig_by', 'packager']) + +REPORT_SIG_TIME = DeveloperReport('signature-time', 'Signature Time', + 'Packages where the signature timestamp is more than 24 hours ' + + 'after the build timestamp', + signature_time, + ['Signature Date', 'Packager'], + ['sig_date', 'packager']) + + +def available_reports(): + return ( + REPORT_OLD, + REPORT_OUTOFDATE, + REPORT_BIG, + REPORT_BADCOMPRESS, + REPORT_MAN, + REPORT_INFO, + REPORT_ORPHANS, + REPORT_SIGNATURE, + REPORT_SIG_TIME, + ) diff --git a/devel/views.py b/devel/views.py index 29954b51..972d0abb 100644 --- a/devel/views.py +++ b/devel/views.py @@ -1,6 +1,5 @@ from datetime import timedelta import operator -import pytz import time from django.http import HttpResponseRedirect @@ -10,21 +9,21 @@ from django.contrib.admin.models import LogEntry, ADDITION from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.db import transaction -from django.db.models import F, Count, Max +from django.db.models import Count, Max from django.http import Http404 from django.shortcuts import get_object_or_404, render -from django.template.defaultfilters import filesizeformat from django.views.decorators.cache import never_cache from django.utils.encoding import force_unicode from django.utils.http import http_date from django.utils.timezone import now from .forms import ProfileForm, UserProfileForm, NewUserForm -from .models import DeveloperKey, UserProfile -from main.models import Package, PackageFile +from .models import UserProfile +from .reports import available_reports +from main.models import Package from main.models import Arch, Repo from news.models import News -from packages.models import PackageRelation, Signoff, FlagRequest, Depend +from packages.models import PackageRelation, Signoff, FlagRequest from packages.utils import get_signoff_groups from todolists.models import TodolistPackage from todolists.utils import get_annotated_todolists @@ -58,7 +57,8 @@ def index(request): 'todos': todolists, 'flagged': flagged, 'todopkgs': todopkgs, - 'signoffs': signoffs + 'signoffs': signoffs, + 'reports': available_reports(), } return render(request, 'devel/index.html', page_dict) @@ -180,10 +180,13 @@ def change_profile(request): @login_required def report(request, report_name, username=None): - title = 'Developer Report' - packages = Package.objects.normal() - names = attrs = user = None + available = {report.slug: report for report in available_reports()} + report = available.get(report_name, None) + if report is None: + raise Http404 + packages = Package.objects.normal() + user = None if username: user = get_object_or_404(User, username=username, is_active=True) maintained = PackageRelation.objects.filter(user=user, @@ -193,126 +196,18 @@ def report(request, report_name, username=None): maints = User.objects.filter(id__in=PackageRelation.objects.filter( type=PackageRelation.MAINTAINER).values('user')) - if report_name == 'old': - title = 'Packages last built more than one year ago' - cutoff = now() - timedelta(days=365) - packages = packages.filter( - build_date__lt=cutoff).order_by('build_date') - elif report_name == 'long-out-of-date': - title = 'Packages marked out-of-date more than 90 days ago' - cutoff = now() - timedelta(days=90) - packages = packages.filter( - flag_date__lt=cutoff).order_by('flag_date') - elif report_name == 'big': - title = 'Packages with compressed size > 50 MiB' - cutoff = 50 * 1024 * 1024 - packages = packages.filter( - compressed_size__gte=cutoff).order_by('-compressed_size') - names = [ 'Compressed Size', 'Installed Size' ] - attrs = [ 'compressed_size_pretty', 'installed_size_pretty' ] - # Format the compressed and installed sizes with MB/GB/etc suffixes - for package in packages: - package.compressed_size_pretty = filesizeformat( - package.compressed_size) - package.installed_size_pretty = filesizeformat( - package.installed_size) - elif report_name == 'badcompression': - title = 'Packages that have little need for compression' - cutoff = 0.90 * F('installed_size') - packages = packages.filter(compressed_size__gt=0, installed_size__gt=0, - compressed_size__gte=cutoff).order_by('-compressed_size') - names = [ 'Compressed Size', 'Installed Size', 'Ratio', 'Type' ] - attrs = [ 'compressed_size_pretty', 'installed_size_pretty', - 'ratio', 'compress_type' ] - # Format the compressed and installed sizes with MB/GB/etc suffixes - for package in packages: - package.compressed_size_pretty = filesizeformat( - package.compressed_size) - package.installed_size_pretty = filesizeformat( - package.installed_size) - ratio = package.compressed_size / float(package.installed_size) - package.ratio = '%.3f' % ratio - package.compress_type = package.filename.split('.')[-1] - elif report_name == 'uncompressed-man': - title = 'Packages with uncompressed manpages' - # checking for all '.0'...'.9' + '.n' extensions - bad_files = PackageFile.objects.filter(is_directory=False, - directory__contains='/man/', - filename__regex=r'\.[0-9n]').exclude( - filename__endswith='.gz').exclude( - filename__endswith='.xz').exclude( - filename__endswith='.bz2').exclude( - filename__endswith='.html') - if username: - pkg_ids = set(packages.values_list('id', flat=True)) - bad_files = bad_files.filter(pkg__in=pkg_ids) - bad_files = bad_files.values_list( - 'pkg_id', flat=True).order_by().distinct() - packages = packages.filter(id__in=set(bad_files)) - elif report_name == 'uncompressed-info': - title = 'Packages with uncompressed infopages' - # we don't worry about looking for '*.info-1', etc., given that an - # uncompressed root page probably exists in the package anyway - bad_files = PackageFile.objects.filter(is_directory=False, - directory__endswith='/info/', filename__endswith='.info') - if username: - pkg_ids = set(packages.values_list('id', flat=True)) - bad_files = bad_files.filter(pkg__in=pkg_ids) - bad_files = bad_files.values_list( - 'pkg_id', flat=True).order_by().distinct() - packages = packages.filter(id__in=set(bad_files)) - elif report_name == 'unneeded-orphans': - title = 'Orphan packages required by no other packages' - owned = PackageRelation.objects.all().values('pkgbase') - required = Depend.objects.all().values('name') - # The two separate calls to exclude is required to do the right thing - packages = packages.exclude(pkgbase__in=owned).exclude( - pkgname__in=required) - elif report_name == 'mismatched-signature': - title = 'Packages with mismatched signatures' - names = [ 'Signature Date', 'Signed By', 'Packager' ] - attrs = [ 'sig_date', 'sig_by', 'packager' ] - cutoff = timedelta(hours=24) - filtered = [] - packages = packages.select_related( - 'arch', 'repo', 'packager').filter(signature_bytes__isnull=False) - known_keys = DeveloperKey.objects.select_related( - 'owner').filter(owner__isnull=False) - known_keys = {dk.key: dk for dk in known_keys} - for package in packages: - bad = False - sig = package.signature - sig_date = sig.creation_time.replace(tzinfo=pytz.utc) - package.sig_date = sig_date.date() - dev_key = known_keys.get(sig.key_id, None) - if dev_key: - package.sig_by = dev_key.owner - if dev_key.owner_id != package.packager_id: - bad = True - else: - package.sig_by = sig.key_id - bad = True - - if sig_date > package.build_date + cutoff: - bad = True - - if bad: - filtered.append(package) - packages = filtered - else: - raise Http404 - arches = {pkg.arch for pkg in packages} repos = {pkg.repo for pkg in packages} context = { 'all_maintainers': maints, - 'title': title, + 'title': report.description, + 'report': report, 'maintainer': user, - 'packages': packages, + 'packages': report.packages(packages, username), 'arches': sorted(arches), 'repos': sorted(repos), - 'column_names': names, - 'column_attrs': attrs, + 'column_names': report.names, + 'column_attrs': report.attrs, } return render(request, 'devel/packages.html', context) @@ -15,6 +15,22 @@ from news.models import News from releng.models import Release +class BatchWritesWrapper(object): + def __init__(self, outfile, chunks=20): + self.outfile = outfile + self.chunks = chunks + self.buf = [] + def write(self, s): + buf = self.buf + buf.append(s) + if len(buf) >= self.chunks: + self.outfile.write(''.join(buf)) + self.buf = [] + def flush(self): + self.outfile.write(''.join(self.buf)) + self.outfile.flush() + + class GuidNotPermalinkFeed(Rss201rev2Feed): @staticmethod def check_for_unique_id(f): @@ -27,13 +43,26 @@ class GuidNotPermalinkFeed(Rss201rev2Feed): return wrapper def write_items(self, handler): - # Totally disgusting. Monkey-patch the hander so if it sees a - # 'unique-id' field come through, add an isPermalink="false" attribute. - # Workaround for http://code.djangoproject.com/ticket/9800 + ''' + Totally disgusting. Monkey-patch the handler so if it sees a + 'unique-id' field come through, add an isPermalink="false" attribute. + Workaround for http://code.djangoproject.com/ticket/9800 + ''' handler.addQuickElement = self.check_for_unique_id( handler.addQuickElement) super(GuidNotPermalinkFeed, self).write_items(handler) + def write(self, outfile, encoding): + ''' + Batch the underlying 'write' calls on the outfile because Python's + default saxutils XmlGenerator is a POS that insists on unbuffered + write/flush calls. This sucks when it is making 1-byte calls to write + '>' closing tags and over 1600 write calls in our package feed. + ''' + wrapper = BatchWritesWrapper(outfile) + super(GuidNotPermalinkFeed, self).write(wrapper, encoding) + wrapper.flush() + def package_etag(request, *args, **kwargs): latest = retrieve_latest(Package) @@ -195,7 +224,10 @@ class ReleaseFeed(Feed): return "%s://%s/%s.torrent" % (proto, domain, item.iso_url()) def item_enclosure_length(self, item): - return item.file_size or "" + if item.torrent_data: + torrent = item.torrent() + return torrent['file_length'] or "" + return "" item_enclosure_mime_type = 'application/x-bittorrent' diff --git a/main/migrations/0068_auto__chg_field_packagefile_directory__chg_field_packagefile_filename.py b/main/migrations/0068_auto__chg_field_packagefile_directory__chg_field_packagefile_filename.py new file mode 100644 index 00000000..5e359806 --- /dev/null +++ b/main/migrations/0068_auto__chg_field_packagefile_directory__chg_field_packagefile_filename.py @@ -0,0 +1,112 @@ +# -*- 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('package_files', 'directory', self.gf('django.db.models.fields.CharField')(max_length=1024)) + db.alter_column('package_files', 'filename', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)) + + def backwards(self, orm): + db.alter_column('package_files', 'directory', self.gf('django.db.models.fields.CharField')(max_length=255)) + db.alter_column('package_files', 'filename', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)) + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'main.arch': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'required_signoffs': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}) + }, + u'main.donor': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Donor', 'db_table': "'donors'"}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'unique_together': "(('pkgname', 'repo', 'arch'),)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': u"orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.fields.PositiveBigIntegerField', [], {}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.fields.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'on_delete': 'models.PROTECT', 'to': u"orm['main.Repo']"}), + 'signature_bytes': ('django.db.models.fields.BinaryField', [], {'null': 'True'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + u'main.packagefile': { + 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"}, + 'directory': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_directory': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['main.Package']"}) + }, + u'main.repo': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + } + } + + complete_apps = ['main'] diff --git a/main/models.py b/main/models.py index bf7a9409..09b1adc0 100644 --- a/main/models.py +++ b/main/models.py @@ -433,8 +433,8 @@ class Package(models.Model): class PackageFile(models.Model): pkg = models.ForeignKey(Package) is_directory = models.BooleanField(default=False) - directory = models.CharField(max_length=255) - filename = models.CharField(max_length=255, null=True, blank=True) + directory = models.CharField(max_length=1024) + filename = models.CharField(max_length=1024, null=True, blank=True) def __unicode__(self): return "%s%s" % (self.directory, self.filename or '') diff --git a/mirrors/migrations/0015_assign_country_codes.py b/mirrors/migrations/0015_assign_country_codes.py index c1b0f969..5d83e02c 100644 --- a/mirrors/migrations/0015_assign_country_codes.py +++ b/mirrors/migrations/0015_assign_country_codes.py @@ -4,12 +4,12 @@ from south.db import db from south.v2 import DataMigration from django.db import models -from django_countries.countries import OFFICIAL_COUNTRIES +from django_countries.data import COUNTRIES class Migration(DataMigration): def forwards(self, orm): - reverse_map = dict((v, k) for k, v in OFFICIAL_COUNTRIES.items()) + reverse_map = dict((v.upper(), k) for k, v in COUNTRIES.items()) # add a few special cases to the list that we know might exist reverse_map['GREAT BRITAIN'] = 'GB' reverse_map['KOREA'] = 'KR' diff --git a/mirrors/models.py b/mirrors/models.py index 57664562..0b053043 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -5,7 +5,7 @@ from urlparse import urlparse from django.core.exceptions import ValidationError from django.db import models from django.db.models.signals import pre_save -from django_countries import CountryField +from django_countries.fields import CountryField from .fields import IPNetworkField from main.utils import set_created_field diff --git a/mirrors/utils.py b/mirrors/utils.py index 0dd26ae0..fe18cd6a 100644 --- a/mirrors/utils.py +++ b/mirrors/utils.py @@ -88,6 +88,8 @@ def annotate_url(url, url_data): ('success_count', 0), ('check_count', 0), ('completion_pct', None), + ('duration_avg', None), + ('duration_stddev', None), ('last_check', None), ('last_sync', None), ('delay', None), @@ -177,7 +179,7 @@ def get_mirror_errors(cutoff=DEFAULT_CUTOFF, mirror_id=None, show_all=False): errors = list(errors) for err in errors: - err['country'] = Country(err['url__country']) + err['country'] = Country(err['url__country'], flag_url='') return errors diff --git a/mirrors/views.py b/mirrors/views.py index 34336165..26b5b802 100644 --- a/mirrors/views.py +++ b/mirrors/views.py @@ -11,7 +11,8 @@ from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils.timezone import now from django.views.decorators.csrf import csrf_exempt -from django_countries.countries import COUNTRIES +from django.views.decorators.http import condition +from django_countries.data import COUNTRIES from .models import (Mirror, MirrorUrl, MirrorProtocol, MirrorLog, CheckLocation) @@ -220,6 +221,11 @@ def url_details(request, name, url_id): return render(request, 'mirrors/url_details.html', context) +def status_last_modified(request, *args, **kwargs): + return MirrorLog.objects.values_list('check_time', flat=True).latest() + + +@condition(last_modified_func=status_last_modified) def status(request, tier=None): if tier is not None: tier = int(tier) @@ -297,6 +303,7 @@ class ExtendedMirrorStatusJSONEncoder(MirrorStatusJSONEncoder): return super(ExtendedMirrorStatusJSONEncoder, self).default(obj) +@condition(last_modified_func=status_last_modified) def status_json(request, tier=None): if tier is not None: tier = int(tier) diff --git a/packages/models.py b/packages/models.py index 6477d412..da8adc56 100644 --- a/packages/models.py +++ b/packages/models.py @@ -28,6 +28,9 @@ class PackageRelation(models.Model): type = models.PositiveIntegerField(choices=TYPE_CHOICES, default=MAINTAINER) created = models.DateTimeField(editable=False) + class Meta: + unique_together = (('pkgbase', 'user', 'type'),) + def get_associated_packages(self): return Package.objects.normal().filter(pkgbase=self.pkgbase) @@ -35,13 +38,13 @@ class PackageRelation(models.Model): packages = self.get_associated_packages() return sorted({p.repo for p in packages}) + def last_update(self): + return Update.objects.filter(pkgbase=self.pkgbase).latest() + def __unicode__(self): return u'%s: %s (%s)' % ( self.pkgbase, self.user, self.get_type_display()) - class Meta: - unique_together = (('pkgbase', 'user', 'type'),) - class SignoffSpecificationManager(models.Manager): def get_from_package(self, pkg): diff --git a/packages/utils.py b/packages/utils.py index fade0855..c38aa840 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -247,7 +247,8 @@ SELECT DISTINCT id cursor = connection.cursor() cursor.execute(sql, [PackageRelation.MAINTAINER]) to_fetch = [row[0] for row in cursor.fetchall()] - relations = PackageRelation.objects.select_related('user').filter( + relations = PackageRelation.objects.select_related( + 'user', 'user__userprofile').filter( id__in=to_fetch) return relations diff --git a/releng/migrations/0008_auto__del_field_release_torrent_infohash__del_field_release_file_size.py b/releng/migrations/0008_auto__del_field_release_torrent_infohash__del_field_release_file_size.py new file mode 100644 index 00000000..4a80fd8e --- /dev/null +++ b/releng/migrations/0008_auto__del_field_release_torrent_infohash__del_field_release_file_size.py @@ -0,0 +1,118 @@ +# -*- 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.delete_column(u'releng_release', 'torrent_infohash') + db.delete_column(u'releng_release', 'file_size') + + def backwards(self, orm): + db.add_column(u'releng_release', 'torrent_infohash', + self.gf('django.db.models.fields.CharField')(default='', max_length=40, blank=True), + keep_default=False) + db.add_column(u'releng_release', 'file_size', + self.gf('main.fields.PositiveBigIntegerField')(null=True, blank=True), + keep_default=False) + + models = { + u'releng.architecture': { + 'Meta': {'object_name': 'Architecture'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.bootloader': { + 'Meta': {'object_name': 'Bootloader'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.boottype': { + 'Meta': {'object_name': 'BootType'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.clockchoice': { + 'Meta': {'object_name': 'ClockChoice'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.filesystem': { + 'Meta': {'object_name': 'Filesystem'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.hardwaretype': { + 'Meta': {'object_name': 'HardwareType'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.installtype': { + 'Meta': {'object_name': 'InstallType'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.iso': { + 'Meta': {'object_name': 'Iso'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + u'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'}) + }, + u'releng.isotype': { + 'Meta': {'object_name': 'IsoType'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.module': { + 'Meta': {'object_name': 'Module'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.release': { + 'Meta': {'ordering': "('-release_date', '-version')", 'object_name': 'Release'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + u'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'}), + 'version': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}) + }, + u'releng.source': { + 'Meta': {'object_name': 'Source'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'releng.test': { + 'Meta': {'object_name': 'Test'}, + 'architecture': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.Architecture']"}), + 'boot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.BootType']"}), + 'bootloader': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.Bootloader']"}), + 'clock_choice': ('django.db.models.fields.related.ForeignKey', [], {'to': u"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': u"orm['releng.Filesystem']"}), + 'hardware_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.HardwareType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'install_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.InstallType']"}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'iso': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.Iso']"}), + 'iso_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.IsoType']"}), + 'modules': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"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': u"orm['releng.Filesystem']"}), + 'rollback_modules': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'rollback_test_set'", 'null': 'True', 'symmetrical': 'False', 'to': u"orm['releng.Module']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['releng.Source']"}), + 'success': ('django.db.models.fields.BooleanField', [], {}), + '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 47803a36..a3af54f9 100644 --- a/releng/models.py +++ b/releng/models.py @@ -119,14 +119,13 @@ 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=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) info = models.TextField('Public information', blank=True) - torrent_data = models.TextField(blank=True) + torrent_data = models.TextField(blank=True, + help_text="base64-encoded torrent file") class Meta: get_latest_by = 'release_date' @@ -150,8 +149,9 @@ class Release(models.Model): ] 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)) + 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): diff --git a/releng/urls.py b/releng/urls.py index 76c36345..ca76eb25 100644 --- a/releng/urls.py +++ b/releng/urls.py @@ -14,6 +14,8 @@ feedback_patterns = patterns('releng.views', 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/$', diff --git a/releng/views.py b/releng/views.py index bc7ddb34..af25b966 100644 --- a/releng/views.py +++ b/releng/views.py @@ -1,7 +1,10 @@ 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 @@ -238,4 +241,46 @@ def release_torrent(request, 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 hasattr(obj, '__iter__'): + # mainly for queryset serialization + return list(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': 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: diff --git a/requirements.txt b/requirements.txt index de8a04e9..81070449 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -e git+git://github.com/fredj/cssmin.git@master#egg=cssmin -Django==1.6.1 +Django==1.6.2 IPy==0.81 -Markdown==2.3.1 +Markdown==2.4 South==0.8.4 bencode==1.0 -django-countries==1.5 -jsmin==2.0.8 -pgpdump==1.4 +django-countries==2.0 +jsmin==2.0.9 +pgpdump==1.5 pytz>=2013.8 diff --git a/requirements_prod.txt b/requirements_prod.txt index e609c5d6..840e0396 100644 --- a/requirements_prod.txt +++ b/requirements_prod.txt @@ -1,13 +1,13 @@ -e git+git://github.com/fredj/cssmin.git@master#egg=cssmin -Django==1.6.1 +Django==1.6.2 IPy==0.81 -Markdown==2.3.1 +Markdown==2.4 South==0.8.4 bencode==1.0 -django-countries==1.5 -jsmin==2.0.8 -pgpdump==1.4 -psycopg2==2.5.1 +django-countries==2.0 +jsmin==2.0.9 +pgpdump==1.5 +psycopg2==2.5.2 pyinotify==0.9.4 python-memcached==1.53 pytz>=2013.8 diff --git a/sitestatic/archweb.css b/sitestatic/archweb.css index b13c7a17..0bd327cd 100644 --- a/sitestatic/archweb.css +++ b/sitestatic/archweb.css @@ -55,6 +55,7 @@ pre { pre code { display: block; background: none; + overflow: auto; } blockquote { diff --git a/sitestatic/archweb.js b/sitestatic/archweb.js index be6f5256..43816c5b 100644 --- a/sitestatic/archweb.js +++ b/sitestatic/archweb.js @@ -123,7 +123,7 @@ if (typeof $ !== 'undefined' && typeof $.tablesorter !== 'undefined') { $.tablesorter.addParser({ id: 'filesize', - re: /^(\d+(?:\.\d+)?) (bytes?|[KMGTPEZY]i?B)$/, + re: /^(\d+(?:\.\d+)?)[ \u00a0](bytes?|[KMGTPEZY]i?B)$/, is: function(s) { return this.re.test(s); }, @@ -387,6 +387,38 @@ function filter_pkgs_reset(callback) { callback(); } +function filter_todolist_save(list_id) { + var state = $('#todolist_filter').serializeArray(); + localStorage['filter_todolist_' + list_id] = JSON.stringify(state); +} +function filter_todolist_load(list_id) { + var state = localStorage['filter_todolist_' + list_id]; + if (!state) + return; + state = JSON.parse(state); + $('#todolist_filter input[type="checkbox"]').removeAttr('checked'); + $.each(state, function (i, v) { + // this assumes our only filters are checkboxes + $('#todolist_filter input[name="' + v['name'] + '"]').attr('checked', 'checked'); + }); +} + +function filter_report_save(report_id) { + var state = $('#report_filter').serializeArray(); + localStorage['filter_report_' + report_id] = JSON.stringify(state); +} +function filter_report_load(report_id) { + var state = localStorage['filter_report_' + report_id]; + if (!state) + return; + state = JSON.parse(state); + $('#report_filter input[type="checkbox"]').removeAttr('checked'); + $.each(state, function (i, v) { + // this assumes our only filters are checkboxes + $('#report_filter input[name="' + v['name'] + '"]').attr('checked', 'checked'); + }); +} + /* signoffs.html */ function signoff_package() { // TODO: fix usage of this @@ -464,6 +496,7 @@ function filter_signoffs() { $('#filter-count').text(rows.length); /* make sure we update the odd/even styling from sorting */ $('.results').trigger('applyWidgets', [false]); + filter_signoffs_save(); } function filter_signoffs_reset() { $('#signoffs_filter .arch_filter').attr('checked', 'checked'); @@ -471,6 +504,21 @@ function filter_signoffs_reset() { $('#id_pending').removeAttr('checked'); filter_signoffs(); } +function filter_signoffs_save() { + var state = $('#signoffs_filter').serializeArray(); + localStorage['filter_signoffs'] = JSON.stringify(state); +} +function filter_signoffs_load() { + var state = localStorage['filter_signoffs']; + if (!state) + return; + state = JSON.parse(state); + $('#signoffs_filter input[type="checkbox"]').removeAttr('checked'); + $.each(state, function (i, v) { + // this assumes our only filters are checkboxes + $('#signoffs_filter input[name="' + v['name'] + '"]').attr('checked', 'checked'); + }); +} function collapseNotes(elements) { // Remove any trailing <br/> tags from the note contents diff --git a/templates/devel/index.html b/templates/devel/index.html index b6287110..72f149a3 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -149,31 +149,11 @@ <h3>Developer Reports</h3> <ul> - <li><a href="reports/old/">Old</a>: - Packages last built more than one year ago - (<a href="reports/old/{{ user.username }}/">yours only</a>)</li> - <li><a href="reports/long-out-of-date/">Long Out-of-date</a>: - Packages marked out-of-date more than 90 days ago - (<a href="reports/long-out-of-date/{{ user.username }}/">yours only</a>)</li> - <li><a href="reports/uncompressed-man/">Uncompressed Manpages</a>: - Self-explanatory - (<a href="reports/uncompressed-man/{{ user.username }}/">yours only</a>)</li> - <li><a href="reports/uncompressed-info/">Uncompressed Info Pages</a>: - Self-explanatory - (<a href="reports/uncompressed-info/{{ user.username }}/">yours only</a>)</li> - <li><a href="reports/mismatched-signature/">Mismatched Signatures</a>: - Packages where 1) signing key is unknown, 2) signer != packager, - or 3) signature timestamp more than 24 hours after build timestamp - (<a href="reports/mismatched-signature/{{ user.username }}/">yours only</a>)</li> - <li><a href="reports/big/">Big</a>: - All packages with compressed size > 50 MiB - (<a href="reports/big/{{ user.username }}/">yours only</a>)</li> - <li><a href="reports/badcompression/">Bad Compression</a>: - Packages with a compression ratio of less than 10% - (<a href="reports/badcompression/{{ user.username }}/">yours only</a>)</li> - <li><a href="reports/unneeded-orphans/">Unneeded Orphans</a>: - Packages that have no maintainer and are not required by any other - package in any repository</li> + {% for report in reports %} + <li><a href="reports/{{ report.slug }}/">{{ report.name }}</a>: + {{ report.description }} + {% if report.personal %}(<a href="reports/{{ report.slug }}/{{ user.username }}/">yours only</a>){% endif %}</li> + {% endfor %} </ul> </div>{# #dev-dashboard #} diff --git a/templates/devel/packages.html b/templates/devel/packages.html index a67553bb..63dd91aa 100644 --- a/templates/devel/packages.html +++ b/templates/devel/packages.html @@ -84,10 +84,14 @@ $(document).ready(function() { $(".results").tablesorter({widgets: ['zebra']}); }); $(document).ready(function() { - var filter_func = function() { filter_pkgs_list('#report_filter', '#dev-report-results tbody'); }; + var filter_func = function() { + filter_pkgs_list('#report_filter', '#dev-report-results tbody'); + filter_report_save('{{ report.slug }}'); + }; $('#report_filter input').change(filter_func); $('#criteria_reset').click(function() { filter_pkgs_reset(filter_func); }); // run on page load to ensure current form selections take effect + filter_report_load('{{ report.slug }}'); filter_func(); }); </script> diff --git a/templates/mirrors/url_details.html b/templates/mirrors/url_details.html index 0b9d2916..54960a0d 100644 --- a/templates/mirrors/url_details.html +++ b/templates/mirrors/url_details.html @@ -15,7 +15,7 @@ <table class="compact"> <tr> <th>URL:</th> - <td>{{ url.url }}</td> + <td>{% if url.protocol.is_download %}<a href="{{ url.url }}">{{ url.url }}</a>{% else %}{{ url.url }}{% endif %}</td> </tr> <tr> <th>Protocol:</th> @@ -42,6 +42,14 @@ <th>Created:</th> <td>{{ url.created }}</td> </tr> + <tr> + <th>First Check:</th> + <td>{{ url.logs.earliest.check_time }}</td> + </tr> + <tr> + <th>Last Check:</th> + <td>{{ url.logs.latest.check_time }}</td> + </tr> {% endif %} </table> diff --git a/templates/packages/flag.html b/templates/packages/flag.html index b9737bb9..ecf2fc78 100644 --- a/templates/packages/flag.html +++ b/templates/packages/flag.html @@ -20,7 +20,7 @@ {% endfor %} </ul> - <p>The message box portion of the flag utility is optional, and meant + <p>The message box portion is meant for short messages only. If you need more than 200 characters for your message, then file a bug report, email the maintainer directly, or send an email to the <a href="{{ MAILMAN_BASE_URL }}/mailman/listinfo/dev" diff --git a/templates/packages/flaghelp.html b/templates/packages/flaghelp.html index 146e6ade..d84dc11c 100644 --- a/templates/packages/flaghelp.html +++ b/templates/packages/flaghelp.html @@ -21,7 +21,7 @@ package so they can update it. If the package is unmaintained, the notification will be sent to a developer mailing list.</p> - <p>The message box portion of the flag utility is optional, and meant + <p>The message box portion of the flag utility is meant for short messages only. If you need more than 200 characters for your message, then file a bug report, email the maintainer directly, or send an email to the <a target="_blank" href="{{ MAILMAN_BASE_URL }}/mailman/listinfo/dev" diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index 48202c60..06766935 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -93,6 +93,7 @@ $(document).ready(function() { $('#signoffs_filter input').change(filter_signoffs); $('#criteria_reset').click(filter_signoffs_reset); // fire function on page load to ensure the current form selections take effect + filter_signoffs_load(); filter_signoffs(); }); $(document).ready(function() { diff --git a/templates/packages/stale_relations.html b/templates/packages/stale_relations.html index cd447a2e..91ea033e 100644 --- a/templates/packages/stale_relations.html +++ b/templates/packages/stale_relations.html @@ -52,6 +52,7 @@ <th>User</th> <th>Type</th> <th>Created</th> + <th>Latest Update</th> </tr> </thead> <tbody> @@ -62,6 +63,7 @@ <td>{{ relation.user.get_full_name }}</td> <td>{{ relation.get_type_display }}</td> <td>{{ relation.created }}</td> + <td>{{ relation.last_update.created }}</td> </tr> {% empty %} <tr class="empty"><td colspan="4"><em>No non-existent pkgbase relations.</em></td></tr> diff --git a/templates/public/index.html b/templates/public/index.html index f5470d14..b97c7909 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -237,8 +237,19 @@ function setupTypeahead() { matcher: function(item) { return true; }, sorter: function(items) { return items; }, menu: '<ul class="pkgsearch-typeahead"></ul>', - items: 10 + items: 10, + updater: function(item) { + $('#pkgsearch-field').val(item); + $('#pkgsearch-form').submit(); + return item; + } }).attr('autocomplete', 'off'); + $('#pkgsearch-field').keyup(function(e) { + if (e.keyCode === 13 && + $('ul.pkgsearch-typeahead li.active').size() === 0) { + $('#pkgsearch-form').submit(); + } + }); } function setupKonami() { var konami = new Konami(function() { diff --git a/templates/releng/release_detail.html b/templates/releng/release_detail.html index 547f82b8..d04533b9 100644 --- a/templates/releng/release_detail.html +++ b/templates/releng/release_detail.html @@ -10,14 +10,12 @@ <li><strong>Release Date:</strong> {{ release.release_date|date }}</li> {% if release.kernel_version %}<li><strong>Kernel Version:</strong> {{ release.kernel_version }}</li>{% endif %} <li><strong>Available:</strong> {{ release.available|yesno }}</li> - {% if release.available %}<li><strong>Download:</strong> <a + {% if release.torrent_data %}<li><strong>Download:</strong> <a href="{% url 'releng-release-torrent' release.version %}" title="Download torrent for {{ release.version }}">Torrent</a>, <a href="{{ release.magnet_uri }}">Magnet</a></li>{% endif %} - {% if release.torrent_infohash %}<li><strong>Torrent Info Hash:</strong> {{ release.torrent_infohash }}</li>{% endif %} {% if release.md5_sum %}<li><strong>MD5:</strong> {{ release.md5_sum }}</li>{% endif %} {% if release.sha1_sum %}<li><strong>SHA1:</strong> {{ release.sha1_sum }}</li>{% endif %} - <li><strong>Download Size:</strong> {% if release.file_size %}{{ release.file_size|filesizeformat }}{% else %}Unknown{% endif %}</li> </ul> {% if release.info %} @@ -38,7 +36,7 @@ <li><strong>File Length:</strong> {{ torrent.file_length|filesizeformat }}</li> <li><strong>Piece Count:</strong> {{ torrent.piece_count }} pieces</li> <li><strong>Piece Length:</strong> {{ torrent.piece_length|filesizeformat }}</li> - <li><strong>Computed Info Hash:</strong> {{ torrent.info_hash }}</li> + <li><strong>Info Hash:</strong> {{ torrent.info_hash }}</li> <li><strong>URL List Length:</strong> {{ torrent.url_list|length }} URLs</li> </ul> {% endwith %}{% endif %} diff --git a/templates/releng/release_list.html b/templates/releng/release_list.html index 79615640..af8b8a61 100644 --- a/templates/releng/release_list.html +++ b/templates/releng/release_list.html @@ -34,7 +34,7 @@ <td>{% if item.available %}<a href="{% url 'releng-release-torrent' item.version %}" title="Download torrent for {{ item.version }}">Torrent</a>{% endif %}</td> <td>{% if item.available %}<a href="{{ item.magnet_uri }}">Magnet</a>{% endif %}</td> - <td>{% if item.file_size %}{{ item.file_size|filesizeformat }}{% endif %}</td> + <td>{% if item.torrent_data %}{{ item.torrent.file_length|filesizeformat }}{% endif %}</td> </tr> {% endfor %} </tbody> diff --git a/templates/todolists/view.html b/templates/todolists/view.html index 5cc2be41..c3ee57f8 100644 --- a/templates/todolists/view.html +++ b/templates/todolists/view.html @@ -118,10 +118,14 @@ $(document).ready(function() { }); $(document).ready(function() { $('a.status-link').click(todolist_flag); - var filter_func = function() { filter_pkgs_list('#todolist_filter', '#dev-todo-pkglist tbody'); }; + var filter_func = function() { + filter_pkgs_list('#todolist_filter', '#dev-todo-pkglist tbody'); + filter_todolist_save({{ list.id }}); + }; $('#todolist_filter input').change(filter_func); $('#criteria_reset').click(function() { filter_pkgs_reset(filter_func); }); // fire function on page load to ensure the current form selections take effect + filter_todolist_load({{ list.id }}); filter_func(); }); </script> |