Initial commit of layerindex-web

Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
This commit is contained in:
Paul Eggleton 2013-02-13 11:45:38 +00:00
commit 2eb5f38b21
48 changed files with 20648 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc
*.db3

22
COPYING.MIT Normal file
View 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
View 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
View 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
View File

77
base.html Normal file
View 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
View File

14
layerindex/admin.py Normal file
View 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
View 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
View 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
View 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
View 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)

View 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
View 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 %}

View 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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6039
layerindex/static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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

File diff suppressed because one or more lines are too long

9404
layerindex/static/js/jquery-1.7.2.js vendored Normal file

File diff suppressed because it is too large Load Diff

View 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);

View 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!

View 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 %}

View 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
View 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
View 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
View 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
View 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)

View 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 %}

View 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 %}

View File

@ -0,0 +1 @@
{% load i18n %}{% trans "Account activation on" %} {{ site.name }}

15
registration/login.html Normal file
View 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
View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<p>{% trans "Logged out" %}</p>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<p>{% trans "Password changed" %}</p>
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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
View 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
View 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')),
)