From ac34e358103ee718369692b9ba5afe6830a1df92 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 12:25:54 -0600 Subject: reporead: fix filesonly needs update checks This was broken after the select for update changes. We really should split the whole filesonly update into another method instead of the current shotgun approach with conditionals everywhere. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index cf101d97..f8cc2034 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -382,11 +382,13 @@ def db_update(archname, reponame, pkgs, options): # files/depends/all related items to be double-imported. if filesonly: with transaction.commit_on_success(): + if not dbpkg.files_last_update or not dbpkg.last_update: + pass + elif dbpkg.files_last_update > dbpkg.last_update: + logger.debug("Files for %s are up to date", pkg.name) + continue # TODO Django 1.4 select_for_update() will work once released dbpkg = select_pkg_for_update(dbpkg) - if pkg_same_version(pkg, dbpkg): - logger.debug("Package %s was already updated", pkg.name) - continue logger.debug("Checking files for package %s", pkg.name) populate_files(dbpkg, pkg, force=force) else: -- cgit v1.2.3-54-g00ecf From 9279d9a04cc99691ab6c7fbafe7db03a757d23e8 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 12:27:57 -0600 Subject: Fix minor validation error Signed-off-by: Dan McGee --- templates/devel/profile.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/devel/profile.html b/templates/devel/profile.html index 2f16801b..b6580ab8 100644 --- a/templates/devel/profile.html +++ b/templates/devel/profile.html @@ -6,7 +6,7 @@

Developer Profile

-
{% csrf_token %} + {% csrf_token %}

Note: This is the public information shown on the developer and/or TU profiles page, so please be appropriate with the information you provide here.

-- cgit v1.2.3-54-g00ecf From b05eae96a4be8990a1de81795013587ded7ed3aa Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 12:28:11 -0600 Subject: Add more precise sort for last update signoff column This allows sorting by the exact time recorded in the database, even if we don't show it directly to the user, which makes finding the most recent updates a lot easier. Signed-off-by: Dan McGee --- media/archweb.js | 13 +++++++++++++ templates/packages/signoffs.html | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/media/archweb.js b/media/archweb.js index 4f098c7d..151d0f81 100644 --- a/media/archweb.js +++ b/media/archweb.js @@ -54,6 +54,19 @@ if (typeof $.tablesorter !== 'undefined') { }, type: 'numeric' }); + $.tablesorter.addParser({ + id: 'epochdate', + is: function(s) { return false; }, + format: function(s, t, c) { + /* TODO: this assumes our magic class is the only one */ + var epoch = $(c).attr('class'); + if (!epoch.indexOf('epoch-') == 0) { + return 0; + } + return epoch.slice(6); + }, + type: 'numeric' + }); $.tablesorter.addParser({ id: 'longDateTime', re: /^(\d{4})-(\d{2})-(\d{2}) ([012]\d):([0-5]\d)(:([0-5]\d))?( (\w+))?$/, diff --git a/templates/packages/signoffs.html b/templates/packages/signoffs.html index bd84289c..b032e656 100644 --- a/templates/packages/signoffs.html +++ b/templates/packages/signoffs.html @@ -56,7 +56,7 @@

Filter Displayed Signoffs

{{ group.target_repo }} {{ group.packager|default:"Unknown" }} {{ group.packages|length }} - {{ group.last_update|date }} + {{ group.last_update|date }} {% if group.specification.known_bad %} Bad {% else %} @@ -85,7 +85,7 @@

Filter Displayed Signoffs

