diff options
author | Alexandru DAMIAN <alexandru.damian@intel.com> | 2014-01-07 13:10:42 +0000 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2014-01-10 15:20:26 +0000 |
commit | 1b636173ca88e5ccca1992f9a12367a1189fa674 (patch) | |
tree | 0220e98e7b7a4027fb8c146bab9b3f81306fc9fe /bitbake | |
parent | 5482409a370552809de75150350defef04ac7144 (diff) | |
download | poky-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')
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 | ||
219 | class Variable(models.Model): | 221 | class 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 | ||
227 | class VariableHistory(models.Model): | 231 | class 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}}">«</a></li> | 12 | <li><a href="javascript:reload_params({'page':{{objects.previous_page_number}}})">«</a></li> |
12 | {%else%} | 13 | {%else%} |
13 | <li class="disabled"><a href="#">«</a></li> | 14 | <li class="disabled"><a href="#">«</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}}">»</a></li> | 20 | <li><a href="javascript:reload_params({'page':{{objects.next_page_number}}})">»</a></li> |
20 | {%else%} | 21 | {%else%} |
21 | <li class="disabled"><a href="#">»</a></li> | 22 | <li class="disabled"><a href="#">»</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"> <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 | ||
19 | from datetime import datetime | 19 | from datetime import datetime, timedelta |
20 | from django import template | 20 | from django import template |
21 | from django.utils import timezone | ||
21 | 22 | ||
22 | register = template.Library() | 23 | register = template.Library() |
23 | 24 | ||
@@ -42,8 +43,14 @@ def query(qs, **kwargs): | |||
42 | 43 | ||
43 | @register.filter | 44 | @register.filter |
44 | def divide(value, arg): | 45 | def 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 |
48 | def multiply(value, arg): | 51 | def multiply(value, arg): |
49 | return int(value) * int(arg) | 52 | return int(value) * int(arg) |
53 | |||
54 | @register.assignment_tag | ||
55 | def 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 | |||
25 | from orm.models import Target_Installed_Package | 25 | from orm.models import Target_Installed_Package |
26 | from django.views.decorators.cache import cache_control | 26 | from django.views.decorators.cache import cache_control |
27 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger | 27 | from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger |
28 | 28 | from django.http import HttpResponseBadRequest | |
29 | from django.utils import timezone | ||
30 | from datetime import timedelta | ||
31 | from django.utils import formats | ||
29 | 32 | ||
30 | def _build_page_range(paginator, index = 1): | 33 | def _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 | ||
78 | FIELD_SEPARATOR = ":" | ||
79 | VALUE_SEPARATOR = ";" | ||
80 | DESCENDING = "-" | ||
81 | |||
82 | def __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 | |||
94 | def _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 | |||
103 | def _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 | ||
107 | def _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 | ||
134 | def _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 | ||
149 | def _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 | ||
163 | def _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 |
77 | def builds(request): | 183 | def 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 | ||
219 | def configuration(request, build_id): | 396 | def 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 | |||
402 | def 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 | |||
230 | def buildtime(request, build_id): | 455 | def 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 | |||
310 | import json | ||
311 | from django.core import serializers | ||
312 | from django.http import HttpResponse, HttpResponseBadRequest | ||
313 | |||
314 | |||
315 | def 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 | |||
399 | def _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 | |||
407 | def _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 | |||
433 | def _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 | ||