]> git.ipfire.org Git - thirdparty/openembedded/openembedded-core-contrib.git/commitdiff
toaster: Toaster GUI, generic search, filter and order
authorAlexandru DAMIAN <alexandru.damian@intel.com>
Tue, 7 Jan 2014 13:10:42 +0000 (13:10 +0000)
committerPaul Eggleton <paul.eggleton@linux.intel.com>
Fri, 10 Jan 2014 14:13:53 +0000 (14:13 +0000)
This patch implements table searching, filtering and ordering, in a
generic mode reusable for all tables.

The search operates list of fields defined in the corresponding
class for each model, search_allowed_fields.

The search expression and filters are sent through GET requests
using a QuerySet-like input. The inputs are filtered and
validated before usage to prevent inadvertent or malicious use.

Filters and table headers are defined in the views for each table,
and rendered by generic code which is easily modified for various
tables.

The Build table and Configuration table are implemented using this
framework as an example of how it should be used.

    [YOCTO #4249]
    [YOCTO #4254]
    [YOCTO #4255]
    [YOCTO #4256]
    [YOCTO #4257]
    [YOCTO #4259]
    [YOCTO #4260]

Signed-off-by: Alexandru DAMIAN <alexandru.damian@intel.com>
lib/toaster/orm/models.py
lib/toaster/toastergui/static/css/default.css
lib/toaster/toastergui/templates/basetable_bottom.html
lib/toaster/toastergui/templates/basetable_top.html
lib/toaster/toastergui/templates/build.html
lib/toaster/toastergui/templates/configuration.html
lib/toaster/toastergui/templates/configvars.html [new file with mode: 0644]
lib/toaster/toastergui/templates/filtersnippet.html [new file with mode: 0644]
lib/toaster/toastergui/templatetags/projecttags.py
lib/toaster/toastergui/urls.py
lib/toaster/toastergui/views.py

index b30e405c0ef181b5076caa5be94cdeddfb4bc7d6..ff26c7d436970ed48cd438e5aa966cc3d62e7ab2 100644 (file)
@@ -31,8 +31,8 @@ class Build(models.Model):
         (IN_PROGRESS, 'In Progress'),
     )
 
-    search_allowed_fields = ['machine',
-                             'cooker_log_path']
+    search_allowed_fields = ['machine', 'image_fstypes',
+                             'cooker_log_path', "target__target"]
 
     machine = models.CharField(max_length=100)
     image_fstypes = models.CharField(max_length=100)
@@ -102,6 +102,8 @@ class Task(models.Model):
         (OUTCOME_NA, 'Not Available'),
     )
 
+    search_allowed_fields = [ "recipe__name", "task_name" ]
+
     build = models.ForeignKey(Build, related_name='task_build')
     order = models.IntegerField(null=True)
     task_executed = models.BooleanField(default=False) # True means Executed, False means Prebuilt
@@ -217,6 +219,8 @@ class Layer_Version(models.Model):
 
 
 class Variable(models.Model):
+    search_allowed_fields = ['variable_name', 'variable_value',
+                             'variablehistory__file_name', "description"]
     build = models.ForeignKey(Build, related_name='variable_build')
     variable_name = models.CharField(max_length=100)
     variable_value = models.TextField(blank=True)
@@ -225,7 +229,7 @@ class Variable(models.Model):
     description = models.TextField(blank=True)
 
 class VariableHistory(models.Model):
-    variable = models.ForeignKey(Variable)
+    variable = models.ForeignKey(Variable, related_name='vhistory')
     file_name = models.FilePathField(max_length=255)
     line_number = models.IntegerField(null=True)
     operation = models.CharField(max_length=16)
index 844f6dcd5676bb431306ff97a0787fc9ae47e459..53c50043bcded39a5a1fccb8719447afabec90f8 100644 (file)
@@ -171,4 +171,7 @@ dd p {line-height:20px;}
 .tooltip { z-index: 2000 !important; } /* this makes tooltips work inside modal dialogs */
 .tooltip code { background-color:transparent; color:#FFFFFF; font-weight:normal; border:none; font-size: 1em; }
 .manual { margin-top:11px;}
-.heading-help { font-size:14px;}
\ No newline at end of file
+.heading-help { font-size:14px;}
+
+
+.no-results { margin: 10px 0 0; }
index 00703fe4c1c4cea263ac83a441159cb09e5b44df..3e4b0cc5a41c643e4899238233fe71bc73e471b2 100644 (file)
@@ -1,3 +1,4 @@
+    </tbody>
     </table>
 
 <!-- Show pagination controls -->
@@ -8,15 +9,15 @@
 
    <ul class="pagination" style="display: block-inline">
 {%if objects.has_previous %}
-  <li><a href="?page={{objects.previous_page_number}}&count={{request.GET.count}}">&laquo;</a></li>
+  <li><a href="javascript:reload_params({'page':{{objects.previous_page_number}}})">&laquo;</a></li>
 {%else%}
   <li class="disabled"><a href="#">&laquo;</a></li>
 {%endif%}
 {% for i in objects.page_range %}
-  <li{%if i == objects.number %} class="active" {%endif%}><a href="?page={{i}}&count={{request.GET.count}}">{{i}}</a></li>
+  <li{%if i == objects.number %} class="active" {%endif%}><a href="javascript:reload_params({'page':{{i}}})">{{i}}</a></li>
 {% endfor %}
 {%if objects.has_next%}
-  <li><a href="?page={{objects.next_page_number}}&count={{request.GET.count}}">&raquo;</a></li>
+  <li><a href="javascript:reload_params({'page':{{objects.next_page_number}}})">&raquo;</a></li>
 {%else%}
   <li class="disabled"><a href="#">&raquo;</a></li>
 {%endif%}
@@ -58,3 +59,9 @@
     });
 });
 </script>
+
+<!-- modal filter boxes -->
+   {% for tc in tablecols %}{% if tc.filter %}{% with f=tc.filter %}
+        {% include "filtersnippet.html" %}
+   {% endwith %}{% endif %} {% endfor %}
+<!-- end modals -->
index b9277b4a3de123378396fef94bc85b74a6998adf..34e0cd721065e97604ee8b3ef992b1e7c2ff3367 100644 (file)
 
 <!-- control header -->
 <div class="navbar">
-                    <div class="navbar-inner">
-                        <form class="navbar-search input-append pull-left">
-                            <input class="input-xxlarge" type="text" placeholder="Search {{objectname}}" />
-                            <button class="btn" type="button">Search</button>
-                        </form>
-                        <div class="pull-right">
-
-    {% if tablecols %}
-                            <div class="btn-group">
-                                <button class="btn dropdown-toggle" data-toggle="dropdown">
-                                    Edit columns
-                                    <span class="caret"></span>
-                                </button>
-                                <ul class="dropdown-menu">
-
-    {% for i in tablecols %}
-                                    <li>
-                                        <label class="checkbox">
-<input type="checkbox" class="chbxtoggle" id="{{i.clclass}}" value="ct{{i.name}}" {% if i.clclass %}{% if not i.hidden %}checked="checked"{%endif%} onchange="showhideTableColumn($(this).attr('id'), $(this).is(':checked'))" {%else%} disabled{% endif %}/>   {{i.name}}
-                                        </label>
-                                    </li>
-    {% endfor %}
-                                </ul>
-                            </div>
-    {% endif %}
-
-    <div style="display:inline">
-                            <span class="divider-vertical"></span>
-                            <span class="help-inline" style="padding-top:5px;">Show rows:</span>
-                            <select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
+    <div class="navbar-inner">
+        <form class="navbar-search input-append pull-left" >
+            <input class="input-xxlarge" name="search" type="text" placeholder="Search {{objectname}}" value="{{request.GET.search}}"/>
+            <input class="btn" type="submit" value="Search"/>
+        </form>
+        <div class="pull-right">
+{% if tablecols %}
+            <div class="btn-group">
+                <button class="btn dropdown-toggle" data-toggle="dropdown">Edit columns
+                    <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu">{% for i in tablecols %}
+                    <li>
+                        <label class="checkbox">
+                            <input type="checkbox" class="chbxtoggle" {% if i.clclass %}id="{{i.clclass}}" value="ct{{i.name}}" {% if not i.hidden %}checked="checked"{%endif%} onchange="showhideTableColumn($(this).attr('id'), $(this).is(':checked'))" {%else%} checked disabled{% endif %}/>   {{i.name}}
+                        </label>
+                    </li>{% endfor %}
+                </ul>
+            </div>
+{% endif %}
+            <div style="display:inline">
+                <span class="divider-vertical"></span>
+                <span class="help-inline" style="padding-top:5px;">Show rows:</span>
+                <select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
   {% with "2 5 10 25 50 100" as list%}
-    {% for i in list.split %}<option{%if i == request.GET.count %} selected{%endif%}>{{i}}</option>
+{% for i in list.split %}                    <option{%if i == request.GET.count %} selected{%endif%}>{{i}}</option>
     {% endfor %}
   {% endwith %}
-                            </select>
-    </div>
-                        </div>
-                    </div>
-                </div>
+                </select>
+           </div>
+        </div>
+    </div> <!-- navbar-inner -->
+</div>
 
 <!-- the actual rows of the table -->
     <table class="table table-bordered table-hover tablesorter" id="otable">
+    <thead>
+        <!-- Table header row; generated from "tablecols" entry in the context dict -->
+        <tr>
+            {% for tc in tablecols %}<th class="{{tc.dclass}} {{tc.clclass}}">
+                {%if tc.qhelp%}<i class="icon-question-sign get-help" data-toggle="tooltip" title="{{tc.qhelp}}"></i>{%endif%}
+                <a href="javascript:reload_params({'orderby' : '{{tc.orderfield}}' })" style="font-weight:normal;">{{tc.name}}</a>
+                {%if tc.filter%}<div class="btn-group pull-right">
+                    <a href="#filter_{{tc.filter.class}}" role="button" class="btn btn-mini{%if request.GET.filter in tc.filter.options.values%} btn-primary{%endif%}" data-toggle="modal"> <i class="icon-filter filtered"></i> </a>
+                </div>{%endif%}
+            </th>{% endfor %}
+        </tr>
+    </thead>
+    <tbody>
 
index 43b491d55815e472ea5b218fd7a3c2d51d031f0a..eb7e03c951846e8eca9da2be45e1f209cb9865e4 100644 (file)
@@ -7,70 +7,77 @@
 {% block pagecontent %}
 <div class="row-fluid">
 
-<div class="page-header" style="margin-top:40px;">
-    <h1>
-        Recent Builds
-     </h1>
-</div>
-{% for build in mru %}
-<div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}">
-    <div class="row-fluid">
-        <div class="lead span5">
-            {%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}
-            <a href="{%url 'builddashboard' build.pk%}">
-            <span data-toggle="tooltip" {%if build.target_set.all.count > 1%}title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|naturaltime}})</span>
-            </a>
-        </div>
-{%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
-        <div class="span2 lead">
-{% if  build.errors_no %}
-            <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a>
-{% endif %}
-        </div>
-        <div class="span2 lead">
-{% if  build.warnings_no %}
-            <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>
-{% endif %}
-        </div>
-        <div class="lead pull-right">
-            Build time: <a href="build-time.html">{{ build|timespent }}</a>
-        </div>
-{%endif%}{%if build.outcome == build.IN_PROGRESS %}
-        <div class="span4">
-            <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete">
-                <div style="width: {{build.completeper}}%;" class="bar"></div>
+  {%if mru.count > 0%}
+  <div class="page-header" style="margin-top:40px;">
+      <h1>
+          Recent Builds
+       </h1>
+  </div>
+  {% for build in mru %}
+    <div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}">
+        <div class="row-fluid">
+            <div class="lead span5">
+                {%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}
+                <a href="{%url 'builddashboard' build.pk%}">
+                <span data-toggle="tooltip" {%if build.target_set.all.count > 1%}title="Targets: {%for target in build.target_set.all%}{{target.target}} {%endfor%}"{%endif%}>{{build.target_set.all.0.target}} {%if build.target_set.all.count > 1%}(+ {{build.target_set.all.count|add:"-1"}}){%endif%} {{build.machine}} ({{build.completed_on|naturaltime}})</span>
+                </a>
+            </div>
+    {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
+            <div class="span2 lead">
+    {% if  build.errors_no %}
+                <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a>
+    {% endif %}
+            </div>
+            <div class="span2 lead">
+    {% if  build.warnings_no %}
+                <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>
+    {% endif %}
             </div>
+            <div class="lead pull-right">
+                Build time: <a href="build-time.html">{{ build|timespent }}</a>
+            </div>
+    {%endif%}{%if build.outcome == build.IN_PROGRESS %}
+            <div class="span4">
+                <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete">
+                    <div style="width: {{build.completeper}}%;" class="bar"></div>
+                </div>
+            </div>
+            <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div>
+    {%endif%}
         </div>
-        <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div>
-{%endif%}
     </div>
-</div>
 
-{% endfor %}
+  {% endfor %}{%endif%}
 
-
-<div class="page-header" style="margin-top:40px;">
-    <h1>
-        All builds
+  <div class="page-header" style="margin-top:40px;">
+     <h1>
+      {% if request.GET.filter or request.GET.search and objects.ocount > 0  %}
+          {{objects.ocount}} build{{objects.ocount|pluralize}} found
+      {%elif objects.ocount == 0%}
+          No builds
+      {%else%}
+          All builds
+      {%endif%}
      </h1>
-</div>
+  </div>
 
-{% include "basetable_top.html" %}
+ {% if objects.ocount == 0 %}
+  <div class="row-fluid">
+      <div class="alert">
+      <form class="no-results">
+          <div class="input-append">
+              <input class="input-xxlarge" type="text" placeholder="{{request.GET.search}}" />
+              <input class="btn" type="submit" value="Search"/>
+              <button class="btn btn-link" onclick="javascript:reload_params({'search':'', 'filter':''})">Show all builds</button>
+          </div>
+      </form>
+      </div>
+  </div>
 
-        <tr>
-            <th class="outcome span2"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The outcome tells you if a build completed successfully or failed"></i> <a href="#" style="font-weight:normal;">Outcome</a> <div class="btn-group pull-right"> <a href="#outcome" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> </th>
-               <th class="target"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="This is the build target(s): one or more recipes or image recipes"></i> <a href="#" style="font-weight:normal;">Target</a> </th>
-            <th class="machine span3"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The machine is the hardware for which you are building"></i> <a href="#" style="font-weight:normal;">Machine</a> </th>
-            <th class="started_on"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The date and time you started the build"></i> <a href="#" style="font-weight:normal;">Started on</a> <div class="btn-group pull-right"> <a href="#started-on" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> </th>
-            <th class="completed_on"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The date and time the build finished"></i> <a href="#" class="sorted"> Completed on </a> <div class="btn-group pull-right"> <a href="#completed-on" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> <i class="icon-caret-down"></i> </th>
-            <th class="failed_tasks"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How many tasks failed during the build"></i> <a href="#" style="font-weight:normal;">Failed tasks</a> <div class="btn-group pull-right"> <a href="#failed-tasks" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> <!--div id="filtered" class="btn-group pull-right" title="<p>Showing only builds with failed tasks</p><p><a class='btn btn-mini btn-primary' href='#'>Show all builds</a></p>"> <a class="btn btn-mini btn-primary"> <i class="icon-filter"></i> </a> </div-->   </th>
-            <th class="errors"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How many errors were encountered during the build (if any)"></i> <a href="#" style="font-weight:normal;">Errors</a> <div class="btn-group pull-right"> <a href="#errors" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> </th>
-            <th class="warnings"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How many warnigns were encountered during the build (if any)"></i> <a href="#" style="font-weight:normal;">Warnings</a> <div class="btn-group pull-right"> <a href="#warnings" role="button" class="btn btn-mini" data-toggle="modal"> <i class="icon-filter"></i> </a> </div> <!--div id="filtered" class="btn-group pull-right" title="<p>Showing only builds without warnings</p><p><a class='btn btn-mini btn-primary' href='#'>Show all builds</a></p>"> <a class="btn btn-mini btn-primary"> <i class="icon-filter"></i> </a> </div--> </th>
-            <th class="time"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="How long it took the build to finish"></i> <a href="#" style="font-weight:normal;">Time</a> </th>
-            <th class="log span4">    <i class="icon-question-sign get-help" data-toggle="tooltip" title="The location in disk of the build main log file"></i> <a href="#" style="font-weight:normal;">Log</a> </th>
-            <th class="output"> <i class="icon-question-sign get-help" data-toggle="tooltip" title="The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory"></i> <a href="#" style="font-weight:normal;">Output</a> </th>
 
-        </tr>
+{% else %}
+{% include "basetable_top.html" %}
+        <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work -->
         {% for build in objects %}
         <tr class="data">
             <td class="outcome"><a href="{% url "builddashboard" build.id %}">{%if build.outcome == build.SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif build.outcome == build.FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a></td>
             <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td>
             <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on}}</a></td>
             <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on}}</a></td>