$(document).ready(function() { $('a.signoff-link').click(signoff_package); $(".results").tablesorter({widgets: ['zebra'], sortList: [[0,0]], - headers: { 7: { sorter: false }, 8: {sorter: false } } }); + headers: { 5: { sorter: 'epochdate' }, 7: { sorter: false }, 8: {sorter: false } } }); $('#signoffs_filter input').change(filter_signoffs); $('#criteria_reset').click(filter_signoffs_reset); // fire function on page load to ensure the current form selections take effect -- cgit v1.2.3-54-g00ecf From dca9fd2ea687b8d20fd5c39c1449846ddef1e3c2 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 13:04:15 -0600 Subject: reporead: split out filesonly update method This removes a bunch of the conditional logic at a slight cost of some code duplication. However, the methods and madness is now much easier to follow. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 170 +++++++++++++++++++--------------- 1 file changed, 95 insertions(+), 75 deletions(-) diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index f8cc2034..e4ba8580 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -282,40 +282,20 @@ def select_pkg_for_update(dbpkg): return list(new_pkg)[0] -def db_update(archname, reponame, pkgs, options): - """ - Parses a list and updates the Arch dev database accordingly. - - Arguments: - pkgs -- A list of Pkg objects. - - """ - logger.info('Updating Arch: %s', archname) - force = options.get('force', False) - filesonly = options.get('filesonly', False) - +def update_common(archname, reponame, pkgs, sanity_check=True): with transaction.commit_manually(): repository = Repo.objects.get(name__iexact=reponame) architecture = Arch.objects.get(name__iexact=archname) # no-arg order_by() removes even the default ordering; we don't need it dbpkgs = Package.objects.filter( arch=architecture, repo=repository).order_by() - # This makes our inner loop where we find packages by name *way* more - # efficient by not having to go to the database for each package to - # SELECT them by name. - dbdict = dict((dbpkg.pkgname, dbpkg) for dbpkg in dbpkgs) - - logger.debug("Creating sets") - dbset = set(dbdict.keys()) - syncset = set([pkg.name for pkg in pkgs]) - logger.info("%d packages in current web DB", len(dbset)) - logger.info("%d packages in new updating db", len(syncset)) - in_sync_not_db = syncset - dbset - logger.info("%d packages in sync not db", len(in_sync_not_db)) + + logger.info("%d packages in current web DB", len(dbpkgs)) + logger.info("%d packages in new updating DB", len(pkgs)) # Try to catch those random package deletions that make Eric so unhappy. - if len(dbset): - dbpercent = 100.0 * len(syncset) / len(dbset) + if len(dbpkgs): + dbpercent = 100.0 * len(pkgs) / len(dbpkgs) else: dbpercent = 0.0 logger.info("DB package ratio: %.1f%%", dbpercent) @@ -324,11 +304,13 @@ def db_update(archname, reponame, pkgs, options): # means we expect the repo to fluctuate a lot. msg = "Package database has %.1f%% the number of packages in the " \ "web database" % dbpercent - if len(dbset) == 0 and len(syncset) == 0: + if not sanity_check: + pass + elif repository.testing or repository.staging: pass - elif not filesonly and \ - len(dbset) > 20 and dbpercent < 50.0 and \ - not repository.testing and not repository.staging: + elif len(dbpkgs) == 0 and len(pkgs) == 0: + pass + elif len(dbpkgs) > 20 and dbpercent < 50.0: logger.error(msg) raise Exception(msg) elif dbpercent < 75.0: @@ -339,27 +321,45 @@ def db_update(archname, reponame, pkgs, options): # to guard against simultaneous updates transaction.commit() - if not filesonly: - # packages in syncdb and not in database (add to database) - for pkg in (pkg for pkg in pkgs if pkg.name in in_sync_not_db): - logger.info("Adding package %s", pkg.name) - dbpkg = Package(pkgname=pkg.name, arch=architecture, repo=repository) - try: - with transaction.commit_on_success(): - populate_pkg(dbpkg, pkg, timestamp=datetime.utcnow()) - except IntegrityError: - logger.warning("Could not add package %s; " - "not fatal if another thread beat us to it.", - pkg.name, exc_info=True) - - # packages in database and not in syncdb (remove from database) - for pkgname in (dbset - syncset): - logger.info("Removing package %s", pkgname) - dbpkg = dbdict[pkgname] + return dbpkgs + +def db_update(archname, reponame, pkgs, force=False): + """ + Parses a list of packages and updates the packages database accordingly. + """ + logger.info('Updating %s (%s)', reponame, archname) + dbpkgs = update_common(archname, reponame, pkgs, True) + + # This makes our inner loop where we find packages by name *way* more + # efficient by not having to go to the database for each package to + # SELECT them by name. + dbdict = dict((dbpkg.pkgname, dbpkg) for dbpkg in dbpkgs) + + dbset = set(dbdict.keys()) + syncset = set([pkg.name for pkg in pkgs]) + + in_sync_not_db = syncset - dbset + logger.info("%d packages in sync not db", len(in_sync_not_db)) + # packages in syncdb and not in database (add to database) + for pkg in (pkg for pkg in pkgs if pkg.name in in_sync_not_db): + logger.info("Adding package %s", pkg.name) + dbpkg = Package(pkgname=pkg.name, arch=architecture, repo=repository) + try: with transaction.commit_on_success(): - # no race condition here as long as simultaneous threads both - # issue deletes; second delete will be a no-op - dbpkg.delete() + populate_pkg(dbpkg, pkg, timestamp=datetime.utcnow()) + except IntegrityError: + logger.warning("Could not add package %s; " + "not fatal if another thread beat us to it.", + pkg.name, exc_info=True) + + # packages in database and not in syncdb (remove from database) + for pkgname in (dbset - syncset): + logger.info("Removing package %s", pkgname) + dbpkg = dbdict[pkgname] + with transaction.commit_on_success(): + # no race condition here as long as simultaneous threads both + # issue deletes; second delete will be a no-op + dbpkg.delete() # packages in both database and in syncdb (update in database) pkg_in_both = syncset & dbset @@ -369,9 +369,7 @@ def db_update(archname, reponame, pkgs, options): timestamp = None # for a force, we don't want to update the timestamp. # for a non-force, we don't want to do anything at all. - if filesonly: - pass - elif pkg_same_version(pkg, dbpkg): + if pkg_same_version(pkg, dbpkg): if not force: continue else: @@ -380,28 +378,45 @@ def db_update(archname, reponame, pkgs, options): # The odd select_for_update song and dance here are to ensure # simultaneous updates don't happen on a package, causing # files/depends/all related items to be double-imported. - if filesonly: - with transaction.commit_on_success(): - if not dbpkg.files_last_update or not dbpkg.last_update: - pass - elif dbpkg.files_last_update > dbpkg.last_update: - logger.debug("Files for %s are up to date", pkg.name) - continue - # TODO Django 1.4 select_for_update() will work once released - dbpkg = select_pkg_for_update(dbpkg) - logger.debug("Checking files for package %s", pkg.name) - populate_files(dbpkg, pkg, force=force) - else: - with transaction.commit_on_success(): - # TODO Django 1.4 select_for_update() will work once released - dbpkg = select_pkg_for_update(dbpkg) - if pkg_same_version(pkg, dbpkg): - logger.debug("Package %s was already updated", pkg.name) - continue - logger.info("Updating package %s", pkg.name) - populate_pkg(dbpkg, pkg, force=force, timestamp=timestamp) + with transaction.commit_on_success(): + # TODO Django 1.4 select_for_update() will work once released + dbpkg = select_pkg_for_update(dbpkg) + if pkg_same_version(pkg, dbpkg): + logger.debug("Package %s was already updated", pkg.name) + continue + logger.info("Updating package %s", pkg.name) + populate_pkg(dbpkg, pkg, force=force, timestamp=timestamp) + + logger.info('Finished updating arch: %s', archname) + + +def filesonly_update(archname, reponame, pkgs, force=False): + """ + Parses a list of packages and updates the packages database accordingly. + """ + logger.info('Updating files for %s (%s)', reponame, archname) + dbpkgs = update_common(archname, reponame, pkgs, False) + dbdict = dict((dbpkg.pkgname, dbpkg) for dbpkg in dbpkgs) + dbset = set(dbdict.keys()) + + for pkg in (pkg for pkg in pkgs if pkg.name in dbset): + dbpkg = dbdict[pkg.name] + + # The odd select_for_update song and dance here are to ensure + # simultaneous updates don't happen on a package, causing + # files to be double-imported. + with transaction.commit_on_success(): + if not dbpkg.files_last_update or not dbpkg.last_update: + pass + elif dbpkg.files_last_update > dbpkg.last_update: + logger.debug("Files for %s are up to date", pkg.name) + continue + # TODO Django 1.4 select_for_update() will work once released + dbpkg = select_pkg_for_update(dbpkg) + logger.debug("Checking files for package %s", pkg.name) + populate_files(dbpkg, pkg, force=force) - logger.info('Finished updating Arch: %s', archname) + logger.info('Finished updating arch: %s', archname) def parse_info(iofile): @@ -490,6 +505,8 @@ def read_repo(primary_arch, repo_file, options): """ # always returns an Arch object, regardless of what is passed in primary_arch = locate_arch(primary_arch) + force = options.get('force', False) + filesonly = options.get('filesonly', False) repo, packages = parse_repo(repo_file) @@ -509,7 +526,10 @@ def read_repo(primary_arch, repo_file, options): logger.info('Starting database updates for %s.', repo_file) for arch in sorted(packages_arches.keys()): - db_update(arch, repo, packages_arches[arch], options) + if filesonly: + filesonly_update(arch, repo, packages_arches[arch], force) + else: + db_update(arch, repo, packages_arches[arch], force) logger.info('Finished database updates for %s.', repo_file) return 0 -- cgit v1.2.3-54-g00ecf From 04e830a1dad665da0b05c3c8349eff21667d805d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 13:11:14 -0600 Subject: Add initial migration for devel app Signed-off-by: Dan McGee --- devel/migrations/0001_initial.py | 18 ++++++++++++++++++ devel/migrations/__init__.py | 0 2 files changed, 18 insertions(+) create mode 100644 devel/migrations/0001_initial.py create mode 100644 devel/migrations/__init__.py diff --git a/devel/migrations/0001_initial.py b/devel/migrations/0001_initial.py new file mode 100644 index 00000000..c28fc20f --- /dev/null +++ b/devel/migrations/0001_initial.py @@ -0,0 +1,18 @@ +# encoding: utf-8 +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + pass + + + def backwards(self, orm): + pass + + + models = {} + + complete_apps = ['devel'] diff --git a/devel/migrations/__init__.py b/devel/migrations/__init__.py new file mode 100644 index 00000000..e69de29b -- cgit v1.2.3-54-g00ecf From 0afedf606f071d23df9ed613abc873b0b25d700d Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 13:24:39 -0600 Subject: Move main fields to separate module Signed-off-by: Dan McGee --- main/fields.py | 42 ++++++++++++++++++++++++++++++++++++++++++ main/models.py | 36 ++---------------------------------- 2 files changed, 44 insertions(+), 34 deletions(-) create mode 100644 main/fields.py diff --git a/main/fields.py b/main/fields.py new file mode 100644 index 00000000..948cb5d9 --- /dev/null +++ b/main/fields.py @@ -0,0 +1,42 @@ +from django.db import models +from django.core.validators import RegexValidator + + +class PositiveBigIntegerField(models.BigIntegerField): + _south_introspects = True + + def get_internal_type(self): + return "BigIntegerField" + + def formfield(self, **kwargs): + defaults = { 'min_value': 0 } + defaults.update(kwargs) + return super(PositiveBigIntegerField, self).formfield(**defaults) + +class PGPKeyField(models.CharField): + _south_introspects = True + + def __init__(self, *args, **kwargs): + super(PGPKeyField, self).__init__(*args, **kwargs) + self.validators.append(RegexValidator(r'^[0-9A-F]{40}$', + "Ensure this value consists of 40 hex characters.", 'hex_char')) + + def to_python(self, value): + if value == '' or value is None: + 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] + # make all (hex letters) uppercase + return value.upper() + + def formfield(self, **kwargs): + # override so we don't set max_length form field attribute + return models.Field.formfield(self, **kwargs) + + +# vim: set ts=4 sw=4 et: diff --git a/main/models.py b/main/models.py index d7780b91..990cc8ca 100644 --- a/main/models.py +++ b/main/models.py @@ -1,47 +1,17 @@ from django.db import models from django.db.models.signals import pre_save -from django.core.validators import RegexValidator from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.forms import ValidationError -from main.utils import cache_function, make_choice, set_created_field +from .fields import PositiveBigIntegerField, PGPKeyField +from .utils import cache_function, make_choice, set_created_field from packages.models import PackageRelation from datetime import datetime from itertools import groupby import pytz -class PositiveBigIntegerField(models.BigIntegerField): - _south_introspects = True - - def get_internal_type(self): - return "BigIntegerField" - - def formfield(self, **kwargs): - defaults = {'min_value': 0} - defaults.update(kwargs) - return super(PositiveBigIntegerField, self).formfield(**defaults) - -class PGPKeyField(models.CharField): - _south_introspects = True - - def to_python(self, value): - if value == '' or value is None: - 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] - # make all (hex letters) uppercase - return value.upper() - - def formfield(self, **kwargs): - # override so we don't set max_length form field attribute - return models.Field.formfield(self, **kwargs) class UserProfile(models.Model): notify = models.BooleanField( @@ -62,8 +32,6 @@ class UserProfile(models.Model): other_contact = models.CharField(max_length=100, null=True, blank=True) pgp_key = PGPKeyField(max_length=40, null=True, blank=True, verbose_name="PGP key fingerprint", - validators=[RegexValidator(r'^[0-9A-F]{40}$', - "Ensure this value consists of 40 hex characters.", 'hex_char')], help_text="consists of 40 hex digits; use `gpg --fingerprint`") website = models.CharField(max_length=200, null=True, blank=True) yob = models.IntegerField("Year of birth", null=True, blank=True) -- cgit v1.2.3-54-g00ecf From da61fed4be6a28c870580f448c7c486f46d7b088 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 13:31:51 -0600 Subject: Add master key model and admin integration --- devel/admin.py | 12 +++++ devel/migrations/0002_auto__add_masterkey.py | 76 ++++++++++++++++++++++++++++ devel/models.py | 17 +++++++ 3 files changed, 105 insertions(+) create mode 100644 devel/admin.py create mode 100644 devel/migrations/0002_auto__add_masterkey.py diff --git a/devel/admin.py b/devel/admin.py new file mode 100644 index 00000000..84082fb8 --- /dev/null +++ b/devel/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + +from .models import MasterKey + + +class MasterKeyAdmin(admin.ModelAdmin): + list_display = ('pgp_key', 'owner', 'created', 'revoker', 'revoked') + search_fields = ('pgp_key', 'owner', 'revoker') + +admin.site.register(MasterKey, MasterKeyAdmin) + +# vim: set ts=4 sw=4 et: diff --git a/devel/migrations/0002_auto__add_masterkey.py b/devel/migrations/0002_auto__add_masterkey.py new file mode 100644 index 00000000..ac1f745a --- /dev/null +++ b/devel/migrations/0002_auto__add_masterkey.py @@ -0,0 +1,76 @@ +# encoding: utf-8 +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + depends_on = ( + ("main", "0051_auto__chg_field_userprofile_pgp_key"), + ) + + def forwards(self, orm): + # Adding model 'MasterKey' + db.create_table('devel_masterkey', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('owner', self.gf('django.db.models.fields.related.ForeignKey')(related_name='masterkey_owner', to=orm['auth.User'])), + ('revoker', self.gf('django.db.models.fields.related.ForeignKey')(related_name='masterkey_revoker', to=orm['auth.User'])), + ('pgp_key', self.gf('main.fields.PGPKeyField')(max_length=40)), + ('created', self.gf('django.db.models.fields.DateTimeField')()), + ('revoked', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), + )) + db.send_create_signal('devel', ['MasterKey']) + + def backwards(self, orm): + db.delete_table('devel_masterkey') + + + 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'}) + }, + 'devel.masterkey': { + 'Meta': {'object_name': 'MasterKey'}, + 'created': ('django.db.models.fields.DateTimeField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'masterkey_owner'", 'to': "orm['auth.User']"}), + 'pgp_key': ('main.fields.PGPKeyField', [], {'max_length': '40'}), + 'revoked': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'revoker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'masterkey_revoker'", 'to': "orm['auth.User']"}) + } + } + + complete_apps = ['devel'] diff --git a/devel/models.py b/devel/models.py index e69de29b..ea0f4d55 100644 --- a/devel/models.py +++ b/devel/models.py @@ -0,0 +1,17 @@ +from django.db import models +from django.contrib.auth.models import User + +from main.fields import PGPKeyField + + +class MasterKey(models.Model): + owner = models.ForeignKey(User, related_name='masterkey_owner', + help_text="The developer holding this master key") + revoker = models.ForeignKey(User, related_name='masterkey_revoker', + help_text="The developer holding the revocation certificate") + pgp_key = PGPKeyField(max_length=40, verbose_name="PGP key fingerprint", + help_text="consists of 40 hex digits; use `gpg --fingerprint`") + created = models.DateTimeField() + revoked = models.DateTimeField(null=True, blank=True) + +# vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 6b8ef446bcd6a1cbc794d0846968e806034d3aad Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 13:55:36 -0600 Subject: Add master key overview page And a bunch of text that may suck, but is better than nothing. Signed-off-by: Dan McGee --- main/models.py | 12 ++++++++++ main/templatetags/pgp.py | 13 +++++++++++ public/views.py | 18 ++++++++++----- templates/public/keys.html | 57 ++++++++++++++++++++++++++++++++++++++++++++++ urls.py | 1 + 5 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 templates/public/keys.html diff --git a/main/models.py b/main/models.py index 990cc8ca..9156fb51 100644 --- a/main/models.py +++ b/main/models.py @@ -53,6 +53,18 @@ class Meta: verbose_name = 'Additional Profile Data' verbose_name_plural = 'Additional Profile Data' + def get_absolute_url(self): + # TODO: this is disgusting. find a way to consolidate this logic with + # public.views.userlist among other places, and make some constants or + # something so we aren't using copies of string names everywhere. + group_names = self.user.groups.values_list('name', flat=True) + if "Developers" in group_names: + prefix = "developers" + elif "Trusted Users" in group_names: + prefix = "trustedusers" + else: + prefix = "fellows" + return '/%s/#%s' % (prefix, self.user.username) class TodolistManager(models.Manager): def incomplete(self): diff --git a/main/templatetags/pgp.py b/main/templatetags/pgp.py index 67f5e08d..d69e2918 100644 --- a/main/templatetags/pgp.py +++ b/main/templatetags/pgp.py @@ -1,5 +1,7 @@ from django import template from django.conf import settings +from django.utils.html import conditional_escape +from django.utils.safestring import mark_safe register = template.Library() @@ -26,4 +28,15 @@ def pgp_key_link(key_id): values = (url, format_key(key_id), key_id[-8:]) return '0x%s' % values +@register.filter +def pgp_fingerprint(key_id, autoescape=True): + if not key_id: + return u'' + if autoescape: + esc = conditional_escape + else: + esc = lambda x: x + return mark_safe(format_key(esc(key_id))) +pgp_fingerprint.needs_autoescape = True + # vim: set ts=4 sw=4 et: diff --git a/public/views.py b/public/views.py index c28fd303..95b590fc 100644 --- a/public/views.py +++ b/public/views.py @@ -1,17 +1,17 @@ -from main.models import Arch, Repo, Donor -from mirrors.models import MirrorUrl -from news.models import News -from . import utils - from django.conf import settings from django.contrib.auth.models import User from django.http import Http404 from django.views.generic import list_detail from django.views.generic.simple import direct_to_template +from devel.models import MasterKey +from main.models import Arch, Repo, Donor +from mirrors.models import MirrorUrl +from news.models import News +from utils import get_recent_updates def index(request): - pkgs = utils.get_recent_updates() + pkgs = get_recent_updates() context = { 'news_updates': News.objects.order_by('-postdate', '-id')[:15], 'pkg_updates': pkgs, @@ -77,4 +77,10 @@ def feeds(request): } return direct_to_template(request, 'public/feeds.html', context) +def keys(request): + context = { + 'keys': MasterKey.objects.select_related('owner', 'revoker').all(), + } + return direct_to_template(request, 'public/keys.html', context) + # vim: set ts=4 sw=4 et: diff --git a/templates/public/keys.html b/templates/public/keys.html new file mode 100644 index 00000000..2e7fcebe --- /dev/null +++ b/templates/public/keys.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} +{% load pgp %} + +{% block title %}Arch Linux - Master Signing Keys{% endblock %} + +{% block content %} +
+

