From fe832ea845f07a79b4580f7bca1dcf44b2f215ee Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Sat, 27 Mar 2010 16:15:20 -0500 Subject: Move package maintainer off of package model This is an attempt to fix our long-standing problems dealing with maintainer information. Move the actual maintainer information off of the package model into a PackageRelation object, which has some flexibility to later represent more than just maintainership. This solves multiple problems: * If a package gets accidentally deleted, so did the maintainer info * Testing packages have always shown up as orphans * With split packages, it was easy to miss some of the sub-packages This commit does not include the deletion of the original maintainer column; that will come at a later time when I feel more confident that the data was migrated correctly. Signed-off-by: Dan McGee --- devel/views.py | 13 +- main/admin.py | 4 +- main/models.py | 21 +- packages/migrations/0001_initial.py | 72 +++++++ .../migrations/0002_populate_package_relation.py | 233 +++++++++++++++++++++ packages/migrations/__init__.py | 0 packages/models.py | 23 ++ packages/views.py | 65 +++--- templates/devel/index.html | 7 +- templates/packages/details.html | 9 +- templates/packages/flagged.html | 2 +- templates/packages/search.html | 6 +- 12 files changed, 410 insertions(+), 45 deletions(-) create mode 100644 packages/migrations/0001_initial.py create mode 100644 packages/migrations/0002_populate_package_relation.py create mode 100644 packages/migrations/__init__.py create mode 100644 packages/models.py diff --git a/devel/views.py b/devel/views.py index 4b278d1..045e60f 100644 --- a/devel/views.py +++ b/devel/views.py @@ -5,10 +5,14 @@ from django.contrib.auth.models import User from django.shortcuts import render_to_response from django.template import RequestContext from django.core.mail import send_mail +from django.db.models import Q + from main.models import Package, Todolist from main.models import Arch, Repo from main.models import UserProfile, News from main.models import Mirror +from packages.models import PackageRelation + import random from string import ascii_letters, digits pwletters = ascii_letters + digits @@ -17,12 +21,15 @@ pwletters = ascii_letters + digits @login_required def index(request): '''the Developer dashboard''' + inner_q = PackageRelation.objects.filter(user=request.user).values('pkgbase') + packages = Package.objects.select_related('arch', 'repo').filter(needupdate=True) + packages = packages.filter(Q(pkgname__in=inner_q) | Q(pkgbase__in=inner_q)) + page_dict = { 'todos': Todolist.objects.incomplete(), 'repos': Repo.objects.all(), 'arches': Arch.objects.all(), - 'maintainers': [ - User(id=None, username="orphan", first_name="Orphans") - ] + list(User.objects.filter(is_active=True).order_by('last_name')) + 'maintainers': User.objects.filter(is_active=True).order_by('last_name'), + 'flagged' : packages, } return render_to_response('devel/index.html', diff --git a/main/admin.py b/main/admin.py index 3ab6d5d..d4a7806 100644 --- a/main/admin.py +++ b/main/admin.py @@ -74,8 +74,8 @@ class RepoAdmin(admin.ModelAdmin): search_fields = ('name',) class PackageAdmin(admin.ModelAdmin): - list_display = ('pkgname', 'repo', 'arch', 'maintainer') - list_filter = ('repo', 'arch', 'maintainer') + list_display = ('pkgname', 'repo', 'arch', 'last_update') + list_filter = ('repo', 'arch') ordering = ['pkgname'] search_fields = ('pkgname',) diff --git a/main/models.py b/main/models.py index 0954f79..b49acd2 100644 --- a/main/models.py +++ b/main/models.py @@ -1,7 +1,9 @@ from django.db import models from django.db.models import Q from django.contrib.auth.models import User + from main.middleware import get_user +from packages.models import PackageRelation ########################### ### User Profile Class #### @@ -202,6 +204,18 @@ class Package(models.Model): return '/packages/%s/%s/%s/' % (self.repo.name.lower(), self.arch.name, self.pkgname) + @property + def pkgbase_safe(self): + if self.pkgbase: + return self.pkgbase + return self.pkgname + + @property + def maintainers(self): + return User.objects.filter( + package_relations__pkgbase=self.pkgbase_safe, + package_relations__type=PackageRelation.MAINTAINER) + @property def signoffs(self): if 'signoffs_cache' in dir(self): @@ -265,16 +279,12 @@ class Package(models.Model): def get_svn_link(self, svnpath): linkbase = "http://repos.archlinux.org/wsvn/%s/%s/%s/" - if self.pkgbase: - dirname = self.pkgbase - else: - dirname = self.pkgname repo = self.repo.name.lower() if repo.startswith('community'): root = 'community' else: root = 'packages' - return linkbase % (root, dirname, svnpath) + return linkbase % (root, self.pkgbase_safe, svnpath) def get_arch_svn_link(self): repo = self.repo.name.lower() @@ -362,4 +372,3 @@ class ExternalProject(models.Model): return self.name # vim: set ts=4 sw=4 et: - diff --git a/packages/migrations/0001_initial.py b/packages/migrations/0001_initial.py new file mode 100644 index 0000000..76e9734 --- /dev/null +++ b/packages/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# 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 'PackageRelation' + db.create_table('packages_packagerelation', ( + ('pkgbase', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('type', self.gf('django.db.models.fields.PositiveIntegerField')(default=1)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='package_relations', to=orm['auth.User'])), + )) + db.send_create_signal('packages', ['PackageRelation']) + # Adding unique constraint on 'PackageRelation', fields ['pkgbase', 'user', 'type'] + db.create_unique('packages_packagerelation', ['pkgbase', 'user_id', 'type']) + + def backwards(self, orm): + # Deleting model 'PackageRelation' + db.delete_table('packages_packagerelation') + # Removing unique constraint on 'PackageRelation', fields ['pkgbase', 'user', 'type'] + db.delete_unique('packages_packagerelation', ['pkgbase', 'user_id', 'type']) + + 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']", 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'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']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + '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']", 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'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'}) + }, + 'packages.packagerelation': { + 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'}, + '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']"}) + } + } + + complete_apps = ['packages'] diff --git a/packages/migrations/0002_populate_package_relation.py b/packages/migrations/0002_populate_package_relation.py new file mode 100644 index 0000000..7f90350 --- /dev/null +++ b/packages/migrations/0002_populate_package_relation.py @@ -0,0 +1,233 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + no_dry_run = True + + def forwards(self, orm): + "Write your forwards methods here." + # search by pkgbase first and insert those records + qs = orm['main.Package'].objects.exclude(maintainer=None).exclude( + pkgbase=None).distinct().values('pkgbase', 'maintainer_id') + for row in qs: + pr, created = orm.PackageRelation.objects.get_or_create( + pkgbase=row['pkgbase'], user__id=row['maintainer_id'], + defaults={'user_id': row['maintainer_id']}) + + # next search by pkgname first and insert those records + qs = orm['main.Package'].objects.exclude(maintainer=None).filter( + pkgbase=None).distinct().values('pkgname', 'maintainer_id') + for row in qs: + pr, created = orm.PackageRelation.objects.get_or_create( + pkgbase=row['pkgname'], user__id=row['maintainer_id'], + defaults={'user_id': row['maintainer_id']}) + + def backwards(self, orm): + "Write your backwards methods here." + if not db.dry_run: + orm.PackageRelation.objects.all().delete() + pass + + 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']", 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'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']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + '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']", 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'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.altforum': { + 'Meta': {'object_name': 'AltForum', 'db_table': "'alt_forums'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.arch': { + 'Meta': {'object_name': 'Arch', 'db_table': "'arches'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.donor': { + 'Meta': {'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'}) + }, + 'main.externalproject': { + 'Meta': {'object_name': 'ExternalProject'}, + 'description': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'main.mirror': { + 'Meta': {'object_name': 'Mirror'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'admin_email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'isos': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'rsync_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}), + 'rsync_user': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}), + 'tier': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}), + 'upstream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Mirror']", 'null': 'True'}) + }, + 'main.mirrorprotocol': { + 'Meta': {'object_name': 'MirrorProtocol'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'protocol': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '10'}) + }, + 'main.mirrorrsync': { + 'Meta': {'object_name': 'MirrorRsync'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.CharField', [], {'max_length': '24'}), + 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'rsync_ips'", 'to': "orm['main.Mirror']"}) + }, + 'main.mirrorurl': { + 'Meta': {'object_name': 'MirrorUrl'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mirror': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['main.Mirror']"}), + 'protocol': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'urls'", 'to': "orm['main.MirrorProtocol']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.news': { + 'Meta': {'object_name': 'News', 'db_table': "'news'"}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'news_author'", 'to': "orm['auth.User']"}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'postdate': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.package': { + 'Meta': {'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': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'maintainer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'maintained_packages'", 'null': 'True', 'to': "orm['auth.User']"}), + 'needupdate': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + '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'}) + }, + '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', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.packagefile': { + 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.press': { + 'Meta': {'object_name': 'Press', 'db_table': "'press'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.repo': { + 'Meta': {'object_name': 'Repo', 'db_table': "'repos'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}) + }, + '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.DateField', [], {'auto_now_add': 'True', 'blank': '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', 'blank': 'True'}), + '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']", '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', 'blank': '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'}), + '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'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'userprofile_user'", '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'}) + }, + 'packages.packagerelation': { + 'Meta': {'unique_together': "(('pkgbase', 'user', 'type'),)", 'object_name': 'PackageRelation'}, + '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']"}) + } + } + + complete_apps = ['main', 'packages'] diff --git a/packages/migrations/__init__.py b/packages/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/models.py b/packages/models.py new file mode 100644 index 0000000..9eff517 --- /dev/null +++ b/packages/models.py @@ -0,0 +1,23 @@ +from django.db import models +from django.contrib.auth.models import User + +class PackageRelation(models.Model): + ''' + Represents maintainership (or interest) in a package by a given developer. + It is not a true foreign key to packages as we want to key off + pkgbase/pkgname instead, as well as preserve this information across + package deletes, adds, and in all repositories. + ''' + MAINTAINER = 1 + WATCHER = 2 + TYPE_CHOICES = ( + (MAINTAINER, 'Maintainer'), + (WATCHER, 'Watcher'), + ) + pkgbase = models.CharField(max_length=255) + user = models.ForeignKey(User, related_name="package_relations") + type = models.PositiveIntegerField(choices=TYPE_CHOICES, default=MAINTAINER) + class Meta: + unique_together = (('pkgbase', 'user', 'type'),) + +# vim: set ts=4 sw=4 et: diff --git a/packages/views.py b/packages/views.py index 6bd54a6..47ad1d6 100644 --- a/packages/views.py +++ b/packages/views.py @@ -16,6 +16,7 @@ import datetime from main.models import Package, PackageFile from main.models import Arch, Repo, Signoff from main.utils import make_choice +from packages.models import PackageRelation def opensearch(request): if request.is_secure(): @@ -36,23 +37,31 @@ def update(request): mode = None if request.POST.has_key('adopt'): mode = 'adopt' - maint = request.user if request.POST.has_key('disown'): mode = 'disown' - maint = None if mode: repos = request.user.userprofile_user.all()[0].allowed_repos.all() pkgs = Package.objects.filter(id__in=ids, repo__in=repos) disallowed_pkgs = Package.objects.filter(id__in=ids).exclude( repo__in=repos) - pkgs.update(maintainer=maint) + for pkg in pkgs: + maints = pkg.maintainers + if mode == 'adopt' and request.user not in maints: + pr = PackageRelation(pkgbase=pkg.pkgbase_safe, + user=request.user, + type=PackageRelation.MAINTAINER) + pr.save() + elif mode == 'disown' and request.user in maints: + rels = PackageRelation.objects.filter(pkgbase=pkg.pkgbase_safe, + user=request.user) + rels.delete() request.user.message_set.create(message="%d packages %sed" % ( len(pkgs), mode)) if disallowed_pkgs: request.user.message_set.create( - message="You do not have permmission to adopt: %s" % ( + message="You do not have permission to adopt: %s" % ( ' '.join([p.pkgname for p in disallowed_pkgs]) )) else: @@ -70,12 +79,13 @@ def details(request, name='', repo='', arch=''): arch.lower(), repo.title(), name)) def getmaintainer(request, name, repo, arch): - "Returns the maintainer as a plaintext." + "Returns the maintainers as plaintext." - pkg= get_object_or_404(Package, + pkg = get_object_or_404(Package, pkgname=name, repo__name__iexact=repo, arch__name=arch) + names = [m.username for m in pkg.maintainers] - return HttpResponse(str(pkg.maintainer)) + return HttpResponse(str('\n'.join(names)), mimetype='text/plain') class PackageSearchForm(forms.Form): repo = forms.ChoiceField(required=False) @@ -122,7 +132,7 @@ class PackageSearchForm(forms.Form): def search(request, page=None): current_query = '?' limit=50 - packages = Package.objects.select_related('arch', 'repo', 'maintainer') + packages = Package.objects.select_related('arch', 'repo') if request.GET: current_query += request.GET.urlencode() @@ -131,18 +141,23 @@ def search(request, page=None): if form.cleaned_data['repo']: packages = packages.filter( repo__name=form.cleaned_data['repo']) + if form.cleaned_data['arch']: packages = packages.filter( arch__name=form.cleaned_data['arch']) + if form.cleaned_data['maintainer'] == 'orphan': - packages=packages.filter(maintainer=None) + inner_q = PackageRelation.objects.all().values('pkgbase') + packages = packages.exclude(Q(pkgname__in=inner_q) | Q(pkgbase__in=inner_q)) elif form.cleaned_data['maintainer']: - packages = packages.filter( - maintainer__username=form.cleaned_data['maintainer']) + inner_q = PackageRelation.objects.filter(user__username=form.cleaned_data['maintainer']).values('pkgbase') + packages = packages.filter(Q(pkgname__in=inner_q) | Q(pkgbase__in=inner_q)) + if form.cleaned_data['flagged'] == 'Flagged': packages=packages.filter(needupdate=True) elif form.cleaned_data['flagged'] == 'Not Flagged': packages = packages.filter(needupdate=False) + if form.cleaned_data['q']: query = form.cleaned_data['q'] q = Q(pkgname__icontains=query) | Q(pkgdesc__icontains=query) @@ -161,7 +176,7 @@ def search(request, page=None): if packages.count() == 1: return HttpResponseRedirect(packages[0].get_absolute_url()) - allowed_sort = ["arch", "repo", "pkgname", "maintainer", "last_update"] + allowed_sort = ["arch", "repo", "pkgname", "last_update"] 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 @@ -258,23 +273,25 @@ def flag(request, pkgid): if request.POST: form = FlagForm(request.POST) if form.is_valid() and form.cleaned_data['website'] == '': - send_email = True # flag all architectures pkgs = Package.objects.filter( pkgname=pkg.pkgname, repo=pkg.repo) pkgs.update(needupdate=True) - if not pkg.maintainer: - toemail = 'arch-notifications@archlinux.org' - subject = 'Orphan %s package [%s] marked out-of-date' % (pkg.repo.name, pkg.pkgname) - elif pkg.maintainer.get_profile().notify == True: - toemail = pkg.maintainer.email - subject = '%s package [%s] marked out-of-date' % (pkg.repo.name, pkg.pkgname) + maints = pkg.maintainers + if not maints: + toemail = ['arch-notifications@archlinux.org'] + subject = 'Orphan %s package [%s] marked out-of-date' % \ + (pkg.repo.name, pkg.pkgname) else: - # no need to send any email, packager didn't want notification - send_email = False - - if send_email: + toemail = [] + subject = '%s package [%s] marked out-of-date' % \ + (pkg.repo.name, pkg.pkgname) + for maint in maints: + if maint.get_profile().notify == True: + toemail.append(maint.email) + + if toemail: # send notification email to the maintainer t = loader.get_template('packages/outofdate.txt') c = Context({ @@ -286,7 +303,7 @@ def flag(request, pkgid): send_mail(subject, t.render(c), 'Arch Website Notification ', - [toemail], + toemail, fail_silently=True) context['confirmed'] = True diff --git a/templates/devel/index.html b/templates/devel/index.html index 12c0791..acbe90b 100644 --- a/templates/devel/index.html +++ b/templates/devel/index.html @@ -65,18 +65,17 @@

+
Counts are by 'pkgbase' and not raw number of packages.

Stats by Maintainer

- {% for maint in maintainers %} - - + {% endfor %} @@ -99,7 +98,7 @@ Version Arch - {% for pkg in user.maintained_packages.flagged %} + {% for pkg in flagged %} {{ pkg.pkgname }} diff --git a/templates/packages/details.html b/templates/packages/details.html index def0750..648b648 100644 --- a/templates/packages/details.html +++ b/templates/packages/details.html @@ -49,7 +49,14 @@ {{ pkg.license }} Maintainer: - {% if pkg.maintainer %}{{ pkg.maintainer.get_full_name }}{% else %}None{% endif %} + {% with pkg.maintainers as maints %} + {% if maints %} + {% for m in maints %} + {{ m.get_full_name }}
+ {% endfor %} + {% else %}Orphan{% endif %} + + {% endwith %} Package Size: {{ pkg.compressed_size|filesizeformat }} diff --git a/templates/packages/flagged.html b/templates/packages/flagged.html index 64cb245..3461bbd 100644 --- a/templates/packages/flagged.html +++ b/templates/packages/flagged.html @@ -3,6 +3,6 @@ {% block content %}

- {{pkg.pkgname}} on {{pkg.arch}} has already been flagged out of date. + {{pkg.pkgname}} has already been flagged out of date.

{% endblock %} diff --git a/templates/packages/search.html b/templates/packages/search.html index e760788..4f7bc77 100644 --- a/templates/packages/search.html +++ b/templates/packages/search.html @@ -44,7 +44,7 @@ {% if paginator %} - @@ -77,7 +77,6 @@ - @@ -95,13 +94,12 @@ {% endif %} - {% endfor %} {% if paginator %} - -- cgit v1.2.3-24-g4f1b
+ {{paginator.count}} packages found. Page {{page_obj.number}} of {{paginator.num_pages}}. Name Version DescriptionMaintainer Last Updated
{{ pkg.pkgver }}-{{ pkg.pkgrel }}{{ pkg.pkgdesc }}{{ pkg.maintainer|default:"Orphan" }} {{ pkg.last_update|date:"Y-m-d" }}
+ {{paginator.count}} packages found. Page {{page_obj.number}} of {{paginator.num_pages}}.