-            <td class="failed_tasks"></td>
-            <td class="errors">{% if  build.errors_no %}<a class="error" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a>{%endif%}</td>
-            <td class="warnings">{% if  build.warnings_no %}<a class="warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td>
+            <td class="failed_tasks">{% query build.task_build outcome=4 order__gt=0 as exectask%}{% if exectask.count == 1 %}{{exectask.0.recipe.name}}.{{exectask.0.task_name}}{% elif exectask.count > 1%}{{exectask.count}}{%endif%}</td>
+            <td class="errors_no">{% if  build.errors_no %}<a class="errors_no" href="{% url "builddashboard" build.id %}#errors">{{build.errors_no}} error{{build.errors_no|pluralize}}</a>{%endif%}</td>
+            <td class="warnings_no">{% if  build.warnings_no %}<a class="warnings_no" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>{%endif%}</td>
             <td class="time"><a href="{% url "buildtime" build.id %}">{{build|timespent}}</a></td>
-            <td class="log">{{build.log}}</td>
+            <td class="log">{{build.cooker_log_path}}</td>
             <td class="output">{% if build.outcome == 0 %}{% for t in build.target_set.all %}{% if t.is_image %}<a href="{%url "builddashboard" build.id%}#images">{{build.image_fstypes}}</a>{% endif %}{% endfor %}{% endif %}</td>
         </tr>
 
