diff options
Diffstat (limited to 'main/models.py')
-rw-r--r-- | main/models.py | 192 |
1 files changed, 133 insertions, 59 deletions
diff --git a/main/models.py b/main/models.py index 70372823..d7780b91 100644 --- a/main/models.py +++ b/main/models.py @@ -1,12 +1,12 @@ 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 +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 @@ -27,7 +27,7 @@ class PGPKeyField(models.CharField): _south_introspects = True def to_python(self, value): - if value == '': + if value == '' or value is None: return None value = super(PGPKeyField, self).to_python(value) # remove all spaces @@ -36,18 +36,13 @@ class PGPKeyField(models.CharField): if value.startswith('0x'): value = value[2:] value = value.split('/')[-1] - return value + # 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) -def validate_pgp_key_length(value): - if len(value) not in (8, 16, 40): - raise ValidationError( - u'Ensure this value has 8, 16, or 40 characters (it has %d).' % len(value), - 'pgp_key_value') - class UserProfile(models.Model): notify = models.BooleanField( "Send notifications", @@ -66,10 +61,10 @@ class UserProfile(models.Model): help_text="Required field") 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", validators=[RegexValidator(r'^[0-9A-F]+$', - "Ensure this value consists of only hex characters.", 'hex_char'), - validate_pgp_key_length], - help_text="PGP Key ID or fingerprint (8, 16, or 40 hex digits)") + 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) location = models.CharField(max_length=50, null=True, blank=True) @@ -82,6 +77,9 @@ class UserProfile(models.Model): help_text="Ideally 125px by 125px") user = models.OneToOneField(User, related_name='userprofile') allowed_repos = models.ManyToManyField('Repo', blank=True) + latin_name = models.CharField(max_length=255, null=True, blank=True, + help_text="Latin-form name; used only for non-Latin full names") + class Meta: db_table = 'user_profiles' verbose_name = 'Additional Profile Data' @@ -97,6 +95,10 @@ class PackageManager(models.Manager): """Used by dev dashboard.""" return self.filter(flag_date__isnull=False) + def signed(self): + """Used by dev dashboard.""" + return self.filter(pgp_signature__isnull=False) + def normal(self): return self.select_related('arch', 'repo') @@ -104,13 +106,15 @@ class Donor(models.Model): name = models.CharField(max_length=255, unique=True) visible = models.BooleanField(default=True, help_text="Should we show this donor on the public page?") + created = models.DateTimeField() def __unicode__(self): return self.name class Meta: db_table = 'donors' - ordering = ['name'] + ordering = ('name',) + get_latest_by = 'when' class Arch(models.Model): name = models.CharField(max_length=255, unique=True) @@ -157,12 +161,12 @@ class Package(models.Model): on_delete=models.PROTECT) arch = models.ForeignKey(Arch, related_name="packages", on_delete=models.PROTECT) - pkgname = models.CharField(max_length=255, db_index=True) + pkgname = models.CharField(max_length=255) 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) - pkgdesc = models.CharField(max_length=255, null=True) + pkgdesc = models.TextField(null=True) url = models.CharField(max_length=255, null=True) filename = models.CharField(max_length=255) compressed_size = PositiveBigIntegerField() @@ -173,13 +177,16 @@ class Package(models.Model): packager_str = models.CharField(max_length=255) packager = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + pgp_signature = models.TextField(null=True, blank=True) flag_date = models.DateTimeField(null=True) objects = PackageManager() + class Meta: db_table = 'packages' ordering = ('pkgname',) get_latest_by = 'last_update' + unique_together = (('pkgname', 'repo', 'arch'),) def __unicode__(self): return self.pkgname @@ -194,26 +201,27 @@ class Package(models.Model): return '/packages/%s/%s/%s/' % (self.repo.name.lower(), self.arch.name, self.pkgname) - def get_full_url(self, proto='http'): + def get_full_url(self, proto='https'): '''get a URL suitable for things like email including the domain''' domain = Site.objects.get_current().domain return '%s://%s%s' % (proto, domain, self.get_absolute_url()) - @property - def maintainers(self): - return User.objects.filter( - package_relations__pkgbase=self.pkgbase, - package_relations__type=PackageRelation.MAINTAINER) + def is_signed(self): + return bool(self.pgp_signature) + + _maintainers = None @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 maintainers(self): + if self._maintainers is None: + self._maintainers = User.objects.filter( + package_relations__pkgbase=self.pkgbase, + package_relations__type=PackageRelation.MAINTAINER) + return self._maintainers - def approved_for_signoff(self): - count = self.signoffs.filter(revoked__isnull=True).count() - return count >= PackageSignoff.REQUIRED + @maintainers.setter + def maintainers(self, maintainers): + self._maintainers = maintainers @cache_function(300) def applicable_arches(self): @@ -229,9 +237,11 @@ class Package(models.Model): list slim by including the corresponding package in the same testing category as this package if that check makes sense. """ + provides = set(self.provides.values_list('name', flat=True)) + provides.add(self.pkgname) requiredby = PackageDepend.objects.select_related('pkg', 'pkg__arch', 'pkg__repo').filter( - depname=self.pkgname).order_by( + depname__in=provides).order_by( 'pkg__pkgname', 'pkg__arch__name', 'pkg__repo__name') if not self.arch.agnostic: # make sure we match architectures if possible @@ -269,36 +279,29 @@ class Package(models.Model): @cache_function(300) def get_depends(self): """ - Returns a list of dicts. Each dict contains ('pkg' and 'dep'). If it - represents a found package both vars will be available; else pkg will - be None if it is a 'virtual' dependency. Packages will match the - testing status of this package if possible. + Returns a list of dicts. Each dict contains ('dep', 'pkg', and + 'providers'). If it represents a found package both vars will be + available; else pkg will be None if it is a 'virtual' dependency. + If pkg is None and providers are known, they will be available in + providers. + Packages will match the testing status of this package if possible. """ deps = [] + arches = None + if not self.arch.agnostic: + arches = self.applicable_arches() # TODO: we can use list comprehension and an 'in' query to make this more effective for dep in self.packagedepend_set.order_by('optional', 'depname'): - pkgs = Package.objects.normal().filter(pkgname=dep.depname) - if not self.arch.agnostic: - # make sure we match architectures if possible - pkgs = pkgs.filter(arch__in=self.applicable_arches()) - if len(pkgs) == 0: - # couldn't find a package in the DB - # it should be a virtual depend (or a removed package) - pkg = None - elif len(pkgs) == 1: - pkg = pkgs[0] - else: - # more than one package, see if we can't shrink it down - # grab the first though in case we fail - pkg = pkgs[0] - # prevents yet more DB queries, these lists should be short - pkgs = [p for p in pkgs if p.repo.testing == self.repo.testing - and p.repo.staging == self.repo.staging] - if len(pkgs) > 0: - pkg = pkgs[0] - deps.append({'dep': dep, 'pkg': pkg}) + pkg = dep.get_best_satisfier(arches, testing=self.repo.testing, + staging=self.repo.staging) + providers = None + if not pkg: + providers = dep.get_providers(arches, + testing=self.repo.testing, staging=self.repo.staging) + deps.append({'dep': dep, 'pkg': pkg, 'providers': providers}) return deps + @cache_function(300) def base_package(self): """ Locate the base package for this package. It may be this very package, @@ -386,6 +389,64 @@ class PackageDepend(models.Model): optional = models.BooleanField(default=False) description = models.TextField(null=True, blank=True) + def get_best_satisfier(self, arches=None, testing=None, staging=None): + '''Find a satisfier for this dependency that best matches the given + criteria. It will not search provisions, but will find packages named + and matching repo characteristics if possible.''' + pkgs = Package.objects.normal().filter(pkgname=self.depname) + if arches is not None: + # make sure we match architectures if possible + pkgs = pkgs.filter(arch__in=arches) + if len(pkgs) == 0: + # couldn't find a package in the DB + # it should be a virtual depend (or a removed package) + return None + if len(pkgs) == 1: + return pkgs[0] + # more than one package, see if we can't shrink it down + # grab the first though in case we fail + pkg = pkgs[0] + # prevents yet more DB queries, these lists should be short; + # after each grab the best available in case we remove all entries + if staging is not None: + pkgs = [p for p in pkgs if p.repo.staging == staging] + if len(pkgs) > 0: + pkg = pkgs[0] + + if testing is not None: + pkgs = [p for p in pkgs if p.repo.testing == testing] + if len(pkgs) > 0: + pkg = pkgs[0] + + return pkg + + def get_providers(self, arches=None, testing=None, staging=None): + '''Return providers of this dep. Does *not* include exact matches as it + checks the Provision names only, use get_best_satisfier() instead.''' + pkgs = Package.objects.normal().filter( + provides__name=self.depname).distinct() + if arches is not None: + pkgs = pkgs.filter(arch__in=arches) + + # Logic here is to filter out packages that are in multiple repos if + # they are not requested. For example, if testing is False, only show a + # testing package if it doesn't exist in a non-testing repo. + if staging is not None: + filtered = {} + for p in pkgs: + if p.pkgname not in filtered or p.repo.staging == staging: + filtered[p.pkgname] = p + pkgs = filtered.values() + + if testing is not None: + filtered = {} + for p in pkgs: + if p.pkgname not in filtered or p.repo.testing == testing: + filtered[p.pkgname] = p + pkgs = filtered.values() + + return pkgs + def __unicode__(self): return "%s%s" % (self.depname, self.depvcmp) @@ -402,12 +463,17 @@ class Todolist(models.Model): def __unicode__(self): return self.name + _packages = None + @property def packages(self): - # select_related() does not use LEFT OUTER JOIN for nullable ForeignKey - # fields. That is why we need to explicitly list the ones we want. - return TodolistPkg.objects.select_related( - 'pkg__repo', 'pkg__arch').filter(list=self).order_by('pkg') + if not self._packages: + # select_related() does not use LEFT OUTER JOIN for nullable + # ForeignKey fields. That is why we need to explicitly list the + # ones we want. + self._packages = TodolistPkg.objects.select_related( + 'pkg__repo', 'pkg__arch').filter(list=self).order_by('pkg') + return self._packages @property def package_names(self): @@ -420,10 +486,16 @@ class Todolist(models.Model): def get_absolute_url(self): return '/todo/%i/' % self.id + def get_full_url(self, proto='https'): + '''get a URL suitable for things like email including the domain''' + domain = Site.objects.get_current().domain + return '%s://%s%s' % (proto, domain, self.get_absolute_url()) + class TodolistPkg(models.Model): list = models.ForeignKey(Todolist) pkg = models.ForeignKey(Package) complete = models.BooleanField(default=False) + class Meta: db_table = 'todolist_pkgs' unique_together = (('list','pkg'),) @@ -441,5 +513,7 @@ post_save.connect(refresh_latest, sender=Package, dispatch_uid="main.models") pre_save.connect(set_todolist_fields, sender=Todolist, dispatch_uid="main.models") +pre_save.connect(set_created_field, sender=Donor, + dispatch_uid="main.models") # vim: set ts=4 sw=4 et: |