summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexandru DAMIAN <alexandru.damian@intel.com>2014-07-16 15:50:53 +0100
committerPaul Eggleton <paul.eggleton@linux.intel.com>2014-09-24 11:32:34 +0100
commit1c9d6be527e334046ea4d8f6ae3617c6e9d166c2 (patch)
tree03124bdbf167e75cac3780741a5d41825188356b
parent99b9b014db31b381e4486c57779ffa5de532c0bd (diff)
downloadopenembedded-core-contrib-1c9d6be527e334046ea4d8f6ae3617c6e9d166c2.tar.gz
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>
-rw-r--r--README1
-rw-r--r--layerindex/querysethelper.py125
-rw-r--r--layerindex/restperm.py7
-rw-r--r--layerindex/restviews.py58
-rw-r--r--layerindex/urls.py13
-rw-r--r--requirements.txt1
-rw-r--r--settings.py8
7 files changed, 213 insertions, 0 deletions
diff --git a/README b/README
index 8f9c80b695..6fee259aef 100644
--- a/README
+++ b/README
@@ -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
diff --git a/layerindex/querysethelper.py b/layerindex/querysethelper.py
new file mode 100644
index 0000000000..8d54d76bea
--- /dev/null
+++ b/layerindex/querysethelper.py
@@ -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()
+
+
diff --git a/layerindex/restperm.py b/layerindex/restperm.py
new file mode 100644
index 0000000000..fb9f715d26
--- /dev/null
+++ b/layerindex/restperm.py
@@ -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
diff --git a/layerindex/restviews.py b/layerindex/restviews.py
new file mode 100644
index 0000000000..61698a9918
--- /dev/null
+++ b/layerindex/restviews.py
@@ -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
diff --git a/layerindex/urls.py b/layerindex/urls.py
index 70060253f1..1bd4f0b5b1 100644
--- a/layerindex/urls.py
+++ b/layerindex/urls.py
@@ -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]+)/$',
diff --git a/requirements.txt b/requirements.txt
index 0bc28a8922..8ad447f342 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/settings.py b/settings.py
index b954b889a2..d53a9d36d3 100644
--- a/settings.py
+++ b/settings.py
@@ -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.