@@ -91,5 +98,7 @@
 
 {% include "basetable_bottom.html" %}
 
-</div>
+{% endif %}
+</div><!-- end row-fluid-->
+
 {% endblock %}
index e390a95ff5ddf876eb01c37d1422cd2f5eecf50d..467fbd02ad3ac080c0bc3bd85f70c3bb24736940 100644 (file)
@@ -4,25 +4,54 @@
 {% endblock %}
 
 {% block buildinfomain %}
+<!-- page title -->
+<div class="row-fluid span10">
+ <div class="page-header">
+     <h1>Configuration</h1>
+ </div>
+</div>
 
-{% include "basetable_top.html" %}
+<!-- configuration table -->
+<div class="row-fluid pull-right span10" id="navTab">
+<ul class="nav nav-pills">
+    <li class="active"><a href="#">Summary</a></li>
+    <li class=""><a href="{% url 'configvars' build.id %}">BitBake variables</a></li>
+</ul>
 
-            <tr>
-            <th>Name</th>
-            <th>Description</th>
-            <th>Definition history</th>
-            <th>Value</th>
-            </tr>
+  <!-- summary -->
+  <div id="summary" class="tab-pane active">
+    <h3>Build configuration</h3>
+    <dl class="dl-horizontal">
+      <dt>BitBake version</dt><dd>1.19.1</dd>
+      <dt>Build system</dt><dd>x86_64-linux</dd>
+      <dt>Host distribution</dt><dd>Ubuntu-12.04</dd>
+      <dt>Target system</dt><dd>i586-poky-linux</dd>
+      <dt><i class="icon-question-sign get-help" data-toggle="tooltip" title="Specifies the target device for which the image is built"></i> Machine</dt><dd>atom-pc</dd>
+      <dt><i class="icon-question-sign get-help" data-toggle="tooltip" title="The short name of the distribution"></i> Distro</dt><dd>poky</dd>
+      <dt>Distro version</dt><dd>1.4+snapshot-20130718</dd>
+      <dt>Tune features</dt><dd>m32 i586</dd>
+      <dt>Target(s)</dt><dd>core-image-sato</dd>
+    </dl>
+    <h3>Layers</h3>
+    <div class="span9" style="margin-left:0px;">
+    <table class="table table-bordered table-hover">
+      <thead>
+        <tr>
+          <th>Layer</th>
+          <th>Layer branch</th>
+          <th>Layer commit</th>
+          <th>Layer directory</th>
+        </tr>
+      </thead>
+      <tbody>{% for lv in build.layer_version_build.all %}
+        <tr>
+            <td>{{lv.layer.name}}<a href="{{lv.layer.layer_index_url}}" target="_blank">&nbsp;<i class="icon-share get-info"></i></a></td><td>{{lv.branch}}</td><td class="layer_commit"><a data-content="{{lv.commit}}" title="" href="#" class="btn" data-original-title="">{{lv.commit|slice:":8"}}...</a></td><td>{{lv.layer.local_path}}</td>
+        </tr>{% endfor %}
+      </tbody>
+    </table>
+    </div>
+  </div>
 
