summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolás Reynolds <fauno@kiwwwi.com.ar>2011-08-03 16:05:35 -0300
committerNicolás Reynolds <fauno@kiwwwi.com.ar>2011-08-03 16:05:35 -0300
commitfbd23db51b7160a308cd88e407e676994eb08b10 (patch)
treee3816cc4e3f0ee07539fb4464b2d886a43ecc318
parenta8b2fc84ba96c83ec1addf89ac04608fbf572705 (diff)
parent0f6c80e9a36bc5770e95543b4374c5ace4383cf5 (diff)
Merge branch 'master' of git://projects.archlinux.org/archweb
Conflicts: packages/urls.py templates/packages/details.html
-rw-r--r--devel/management/commands/rematch_packager.py64
-rw-r--r--devel/management/commands/reporead.py55
-rw-r--r--devel/tests.py88
-rw-r--r--devel/utils.py80
-rw-r--r--devel/views.py21
-rw-r--r--main/migrations/0051_auto__chg_field_userprofile_pgp_key.py160
-rw-r--r--main/migrations/0052_auto__del_signoff.py166
-rw-r--r--main/models.py79
-rw-r--r--main/templatetags/pgp.py17
-rw-r--r--main/utils.py45
-rw-r--r--media/archweb.css3
-rw-r--r--mirrors/models.py2
-rw-r--r--packages/migrations/0008_add_signoff_model.py166
-rw-r--r--packages/migrations/0009_add_packagegroup_name_index.py149
-rw-r--r--packages/models.py56
-rw-r--r--packages/urls.py8
-rw-r--r--packages/utils.py46
-rw-r--r--packages/views.py161
-rw-r--r--public/utils.py38
-rw-r--r--releng/models.py13
-rw-r--r--settings.py1
-rw-r--r--sitemaps.py34
-rw-r--r--templates/devel/index.html46
-rw-r--r--templates/devel/packages.html12
-rw-r--r--templates/mirrors/mirrors.html2
-rw-r--r--templates/packages/details.html2
-rw-r--r--templates/packages/files-list.html6
-rw-r--r--templates/packages/flag_confirmed.html5
-rw-r--r--templates/packages/packages_list.html1
-rw-r--r--templates/packages/signoffs.html33
-rw-r--r--todolists/views.py4
-rw-r--r--urls.py1
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&nbsp; %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&amp;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 }}&amp;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
diff --git a/urls.py b/urls.py
index 209fb899..27632dfc 100644
--- a/urls.py
+++ b/urls.py
@@ -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,