From ac2278423a3d449fdfe8c813f1f2d391ef9aff08 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 3 Nov 2011 14:59:00 -0500 Subject: Many signoff page improvements Add a new 'SignoffSpecification' model which will capture metadata regarding a specific package if it differs from the norm- e.g. more or less than 2 required signoffs, is known to be bad, a comment from the maintainer, etc. The groundwork is laid here; much of this will still need to be wired up in the future. Enhance the view with a lot more JS prettiness and add revoking of signoffs. The signoff page can be filtered and the links and all the fun stuff are totally dynamic now. Signed-off-by: Dan McGee --- main/models.py | 11 -- media/archweb.css | 1 - media/archweb.js | 72 ++++++-- .../0010_auto__add_signoffspecification.py | 183 +++++++++++++++++++++ packages/models.py | 45 ++++- packages/urls.py | 1 + packages/views.py | 71 +++++--- templates/packages/differences.html | 2 +- templates/packages/signoff_cell.html | 12 ++ templates/packages/signoffs.html | 50 +++--- 10 files changed, 379 insertions(+), 69 deletions(-) create mode 100644 packages/migrations/0010_auto__add_signoffspecification.py create mode 100644 templates/packages/signoff_cell.html diff --git a/main/models.py b/main/models.py index 780453c0..d55a9673 100644 --- a/main/models.py +++ b/main/models.py @@ -7,7 +7,6 @@ from django.forms import ValidationError from main.utils import cache_function, make_choice, set_created_field from packages.models import PackageRelation -from packages.models import Signoff as PackageSignoff from datetime import datetime from itertools import groupby @@ -213,16 +212,6 @@ class Package(models.Model): package_relations__pkgbase=self.pkgbase, package_relations__type=PackageRelation.MAINTAINER) - @property - def signoffs(self): - 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): - count = self.signoffs.filter(revoked__isnull=True).count() - return count >= PackageSignoff.REQUIRED - @cache_function(300) def applicable_arches(self): '''The list of (this arch) + (available agnostic arches).''' diff --git a/media/archweb.css b/media/archweb.css index ea2f3fb5..62dc4fbc 100644 --- a/media/archweb.css +++ b/media/archweb.css @@ -912,7 +912,6 @@ ul.admin-actions { #dev-signoffs .signed-username { color: #888; - margin-left: 0.5em; } /* iso testing feedback form */ diff --git a/media/archweb.js b/media/archweb.js index a51ae460..43812b33 100644 --- a/media/archweb.js +++ b/media/archweb.js @@ -139,7 +139,7 @@ function ajaxifyFiles() { /* packages/differences.html */ function filter_packages() { - // start with all rows, and then remove ones we shouldn't show + /* start with all rows, and then remove ones we shouldn't show */ var rows = $('#tbody_differences').children(); var all_rows = rows; if (!$('#id_multilib').is(':checked')) { @@ -150,12 +150,12 @@ function filter_packages() { rows = rows.filter('.' + arch); } if (!$('#id_minor').is(':checked')) { - // this check is done last because it is the most expensive + /* this check is done last because it is the most expensive */ var pat = /(.*)-(.+)/; rows = rows.filter(function(index) { var cells = $(this).children('td'); - // all this just to get the split version out of the table cell + /* all this just to get the split version out of the table cell */ var ver_a = cells.eq(2).find('span').text().match(pat); if (!ver_a) { return true; @@ -166,26 +166,26 @@ function filter_packages() { return true; } - // first check pkgver + /* first check pkgver */ if (ver_a[1] !== ver_b[1]) { return true; } - // pkgver matched, so see if rounded pkgrel matches + /* pkgver matched, so see if rounded pkgrel matches */ if (Math.floor(parseFloat(ver_a[2])) === Math.floor(parseFloat(ver_b[2]))) { return false; } - // pkgrel didn't match, so keep the row + /* pkgrel didn't match, so keep the row */ return true; }); } - // hide all rows, then show the set we care about + /* hide all rows, then show the set we care about */ all_rows.hide(); rows.show(); - // make sure we update the odd/even styling from sorting + /* make sure we update the odd/even styling from sorting */ $('.results').trigger('applyWidgets'); } -function filter_reset() { +function filter_packages_reset() { $('#id_archonly').val('both'); $('#id_multilib').removeAttr('checked'); $('#id_minor').removeAttr('checked'); @@ -213,26 +213,72 @@ function todolist_flag() { function signoff_package() { var link = this; $.getJSON(link.href, function(data) { + link = $(link); + var signoff = null; if (data.created) { - var signoff = $('
  • ').addClass('signed-username').text(data.user); - $(link).append(signoff); + signoff = $('
  • ').addClass('signed-username').text(data.user); + link.closest('td').children('ul').append(signoff); + } else if(data.user) { + signoff = link.closest('td').find('li').filter(function(index) { + return $(this).text() == data.user; + }); + } + console.log(signoff, data.revoked, data.user); + if (signoff && data.revoked) { + signoff.text(signoff.text() + ' (revoked)'); } /* update the approved column to reflect reality */ var approved; if (data.approved) { - approved = $(link).closest('tr').children('.signoff-no'); + approved = link.closest('tr').children('.signoff-no'); approved.text('Yes').addClass( 'signoff-yes').removeClass('signoff-no'); } else { - approved = $(link).closest('tr').children('.signoff-yes'); + approved = link.closest('tr').children('.signoff-yes'); approved.text('No').addClass( 'signoff-no').removeClass('signoff-yes'); } + link.removeAttr('title'); + /* Form our new link. The current will be something like + * '/packages/repo/arch/package/...' */ + var base_href = link.attr('href').split('/').slice(0, 5).join('/'); + if (data.revoked) { + link.text('Signoff'); + link.attr('href', base_href + '/signoff/'); + } else { + link.text('Revoke Signoff'); + link.attr('href', base_href + '/signoff/revoke/'); + } $('.results').trigger('updateCell', approved); }); return false; } +function filter_signoffs() { + /* start with all rows, and then remove ones we shouldn't show */ + var rows = $('#tbody_signoffs').children(); + var all_rows = rows; + $('#signoffs_filter .arch_filter').each(function() { + if (!$(this).is(':checked')) { + console.log($(this).val()); + rows = rows.not('.' + $(this).val()); + } + }); + if ($('#id_pending').is(':checked')) { + rows = rows.has('td.signoff-no'); + } + /* hide all rows, then show the set we care about */ + all_rows.hide(); + rows.show(); + /* make sure we update the odd/even styling from sorting */ + $('.results').trigger('applyWidgets'); +} +function filter_signoffs_reset() { + $('#signoffs_filter .arch_filter').attr('checked', 'checked'); + $('#id_pending').removeAttr('checked'); + filter_signoffs(); +} + /* visualizations */ function format_filesize(size, decimals) { /*var labels = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];*/ diff --git a/packages/migrations/0010_auto__add_signoffspecification.py b/packages/migrations/0010_auto__add_signoffspecification.py new file mode 100644 index 00000000..da24824e --- /dev/null +++ b/packages/migrations/0010_auto__add_signoffspecification.py @@ -0,0 +1,183 @@ +# 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_table('packages_signoffspecification', ( + ('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')(to=orm['auth.User'])), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('required', self.gf('django.db.models.fields.PositiveIntegerField')(default=2)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('known_bad', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('comments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + )) + db.send_create_signal('packages', ['SignoffSpecification']) + + + def backwards(self, orm): + db.delete_table('packages_signoffspecification') + + + 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'}), + 'pgp_signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + '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']"}) + }, + 'packages.signoffspecification': { + 'Meta': {'object_name': 'SignoffSpecification'}, + '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', [], {}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'known_bad': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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']"}), + 'required': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/models.py b/packages/models.py index 4cd3b1b5..ad082501 100644 --- a/packages/models.py +++ b/packages/models.py @@ -38,6 +38,49 @@ class PackageRelation(models.Model): class Meta: unique_together = (('pkgbase', 'user', 'type'),) +class SignoffSpecification(models.Model): + ''' + A specification for the signoff policy for this particular revision of a + pakcage. The default is requiring two signoffs for a given package. These + are created only if necessary; e.g., if one wanted to override the + required=2 attribute, otherwise a sane default object is used. + ''' + 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) + created = models.DateTimeField(editable=False) + required = models.PositiveIntegerField(default=2) + enabled = models.BooleanField(default=True) + known_bad = models.BooleanField(default=False) + comments = models.TextField(null=True, blank=True) + +class SignoffManager(models.Manager): + def get_from_package(self, pkg, user, revoked=False): + '''Utility method to pull all relevant name-version fields from a + package and create a matching signoff.''' + not_revoked = not revoked + return Signoff.objects.get( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, + revoked__isnull=not_revoked, user=user) + + def get_or_create_from_package(self, pkg, user): + '''Utility method to pull all relevant name-version fields from a + package and create a matching signoff.''' + return Signoff.objects.get_or_create( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, + revoked=None, user=user) + + def for_package(self, pkg): + return self.select_related('user').filter( + pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, + epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo) + class Signoff(models.Model): ''' A signoff for a package (by pkgbase) at a given point in time. These are @@ -55,7 +98,7 @@ class Signoff(models.Model): revoked = models.DateTimeField(null=True) comments = models.TextField(null=True, blank=True) - REQUIRED = 2 + objects = SignoffManager() @property def packages(self): diff --git a/packages/urls.py b/packages/urls.py index d7d01170..576e3279 100644 --- a/packages/urls.py +++ b/packages/urls.py @@ -10,6 +10,7 @@ package_patterns = patterns('packages.views', (r'^unflag/$', 'unflag'), (r'^unflag/all/$', 'unflag_all'), (r'^signoff/$', 'signoff_package'), + (r'^signoff/revoke/$', 'signoff_package', {'revoke': True}), (r'^download/$', 'download'), ) diff --git a/packages/views.py b/packages/views.py index 5114c87f..035d51cb 100644 --- a/packages/views.py +++ b/packages/views.py @@ -25,7 +25,7 @@ from urllib import urlencode 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, Signoff +from .models import PackageRelation, PackageGroup, SignoffSpecification, Signoff from .utils import (get_group_info, get_differences_info, get_wrong_permissions, get_current_signoffs) @@ -369,14 +369,24 @@ def unflag_all(request, name, repo, arch): pkgs.update(flag_date=None) return redirect(pkg) +DEFAULT_SIGNOFF_SPEC = SignoffSpecification(required=2) + +def approved_by_signoffs(signoffs, spec=DEFAULT_SIGNOFF_SPEC): + if signoffs: + good_signoffs = sum(1 for s in signoffs if not s.revoked) + return good_signoffs >= spec.required + return False + class PackageSignoffGroup(object): '''Encompasses all packages in testing with the same pkgbase.''' - def __init__(self, packages, target_repo=None, signoffs=None): + def __init__(self, packages, user=None): if len(packages) == 0: raise Exception self.packages = packages - self.target_repo = target_repo - self.signoffs = signoffs + self.user = user + self.target_repo = None + self.signoffs = set() + self.specification = DEFAULT_SIGNOFF_SPEC first = packages[0] self.pkgbase = first.pkgbase @@ -406,21 +416,24 @@ class PackageSignoffGroup(object): 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) + self.signoffs.add(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 + return approved_by_signoffs(self.signoffs, self.specification) + + def user_signed_off(self, user=None): + '''Did a given user signoff on this package? user can be passed as an + argument, or attached to the group object itself so this can be called + from a template.''' + if user is None: + user = self.user + return user in (s.user for s in self.signoffs if not s.revoked) @permission_required('main.change_package') @never_cache @@ -443,7 +456,7 @@ def signoffs(request): grouped = groupby_preserve_order(packages, same_pkgbase_key) signoff_groups = [] for group in grouped: - signoff_group = PackageSignoffGroup(group) + signoff_group = PackageSignoffGroup(group, user=request.user) signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase, "Unknown") signoff_group.find_signoffs(signoffs) @@ -451,27 +464,43 @@ def signoffs(request): signoff_groups.sort(key=attrgetter('pkgbase')) - return direct_to_template(request, 'packages/signoffs.html', - {'signoff_groups': signoff_groups}) + context = { + 'signoff_groups': signoff_groups, + 'arches': Arch.objects.all(), + } + return direct_to_template(request, 'packages/signoffs.html', context) @permission_required('main.change_package') @never_cache -def signoff_package(request, name, repo, arch): +def signoff_package(request, name, repo, arch, revoke=False): 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( - pkgbase=pkg.pkgbase, pkgver=pkg.pkgver, pkgrel=pkg.pkgrel, - epoch=pkg.epoch, arch=pkg.arch, repo=pkg.repo, user=request.user) + package = packages[0] + + if revoke: + try: + signoff = Signoff.objects.get_from_package( + package, request.user, False) + except Signoff.DoesNotExist: + raise Http404 + signoff.revoked = datetime.utcnow() + signoff.save() + created = False + else: + signoff, created = Signoff.objects.get_or_create_from_package( + package, request.user) + + all_signoffs = Signoff.objects.for_package(package) if request.is_ajax(): data = { 'created': created, - 'approved': pkg.approved_for_signoff(), + 'revoked': bool(signoff.revoked), + 'approved': approved_by_signoffs(all_signoffs), 'user': str(request.user), } - return HttpResponse(simplejson.dumps(data), + return HttpResponse(simplejson.dumps(data, ensure_ascii=False), mimetype='application/json') return redirect('package-signoffs') diff --git a/templates/packages/differences.html b/templates/packages/differences.html index dd1046bc..0400ea37 100644 --- a/templates/packages/differences.html +++ b/templates/packages/differences.html @@ -65,7 +65,7 @@ $(document).ready(function() { $('.results').tablesorter({widgets: ['zebra'], sortList: [[1,0], [0,0]]}); $('#diff_filter select').change(filter_packages); $('#diff_filter input').change(filter_packages); - $('#criteria_reset').click(filter_reset); + $('#criteria_reset').click(filter_differences_reset); // fire function on page load to ensure the current form selections take effect filter_packages(); }); diff --git a/templates/packages/signoff_cell.html b/templates/packages/signoff_cell.html new file mode 100644 index 00000000..fce5d551 --- /dev/null +++ b/templates/packages/signoff_cell.html @@ -0,0 +1,12 @@ +
      + {% for signoff in group.signoffs %} +
    • {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
    • + {% endfor %} +
    +{% if group.user_signed_off %} +
    Revoke Signoff
    +{% else %} +
    Signoff
    +{% endif %} diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index a8aa4de2..4a2f6c99 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -12,42 +12,46 @@

    {{ signoff_groups|length }} signoff group{{ signoff_groups|pluralize }} found. A "signoff group" consists of packages grouped by pkgbase, architecture, and repository.

    +
    +

    Filter Displayed Signoffs

    +
    +
    + Select filter criteria + {% for arch in arches %} +
    +
    + {% endfor %} +
    +
    +
    +
    +
    +
    + + - + - - - + - + {% for group in signoff_groups %} {% with group.package as pkg %} - + + - + - - - + {% endwith %} {% endfor %} @@ -60,8 +64,12 @@ {% endblock %} -- cgit v1.2.3-55-g3dc8
    Package Base/Version ArchPackage BaseTarget Repo # of PackagesVersion Last UpdatedTarget Repo ApprovedSignoffSignoffs
    {% pkg_details_link pkg %} {{ pkg.full_version }} {{ pkg.arch.name }}{% pkg_details_link pkg %}{{ group.target_repo }} {{ group.packages|length }}{{ pkg.full_version }} {{ pkg.last_update|date }}{{ group.target_repo }} {{ group.approved|yesno|capfirst }} -
      -
    • Signoff -
    • - {% for signoff in group.signoffs %} -
    • - {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
    • - {% endfor %} -
    -
    {% include "packages/signoff_cell.html" %}