-            {% for variable in objects %}
-
-            <tr class="data">
-                <td>{{variable.variable_name}}</td>
-                <td>{% if variable.description %}{{variable.description}}{% endif %}</td>
-                <td>{% for vh in variable.variablehistory_set.all %}{{vh.operation}} in {{vh.file_name}}:{{vh.line_number}}<br/>{%endfor%}</td>
-                <td>{{variable.variable_value}}</td>
-            {% endfor %}
-
-{% include "basetable_bottom.html" %}
 
+</div>
 {% endblock %}
diff --git a/lib/toaster/toastergui/templates/configvars.html b/lib/toaster/toastergui/templates/configvars.html
new file mode 100644 (file)
index 0000000..8ce04b8
--- /dev/null
@@ -0,0 +1,40 @@
+{% extends "basebuildpage.html" %}
+{% block localbreadcrumb %}
+<li>Configuration</li>
+{% endblock %}
+
+{% block buildinfomain %}
+<!-- page title -->
+<div class="row-fluid span10">
+ <div class="page-header">
+     <h1>Configuration</h1>
+ </div>
+</div>
+
+<!-- configuration table -->
+<div class="row-fluid pull-right span10" id="navTab">
+<ul class="nav nav-pills">
+    <li class=""><a href="{% url 'configuration' build.id %}">Summary</a></li>
+    <li class="active"><a href="#" >BitBake variables</a></li>
+</ul>
+
+
+  <!-- variables -->
+  <div id="variables" class="tab-pane">
+{% include "basetable_top.html" %}
+
+{% for variable in objects %}
+   <tr class="data">
+        <td class="variable">{{variable.variable_name}}</td>
+        <td class="variable_value">{{variable.variable_value}}</td>
+        <td class="file">{% for vh in variable.variablehistory_set.all %}{{vh.operation}} in {{vh.file_name}}:{{vh.line_number}}<br/>{%endfor%}</td>
+        <td class="description">{% if variable.description %}{{variable.description}}{% endif %}</td>
+    </tr>
+{% endfor %}
+
+{% include "basetable_bottom.html" %}
+
+  </div> <!-- endvariables -->
+
+</div>
+{% endblock %}
diff --git a/lib/toaster/toastergui/templates/filtersnippet.html b/lib/toaster/toastergui/templates/filtersnippet.html
new file mode 100644 (file)
index 0000000..26ff675
--- /dev/null
@@ -0,0 +1,19 @@
+
+ <!-- '{{f.class}}' filter -->
+ <form id="filter_{{f.class}}" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
+        <input type="hidden" name="search" value="{{request.GET.search}}"/>
+        <div class="modal-header">
+            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
+            <h3>Filter builds by {{tc.name}}</h3>
+        </div>
+        <div class="modal-body">
+            <label>{{f.label}}</label>
+            <select name="filter">
+                <option value="">No Filter</option>{% for key, value in f.options.items %}
+                <option {%if request.GET.filter == value %}selected="" {%endif%}value="{{value}}">{{key}}</option>{% endfor %}
+            </select>
+        </div>
+        <div class="modal-footer">
+            <button type="submit" class="btn btn-primary disabled">Apply</button>
+        </div>
+  </form>
index 14550267545abf764a0ce9ec36a33e522caa0094..15a1757b35917e239d1a21188f1d125e131a85b3 100644 (file)
@@ -16,8 +16,9 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
-from datetime import datetime
+from datetime import datetime, timedelta
 from django import template
