summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexandru DAMIAN <alexandru.damian@intel.com>2014-01-07 13:10:42 +0000
committerPaul Eggleton <paul.eggleton@linux.intel.com>2014-01-10 14:13:53 +0000
commit2ca15117e4bbda38cda07511d0ff317273f91528 (patch)
treedb0342b2d339e9cb2369b2427134d221a3e8f142
parentbf7fbf5c0ee39564d813f82e194242f9d4f73c47 (diff)
downloadbitbake-2ca15117e4bbda38cda07511d0ff317273f91528.tar.gz
toaster: Toaster GUI, generic search, filter and order
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>
-rw-r--r--lib/toaster/orm/models.py10
-rw-r--r--lib/toaster/toastergui/static/css/default.css5
-rw-r--r--lib/toaster/toastergui/templates/basetable_bottom.html13
-rw-r--r--lib/toaster/toastergui/templates/basetable_top.html79
-rw-r--r--lib/toaster/toastergui/templates/build.html129
-rw-r--r--lib/toaster/toastergui/templates/configuration.html63
-rw-r--r--lib/toaster/toastergui/templates/configvars.html40
-rw-r--r--lib/toaster/toastergui/templates/filtersnippet.html19
-rw-r--r--lib/toaster/toastergui/templatetags/projecttags.py9
-rw-r--r--lib/toaster/toastergui/urls.py1
-rw-r--r--lib/toaster/toastergui/views.py412
11 files changed, 499 insertions, 281 deletions
diff --git a/lib/toaster/orm/models.py b/lib/toaster/orm/models.py
index b30e405c0..ff26c7d43 100644
--- a/lib/toaster/orm/models.py
+++ b/lib/toaster/orm/models.py
@@ -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)
diff --git a/lib/toaster/toastergui/static/css/default.css b/lib/toaster/toastergui/static/css/default.css
index 844f6dcd5..53c50043b 100644
--- a/lib/toaster/toastergui/static/css/default.css
+++ b/lib/toaster/toastergui/static/css/default.css
@@ -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; }
diff --git a/lib/toaster/toastergui/templates/basetable_bottom.html b/lib/toaster/toastergui/templates/basetable_bottom.html
index 00703fe4c..3e4b0cc5a 100644
--- a/lib/toaster/toastergui/templates/basetable_bottom.html
+++ b/lib/toaster/toastergui/templates/basetable_bottom.html
@@ -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 -->
diff --git a/lib/toaster/toastergui/templates/basetable_top.html b/lib/toaster/toastergui/templates/basetable_top.html
index b9277b4a3..34e0cd721 100644
--- a/lib/toaster/toastergui/templates/basetable_top.html
+++ b/lib/toaster/toastergui/templates/basetable_top.html
@@ -21,46 +21,53 @@
<!-- 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>
diff --git a/lib/toaster/toastergui/templates/build.html b/lib/toaster/toastergui/templates/build.html
index 43b491d55..eb7e03c95 100644
--- a/lib/toaster/toastergui/templates/build.html
+++ b/lib/toaster/toastergui/templates/build.html
@@ -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>
@@ -78,11 +85,11 @@
<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 %}
diff --git a/lib/toaster/toastergui/templates/configuration.html b/lib/toaster/toastergui/templates/configuration.html
index e390a95ff..467fbd02a 100644
--- a/lib/toaster/toastergui/templates/configuration.html
+++ b/lib/toaster/toastergui/templates/configuration.html
@@ -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
index 000000000..8ce04b883
--- /dev/null
+++ b/lib/toaster/toastergui/templates/configvars.html
@@ -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
index 000000000..26ff67563
--- /dev/null
+++ b/lib/toaster/toastergui/templates/filtersnippet.html
@@ -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>
diff --git a/lib/toaster/toastergui/templatetags/projecttags.py b/lib/toaster/toastergui/templatetags/projecttags.py
index 145502675..15a1757b3 100644
--- a/lib/toaster/toastergui/templatetags/projecttags.py
+++ b/lib/toaster/toastergui/templatetags/projecttags.py
@@ -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)
diff --git a/lib/toaster/toastergui/urls.py b/lib/toaster/toastergui/urls.py
index f531eb013..585578316 100644
--- a/lib/toaster/toastergui/urls.py
+++ b/lib/toaster/toastergui/urls.py
@@ -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'),
diff --git a/lib/toaster/toastergui/views.py b/lib/toaster/toastergui/views.py
index 7d4d710f8..09da9c2a2 100644
--- a/lib/toaster/toastergui/views.py
+++ b/lib/toaster/toastergui/views.py
@@ -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