Add support for importing OE-Classic recipes

Add a script for doing a one-time import of OE-Classic recipe
information, so comparisons against OE-Core can be performed; this
is stored using a new ClassicRecipe model supporting additional fields
for tracking migration status. The migration status fields can be
updated as well as viewed and summarised in graph format.

Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
This commit is contained in:
Paul Eggleton 2013-07-28 18:40:07 +01:00
parent e8d734a377
commit c3a8eb4d82
25 changed files with 19810 additions and 77 deletions

5
README
View File

@ -24,6 +24,7 @@ In order to make use of this application you will need:
* django-reversion (1.6.5)
* django-reversion-compare (0.3.5)
* django-simple-captcha (0.3.6)
* django-nvd3 (0.1.11)
* On the machine that will run the backend update script (which does not
have to be the same machine as the web server, however it does still
have to have Django installed, have the same or similar configuration
@ -115,6 +116,10 @@ Bundled jQuery is redistributed under the MIT license.
Bundled uitablefilter.js is redistributed under the MIT license.
Bundled nv.d3.js is redistributed under the Apache License 2.0.
Bundled d3.v2.js is redistributed under the BSD License.
All other content is copyright (C) 2013 Intel Corporation and licensed
under the MIT license (unless otherwise noted) - see COPYING.MIT for
details.

View File

@ -45,6 +45,15 @@ class LayerNoteAdmin(CompareVersionAdmin):
list_filter = ['layer__name']
class RecipeAdmin(admin.ModelAdmin):
search_fields = ['filename', 'pn']
list_filter = ['layerbranch__layer__name', 'layerbranch__branch__name']
readonly_fields = [fieldname for fieldname in Recipe._meta.get_all_field_names() if fieldname not in ['recipefiledependency', 'classicrecipe']]
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
class ClassicRecipeAdmin(admin.ModelAdmin):
search_fields = ['filename', 'pn']
list_filter = ['layerbranch__layer__name', 'layerbranch__branch__name']
readonly_fields = [fieldname for fieldname in Recipe._meta.get_all_field_names() if fieldname != 'recipefiledependency']
@ -101,3 +110,4 @@ admin.site.register(Machine, MachineAdmin)
admin.site.register(BBAppend, BBAppendAdmin)
admin.site.register(BBClass, BBClassAdmin)
admin.site.register(RecipeChangeset, RecipeChangesetAdmin)
admin.site.register(ClassicRecipe, ClassicRecipeAdmin)

View File

@ -11,7 +11,8 @@ def layerindex_context(request):
if not current_branch:
current_branch = 'master'
return {
'all_branches': Branch.objects.all().order_by('sort_priority'),
'all_branches': Branch.objects.exclude(name='oe-classic').order_by('sort_priority'),
'current_branch': current_branch,
'unpublished_count': LayerItem.objects.filter(status='N').count(),
'oe_classic': Branch.objects.filter(name='oe-classic')
}

View File

@ -4,7 +4,7 @@
#
# Licensed under the MIT license, see COPYING.MIT for details
from layerindex.models import LayerItem, LayerBranch, LayerMaintainer, LayerNote, RecipeChangeset, RecipeChange
from layerindex.models import LayerItem, LayerBranch, LayerMaintainer, LayerNote, RecipeChangeset, RecipeChange, ClassicRecipe
from django import forms
from django.core.validators import URLValidator, RegexValidator, email_re
from django.forms.models import inlineformset_factory, modelformset_factory
@ -48,7 +48,7 @@ LayerMaintainerFormSet = inlineformset_factory(LayerBranch, LayerMaintainer, for
class EditLayerForm(forms.ModelForm):
# Additional form fields
vcs_subdir = forms.CharField(label='Repository subdirectory', max_length=40, required=False, 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)')
deps = forms.ModelMultipleChoiceField(label='Other layers this layer depends upon', queryset=LayerItem.objects.all(), required=False)
deps = forms.ModelMultipleChoiceField(label='Other layers this layer depends upon', queryset=LayerItem.objects.filter(classic=False), required=False)
captcha = CaptchaField(label='Verification', help_text='Please enter the letters displayed for verification purposes', error_messages={'invalid':'Incorrect entry, please try again'})
class Meta:
@ -149,6 +149,12 @@ class EditProfileForm(forms.ModelForm):
fields = ('first_name', 'last_name', 'email')
class ClassicRecipeForm(forms.ModelForm):
class Meta:
model = ClassicRecipe
fields = ('cover_layerbranch', 'cover_pn', 'cover_status', 'cover_verified', 'cover_comment', 'classic_category')
class AdvancedRecipeSearchForm(forms.Form):
FIELD_CHOICES = (
('pn', 'Name'),
@ -168,7 +174,7 @@ class AdvancedRecipeSearchForm(forms.Form):
field = forms.ChoiceField(choices=FIELD_CHOICES)
match_type = forms.ChoiceField(choices=MATCH_TYPE_CHOICES)
value = forms.CharField(max_length=255, required=False)
layer = forms.ModelChoiceField(queryset=LayerItem.objects.filter(status='P').order_by('name'), empty_label="(any)", required=False)
layer = forms.ModelChoiceField(queryset=LayerItem.objects.filter(classic=False).filter(status='P').order_by('name'), empty_label="(any)", required=False)
class RecipeChangesetForm(forms.ModelForm):
@ -202,3 +208,18 @@ class BulkChangeEditForm(forms.ModelForm):
setattr(self.instance, fieldname, '')
BulkChangeEditFormSet = modelformset_factory(RecipeChange, form=BulkChangeEditForm, extra=0)
class ClassicRecipeSearchForm(forms.Form):
COVER_STATUS_CHOICES = [('','(any)'), ('!','(not migrated)')] + ClassicRecipe.COVER_STATUS_CHOICES
VERIFIED_CHOICES = [
('', '(any)'),
('1', 'Verified'),
('0', 'Unverified'),
]
q = forms.CharField(label='Keyword', max_length=255, required=False)
category = forms.CharField(max_length=255, required=False)
cover_status = forms.ChoiceField(label='Status', choices=COVER_STATUS_CHOICES, required=False)
cover_verified = forms.ChoiceField(label='Verified', choices=VERIFIED_CHOICES, required=False)

View File

@ -0,0 +1,205 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'ClassicRecipe'
db.create_table('layerindex_classicrecipe', (
('recipe_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['layerindex.Recipe'], unique=True, primary_key=True)),
('cover_layerbranch', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['layerindex.LayerBranch'], null=True, on_delete=models.SET_NULL, blank=True)),
('cover_pn', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)),
('cover_status', self.gf('django.db.models.fields.CharField')(default='U', max_length=1)),
('cover_verified', self.gf('django.db.models.fields.BooleanField')(default=False)),
('cover_comment', self.gf('django.db.models.fields.TextField')(blank=True)),
('classic_category', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)),
))
db.send_create_signal('layerindex', ['ClassicRecipe'])
# Adding field 'LayerItem.classic'
db.add_column('layerindex_layeritem', 'classic',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting model 'ClassicRecipe'
db.delete_table('layerindex_classicrecipe')
# Deleting field 'LayerItem.classic'
db.delete_column('layerindex_layeritem', 'classic')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'layerindex.bbappend': {
'Meta': {'object_name': 'BBAppend'},
'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'filepath': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerBranch']"})
},
'layerindex.bbclass': {
'Meta': {'object_name': 'BBClass'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerBranch']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'layerindex.branch': {
'Meta': {'object_name': 'Branch'},
'bitbake_branch': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'short_description': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'sort_priority': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
'layerindex.classicrecipe': {
'Meta': {'object_name': 'ClassicRecipe', '_ormbases': ['layerindex.Recipe']},
'classic_category': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'cover_comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'cover_layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerBranch']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'cover_pn': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'cover_status': ('django.db.models.fields.CharField', [], {'default': "'U'", 'max_length': '1'}),
'cover_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'recipe_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['layerindex.Recipe']", 'unique': 'True', 'primary_key': 'True'})
},
'layerindex.layerbranch': {
'Meta': {'object_name': 'LayerBranch'},
'actual_branch': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
'branch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.Branch']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerItem']"}),
'vcs_last_commit': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'vcs_last_fetch': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'vcs_last_rev': ('django.db.models.fields.CharField', [], {'max_length': '80', 'blank': 'True'}),
'vcs_subdir': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'})
},
'layerindex.layerdependency': {
'Meta': {'object_name': 'LayerDependency'},
'dependency': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependents_set'", 'to': "orm['layerindex.LayerItem']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependencies_set'", 'to': "orm['layerindex.LayerBranch']"})
},
'layerindex.layeritem': {
'Meta': {'object_name': 'LayerItem'},
'classic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'description': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'index_preference': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'layer_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
'mailing_list_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'N'", 'max_length': '1'}),
'summary': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
'usage_url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'vcs_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'vcs_web_file_base_url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'vcs_web_tree_base_url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'vcs_web_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'layerindex.layermaintainer': {
'Meta': {'object_name': 'LayerMaintainer'},
'email': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerBranch']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'responsibility': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'A'", 'max_length': '1'})
},
'layerindex.layernote': {
'Meta': {'object_name': 'LayerNote'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerItem']"}),
'text': ('django.db.models.fields.TextField', [], {})
},
'layerindex.machine': {
'Meta': {'object_name': 'Machine'},
'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerBranch']"}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'layerindex.recipe': {
'Meta': {'object_name': 'Recipe'},
'bbclassextend': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'bugtracker': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'filepath': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.LayerBranch']"}),
'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'pn': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'provides': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'pv': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'section': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'summary': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
},
'layerindex.recipechange': {
'Meta': {'object_name': 'RecipeChange'},
'bugtracker': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'changeset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.RecipeChangeset']"}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'homepage': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'recipe': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['layerindex.Recipe']"}),
'section': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'summary': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
},
'layerindex.recipechangeset': {
'Meta': {'object_name': 'RecipeChangeset'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'layerindex.recipefiledependency': {
'Meta': {'object_name': 'RecipeFileDependency'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'layerbranch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['layerindex.LayerBranch']"}),
'path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'recipe': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['layerindex.Recipe']"})
}
}
complete_apps = ['layerindex']

View File

@ -51,6 +51,7 @@ class LayerItem(models.Model):
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)')
classic = models.BooleanField('Classic', default=False, help_text='Is this OE-Classic?')
class Meta:
verbose_name = "Layer"
@ -260,6 +261,54 @@ class RecipeFileDependency(models.Model):
return '%s' % self.path
class ClassicRecipe(Recipe):
COVER_STATUS_CHOICES = [
('U', 'Unknown'),
('N', 'Not available'),
('R', 'Replaced'),
('P', 'Provided (BBCLASSEXTEND)'),
('C', 'Provided (PACKAGECONFIG)'),
('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)
class Meta:
permissions = (
("edit_classic", "Can edit OE-Classic recipes"),
)
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
class Machine(models.Model):
layerbranch = models.ForeignKey(LayerBranch)
name = models.CharField(max_length=255)

96
layerindex/recipedesc.py Normal file
View File

@ -0,0 +1,96 @@
#!/usr/bin/env python
# Test script
#
# Copyright (C) 2012 Intel Corporation
# Author: Paul Eggleton <paul.eggleton@linux.intel.com>
import sys
import os.path
import logging
import subprocess
from datetime import datetime
import fnmatch
from distutils.version import LooseVersion
logger = None
def sanitise_path(inpath):
outpath = ""
for c in inpath:
if c in '/ .=+?:':
outpath += "_"
else:
outpath += c
return outpath
def main():
# Get access to our Django model
newpath = os.path.abspath(os.path.dirname(os.path.abspath(sys.argv[0])) + '/..')
sys.path.append(newpath)
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from django.core.management import setup_environ
from django.conf import settings
from layerindex.models import LayerItem, Recipe
from django.db import transaction
import settings
setup_environ(settings)
# Set path to bitbake lib dir
basepath = os.path.abspath(sys.argv[1])
bitbakedir_env = os.environ.get('BITBAKEDIR', '')
if bitbakedir_env and os.path.exists(bitbakedir_env + '/lib/bb'):
bitbakepath = bitbakedir_env
elif os.path.exists(basepath + '/bitbake/lib/bb'):
bitbakepath = basepath + '/bitbake'
elif os.path.exists(basepath + '/../bitbake/lib/bb'):
bitbakepath = os.path.abspath(basepath + '/../bitbake')
else:
# look for bitbake/bin dir in PATH
bitbakepath = None
for pth in os.environ['PATH'].split(':'):
if os.path.exists(os.path.join(pth, '../lib/bb')):
bitbakepath = os.path.abspath(os.path.join(pth, '..'))
break
if not bitbakepath:
print("Unable to find bitbake by searching BITBAKEDIR, specified path '%s' or its parent, or PATH" % basepath)
sys.exit(1)
# Skip sanity checks
os.environ['BB_ENV_EXTRAWHITE'] = 'DISABLE_SANITY_CHECKS'
os.environ['DISABLE_SANITY_CHECKS'] = '1'
sys.path.extend([bitbakepath + '/lib'])
import bb.tinfoil
tinfoil = bb.tinfoil.Tinfoil()
tinfoil.prepare(config_only = True)
logger = logging.getLogger('BitBake')
fetchdir = settings.LAYER_FETCH_DIR
if not fetchdir:
logger.error("Please set LAYER_FETCH_DIR in settings.py")
sys.exit(1)
for layer in LayerItem.objects.filter(status='P'):
urldir = sanitise_path(layer.vcs_url)
repodir = os.path.join(fetchdir, urldir)
layerrecipes = Recipe.objects.filter(layer=layer)
for recipe in layerrecipes:
fullpath = str(os.path.join(repodir, layer.vcs_subdir, recipe.filepath, recipe.filename))
print fullpath
try:
envdata = bb.cache.Cache.loadDataFull(fullpath, [], tinfoil.config_data)
print "DESCRIPTION = \"%s\"" % envdata.getVar("DESCRIPTION", True)
except Exception as e:
logger.info("Unable to read %s: %s", fullpath, str(e))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -10,6 +10,8 @@ import os
import os.path
import utils
import tempfile
import re
import fnmatch
class RecipeParseError(Exception):
def __init__(self, msg):
@ -41,8 +43,8 @@ def _parse_layer_conf(layerdir, data):
data.expandVarref('LAYERDIR')
def init_parser(settings, branch, bitbakepath, enable_tracking=False, nocheckout=False):
if not nocheckout:
def init_parser(settings, branch, bitbakepath, enable_tracking=False, nocheckout=False, classic=False):
if not (nocheckout or classic):
# Check out the branch of BitBake appropriate for this branch and clean out any stale files (e.g. *.pyc)
out = utils.runcmd("git checkout origin/%s" % branch.bitbake_branch, bitbakepath)
out = utils.runcmd("git clean -f -x", bitbakepath)
@ -53,6 +55,7 @@ def init_parser(settings, branch, bitbakepath, enable_tracking=False, nocheckout
fetchdir = settings.LAYER_FETCH_DIR
if not classic:
# Ensure we have OE-Core set up to get some base configuration
core_layer = utils.get_layer(settings.CORE_LAYER_NAME)
if not core_layer:
@ -86,6 +89,7 @@ def init_parser(settings, branch, bitbakepath, enable_tracking=False, nocheckout
# Ensure TMPDIR exists (or insane.bbclass will blow up trying to write to the QA log)
oe_tmpdir = tinfoil.config_data.getVar('TMPDIR', True)
if not os.path.exists(oe_tmpdir):
os.makedirs(oe_tmpdir)
return (tinfoil, tempdir)
@ -130,3 +134,34 @@ def get_var_files(fn, varlist, d):
varfiles[v] = actualfile
return varfiles
machine_conf_re = re.compile(r'conf/machine/([^/.]*).conf$')
bbclass_re = re.compile(r'classes/([^/.]*).bbclass$')
def detect_file_type(path, subdir_start):
typename = None
if fnmatch.fnmatch(path, "*.bb"):
typename = 'recipe'
elif fnmatch.fnmatch(path, "*.bbappend"):
typename = 'bbappend'
else:
# Check if it's a machine conf file
subpath = path[len(subdir_start):]
res = machine_conf_re.match(subpath)
if res:
typename = 'machine'
return (typename, None, res.group(1))
else:
res = bbclass_re.match(subpath)
if res:
typename = 'bbclass'
return (typename, None, res.group(1))
if typename == 'recipe' or typename == 'bbappend':
if subdir_start:
filepath = os.path.relpath(os.path.dirname(path), subdir_start)
else:
filepath = os.path.dirname(path)
return (typename, filepath, os.path.basename(path))
return (None, None, None)

View File

@ -0,0 +1,645 @@
/********************
* HTML CSS
*/
.chartWrap {
margin: 0;
padding: 0;
overflow: hidden;
}
/********************
* TOOLTIP CSS
*/
.nvtooltip {
position: absolute;
background-color: rgba(255,255,255,1);
padding: 10px;
border: 1px solid #ddd;
z-index: 10000;
font-family: Arial;
font-size: 13px;
transition: opacity 500ms linear;
-moz-transition: opacity 500ms linear;
-webkit-transition: opacity 500ms linear;
transition-delay: 500ms;
-moz-transition-delay: 500ms;
-webkit-transition-delay: 500ms;
-moz-box-shadow: 4px 4px 8px rgba(0,0,0,.5);
-webkit-box-shadow: 4px 4px 8px rgba(0,0,0,.5);
box-shadow: 4px 4px 8px rgba(0,0,0,.5);
-moz-border-radius: 10px;
border-radius: 10px;
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.nvtooltip h3 {
margin: 0;
padding: 0;
text-align: center;
}
.nvtooltip p {
margin: 0;
padding: 0;
text-align: center;
}
.nvtooltip span {
display: inline-block;
margin: 2px 0;
}
.nvtooltip-pending-removal {
position: absolute;
pointer-events: none;
}
/********************
* SVG CSS
*/
svg {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Trying to get SVG to act like a greedy block in all browsers */
display: block;
width:100%;
height:100%;
}
svg text {
font: normal 12px sans-serif;
}
svg .title {
font: bold 14px Arial;
}
.nvd3 .nv-background {
fill: white;
fill-opacity: 0;
/*
pointer-events: none;
*/
}
.nvd3.nv-noData {
font-size: 18px;
font-weight: bolf;
}
/**********
* Brush
*/
.nv-brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
/**********
* Legend
*/
.nvd3 .nv-legend .nv-series {
}
.nvd3 .nv-legend .disabled circle {
fill-opacity: 0;
}
/**********
* Axes
*/
.nvd3 .nv-axis path {
fill: none;
stroke: #000;
stroke-opacity: .75;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis path.domain {
stroke-opacity: .75;
}
.nvd3 .nv-axis.nv-x path.domain {
stroke-opacity: 0;
}
.nvd3 .nv-axis line {
fill: none;
stroke: #000;
stroke-opacity: .25;
shape-rendering: crispEdges;
}
.nvd3 .nv-axis line.zero {
stroke-opacity: .75;
}
.nvd3 .nv-axis .nv-axisMaxMin text {
font-weight: bold;
}
.nvd3 .x .nv-axis .nv-axisMaxMin text,
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
text-anchor: middle
}
/**********
* Brush
*/
.nv-brush .resize path {
fill: #eee;
stroke: #666;
}
/**********
* Bars
*/
.nvd3 .nv-bars .negative rect {
zfill: brown;
}
.nvd3 .nv-bars rect {
zfill: steelblue;
fill-opacity: .75;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-bars rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-bars .hover rect {
fill: lightblue;
}
.nvd3 .nv-bars text {
fill: rgba(0,0,0,0);
}
.nvd3 .nv-bars .hover text {
fill: rgba(0,0,0,1);
}
.nvd3 .nv-x.nv-axis text {
transform: rotate(90);
}
/**********
* Bars
*/
.nvd3 .nv-multibar .nv-groups rect,
.nvd3 .nv-multibarHorizontal .nv-groups rect,
.nvd3 .nv-discretebar .nv-groups rect {
stroke-opacity: 0;
transition: fill-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear;
}
.nvd3 .nv-multibar .nv-groups rect:hover,
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
.nvd3 .nv-discretebar .nv-groups rect:hover {
fill-opacity: 1;
}
.nvd3 .nv-discretebar .nv-groups text,
.nvd3 .nv-multibarHorizontal .nv-groups text {
font-weight: bold;
fill: rgba(0,0,0,1);
stroke: rgba(0,0,0,0);
}
/***********
* Pie Chart
*/
.nvd3.nv-pie path {
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-pie .nv-slice text {
stroke: #000;
stroke-width: 0;
}
.nvd3.nv-pie path {
stroke: #fff;
stroke-width: 1px;
stroke-opacity: 1;
}
.nvd3.nv-pie .hover path {
fill-opacity: .7;
/*
stroke-width: 6px;
stroke-opacity: 1;
*/
}
.nvd3.nv-pie .nv-label rect {
fill-opacity: 0;
stroke-opacity: 0;
}
/**********
* Lines
*/
.nvd3 .nv-groups path.nv-line {
fill: none;
stroke-width: 2.5px;
stroke-linecap: round;
shape-rendering: geometricPrecision;
/*
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-groups path.nv-area {
stroke: none;
stroke-linecap: round;
shape-rendering: geometricPrecision;
/*
stroke-width: 2.5px;
transition: stroke-width 250ms linear;
-moz-transition: stroke-width 250ms linear;
-webkit-transition: stroke-width 250ms linear;
transition-delay: 250ms
-moz-transition-delay: 250ms;
-webkit-transition-delay: 250ms;
*/
}
.nvd3 .nv-line.hover path {
stroke-width: 6px;
}
/*
.nvd3.scatter .groups .point {
fill-opacity: 0.1;
stroke-opacity: 0.1;
}
*/
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.nvd3 .nv-groups .nv-point {
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
}
.nvd3.nv-scatter .nv-groups .nv-point.hover,
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
fill-opacity: .5 !important;
stroke-opacity: .5 !important;
}
.nvd3 .nv-point-paths path {
stroke: #aaa;
stroke-opacity: 0;
fill: #eee;
fill-opacity: 0;
}
.nvd3 .nv-indexLine {
cursor: ew-resize;
}
/**********
* Distribution
*/
.nvd3 .nv-distribution {
pointer-events: none;
}
/**********
* Scatter
*/
.nvd3 .nv-groups .nv-point {
pointer-events: none;
}
.nvd3 .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .5;
}
.nvd3 .nv-scatter .nv-point.hover {
fill-opacity: 1;
}
/*
.nv-group.hover .nv-point {
fill-opacity: 1;
}
*/
/**********
* Stacked Area
*/
.nvd3.nv-stackedarea path.nv-area {
fill-opacity: .7;
/*
stroke-opacity: .65;
fill-opacity: 1;
*/
stroke-opacity: 0;
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
/*
transition-delay: 500ms;
-moz-transition-delay: 500ms;
-webkit-transition-delay: 500ms;
*/
}
.nvd3.nv-stackedarea path.nv-area.hover {
fill-opacity: .9;
/*
stroke-opacity: .85;
*/
}
/*
.d3stackedarea .groups path {
stroke-opacity: 0;
}
*/
.nvd3.nv-stackedarea .nv-groups .nv-point {
stroke-opacity: 0;
fill-opacity: 0;
}
.nvd3.nv-stackedarea .nv-groups .nv-point.hover {
stroke-width: 20px;
stroke-opacity: .75;
fill-opacity: 1;
}
/**********
* Line Plus Bar
*/
.nvd3.nv-linePlusBar .nv-bar rect {
fill-opacity: .75;
}
.nvd3.nv-linePlusBar .nv-bar rect:hover {
fill-opacity: 1;
}
/**********
* Bullet
*/
.nvd3.nv-bullet { font: 10px sans-serif; }
.nvd3.nv-bullet rect { fill-opacity: .6; }
.nvd3.nv-bullet rect:hover { fill-opacity: 1; }
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
/**********
* Sparkline
*/
.nvd3.nv-sparkline path {
fill: none;
}
.nvd3.nv-sparklineplus g.nv-hoverValue {
pointer-events: none;
}
.nvd3.nv-sparklineplus .nv-hoverValue line {
stroke: #f44;
stroke-width: 1.5px;
}
.nvd3.nv-sparklineplus,
.nvd3.nv-sparklineplus g {
pointer-events: all;
}
.nvd3 .nv-hoverArea {
fill-opacity: 0;
stroke-opacity: 0;
}
.nvd3.nv-sparklineplus .nv-xValue,
.nvd3.nv-sparklineplus .nv-yValue {
/*
stroke: #666;
*/
stroke-width: 0;
font-size: .8em;
font-weight: normal;
}
.nvd3.nv-sparklineplus .nv-yValue {
stroke: #f66;
}
.nvd3.nv-sparklineplus .nv-maxValue {
stroke: #2ca02c;
fill: #2ca02c;
}
.nvd3.nv-sparklineplus .nv-minValue {
stroke: #d62728;
fill: #d62728;
}
.nvd3.nv-sparklineplus .nv-currentValue {
stroke: #444;
fill: #444;
}
/**********
* historical stock
*/
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
stroke-width: 2px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
stroke-width: 4px;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
stroke: #2ca02c;
}
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
stroke: #d62728;
}
.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel {
font-weight: bold;
}
.nvd3.nv-historicalStockChart .nv-dragTarget {
fill-opacity: 0;
stroke: none;
cursor: move;
}
.nvd3 .nv-brush .extent {
/*
cursor: ew-resize !important;
*/
fill-opacity: 0 !important;
}
.nvd3 .nv-brushBackground rect {
stroke: #000;
stroke-width: .4;
fill: #fff;
fill-opacity: .7;
}
/**********
* Indented Tree
*/
/**
* TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library
*/
.nvd3.nv-indentedtree .name {
margin-left: 5px;
}
.nvd3.nv-indentedtree .clickable {
color: #08C;
cursor: pointer;
}
.nvd3.nv-indentedtree span.clickable:hover {
color: #005580;
text-decoration: underline;
}
.nvd3.nv-indentedtree .nv-childrenCount {
display: inline-block;
margin-left: 5px;
}
.nvd3.nv-indentedtree .nv-treeicon {
cursor: pointer;
/*
cursor: n-resize;
*/
}
.nvd3.nv-indentedtree .nv-treeicon.nv-folded {
cursor: pointer;
/*
cursor: s-resize;
*/
}

7033
layerindex/static/js/d3.v2.js vendored Normal file

File diff suppressed because it is too large Load Diff

10563
layerindex/static/js/nv.d3.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,207 @@
#!/usr/bin/env python
# Import OE-Classic recipe data into the layer index database
#
# Copyright (C) 2013 Intel Corporation
# Author: Paul Eggleton <paul.eggleton@linux.intel.com>
#
# Licensed under the MIT license, see COPYING.MIT for details
import sys
import os.path
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
import optparse
import logging
from datetime import datetime
import fnmatch
import re
import tempfile
import shutil
from distutils.version import LooseVersion
import utils
import recipeparse
logger = utils.logger_create('LayerIndexUpdate')
def update_recipe_file(data, path, recipe, layerdir_start, repodir):
fn = str(os.path.join(path, recipe.filename))
try:
logger.debug('Updating recipe %s' % fn)
envdata = bb.cache.Cache.loadDataFull(fn, [], data)
envdata.setVar('SRCPV', 'X')
envdata.setVar('SRCDATE', 'X')
envdata.setVar('SRCREV', 'X')
envdata.setVar('OPIE_SRCREV', 'X')
recipe.pn = envdata.getVar("PN", True)
recipe.pv = envdata.getVar("PV", True)
recipe.summary = envdata.getVar("SUMMARY", True)
recipe.description = envdata.getVar("DESCRIPTION", True)
recipe.section = envdata.getVar("SECTION", True)
recipe.license = envdata.getVar("LICENSE", True)
recipe.homepage = envdata.getVar("HOMEPAGE", True)
recipe.provides = envdata.getVar("PROVIDES", True) or ""
recipe.bbclassextend = envdata.getVar("BBCLASSEXTEND", True) or ""
recipe.save()
except KeyboardInterrupt:
raise
except BaseException as e:
if not recipe.pn:
recipe.pn = recipe.filename[:-3].split('_')[0]
logger.error("Unable to read %s: %s", fn, str(e))
def main():
parser = optparse.OptionParser(
usage = """
%prog [options] <bitbakepath> <oeclassicpath>""")
parser.add_option("-b", "--branch",
help = "Specify branch to import into",
action="store", dest="branch", default='oe-classic')
parser.add_option("-l", "--layer",
help = "Specify layer to import into",
action="store", dest="layer", default='oe-classic')
parser.add_option("-n", "--dry-run",
help = "Don't write any data back to the database",
action="store_true", dest="dryrun")
parser.add_option("-d", "--debug",
help = "Enable debug output",
action="store_const", const=logging.DEBUG, dest="loglevel", default=logging.INFO)
parser.add_option("-q", "--quiet",
help = "Hide all output except error messages",
action="store_const", const=logging.ERROR, dest="loglevel")
options, args = parser.parse_args(sys.argv)
if len(args) < 3:
logger.error('You must specify bitbakepath and oeclassicpath')
parser.print_help()
sys.exit(1)
if len(args) > 3:
logger.error('unexpected argument "%s"' % args[3])
parser.print_help()
sys.exit(1)
utils.setup_django()
import settings
from layerindex.models import LayerItem, LayerBranch, Recipe, ClassicRecipe, Machine, BBAppend, BBClass
from django.db import transaction
logger.setLevel(options.loglevel)
branch = utils.get_branch(options.branch)
if not branch:
logger.error("Specified branch %s is not valid" % options.branch)
sys.exit(1)
res = list(LayerItem.objects.filter(name=options.layer)[:1])
if res:
layer = res[0]
else:
layer = LayerItem()
layer.name = options.layer
layer.status = 'P'
layer.layer_type = 'M'
layer.summary = 'OE-Classic'
layer.description = 'OpenEmbedded-Classic'
layer.vcs_url = 'git://git.openembedded.org/openembedded'
layer.vcs_web_url = 'http://cgit.openembedded.org/cgit.cgi/openembedded'
layer.vcs_web_tree_base_url = 'http://cgit.openembedded.org/cgit.cgi/openembedded/tree/%path%'
layer.vcs_web_file_base_url = 'http://cgit.openembedded.org/cgit.cgi/openembedded/tree/%path%'
layer.classic = True
layer.save()
layerbranch = layer.get_layerbranch(options.branch)
if not layerbranch:
# LayerBranch doesn't exist for this branch, create it
layerbranch = LayerBranch()
layerbranch.layer = layer
layerbranch.branch = branch
layerbranch.save()
fetchdir = settings.LAYER_FETCH_DIR
if not fetchdir:
logger.error("Please set LAYER_FETCH_DIR in settings.py")
sys.exit(1)
if not os.path.exists(fetchdir):
os.makedirs(fetchdir)
fetchedrepos = []
failedrepos = []
bitbakepath = args[1]
oeclassicpath = args[2]
confparentdir = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../oe-classic'))
os.environ['BBPATH'] = str("%s:%s" % (confparentdir, oeclassicpath))
try:
(tinfoil, tempdir) = recipeparse.init_parser(settings, branch, bitbakepath, nocheckout=True, classic=True)
except recipeparse.RecipeParseError as e:
logger.error(str(e))
sys.exit(1)
# Clear the default value of SUMMARY so that we can use DESCRIPTION instead if it hasn't been set
tinfoil.config_data.setVar('SUMMARY', '')
# Clear the default value of DESCRIPTION so that we can see where it's not set
tinfoil.config_data.setVar('DESCRIPTION', '')
# Clear the default value of HOMEPAGE ('unknown')
tinfoil.config_data.setVar('HOMEPAGE', '')
transaction.enter_transaction_management()
transaction.managed(True)
try:
layerdir_start = os.path.normpath(oeclassicpath) + os.sep
layerrecipes = Recipe.objects.filter(layerbranch=layerbranch)
layermachines = Machine.objects.filter(layerbranch=layerbranch)
layerappends = BBAppend.objects.filter(layerbranch=layerbranch)
layerclasses = BBClass.objects.filter(layerbranch=layerbranch)
try:
config_data_copy = recipeparse.setup_layer(tinfoil.config_data, fetchdir, oeclassicpath, layer, layerbranch)
except recipeparse.RecipeParseError as e:
logger.error(str(e))
transaction.rollback()
sys.exit(1)
layerrecipes.delete()
layermachines.delete()
layerappends.delete()
layerclasses.delete()
for root, dirs, files in os.walk(oeclassicpath):
if '.git' in dirs:
dirs.remove('.git')
for f in files:
fullpath = os.path.join(root, f)
(typename, filepath, filename) = recipeparse.detect_file_type(fullpath, layerdir_start)
if typename == 'recipe':
recipe = ClassicRecipe()
recipe.layerbranch = layerbranch
recipe.filename = filename
recipe.filepath = filepath
update_recipe_file(config_data_copy, root, recipe, layerdir_start, oeclassicpath)
recipe.save()
layerbranch.vcs_last_fetch = datetime.now()
layerbranch.save()
if options.dryrun:
transaction.rollback()
else:
transaction.commit()
except:
import traceback
traceback.print_exc()
transaction.rollback()
finally:
transaction.leave_transaction_management()
shutil.rmtree(tempdir)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,203 @@
#!/usr/bin/env python
# Import migration info from OE-Classic recipes wiki page into OE
# layer index database
#
# Copyright (C) 2013 Intel Corporation
# Author: Paul Eggleton <paul.eggleton@linux.intel.com>
#
# Licensed under the MIT license, see COPYING.MIT for details
import sys
import os.path
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
import optparse
import re
import utils
import logging
logger = utils.logger_create('LayerIndexImport')
def read_page(site, path):
ret = {}
import httplib
conn = httplib.HTTPConnection(site)
conn.request("GET", path)
resp = conn.getresponse()
if resp.status in [200, 302]:
data = resp.read()
in_table = False
for line in data.splitlines():
if line.startswith('{|'):
in_table = True
continue
if in_table:
if line.startswith('|}'):
# We're done
in_table = False
continue
elif line.startswith('!'):
pass
elif not line.startswith('|-'):
if line.startswith("|| ''"):
continue
fields = line.split('||')
pn = fields[0].strip('|[]').split()[1]
comment = fields[1]
if comment:
ret[pn] = comment
else:
logger.error('Fetch failed: %d: %s' % (resp.status, resp.reason))
return ret
def main():
parser = optparse.OptionParser(
usage = """
%prog [options]""")
parser.add_option("-b", "--branch",
help = "Specify branch to import into",
action="store", dest="branch", default='oe-classic')
parser.add_option("-l", "--layer",
help = "Specify layer to import into",
action="store", dest="layer", default='oe-classic')
parser.add_option("-n", "--dry-run",
help = "Don't write any data back to the database",
action="store_true", dest="dryrun")
parser.add_option("-d", "--debug",
help = "Enable debug output",
action="store_const", const=logging.DEBUG, dest="loglevel", default=logging.INFO)
parser.add_option("-q", "--quiet",
help = "Hide all output except error messages",
action="store_const", const=logging.ERROR, dest="loglevel")
options, args = parser.parse_args(sys.argv)
utils.setup_django()
from layerindex.models import LayerItem, LayerBranch, Recipe, ClassicRecipe
from django.db import transaction
logger.setLevel(options.loglevel)
res = list(LayerItem.objects.filter(name=options.layer)[:1])
if res:
layer = res[0]
else:
logger.error('Specified layer %s does not exist in database' % options.layer)
sys.exit(1)
layerbranch = layer.get_layerbranch(options.branch)
if not layerbranch:
logger.error("Specified branch %s does not exist in database" % options.branch)
sys.exit(1)
recipes_ai = read_page("www.openembedded.org", "/wiki/OE-Classic_Recipes_A-I?action=raw")
recipes_jz = read_page("www.openembedded.org", "/wiki/OE-Classic_Recipes_J-Z?action=raw")
transaction.enter_transaction_management()
transaction.managed(True)
try:
recipes = dict(list(recipes_ai.items()) + list(recipes_jz.items()))
for pn, comment in recipes.items():
newpn = ''
newlayer = ''
status = 'U'
comment = comment.strip(' -')
if 'provided by' in comment:
res = re.match(r'[a-zA-Z- ]*provided by ([a-zA-Z0-9-]*) in ([a-zA-Z0-9-]*)(.*)', comment)
if res:
newpn = res.group(1)
newlayer = res.group(2)
comment = res.group(3)
if pn.endswith('-native') or pn.endswith('-cross'):
status = 'P'
else:
status = 'R'
elif 'replaced by' in comment or 'renamed to' in comment or ' is in ' in comment:
res = re.match(r'.*replaced by ([a-zA-Z0-9-.]*) in ([a-zA-Z0-9-]*)(.*)', comment)
if not res:
res = re.match(r'.*renamed to ([a-zA-Z0-9-.]*) in ([a-zA-Z0-9-]*)(.*)', comment)
if not res:
res = re.match(r'([a-zA-Z0-9-.]*) is in ([a-zA-Z0-9-]*)(.*)', comment)
if res:
newpn = res.group(1)
newlayer = res.group(2)
comment = res.group(3)
status = 'R'
elif 'obsolete' in comment or 'superseded' in comment:
res = re.match(r'provided by ([a-zA-Z0-9-]*) in ([a-zA-Z0-9-]*)(.*)', comment)
if res:
newpn = res.group(1)
newlayer = res.group(2)
comment = res.group(3)
elif comment.startswith('superseded by'):
comment = comment[14:]
elif comment.startswith('obsolete'):
comment = comment[9:]
status = 'O'
elif 'PACKAGECONFIG' in comment:
res = re.match(r'[a-zA-Z ]* PACKAGECONFIG [a-zA-Z ]* to ([a-zA-Z0-9-]*) in ([a-zA-Z0-9-]*)(.*)', comment)
if res:
newpn = res.group(1)
newlayer = res.group(2)
comment = res.group(3)
status = 'C'
if newlayer:
if newlayer.lower() == 'oe-core':
newlayer = 'openembedded-core'
# Remove all links from comments because they'll be picked up as categories
comment = re.sub(r'\[(http[^[]*)\]', r'\1', comment)
# Split out categories
categories = re.findall(r'\[([^]]*)\]', comment)
for cat in categories:
comment = comment.replace('[%s]' % cat, '')
if '(GPE)' in comment or pn.startswith('gpe'):
categories.append('GPE')
comment = comment.replace('(GPE)', '')
comment = comment.strip('- ')
logger.debug("%s|%s|%s|%s|%s|%s" % (pn, status, newpn, newlayer, categories, comment))
recipequery = ClassicRecipe.objects.filter(layerbranch=layerbranch).filter(pn=pn)
if recipequery:
for recipe in recipequery:
recipe.cover_layerbranch = None
if newlayer:
res = list(LayerItem.objects.filter(name=newlayer)[:1])
if res:
newlayeritem = res[0]
recipe.cover_layerbranch = newlayeritem.get_layerbranch('master')
else:
logger.info('Replacement layer "%s" for %s could not be found' % (newlayer, pn))
recipe.cover_pn = newpn
recipe.cover_status = status
recipe.cover_verified = True
recipe.cover_comment = comment
recipe.classic_category = " ".join(categories)
recipe.save()
else:
logger.info('No OE-Classic recipe with name "%s" count be found' % pn)
sys.exit(1)
if options.dryrun:
transaction.rollback()
else:
transaction.commit()
except:
transaction.rollback()
raise
finally:
transaction.leave_transaction_management()
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,125 @@
#!/usr/bin/env python
# Update migration info in OE-Classic recipes in OE layer index database
#
# Copyright (C) 2013 Intel Corporation
# Author: Paul Eggleton <paul.eggleton@linux.intel.com>
#
# Licensed under the MIT license, see COPYING.MIT for details
import sys
import os.path
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
import optparse
import re
import utils
import logging
logger = utils.logger_create('LayerIndexClassicUpdate')
def main():
parser = optparse.OptionParser(
usage = """
%prog [options]""")
parser.add_option("-b", "--branch",
help = "Specify branch to import into",
action="store", dest="branch", default='oe-classic')
parser.add_option("-l", "--layer",
help = "Specify layer to import into",
action="store", dest="layer", default='oe-classic')
parser.add_option("-n", "--dry-run",
help = "Don't write any data back to the database",
action="store_true", dest="dryrun")
parser.add_option("-d", "--debug",
help = "Enable debug output",
action="store_const", const=logging.DEBUG, dest="loglevel", default=logging.INFO)
parser.add_option("-q", "--quiet",
help = "Hide all output except error messages",
action="store_const", const=logging.ERROR, dest="loglevel")
options, args = parser.parse_args(sys.argv)
utils.setup_django()
from layerindex.models import LayerItem, LayerBranch, Recipe, ClassicRecipe
from django.db import transaction
logger.setLevel(options.loglevel)
res = list(LayerItem.objects.filter(name=options.layer)[:1])
if res:
layer = res[0]
else:
logger.error('Specified layer %s does not exist in database' % options.layer)
sys.exit(1)
layerbranch = layer.get_layerbranch(options.branch)
if not layerbranch:
logger.error("Specified branch %s does not exist in database" % options.branch)
sys.exit(1)
transaction.enter_transaction_management()
transaction.managed(True)
try:
def recipe_pn_query(pn):
return Recipe.objects.filter(layerbranch__branch__name='master').filter(pn=pn).order_by('layerbranch__layer__index_preference')
recipequery = ClassicRecipe.objects.filter(layerbranch=layerbranch).filter(cover_status__in=['U', 'N'])
for recipe in recipequery:
replquery = recipe_pn_query(recipe.pn)
found = False
for replrecipe in replquery:
logger.debug('Matched %s in layer %s' % (recipe.pn, replrecipe.layerbranch.layer.name))
recipe.cover_layerbranch = replrecipe.layerbranch
recipe.cover_status = 'D'
recipe.cover_verified = False
recipe.save()
found = True
break
if not found:
if recipe.pn.endswith('-native') or recipe.pn.endswith('-nativesdk'):
searchpn, _, suffix = recipe.pn.rpartition('-')
replquery = recipe_pn_query(searchpn)
for replrecipe in replquery:
if suffix in replrecipe.bbclassextend.split():
logger.debug('Found BBCLASSEXTEND of %s to cover %s in layer %s' % (replrecipe.pn, recipe.pn, replrecipe.layerbranch.layer.name))
recipe.cover_layerbranch = replrecipe.layerbranch
recipe.cover_pn = replrecipe.pn
recipe.cover_status = 'P'
recipe.cover_verified = False
recipe.save()
found = True
break
if not found and recipe.pn.endswith('-nativesdk'):
searchpn, _, _ = recipe.pn.rpartition('-')
replquery = recipe_pn_query('nativesdk-%s' % searchpn)
for replrecipe in replquery:
logger.debug('Found replacement %s to cover %s in layer %s' % (replrecipe.pn, recipe.pn, replrecipe.layerbranch.layer.name))
recipe.cover_layerbranch = replrecipe.layerbranch
recipe.cover_pn = replrecipe.pn
recipe.cover_status = 'R'
recipe.cover_verified = False
recipe.save()
found = True
break
if options.dryrun:
transaction.rollback()
else:
transaction.commit()
except:
transaction.rollback()
raise
finally:
transaction.leave_transaction_management()
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -13,7 +13,6 @@ import os.path
import optparse
import logging
from datetime import datetime
import fnmatch
import re
import tempfile
import shutil
@ -31,37 +30,6 @@ except ImportError:
sys.exit(1)
machine_conf_re = re.compile(r'conf/machine/([^/.]*).conf$')
bbclass_re = re.compile(r'classes/([^/.]*).bbclass$')
def detect_file_type(path, subdir_start):
typename = None
if fnmatch.fnmatch(path, "*.bb"):
typename = 'recipe'
elif fnmatch.fnmatch(path, "*.bbappend"):
typename = 'bbappend'
else:
# Check if it's a machine conf file
subpath = path[len(subdir_start):]
res = machine_conf_re.match(subpath)
if res:
typename = 'machine'
return (typename, None, res.group(1))
else:
res = bbclass_re.match(subpath)
if res:
typename = 'bbclass'
return (typename, None, res.group(1))
if typename == 'recipe' or typename == 'bbappend':
if subdir_start:
filepath = os.path.relpath(os.path.dirname(path), subdir_start)
else:
filepath = os.path.dirname(path)
return (typename, filepath, os.path.basename(path))
return (None, None, None)
def check_machine_conf(path, subdir_start):
subpath = path[len(subdir_start):]
res = conf_re.match(subpath)
@ -191,12 +159,12 @@ def main():
sys.exit(1)
if options.layers:
layerquery = LayerItem.objects.filter(name__in=options.layers.split(','))
layerquery = LayerItem.objects.filter(classic=False).filter(name__in=options.layers.split(','))
if layerquery.count() == 0:
logger.error('No layers matching specified query "%s"' % options.layers)
sys.exit(1)
else:
layerquery = LayerItem.objects.filter(status='P')
layerquery = LayerItem.objects.filter(classic=False).filter(status='P')
if layerquery.count() == 0:
logger.info("No published layers to update")
sys.exit(1)
@ -379,7 +347,7 @@ def main():
for d in diff.iter_change_type('D'):
path = d.a_blob.path
if path.startswith(subdir_start):
(typename, filepath, filename) = detect_file_type(path, subdir_start)
(typename, filepath, filename) = recipeparse.detect_file_type(path, subdir_start)
if typename == 'recipe':
values = layerrecipes.filter(filepath=filepath).filter(filename=filename).values('id', 'filepath', 'filename', 'pn')
layerrecipes_delete.append(values[0])
@ -395,7 +363,7 @@ def main():
for d in diff.iter_change_type('A'):
path = d.b_blob.path
if path.startswith(subdir_start):
(typename, filepath, filename) = detect_file_type(path, subdir_start)
(typename, filepath, filename) = recipeparse.detect_file_type(path, subdir_start)
if typename == 'recipe':
layerrecipes_add.append(os.path.join(repodir, path))
logger.debug("Mark %s for addition" % path)
@ -422,7 +390,7 @@ def main():
for d in diff.iter_change_type('M'):
path = d.a_blob.path
if path.startswith(subdir_start):
(typename, filepath, filename) = detect_file_type(path, subdir_start)
(typename, filepath, filename) = recipeparse.detect_file_type(path, subdir_start)
if typename == 'recipe':
logger.debug("Mark %s for update" % path)
results = layerrecipes.filter(filepath=filepath).filter(filename=filename)[:1]
@ -472,7 +440,7 @@ def main():
dirs.remove('.git')
for f in files:
fullpath = os.path.join(root, f)
(typename, _, filename) = detect_file_type(fullpath, layerdir_start)
(typename, _, filename) = recipeparse.detect_file_type(fullpath, layerdir_start)
if typename == 'recipe':
if fullpath not in layerrecipe_fns:
layerrecipes_add.append(fullpath)

View File

@ -7,7 +7,7 @@
from django.conf.urls.defaults import *
from django.views.generic import TemplateView, DetailView, ListView
from django.views.defaults import page_not_found
from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, MachineSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, switch_branch_view, HistoryListView, EditProfileFormView, DuplicatesView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView
from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, MachineSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, switch_branch_view, HistoryListView, EditProfileFormView, DuplicatesView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView, ClassicRecipeSearchView, ClassicRecipeDetailView, ClassicRecipeStatsView
from layerindex.models import LayerItem, Recipe, RecipeChangeset
urlpatterns = patterns('',
@ -104,5 +104,20 @@ urlpatterns = patterns('',
TemplateView.as_view(
template_name='layerindex/about.html'),
name="about"),
url(r'^oe-classic/$',
redirect_to, {'url' : reverse_lazy('classic_recipe_search')},
name='classic'),
url(r'^oe-classic/recipes/$',
ClassicRecipeSearchView.as_view(
template_name='layerindex/classicrecipes.html'),
name='classic_recipe_search'),
url(r'^oe-classic/stats/$',
ClassicRecipeStatsView.as_view(
template_name='layerindex/classicstats.html'),
name='classic_recipe_stats'),
url(r'^oe-classic/recipe/(?P<pk>[-\w]+)/$',
ClassicRecipeDetailView.as_view(
template_name='layerindex/classicrecipedetail.html'),
name='classic_recipe'),
url(r'.*', page_not_found)
)

View File

@ -5,15 +5,15 @@
# Licensed under the MIT license, see COPYING.MIT for details
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404
from django.core.urlresolvers import reverse, reverse_lazy
from django.core.exceptions import PermissionDenied
from django.template import RequestContext
from layerindex.models import Branch, LayerItem, LayerMaintainer, LayerBranch, LayerDependency, LayerNote, Recipe, Machine, BBClass, BBAppend, RecipeChange, RecipeChangeset
from layerindex.models import Branch, LayerItem, LayerMaintainer, LayerBranch, LayerDependency, LayerNote, Recipe, Machine, BBClass, BBAppend, RecipeChange, RecipeChangeset, ClassicRecipe
from datetime import datetime
from django.views.generic import TemplateView, DetailView, ListView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from layerindex.forms import EditLayerForm, LayerMaintainerFormSet, EditNoteForm, EditProfileForm, RecipeChangesetForm, AdvancedRecipeSearchForm, BulkChangeEditFormSet
from layerindex.forms import EditLayerForm, LayerMaintainerFormSet, EditNoteForm, EditProfileForm, RecipeChangesetForm, AdvancedRecipeSearchForm, BulkChangeEditFormSet, ClassicRecipeForm, ClassicRecipeSearchForm
from django.db import transaction
from django.contrib.auth.models import User, Permission
from django.db.models import Q, Count
@ -32,6 +32,8 @@ import reversion
def edit_layernote_view(request, template_name, slug, pk=None):
layeritem = get_object_or_404(LayerItem, name=slug)
if layeritem.classic:
raise Http404
if not (request.user.is_authenticated() and (request.user.has_perm('layerindex.publish_layer') or layeritem.user_can_edit(request.user))):
raise PermissionDenied
if pk:
@ -56,6 +58,8 @@ def edit_layernote_view(request, template_name, slug, pk=None):
def delete_layernote_view(request, template_name, slug, pk):
layeritem = get_object_or_404(LayerItem, name=slug)
if layeritem.classic:
raise Http404
if not (request.user.is_authenticated() and (request.user.has_perm('layerindex.publish_layer') or layeritem.user_can_edit(request.user))):
raise PermissionDenied
layernote = get_object_or_404(LayerNote, pk=pk)
@ -71,6 +75,8 @@ def delete_layernote_view(request, template_name, slug, pk):
def delete_layer_view(request, template_name, slug):
layeritem = get_object_or_404(LayerItem, name=slug)
if layeritem.classic:
raise Http404
if not (request.user.is_authenticated() and request.user.has_perm('layerindex.publish_layer') and layeritem.status == 'N'):
raise PermissionDenied
if request.method == 'POST':
@ -89,6 +95,8 @@ def edit_layer_view(request, template_name, slug=None):
# Edit mode
branch = Branch.objects.filter(name=request.session.get('branch', 'master'))[:1].get()
layeritem = get_object_or_404(LayerItem, name=slug)
if layeritem.classic:
raise Http404
if not (request.user.is_authenticated() and (request.user.has_perm('layerindex.publish_layer') or layeritem.user_can_edit(request.user))):
raise PermissionDenied
layerbranch = get_object_or_404(LayerBranch, layer=layeritem, branch=branch)
@ -101,7 +109,7 @@ def edit_layer_view(request, template_name, slug=None):
branch = Branch.objects.filter(name='master')[:1].get()
layeritem = LayerItem()
layerbranch = LayerBranch(layer=layeritem, branch=branch)
deplistlayers = LayerItem.objects.all().order_by('name')
deplistlayers = LayerItem.objects.filter(classic=False).order_by('name')
if request.method == 'POST':
last_vcs_url = layeritem.vcs_url
@ -249,6 +257,8 @@ def publish(request, name):
def _statuschange(request, name, newstatus):
w = get_object_or_404(LayerItem, name=name)
if w.classic:
raise Http404
if w.status != newstatus:
w.change_status(newstatus, request.user.username)
w.save()
@ -287,6 +297,8 @@ class LayerDetailView(DetailView):
res = super(LayerDetailView, self).dispatch(request, *args, **kwargs)
l = self.get_object()
if l:
if l.classic:
raise Http404
if l.status == 'N':
if not (request.user.is_authenticated() and request.user.has_perm('layerindex.publish_layer')):
raise PermissionDenied
@ -297,6 +309,7 @@ class LayerDetailView(DetailView):
layer = context['layeritem']
context['useredit'] = layer.user_can_edit(self.user)
layerbranch = layer.get_layerbranch(self.request.session.get('branch', 'master'))
if layerbranch:
context['layerbranch'] = layerbranch
context['machines'] = layerbranch.machine_set.order_by('name')
context['appends'] = layerbranch.bbappend_set.order_by('filename')
@ -337,6 +350,7 @@ class RecipeSearchView(ListView):
paginate_by = 50
def get_queryset(self):
_check_branch(self.request)
query_string = self.request.GET.get('q', '')
init_qs = Recipe.objects.filter(layerbranch__branch__name=self.request.session.get('branch', 'master'))
if query_string.strip():
@ -607,3 +621,123 @@ class RecipeDetailView(DetailView):
appendprefix = "%s_" % recipe.pn
context['appends'] = BBAppend.objects.filter(layerbranch__branch__name=self.request.session.get('branch', 'master')).filter(filename__startswith=appendprefix)
return context
class ClassicRecipeSearchView(RecipeSearchView):
def get_queryset(self):
self.kwargs['branch'] = 'oe-classic'
query_string = self.request.GET.get('q', '')
cover_status = self.request.GET.get('cover_status', None)
cover_verified = self.request.GET.get('cover_verified', None)
category = self.request.GET.get('category', None)
init_qs = ClassicRecipe.objects.filter(layerbranch__branch__name='oe-classic')
if cover_status:
if cover_status == '!':
init_qs = init_qs.filter(cover_status__in=['U', 'N'])
else:
init_qs = init_qs.filter(cover_status=cover_status)
if cover_verified:
init_qs = init_qs.filter(cover_verified=(cover_verified=='1'))
if category:
init_qs = init_qs.filter(classic_category__icontains=category)
if query_string.strip():
entry_query = simplesearch.get_query(query_string, ['pn', 'summary', 'description', 'filename'])
qs = init_qs.filter(entry_query).order_by('pn', 'layerbranch__layer')
else:
if 'q' in self.request.GET:
qs = init_qs.order_by('pn', 'layerbranch__layer')
else:
# It's a bit too slow to return all records by default, and most people
# won't actually want that (if they do they can just hit the search button
# with no query string)
return Recipe.objects.none()
return qs
def get_context_data(self, **kwargs):
context = super(ClassicRecipeSearchView, self).get_context_data(**kwargs)
context['multi_classic_layers'] = LayerItem.objects.filter(classic=True).count() > 1
if 'q' in self.request.GET:
searched = True
search_form = ClassicRecipeSearchForm(self.request.GET)
else:
searched = False
search_form = ClassicRecipeSearchForm()
context['search_form'] = search_form
context['searched'] = searched
return context
class ClassicRecipeDetailView(UpdateView):
model = ClassicRecipe
form_class = ClassicRecipeForm
context_object_name = 'recipe'
def _can_edit(self):
if self.request.user.is_authenticated():
if not self.request.user.has_perm('layerindex.edit_classic'):
user_email = self.request.user.email.strip().lower()
if not LayerMaintainer.objects.filter(email__iexact=user_email):
return False
else:
return False
return True
def post(self, request, *args, **kwargs):
if not self._can_edit():
raise PermissionDenied
return super(ClassicRecipeDetailView, self).post(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy('classic_recipe_search')
def get_context_data(self, **kwargs):
context = super(ClassicRecipeDetailView, self).get_context_data(**kwargs)
context['can_edit'] = self._can_edit()
return context
class ClassicRecipeStatsView(TemplateView):
def get_context_data(self, **kwargs):
context = super(ClassicRecipeStatsView, self).get_context_data(**kwargs)
# *** Cover status chart ***
statuses = []
status_counts = {}
for choice, desc in ClassicRecipe.COVER_STATUS_CHOICES:
statuses.append(desc)
status_counts[desc] = ClassicRecipe.objects.filter(cover_status=choice).count()
statuses = sorted(statuses, key=lambda status: status_counts[status], reverse=True)
chartdata = {'x': statuses, 'y': [status_counts[k] for k in statuses]}
context['charttype_status'] = 'pieChart'
context['chartdata_status'] = chartdata
# *** Categories chart ***
categories = ['obsoletedir', 'nonworkingdir']
uniquevals = ClassicRecipe.objects.exclude(classic_category='').values_list('classic_category', flat=True).distinct()
for value in uniquevals:
cats = value.split()
for cat in cats:
if not cat in categories:
categories.append(cat)
categories.append('none')
catcounts = dict.fromkeys(categories, 0)
unmigrated = ClassicRecipe.objects.filter(cover_status='U')
catcounts['none'] = unmigrated.filter(classic_category='').count()
values = unmigrated.exclude(classic_category='').values_list('classic_category', flat=True)
# We gather data this way because an item might be in more than one category, thus
# the categories list must be in priority order
for value in values:
recipecats = value.split()
foundcat = 'none'
for cat in categories:
if cat in recipecats:
foundcat = cat
break
catcounts[foundcat] += 1
# Eliminate categories with zero count
categories = [cat for cat in categories if catcounts[cat] > 0]
categories = sorted(categories, key=lambda cat: catcounts[cat], reverse=True)
chartdata_category = {'x': categories, 'y': [catcounts[k] for k in categories]}
context['charttype_category'] = 'pieChart'
context['chartdata_category'] = chartdata_category
return context

View File

@ -0,0 +1,15 @@
#
# This is a version of local.conf trimmed specially for parsing recipes
# within the OE Layer index update script. Since we're not doing any
# actual building and many variables are defaulted via bitbake.conf or
# the "minimal" distro config, we don't need to set very much.
# Just select something basic here:
MACHINE = "qemux86"
# OE-Classic basic distro configuration
DISTRO = "minimal"
# Ensure we select a packaging backend so image recipes parse
INHERIT += "package_ipk"

View File

@ -143,7 +143,8 @@ INSTALLED_APPS = (
'reversion',
'reversion_compare',
'captcha',
'south'
'south',
'django_nvd3'
)
# A sample logging configuration. The only tangible logging

View File

@ -48,6 +48,9 @@
{% if branch.name = current_branch %}</b>{% endif %}
</a></li>
{% endfor %}
{% if oe_classic %}
<li><a href="{% url classic %}">OE-Classic</a></li>
{% endif %}
</ul>
</li>
{% endblock %}

View File

@ -24,11 +24,13 @@
<li><a href="http://github.com/etianen/django-reversion">django-reversion</a></li>
<li><a href="http://pypi.python.org/pypi/django-reversion-compare/">django-reversion-compare</a></li>
<li><a href="http://github.com/mbi/django-simple-captcha">django-simple-captcha</a></li>
<li><a href="http://github.com/areski/django-nvd3">django-nvd3</a></li>
</ul>
</li>
<li><a href="http://twitter.github.com/bootstrap/">Twitter Bootstrap</a></li>
<li><a href="http://glyphicons.com/">Glyphicons</a> (via Bootstrap)</li>
<li><a href="http://jquery.com/">jQuery</a> (via Bootstrap)</li>
<li><a href="http://nvd3.org/">NVD3</a></li>
<li><a href="http://www.openembedded.org/wiki/BitBake_%28user%29">BitBake</a></li>
</ul>

View File

@ -0,0 +1,61 @@
{% extends "base_toplevel.html" %}
{% load i18n %}
{% comment %}
layerindex-web - OE-Classic index page template
Copyright (C) 2013 Intel Corporation
Licensed under the MIT license, see COPYING.MIT for details
{% endcomment %}
<!--
{% block title_append %} - OE-Classic{% endblock %}
-->
{% block branch_selector %}
{% autoescape on %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Branch: <b>OE-Classic</b>
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
{% for branch in all_branches %}
<li><a href="{% url layer_list branch.name %}">
{{ branch.name }}
{% if branch.short_description %}
({{ branch.short_description }})
{% endif %}
</a></li>
{% endfor %}
<li class="divider"></li>
<li><a href="{% url classic %}"><b>OE-Classic</b></a></li>
</ul>
</li>
{% endautoescape %}
{% endblock %}
{% block content_inner %}
{% autoescape on %}
<div class="container-fluid">
<div class="row-fluid">
<h2>OE-Classic</h2>
<p>OpenEmbedded-Classic (OE-Classic) is the name for the old monolithic version of OpenEmbedded. It contained a number of recipes some of which have not yet been migrated on top of OE-Core. To help people to find and migrate these recipes we provide an index here as well as some statistics to get an idea of the migration.</p>
<a class="btn btn-large btn-primary" href="{% url classic_recipe_search %}">Recipes</a>
<a class="btn btn-large" href="{% url classic_recipe_search %}?q=&cover_status=!">Unmigrated Recipes</a>
<a class="btn btn-large btn-primary" href="{% url classic_recipe_stats %}">Stats</a>
</div>
</div>
{% endautoescape %}
{% endblock %}

View File

@ -0,0 +1,165 @@
{% extends "base.html" %}
{% load i18n %}
{% comment %}
layerindex-web - recipe detail page template
Copyright (C) 2013 Intel Corporation
Licensed under the MIT license, see COPYING.MIT for details
{% endcomment %}
<!--
{% autoescape on %}
{% block title_append %} - OE-Classic - {{ recipe.pn }}{% endblock %}
{% endautoescape %}
-->
{% block content %}
{% autoescape on %}
<ul class="breadcrumb">
<li><a href="{% url classic_recipe_search %}">OE-Classic</a> <span class="divider">&rarr;</span></li>
<li class="active">{{ recipe.name }}</li>
</ul>
<div class="container-fluid">
<div class="row-fluid">
<div class="page-header">
<h1>{{ recipe.name }} {{ recipe.pv }}</h1>
</div>
<div class="alert alert-warning">
<b>NOTE:</b> This recipe is for OE-Classic, the older monolithic version of OpenEmbedded which is no longer actively developed. See below for migration information. If no replacement is available in current OpenEmbedded layers, you may be able to <a href="http://www.openembedded.org/wiki/Migrating_metadata_to_OE-Core">migrate the recipe</a> yourself.
</div>
<table class="table table-striped table-bordered">
<tbody>
<tr>
<th>Name</th>
<td>{{ recipe.name }}</td>
</tr>
<tr>
<th>Version</th>
<td>{{ recipe.pv }}</td>
</tr>
<tr>
<th>Summary</th>
<td>{{ recipe.summary }}</td>
</tr>
<tr>
<th>Description</th>
<td>{{ recipe.description }}</td>
</tr>
<tr>
<th>Section</th>
<td>{{ recipe.section }}</td>
</tr>
<tr>
<th>License</th>
<td>{{ recipe.license }}*</td>
</tr>
<tr>
<th>Homepage</th>
<td>{% if recipe.homepage %}<a href="{{ recipe.homepage }}">{{ recipe.homepage }}</a>{% endif %}</td>
</tr>
{% if recipe.bugtracker %}
<tr>
<th>Bug tracker</th>
<td><a href="{{ recipe.bugtracker }}">{{ recipe.bugtracker }}</a></td>
</tr>
{% endif %}
<tr>
<th>Recipe file</th>
<td>
{% if recipe.vcs_web_url %}
<a href="{{ recipe.vcs_web_url }}">{{ recipe.full_path }}</a>
{% else %}
{{ recipe.full_path }}
{% endif %}
</td>
</tr>
</tbody>
</table>
<p>* - in OE-Classic, some of the license values were not accurate. Please refer to current recipes (if available) for this information.</p>
<h2>Migration information</h2>
{% if can_edit %}
<form id="migration_form" class="form-inline" method="post">
{% csrf_token %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
Coverage {{ form.cover_status }} <span id="id_span_cover_opts">by {{ form.cover_pn }} in {{ form.cover_layerbranch }}</span>
<label class="checkbox" id="id_label_cover_verified">
{{ form.cover_verified }} verified
</label>
<p>
<label>
Comment
{{ form.cover_comment }}
</label>
</p>
<p>
<label>
Category
{{ form.classic_category }}
</p>
</label>
<input type="submit" value="Save" class='btn' />
</form>
{% else %}
<table class="table table-striped table-bordered">
<tbody>
<tr>
<th class="span2">Coverage</th>
<td>{{ recipe.get_cover_desc }}</td>
</tr>
<tr>
<th>Categories</th>
<td>{{ recipe.classic_category }}</td>
</tr>
</tbody>
</table>
{% endif %}
</div>
</div>
{% endautoescape %}
{% endblock %}
{% block scripts %}
<script>
enable_value_field = function() {
cover_status = $('#id_cover_status').val()
if( cover_status == 'U' || cover_status == 'N' ) {
$('#id_cover_pn').prop('disabled', true);
$('#id_cover_layerbranch').prop('disabled', true);
$('#id_cover_verified').prop('disabled', true);
$('#id_label_cover_verified').addClass('muted');
$('#id_span_cover_opts').addClass('muted');
}
else {
$('#id_cover_pn').prop('disabled', false);
$('#id_cover_layerbranch').prop('disabled', false);
$('#id_cover_verified').prop('disabled', false);
$('#id_label_cover_verified').removeClass('muted');
$('#id_span_cover_opts').removeClass('muted');
}
}
$(document).ready(function() {
$('#id_cover_status').change(enable_value_field)
enable_value_field()
});
</script>
{% endblock %}

View File

@ -0,0 +1,115 @@
{% extends "layerindex/classic_base.html" %}
{% load i18n %}
{% comment %}
layerindex-web - OE-Classic recipe search page template
Copyright (C) 2013 Intel Corporation
Licensed under the MIT license, see COPYING.MIT for details
{% endcomment %}
<!--
{% block title_append %} - OE-Classic recipes{% endblock %}
-->
{% block navs %}
{% autoescape on %}
<li class="active"><a href="{% url classic_recipe_search %}">Recipes</a></li>
<li><a href="{% url classic_recipe_stats %}">Stats</a></li>
{% endautoescape %}
{% endblock %}
{% block content_inner %}
{% autoescape on %}
<div class="row-fluid">
<div class="span12">
<h2>OE-Classic recipes</h2>
<div class="alert alert-warning">
<b>NOTE:</b> This is the recipe search for OE-Classic, the older monolithic version of OpenEmbedded which is no longer actively developed. <a href="{% url recipe_search 'master' %}">Click here</a> to search current recipes.
</div>
<div class="row-fluid">
<form id="search-form" class="form-inline" method="GET">
<table class="search-form-table">
<tbody>
{% for field in search_form.visible_fields %}
<tr>
<td>
{{ field.label }}
</td>
<td>
{{ field }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<button class="btn" type="submit">Search</button>
</form>
</div>
{% if recipe_list %}
<table class="table table-striped table-bordered recipestable">
<thead>
<tr>
<th>Recipe name</th>
<th>Version</th>
<th class="span7">Description</th>
<th class="span5">Status</th>
<th>Categories</th>
{% if multi_classic_layers %}
<th>Layer</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for recipe in recipe_list %}
<tr {% if recipe.preferred_count > 0 %}class="muted"{% endif %}>
<td><a href="{% url classic_recipe recipe.id %}">{{ recipe.name }}</a></td>
<td>{{ recipe.pv }}</td>
<td>{{ recipe.short_desc }}</td>
<td>{{ recipe.get_cover_desc }}</td>
<td>{{ recipe.classic_category }}</td>
{% if multi_classic_layers %}
<td><a href="{% url layer_item recipe.layerbranch.layer.name %}">{{ recipe.layerbranch.layer.name }}</a></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% if is_paginated %}
{% load pagination %}
{% pagination page_obj %}
{% endif %}
{% else %}
{% if searched %}
<p>No matching OE-Classic recipes in database.</p>
{% endif %}
{% endif %}
</div>
</div>
{% endautoescape %}
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function() {
firstfield = $("#search-form input:text").first()
if( ! firstfield.val() )
firstfield.focus()
});
</script>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "layerindex/classic_base.html" %}
{% load i18n %}
{% load nvd3_tags %}
{% load staticfiles %}
{% comment %}
layerindex-web - OE-Classic recipe migration stats template
Copyright (C) 2013 Intel Corporation
Licensed under the MIT license, see COPYING.MIT for details
{% endcomment %}
<!--
{% block title_append %} - OE-Classic recipe statistics{% endblock %}
-->
{% block navs %}
{% autoescape on %}
<li><a href="{% url classic_recipe_search %}">Recipes</a></li>
<li class="active"><a href="{% url classic_recipe_stats %}">Stats</a></li>
{% endautoescape %}
{% endblock %}
{% block content_inner %}
{% autoescape on %}
<div class="row-fluid">
<h2>OE-Classic statistics</h2>
<h3>Migration status</h3>
{% include_container "chart_status" 400 600 %}
<h3>Unmigrated recipes by category</h3>
{% include_container "chart_category" 400 600 %}
</div>
{% endautoescape %}
{% endblock %}
{% block scripts %}
<link media="all" href="{% static "css/nv.d3.css" %}" type="text/css" rel="stylesheet" />
<script src="{% static "js/d3.v2.js" %}" type="text/javascript"></script>
<script src="{% static "js/nv.d3.js" %}" type="text/javascript"></script>
{% load_chart charttype_status chartdata_status "chart_status" %}
{% load_chart charttype_category chartdata_category "chart_category" %}
{% endblock %}