summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--devel/models.py2
-rw-r--r--devel/reports.py198
-rw-r--r--devel/views.py141
-rw-r--r--feeds.py40
-rw-r--r--main/migrations/0068_auto__chg_field_packagefile_directory__chg_field_packagefile_filename.py112
-rw-r--r--main/models.py4
-rw-r--r--mirrors/migrations/0015_assign_country_codes.py4
-rw-r--r--mirrors/models.py2
-rw-r--r--mirrors/utils.py4
-rw-r--r--mirrors/views.py9
-rw-r--r--packages/models.py9
-rw-r--r--packages/utils.py3
-rw-r--r--releng/migrations/0008_auto__del_field_release_torrent_infohash__del_field_release_file_size.py118
-rw-r--r--releng/models.py10
-rw-r--r--releng/urls.py2
-rw-r--r--releng/views.py45
-rw-r--r--requirements.txt10
-rw-r--r--requirements_prod.txt12
-rw-r--r--sitestatic/archweb.css1
-rw-r--r--sitestatic/archweb.js50
-rw-r--r--templates/devel/index.html30
-rw-r--r--templates/devel/packages.html6
-rw-r--r--templates/mirrors/url_details.html10
-rw-r--r--templates/packages/flag.html2
-rw-r--r--templates/packages/flaghelp.html2
-rw-r--r--templates/packages/signoffs.html1
-rw-r--r--templates/packages/stale_relations.html2
-rw-r--r--templates/public/download.html2
-rw-r--r--templates/public/index.html13
-rw-r--r--templates/releng/release_detail.html6
-rw-r--r--templates/releng/release_list.html2
-rw-r--r--templates/todolists/view.html6
32 files changed, 665 insertions, 193 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)
diff --git a/feeds.py b/feeds.py
index 8a9c59d1..6a17949c 100644
--- a/feeds.py
+++ b/feeds.py
@@ -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 6ec39483..0f47f170 100644
--- a/packages/utils.py
+++ b/packages/utils.py
@@ -243,7 +243,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 b7d6e1ee..53fa6274 100644
--- a/sitestatic/archweb.css
+++ b/sitestatic/archweb.css
@@ -79,6 +79,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 02940ab1..8ed9f9ac 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 &gt; 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 063e1d1e..e66d3472 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/arch-general"
diff --git a/templates/packages/flaghelp.html b/templates/packages/flaghelp.html
index 51ace2fd..5f013cd7 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/arch-general"
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/download.html b/templates/public/download.html
index 274d6cfd..9f67733e 100644
--- a/templates/public/download.html
+++ b/templates/public/download.html
@@ -28,7 +28,7 @@
<ul>
{% if release.version %}<li><strong>Current Release:</strong> {{ release.version }}</li>{% endif %}
{% if release.kernel_version %}<li><strong>Included Kernel:</strong> {{ release.kernel_version }}</li>{% endif %}
- {% if release.file_size %}<li><strong>ISO Size:</strong> {{ release.file_size|filesizeformat }}</li>{% endif %}
+ {% if release.torrent_data %}<li><strong>ISO Size:</strong> {{ release.torrent.file_length|filesizeformat }}</li>{% endif %}
<li><a href="{% wiki_url 'Installation_Guide' %}">Installation Guide</a></li>
<li><strong>Resources:</strong>
<ul>
diff --git a/templates/public/index.html b/templates/public/index.html
index bc0fae0c..d3cf797a 100644
--- a/templates/public/index.html
+++ b/templates/public/index.html
@@ -226,8 +226,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>