summaryrefslogtreecommitdiff
path: root/main/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'main/models.py')
-rw-r--r--main/models.py192
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: