expose REST API for layerindex

This patch enables a read-only REST API for the layerindex
application using Django REST Framework.

The objects of types Branch, LayerBranch and LayerItem are
exposed to queries so that the layerindex application can
function as a Layer Source in Toaster.

The library dependencies are documented in the requirements.txt
file.

Signed-off-by: Alexandru DAMIAN <alexandru.damian@intel.com>
This commit is contained in:
Alexandru DAMIAN 2014-07-16 15:50:53 +01:00 committed by Paul Eggleton
parent 99b9b014db
commit 1c9d6be527
7 changed files with 213 additions and 0 deletions

1
README
View File

@ -25,6 +25,7 @@ In order to make use of this application you will need:
* django-reversion-compare (0.3.5)
* django-simple-captcha (0.4.1)
* django-nvd3 (0.6.0)
* djangorestframework (2.3.14)
* On the machine that will run the backend update script (which does not
have to be the same machine as the web server, however it does still
have to have Django installed, have the same or similar configuration

View File

@ -0,0 +1,125 @@
import operator
from django.db.models import Q
def _verify_parameters(g, mandatory_parameters):
miss = []
for mp in mandatory_parameters:
if not mp in g:
miss.append(mp)
if len(miss):
return miss
return None
def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs):
import urllib
url = reverse(view, kwargs=kwargs)
params = {}
for i in g:
params[i] = g[i]
for i in mandatory_parameters:
if not i in params:
params[i] = mandatory_parameters[i]
return redirect(url + "?%s" % urllib.urlencode(params), *args, **kwargs)
FIELD_SEPARATOR = ":"
VALUE_SEPARATOR = "!"
DESCENDING = "-"
def __get_q_for_val(name, value):
if "OR" in value:
return reduce(operator.or_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("OR") ]))
if "AND" in value:
return reduce(operator.and_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("AND") ]))
if value.startswith("NOT"):
kwargs = { name : value.strip("NOT") }
return ~Q(**kwargs)
else:
kwargs = { name : value }
return Q(**kwargs)
def _get_filtering_query(filter_string):
search_terms = filter_string.split(FIELD_SEPARATOR)
keys = search_terms[0].split(VALUE_SEPARATOR)
values = search_terms[1].split(VALUE_SEPARATOR)
querydict = dict(zip(keys, values))
return reduce(operator.and_, map(lambda x: __get_q_for_val(x, querydict[x]), [k for k in querydict]))
# we check that the input comes in a valid form that we can recognize
def _validate_input(input, model):
invalid = None
if input:
input_list = input.split(FIELD_SEPARATOR)
# Check we have only one colon
if len(input_list) != 2:
invalid = "We have an invalid number of separators: " + input + " -> " + str(input_list)
return None, invalid
# Check we have an equal number of terms both sides of the colon
if len(input_list[0].split(VALUE_SEPARATOR)) != len(input_list[1].split(VALUE_SEPARATOR)):
invalid = "Not all arg names got values"
return None, invalid + str(input_list)
# Check we are looking for a valid field
valid_fields = model._meta.get_all_field_names()
for field in input_list[0].split(VALUE_SEPARATOR):
if not reduce(lambda x, y: x or y, map(lambda x: field.startswith(x), [ x for x in valid_fields ])):
return None, (field, [ x for x in valid_fields ])
return input, invalid
# uses search_allowed_fields in orm/models.py to create a search query
# for these fields with the supplied input text
def _get_search_results(search_term, queryset, model):
search_objects = []
for st in search_term.split(" "):
q_map = map(lambda x: Q(**{x+'__icontains': st}),
model.search_allowed_fields)
search_objects.append(reduce(operator.or_, q_map))
search_object = reduce(operator.and_, search_objects)
queryset = queryset.filter(search_object)
return queryset
# function to extract the search/filter/ordering parameters from the request
# it uses the request and the model to validate input for the filter and orderby values
def get_search_tuple(request, model):
ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model)
if invalid:
raise BaseException("Invalid ordering model:" + str(model) + str(invalid))
filter_string, invalid = _validate_input(request.GET.get('filter', ''), model)
if invalid:
raise BaseException("Invalid filter " + str(invalid))
search_term = request.GET.get('search', '')
return (filter_string, search_term, ordering_string)
# returns a lazy-evaluated queryset for a filter/search/order combination
def params_to_queryset(model, queryset, filter_string, search_term, ordering_string):
if filter_string:
filter_query = _get_filtering_query(filter_string)
queryset = queryset.filter(filter_query)
else:
queryset = queryset.all()
if search_term:
queryset = _get_search_results(search_term, queryset, model)
if ordering_string and queryset:
column, order = ordering_string.split(':')
if order.lower() == DESCENDING:
column = '-' + column
# insure only distinct records (e.g. from multiple search hits) are returned
return queryset.distinct()

7
layerindex/restperm.py Normal file
View File

@ -0,0 +1,7 @@
from rest_framework import permissions
class ReadOnlyPermission(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return False

58
layerindex/restviews.py Normal file
View File

@ -0,0 +1,58 @@
from models import Branch, LayerItem, LayerNote, LayerBranch, LayerDependency, Recipe, Machine
from rest_framework import viewsets, serializers
from querysethelper import params_to_queryset, get_search_tuple
class ParametricSearchableModelViewSet(viewsets.ModelViewSet):
def get_queryset(self):
model = self.__class__.serializer_class.Meta.model
qs = model.objects.all()
(filter_string, search_term, ordering_string) = get_search_tuple(self.request, model)
return params_to_queryset(model, qs, filter_string, search_term, ordering_string)
class BranchSerializer(serializers.ModelSerializer):
class Meta:
model = Branch
class BranchViewSet(ParametricSearchableModelViewSet):
queryset = Branch.objects.all()
serializer_class = BranchSerializer
class LayerItemSerializer(serializers.ModelSerializer):
class Meta:
model = LayerItem
class LayerItemViewSet(ParametricSearchableModelViewSet):
queryset = LayerItem.objects.all()
serializer_class = LayerItemSerializer
class LayerBranchSerializer(serializers.ModelSerializer):
class Meta:
model = LayerBranch
class LayerBranchViewSet(ParametricSearchableModelViewSet):
queryset = LayerBranch.objects.all()
serializer_class = LayerBranchSerializer
class LayerDependencySerializer(serializers.ModelSerializer):
class Meta:
model = LayerDependency
class LayerDependencyViewSet(ParametricSearchableModelViewSet):
queryset = LayerDependency.objects.all()
serializer_class = LayerDependencySerializer
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
class RecipeViewSet(ParametricSearchableModelViewSet):
queryset = Recipe.objects.all()
serializer_class = RecipeSerializer
class MachineSerializer(serializers.ModelSerializer):
class Meta:
model = Machine
class MachineViewSet(ParametricSearchableModelViewSet):
queryset = Machine.objects.all()
serializer_class = MachineSerializer

View File

@ -11,11 +11,24 @@ from django.views.defaults import page_not_found
from django.core.urlresolvers import reverse_lazy
from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, MachineSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, HistoryListView, EditProfileFormView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView, RedirectParamsView, ClassicRecipeSearchView, ClassicRecipeDetailView, ClassicRecipeStatsView
from layerindex.models import LayerItem, Recipe, RecipeChangeset
from rest_framework import routers
import restviews
from django.conf.urls import include
router = routers.DefaultRouter()
router.register(r'branches', restviews.BranchViewSet)
router.register(r'layerItems', restviews.LayerItemViewSet)
router.register(r'layerBranches', restviews.LayerBranchViewSet)
router.register(r'layerDependencies', restviews.LayerDependencyViewSet)
router.register(r'recipes', restviews.RecipeViewSet)
router.register(r'machines', restviews.MachineViewSet)
urlpatterns = patterns('',
url(r'^$', redirect_to, {'url' : reverse_lazy('layer_list', args=('master',))},
name='frontpage'),
url(r'^api/', include(router.urls)),
url(r'^layers/$',
redirect_to, {'url' : reverse_lazy('layer_list', args=('master',))}),
url(r'^layer/(?P<slug>[-\w]+)/$',

View File

@ -11,6 +11,7 @@ django-registration==0.8
django-reversion==1.6.0
django-reversion-compare==0.3.5
django-simple-captcha==0.4.2
djangorestframework==2.3.14
python-nvd3==0.12.2
regex==2014.06.28
six==1.7.3

View File

@ -144,9 +144,17 @@ INSTALLED_APPS = (
'reversion_compare',
'captcha',
'south',
'rest_framework',
'django_nvd3'
)
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'layerindex.restperm.ReadOnlyPermission',
),
'DATETIME_FORMAT': '%Y-%m-%dT%H:%m:%S+0000',
}
# 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.