+from django.utils import timezone
 
 register = template.Library()
 
@@ -42,8 +43,14 @@ def query(qs, **kwargs):
 
 @register.filter
 def divide(value, arg):
+    if int(arg) == 0:
+        return -1
     return int(value) / int(arg)
 
 @register.filter
 def multiply(value, arg):
     return int(value) * int(arg)
+
+@register.assignment_tag
+def datecompute(delta, start = timezone.now()):
+    return start + timedelta(delta)
index f531eb01373b8932661b906bb2ca7a1c7a7049a0..585578316ab8a6432747de3f61144483cb4c7e11 100644 (file)
@@ -39,6 +39,7 @@ urlpatterns = patterns('toastergui.views',
         url(r'^build/(?P<build_id>\d+)/target/(?P<target_id>\d+)/packages$', 'tpackage', name='targetpackages'),
 
         url(r'^build/(?P<build_id>\d+)/configuration$', 'configuration', name='configuration'),
+        url(r'^build/(?P<build_id>\d+)/configvars$', 'configvars', name='configvars'),
         url(r'^build/(?P<build_id>\d+)/buildtime$', 'buildtime', name='buildtime'),
         url(r'^build/(?P<build_id>\d+)/cpuusage$', 'cpuusage', name='cpuusage'),
         url(r'^build/(?P<build_id>\d+)/diskio$', 'diskio', name='diskio'),
index 7d4d710f83379f842371ddea7edae336ab419926..09da9c2a2e71f25872ffd005b6853960ba47f672 100644 (file)
@@ -25,7 +25,10 @@ from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File
 from orm.models import Target_Installed_Package
 from django.views.decorators.cache import cache_control
 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-
+from django.http import HttpResponseBadRequest
+from django.utils import timezone
+from datetime import timedelta
+from django.utils import formats
 
 def _build_page_range(paginator, index = 1):
     try:
@@ -72,6 +75,109 @@ def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs):
 
     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(lambda x, y: x & y, map(lambda x: __get_q_for_val(k, querydict[k]),[k for k in querydict]))