Master Signing Keys

+ +

This page lists the Arch Linux Master Keys. This is a distributed set of + keys that are seen as "official" signing keys of the distribution. Each key + is held by a different developer, and a revocation certificate for the key + is held by a different developer. Thus, no one developer has absolute hold + on any sort of absolute, root trust.

+

The {{ keys|length }} key{{ keys|pluralize }} listed below should be + regarded as the current set of master keys. They are available on public + keyservers and should be signed by the owner of the key.

+

All official Arch Linux developers and trusted users should have their + key signed by at least three of these master keys. This is in accordance + with the PGP web of trust concept. If a user is willing to + marginally trust all of the master keys, three signatures from different + master keys will consider a given developer's key as valid. For more + information on trust, please consult the + GNU Privacy Handbook + and Using trust to + validate keys.

+ + + + + + + + + + + + + + {% for key in keys %} + + + + {% with key.owner.userprofile as owner_profile %} + + + {% endwith %} + {% with key.revoker.userprofile as revoker_profile %} + + + {% endwith %} + + {% endfor %} + +
Master KeyFull FingerprintOwnerOwner's Signing KeyRevokerRevoker's Signing Key
{% pgp_key_link key.pgp_key %}{{ key.pgp_key|pgp_fingerprint }}{{ key.owner.get_full_name }}{% pgp_key_link owner_profile.pgp_key %}{{ key.revoker.get_full_name }}{% pgp_key_link revoker_profile.pgp_key %}
+
+{% endblock %} diff --git a/urls.py b/urls.py index 1d06f0f2..b01d2ee3 100644 --- a/urls.py +++ b/urls.py @@ -67,6 +67,7 @@ (r'^fellows/$', 'userlist', { 'user_type':'fellows' }, 'page-fellows'), (r'^donate/$', 'donate', {}, 'page-donate'), (r'^download/$', 'download', {}, 'page-download'), + (r'^master-keys/$', 'keys', {}, 'page-keys'), ) # Includes and other remaining stuff -- cgit v1.2.3-54-g00ecf From 4590196d79273c49172e2da74e7a7b31e59d7a27 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Wed, 30 Nov 2011 14:07:35 -0600 Subject: Integrate master key into rest of site Signed-off-by: Dan McGee --- devel/management/commands/generate_keyring.py | 4 ++++ sitemaps.py | 22 ++++++++++++++++++---- templates/public/index.html | 2 ++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/devel/management/commands/generate_keyring.py b/devel/management/commands/generate_keyring.py index 35ab8874..a3a764b4 100644 --- a/devel/management/commands/generate_keyring.py +++ b/devel/management/commands/generate_keyring.py @@ -13,6 +13,7 @@ import subprocess import sys +from devel.models import MasterKey from main.models import UserProfile logging.basicConfig( @@ -48,11 +49,14 @@ def generate_keyring(keyserver, keyring): pgp_key__isnull=False).extra(where=["pgp_key != ''"]).values_list( "pgp_key", flat=True) logger.info("%d keys fetched from user profiles", len(key_ids)) + master_key_ids = MasterKey.objects.values_list("pgp_key", flat=True) + logger.info("%d keys fetched from master keys", len(master_key_ids)) gpg_cmd = ["gpg", "--no-default-keyring", "--keyring", keyring, "--keyserver", keyserver, "--recv-keys"] logger.info("running command: %r", gpg_cmd) gpg_cmd.extend(key_ids) + gpg_cmd.extend(master_key_ids) subprocess.check_call(gpg_cmd) logger.info("keyring at %s successfully updated", keyring) diff --git a/sitemaps.py b/sitemaps.py index 7718002d..958d1f44 100644 --- a/sitemaps.py +++ b/sitemaps.py @@ -71,10 +71,24 @@ 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', + ('page-keys', 0.8, 'weekly'), + ('news-list', 0.7, 'weekly'), + ('groups-list', 0.5, 'weekly'), + ('mirror-status', 0.4, 'hourly'), + 'page-about', + 'page-art', + 'page-svn', + 'page-devs', + 'page-tus', + 'page-fellows', + 'page-donate', + 'page-download', + 'feeds-list', + 'mirror-list', + 'mirrorlist', + 'packages-differences', + 'releng-test-overview', + 'visualize-index', ) def items(self): diff --git a/templates/public/index.html b/templates/public/index.html index 854bd447..4bd26f6b 100644 --- a/templates/public/index.html +++ b/templates/public/index.html @@ -170,6 +170,8 @@

Tools

Development