diff options
author | Nicolás Reynolds <fauno@kiwwwi.com.ar> | 2011-08-03 16:05:35 -0300 |
---|---|---|
committer | Nicolás Reynolds <fauno@kiwwwi.com.ar> | 2011-08-03 16:05:35 -0300 |
commit | fbd23db51b7160a308cd88e407e676994eb08b10 (patch) | |
tree | e3816cc4e3f0ee07539fb4464b2d886a43ecc318 | |
parent | a8b2fc84ba96c83ec1addf89ac04608fbf572705 (diff) | |
parent | 0f6c80e9a36bc5770e95543b4374c5ace4383cf5 (diff) |
Merge branch 'master' of git://projects.archlinux.org/archweb
Conflicts:
packages/urls.py
templates/packages/details.html
32 files changed, 1334 insertions, 230 deletions
diff --git a/devel/management/commands/rematch_packager.py b/devel/management/commands/rematch_packager.py new file mode 100644 index 00000000..ba6e6a54 --- /dev/null +++ b/devel/management/commands/rematch_packager.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +rematch_packager command + +Match all packages with a packager_str but NULL packager_id to a packager if we +can find one. + +Usage: ./manage.py rematch_packager +""" + +from django.core.management.base import NoArgsCommand + +import sys +import logging + +from devel.utils import UserFinder +from main.models import Package + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s -> %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + stream=sys.stderr) +logger = logging.getLogger() + +class Command(NoArgsCommand): + help = "Runs a check on all active mirror URLs to determine if they are reachable via IPv4 and/or v6." + + def handle_noargs(self, **options): + v = int(options.get('verbosity', None)) + if v == 0: + logger.level = logging.ERROR + elif v == 1: + logger.level = logging.INFO + elif v == 2: + logger.level = logging.DEBUG + + return match_packager() + +def match_packager(): + finder = UserFinder() + logger.info("getting all unmatched packages") + package_count = matched_count = 0 + unknown = set() + + for package in Package.objects.filter(packager__isnull=True): + logger.debug("package %s, packager string %s", + package.pkgname, package.packager_str) + package_count += 1 + user = finder.find(package.packager_str) + if user: + package.packager = user + logger.debug(" found user %s" % user.username) + package.save() + matched_count += 1 + else: + unknown.add(package.packager_str) + + logger.info("%d packages checked, %d newly matched", + package_count, matched_count) + logger.debug("unknown packagers:\n%s", + "\n".join(unknown)) + +# vim: set ts=4 sw=4 et: diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index baf7fee1..470b785d 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -16,7 +16,6 @@ Example: from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import User from django.db import transaction -from django.db.models import Q from collections import defaultdict import io @@ -28,6 +27,7 @@ import logging from datetime import datetime from optparse import make_option +from devel.utils import UserFinder from main.models import Arch, Package, PackageDepend, PackageFile, Repo from packages.models import Conflict, Provision, Replacement @@ -130,55 +130,6 @@ class Pkg(object): return u'%s-%s' % (self.ver, self.rel) -def find_user(userstring): - ''' - Attempt to find the corresponding User object for a standard - packager string, e.g. something like - 'A. U. Thor <author@example.com>'. - We start by searching for a matching email address; we then move onto - matching by first/last name. If we cannot find a user, then return None. - ''' - if userstring in find_user.cache: - return find_user.cache[userstring] - matches = re.match(r'^([^<]+)? ?<([^>]*)>', userstring) - if not matches: - return None - - user = None - name = matches.group(1) - email = matches.group(2) - - def user_email(): - return User.objects.get(email=email) - def profile_email(): - return User.objects.get(userprofile__public_email=email) - def user_name(): - # yes, a bit odd but this is the easiest way since we can't always be - # sure how to split the name. Ensure every 'token' appears in at least - # one of the two name fields. - name_q = Q() - for token in name.split(): - # ignore quoted parts; e.g. nicknames in strings - if re.match(r'^[\'"].*[\'"]$', token): - continue - name_q &= (Q(first_name__icontains=token) | - Q(last_name__icontains=token)) - return User.objects.get(name_q) - - for matcher in (user_email, profile_email, user_name): - try: - user = matcher() - break - except (User.DoesNotExist, User.MultipleObjectsReturned): - pass - - find_user.cache[userstring] = user - return user - -# cached mappings of user strings -> User objects so we don't have to do the -# lookup more than strictly necessary. -find_user.cache = {} - DEPEND_RE = re.compile(r"^(.+?)((>=|<=|=|>|<)(.*))?$") def create_depend(package, dep_str, optional=False): @@ -231,6 +182,8 @@ def create_multivalued(dbpkg, repopkg, db_attr, repo_attr): for name in getattr(repopkg, repo_attr): collection.create(name=name) +finder = UserFinder() + def populate_pkg(dbpkg, repopkg, force=False, timestamp=None): db_score = 1 @@ -249,7 +202,7 @@ def populate_pkg(dbpkg, repopkg, force=False, timestamp=None): dbpkg.build_date = repopkg.builddate dbpkg.packager_str = repopkg.packager # attempt to find the corresponding django user for this string - dbpkg.packager = find_user(repopkg.packager) + dbpkg.packager = finder.find(repopkg.packager) if timestamp: dbpkg.flag_date = None diff --git a/devel/tests.py b/devel/tests.py index 604da74c..36691179 100644 --- a/devel/tests.py +++ b/devel/tests.py @@ -1,8 +1,10 @@ from django.test import TestCase +from django.contrib.auth.models import User +from devel.utils import UserFinder +from main.models import UserProfile class DevelTest(TestCase): - def test_index(self): response = self.client.get('/devel/') self.assertEqual(response.status_code, 302) @@ -27,3 +29,87 @@ class DevelTest(TestCase): def test_mirrors(self): response = self.client.get('/mirrors/') self.assertEqual(response.status_code, 200) + +class FindUserTest(TestCase): + + def setUp(self): + self.finder = UserFinder() + + self.user1 = User.objects.create(username="joeuser", first_name="Joe", + last_name="User", email="user1@example.com") + self.user2 = User.objects.create(username="john", first_name="John", + last_name="", email="user2@example.com") + self.user3 = User.objects.create(username="bjones", first_name="Bob", + last_name="Jones", email="user3@example.com") + + for user in (self.user1, self.user2, self.user3): + email_addr = "%s@awesome.com" % user.username + UserProfile.objects.create(user=user, public_email=email_addr) + + self.user4 = User.objects.create(username="tim1", first_name="Tim", + last_name="One", email="tim@example.com") + self.user5 = User.objects.create(username="tim2", first_name="Tim", + last_name="Two", email="timtwo@example.com") + + def test_not_matching(self): + self.assertIsNone(self.finder.find(None)) + self.assertIsNone(self.finder.find("")) + self.assertIsNone(self.finder.find("Bogus")) + self.assertIsNone(self.finder.find("Bogus <invalid")) + self.assertIsNone(self.finder.find("Bogus User <bogus@example.com>")) + self.assertIsNone(self.finder.find("<bogus@example.com>")) + self.assertIsNone(self.finder.find("bogus@example.com")) + self.assertIsNone(self.finder.find("Unknown Packager")) + + def test_by_email(self): + self.assertEqual(self.user1, + self.finder.find("XXX YYY <user1@example.com>")) + self.assertEqual(self.user2, + self.finder.find("YYY ZZZ <user2@example.com>")) + + def test_by_profile_email(self): + self.assertEqual(self.user1, + self.finder.find("XXX <joeuser@awesome.com>")) + self.assertEqual(self.user2, + self.finder.find("YYY <john@awesome.com>")) + self.assertEqual(self.user3, + self.finder.find("ZZZ <bjones@awesome.com>")) + + def test_by_name(self): + self.assertEqual(self.user1, + self.finder.find("Joe User <joe@differentdomain.com>")) + self.assertEqual(self.user1, + self.finder.find("Joe User")) + self.assertEqual(self.user2, + self.finder.find("John <john@differentdomain.com>")) + self.assertEqual(self.user2, + self.finder.find("John")) + self.assertEqual(self.user3, + self.finder.find("Bob Jones <bjones AT Arch Linux DOT org>")) + + def test_by_invalid(self): + self.assertEqual(self.user1, + self.finder.find("Joe User <user1@example.com")) + self.assertEqual(self.user1, + self.finder.find("Joe 'nickname' User <user1@example.com")) + self.assertEqual(self.user1, + self.finder.find("Joe \"nickname\" User <user1@example.com")) + self.assertEqual(self.user1, + self.finder.find("Joe User <joe@differentdomain.com")) + + def test_cache(self): + # simply look two of them up, but then do it repeatedly + for i in range(50): + self.assertEqual(self.user1, + self.finder.find("XXX YYY <user1@example.com>")) + self.assertEqual(self.user3, + self.finder.find("Bob Jones <bjones AT Arch Linux DOT org>")) + + def test_ambiguous(self): + self.assertEqual(self.user4, + self.finder.find("Tim One <tim@anotherdomain.com>")) + self.assertEqual(self.user5, + self.finder.find("Tim Two <tim@anotherdomain.com>")) + self.assertIsNone(self.finder.find("Tim <tim@anotherdomain.com>")) + +# vim: set ts=4 sw=4 et: diff --git a/devel/utils.py b/devel/utils.py index abfdabe5..d7a154a8 100644 --- a/devel/utils.py +++ b/devel/utils.py @@ -1,7 +1,11 @@ +import re + from django.contrib.auth.models import User from django.db import connection +from django.db.models import Count, Q from main.utils import cache_function +from main.models import Package from packages.models import PackageRelation @cache_function(300) @@ -28,10 +32,86 @@ SELECT pr.user_id, COUNT(*), COUNT(p.flag_date) pkg_count[k] = total flag_count[k] = flagged + update_count = Package.objects.values_list('packager').order_by( + 'packager').annotate(Count('packager')) + update_count = dict(update_count) + for m in maintainers: m.package_count = pkg_count.get(m.id, 0) m.flagged_count = flag_count.get(m.id, 0) + m.updated_count = update_count.get(m.id, 0) return maintainers + +class UserFinder(object): + def __init__(self): + self.cache = {} + + @staticmethod + def user_email(name, email): + if email: + return User.objects.get(email=email) + return None + + @staticmethod + def profile_email(name, email): + if email: + return User.objects.get(userprofile__public_email=email) + return None + + @staticmethod + def user_name(name, email): + # yes, a bit odd but this is the easiest way since we can't always be + # sure how to split the name. Ensure every 'token' appears in at least + # one of the two name fields. + if not name: + return None + name_q = Q() + for token in name.split(): + # ignore quoted parts; e.g. nicknames in strings + if re.match(r'^[\'"].*[\'"]$', token): + continue + name_q &= (Q(first_name__icontains=token) | + Q(last_name__icontains=token)) + return User.objects.get(name_q) + + def find(self, userstring): + ''' + Attempt to find the corresponding User object for a standard + packager string, e.g. something like + 'A. U. Thor <author@example.com>'. + We start by searching for a matching email address; we then move onto + matching by first/last name. If we cannot find a user, then return None. + ''' + if not userstring: + return None + if userstring in self.cache: + return self.cache[userstring] + + name = email = None + + matches = re.match(r'^([^<]+)? ?<([^>]*)>?', userstring) + if not matches: + name = userstring.strip() + else: + name = matches.group(1) + email = matches.group(2) + + user = None + find_methods = (self.user_email, self.profile_email, self.user_name) + for matcher in find_methods: + try: + user = matcher(name, email) + if user != None: + break + except (User.DoesNotExist, User.MultipleObjectsReturned): + pass + + self.cache[userstring] = user + return user + + def clear_cache(self): + self.cache = {} + # vim: set ts=4 sw=4 et: diff --git a/devel/views.py b/devel/views.py index 3bf68a58..27c32e7b 100644 --- a/devel/views.py +++ b/devel/views.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User, Group from django.contrib.sites.models import Site from django.core.mail import send_mail from django.db import transaction -from django.db.models import Q +from django.db.models import F, Q from django.http import Http404 from django.shortcuts import get_object_or_404 from django.template import loader, Context @@ -50,9 +50,11 @@ def index(request): total_orphans = Package.objects.exclude(pkgbase__in=maintained).count() total_flagged_orphans = Package.objects.filter( flag_date__isnull=False).exclude(pkgbase__in=maintained).count() + total_updated = Package.objects.filter(packager__isnull=True).count() orphan = { 'package_count': total_orphans, 'flagged_count': total_flagged_orphans, + 'updated_count': total_updated, } page_dict = { @@ -161,6 +163,23 @@ def report(request, report, username=None): package.compressed_size) package.installed_size_pretty = filesizeformat( package.installed_size) + elif report == '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 = '%.2f' % ratio + package.compress_type = package.filename.split('.')[-1] elif report == 'uncompressed-man': title = 'Packages with uncompressed manpages' # magic going on here! Checking for all '.1'...'.9' extensions diff --git a/main/migrations/0051_auto__chg_field_userprofile_pgp_key.py b/main/migrations/0051_auto__chg_field_userprofile_pgp_key.py new file mode 100644 index 00000000..6b8abf6e --- /dev/null +++ b/main/migrations/0051_auto__chg_field_userprofile_pgp_key.py @@ -0,0 +1,160 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.alter_column('user_profiles', 'pgp_key', self.gf('main.models.PGPKeyField')(max_length=40, null=True)) + + + def backwards(self, orm): + db.alter_column('user_profiles', 'pgp_key', self.gf('django.db.models.fields.CharField')(max_length=40, null=True)) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + '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'}), + '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'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.donor': { + 'Meta': {'ordering': "['name']", 'object_name': 'Donor', 'db_table': "'donors'"}, + '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'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + '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'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': '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.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + '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'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.packagedepend': { + 'Meta': {'object_name': 'PackageDepend', 'db_table': "'package_depends'"}, + 'depname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'depvcmp': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.packagefile': { + 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"}, + 'directory': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + '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': "orm['main.Package']"}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + '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'}) + }, + 'main.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.todolist': { + 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.todolistpkg': { + 'Meta': {'unique_together': "(('list', 'pkg'),)", 'object_name': 'TodolistPkg', 'db_table': "'todolist_pkgs'"}, + 'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Todolist']"}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'user_profiles'"}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'allowed_repos': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Repo']", 'symmetrical': 'False', 'blank': 'True'}), + 'favorite_distros': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'languages': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'notify': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'occupation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'other_contact': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'pgp_key': ('main.models.PGPKeyField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'picture': ('django.db.models.fields.files.FileField', [], {'default': "'devs/silhouette.png'", 'max_length': '100'}), + 'public_email': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'roles': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'default': "'UTC'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'website': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'yob': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['main'] diff --git a/main/migrations/0052_auto__del_signoff.py b/main/migrations/0052_auto__del_signoff.py new file mode 100644 index 00000000..8da6becc --- /dev/null +++ b/main/migrations/0052_auto__del_signoff.py @@ -0,0 +1,166 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + depends_on = ( + ('packages', '0008_add_signoff_model'), + ) + + def forwards(self, orm): + # Deleting model 'Signoff' + db.delete_table('main_signoff') + + + def backwards(self, orm): + # Adding model 'Signoff' + db.create_table('main_signoff', ( + ('pkgrel', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('packager', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('pkg', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.Package'])), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('pkgver', self.gf('django.db.models.fields.CharField')(max_length=255)), + )) + db.send_create_signal('main', ['Signoff']) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + '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'}), + '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'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.donor': { + 'Meta': {'ordering': "['name']", 'object_name': 'Donor', 'db_table': "'donors'"}, + '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'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + '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'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': '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.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + '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'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.packagedepend': { + 'Meta': {'object_name': 'PackageDepend', 'db_table': "'package_depends'"}, + 'depname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'depvcmp': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.packagefile': { + 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"}, + 'directory': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + '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': "orm['main.Package']"}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + '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'}) + }, + 'main.todolist': { + 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.todolistpkg': { + 'Meta': {'unique_together': "(('list', 'pkg'),)", 'object_name': 'TodolistPkg', 'db_table': "'todolist_pkgs'"}, + 'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Todolist']"}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'user_profiles'"}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'allowed_repos': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Repo']", 'symmetrical': 'False', 'blank': 'True'}), + 'favorite_distros': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'languages': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'notify': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'occupation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'other_contact': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'pgp_key': ('main.models.PGPKeyField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'picture': ('django.db.models.fields.files.FileField', [], {'default': "'devs/silhouette.png'", 'max_length': '100'}), + 'public_email': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'roles': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'default': "'UTC'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'website': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'yob': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['main'] diff --git a/main/models.py b/main/models.py index a5d6c42e..70372823 100644 --- a/main/models.py +++ b/main/models.py @@ -6,6 +6,7 @@ from django.forms import ValidationError from main.utils import cache_function, make_choice from packages.models import PackageRelation +from packages.models import Signoff as PackageSignoff from datetime import datetime from itertools import groupby @@ -22,6 +23,25 @@ class PositiveBigIntegerField(models.BigIntegerField): defaults.update(kwargs) return super(PositiveBigIntegerField, self).formfield(**defaults) +class PGPKeyField(models.CharField): + _south_introspects = True + + def to_python(self, value): + if value == '': + return None + value = super(PGPKeyField, self).to_python(value) + # remove all spaces + value = value.replace(' ', '') + # prune prefixes, either 0x or 2048R/ type + if value.startswith('0x'): + value = value[2:] + value = value.split('/')[-1] + return value + + def formfield(self, **kwargs): + # override so we don't set max_length form field attribute + return models.Field.formfield(self, **kwargs) + def validate_pgp_key_length(value): if len(value) not in (8, 16, 40): raise ValidationError( @@ -45,7 +65,7 @@ class UserProfile(models.Model): max_length=50, help_text="Required field") other_contact = models.CharField(max_length=100, null=True, blank=True) - pgp_key = models.CharField(max_length=40, null=True, blank=True, + pgp_key = PGPKeyField(max_length=40, null=True, blank=True, verbose_name="PGP key", validators=[RegexValidator(r'^[0-9A-F]+$', "Ensure this value consists of only hex characters.", 'hex_char'), validate_pgp_key_length], @@ -73,6 +93,10 @@ class TodolistManager(models.Manager): return self.filter(todolistpkg__complete=False).distinct() class PackageManager(models.Manager): + def flagged(self): + """Used by dev dashboard.""" + return self.filter(flag_date__isnull=False) + def normal(self): return self.select_related('arch', 'repo') @@ -183,16 +207,13 @@ class Package(models.Model): @property def signoffs(self): - if 'signoffs_cache' in dir(self): - return self.signoffs_cache - self.signoffs_cache = list(Signoff.objects.filter( - pkg=self, - pkgver=self.pkgver, - pkgrel=self.pkgrel)) - return self.signoffs_cache + return PackageSignoff.objects.select_related('user').filter( + pkgbase=self.pkgbase, pkgver=self.pkgver, pkgrel=self.pkgrel, + epoch=self.epoch, arch=self.arch, repo=self.repo) def approved_for_signoff(self): - return len(self.signoffs) >= 2 + count = self.signoffs.filter(revoked__isnull=True).count() + return count >= PackageSignoff.REQUIRED @cache_function(300) def applicable_arches(self): @@ -222,6 +243,7 @@ class Package(models.Model): groupby(requiredby, lambda x: x.pkg.id)] # find another package by this name in the opposite testing setup + # TODO: figure out staging exclusions too if not Package.objects.filter(pkgname=self.pkgname, arch=self.arch).exclude(id=self.id).exclude( repo__testing=self.repo.testing).exists(): @@ -237,7 +259,8 @@ class Package(models.Model): dep = dep_pkgs[0] if len(dep_pkgs) > 1: dep_pkgs = [d for d in dep_pkgs - if d.pkg.repo.testing == self.repo.testing] + if d.pkg.repo.testing == self.repo.testing and + d.pkg.repo.staging == self.repo.staging] if len(dep_pkgs) > 0: dep = dep_pkgs[0] trimmed.append(dep) @@ -269,7 +292,8 @@ class Package(models.Model): # grab the first though in case we fail pkg = pkgs[0] # prevents yet more DB queries, these lists should be short - pkgs = [p for p in pkgs if p.repo.testing == self.repo.testing] + pkgs = [p for p in pkgs if p.repo.testing == self.repo.testing + and p.repo.staging == self.repo.staging] if len(pkgs) > 0: pkg = pkgs[0] deps.append({'dep': dep, 'pkg': pkg}) @@ -283,13 +307,14 @@ class Package(models.Model): """ try: # start by looking for something in this repo - return Package.objects.get(arch=self.arch, + return Package.objects.normal().get(arch=self.arch, repo=self.repo, pkgname=self.pkgbase) except Package.DoesNotExist: # this package might be split across repos? just find one # that matches the correct [testing] repo flag - pkglist = Package.objects.filter(arch=self.arch, - repo__testing=self.repo.testing, pkgname=self.pkgbase) + pkglist = Package.objects.normal().filter(arch=self.arch, + repo__testing=self.repo.testing, + repo__staging=self.repo.staging, pkgname=self.pkgbase) if len(pkglist) > 0: return pkglist[0] return None @@ -299,11 +324,12 @@ class Package(models.Model): Return all packages that were built with this one (e.g. share a pkgbase value). The package this method is called on will never be in the list, and we will never return a package that does not have the same - repo.testing flag. For any non-split packages, the return value will be - an empty list. + repo.testing and repo.staging flags. For any non-split packages, the + return value will be an empty list. """ - return Package.objects.filter(arch__in=self.applicable_arches(), - repo__testing=self.repo.testing, pkgbase=self.pkgbase).exclude(id=self.id) + return Package.objects.normal().filter(arch__in=self.applicable_arches(), + repo__testing=self.repo.testing, repo__staging=self.repo.staging, + pkgbase=self.pkgbase).exclude(id=self.id) def is_same_version(self, other): 'is this package similar, name and version-wise, to another' @@ -323,6 +349,17 @@ class Package(models.Model): except Package.DoesNotExist: return None + def in_staging(self): + '''attempt to locate this package in a staging repo; if we are in + a staging repo we will always return None.''' + if self.repo.staging: + return None + try: + return Package.objects.normal().get(repo__staging=True, + pkgname=self.pkgname, arch=self.arch) + except Package.DoesNotExist: + return None + def elsewhere(self): '''attempt to locate this package anywhere else, regardless of architecture or repository. Excludes this package from the list.''' @@ -330,12 +367,6 @@ class Package(models.Model): pkgname=self.pkgname).exclude(id=self.id).order_by( 'arch__name', 'repo__name') -class Signoff(models.Model): - pkg = models.ForeignKey(Package) - pkgver = models.CharField(max_length=255) - pkgrel = models.CharField(max_length=255) - packager = models.ForeignKey(User) - class PackageFile(models.Model): pkg = models.ForeignKey(Package) is_directory = models.BooleanField(default=False) diff --git a/main/templatetags/pgp.py b/main/templatetags/pgp.py index d9126dbd..956de892 100644 --- a/main/templatetags/pgp.py +++ b/main/templatetags/pgp.py @@ -3,6 +3,17 @@ from django.conf import settings register = template.Library() +def format_key(key_id): + print len(key_id) + if len(key_id) in (8, 20): + return u'0x%s' % key_id + elif len(key_id) == 40: + # normal display format is 5 groups of 4 hex chars seperated by spaces, + # double space, then 5 more groups of 4 hex chars + split = tuple(key_id[i:i+4] for i in range(0, 40, 4)) + return u'%s %s' % (' '.join(split[0:5]), ' '.join(split[5:10])) + return u'0x%s' % key_id + @register.simple_tag def pgp_key_link(key_id): if not key_id: @@ -10,10 +21,10 @@ def pgp_key_link(key_id): # Something like 'pgp.mit.edu:11371' pgp_server = getattr(settings, 'PGP_SERVER', None) if not pgp_server: - return "0x%s" % key_id + return format_key(key_id) url = 'http://%s/pks/lookup?op=vindex&fingerprint=on&exact=on&search=0x%s' % \ (pgp_server, key_id) - values = (url, key_id, key_id) - return '<a href="%s" title="PGP key search for 0x%s">0x%s</a>' % values + values = (url, key_id, format_key(key_id)) + return '<a href="%s" title="PGP key search for 0x%s">%s</a>' % values # vim: set ts=4 sw=4 et: diff --git a/main/utils.py b/main/utils.py index 12d12503..81f689e7 100644 --- a/main/utils.py +++ b/main/utils.py @@ -2,6 +2,9 @@ try: import cPickle as pickle except ImportError: import pickle + +from datetime import datetime + from django.core.cache import cache from django.utils.hashcompat import md5_constructor @@ -81,4 +84,46 @@ def retrieve_latest(sender): pass return None +def set_created_field(sender, **kwargs): + '''This will set the 'created' field on any object to datetime.utcnow() if + it is unset. For use as a pre_save signal handler.''' + obj = kwargs['instance'] + if hasattr(obj, 'created') and not obj.created: + obj.created = datetime.utcnow() + +def groupby_preserve_order(iterable, keyfunc): + '''Take an iterable and regroup using keyfunc to determine whether items + belong to the same group. The order of the iterable is preserved and + similar keys do not have to be consecutive. This means the earliest + occurrence of a given key will determine the order of the lists in the + returned list.''' + seen_keys = {} + result = [] + for item in iterable: + key = keyfunc(item) + + group = seen_keys.get(key, None) + if group is None: + group = [] + seen_keys[key] = group + result.append(group) + + group.append(item) + + return result + +class PackageStandin(object): + '''Resembles a Package object, and has a few of the same fields, but is + really a link to a pkgbase that has no package with matching pkgname.''' + def __init__(self, package): + self.package = package + self.pkgname = package.pkgbase + + def __getattr__(self, name): + return getattr(self.package, name) + + def get_absolute_url(self): + return '/packages/%s/%s/%s/' % ( + self.repo.name.lower(), self.arch.name, self.pkgbase) + # vim: set ts=4 sw=4 et: diff --git a/media/archweb.css b/media/archweb.css index add0fda3..f01f277c 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -20,8 +20,9 @@ body { min-width: 650px; background: #f6f9fc; color: #222; font: normal 100% san p { margin: .33em 0 1em; } ol, ul { margin-bottom: 1em; padding-left: 2em; } ul { list-style: square; } -code { font: 1.2em monospace; background: #ffa; padding: 0.15em 0.25em; } +code { font: 1.2em monospace; background: #ffd; padding: 0.15em 0.25em; } pre { font: 1.2em monospace; border: 1px solid #bdb; background: #dfd; padding: 0.5em; margin: 0.25em 2em; } +pre code { display: block; background: none; } blockquote { margin: 1.5em 2em; } input { vertical-align: middle; } select[multiple] { padding-top: 1px; padding-bottom: 1px; } diff --git a/mirrors/models.py b/mirrors/models.py index 4f70e5a9..a8217844 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -42,7 +42,7 @@ class Mirror(models.Model): def supported_protocols(self): protocols = MirrorProtocol.objects.filter( urls__mirror=self).order_by('protocol').distinct() - return ", ".join([p.protocol for p in protocols]) + return sorted(protocols) def downstream(self): return Mirror.objects.filter(upstream=self).order_by('name') diff --git a/packages/migrations/0008_add_signoff_model.py b/packages/migrations/0008_add_signoff_model.py new file mode 100644 index 00000000..5feed909 --- /dev/null +++ b/packages/migrations/0008_add_signoff_model.py @@ -0,0 +1,166 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Signoff' + db.create_table('packages_signoff', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('pkgbase', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('pkgver', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('pkgrel', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('epoch', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + ('arch', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.Arch'])), + ('repo', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['main.Repo'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='package_signoffs', to=orm['auth.User'])), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('revoked', self.gf('django.db.models.fields.DateTimeField')(null=True)), + ('comments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('packages', ['Signoff']) + + + def backwards(self, orm): + # Deleting model 'Signoff' + db.delete_table('packages_signoff') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + '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'}), + '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'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + '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'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': '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.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + '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'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + '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'}) + }, + 'packages.conflict': { + 'Meta': {'ordering': "['name']", 'object_name': 'Conflict'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'conflicts'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.license': { + 'Meta': {'ordering': "['name']", 'object_name': 'License'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'licenses'", 'to': "orm['main.Package']"}) + }, + 'packages.packagegroup': { + 'Meta': {'object_name': 'PackageGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Package']"}) + }, + 'packages.packagerelation': { + 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_relations'", 'to': "orm['auth.User']"}) + }, + 'packages.provision': { + 'Meta': {'ordering': "['name']", 'object_name': 'Provision'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provides'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.replacement': { + 'Meta': {'ordering': "['name']", 'object_name': 'Replacement'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replaces'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + '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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/migrations/0009_add_packagegroup_name_index.py b/packages/migrations/0009_add_packagegroup_name_index.py new file mode 100644 index 00000000..f81e77fc --- /dev/null +++ b/packages/migrations/0009_add_packagegroup_name_index.py @@ -0,0 +1,149 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.create_index('packages_packagegroup', ['name']) + + def backwards(self, orm): + db.delete_index('packages_packagegroup', ['name']) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + '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'}), + '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'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('main.models.PositiveBigIntegerField', [], {}), + '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'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('main.models.PositiveBigIntegerField', [], {}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': '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.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + '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'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_category': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + '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'}) + }, + 'packages.conflict': { + 'Meta': {'ordering': "['name']", 'object_name': 'Conflict'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'conflicts'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.license': { + 'Meta': {'ordering': "['name']", 'object_name': 'License'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'licenses'", 'to': "orm['main.Package']"}) + }, + 'packages.packagegroup': { + 'Meta': {'object_name': 'PackageGroup'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Package']"}) + }, + 'packages.packagerelation': { + 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_relations'", 'to': "orm['auth.User']"}) + }, + 'packages.provision': { + 'Meta': {'ordering': "['name']", 'object_name': 'Provision'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'provides'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.replacement': { + 'Meta': {'ordering': "['name']", 'object_name': 'Replacement'}, + 'comparison': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'replaces'", 'to': "orm['main.Package']"}), + 'version': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}) + }, + 'packages.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Arch']"}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + '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', [], {'to': "orm['main.Repo']"}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'package_signoffs'", 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index a950bddb..d2fe1878 100644 --- a/packages/models.py +++ b/packages/models.py @@ -1,9 +1,9 @@ -from datetime import datetime - from django.db import models from django.db.models.signals import pre_save, post_save from django.contrib.auth.models import User +from main.utils import set_created_field + class PackageRelation(models.Model): ''' Represents maintainership (or interest) in a package by a given developer. @@ -25,27 +25,63 @@ class PackageRelation(models.Model): def get_associated_packages(self): # TODO: delayed import to avoid circular reference from main.models import Package - return Package.objects.filter(pkgbase=self.pkgbase).select_related( - 'arch', 'repo') + return Package.objects.normal().filter(pkgbase=self.pkgbase) def repositories(self): packages = self.get_associated_packages() return sorted(set([p.repo for p in packages])) def __unicode__(self): - return "%s: %s (%s)" % ( + return u'%s: %s (%s)' % ( self.pkgbase, self.user, self.get_type_display()) class Meta: unique_together = (('pkgbase', 'user', 'type'),) +class Signoff(models.Model): + ''' + A signoff for a package (by pkgbase) at a given point in time. These are + not keyed directly to a Package object so they don't ever get deleted when + Packages come and go from testing repositories. + ''' + pkgbase = models.CharField(max_length=255, db_index=True) + pkgver = models.CharField(max_length=255) + pkgrel = models.CharField(max_length=255) + epoch = models.PositiveIntegerField(default=0) + arch = models.ForeignKey('main.Arch') + repo = models.ForeignKey('main.Repo') + user = models.ForeignKey(User, related_name="package_signoffs") + created = models.DateTimeField(editable=False) + revoked = models.DateTimeField(null=True) + comments = models.TextField(null=True, blank=True) + + REQUIRED = 2 + + @property + def packages(self): + # TODO: delayed import to avoid circular reference + from main.models import Package + return Package.objects.normal().filter(pkgbase=self.pkgbase, + pkgver=self.pkgver, pkgrel=self.pkgrel, epoch=pkg.epoch, + arch=self.arch, repo=self.repo) + + @property + def full_version(self): + if self.epoch > 0: + return u'%d:%s-%s' % (self.epoch, self.pkgver, self.pkgrel) + return u'%s-%s' % (self.pkgver, self.pkgrel) + + def __unicode__(self): + return u'%s-%s: %s' % ( + self.pkgbase, self.full_version, self.user) + class PackageGroup(models.Model): ''' Represents a group a package is in. There is no actual group entity, only names that link to given packages. ''' pkg = models.ForeignKey('main.Package', related_name='groups') - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, db_index=True) def __unicode__(self): return "%s: %s" % (self.name, self.pkg) @@ -112,15 +148,11 @@ def remove_inactive_maintainers(sender, instance, created, **kwargs): type=PackageRelation.MAINTAINER) maint_relations.delete() -def set_created_field(sender, **kwargs): - # We use this same callback for both Isos and Tests - obj = kwargs['instance'] - if not obj.created: - obj.created = datetime.utcnow() - post_save.connect(remove_inactive_maintainers, sender=User, dispatch_uid="packages.models") pre_save.connect(set_created_field, sender=PackageRelation, dispatch_uid="packages.models") +pre_save.connect(set_created_field, sender=Signoff, + dispatch_uid="packages.models") # vim: set ts=4 sw=4 et: diff --git a/packages/urls.py b/packages/urls.py index 3f0755b3..d7d01170 100644 --- a/packages/urls.py +++ b/packages/urls.py @@ -9,19 +9,19 @@ package_patterns = patterns('packages.views', (r'^flag/done/$', 'flag_confirmed', {}, 'package-flag-confirmed'), (r'^unflag/$', 'unflag'), (r'^unflag/all/$', 'unflag_all'), + (r'^signoff/$', 'signoff_package'), + (r'^download/$', 'download'), ) urlpatterns = patterns('packages.views', (r'^flaghelp/$', 'flaghelp'), (r'^signoffs/$', 'signoffs', {}, 'package-signoffs'), - (r'^signoff_package/(?P<arch>[A-z0-9]+)/(?P<pkgname>[A-z0-9\-+.]+)/$', - 'signoff_package'), (r'^update/$', 'update'), - (r'^$', 'search'), + (r'^$', 'search', {}, 'packages-search'), (r'^(?P<page>\d+)/$', 'search'), - (r'^differences/$', 'arch_differences'), + (r'^differences/$', 'arch_differences', {}, 'packages-differences'), (r'^stale_relations/$', 'stale_relations'), (r'^stale_relations/update/$','stale_relations_update'), diff --git a/packages/utils.py b/packages/utils.py index af4675bb..c8c1f8a6 100644 --- a/packages/utils.py +++ b/packages/utils.py @@ -1,11 +1,12 @@ +from collections import defaultdict +from operator import itemgetter + from django.db import connection from django.db.models import Count, Max -from operator import itemgetter - from main.models import Package from main.utils import cache_function -from .models import PackageGroup, PackageRelation +from .models import PackageGroup, PackageRelation, Signoff @cache_function(300) def get_group_info(include_arches=None): @@ -92,21 +93,16 @@ SELECT p.id, q.id WHERE p.arch_id IN (%s, %s) AND ( q.id IS NULL - OR - p.pkgver != q.pkgver - OR - p.pkgrel != q.pkgrel - OR - p.epoch != q.epoch + OR p.pkgver != q.pkgver + OR p.pkgrel != q.pkgrel + OR p.epoch != q.epoch ) """ cursor = connection.cursor() cursor.execute(sql, [arch_a.id, arch_b.id]) results = cursor.fetchall() - to_fetch = [] - for row in results: - # column A will always have a value, column B might be NULL - to_fetch.append(row[0]) + # column A will always have a value, column B might be NULL + to_fetch = [row[0] for row in results] # fetch all of the necessary packages pkgs = Package.objects.normal().in_bulk(to_fetch) # now build a list of tuples containing differences @@ -152,4 +148,28 @@ SELECT DISTINCT id id__in=to_fetch) return relations +def get_current_signoffs(): + '''Returns a mapping of pkgbase -> signoff objects.''' + sql = """ +SELECT DISTINCT s.id + FROM packages_signoff s + JOIN packages p ON ( + s.pkgbase = p.pkgbase + AND s.pkgver = p.pkgver + AND s.pkgrel = p.pkgrel + AND s.epoch = p.epoch + AND s.arch_id = p.arch_id + AND s.repo_id = p.repo_id + ) + JOIN repos r ON p.repo_id = r.id + WHERE r.testing = %s +""" + cursor = connection.cursor() + cursor.execute(sql, [True]) + results = cursor.fetchall() + # fetch all of the returned signoffs by ID + to_fetch = [row[0] for row in results] + signoffs = Signoff.objects.select_related('user').in_bulk(to_fetch) + return signoffs.values() + # vim: set ts=4 sw=4 et: diff --git a/packages/views.py b/packages/views.py index b64beb68..f45c25d6 100644 --- a/packages/views.py +++ b/packages/views.py @@ -8,7 +8,7 @@ from django.core.mail import send_mail from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Q from django.http import HttpResponse, Http404 -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, get_list_or_404, redirect from django.template import loader, Context from django.utils import simplejson from django.views.decorators.cache import never_cache @@ -18,15 +18,16 @@ from django.views.generic import list_detail from django.views.generic.simple import direct_to_template from datetime import datetime +from operator import attrgetter import string from urllib import urlencode -from main.models import Package, PackageFile -from main.models import Arch, Repo, Signoff -from main.utils import make_choice +from main.models import Package, PackageFile, Arch, Repo +from main.utils import make_choice, groupby_preserve_order, PackageStandin from mirrors.models import MirrorUrl -from .models import PackageRelation, PackageGroup -from .utils import get_group_info, get_differences_info, get_wrong_permissions +from .models import PackageRelation, PackageGroup, Signoff +from .utils import (get_group_info, get_differences_info, + get_wrong_permissions, get_current_signoffs) class PackageJSONEncoder(DjangoJSONEncoder): pkg_attributes = [ 'pkgname', 'pkgbase', 'repo', 'arch', 'pkgver', @@ -118,7 +119,8 @@ def details(request, name='', repo='', arch=''): arches.extend(Arch.objects.filter(agnostic=True)) repo = get_object_or_404(Repo, name__iexact=repo) pkgs = Package.objects.normal().filter(pkgbase=name, - repo__testing=repo.testing, arch__in=arches).order_by('pkgname') + repo__testing=repo.testing, repo__staging=repo.staging, + arch__in=arches).order_by('pkgname') if len(pkgs) == 0: raise Http404 context = { @@ -191,6 +193,7 @@ class PackageSearchForm(forms.Form): arch = forms.MultipleChoiceField(required=False) q = forms.CharField(required=False) maintainer = forms.ChoiceField(required=False) + packager = forms.ChoiceField(required=False) last_update = forms.DateField(required=False, widget=AdminDateWidget(), label='Last Updated After') flagged = forms.ChoiceField( @@ -213,6 +216,9 @@ class PackageSearchForm(forms.Form): self.fields['maintainer'].choices = \ [('', 'All'), ('orphan', 'Orphan')] + \ [(m.username, m.get_full_name()) for m in maints] + self.fields['packager'].choices = \ + [('', 'All'), ('unknown', 'Unknown')] + \ + [(m.username, m.get_full_name()) for m in maints] def search(request, page=None): limit = 50 @@ -237,6 +243,12 @@ def search(request, page=None): user__username=form.cleaned_data['maintainer']).values('pkgbase') packages = packages.filter(pkgbase__in=inner_q) + if form.cleaned_data['packager'] == 'unknown': + packages = packages.filter(packager__isnull=True) + elif form.cleaned_data['packager']: + packages = packages.filter( + packager__username=form.cleaned_data['packager']) + if form.cleaned_data['flagged'] == 'Flagged': packages = packages.filter(flag_date__isnull=False) elif form.cleaned_data['flagged'] == 'Not Flagged': @@ -262,15 +274,14 @@ def search(request, page=None): else: form = PackageSearchForm() - if packages.count() == 1: - return redirect(packages[0]) - current_query = request.GET.urlencode() page_dict = { 'search_form': form, 'current_query': current_query } - allowed_sort = ["arch", "repo", "pkgname", "last_update", "flag_date"] + allowed_sort = ["arch", "repo", "pkgname", "pkgbase", + "compressed_size", "installed_size", + "build_date", "last_update", "flag_date"] allowed_sort += ["-" + s for s in allowed_sort] sort = request.GET.get('sort', None) # TODO: sorting by multiple fields makes using a DB index much harder @@ -293,11 +304,14 @@ def files(request, name, repo, arch): pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) fileslist = PackageFile.objects.filter(pkg=pkg).order_by('directory', 'filename') + context = { + 'pkg': pkg, + 'files': fileslist, + } template = 'packages/files.html' if request.is_ajax(): template = 'packages/files-list.html' - return direct_to_template(request, template, - {'pkg':pkg, 'files':fileslist}) + return direct_to_template(request, template, context) def details_json(request, name, repo, arch): pkg = get_object_or_404(Package, @@ -333,45 +347,106 @@ def unflag_all(request, name, repo, arch): pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) # find all packages from (hopefully) the same PKGBUILD - pkgs = Package.objects.filter( - pkgbase=pkg.pkgbase, repo__testing=pkg.repo.testing) + pkgs = Package.objects.filter(pkgbase=pkg.pkgbase, + repo__testing=pkg.repo.testing, repo__staging=pkg.repo.staging) pkgs.update(flag_date=None) return redirect(pkg) +class PackageSignoffGroup(object): + '''Encompasses all packages in testing with the same pkgbase.''' + def __init__(self, packages, target_repo=None, signoffs=None): + if len(packages) == 0: + raise Exception + self.packages = packages + self.target_repo = target_repo + self.signoffs = signoffs + + first = packages[0] + self.pkgbase = first.pkgbase + self.arch = first.arch + self.repo = first.repo + self.version = '' + + version = first.full_version + if all(version == pkg.full_version for pkg in packages): + self.version = version + + @property + def package(self): + '''Try and return a relevant single package object representing this + group. Start by seeing if there is only one package, then look for the + matching package by name, finally falling back to a standin package + object.''' + if len(self.packages) == 1: + return self.packages[0] + + same_pkgs = [p for p in self.packages if p.pkgname == p.pkgbase] + if same_pkgs: + return same_pkgs[0] + + return PackageStandin(self.packages[0]) + + def find_signoffs(self, all_signoffs): + '''Look through a list of Signoff objects for ones matching this + particular group and store them on the object.''' + if self.signoffs is None: + self.signoffs = [] + for s in all_signoffs: + if s.pkgbase != self.pkgbase: + continue + if self.version and not s.full_version == self.version: + continue + if s.arch_id == self.arch.id and s.repo_id == self.repo.id: + self.signoffs.append(s) + + def approved(self): + if self.signoffs: + good_signoffs = [s for s in self.signoffs if not s.revoked] + return len(good_signoffs) >= Signoff.REQUIRED + return False + @permission_required('main.change_package') @never_cache def signoffs(request): - packages = Package.objects.select_related('arch', 'repo', 'signoffs').filter(repo__testing=True).order_by("pkgname") - package_list = [] - - q_pkgname = Package.objects.filter(repo__testing=True).values('pkgname').distinct().query - package_repos = Package.objects.values('pkgname', 'repo__name').exclude(repo__testing=True).filter(pkgname__in=q_pkgname) - pkgtorepo = dict() - for pr in package_repos: - pkgtorepo[pr['pkgname']] = pr['repo__name'] - - for package in packages: - if package.pkgname in pkgtorepo: - repo = pkgtorepo[package.pkgname] - else: - repo = "Unknown" - package_list.append((package, repo)) + test_pkgs = Package.objects.normal().filter(repo__testing=True) + packages = test_pkgs.order_by('pkgname') + + # Collect all pkgbase values in testing repos + q_pkgbase = test_pkgs.values('pkgbase') + package_repos = Package.objects.order_by().values_list( + 'pkgbase', 'repo__name').filter( + repo__testing=False, repo__staging=False, + pkgbase__in=q_pkgbase).distinct() + pkgtorepo = dict(package_repos) + + # Collect all existing signoffs for these packages + signoffs = get_current_signoffs() + + same_pkgbase_key = lambda x: (x.repo.name, x.arch.name, x.pkgbase) + grouped = groupby_preserve_order(packages, same_pkgbase_key) + signoff_groups = [] + for group in grouped: + signoff_group = PackageSignoffGroup(group) + signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase, + "Unknown") + signoff_group.find_signoffs(signoffs) + signoff_groups.append(signoff_group) + + signoff_groups.sort(key=attrgetter('pkgbase')) + return direct_to_template(request, 'packages/signoffs.html', - {'packages': package_list}) + {'signoff_groups': signoff_groups}) @permission_required('main.change_package') @never_cache -def signoff_package(request, arch, pkgname): - pkg = get_object_or_404(Package, - arch__name=arch, - pkgname=pkgname, - repo__testing=True) +def signoff_package(request, name, repo, arch): + packages = get_list_or_404(Package, pkgbase=name, + arch__name=arch, repo__name__iexact=repo, repo__testing=True) + pkg = packages[0] signoff, created = Signoff.objects.get_or_create( - pkg=pkg, - pkgver=pkg.pkgver, - pkgrel=pkg.pkgrel, - packager=request.user) + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, user=request.user) if request.is_ajax(): data = { @@ -406,7 +481,8 @@ def flag(request, name, repo, arch): # find all packages from (hopefully) the same PKGBUILD pkgs = Package.objects.normal().filter( pkgbase=pkg.pkgbase, flag_date__isnull=True, - repo__testing=pkg.repo.testing).order_by( + repo__testing=pkg.repo.testing, + repo__staging=pkg.repo.staging).order_by( 'pkgname', 'repo__name', 'arch__name') if request.POST: @@ -461,7 +537,8 @@ def flag_confirmed(request, name, repo, arch): pkgname=name, repo__name__iexact=repo, arch__name=arch) pkgs = Package.objects.normal().filter( pkgbase=pkg.pkgbase, flag_date=pkg.flag_date, - repo__testing=pkg.repo.testing).order_by( + repo__testing=pkg.repo.testing, + repo__staging=pkg.repo.staging).order_by( 'pkgname', 'repo__name', 'arch__name') context = {'package': pkg, 'packages': pkgs} diff --git a/public/utils.py b/public/utils.py index 0be3ebaa..5900c674 100644 --- a/public/utils.py +++ b/public/utils.py @@ -1,7 +1,7 @@ from operator import attrgetter from main.models import Arch, Package, Repo -from main.utils import cache_function +from main.utils import cache_function, groupby_preserve_order, PackageStandin class RecentUpdate(object): def __init__(self, packages): @@ -35,18 +35,12 @@ class RecentUpdate(object): for package in self.packages: yield package else: - # time to fake out the template, this is a tad dirty - arches = set(pkg.arch for pkg in self.others) - for arch in arches: - url = '/packages/%s/%s/%s/' % ( - self.repo.name.lower(), arch.name, self.pkgbase) - package_stub = { - 'pkgname': self.pkgbase, - 'arch': arch, - 'repo': self.repo, - 'get_absolute_url': url - } - yield package_stub + # fake out the template- this is slightly hacky but yields one + # 'package-like' object per arch which is what the normal loop does + arches = set() + for package in self.others: + if package.arch not in arches and not arches.add(package.arch): + yield PackageStandin(package) @cache_function(300) def get_recent_updates(number=15): @@ -59,20 +53,14 @@ def get_recent_updates(number=15): for arch in Arch.objects.all(): pkgs += list(Package.objects.normal().filter( arch=arch).order_by('-last_update')[:fetch]) - pkgs.sort(key=attrgetter('last_update')) + pkgs.sort(key=attrgetter('last_update'), reverse=True) - updates = [] - while len(pkgs) > 0: - pkg = pkgs.pop() - - in_group = lambda x: pkg.repo == x.repo and pkg.pkgbase == x.pkgbase - samepkgs = [other for other in pkgs if in_group(other)] - samepkgs.append(pkg) + same_pkgbase_key = lambda x: (x.repo.name, x.pkgbase) + grouped = groupby_preserve_order(pkgs, same_pkgbase_key) - # now remove all the packages we just pulled out - pkgs = [other for other in pkgs if other not in samepkgs] - - update = RecentUpdate(samepkgs) + updates = [] + for group in grouped: + update = RecentUpdate(group) updates.append(update) return updates[:number] diff --git a/releng/models.py b/releng/models.py index 07ede1c5..5510db6a 100644 --- a/releng/models.py +++ b/releng/models.py @@ -1,6 +1,7 @@ -from datetime import datetime - from django.db import models +from django.db.models.signals import pre_save + +from main.utils import set_created_field class IsoOption(models.Model): name = models.CharField(max_length=200) @@ -104,14 +105,6 @@ class Test(models.Model): success = models.BooleanField() comments = models.TextField(null=True, blank=True) -def set_created_field(sender, **kwargs): - # We use this same callback for both Isos and Tests - obj = kwargs['instance'] - if not obj.created: - obj.created = datetime.utcnow() - -from django.db.models.signals import pre_save - pre_save.connect(set_created_field, sender=Iso, dispatch_uid="releng.models") pre_save.connect(set_created_field, sender=Test, diff --git a/settings.py b/settings.py index b1063dfe..0bc1d084 100644 --- a/settings.py +++ b/settings.py @@ -32,6 +32,7 @@ SITE_ID = 1 # Default date format in templates for 'date' filter DATE_FORMAT = 'Y-m-d' +DATETIME_FORMAT = 'Y-m-d H:i' # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. diff --git a/sitemaps.py b/sitemaps.py index 37387f15..8ac5bc4f 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -1,4 +1,6 @@ from django.contrib.sitemaps import Sitemap +from django.core.urlresolvers import reverse + from main.models import Package from news.models import News from packages.utils import get_group_info @@ -41,7 +43,7 @@ class PackageGroupsSitemap(Sitemap): class NewsSitemap(Sitemap): changefreq = "never" - priority = "0.7" + priority = "0.8" def items(self): return News.objects.all() @@ -49,4 +51,34 @@ class NewsSitemap(Sitemap): def lastmod(self, obj): return obj.last_modified + +class BaseSitemap(Sitemap): + base_viewnames = ( + ('index', 1.0, 'hourly'), + ('packages-search', 0.8, 'hourly'), + 'page-about', 'page-art', 'page-svn', 'page-devs', 'page-tus', + 'page-fellows', 'page-donate', 'page-download', 'news-list', + 'feeds-list', 'groups-list', 'mirror-list', 'mirror-status', + 'mirrorlist', 'packages-differences', 'releng-test-overview', + ) + + def items(self): + return self.base_viewnames + + def location(self, obj): + name = obj + if isinstance(obj, tuple): + name = obj[0] + return reverse(name) + + def priority(self, obj): + if isinstance(obj, tuple): + return obj[1] + return 0.7 + + def changefreq(self, obj): + if isinstance(obj, tuple): + return obj[2] + return 'monthly' + # vim: set ts=4 sw=4 et: diff --git a/templates/devel/index.html b/templates/devel/index.html index 99973f1b..6b50d3a0 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -27,7 +27,7 @@ <td><a href="{{ pkg.get_absolute_url }}" title="View package details for {{ pkg.pkgname }}">{{ pkg.pkgname }}</a></td> <td>{{ pkg.repo.name }}</td> - <td>{{ pkg.pkgver }}</td> + <td>{{ pkg.full_version }}</td> <td>{{ pkg.arch.name }}</td> <td>{{ pkg.flag_date|date }}</td> <td>{{ pkg.last_update|date }}</td> @@ -115,6 +115,9 @@ <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/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> @@ -182,10 +185,10 @@ </div>{# dash-by-arch #} {% endcache %} -{% cache 60 dev-dash-by-maintainer %} -<div id="dash-by-maintainer" class="box"> +{% cache 60 dev-dash-by-developer %} +<div id="dash-by-developer" class="box"> - <h2>Stats by Maintainer</h2> + <h2>Stats by Developer</h2> {% if perms.main.change_package %} <p><a href="/packages/stale_relations/">Look for stale relations</a></p> @@ -195,17 +198,24 @@ <thead> <tr> <th class="key">Maintainer</th> - <th># Packages</th> + <th># Maintained</th> <th># Flagged</th> + <th># Last Packager</th> </tr> <tr class="even"> - <td><em>Orphan</em></td> + <td><em>Orphan/Unknown</em></td> <td><a href="/packages/?maintainer=orphan" title="View all orphan packages"> - <strong>{{ orphan.package_count }}</strong> packages</a></td> + <strong>{{ orphan.package_count }}</strong> packages</a> + </td> <td><a href="/packages/?maintainer=orphan&flagged=Flagged" title="View all flagged orphan packages"> - <strong>{{ orphan.flagged_count }}</strong> packages</a></td> + <strong>{{ orphan.flagged_count }}</strong> packages</a> + </td> + <td><a href="/packages/?packager=unknown" + title="View all packages last updated by unknown"> + <strong>{{ orphan.updated_count }}</strong> packages</a> + </td> </tr> </thead> <tbody> @@ -214,15 +224,21 @@ <td>{{ maint.get_full_name }}</td> <td><a href="/packages/?maintainer={{ maint.username }}" title="View all packages maintained by {{ maint.get_full_name }}"> - <strong>{{ maint.package_count }}</strong> packages</a></td> + <strong>{{ maint.package_count }}</strong> packages</a> + </td> <td><a href="/packages/?maintainer={{ maint.username }}&flagged=Flagged" title="View all flagged packages maintained by {{ maint.get_full_name }}"> - <strong>{{ maint.flagged_count }}</strong> packages</a></td> + <strong>{{ maint.flagged_count }}</strong> packages</a> + </td> + <td><a href="/packages/?packager={{ maint.username }}" + title="View all packages last updated by {{ maint.get_full_name }}"> + <strong>{{ maint.updated_count }}</strong> packages</a> + </td> </tr> {% endfor %} </tbody> </table> -</div>{# #dash-by-maintainer #} +</div>{# #dash-by-developer #} {% endcache %} {% load cdn %}{% jquery %} @@ -236,9 +252,11 @@ $(document).ready(function() { {widgets: ['zebra'], sortList: [[0,0], [1,0]]}); $("#dash-todo:not(:has(tbody tr.empty))").tablesorter( {widgets: ['zebra'], sortList: [[1,1]]}); - $(".dash-stats").tablesorter( - {widgets: ['zebra'], sortList: [[0,0]], - headers: { 1: { sorter: 'pkgcount' }, 2: { sorter: 'pkgcount' } } }); + $(".dash-stats").tablesorter({ + widgets: ['zebra'], + sortList: [[0,0]], + headers: { 1: { sorter: 'pkgcount' }, 2: { sorter: 'pkgcount' }, 3: { sorter: 'pkgcount' } } + }); }); </script> {% endblock %} diff --git a/templates/devel/packages.html b/templates/devel/packages.html index b8deb0bc..8f149a5c 100644 --- a/templates/devel/packages.html +++ b/templates/devel/packages.html @@ -5,12 +5,12 @@ {% block content %} <div class="box"> - <h2>{{ title }}{% if maintainer %}, - maintained by {{ maintainer.get_full_name }}{% endif%}</h2> - <p>{{ packages|length }} package{{ packages|pluralize }} found. - {% if maintainer %}This report only includes packages maintained by - {{ maintainer.get_full_name }} ({{ maintainer.username }}).{% endif %} - </p> + <h2>{{ title }}{% if maintainer %}, + maintained by {{ maintainer.get_full_name }}{% endif%}</h2> + <p>{{ packages|length }} package{{ packages|pluralize }} found. + {% if maintainer %}This report only includes packages maintained by + {{ maintainer.get_full_name }} ({{ maintainer.username }}).{% endif %} + </p> <table class="results"> <thead> <tr> diff --git a/templates/mirrors/mirrors.html b/templates/mirrors/mirrors.html index 41cca6fa..53a68005 100644 --- a/templates/mirrors/mirrors.html +++ b/templates/mirrors/mirrors.html @@ -28,7 +28,7 @@ <td>{{mirror.get_tier_display}}</td> <td>{{mirror.country}}</td> <td>{{mirror.isos|yesno}}</td> - <td class="wrap">{{mirror.supported_protocols}}</td> + <td class="wrap">{{mirror.supported_protocols|join:", "}}</td> {% if user.is_authenticated %} <td>{{mirror.public|yesno}}</td> <td>{{mirror.active|yesno}}</td> diff --git a/templates/packages/details.html b/templates/packages/details.html index d1e6eee9..068d1f1a 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -142,7 +142,7 @@ <td>{% with pkg.packager as pkgr %}{% if pkgr %}{% userpkgs pkgr %}{% else %}{{ pkg.packager_str }}{% endif %}{% endwith %}</td> </tr><tr> <th>Build Date:</th> - <td>{{ pkg.build_date }} UTC</td> + <td>{{ pkg.build_date|date:"DATETIME_FORMAT" }} UTC</td> </tr><tr> <th>Last Updated:</th> <td>{{ pkg.last_update|date }}</td> diff --git a/templates/packages/files-list.html b/templates/packages/files-list.html index bb89b663..95a85d24 100644 --- a/templates/packages/files-list.html +++ b/templates/packages/files-list.html @@ -2,12 +2,16 @@ <p class="message">Note: This file list was generated from a previous version of the package; it may be out of date.</p> {% endif %} -{% if files.count %} +{% if pkg.files_last_update %} +{% if files|length %} <ul> {% for file in files %} <li>{{ file.directory }}{{ file.filename|default:'' }}</li> {% endfor %} </ul> {% else %} +<p class="message">Package has no files.</p> +{% endif %} +{% else %} <p class="message">No file list available.</p> {% endif %} diff --git a/templates/packages/flag_confirmed.html b/templates/packages/flag_confirmed.html index baa466dd..ebb14608 100644 --- a/templates/packages/flag_confirmed.html +++ b/templates/packages/flag_confirmed.html @@ -4,12 +4,13 @@ {% block content %} <div id="pkg-flag" class="box"> - <h2>Package Flagged - {{ package.pkgname }}</h2> + <h2>Package Flagged - {{ package.pkgname }}</h2> <p>Thank you, the maintainers have been notified the following packages are out-of-date:</p> <ul> {% for pkg in packages %} - <li>{{ pkg.pkgname }} {{ pkg.full_version }} [{{ pkg.repo.name|lower }}] ({{ pkg.arch.name }})</li> + <li><a href="{{ pkg.get_absolute_url }}" + title="Package details for {{package.pkgname}}">{{ pkg.pkgname }} {{ pkg.full_version }}</a> [{{ pkg.repo.name|lower }}] ({{ pkg.arch.name }})</li> {% endfor %} </ul> diff --git a/templates/packages/packages_list.html b/templates/packages/packages_list.html index 2508d8fd..13b8caba 100644 --- a/templates/packages/packages_list.html +++ b/templates/packages/packages_list.html @@ -5,6 +5,7 @@ {% block content %} <div class="box"> <h2>{{ list_title }} - {{ name }} ({{ arch.name }})</h2> + <p>{{ packages|length }} package{{ packages|pluralize }} found.</p> <table class="results"> <thead> <tr> diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index 157843ac..53e9e46d 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -3,16 +3,19 @@ {% block navbarclass %}anb-packages{% endblock %} {% block content %} -{% if packages %} <div id="dev-signoffs" class="box"> <h2>Package Signoffs</h2> + <p>{{ signoff_groups|length }} signoff group{{ signoff_groups|pluralize }} found. + A "signoff group" consists of packages grouped by pkgbase, architecture, and repository.</p> + <table id="signoffs" class="results"> <thead> <tr> <th>Arch</th> - <th>Package</th> + <th>Package Base</th> + <th># of Packages</th> <th>Version</th> <th>Last Updated</th> <th>Target Repo</th> @@ -21,29 +24,32 @@ </tr> </thead> <tbody> - {% for pkg,target in packages %} + {% for group in signoff_groups %} + {% with group.package as pkg %} <tr class="{% cycle 'odd' 'even' %}"> <td>{{ pkg.arch.name }}</td> <td><a href="{{ pkg.get_absolute_url }}" title="View package details for {{ pkg.pkgname }}">{{ pkg.pkgname }}</a></td> + <td>{{ group.packages|length }}</td> <td>{{ pkg.full_version }}</td> - <td>{{ pkg.last_update }}</td> - <td>{{ target }}</td> - <td class="signoff-{{pkg.approved_for_signoff|yesno}}"> - {{ pkg.approved_for_signoff|yesno:"Yes,No" }}</td> + <td>{{ pkg.last_update|date }}</td> + <td>{{ group.target_repo }}</td> + <td class="signoff-{{group.approved|yesno}}"> + {{ group.approved|yesno:"Yes,No" }}</td> <td> <ul> - <li><a class="signoff-link" href="/packages/signoff_package/{{pkg.arch}}/{{pkg.pkgname}}/" - title="Signoff {{pkg.pkgname}} for {{pkg.arch}}">Signoff</a> + <li><a class="signoff-link" href="{{ pkg.get_absolute_url }}signoff/" + title="Signoff {{ pkg.pkgname }} for {{ pkg.arch }}">Signoff</a> </li> - {% for signoff in pkg.signoffs %} - <li class="signed-username" title="Signed off by {{signoff.packager}}"> - {{signoff.packager}}</li> + {% for signoff in group.signoffs %} + <li class="signed-username" title="Signed off by {{ signoff.user }}"> + {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}</li> {% endfor %} </ul> </td> </tr> - {% endfor %} + {% endwith %} + {% endfor %} </tbody> </table> </div> @@ -57,5 +63,4 @@ $(document).ready(function() { headers: { 6: { sorter: false } } }); }); </script> -{% endif %} {% endblock %} diff --git a/todolists/views.py b/todolists/views.py index d6a25463..a63516e8 100644 --- a/todolists/views.py +++ b/todolists/views.py @@ -23,8 +23,8 @@ class TodoListForm(forms.ModelForm): package_names = [s.strip() for s in self.cleaned_data['packages'].split("\n")] package_names = set(package_names) - packages = Package.objects.filter(pkgname__in=package_names).exclude( - repo__testing=True).select_related( + packages = Package.objects.filter(pkgname__in=package_names).filter( + repo__testing=False, repo__staging=False).select_related( 'arch', 'repo').order_by('arch') return packages @@ -10,6 +10,7 @@ from feeds import PackageFeed, NewsFeed import sitemaps sitemaps = { + 'base': sitemaps.BaseSitemap, 'news': sitemaps.NewsSitemap, 'packages': sitemaps.PackagesSitemap, 'package-files': sitemaps.PackageFilesSitemap, |