+
+def _get_toggle_order(request, orderkey):
+    return "%s:-" % orderkey if request.GET.get('orderby', "") == "%s:+" % orderkey else "%s:+" % orderkey
+
+# 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"
+            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 _search_tuple(request, model):
+    ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model)
+    if invalid:
+        raise BaseException("Invalid ordering " + 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 _get_queryset(model, filter_string, search_term, ordering_string):
+    if filter_string:
+        filter_query = _get_filtering_query(filter_string)
+        queryset = model.objects.filter(filter_query)
+    else:
+        queryset = model.objects.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:
+            queryset = queryset.order_by('-' + column)
+        else:
+            queryset = queryset.order_by(column)
+
+    return queryset
 
 # shows the "all builds" page
 def builds(request):
@@ -84,16 +190,24 @@ def builds(request):
     if retval:
         return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters)
 
-    # retrieve the objects that will be displayed in the table
-    build_info = _build_page_range(Paginator(Build.objects.exclude(outcome = Build.IN_PROGRESS).order_by("-id"), request.GET.get('count', 10)),request.GET.get('page', 1))
+    # boilerplate code that takes a request for an object type and returns a queryset
+    # for that object type. copypasta for all needed table searches
+    (filter_string, search_term, ordering_string) = _search_tuple(request, Build)
+    queryset = _get_queryset(Build, filter_string, search_term, ordering_string)
+
+    # retrieve the objects that will be displayed in the table; builds a paginator and gets a page range to display
+    build_info = _build_page_range(Paginator(queryset.exclude(outcome = Build.IN_PROGRESS), request.GET.get('count', 10)),request.GET.get('page', 1))
 
-    # build view-specific information; this is rendered specifically in the builds page
-    build_mru = Build.objects.order_by("-started_on")[:3]
+    # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
+    build_mru = Build.objects.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).order_by("-started_on")[:3]
     for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]:
         tf = Task.objects.filter(build = b)
         b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count()
