summaryrefslogtreecommitdiff
path: root/main/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'main/models.py')
-rw-r--r--main/models.py246
1 files changed, 176 insertions, 70 deletions
diff --git a/main/models.py b/main/models.py
index 59dc154b..9156fb51 100644
--- a/main/models.py
+++ b/main/models.py
@@ -1,14 +1,18 @@
from django.db import models
+from django.db.models.signals import pre_save
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 .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 UserProfile(models.Model):
notify = models.BooleanField(
"Send notifications",
@@ -26,6 +30,9 @@ class UserProfile(models.Model):
max_length=50,
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 fingerprint",
+ 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)
@@ -38,11 +45,26 @@ 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'
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):
@@ -50,20 +72,29 @@ class TodolistManager(models.Manager):
class PackageManager(models.Manager):
def flagged(self):
- return self.get_query_set().filter(flag_date__isnull=False)
+ """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')
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)
@@ -110,30 +141,32 @@ 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)
- # TODO: it would be nice to have the >0 check constraint back here
- compressed_size = models.BigIntegerField(null=True)
- installed_size = models.BigIntegerField(null=True)
+ compressed_size = PositiveBigIntegerField()
+ installed_size = PositiveBigIntegerField()
build_date = models.DateTimeField(null=True)
- last_update = models.DateTimeField(null=True, blank=True)
+ last_update = models.DateTimeField()
files_last_update = models.DateTimeField(null=True, blank=True)
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
@@ -148,29 +181,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())
+ def is_signed(self):
+ return bool(self.pgp_signature)
+
+ _maintainers = None
+
@property
def maintainers(self):
- return User.objects.filter(
- package_relations__pkgbase=self.pkgbase,
- package_relations__type=PackageRelation.MAINTAINER)
+ if self._maintainers is None:
+ self._maintainers = User.objects.filter(
+ package_relations__pkgbase=self.pkgbase,
+ package_relations__type=PackageRelation.MAINTAINER)
+ return self._maintainers
- @property
- def signoffs(self):
- if 'signoffs_cache' in dir(self):
- return self.signoffs_cache
- self.signoffs_cache = list(Signoff.objects.filter(
- pkg=self,
- pkgver=self.pkgver,
- pkgrel=self.pkgrel))
- return self.signoffs_cache
-
- def approved_for_signoff(self):
- return len(self.signoffs) >= 2
+ @maintainers.setter
+ def maintainers(self, maintainers):
+ self._maintainers = maintainers
@cache_function(300)
def applicable_arches(self):
@@ -186,9 +217,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
@@ -200,6 +233,7 @@ class Package(models.Model):
groupby(requiredby, lambda x: x.pkg.id)]
# find another package by this name in the opposite testing setup
+ # TODO: figure out staging exclusions too
if not Package.objects.filter(pkgname=self.pkgname,
arch=self.arch).exclude(id=self.id).exclude(
repo__testing=self.repo.testing).exists():
@@ -215,7 +249,8 @@ class Package(models.Model):
dep = dep_pkgs[0]
if len(dep_pkgs) > 1:
dep_pkgs = [d for d in dep_pkgs
- if d.pkg.repo.testing == self.repo.testing]
+ if d.pkg.repo.testing == self.repo.testing and
+ d.pkg.repo.staging == self.repo.staging]
if len(dep_pkgs) > 0:
dep = dep_pkgs[0]
trimmed.append(dep)
@@ -224,36 +259,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.select_related('arch', 'repo').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]
- 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,
@@ -262,13 +290,14 @@ class Package(models.Model):
"""
try:
# start by looking for something in this repo
- return Package.objects.get(arch=self.arch,
+ return Package.objects.normal().get(arch=self.arch,
repo=self.repo, pkgname=self.pkgbase)
except Package.DoesNotExist:
# this package might be split across repos? just find one
# that matches the correct [testing] repo flag
- pkglist = Package.objects.filter(arch=self.arch,
- repo__testing=self.repo.testing, pkgname=self.pkgbase)
+ pkglist = Package.objects.normal().filter(arch=self.arch,
+ repo__testing=self.repo.testing,
+ repo__staging=self.repo.staging, pkgname=self.pkgbase)
if len(pkglist) > 0:
return pkglist[0]
return None
@@ -278,11 +307,12 @@ class Package(models.Model):
Return all packages that were built with this one (e.g. share a pkgbase
value). The package this method is called on will never be in the list,
and we will never return a package that does not have the same
- repo.testing flag. For any non-split packages, the return value will be
- an empty list.
+ repo.testing and repo.staging flags. For any non-split packages, the
+ return value will be an empty list.
"""
- return Package.objects.filter(arch__in=self.applicable_arches(),
- repo__testing=self.repo.testing, pkgbase=self.pkgbase).exclude(id=self.id)
+ return Package.objects.normal().filter(arch__in=self.applicable_arches(),
+ repo__testing=self.repo.testing, repo__staging=self.repo.staging,
+ pkgbase=self.pkgbase).exclude(id=self.id)
def is_same_version(self, other):
'is this package similar, name and version-wise, to another'
@@ -297,7 +327,18 @@ class Package(models.Model):
if self.repo.testing:
return None
try:
- return Package.objects.get(repo__testing=True,
+ return Package.objects.normal().get(repo__testing=True,
+ pkgname=self.pkgname, arch=self.arch)
+ except Package.DoesNotExist:
+ return None
+
+ def in_staging(self):
+ '''attempt to locate this package in a staging repo; if we are in
+ a staging repo we will always return None.'''
+ if self.repo.staging:
+ return None
+ try:
+ return Package.objects.normal().get(repo__staging=True,
pkgname=self.pkgname, arch=self.arch)
except Package.DoesNotExist:
return None
@@ -305,16 +346,10 @@ class Package(models.Model):
def elsewhere(self):
'''attempt to locate this package anywhere else, regardless of
architecture or repository. Excludes this package from the list.'''
- return Package.objects.select_related('arch', 'repo').filter(
+ return Package.objects.normal().filter(
pkgname=self.pkgname).exclude(id=self.id).order_by(
'arch__name', 'repo__name')
-class Signoff(models.Model):
- pkg = models.ForeignKey(Package)
- pkgver = models.CharField(max_length=255)
- pkgrel = models.CharField(max_length=255)
- packager = models.ForeignKey(User)
-
class PackageFile(models.Model):
pkg = models.ForeignKey(Package)
is_directory = models.BooleanField(default=False)
@@ -334,6 +369,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)
@@ -350,12 +443,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):
@@ -368,10 +466,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'),)
@@ -389,5 +493,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: