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