mirror of
git://git.yoctoproject.org/layerindex-web.git
synced 2025-07-19 20:59:01 +02:00
Initial commit of layerindex-web
Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
This commit is contained in:
commit
2eb5f38b21
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.pyc
|
||||||
|
*.db3
|
22
COPYING.MIT
Normal file
22
COPYING.MIT
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
Note: for details on which portions of this application are covered by this
|
||||||
|
license, please see the README file.
|
||||||
|
|
||||||
|
Portions copyright (C) 2013 Intel Corporation.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
93
README
Normal file
93
README
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
OE Layer Index web interface
|
||||||
|
============================
|
||||||
|
|
||||||
|
This is a small Django-based web application that provides a way to
|
||||||
|
manage an index of OpenEmbedded metadata layers for use on top of
|
||||||
|
OE-Core.
|
||||||
|
|
||||||
|
|
||||||
|
Setup
|
||||||
|
-----
|
||||||
|
|
||||||
|
In order to make use of this application you will need:
|
||||||
|
|
||||||
|
* A web server set up to host Django applications
|
||||||
|
* A database supported by Django (SQLite, MySQL, etc.). Django takes
|
||||||
|
care of creating the database itself, you just need to ensure that the
|
||||||
|
database server (if not using SQLite) is configured and running.
|
||||||
|
* On the machine that will run the update script (which does not have to
|
||||||
|
be the same machine as the web server, however it does still have to
|
||||||
|
have Django installed and have access to the database used by the web
|
||||||
|
application):
|
||||||
|
* Python 2.6 or Python 2.7
|
||||||
|
* A copy of BitBake and OE-Core (or Poky, which includes both) -
|
||||||
|
the "danny" release or newer is required. It does not need to be
|
||||||
|
configured specially nor do all of the normal pre-requisites need to
|
||||||
|
be installed (it is only used for parsing recipes, not actual
|
||||||
|
building).
|
||||||
|
* GitPython (python-git) version 0.3.1 or later
|
||||||
|
* django-registration
|
||||||
|
|
||||||
|
Setup instructions:
|
||||||
|
|
||||||
|
1. Edit settings.py to specify a database, EMAIL_HOST and other settings
|
||||||
|
specific to your installation. Ensure you set LAYER_FETCH_DIR to a
|
||||||
|
location with sufficient space for fetching layer repositories.
|
||||||
|
|
||||||
|
2. Run the following command within the layerindex-web directory to
|
||||||
|
initialise the database:
|
||||||
|
|
||||||
|
python manage.py syncdb
|
||||||
|
|
||||||
|
3. You can test the web application locally by running the following:
|
||||||
|
|
||||||
|
python manage.py runserver
|
||||||
|
|
||||||
|
Then visit http://127.0.0.1:8000/layerindex/ with your browser. This
|
||||||
|
should only be used for testing - for production you need to use a
|
||||||
|
proper web server.
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
On a regular basis you need to run the update script within an
|
||||||
|
environment set up for OE-Core build:
|
||||||
|
|
||||||
|
$ cd path/to/oe-core
|
||||||
|
$ . ./oe-init-build-env
|
||||||
|
$ path/to/layerindex/update.py
|
||||||
|
|
||||||
|
This will fetch all of the layer repositories, analyse their contents
|
||||||
|
and update the database with the results.
|
||||||
|
|
||||||
|
|
||||||
|
Maintenance
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The code for this application is maintained by the Yocto Project.
|
||||||
|
|
||||||
|
The latest version of the code can always be found here:
|
||||||
|
|
||||||
|
http://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/
|
||||||
|
|
||||||
|
Contributions are welcome. Please send patches / pull requests to
|
||||||
|
yocto@yoctoproject.org with '[layerindex-web]' in the subject.
|
||||||
|
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
This application is based upon the Django project template, whose files
|
||||||
|
are covered by the BSD license and are copyright (c) Django Software
|
||||||
|
Foundation and individual contributors.
|
||||||
|
|
||||||
|
Bundled Twitter Bootstrap is redistributed under the Apache License 2.0.
|
||||||
|
|
||||||
|
Bundled jQuery is redistributed under the MIT license.
|
||||||
|
|
||||||
|
Bundled uitablefilter.js is redistributed under the MIT license.
|
||||||
|
|
||||||
|
All other content is copyright (C) 2013 Intel Corporation and licensed
|
||||||
|
under the MIT license - see COPYING.MIT for details.
|
||||||
|
|
33
TODO
Normal file
33
TODO
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
TODO:
|
||||||
|
|
||||||
|
- change the behaviour of the filters menus: they should not close on select (they should stay open so that you can check / uncheck multiple checkboxes)
|
||||||
|
|
||||||
|
- display a no-results found message when a search does not return any results (all tables)
|
||||||
|
|
||||||
|
- we might consider adding a link to the all layers and all recipes tables from the layer details page
|
||||||
|
|
||||||
|
* Ensure publishing a published layer doesn't do anything
|
||||||
|
* Auditing
|
||||||
|
|
||||||
|
* Need an "About" section descriptibing what the site is for
|
||||||
|
* Need an admin contact in footer
|
||||||
|
* Some columns are a bit crushed
|
||||||
|
* Description is not formatted nicely on detail page
|
||||||
|
* Need to show on detail:
|
||||||
|
* Last commit date
|
||||||
|
* Last update date
|
||||||
|
* Usage links in list page?
|
||||||
|
* Layer submission interface design
|
||||||
|
* Recipe info page
|
||||||
|
* Captcha for layer submission interface?
|
||||||
|
* Touch up publishing interface
|
||||||
|
* Show layer notes records
|
||||||
|
|
||||||
|
Later:
|
||||||
|
* Ability for users to edit existing layers
|
||||||
|
* Something to help with compatibility (although maybe this should just be handled using the existing versioned layer dependencies in layer.conf)
|
||||||
|
* Query backend service? i.e. special URL to query information
|
||||||
|
* Tool for finding/comparing duplicate recipes?
|
||||||
|
* Tool for editing SUMMARY/DESCRIPTION?
|
||||||
|
* Dynamic loading/filtering for recipes list
|
||||||
|
* Collect information on machines added by BSP layers?
|
0
__init__.py
Normal file
0
__init__.py
Normal file
77
base.html
Normal file
77
base.html
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - base template for output pages
|
||||||
|
|
||||||
|
Copyright (C) 2013 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/static/css/bootstrap.css" />
|
||||||
|
<link rel="stylesheet" href="/static/css/bootstrap-responsive.css" />
|
||||||
|
<link rel="stylesheet" href="/static/css/additional.css" />
|
||||||
|
<title>{% block title %}OpenEmbedded metadata index{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block header %}
|
||||||
|
<div class="navbar navbar-fixed-top">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<div class="container">
|
||||||
|
<a class="brand" href="{% url layer_list %}">OpenEmbedded metadata index</a>
|
||||||
|
|
||||||
|
{% if user.is_authenticated and perms.layeritem.publish_layer %}
|
||||||
|
<ul class="nav">
|
||||||
|
<li><a href="{% url layer_list_review %}">{% trans "review" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<ul class="nav pull-right">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li><a href="{% url submit_layer %}">Submit layer</a></li>
|
||||||
|
<li class="divider-vertical"></li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
|
{{ user.username }}
|
||||||
|
<b class="caret"></b>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="{% url auth_logout %}">{% trans "Log out" %}</a></li>
|
||||||
|
<li><a href="{% url auth_password_change %}">{% trans "Change password" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url auth_login %}">{% trans "log in" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div> <!-- end of "container" -->
|
||||||
|
</div> <!-- "end of "navbar-inner" -->
|
||||||
|
</div> <!-- end of "navbar" -->
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div id="content" class="container top-padded">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="footer">
|
||||||
|
{% block footer %}
|
||||||
|
<hr />
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/jquery-1.7.2.js"></script>
|
||||||
|
<script src="/static/js/bootstrap.js"></script>
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
0
layerindex/__init__.py
Normal file
0
layerindex/__init__.py
Normal file
14
layerindex/admin.py
Normal file
14
layerindex/admin.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# layerindex-web - admin interface definitions
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 Intel Corporation
|
||||||
|
#
|
||||||
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
from layerindex.models import *
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
admin.site.register(LayerItem)
|
||||||
|
admin.site.register(LayerMaintainer)
|
||||||
|
admin.site.register(LayerDependency)
|
||||||
|
admin.site.register(LayerNote)
|
||||||
|
admin.site.register(Recipe)
|
227
layerindex/detail.html
Normal file
227
layerindex/detail.html
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - layer detail page template
|
||||||
|
|
||||||
|
Copyright (C) 2013 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title %}OpenEmbedded metadata index - {{ layeritem.name }}{% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span9 offset1">
|
||||||
|
<h1 style="margin-bottom: 1em;">{{ layeritem.name }}</h1>
|
||||||
|
</div> <!-- end of span9 -->
|
||||||
|
</div> <!-- end of row-fluid -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row-fluid offset1">
|
||||||
|
<div class="span3">
|
||||||
|
<div class="accordion" id="accordion2">
|
||||||
|
|
||||||
|
<div class="accordion-group">
|
||||||
|
<div class="accordion-heading">
|
||||||
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseOne">
|
||||||
|
Description
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="collapseOne" class="accordion-body collapse in">
|
||||||
|
<div class="accordion-inner">
|
||||||
|
<p>
|
||||||
|
{{ layeritem.description }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% if layeritem.usage_url %}
|
||||||
|
<span class="label label-info">
|
||||||
|
<a href="{{ layeritem.usage_url }}">Setup information</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if layeritem.dependencies_set.all %}
|
||||||
|
<div class="accordion-group">
|
||||||
|
|
||||||
|
<div class="accordion-heading">
|
||||||
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseTwo">
|
||||||
|
Dependencies
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="collapseTwo" class="accordion-body collapse">
|
||||||
|
<div class="accordion-inner">
|
||||||
|
<p>The {{ layeritem.name }} layer depends upon:</p>
|
||||||
|
<ul>
|
||||||
|
{% for dep in layeritem.dependencies_set.all %}
|
||||||
|
<li><a href="{% url layer_item dep.dependency.name %}">{{ dep.dependency.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="accordion-group">
|
||||||
|
|
||||||
|
<div class="accordion-heading">
|
||||||
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseThree">
|
||||||
|
Repository
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="collapseThree" class="accordion-body collapse">
|
||||||
|
<div class="accordion-inner">
|
||||||
|
<h4>Git repository</h4>
|
||||||
|
<p>{{ layeritem.vcs_url }}</p>
|
||||||
|
{% if layeritem.vcs_subdir %}
|
||||||
|
<h4>Subdirectory</h4>
|
||||||
|
<p>{{ layeritem.vcs_subdir }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
{% if layeritem.vcs_web_url %}
|
||||||
|
<span class="label label-info">
|
||||||
|
<a href="{{ layeritem.vcs_web_url }}">web repo</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if layeritem.tree_url %}
|
||||||
|
<span class="label label-info">
|
||||||
|
<a href="{{ layeritem.tree_url }}">tree</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-group">
|
||||||
|
|
||||||
|
<div class="accordion-heading">
|
||||||
|
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseFour">
|
||||||
|
Maintainer information
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="collapseFour" class="accordion-body collapse">
|
||||||
|
<div class="accordion-inner">
|
||||||
|
<dl>
|
||||||
|
{% for maintainer in layeritem.active_maintainers %}
|
||||||
|
<dt class="showRollie">
|
||||||
|
{{ maintainer.name }}
|
||||||
|
<a class="rollie" href="mailto:{{ maintainer.email }}">
|
||||||
|
<span class="label label-info">
|
||||||
|
<i class="icon-envelope icon-white"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</dt>
|
||||||
|
{% if maintainer.responsibility %}
|
||||||
|
<dd>- {{ maintainer.responsibility }}</dd>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div> <!-- end of accordion2 -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="span6">
|
||||||
|
|
||||||
|
<div class="navbar">
|
||||||
|
|
||||||
|
<div class="navbar-inner">
|
||||||
|
|
||||||
|
<!-- a class="btn btn-big pull-left back-button" href="index.html"><i class="icon-chevron-left"></i></a -->
|
||||||
|
|
||||||
|
<a class="brand pull-left">{{ layeritem.name }} recipes</a>
|
||||||
|
|
||||||
|
<ul class="nav pull-right">
|
||||||
|
<li>
|
||||||
|
<form action="" class="navbar-search pull-right" id="filter-form">
|
||||||
|
<input type="text" placeholder="Search recipes" class="search-query" id="filter">
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered recipestable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Recipe name</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for recipe in layeritem.sorted_recipes %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ recipe.vcs_web_url }}">{{ recipe.pn }}</a></td>
|
||||||
|
<td>{{ recipe.pv }}</td>
|
||||||
|
<td>{{ recipe.short_desc }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="well">
|
||||||
|
{% if layeritem.status = "N" %}
|
||||||
|
<a href="{% url publish layeritem.name %}" class="btn">Publish</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="/static/js/uitablefilter.js" ></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
var theTable = $('table.recipestable');
|
||||||
|
|
||||||
|
$("#filter").keyup(function() {
|
||||||
|
$.uiTableFilter( theTable, this.value );
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#filter-form').submit(function(){
|
||||||
|
theTable.find("tbody > tr:visible > td:eq(1)").mousedown();
|
||||||
|
return false;
|
||||||
|
}).focus(); //Give focus to input field
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
61
layerindex/forms.py
Normal file
61
layerindex/forms.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# layerindex-web - form definitions
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 Intel Corporation
|
||||||
|
#
|
||||||
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
from layerindex.models import LayerItem
|
||||||
|
from django import forms
|
||||||
|
from django.core.validators import URLValidator, RegexValidator, email_re
|
||||||
|
import re
|
||||||
|
|
||||||
|
class SubmitLayerForm(forms.ModelForm):
|
||||||
|
# Additional form fields
|
||||||
|
maintainers = forms.CharField(max_length=200)
|
||||||
|
deps = forms.ModelMultipleChoiceField(label='Other layers this layer depends upon', queryset=LayerItem.objects.all(), required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = LayerItem
|
||||||
|
fields = ('name', 'layer_type', 'summary', 'description', 'vcs_url', 'vcs_subdir', 'vcs_web_url', 'vcs_web_tree_base_url', 'usage_url')
|
||||||
|
|
||||||
|
def clean_name(self):
|
||||||
|
name = self.cleaned_data['name'].strip()
|
||||||
|
if re.compile(r'[^a-z0-9-]').search(name):
|
||||||
|
raise forms.ValidationError("Name must only contain alphanumeric characters and dashes")
|
||||||
|
if name.startswith('-'):
|
||||||
|
raise forms.ValidationError("Name must not start with a dash")
|
||||||
|
if name.endswith('-'):
|
||||||
|
raise forms.ValidationError("Name must not end with a dash")
|
||||||
|
if '--' in name:
|
||||||
|
raise forms.ValidationError("Name cannot contain consecutive dashes")
|
||||||
|
return name
|
||||||
|
|
||||||
|
def clean_vcs_url(self):
|
||||||
|
url = self.cleaned_data['vcs_url'].strip()
|
||||||
|
val = RegexValidator(regex=r'[a-z]+://.*', message='Please enter a valid repository URL, e.g. git://server.name/path')
|
||||||
|
val(url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def clean_vcs_web_tree_base_url(self):
|
||||||
|
url = self.cleaned_data['vcs_web_tree_base_url'].strip()
|
||||||
|
val = URLValidator(verify_exists=False)
|
||||||
|
val(url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def clean_maintainers(self):
|
||||||
|
maintainers = self.cleaned_data['maintainers'].strip()
|
||||||
|
addrs = re.split(r'"?([^"@$<>]+)"? *<([^<> ]+)>,? *', maintainers)
|
||||||
|
addrs = [addr.strip() for addr in addrs if addr]
|
||||||
|
if addrs and len(addrs) % 2 == 0:
|
||||||
|
addrdict = {}
|
||||||
|
reg = re.compile(email_re)
|
||||||
|
for i in range(0, len(addrs)-1,2):
|
||||||
|
email = addrs[i+1]
|
||||||
|
if not reg.match(email):
|
||||||
|
raise forms.ValidationError('%s is not a valid email address' % email)
|
||||||
|
addrdict[addrs[i]] = email
|
||||||
|
maintainers = addrdict
|
||||||
|
else:
|
||||||
|
raise forms.ValidationError('Please enter one or more maintainers in email address format (i.e. "Full Name <emailaddress@example.com> separated by commas")')
|
||||||
|
|
||||||
|
return maintainers
|
167
layerindex/index.html
Normal file
167
layerindex/index.html
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - main layer index page template
|
||||||
|
|
||||||
|
Copyright (C) 2013 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title %}OpenEmbedded metadata index - layers{% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
{% if layer_list %}
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span9 offset1">
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="active">
|
||||||
|
<a href="{% url layer_list %}">Layer index</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="{% url recipe_search %}">Recipe index</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span5">
|
||||||
|
<form id="filter-form">
|
||||||
|
<input type="text" class="input-xxlarge" id="filter" placeholder="Search layers">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pull-right">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
Filter layers
|
||||||
|
<span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu">
|
||||||
|
{% for choice_id, choice_label in layer_type_choices %}
|
||||||
|
<li><a tabindex="-1" href="#">
|
||||||
|
<label class="checkbox">
|
||||||
|
<input type="checkbox" checked value="{{ choice_id }}">{{ choice_label }}
|
||||||
|
</label>
|
||||||
|
</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-striped table-bordered layerstable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Layer name</th>
|
||||||
|
<th class="span4">Description</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Repository</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for layeritem in layer_list %}
|
||||||
|
<tr class="layertype_{{ layeritem.layer_type }}">
|
||||||
|
<td><a href="{% url layer_item layeritem.name %}">{{ layeritem.name }}</a></td>
|
||||||
|
<td>{{ layeritem.summary }}</td>
|
||||||
|
<td>{{ layeritem.get_layer_type_display }}</td>
|
||||||
|
<td class="showRollie">
|
||||||
|
{{ layeritem.vcs_url }}
|
||||||
|
{% if layeritem.vcs_web_url %}
|
||||||
|
<a class="rollie" href="{{ layeritem.vcs_web_url }}">
|
||||||
|
<span class="label label-info">
|
||||||
|
web repo
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if layeritem.tree_url %}
|
||||||
|
<a class="rollie" href="{{ layeritem.tree_url }}">
|
||||||
|
<span class="label label-info">
|
||||||
|
tree
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if is_paginated %}
|
||||||
|
<div class="pagination pagination-centered">
|
||||||
|
<ul>
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li><a href="?page={{ page_obj.previous_page_number }}">prev</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#">prev</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in paginator.page_range %}
|
||||||
|
{% if i == page_obj.number %}
|
||||||
|
<li class="active"><a href="#">{{ page_obj.number }}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="?page={{i}}">{{i}}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li><a href="?page={{ page_obj.next_page_number }}">next</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#">next</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p>No matching layers in database.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="/static/js/uitablefilter.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$("input:checkbox").change();
|
||||||
|
|
||||||
|
$("input:checkbox").change(function(){
|
||||||
|
var selectedType = $(this).val();
|
||||||
|
if($(this).is(":checked")){
|
||||||
|
$(".layertype_"+selectedType).show();
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$(".layertype_"+selectedType).hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
var theTable = $('table.layerstable');
|
||||||
|
|
||||||
|
$("#filter").keyup(function() {
|
||||||
|
$.uiTableFilter( theTable, this.value );
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#filter-form').submit(function(){
|
||||||
|
theTable.find("tbody > tr:visible > td:eq(1)").mousedown();
|
||||||
|
return false;
|
||||||
|
}).focus(); //Give focus to input field
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
122
layerindex/models.py
Normal file
122
layerindex/models.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
# layerindex-web - model definitions
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 Intel Corporation
|
||||||
|
#
|
||||||
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from datetime import datetime
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
class LayerItem(models.Model):
|
||||||
|
LAYER_STATUS_CHOICES = (
|
||||||
|
('N', 'New'),
|
||||||
|
('P', 'Published'),
|
||||||
|
)
|
||||||
|
LAYER_TYPE_CHOICES = (
|
||||||
|
('A', 'Base'),
|
||||||
|
('B', 'BSP'),
|
||||||
|
('S', 'Software'),
|
||||||
|
('D', 'Distribution'),
|
||||||
|
('M', 'Miscellaneous'),
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=40, unique=True)
|
||||||
|
created_date = models.DateTimeField('Created')
|
||||||
|
status = models.CharField(max_length=1, choices=LAYER_STATUS_CHOICES, default='N')
|
||||||
|
layer_type = models.CharField(max_length=1, choices=LAYER_TYPE_CHOICES, default='M')
|
||||||
|
summary = models.CharField(max_length=200)
|
||||||
|
description = models.TextField()
|
||||||
|
vcs_last_fetch = models.DateTimeField('Last successful fetch', blank=True, null=True)
|
||||||
|
vcs_last_rev = models.CharField(max_length=80, blank=True)
|
||||||
|
vcs_last_commit = models.DateTimeField('Last commit date', blank=True, null=True)
|
||||||
|
vcs_subdir = models.CharField('Repository subdirectory', max_length=40, blank=True)
|
||||||
|
vcs_url = models.CharField('Repository URL', max_length=200)
|
||||||
|
vcs_web_url = models.URLField('Repository web interface URL', blank=True)
|
||||||
|
vcs_web_tree_base_url = models.CharField('Repository web interface tree start URL', max_length=200, blank=True)
|
||||||
|
usage_url = models.URLField('Usage web page URL', blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
("publish_layer", "Can publish layers"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def change_status(self, newstatus, username):
|
||||||
|
self.status = newstatus
|
||||||
|
|
||||||
|
def tree_url(self):
|
||||||
|
if self.vcs_subdir and self.vcs_web_tree_base_url:
|
||||||
|
return self.vcs_web_tree_base_url + self.vcs_subdir
|
||||||
|
else:
|
||||||
|
return self.vcs_web_tree_base_url
|
||||||
|
|
||||||
|
def sorted_recipes(self):
|
||||||
|
return self.recipe_set.order_by('filename')
|
||||||
|
|
||||||
|
def active_maintainers(self):
|
||||||
|
return self.layermaintainer_set.filter(status='A')
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class LayerMaintainer(models.Model):
|
||||||
|
MAINTAINER_STATUS_CHOICES = (
|
||||||
|
('A', 'Active'),
|
||||||
|
('I', 'Inactive'),
|
||||||
|
)
|
||||||
|
layer = models.ForeignKey(LayerItem)
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
email = models.CharField(max_length=255)
|
||||||
|
responsibility = models.CharField(max_length=200, blank=True)
|
||||||
|
status = models.CharField(max_length=1, choices=MAINTAINER_STATUS_CHOICES, default='A')
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
respstr = ""
|
||||||
|
if self.responsibility:
|
||||||
|
respstr = " - %s" % self.responsibility
|
||||||
|
return "%s <%s>%s" % (self.name, self.email, respstr)
|
||||||
|
|
||||||
|
|
||||||
|
class LayerDependency(models.Model):
|
||||||
|
layer = models.ForeignKey(LayerItem, related_name='dependencies_set')
|
||||||
|
dependency = models.ForeignKey(LayerItem, related_name='dependents_set')
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return "%s depends on %s" % (self.layer.name, self.dependency.name)
|
||||||
|
|
||||||
|
|
||||||
|
class LayerNote(models.Model):
|
||||||
|
layer = models.ForeignKey(LayerItem)
|
||||||
|
text = models.TextField()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.text
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(models.Model):
|
||||||
|
layer = models.ForeignKey(LayerItem)
|
||||||
|
filename = models.CharField(max_length=255)
|
||||||
|
filepath = models.CharField(max_length=255, blank=True)
|
||||||
|
pn = models.CharField(max_length=40, blank=True)
|
||||||
|
pv = models.CharField(max_length=100, blank=True)
|
||||||
|
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(blank=True)
|
||||||
|
|
||||||
|
def vcs_web_url(self):
|
||||||
|
return os.path.join(self.layer.tree_url(), self.filepath, self.filename)
|
||||||
|
|
||||||
|
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 __unicode__(self):
|
||||||
|
return os.path.join(self.filepath, self.filename)
|
2
layerindex/rawrecipes.txt
Normal file
2
layerindex/rawrecipes.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{% for recipe in recipe_list %}{{ recipe.layer.name }} {{ recipe.pn }} {{ recipe.pv }} {{ recipe.full_path }}
|
||||||
|
{% endfor %}
|
100
layerindex/recipes.html
Normal file
100
layerindex/recipes.html
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - recipe index page template
|
||||||
|
|
||||||
|
Copyright (C) 2013 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title %}OpenEmbedded metadata index - recipes{% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
|
||||||
|
<div class="span9 offset1">
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li>
|
||||||
|
<a href="{% url layer_list %}">Layer index</a>
|
||||||
|
</li>
|
||||||
|
<li class="active"><a href="{% url recipe_search %}">Recipe index</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="input-append">
|
||||||
|
<form id="filter-form" action="{% url recipe_search %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="text" class="input-xxlarge" id="appendedInputButtons" placeholder="Search recipes" name="filter" value="{{ search_keyword }}" />
|
||||||
|
<button class="btn" type="submit">search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if recipe_list %}
|
||||||
|
<table class="table table-striped table-bordered recipestable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Recipe name</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th class="span9">Description</th>
|
||||||
|
<th>Layer</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for recipe in recipe_list %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ recipe.vcs_web_url }}">{{ recipe.pn }}</a></td>
|
||||||
|
<td>{{ recipe.pv }}</td>
|
||||||
|
<td>{{ recipe.short_desc }}</td>
|
||||||
|
<td><a href="{% url layer_item recipe.layer.name %}">{{ recipe.layer.name }}</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if is_paginated %}
|
||||||
|
<div class="pagination pagination-centered">
|
||||||
|
<ul>
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li><a href="?page={{ page_obj.previous_page_number }}">prev</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#">prev</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in paginator.page_range %}
|
||||||
|
{% if i == page_obj.number %}
|
||||||
|
<li class="active"><a href="#">{{ page_obj.number }}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="?page={{i}}">{{i}}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li><a href="?page={{ page_obj.next_page_number }}">next</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="disabled"><a href="#">next</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if search_keyword %}
|
||||||
|
<p>No matching recipes in database.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
51
layerindex/static/css/additional.css
Normal file
51
layerindex/static/css/additional.css
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
.top-padded {
|
||||||
|
padding-top: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width:98%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rollie {
|
||||||
|
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showRollie:hover .rollie{
|
||||||
|
|
||||||
|
visibility:visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
|
||||||
|
margin-right:2em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.label a {
|
||||||
|
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
bs-docs-example:after {
|
||||||
|
background-color: #F5F5F5;
|
||||||
|
border: 1px solid #DDDDDD;
|
||||||
|
border-radius: 4px 0 4px 0;
|
||||||
|
color: #9DA0A4;
|
||||||
|
content: "Recipes";
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
left: -1px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-docs-example {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 1px solid #DDDDDD;
|
||||||
|
border-radius: 4px 4px 4px 4px;
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 39px 19px 14px;
|
||||||
|
position: relative;
|
||||||
|
}
|
1092
layerindex/static/css/bootstrap-responsive.css
vendored
Normal file
1092
layerindex/static/css/bootstrap-responsive.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
layerindex/static/css/bootstrap-responsive.min.css
vendored
Normal file
9
layerindex/static/css/bootstrap-responsive.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6039
layerindex/static/css/bootstrap.css
vendored
Normal file
6039
layerindex/static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
layerindex/static/css/bootstrap.min.css
vendored
Normal file
9
layerindex/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
layerindex/static/img/glyphicons-halflings-white.png
Normal file
BIN
layerindex/static/img/glyphicons-halflings-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
layerindex/static/img/glyphicons-halflings.png
Normal file
BIN
layerindex/static/img/glyphicons-halflings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
2159
layerindex/static/js/bootstrap.js
vendored
Normal file
2159
layerindex/static/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
layerindex/static/js/bootstrap.min.js
vendored
Normal file
6
layerindex/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9404
layerindex/static/js/jquery-1.7.2.js
vendored
Normal file
9404
layerindex/static/js/jquery-1.7.2.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
91
layerindex/static/js/uitablefilter.js
Normal file
91
layerindex/static/js/uitablefilter.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2008 Greg Weber greg at gregweber.info
|
||||||
|
* Dual licensed under the MIT and GPLv2 licenses just as jQuery is:
|
||||||
|
* http://jquery.org/license
|
||||||
|
*
|
||||||
|
* documentation at http://gregweber.info/projects/uitablefilter
|
||||||
|
*
|
||||||
|
* allows table rows to be filtered (made invisible)
|
||||||
|
* <code>
|
||||||
|
* t = $('table')
|
||||||
|
* $.uiTableFilter( t, phrase )
|
||||||
|
* </code>
|
||||||
|
* arguments:
|
||||||
|
* jQuery object containing table rows
|
||||||
|
* phrase to search for
|
||||||
|
* optional arguments:
|
||||||
|
* column to limit search too (the column title in the table header)
|
||||||
|
* ifHidden - callback to execute if one or more elements was hidden
|
||||||
|
*/
|
||||||
|
(function($) {
|
||||||
|
$.uiTableFilter = function(jq, phrase, column, ifHidden){
|
||||||
|
var new_hidden = false;
|
||||||
|
if( this.last_phrase === phrase ) return false;
|
||||||
|
|
||||||
|
var phrase_length = phrase.length;
|
||||||
|
var words = phrase.toLowerCase().split(" ");
|
||||||
|
|
||||||
|
// these function pointers may change
|
||||||
|
var matches = function(elem) { elem.show() }
|
||||||
|
var noMatch = function(elem) { elem.hide(); new_hidden = true }
|
||||||
|
var getText = function(elem) { return elem.text() }
|
||||||
|
|
||||||
|
if( column ) {
|
||||||
|
var index = null;
|
||||||
|
jq.find("thead > tr:last > th").each( function(i){
|
||||||
|
if( $.trim($(this).text()) == column ){
|
||||||
|
index = i; return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if( index == null ) throw("given column: " + column + " not found")
|
||||||
|
|
||||||
|
getText = function(elem){ return $(elem.find(
|
||||||
|
("td:eq(" + index + ")") )).text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if added one letter to last time,
|
||||||
|
// just check newest word and only need to hide
|
||||||
|
if( (words.size > 1) && (phrase.substr(0, phrase_length - 1) ===
|
||||||
|
this.last_phrase) ) {
|
||||||
|
|
||||||
|
if( phrase[-1] === " " )
|
||||||
|
{ this.last_phrase = phrase; return false; }
|
||||||
|
|
||||||
|
var words = words[-1]; // just search for the newest word
|
||||||
|
|
||||||
|
// only hide visible rows
|
||||||
|
matches = function(elem) {;}
|
||||||
|
var elems = jq.find("tbody:first > tr:visible")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new_hidden = true;
|
||||||
|
var elems = jq.find("tbody:first > tr")
|
||||||
|
}
|
||||||
|
|
||||||
|
elems.each(function(){
|
||||||
|
var elem = $(this);
|
||||||
|
$.uiTableFilter.has_words( getText(elem), words, false ) ?
|
||||||
|
matches(elem) : noMatch(elem);
|
||||||
|
});
|
||||||
|
|
||||||
|
last_phrase = phrase;
|
||||||
|
if( ifHidden && new_hidden ) ifHidden();
|
||||||
|
return jq;
|
||||||
|
};
|
||||||
|
|
||||||
|
// caching for speedup
|
||||||
|
$.uiTableFilter.last_phrase = ""
|
||||||
|
|
||||||
|
// not jQuery dependent
|
||||||
|
// "" [""] -> Boolean
|
||||||
|
// "" [""] Boolean -> Boolean
|
||||||
|
$.uiTableFilter.has_words = function( str, words, caseSensitive )
|
||||||
|
{
|
||||||
|
var text = caseSensitive ? str : str.toLowerCase();
|
||||||
|
for (var i=0; i < words.length; i++) {
|
||||||
|
if (text.indexOf(words[i]) === -1) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}) (jQuery);
|
7
layerindex/submitemail.txt
Normal file
7
layerindex/submitemail.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Hi {{ user_name }},
|
||||||
|
|
||||||
|
A user has submitted a new layer, {{ layer_name }}. Please review it at the following URL:
|
||||||
|
|
||||||
|
{{ layer_url }}
|
||||||
|
|
||||||
|
Thanks!
|
29
layerindex/submitlayer.html
Normal file
29
layerindex/submitlayer.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - layer submission form page template
|
||||||
|
|
||||||
|
Copyright (C) 2013 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title %}OpenEmbedded metadata index - submit layer{% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
<form action="{% url submit_layer %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<input type="submit" value="Submit" class='btn' />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
26
layerindex/submitthanks.html
Normal file
26
layerindex/submitthanks.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - layer submission acknowledgement page template
|
||||||
|
|
||||||
|
Copyright (C) 2013 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title %}OpenEmbedded metadata index - layer submitted{% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
<p>Thank you for submitting a layer. Your submission will be reviewed and if accepted should appear in the index soon.</p>
|
||||||
|
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
299
layerindex/update.py
Executable file
299
layerindex/update.py
Executable file
|
@ -0,0 +1,299 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# Fetch layer repositories and update 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
|
||||||
|
import optparse
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
import fnmatch
|
||||||
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
|
def logger_create():
|
||||||
|
logger = logging.getLogger("LayerIndexUpdate")
|
||||||
|
loggerhandler = logging.StreamHandler()
|
||||||
|
loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
|
||||||
|
logger.addHandler(loggerhandler)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
logger = logger_create()
|
||||||
|
|
||||||
|
# Ensure PythonGit is installed (buildhistory_analysis needs it)
|
||||||
|
try:
|
||||||
|
import git
|
||||||
|
except ImportError:
|
||||||
|
logger.error("Please install PythonGit 0.3.1 or later in order to use this script")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def runcmd(cmd,destdir=None,printerr=True):
|
||||||
|
"""
|
||||||
|
execute command, raise CalledProcessError if fail
|
||||||
|
return output if succeed
|
||||||
|
"""
|
||||||
|
logger.debug("run cmd '%s' in %s" % (cmd, os.getcwd() if destdir is None else destdir))
|
||||||
|
out = os.tmpfile()
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd, stdout=out, stderr=out, cwd=destdir, shell=True)
|
||||||
|
except subprocess.CalledProcessError,e:
|
||||||
|
out.seek(0)
|
||||||
|
if printerr:
|
||||||
|
logger.error("%s" % out.read())
|
||||||
|
raise e
|
||||||
|
|
||||||
|
out.seek(0)
|
||||||
|
output = out.read()
|
||||||
|
logger.debug("output: %s" % output.rstrip() )
|
||||||
|
return output
|
||||||
|
|
||||||
|
def sanitise_path(inpath):
|
||||||
|
outpath = ""
|
||||||
|
for c in inpath:
|
||||||
|
if c in '/ .=+?:':
|
||||||
|
outpath += "_"
|
||||||
|
else:
|
||||||
|
outpath += c
|
||||||
|
return outpath
|
||||||
|
|
||||||
|
|
||||||
|
def split_path(subdir_start, recipe_path):
|
||||||
|
if recipe_path.startswith(subdir_start) and fnmatch.fnmatch(recipe_path, "*.bb"):
|
||||||
|
if subdir_start:
|
||||||
|
filepath = os.path.relpath(os.path.dirname(recipe_path), subdir_start)
|
||||||
|
else:
|
||||||
|
filepath = os.path.dirname(recipe_path)
|
||||||
|
return (filepath, os.path.basename(recipe_path))
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def update_recipe_file(bbhandler, path, recipe):
|
||||||
|
fn = str(os.path.join(path, recipe.filename))
|
||||||
|
try:
|
||||||
|
envdata = bb.cache.Cache.loadDataFull(fn, [], bbhandler.config_data)
|
||||||
|
envdata.setVar('SRCPV', '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)
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Unable to read %s: %s", fn, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def setup_bitbake_path(basepath):
|
||||||
|
# Set path to bitbake lib dir
|
||||||
|
bitbakedir_env = os.environ.get('BITBAKEDIR', '')
|
||||||
|
if bitbakedir_env and os.path.exists(bitbakedir_env + '/lib/bb'):
|
||||||
|
bitbakepath = bitbakedir_env
|
||||||
|
elif basepath and os.path.exists(basepath + '/bitbake/lib/bb'):
|
||||||
|
bitbakepath = basepath + '/bitbake'
|
||||||
|
elif basepath and 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:
|
||||||
|
if basepath:
|
||||||
|
logger.error("Unable to find bitbake by searching BITBAKEDIR, specified path '%s' or its parent, or PATH" % basepath)
|
||||||
|
else:
|
||||||
|
logger.error("Unable to find bitbake by searching BITBAKEDIR or PATH")
|
||||||
|
sys.exit(1)
|
||||||
|
return bitbakepath
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if LooseVersion(git.__version__) < '0.3.1':
|
||||||
|
logger.error("Version of GitPython is too old, please install GitPython (python-git) 0.3.1 or later in order to use this script")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
parser = optparse.OptionParser(
|
||||||
|
usage = """
|
||||||
|
%prog [options]""")
|
||||||
|
|
||||||
|
parser.add_option("-r", "--reload",
|
||||||
|
help = "Discard existing recipe data and fetch it from scratch",
|
||||||
|
action="store_true", dest="reload")
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
basepath = os.path.abspath(sys.argv[1])
|
||||||
|
else:
|
||||||
|
basepath = None
|
||||||
|
bitbakepath = setup_bitbake_path(None)
|
||||||
|
|
||||||
|
# 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.setLevel(options.loglevel)
|
||||||
|
|
||||||
|
# Clear the default value of SUMMARY so that we can use DESCRIPTION instead if it hasn't been set
|
||||||
|
tinfoil.config_data.setVar('SUMMARY', '')
|
||||||
|
|
||||||
|
|
||||||
|
# Fetch all layers
|
||||||
|
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 = []
|
||||||
|
for layer in LayerItem.objects.filter(status='P'):
|
||||||
|
transaction.enter_transaction_management()
|
||||||
|
transaction.managed(True)
|
||||||
|
try:
|
||||||
|
# Handle multiple layers in a single repo
|
||||||
|
urldir = sanitise_path(layer.vcs_url)
|
||||||
|
repodir = os.path.join(fetchdir, urldir)
|
||||||
|
if layer.vcs_url in failedrepos:
|
||||||
|
logger.info("Skipping remote repository %s as it has already failed" % layer.vcs_url)
|
||||||
|
transaction.rollback()
|
||||||
|
continue
|
||||||
|
if not layer.vcs_url in fetchedrepos:
|
||||||
|
logger.info("Fetching remote repository %s" % layer.vcs_url)
|
||||||
|
out = None
|
||||||
|
try:
|
||||||
|
if not os.path.exists(repodir):
|
||||||
|
out = runcmd("git clone %s %s" % (layer.vcs_url, urldir), fetchdir)
|
||||||
|
else:
|
||||||
|
out = runcmd("git pull", repodir)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("fetch failed: %s" % str(e))
|
||||||
|
failedrepos.append(layer.vcs_url)
|
||||||
|
transaction.rollback()
|
||||||
|
continue
|
||||||
|
fetchedrepos.append(layer.vcs_url)
|
||||||
|
|
||||||
|
# Collect repo info
|
||||||
|
repo = git.Repo(repodir)
|
||||||
|
assert repo.bare == False
|
||||||
|
topcommit = repo.commit('master')
|
||||||
|
|
||||||
|
layerdir = os.path.join(repodir, layer.vcs_subdir)
|
||||||
|
layerrecipes = Recipe.objects.filter(layer=layer)
|
||||||
|
if layer.vcs_last_rev != topcommit.hexsha or options.reload:
|
||||||
|
logger.info("Collecting data for layer %s" % layer.name)
|
||||||
|
|
||||||
|
if layer.vcs_last_rev and not options.reload:
|
||||||
|
try:
|
||||||
|
diff = repo.commit(layer.vcs_last_rev).diff(topcommit)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn("Unable to get diff from last commit hash for layer %s - falling back to slow update: %s" % (layer.name, str(e)))
|
||||||
|
diff = None
|
||||||
|
else:
|
||||||
|
diff = None
|
||||||
|
|
||||||
|
if diff:
|
||||||
|
# Apply git changes to existing recipe list
|
||||||
|
|
||||||
|
if layer.vcs_subdir:
|
||||||
|
subdir_start = os.path.normpath(layer.vcs_subdir) + os.sep
|
||||||
|
else:
|
||||||
|
subdir_start = ""
|
||||||
|
|
||||||
|
for d in diff.iter_change_type('D'):
|
||||||
|
path = d.a_blob.path
|
||||||
|
(filepath, filename) = split_path(subdir_start, path)
|
||||||
|
if filename:
|
||||||
|
layerrecipes.filter(filepath=filepath).filter(filename=filename).delete()
|
||||||
|
|
||||||
|
for d in diff.iter_change_type('A'):
|
||||||
|
path = d.b_blob.path
|
||||||
|
(filepath, filename) = split_path(subdir_start, path)
|
||||||
|
if filename:
|
||||||
|
recipe = Recipe()
|
||||||
|
recipe.layer = layer
|
||||||
|
recipe.filename = filename
|
||||||
|
recipe.filepath = filepath
|
||||||
|
update_recipe_file(tinfoil, os.path.join(layerdir, filepath), recipe)
|
||||||
|
recipe.save()
|
||||||
|
|
||||||
|
for d in diff.iter_change_type('M'):
|
||||||
|
path = d.a_blob.path
|
||||||
|
(filepath, filename) = split_path(subdir_start, path)
|
||||||
|
if filename:
|
||||||
|
results = layerrecipes.filter(filepath=filepath).filter(filename=filename)[:1]
|
||||||
|
if results:
|
||||||
|
recipe = results[0]
|
||||||
|
update_recipe_file(tinfoil, os.path.join(layerdir, filepath), recipe)
|
||||||
|
recipe.save()
|
||||||
|
else:
|
||||||
|
# Collect recipe data from scratch
|
||||||
|
layerrecipes.delete()
|
||||||
|
for root, dirs, files in os.walk(layerdir):
|
||||||
|
for f in files:
|
||||||
|
if fnmatch.fnmatch(f, "*.bb"):
|
||||||
|
recipe = Recipe()
|
||||||
|
recipe.layer = layer
|
||||||
|
recipe.filename = f
|
||||||
|
recipe.filepath = os.path.relpath(root, layerdir)
|
||||||
|
update_recipe_file(tinfoil, root, recipe)
|
||||||
|
recipe.save()
|
||||||
|
|
||||||
|
# Save repo info
|
||||||
|
layer.vcs_last_rev = topcommit.hexsha
|
||||||
|
layer.vcs_last_commit = datetime.fromtimestamp(topcommit.committed_date)
|
||||||
|
else:
|
||||||
|
logger.info("Layer %s is already up-to-date" % layer.name)
|
||||||
|
|
||||||
|
layer.vcs_last_fetch = datetime.now()
|
||||||
|
layer.save()
|
||||||
|
|
||||||
|
transaction.commit()
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
transaction.rollback()
|
||||||
|
finally:
|
||||||
|
transaction.leave_transaction_management()
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
42
layerindex/urls.py
Normal file
42
layerindex/urls.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# layerindex-web - URL definitions
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 Intel Corporation
|
||||||
|
#
|
||||||
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from django.views.generic import DetailView, ListView
|
||||||
|
from layerindex.models import LayerItem, Recipe
|
||||||
|
from layerindex.views import LayerListView, RecipeSearchView, PlainTextListView
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^$',
|
||||||
|
LayerListView.as_view(
|
||||||
|
template_name='layerindex/index.html'),
|
||||||
|
name='layer_list'),
|
||||||
|
url(r'^submit/$', 'layerindex.views.submit_layer', name="submit_layer"),
|
||||||
|
url(r'^submit/thanks$', 'layerindex.views.submit_layer_thanks', name="submit_layer_thanks"),
|
||||||
|
url(r'^recipes/$',
|
||||||
|
RecipeSearchView.as_view(
|
||||||
|
template_name='layerindex/recipes.html'),
|
||||||
|
name='recipe_search'),
|
||||||
|
url(r'^review/$',
|
||||||
|
ListView.as_view(
|
||||||
|
queryset=LayerItem.objects.order_by('name').filter(status__in='N'),
|
||||||
|
context_object_name='layer_list',
|
||||||
|
template_name='layerindex/index.html'),
|
||||||
|
name='layer_list_review'),
|
||||||
|
url(r'^layer/(?P<slug>[-\w]+)/$',
|
||||||
|
DetailView.as_view(
|
||||||
|
model=LayerItem,
|
||||||
|
slug_field = 'name',
|
||||||
|
template_name='layerindex/detail.html'),
|
||||||
|
name='layer_item'),
|
||||||
|
url(r'^layer/(?P<name>[-\w]+)/publish/$', 'layerindex.views.publish', name="publish"),
|
||||||
|
url(r'^raw/recipes.txt$',
|
||||||
|
PlainTextListView.as_view(
|
||||||
|
queryset=Recipe.objects.order_by('pn', 'layer'),
|
||||||
|
context_object_name='recipe_list',
|
||||||
|
template_name='layerindex/rawrecipes.txt'),
|
||||||
|
name='recipe_list_raw'),
|
||||||
|
)
|
122
layerindex/views.py
Normal file
122
layerindex/views.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
# layerindex-web - view definitions
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013 Intel Corporation
|
||||||
|
#
|
||||||
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.template import RequestContext
|
||||||
|
from layerindex.models import LayerItem, LayerMaintainer, LayerDependency, Recipe
|
||||||
|
from datetime import datetime
|
||||||
|
from django.views.generic import DetailView, ListView
|
||||||
|
from layerindex.forms import SubmitLayerForm
|
||||||
|
from django.db import transaction
|
||||||
|
from django.contrib.auth.models import User, Permission
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.core.mail import EmailMessage
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from django.template import Context
|
||||||
|
import settings
|
||||||
|
|
||||||
|
|
||||||
|
def submit_layer(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
layeritem = LayerItem()
|
||||||
|
form = SubmitLayerForm(request.POST, instance=layeritem)
|
||||||
|
if form.is_valid():
|
||||||
|
with transaction.commit_on_success():
|
||||||
|
layeritem.created_date = datetime.now()
|
||||||
|
form.save()
|
||||||
|
# Save maintainers
|
||||||
|
for name, email in form.cleaned_data['maintainers'].items():
|
||||||
|
maint = LayerMaintainer()
|
||||||
|
maint.layer = layeritem
|
||||||
|
maint.name = name
|
||||||
|
maint.email = email
|
||||||
|
maint.save()
|
||||||
|
# Save dependencies
|
||||||
|
for dep in form.cleaned_data['deps']:
|
||||||
|
deprec = LayerDependency()
|
||||||
|
deprec.layer = layeritem
|
||||||
|
deprec.dependency = dep
|
||||||
|
deprec.save()
|
||||||
|
# Send email
|
||||||
|
plaintext = get_template('layerindex/submitemail.txt')
|
||||||
|
perm = Permission.objects.get(codename='publish_layer')
|
||||||
|
users = User.objects.filter(Q(groups__permissions=perm) | Q(user_permissions=perm) ).distinct()
|
||||||
|
for user in users:
|
||||||
|
d = Context({
|
||||||
|
'user_name': user.get_full_name(),
|
||||||
|
'layer_name': layeritem.name,
|
||||||
|
'layer_url': request.build_absolute_uri(reverse('layer_item', args=(layeritem.name,))),
|
||||||
|
})
|
||||||
|
subject = '%s - %s' % (settings.SUBMIT_EMAIL_SUBJECT, layeritem.name)
|
||||||
|
from_email = settings.SUBMIT_EMAIL_FROM
|
||||||
|
to_email = user.email
|
||||||
|
text_content = plaintext.render(d)
|
||||||
|
msg = EmailMessage(subject, text_content, from_email, [to_email])
|
||||||
|
msg.send()
|
||||||
|
return HttpResponseRedirect(reverse('submit_layer_thanks'))
|
||||||
|
else:
|
||||||
|
form = SubmitLayerForm()
|
||||||
|
|
||||||
|
return render(request, 'layerindex/submitlayer.html', {
|
||||||
|
'form': form,
|
||||||
|
})
|
||||||
|
|
||||||
|
def submit_layer_thanks(request):
|
||||||
|
return render(request, 'layerindex/submitthanks.html')
|
||||||
|
|
||||||
|
def publish(request, name):
|
||||||
|
if not (request.user.is_authenticated() and request.user.has_perm('layerindex.publish_layer')):
|
||||||
|
raise PermissionDenied
|
||||||
|
return _statuschange(request, name, 'P')
|
||||||
|
|
||||||
|
def _statuschange(request, name, newstatus):
|
||||||
|
w = get_object_or_404(LayerItem, name=name)
|
||||||
|
w.change_status(newstatus, request.user.username)
|
||||||
|
w.save()
|
||||||
|
return HttpResponseRedirect(reverse('layer_item', args=(name,)))
|
||||||
|
|
||||||
|
class LayerListView(ListView):
|
||||||
|
context_object_name = 'layer_list'
|
||||||
|
paginate_by = 20
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return LayerItem.objects.filter(status__in=self.request.session.get('status_filter', 'P')).order_by('name')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(LayerListView, self).get_context_data(**kwargs)
|
||||||
|
context['layer_type_choices'] = LayerItem.LAYER_TYPE_CHOICES
|
||||||
|
return context
|
||||||
|
|
||||||
|
class RecipeSearchView(ListView):
|
||||||
|
context_object_name = 'recipe_list'
|
||||||
|
paginate_by = 20
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
keyword = self.request.session.get('keyword')
|
||||||
|
if keyword:
|
||||||
|
return Recipe.objects.all().filter(pn__icontains=keyword).order_by('pn', 'layer')
|
||||||
|
else:
|
||||||
|
return Recipe.objects.all().order_by('pn', 'layer')
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
request.session['keyword'] = request.POST['filter']
|
||||||
|
return HttpResponseRedirect(reverse('recipe_search'))
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(RecipeSearchView, self).get_context_data(**kwargs)
|
||||||
|
context['search_keyword'] = self.request.session.get('keyword') or ''
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class PlainTextListView(ListView):
|
||||||
|
def render_to_response(self, context):
|
||||||
|
"Returns a plain text response rendering of the template"
|
||||||
|
template = get_template(self.template_name)
|
||||||
|
return HttpResponse(template.render(Context(context)),
|
||||||
|
content_type='text/plain')
|
22
manage.py
Normal file
22
manage.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# layerindex-web - Django management script
|
||||||
|
#
|
||||||
|
# Based on the Django project template
|
||||||
|
#
|
||||||
|
# Copyright (c) Django Software Foundation and individual contributors.
|
||||||
|
# All rights reserved.
|
||||||
|
|
||||||
|
from django.core.management import execute_manager
|
||||||
|
import imp
|
||||||
|
try:
|
||||||
|
imp.find_module('settings') # Assumed to be in the same directory.
|
||||||
|
except ImportError:
|
||||||
|
import sys
|
||||||
|
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
execute_manager(settings)
|
18
registration/activate.html
Normal file
18
registration/activate.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if account %}
|
||||||
|
|
||||||
|
<p>{% trans "Account successfully activated" %}</p>
|
||||||
|
|
||||||
|
<p><a href="{% url auth_login %}">{% trans "Log in" %}</a></p>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<p>{% trans "Account activation failed" %}</p>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
12
registration/activation_email.txt
Normal file
12
registration/activation_email.txt
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% blocktrans %}
|
||||||
|
A request has been made to activate an account at {{ site.name }} using your email address.
|
||||||
|
|
||||||
|
If you made this request, please click on the link below to activate your account. The
|
||||||
|
link is valid for {{ expiration_days }} days.
|
||||||
|
|
||||||
|
http://{{ site.domain }}{% url registration_activate activation_key %}
|
||||||
|
|
||||||
|
If you did not make this request, please ignore this message.
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
1
registration/activation_email_subject.txt
Normal file
1
registration/activation_email_subject.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{% load i18n %}{% trans "Account activation on" %} {{ site.name }}
|
15
registration/login.html
Normal file
15
registration/login.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" action=".">
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" class="btn" value="{% trans 'Log in' %}" />
|
||||||
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>{% trans "Forgot password" %}? <a href="{% url auth_password_reset %}">{% trans "Reset it" %}</a>!</p>
|
||||||
|
<p>{% trans "Not a member" %}? <a href="{% url registration_register %}">{% trans "Register" %}</a>!</p>
|
||||||
|
{% endblock %}
|
6
registration/logout.html
Normal file
6
registration/logout.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>{% trans "Logged out" %}</p>
|
||||||
|
{% endblock %}
|
6
registration/password_change_done.html
Normal file
6
registration/password_change_done.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>{% trans "Password changed" %}</p>
|
||||||
|
{% endblock %}
|
10
registration/password_change_form.html
Normal file
10
registration/password_change_form.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" action=".">
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" class="btn" value="{% trans 'Submit' %}" />
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
10
registration/password_reset_complete.html
Normal file
10
registration/password_reset_complete.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<p>{% trans "Password reset successfully" %}</p>
|
||||||
|
|
||||||
|
<p><a href="{% url auth_login %}">{% trans "Log in" %}</a></p>
|
||||||
|
|
||||||
|
{% endblock %}
|
20
registration/password_reset_confirm.html
Normal file
20
registration/password_reset_confirm.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if validlink %}
|
||||||
|
|
||||||
|
<form method="post" action=".">
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" class="btn" value="{% trans 'Submit' %}" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<p>{% trans "Password reset failed" %}</p>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
6
registration/password_reset_done.html
Normal file
6
registration/password_reset_done.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>{% trans "An email with password reset instructions has been sent." %}</p>
|
||||||
|
{% endblock %}
|
5
registration/password_reset_email.html
Normal file
5
registration/password_reset_email.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% blocktrans %}Reset password at {{ site_name }}{% endblocktrans %}:
|
||||||
|
{% block reset_link %}
|
||||||
|
{{ protocol }}://{{ domain }}{% url auth_password_reset_confirm uidb36=uid, token=token %}
|
||||||
|
{% endblock %}
|
10
registration/password_reset_form.html
Normal file
10
registration/password_reset_form.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" action=".">
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" class="btn" value="{% trans 'Submit' %}" />
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
6
registration/registration_complete.html
Normal file
6
registration/registration_complete.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>{% trans "You are now registered, however your account must now be activated. An email has been sent with instructions on how to activate your account." %}</p>
|
||||||
|
{% endblock %}
|
11
registration/registration_form.html
Normal file
11
registration/registration_form.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post" action=".">
|
||||||
|
{{ form.as_p }}
|
||||||
|
|
||||||
|
<input type="submit" class="btn" value="{% trans 'Submit' %}" />
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
167
settings.py
Normal file
167
settings.py
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
# Django settings for layerindex project.
|
||||||
|
#
|
||||||
|
# Based on settings.py from the Django project template
|
||||||
|
# Copyright (c) Django Software Foundation and individual contributors.
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
|
||||||
|
ADMINS = (
|
||||||
|
# ('Your Name', 'your_email@example.com'),
|
||||||
|
)
|
||||||
|
|
||||||
|
MANAGERS = ADMINS
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||||
|
'NAME': '', # Or path to database file if using sqlite3 (full path recommended).
|
||||||
|
'USER': '', # Not used with sqlite3.
|
||||||
|
'PASSWORD': '', # Not used with sqlite3.
|
||||||
|
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||||
|
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Local time zone for this installation. Choices can be found here:
|
||||||
|
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||||
|
# although not all choices may be available on all operating systems.
|
||||||
|
# On Unix systems, a value of None will cause Django to use the same
|
||||||
|
# timezone as the operating system.
|
||||||
|
# If running in a Windows environment this must be set to the same as your
|
||||||
|
# system time zone.
|
||||||
|
TIME_ZONE = 'Europe/London'
|
||||||
|
|
||||||
|
# Language code for this installation. All choices can be found here:
|
||||||
|
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
# If you set this to False, Django will make some optimizations so as not
|
||||||
|
# to load the internationalization machinery.
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
# If you set this to False, Django will not format dates, numbers and
|
||||||
|
# calendars according to the current locale
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
# Avoid specific paths (added by paule)
|
||||||
|
import os
|
||||||
|
BASE_DIR = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||||
|
# Example: "/home/media/media.lawrence.com/media/"
|
||||||
|
MEDIA_ROOT = ''
|
||||||
|
|
||||||
|
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||||
|
# trailing slash.
|
||||||
|
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||||
|
MEDIA_URL = ''
|
||||||
|
|
||||||
|
# Absolute path to the directory static files should be collected to.
|
||||||
|
# Don't put anything in this directory yourself; store your static files
|
||||||
|
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||||
|
# Example: "/home/media/media.lawrence.com/static/"
|
||||||
|
STATIC_ROOT = ''
|
||||||
|
|
||||||
|
# URL prefix for static files.
|
||||||
|
# Example: "http://media.lawrence.com/static/"
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
# URL prefix for admin static files -- CSS, JavaScript and images.
|
||||||
|
# Make sure to use a trailing slash.
|
||||||
|
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||||
|
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||||
|
|
||||||
|
# Additional locations of static files
|
||||||
|
STATICFILES_DIRS = (
|
||||||
|
# Put strings here, like "/home/html/static" or "C:/www/django/static".
|
||||||
|
# Always use forward slashes, even on Windows.
|
||||||
|
# Don't forget to use absolute paths, not relative paths.
|
||||||
|
)
|
||||||
|
|
||||||
|
# List of finder classes that know how to find static files in
|
||||||
|
# various locations.
|
||||||
|
STATICFILES_FINDERS = (
|
||||||
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
|
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make this unique, and don't share it with anybody.
|
||||||
|
SECRET_KEY = 'r%l4mj2q3+2dy)hpl%f3fm$fj+=3+(e1$$o#=7k^#t3x*c)1*l'
|
||||||
|
|
||||||
|
# List of callables that know how to import templates from various sources.
|
||||||
|
TEMPLATE_LOADERS = (
|
||||||
|
'django.template.loaders.filesystem.Loader',
|
||||||
|
'django.template.loaders.app_directories.Loader',
|
||||||
|
# 'django.template.loaders.eggs.Loader',
|
||||||
|
)
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES = (
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
)
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'urls'
|
||||||
|
|
||||||
|
TEMPLATE_DIRS = (
|
||||||
|
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||||
|
# Always use forward slashes, even on Windows.
|
||||||
|
# Don't forget to use absolute paths, not relative paths.
|
||||||
|
BASE_DIR
|
||||||
|
)
|
||||||
|
|
||||||
|
INSTALLED_APPS = (
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
# Uncomment the next line to enable the admin:
|
||||||
|
'django.contrib.admin',
|
||||||
|
# Uncomment the next line to enable admin documentation:
|
||||||
|
# 'django.contrib.admindocs',
|
||||||
|
'layerindex',
|
||||||
|
'registration'
|
||||||
|
)
|
||||||
|
|
||||||
|
# A sample logging configuration. The only tangible logging
|
||||||
|
# performed by this configuration is to send an email to
|
||||||
|
# the site admins on every HTTP 500 error.
|
||||||
|
# See http://docs.djangoproject.com/en/dev/topics/logging for
|
||||||
|
# more details on how to customize your logging configuration.
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'handlers': {
|
||||||
|
'mail_admins': {
|
||||||
|
'level': 'ERROR',
|
||||||
|
'class': 'django.utils.log.AdminEmailHandler'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django.request': {
|
||||||
|
'handlers': ['mail_admins'],
|
||||||
|
'level': 'ERROR',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Registration settings
|
||||||
|
ACCOUNT_ACTIVATION_DAYS = 2
|
||||||
|
EMAIL_HOST = 'smtp.example.com'
|
||||||
|
LOGIN_REDIRECT_URL = '/layerindex'
|
||||||
|
|
||||||
|
# Full path to directory where layers should be fetched into by the update script
|
||||||
|
LAYER_FETCH_DIR = ""
|
||||||
|
|
||||||
|
# Settings for layer submission feature
|
||||||
|
SUBMIT_EMAIL_FROM = 'noreply@example.com'
|
||||||
|
SUBMIT_EMAIL_SUBJECT = 'OE Layerindex layer submission'
|
18
urls.py
Normal file
18
urls.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# layerindex-web - URLs
|
||||||
|
#
|
||||||
|
# Based on the Django project template
|
||||||
|
#
|
||||||
|
# Copyright (c) Django Software Foundation and individual contributors.
|
||||||
|
# All rights reserved.
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import patterns, include, url
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
admin.autodiscover()
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
url(r'^layerindex/', include('layerindex.urls')),
|
||||||
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
|
url(r'^accounts/', include('registration.urls')),
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user