-        from django.utils import timezone
-        b.eta = timezone.now() + ((timezone.now() - b.started_on)*100/b.completeper)
+        b.eta = timezone.now()
+        if b.completeper > 0:
+            b.eta += ((timezone.now() - b.started_on)*100/b.completeper)
+        else:
+            b.eta = 0
 
     # send the data to the template
     context = {
@@ -101,19 +215,78 @@ def builds(request):
                 'mru' : build_mru,
             # TODO: common objects for all table views, adapt as needed
                 'objects' : build_info,
+            # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
                 'tablecols' : [
-                {'name': 'Target ', 'clclass': 'target',},
-                {'name': 'Machine ', 'clclass': 'machine'},
-                {'name': 'Completed on ', 'clclass': 'completed_on'},
-                {'name': 'Failed tasks ', 'clclass': 'failed_tasks'},
-                {'name': 'Errors ', 'clclass': 'errors_no'},
-                {'name': 'Warnings', 'clclass': 'warnings_no'},
-                {'name': 'Output ', 'clclass': 'output'},
-                {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1},
-                {'name': 'Time ', 'clclass': 'time', 'hidden' : 1},
-                {'name': 'Output', 'clclass': 'output'},
-                {'name': 'Log', 'clclass': 'log', 'hidden': 1},
-            ]}
+                {'name': 'Outcome ',                                                # column with a single filter
+                 'qhelp' : "The outcome tells you if a build completed successfully or failed",     # the help button content
+                 'dclass' : "span2",                                                # indication about column width; comes from the design
+                 'orderfield': _get_toggle_order(request, "outcome"),               # adds ordering by the field value; default ascending unless clicked from ascending into descending
+                  # filter field will set a filter on that column with the specs in the filter description
+                  # the class field in the filter has no relation with clclass; the control different aspects of the UI
+                  # still, it is recommended for the values to be identical for easy tracking in the generated HTML
+                 'filter' : {'class' : 'outcome', 'label': 'Show only', 'options' : {
+                        'Successful builds': 'outcome:' + str(Build.SUCCEEDED),  # this is the field search expression
+                        'Failed builds': 'outcome:'+ str(Build.FAILED),
+                    }
+                  }
+                },
+                {'name': 'Target ',                                                 # default column, disabled box, with just the name in the list
+                 'qhelp': "This is the build target(s): one or more recipes or image recipes",
+                 'orderfield': _get_toggle_order(request, "target__target"),
+                },
+                {'name': 'Machine ',
+                 'qhelp': "The machine is the hardware for which you are building",
+                 'dclass': 'span3'},                           # a slightly wider column
+                {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1,      # this is an unchecked box, which hides the column
+                 'qhelp': "The date and time you started the build",
+                  'filter' : {'class' : 'started_on', 'label': 'Show only builds started', 'options' : {
+                        'Today' : 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"),
+                        'Yesterday' : 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"),
+                        'Within one week' : 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"),
+                    }}
+                },
+                {'name': 'Completed on ',
+                 'qhelp': "The date and time the build finished",
+                 'orderfield': _get_toggle_order(request, "completed_on"),
+                 'filter' : {'class' : 'completed_on', 'label': 'Show only builds completed', 'options' : {
+                        'Today' : 'completed_on__gte:'+timezone.now().strftime("%Y-%m-%d"),
+                        'Yesterday' : 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"),
+                        'Within one week' : 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"),
+                    }}
+                },
+                {'name': 'Failed tasks ', 'clclass': 'failed_tasks',                # specifing a clclass will enable the checkbox
+                 'qhelp': "How many tasks failed during the build",
+                 'filter' : {'class' : 'failed_tasks', 'label': 'Show only ', 'options' : {
+                        'Builds with failed tasks' : 'task_build__outcome:4',
+                        'Builds without failed tasks' : 'task_build__outcome:NOT4',
+                    }}
+                },
+                {'name': 'Errors ', 'clclass': 'errors_no',
+                 'qhelp': "How many errors were encountered during the build (if any)",
+                 'orderfield': _get_toggle_order(request, "errors_no"),
+                 'filter' : {'class' : 'errors_no', 'label': 'Show only ', 'options' : {
+                        'Builds with errors' : 'errors_no__gte:1',
+                        'Builds without errors' : 'errors_no:0',
+                    }}
+                },
+                {'name': 'Warnings', 'clclass': 'warnings_no',
+                 'qhelp': "How many warnigns were encountered during the build (if any)",
+                 'orderfield': _get_toggle_order(request, "warnings_no"),
+                 'filter' : {'class' : 'warnings_no', 'label': 'Show only ', 'options' : {
+                        'Builds with warnings' : 'warnings_no__gte:1',
+                        'Builds without warnings' : 'warnings_no:0',
+                    }}
+                },
+                {'name': 'Time ', 'clclass': 'time', 'hidden' : 1,
+                 'qhelp': "How long it took the build to finish",},
+                {'name': 'Log',
+                 'dclass': "span4",
+                 'qhelp': "The location in disk of the build main log file",
+                 'clclass': 'log', 'hidden': 1},
+                {'name': 'Output', 'clclass': 'output',
+                 'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory"},
+                ]
+            }
 
     return render(request, template, context)
 
@@ -191,8 +364,10 @@ def tasks(request, build_id):
     retval = _verify_parameters( request.GET, mandatory_parameters )
     if retval:
         return _redirect_parameters( 'tasks', request.GET, mandatory_parameters, build_id = build_id)
+    (filter_string, search_term, ordering_string) = _search_tuple(request, Task)
+    queryset = _get_queryset(Task, filter_string, search_term, ordering_string)
 
-    tasks = _build_page_range(Paginator(Task.objects.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1))
+    tasks = _build_page_range(Paginator(queryset.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1))
 
     for t in tasks:
         if t.outcome == Task.OUTCOME_COVERED:
@@ -208,8 +383,10 @@ def recipes(request, build_id):
     retval = _verify_parameters( request.GET, mandatory_parameters )
     if retval:
         return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id)
+    (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe)
+    queryset = _get_queryset(Recipe, filter_string, search_term, ordering_string)
 
-    recipes = _build_page_range(Paginator(Recipe.objects.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)), request.GET.get('count', 100)),request.GET.get('page', 1))
+    recipes = _build_page_range(Paginator(queryset.filter(layer_version__id__in=Layer_Version.objects.filter(build=build_id)), request.GET.get('count', 100)),request.GET.get('page', 1))
 
     context = {'build': Build.objects.filter(pk=build_id)[0], 'objects': recipes, }
 
@@ -218,15 +395,63 @@ def recipes(request, build_id):
 
 def configuration(request, build_id):
     template = 'configuration.html'
+    context = {'build': Build.objects.filter(pk=build_id)[0]}
+    return render(request, template, context)
+
+
+def configvars(request, build_id):
+    template = 'configvars.html'
     mandatory_parameters = { 'count': 100,  'page' : 1};
     retval = _verify_parameters( request.GET, mandatory_parameters )
     if retval:
