]> git.ipfire.org Git - thirdparty/openembedded/openembedded-core-contrib.git/commitdiff
toaster: add sort, search, paging to recipe package page
authorDave Lerner <dave.lerner@windriver.com>
Tue, 24 Feb 2015 22:14:41 +0000 (16:14 -0600)
committerAlexandru DAMIAN <alexandru.damian@intel.com>
Mon, 9 Mar 2015 13:15:20 +0000 (13:15 +0000)
When selecting the packages tab on a recipe detail page, the page now
includes:
    column sort on package name and size columns,
    search on the package name, and
    pagination.

Column sort is added by splitting the recipe view/html for a recipe's
package list into a new url path, view name and template, so that
the sorting routine, views.reload_params(), interfaces similar to other
views.

Search, sorting,  and pagination are implemented for this detail page
using three new templates.
  templates/detail_pagination_bottom.html
  templates/detail_search_header.html
  templates/detail_sorted_header.html

views.recipe() is optimized since the recipe's package list is no
longer needed by the recipe template, only the recipe's package count
is required for the first page.

The recipe view and template also changes to support tabbing to the
right context on the recipe detail page from the recipe-package page.

[YOCTO #6154]

Signed-off-by: Dave Lerner <dave.lerner@windriver.com>
lib/toaster/toastergui/templates/detail_pagination_bottom.html [new file with mode: 0644]
lib/toaster/toastergui/templates/detail_search_header.html [new file with mode: 0644]
lib/toaster/toastergui/templates/detail_sorted_header.html [new file with mode: 0644]
lib/toaster/toastergui/templates/recipe.html
lib/toaster/toastergui/templates/recipe_packages.html [new file with mode: 0644]
lib/toaster/toastergui/urls.py
lib/toaster/toastergui/views.py

diff --git a/lib/toaster/toastergui/templates/detail_pagination_bottom.html b/lib/toaster/toastergui/templates/detail_pagination_bottom.html
new file mode 100644 (file)
index 0000000..355ae90
--- /dev/null
@@ -0,0 +1,60 @@
+{% comment %}
+    Show pagination controls as per search/pagination table detail spec.
+    Input: objects, setup for pagination using the standard method in views.
+           object_count, count for complete list of objects, (all pages, no pattern)
+{% endcomment %}
+
+{# only paginate if 10 or more rows unfiltered, all pages #}
+{% if object_count >= 10 %}
+<div class="pagination">
+  <ul>
+{%if objects.has_previous %}
+    <li><a href="javascript:reload_params({'page':{{objects.previous_page_number}}})">&laquo;</a></li>
+{%else%}
+    <li class="disabled"><a href="#">&laquo;</a></li>
+{%endif%}
+{% for i in objects.page_range %}
+    <li{%if i == objects.number %} class="active" {%endif%}><a href="javascript:reload_params({'page':{{i}}})">{{i}}</a></li>
+{% endfor %}
+{%if objects.has_next%}
+    <li><a href="javascript:reload_params({'page':{{objects.next_page_number}}})">&raquo;</a></li>
+{%else%}
+    <li class="disabled"><a href="#">&raquo;</a></li>
+{%endif%}
+  </ul>
+
+  <div class="pull-right">
+    <span class="help-inline" style="padding-bottom:10px;">Show rows:</span>
+    <select class="pagesize">
+      {% with "10 25 50 100 150" as list%}
+        {% for i in list.split %}
+          <option value="{{i}}">{{i}}</option>
+        {% endfor %}
+      {% endwith %}
+    </select>
+  </div>
+</div>
+
+<!-- Update page display settings -->
+<script>
+ $(document).ready(function() {
+    // load cookie for number of entries to be displayed on page
+    if ({{request.GET.count}} != "") {
+      pagesize = {{request.GET.count}};
+    } else {
+      pagesize = $.cookie('_count');
+    }
+
+    $('.pagesize option').prop('selected', false)
+                         .filter('[value="' + pagesize + '"]')
+                         .attr('selected', true);
+
+    $(".pagesize").change(function () {
+        // save cookie with pagesize
+        $.cookie("_count", $(this).val(), { path : $(location).attr('pathname') });
+        reload_params({"count":$(this).val()});
+    });
+});
+</script>
+{% endif %}
+
diff --git a/lib/toaster/toastergui/templates/detail_search_header.html b/lib/toaster/toastergui/templates/detail_search_header.html
new file mode 100644 (file)
index 0000000..ca8e158
--- /dev/null
@@ -0,0 +1,68 @@
+{% comment %}
+  Show a detail table Search field and Rows per page.
+  Input:
+    objects, our boilerplated paginated with search fields set.
+    object_count, count of full, unfiltered, objects list
+    search_what, fills in "Search ___"
+  Only show the search form if we have more than 10 results,
+  or if return from a previous search.
+{% endcomment %}
+
+
+<script>
+$(document).ready(function() {
+  /* Clear the current search selection and reload the results */
+  $(".search-clear").click(function(){
+    $("#search").val("");
+    $(this).parents("form").submit();
+  });
+});
+</script>
+<div class="row-fluid">
+{% if objects.paginator.count > 10 or request.GET.search %}
+  {% if objects.paginator.count == 0 %}
+  <div class="alert">
+    <h3>No {{search_what}} found</h3>
+    <form id="searchform" class="input-append">
+  {% else %}
+    <form id="searchform" class="navbar-search input-append pull-left">
+  {% endif %}
+
+      <input id="search" class="input-xlarge" type="text" placeholder="Search {{search_what}}" name="search" value="{{request.GET.search}}">
+      <input type="hidden" value="name:+" name="orderby">
+      <input type="hidden" value="l" name="page">
+  {% if request.GET.search %}
+      <a class="add-on btn search-clear">
+        <i class="icon-remove"></i>
+      </a>
+  {% endif %}
+      <button type="submit" class="btn">Search</button>
+  {% if objects.paginator.count == 0 %}
+      <button type="submit" class="btn btn-link search-clear">
+        Show all {{search_what}}
+      </button>
+  {% endif %}
+    </form>
+{% endif %}
+
+{% if objects.paginator.count == 0 %}
+  </div> {# end alert #}
+{% else %}
+  {% if object_count > 10 %}
+  <div class="pull-right">
+    <span class="help-inline" style="padding-top:5px;">Show rows:</span>
+    <select style="margin-top:5px;margin-bottom:0px;" class="pagesize">
+    {% with "10 25 50 100 150" as list%}
+      {% for i in list.split %}
+        {% if request.session.limit == i %}
+      <option value="{{i}}" selected>{{i}}</option>
+        {% else %}
+      <option value="{{i}}">{{i}}</option>
+        {% endif %}
+      {% endfor %}
+    {% endwith %}
+    </select>
+  </div>
+  {% endif %}
+{% endif %}
+</div> {# row-fluid #}
diff --git a/lib/toaster/toastergui/templates/detail_sorted_header.html b/lib/toaster/toastergui/templates/detail_sorted_header.html
new file mode 100644 (file)
index 0000000..a7917dc
--- /dev/null
@@ -0,0 +1,25 @@
+{% comment %}
+    Adds sorted columns to a detail table.
+    Must be preceded by <table class="table table-bordered table-hover tablesorter" id="otable">
+    Must be followed by <tbody>...</tbody></table>.
+    Requires tablecols setup column fields dclass, clclass, qhelp, orderfield.
+{% endcomment %}
+{% load projecttags %}
+{#    <table class="table table-bordered table-hover tablesorter" id="otable"> #}
+    <thead>
+        <!-- Table header row; generated from "tablecols" entry in the context dict -->
+        <tr>
+            {% for tc in tablecols %}<th class="{{tc.dclass}} {{tc.clclass}}">
+                {%if tc.qhelp%}<i class="icon-question-sign get-help" title="{{tc.qhelp}}"></i>{%endif%}
+                {%if tc.orderfield%}<a {%if tc.ordericon%} class="sorted" {%endif%}href="javascript:reload_params({'page': 1, 'orderby' : '{{tc.orderfield}}' })" onclick="saveOrderCookie('{{tc.orderfield}}')">{{tc.name}}</a>{%else%}<span class="muted">{{tc.name}}</span>{%endif%}
+                {%if tc.ordericon%} <i class="icon-caret-{{tc.ordericon}}"></i>{%endif%}
+                {% if request.GET.search and forloop.first %}
+                    <span class="badge badge-info">{{objects.paginator.count}}</span>
+                {% endif %}
+                {%if tc.filter%}<div class="btn-group pull-right">
+                    <a href="#filter_{{tc.filter.class}}" role="button" class="btn btn-mini {%if request.GET.filter%}{{tc.filter.options|filtered_icon:request.GET.filter}} {%endif%}" {%if request.GET.filter and tc.filter.options|filtered_tooltip:request.GET.filter %} title="<p>{{tc.filter.options|filtered_tooltip:request.GET.filter}}</p><p><a class='btn btn-small btn-primary' href=javascript:reload_params({'filter':''})>Show all {% if filter_search_display %}{{filter_search_display}}{% else %}{{objectname}}{% endif %}</a></p>" {%endif%} data-toggle="modal"> <i class="icon-filter filtered"></i> </a>
+                </div>{%endif%}
+            </th>{% endfor %}
+        </tr>
+    </thead>
+
index 621b852e77b8c58932b681f2b4481f61614a1331..6eba9b6f0bbf261e1e7349a5d41618fbce962e4f 100644 (file)
 
 <div class="row span7 tabbable">
     <ul class="nav nav-pills">
-        <li class="active">
+        <li class="{{tab_states.1}}">
             <a href="#information" data-toggle="tab">
                 <i class="icon-question-sign get-help" title="Build-related information about the recipe"></i>
                 Recipe details
             </a>
         </li>
         <li>
-            <a href="#packages-built" data-toggle="tab">
+             <a href="{% url "recipe_packages" build.pk object.id %}">
                 <i class="icon-question-sign get-help" title="The packaged output resulting from building the recipe"></i>
-                Packages ({{packages.count}})
+                Packages ({{package_count}})
             </a>
         </li>
-        <li>
+        <li class="{{tab_states.3}}">
             <a href="#dependencies" data-toggle="tab">
                 <i class="icon-question-sign get-help" title="The recipe build-time dependencies (i.e. other recipes)"></i>
                 Build dependencies ({{object.r_dependencies_recipe.all.count}})
             </a>
         </li>
-        <li>
+        <li class="{{tab_states.4}}">
             <a href="#brought-in-by" data-toggle="tab">
                 <i class="icon-question-sign get-help" title="The recipe build-time reverse dependencies (i.e. the recipes that depend on this recipe)"></i>
                 Reverse build dependencies ({{object.r_dependencies_depends.all.count}})
@@ -45,7 +45,7 @@
         </li>
     </ul>
     <div class="tab-content">
-        <div class="tab-pane active" id="information" name="information">
+        <div class="tab-pane {{tab_states.1}}" id="information" name="information">
             <dl class="dl-horizontal">
                 <dt>
                     <i class="icon-question-sign get-help" title="The name of the layer providing the recipe"></i>
             </table>
             {% endif %}
         </div>
-        <div class="tab-pane" id="packages-built" name="packages-built">
-            {% if not packages %}
-            <div class="alert alert-info">
-                <strong>{{object.name}}_{{object.version}}</strong> does not build any packages.
-            </div>
-            {% else %}
-            <table class="table table-bordered table-hover" style="margin-top:10px;">
-                <thead>
-                    <tr>
-                        <th>
-                            Package
-                        </th>
-                        <th>
-                            Version
-                        </th>
-                        <th class="sizecol span2">
-                            Size
-                        </th>
-                    </tr>
-                </thead>
-                <tbody>
-
-                    {% for package in packages|dictsort:"name" %}
-
-                    <tr>
-                        <td><a href="{% url "package_built_detail" build.pk package.pk %}">{{package.name}}</a></td>
-                        <td><a href="{% url "package_built_detail" build.pk package.pk %}">{{package.version}}_{{package.revision}}</a></td>
-                        <td class="sizecol"><a href="{% url "package_built_detail" build.pk package.pk %}">{{package.size|filtered_filesizeformat}}</a></td>
-                    </tr>
-
-                    {% endfor %}
-
-                </tbody>
-            </table>
-            {% endif %}
-        </div>
-        <div class="tab-pane" id="dependencies" name="dependencies">
+        <div class="tab-pane {{tab_states.3}}" id="dependencies" name="dependencies">
 
             {% if not object.r_dependencies_recipe.all %}
             <div class="alert alert-info">
             {% endif %}
 
         </div>
-        <div class="tab-pane" id="brought-in-by" name="brought-in-by">
+        <div class="tab-pane {{tab_states.4}}" id="brought-in-by" name="brought-in-by">
 
             {% if not object.r_dependencies_depends.all %}
             <div class="alert alert-info">
diff --git a/lib/toaster/toastergui/templates/recipe_packages.html b/lib/toaster/toastergui/templates/recipe_packages.html
new file mode 100644 (file)
index 0000000..d25847b
--- /dev/null
@@ -0,0 +1,123 @@
+{% extends "basebuilddetailpage.html" %}
+
+{% load projecttags %}
+{% load humanize %}
+{% block localbreadcrumb %}
+<li><a href="{% url 'recipes' build.pk %}">Recipes</a></li>
+<li>{{recipe.name}}_{{recipe.version}} </li>
+{% endblock %}
+
+{% block pagedetailinfomain %}
+
+<!-- Begin container -->
+
+<div class="row-fluid span11">
+    <div class="page-header">
+        <h1>{{recipe.name}}_{{recipe.version}}</h1>
+    </div>
+</div>
+
+<div class="row-fluid span7 tabbable">
+    <ul class="nav nav-pills">
+        <li>
+            <a href="{% url "recipe" build.pk recipe.id "1" %}">
+                <i class="icon-question-sign get-help" title="Build-related information about the recipe"></i>
+                Recipe details
+            </a>
+        </li>
+        <li class="active">
+                       <a href="#packages-built" data-toggle="tab">
+                <i class="icon-question-sign get-help" title="The packaged output resulting from building the recipe"></i>
+                Packages ({{object_count}})
+            </a>
+        </li>
+        <li>
+            <a href="{% url "recipe" build.pk recipe.id "3" %}">
+                <i class="icon-question-sign get-help" title="The recipe build-time dependencies (i.e. other recipes)"></i>
+                Build dependencies ({{recipe.r_dependencies_recipe.all.count}})
+            </a>
+        </li>
+        <li>
+            <a href="{% url "recipe" build.pk recipe.id "4" %}">
+                <i class="icon-question-sign get-help" title="The recipe build-time reverse dependencies (i.e. the recipes that depend on this recipe)"></i>
+                Reverse build dependencies ({{recipe.r_dependencies_depends.all.count}})
+            </a>
+        </li>
+    </ul>
+    <div class="tab-content">
+{#        <div class="tab-pane active" id="packages-built" name="packages-built">#}
+       <div class="tab-pane active" id="packages-built">
+            {% if not objects and not request.GET.search %}
+            <div class="alert alert-info">
+                <strong>{{recipe.name}}_{{recipe.version}}</strong> does not build any packages.
+            </div>
+
+            {% elif not objects %}
+                {# have empty search results, no table nor pagination #}
+                {% with "packages" as search_what %}
+                {% include "detail_search_header.html" %}
+                {% endwith %}
+
+            {% else %}
+
+
+                {% with "packages" as search_what %}
+                {% include "detail_search_header.html" %}
+                {% endwith %}
+            <table class="table table-bordered table-hover tablesorter" id="otable">
+                {% include "detail_sorted_header.html" %}
+
+                <tbody>
+                    {% for package in objects %}
+
+                    <tr>
+                        <td><a href="{% url "package_built_detail" build.pk package.pk %}">{{package.name}}</a></td>
+                        <td><a href="{% url "package_built_detail" build.pk package.pk %}">{{package.version}}_{{package.revision}}</a></td>
+                        <td class="sizecol"><a href="{% url "package_built_detail" build.pk package.pk %}">{{package.size|filtered_filesizeformat}}</a></td>
+                    </tr>
+
+                        {% endfor %}
+
+            {% endif %}
+            {% if objects %}
+                </tbody>
+            </table>
+                {% include "detail_pagination_bottom.html" %}
+            {% endif %}
+        </div> {# tab-pane #}
+    </div> {# tab-content #}
+</div> {# span7 #}
+
+<div class="row span4 well">
+    <h2>About {{recipe.name}}</h2>
+    <dl class="item-info">
+        {% if recipe.summary %}
+            <dt>Summary</dt>
+            <dd>{{recipe.summary}}</dd>
+        {% endif %}
+        {% if recipe.description %}
+            <dt>Description</dt>
+            <dd>{{recipe.description}}</dd>
+        {% endif %}
+        {% if recipe.homepage %}
+            <dt>Homepage</dt>
+            <dd><a href="{{recipe.homepage}}">{{recipe.homepage}}</a></dd>
+        {% endif %}
+        {% if recipe.bugtracker %}
+            <dt>Bugtracker</dt>
+            <dd><a href="{{recipe.bugtracker}}">{{recipe.bugtracker}}</a></dd>
+        {% endif %}
+        {% if recipe.section %}
+            <dt>
+            Section
+            <i class="icon-question-sign get-help" title="The section in which recipes should be categorized"></i>
+            </dt>
+            <dd>{{recipe.section}}</dd>
+        {% endif %}
+        {% if recipe.license %}
+            <dt>License</dt>
+            <dd>{{recipe.license}}</dd>
+        {% endif %}
+    </dl>
+</div>
+{% endblock pagedetailinfomain %}
index 1c83090f5834f2899e44ef183153d333d9d38902..fc03f19a7f2cc34c612f4dd8dbd6234622c4f9c3 100644 (file)
@@ -32,7 +32,9 @@ urlpatterns = patterns('toastergui.views',
         url(r'^build/(?P<build_id>\d+)/task/(?P<task_id>\d+)$', 'task', name='task'),
 
         url(r'^build/(?P<build_id>\d+)/recipes/$', 'recipes', name='recipes'),
+        url(r'^build/(?P<build_id>\d+)/recipe/(?P<recipe_id>\d+)/active_tab/(?P<active_tab>\d{1})$', 'recipe', name='recipe'),
         url(r'^build/(?P<build_id>\d+)/recipe/(?P<recipe_id>\d+)$', 'recipe', name='recipe'),
+        url(r'^build/(?P<build_id>\d+)/recipe_packages/(?P<recipe_id>\d+)$', 'recipe_packages', name='recipe_packages'),
 
         url(r'^build/(?P<build_id>\d+)/packages/$', 'bpackage', name='packages'),
         url(r'^build/(?P<build_id>\d+)/package/(?P<package_id>\d+)$', 'package_built_detail',
index 4f4ae67ca77b0655649c3dee4c3ce68c8d084032..ed27ca0ac978aa1756827b932c442168c4dc79c9 100755 (executable)
@@ -419,8 +419,7 @@ def task( request, build_id, task_id ):
 
     return render( request, template, context )
 
-
-def recipe(request, build_id, recipe_id):
+def recipe(request, build_id, recipe_id, active_tab="1"):
     template = "recipe.html"
     if Recipe.objects.filter(pk=recipe_id).count() == 0 :
         return redirect(builds)
@@ -429,7 +428,12 @@ def recipe(request, build_id, recipe_id):
     layer_version = Layer_Version.objects.get(pk=object.layer_version_id)
     layer  = Layer.objects.get(pk=layer_version.layer_id)
     tasks  = Task.objects.filter(recipe_id = recipe_id, build_id = build_id).exclude(order__isnull=True).exclude(task_name__endswith='_setscene').exclude(outcome=Task.OUTCOME_NA)
-    packages = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0)
+    package_count = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0).count()
+
+    if active_tab != '1' and active_tab != '3' and active_tab != '4' :
+        active_tab = '1'
+    tab_states = {'1': '', '3': '', '4': ''}
+    tab_states[active_tab] = 'active'
 
     context = {
             'build'   : Build.objects.get(pk=build_id),
@@ -437,10 +441,58 @@ def recipe(request, build_id, recipe_id):
             'layer_version' : layer_version,
             'layer'   : layer,
             'tasks'   : tasks,
-            'packages': packages,
+            'package_count' : package_count,
+            'tab_states' : tab_states,
     }
     return render(request, template, context)
 
+def recipe_packages(request, build_id, recipe_id):
+    template = "recipe_packages.html"
+    if Recipe.objects.filter(pk=recipe_id).count() == 0 :
+        return redirect(builds)
+
+    (pagesize, orderby) = _get_parameters_values(request, 10, 'name:+')
+    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby': orderby }
+    retval = _verify_parameters( request.GET, mandatory_parameters )
+    if retval:
+        return _redirect_parameters( 'recipe_packages', request.GET, mandatory_parameters, build_id = build_id, recipe_id = recipe_id)
+    (filter_string, search_term, ordering_string) = _search_tuple(request, Package)
+
+    recipe = Recipe.objects.get(pk=recipe_id)
+    queryset = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0)
+    package_count = queryset.count()
+    queryset = _get_queryset(Package, queryset, filter_string, search_term, ordering_string, 'name')
+
+    packages = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1))
+
+    context = {
+            'build'   : Build.objects.get(pk=build_id),
+            'recipe'  : recipe,
+            'objects'  : packages,
+            'object_count' : package_count,
+            'tablecols':[
+                {
+                    'name':'Package',
+                    'orderfield': _get_toggle_order(request,"name"),
+                    'ordericon': _get_toggle_order_icon(request,"name"),
+                    'orderkey': "name",
+                },
+                {
+                    'name':'Version',
+                },
+                {
+                    'name':'Size',
+                    'orderfield': _get_toggle_order(request,"size", True),
+                    'ordericon': _get_toggle_order_icon(request,"size"),
+                    'orderkey': 'size',
+                    'dclass': 'sizecol span2',
+                },
+           ]
+       }
+    response = render(request, template, context)
+    _save_parameters_cookies(response, pagesize, orderby, request)
+    return response
+
 def target_common( request, build_id, target_id, variant ):
     template = "target.html"
     (pagesize, orderby) = _get_parameters_values(request, 25, 'name:+')