summaryrefslogtreecommitdiffstats
path: root/bitbake
diff options
context:
space:
mode:
authorAlexandru DAMIAN <alexandru.damian@intel.com>2014-01-07 13:10:42 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2014-01-10 15:20:26 +0000
commit1b636173ca88e5ccca1992f9a12367a1189fa674 (patch)
tree0220e98e7b7a4027fb8c146bab9b3f81306fc9fe /bitbake
parent5482409a370552809de75150350defef04ac7144 (diff)
downloadpoky-1b636173ca88e5ccca1992f9a12367a1189fa674.tar.gz
bitbake: 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] (Bitbake rev: 2ca15117e4bbda38cda07511d0ff317273f91528) Signed-off-by: Alexandru DAMIAN <alexandru.damian@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake')
-rw-r--r--bitbake/lib/toaster/orm/models.py10
-rw-r--r--bitbake/lib/toaster/toastergui/static/css/default.css5
-rw-r--r--bitbake/lib/toaster/toastergui/templates/basetable_bottom.html13
-rw-r--r--bitbake/lib/toaster/toastergui/templates/basetable_top.html79
-rw-r--r--bitbake/lib/toaster/toastergui/templates/build.html129
-rw-r--r--bitbake/lib/toaster/toastergui/templates/configuration.html63
-rw-r--r--bitbake/lib/toaster/toastergui/templates/configvars.html40
-rw-r--r--bitbake/lib/toaster/toastergui/templates/filtersnippet.html19
-rw-r--r--bitbake/lib/toaster/toastergui/templatetags/projecttags.py9
-rw-r--r--bitbake/lib/toaster/toastergui/urls.py1
-rw-r--r--bitbake/lib/toaster/toastergui/views.py412
11 files changed, 499 insertions, 281 deletions
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index b30e405c0e..ff26c7d436 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -31,8 +31,8 @@ class Build(models.Model):
31 (IN_PROGRESS, 'In Progress'), 31 (IN_PROGRESS, 'In Progress'),
32 ) 32 )
33 33
34 search_allowed_fields = ['machine', 34 search_allowed_fields = ['machine', 'image_fstypes',
35 'cooker_log_path'] 35 'cooker_log_path', "target__target"]
36 36
37 machine = models.CharField(max_length=100) 37 machine = models.CharField(max_length=100)
38 image_fstypes = models.CharField(max_length=100) 38 image_fstypes = models.CharField(max_length=100)
@@ -102,6 +102,8 @@ class Task(models.Model):
102 (OUTCOME_NA, 'Not Available'), 102 (OUTCOME_NA, 'Not Available'),
103 ) 103 )
104 104
105 search_allowed_fields = [ "recipe__name", "task_name" ]
106
105 build = models.ForeignKey(Build, related_name='task_build') 107 build = models.ForeignKey(Build, related_name='task_build')
106 order = models.IntegerField(null=True) 108 order = models.IntegerField(null=True)
107 task_executed = models.BooleanField(default=False) # True means Executed, False means Prebuilt 109 task_executed = models.BooleanField(default=False) # True means Executed, False means Prebuilt
@@ -217,6 +219,8 @@ class Layer_Version(models.Model):
217 219
218 220
219class Variable(models.Model): 221class Variable(models.Model):
222 search_allowed_fields = ['variable_name', 'variable_value',
223 'variablehistory__file_name', "description"]
220 build = models.ForeignKey(Build, related_name='variable_build') 224 build = models.ForeignKey(Build, related_name='variable_build')
221 variable_name = models.CharField(max_length=100) 225 variable_name = models.CharField(max_length=100)
222 variable_value = models.TextField(blank=True) 226 variable_value = models.TextField(blank=True)
@@ -225,7 +229,7 @@ class Variable(models.Model):
225 description = models.TextField(blank=True) 229 description = models.TextField(blank=True)
226 230
227class VariableHistory(models.Model): 231class VariableHistory(models.Model):
228 variable = models.ForeignKey(Variable) 232 variable = models.ForeignKey(Variable, related_name='vhistory')
229 file_name = models.FilePathField(max_length=255) 233 file_name = models.FilePathField(max_length=255)
230 line_number = models.IntegerField(null=True) 234 line_number = models.IntegerField(null=True)
231 operation = models.CharField(max_length=16) 235 operation = models.CharField(max_length=16)
diff --git a/bitbake/lib/toaster/toastergui/static/css/default.css b/bitbake/lib/toaster/toastergui/static/css/default.css
index 844f6dcd56..53c50043bc 100644
--- a/bitbake/lib/toaster/toastergui/static/css/default.css
+++ b/bitbake/lib/toaster/toastergui/static/css/default.css
@@ -171,4 +171,7 @@ dd p {line-height:20px;}
171.tooltip { z-index: 2000 !important; } /* this makes tooltips work inside modal dialogs */ 171.tooltip { z-index: 2000 !important; } /* this makes tooltips work inside modal dialogs */
172.tooltip code { background-color:transparent; color:#FFFFFF; font-weight:normal; border:none; font-size: 1em; } 172.tooltip code { background-color:transparent; color:#FFFFFF; font-weight:normal; border:none; font-size: 1em; }
173.manual { margin-top:11px;} 173.manual { margin-top:11px;}
174.heading-help { font-size:14px;} \ No newline at end of file 174.heading-help { font-size:14px;}
175
176
177.no-results { margin: 10px 0 0; }
diff --git a/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html b/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html
index 00703fe4c1..3e4b0cc5a4 100644
--- a/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html
+++ b/bitbake/lib/toaster/toastergui/templates/basetable_bottom.html
@@ -1,3 +1,4 @@
1 </tbody>
1 </table> 2 </table>
2 3
3<!-- Show pagination controls --> 4<!-- Show pagination controls -->
@@ -8,15 +9,15 @@
8 9
9 <ul class="pagination" style="display: block-inline"> 10 <ul class="pagination" style="display: block-inline">
10{%if objects.has_previous %} 11{%if objects.has_previous %}
11 <li><a href="?page={{objects.previous_page_number}}&count={{request.GET.count}}">&laquo;</a></li> 12 <li><a href="javascript:reload_params({'page':{{objects.previous_page_number}}})">&laquo;</a></li>
12{%else%} 13{%else%}
13 <li class="disabled"><a href="#">&laquo;</a></li> 14 <li class="disabled"><a href="#">&laquo;</a></li>
14{%endif%} 15{%endif%}
15{% for i in objects.page_range %} 16{% for i in objects.page_range %}
16 <li{%if i == objects.number %} class="active" {%endif%}><a href="?page={{i}}&count={{request.GET.count}}">{{i}}</a></li> 17 <li{%if i == objects.number %} class="active" {%endif%}><a href="javascript:reload_params({'page':{{i}}})">{{i}}</a></li>
17{% endfor %} 18{% endfor %}
18{%if objects.has_next%} 19{%if objects.has_next%}
19 <li><a href="?page={{objects.next_page_number}}&count={{request.GET.count}}">&raquo;</a></li> 20 <li><a href="javascript:reload_params({'page':{{objects.next_page_number}}})">&raquo;</a></li>
20{%else%} 21{%else%}
21 <li class="disabled"><a href="#">&raquo;</a></li> 22 <li class="disabled"><a href="#">&raquo;</a></li>
22{%endif%} 23{%endif%}
@@ -58,3 +59,9 @@
58 }); 59 });
59}); 60});
60</script> 61</script>
62
63<!-- modal filter boxes -->
64 {% for tc in tablecols %}{% if tc.filter %}{% with f=tc.filter %}
65 {% include "filtersnippet.html" %}
66 {% endwith %}{% endif %} {% endfor %}
67<!-- end modals -->
diff --git a/bitbake/lib/toaster/toastergui/templates/basetable_top.html b/bitbake/lib/toaster/toastergui/templates/basetable_top.html
index b9277b4a3d..34e0cd7210 100644
--- a/bitbake/lib/toaster/toastergui/templates/basetable_top.html
+++ b/bitbake/lib/toaster/toastergui/templates/basetable_top.html
@@ -21,46 +21,53 @@
21 21
22<!-- control header --> 22<!-- control header -->
23<div class="navbar"> 23<div class="navbar">
24 <div class="navbar-inner"> 24 <div class="navbar-inner">
25 <form class="navbar-search input-append pull-left"> 25 <form class="navbar-search input-append pull-left" >
26 <input class="input-xxlarge" type="text" placeholder="Search {{objectname}}" /> 26 <input class="input-xxlarge" name="search" type="text" placeholder="Search {{objectname}}" value="{{request.GET.search}}"/>
27 <button class="btn" type="button">Search</button> 27 <input class="btn" type="submit" value="Search"/>
28 </form> 28 </form>
29 <div class="pull-right"> 29 <div class="pull-right">
30 30{% if tablecols %}
31 {% if tablecols %} 31 <div class="btn-group">
32 <div class="btn-group"> 32 <button class="btn dropdown-toggle" data-toggle="dropdown">Edit columns
33 <button class="btn dropdown-toggle" data-toggle="dropdown"> 33 <span class="caret"></span>
34 Edit columns 34 </button>
35 <span class="caret"></span> 35 <ul class="dropdown-menu">{% for i in tablecols %}
36 </button> 36 <li>
37 <ul class="dropdown-menu"> 37 <label class="checkbox">
38 38 <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}}
39 {% for i in tablecols %} 39 </label>
40 <li> 40 </li>{% endfor %}
41 <label class="checkbox"> 41 </ul>
42<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}} 42 </div>
43 </label> 43{% endif %}
44 </li> 44 <div style="display:inline">
45 {% endfor %} 45 <span class="divider-vertical"></span>
46 </ul> 46 <span class="help-inline" style="padding-top:5px;">Show rows:</span>
47 </div> 47 <select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
48 {% endif %}
49
50 <div style="display:inline">
51 <span class="divider-vertical"></span>
52 <span class="help-inline" style="padding-top:5px;">Show rows:</span>
53 <select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
54 {% with "2 5 10 25 50 100" as list%} 48 {% with "2 5 10 25 50 100" as list%}
55 {% for i in list.split %}<option{%if i == request.GET.count %} selected{%endif%}>{{i}}</option> 49{% for i in list.split %} <option{%if i == request.GET.count %} selected{%endif%}>{{i}}</option>
56 {% endfor %} 50 {% endfor %}
57 {% endwith %} 51 {% endwith %}
58 </select> 52 </select>
59 </div> 53 </div>
60 </div> 54 </div>
61 </div> 55 </div> <!-- navbar-inner -->
62 </div> 56</div>
63 57
64<!-- the actual rows of the table --> 58<!-- the actual rows of the table -->
65 <table class="table table-bordered table-hover tablesorter" id="otable"> 59 <table class="table table-bordered table-hover tablesorter" id="otable">
60 <thead>
61 <!-- Table header row; generated from "tablecols" entry in the context dict -->
62 <tr>
63 {% for tc in tablecols %}<th class="{{tc.dclass}} {{tc.clclass}}">
64 {%if tc.qhelp%}<i class="icon-question-sign get-help" data-toggle="tooltip" title="{{tc.qhelp}}"></i>{%endif%}
65 <a href="javascript:reload_params({'orderby' : '{{tc.orderfield}}' })" style="font-weight:normal;">{{tc.name}}</a>
66 {%if tc.filter%}<div class="btn-group pull-right">
67 <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>
68 </div>{%endif%}
69 </th>{% endfor %}
70 </tr>
71 </thead>
72 <tbody>
66 73
diff --git a/bitbake/lib/toaster/toastergui/templates/build.html b/bitbake/lib/toaster/toastergui/templates/build.html
index 43b491d558..eb7e03c951 100644
--- a/bitbake/lib/toaster/toastergui/templates/build.html
+++ b/bitbake/lib/toaster/toastergui/templates/build.html
@@ -7,70 +7,77 @@
7{% block pagecontent %} 7{% block pagecontent %}
8<div class="row-fluid"> 8<div class="row-fluid">
9 9
10<div class="page-header" style="margin-top:40px;"> 10 {%if mru.count > 0%}
11 <h1> 11 <div class="page-header" style="margin-top:40px;">
12 Recent Builds 12 <h1>
13 </h1> 13 Recent Builds
14</div> 14 </h1>
15{% for build in mru %} 15 </div>
16<div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}"> 16 {% for build in mru %}
17 <div class="row-fluid"> 17 <div class="alert {%if build.outcome == build.SUCCEEDED%}alert-success{%elif build.outcome == build.FAILED%}alert-error{%else%}alert-info{%endif%}">
18 <div class="lead span5"> 18 <div class="row-fluid">
19 {%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%} 19 <div class="lead span5">
20 <a href="{%url 'builddashboard' build.pk%}"> 20 {%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%}
21 <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> 21 <a href="{%url 'builddashboard' build.pk%}">
22 </a> 22 <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>
23 </div> 23 </a>
24{%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %} 24 </div>
25 <div class="span2 lead"> 25 {%if build.outcome == build.SUCCEEDED or build.outcome == build.FAILED %}
26{% if build.errors_no %} 26 <div class="span2 lead">
27 <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a> 27 {% if build.errors_no %}
28{% endif %} 28 <i class="icon-minus-sign red"></i> <a href="{%url 'builddashboard' build.pk%}" class="error">{{build.errors_no}} error{{build.errors_no|pluralize}}</a>
29 </div> 29 {% endif %}
30 <div class="span2 lead"> 30 </div>
31{% if build.warnings_no %} 31 <div class="span2 lead">
32 <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a> 32 {% if build.warnings_no %}
33{% endif %} 33 <i class="icon-warning-sign yellow"></i> <a href="{%url 'builddashboard' build.pk%}" class="warning">{{build.warnings_no}} warning{{build.warnings_no|pluralize}}</a>
34 </div> 34 {% endif %}
35 <div class="lead pull-right">
36 Build time: <a href="build-time.html">{{ build|timespent }}</a>
37 </div>
38{%endif%}{%if build.outcome == build.IN_PROGRESS %}
39 <div class="span4">
40 <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete">
41 <div style="width: {{build.completeper}}%;" class="bar"></div>
42 </div> 35 </div>
36 <div class="lead pull-right">
37 Build time: <a href="build-time.html">{{ build|timespent }}</a>
38 </div>
39 {%endif%}{%if build.outcome == build.IN_PROGRESS %}
40 <div class="span4">
41 <div class="progress" style="margin-top:5px;" data-toggle="tooltip" title="{{build.completeper}}% of tasks complete">
42 <div style="width: {{build.completeper}}%;" class="bar"></div>
43 </div>
44 </div>
45 <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div>
46 {%endif%}
43 </div> 47 </div>
44 <div class="lead pull-right">ETA: in {{build.eta|naturaltime}}</div>
45{%endif%}
46 </div> 48 </div>
47</div>
48 49
49{% endfor %} 50 {% endfor %}{%endif%}
50 51
51 52 <div class="page-header" style="margin-top:40px;">
52<div class="page-header" style="margin-top:40px;"> 53 <h1>
53 <h1> 54 {% if request.GET.filter or request.GET.search and objects.ocount > 0 %}
54 All builds 55 {{objects.ocount}} build{{objects.ocount|pluralize}} found
56 {%elif objects.ocount == 0%}
57 No builds
58 {%else%}
59 All builds
60 {%endif%}
55 </h1> 61 </h1>
56</div> 62 </div>
57 63
58{% include "basetable_top.html" %} 64 {% if objects.ocount == 0 %}
65 <div class="row-fluid">
66 <div class="alert">
67 <form class="no-results">
68 <div class="input-append">
69 <input class="input-xxlarge" type="text" placeholder="{{request.GET.search}}" />
70 <input class="btn" type="submit" value="Search"/>
71 <button class="btn btn-link" onclick="javascript:reload_params({'search':'', 'filter':''})">Show all builds</button>
72 </div>
73 </form>
74 </div>
75 </div>
59 76
60 <tr>
61 <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>
62 <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>
63 <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>
64 <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>
65 <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>
66 <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>
67 <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>
68 <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>
69 <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>
70 <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>
71 <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>
72 77
73 </tr> 78{% else %}
79{% include "basetable_top.html" %}
80 <!-- 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 -->
74 {% for build in objects %} 81 {% for build in objects %}
75 <tr class="data"> 82 <tr class="data">
76 <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> 83 <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 @@
78 <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td> 85 <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td>
79 <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on}}</a></td> 86 <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on}}</a></td>
80 <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on}}</a></td> 87 <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on}}</a></td>
81 <td class="failed_tasks"></td> 88 <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>
82 <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> 89 <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>
83 <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> 90 <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>
84 <td class="time"><a href="{% url "buildtime" build.id %}">{{build|timespent}}</a></td> 91 <td class="time"><a href="{% url "buildtime" build.id %}">{{build|timespent}}</a></td>
85 <td class="log">{{build.log}}</td> 92 <td class="log">{{build.cooker_log_path}}</td>
86 <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> 93 <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>
87 </tr> 94 </tr>
88 95
@@ -91,5 +98,7 @@
91 98
92{% include "basetable_bottom.html" %} 99{% include "basetable_bottom.html" %}
93 100
94</div> 101{% endif %}
102</div><!-- end row-fluid-->
103
95{% endblock %} 104{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/configuration.html b/bitbake/lib/toaster/toastergui/templates/configuration.html
index e390a95ff5..467fbd02ad 100644
--- a/bitbake/lib/toaster/toastergui/templates/configuration.html
+++ b/bitbake/lib/toaster/toastergui/templates/configuration.html
@@ -4,25 +4,54 @@
4{% endblock %} 4{% endblock %}
5 5
6{% block buildinfomain %} 6{% block buildinfomain %}
7<!-- page title -->
8<div class="row-fluid span10">
9 <div class="page-header">
10 <h1>Configuration</h1>
11 </div>
12</div>
7 13
8{% include "basetable_top.html" %} 14<!-- configuration table -->
15<div class="row-fluid pull-right span10" id="navTab">
16<ul class="nav nav-pills">
17 <li class="active"><a href="#">Summary</a></li>
18 <li class=""><a href="{% url 'configvars' build.id %}">BitBake variables</a></li>
19</ul>
9 20
10 <tr> 21 <!-- summary -->
11 <th>Name</th> 22 <div id="summary" class="tab-pane active">
12 <th>Description</th> 23 <h3>Build configuration</h3>
13 <th>Definition history</th> 24 <dl class="dl-horizontal">
14 <th>Value</th> 25 <dt>BitBake version</dt><dd>1.19.1</dd>
15 </tr> 26 <dt>Build system</dt><dd>x86_64-linux</dd>
27 <dt>Host distribution</dt><dd>Ubuntu-12.04</dd>
28 <dt>Target system</dt><dd>i586-poky-linux</dd>
29 <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>
30 <dt><i class="icon-question-sign get-help" data-toggle="tooltip" title="The short name of the distribution"></i> Distro</dt><dd>poky</dd>
31 <dt>Distro version</dt><dd>1.4+snapshot-20130718</dd>
32 <dt>Tune features</dt><dd>m32 i586</dd>
33 <dt>Target(s)</dt><dd>core-image-sato</dd>
34 </dl>
35 <h3>Layers</h3>
36 <div class="span9" style="margin-left:0px;">
37 <table class="table table-bordered table-hover">
38 <thead>
39 <tr>
40 <th>Layer</th>
41 <th>Layer branch</th>
42 <th>Layer commit</th>
43 <th>Layer directory</th>
44 </tr>
45 </thead>
46 <tbody>{% for lv in build.layer_version_build.all %}
47 <tr>
48 <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>
49 </tr>{% endfor %}
50 </tbody>
51 </table>
52 </div>
53 </div>
16 54
17 {% for variable in objects %}
18
19 <tr class="data">
20 <td>{{variable.variable_name}}</td>
21 <td>{% if variable.description %}{{variable.description}}{% endif %}</td>
22 <td>{% for vh in variable.variablehistory_set.all %}{{vh.operation}} in {{vh.file_name}}:{{vh.line_number}}<br/>{%endfor%}</td>
23 <td>{{variable.variable_value}}</td>
24 {% endfor %}
25
26{% include "basetable_bottom.html" %}
27 55
56</div>
28{% endblock %} 57{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/configvars.html b/bitbake/lib/toaster/toastergui/templates/configvars.html
new file mode 100644
index 0000000000..8ce04b883d
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/configvars.html
@@ -0,0 +1,40 @@
1{% extends "basebuildpage.html" %}
2{% block localbreadcrumb %}
3<li>Configuration</li>
4{% endblock %}
5
6{% block buildinfomain %}
7<!-- page title -->
8<div class="row-fluid span10">
9 <div class="page-header">
10 <h1>Configuration</h1>
11 </div>
12</div>
13
14<!-- configuration table -->
15<div class="row-fluid pull-right span10" id="navTab">
16<ul class="nav nav-pills">
17 <li class=""><a href="{% url 'configuration' build.id %}">Summary</a></li>
18 <li class="active"><a href="#" >BitBake variables</a></li>
19</ul>
20
21
22 <!-- variables -->
23 <div id="variables" class="tab-pane">
24{% include "basetable_top.html" %}
25
26{% for variable in objects %}
27 <tr class="data">
28 <td class="variable">{{variable.variable_name}}</td>
29 <td class="variable_value">{{variable.variable_value}}</td>
30 <td class="file">{% for vh in variable.variablehistory_set.all %}{{vh.operation}} in {{vh.file_name}}:{{vh.line_number}}<br/>{%endfor%}</td>
31 <td class="description">{% if variable.description %}{{variable.description}}{% endif %}</td>
32 </tr>
33{% endfor %}
34
35{% include "basetable_bottom.html" %}
36
37 </div> <!-- endvariables -->
38
39</div>
40{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/filtersnippet.html b/bitbake/lib/toaster/toastergui/templates/filtersnippet.html
new file mode 100644
index 0000000000..26ff67563e
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/filtersnippet.html
@@ -0,0 +1,19 @@
1
2 <!-- '{{f.class}}' filter -->
3 <form id="filter_{{f.class}}" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
4 <input type="hidden" name="search" value="{{request.GET.search}}"/>
5 <div class="modal-header">
6 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
7 <h3>Filter builds by {{tc.name}}</h3>
8 </div>
9 <div class="modal-body">
10 <label>{{f.label}}</label>
11 <select name="filter">
12 <option value="">No Filter</option>{% for key, value in f.options.items %}
13 <option {%if request.GET.filter == value %}selected="" {%endif%}value="{{value}}">{{key}}</option>{% endfor %}
14 </select>
15 </div>
16 <div class="modal-footer">
17 <button type="submit" class="btn btn-primary disabled">Apply</button>
18 </div>
19 </form>
diff --git a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
index 1455026754..15a1757b35 100644
--- a/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
+++ b/bitbake/lib/toaster/toastergui/templatetags/projecttags.py
@@ -16,8 +16,9 @@
16# with this program; if not, write to the Free Software Foundation, Inc., 16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 18
19from datetime import datetime 19from datetime import datetime, timedelta
20from django import template 20from django import template
21from django.utils import timezone
21 22
22register = template.Library() 23register = template.Library()
23 24
@@ -42,8 +43,14 @@ def query(qs, **kwargs):
42 43
43@register.filter 44@register.filter
44def divide(value, arg): 45def divide(value, arg):
46 if int(arg) == 0:
47 return -1
45 return int(value) / int(arg) 48 return int(value) / int(arg)
46 49
47@register.filter 50@register.filter
48def multiply(value, arg): 51def multiply(value, arg):
49 return int(value) * int(arg) 52 return int(value) * int(arg)
53
54@register.assignment_tag
55def datecompute(delta, start = timezone.now()):
56 return start + timedelta(delta)
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index f531eb0137..585578316a 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -39,6 +39,7 @@ urlpatterns = patterns('toastergui.views',
39 url(r'^build/(?P<build_id>\d+)/target/(?P<target_id>\d+)/packages$', 'tpackage', name='targetpackages'), 39 url(r'^build/(?P<build_id>\d+)/target/(?P<target_id>\d+)/packages$', 'tpackage', name='targetpackages'),
40 40
41 url(r'^build/(?P<build_id>\d+)/configuration$', 'configuration', name='configuration'), 41 url(r'^build/(?P<build_id>\d+)/configuration$', 'configuration', name='configuration'),
42 url(r'^build/(?P<build_id>\d+)/configvars$', 'configvars', name='configvars'),
42 url(r'^build/(?P<build_id>\d+)/buildtime$', 'buildtime', name='buildtime'), 43 url(r'^build/(?P<build_id>\d+)/buildtime$', 'buildtime', name='buildtime'),
43 url(r'^build/(?P<build_id>\d+)/cpuusage$', 'cpuusage', name='cpuusage'), 44 url(r'^build/(?P<build_id>\d+)/cpuusage$', 'cpuusage', name='cpuusage'),
44 url(r'^build/(?P<build_id>\d+)/diskio$', 'diskio', name='diskio'), 45 url(r'^build/(?P<build_id>\d+)/diskio$', 'diskio', name='diskio'),
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 7d4d710f83..09da9c2a2e 100644
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -25,7 +25,10 @@ from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File
25from orm.models import Target_Installed_Package 25from orm.models import Target_Installed_Package
26from django.views.decorators.cache import cache_control 26from django.views.decorators.cache import cache_control
27from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 27from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
28 28from django.http import HttpResponseBadRequest
29from django.utils import timezone
30from datetime import timedelta
31from django.utils import formats
29 32
30def _build_page_range(paginator, index = 1): 33def _build_page_range(paginator, index = 1):
31 try: 34 try:
@@ -72,6 +75,109 @@ def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs):
72 75
73 return redirect(url + "?%s" % urllib.urlencode(params), *args, **kwargs) 76 return redirect(url + "?%s" % urllib.urlencode(params), *args, **kwargs)
74 77
78FIELD_SEPARATOR = ":"
79VALUE_SEPARATOR = ";"
80DESCENDING = "-"
81
82def __get_q_for_val(name, value):
83 if "OR" in value:
84 return reduce(operator.or_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("OR") ]))
85 if "AND" in value:
86 return reduce(operator.and_, map(lambda x: __get_q_for_val(name, x), [ x for x in value.split("AND") ]))
87 if value.startswith("NOT"):
88 kwargs = { name : value.strip("NOT") }
89 return ~Q(**kwargs)
90 else:
91 kwargs = { name : value }
92 return Q(**kwargs)
93
94def _get_filtering_query(filter_string):
95
96 search_terms = filter_string.split(FIELD_SEPARATOR)
97 keys = search_terms[0].split(VALUE_SEPARATOR)
98 values = search_terms[1].split(VALUE_SEPARATOR)
99
100 querydict = dict(zip(keys, values))
101 return reduce(lambda x, y: x & y, map(lambda x: __get_q_for_val(k, querydict[k]),[k for k in querydict]))
102
103def _get_toggle_order(request, orderkey):
104 return "%s:-" % orderkey if request.GET.get('orderby', "") == "%s:+" % orderkey else "%s:+" % orderkey
105
106# we check that the input comes in a valid form that we can recognize
107def _validate_input(input, model):
108
109 invalid = None
110
111 if input:
112 input_list = input.split(FIELD_SEPARATOR)
113
114 # Check we have only one colon
115 if len(input_list) != 2:
116 invalid = "We have an invalid number of separators"
117 return None, invalid
118
119 # Check we have an equal number of terms both sides of the colon
120 if len(input_list[0].split(VALUE_SEPARATOR)) != len(input_list[1].split(VALUE_SEPARATOR)):
121 invalid = "Not all arg names got values"
122 return None, invalid + str(input_list)
123
124 # Check we are looking for a valid field
125 valid_fields = model._meta.get_all_field_names()
126 for field in input_list[0].split(VALUE_SEPARATOR):
127 if not reduce(lambda x, y: x or y, map(lambda x: field.startswith(x), [ x for x in valid_fields ])):
128 return None, (field, [ x for x in valid_fields ])
129
130 return input, invalid
131
132# uses search_allowed_fields in orm/models.py to create a search query
133# for these fields with the supplied input text
134def _get_search_results(search_term, queryset, model):
135 search_objects = []
136 for st in search_term.split(" "):
137 q_map = map(lambda x: Q(**{x+'__icontains': st}),
138 model.search_allowed_fields)
139
140 search_objects.append(reduce(operator.or_, q_map))
141 search_object = reduce(operator.and_, search_objects)
142 queryset = queryset.filter(search_object)
143
144 return queryset
145
146
147# function to extract the search/filter/ordering parameters from the request
148# it uses the request and the model to validate input for the filter and orderby values
149def _search_tuple(request, model):
150 ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model)
151 if invalid:
152 raise BaseException("Invalid ordering " + str(invalid))
153
154 filter_string, invalid = _validate_input(request.GET.get('filter', ''), model)
155 if invalid:
156 raise BaseException("Invalid filter " + str(invalid))
157
158 search_term = request.GET.get('search', '')
159 return (filter_string, search_term, ordering_string)
160
161
162# returns a lazy-evaluated queryset for a filter/search/order combination
163def _get_queryset(model, filter_string, search_term, ordering_string):
164 if filter_string:
165 filter_query = _get_filtering_query(filter_string)
166 queryset = model.objects.filter(filter_query)
167 else:
168 queryset = model.objects.all()
169
170 if search_term:
171 queryset = _get_search_results(search_term, queryset, model)
172
173 if ordering_string and queryset:
174 column, order = ordering_string.split(':')
175 if order.lower() == DESCENDING:
176 queryset = queryset.order_by('-' + column)
177 else:
178 queryset = queryset.order_by(column)
179
180 return queryset
75 181
76# shows the "all builds" page 182# shows the "all builds" page
77def builds(request): 183def builds(request):
@@ -84,16 +190,24 @@ def builds(request):
84 if retval: 190 if retval:
85 return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters) 191 return _redirect_parameters( 'all-builds', request.GET, mandatory_parameters)
86 192
87 # retrieve the objects that will be displayed in the table 193 # boilerplate code that takes a request for an object type and returns a queryset
88 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)) 194 # for that object type. copypasta for all needed table searches
195 (filter_string, search_term, ordering_string) = _search_tuple(request, Build)
196 queryset = _get_queryset(Build, filter_string, search_term, ordering_string)
197
198 # retrieve the objects that will be displayed in the table; builds a paginator and gets a page range to display
199 build_info = _build_page_range(Paginator(queryset.exclude(outcome = Build.IN_PROGRESS), request.GET.get('count', 10)),request.GET.get('page', 1))
89 200
90 # build view-specific information; this is rendered specifically in the builds page 201 # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
91 build_mru = Build.objects.order_by("-started_on")[:3] 202 build_mru = Build.objects.filter(completed_on__gte=(timezone.now()-timedelta(hours=24))).order_by("-started_on")[:3]
92 for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]: 203 for b in [ x for x in build_mru if x.outcome == Build.IN_PROGRESS ]:
93 tf = Task.objects.filter(build = b) 204 tf = Task.objects.filter(build = b)
94 b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count() 205 b.completeper = tf.exclude(order__isnull=True).count()*100/tf.count()
95 from django.utils import timezone 206 b.eta = timezone.now()
96 b.eta = timezone.now() + ((timezone.now() - b.started_on)*100/b.completeper) 207 if b.completeper > 0:
208 b.eta += ((timezone.now() - b.started_on)*100/b.completeper)
209 else:
210 b.eta = 0
97 211
98 # send the data to the template 212 # send the data to the template
99 context = { 213 context = {
@@ -101,19 +215,78 @@ def builds(request):
101 'mru' : build_mru, 215 'mru' : build_mru,
102 # TODO: common objects for all table views, adapt as needed 216 # TODO: common objects for all table views, adapt as needed
103 'objects' : build_info, 217 'objects' : build_info,
218 # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
104 'tablecols' : [ 219 'tablecols' : [
105 {'name': 'Target ', 'clclass': 'target',}, 220 {'name': 'Outcome ', # column with a single filter
106 {'name': 'Machine ', 'clclass': 'machine'}, 221 'qhelp' : "The outcome tells you if a build completed successfully or failed", # the help button content
107 {'name': 'Completed on ', 'clclass': 'completed_on'}, 222 'dclass' : "span2", # indication about column width; comes from the design
108 {'name': 'Failed tasks ', 'clclass': 'failed_tasks'}, 223 'orderfield': _get_toggle_order(request, "outcome"), # adds ordering by the field value; default ascending unless clicked from ascending into descending
109 {'name': 'Errors ', 'clclass': 'errors_no'}, 224 # filter field will set a filter on that column with the specs in the filter description
110 {'name': 'Warnings', 'clclass': 'warnings_no'}, 225 # the class field in the filter has no relation with clclass; the control different aspects of the UI
111 {'name': 'Output ', 'clclass': 'output'}, 226 # still, it is recommended for the values to be identical for easy tracking in the generated HTML
112 {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1}, 227 'filter' : {'class' : 'outcome', 'label': 'Show only', 'options' : {
113 {'name': 'Time ', 'clclass': 'time', 'hidden' : 1}, 228 'Successful builds': 'outcome:' + str(Build.SUCCEEDED), # this is the field search expression
114 {'name': 'Output', 'clclass': 'output'}, 229 'Failed builds': 'outcome:'+ str(Build.FAILED),
115 {'name': 'Log', 'clclass': 'log', 'hidden': 1}, 230 }
116 ]} 231 }
232 },
233 {'name': 'Target ', # default column, disabled box, with just the name in the list
234 'qhelp': "This is the build target(s): one or more recipes or image recipes",
235 'orderfield': _get_toggle_order(request, "target__target"),
236 },
237 {'name': 'Machine ',
238 'qhelp': "The machine is the hardware for which you are building",
239 'dclass': 'span3'}, # a slightly wider column
240 {'name': 'Started on ', 'clclass': 'started_on', 'hidden' : 1, # this is an unchecked box, which hides the column
241 'qhelp': "The date and time you started the build",
242 'filter' : {'class' : 'started_on', 'label': 'Show only builds started', 'options' : {
243 'Today' : 'started_on__gte:'+timezone.now().strftime("%Y-%m-%d"),
244 'Yesterday' : 'started_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"),
245 'Within one week' : 'started_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"),
246 }}
247 },
248 {'name': 'Completed on ',
249 'qhelp': "The date and time the build finished",
250 'orderfield': _get_toggle_order(request, "completed_on"),
251 'filter' : {'class' : 'completed_on', 'label': 'Show only builds completed', 'options' : {
252 'Today' : 'completed_on__gte:'+timezone.now().strftime("%Y-%m-%d"),
253 'Yesterday' : 'completed_on__gte:'+(timezone.now()-timedelta(hours=24)).strftime("%Y-%m-%d"),
254 'Within one week' : 'completed_on__gte:'+(timezone.now()-timedelta(days=7)).strftime("%Y-%m-%d"),
255 }}
256 },
257 {'name': 'Failed tasks ', 'clclass': 'failed_tasks', # specifing a clclass will enable the checkbox
258 'qhelp': "How many tasks failed during the build",
259 'filter' : {'class' : 'failed_tasks', 'label': 'Show only ', 'options' : {
260 'Builds with failed tasks' : 'task_build__outcome:4',
261 'Builds without failed tasks' : 'task_build__outcome:NOT4',
262 }}
263 },
264 {'name': 'Errors ', 'clclass': 'errors_no',
265 'qhelp': "How many errors were encountered during the build (if any)",
266 'orderfield': _get_toggle_order(request, "errors_no"),
267 'filter' : {'class' : 'errors_no', 'label': 'Show only ', 'options' : {
268 'Builds with errors' : 'errors_no__gte:1',
269 'Builds without errors' : 'errors_no:0',
270 }}
271 },
272 {'name': 'Warnings', 'clclass': 'warnings_no',
273 'qhelp': "How many warnigns were encountered during the build (if any)",
274 'orderfield': _get_toggle_order(request, "warnings_no"),
275 'filter' : {'class' : 'warnings_no', 'label': 'Show only ', 'options' : {
276 'Builds with warnings' : 'warnings_no__gte:1',
277 'Builds without warnings' : 'warnings_no:0',
278 }}
279 },
280 {'name': 'Time ', 'clclass': 'time', 'hidden' : 1,
281 'qhelp': "How long it took the build to finish",},
282 {'name': 'Log',
283 'dclass': "span4",
284 'qhelp': "The location in disk of the build main log file",
285 'clclass': 'log', 'hidden': 1},
286 {'name': 'Output', 'clclass': 'output',
287 'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory"},
288 ]
289 }
117 290
118 return render(request, template, context) 291 return render(request, template, context)
119 292
@@ -191,8 +364,10 @@ def tasks(request, build_id):
191 retval = _verify_parameters( request.GET, mandatory_parameters ) 364 retval = _verify_parameters( request.GET, mandatory_parameters )
192 if retval: 365 if retval:
193 return _redirect_parameters( 'tasks', request.GET, mandatory_parameters, build_id = build_id) 366 return _redirect_parameters( 'tasks', request.GET, mandatory_parameters, build_id = build_id)
367 (filter_string, search_term, ordering_string) = _search_tuple(request, Task)
368 queryset = _get_queryset(Task, filter_string, search_term, ordering_string)
194 369
195 tasks = _build_page_range(Paginator(Task.objects.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1)) 370 tasks = _build_page_range(Paginator(queryset.filter(build=build_id, order__gt=0), request.GET.get('count', 100)),request.GET.get('page', 1))
196 371
197 for t in tasks: 372 for t in tasks:
198 if t.outcome == Task.OUTCOME_COVERED: 373 if t.outcome == Task.OUTCOME_COVERED:
@@ -208,8 +383,10 @@ def recipes(request, build_id):
208 retval = _verify_parameters( request.GET, mandatory_parameters ) 383 retval = _verify_parameters( request.GET, mandatory_parameters )
209 if retval: 384 if retval:
210 return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id) 385 return _redirect_parameters( 'recipes', request.GET, mandatory_parameters, build_id = build_id)
386 (filter_string, search_term, ordering_string) = _search_tuple(request, Recipe)
387 queryset = _get_queryset(Recipe, filter_string, search_term, ordering_string)
211 388
212 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)) 389 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))
213 390
214 context = {'build': Build.objects.filter(pk=build_id)[0], 'objects': recipes, } 391 context = {'build': Build.objects.filter(pk=build_id)[0], 'objects': recipes, }
215 392
@@ -218,15 +395,63 @@ def recipes(request, build_id):
218 395
219def configuration(request, build_id): 396def configuration(request, build_id):
220 template = 'configuration.html' 397 template = 'configuration.html'
398 context = {'build': Build.objects.filter(pk=build_id)[0]}
399 return render(request, template, context)
400
401
402def configvars(request, build_id):
403 template = 'configvars.html'
221 mandatory_parameters = { 'count': 100, 'page' : 1}; 404 mandatory_parameters = { 'count': 100, 'page' : 1};
222 retval = _verify_parameters( request.GET, mandatory_parameters ) 405 retval = _verify_parameters( request.GET, mandatory_parameters )
223 if retval: 406 if retval:
224 return _redirect_parameters( 'configuration', request.GET, mandatory_parameters, build_id = build_id) 407 return _redirect_parameters( 'configvars', request.GET, mandatory_parameters, build_id = build_id)
408
409 (filter_string, search_term, ordering_string) = _search_tuple(request, Variable)
410 queryset = _get_queryset(Variable, filter_string, search_term, ordering_string)
411
412 variables = _build_page_range(Paginator(queryset.filter(build=build_id), request.GET.get('count', 50)), request.GET.get('page', 1))
413
414 context = {
415 'build': Build.objects.filter(pk=build_id)[0],
416 'objects' : variables,
417 # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
418 'tablecols' : [
419 {'name': 'Variable ',
420 'qhelp': "Base variable expanded name",
421 'clclass' : 'variable',
422 'dclass' : "span3",
423 'orderfield': _get_toggle_order(request, "variable_name"),
424 },
425 {'name': 'Value ',
426 'qhelp': "The value assigned to the variable",
427 'clclass': 'variable_value',
428 'dclass': "span4",
429 'orderfield': _get_toggle_order(request, "variable_value"),
430 },
431 {'name': 'Configuration file(s) ',
432 'qhelp': "The configuration file(s) that touched the variable value",
433 'clclass': 'file',
434 'dclass': "span6",
435 'orderfield': _get_toggle_order(request, "variable_vhistory__file_name"),
436 'filter' : { 'class': 'file', 'label' : 'Show only', 'options' : {
437 }
438 }
439 },
440 {'name': 'Description ',
441 'qhelp': "A brief explanation of a variable",
442 'clclass': 'description',
443 'dclass': "span5",
444 'orderfield': _get_toggle_order(request, "description"),
445 'filter' : { 'class' : 'description', 'label' : 'No', 'options' : {
446 }
447 },
448 }
449 ]
450 }
225 451
226 variables = _build_page_range(Paginator(Variable.objects.filter(build=build_id), 50), request.GET.get('page', 1))
227 context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : variables}
228 return render(request, template, context) 452 return render(request, template, context)
229 453
454
230def buildtime(request, build_id): 455def buildtime(request, build_id):
231 template = "buildtime.html" 456 template = "buildtime.html"
232 if Build.objects.filter(pk=build_id).count() == 0 : 457 if Build.objects.filter(pk=build_id).count() == 0 :
@@ -263,8 +488,10 @@ def bpackage(request, build_id):
263 retval = _verify_parameters( request.GET, mandatory_parameters ) 488 retval = _verify_parameters( request.GET, mandatory_parameters )
264 if retval: 489 if retval:
265 return _redirect_parameters( 'packages', request.GET, mandatory_parameters, build_id = build_id) 490 return _redirect_parameters( 'packages', request.GET, mandatory_parameters, build_id = build_id)
491 (filter_string, search_term, ordering_string) = _search_tuple(request, Package)
492 queryset = _get_queryset(Package, filter_string, search_term, ordering_string)
266 493
267 packages = _build_page_range(Paginator(Package.objects.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1)) 494 packages = _build_page_range(Paginator(queryset.filter(build = build_id), request.GET.get('count', 100)),request.GET.get('page', 1))
268 495
269 context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : packages} 496 context = {'build': Build.objects.filter(pk=build_id)[0], 'objects' : packages}
270 return render(request, template, context) 497 return render(request, template, context)
@@ -305,139 +532,4 @@ def layer_versions_recipes(request, layerversion_id):
305 532
306 return render(request, template, context) 533 return render(request, template, context)
307 534
308#### API
309
310import json
311from django.core import serializers
312from django.http import HttpResponse, HttpResponseBadRequest
313
314
315def model_explorer(request, model_name):
316
317 DESCENDING = 'desc'
318 response_data = {}
319 model_mapping = {
320 'build': Build,
321 'target': Target,
322 'task': Task,
323 'task_dependency': Task_Dependency,
324 'package': Package,
325 'layer': Layer,
326 'layerversion': Layer_Version,
327 'recipe': Recipe,
328 'recipe_dependency': Recipe_Dependency,
329 'package': Package,
330 'package_dependency': Package_Dependency,
331 'build_file': Package_File,
332 'variable': Variable,
333 'logmessage': LogMessage,
334 }
335
336 if model_name not in model_mapping.keys():
337 return HttpResponseBadRequest()
338
339 model = model_mapping[model_name]
340
341 try:
342 limit = int(request.GET.get('limit', 0))
343 except ValueError:
344 limit = 0
345
346 try:
347 offset = int(request.GET.get('offset', 0))
348 except ValueError:
349 offset = 0
350
351 ordering_string, invalid = _validate_input(request.GET.get('orderby', ''),
352 model)
353 if invalid:
354 return HttpResponseBadRequest()
355
356 filter_string, invalid = _validate_input(request.GET.get('filter', ''),
357 model)
358 if invalid:
359 return HttpResponseBadRequest()
360
361 search_term = request.GET.get('search', '')
362
363 if filter_string:
364 filter_terms = _get_filtering_terms(filter_string)
365 try:
366 queryset = model.objects.filter(**filter_terms)
367 except ValueError:
368 queryset = []
369 else:
370 queryset = model.objects.all()
371 535
372 if search_term:
373 queryset = _get_search_results(search_term, queryset, model)
374
375 if ordering_string and queryset:
376 column, order = ordering_string.split(':')
377 if order.lower() == DESCENDING:
378 queryset = queryset.order_by('-' + column)
379 else:
380 queryset = queryset.order_by(column)
381
382 if offset and limit:
383 queryset = queryset[offset:(offset+limit)]
384 elif offset:
385 queryset = queryset[offset:]
386 elif limit:
387 queryset = queryset[:limit]
388
389 if queryset:
390 response_data['count'] = queryset.count()
391 else:
392 response_data['count'] = 0
393 response_data['list'] = serializers.serialize('json', queryset)
394# response_data = serializers.serialize('json', queryset)
395
396 return HttpResponse(json.dumps(response_data),
397 content_type='application/json')
398
399def _get_filtering_terms(filter_string):
400
401 search_terms = filter_string.split(":")
402 keys = search_terms[0].split(',')
403 values = search_terms[1].split(',')
404
405 return dict(zip(keys, values))
406
407def _validate_input(input, model):
408
409 invalid = 0
410
411 if input:
412 input_list = input.split(":")
413
414 # Check we have only one colon
415 if len(input_list) != 2:
416 invalid = 1
417 return None, invalid
418
419 # Check we have an equal number of terms both sides of the colon
420 if len(input_list[0].split(',')) != len(input_list[1].split(',')):
421 invalid = 1
422 return None, invalid
423
424 # Check we are looking for a valid field
425 valid_fields = model._meta.get_all_field_names()
426 for field in input_list[0].split(','):
427 if field not in valid_fields:
428 invalid = 1
429 return None, invalid
430
431 return input, invalid
432
433def _get_search_results(search_term, queryset, model):
434 search_objects = []
435 for st in search_term.split(" "):
436 q_map = map(lambda x: Q(**{x+'__icontains': st}),
437 model.search_allowed_fields)
438
439 search_objects.append(reduce(operator.or_, q_map))
440 search_object = reduce(operator.and_, search_objects)
441 queryset = queryset.filter(search_object)
442
443 return queryset