layerindex-web/layerindex/models.py
Tim Orling c2a8e07020 layerindex/models: add BBClassRecipe BBClassGlobal
Add support for classes-global and classes-recipe, but use
"proxy=True" to not create new tables for the new classes.
Rather, we use the bbclass_type field.

[YOCTO #15238]

Signed-off-by: Tim Orling <tim.orling@konsulko.com>
2024-01-22 17:13:20 -08:00

973 lines
38 KiB
Python

# layerindex-web - model definitions
#
# Copyright (C) 2013-2018 Intel Corporation
#
# Licensed under the MIT license, see COPYING.MIT for details
#
# SPDX-License-Identifier: MIT
from django.db import models
from datetime import datetime
from django.contrib.auth.models import User
from django.urls import reverse
from django.core.validators import URLValidator
from django.db.models.signals import pre_save
from django.dispatch import receiver
from collections import namedtuple
import os.path
import re
import posixpath
import codecs
from . import utils
logger = utils.logger_create('LayerIndexModels')
@receiver(pre_save)
def truncate_charfield_values(sender, instance, *args, **kwargs):
# Instead of leaving this up to the database, check and handle it
# ourselves to avoid nasty exceptions; as a bonus we won't miss when
# the max length is too short with databases that don't enforce
# the limits (e.g. sqlite)
for field in instance._meta.get_fields():
if isinstance(field, models.CharField):
value = getattr(instance, field.name)
if value and len(value) > field.max_length:
logger.warning('%s.%s: %s: length %s exceeds maximum (%s), truncating' % (instance.__class__.__name__, field.name, str(instance), len(value), field.max_length))
setattr(instance, field.name, value[:field.max_length])
class PythonEnvironment(models.Model):
name = models.CharField(max_length=50)
python_command = models.CharField(max_length=255, default='python')
virtualenv_path = models.CharField(max_length=255, blank=True)
def get_command(self):
if self.virtualenv_path:
cmd = '. %s/bin/activate; %s' % (self.virtualenv_path, self.python_command)
else:
cmd = self.python_command
return cmd
@staticmethod
def get_default_python2_environment():
for env in PythonEnvironment.objects.all().order_by('id'):
if env.name.replace(' ', '').lower().startswith(('python2', 'py2')):
return env
return None
@staticmethod
def get_default_python3_environment():
for env in PythonEnvironment.objects.all().order_by('id'):
if env.name.replace(' ', '').lower().startswith(('python3', 'py3')):
return env
return None
def __str__(self):
return self.name
class Branch(models.Model):
name = models.CharField('Branch name', max_length=50)
bitbake_branch = models.CharField(max_length=50)
short_description = models.CharField(max_length=50, blank=True)
sort_priority = models.IntegerField(blank=True, null=True)
updates_enabled = models.BooleanField('Enable updates', default=True, help_text='Enable automatically updating layer metadata for this branch via the update script')
comparison = models.BooleanField('Comparison', default=False, help_text='If enabled, branch is for comparison purposes only and will appear separately')
update_environment = models.ForeignKey(PythonEnvironment, blank=True, null=True, on_delete=models.SET_NULL)
hidden = models.BooleanField('Hidden', default=False, help_text='Hide from normal selections')
updated = models.DateTimeField(auto_now=True, blank=True, null=True)
class Meta:
verbose_name_plural = "Branches"
ordering = ['sort_priority']
def __str__(self):
if self.comparison and self.short_description:
return self.short_description
elif self.short_description:
return '%s (%s)' % (self.name, self.short_description)
else:
return self.name
class Update(models.Model):
started = models.DateTimeField()
finished = models.DateTimeField(blank=True, null=True)
log = models.TextField(blank=True)
reload = models.BooleanField('Reloaded', default=False, help_text='Was this update a reload?')
task_id = models.CharField(max_length=50, blank=True, db_index=True)
triggered_by = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
retcode = models.IntegerField(default=0)
def error_count(self):
sums = self.layerupdate_set.aggregate(errors=models.Sum('errors'))
return (sums['errors'] or 0) + self.log.count('ERROR:')
def warning_count(self):
sums = self.layerupdate_set.aggregate(warnings=models.Sum('warnings'))
return (sums['warnings'] or 0) + self.log.count('WARNING:')
def __str__(self):
return '%s' % self.started
class LayerItem(models.Model):
LAYER_STATUS_CHOICES = (
('N', 'New'),
('P', 'Published'),
('X', 'No update'),
)
LAYER_TYPE_CHOICES = (
('A', 'Base'),
('B', 'Machine (BSP)'),
('S', 'Software'),
('D', 'Distribution'),
('M', 'Miscellaneous'),
)
name = models.CharField('Layer name', max_length=40, unique=True, help_text='Name of the layer - must be unique and can only contain letters, numbers and dashes')
status = models.CharField(max_length=1, choices=LAYER_STATUS_CHOICES, default='N')
layer_type = models.CharField(max_length=1, choices=LAYER_TYPE_CHOICES)
summary = models.CharField(max_length=200, help_text='One-line description of the layer')
description = models.TextField()
vcs_url = models.CharField('Repository URL', max_length=255, help_text='Fetch/clone URL of the repository')
vcs_web_url = models.URLField('Repository web interface URL', blank=True, help_text='URL of the web interface for browsing the repository, if any')
vcs_web_tree_base_url = models.CharField('Repository web interface tree base URL', max_length=255, blank=True, help_text='Base URL for the web interface for browsing directories within the repository, if any')
vcs_web_file_base_url = models.CharField('Repository web interface file base URL', max_length=255, blank=True, help_text='Base URL for the web interface for viewing files (blobs) within the repository, if any')
vcs_web_commit_url = models.CharField('Repository web interface commit URL', max_length=255, blank=True, help_text='Base URL for the web interface for viewing a single commit within the repository, if any')
usage_url = models.CharField('Usage web page URL', max_length=255, blank=True, help_text='URL of a web page with more information about the layer and how to use it, if any (or path to file within repository)')
mailing_list_url = models.URLField('Mailing list URL', blank=True, help_text='URL of the info page for a mailing list for discussing the layer, if any')
index_preference = models.IntegerField('Preference', default=0, help_text='Number used to find preferred recipes in recipe search results (higher number is greater preference)')
comparison = models.BooleanField('Comparison', default=False, help_text='Is this a comparison layer?')
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Layer"
permissions = (
("publish_layer", "Can publish layers"),
)
def change_status(self, newstatus, username):
self.status = newstatus
def get_layerbranch(self, branchname):
if branchname:
res = list(self.layerbranch_set.filter(branch__name=branchname)[:1])
else:
res = list(self.layerbranch_set.all()[:1])
if res:
return res[0]
return None
def active_maintainers(self):
matches = None
for layerbranch in self.layerbranch_set.all():
branchmatches = layerbranch.layermaintainer_set.filter(status='A')
if matches:
matches |= branchmatches
else:
matches = branchmatches
return matches
def user_can_edit(self, user):
if user.is_authenticated:
user_email = user.email.strip().lower()
for maintainer in self.active_maintainers():
if maintainer.email.strip().lower() == user_email:
return True
return False
def get_fetch_dir(self):
fetch_dir = ""
for c in self.vcs_url:
if c in '/ .=+?:':
fetch_dir += "_"
else:
fetch_dir += c
return fetch_dir
def get_absolute_url(self):
return reverse('layer_item', args=('master',self.name));
def __str__(self):
return self.name
class LayerRecipeExtraURL(models.Model):
layer = models.ForeignKey(LayerItem, on_delete=models.CASCADE)
name = models.CharField(max_length=50, help_text='Name to display for link')
url = models.CharField('URL', max_length=255, help_text='Template for URL to link to (macros: %pn% %pv% %branch% %actual_branch%)')
class Meta:
verbose_name = "Layer Recipe Extra URL"
def render_url(self, recipe):
url = self.url
url = url.replace('%pn%', recipe.pn)
url = url.replace('%pv%', recipe.pv)
url = url.replace('%branch%', recipe.layerbranch.branch.name)
url = url.replace('%actual_branch%', recipe.layerbranch.get_checkout_branch())
return url
def __str__(self):
return '%s - %s' % (self.layer.name, self.name)
class YPCompatibleVersion(models.Model):
name = models.CharField('Yocto Project Version', max_length=25, unique=True, help_text='Name of this Yocto Project compatible version (e.g. "2.0")')
description = models.TextField(blank=True)
image_url = models.CharField('Image URL', max_length=300, blank=True)
link_url = models.CharField('Link URL', max_length=100, blank=True)
class Meta:
verbose_name = 'Yocto Project Compatible version'
ordering = ('name',)
def __str__(self):
return self.name
class LayerBranch(models.Model):
layer = models.ForeignKey(LayerItem, on_delete=models.CASCADE)
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
collection = models.CharField('Layer Collection', max_length=40, null=True, blank=True, help_text='Name of the collection that the layer provides for the purpose of expressing dependencies (as specified in BBFILE_COLLECTIONS). Can only contain letters, numbers and dashes.')
version = models.CharField('Layer Version', max_length=10, null=True, blank=True, help_text='The layer version for this particular branch.')
vcs_subdir = models.CharField('Repository subdirectory', max_length=40, blank=True, help_text='Subdirectory within the repository where the layer is located, if not in the root (usually only used if the repository contains more than one layer)')
vcs_last_fetch = models.DateTimeField('Last successful fetch', blank=True, null=True)
vcs_last_rev = models.CharField('Last revision fetched', max_length=80, blank=True)
vcs_last_commit = models.DateTimeField('Last commit date', blank=True, null=True)
actual_branch = models.CharField('Actual Branch', max_length=80, blank=True, help_text='Name of the actual branch in the repository matching the core branch')
yp_compatible_version = models.ForeignKey(YPCompatibleVersion, verbose_name='Yocto Project Compatible version', null=True, blank=True, on_delete=models.SET_NULL, help_text='Which version of the Yocto Project Compatible program has this layer been approved for for?')
local_path = models.CharField(max_length=255, blank=True, help_text='Local subdirectory where layer data can be found')
updates_enabled = models.BooleanField('Enable updates', default=True, help_text='Enable automatically updating layer metadata for this layer:branch via the update script')
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = "Layer branches"
permissions = (
("set_yp_compatibility", "Can set YP compatibility"),
)
def sorted_recipes(self):
return self.recipe_set.order_by('pn', '-pv')
def active_maintainers(self):
return self.layermaintainer_set.filter(status='A')
def _handle_url_path(self, base_url, path):
if base_url:
if self.vcs_subdir:
if path:
extra_path = self.vcs_subdir + '/' + path
# Normalise out ../ in path for usage URL
extra_path = posixpath.normpath(extra_path)
# Minor workaround to handle case where subdirectory has been added between branches
# (should probably support usage URL per branch to handle this... sigh...)
if extra_path.startswith('../'):
extra_path = extra_path[3:]
else:
extra_path = self.vcs_subdir
else:
extra_path = path
if self.actual_branch:
branchname = self.actual_branch
else:
branchname = self.branch.name
url = base_url.replace('%branch%', branchname)
if path:
splitpath = path.split('/')
for match in re.findall('(%pathelement\[([0-9]*)(:[0-9]*)?\]%)', url):
if match[1] == '':
start = None
else:
start = int(match[1])
if ':' in match[2]:
stopstr = match[2][1:]
if stopstr == '':
stop = None
else:
stop = int(stopstr)
url = url.replace(match[0], '/'.join(splitpath[start:stop]))
else:
url = url.replace(match[0], splitpath[start])
# If there's a % in the path (e.g. a wildcard bbappend) we need to encode it
if extra_path:
extra_path = extra_path.replace('%', '%25')
if '%path%' in base_url or '%pathelement[' in base_url:
if extra_path:
url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url)
else:
url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url)
return url.replace('%path%', extra_path)
else:
return url + extra_path
return None
def tree_url(self, path = ''):
return self._handle_url_path(self.layer.vcs_web_tree_base_url, path)
def file_url(self, path = ''):
return self._handle_url_path(self.layer.vcs_web_file_base_url, path)
def commit_url(self, commit_hash):
url = self.layer.vcs_web_commit_url
url = url.replace('%hash%', commit_hash)
url = url.replace('%branch%', self.get_checkout_branch())
return url
def test_tree_url(self):
return self.tree_url('conf')
def test_file_url(self):
return self.file_url('conf/layer.conf')
def get_checkout_branch(self):
"""Get the branch that we actually need to check out in the repo"""
if self.actual_branch:
return self.actual_branch
else:
return self.branch.name
def get_usage_url(self):
usage_url = self.layer.usage_url
if usage_url.startswith('http'):
return usage_url
else:
url = self.file_url(usage_url)
if url:
if '/../' in url:
url = resolveComponents(url)
return url
def __str__(self):
return "%s: %s" % (self.layer.name, self.branch.name)
def get_required(self):
return self.dependencies_set.filter(required=True)
def get_recommends(self):
return self.dependencies_set.filter(required=False)
def get_recursive_dependencies(self, required=True, include_self=False):
deplist = []
def recurse_deps(layerbranch):
deplist.append(layerbranch)
if required:
dep_set = layerbranch.dependencies_set.filter(required=True)
else:
dep_set = layerbranch.dependencies_set.all()
for dep in dep_set:
deplayerbranch = dep.dependency.get_layerbranch(layerbranch.branch.name)
if deplayerbranch and deplayerbranch not in deplist:
recurse_deps(deplayerbranch)
recurse_deps(self)
if include_self:
return deplist
else:
return deplist[1:]
class LayerMaintainer(models.Model):
MAINTAINER_STATUS_CHOICES = (
('A', 'Active'),
('I', 'Inactive'),
)
layerbranch = models.ForeignKey(LayerBranch, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
email = models.CharField(max_length=255)
responsibility = models.CharField(max_length=200, blank=True, help_text='Specific area(s) this maintainer is responsible for, if not the entire layer')
status = models.CharField(max_length=1, choices=MAINTAINER_STATUS_CHOICES, default='A')
def __str__(self):
respstr = ""
if self.responsibility:
respstr = " (%s)" % self.responsibility
return "%s: %s <%s>%s" % (self.layerbranch, self.name, self.email, respstr)
class LayerDependency(models.Model):
layerbranch = models.ForeignKey(LayerBranch, related_name='dependencies_set', on_delete=models.CASCADE)
dependency = models.ForeignKey(LayerItem, related_name='dependents_set', on_delete=models.CASCADE)
required = models.BooleanField(default=True)
class Meta:
verbose_name_plural = "Layer dependencies"
def __str__(self):
return "%s depends on %s" % (self.layerbranch.layer.name, self.dependency.name)
class LayerNote(models.Model):
layer = models.ForeignKey(LayerItem, on_delete=models.CASCADE)
text = models.TextField()
def __str__(self):
return "%s: %s" % (self.layer.name, self.text)
class LayerUpdate(models.Model):
layer = models.ForeignKey(LayerItem, on_delete=models.CASCADE)
branch = models.ForeignKey(Branch, on_delete=models.CASCADE)
update = models.ForeignKey(Update, on_delete=models.CASCADE)
started = models.DateTimeField()
finished = models.DateTimeField(blank=True, null=True)
errors = models.IntegerField(default=0)
warnings = models.IntegerField(default=0)
vcs_before_rev = models.CharField('Revision before', max_length=80, blank=True)
vcs_after_rev = models.CharField('Revision after', max_length=80, blank=True)
log = models.TextField(blank=True)
retcode = models.IntegerField(default=0)
def layerbranch_exists(self):
"""Helper function for linking"""
return LayerBranch.objects.filter(layer=self.layer, branch=self.branch).exists()
def vcs_before_commit_url(self):
if self.vcs_before_rev:
layerbranch = LayerBranch.objects.filter(layer=self.layer, branch=self.branch).first()
if layerbranch:
return layerbranch.commit_url(self.vcs_before_rev)
return None
def vcs_after_commit_url(self):
if self.vcs_after_rev:
layerbranch = LayerBranch.objects.filter(layer=self.layer, branch=self.branch).first()
if layerbranch:
return layerbranch.commit_url(self.vcs_after_rev)
return None
def save(self):
warnings = 0
errors = 0
for line in self.log.splitlines():
if line.startswith('WARNING:'):
warnings += 1
elif line.startswith('ERROR:'):
errors += 1
self.warnings = warnings
self.errors = errors
super(LayerUpdate, self).save()
def __str__(self):
return "%s: %s: %s" % (self.layer.name, self.branch.name, self.started)
class Recipe(models.Model):
layerbranch = models.ForeignKey(LayerBranch, on_delete=models.CASCADE)
filename = models.CharField(max_length=255)
filepath = models.CharField(max_length=255, blank=True)
pn = models.CharField(max_length=100, blank=True)
pv = models.CharField(max_length=100, blank=True)
pr = models.CharField(max_length=100, blank=True)
pe = models.CharField(max_length=100, blank=True)
summary = models.CharField(max_length=200, blank=True)
description = models.TextField(blank=True)
section = models.CharField(max_length=100, blank=True)
license = models.CharField(max_length=2048, blank=True)
homepage = models.URLField(blank=True)
bugtracker = models.URLField(blank=True)
provides = models.CharField(max_length=2048, blank=True)
bbclassextend = models.CharField(max_length=100, blank=True)
inherits = models.CharField(max_length=255, blank=True)
updated = models.DateTimeField(auto_now=True)
blacklisted = models.CharField(max_length=255, blank=True)
configopts = models.CharField(max_length=4096, blank=True)
srcrev = models.CharField(max_length=64, blank=True)
def vcs_web_url(self):
url = self.layerbranch.file_url(os.path.join(self.filepath, self.filename))
return url or ''
def full_path(self):
return os.path.join(self.filepath, self.filename)
def short_desc(self):
if self.summary:
return self.summary
else:
return self.description
def name(self):
if self.pn:
return self.pn
else:
return self.filename.split('_')[0]
def homepage_url_only(self):
if '://' in self.homepage:
return self.homepage
else:
return None
def extra_urls(self):
ExtraURL = namedtuple('ExtraURL', 'name url')
for item in self.layerbranch.layer.layerrecipeextraurl_set.all():
eu = ExtraURL(name=item.name, url=item.render_url(self))
yield eu
def adjacent_includes(self):
"""Returns an iterator over any files included by this recipe that are adjacent to the recipe (usually .inc files)"""
recipepath = os.path.join(self.layerbranch.vcs_subdir, self.filepath)
if not recipepath.endswith('/'):
recipepath += '/'
IncludeFile = namedtuple('IncludeFile', 'filepath vcs_web_url')
for rfd in self.recipefiledependency_set.all():
if rfd.path.startswith(recipepath) and not os.path.basename(rfd.path) == self.filename:
ifile = IncludeFile(filepath=rfd.layer_path(), vcs_web_url=rfd.vcs_web_url())
yield ifile
def comparison_recipes(self):
return ClassicRecipe.objects.filter(cover_layerbranch=self.layerbranch).filter(cover_pn=self.pn)
def __str__(self):
return os.path.join(self.filepath, self.filename)
class Source(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
url = models.CharField(max_length=255)
sha256sum = models.CharField(max_length=64, blank=True)
def web_url(self):
def drop_dotgit(url):
if url.endswith('.git'):
url = url[:-4]
return url
if self.url and self.url.startswith(('http', 'ftp')):
return self.url
elif self.url.startswith('git://github.com'):
return drop_dotgit('https' + self.url[3:])
elif self.url.startswith('git://git.yoctoproject.org'):
return drop_dotgit('https://git.yoctoproject.org/cgit/cgit.cgi' + self.url[26:])
elif self.url.startswith('git://git.kernel.org'):
return 'https' + self.url[3:]
return None
def __str__(self):
return '%s - %s - %s' % (self.recipe.layerbranch, self.recipe.pn, self.url)
patch_status_re = re.compile(r"^[\t ]*(Upstream[-_ ]Status:?)[\t ]*([\w\-]+)([\t ]+.*)?", re.IGNORECASE | re.MULTILINE)
class Patch(models.Model):
PATCH_STATUS_CHOICES = [
('U', 'Unknown'),
('A', 'Accepted'),
('P', 'Pending'),
('I', 'Inappropriate'),
('B', 'Backport'),
('S', 'Submitted'),
('D', 'Denied'),
('N', 'Inactive-Upstream'),
]
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
path = models.CharField(max_length=255)
src_path = models.CharField(max_length=255)
status = models.CharField(max_length=1, choices=PATCH_STATUS_CHOICES, default='U')
status_extra = models.CharField(max_length=255, blank=True)
apply_order = models.IntegerField(blank=True, null=True)
applied = models.BooleanField(default=True)
striplevel = models.IntegerField(default=1)
class Meta:
verbose_name_plural = 'Patches'
ordering = ['recipe', 'apply_order']
def vcs_web_url(self):
url = self.recipe.layerbranch.file_url(self.path)
return url or ''
def read_status_from_file(self, patchfn, logger=None):
for encoding in ['utf-8', 'latin-1']:
try:
with codecs.open(patchfn, 'r', encoding=encoding) as f:
for line in f:
line = line.rstrip()
if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('+++ '):
break
res = patch_status_re.match(line)
if res:
status = res.group(2).lower()
for key, value in dict(Patch.PATCH_STATUS_CHOICES).items():
if status == value.lower():
self.status = key
if res.group(3):
self.status_extra = res.group(3).strip()
break
else:
if logger:
logger.warn('Invalid upstream status in %s: %s' % (patchfn, line))
except UnicodeDecodeError:
continue
break
else:
if logger:
logger.error('Unable to find suitable encoding to read patch %s' % patchfn)
def __str__(self):
return "%s - %s" % (self.recipe, self.src_path)
class PackageConfig(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
feature = models.CharField(max_length=255)
with_option = models.CharField(max_length=255, blank=True)
without_option = models.CharField(max_length=255, blank=True)
build_deps = models.CharField(max_length=255, blank=True)
def __str__(self):
return "%s - %s" % (self.recipe, self.feature)
def get_deps_list(self):
return self.build_deps.split()
class StaticBuildDep(models.Model):
recipes = models.ManyToManyField(Recipe)
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class DynamicBuildDep(models.Model):
package_configs = models.ManyToManyField(PackageConfig)
recipes = models.ManyToManyField(Recipe)
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class RecipeFileDependency(models.Model):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
layerbranch = models.ForeignKey(LayerBranch, related_name='+', on_delete=models.CASCADE)
path = models.CharField(max_length=255, db_index=True)
class Meta:
verbose_name_plural = "Recipe file dependencies"
def layer_path(self):
return os.path.relpath(self.path, self.layerbranch.vcs_subdir)
def vcs_web_url(self):
url = self.layerbranch.file_url(self.layer_path())
return url or ''
def __str__(self):
return '%s' % self.path
class ClassicRecipe(Recipe):
COVER_STATUS_CHOICES = [
('U', 'Unknown'),
('N', 'Not available'),
('R', 'Replaced'),
('P', 'Provided (BBCLASSEXTEND)'),
('C', 'Provided (PACKAGECONFIG)'),
('S', 'Distro-specific'),
('O', 'Obsolete'),
('E', 'Equivalent functionality'),
('D', 'Direct match'),
]
cover_layerbranch = models.ForeignKey(LayerBranch, verbose_name='Covering layer', blank=True, null=True, limit_choices_to = {'branch__name': 'master'}, on_delete=models.SET_NULL)
cover_pn = models.CharField('Covering recipe', max_length=100, blank=True)
cover_status = models.CharField(max_length=1, choices=COVER_STATUS_CHOICES, default='U')
cover_verified = models.BooleanField(default=False)
cover_comment = models.TextField(blank=True)
classic_category = models.CharField('OE-Classic Category', max_length=100, blank=True)
deleted = models.BooleanField(default=False)
needs_attention = models.BooleanField(default=False)
class Meta:
permissions = (
("edit_classic", "Can edit OE-Classic recipes"),
("update_comparison_branch", "Can update comparison branches"),
)
def get_cover_desc(self):
desc = self.get_cover_status_display()
if self.cover_layerbranch:
cover_layer = self.cover_layerbranch.layer.name
else:
cover_layer = '(unknown layer)'
if self.cover_status == 'D':
desc = 'Direct match exists in %s' % cover_layer
elif self.cover_pn:
if self.cover_status == 'R':
desc = 'Replaced by %s in %s' % (self.cover_pn, cover_layer)
elif self.cover_status == 'P':
desc = 'Provided by %s in %s (BBCLASSEXTEND)' % (self.cover_pn, cover_layer)
elif self.cover_status == 'C':
desc = 'Provided by %s in %s (as a PACKAGECONFIG option)' % (self.cover_pn, cover_layer)
elif self.cover_status == 'E':
desc = 'Equivalent functionality provided by %s in %s' % (self.cover_pn, cover_layer)
if self.cover_comment:
if self.cover_comment[0] == '(':
desc = "%s %s" % (desc, self.cover_comment)
else:
desc = "%s - %s" % (desc, self.cover_comment)
return desc
def get_cover_recipe(self):
if self.cover_layerbranch and self.cover_pn:
return Recipe.objects.filter(layerbranch=self.cover_layerbranch).filter(pn=self.cover_pn).first()
else:
return None
class ComparisonRecipeUpdate(models.Model):
update = models.ForeignKey(Update, on_delete=models.CASCADE)
recipe = models.ForeignKey(ClassicRecipe, on_delete=models.CASCADE)
meta_updated = models.BooleanField(default=False)
link_updated = models.BooleanField(default=False)
def __str__(self):
return '%s - %s' % (self.update, self.recipe)
class Machine(models.Model):
layerbranch = models.ForeignKey(LayerBranch, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.CharField(max_length=255)
updated = models.DateTimeField(auto_now=True)
def vcs_web_url(self):
url = self.layerbranch.file_url(os.path.join('conf/machine/%s.conf' % self.name))
return url or ''
def __str__(self):
return '%s (%s)' % (self.name, self.layerbranch.layer.name)
class Distro(models.Model):
layerbranch = models.ForeignKey(LayerBranch, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.CharField(max_length=255)
updated = models.DateTimeField(auto_now=True)
def vcs_web_url(self):
url = self.layerbranch.file_url(os.path.join('conf/distro/%s.conf' % self.name))
return url or ''
def __str__(self):
return '%s (%s)' % (self.name, self.layerbranch.layer.name)
class BBAppend(models.Model):
layerbranch = models.ForeignKey(LayerBranch, on_delete=models.CASCADE)
filename = models.CharField(max_length=255)
filepath = models.CharField(max_length=255, blank=True)
class Meta:
verbose_name = "Append"
def vcs_web_url(self):
url = self.layerbranch.file_url(os.path.join(self.filepath, self.filename))
return url or ''
def matches_recipe(self, recipe):
recipename = recipe.filename[:-3]
appendname = self.filename[:-9]
if recipename == appendname:
return True
elif '%' in appendname:
import fnmatch
return fnmatch.fnmatch(recipename, appendname.replace('%', '*'))
return False
def __str__(self):
return '%s: %s' % (self.layerbranch, os.path.join(self.filepath, self.filename))
class BBClass(models.Model):
layerbranch = models.ForeignKey(LayerBranch, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
# Type field
TYPE_CHOICES = [
('', 'Class'),
('Global', 'Global'),
('Recipe', 'Recipe')
]
bbclass_type = models.CharField(max_length=25, choices=TYPE_CHOICES, default='')
class Meta:
verbose_name = "Class"
verbose_name_plural = "Classes"
def vcs_web_url(self):
# We cannot override vcs_web_url in the sub-classes without defining self.url
# but we want url to be dynamic
if self.bbclass_type == 'Global':
url = self.layerbranch.file_url(os.path.join('classes-global', "%s.bbclass" % self.name))
elif self.bbclass_type == 'Recipe':
url = self.layerbranch.file_url(os.path.join('classes-recipe', "%s.bbclass" % self.name))
else:
url = self.layerbranch.file_url(os.path.join('classes', "%s.bbclass" % self.name))
return url or ''
def __str__(self):
return '%s (%s)' % (self.name, self.layerbranch.layer.name)
class BBClassGlobalManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(bbclass_type='Global')
class BBClassGlobal(BBClass):
objects = BBClassGlobalManager()
class Meta:
proxy = True
def save(self, *args, **kwargs):
self.bbclass_type = 'Global'
return super(BBClassGlobal, self).save(*args, **kwargs)
class BBClassRecipeManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(bbclass_type='Recipe')
class BBClassRecipe(BBClass):
objects = BBClassRecipeManager()
class Meta:
proxy = True
def save(self, *args, **kwargs):
self.bbclass_type = 'Recipe'
return super(BBClassRecipe, self).save(*args, **kwargs)
class IncFile(models.Model):
layerbranch = models.ForeignKey(LayerBranch, on_delete=models.CASCADE)
path = models.CharField(max_length=255)
def vcs_web_url(self):
url = self.layerbranch.file_url(self.path)
return url or ''
def __str__(self):
return '%s (%s)' % (self.path, self.layerbranch.layer.name)
class RecipeChangeset(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
def __str__(self):
return '%s' % (self.name)
class RecipeChange(models.Model):
RECIPE_VARIABLE_MAP = {
'summary': 'SUMMARY',
'description': 'DESCRIPTION',
'section': 'SECTION',
'license': 'LICENSE',
'homepage': 'HOMEPAGE',
'bugtracker': 'BUGTRACKER',
}
changeset = models.ForeignKey(RecipeChangeset, on_delete=models.CASCADE)
recipe = models.ForeignKey(Recipe, related_name='+', on_delete=models.CASCADE)
summary = models.CharField(max_length=100, blank=True)
description = models.TextField(blank=True)
section = models.CharField(max_length=100, blank=True)
license = models.CharField(max_length=100, blank=True)
homepage = models.URLField("Homepage URL", blank=True)
bugtracker = models.URLField("Bug tracker URL", blank=True)
def changed_fields(self, mapped = False):
res = {}
for fieldname in self.RECIPE_VARIABLE_MAP:
value = getattr(self, fieldname)
origvalue = getattr(self.recipe, fieldname)
if value != origvalue:
if mapped:
res[self.RECIPE_VARIABLE_MAP[fieldname]] = value
else:
res[fieldname] = value
return res
def reset_fields(self):
for fieldname in self.RECIPE_VARIABLE_MAP:
setattr(self, fieldname, getattr(self.recipe, fieldname))
class SiteNotice(models.Model):
NOTICE_LEVEL_CHOICES = [
('I', 'Info'),
('S', 'Success'),
('W', 'Warning'),
('E', 'Error'),
]
text = models.TextField(help_text='Text to show in the notice. A limited subset of HTML is supported for formatting.')
level = models.CharField(max_length=1, choices=NOTICE_LEVEL_CHOICES, default='I', help_text='Level of notice to display')
disabled = models.BooleanField('Disabled', default=False, help_text='Use to temporarily disable this notice')
expires = models.DateTimeField(blank=True, null=True, help_text='Optional date/time when this notice will stop showing')
def __str__(self):
prefix = ''
if self.expires and datetime.now() >= self.expires:
prefix = '[expired] '
elif self.disabled:
prefix = '[disabled] '
return '%s%s' % (prefix, self.text)
def text_sanitised(self):
return utils.sanitise_html(self.text)
class SecurityQuestion(models.Model):
question = models.CharField(max_length = 250, null=False)
def __str__(self):
return '%s' % (self.question)
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
answer_attempts = models.IntegerField(default=0)
def __str__(self):
return '%s' % (self.user)
class SecurityQuestionAnswer(models.Model):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
security_question = models.ForeignKey(SecurityQuestion, on_delete=models.CASCADE)
answer = models.CharField(max_length = 250, null=False)
def __str__(self):
return '%s - %s' % (self.user, self.security_question)
class PatchDisposition(models.Model):
PATCH_DISPOSITION_CHOICES = (
('A', 'Apply'),
('R', 'Further review'),
('E', 'Existing'),
('N', 'Not needed'),
('V', 'Different version'),
('I', 'Invalid'),
)
patch = models.OneToOneField(Patch, on_delete=models.CASCADE)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
disposition = models.CharField(max_length=1, choices=PATCH_DISPOSITION_CHOICES, default='A')
comment = models.TextField(blank=True)
class Meta:
permissions = (
("patch_disposition", "Can disposition patches"),
)
def __str__(self):
return '%s - %s' % (self.patch, self.get_disposition_display())
class ExtendedProvide(models.Model):
recipes = models.ManyToManyField(Recipe)
name = models.CharField(max_length=255, unique=True)
def __str__(self):
return self.name