-        return _redirect_parameters( 'configuration', request.GET, mandatory_parameters, build_id = build_id)
+        return _redirect_parameters( 'configvars', request.GET, mandatory_parameters, build_id = build_id)
+
+    (filter_string, search_term, ordering_string) = _search_tuple(request, Variable)
+    queryset = _get_queryset(Variable, filter_string, search_term, ordering_string)
+
+    variables = _build_page_range(Paginator(queryset.filter(build=build_id), request.GET.get('count', 50)), request.GET.get('page', 1))
+
+    context = {
+                'build': Build.objects.filter(pk=build_id)[0],
+                'objects' : variables,
+            # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
+                'tablecols' : [
+                {'name': 'Variable ',
+                 'qhelp': "Base variable expanded name",
+                 'clclass' : 'variable',
+                 'dclass' : "span3",
+                 'orderfield': _get_toggle_order(request, "variable_name"),
+                },
+                {'name': 'Value ',
+                 'qhelp': "The value assigned to the variable",
+                 'clclass': 'variable_value',
+                 'dclass': "span4",
+                 'orderfield': _get_toggle_order(request, "variable_value"),
+                },
+                {'name': 'Configuration file(s) ',
+                 'qhelp': "The configuration file(s) that touched the variable value",
+                 'clclass': 'file',
+                 'dclass': "span6",
+                 'orderfield': _get_toggle_order(request, "variable_vhistory__file_name"),
+                 'filter' : { 'class': 'file', 'label' : 'Show only', 'options' : {
+                        }
+                 }
+                },
+                {'name': 'Description ',
+                 'qhelp': "A brief explanation of a variable",
+                 'clclass': 'description',
+                 'dclass': "span5",
+                 'orderfield': _get_toggle_order(request, "description"),
+                 'filter' : { 'class' : 'description', 'label' : 'No', 'options' : {
+                        }
+                 },
+                }
+                ]
+            }
 
-    variables = _build_page_range(Paginator(Variable.objects.filter(build=build_id), 50), request.GET.get('page', 1))
-    context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : variables}
     return render(request, template, context)
 
+
 def buildtime(request, build_id):
     template = "buildtime.html"
     if Build.objects.filter(pk=build_id).count() == 0 :
@@ -263,8 +488,10 @@ def bpackage(request, build_id):
     retval = _verify_parameters( request.GET, mandatory_parameters )
     if retval:
         return _redirect_parameters( 'packages', request.GET, mandatory_parameters, build_id = build_id)
+    (filter_string, search_term, ordering_string) = _search_tuple(request, Package)
+    queryset = _get_queryset(Package, filter_string, search_term, ordering_string)
 
-    packages = _build_page_range(Paginator(Package.objects.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1))
+    packages = _build_page_range(Paginator(queryset.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1))
 
     context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : packages}
     return render(request, template, context)
@@ -305,139 +532,4 @@ def layer_versions_recipes(request, layerversion_id):
 
     return render(request, template, context)
 
-#### API
-
-import json
-from django.core import serializers
-from django.http import HttpResponse, HttpResponseBadRequest
-
-
-def model_explorer(request, model_name):
-
-    DESCENDING = 'desc'
-    response_data = {}
-    model_mapping = {
-        'build': Build,
-        'target': Target,
-        'task': Task,
-        'task_dependency': Task_Dependency,
-        'package': Package,
-        'layer': Layer,
-        'layerversion': Layer_Version,
-        'recipe': Recipe,
-        'recipe_dependency': Recipe_Dependency,
-        'package': Package,
-        'package_dependency': Package_Dependency,
-        'build_file': Package_File,
-        'variable': Variable,
-        'logmessage': LogMessage,
-        }
-
-    if model_name not in model_mapping.keys():
-        return HttpResponseBadRequest()
-
-    model = model_mapping[model_name]
-
-    try:
-        limit = int(request.GET.get('limit', 0))
-    except ValueError:
-        limit = 0
-
-    try:
-        offset = int(request.GET.get('offset', 0))
-    except ValueError:
-        offset = 0
-
-    ordering_string, invalid = _validate_input(request.GET.get('orderby', ''),
-                                               model)
-    if invalid:
-        return HttpResponseBadRequest()
-
-    filter_string, invalid = _validate_input(request.GET.get('filter', ''),
-                                             model)
-    if invalid:
-        return HttpResponseBadRequest()
-
-    search_term = request.GET.get('search', '')
-
-    if filter_string:
-        filter_terms = _get_filtering_terms(filter_string)
-        try:
-            queryset = model.objects.filter(**filter_terms)
-        except ValueError:
-            queryset = []
-    else:
-        queryset = model.objects.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:
-            queryset = queryset.order_by('-' + column)
-        else:
-            queryset = queryset.order_by(column)
-
-    if offset and limit:
-        queryset = queryset[offset:(offset+limit)]
-    elif offset:
-        queryset = queryset[offset:]
-    elif limit:
-        queryset = queryset[:limit]
-
-    if queryset:
-        response_data['count'] = queryset.count()
-    else:
-        response_data['count'] = 0
-    response_data['list'] = serializers.serialize('json', queryset)
-#    response_data = serializers.serialize('json', queryset)
-
-    return HttpResponse(json.dumps(response_data),
-                        content_type='application/json')
-
-def _get_filtering_terms(filter_string):
-
-    search_terms = filter_string.split(":")
-    keys = search_terms[0].split(',')
-    values = search_terms[1].split(',')
-
-    return dict(zip(keys, values))
-
-def _validate_input(input, model):
-
-    invalid = 0
-
-    if input:
-        input_list = input.split(":")
-
-        # Check we have only one colon
-        if len(input_list) != 2:
-            invalid = 1
-            return None, invalid
-
-        # Check we have an equal number of terms both sides of the colon
-        if len(input_list[0].split(',')) != len(input_list[1].split(',')):
-            invalid = 1
-            return None, invalid
-
-        # Check we are looking for a valid field
-        valid_fields = model._meta.get_all_field_names()
-        for field in input_list[0].split(','):
-            if field not in valid_fields:
-                invalid = 1
-                return None, invalid
-
-    return input, invalid
-
-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