summaryrefslogtreecommitdiff
path: root/packages/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/models.py')
-rw-r--r--packages/models.py311
1 files changed, 271 insertions, 40 deletions
diff --git a/packages/models.py b/packages/models.py
index 820e61ba..f830aade 100644
--- a/packages/models.py
+++ b/packages/models.py
@@ -2,10 +2,13 @@ from collections import namedtuple
from django.db import models
from django.db.models.signals import pre_save
+from django.contrib.admin.models import ADDITION, CHANGE, DELETION
from django.contrib.auth.models import User
-from main.models import Arch, Repo
-from main.utils import set_created_field
+from main.models import Arch, Repo, Package
+from main.utils import set_created_field, database_vendor
+from packages.alpm import AlpmAPI
+
class PackageRelation(models.Model):
'''
@@ -26,13 +29,11 @@ class PackageRelation(models.Model):
created = models.DateTimeField(editable=False)
def get_associated_packages(self):
- # TODO: delayed import to avoid circular reference
- from main.models import Package
return Package.objects.normal().filter(pkgbase=self.pkgbase)
def repositories(self):
packages = self.get_associated_packages()
- return sorted(set([p.repo for p in packages]))
+ return sorted({p.repo for p in packages})
def __unicode__(self):
return u'%s: %s (%s)' % (
@@ -138,7 +139,7 @@ class Signoff(models.Model):
arch = models.ForeignKey(Arch)
repo = models.ForeignKey(Repo)
user = models.ForeignKey(User, related_name="package_signoffs")
- created = models.DateTimeField(editable=False)
+ created = models.DateTimeField(editable=False, db_index=True)
revoked = models.DateTimeField(null=True)
comments = models.TextField(null=True, blank=True)
@@ -146,8 +147,6 @@ class Signoff(models.Model):
@property
def packages(self):
- # TODO: delayed import to avoid circular reference
- from main.models import Package
return Package.objects.normal().filter(pkgbase=self.pkgbase,
pkgver=self.pkgver, pkgrel=self.pkgrel, epoch=self.epoch,
arch=self.arch, repo=self.repo)
@@ -172,10 +171,14 @@ class FlagRequest(models.Model):
'''
user = models.ForeignKey(User, blank=True, null=True)
user_email = models.EmailField('email address')
- created = models.DateTimeField(editable=False)
- ip_address = models.IPAddressField('IP address')
+ created = models.DateTimeField(editable=False, db_index=True)
+ # Great work, Django... https://code.djangoproject.com/ticket/18212
+ ip_address = models.GenericIPAddressField(verbose_name='IP address',
+ unpack_ipv4=True)
pkgbase = models.CharField(max_length=255, db_index=True)
- version = models.CharField(max_length=255, default='')
+ pkgver = models.CharField(max_length=255)
+ pkgrel = models.CharField(max_length=255)
+ epoch = models.PositiveIntegerField(default=0)
repo = models.ForeignKey(Repo)
num_packages = models.PositiveIntegerField('number of packages', default=1)
message = models.TextField('message to developer', blank=True)
@@ -192,76 +195,304 @@ class FlagRequest(models.Model):
return self.user.get_full_name()
return self.user_email
+ @property
+ def full_version(self):
+ # Difference here from other implementations at the moment: we need to
+ # handle the case of pkgver and pkgrel being null as this table didn't
+ # originally have version columns.
+ if self.pkgver == '' and self.pkgrel == '':
+ return u''
+ if self.epoch > 0:
+ return u'%d:%s-%s' % (self.epoch, self.pkgver, self.pkgrel)
+ return u'%s-%s' % (self.pkgver, self.pkgrel)
+
+ def get_associated_packages(self):
+ return Package.objects.normal().filter(
+ pkgbase=self.pkgbase,
+ repo__testing=self.repo.testing,
+ repo__staging=self.repo.staging).order_by(
+ 'pkgname', 'repo__name', 'arch__name')
+
def __unicode__(self):
return u'%s from %s on %s' % (self.pkgbase, self.who(), self.created)
+
+class UpdateManager(models.Manager):
+ def log_update(self, old_pkg, new_pkg):
+ '''Utility method to help log an update. This will determine the type
+ based on how many packages are passed in, and will pull the relevant
+ necesary fields off the given packages.
+ Note that in some cases, this is a no-op if we know this database type
+ supports triggers to add these rows instead.'''
+ if database_vendor(Package, 'write') in ('sqlite', 'postgresql'):
+ # we log updates using database triggers for these backends
+ return
+ update = Update()
+ if new_pkg:
+ update.action_flag = ADDITION
+ update.package = new_pkg
+ update.arch = new_pkg.arch
+ update.repo = new_pkg.repo
+ update.pkgname = new_pkg.pkgname
+ update.pkgbase = new_pkg.pkgbase
+ update.new_pkgver = new_pkg.pkgver
+ update.new_pkgrel = new_pkg.pkgrel
+ update.new_epoch = new_pkg.epoch
+ if old_pkg:
+ if new_pkg:
+ update.action_flag = CHANGE
+ # ensure we should even be logging this
+ if (old_pkg.pkgver == new_pkg.pkgver and
+ old_pkg.pkgrel == new_pkg.pkgrel and
+ old_pkg.epoch == new_pkg.epoch):
+ # all relevant fields were the same; e.g. a force update
+ return
+ else:
+ update.action_flag = DELETION
+ update.arch = old_pkg.arch
+ update.repo = old_pkg.repo
+ update.pkgname = old_pkg.pkgname
+ update.pkgbase = old_pkg.pkgbase
+
+ update.old_pkgver = old_pkg.pkgver
+ update.old_pkgrel = old_pkg.pkgrel
+ update.old_epoch = old_pkg.epoch
+
+ update.save(force_insert=True)
+ return update
+
+
+class Update(models.Model):
+ UPDATE_ACTION_CHOICES = (
+ (ADDITION, 'Addition'),
+ (CHANGE, 'Change'),
+ (DELETION, 'Deletion'),
+ )
+
+ package = models.ForeignKey(Package, related_name="updates",
+ null=True, on_delete=models.SET_NULL)
+ repo = models.ForeignKey(Repo, related_name="updates")
+ arch = models.ForeignKey(Arch, related_name="updates")
+ pkgname = models.CharField(max_length=255, db_index=True)
+ pkgbase = models.CharField(max_length=255)
+ action_flag = models.PositiveSmallIntegerField('action flag',
+ choices=UPDATE_ACTION_CHOICES)
+ created = models.DateTimeField(editable=False, db_index=True)
+
+ old_pkgver = models.CharField(max_length=255, null=True)
+ old_pkgrel = models.CharField(max_length=255, null=True)
+ old_epoch = models.PositiveIntegerField(null=True)
+
+ new_pkgver = models.CharField(max_length=255, null=True)
+ new_pkgrel = models.CharField(max_length=255, null=True)
+ new_epoch = models.PositiveIntegerField(null=True)
+
+ objects = UpdateManager()
+
+ class Meta:
+ get_latest_by = 'created'
+
+ def is_addition(self):
+ return self.action_flag == ADDITION
+
+ def is_change(self):
+ return self.action_flag == CHANGE
+
+ def is_deletion(self):
+ return self.action_flag == DELETION
+
+ @property
+ def old_version(self):
+ if self.action_flag == ADDITION:
+ return None
+ if self.old_epoch > 0:
+ return u'%d:%s-%s' % (self.old_epoch, self.old_pkgver, self.old_pkgrel)
+ return u'%s-%s' % (self.old_pkgver, self.old_pkgrel)
+
+ @property
+ def new_version(self):
+ if self.action_flag == DELETION:
+ return None
+ if self.new_epoch > 0:
+ return u'%d:%s-%s' % (self.new_epoch, self.new_pkgver, self.new_pkgrel)
+ return u'%s-%s' % (self.new_pkgver, self.new_pkgrel)
+
+ def elsewhere(self):
+ return Package.objects.normal().filter(
+ pkgname=self.pkgname, arch=self.arch)
+
+ def replacements(self):
+ pkgs = Package.objects.normal().filter(
+ replaces__name=self.pkgname)
+ if not self.arch.agnostic:
+ # make sure we match architectures if possible
+ arches = set(Arch.objects.filter(agnostic=True))
+ arches.add(self.arch)
+ pkgs = pkgs.filter(arch__in=arches)
+ return pkgs
+
+ def __unicode__(self):
+ return u'%s of %s on %s' % (self.get_action_flag_display(),
+ self.pkgname, self.created)
+
+
class PackageGroup(models.Model):
'''
Represents a group a package is in. There is no actual group entity,
only names that link to given packages.
'''
- pkg = models.ForeignKey('main.Package', related_name='groups')
+ pkg = models.ForeignKey(Package, related_name='groups')
name = models.CharField(max_length=255, db_index=True)
def __unicode__(self):
return "%s: %s" % (self.name, self.pkg)
+ class Meta:
+ ordering = ('name',)
+
+
class License(models.Model):
- pkg = models.ForeignKey('main.Package', related_name='licenses')
+ pkg = models.ForeignKey(Package, related_name='licenses')
name = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class Meta:
- ordering = ['name']
+ ordering = ('name',)
-class Conflict(models.Model):
- pkg = models.ForeignKey('main.Package', related_name='conflicts')
+
+class RelatedToBase(models.Model):
+ '''A base class for conflicts/provides/replaces/etc.'''
name = models.CharField(max_length=255, db_index=True)
- comparison = models.CharField(max_length=255, default='')
version = models.CharField(max_length=255, default='')
+ def get_best_satisfier(self):
+ '''Find a satisfier for this related package 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.name)
+ if not self.pkg.arch.agnostic:
+ # make sure we match architectures if possible
+ arches = self.pkg.applicable_arches()
+ pkgs = pkgs.filter(arch__in=arches)
+ # if we have a comparison operation, make sure the packages we grab
+ # actually satisfy the requirements
+ if self.comparison and self.version:
+ alpm = AlpmAPI()
+ pkgs = [pkg for pkg in pkgs if not alpm.available or
+ alpm.compare_versions(pkg.full_version, self.comparison,
+ self.version)]
+ 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
+ pkgs = [p for p in pkgs if p.repo.staging == self.pkg.repo.staging]
+ if len(pkgs) > 0:
+ pkg = pkgs[0]
+
+ pkgs = [p for p in pkgs if p.repo.testing == self.pkg.repo.testing]
+ if len(pkgs) > 0:
+ pkg = pkgs[0]
+
+ return pkg
+
+ def get_providers(self):
+ '''Return providers of this related package. Does *not* include exact
+ matches as it checks the Provision names only, use get_best_satisfier()
+ instead for exact matches.'''
+ pkgs = Package.objects.normal().filter(
+ provides__name=self.name).order_by().distinct()
+ if not self.pkg.arch.agnostic:
+ # make sure we match architectures if possible
+ arches = self.pkg.applicable_arches()
+ pkgs = pkgs.filter(arch__in=arches)
+
+ # If we have a comparison operation, make sure the packages we grab
+ # actually satisfy the requirements.
+ alpm = AlpmAPI()
+ if alpm.available and self.comparison and self.version:
+ pkgs = pkgs.prefetch_related('provides')
+ new_pkgs = []
+ for package in pkgs:
+ for provide in package.provides.all():
+ if provide.name != self.name:
+ continue
+ if alpm.compare_versions(provide.version,
+ self.comparison, self.version):
+ new_pkgs.append(package)
+ pkgs = new_pkgs
+
+ # Sort providers by preference. We sort those in same staging/testing
+ # combination first, followed by others. We sort by a (staging,
+ # testing) match tuple that will be (True, True) in the best case.
+ key_func = lambda x: (x.repo.staging == self.pkg.repo.staging,
+ x.repo.testing == self.pkg.repo.testing)
+ return sorted(pkgs, key=key_func, reverse=True)
+
def __unicode__(self):
if self.version:
return u'%s%s%s' % (self.name, self.comparison, self.version)
return self.name
class Meta:
- ordering = ['name']
+ abstract = True
+ ordering = ('name',)
-class Provision(models.Model):
- pkg = models.ForeignKey('main.Package', related_name='provides')
- name = models.CharField(max_length=255, db_index=True)
- # comparison must be '=' for provides
- comparison = '='
- version = models.CharField(max_length=255, default='')
+
+class Depend(RelatedToBase):
+ DEPTYPE_CHOICES = (
+ ('D', 'Depend'),
+ ('O', 'Optional Depend'),
+ ('M', 'Make Depend'),
+ ('C', 'Check Depend'),
+ )
+
+ pkg = models.ForeignKey(Package, related_name='depends')
+ comparison = models.CharField(max_length=255, default='')
+ description = models.TextField(null=True, blank=True)
+ deptype = models.CharField(max_length=1, default='D',
+ choices=DEPTYPE_CHOICES)
def __unicode__(self):
- if self.version:
- return u'%s=%s' % (self.name, self.version)
- return self.name
+ '''For depends, we may also have a description and a modifier.'''
+ to_str = super(Depend, self).__unicode__()
+ if self.description:
+ return u'%s: %s' % (to_str, self.description)
+ return to_str
- class Meta:
- ordering = ['name']
-class Replacement(models.Model):
- pkg = models.ForeignKey('main.Package', related_name='replaces')
- name = models.CharField(max_length=255, db_index=True)
+class Conflict(RelatedToBase):
+ pkg = models.ForeignKey(Package, related_name='conflicts')
comparison = models.CharField(max_length=255, default='')
- version = models.CharField(max_length=255, default='')
- def __unicode__(self):
- if self.version:
- return u'%s%s%s' % (self.name, self.comparison, self.version)
- return self.name
- class Meta:
- ordering = ['name']
+class Provision(RelatedToBase):
+ pkg = models.ForeignKey(Package, related_name='provides')
+ # comparison must be '=' for provides
+
+ @property
+ def comparison(self):
+ if self.version is not None and self.version != '':
+ return '='
+ return None
+
+
+class Replacement(RelatedToBase):
+ pkg = models.ForeignKey(Package, related_name='replaces')
+ comparison = models.CharField(max_length=255, default='')
# hook up some signals
-for sender in (PackageRelation, SignoffSpecification, Signoff):
+for sender in (FlagRequest, PackageRelation,
+ SignoffSpecification, Signoff, Update):
pre_save.connect(set_created_field, sender=sender,
dispatch_uid="packages.models")