diff options
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 | ||
