All of lore.kernel.org
 help / color / mirror / Atom feed
* Re: [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
@ 2016-01-15 11:07 ` Ed Bartosh
  2016-01-15 11:43 ` [review-request][PATCH 01/23] toaster: toastergui: use ToasterTable for projects page Elliot Smith
                   ` (22 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Ed Bartosh @ 2016-01-15 11:07 UTC (permalink / raw)
  To: Elliot Smith; +Cc: toaster

Hi,

Patchset looks ok to me. Submitted upstream.

Regards,
Ed

On Fri, Jan 15, 2016 at 11:43:27AM +0000, Elliot Smith wrote:
> V2:
> 
> Minor code formatting and fixes in line with code review by Ed Bartosh.
> 
> V1:
> 
> Many of the tables displayed by Toaster are backed by ToasterTable, but some
> of the key ones (like "projects", "all builds", and "project builds") aren't.
> 
> This makes it difficult to keep styling consistent between tables, as some
> are styled by making changes to ToasterTable, while others are styled
> by modifying Django templates.
> 
> Improve the consistency and maintainability of these tables by converting
> them to use ToasterTable.
> 
> Note that this patchset also reworks the ToasterTable API so that we can easily
> support different column filter types.
> 
> To test:
> 
> 1. Open the "projects", "all builds" and "project builds" pages in Toaster.
> 2. Check that the column filtering and sorting works correctly, and projects/builds
>    display correctly.
> 
> Changes since ea666f6 (toaster-next) are in
> git://git.yoctoproject.org/poky-contrib, elliot/toaster/tables-8738
> http://git.yoctoproject.org/cgit.cgi/poky-contrib/log/?h=elliot/toaster/tables-8738
> 
> Related bug: https://bugzilla.yoctoproject.org/show_bug.cgi?id=8738
> 
> Elliot Smith (23):
>   toaster: toastergui: use ToasterTable for projects page
>   toaster: move image file suffix list to model
>   toaster: check inferred file suffixes against list of known types
>   toaster: toastergui: switch projects/ view to ToasterTable
>   toaster: toastergui: use event delegates for hover help elements
>   toaster: toastergui: convert all builds page to ToasterTable
>   toaster: toastergui: refactor ToasterTable filtering
>   toaster: toastergui: switch off filter highlights when inactive
>   toaster: toastergui: show recent builds on all builds page
>   toaster: toastergui: implement date range filters for builds
>   toaster: toastergui: implement "today" and "yesterday" filters
>   toaster: toastergui: convert project builds page to ToasterTable
>   toaster: toastergui: don't hide all elements with .col class
>   toaster: toastergui: ensure filter_value updates
>   toaster: toastergui: streamline construction of filter objects
>   toaster: toastergui: serialise decimals correctly
>   toaster: toastergui: set default visible and hideable columns
>   toaster: toastergui: mute label for filter actions with no records
>   toaster: toastergui: make "Apply" button state depend on filter range
>   toaster: toastergui: fix error and warning counts for builds
>   toaster: toastergui: remove unused views and template code
>   toaster: tests: fix Django tests for new ToasterTable pages
>   toaster: toastergui: code formatting and clean-up
> 
>  .../contrib/django-aggregate-if-master/.gitignore  |  10 -
>  .../contrib/django-aggregate-if-master/.travis.yml |  50 --
>  .../contrib/django-aggregate-if-master/LICENSE     |  21 -
>  .../contrib/django-aggregate-if-master/README.rst  | 156 ----
>  .../django-aggregate-if-master/aggregate_if.py     | 164 ----
>  .../contrib/django-aggregate-if-master/runtests.py |  48 --
>  .../contrib/django-aggregate-if-master/setup.py    |  33 -
>  .../contrib/django-aggregate-if-master/tox.ini     | 198 -----
>  bitbake/lib/toaster/orm/models.py                  | 103 ++-
>  .../lib/toaster/toastergui/static/js/libtoaster.js |   6 +-
>  .../toaster/toastergui/static/js/projecttopbar.js  |   9 +
>  bitbake/lib/toaster/toastergui/static/js/table.js  | 325 ++++++--
>  bitbake/lib/toaster/toastergui/tablefilter.py      | 292 +++++++
>  bitbake/lib/toaster/toastergui/tables.py           | 858 +++++++++++++++++++--
>  .../toastergui/templates/baseprojectpage.html      |   1 +
>  .../toastergui/templates/builds-toastertable.html  |  48 ++
>  .../lib/toaster/toastergui/templates/builds.html   | 125 ---
>  .../toaster/toastergui/templates/mrb_section.html  |   4 +-
>  .../templates/projectbuilds-toastertable.html      |  56 ++
>  .../templates/projects-toastertable.html           |  36 +
>  .../lib/toaster/toastergui/templates/projects.html |  92 ---
>  .../toastergui/templates/toastertable-filter.html  |   4 +-
>  .../toaster/toastergui/templates/toastertable.html |   7 +-
>  bitbake/lib/toaster/toastergui/tests.py            | 265 +++++--
>  bitbake/lib/toaster/toastergui/urls.py             |  14 +-
>  bitbake/lib/toaster/toastergui/views.py            | 491 +-----------
>  bitbake/lib/toaster/toastergui/widgets.py          | 112 +--
>  bitbake/lib/toaster/toastermain/settings.py        |   9 -
>  28 files changed, 1898 insertions(+), 1639 deletions(-)
>  delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore
>  delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml
>  delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE
>  delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst
>  delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py
>  delete mode 100755 bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py
>  delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py
>  delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini
>  create mode 100644 bitbake/lib/toaster/toastergui/tablefilter.py
>  create mode 100644 bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
>  delete mode 100644 bitbake/lib/toaster/toastergui/templates/builds.html
>  create mode 100644 bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
>  create mode 100644 bitbake/lib/toaster/toastergui/templates/projects-toastertable.html
>  delete mode 100644 bitbake/lib/toaster/toastergui/templates/projects.html
> 
> --
> Elliot Smith
> Software Engineer
> Intel OTC
> 
> ---------------------------------------------------------------------
> Intel Corporation (UK) Limited
> Registered No. 1134945 (England)
> Registered Office: Pipers Way, Swindon SN3 1RJ
> VAT No: 860 2173 47
> 
> This e-mail and any attachments may contain confidential material for
> the sole use of the intended recipient(s). Any review or distribution
> by others is strictly prohibited. If you are not the intended
> recipient, please contact the sender and delete all copies.
> 
> -- 
> _______________________________________________
> toaster mailing list
> toaster@yoctoproject.org
> https://lists.yoctoproject.org/listinfo/toaster

-- 
--
Regards,
Ed


^ permalink raw reply	[flat|nested] 25+ messages in thread

* [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable
@ 2016-01-15 11:43 Elliot Smith
  2016-01-15 11:07 ` Ed Bartosh
                   ` (23 more replies)
  0 siblings, 24 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

V2:

Minor code formatting and fixes in line with code review by Ed Bartosh.

V1:

Many of the tables displayed by Toaster are backed by ToasterTable, but some
of the key ones (like "projects", "all builds", and "project builds") aren't.

This makes it difficult to keep styling consistent between tables, as some
are styled by making changes to ToasterTable, while others are styled
by modifying Django templates.

Improve the consistency and maintainability of these tables by converting
them to use ToasterTable.

Note that this patchset also reworks the ToasterTable API so that we can easily
support different column filter types.

To test:

1. Open the "projects", "all builds" and "project builds" pages in Toaster.
2. Check that the column filtering and sorting works correctly, and projects/builds
   display correctly.

Changes since ea666f6 (toaster-next) are in
git://git.yoctoproject.org/poky-contrib, elliot/toaster/tables-8738
http://git.yoctoproject.org/cgit.cgi/poky-contrib/log/?h=elliot/toaster/tables-8738

Related bug: https://bugzilla.yoctoproject.org/show_bug.cgi?id=8738

Elliot Smith (23):
  toaster: toastergui: use ToasterTable for projects page
  toaster: move image file suffix list to model
  toaster: check inferred file suffixes against list of known types
  toaster: toastergui: switch projects/ view to ToasterTable
  toaster: toastergui: use event delegates for hover help elements
  toaster: toastergui: convert all builds page to ToasterTable
  toaster: toastergui: refactor ToasterTable filtering
  toaster: toastergui: switch off filter highlights when inactive
  toaster: toastergui: show recent builds on all builds page
  toaster: toastergui: implement date range filters for builds
  toaster: toastergui: implement "today" and "yesterday" filters
  toaster: toastergui: convert project builds page to ToasterTable
  toaster: toastergui: don't hide all elements with .col class
  toaster: toastergui: ensure filter_value updates
  toaster: toastergui: streamline construction of filter objects
  toaster: toastergui: serialise decimals correctly
  toaster: toastergui: set default visible and hideable columns
  toaster: toastergui: mute label for filter actions with no records
  toaster: toastergui: make "Apply" button state depend on filter range
  toaster: toastergui: fix error and warning counts for builds
  toaster: toastergui: remove unused views and template code
  toaster: tests: fix Django tests for new ToasterTable pages
  toaster: toastergui: code formatting and clean-up

 .../contrib/django-aggregate-if-master/.gitignore  |  10 -
 .../contrib/django-aggregate-if-master/.travis.yml |  50 --
 .../contrib/django-aggregate-if-master/LICENSE     |  21 -
 .../contrib/django-aggregate-if-master/README.rst  | 156 ----
 .../django-aggregate-if-master/aggregate_if.py     | 164 ----
 .../contrib/django-aggregate-if-master/runtests.py |  48 --
 .../contrib/django-aggregate-if-master/setup.py    |  33 -
 .../contrib/django-aggregate-if-master/tox.ini     | 198 -----
 bitbake/lib/toaster/orm/models.py                  | 103 ++-
 .../lib/toaster/toastergui/static/js/libtoaster.js |   6 +-
 .../toaster/toastergui/static/js/projecttopbar.js  |   9 +
 bitbake/lib/toaster/toastergui/static/js/table.js  | 325 ++++++--
 bitbake/lib/toaster/toastergui/tablefilter.py      | 292 +++++++
 bitbake/lib/toaster/toastergui/tables.py           | 858 +++++++++++++++++++--
 .../toastergui/templates/baseprojectpage.html      |   1 +
 .../toastergui/templates/builds-toastertable.html  |  48 ++
 .../lib/toaster/toastergui/templates/builds.html   | 125 ---
 .../toaster/toastergui/templates/mrb_section.html  |   4 +-
 .../templates/projectbuilds-toastertable.html      |  56 ++
 .../templates/projects-toastertable.html           |  36 +
 .../lib/toaster/toastergui/templates/projects.html |  92 ---
 .../toastergui/templates/toastertable-filter.html  |   4 +-
 .../toaster/toastergui/templates/toastertable.html |   7 +-
 bitbake/lib/toaster/toastergui/tests.py            | 265 +++++--
 bitbake/lib/toaster/toastergui/urls.py             |  14 +-
 bitbake/lib/toaster/toastergui/views.py            | 491 +-----------
 bitbake/lib/toaster/toastergui/widgets.py          | 112 +--
 bitbake/lib/toaster/toastermain/settings.py        |   9 -
 28 files changed, 1898 insertions(+), 1639 deletions(-)
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py
 delete mode 100755 bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini
 create mode 100644 bitbake/lib/toaster/toastergui/tablefilter.py
 create mode 100644 bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
 delete mode 100644 bitbake/lib/toaster/toastergui/templates/builds.html
 create mode 100644 bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
 create mode 100644 bitbake/lib/toaster/toastergui/templates/projects-toastertable.html
 delete mode 100644 bitbake/lib/toaster/toastergui/templates/projects.html

--
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply	[flat|nested] 25+ messages in thread

* [review-request][PATCH 01/23] toaster: toastergui: use ToasterTable for projects page
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
  2016-01-15 11:07 ` Ed Bartosh
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 02/23] toaster: move image file suffix list to model Elliot Smith
                   ` (21 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The projects page uses the old approach for showing tables,
which means a template for each table. This means that applying
changes to ToasterTable (which is used for most tables) has
no effect on the layout, styling and behaviour for these older
tables, and requires additional duplicated effort.

Move the projects page to use ToasterTable instead, to remove
the duplication of effort.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/tables.py           | 226 ++++++++++++++++++++-
 .../templates/projects-toastertable.html           |  36 ++++
 bitbake/lib/toaster/toastergui/urls.py             |   6 +
 3 files changed, 265 insertions(+), 3 deletions(-)
 create mode 100644 bitbake/lib/toaster/toastergui/templates/projects-toastertable.html

diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 3808820..e5cab48 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -21,13 +21,12 @@
 
 from toastergui.widgets import ToasterTable
 from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
-from orm.models import CustomImageRecipe, Package
-from django.db.models import Q, Max
+from orm.models import CustomImageRecipe, Package, Build
+from django.db.models import Q, Max, Count
 from django.conf.urls import url
 from django.core.urlresolvers import reverse
 from django.views.generic import TemplateView
 
-
 class ProjectFiltersMixin(object):
     """Common mixin for recipe, machine in project filters"""
 
@@ -633,3 +632,224 @@ class SelectPackagesTable(ToasterTable):
                         "the package content of you custom image",
                         static_data_name="add_rm_pkg_btn",
                         static_data_template='{% include "pkg_add_rm_btn.html" %}')
+
+class ProjectsTable(ToasterTable):
+    """Table of projects in Toaster"""
+
+    def __init__(self, *args, **kwargs):
+        super(ProjectsTable, self).__init__(*args, **kwargs)
+        self.default_orderby = 'updated'
+        self.title = 'All projects'
+        self.static_context_extra['Build'] = Build
+
+    def get_context_data(self, **kwargs):
+        return super(ProjectsTable, self).get_context_data(**kwargs)
+
+    def setup_queryset(self, *args, **kwargs):
+        queryset = Project.objects.all()
+
+        # annotate each project with its number of builds
+        queryset = queryset.annotate(num_builds=Count('build'))
+
+        # exclude the command line builds project if it has no builds
+        q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
+        queryset = queryset.filter(Q(is_default=False) |
+                                   q_default_with_builds)
+
+        # order rows
+        queryset = queryset.order_by(self.default_orderby)
+
+        self.queryset = queryset
+
+    # columns: last activity on (updated) - DEFAULT, project (name), release, machine, number of builds, last build outcome, recipe (name), errors, warnings, image files
+    def setup_columns(self, *args, **kwargs):
+        name_template = '''
+        {% load project_url_tag %}
+        <span data-project-field="name">
+          <a href="{% project_url data %}">
+            {{data.name}}
+          </a>
+        </span>
+        '''
+
+        last_activity_on_template = '''
+        {% load project_url_tag %}
+        <span data-project-field="updated">
+          <a href="{% project_url data %}">
+            {{data.updated | date:"d/m/y H:i"}}
+          </a>
+        </span>
+        '''
+
+        release_template = '''
+        <span data-project-field="release">
+          {% if data.release %}
+            <a href="{% url 'project' data.id %}#project-details">
+                {{data.release.name}}
+            </a>
+          {% elif data.is_default %}
+            <span class="muted">Not applicable</span>
+            <i class="icon-question-sign get-help hover-help"
+               data-original-title="This project does not have a release set.
+               It simply collects information about the builds you start from
+               the command line while Toaster is running"
+               style="visibility: hidden;">
+            </i>
+          {% else %}
+            No release available
+          {% endif %}
+        </span>
+        '''
+
+        machine_template = '''
+        <span data-project-field="machine">
+          {% if data.is_default %}
+            <span class="muted">Not applicable</span>
+            <i class="icon-question-sign get-help hover-help"
+               data-original-title="This project does not have a machine
+               set. It simply collects information about the builds you
+               start from the command line while Toaster is running"
+               style="visibility: hidden;"></i>
+          {% else %}
+            <a href="{% url 'project' data.id %}#machine-distro">
+              {{data.get_current_machine_name}}
+            </a>
+          {% endif %}
+        </span>
+        '''
+
+        number_of_builds_template = '''
+        {% if data.get_number_of_builds > 0 %}
+          <a href="{% url 'projectbuilds' data.id %}">
+            {{data.get_number_of_builds}}
+          </a>
+        {% else %}
+          <span class="muted">0</span>
+        {% endif %}
+        '''
+
+        last_build_outcome_template = '''
+        {% if data.get_number_of_builds > 0 %}
+          <a href="{% url 'builddashboard' data.get_last_build_id %}">
+            {% if data.get_last_outcome == extra.Build.SUCCEEDED %}
+              <i class="icon-ok-sign success"></i>
+            {% elif data.get_last_outcome == extra.Build.FAILED %}
+              <i class="icon-minus-sign error"></i>
+            {% endif %}
+          </a>
+        {% endif %}
+        '''
+
+        recipe_template = '''
+        {% if data.get_number_of_builds > 0 %}
+          <a href="{% url "builddashboard" data.get_last_build_id %}">
+            {{data.get_last_target}}
+          </a>
+        {% endif %}
+        '''
+
+        errors_template = '''
+        {% if data.get_number_of_builds > 0 %}
+          <a class="errors.count error"
+             href="{% url "builddashboard" data.get_last_build_id %}#errors">
+            {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
+          </a>
+        {% endif %}
+        '''
+
+        warnings_template = '''
+        {% if data.get_number_of_builds > 0 %}
+          <a class="warnings.count warning"
+             href="{% url "builddashboard" data.get_last_build_id %}#warnings">
+            {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
+          </a>
+        {% endif %}
+        '''
+
+        image_files_template = '''
+        {% load projecttags %}
+        {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
+          <a href="{% url "builddashboard" data.get_last_build_id %}#images">
+            {{fstypes | get_dict_value:data.id}}
+          </a>
+        {% endif %}
+        '''
+
+        self.add_column(title='Project',
+                        hideable=False,
+                        orderable=True,
+                        static_data_name='name',
+                        static_data_template=name_template)
+
+        self.add_column(title='Last activity on',
+                        help_text='Starting date and time of the \
+                                   last project build. If the project has no \
+                                   builds, this shows the date the project was \
+                                   created.',
+                        hideable=True,
+                        orderable=True,
+                        static_data_name='updated',
+                        static_data_template=last_activity_on_template)
+
+        self.add_column(title='Release',
+                        help_text='The version of the build system used by \
+                                   the project',
+                        hideable=False,
+                        orderable=True,
+                        static_data_name='release',
+                        static_data_template=release_template)
+
+        self.add_column(title='Machine',
+                        help_text='The hardware currently selected for the \
+                                   project',
+                        hideable=False,
+                        orderable=False,
+                        static_data_name='machine',
+                        static_data_template=machine_template)
+
+        self.add_column(title='Number of builds',
+                        help_text='The number of builds which have been run \
+                                   for the project',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='number_of_builds',
+                        static_data_template=number_of_builds_template)
+
+        self.add_column(title='Last build outcome',
+                        help_text='Indicates whether the last project build \
+                                   completed successfully or failed',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='last_build_outcome',
+                        static_data_template=last_build_outcome_template)
+
+        self.add_column(title='Recipe',
+                        help_text='The last recipe which was built in this \
+                                   project',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='recipe_name',
+                        static_data_template=recipe_template)
+
+        self.add_column(title='Errors',
+                        help_text='The number of errors encountered during \
+                                   the last project build (if any)',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='errors',
+                        static_data_template=errors_template)
+
+        self.add_column(title='Warnings',
+                        help_text='The number of warnings encountered during \
+                                   the last project build (if any)',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='warnings',
+                        static_data_template=warnings_template)
+
+        self.add_column(title='Image files',
+                        help_text='',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='image_files',
+                        static_data_template=image_files_template)
diff --git a/bitbake/lib/toaster/toastergui/templates/projects-toastertable.html b/bitbake/lib/toaster/toastergui/templates/projects-toastertable.html
new file mode 100644
index 0000000..5814f32
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/projects-toastertable.html
@@ -0,0 +1,36 @@
+{% extends 'base.html' %}
+
+{% block title %} All projects - Toaster {% endblock %}
+
+{% block pagecontent %}
+
+  <div class="page-header top-air">
+    <h1 data-role="page-title"></h1>
+  </div>
+
+  {% url 'projects' as xhr_table_url %}
+  {% include 'toastertable.html' %}
+
+  <script>
+    $(document).ready(function () {
+      var tableElt = $("#{{table_name}}");
+      var titleElt = $("[data-role='page-title']");
+
+      tableElt.on("table-done", function (e, total, tableParams) {
+        var title = "All projects";
+
+        if (tableParams.search || tableParams.filter) {
+          if (total === 0) {
+            title = "No projects found";
+          }
+          else if (total > 0) {
+            title = total + " project" + (total > 1 ? 's' : '') + " found";
+          }
+        }
+
+        titleElt.text(title);
+      });
+    });
+  </script>
+
+{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index 2bf2d99..da97a31 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -75,8 +75,14 @@ urlpatterns = patterns('toastergui.views',
         url(r'^newproject/$', 'newproject', name='newproject'),
 
 
+        # TODO remove when new toaster table is ready
         url(r'^projects/$', 'projects', name='all-projects'),
 
+        # TODO move to /projects/ when new toaster table is ready
+        url(r'^projects-new/$',
+            tables.ProjectsTable.as_view(template_name="projects-toastertable.html"),
+            name='all-projects-new'),
+
         url(r'^project/(?P<pid>\d+)/$', 'project', name='project'),
         url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'),
         url(r'^project/(?P<pid>\d+)/builds/$', 'projectbuilds', name='projectbuilds'),
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 02/23] toaster: move image file suffix list to model
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
  2016-01-15 11:07 ` Ed Bartosh
  2016-01-15 11:43 ` [review-request][PATCH 01/23] toaster: toastergui: use ToasterTable for projects page Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 03/23] toaster: check inferred file suffixes against list of known types Elliot Smith
                   ` (20 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Image file suffixes are used in the project configuration page to
show a list of available image file types. This list is stored
as a function in the views code.

However, this list is also needed when parsing image file paths,
so that the suffixes can be shown in the "all builds" and "project
builds" tables.

Move the list of valid image file suffixes to the Target_Image_File
class to make is accessible in other places where it may be needed.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/orm/models.py       | 9 +++++++++
 bitbake/lib/toaster/toastergui/views.py | 7 +------
 2 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index e4ab0bb..7e0cf96 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -461,6 +461,15 @@ class Target(models.Model):
         return self.target
 
 class Target_Image_File(models.Model):
+    # valid suffixes for image files produced by a build
+    SUFFIXES = {
+        'btrfs', 'cpio', 'cpio.gz', 'cpio.lz4', 'cpio.lzma', 'cpio.xz',
+        'cramfs', 'elf', 'ext2', 'ext2.bz2', 'ext2.gz', 'ext2.lzma', 'ext4',
+        'ext4.gz', 'ext3', 'ext3.gz', 'hddimg', 'iso', 'jffs2', 'jffs2.sum',
+        'squashfs', 'squashfs-lzo', 'squashfs-xz', 'tar.bz2', 'tar.lz4',
+        'tar.xz', 'tartar.gz', 'ubi', 'ubifs', 'vmdk'
+    }
+
     target = models.ForeignKey(Target)
     file_name = models.FilePathField(max_length=254)
     file_size = models.IntegerField()
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index bd334b9..4cd7afd 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -2808,12 +2808,7 @@ if True:
             'all_proxy','ftp_proxy','http_proxy ','https_proxy'
             }
 
-        vars_fstypes  = {
-            'btrfs','cpio','cpio.gz','cpio.lz4','cpio.lzma','cpio.xz','cramfs',
-            'elf','ext2','ext2.bz2','ext2.gz','ext2.lzma', 'ext4', 'ext4.gz', 'ext3','ext3.gz','hddimg',
-            'iso','jffs2','jffs2.sum','squashfs','squashfs-lzo','squashfs-xz','tar.bz2',
-            'tar.lz4','tar.xz','tartar.gz','ubi','ubifs','vmdk'
-        }
+        vars_fstypes = Target_Image_File.SUFFIXES
 
         return(vars_managed,sorted(vars_fstypes),vars_blacklist)
 
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 03/23] toaster: check inferred file suffixes against list of known types
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (2 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 02/23] toaster: move image file suffix list to model Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 04/23] toaster: toastergui: switch projects/ view to ToasterTable Elliot Smith
                   ` (19 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The algorithm for finding the suffix for image files produced by
the build doesn't reference a list of known file suffixes, so
could be prone to error.

Modify how file suffixes are parsed from the file path so that
they are compared against a list of known types; if this fails,
use the part of the basename of the file path after the first
'.' character.

Also rationalise the places in the views code where we
extract the file name extensions for builds, so they both use
the same algorithm (before, the same code was duplicated in
two places).

[YOCTO #8417]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/orm/models.py        | 68 ++++++++++++++++++++++++++++++--
 bitbake/lib/toaster/toastergui/tables.py |  8 ++--
 bitbake/lib/toaster/toastergui/views.py  | 38 ++----------------
 3 files changed, 73 insertions(+), 41 deletions(-)

diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index 7e0cf96..b7975ef 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -29,6 +29,9 @@ from django.core import validators
 from django.conf import settings
 import django.db.models.signals
 
+import os.path
+import re
+
 import logging
 logger = logging.getLogger("toaster")
 
@@ -234,6 +237,14 @@ class Project(models.Model):
         except (Build.DoesNotExist,IndexError):
             return( "not_found" )
 
+    def get_last_build_extensions(self):
+        """
+        Get list of file name extensions for images produced by the most
+        recent build
+        """
+        last_build = Build.objects.get(pk = self.get_last_build_id())
+        return last_build.get_image_file_extensions()
+
     def get_last_imgfiles(self):
         build_id = self.get_last_build_id
         if (-1 == build_id):
@@ -376,6 +387,57 @@ class Build(models.Model):
             eta += ((eta - self.started_on)*(100-completeper))/completeper
         return eta
 
+    def get_image_file_extensions(self):
+        """
+        Get list of file name extensions for images produced by this build
+        """
+        targets = Target.objects.filter(build_id = self.id)
+        extensions = []
+
+        # pattern to match against file path for building extension string
+        pattern = re.compile('\.([^\.]+?)$')
+
+        for target in targets:
+            if (not target.is_image):
+                continue
+
+            target_image_files = Target_Image_File.objects.filter(target_id = target.id)
+
+            for target_image_file in target_image_files:
+                file_name = os.path.basename(target_image_file.file_name)
+                suffix = ''
+
+                continue_matching = True
+
+                # incrementally extract the suffix from the file path,
+                # checking it against the list of valid suffixes at each
+                # step; if the path is stripped of all potential suffix
+                # parts without matching a valid suffix, this returns all
+                # characters after the first '.' in the file name
+                while continue_matching:
+                    matches = pattern.search(file_name)
+
+                    if None == matches:
+                        continue_matching = False
+                        suffix = re.sub('^\.', '', suffix)
+                        continue
+                    else:
+                        suffix = matches.group(1) + suffix
+
+                    if suffix in Target_Image_File.SUFFIXES:
+                        continue_matching = False
+                        continue
+                    else:
+                        # reduce the file name and try to find the next
+                        # segment from the path which might be part
+                        # of the suffix
+                        file_name = re.sub('.' + matches.group(1), '', file_name)
+                        suffix = '.' + suffix
+
+                if not suffix in extensions:
+                    extensions.append(suffix)
+
+        return ', '.join(extensions)
 
     def get_sorted_target_list(self):
         tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
@@ -418,7 +480,7 @@ class Build(models.Model):
             return self.get_outcome_text()
 
     def __str__(self):
-        return "%s %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
+        return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
 
 
 # an Artifact is anything that results from a Build, and may be of interest to the user, and is not stored elsewhere
@@ -609,7 +671,7 @@ class Task(models.Model):
     sstate_text  = property(get_sstate_text)
 
     def __unicode__(self):
-        return "%s(%s) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
+        return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
 
     class Meta:
         ordering = ('order', 'recipe' ,)
@@ -1265,7 +1327,7 @@ class Layer_Version(models.Model):
         return sorted(result, key=lambda x: x.layer.name)
 
     def __unicode__(self):
-        return "%s %s (VCS %s, Project %s)" % (self.pk, str(self.layer), self.get_vcs_reference(), self.build.project if self.build is not None else "No project")
+        return "%d %s (VCS %s, Project %s)" % (self.pk, str(self.layer), self.get_vcs_reference(), self.build.project if self.build is not None else "No project")
 
     class Meta:
         unique_together = ("layer_source", "up_id")
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index e5cab48..cc9b5ae 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -661,7 +661,7 @@ class ProjectsTable(ToasterTable):
 
         self.queryset = queryset
 
-    # columns: last activity on (updated) - DEFAULT, project (name), release, machine, number of builds, last build outcome, recipe (name), errors, warnings, image files
+    # columns: last activity on (updated) - DEFAULT, project (name), release, machine, number of builds, last build outcome, recipe (name),  errors, warnings, image files
     def setup_columns(self, *args, **kwargs):
         name_template = '''
         {% load project_url_tag %}
@@ -767,10 +767,9 @@ class ProjectsTable(ToasterTable):
         '''
 
         image_files_template = '''
-        {% load projecttags %}
         {% if data.get_number_of_builds > 0 and data.get_last_outcome == extra.Build.SUCCEEDED %}
           <a href="{% url "builddashboard" data.get_last_build_id %}#images">
-            {{fstypes | get_dict_value:data.id}}
+            {{data.get_last_build_extensions}}
           </a>
         {% endif %}
         '''
@@ -848,7 +847,8 @@ class ProjectsTable(ToasterTable):
                         static_data_template=warnings_template)
 
         self.add_column(title='Image files',
-                        help_text='',
+                        help_text='The root file system types produced by \
+                                   the last project build',
                         hideable=True,
                         orderable=False,
                         static_data_name='image_files',
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 4cd7afd..8148623 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -2008,23 +2008,10 @@ if True:
         context_date,today_begin,yesterday_begin = _add_daterange_context(queryset_all, request, {'started_on','completed_on'})
 
         # set up list of fstypes for each build
-        fstypes_map = {};
+        fstypes_map = {}
+
         for build in build_info:
-            targets = Target.objects.filter( build_id = build.id )
-            comma = "";
-            extensions = "";
-            for t in targets:
-                if ( not t.is_image ):
-                    continue
-                tif = Target_Image_File.objects.filter( target_id = t.id )
-                for i in tif:
-                    s=re.sub('.*tar.bz2', 'tar.bz2', i.file_name)
-                    if s == i.file_name:
-                        s=re.sub('.*\.', '', i.file_name)
-                    if None == re.search(s,extensions):
-                        extensions += comma + s
-                        comma = ", "
-            fstypes_map[build.id]=extensions
+            fstypes_map[build.id] = build.get_image_file_extensions()
 
         # send the data to the template
         context = {
@@ -3047,24 +3034,7 @@ if True:
         # translate the project's build target strings
         fstypes_map = {};
         for project in project_info:
-            try:
-                targets = Target.objects.filter( build_id = project.get_last_build_id() )
-                comma = "";
-                extensions = "";
-                for t in targets:
-                    if ( not t.is_image ):
-                        continue
-                    tif = Target_Image_File.objects.filter( target_id = t.id )
-                    for i in tif:
-                        s=re.sub('.*tar.bz2', 'tar.bz2', i.file_name)
-                        if s == i.file_name:
-                            s=re.sub('.*\.', '', i.file_name)
-                        if None == re.search(s,extensions):
-                            extensions += comma + s
-                            comma = ", "
-                fstypes_map[project.id]=extensions
-            except (Target.DoesNotExist,IndexError):
-                fstypes_map[project.id]=project.get_last_imgfiles
+            fstypes_map[project.id] = project.get_last_build_extensions()
 
         context = {
                 'mru' : build_mru,
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 04/23] toaster: toastergui: switch projects/ view to ToasterTable
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (3 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 03/23] toaster: check inferred file suffixes against list of known types Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 05/23] toaster: toastergui: use event delegates for hover help elements Elliot Smith
                   ` (18 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Remove the old projects page and replace with the new
ToasterTable-based version.

NB although the projects.html template is no longer required,
it's been left in as there will be changes applied to it for
the new theme. These changes will have to then be transferred
from the projects.html template to projects-toastertable.html.

Similarly, the code for the projects page in views.py has been
retained.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/tables.py |  2 +-
 bitbake/lib/toaster/toastergui/urls.py   |  9 ++-------
 bitbake/lib/toaster/toastergui/views.py  | 24 ++++++++++++++++++++----
 3 files changed, 23 insertions(+), 12 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index cc9b5ae..2e3c8a6 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -638,7 +638,7 @@ class ProjectsTable(ToasterTable):
 
     def __init__(self, *args, **kwargs):
         super(ProjectsTable, self).__init__(*args, **kwargs)
-        self.default_orderby = 'updated'
+        self.default_orderby = '-updated'
         self.title = 'All projects'
         self.static_context_extra['Build'] = Build
 
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index da97a31..b5e9a05 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -74,14 +74,9 @@ urlpatterns = patterns('toastergui.views',
         # project URLs
         url(r'^newproject/$', 'newproject', name='newproject'),
 
-
-        # TODO remove when new toaster table is ready
-        url(r'^projects/$', 'projects', name='all-projects'),
-
-        # TODO move to /projects/ when new toaster table is ready
-        url(r'^projects-new/$',
+        url(r'^projects/$',
             tables.ProjectsTable.as_view(template_name="projects-toastertable.html"),
-            name='all-projects-new'),
+            name='all-projects'),
 
         url(r'^project/(?P<pid>\d+)/$', 'project', name='project'),
         url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'),
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 8148623..a79261d 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -2990,9 +2990,7 @@ if True:
             }
             return render(request, "unavailable_artifact.html", context)
 
-
-
-
+    """
     @_template_renderer("projects.html")
     def projects(request):
         (pagesize, orderby) = _get_parameters_values(request, 10, 'updated:-')
@@ -3034,7 +3032,24 @@ if True:
         # translate the project's build target strings
         fstypes_map = {};
         for project in project_info:
-            fstypes_map[project.id] = project.get_last_build_extensions()
+            try:
+                targets = Target.objects.filter( build_id = project.get_last_build_id() )
+                comma = "";
+                extensions = "";
+                for t in targets:
+                    if ( not t.is_image ):
+                        continue
+                    tif = Target_Image_File.objects.filter( target_id = t.id )
+                    for i in tif:
+                        s=re.sub('.*tar.bz2', 'tar.bz2', i.file_name)
+                        if s == i.file_name:
+                            s=re.sub('.*\.', '', i.file_name)
+                        if None == re.search(s,extensions):
+                            extensions += comma + s
+                            comma = ", "
+                fstypes_map[project.id]=extensions
+            except (Target.DoesNotExist,IndexError):
+                fstypes_map[project.id]=project.get_last_imgfiles
 
         context = {
                 'mru' : build_mru,
@@ -3092,3 +3107,4 @@ if True:
 
         _set_parameters_values(pagesize, orderby, request)
         return context
+    """
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 05/23] toaster: toastergui: use event delegates for hover help elements
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (4 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 04/23] toaster: toastergui: switch projects/ view to ToasterTable Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 06/23] toaster: toastergui: convert all builds page to ToasterTable Elliot Smith
                   ` (17 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

libtoaster.js binds to hover help elements via their hover() and
mouseout() methods. However, any elements added to the DOM after
libtoaster has initialised will not have these bindings added.
This causes a problem for ToasterTables which have hover-help
elements (e.g. the builds/ table).

Use the on() method instead. This uses event delegation to bind
a handler to any th or td elements already in the DOM, or
which will be added to the DOM in future. ToasterTables can
now reconstruct the table DOM and still have the correct handlers
attached once the table is done.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/static/js/libtoaster.js | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
index c04f7ab..1012034 100644
--- a/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
+++ b/bitbake/lib/toaster/toastergui/static/js/libtoaster.js
@@ -448,10 +448,12 @@ $(document).ready(function() {
 
     // show help bubble only on hover inside tables
     $(".hover-help").css("visibility","hidden");
-    $("th, td").hover(function () {
+
+    $("table").on("mouseover", "th, td", function () {
         $(this).find(".hover-help").css("visibility","visible");
     });
-    $("th, td").mouseleave(function () {
+
+    $("table").on("mouseleave", "th, td", function () {
         $(this).find(".hover-help").css("visibility","hidden");
     });
 
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 06/23] toaster: toastergui: convert all builds page to ToasterTable
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (5 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 05/23] toaster: toastergui: use event delegates for hover help elements Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 07/23] toaster: toastergui: refactor ToasterTable filtering Elliot Smith
                   ` (16 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

For better long-term maintainability, use ToasterTable instead
of Django template and view code to display the all builds page.

NB the builds.html template has been left in, as this will
otherwise cause conflicts when merging the new theme.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/orm/models.py                  |  32 +-
 bitbake/lib/toaster/toastergui/querysetfilter.py   |  24 ++
 bitbake/lib/toaster/toastergui/tables.py           | 343 ++++++++++++++++++---
 .../toastergui/templates/builds-toastertable.html  |  62 ++++
 bitbake/lib/toaster/toastergui/urls.py             |   5 +-
 bitbake/lib/toaster/toastergui/views.py            |  32 --
 bitbake/lib/toaster/toastergui/widgets.py          |  16 +-
 7 files changed, 434 insertions(+), 80 deletions(-)
 create mode 100644 bitbake/lib/toaster/toastergui/querysetfilter.py
 create mode 100644 bitbake/lib/toaster/toastergui/templates/builds-toastertable.html

diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index b7975ef..3dc4d6d 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -447,6 +447,12 @@ class Build(models.Model):
         return Build.BUILD_OUTCOME[int(self.outcome)][1]
 
     @property
+    def failed_tasks(self):
+        """ Get failed tasks for the build """
+        tasks = self.task_build.all()
+        return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED)
+
+    @property
     def errors(self):
         return (self.logmessage_set.filter(level=LogMessage.ERROR) |
                 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
@@ -457,8 +463,32 @@ class Build(models.Model):
         return self.logmessage_set.filter(level=LogMessage.WARNING)
 
     @property
+    def timespent(self):
+        return self.completed_on - self.started_on
+
+    @property
     def timespent_seconds(self):
-        return (self.completed_on - self.started_on).total_seconds()
+        return self.timespent.total_seconds()
+
+    @property
+    def target_labels(self):
+        """
+        Sorted (a-z) "target1:task, target2, target3" etc. string for all
+        targets in this build
+        """
+        targets = self.target_set.all()
+        target_labels = []
+        target_label = None
+
+        for target in targets:
+            target_label = target.target
+            if target.task:
+                target_label = target_label + ':' + target.task
+            target_labels.append(target_label)
+
+        target_labels.sort()
+
+        return target_labels
 
     def get_current_status(self):
         """
diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
new file mode 100644
index 0000000..62297e9
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/querysetfilter.py
@@ -0,0 +1,24 @@
+class QuerysetFilter(object):
+    """ Filter for a queryset """
+
+    def __init__(self, criteria=None):
+        if criteria:
+            self.set_criteria(criteria)
+
+    def set_criteria(self, criteria):
+        """
+        criteria is an instance of django.db.models.Q;
+        see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects
+        """
+        self.criteria = criteria
+
+    def filter(self, queryset):
+        """
+        Filter queryset according to the criteria for this filter,
+        returning the filtered queryset
+        """
+        return queryset.filter(self.criteria)
+
+    def count(self, queryset):
+        """ Returns a count of the elements in the filtered queryset """
+        return self.filter(queryset).count()
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 2e3c8a6..116cff3 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -20,29 +20,18 @@
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 from toastergui.widgets import ToasterTable
+from toastergui.querysetfilter import QuerysetFilter
 from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
-from orm.models import CustomImageRecipe, Package, Build
+from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task
 from django.db.models import Q, Max, Count
 from django.conf.urls import url
 from django.core.urlresolvers import reverse
 from django.views.generic import TemplateView
 
-class ProjectFiltersMixin(object):
-    """Common mixin for recipe, machine in project filters"""
-
-    def filter_in_project(self, count_only=False):
-        query = self.queryset.filter(layer_version__in=self.project_layers)
-        if count_only:
-            return query.count()
-
-        self.queryset = query
-
-    def filter_not_in_project(self, count_only=False):
-        query = self.queryset.exclude(layer_version__in=self.project_layers)
-        if count_only:
-            return query.count()
-
-        self.queryset = query
+class ProjectFilters(object):
+    def __init__(self, project_layers):
+        self.in_project = QuerysetFilter(Q(layer_version__in=project_layers))
+        self.not_in_project = QuerysetFilter(~Q(layer_version__in=project_layers))
 
 class LayersTable(ToasterTable):
     """Table of layers in Toaster"""
@@ -60,34 +49,21 @@ class LayersTable(ToasterTable):
 
         return context
 
-
     def setup_filters(self, *args, **kwargs):
         project = Project.objects.get(pk=kwargs['pid'])
         self.project_layers = ProjectLayer.objects.filter(project=project)
 
+        criteria = Q(projectlayer__in=self.project_layers)
+        in_project_filter = QuerysetFilter(criteria)
+        not_in_project_filter = QuerysetFilter(~criteria)
 
         self.add_filter(title="Filter by project layers",
                         name="in_current_project",
                         filter_actions=[
-                            self.make_filter_action("in_project", "Layers added to this project", self.filter_in_project),
-                            self.make_filter_action("not_in_project", "Layers not added to this project", self.filter_not_in_project)
+                            self.make_filter_action("in_project", "Layers added to this project", in_project_filter),
+                            self.make_filter_action("not_in_project", "Layers not added to this project", not_in_project_filter)
                         ])
 
-    def filter_in_project(self, count_only=False):
-        query = self.queryset.filter(projectlayer__in=self.project_layers)
-        if count_only:
-            return query.count()
-
-        self.queryset = query
-
-    def filter_not_in_project(self, count_only=False):
-        query = self.queryset.exclude(projectlayer__in=self.project_layers)
-        if count_only:
-            return query.count()
-
-        self.queryset = query
-
-
     def setup_queryset(self, *args, **kwargs):
         prj = Project.objects.get(pk = kwargs['pid'])
         compatible_layers = prj.get_all_compatible_layer_versions()
@@ -204,7 +180,7 @@ class LayersTable(ToasterTable):
                         computation = lambda x: x.layer.name)
 
 
-class MachinesTable(ToasterTable, ProjectFiltersMixin):
+class MachinesTable(ToasterTable):
     """Table of Machines in Toaster"""
 
     def __init__(self, *args, **kwargs):
@@ -221,11 +197,13 @@ class MachinesTable(ToasterTable, ProjectFiltersMixin):
     def setup_filters(self, *args, **kwargs):
         project = Project.objects.get(pk=kwargs['pid'])
 
+        project_filters = ProjectFilters(self.project_layers)
+
         self.add_filter(title="Filter by project machines",
                         name="in_current_project",
                         filter_actions=[
-                            self.make_filter_action("in_project", "Machines provided by layers added to this project", self.filter_in_project),
-                            self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", self.filter_not_in_project)
+                            self.make_filter_action("in_project", "Machines provided by layers added to this project", project_filters.in_project),
+                            self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", project_filters.not_in_project)
                         ])
 
     def setup_queryset(self, *args, **kwargs):
@@ -313,7 +291,7 @@ class LayerMachinesTable(MachinesTable):
                         static_data_template=select_btn_template)
 
 
-class RecipesTable(ToasterTable, ProjectFiltersMixin):
+class RecipesTable(ToasterTable):
     """Table of All Recipes in Toaster"""
 
     def __init__(self, *args, **kwargs):
@@ -338,11 +316,13 @@ class RecipesTable(ToasterTable, ProjectFiltersMixin):
         return context
 
     def setup_filters(self, *args, **kwargs):
+        project_filters = ProjectFilters(self.project_layers)
+
         self.add_filter(title="Filter by project recipes",
                         name="in_current_project",
                         filter_actions=[
-                            self.make_filter_action("in_project", "Recipes provided by layers added to this project", self.filter_in_project),
-                            self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", self.filter_not_in_project)
+                            self.make_filter_action("in_project", "Recipes provided by layers added to this project", project_filters.in_project),
+                            self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", project_filters.not_in_project)
                         ])
 
     def setup_queryset(self, *args, **kwargs):
@@ -853,3 +833,284 @@ class ProjectsTable(ToasterTable):
                         orderable=False,
                         static_data_name='image_files',
                         static_data_template=image_files_template)
+
+class BuildsTable(ToasterTable):
+    """Table of builds in Toaster"""
+
+    def __init__(self, *args, **kwargs):
+        super(BuildsTable, self).__init__(*args, **kwargs)
+        self.default_orderby = '-completed_on'
+        self.title = 'All builds'
+        self.static_context_extra['Build'] = Build
+        self.static_context_extra['Task'] = Task
+
+    def get_context_data(self, **kwargs):
+        return super(BuildsTable, self).get_context_data(**kwargs)
+
+    def setup_queryset(self, *args, **kwargs):
+        queryset = Build.objects.all()
+
+        # don't include in progress builds
+        queryset = queryset.exclude(outcome=Build.IN_PROGRESS)
+
+        # sort
+        queryset = queryset.order_by(self.default_orderby)
+
+        # annotate with number of ERROR and EXCEPTION log messages
+        queryset = queryset.annotate(
+            errors_no = Count(
+                'logmessage',
+                only = Q(logmessage__level=LogMessage.ERROR) |
+                       Q(logmessage__level=LogMessage.EXCEPTION)
+            )
+        )
+
+        # annotate with number of WARNING log messages
+        queryset = queryset.annotate(
+            warnings_no = Count(
+                'logmessage',
+                only = Q(logmessage__level=LogMessage.WARNING)
+            )
+        )
+
+        self.queryset = queryset
+
+    def setup_columns(self, *args, **kwargs):
+        outcome_template = '''
+        <a href="{% url "builddashboard" data.id %}">
+            {% if data.outcome == data.SUCCEEDED %}
+                <i class="icon-ok-sign success"></i>
+            {% elif data.outcome == data.FAILED %}
+                <i class="icon-minus-sign error"></i>
+            {% endif %}
+        </a>
+
+        {% if data.cooker_log_path %}
+            &nbsp;
+            <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
+               <i class="icon-download-alt" title="Download build log"></i>
+            </a>
+        {% endif %}
+        '''
+
+        recipe_template = '''
+        {% for target_label in data.target_labels %}
+            <a href="{% url "builddashboard" data.id %}">
+                {{target_label}}
+            </a>
+            <br />
+        {% endfor %}
+        '''
+
+        machine_template = '''
+        <a href="{% url "builddashboard" data.id %}">
+            {{data.machine}}
+        </a>
+        '''
+
+        started_on_template = '''
+        <a href="{% url "builddashboard" data.id %}">
+            {{data.started_on | date:"d/m/y H:i"}}
+        </a>
+        '''
+
+        completed_on_template = '''
+        <a href="{% url "builddashboard" data.id %}">
+            {{data.completed_on | date:"d/m/y H:i"}}
+        </a>
+        '''
+
+        failed_tasks_template = '''
+        {% if data.failed_tasks.count == 1 %}
+            <a href="{% url "task" data.id data.failed_tasks.0.id %}">
+                <span class="error">
+                    {{data.failed_tasks.0.recipe.name}}.{{data.failed_tasks.0.task_name}}
+                </span>
+            </a>
+            <a href="{% url "build_artifact" data.id "tasklogfile" data.failed_tasks.0.id %}">
+                <i class="icon-download-alt"
+                   data-original-title="Download task log file">
+                </i>
+            </a>
+        {% elif data.failed_tasks.count > 1 %}
+            <a href="{% url "tasks" data.id %}?filter=outcome%3A{{extra.Task.OUTCOME_FAILED}}">
+                <span class="error">{{data.failed_tasks.count}} tasks</span>
+            </a>
+        {% endif %}
+        '''
+
+        errors_template = '''
+        {% if data.errors.count %}
+            <a class="errors.count error" href="{% url "builddashboard" data.id %}#errors">
+                {{data.errors.count}} error{{data.errors.count|pluralize}}
+            </a>
+        {% endif %}
+        '''
+
+        warnings_template = '''
+        {% if data.warnings.count %}
+            <a class="warnings.count warning" href="{% url "builddashboard" data.id %}#warnings">
+                {{data.warnings.count}} warning{{data.warnings.count|pluralize}}
+            </a>
+        {% endif %}
+        '''
+
+        time_template = '''
+        {% load projecttags %}
+        <a href="{% url "buildtime" data.id %}">
+            {{data.timespent_seconds | sectohms}}
+        </a>
+        '''
+
+        image_files_template = '''
+        {% if data.outcome == extra.Build.SUCCEEDED %}
+          <a href="{% url "builddashboard" data.id %}#images">
+            {{data.get_image_file_extensions}}
+          </a>
+        {% endif %}
+        '''
+
+        project_template = '''
+        {% load project_url_tag %}
+        <a href="{% project_url data.project %}">
+            {{data.project.name}}
+        </a>
+        {% if data.project.is_default %}
+            <i class="icon-question-sign get-help hover-help" title=""
+               data-original-title="This project shows information about
+               the builds you start from the command line while Toaster is
+               running" style="visibility: hidden;"></i>
+        {% endif %}
+        '''
+
+        self.add_column(title='Outcome',
+                        help_text='Final state of the build (successful \
+                                   or failed)',
+                        hideable=False,
+                        orderable=True,
+                        filter_name='outcome_filter',
+                        static_data_name='outcome',
+                        static_data_template=outcome_template)
+
+        self.add_column(title='Recipe',
+                        help_text='What was built (i.e. one or more recipes \
+                                   or image recipes)',
+                        hideable=False,
+                        orderable=False,
+                        static_data_name='target',
+                        static_data_template=recipe_template)
+
+        self.add_column(title='Machine',
+                        help_text='Hardware for which you are building a \
+                                   recipe or image recipe',
+                        hideable=False,
+                        orderable=True,
+                        static_data_name='machine',
+                        static_data_template=machine_template)
+
+        self.add_column(title='Started on',
+                        help_text='The date and time when the build started',
+                        hideable=True,
+                        orderable=True,
+                        static_data_name='started_on',
+                        static_data_template=started_on_template)
+
+        self.add_column(title='Completed on',
+                        help_text='The date and time when the build finished',
+                        hideable=False,
+                        orderable=True,
+                        static_data_name='completed_on',
+                        static_data_template=completed_on_template)
+
+        self.add_column(title='Failed tasks',
+                        help_text='The number of tasks which failed during \
+                                   the build',
+                        hideable=True,
+                        orderable=False,
+                        filter_name='failed_tasks_filter',
+                        static_data_name='failed_tasks',
+                        static_data_template=failed_tasks_template)
+
+        self.add_column(title='Errors',
+                        help_text='The number of errors encountered during \
+                                   the build (if any)',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='errors',
+                        static_data_template=errors_template)
+
+        self.add_column(title='Warnings',
+                        help_text='The number of warnings encountered during \
+                                   the build (if any)',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='warnings',
+                        static_data_template=warnings_template)
+
+        self.add_column(title='Time',
+                        help_text='How long the build took to finish',
+                        hideable=False,
+                        orderable=False,
+                        static_data_name='time',
+                        static_data_template=time_template)
+
+        self.add_column(title='Image files',
+                        help_text='The root file system types produced by \
+                                   the build',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='image_files',
+                        static_data_template=image_files_template)
+
+        self.add_column(title='Project',
+                        hideable=True,
+                        orderable=False,
+                        static_data_name='project-name',
+                        static_data_template=project_template)
+
+    def setup_filters(self, *args, **kwargs):
+        # outcomes
+        filter_only_successful_builds = QuerysetFilter(Q(outcome=Build.SUCCEEDED))
+        successful_builds_filter = self.make_filter_action(
+            'successful_builds',
+            'Successful builds',
+            filter_only_successful_builds
+        )
+
+        filter_only_failed_builds = QuerysetFilter(Q(outcome=Build.FAILED))
+        failed_builds_filter = self.make_filter_action(
+            'failed_builds',
+            'Failed builds',
+            filter_only_failed_builds
+        )
+
+        self.add_filter(title='Filter builds by outcome',
+                        name='outcome_filter',
+                        filter_actions = [
+                            successful_builds_filter,
+                            failed_builds_filter
+                        ])
+
+        # failed tasks
+        criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
+        filter_only_builds_with_failed_tasks = QuerysetFilter(criteria)
+        with_failed_tasks_filter = self.make_filter_action(
+            'with_failed_tasks',
+            'Builds with failed tasks',
+            filter_only_builds_with_failed_tasks
+        )
+
+        criteria = ~Q(task_build__outcome=Task.OUTCOME_FAILED)
+        filter_only_builds_without_failed_tasks = QuerysetFilter(criteria)
+        without_failed_tasks_filter = self.make_filter_action(
+            'without_failed_tasks',
+            'Builds without failed tasks',
+            filter_only_builds_without_failed_tasks
+        )
+
+        self.add_filter(title='Filter builds by failed tasks',
+                        name='failed_tasks_filter',
+                        filter_actions = [
+                            with_failed_tasks_filter,
+                            without_failed_tasks_filter
+                        ])
diff --git a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
new file mode 100644
index 0000000..419d2b5
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
@@ -0,0 +1,62 @@
+{% extends 'base.html' %}
+
+{% block title %} All builds - Toaster {% endblock %}
+
+{% block pagecontent %}
+  <div class="page-header top-air">
+    <h1 data-role="page-title"></h1>
+  </div>
+
+  <div class="row-fluid">
+    {# TODO need to pass this data to context #}
+    {#% include 'mrb_section.html' %#}
+
+    {% url 'builds' as xhr_table_url %}
+    {% include 'toastertable.html' %}
+  </div>
+
+  <script>
+    $(document).ready(function () {
+      var tableElt = $("#{{table_name}}");
+      var titleElt = $("[data-role='page-title']");
+
+      tableElt.on("table-done", function (e, total, tableParams) {
+        var title = "All builds";
+
+        if (tableParams.search || tableParams.filter) {
+          if (total === 0) {
+            title = "No builds found";
+          }
+          else if (total > 0) {
+            title = total + " build" + (total > 1 ? 's' : '') + " found";
+          }
+        }
+
+        titleElt.text(title);
+      });
+
+      /* {% if last_date_from and last_date_to %}
+      // TODO initialize the date range controls;
+      // this will need to be added via ToasterTable
+      date_init(
+        "started_on",
+        "{{last_date_from}}",
+        "{{last_date_to}}",
+        "{{dateMin_started_on}}",
+        "{{dateMax_started_on}}",
+        "{{daterange_selected}}"
+      );
+
+      date_init(
+        "completed_on",
+        "{{last_date_from}}",
+        "{{last_date_to}}",
+        "{{dateMin_completed_on}}",
+        "{{dateMax_completed_on}}",
+        "{{daterange_selected}}"
+      );
+      {% endif %}
+      */
+    });
+  </script>
+{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index b5e9a05..707b7d5 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -27,7 +27,10 @@ urlpatterns = patterns('toastergui.views',
         # landing page
         url(r'^landing/$', 'landing', name='landing'),
 
-        url(r'^builds/$', 'builds', name='all-builds'),
+        url(r'^builds/$',
+            tables.BuildsTable.as_view(template_name="builds-toastertable.html"),
+            name='all-builds'),
+
         # build info navigation
         url(r'^build/(?P<build_id>\d+)$', 'builddashboard', name="builddashboard"),
 
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index a79261d..295773f 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -1915,34 +1915,6 @@ if True:
         ''' The exception raised on invalid POST requests '''
         pass
 
-    # shows the "all builds" page for managed mode; it displays build requests (at least started!) instead of actual builds
-    # WARNING _build_list_helper() may raise a RedirectException, which
-    # will set the GET parameters and redirect back to the
-    # all-builds or projectbuilds page as appropriate;
-    # TODO don't use exceptions to control program flow
-    @_template_renderer("builds.html")
-    def builds(request):
-        # define here what parameters the view needs in the GET portion in order to
-        # be able to display something.  'count' and 'page' are mandatory for all views
-        # that use paginators.
-
-        queryset = Build.objects.all()
-
-        redirect_page = resolve(request.path_info).url_name
-
-        context, pagesize, orderby = _build_list_helper(request,
-                                                        queryset,
-                                                        redirect_page)
-        # all builds page as a Project column
-        context['tablecols'].append({
-            'name': 'Project',
-            'clclass': 'project_column'
-        })
-
-        _set_parameters_values(pagesize, orderby, request)
-        return context
-
-
     # helper function, to be used on "all builds" and "project builds" pages
     def _build_list_helper(request, queryset_all, redirect_page, pid=None):
         default_orderby = 'completed_on:-'
@@ -1986,10 +1958,6 @@ if True:
             warnings_no = Count('logmessage', only=q_warnings)
         )
 
-        # add timespent field
-        timespent = 'completed_on - started_on'
-        queryset_all = queryset_all.extra(select={'timespent': timespent})
-
         queryset_with_search = _get_queryset(Build, queryset_all,
                                              None, search_term,
                                              ordering_string, '-completed_on')
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index 6bb3889..71b29ea 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -32,6 +32,7 @@ from django.template import Context, Template
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.exceptions import FieldError
 from django.conf.urls import url, patterns
+from toastergui.querysetfilter import QuerysetFilter
 
 import types
 import json
@@ -113,7 +114,8 @@ class ToasterTable(TemplateView):
                               cls=DjangoJSONEncoder)
         else:
             for actions in self.filters[name]['filter_actions']:
-                actions['count'] = self.filter_actions[actions['name']](count_only=True)
+                queryset_filter = self.filter_actions[actions['name']]
+                actions['count'] = queryset_filter.count(self.queryset)
 
             # Add the "All" items filter action
             self.filters[name]['filter_actions'].insert(0, {
@@ -151,15 +153,18 @@ class ToasterTable(TemplateView):
           'filter_actions' : filter_actions,
         }
 
-    def make_filter_action(self, name, title, action_function):
-        """ Utility to make a filter_action """
+    def make_filter_action(self, name, title, queryset_filter):
+        """
+        Utility to make a filter_action; queryset_filter is an instance
+        of QuerysetFilter or a function
+        """
 
         action = {
           'title' : title,
           'name' : name,
         }
 
-        self.filter_actions[name] = action_function
+        self.filter_actions[name] = queryset_filter
 
         return action
 
@@ -222,7 +227,8 @@ class ToasterTable(TemplateView):
             return
 
         try:
-            self.filter_actions[filter_action]()
+            queryset_filter = self.filter_actions[filter_action]
+            self.queryset = queryset_filter.filter(self.queryset)
         except KeyError:
             # pass it to the user - programming error here
             raise
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 07/23] toaster: toastergui: refactor ToasterTable filtering
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (6 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 06/23] toaster: toastergui: convert all builds page to ToasterTable Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 08/23] toaster: toastergui: switch off filter highlights when inactive Elliot Smith
                   ` (15 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The filter code for ToasterTable was difficult to follow
and inflexible (not allowing different types of filter, for example).

Refactor to a set of filter classes to make the structure cleaner
and provide the flexibility needed for other filter types
(e.g. date range filter).

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/querysetfilter.py  |   7 +-
 bitbake/lib/toaster/toastergui/static/js/table.js |  80 +++++++++----
 bitbake/lib/toaster/toastergui/tablefilter.py     | 119 +++++++++++++++++++
 bitbake/lib/toaster/toastergui/tables.py          | 132 ++++++++++++++--------
 bitbake/lib/toaster/toastergui/widgets.py         |  90 +++++++--------
 5 files changed, 310 insertions(+), 118 deletions(-)
 create mode 100644 bitbake/lib/toaster/toastergui/tablefilter.py

diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
index 62297e9..dbae239 100644
--- a/bitbake/lib/toaster/toastergui/querysetfilter.py
+++ b/bitbake/lib/toaster/toastergui/querysetfilter.py
@@ -5,7 +5,7 @@ class QuerysetFilter(object):
         if criteria:
             self.set_criteria(criteria)
 
-    def set_criteria(self, criteria):
+    def set_criteria(self, criteria = None):
         """
         criteria is an instance of django.db.models.Q;
         see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects
@@ -17,7 +17,10 @@ class QuerysetFilter(object):
         Filter queryset according to the criteria for this filter,
         returning the filtered queryset
         """
-        return queryset.filter(self.criteria)
+        if self.criteria:
+            return queryset.filter(self.criteria)
+        else:
+            return queryset
 
     def count(self, queryset):
         """ Returns a count of the elements in the filtered queryset """
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index c69c205..fa01ddf 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -415,38 +415,76 @@ function tableInit(ctx){
         data: params,
         headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
         success: function (filterData) {
-          var filterActionRadios = $('#filter-actions-'+ctx.tableName);
+          /*
+            filterData structure:
+
+            {
+              title: '<title for the filter popup>',
+              filter_actions: [
+                {
+                  title: '<label for radio button inside the popup>',
+                  name: '<name of the filter action>',
+                  count: <number of items this filter will show>
+                }
+              ]
+            }
 
-          $('#filter-modal-title-'+ctx.tableName).text(filterData.title);
+            each filter_action gets a radio button; the value of this is
+            set to filterName + ':' + filter_action.name; e.g.
 
-          filterActionRadios.text("");
+              in_current_project:in_project
 
-          for (var i in filterData.filter_actions){
-            var filterAction = filterData.filter_actions[i];
+            specifies the "in_project" action of the "in_current_project"
+            filter
 
-            var action = $('<label class="radio"><input type="radio" name="filter" value=""><span class="filter-title"></span></label>');
-            var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
+            the filterName is set on the column filter icon, and corresponds
+            to a value in the table's filters property
 
-            var radioInput = action.children("input");
+            when the filter popup's "Apply" button is clicked, the
+            value for the radio button which is checked is passed in the
+            querystring and applied to the queryset on the table
+           */
 
-            if (Number(filterAction.count) == 0){
-              radioInput.attr("disabled", "disabled");
-            }
+          var filterActionRadios = $('#filter-actions-'+ctx.tableName);
 
-            action.children(".filter-title").text(actionTitle);
+          $('#filter-modal-title-'+ctx.tableName).text(filterData.title);
 
-            radioInput.val(filterName + ':' + filterAction.name);
+          filterActionRadios.text("");
 
-            /* Setup the current selected filter, default to 'all' if
-             * no current filter selected.
-             */
-            if ((tableParams.filter &&
-                tableParams.filter === radioInput.val()) ||
-                filterAction.name == 'all') {
-                radioInput.attr("checked", "checked");
+          for (var i in filterData.filter_actions) {
+            var filterAction = filterData.filter_actions[i];
+            var action = null;
+
+            if (filterAction.type === 'toggle') {
+              var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
+
+              action = $('<label class="radio">' +
+                         '<input type="radio" name="filter" value="">' +
+                         '<span class="filter-title">' +
+                         actionTitle +
+                         '</span>' +
+                         '</label>');
+
+              var radioInput = action.children("input");
+              if (Number(filterAction.count) == 0) {
+                radioInput.attr("disabled", "disabled");
+              }
+
+              radioInput.val(filterData.name + ':' + filterAction.action_name);
+
+              /* Setup the current selected filter, default to 'all' if
+               * no current filter selected.
+               */
+              if ((tableParams.filter &&
+                  tableParams.filter === radioInput.val()) ||
+                  filterAction.action_name == 'all') {
+                  radioInput.attr("checked", "checked");
+              }
             }
 
-            filterActionRadios.append(action);
+            if (action) {
+              filterActionRadios.append(action);
+            }
           }
 
           $('#filter-modal-'+ctx.tableName).modal('show');
diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py b/bitbake/lib/toaster/toastergui/tablefilter.py
new file mode 100644
index 0000000..b42fd52
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/tablefilter.py
@@ -0,0 +1,119 @@
+#
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2015        Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+class TableFilter(object):
+    """
+    Stores a filter for a named field, and can retrieve the action
+    requested for that filter
+    """
+    def __init__(self, name, title):
+        self.name = name
+        self.title = title
+        self.__filter_action_map = {}
+
+    def add_action(self, action):
+        self.__filter_action_map[action.name] = action
+
+    def get_action(self, action_name):
+        return self.__filter_action_map[action_name]
+
+    def to_json(self, queryset):
+        """
+        Dump all filter actions as an object which can be JSON serialised;
+        this is used to generate the JSON for processing in
+        table.js / filterOpenClicked()
+        """
+        filter_actions = []
+
+        # add the "all" pseudo-filter action, which just selects the whole
+        # queryset
+        filter_actions.append({
+            'action_name' : 'all',
+            'title' : 'All',
+            'type': 'toggle',
+            'count' : queryset.count()
+        })
+
+        # add other filter actions
+        for action_name, filter_action in self.__filter_action_map.iteritems():
+            obj = filter_action.to_json(queryset)
+            obj['action_name'] = action_name
+            filter_actions.append(obj)
+
+        return {
+            'name': self.name,
+            'title': self.title,
+            'filter_actions': filter_actions
+        }
+
+class TableFilterActionToggle(object):
+    """
+    Stores a single filter action which will populate one radio button of
+    a ToasterTable filter popup; this filter can either be on or off and
+    has no other parameters
+    """
+
+    def __init__(self, name, title, queryset_filter):
+        self.name = name
+        self.title = title
+        self.__queryset_filter = queryset_filter
+        self.type = 'toggle'
+
+    def set_params(self, params):
+        """
+        params: (str) a string of extra parameters for the action;
+        the structure of this string depends on the type of action;
+        it's ignored for a toggle filter action, which is just on or off
+        """
+        pass
+
+    def filter(self, queryset):
+        return self.__queryset_filter.filter(queryset)
+
+    def to_json(self, queryset):
+        """ Dump as a JSON object """
+        return {
+            'title': self.title,
+            'type': self.type,
+            'count': self.__queryset_filter.count(queryset)
+        }
+
+class TableFilterMap(object):
+    """
+    Map from field names to Filter objects for those fields
+    """
+    def __init__(self):
+        self.__filters = {}
+
+    def add_filter(self, filter_name, table_filter):
+        """ table_filter is an instance of Filter """
+        self.__filters[filter_name] = table_filter
+
+    def get_filter(self, filter_name):
+        return self.__filters[filter_name]
+
+    def to_json(self, queryset):
+        data = {}
+
+        for filter_name, table_filter in self.__filters.iteritems():
+            data[filter_name] = table_filter.to_json()
+
+        return data
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 116cff3..a0991ec 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -28,6 +28,8 @@ from django.conf.urls import url
 from django.core.urlresolvers import reverse
 from django.views.generic import TemplateView
 
+from toastergui.tablefilter import TableFilter, TableFilterActionToggle
+
 class ProjectFilters(object):
     def __init__(self, project_layers):
         self.in_project = QuerysetFilter(Q(layer_version__in=project_layers))
@@ -53,16 +55,28 @@ class LayersTable(ToasterTable):
         project = Project.objects.get(pk=kwargs['pid'])
         self.project_layers = ProjectLayer.objects.filter(project=project)
 
+        in_current_project_filter = TableFilter(
+            "in_current_project",
+            "Filter by project layers"
+        )
+
         criteria = Q(projectlayer__in=self.project_layers)
-        in_project_filter = QuerysetFilter(criteria)
-        not_in_project_filter = QuerysetFilter(~criteria)
 
-        self.add_filter(title="Filter by project layers",
-                        name="in_current_project",
-                        filter_actions=[
-                            self.make_filter_action("in_project", "Layers added to this project", in_project_filter),
-                            self.make_filter_action("not_in_project", "Layers not added to this project", not_in_project_filter)
-                        ])
+        in_project_filter_action = TableFilterActionToggle(
+            "in_project",
+            "Layers added to this project",
+            QuerysetFilter(criteria)
+        )
+
+        not_in_project_filter_action = TableFilterActionToggle(
+            "not_in_project",
+            "Layers not added to this project",
+            QuerysetFilter(~criteria)
+        )
+
+        in_current_project_filter.add_action(in_project_filter_action)
+        in_current_project_filter.add_action(not_in_project_filter_action)
+        self.add_filter(in_current_project_filter)
 
     def setup_queryset(self, *args, **kwargs):
         prj = Project.objects.get(pk = kwargs['pid'])
@@ -199,12 +213,26 @@ class MachinesTable(ToasterTable):
 
         project_filters = ProjectFilters(self.project_layers)
 
-        self.add_filter(title="Filter by project machines",
-                        name="in_current_project",
-                        filter_actions=[
-                            self.make_filter_action("in_project", "Machines provided by layers added to this project", project_filters.in_project),
-                            self.make_filter_action("not_in_project", "Machines provided by layers not added to this project", project_filters.not_in_project)
-                        ])
+        in_current_project_filter = TableFilter(
+            "in_current_project",
+            "Filter by project machines"
+        )
+
+        in_project_filter_action = TableFilterActionToggle(
+            "in_project",
+            "Machines provided by layers added to this project",
+            project_filters.in_project
+        )
+
+        not_in_project_filter_action = TableFilterActionToggle(
+            "not_in_project",
+            "Machines provided by layers not added to this project",
+            project_filters.not_in_project
+        )
+
+        in_current_project_filter.add_action(in_project_filter_action)
+        in_current_project_filter.add_action(not_in_project_filter_action)
+        self.add_filter(in_current_project_filter)
 
     def setup_queryset(self, *args, **kwargs):
         prj = Project.objects.get(pk = kwargs['pid'])
@@ -318,12 +346,26 @@ class RecipesTable(ToasterTable):
     def setup_filters(self, *args, **kwargs):
         project_filters = ProjectFilters(self.project_layers)
 
-        self.add_filter(title="Filter by project recipes",
-                        name="in_current_project",
-                        filter_actions=[
-                            self.make_filter_action("in_project", "Recipes provided by layers added to this project", project_filters.in_project),
-                            self.make_filter_action("not_in_project", "Recipes provided by layers not added to this project", project_filters.not_in_project)
-                        ])
+        table_filter = TableFilter(
+            'in_current_project',
+            'Filter by project recipes'
+        )
+
+        in_project_filter_action = TableFilterActionToggle(
+            'in_project',
+            'Recipes provided by layers added to this project',
+            project_filters.in_project
+        )
+
+        not_in_project_filter_action = TableFilterActionToggle(
+            'not_in_project',
+            'Recipes provided by layers not added to this project',
+            project_filters.not_in_project
+        )
+
+        table_filter.add_action(in_project_filter_action)
+        table_filter.add_action(not_in_project_filter_action)
+        self.add_filter(table_filter)
 
     def setup_queryset(self, *args, **kwargs):
         prj = Project.objects.get(pk = kwargs['pid'])
@@ -1070,47 +1112,47 @@ class BuildsTable(ToasterTable):
 
     def setup_filters(self, *args, **kwargs):
         # outcomes
-        filter_only_successful_builds = QuerysetFilter(Q(outcome=Build.SUCCEEDED))
-        successful_builds_filter = self.make_filter_action(
+        outcome_filter = TableFilter(
+            'outcome_filter',
+            'Filter builds by outcome'
+        )
+
+        successful_builds_filter_action = TableFilterActionToggle(
             'successful_builds',
             'Successful builds',
-            filter_only_successful_builds
+            QuerysetFilter(Q(outcome=Build.SUCCEEDED))
         )
 
-        filter_only_failed_builds = QuerysetFilter(Q(outcome=Build.FAILED))
-        failed_builds_filter = self.make_filter_action(
+        failed_builds_filter_action = TableFilterActionToggle(
             'failed_builds',
             'Failed builds',
-            filter_only_failed_builds
+            QuerysetFilter(Q(outcome=Build.FAILED))
         )
 
-        self.add_filter(title='Filter builds by outcome',
-                        name='outcome_filter',
-                        filter_actions = [
-                            successful_builds_filter,
-                            failed_builds_filter
-                        ])
+        outcome_filter.add_action(successful_builds_filter_action)
+        outcome_filter.add_action(failed_builds_filter_action)
+        self.add_filter(outcome_filter)
 
         # failed tasks
+        failed_tasks_filter = TableFilter(
+            'failed_tasks_filter',
+            'Filter builds by failed tasks'
+        )
+
         criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
-        filter_only_builds_with_failed_tasks = QuerysetFilter(criteria)
-        with_failed_tasks_filter = self.make_filter_action(
+
+        with_failed_tasks_filter_action = TableFilterActionToggle(
             'with_failed_tasks',
             'Builds with failed tasks',
-            filter_only_builds_with_failed_tasks
+            QuerysetFilter(criteria)
         )
 
-        criteria = ~Q(task_build__outcome=Task.OUTCOME_FAILED)
-        filter_only_builds_without_failed_tasks = QuerysetFilter(criteria)
-        without_failed_tasks_filter = self.make_filter_action(
+        without_failed_tasks_filter_action = TableFilterActionToggle(
             'without_failed_tasks',
             'Builds without failed tasks',
-            filter_only_builds_without_failed_tasks
+            QuerysetFilter(~criteria)
         )
 
-        self.add_filter(title='Filter builds by failed tasks',
-                        name='failed_tasks_filter',
-                        filter_actions = [
-                            with_failed_tasks_filter,
-                            without_failed_tasks_filter
-                        ])
+        failed_tasks_filter.add_action(with_failed_tasks_filter_action)
+        failed_tasks_filter.add_action(without_failed_tasks_filter_action)
+        self.add_filter(failed_tasks_filter)
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index 71b29ea..8790340 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -39,11 +39,13 @@ import json
 import collections
 import operator
 import re
+import urllib
 
 import logging
 logger = logging.getLogger("toaster")
 
 from toastergui.views import objtojson
+from toastergui.tablefilter import TableFilterMap
 
 class ToasterTable(TemplateView):
     def __init__(self, *args, **kwargs):
@@ -53,7 +55,10 @@ class ToasterTable(TemplateView):
         self.title = "Table"
         self.queryset = None
         self.columns = []
-        self.filters = {}
+
+        # map from field names to Filter instances
+        self.filter_map = TableFilterMap()
+
         self.total_count = 0
         self.static_context_extra = {}
         self.filter_actions = {}
@@ -66,7 +71,7 @@ class ToasterTable(TemplateView):
                         orderable=True,
                         field_name="id")
 
-        # prevent HTTP caching of table data
+    # prevent HTTP caching of table data
     @cache_control(must_revalidate=True, max_age=0, no_store=True, no_cache=True)
     def dispatch(self, *args, **kwargs):
         return super(ToasterTable, self).dispatch(*args, **kwargs)
@@ -108,27 +113,10 @@ class ToasterTable(TemplateView):
             self.apply_search(search)
 
         name = request.GET.get("name", None)
-        if name is None:
-            data = json.dumps(self.filters,
-                              indent=2,
-                              cls=DjangoJSONEncoder)
-        else:
-            for actions in self.filters[name]['filter_actions']:
-                queryset_filter = self.filter_actions[actions['name']]
-                actions['count'] = queryset_filter.count(self.queryset)
-
-            # Add the "All" items filter action
-            self.filters[name]['filter_actions'].insert(0, {
-                'name' : 'all',
-                'title' : 'All',
-                'count' : self.queryset.count(),
-            })
-
-            data = json.dumps(self.filters[name],
-                              indent=2,
-                              cls=DjangoJSONEncoder)
-
-            return data
+        table_filter = self.filter_map.get_filter(name)
+        return json.dumps(table_filter.to_json(self.queryset),
+                          indent=2,
+                          cls=DjangoJSONEncoder)
 
     def setup_columns(self, *args, **kwargs):
         """ function to implement in the subclass which sets up the columns """
@@ -140,33 +128,13 @@ class ToasterTable(TemplateView):
         """ function to implement in the subclass which sets up the queryset"""
         pass
 
-    def add_filter(self, name, title, filter_actions):
+    def add_filter(self, table_filter):
         """Add a filter to the table.
 
         Args:
-            name (str): Unique identifier of the filter.
-            title (str): Title of the filter.
-            filter_actions: Actions for all the filters.
+            table_filter: Filter instance
         """
-        self.filters[name] = {
-          'title' : title,
-          'filter_actions' : filter_actions,
-        }
-
-    def make_filter_action(self, name, title, queryset_filter):
-        """
-        Utility to make a filter_action; queryset_filter is an instance
-        of QuerysetFilter or a function
-        """
-
-        action = {
-          'title' : title,
-          'name' : name,
-        }
-
-        self.filter_actions[name] = queryset_filter
-
-        return action
+        self.filter_map.add_filter(table_filter.name, table_filter)
 
     def add_column(self, title="", help_text="",
                    orderable=False, hideable=True, hidden=False,
@@ -216,19 +184,41 @@ class ToasterTable(TemplateView):
         return template.render(context)
 
     def apply_filter(self, filters, **kwargs):
+        """
+        Apply a filter submitted in the querystring to the ToasterTable
+
+        filters: (str) in the format:
+          '<filter name>:<action name>!<action params>'
+        where <action params> is optional
+
+        <filter name> and <action name> are used to look up the correct filter
+        in the ToasterTable's filter map; the <action params> are set on
+        TableFilterAction* before its filter is applied and may modify the
+        queryset returned by the filter
+        """
         self.setup_filters(**kwargs)
 
         try:
-            filter_name, filter_action = filters.split(':')
+            filter_name, action_name_and_params = filters.split(':')
+
+            action_name = None
+            action_params = None
+            if re.search('!', action_name_and_params):
+                action_name, action_params = action_name_and_params.split('!')
+                action_params = urllib.unquote_plus(action_params)
+            else:
+                action_name = action_name_and_params
         except ValueError:
             return
 
-        if "all" in filter_action:
+        if "all" in action_name:
             return
 
         try:
-            queryset_filter = self.filter_actions[filter_action]
-            self.queryset = queryset_filter.filter(self.queryset)
+            table_filter = self.filter_map.get_filter(filter_name)
+            action = table_filter.get_action(action_name)
+            action.set_params(action_params)
+            self.queryset = action.filter(self.queryset)
         except KeyError:
             # pass it to the user - programming error here
             raise
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 08/23] toaster: toastergui: switch off filter highlights when inactive
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (7 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 07/23] toaster: toastergui: refactor ToasterTable filtering Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 09/23] toaster: toastergui: show recent builds on all builds page Elliot Smith
                   ` (14 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

In ToasterTables with multiple columns which allow filtering
(e.g. all builds), selecting one filter, then a second filter
(e.g. selecting "failed builds" then "outcome" for all builds),
would result in both filters being highlighted at the same time.

Fix this by removing the "active" highlight on all column filter
buttons when a new filter value is submitted (via the filter modal).

NB to enable this, added a data-filter-on attribute to all
column filter buttons to make them easy to select.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/static/js/table.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index fa01ddf..63f8a1f 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -248,7 +248,7 @@ function tableInit(ctx){
 
       /* Setup the filter button */
       if (col.filter_name){
-        var filterBtn = $('<a href="#" role="button" class="pull-right btn btn-mini" data-toggle="modal"><i class="icon-filter filtered"></i></a>');
+        var filterBtn = $('<a href="#" role="button" data-filter-on="' + col.filter_name + '" class="pull-right btn btn-mini" data-toggle="modal"><i class="icon-filter filtered"></i></a>');
 
         filterBtn.data('filter-name', col.filter_name);
         filterBtn.prop('id', col.filter_name);
@@ -565,6 +565,12 @@ function tableInit(ctx){
   $("#filter-modal-form-"+ctx.tableName).submit(function(e){
     e.preventDefault();
 
+    /* remove active status from all filter buttons so that only one filter
+       can be active at a time */
+    $('[data-filter-on]').each(function (index, filterBtn) {
+      filterBtnActive($(filterBtn), false);
+    });
+
     tableParams.filter = $(this).find("input[type='radio']:checked").val();
 
     var filterBtn = $("#" + tableParams.filter.split(":")[0]);
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 09/23] toaster: toastergui: show recent builds on all builds page
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (8 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 08/23] toaster: toastergui: switch off filter highlights when inactive Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 10/23] toaster: toastergui: implement date range filters for builds Elliot Smith
                   ` (13 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The recent builds section was disabled while converting the
all builds page to ToasterTable.

Re-enable the recent builds area and add the data it requires
to the ToasterTable context.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/tables.py               | 18 +++++++++++++++++-
 .../toastergui/templates/builds-toastertable.html      | 10 +++++-----
 .../lib/toaster/toastergui/templates/mrb_section.html  |  2 +-
 3 files changed, 23 insertions(+), 7 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index a0991ec..0941637 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -27,6 +27,7 @@ from django.db.models import Q, Max, Count
 from django.conf.urls import url
 from django.core.urlresolvers import reverse
 from django.views.generic import TemplateView
+import itertools
 
 from toastergui.tablefilter import TableFilter, TableFilterActionToggle
 
@@ -887,7 +888,22 @@ class BuildsTable(ToasterTable):
         self.static_context_extra['Task'] = Task
 
     def get_context_data(self, **kwargs):
-        return super(BuildsTable, self).get_context_data(**kwargs)
+        context = super(BuildsTable, self).get_context_data(**kwargs)
+
+        # for the latest builds section
+        queryset = Build.objects.all()
+
+        finished_criteria = Q(outcome=Build.SUCCEEDED) | Q(outcome=Build.FAILED)
+
+        latest_builds = itertools.chain(
+            queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
+            queryset.filter(finished_criteria).order_by("-completed_on")[:3]
+        )
+
+        context['mru'] = list(latest_builds)
+        context['mrb_type'] = 'all'
+
+        return context
 
     def setup_queryset(self, *args, **kwargs):
         queryset = Build.objects.all()
diff --git a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
index 419d2b5..f7604fd 100644
--- a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
@@ -3,13 +3,13 @@
 {% block title %} All builds - Toaster {% endblock %}
 
 {% block pagecontent %}
-  <div class="page-header top-air">
-    <h1 data-role="page-title"></h1>
-  </div>
 
   <div class="row-fluid">
-    {# TODO need to pass this data to context #}
-    {#% include 'mrb_section.html' %#}
+    {% with mru=mru mrb_type=mrb_type %}
+      {% include 'mrb_section.html' %}
+    {% endwith %}
+
+    <h1  class="page-header top-air" data-role="page-title"></h1>
 
     {% url 'builds' as xhr_table_url %}
     {% include 'toastertable.html' %}
diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index bd8f991..52b3f1a 100644
--- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -14,7 +14,7 @@
       {% endif %}
       </h2>
   {% else %}
-    <div class="page-header">
+    <div class="page-header top-air">
       <h1>
       Latest builds
       </h1>
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 10/23] toaster: toastergui: implement date range filters for builds
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (9 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 09/23] toaster: toastergui: show recent builds on all builds page Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 11/23] toaster: toastergui: implement "today" and "yesterday" filters Elliot Smith
                   ` (12 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Implement the completed_on and started_on filtering for
builds.

Also separate the name of a filter ("filter" in the querystring)
from its value ("filter_value" in the querystring). This enables
filtering to be defined in the querystring more intuitively,
and also makes it easier to add other types of filter (e.g.
by day).

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/querysetfilter.py   |   3 +-
 bitbake/lib/toaster/toastergui/static/js/table.js  | 196 +++++++++++++++++----
 bitbake/lib/toaster/toastergui/tablefilter.py      | 113 ++++++++++--
 bitbake/lib/toaster/toastergui/tables.py           |  38 +++-
 .../toastergui/templates/builds-toastertable.html  |  32 +---
 bitbake/lib/toaster/toastergui/widgets.py          |  32 ++--
 6 files changed, 330 insertions(+), 84 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
index dbae239..efa8507 100644
--- a/bitbake/lib/toaster/toastergui/querysetfilter.py
+++ b/bitbake/lib/toaster/toastergui/querysetfilter.py
@@ -2,10 +2,11 @@ class QuerysetFilter(object):
     """ Filter for a queryset """
 
     def __init__(self, criteria=None):
+        self.criteria = None
         if criteria:
             self.set_criteria(criteria)
 
-    def set_criteria(self, criteria = None):
+    def set_criteria(self, criteria):
         """
         criteria is an instance of django.db.models.Q;
         see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index 63f8a1f..b0a8ffb 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -397,11 +397,140 @@ function tableInit(ctx){
     $.cookie("cols", JSON.stringify(disabled_cols));
   }
 
+  /**
+   * Create the DOM/JS for the client side of a TableFilterActionToggle
+   *
+   * filterName: (string) internal name for the filter action
+   * filterActionData: (object)
+   * filterActionData.count: (number) The number of items this filter will
+   * show when selected
+   */
+  function createActionToggle(filterName, filterActionData) {
+    var actionStr = '<div class="radio">' +
+                    '<input type="radio" name="filter"' +
+                    '       value="' + filterName + '"';
+
+    if (Number(filterActionData.count) == 0) {
+      actionStr += ' disabled="disabled"';
+    }
+
+    actionStr += ' id="' + filterName + '">' +
+                 '<input type="hidden" name="filter_value" value="on"' +
+                 '       data-value-for="' + filterName + '">' +
+                 '<label class="filter-title"' +
+                 '       for="' + filterName + '">' +
+                 filterActionData.title +
+                 ' (' + filterActionData.count + ')' +
+                 '</label>' +
+                 '</div>';
+
+    return $(actionStr);
+  }
+
+  /**
+   * Create the DOM/JS for the client side of a TableFilterActionDateRange
+   *
+   * filterName: (string) internal name for the filter action
+   * filterValue: (string) from,to date range in format yyyy-mm-dd,yyyy-mm-dd;
+   * used to select the current values for the from/to datepickers;
+   * if this is partial (e.g. "yyyy-mm-dd,") only the applicable datepicker
+   * will have a date pre-selected; if empty, neither will
+   * filterActionData: (object) data for generating the action's HTML
+   * filterActionData.title: label for the radio button
+   * filterActionData.max: (string) maximum date for the pickers, in ISO 8601
+   * datetime format
+   * filterActionData.min: (string) minimum date for the pickers, ISO 8601
+   * datetime
+   */
+  function createActionDateRange(filterName, filterValue, filterActionData) {
+    var action = $('<div class="radio">' +
+                   '<input type="radio" name="filter"' +
+                   '       value="' + filterName + '" ' +
+                   '       id="' + filterName + '">' +
+                   '<input type="hidden" name="filter_value" value=""' +
+                   '       data-value-for="' + filterName + '">' +
+                   '<label class="filter-title"' +
+                   '       for="' + filterName + '">' +
+                   filterActionData.title +
+                   '</label>' +
+                   '<input type="text" maxlength="10" class="input-small"' +
+                   '       data-date-from-for="' + filterName + '">' +
+                   '<span class="help-inline">to</span>' +
+                   '<input type="text" maxlength="10" class="input-small"' +
+                   '       data-date-to-for="' + filterName + '">' +
+                   '<span class="help-inline get-help">(yyyy-mm-dd)</span>' +
+                   '</div>');
+
+    var radio = action.find('[type="radio"]');
+    var value = action.find('[data-value-for]');
+
+    // make the datepickers for the range
+    var options = {
+      dateFormat: 'yy-mm-dd',
+      maxDate: new Date(filterActionData.max),
+      minDate: new Date(filterActionData.min)
+    };
+
+    // create date pickers, setting currently-selected from and to
+    // dates
+    var selectedFrom = null;
+    var selectedTo = null;
+
+    var selectedFromAndTo = [];
+    if (filterValue) {
+      selectedFromAndTo = filterValue.split(',');
+    }
+
+    if (selectedFromAndTo.length == 2) {
+      selectedFrom = selectedFromAndTo[0];
+      selectedTo = selectedFromAndTo[1];
+    }
+
+    options.defaultDate = selectedFrom;
+    var inputFrom =
+      action.find('[data-date-from-for]').datepicker(options);
+    inputFrom.val(selectedFrom);
+
+    options.defaultDate = selectedTo;
+    var inputTo =
+      action.find('[data-date-to-for]').datepicker(options);
+    inputTo.val(selectedTo);
+
+    // set filter_value based on date pickers when
+    // one of their values changes
+    var changeHandler = function () {
+      value.val(inputFrom.val() + ',' + inputTo.val());
+    };
+
+    inputFrom.change(changeHandler);
+    inputTo.change(changeHandler);
+
+    // check the associated radio button on clicking a date picker
+    var checkRadio = function () {
+      radio.prop('checked', 'checked');
+    };
+
+    inputFrom.focus(checkRadio);
+    inputTo.focus(checkRadio);
+
+    // selecting a date in a picker constrains the date you can
+    // set in the other picker
+    inputFrom.change(function () {
+      inputTo.datepicker('option', 'minDate', inputFrom.val());
+    });
+
+    inputTo.change(function () {
+      inputFrom.datepicker('option', 'maxDate', inputTo.val());
+    });
+
+    return action;
+  }
+
   function filterOpenClicked(){
     var filterName = $(this).data('filter-name');
 
-    /* We need to pass in the curren search so that the filter counts take
-     * into account the current search filter
+    /* We need to pass in the current search so that the filter counts take
+     * into account the current search term
      */
     var params = {
       'name' : filterName,
@@ -443,46 +572,44 @@ function tableInit(ctx){
             when the filter popup's "Apply" button is clicked, the
             value for the radio button which is checked is passed in the
             querystring and applied to the queryset on the table
-           */
+          */
+          var filterActionRadios = $('#filter-actions-' + ctx.tableName);
 
-          var filterActionRadios = $('#filter-actions-'+ctx.tableName);
+          $('#filter-modal-title-' + ctx.tableName).text(filterData.title);
 
-          $('#filter-modal-title-'+ctx.tableName).text(filterData.title);
-
-          filterActionRadios.text("");
+          filterActionRadios.empty();
 
+          // create a radio button + form elements for each action associated
+          // with the filter on this column of the table
           for (var i in filterData.filter_actions) {
-            var filterAction = filterData.filter_actions[i];
             var action = null;
+            var filterActionData = filterData.filter_actions[i];
+            var filterName = filterData.name + ':' +
+                             filterActionData.action_name;
 
-            if (filterAction.type === 'toggle') {
-              var actionTitle = filterAction.title + ' (' + filterAction.count + ')';
-
-              action = $('<label class="radio">' +
-                         '<input type="radio" name="filter" value="">' +
-                         '<span class="filter-title">' +
-                         actionTitle +
-                         '</span>' +
-                         '</label>');
-
-              var radioInput = action.children("input");
-              if (Number(filterAction.count) == 0) {
-                radioInput.attr("disabled", "disabled");
-              }
-
-              radioInput.val(filterData.name + ':' + filterAction.action_name);
+            if (filterActionData.type === 'toggle') {
+              action = createActionToggle(filterName, filterActionData);
+            }
+            else if (filterActionData.type === 'daterange') {
+              var filterValue = tableParams.filter_value;
+
+              action = createActionDateRange(
+                filterName,
+                filterValue,
+                filterActionData
+              );
+            }
 
-              /* Setup the current selected filter, default to 'all' if
-               * no current filter selected.
-               */
+            if (action) {
+              // Setup the current selected filter, default to 'all' if
+              // no current filter selected
+              var radioInput = action.children('input[name="filter"]');
               if ((tableParams.filter &&
                   tableParams.filter === radioInput.val()) ||
-                  filterAction.action_name == 'all') {
+                  filterActionData.action_name == 'all') {
                   radioInput.attr("checked", "checked");
               }
-            }
 
-            if (action) {
               filterActionRadios.append(action);
             }
           }
@@ -571,7 +698,14 @@ function tableInit(ctx){
       filterBtnActive($(filterBtn), false);
     });
 
-    tableParams.filter = $(this).find("input[type='radio']:checked").val();
+    // checked radio button
+    var checkedFilter = $(this).find("input[name='filter']:checked");
+    tableParams.filter = checkedFilter.val();
+
+    // hidden field holding the value for the checked filter
+    var checkedFilterValue = $(this).find("input[data-value-for='" +
+                                          tableParams.filter + "']");
+    tableParams.filter_value = checkedFilterValue.val();
 
     var filterBtn = $("#" + tableParams.filter.split(":")[0]);
 
diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py b/bitbake/lib/toaster/toastergui/tablefilter.py
index b42fd52..1ea30da 100644
--- a/bitbake/lib/toaster/toastergui/tablefilter.py
+++ b/bitbake/lib/toaster/toastergui/tablefilter.py
@@ -18,12 +18,15 @@
 # You should have received a copy of the GNU General Public License along
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+from django.db.models import Q, Max, Min
+from django.utils import dateparse, timezone
 
 class TableFilter(object):
     """
     Stores a filter for a named field, and can retrieve the action
-    requested for that filter
+    requested from the set of actions for that filter
     """
+
     def __init__(self, name, title):
         self.name = name
         self.title = title
@@ -64,42 +67,128 @@ class TableFilter(object):
             'filter_actions': filter_actions
         }
 
-class TableFilterActionToggle(object):
+class TableFilterAction(object):
     """
-    Stores a single filter action which will populate one radio button of
-    a ToasterTable filter popup; this filter can either be on or off and
-    has no other parameters
+    A filter action which displays in the filter popup for a ToasterTable
+    and uses an associated QuerysetFilter to filter the queryset for that
+    ToasterTable
     """
 
     def __init__(self, name, title, queryset_filter):
         self.name = name
         self.title = title
-        self.__queryset_filter = queryset_filter
-        self.type = 'toggle'
+        self.queryset_filter = queryset_filter
+
+        # set in subclasses
+        self.type = None
 
-    def set_params(self, params):
+    def set_filter_params(self, params):
         """
         params: (str) a string of extra parameters for the action;
         the structure of this string depends on the type of action;
         it's ignored for a toggle filter action, which is just on or off
         """
-        pass
+        if not params:
+            return
 
     def filter(self, queryset):
-        return self.__queryset_filter.filter(queryset)
+        return self.queryset_filter.filter(queryset)
 
     def to_json(self, queryset):
         """ Dump as a JSON object """
         return {
             'title': self.title,
             'type': self.type,
-            'count': self.__queryset_filter.count(queryset)
+            'count': self.queryset_filter.count(queryset)
         }
 
+class TableFilterActionToggle(TableFilterAction):
+    """
+    A single filter action which will populate one radio button of
+    a ToasterTable filter popup; this filter can either be on or off and
+    has no other parameters
+    """
+
+    def __init__(self, *args):
+        super(TableFilterActionToggle, self).__init__(*args)
+        self.type = 'toggle'
+
+class TableFilterActionDateRange(TableFilterAction):
+    """
+    A filter action which will filter the queryset by a date range.
+    The date range can be set via set_params()
+    """
+
+    def __init__(self, name, title, field, queryset_filter):
+        """
+        field: the field to find the max/min range from in the queryset
+        """
+        super(TableFilterActionDateRange, self).__init__(
+            name,
+            title,
+            queryset_filter
+        )
+
+        self.type = 'daterange'
+        self.field = field
+
+    def set_filter_params(self, params):
+        """
+        params: (str) a string of extra parameters for the filtering
+        in the format "2015-12-09,2015-12-11" (from,to); this is passed in the
+        querystring and used to set the criteria on the QuerysetFilter
+        associated with this action
+        """
+
+        # if params are invalid, return immediately, resetting criteria
+        # on the QuerysetFilter
+        try:
+            from_date_str, to_date_str = params.split(',')
+        except ValueError:
+            self.queryset_filter.set_criteria(None)
+            return
+
+        # one of the values required for the filter is missing, so set
+        # it to the one which was supplied
+        if from_date_str == '':
+            from_date_str = to_date_str
+        elif to_date_str == '':
+            to_date_str = from_date_str
+
+        date_from_naive = dateparse.parse_datetime(from_date_str + ' 00:00:00')
+        date_to_naive = dateparse.parse_datetime(to_date_str + ' 23:59:59')
+
+        tz = timezone.get_default_timezone()
+        date_from = timezone.make_aware(date_from_naive, tz)
+        date_to = timezone.make_aware(date_to_naive, tz)
+
+        args = {}
+        args[self.field + '__gte'] = date_from
+        args[self.field + '__lte'] = date_to
+
+        criteria = Q(**args)
+        self.queryset_filter.set_criteria(criteria)
+
+    def to_json(self, queryset):
+        """ Dump as a JSON object """
+        data = super(TableFilterActionDateRange, self).to_json(queryset)
+
+        # additional data about the date range covered by the queryset's
+        # records, retrieved from its <field> column
+        data['min'] = queryset.aggregate(Min(self.field))[self.field + '__min']
+        data['max'] = queryset.aggregate(Max(self.field))[self.field + '__max']
+
+        # a range filter has a count of None, as the number of records it
+        # will select depends on the date range entered
+        data['count'] = None
+
+        return data
+
 class TableFilterMap(object):
     """
-    Map from field names to Filter objects for those fields
+    Map from field names to TableFilter objects for those fields
     """
+
     def __init__(self):
         self.__filters = {}
 
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 0941637..06ced52 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -29,7 +29,9 @@ from django.core.urlresolvers import reverse
 from django.views.generic import TemplateView
 import itertools
 
-from toastergui.tablefilter import TableFilter, TableFilterActionToggle
+from toastergui.tablefilter import TableFilter
+from toastergui.tablefilter import TableFilterActionToggle
+from toastergui.tablefilter import TableFilterActionDateRange
 
 class ProjectFilters(object):
     def __init__(self, project_layers):
@@ -1070,6 +1072,7 @@ class BuildsTable(ToasterTable):
                         help_text='The date and time when the build started',
                         hideable=True,
                         orderable=True,
+                        filter_name='started_on_filter',
                         static_data_name='started_on',
                         static_data_template=started_on_template)
 
@@ -1077,6 +1080,7 @@ class BuildsTable(ToasterTable):
                         help_text='The date and time when the build finished',
                         hideable=False,
                         orderable=True,
+                        filter_name='completed_on_filter',
                         static_data_name='completed_on',
                         static_data_template=completed_on_template)
 
@@ -1149,6 +1153,38 @@ class BuildsTable(ToasterTable):
         outcome_filter.add_action(failed_builds_filter_action)
         self.add_filter(outcome_filter)
 
+        # started on
+        started_on_filter = TableFilter(
+            'started_on_filter',
+            'Filter by date when build was started'
+        )
+
+        by_started_date_range_filter_action = TableFilterActionDateRange(
+            'date_range',
+            'Build date range',
+            'started_on',
+            QuerysetFilter()
+        )
+
+        started_on_filter.add_action(by_started_date_range_filter_action)
+        self.add_filter(started_on_filter)
+
+        # completed on
+        completed_on_filter = TableFilter(
+            'completed_on_filter',
+            'Filter by date when build was completed'
+        )
+
+        by_completed_date_range_filter_action = TableFilterActionDateRange(
+            'date_range',
+            'Build date range',
+            'completed_on',
+            QuerysetFilter()
+        )
+
+        completed_on_filter.add_action(by_completed_date_range_filter_action)
+        self.add_filter(completed_on_filter)
+
         # failed tasks
         failed_tasks_filter = TableFilter(
             'failed_tasks_filter',
diff --git a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
index f7604fd..2e32edb 100644
--- a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
@@ -1,4 +1,13 @@
 {% extends 'base.html' %}
+{% load static %}
+
+{% block extraheadcontent %}
+  <link rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" type='text/css'>
+  <link rel="stylesheet" href="{% static 'css/jquery-ui.structure.min.css' %}" type='text/css'>
+  <link rel="stylesheet" href="{% static 'css/jquery-ui.theme.min.css' %}" type='text/css'>
+  <script src="{% static 'js/jquery-ui.min.js' %}">
+  </script>
+{% endblock %}
 
 {% block title %} All builds - Toaster {% endblock %}
 
@@ -34,29 +43,6 @@
 
         titleElt.text(title);
       });
-
-      /* {% if last_date_from and last_date_to %}
-      // TODO initialize the date range controls;
-      // this will need to be added via ToasterTable
-      date_init(
-        "started_on",
-        "{{last_date_from}}",
-        "{{last_date_to}}",
-        "{{dateMin_started_on}}",
-        "{{dateMax_started_on}}",
-        "{{daterange_selected}}"
-      );
-
-      date_init(
-        "completed_on",
-        "{{last_date_from}}",
-        "{{last_date_to}}",
-        "{{dateMin_completed_on}}",
-        "{{dateMax_completed_on}}",
-        "{{daterange_selected}}"
-      );
-      {% endif %}
-      */
     });
   </script>
 {% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index 8790340..47de30d 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -183,13 +183,13 @@ class ToasterTable(TemplateView):
 
         return template.render(context)
 
-    def apply_filter(self, filters, **kwargs):
+    def apply_filter(self, filters, filter_value, **kwargs):
         """
         Apply a filter submitted in the querystring to the ToasterTable
 
         filters: (str) in the format:
-          '<filter name>:<action name>!<action params>'
-        where <action params> is optional
+          '<filter name>:<action name>'
+        filter_value: (str) parameters to pass to the named filter
 
         <filter name> and <action name> are used to look up the correct filter
         in the ToasterTable's filter map; the <action params> are set on
@@ -199,15 +199,8 @@ class ToasterTable(TemplateView):
         self.setup_filters(**kwargs)
 
         try:
-            filter_name, action_name_and_params = filters.split(':')
-
-            action_name = None
-            action_params = None
-            if re.search('!', action_name_and_params):
-                action_name, action_params = action_name_and_params.split('!')
-                action_params = urllib.unquote_plus(action_params)
-            else:
-                action_name = action_name_and_params
+            filter_name, action_name = filters.split(':')
+            action_params = urllib.unquote_plus(filter_value)
         except ValueError:
             return
 
@@ -217,7 +210,7 @@ class ToasterTable(TemplateView):
         try:
             table_filter = self.filter_map.get_filter(filter_name)
             action = table_filter.get_action(action_name)
-            action.set_params(action_params)
+            action.set_filter_params(action_params)
             self.queryset = action.filter(self.queryset)
         except KeyError:
             # pass it to the user - programming error here
@@ -247,13 +240,20 @@ class ToasterTable(TemplateView):
 
 
     def get_data(self, request, **kwargs):
-        """Returns the data for the page requested with the specified
-        parameters applied"""
+        """
+        Returns the data for the page requested with the specified
+        parameters applied
+
+        filters: filter and action name, e.g. "outcome:build_succeeded"
+        filter_value: value to pass to the named filter+action, e.g. "on"
+        (for a toggle filter) or "2015-12-11,2015-12-12" (for a date range filter)
+        """
 
         page_num = request.GET.get("page", 1)
         limit = request.GET.get("limit", 10)
         search = request.GET.get("search", None)
         filters = request.GET.get("filter", None)
+        filter_value = request.GET.get("filter_value", "on")
         orderby = request.GET.get("orderby", None)
         nocache = request.GET.get("nocache", None)
 
@@ -285,7 +285,7 @@ class ToasterTable(TemplateView):
         if search:
             self.apply_search(search)
         if filters:
-            self.apply_filter(filters, **kwargs)
+            self.apply_filter(filters, filter_value, **kwargs)
         if orderby:
             self.apply_orderby(orderby)
 
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 11/23] toaster: toastergui: implement "today" and "yesterday" filters
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (10 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 10/23] toaster: toastergui: implement date range filters for builds Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 12/23] toaster: toastergui: convert project builds page to ToasterTable Elliot Smith
                   ` (11 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Add the "today" and "yesterday" filters to the started_on
and completed_on columns in the builds table.

During this work, some minor adjustments were made to the
behaviour of the builds table:

* Amend filter action variable names so they're more succinct.
* Retain order in which actions are added to a filter, as this
ordering is used in the UI when displaying the filter actions.
* Always show the table chrome, otherwise it's not possible
to edit the columns shown until there are 10 or more results.
* Because date range searches may return no results, make sure
that the search bar and "show all results" link are visible
when the query returns no results.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/querysetfilter.py   |   4 -
 bitbake/lib/toaster/toastergui/static/js/table.js  |  56 +++++----
 bitbake/lib/toaster/toastergui/tablefilter.py      | 140 +++++++++++++++++----
 bitbake/lib/toaster/toastergui/tables.py           |  87 ++++++++-----
 .../toastergui/templates/builds-toastertable.html  |   2 +-
 .../toaster/toastergui/templates/toastertable.html |   7 +-
 6 files changed, 212 insertions(+), 84 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
index efa8507..10cc988 100644
--- a/bitbake/lib/toaster/toastergui/querysetfilter.py
+++ b/bitbake/lib/toaster/toastergui/querysetfilter.py
@@ -22,7 +22,3 @@ class QuerysetFilter(object):
             return queryset.filter(self.criteria)
         else:
             return queryset
-
-    def count(self, queryset):
-        """ Returns a count of the elements in the filtered queryset """
-        return self.filter(queryset).count()
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index b0a8ffb..afe16b5 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -71,22 +71,11 @@ function tableInit(ctx){
 
     if (tableData.total === 0){
       tableContainer.hide();
-      /* If we were searching show the new search bar and return */
-      if (tableParams.search){
-        $("#new-search-input-"+ctx.tableName).val(tableParams.search);
-        $("#no-results-"+ctx.tableName).show();
-      }
+      $("#new-search-input-"+ctx.tableName).val(tableParams.search);
+      $("#no-results-"+ctx.tableName).show();
       table.trigger("table-done", [tableData.total, tableParams]);
 
       return;
-
-    /* We don't want to clutter the place with the table chrome if there
-     * are only a few results */
-    } else if (tableData.total <= 10 &&
-               !tableParams.filter &&
-               !tableParams.search){
-      $("#table-chrome-"+ctx.tableName).hide();
-      pagination.hide();
     } else {
       tableContainer.show();
       $("#no-results-"+ctx.tableName).hide();
@@ -399,13 +388,14 @@ function tableInit(ctx){
 
   /**
    * Create the DOM/JS for the client side of a TableFilterActionToggle
+   * or TableFilterActionDay
    *
    * filterName: (string) internal name for the filter action
    * filterActionData: (object)
    * filterActionData.count: (number) The number of items this filter will
    * show when selected
    */
-  function createActionToggle(filterName, filterActionData) {
+  function createActionRadio(filterName, filterActionData) {
     var actionStr = '<div class="radio">' +
                     '<input type="radio" name="filter"' +
                     '       value="' + filterName + '"';
@@ -471,8 +461,7 @@ function tableInit(ctx){
       minDate: new Date(filterActionData.min)
     };
 
-    // create date pickers, setting currently-selected from and to
-    // dates
+    // create date pickers, setting currently-selected from and to dates
     var selectedFrom = null;
     var selectedTo = null;
 
@@ -496,6 +485,20 @@ function tableInit(ctx){
       action.find('[data-date-to-for]').datepicker(options);
     inputTo.val(selectedTo);
 
+    // if the radio button is checked and one or both of the datepickers are
+    // empty, populate them with today's date
+    radio.change(function () {
+      var now = new Date();
+
+      if (inputFrom.val() === '') {
+        inputFrom.datepicker('setDate', now);
+      }
+
+      if (inputTo.val() === '') {
+        inputTo.datepicker('setDate', now);
+      }
+    });
+
     // set filter_value based on date pickers when
     // one of their values changes
     var changeHandler = function () {
@@ -553,7 +556,8 @@ function tableInit(ctx){
                 {
                   title: '<label for radio button inside the popup>',
                   name: '<name of the filter action>',
-                  count: <number of items this filter will show>
+                  count: <number of items this filter will show>,
+                  ... additional data for the action ...
                 }
               ]
             }
@@ -567,11 +571,12 @@ function tableInit(ctx){
             filter
 
             the filterName is set on the column filter icon, and corresponds
-            to a value in the table's filters property
+            to a value in the table's filter map
 
             when the filter popup's "Apply" button is clicked, the
             value for the radio button which is checked is passed in the
-            querystring and applied to the queryset on the table
+            querystring, along with a filter_value, and applied to the
+            queryset on the table
           */
           var filterActionRadios = $('#filter-actions-' + ctx.tableName);
 
@@ -587,10 +592,12 @@ function tableInit(ctx){
             var filterName = filterData.name + ':' +
                              filterActionData.action_name;
 
-            if (filterActionData.type === 'toggle') {
-              action = createActionToggle(filterName, filterActionData);
+            if (filterActionData.type === 'toggle' ||
+                filterActionData.type === 'day') {
+              action = createActionRadio(filterName, filterActionData);
             }
             else if (filterActionData.type === 'daterange') {
+              // current values for the from/to dates
               var filterValue = tableParams.filter_value;
 
               action = createActionDateRange(
@@ -601,7 +608,7 @@ function tableInit(ctx){
             }
 
             if (action) {
-              // Setup the current selected filter, default to 'all' if
+              // Setup the current selected filter; default to 'all' if
               // no current filter selected
               var radioInput = action.children('input[name="filter"]');
               if ((tableParams.filter &&
@@ -707,13 +714,12 @@ function tableInit(ctx){
                                           tableParams.filter + "']");
     tableParams.filter_value = checkedFilterValue.val();
 
-    var filterBtn = $("#" + tableParams.filter.split(":")[0]);
-
     /* All === remove filter */
     if (tableParams.filter.match(":all$")) {
       tableParams.filter = null;
-      filterBtnActive(filterBtn, false);
+      tableParams.filter_value = null;
     } else {
+      var filterBtn = $("#" + tableParams.filter.split(":")[0]);
       filterBtnActive(filterBtn, true);
     }
 
diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py b/bitbake/lib/toaster/toastergui/tablefilter.py
index 1ea30da..bd8decd 100644
--- a/bitbake/lib/toaster/toastergui/tablefilter.py
+++ b/bitbake/lib/toaster/toastergui/tablefilter.py
@@ -18,13 +18,18 @@
 # You should have received a copy of the GNU General Public License along
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
 from django.db.models import Q, Max, Min
 from django.utils import dateparse, timezone
+from datetime import timedelta
+from querysetfilter import QuerysetFilter
 
 class TableFilter(object):
     """
     Stores a filter for a named field, and can retrieve the action
-    requested from the set of actions for that filter
+    requested from the set of actions for that filter;
+    the order in which actions are added governs the order in which they
+    are returned in the JSON for the filter
     """
 
     def __init__(self, name, title):
@@ -32,7 +37,11 @@ class TableFilter(object):
         self.title = title
         self.__filter_action_map = {}
 
+        # retains the ordering of actions
+        self.__filter_action_keys = []
+
     def add_action(self, action):
+        self.__filter_action_keys.append(action.name)
         self.__filter_action_map[action.name] = action
 
     def get_action(self, action_name):
@@ -56,7 +65,8 @@ class TableFilter(object):
         })
 
         # add other filter actions
-        for action_name, filter_action in self.__filter_action_map.iteritems():
+        for action_name in self.__filter_action_keys:
+            filter_action = self.__filter_action_map[action_name]
             obj = filter_action.to_json(queryset)
             obj['action_name'] = action_name
             filter_actions.append(obj)
@@ -67,6 +77,40 @@ class TableFilter(object):
             'filter_actions': filter_actions
         }
 
+class TableFilterQueryHelper(object):
+    def dateStringsToQ(self, field_name, date_from_str, date_to_str):
+        """
+        Convert the date strings from_date_str and to_date_str into a
+        set of args in the form
+
+          {'<field_name>__gte': <date from>, '<field_name>__lte': <date to>}
+
+        where date_from and date_to are Django-timezone-aware dates; then
+        convert that into a Django Q object
+
+        Returns the Q object based on those criteria
+        """
+
+        # one of the values required for the filter is missing, so set
+        # it to the one which was supplied
+        if date_from_str == '':
+            date_from_str = date_to_str
+        elif date_to_str == '':
+            date_to_str = date_from_str
+
+        date_from_naive = dateparse.parse_datetime(date_from_str + ' 00:00:00')
+        date_to_naive = dateparse.parse_datetime(date_to_str + ' 23:59:59')
+
+        tz = timezone.get_default_timezone()
+        date_from = timezone.make_aware(date_from_naive, tz)
+        date_to = timezone.make_aware(date_to_naive, tz)
+
+        args = {}
+        args[field_name + '__gte'] = date_from
+        args[field_name + '__lte'] = date_to
+
+        return Q(**args)
+
 class TableFilterAction(object):
     """
     A filter action which displays in the filter popup for a ToasterTable
@@ -99,7 +143,7 @@ class TableFilterAction(object):
         return {
             'title': self.title,
             'type': self.type,
-            'count': self.queryset_filter.count(queryset)
+            'count': self.filter(queryset).count()
         }
 
 class TableFilterActionToggle(TableFilterAction):
@@ -113,15 +157,70 @@ class TableFilterActionToggle(TableFilterAction):
         super(TableFilterActionToggle, self).__init__(*args)
         self.type = 'toggle'
 
+class TableFilterActionDay(TableFilterAction):
+    """
+    A filter action which filters according to the named datetime field and a
+    string representing a day ("today" or "yesterday")
+    """
+
+    TODAY = 'today'
+    YESTERDAY = 'yesterday'
+
+    def __init__(self, name, title, field, day,
+    queryset_filter = QuerysetFilter(), query_helper = TableFilterQueryHelper()):
+        """
+        field: (string) the datetime field to filter by
+        day: (string) "today" or "yesterday"
+        """
+        super(TableFilterActionDay, self).__init__(
+            name,
+            title,
+            queryset_filter
+        )
+        self.type = 'day'
+        self.field = field
+        self.day = day
+        self.query_helper = query_helper
+
+    def filter(self, queryset):
+        """
+        Apply the day filtering before returning the queryset;
+        this is done here as the value of the filter criteria changes
+        depending on when the filtering is applied
+        """
+
+        criteria = None
+        date_str = None
+        now = timezone.now()
+
+        if self.day == self.YESTERDAY:
+            increment = timedelta(days=1)
+            wanted_date = now - increment
+        else:
+            wanted_date = now
+
+        wanted_date_str = wanted_date.strftime('%Y-%m-%d')
+
+        criteria = self.query_helper.dateStringsToQ(
+            self.field,
+            wanted_date_str,
+            wanted_date_str
+        )
+
+        self.queryset_filter.set_criteria(criteria)
+
+        return self.queryset_filter.filter(queryset)
+
 class TableFilterActionDateRange(TableFilterAction):
     """
     A filter action which will filter the queryset by a date range.
     The date range can be set via set_params()
     """
 
-    def __init__(self, name, title, field, queryset_filter):
+    def __init__(self, name, title, field,
+    queryset_filter = QuerysetFilter(), query_helper = TableFilterQueryHelper()):
         """
-        field: the field to find the max/min range from in the queryset
+        field: (string) the field to find the max/min range from in the queryset
         """
         super(TableFilterActionDateRange, self).__init__(
             name,
@@ -131,9 +230,13 @@ class TableFilterActionDateRange(TableFilterAction):
 
         self.type = 'daterange'
         self.field = field
+        self.query_helper = query_helper
 
     def set_filter_params(self, params):
         """
+        This filter depends on the user selecting some input, so it needs
+        to have its parameters set before its queryset is filtered
+
         params: (str) a string of extra parameters for the filtering
         in the format "2015-12-09,2015-12-11" (from,to); this is passed in the
         querystring and used to set the criteria on the QuerysetFilter
@@ -143,30 +246,18 @@ class TableFilterActionDateRange(TableFilterAction):
         # if params are invalid, return immediately, resetting criteria
         # on the QuerysetFilter
         try:
-            from_date_str, to_date_str = params.split(',')
+            date_from_str, date_to_str = params.split(',')
         except ValueError:
             self.queryset_filter.set_criteria(None)
             return
 
         # one of the values required for the filter is missing, so set
         # it to the one which was supplied
-        if from_date_str == '':
-            from_date_str = to_date_str
-        elif to_date_str == '':
-            to_date_str = from_date_str
-
-        date_from_naive = dateparse.parse_datetime(from_date_str + ' 00:00:00')
-        date_to_naive = dateparse.parse_datetime(to_date_str + ' 23:59:59')
-
-        tz = timezone.get_default_timezone()
-        date_from = timezone.make_aware(date_from_naive, tz)
-        date_to = timezone.make_aware(date_to_naive, tz)
-
-        args = {}
-        args[self.field + '__gte'] = date_from
-        args[self.field + '__lte'] = date_to
-
-        criteria = Q(**args)
+        criteria = self.query_helper.dateStringsToQ(
+            self.field,
+            date_from_str,
+            date_to_str
+        )
         self.queryset_filter.set_criteria(criteria)
 
     def to_json(self, queryset):
@@ -179,7 +270,8 @@ class TableFilterActionDateRange(TableFilterAction):
         data['max'] = queryset.aggregate(Max(self.field))[self.field + '__max']
 
         # a range filter has a count of None, as the number of records it
-        # will select depends on the date range entered
+        # will select depends on the date range entered and we don't know
+        # that ahead of time
         data['count'] = None
 
         return data
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 06ced52..58abe36 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -32,6 +32,7 @@ import itertools
 from toastergui.tablefilter import TableFilter
 from toastergui.tablefilter import TableFilterActionToggle
 from toastergui.tablefilter import TableFilterActionDateRange
+from toastergui.tablefilter import TableFilterActionDay
 
 class ProjectFilters(object):
     def __init__(self, project_layers):
@@ -65,20 +66,20 @@ class LayersTable(ToasterTable):
 
         criteria = Q(projectlayer__in=self.project_layers)
 
-        in_project_filter_action = TableFilterActionToggle(
+        in_project_action = TableFilterActionToggle(
             "in_project",
             "Layers added to this project",
             QuerysetFilter(criteria)
         )
 
-        not_in_project_filter_action = TableFilterActionToggle(
+        not_in_project_action = TableFilterActionToggle(
             "not_in_project",
             "Layers not added to this project",
             QuerysetFilter(~criteria)
         )
 
-        in_current_project_filter.add_action(in_project_filter_action)
-        in_current_project_filter.add_action(not_in_project_filter_action)
+        in_current_project_filter.add_action(in_project_action)
+        in_current_project_filter.add_action(not_in_project_action)
         self.add_filter(in_current_project_filter)
 
     def setup_queryset(self, *args, **kwargs):
@@ -221,20 +222,20 @@ class MachinesTable(ToasterTable):
             "Filter by project machines"
         )
 
-        in_project_filter_action = TableFilterActionToggle(
+        in_project_action = TableFilterActionToggle(
             "in_project",
             "Machines provided by layers added to this project",
             project_filters.in_project
         )
 
-        not_in_project_filter_action = TableFilterActionToggle(
+        not_in_project_action = TableFilterActionToggle(
             "not_in_project",
             "Machines provided by layers not added to this project",
             project_filters.not_in_project
         )
 
-        in_current_project_filter.add_action(in_project_filter_action)
-        in_current_project_filter.add_action(not_in_project_filter_action)
+        in_current_project_filter.add_action(in_project_action)
+        in_current_project_filter.add_action(not_in_project_action)
         self.add_filter(in_current_project_filter)
 
     def setup_queryset(self, *args, **kwargs):
@@ -354,20 +355,20 @@ class RecipesTable(ToasterTable):
             'Filter by project recipes'
         )
 
-        in_project_filter_action = TableFilterActionToggle(
+        in_project_action = TableFilterActionToggle(
             'in_project',
             'Recipes provided by layers added to this project',
             project_filters.in_project
         )
 
-        not_in_project_filter_action = TableFilterActionToggle(
+        not_in_project_action = TableFilterActionToggle(
             'not_in_project',
             'Recipes provided by layers not added to this project',
             project_filters.not_in_project
         )
 
-        table_filter.add_action(in_project_filter_action)
-        table_filter.add_action(not_in_project_filter_action)
+        table_filter.add_action(in_project_action)
+        table_filter.add_action(not_in_project_action)
         self.add_filter(table_filter)
 
     def setup_queryset(self, *args, **kwargs):
@@ -1137,20 +1138,20 @@ class BuildsTable(ToasterTable):
             'Filter builds by outcome'
         )
 
-        successful_builds_filter_action = TableFilterActionToggle(
+        successful_builds_action = TableFilterActionToggle(
             'successful_builds',
             'Successful builds',
             QuerysetFilter(Q(outcome=Build.SUCCEEDED))
         )
 
-        failed_builds_filter_action = TableFilterActionToggle(
+        failed_builds_action = TableFilterActionToggle(
             'failed_builds',
             'Failed builds',
             QuerysetFilter(Q(outcome=Build.FAILED))
         )
 
-        outcome_filter.add_action(successful_builds_filter_action)
-        outcome_filter.add_action(failed_builds_filter_action)
+        outcome_filter.add_action(successful_builds_action)
+        outcome_filter.add_action(failed_builds_action)
         self.add_filter(outcome_filter)
 
         # started on
@@ -1159,14 +1160,29 @@ class BuildsTable(ToasterTable):
             'Filter by date when build was started'
         )
 
-        by_started_date_range_filter_action = TableFilterActionDateRange(
+        started_today_action = TableFilterActionDay(
+            'today',
+            'Today\'s builds',
+            'started_on',
+            'today'
+        )
+
+        started_yesterday_action = TableFilterActionDay(
+            'yesterday',
+            'Yesterday\'s builds',
+            'started_on',
+            'yesterday'
+        )
+
+        by_started_date_range_action = TableFilterActionDateRange(
             'date_range',
             'Build date range',
-            'started_on',
-            QuerysetFilter()
+            'started_on'
         )
 
-        started_on_filter.add_action(by_started_date_range_filter_action)
+        started_on_filter.add_action(started_today_action)
+        started_on_filter.add_action(started_yesterday_action)
+        started_on_filter.add_action(by_started_date_range_action)
         self.add_filter(started_on_filter)
 
         # completed on
@@ -1175,14 +1191,29 @@ class BuildsTable(ToasterTable):
             'Filter by date when build was completed'
         )
 
-        by_completed_date_range_filter_action = TableFilterActionDateRange(
+        completed_today_action = TableFilterActionDay(
+            'today',
+            'Today\'s builds',
+            'completed_on',
+            'today'
+        )
+
+        completed_yesterday_action = TableFilterActionDay(
+            'yesterday',
+            'Yesterday\'s builds',
+            'completed_on',
+            'yesterday'
+        )
+
+        by_completed_date_range_action = TableFilterActionDateRange(
             'date_range',
             'Build date range',
-            'completed_on',
-            QuerysetFilter()
+            'completed_on'
         )
 
-        completed_on_filter.add_action(by_completed_date_range_filter_action)
+        completed_on_filter.add_action(completed_today_action)
+        completed_on_filter.add_action(completed_yesterday_action)
+        completed_on_filter.add_action(by_completed_date_range_action)
         self.add_filter(completed_on_filter)
 
         # failed tasks
@@ -1193,18 +1224,18 @@ class BuildsTable(ToasterTable):
 
         criteria = Q(task_build__outcome=Task.OUTCOME_FAILED)
 
-        with_failed_tasks_filter_action = TableFilterActionToggle(
+        with_failed_tasks_action = TableFilterActionToggle(
             'with_failed_tasks',
             'Builds with failed tasks',
             QuerysetFilter(criteria)
         )
 
-        without_failed_tasks_filter_action = TableFilterActionToggle(
+        without_failed_tasks_action = TableFilterActionToggle(
             'without_failed_tasks',
             'Builds without failed tasks',
             QuerysetFilter(~criteria)
         )
 
-        failed_tasks_filter.add_action(with_failed_tasks_filter_action)
-        failed_tasks_filter.add_action(without_failed_tasks_filter_action)
+        failed_tasks_filter.add_action(with_failed_tasks_action)
+        failed_tasks_filter.add_action(without_failed_tasks_action)
         self.add_filter(failed_tasks_filter)
diff --git a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
index 2e32edb..bf13a66 100644
--- a/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/builds-toastertable.html
@@ -18,7 +18,7 @@
       {% include 'mrb_section.html' %}
     {% endwith %}
 
-    <h1  class="page-header top-air" data-role="page-title"></h1>
+    <h1 class="page-header top-air" data-role="page-title"></h1>
 
     {% url 'builds' as xhr_table_url %}
     {% include 'toastertable.html' %}
diff --git a/bitbake/lib/toaster/toastergui/templates/toastertable.html b/bitbake/lib/toaster/toastergui/templates/toastertable.html
index 98a715f..f0a3aed 100644
--- a/bitbake/lib/toaster/toastergui/templates/toastertable.html
+++ b/bitbake/lib/toaster/toastergui/templates/toastertable.html
@@ -32,8 +32,11 @@
       <a href="#" class="add-on btn remove-search-btn-{{table_name}}" tabindex="-1">
         <i class="icon-remove"></i>
       </a>
-      <button class="btn search-submit-{{table_name}}" >Search</button>
-      <button class="btn btn-link remove-search-btn-{{table_name}}">Show {{title|lower}}
+      <button class="btn search-submit-{{table_name}}">
+        Search
+      </button>
+      <button class="btn btn-link show-all-{{table_name}}">
+        Show {{title|lower}}
       </button>
     </form>
   </div>
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 12/23] toaster: toastergui: convert project builds page to ToasterTable
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (11 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 11/23] toaster: toastergui: implement "today" and "yesterday" filters Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 13/23] toaster: toastergui: don't hide all elements with .col class Elliot Smith
                   ` (10 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Use the all builds ToasterTable as the basis for the project builds
ToasterTable.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 .../toaster/toastergui/static/js/projecttopbar.js  |   9 +
 bitbake/lib/toaster/toastergui/static/js/table.js  |  13 +-
 bitbake/lib/toaster/toastergui/tables.py           | 184 +++++++++++++++++----
 .../toastergui/templates/baseprojectpage.html      |   1 +
 .../toaster/toastergui/templates/mrb_section.html  |   2 +-
 .../templates/projectbuilds-toastertable.html      |  56 +++++++
 bitbake/lib/toaster/toastergui/urls.py             |   6 +-
 bitbake/lib/toaster/toastergui/views.py            |  16 +-
 bitbake/lib/toaster/toastergui/widgets.py          |   1 -
 9 files changed, 239 insertions(+), 49 deletions(-)
 create mode 100644 bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html

diff --git a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
index b6ad380..58a32a0 100644
--- a/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
+++ b/bitbake/lib/toaster/toastergui/static/js/projecttopbar.js
@@ -7,7 +7,10 @@ function projectTopBarInit(ctx) {
   var projectName = $("#project-name");
   var projectNameFormToggle = $("#project-change-form-toggle");
   var projectNameChangeCancel = $("#project-name-change-cancel");
+
+  // this doesn't exist for command-line builds
   var newBuildTargetInput = $("#build-input");
+
   var newBuildTargetBuildBtn = $("#build-button");
   var selectedTarget;
 
@@ -42,6 +45,12 @@ function projectTopBarInit(ctx) {
       $(this).parent().removeClass('active');
   });
 
+  if (!newBuildTargetInput.length) {
+    return;
+  }
+
+  /* the following only applies for non-command-line projects */
+
   /* Recipe build input functionality */
   if (ctx.numProjectLayers > 0 && ctx.machine){
     newBuildTargetInput.removeAttr("disabled");
diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index afe16b5..7ac4ed5 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -33,6 +33,10 @@ function tableInit(ctx){
 
   loadData(tableParams);
 
+  // clicking on this set of elements removes the search
+  var clearSearchElements = $('.remove-search-btn-'+ctx.tableName +
+                              ', .show-all-'+ctx.tableName);
+
   function loadData(tableParams){
     $.ajax({
         type: "GET",
@@ -62,9 +66,9 @@ function tableInit(ctx){
     paginationBtns.html("");
 
     if (tableParams.search)
-      $('.remove-search-btn-'+ctx.tableName).show();
+      clearSearchElements.show();
     else
-      $('.remove-search-btn-'+ctx.tableName).hide();
+      clearSearchElements.hide();
 
     $('.table-count-' + ctx.tableName).text(tableData.total);
     tableTotal = tableData.total;
@@ -230,9 +234,8 @@ function tableInit(ctx){
 
       } else {
         /* Not orderable */
-        header.addClass("muted");
         header.css("font-weight", "normal");
-        header.append(col.title+' ');
+        header.append('<span class="muted">' + col.title + '</span> ');
       }
 
       /* Setup the filter button */
@@ -665,7 +668,7 @@ function tableInit(ctx){
     loadData(tableParams);
   });
 
-  $('.remove-search-btn-'+ctx.tableName).click(function(e){
+  clearSearchElements.click(function(e){
     e.preventDefault();
 
     tableParams.page = 1;
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 58abe36..d0ed496 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -23,9 +23,11 @@ from toastergui.widgets import ToasterTable
 from toastergui.querysetfilter import QuerysetFilter
 from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
 from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task
+from orm.models import ProjectTarget
 from django.db.models import Q, Max, Count
 from django.conf.urls import url
-from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse, resolve
+from django.http import HttpResponse
 from django.views.generic import TemplateView
 import itertools
 
@@ -775,7 +777,7 @@ class ProjectsTable(ToasterTable):
         '''
 
         errors_template = '''
-        {% if data.get_number_of_builds > 0 %}
+        {% if data.get_number_of_builds > 0 and data.get_last_errors > 0 %}
           <a class="errors.count error"
              href="{% url "builddashboard" data.get_last_build_id %}#errors">
             {{data.get_last_errors}} error{{data.get_last_errors | pluralize}}
@@ -784,7 +786,7 @@ class ProjectsTable(ToasterTable):
         '''
 
         warnings_template = '''
-        {% if data.get_number_of_builds > 0 %}
+        {% if data.get_number_of_builds > 0 and data.get_last_warnings > 0 %}
           <a class="warnings.count warning"
              href="{% url "builddashboard" data.get_last_build_id %}#warnings">
             {{data.get_last_warnings}} warning{{data.get_last_warnings | pluralize}}
@@ -886,30 +888,45 @@ class BuildsTable(ToasterTable):
     def __init__(self, *args, **kwargs):
         super(BuildsTable, self).__init__(*args, **kwargs)
         self.default_orderby = '-completed_on'
-        self.title = 'All builds'
         self.static_context_extra['Build'] = Build
         self.static_context_extra['Task'] = Task
 
+        # attributes that are overridden in subclasses
+
+        # title for the page
+        self.title = ''
+
+        # 'project' or 'all'; determines how the mrb (most recent builds)
+        # section is displayed
+        self.mrb_type = ''
+
+    def get_builds(self):
+        """
+        overridden in ProjectBuildsTable to return builds for a
+        single project
+        """
+        return Build.objects.all()
+
     def get_context_data(self, **kwargs):
         context = super(BuildsTable, self).get_context_data(**kwargs)
 
         # for the latest builds section
-        queryset = Build.objects.all()
+        builds = self.get_builds()
 
         finished_criteria = Q(outcome=Build.SUCCEEDED) | Q(outcome=Build.FAILED)
 
         latest_builds = itertools.chain(
-            queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
-            queryset.filter(finished_criteria).order_by("-completed_on")[:3]
+            builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
+            builds.filter(finished_criteria).order_by("-completed_on")[:3]
         )
 
         context['mru'] = list(latest_builds)
-        context['mrb_type'] = 'all'
+        context['mrb_type'] = self.mrb_type
 
         return context
 
     def setup_queryset(self, *args, **kwargs):
-        queryset = Build.objects.all()
+        queryset = self.get_builds()
 
         # don't include in progress builds
         queryset = queryset.exclude(outcome=Build.IN_PROGRESS)
@@ -949,7 +966,8 @@ class BuildsTable(ToasterTable):
         {% if data.cooker_log_path %}
             &nbsp;
             <a href="{% url "build_artifact" data.id "cookerlog" data.id %}">
-               <i class="icon-download-alt" title="Download build log"></i>
+               <i class="icon-download-alt get-help"
+               data-original-title="Download build log"></i>
             </a>
         {% endif %}
         '''
@@ -1031,19 +1049,6 @@ class BuildsTable(ToasterTable):
         {% endif %}
         '''
 
-        project_template = '''
-        {% load project_url_tag %}
-        <a href="{% project_url data.project %}">
-            {{data.project.name}}
-        </a>
-        {% if data.project.is_default %}
-            <i class="icon-question-sign get-help hover-help" title=""
-               data-original-title="This project shows information about
-               the builds you start from the command line while Toaster is
-               running" style="visibility: hidden;"></i>
-        {% endif %}
-        '''
-
         self.add_column(title='Outcome',
                         help_text='Final state of the build (successful \
                                    or failed)',
@@ -1098,16 +1103,16 @@ class BuildsTable(ToasterTable):
                         help_text='The number of errors encountered during \
                                    the build (if any)',
                         hideable=True,
-                        orderable=False,
-                        static_data_name='errors',
+                        orderable=True,
+                        static_data_name='errors_no',
                         static_data_template=errors_template)
 
         self.add_column(title='Warnings',
                         help_text='The number of warnings encountered during \
                                    the build (if any)',
                         hideable=True,
-                        orderable=False,
-                        static_data_name='warnings',
+                        orderable=True,
+                        static_data_name='warnings_no',
                         static_data_template=warnings_template)
 
         self.add_column(title='Time',
@@ -1125,12 +1130,6 @@ class BuildsTable(ToasterTable):
                         static_data_name='image_files',
                         static_data_template=image_files_template)
 
-        self.add_column(title='Project',
-                        hideable=True,
-                        orderable=False,
-                        static_data_name='project-name',
-                        static_data_template=project_template)
-
     def setup_filters(self, *args, **kwargs):
         # outcomes
         outcome_filter = TableFilter(
@@ -1239,3 +1238,122 @@ class BuildsTable(ToasterTable):
         failed_tasks_filter.add_action(with_failed_tasks_action)
         failed_tasks_filter.add_action(without_failed_tasks_action)
         self.add_filter(failed_tasks_filter)
+
+    def post(self, request, *args, **kwargs):
+        """ Process HTTP POSTs which make build requests """
+
+        project = Project.objects.get(pk=kwargs['pid'])
+
+        if 'buildCancel' in request.POST:
+            for i in request.POST['buildCancel'].strip().split(" "):
+                try:
+                    br = BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_QUEUED)
+                    br.state = BuildRequest.REQ_DELETED
+                    br.save()
+                except BuildRequest.DoesNotExist:
+                    pass
+
+        if 'buildDelete' in request.POST:
+            for i in request.POST['buildDelete'].strip().split(" "):
+                try:
+                    BuildRequest.objects.select_for_update().get(project = project, pk = i, state__lte = BuildRequest.REQ_DELETED).delete()
+                except BuildRequest.DoesNotExist:
+                    pass
+
+        if 'targets' in request.POST:
+            ProjectTarget.objects.filter(project = project).delete()
+            s = str(request.POST['targets'])
+            for t in s.translate(None, ";%|\"").split(" "):
+                if ":" in t:
+                    target, task = t.split(":")
+                else:
+                    target = t
+                    task = ""
+                ProjectTarget.objects.create(project = project,
+                                             target = target,
+                                             task = task)
+            project.schedule_build()
+
+        # redirect back to builds page so any new builds in progress etc.
+        # are visible
+        response = HttpResponse()
+        response.status_code = 302
+        response['Location'] = request.build_absolute_uri()
+        return response
+
+class AllBuildsTable(BuildsTable):
+    """ Builds page for all builds """
+
+    def __init__(self, *args, **kwargs):
+        super(AllBuildsTable, self).__init__(*args, **kwargs)
+        self.title = 'All builds'
+        self.mrb_type = 'all'
+
+    def setup_columns(self, *args, **kwargs):
+        """
+        All builds page shows a column for the project
+        """
+
+        super(AllBuildsTable, self).setup_columns(*args, **kwargs)
+
+        project_template = '''
+        {% load project_url_tag %}
+        <a href="{% project_url data.project %}">
+            {{data.project.name}}
+        </a>
+        {% if data.project.is_default %}
+            <i class="icon-question-sign get-help hover-help" title=""
+               data-original-title="This project shows information about
+               the builds you start from the command line while Toaster is
+               running" style="visibility: hidden;"></i>
+        {% endif %}
+        '''
+
+        self.add_column(title='Project',
+                        hideable=True,
+                        orderable=True,
+                        static_data_name='project',
+                        static_data_template=project_template)
+
+class ProjectBuildsTable(BuildsTable):
+    """
+    Builds page for a single project; a BuildsTable, with the queryset
+    filtered by project
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(ProjectBuildsTable, self).__init__(*args, **kwargs)
+        self.title = 'All project builds'
+        self.mrb_type = 'project'
+
+        # set from the querystring
+        self.project_id = None
+
+    def setup_queryset(self, *args, **kwargs):
+        """
+        NOTE: self.project_id must be set before calling super(),
+        as it's used in setup_queryset()
+        """
+        self.project_id = kwargs['pid']
+        super(ProjectBuildsTable, self).setup_queryset(*args, **kwargs)
+
+        project = Project.objects.get(pk=self.project_id)
+        self.queryset = self.queryset.filter(project=project)
+
+    def get_context_data(self, **kwargs):
+        """
+        NOTE: self.project_id must be set before calling super(),
+        as it's used in get_context_data()
+        """
+        self.project_id = kwargs['pid']
+
+        context = super(ProjectBuildsTable, self).get_context_data(**kwargs)
+        context['project'] = Project.objects.get(pk=self.project_id)
+
+        return context
+
+    def get_builds(self):
+        """ override: only return builds for the relevant project """
+
+        project = Project.objects.get(pk=self.project_id)
+        return Build.objects.filter(project=project)
diff --git a/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
index 1f45be4..b143b78 100644
--- a/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
+++ b/bitbake/lib/toaster/toastergui/templates/baseprojectpage.html
@@ -1,4 +1,5 @@
 {% extends "base.html" %}
+
 {% load projecttags %}
 {% load humanize %}
 
diff --git a/bitbake/lib/toaster/toastergui/templates/mrb_section.html b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
index 52b3f1a..2f4820c 100644
--- a/bitbake/lib/toaster/toastergui/templates/mrb_section.html
+++ b/bitbake/lib/toaster/toastergui/templates/mrb_section.html
@@ -6,7 +6,7 @@
 {%if mru and mru.count > 0%}
 
   {%if mrb_type == 'project' %}
-      <h2>
+      <h2 class="page-header">
       Latest project builds
 
       {% if project.is_default %}
diff --git a/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
new file mode 100644
index 0000000..6d7e10b
--- /dev/null
+++ b/bitbake/lib/toaster/toastergui/templates/projectbuilds-toastertable.html
@@ -0,0 +1,56 @@
+{% extends 'base.html' %}
+
+{% load static %}
+
+{% block extraheadcontent %}
+  <link rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" type='text/css'>
+  <link rel="stylesheet" href="{% static 'css/jquery-ui.structure.min.css' %}" type='text/css'>
+  <link rel="stylesheet" href="{% static 'css/jquery-ui.theme.min.css' %}" type='text/css'>
+  <script src="{% static 'js/jquery-ui.min.js' %}">
+  </script>
+{% endblock %}
+
+{% block title %} {{title}} - {{project.name}} - Toaster {% endblock %}
+
+{% block pagecontent %}
+
+  {% include "projecttopbar.html" %}
+
+  <div class="row-fluid">
+    {% with mru=mru mrb_type=mrb_type %}
+      {% include 'mrb_section.html' %}
+    {% endwith %}
+
+    <h2 class="page-header top-air" data-role="page-title"></h2>
+
+    {% url 'projectbuilds' project.id as xhr_table_url %}
+    {% include 'toastertable.html' %}
+  </div>
+
+  <script>
+    $(document).ready(function () {
+      // title
+      var tableElt = $("#{{table_name}}");
+      var titleElt = $("[data-role='page-title']");
+
+      tableElt.on("table-done", function (e, total, tableParams) {
+        var title = "All project builds";
+
+        if (tableParams.search || tableParams.filter) {
+          if (total === 0) {
+            title = "No project builds found";
+          }
+          else if (total > 0) {
+            title = total + " project build" + (total > 1 ? 's' : '') + " found";
+          }
+        }
+
+        titleElt.text(title);
+      });
+
+      // highlight builds tab
+      $("#topbar-builds-tab").addClass("active")
+    });
+  </script>
+
+{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index 707b7d5..c8c1c6a 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -28,7 +28,7 @@ urlpatterns = patterns('toastergui.views',
         url(r'^landing/$', 'landing', name='landing'),
 
         url(r'^builds/$',
-            tables.BuildsTable.as_view(template_name="builds-toastertable.html"),
+            tables.AllBuildsTable.as_view(template_name="builds-toastertable.html"),
             name='all-builds'),
 
         # build info navigation
@@ -83,7 +83,9 @@ urlpatterns = patterns('toastergui.views',
 
         url(r'^project/(?P<pid>\d+)/$', 'project', name='project'),
         url(r'^project/(?P<pid>\d+)/configuration$', 'projectconf', name='projectconf'),
-        url(r'^project/(?P<pid>\d+)/builds/$', 'projectbuilds', name='projectbuilds'),
+        url(r'^project/(?P<pid>\d+)/builds/$',
+            tables.ProjectBuildsTable.as_view(template_name="projectbuilds-toastertable.html"),
+            name='projectbuilds'),
 
         # the import layer is a project-specific functionality;
         url(r'^project/(?P<pid>\d+)/importlayer$', 'importlayer', name='importlayer'),
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 295773f..fbae36c 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -91,6 +91,7 @@ def landing(request):
 
     return render(request, 'landing.html', context)
 
+"""
 # returns a list for most recent builds;
 def _get_latest_builds(prj=None):
     queryset = Build.objects.all()
@@ -101,8 +102,9 @@ def _get_latest_builds(prj=None):
     return list(itertools.chain(
         queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
         queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] ))
+"""
 
-
+"""
 # a JSON-able dict of recent builds; for use in the Project page, xhr_ updates,  and other places, as needed
 def _project_recent_build_list(prj):
     data = []
@@ -131,8 +133,7 @@ def _project_recent_build_list(prj):
         data.append(d)
 
     return data
-
-
+"""
 
 def objtojson(obj):
     from django.db.models.query import QuerySet
@@ -1915,6 +1916,7 @@ if True:
         ''' The exception raised on invalid POST requests '''
         pass
 
+    """
     # helper function, to be used on "all builds" and "project builds" pages
     def _build_list_helper(request, queryset_all, redirect_page, pid=None):
         default_orderby = 'completed_on:-'
@@ -2119,6 +2121,7 @@ if True:
         # merge daterange values
         context.update(context_date)
         return context, pagesize, orderby
+    """
 
 
 
@@ -2256,7 +2259,7 @@ if True:
             "completedbuilds": Build.objects.exclude(outcome = Build.IN_PROGRESS).filter(project_id = pid),
             "prj" : {"name": prj.name, },
             "buildrequests" : prj.build_set.filter(outcome=Build.IN_PROGRESS),
-            "builds" : _project_recent_build_list(prj),
+            #"builds" : _project_recent_build_list(prj),
             "layers" :  map(lambda x: {
                         "id": x.layercommit.pk,
                         "orderid": x.pk,
@@ -2827,10 +2830,8 @@ if True:
     # will set the GET parameters and redirect back to the
     # all-builds or projectbuilds page as appropriate;
     # TODO don't use exceptions to control program flow
-    @_template_renderer('projectbuilds.html')
+    """
     def projectbuilds(request, pid):
-        prj = Project.objects.get(id = pid)
-
         if request.method == "POST":
             # process any build request
 
@@ -2880,6 +2881,7 @@ if True:
         context['mru'] = _get_latest_builds(prj)
 
         return context
+    """
 
 
     def _file_name_for_artifact(b, artifact_type, artifact_id):
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index 47de30d..bc081b8 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -61,7 +61,6 @@ class ToasterTable(TemplateView):
 
         self.total_count = 0
         self.static_context_extra = {}
-        self.filter_actions = {}
         self.empty_state = "Sorry - no data found"
         self.default_orderby = ""
 
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 13/23] toaster: toastergui: don't hide all elements with .col class
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (12 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 12/23] toaster: toastergui: convert project builds page to ToasterTable Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 14/23] toaster: toastergui: ensure filter_value updates Elliot Smith
                   ` (9 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Disabling the "project" column in a ToasterTable for builds
causes the recent builds area to be hidden. This is because
the column hiding code hides all elements with a class matching
".<column>", regardless of where they occur on the page; and
the recent builds area was using the ".project-name" class,
which means it is included in the set of elements which are hidden.

Scope the element search to the table so that only elements
within the table are hidden or shown.

[YOCTO #8792]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/static/js/table.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index 7ac4ed5..c154844 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -316,7 +316,7 @@ function tableInit(ctx){
       $("#"+ctx.tableName+" th").each(function(){
         for (var i in cols_hidden){
           if ($(this).hasClass(cols_hidden[i])){
-            $("."+cols_hidden[i]).hide();
+            table.find("."+cols_hidden[i]).hide();
             $("#checkbox-"+cols_hidden[i]).removeAttr("checked");
           }
         }
@@ -326,7 +326,7 @@ function tableInit(ctx){
          * user setting.
          */
         for (var i in defaultHiddenCols) {
-          $("."+defaultHiddenCols[i]).hide();
+          table.find("."+defaultHiddenCols[i]).hide();
           $("#checkbox-"+defaultHiddenCols[i]).removeAttr("checked");
         }
     }
@@ -370,9 +370,9 @@ function tableInit(ctx){
     var disabled_cols = [];
 
     if ($(this).prop("checked")) {
-      $("."+col).show();
+      table.find("."+col).show();
     }  else {
-      $("."+col).hide();
+      table.find("."+col).hide();
       /* If we're ordered by the column we're hiding remove the order by */
       if (col === tableParams.orderby ||
           '-' + col === tableParams.orderby){
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 14/23] toaster: toastergui: ensure filter_value updates
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (13 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 13/23] toaster: toastergui: don't hide all elements with .col class Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 15/23] toaster: toastergui: streamline construction of filter objects Elliot Smith
                   ` (8 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Clicking on the radio button for a date range filter action
populates the from and to fields for that action if they are empty.

However, because this doesn't fire "change" events, clicking on
the radio button doesn't update the filter_value hidden field. This
means that the date range action's filter_value parameter isn't
set correctly when the filter popup is submitted.

Manually call the changeHandler() to set the filter_value whenever
the radio for a date range filter is clicked.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/static/js/table.js | 32 +++++++++++++----------
 1 file changed, 18 insertions(+), 14 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index c154844..a253917 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -488,20 +488,6 @@ function tableInit(ctx){
       action.find('[data-date-to-for]').datepicker(options);
     inputTo.val(selectedTo);
 
-    // if the radio button is checked and one or both of the datepickers are
-    // empty, populate them with today's date
-    radio.change(function () {
-      var now = new Date();
-
-      if (inputFrom.val() === '') {
-        inputFrom.datepicker('setDate', now);
-      }
-
-      if (inputTo.val() === '') {
-        inputTo.datepicker('setDate', now);
-      }
-    });
-
     // set filter_value based on date pickers when
     // one of their values changes
     var changeHandler = function () {
@@ -529,6 +515,24 @@ function tableInit(ctx){
       inputFrom.datepicker('option', 'maxDate', inputTo.val());
     });
 
+    // if the radio button is checked and one or both of the datepickers are
+    // empty, populate them with today's date
+    radio.change(function () {
+      var now = new Date();
+
+      if (inputFrom.val() === '') {
+        inputFrom.datepicker('setDate', now);
+      }
+
+      if (inputTo.val() === '') {
+        inputTo.datepicker('setDate', now);
+      }
+
+      // setting the date like this doesn't fire the changeHandler to
+      // update the filter_value, so do that manually instead
+      changeHandler()
+    });
+
     return action;
   }
 
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 15/23] toaster: toastergui: streamline construction of filter objects
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (14 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 14/23] toaster: toastergui: ensure filter_value updates Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 16/23] toaster: toastergui: serialise decimals correctly Elliot Smith
                   ` (7 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

In line with comments from review, remove the QuerysetFilter
class (redundant) and convert ProjectFilters into a class
with static methods.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/querysetfilter.py | 24 ---------------
 bitbake/lib/toaster/toastergui/tablefilter.py    | 38 ++++++++++--------------
 bitbake/lib/toaster/toastergui/tables.py         | 35 +++++++++++-----------
 bitbake/lib/toaster/toastergui/widgets.py        |  1 -
 4 files changed, 32 insertions(+), 66 deletions(-)
 delete mode 100644 bitbake/lib/toaster/toastergui/querysetfilter.py

diff --git a/bitbake/lib/toaster/toastergui/querysetfilter.py b/bitbake/lib/toaster/toastergui/querysetfilter.py
deleted file mode 100644
index 10cc988..0000000
--- a/bitbake/lib/toaster/toastergui/querysetfilter.py
+++ /dev/null
@@ -1,24 +0,0 @@
-class QuerysetFilter(object):
-    """ Filter for a queryset """
-
-    def __init__(self, criteria=None):
-        self.criteria = None
-        if criteria:
-            self.set_criteria(criteria)
-
-    def set_criteria(self, criteria):
-        """
-        criteria is an instance of django.db.models.Q;
-        see https://docs.djangoproject.com/en/1.9/ref/models/querysets/#q-objects
-        """
-        self.criteria = criteria
-
-    def filter(self, queryset):
-        """
-        Filter queryset according to the criteria for this filter,
-        returning the filtered queryset
-        """
-        if self.criteria:
-            return queryset.filter(self.criteria)
-        else:
-            return queryset
diff --git a/bitbake/lib/toaster/toastergui/tablefilter.py b/bitbake/lib/toaster/toastergui/tablefilter.py
index bd8decd..9d15bcf 100644
--- a/bitbake/lib/toaster/toastergui/tablefilter.py
+++ b/bitbake/lib/toaster/toastergui/tablefilter.py
@@ -22,7 +22,6 @@
 from django.db.models import Q, Max, Min
 from django.utils import dateparse, timezone
 from datetime import timedelta
-from querysetfilter import QuerysetFilter
 
 class TableFilter(object):
     """
@@ -118,10 +117,10 @@ class TableFilterAction(object):
     ToasterTable
     """
 
-    def __init__(self, name, title, queryset_filter):
+    def __init__(self, name, title, criteria):
         self.name = name
         self.title = title
-        self.queryset_filter = queryset_filter
+        self.criteria = criteria
 
         # set in subclasses
         self.type = None
@@ -132,11 +131,13 @@ class TableFilterAction(object):
         the structure of this string depends on the type of action;
         it's ignored for a toggle filter action, which is just on or off
         """
-        if not params:
-            return
+        pass
 
     def filter(self, queryset):
-        return self.queryset_filter.filter(queryset)
+        if self.criteria:
+            return queryset.filter(self.criteria)
+        else:
+            return queryset
 
     def to_json(self, queryset):
         """ Dump as a JSON object """
@@ -167,16 +168,12 @@ class TableFilterActionDay(TableFilterAction):
     YESTERDAY = 'yesterday'
 
     def __init__(self, name, title, field, day,
-    queryset_filter = QuerysetFilter(), query_helper = TableFilterQueryHelper()):
+    query_helper = TableFilterQueryHelper()):
         """
         field: (string) the datetime field to filter by
         day: (string) "today" or "yesterday"
         """
-        super(TableFilterActionDay, self).__init__(
-            name,
-            title,
-            queryset_filter
-        )
+        super(TableFilterActionDay, self).__init__(name, title, None)
         self.type = 'day'
         self.field = field
         self.day = day
@@ -189,8 +186,6 @@ class TableFilterActionDay(TableFilterAction):
         depending on when the filtering is applied
         """
 
-        criteria = None
-        date_str = None
         now = timezone.now()
 
         if self.day == self.YESTERDAY:
@@ -201,15 +196,13 @@ class TableFilterActionDay(TableFilterAction):
 
         wanted_date_str = wanted_date.strftime('%Y-%m-%d')
 
-        criteria = self.query_helper.dateStringsToQ(
+        self.criteria = self.query_helper.dateStringsToQ(
             self.field,
             wanted_date_str,
             wanted_date_str
         )
 
-        self.queryset_filter.set_criteria(criteria)
-
-        return self.queryset_filter.filter(queryset)
+        return queryset.filter(self.criteria)
 
 class TableFilterActionDateRange(TableFilterAction):
     """
@@ -218,14 +211,14 @@ class TableFilterActionDateRange(TableFilterAction):
     """
 
     def __init__(self, name, title, field,
-    queryset_filter = QuerysetFilter(), query_helper = TableFilterQueryHelper()):
+    query_helper = TableFilterQueryHelper()):
         """
         field: (string) the field to find the max/min range from in the queryset
         """
         super(TableFilterActionDateRange, self).__init__(
             name,
             title,
-            queryset_filter
+            None
         )
 
         self.type = 'daterange'
@@ -248,17 +241,16 @@ class TableFilterActionDateRange(TableFilterAction):
         try:
             date_from_str, date_to_str = params.split(',')
         except ValueError:
-            self.queryset_filter.set_criteria(None)
+            self.criteria = None
             return
 
         # one of the values required for the filter is missing, so set
         # it to the one which was supplied
-        criteria = self.query_helper.dateStringsToQ(
+        self.criteria = self.query_helper.dateStringsToQ(
             self.field,
             date_from_str,
             date_to_str
         )
-        self.queryset_filter.set_criteria(criteria)
 
     def to_json(self, queryset):
         """ Dump as a JSON object """
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index d0ed496..b7d977e 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -20,7 +20,6 @@
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 from toastergui.widgets import ToasterTable
-from toastergui.querysetfilter import QuerysetFilter
 from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
 from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task
 from orm.models import ProjectTarget
@@ -37,9 +36,13 @@ from toastergui.tablefilter import TableFilterActionDateRange
 from toastergui.tablefilter import TableFilterActionDay
 
 class ProjectFilters(object):
-    def __init__(self, project_layers):
-        self.in_project = QuerysetFilter(Q(layer_version__in=project_layers))
-        self.not_in_project = QuerysetFilter(~Q(layer_version__in=project_layers))
+    @staticmethod
+    def in_project(project_layers):
+        return Q(layer_version__in=project_layers)
+
+    @staticmethod
+    def not_in_project(project_layers):
+        return ~(ProjectFilters.in_project(project_layers))
 
 class LayersTable(ToasterTable):
     """Table of layers in Toaster"""
@@ -71,13 +74,13 @@ class LayersTable(ToasterTable):
         in_project_action = TableFilterActionToggle(
             "in_project",
             "Layers added to this project",
-            QuerysetFilter(criteria)
+            criteria
         )
 
         not_in_project_action = TableFilterActionToggle(
             "not_in_project",
             "Layers not added to this project",
-            QuerysetFilter(~criteria)
+            ~criteria
         )
 
         in_current_project_filter.add_action(in_project_action)
@@ -217,8 +220,6 @@ class MachinesTable(ToasterTable):
     def setup_filters(self, *args, **kwargs):
         project = Project.objects.get(pk=kwargs['pid'])
 
-        project_filters = ProjectFilters(self.project_layers)
-
         in_current_project_filter = TableFilter(
             "in_current_project",
             "Filter by project machines"
@@ -227,13 +228,13 @@ class MachinesTable(ToasterTable):
         in_project_action = TableFilterActionToggle(
             "in_project",
             "Machines provided by layers added to this project",
-            project_filters.in_project
+            ProjectFilters.in_project(self.project_layers)
         )
 
         not_in_project_action = TableFilterActionToggle(
             "not_in_project",
             "Machines provided by layers not added to this project",
-            project_filters.not_in_project
+            ProjectFilters.not_in_project(self.project_layers)
         )
 
         in_current_project_filter.add_action(in_project_action)
@@ -350,8 +351,6 @@ class RecipesTable(ToasterTable):
         return context
 
     def setup_filters(self, *args, **kwargs):
-        project_filters = ProjectFilters(self.project_layers)
-
         table_filter = TableFilter(
             'in_current_project',
             'Filter by project recipes'
@@ -360,13 +359,13 @@ class RecipesTable(ToasterTable):
         in_project_action = TableFilterActionToggle(
             'in_project',
             'Recipes provided by layers added to this project',
-            project_filters.in_project
+            ProjectFilters.in_project(self.project_layers)
         )
 
         not_in_project_action = TableFilterActionToggle(
             'not_in_project',
             'Recipes provided by layers not added to this project',
-            project_filters.not_in_project
+            ProjectFilters.not_in_project(self.project_layers)
         )
 
         table_filter.add_action(in_project_action)
@@ -1140,13 +1139,13 @@ class BuildsTable(ToasterTable):
         successful_builds_action = TableFilterActionToggle(
             'successful_builds',
             'Successful builds',
-            QuerysetFilter(Q(outcome=Build.SUCCEEDED))
+            Q(outcome=Build.SUCCEEDED)
         )
 
         failed_builds_action = TableFilterActionToggle(
             'failed_builds',
             'Failed builds',
-            QuerysetFilter(Q(outcome=Build.FAILED))
+            Q(outcome=Build.FAILED)
         )
 
         outcome_filter.add_action(successful_builds_action)
@@ -1226,13 +1225,13 @@ class BuildsTable(ToasterTable):
         with_failed_tasks_action = TableFilterActionToggle(
             'with_failed_tasks',
             'Builds with failed tasks',
-            QuerysetFilter(criteria)
+            criteria
         )
 
         without_failed_tasks_action = TableFilterActionToggle(
             'without_failed_tasks',
             'Builds without failed tasks',
-            QuerysetFilter(~criteria)
+            ~criteria
         )
 
         failed_tasks_filter.add_action(with_failed_tasks_action)
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index bc081b8..d9328d4 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -32,7 +32,6 @@ from django.template import Context, Template
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.exceptions import FieldError
 from django.conf.urls import url, patterns
-from toastergui.querysetfilter import QuerysetFilter
 
 import types
 import json
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 16/23] toaster: toastergui: serialise decimals correctly
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (15 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 15/23] toaster: toastergui: streamline construction of filter objects Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 17/23] toaster: toastergui: set default visible and hideable columns Elliot Smith
                   ` (6 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The conversion of some ToasterTable Build object querysets to
JSON caused a serialisation error. This is because one of the
fields in the queryset was of type decimal.Decimal, and our
serialiser didn't know what to do with it.

Add a clause to check for decimal fields and serialise them
so that correct JSON can be generated.

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/views.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index fbae36c..3e8a66b 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -43,6 +43,7 @@ from django.utils.html import escape
 from datetime import timedelta, datetime
 from django.utils import formats
 from toastergui.templatetags.projecttags import json as jsonfilter
+from decimal import Decimal
 import json
 from os.path import dirname
 from functools import wraps
@@ -145,6 +146,8 @@ def objtojson(obj):
         return obj.total_seconds()
     elif isinstance(obj, QuerySet) or isinstance(obj, set):
         return list(obj)
+    elif isinstance(obj, Decimal):
+        return str(obj)
     elif type(obj).__name__ == "RelatedManager":
         return [x.pk for x in obj.all()]
     elif hasattr( obj, '__dict__') and isinstance(obj, Model):
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 17/23] toaster: toastergui: set default visible and hideable columns
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (16 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 16/23] toaster: toastergui: serialise decimals correctly Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 18/23] toaster: toastergui: mute label for filter actions with no records Elliot Smith
                   ` (5 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Incorrect columns were shown by default in the "all builds",
"project builds" and "all projects" pages.

Set the "hidden" property on columns in these tables to hide the
correct columns.

Add a set_column_hidden() method to ToasterTable so that the
"hidden" property can be overridden for the machines column
in the project builds page (it shares a superclass with
all builds).

Make the time column on all builds page hideable.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/tables.py  | 23 ++++++++++++++++++++---
 bitbake/lib/toaster/toastergui/widgets.py | 18 ++++++++++++++++++
 2 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index b7d977e..14077e1 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -812,7 +812,7 @@ class ProjectsTable(ToasterTable):
                                    last project build. If the project has no \
                                    builds, this shows the date the project was \
                                    created.',
-                        hideable=True,
+                        hideable=False,
                         orderable=True,
                         static_data_name='updated',
                         static_data_template=last_activity_on_template)
@@ -836,7 +836,7 @@ class ProjectsTable(ToasterTable):
         self.add_column(title='Number of builds',
                         help_text='The number of builds which have been run \
                                    for the project',
-                        hideable=True,
+                        hideable=False,
                         orderable=False,
                         static_data_name='number_of_builds',
                         static_data_template=number_of_builds_template)
@@ -869,6 +869,7 @@ class ProjectsTable(ToasterTable):
                         help_text='The number of warnings encountered during \
                                    the last project build (if any)',
                         hideable=True,
+                        hidden=True,
                         orderable=False,
                         static_data_name='warnings',
                         static_data_template=warnings_template)
@@ -877,6 +878,7 @@ class ProjectsTable(ToasterTable):
                         help_text='The root file system types produced by \
                                    the last project build',
                         hideable=True,
+                        hidden=True,
                         orderable=False,
                         static_data_name='image_files',
                         static_data_template=image_files_template)
@@ -1076,6 +1078,7 @@ class BuildsTable(ToasterTable):
         self.add_column(title='Started on',
                         help_text='The date and time when the build started',
                         hideable=True,
+                        hidden=True,
                         orderable=True,
                         filter_name='started_on_filter',
                         static_data_name='started_on',
@@ -1116,7 +1119,8 @@ class BuildsTable(ToasterTable):
 
         self.add_column(title='Time',
                         help_text='How long the build took to finish',
-                        hideable=False,
+                        hideable=True,
+                        hidden=True,
                         orderable=False,
                         static_data_name='time',
                         static_data_template=time_template)
@@ -1328,6 +1332,19 @@ class ProjectBuildsTable(BuildsTable):
         # set from the querystring
         self.project_id = None
 
+    def setup_columns(self, *args, **kwargs):
+        """
+        Project builds table doesn't show the machines column by default
+        """
+
+        super(ProjectBuildsTable, self).setup_columns(*args, **kwargs)
+
+        # hide the machine column
+        self.set_column_hidden('Machine', True)
+
+        # allow the machine column to be hidden by the user
+        self.set_column_hideable('Machine', True)
+
     def setup_queryset(self, *args, **kwargs):
         """
         NOTE: self.project_id must be set before calling super(),
diff --git a/bitbake/lib/toaster/toastergui/widgets.py b/bitbake/lib/toaster/toastergui/widgets.py
index d9328d4..d2ef5d3 100644
--- a/bitbake/lib/toaster/toastergui/widgets.py
+++ b/bitbake/lib/toaster/toastergui/widgets.py
@@ -168,6 +168,24 @@ class ToasterTable(TemplateView):
                              'computation': computation,
                             })
 
+    def set_column_hidden(self, title, hidden):
+        """
+        Set the hidden state of the column to the value of hidden
+        """
+        for col in self.columns:
+            if col['title'] == title:
+                col['hidden'] = hidden
+                break
+
+    def set_column_hideable(self, title, hideable):
+        """
+        Set the hideable state of the column to the value of hideable
+        """
+        for col in self.columns:
+            if col['title'] == title:
+                col['hideable'] = hideable
+                break
+
     def render_static_data(self, template, row):
         """Utility function to render the static data template"""
 
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 18/23] toaster: toastergui: mute label for filter actions with no records
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (17 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 17/23] toaster: toastergui: set default visible and hideable columns Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 19/23] toaster: toastergui: make "Apply" button state depend on filter range Elliot Smith
                   ` (4 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The radio button for a filter action is disabled if that filter
action has no associated records. However, the label retains
the normal font styling, so it's unclear that the action is
not available.

Add the "muted" class to the label for a filter action (and still
disable its radio button) if it has no records associated with it.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/static/js/table.js | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index a253917..9384386 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -399,18 +399,21 @@ function tableInit(ctx){
    * show when selected
    */
   function createActionRadio(filterName, filterActionData) {
+    var hasNoRecords = (Number(filterActionData.count) == 0);
+
     var actionStr = '<div class="radio">' +
                     '<input type="radio" name="filter"' +
                     '       value="' + filterName + '"';
 
-    if (Number(filterActionData.count) == 0) {
+    if (hasNoRecords) {
       actionStr += ' disabled="disabled"';
     }
 
     actionStr += ' id="' + filterName + '">' +
                  '<input type="hidden" name="filter_value" value="on"' +
                  '       data-value-for="' + filterName + '">' +
-                 '<label class="filter-title"' +
+                 '<label class="filter-title' +
+                 (hasNoRecords ? ' muted' : '') + '"' +
                  '       for="' + filterName + '">' +
                  filterActionData.title +
                  ' (' + filterActionData.count + ')' +
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 19/23] toaster: toastergui: make "Apply" button state depend on filter range
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (18 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 18/23] toaster: toastergui: mute label for filter actions with no records Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 20/23] toaster: toastergui: fix error and warning counts for builds Elliot Smith
                   ` (3 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

If a range filter action had an empty from/to field, the range
filter could still be applied. This was confusing, as an invalid
filter range caused all records to display, even though a filter
appeared to have been applied (by the highlighted state of
the filter button).

Change the state of the "Apply" button, disabling it if the radio
button for a range filter action is selected but the range is
incomplete (from or to field is empty).

When a non-range filter is selected, the "Apply" button always
enable the "Apply" button.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/static/js/table.js  | 77 ++++++++++++++++------
 .../toastergui/templates/toastertable-filter.html  |  4 +-
 2 files changed, 59 insertions(+), 22 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/static/js/table.js b/bitbake/lib/toaster/toastergui/static/js/table.js
index 9384386..87cac60 100644
--- a/bitbake/lib/toaster/toastergui/static/js/table.js
+++ b/bitbake/lib/toaster/toastergui/static/js/table.js
@@ -397,6 +397,8 @@ function tableInit(ctx){
    * filterActionData: (object)
    * filterActionData.count: (number) The number of items this filter will
    * show when selected
+   *
+   * NB this triggers a filtervalue event each time its radio button is checked
    */
   function createActionRadio(filterName, filterActionData) {
     var hasNoRecords = (Number(filterActionData.count) == 0);
@@ -420,7 +422,17 @@ function tableInit(ctx){
                  '</label>' +
                  '</div>';
 
-    return $(actionStr);
+    var action = $(actionStr);
+
+    // fire the filtervalue event from this action when the radio button
+    // is active so that the apply button can be enabled
+    action.find('[type="radio"]').change(function () {
+      if ($(this).is(':checked')) {
+        action.trigger('filtervalue', 'on');
+      }
+    });
+
+    return action;
   }
 
   /**
@@ -437,6 +449,8 @@ function tableInit(ctx){
    * datetime format
    * filterActionData.min: (string) minimum date for the pickers, ISO 8601
    * datetime
+   *
+   * NB this triggers a filtervalue event each time its radio button is checked
    */
   function createActionDateRange(filterName, filterValue, filterActionData) {
     var action = $('<div class="radio">' +
@@ -492,9 +506,26 @@ function tableInit(ctx){
     inputTo.val(selectedTo);
 
     // set filter_value based on date pickers when
-    // one of their values changes
+    // one of their values changes; if either from or to are unset,
+    // the new value is null;
+    // this triggers a 'filter_value-change' event on the action's element,
+    // which is used to determine the disabled/enabled state of the "Apply"
+    // button
     var changeHandler = function () {
-      value.val(inputFrom.val() + ',' + inputTo.val());
+      var fromValue = inputFrom.val();
+      var toValue = inputTo.val();
+
+      var newValue = undefined;
+      if (fromValue !== '' && toValue !== '') {
+        newValue = fromValue + ',' + toValue;
+      }
+
+      value.val(newValue);
+
+      // if this action is selected, fire an event for the new range
+      if (radio.is(':checked')) {
+        action.trigger('filtervalue', newValue);
+      }
     };
 
     inputFrom.change(changeHandler);
@@ -503,6 +534,10 @@ function tableInit(ctx){
     // check the associated radio button on clicking a date picker
     var checkRadio = function () {
       radio.prop('checked', 'checked');
+
+      // checking the radio button this way doesn't cause the "change"
+      // event to fire, so we manually call the changeHandler
+      changeHandler();
     };
 
     inputFrom.focus(checkRadio);
@@ -518,23 +553,9 @@ function tableInit(ctx){
       inputFrom.datepicker('option', 'maxDate', inputTo.val());
     });
 
-    // if the radio button is checked and one or both of the datepickers are
-    // empty, populate them with today's date
-    radio.change(function () {
-      var now = new Date();
-
-      if (inputFrom.val() === '') {
-        inputFrom.datepicker('setDate', now);
-      }
-
-      if (inputTo.val() === '') {
-        inputTo.datepicker('setDate', now);
-      }
-
-      // setting the date like this doesn't fire the changeHandler to
-      // update the filter_value, so do that manually instead
-      changeHandler()
-    });
+    // checking the radio input causes the "Apply" button disabled state to
+    // change, depending on which from/to dates are supplied
+    radio.change(changeHandler);
 
     return action;
   }
@@ -589,6 +610,16 @@ function tableInit(ctx){
             queryset on the table
           */
           var filterActionRadios = $('#filter-actions-' + ctx.tableName);
+          var filterApplyBtn = $('[data-role="filter-apply"]');
+
+          var setApplyButtonState = function (e, filterActionValue) {
+            if (filterActionValue !== undefined) {
+              filterApplyBtn.removeAttr('disabled');
+            }
+            else {
+              filterApplyBtn.attr('disabled', 'disabled');
+            }
+          };
 
           $('#filter-modal-title-' + ctx.tableName).text(filterData.title);
 
@@ -624,10 +655,14 @@ function tableInit(ctx){
               if ((tableParams.filter &&
                   tableParams.filter === radioInput.val()) ||
                   filterActionData.action_name == 'all') {
-                  radioInput.attr("checked", "checked");
+                  radioInput.prop("checked", "checked");
               }
 
               filterActionRadios.append(action);
+
+              // if the action's filter_value changes but is falsy, disable
+              // the "Apply" button
+              action.on('filtervalue', setApplyButtonState);
             }
           }
 
diff --git a/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html b/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html
index 7c8dc49..4d28793 100644
--- a/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html
+++ b/bitbake/lib/toaster/toastergui/templates/toastertable-filter.html
@@ -10,7 +10,9 @@
       <span id="filter-actions-{{table_name}}"></span>
     </div>
     <div class="modal-footer">
-      <button class="btn btn-primary" type="submit">Apply</button>
+      <button class="btn btn-primary" type="submit" data-role="filter-apply">
+        Apply
+      </button>
     </div>
   </form>
 </div>
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 20/23] toaster: toastergui: fix error and warning counts for builds
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (19 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 19/23] toaster: toastergui: make "Apply" button state depend on filter range Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 21/23] toaster: toastergui: remove unused views and template code Elliot Smith
                   ` (2 subsequent siblings)
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The error and warning counts displayed for builds were counts of
the errors and warnings objects associated with a build. Because these
values were being derived on the fly, it was not possible to sort by
them.

Previously, the 3rd party django-aggregate-if library was used to
add aggregate fields to Build objects and should then have been
used to populate the "all builds" and "project builds" tables. However,
at some point the templates had changed so that the error and warning
counts were coming from the properties on the Build model and not from
these aggregates. This meant that it was not possible to sort by these
fields.

Django 1.8 supports conditional aggregates in annotation fields on
querysets. This means we can remove django-aggregate-if, use the new Django
1.8 feature to derive errors_no and warnings_no fields as annotations,
then use those annotation fields in the templates. This makes the "builds"
tables sortable again.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 .../contrib/django-aggregate-if-master/.gitignore  |  10 --
 .../contrib/django-aggregate-if-master/.travis.yml |  50 ------
 .../contrib/django-aggregate-if-master/LICENSE     |  21 ---
 .../contrib/django-aggregate-if-master/README.rst  | 156 ----------------
 .../django-aggregate-if-master/aggregate_if.py     | 164 -----------------
 .../contrib/django-aggregate-if-master/runtests.py |  48 -----
 .../contrib/django-aggregate-if-master/setup.py    |  33 ----
 .../contrib/django-aggregate-if-master/tox.ini     | 198 ---------------------
 bitbake/lib/toaster/toastergui/tables.py           |  40 +++--
 bitbake/lib/toaster/toastermain/settings.py        |   9 -
 10 files changed, 27 insertions(+), 702 deletions(-)
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py
 delete mode 100755 bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py
 delete mode 100644 bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini

diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore b/bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore
deleted file mode 100644
index c45652d..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-*.pyc
-*.swp
-*.swo
-*.kpf
-*.egg-info/
-.idea
-.tox
-tmp/
-dist/
-.DS_Store
diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml b/bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml
deleted file mode 100644
index a920f39..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/.travis.yml
+++ /dev/null
@@ -1,50 +0,0 @@
-language: python
-python:
-  - "2.7"
-  - "3.4"
-services:
-  - mysql
-  - postgresql
-env:
-  - DJANGO=1.4 DB=sqlite
-  - DJANGO=1.4 DB=mysql
-  - DJANGO=1.4 DB=postgres
-  - DJANGO=1.5 DB=sqlite
-  - DJANGO=1.5 DB=mysql
-  - DJANGO=1.5 DB=postgres
-  - DJANGO=1.6 DB=sqlite
-  - DJANGO=1.6 DB=mysql
-  - DJANGO=1.6 DB=postgres
-  - DJANGO=1.7 DB=sqlite
-  - DJANGO=1.7 DB=mysql
-  - DJANGO=1.7 DB=postgres
-
-matrix:
-  exclude:
-    - python: "3.4"
-      env: DJANGO=1.4 DB=sqlite
-    - python: "3.4"
-      env: DJANGO=1.4 DB=mysql
-    - python: "3.4"
-      env: DJANGO=1.4 DB=postgres
-    - python: "3.4"
-      env: DJANGO=1.5 DB=sqlite
-    - python: "3.4"
-      env: DJANGO=1.5 DB=mysql
-    - python: "3.4"
-      env: DJANGO=1.5 DB=postgres
-    - python: "3.4"
-      env: DJANGO=1.6 DB=mysql
-    - python: "3.4"
-      env: DJANGO=1.7 DB=mysql
-
-before_script:
-  - mysql -e 'create database aggregation;'
-  - psql -c 'create database aggregation;' -U postgres
-install:
-  - pip install six
-  - if [ "$DB" == "mysql" ]; then pip install mysql-python; fi
-  - if [ "$DB" == "postgres" ]; then pip install psycopg2; fi
-  - pip install -q Django==$DJANGO --use-mirrors
-script:
-  - ./runtests.py --settings=tests.test_$DB
diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE b/bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE
deleted file mode 100644
index 6b7c3b1..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License
-
-Copyright (c) 2012 Henrique Bastos
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst b/bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst
deleted file mode 100644
index 739d4da..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/README.rst
+++ /dev/null
@@ -1,156 +0,0 @@
-Django Aggregate If: Condition aggregates for Django
-====================================================
-
-.. image:: https://travis-ci.org/henriquebastos/django-aggregate-if.png?branch=master
-    :target: https://travis-ci.org/henriquebastos/django-aggregate-if
-    :alt: Test Status
-
-.. image:: https://landscape.io/github/henriquebastos/django-aggregate-if/master/landscape.png
-    :target: https://landscape.io/github/henriquebastos/django-aggregate-if/master
-    :alt: Code Helth
-
-.. image:: https://pypip.in/v/django-aggregate-if/badge.png
-    :target: https://crate.io/packages/django-aggregate-if/
-    :alt: Latest PyPI version
-
-.. image:: https://pypip.in/d/django-aggregate-if/badge.png
-    :target: https://crate.io/packages/django-aggregate-if/
-    :alt: Number of PyPI downloads
-
-*Aggregate-if* adds conditional aggregates to Django.
-
-Conditional aggregates can help you reduce the ammount of queries to obtain
-aggregated information, like statistics for example.
-
-Imagine you have a model ``Offer`` like this one:
-
-.. code-block:: python
-
-    class Offer(models.Model):
-        sponsor = models.ForeignKey(User)
-        price = models.DecimalField(max_digits=9, decimal_places=2)
-        status = models.CharField(max_length=30)
-        expire_at = models.DateField(null=True, blank=True)
-        created_at = models.DateTimeField(auto_now_add=True)
-        updated_at = models.DateTimeField(auto_now=True)
-
-        OPEN = "OPEN"
-        REVOKED = "REVOKED"
-        PAID = "PAID"
-
-Let's say you want to know:
-
-#. How many offers exists in total;
-#. How many of them are OPEN, REVOKED or PAID;
-#. How much money was offered in total;
-#. How much money is in OPEN, REVOKED and PAID offers;
-
-To get these informations, you could query:
-
-.. code-block:: python
-
-    from django.db.models import Count, Sum
-
-    Offer.objects.count()
-    Offer.objects.filter(status=Offer.OPEN).aggregate(Count('pk'))
-    Offer.objects.filter(status=Offer.REVOKED).aggregate(Count('pk'))
-    Offer.objects.filter(status=Offer.PAID).aggregate(Count('pk'))
-    Offer.objects.aggregate(Sum('price'))
-    Offer.objects.filter(status=Offer.OPEN).aggregate(Sum('price'))
-    Offer.objects.filter(status=Offer.REVOKED).aggregate(Sum('price'))
-    Offer.objects.filter(status=Offer.PAID).aggregate(Sum('price'))
-
-In this case, **8 queries** were needed to retrieve the desired information.
-
-With conditional aggregates you can get it all with only **1 query**:
-
-.. code-block:: python
-
-    from django.db.models import Q
-    from aggregate_if import Count, Sum
-
-    Offer.objects.aggregate(
-        pk__count=Count('pk'),
-        pk__open__count=Count('pk', only=Q(status=Offer.OPEN)),
-        pk__revoked__count=Count('pk', only=Q(status=Offer.REVOKED)),
-        pk__paid__count=Count('pk', only=Q(status=Offer.PAID)),
-        pk__sum=Sum('price'),
-        pk__open__sum=Sum('price', only=Q(status=Offer.OPEN)),
-        pk__revoked__sum=Sum('price'), only=Q(status=Offer.REVOKED)),
-        pk__paid__sum=Sum('price'), only=Q(status=Offer.PAID))
-    )
-
-Installation
-------------
-
-*Aggregate-if* works with Django 1.4, 1.5, 1.6 and 1.7.
-
-To install it, simply:
-
-.. code-block:: bash
-
-    $ pip install django-aggregate-if
-
-Inspiration
------------
-
-There is a 5 years old `ticket 11305`_ that will (*hopefully*) implement this feature into
-Django 1.8.
-
-Using Django 1.6, I still wanted to avoid creating custom queries for very simple
-conditional aggregations. So I've cherry picked those ideas and others from the
-internet and built this library.
-
-This library uses the same API and tests proposed on `ticket 11305`_, so when the
-new feature is available you can easily replace ``django-aggregate-if``.
-
-Limitations
------------
-
-Conditions involving joins with aliases are not supported yet. If you want to
-help adding this feature, you're welcome to check the `first issue`_.
-
-Contributors
-------------
-
-* `Henrique Bastos <http://github.com/henriquebastos>`_
-* `Iuri de Silvio <https://github.com/iurisilvio>`_
-* `Hampus Stjernhav <https://github.com/champ>`_
-* `Bradley Martsberger <https://github.com/martsberger>`_
-* `Markus Bertheau <https://github.com/mbertheau>`_
-* `end0 <https://github.com/end0>`_
-* `Scott Sexton <https://github.com/scottsexton>`_
-* `Mauler <https://github.com/mauler>`_
-* `trbs <https://github.com/trbs>`_
-
-Changelog
----------
-
-0.5
-    - Support for Django 1.7
-
-0.4
-    - Use tox to run tests.
-    - Add support for Django 1.6.
-    - Add support for Python3.
-    - The ``only`` parameter now freely supports joins independent of the main query.
-    - Adds support for alias relabeling permitting excludes and updates with aggregates filtered on remote foreign key relations.
-
-0.3.1
-    - Fix quotation escaping.
-    - Fix boolean casts on Postgres.
-
-0.2
-    - Fix postgres issue with LIKE conditions.
-
-0.1
-    - Initial release.
-
-
-License
-=======
-
-The MIT License.
-
-.. _ticket 11305: https://code.djangoproject.com/ticket/11305
-.. _first issue: https://github.com/henriquebastos/django-aggregate-if/issues/1
diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py b/bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py
deleted file mode 100644
index d5f3427..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/aggregate_if.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# coding: utf-8
-'''
-Implements conditional aggregates.
-
-This code was based on the work of others found on the internet:
-
-1. http://web.archive.org/web/20101115170804/http://www.voteruniverse.com/Members/jlantz/blog/conditional-aggregates-in-django
-2. https://code.djangoproject.com/ticket/11305
-3. https://groups.google.com/forum/?fromgroups=#!topic/django-users/cjzloTUwmS0
-4. https://groups.google.com/forum/?fromgroups=#!topic/django-users/vVprMpsAnPo
-'''
-from __future__ import unicode_literals
-from django.utils import six
-import django
-from django.db.models.aggregates import Aggregate as DjangoAggregate
-from django.db.models.sql.aggregates import Aggregate as DjangoSqlAggregate
-
-
-VERSION = django.VERSION[:2]
-
-
-class SqlAggregate(DjangoSqlAggregate):
-    conditional_template = '%(function)s(CASE WHEN %(condition)s THEN %(field)s ELSE null END)'
-
-    def __init__(self, col, source=None, is_summary=False, condition=None, **extra):
-        super(SqlAggregate, self).__init__(col, source, is_summary, **extra)
-        self.condition = condition
-
-    def relabel_aliases(self, change_map):
-        if VERSION < (1, 7):
-            super(SqlAggregate, self).relabel_aliases(change_map)
-        if self.has_condition:
-            condition_change_map = dict((k, v) for k, v in \
-                change_map.items() if k in self.condition.query.alias_map
-            )
-            self.condition.query.change_aliases(condition_change_map)
-
-    def relabeled_clone(self, change_map):
-        self.relabel_aliases(change_map)
-        return super(SqlAggregate, self).relabeled_clone(change_map)
-
-    def as_sql(self, qn, connection):
-        if self.has_condition:
-            self.sql_template = self.conditional_template
-            self.extra['condition'] = self._condition_as_sql(qn, connection)
-
-        return super(SqlAggregate, self).as_sql(qn, connection)
-
-    @property
-    def has_condition(self):
-        # Warning: bool(QuerySet) will hit the database
-        return self.condition is not None
-
-    def _condition_as_sql(self, qn, connection):
-        '''
-        Return sql for condition.
-        '''
-        def escape(value):
-            if isinstance(value, bool):
-                value = str(int(value))
-            if isinstance(value, six.string_types):
-                # Escape params used with LIKE
-                if '%' in value:
-                    value = value.replace('%', '%%')
-                # Escape single quotes
-                if "'" in value:
-                    value = value.replace("'", "''")
-                # Add single quote to text values
-                value = "'" + value + "'"
-            return value
-
-        sql, param = self.condition.query.where.as_sql(qn, connection)
-        param = map(escape, param)
-
-        return sql % tuple(param)
-
-
-class SqlSum(SqlAggregate):
-    sql_function = 'SUM'
-
-
-class SqlCount(SqlAggregate):
-    is_ordinal = True
-    sql_function = 'COUNT'
-    sql_template = '%(function)s(%(distinct)s%(field)s)'
-    conditional_template = '%(function)s(%(distinct)sCASE WHEN %(condition)s THEN %(field)s ELSE null END)'
-
-    def __init__(self, col, distinct=False, **extra):
-        super(SqlCount, self).__init__(col, distinct=distinct and 'DISTINCT ' or '', **extra)
-
-
-class SqlAvg(SqlAggregate):
-    is_computed = True
-    sql_function = 'AVG'
-
-
-class SqlMax(SqlAggregate):
-    sql_function = 'MAX'
-
-
-class SqlMin(SqlAggregate):
-    sql_function = 'MIN'
-
-
-class Aggregate(DjangoAggregate):
-    def __init__(self, lookup, only=None, **extra):
-        super(Aggregate, self).__init__(lookup, **extra)
-        self.only = only
-        self.condition = None
-
-    def _get_fields_from_Q(self, q):
-        fields = []
-        for child in q.children:
-            if hasattr(child, 'children'):
-                fields.extend(self._get_fields_from_Q(child))
-            else:
-                fields.append(child)
-        return fields
-
-    def add_to_query(self, query, alias, col, source, is_summary):
-        if self.only:
-            self.condition = query.model._default_manager.filter(self.only)
-            for child in self._get_fields_from_Q(self.only):
-                field_list = child[0].split('__')
-                # Pop off the last field if it's a query term ('gte', 'contains', 'isnull', etc.)
-                if field_list[-1] in query.query_terms:
-                    field_list.pop()
-                # setup_joins have different returns in Django 1.5 and 1.6, but the order of what we need remains.
-                result = query.setup_joins(field_list, query.model._meta, query.get_initial_alias(), None)
-                join_list = result[3]
-
-                fname = 'promote_alias_chain' if VERSION < (1, 5) else 'promote_joins'
-                args = (join_list, True) if VERSION < (1, 7) else (join_list,)
-
-                promote = getattr(query, fname)
-                promote(*args)
-
-        aggregate = self.sql_klass(col, source=source, is_summary=is_summary, condition=self.condition, **self.extra)
-        query.aggregates[alias] = aggregate
-
-
-class Sum(Aggregate):
-    name = 'Sum'
-    sql_klass = SqlSum
-
-
-class Count(Aggregate):
-    name = 'Count'
-    sql_klass = SqlCount
-
-
-class Avg(Aggregate):
-    name = 'Avg'
-    sql_klass = SqlAvg
-
-
-class Max(Aggregate):
-    name = 'Max'
-    sql_klass = SqlMax
-
-
-class Min(Aggregate):
-    name = 'Min'
-    sql_klass = SqlMin
diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py b/bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py
deleted file mode 100755
index 2e55864..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/runtests.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python
-
-import os
-import sys
-from optparse import OptionParser
-
-
-def parse_args():
-    parser = OptionParser()
-    parser.add_option('-s', '--settings', help='Define settings.')
-    parser.add_option('-t', '--unittest', help='Define which test to run. Default all.')
-    options, args = parser.parse_args()
-
-    if not options.settings:
-        parser.print_help()
-        sys.exit(1)
-
-    if not options.unittest:
-        options.unittest = ['aggregation']
-
-    return options
-
-
-def get_runner(settings_module):
-    '''
-    Asks Django for the TestRunner defined in settings or the default one.
-    '''
-    os.environ['DJANGO_SETTINGS_MODULE'] = settings_module
-
-    import django
-    from django.test.utils import get_runner
-    from django.conf import settings
-
-    if hasattr(django, 'setup'):
-        django.setup()
-
-    return get_runner(settings)
-
-
-def runtests():
-    options = parse_args()
-    TestRunner = get_runner(options.settings)
-    runner = TestRunner(verbosity=1, interactive=True, failfast=False)
-    sys.exit(runner.run_tests([]))
-
-
-if __name__ == '__main__':
-    runtests()
diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py b/bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py
deleted file mode 100644
index aed3db1..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/setup.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# coding: utf-8
-from setuptools import setup
-import os
-
-
-setup(name='django-aggregate-if',
-      version='0.5',
-      description='Conditional aggregates for Django, just like the famous SumIf in Excel.',
-      long_description=open(os.path.join(os.path.dirname(__file__), "README.rst")).read(),
-      author="Henrique Bastos", author_email="henrique@bastos.net",
-      license="MIT",
-      py_modules=['aggregate_if'],
-      install_requires=[
-          'six>=1.6.1',
-      ],
-      zip_safe=False,
-      platforms='any',
-      include_package_data=True,
-      classifiers=[
-          'Development Status :: 5 - Production/Stable',
-          'Framework :: Django',
-          'Intended Audience :: Developers',
-          'License :: OSI Approved :: MIT License',
-          'Natural Language :: English',
-          'Operating System :: OS Independent',
-          'Programming Language :: Python',
-          'Programming Language :: Python :: 2.7',
-          'Programming Language :: Python :: 3',
-          'Topic :: Database',
-          'Topic :: Software Development :: Libraries',
-      ],
-      url='http://github.com/henriquebastos/django-aggregate-if/',
-)
diff --git a/bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini b/bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini
deleted file mode 100644
index 78beb14..0000000
--- a/bitbake/lib/toaster/contrib/django-aggregate-if-master/tox.ini
+++ /dev/null
@@ -1,198 +0,0 @@
-[tox]
-envlist =
-    py27-django1.4-sqlite,
-    py27-django1.4-postgres,
-    py27-django1.4-mysql,
-
-    py27-django1.5-sqlite,
-    py27-django1.5-postgres,
-    py27-django1.5-mysql,
-
-    py27-django1.6-sqlite,
-    py27-django1.6-postgres,
-    py27-django1.6-mysql,
-
-    py27-django1.7-sqlite,
-    py27-django1.7-postgres,
-    py27-django1.7-mysql,
-
-    py34-django1.6-sqlite,
-    py34-django1.6-postgres,
-    #py34-django1.6-mysql
-
-    py34-django1.7-sqlite,
-    py34-django1.7-postgres,
-    #py34-django1.7-mysql
-
-[testenv]
-whitelist_externals=
-    mysql
-    psql
-
-# Python 2.7
-# Django 1.4
-[testenv:py27-django1.4-sqlite]
-basepython = python2.7
-deps =
-    django==1.4
-commands = python runtests.py --settings tests.test_sqlite
-
-[testenv:py27-django1.4-postgres]
-basepython = python2.7
-deps =
-    django==1.4
-    psycopg2
-commands =
-    psql -c 'create database aggregation;' postgres
-    python runtests.py --settings tests.test_postgres
-    psql -c 'drop database aggregation;' postgres
-
-[testenv:py27-django1.4-mysql]
-basepython = python2.7
-deps =
-    django==1.4
-    mysql-python
-commands =
-    mysql -e 'create database aggregation;'
-    python runtests.py --settings tests.test_mysql
-    mysql -e 'drop database aggregation;'
-
-# Django 1.5
-[testenv:py27-django1.5-sqlite]
-basepython = python2.7
-deps =
-    django==1.5
-commands = python runtests.py --settings tests.test_sqlite
-
-[testenv:py27-django1.5-postgres]
-basepython = python2.7
-deps =
-    django==1.5
-    psycopg2
-commands =
-    psql -c 'create database aggregation;' postgres
-    python runtests.py --settings tests.test_postgres
-    psql -c 'drop database aggregation;' postgres
-
-[testenv:py27-django1.5-mysql]
-basepython = python2.7
-deps =
-    django==1.5
-    mysql-python
-commands =
-    mysql -e 'create database aggregation;'
-    python runtests.py --settings tests.test_mysql
-    mysql -e 'drop database aggregation;'
-
-# Django 1.6
-[testenv:py27-django1.6-sqlite]
-basepython = python2.7
-deps =
-    django==1.6
-commands = python runtests.py --settings tests.test_sqlite
-
-[testenv:py27-django1.6-postgres]
-basepython = python2.7
-deps =
-    django==1.6
-    psycopg2
-commands =
-    psql -c 'create database aggregation;' postgres
-    python runtests.py --settings tests.test_postgres
-    psql -c 'drop database aggregation;' postgres
-
-[testenv:py27-django1.6-mysql]
-basepython = python2.7
-deps =
-    django==1.6
-    mysql-python
-commands =
-    mysql -e 'create database aggregation;'
-    python runtests.py --settings tests.test_mysql
-    mysql -e 'drop database aggregation;'
-
-
-# Python 2.7 and Django 1.7
-[testenv:py27-django1.7-sqlite]
-basepython = python2.7
-deps =
-    django==1.7
-commands = python runtests.py --settings tests.test_sqlite
-
-[testenv:py27-django1.7-postgres]
-basepython = python2.7
-deps =
-    django==1.7
-    psycopg2
-commands =
-    psql -c 'create database aggregation;' postgres
-    python runtests.py --settings tests.test_postgres
-    psql -c 'drop database aggregation;' postgres
-
-[testenv:py27-django1.7-mysql]
-basepython = python2.7
-deps =
-    django==1.7
-    mysql-python
-commands =
-    mysql -e 'create database aggregation;'
-    python runtests.py --settings tests.test_mysql
-    mysql -e 'drop database aggregation;'
-
-
-# Python 3.4
-# Django 1.6
-[testenv:py34-django1.6-sqlite]
-basepython = python3.4
-deps =
-    django==1.6
-commands = python runtests.py --settings tests.test_sqlite
-
-[testenv:py34-django1.6-postgres]
-basepython = python3.4
-deps =
-    django==1.6
-    psycopg2
-commands =
-    psql -c 'create database aggregation;' postgres
-    python runtests.py --settings tests.test_postgres
-    psql -c 'drop database aggregation;' postgres
-
-[testenv:py34-django1.6-mysql]
-basepython = python3.4
-deps =
-    django==1.6
-    mysql-python3
-commands =
-    mysql -e 'create database aggregation;'
-    python runtests.py --settings tests.test_mysql
-    mysql -e 'drop database aggregation;'
-
-
-# Python 3.4
-# Django 1.7
-[testenv:py34-django1.7-sqlite]
-basepython = python3.4
-deps =
-    django==1.7
-commands = python runtests.py --settings tests.test_sqlite
-
-[testenv:py34-django1.7-postgres]
-basepython = python3.4
-deps =
-    django==1.7
-    psycopg2
-commands =
-    psql -c 'create database aggregation;' postgres
-    python runtests.py --settings tests.test_postgres
-    psql -c 'drop database aggregation;' postgres
-
-[testenv:py34-django1.7-mysql]
-basepython = python3.4
-deps =
-    django==1.7
-    mysql-python3
-commands =
-    mysql -e 'create database aggregation;'
-    python runtests.py --settings tests.test_mysql
-    mysql -e 'drop database aggregation;'
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 14077e1..2279731 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -23,7 +23,7 @@ from toastergui.widgets import ToasterTable
 from orm.models import Recipe, ProjectLayer, Layer_Version, Machine, Project
 from orm.models import CustomImageRecipe, Package, Build, LogMessage, Task
 from orm.models import ProjectTarget
-from django.db.models import Q, Max, Count
+from django.db.models import Q, Max, Count, When, Case, Value, IntegerField
 from django.conf.urls import url
 from django.core.urlresolvers import reverse, resolve
 from django.http import HttpResponse
@@ -927,6 +927,13 @@ class BuildsTable(ToasterTable):
         return context
 
     def setup_queryset(self, *args, **kwargs):
+        """
+        The queryset is annotated so that it can be sorted by number of
+        errors and number of warnings; but note that the criteria for
+        finding the log messages to populate these fields should match those
+        used in the Build model (orm/models.py) to populate the errors and
+        warnings properties
+        """
         queryset = self.get_builds()
 
         # don't include in progress builds
@@ -935,20 +942,27 @@ class BuildsTable(ToasterTable):
         # sort
         queryset = queryset.order_by(self.default_orderby)
 
-        # annotate with number of ERROR and EXCEPTION log messages
+        # annotate with number of ERROR, EXCEPTION and CRITICAL log messages
+        criteria = (Q(logmessage__level=LogMessage.ERROR) |
+                    Q(logmessage__level=LogMessage.EXCEPTION) |
+                    Q(logmessage__level=LogMessage.CRITICAL))
+
         queryset = queryset.annotate(
-            errors_no = Count(
-                'logmessage',
-                only = Q(logmessage__level=LogMessage.ERROR) |
-                       Q(logmessage__level=LogMessage.EXCEPTION)
+            errors_no=Count(
+                Case(
+                    When(criteria, then=Value(1)),
+                    output_field=IntegerField()
+                )
             )
         )
 
         # annotate with number of WARNING log messages
         queryset = queryset.annotate(
-            warnings_no = Count(
-                'logmessage',
-                only = Q(logmessage__level=LogMessage.WARNING)
+            warnings_no=Count(
+                Case(
+                    When(logmessage__level=LogMessage.WARNING, then=Value(1)),
+                    output_field=IntegerField()
+                )
             )
         )
 
@@ -1020,17 +1034,17 @@ class BuildsTable(ToasterTable):
         '''
 
         errors_template = '''
-        {% if data.errors.count %}
+        {% if data.errors_no %}
             <a class="errors.count error" href="{% url "builddashboard" data.id %}#errors">
-                {{data.errors.count}} error{{data.errors.count|pluralize}}
+                {{data.errors_no}} error{{data.errors_no|pluralize}}
             </a>
         {% endif %}
         '''
 
         warnings_template = '''
-        {% if data.warnings.count %}
+        {% if data.warnings_no %}
             <a class="warnings.count warning" href="{% url "builddashboard" data.id %}#warnings">
-                {{data.warnings.count}} warning{{data.warnings.count|pluralize}}
+                {{data.warnings_no}} warning{{data.warnings_no|pluralize}}
             </a>
         {% endif %}
         '''
diff --git a/bitbake/lib/toaster/toastermain/settings.py b/bitbake/lib/toaster/toastermain/settings.py
index 74103f3..c4f3d6b 100644
--- a/bitbake/lib/toaster/toastermain/settings.py
+++ b/bitbake/lib/toaster/toastermain/settings.py
@@ -399,12 +399,3 @@ class InvalidString(str):
             "Undefined variable or unknown value for: \"%s\"" % other)
 
 TEMPLATE_STRING_IF_INVALID = InvalidString("%s")
-
-import sys
-sys.path.append(
-    os.path.join(
-    os.path.join(
-        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
-            "contrib"),
-            "django-aggregate-if-master")
-    )
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 21/23] toaster: toastergui: remove unused views and template code
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (20 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 20/23] toaster: toastergui: fix error and warning counts for builds Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 22/23] toaster: tests: fix Django tests for new ToasterTable pages Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 23/23] toaster: toastergui: code formatting and clean-up Elliot Smith
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The code in views.py for setting up the template context for
old non-ToasterTable views is no longer necessary, as this
is now implemented in tables.py.

The template files for these views have also been removed.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 .../lib/toaster/toastergui/templates/builds.html   | 125 ------
 .../lib/toaster/toastergui/templates/projects.html |  92 -----
 bitbake/lib/toaster/toastergui/views.py            | 430 ---------------------
 3 files changed, 647 deletions(-)
 delete mode 100644 bitbake/lib/toaster/toastergui/templates/builds.html
 delete mode 100644 bitbake/lib/toaster/toastergui/templates/projects.html

diff --git a/bitbake/lib/toaster/toastergui/templates/builds.html b/bitbake/lib/toaster/toastergui/templates/builds.html
deleted file mode 100644
index c3cc266..0000000
--- a/bitbake/lib/toaster/toastergui/templates/builds.html
+++ /dev/null
@@ -1,125 +0,0 @@
-{% extends "base.html" %}
-
-{% load static %}
-{% load projecttags %}
-{% load project_url_tag %}
-{% load humanize %}
-
-{% block title %} All builds - Toaster {% endblock %}
-{% block extraheadcontent %}
-<link rel="stylesheet" href="/static/css/jquery-ui.min.css" type='text/css'>
-<link rel="stylesheet" href="/static/css/jquery-ui.structure.min.css" type='text/css'>
-<link rel="stylesheet" href="/static/css/jquery-ui.theme.min.css" type='text/css'>
-<script src="/static/js/jquery-ui.min.js"></script>
-<script src="/static/js/filtersnippet.js"></script>
-{% endblock %}
-
-{% block pagecontent %}
-
-{% if last_date_from and last_date_to %}
-<script>
-    // initialize the date range controls
-    $(document).ready(function () {
-        date_init('started_on','{{last_date_from}}','{{last_date_to}}','{{dateMin_started_on}}','{{dateMax_started_on}}','{{daterange_selected}}');
-        date_init('completed_on','{{last_date_from}}','{{last_date_to}}','{{dateMin_completed_on}}','{{dateMax_completed_on}}','{{daterange_selected}}');
-    });
-</script>
-{%endif%} {# last_date_from and last_date_to #}
-
-<div class="row-fluid">
-
-  {% include "mrb_section.html" %}
-
-  <div class="page-header top-air">
-     <h1>
-      {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %}
-          {{objects.paginator.count}} build{{objects.paginator.count|pluralize}} found
-      {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %}
-          No builds found
-      {%else%}
-          All builds
-      {%endif%}
-     </h1>
-  </div>
-
-  {% if objects.paginator.count == 0 %}
-    <div class="row-fluid">
-      <div class="alert">
-        <form class="no-results input-append" id="searchform">
-            <input id="search" name="search" class="input-xxlarge" type="text" value="
-            {% if request.GET.search %}
-                {{request.GET.search}}
-        {% endif %}"/>
-            {% if request.GET.search %}<a href="javascript:$('#search').val('');searchform.submit()" class="add-on btn" tabindex="-1"><i class="icon-remove"></i></a>{% endif %}
-            <button class="btn" type="submit" value="Search">Search</button>
-            <button class="btn btn-link" onclick="javascript:$('#search').val('');searchform.submit()">Show all builds</button>
-        </form>
-      </div>
-    </div>
-  {% else %}
-  {% include "basetable_top.html" %}
-        <!-- Table data rows; the order needs to match the order of "tablecols" definitions; and the <td class value needs to match the tablecols clclass value for show/hide buttons to work -->
-        {% for build in objects %}
-        <tr class="data" data-table-build-result="{{ build.id }}">
-            <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>
-                {% if build.cooker_log_path %}
-                  &nbsp;
-                  <a href="{% url 'build_artifact' build.id "cookerlog" build.id %}">
-                    <i class="icon-download-alt" title="Download build log"></i>
-                  </a>
-                {% endif %}
-            </td>
-            <td class="target">
-                {% for t in build.target_set.all %}
-                    <a href="{% url "builddashboard" build.id %}">
-                        {% if t.task %}
-                            {{t.target}}:{{t.task}}
-                        {% else %}
-                            {{t.target}}
-                        {% endif %}
-                    </a> <br />
-                {% endfor %}
-            </td>
-            <td class="machine"><a href="{% url "builddashboard" build.id %}">{{build.machine}}</a></td>
-            <td class="started_on"><a href="{% url "builddashboard" build.id %}">{{build.started_on|date:"d/m/y H:i"}}</a></td>
-            <td class="completed_on"><a href="{% url "builddashboard" build.id %}">{{build.completed_on|date:"d/m/y H:i"}}</a></td>
-            <td class="failed_tasks error">
-                {% query build.task_build outcome=4 order__gt=0 as exectask%}
-                    {% if exectask.count == 1 %}
-                        <a href="{% url "task" build.id exectask.0.id %}">{{exectask.0.recipe.name}}.{{exectask.0.task_name}}</a>
-                        <a href="{% url 'build_artifact' build.id "tasklogfile" exectask.0.id %}">
-                            <i class="icon-download-alt" title="" data-original-title="Download task log file"></i>
-                        </a>
-                    {% elif exectask.count > 1%}
-                        <a href="{% url "tasks" build.id %}?filter=outcome%3A4">{{exectask.count}} task{{exectask.count|pluralize}}</a>
-                    {%endif%}
-            </td>
-            <td class="errors.count errors_no">
-                {% if  build.errors.count %}
-                    <a class="errors.count error" href="{% url "builddashboard" build.id %}#errors">{{build.errors.count}} error{{build.errors.count|pluralize}}</a>
-                {%endif%}
-            </td>
-            <td class="warnings.count warnings_no">{% if  build.warnings.count %}<a class="warnings.count warning" href="{% url "builddashboard" build.id %}#warnings">{{build.warnings.count}} warning{{build.warnings.count|pluralize}}</a>{%endif%}</td>
-            <td class="time"><a href="{% url "buildtime" build.id %}">{{build.timespent_seconds|sectohms}}</a></td>
-            <td class="output">
-              {% if build.outcome == build.SUCCEEDED %}
-              <a href="{%url "builddashboard" build.id%}#images">{{fstypes|get_dict_value:build.id}}</a>
-              {% endif %}
-            </td>
-            <td class="project-name">
-                <a href="{% project_url build.project %}">{{build.project.name}}</a>
-                {% if build.project.is_default %}
-                    <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project shows information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
-                {% endif %}
-            </td>
-        </tr>
-
-        {% endfor %}
-
-
-  {% include "basetable_bottom.html" %}
-  {% endif %} {# objects.paginator.count #}
-</div><!-- end row-fluid-->
-
-{% endblock %}
diff --git a/bitbake/lib/toaster/toastergui/templates/projects.html b/bitbake/lib/toaster/toastergui/templates/projects.html
deleted file mode 100644
index 678a796..0000000
--- a/bitbake/lib/toaster/toastergui/templates/projects.html
+++ /dev/null
@@ -1,92 +0,0 @@
-{% extends "base.html" %}
-
-{% load static %}
-{% load projecttags %}
-{% load project_url_tag %}
-{% load humanize %}
-
-{% block title %} All projects - Toaster {% endblock %}
-
-{% block pagecontent %}
-
-
-  <div class="page-header top-air">
-      <h1>
-      {% if request.GET.filter and objects.paginator.count > 0 or request.GET.search and objects.paginator.count > 0 %}
-          {{objects.paginator.count}} project{{objects.paginator.count|pluralize}} found
-      {%elif request.GET.filter and objects.paginator.count == 0 or request.GET.search and objects.paginator.count == 0 %}
-          No projects found
-      {%else%}
-          All projects
-      {%endif%}
-       </h1>
-  </div>
-
-  {% if objects.paginator.count == 0 %}
-    <div class="row-fluid">
-      <div class="alert">
-        <form class="no-results input-append" id="searchform">
-            <input id="search" name="search" class="input-xxlarge" type="text" value="
-                {% if request.GET.search %}
-                    {{request.GET.search}}
-                {% endif %}"/>{% if request.GET.search %}<a href="javascript:$('#search').val('');searchform.submit()" class="add-on btn" tabindex="-1"><i class="icon-remove"></i></a>{% endif %}
-            <button class="btn" type="submit" value="Search">Search</button>
-            <button class="btn btn-link" onclick="javascript:$('#search').val('');searchform.submit()">Show all projects</button>
-        </form>
-      </div>
-    </div>
-
-  {% else %} {# We have builds to display #}
-  {% include "basetable_top.html" %}
-  {% for o in objects %}
-    <tr class="data" data-project="{{ o.id }}">
-      <td data-project-field="name">
-          <a href="{% project_url o %}">{{o.name}}</a>
-      </td>
-      <td class="updated"><a href="{% project_url o %}">{{o.updated|date:"d/m/y H:i"}}</a></td>
-      <td data-project-field="release">
-        {% if o.release %}
-            <a href="{% url 'project' o.id %}#project-details">{{o.release.name}}</a>
-        {% elif o.is_default %}
-            <span class="muted">Not applicable</span>
-            <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a release set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
-        {% else %}
-            No release available
-        {% endif %}
-      </td>
-      <td data-project-field="machine">
-        {% if o.is_default %}
-            <span class="muted">Not applicable</span>
-            <i class="icon-question-sign get-help hover-help" title="" data-original-title="This project does not have a machine set. It simply collects information about the builds you start from the command line while Toaster is running" style="visibility: hidden;"></i>
-        {% else %}
-            <a href="{% url 'project' o.id %}#machine-distro">{{o.get_current_machine_name}}</a>
-        {% endif %}
-      </td>
-      {% if o.get_number_of_builds == 0 %}
-      <td class="muted">{{o.get_number_of_builds}}</td>
-      <td class="loutcome"></td>
-      <td class="ltarget"></td>
-      <td class="lerrors"></td>
-      <td class="lwarnings"></td>
-      <td class="limagefiles"></td>
-      {% else %}
-      <td><a href="{% url 'projectbuilds' o.id %}">{{o.get_number_of_builds}}</a></td>
-      <td class="loutcome"><a href="{% url "builddashboard" o.get_last_build_id %}">{%if o.get_last_outcome == build_SUCCEEDED%}<i class="icon-ok-sign success"></i>{%elif o.get_last_outcome == build_FAILED%}<i class="icon-minus-sign error"></i>{%else%}{%endif%}</a></td>
-      <td class="ltarget"><a href="{% url "builddashboard" o.get_last_build_id %}">{{o.get_last_target}} </a></td>
-      <td class="lerrors">{% if o.get_last_errors %}<a class="errors.count error" href="{% url "builddashboard" o.get_last_build_id %}#errors">{{o.get_last_errors}} error{{o.get_last_errors|pluralize}}</a>{%endif%}</td>
-      <td class="lwarnings">{% if o.get_last_warnings %}<a class="warnings.count warning" href="{% url "builddashboard" o.get_last_build_id %}#warnings">{{o.get_last_warnings}} warning{{o.get_last_warnings|pluralize}}</a>{%endif%}</td>
-      <td class="limagefiles">
-        {% if o.get_last_outcome == build_SUCCEEDED %}
-        <a href="{%url "builddashboard" o.get_last_build_id %}#images">{{fstypes|get_dict_value:o.id}}</a>
-        {% endif %}
-      </td>
-
-      {% endif %}
-    </tr>
-  {% endfor %}
-  {% include "basetable_bottom.html" %}
-  {% endif %} {# empty #}
-
-{% endblock %}
-
-
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 3e8a66b..4f7b50f 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -92,50 +92,6 @@ def landing(request):
 
     return render(request, 'landing.html', context)
 
-"""
-# returns a list for most recent builds;
-def _get_latest_builds(prj=None):
-    queryset = Build.objects.all()
-
-    if prj is not None:
-        queryset = queryset.filter(project = prj)
-
-    return list(itertools.chain(
-        queryset.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
-        queryset.filter(outcome__lt=Build.IN_PROGRESS).order_by("-started_on")[:3] ))
-"""
-
-"""
-# a JSON-able dict of recent builds; for use in the Project page, xhr_ updates,  and other places, as needed
-def _project_recent_build_list(prj):
-    data = []
-    # take the most recent 3 completed builds, plus any builds in progress
-    for x in _get_latest_builds(prj):
-        d = {
-            "id":  x.pk,
-            "targets" : map(lambda y: {"target": y.target, "task": y.task }, x.target_set.all()), # TODO: create the task entry in the Target table
-            "status": x.get_current_status(),
-            "errors": map(lambda y: {"type": y.lineno, "msg": y.message, "tb": y.pathname}, (x.logmessage_set.filter(level__gte=LogMessage.WARNING)|x.logmessage_set.filter(level=LogMessage.EXCEPTION))),
-            "updated": x.completed_on.strftime('%s')+"000",
-            "command_time": (x.completed_on - x.started_on).total_seconds(),
-            "br_page_url": reverse('builddashboard', args=(x.pk,) ),
-            "build" : map( lambda y: {"id": y.pk,
-                        "status": y.get_outcome_display(),
-                        "completed_on" : y.completed_on.strftime('%s')+"000",
-                        "build_time" : (y.completed_on - y.started_on).total_seconds(),
-                        "build_page_url" : reverse('builddashboard', args=(y.pk,)),
-                        'build_time_page_url': reverse('buildtime', args=(y.pk,)),
-                        "errors": y.errors.count(),
-                        "warnings": y.warnings.count(),
-                        "completeper": y.completeper() if y.outcome == Build.IN_PROGRESS else "0",
-                        "eta": y.eta().strftime('%s')+"000" if y.outcome == Build.IN_PROGRESS else "0",
-                        }, [x]),
-            }
-        data.append(d)
-
-    return data
-"""
-
 def objtojson(obj):
     from django.db.models.query import QuerySet
     from django.db.models import Model
@@ -1919,215 +1875,6 @@ if True:
         ''' The exception raised on invalid POST requests '''
         pass
 
-    """
-    # helper function, to be used on "all builds" and "project builds" pages
-    def _build_list_helper(request, queryset_all, redirect_page, pid=None):
-        default_orderby = 'completed_on:-'
-        (pagesize, orderby) = _get_parameters_values(request, 10, default_orderby)
-        mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby }
-        retval = _verify_parameters( request.GET, mandatory_parameters )
-        if retval:
-            params = {}
-            if pid:
-                params = {'pid': pid}
-            raise RedirectException(redirect_page,
-                                    request.GET,
-                                    mandatory_parameters,
-                                    **params)
-
-        # boilerplate code that takes a request for an object type and returns a queryset
-        # for that object type. copypasta for all needed table searches
-        (filter_string, search_term, ordering_string) = _search_tuple(request, Build)
-
-        # post-process any date range filters
-        filter_string, daterange_selected = _modify_date_range_filter(filter_string)
-
-        # don't show "in progress" builds in "all builds" or "project builds"
-        queryset_all = queryset_all.exclude(outcome = Build.IN_PROGRESS)
-
-        # append project info
-        queryset_all = queryset_all.select_related("project")
-
-        # annotate with number of ERROR and EXCEPTION log messages
-        queryset_all = queryset_all.annotate(
-            errors_no = Count(
-                'logmessage',
-                only=Q(logmessage__level=LogMessage.ERROR) |
-                     Q(logmessage__level=LogMessage.EXCEPTION)
-            )
-        )
-
-        # annotate with number of warnings
-        q_warnings = Q(logmessage__level=LogMessage.WARNING)
-        queryset_all = queryset_all.annotate(
-            warnings_no = Count('logmessage', only=q_warnings)
-        )
-
-        queryset_with_search = _get_queryset(Build, queryset_all,
-                                             None, search_term,
-                                             ordering_string, '-completed_on')
-
-        queryset = _get_queryset(Build, queryset_all,
-                                 filter_string, search_term,
-                                 ordering_string, '-completed_on')
-
-        # retrieve the objects that will be displayed in the table; builds a paginator and gets a page range to display
-        build_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1))
-
-        # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
-        build_mru = _get_latest_builds()[:3]
-
-        # calculate the exact begining of local today and yesterday, append context
-        context_date,today_begin,yesterday_begin = _add_daterange_context(queryset_all, request, {'started_on','completed_on'})
-
-        # set up list of fstypes for each build
-        fstypes_map = {}
-
-        for build in build_info:
-            fstypes_map[build.id] = build.get_image_file_extensions()
-
-        # send the data to the template
-        context = {
-                # specific info for
-                    'mru' : build_mru,
-                # TODO: common objects for all table views, adapt as needed
-                    'objects' : build_info,
-                    'objectname' : "builds",
-                    'default_orderby' : default_orderby,
-                    'fstypes' : fstypes_map,
-                    'search_term' : search_term,
-                    'total_count' : queryset_with_search.count(),
-                    'daterange_selected' : daterange_selected,
-                # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
-                    'tablecols' : [
-                    {'name': 'Outcome',                                                # column with a single filter
-                     'qhelp' : "The outcome tells you if a build successfully completed or failed",     # the help button content
-                     'dclass' : "span2",                                                # indication about column width; comes from the design
-                     'orderfield': _get_toggle_order(request, "outcome"),               # adds ordering by the field value; default ascending unless clicked from ascending into descending
-                     'ordericon':_get_toggle_order_icon(request, "outcome"),
-                      # filter field will set a filter on that column with the specs in the filter description
-                      # the class field in the filter has no relation with clclass; the control different aspects of the UI
-                      # still, it is recommended for the values to be identical for easy tracking in the generated HTML
-                     'filter' : {'class' : 'outcome',
-                                 'label': 'Show:',
-                                 'options' : [
-                                             ('Successful builds', 'outcome:' + str(Build.SUCCEEDED), queryset_with_search.filter(outcome=str(Build.SUCCEEDED)).count()),  # this is the field search expression
-                                             ('Failed builds', 'outcome:'+ str(Build.FAILED), queryset_with_search.filter(outcome=str(Build.FAILED)).count()),
-                                             ]
-                                }
-                    },
-                    {'name': 'Recipe',                                                 # default column, disabled box, with just the name in the list
-                     'qhelp': "What you built (i.e. one or more recipes or image recipes)",
-                     'orderfield': _get_toggle_order(request, "target__target"),
-                     'ordericon':_get_toggle_order_icon(request, "target__target"),
-                    },
-                    {'name': 'Machine',
-                     'qhelp': "The machine is the hardware for which you are building a recipe or image recipe",
-                     'orderfield': _get_toggle_order(request, "machine"),
-                     'ordericon':_get_toggle_order_icon(request, "machine"),
-                     'dclass': 'span3'
-                    },                           # a slightly wider column
-                    {'name': 'Started on', 'clclass': 'started_on', 'hidden' : 1,      # this is an unchecked box, which hides the column
-                     'qhelp': "The date and time you started the build",
-                     'orderfield': _get_toggle_order(request, "started_on", True),
-                     'ordericon':_get_toggle_order_icon(request, "started_on"),
-                     'orderkey' : "started_on",
-                     'filter' : {'class' : 'started_on',
-                                 'label': 'Show:',
-                                 'options' : [
-                                             ("Today's builds" , 'started_on__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(started_on__gte=today_begin).count()),
-                                             ("Yesterday's builds",
-                                                 'started_on__gte!started_on__lt:'
-                                                     +yesterday_begin.strftime("%Y-%m-%d")+'!'
-                                                     +today_begin.strftime("%Y-%m-%d"),
-                                                 queryset_all.filter(
-                                                     started_on__gte=yesterday_begin,
-                                                     started_on__lt=today_begin
-                                                     ).count()),
-                                             ("Build date range", 'daterange', 1, '', 'started_on'),
-                                             ]
-                                }
-                     },
-                    {'name': 'Completed on',
-                     'qhelp': "The date and time the build finished",
-                     'orderfield': _get_toggle_order(request, "completed_on", True),
-                     'ordericon':_get_toggle_order_icon(request, "completed_on"),
-                     'orderkey' : 'completed_on',
-                     'filter' : {'class' : 'completed_on',
-                                 'label': 'Show:',
-                                 'options' : [
-                                             ("Today's builds" , 'completed_on__gte:'+today_begin.strftime("%Y-%m-%d"), queryset_all.filter(completed_on__gte=today_begin).count()),
-                                             ("Yesterday's builds",
-                                                 'completed_on__gte!completed_on__lt:'
-                                                     +yesterday_begin.strftime("%Y-%m-%d")+'!'
-                                                     +today_begin.strftime("%Y-%m-%d"),
-                                                 queryset_all.filter(
-                                                     completed_on__gte=yesterday_begin,
-                                                     completed_on__lt=today_begin
-                                                     ).count()),
-                                             ("Build date range", 'daterange', 1, '', 'completed_on'),
-                                             ]
-                                }
-                    },
-                    {'name': 'Failed tasks', 'clclass': 'failed_tasks',                # specifing a clclass will enable the checkbox
-                     'qhelp': "How many tasks failed during the build",
-                     'filter' : {'class' : 'failed_tasks',
-                                 'label': 'Show:',
-                                 'options' : [
-                                             ('Builds with failed tasks', 'task_build__outcome:4', queryset_with_search.filter(task_build__outcome=4).count()),
-                                             ('Builds without failed tasks', 'task_build__outcome:NOT4', queryset_with_search.filter(~Q(task_build__outcome=4)).count()),
-                                             ]
-                                }
-                    },
-                    {'name': 'Errors', 'clclass': 'errors_no',
-                     'qhelp': "How many errors were encountered during the build (if any)",
-                     # Comment out sorting and filter until YOCTO #8131 is fixed
-                     #'orderfield': _get_toggle_order(request, "errors_no", True),
-                     #'ordericon':_get_toggle_order_icon(request, "errors_no"),
-                     #'orderkey' : 'errors_no',
-                     #'filter' : {'class' : 'errors_no',
-                     #            'label': 'Show:',
-                     #            'options' : [
-                     #                        ('Builds with errors', 'errors_no__gte:1', queryset_with_search.filter(errors_no__gte=1).count()),
-                     #                        ('Builds without errors', 'errors_no:0', queryset_with_search.filter(errors_no=0).count()),
-                     #                        ]
-                     #           }
-                    },
-                    {'name': 'Warnings', 'clclass': 'warnings_no',
-                     'qhelp': "How many warnings were encountered during the build (if any)",
-                     # Comment out sorting and filter until YOCTO #8131 is fixed
-                     #'orderfield': _get_toggle_order(request, "warnings_no", True),
-                     #'ordericon':_get_toggle_order_icon(request, "warnings_no"),
-                     #'orderkey' : 'warnings_no',
-                     #'filter' : {'class' : 'warnings_no',
-                     #            'label': 'Show:',
-                     #            'options' : [
-                     #                        ('Builds with warnings','warnings_no__gte:1', queryset_with_search.filter(warnings_no__gte=1).count()),
-                     #                        ('Builds without warnings','warnings_no:0', queryset_with_search.filter(warnings_no=0).count()),
-                     #                        ]
-                     #           }
-                    },
-                    {'name': 'Time', 'clclass': 'time', 'hidden' : 1,
-                     'qhelp': "How long it took the build to finish",
-                     # Comment out sorting until YOCTO #8131 is fixed
-                     #'orderfield': _get_toggle_order(request, "timespent", True),
-                     #'ordericon':_get_toggle_order_icon(request, "timespent"),
-                     #'orderkey' : 'timespent',
-                    },
-                    {'name': 'Image files', 'clclass': 'output',
-                     'qhelp': "The root file system types produced by the build. You can find them in your <code>/build/tmp/deploy/images/</code> directory",
-                        # TODO: compute image fstypes from Target_Image_File
-                    }
-                    ]
-                }
-
-        # merge daterange values
-        context.update(context_date)
-        return context, pagesize, orderby
-    """
-
-
-
     # new project
     def newproject(request):
         template = "newproject.html"
@@ -2829,64 +2576,6 @@ if True:
 
         return context
 
-    # WARNING _build_list_helper() may raise a RedirectException, which
-    # will set the GET parameters and redirect back to the
-    # all-builds or projectbuilds page as appropriate;
-    # TODO don't use exceptions to control program flow
-    """
-    def projectbuilds(request, pid):
-        if request.method == "POST":
-            # process any build request
-
-            if 'buildCancel' in request.POST:
-                for i in request.POST['buildCancel'].strip().split(" "):
-                    try:
-                        br = BuildRequest.objects.select_for_update().get(project = prj, pk = i, state__lte = BuildRequest.REQ_QUEUED)
-                        br.state = BuildRequest.REQ_DELETED
-                        br.save()
-                    except BuildRequest.DoesNotExist:
-                        pass
-
-            if 'buildDelete' in request.POST:
-                for i in request.POST['buildDelete'].strip().split(" "):
-                    try:
-                        BuildRequest.objects.select_for_update().get(project = prj, pk = i, state__lte = BuildRequest.REQ_DELETED).delete()
-                    except BuildRequest.DoesNotExist:
-                        pass
-
-            if 'targets' in request.POST:
-                ProjectTarget.objects.filter(project = prj).delete()
-                s = str(request.POST['targets'])
-                for t in s.translate(None, ";%|\"").split(" "):
-                    if ":" in t:
-                        target, task = t.split(":")
-                    else:
-                        target = t
-                        task = ""
-                    ProjectTarget.objects.create(project = prj,
-                                                 target = target,
-                                                 task = task)
-                prj.schedule_build()
-
-        queryset = Build.objects.filter(project_id = pid)
-
-        redirect_page = resolve(request.path_info).url_name
-
-        context, pagesize, orderby = _build_list_helper(request,
-                                                        queryset,
-                                                        redirect_page,
-                                                        pid)
-
-        context['project'] = prj
-        _set_parameters_values(pagesize, orderby, request)
-
-        # add the most recent builds for this project
-        context['mru'] = _get_latest_builds(prj)
-
-        return context
-    """
-
-
     def _file_name_for_artifact(b, artifact_type, artifact_id):
         file_name = None
         # Target_Image_File file_name
@@ -2962,122 +2651,3 @@ if True:
                 'build' : Build.objects.get(pk = build_id),
             }
             return render(request, "unavailable_artifact.html", context)
-
-    """
-    @_template_renderer("projects.html")
-    def projects(request):
-        (pagesize, orderby) = _get_parameters_values(request, 10, 'updated:-')
-        mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby }
-        retval = _verify_parameters( request.GET, mandatory_parameters )
-        if retval:
-            raise RedirectException( 'all-projects', request.GET, mandatory_parameters )
-
-        queryset_all = Project.objects.all()
-
-        # annotate each project with its number of builds
-        queryset_all = queryset_all.annotate(num_builds=Count('build'))
-
-        # exclude the command line builds project if it has no builds
-        q_default_with_builds = Q(is_default=True) & Q(num_builds__gt=0)
-        queryset_all = queryset_all.filter(Q(is_default=False) |
-                                           q_default_with_builds)
-
-        # boilerplate code that takes a request for an object type and returns a queryset
-        # for that object type. copypasta for all needed table searches
-        (filter_string, search_term, ordering_string) = _search_tuple(request, Project)
-        queryset_with_search = _get_queryset(Project, queryset_all, None, search_term, ordering_string, '-updated')
-        queryset = _get_queryset(Project, queryset_all, filter_string, search_term, ordering_string, '-updated')
-
-        # retrieve the objects that will be displayed in the table; projects a paginator and gets a page range to display
-        project_info = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1))
-
-        # add fields needed in JSON dumps for API call support
-        for p in project_info.object_list:
-            p.id = p.pk
-            p.projectPageUrl = reverse('project', args=(p.id,))
-            p.layersTypeAheadUrl = reverse('xhr_layerstypeahead', args=(p.id,))
-            p.recipesTypeAheadUrl = reverse('xhr_recipestypeahead', args=(p.id,))
-            p.projectBuildsUrl = reverse('projectbuilds', args=(p.id,))
-
-        # build view-specific information; this is rendered specifically in the builds page, at the top of the page (i.e. Recent builds)
-        build_mru = _get_latest_builds()
-
-        # translate the project's build target strings
-        fstypes_map = {};
-        for project in project_info:
-            try:
-                targets = Target.objects.filter( build_id = project.get_last_build_id() )
-                comma = "";
-                extensions = "";
-                for t in targets:
-                    if ( not t.is_image ):
-                        continue
-                    tif = Target_Image_File.objects.filter( target_id = t.id )
-                    for i in tif:
-                        s=re.sub('.*tar.bz2', 'tar.bz2', i.file_name)
-                        if s == i.file_name:
-                            s=re.sub('.*\.', '', i.file_name)
-                        if None == re.search(s,extensions):
-                            extensions += comma + s
-                            comma = ", "
-                fstypes_map[project.id]=extensions
-            except (Target.DoesNotExist,IndexError):
-                fstypes_map[project.id]=project.get_last_imgfiles
-
-        context = {
-                'mru' : build_mru,
-
-                'objects' : project_info,
-                'objectname' : "projects",
-                'default_orderby' : 'id:-',
-                'search_term' : search_term,
-                'total_count' : queryset_with_search.count(),
-                'fstypes' : fstypes_map,
-                'build_FAILED' : Build.FAILED,
-                'build_SUCCEEDED' : Build.SUCCEEDED,
-                'tablecols': [
-                    {'name': 'Project',
-                    'orderfield': _get_toggle_order(request, "name"),
-                    'ordericon':_get_toggle_order_icon(request, "name"),
-                    'orderkey' : 'name',
-                    },
-                    {'name': 'Last activity on',
-                    'clclass': 'updated',
-                    'qhelp': "Shows the starting date and time of the last project build. If the project has no builds, it shows the date the project was created",
-                    'orderfield': _get_toggle_order(request, "updated", True),
-                    'ordericon':_get_toggle_order_icon(request, "updated"),
-                    'orderkey' : 'updated',
-                    },
-                    {'name': 'Release',
-                    'qhelp' : "The version of the build system used by the project",
-                    'orderfield': _get_toggle_order(request, "release__name"),
-                    'ordericon':_get_toggle_order_icon(request, "release__name"),
-                    'orderkey' : 'release__name',
-                    },
-                    {'name': 'Machine',
-                    'qhelp': "The hardware currently selected for the project",
-                    },
-                    {'name': 'Number of builds',
-                    'qhelp': "How many builds have been run for the project",
-                    },
-                    {'name': 'Last build outcome', 'clclass': 'loutcome',
-                    'qhelp': "Tells you if the last project build completed successfully or failed",
-                    },
-                    {'name': 'Recipe', 'clclass': 'ltarget',
-                    'qhelp': "The last recipe that was built in this project",
-                    },
-                    {'name': 'Errors', 'clclass': 'lerrors',
-                    'qhelp': "How many errors were encountered during the last project build (if any)",
-                    },
-                    {'name': 'Warnings', 'clclass': 'lwarnings',
-                    'qhelp': "How many warnigns were encountered during the last project build (if any)",
-                    },
-                    {'name': 'Image files', 'clclass': 'limagefiles', 'hidden': 1,
-                    'qhelp': "The root file system types produced by the last project build",
-                    },
-                    ]
-            }
-
-        _set_parameters_values(pagesize, orderby, request)
-        return context
-    """
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 22/23] toaster: tests: fix Django tests for new ToasterTable pages
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (21 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 21/23] toaster: toastergui: remove unused views and template code Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  2016-01-15 11:43 ` [review-request][PATCH 23/23] toaster: toastergui: code formatting and clean-up Elliot Smith
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

The Django command-line tests can no longer test the content
of the projects/, builds/ and projectbuilds/ pages, as
ToasterTable pages are populated by JavaScript.

Fix/remove affected tests by converting them to tests on the
JSON returned by the ToasterTable.

[YOCTO #8738]

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/toastergui/tests.py | 265 ++++++++++++++++++++++----------
 1 file changed, 180 insertions(+), 85 deletions(-)

diff --git a/bitbake/lib/toaster/toastergui/tests.py b/bitbake/lib/toaster/toastergui/tests.py
index c927fe1..0987721 100644
--- a/bitbake/lib/toaster/toastergui/tests.py
+++ b/bitbake/lib/toaster/toastergui/tests.py
@@ -38,11 +38,14 @@ import toastergui
 
 from toastergui.tables import SoftwareRecipesTable
 import json
+from datetime import timedelta
 from bs4 import BeautifulSoup
 import re
 import string
+import json
 
 PROJECT_NAME = "test project"
+PROJECT_NAME2 = "test project 2"
 CLI_BUILDS_PROJECT_NAME = 'Command line builds'
 
 class ViewTests(TestCase):
@@ -54,14 +57,46 @@ class ViewTests(TestCase):
         release = Release.objects.create(name="test release",
                                          branch_name="master",
                                          bitbake_version=bbv)
+        release2 = Release.objects.create(name="test release 2",
+                                          branch_name="master",
+                                          bitbake_version=bbv)
+
         self.project = Project.objects.create_project(name=PROJECT_NAME,
                                                       release=release)
+
+        self.project2 = Project.objects.create_project(name=PROJECT_NAME2,
+                                                       release=release2)
+
         now = timezone.now()
+        later = now + timedelta(days=1)
 
         build = Build.objects.create(project=self.project,
                                      started_on=now,
                                      completed_on=now)
 
+        # for testing BuildsTable
+        build1 = Build.objects.create(project=self.project,
+                                      started_on=now,
+                                      completed_on=now,
+                                      outcome=Build.SUCCEEDED,
+                                      machine="raspberrypi2")
+
+        Build.objects.create(project=self.project,
+                             started_on=later,
+                             completed_on=later,
+                             outcome=Build.FAILED,
+                             machine="qemux86")
+
+        Build.objects.create(project=self.project2,
+                             started_on=later,
+                             completed_on=later,
+                             outcome=Build.SUCCEEDED,
+                             machine="qemux86")
+
+        # to test sorting by errors and warnings in BuildsTable
+        LogMessage.objects.create(build=build1, level=LogMessage.WARNING)
+        LogMessage.objects.create(build=build1, level=LogMessage.ERROR)
+
         layersrc = LayerSource.objects.create(sourcetype=LayerSource.TYPE_IMPORTED)
         self.priority = ReleaseLayerSourcePriority.objects.create(release=release,
                                                                   layer_source=layersrc)
@@ -172,8 +207,7 @@ class ViewTests(TestCase):
         response = self.client.get(reverse('all-projects'), follow=True)
         self.assertEqual(response.status_code, 200)
         self.assertTrue(response['Content-Type'].startswith('text/html'))
-        self.assertTemplateUsed(response, "projects.html")
-        self.assertTrue(PROJECT_NAME in response.content)
+        self.assertTemplateUsed(response, "projects-toastertable.html")
 
     def test_get_json_call_returns_json(self):
         """Test for all projects output in json format"""
@@ -191,13 +225,6 @@ class ViewTests(TestCase):
         self.assertTrue(PROJECT_NAME in [x["name"] for x in data["rows"]])
         self.assertTrue("id" in data["rows"][0])
 
-        self.assertEqual(sorted(data["rows"][0]),
-                         ['bitbake_version_id', 'created', 'id',
-                          'is_default', 'layersTypeAheadUrl', 'name',
-                          'num_builds', 'projectBuildsUrl', 'projectPageUrl',
-                          'recipesTypeAheadUrl', 'release_id',
-                          'short_description', 'updated', 'user_id'])
-
     def test_typeaheads(self):
         """Test typeahead ReST API"""
         layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,))
@@ -450,7 +477,7 @@ class ViewTests(TestCase):
             all_data = get_data(table)
 
             self.assertTrue(len(all_data['rows']) > 1,
-                            "Cannot test on a table with < 1 row")
+                            "Cannot test on the table %s with < 1 row" % name)
 
             if table.default_orderby:
                 row_one = all_data['rows'][0][table.default_orderby.strip("-")]
@@ -512,16 +539,20 @@ class ViewTests(TestCase):
                         # This is the name of the filter:action
                         # e.g. project_filter:not_in_project
                         filter_string = "%s:%s" % (column['filter_name'],
-                                                   filter_action['name'])
+                                                   filter_action['action_name'])
                         # Now get the data with the filter applied
                         filtered_data = get_data(table_cls(),
                                                  {"filter" : filter_string})
-                        self.assertEqual(len(filtered_data['rows']),
-                                         int(filter_action['count']),
-                                         "We added a table filter for %s but "
-                                         "the number of rows returned was not "
-                                         "what the filter info said there "
-                                         "would be" % name)
+
+                        # date range filter actions can't specify the
+                        # number of results they return, so their count is 0
+                        if filter_action['count'] != None:
+                            self.assertEqual(len(filtered_data['rows']),
+                                             int(filter_action['count']),
+                                             "We added a table filter for %s but "
+                                             "the number of rows returned was not "
+                                             "what the filter info said there "
+                                             "would be" % name)
 
 
             # Test search functionality on the table
@@ -673,6 +704,10 @@ class AllProjectsPageTests(TestCase):
                                                      value=self.MACHINE_NAME)
         project_var.save()
 
+    def _get_row_for_project(self, data, project_id):
+        """ Get the object representing the table data for a project """
+        return [row for row in data['rows'] if row['id'] == project_id][0]
+
     def test_default_project_hidden(self):
         """ The default project should be hidden if it has no builds """
         params = {"count": 10, "orderby": "updated:-", "page": 1}
@@ -688,11 +723,20 @@ class AllProjectsPageTests(TestCase):
         self._add_build_to_default_project()
 
         params = {"count": 10, "orderby": "updated:-", "page": 1}
-        response = self.client.get(reverse('all-projects'), params)
 
-        self.assertTrue('tr class="data"' in response.content,
-                        'should be a project row in the page')
-        self.assertTrue(CLI_BUILDS_PROJECT_NAME in response.content,
+        response = self.client.get(
+            reverse('all-projects'),
+            {'format': 'json'},
+            params
+        )
+
+        data = json.loads(response.content)
+
+        # find the row for the default project
+        default_project_row = self._get_row_for_project(data, self.default_project.id)
+
+        # check its name template has the correct text
+        self.assertEqual(default_project_row['name'], CLI_BUILDS_PROJECT_NAME,
                         'default project "cli builds" should be in page')
 
     def test_default_project_release(self):
@@ -706,24 +750,32 @@ class AllProjectsPageTests(TestCase):
         # another project to test, which should show release
         self._add_non_default_project()
 
-        response = self.client.get(reverse('all-projects'), follow=True)
-        soup = BeautifulSoup(response.content)
+        response = self.client.get(
+            reverse('all-projects'),
+            {'format': 'json'},
+            follow=True
+        )
 
-        # check the release cell for the default project
-        attrs = {'data-project': str(self.default_project.id)}
-        rows = soup.find_all('tr', attrs=attrs)
-        self.assertEqual(len(rows), 1, 'should be one row for default project')
-        cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
-        self.assertEqual(len(cells), 1, 'should be one release cell')
-        text = cells[0].select('span.muted')[0].text
+        data = json.loads(response.content)
+
+        # used to find the correct span in the template output
+        attrs = {'data-project-field': 'release'}
+
+        # find the row for the default project
+        default_project_row = self._get_row_for_project(data, self.default_project.id)
+
+        # check the release text for the default project
+        soup = BeautifulSoup(default_project_row['static:release'])
+        text = soup.find('span', attrs=attrs).select('span.muted')[0].text
         self.assertEqual(text, 'Not applicable',
                          'release should be not applicable for default project')
 
+        # find the row for the default project
+        other_project_row = self._get_row_for_project(data, self.project.id)
+
         # check the link in the release cell for the other project
-        attrs = {'data-project': str(self.project.id)}
-        rows = soup.find_all('tr', attrs=attrs)
-        cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
-        text = cells[0].select('a')[0].text
+        soup = BeautifulSoup(other_project_row['static:release'])
+        text = soup.find('span', attrs=attrs).select('a')[0].text.strip()
         self.assertEqual(text, self.release.name,
                          'release name should be shown for non-default project')
 
@@ -738,24 +790,32 @@ class AllProjectsPageTests(TestCase):
         # another project to test, which should show machine
         self._add_non_default_project()
 
-        response = self.client.get(reverse('all-projects'), follow=True)
-        soup = BeautifulSoup(response.content)
+        response = self.client.get(
+            reverse('all-projects'),
+            {'format': 'json'},
+            follow=True
+        )
+
+        data = json.loads(response.content)
+
+        # used to find the correct span in the template output
+        attrs = {'data-project-field': 'machine'}
+
+        # find the row for the default project
+        default_project_row = self._get_row_for_project(data, self.default_project.id)
 
         # check the machine cell for the default project
-        attrs = {'data-project': str(self.default_project.id)}
-        rows = soup.find_all('tr', attrs=attrs)
-        self.assertEqual(len(rows), 1, 'should be one row for default project')
-        cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
-        self.assertEqual(len(cells), 1, 'should be one machine cell')
-        text = cells[0].select('span.muted')[0].text
+        soup = BeautifulSoup(default_project_row['static:machine'])
+        text = soup.find('span', attrs=attrs).select('span.muted')[0].text.strip()
         self.assertEqual(text, 'Not applicable',
-                         'machine should be not applicable for default project')
+            'machine should be not applicable for default project')
+
+        # find the row for the default project
+        other_project_row = self._get_row_for_project(data, self.project.id)
 
         # check the link in the machine cell for the other project
-        attrs = {'data-project': str(self.project.id)}
-        rows = soup.find_all('tr', attrs=attrs)
-        cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
-        text = cells[0].select('a')[0].text
+        soup = BeautifulSoup(other_project_row['static:machine'])
+        text = soup.find('span', attrs=attrs).find('a').text.strip()
         self.assertEqual(text, self.MACHINE_NAME,
                          'machine name should be shown for non-default project')
 
@@ -769,24 +829,33 @@ class AllProjectsPageTests(TestCase):
         # need a build, otherwise project doesn't display at all
         self._add_build_to_default_project()
 
-        # another project to test, which should show machine
+        # another project to test
         self._add_non_default_project()
 
-        response = self.client.get(reverse('all-projects'), follow=True)
-        soup = BeautifulSoup(response.content)
+        response = self.client.get(
+            reverse('all-projects'),
+            {'format': 'json'},
+            follow=True
+        )
+
+        data = json.loads(response.content)
 
-        # link for default project
-        row = soup.find('tr', attrs={'data-project': self.default_project.id})
-        cell = row.find('td', attrs={'data-project-field': 'name'})
+        # find the row for the default project
+        default_project_row = self._get_row_for_project(data, self.default_project.id)
+
+        # check the link on the name field
+        soup = BeautifulSoup(default_project_row['static:name'])
         expected_url = reverse('projectbuilds', args=(self.default_project.id,))
-        self.assertEqual(cell.find('a')['href'], expected_url,
+        self.assertEqual(soup.find('a')['href'], expected_url,
                          'link on default project name should point to builds')
 
-        # link for other project
-        row = soup.find('tr', attrs={'data-project': self.project.id})
-        cell = row.find('td', attrs={'data-project-field': 'name'})
+        # find the row for the other project
+        other_project_row = self._get_row_for_project(data, self.project.id)
+
+        # check the link for the other project
+        soup = BeautifulSoup(other_project_row['static:name'])
         expected_url = reverse('project', args=(self.project.id,))
-        self.assertEqual(cell.find('a')['href'], expected_url,
+        self.assertEqual(soup.find('a')['href'], expected_url,
                          'link on project name should point to configuration')
 
 class ProjectBuildsPageTests(TestCase):
@@ -846,9 +915,9 @@ class ProjectBuildsPageTests(TestCase):
     def _get_rows_for_project(self, project_id):
         """ Helper to retrieve HTML rows for a project """
         url = reverse("projectbuilds", args=(project_id,))
-        response = self.client.get(url, follow=True)
-        soup = BeautifulSoup(response.content)
-        return soup.select('tr[class="data"]')
+        response = self.client.get(url, {'format': 'json'}, follow=True)
+        data = json.loads(response.content)
+        return data['rows']
 
     def test_show_builds_for_project(self):
         """ Builds for a project should be displayed """
@@ -889,10 +958,14 @@ class ProjectBuildsPageTests(TestCase):
         """ Task should be shown as suffix on build name """
         build = Build.objects.create(**self.project1_build_success)
         Target.objects.create(build=build, target='bash', task='clean')
-        url = reverse("projectbuilds", args=(self.project1.id,))
-        response = self.client.get(url, follow=True)
-        result = re.findall('^ +bash:clean$', response.content, re.MULTILINE)
-        self.assertEqual(len(result), 2)
+
+        url = reverse('projectbuilds', args=(self.project1.id,))
+        response = self.client.get(url, {'format': 'json'}, follow=True)
+        data = json.loads(response.content)
+        cell = data['rows'][0]['static:target']
+
+        result = re.findall('^ +bash:clean', cell, re.MULTILINE)
+        self.assertEqual(len(result), 1)
 
     def test_cli_builds_hides_tabs(self):
         """
@@ -952,32 +1025,46 @@ class AllBuildsPageTests(TestCase):
             "outcome": Build.SUCCEEDED
         }
 
+    def _get_row_for_build(self, data, build_id):
+        """ Get the object representing the table data for a project """
+        return [row for row in data['rows']
+                    if row['id'] == build_id][0]
+
     def test_show_tasks_in_allbuilds(self):
         """ Task should be shown as suffix on build name """
         build = Build.objects.create(**self.project1_build_success)
         Target.objects.create(build=build, target='bash', task='clean')
+
         url = reverse('all-builds')
-        response = self.client.get(url, follow=True)
-        result = re.findall('bash:clean', response.content, re.MULTILINE)
-        self.assertEqual(len(result), 3)
+        response = self.client.get(url, {'format': 'json'}, follow=True)
+        data = json.loads(response.content)
+        cell = data['rows'][0]['static:target']
 
-    def test_no_run_again_for_cli_build(self):
-        """ "Run again" button should not be shown for command-line builds """
-        build = Build.objects.create(**self.default_project_build_success)
+        result = re.findall('bash:clean', cell, re.MULTILINE)
+        self.assertEqual(len(result), 1)
+
+    def test_run_again(self):
+        """
+        "Run again" button should not be shown for command-line builds,
+        but should be shown for other builds
+        """
+        build1 = Build.objects.create(**self.project1_build_success)
+        default_build = Build.objects.create(**self.default_project_build_success)
         url = reverse('all-builds')
         response = self.client.get(url, follow=True)
         soup = BeautifulSoup(response.content)
 
-        attrs = {'data-latest-build-result': build.id}
-        result = soup.find('div', attrs=attrs)
-
         # shouldn't see a run again button for command-line builds
+        attrs = {'data-latest-build-result': default_build.id}
+        result = soup.find('div', attrs=attrs)
         run_again_button = result.select('button')
         self.assertEqual(len(run_again_button), 0)
 
-        # should see a help icon for command-line builds
-        help_icon = result.select('i.get-help-green')
-        self.assertEqual(len(help_icon), 1)
+        # should see a run again button for non-command-line builds
+        attrs = {'data-latest-build-result': build1.id}
+        result = soup.find('div', attrs=attrs)
+        run_again_button = result.select('button')
+        self.assertEqual(len(run_again_button), 1)
 
     def test_tooltips_on_project_name(self):
         """
@@ -989,20 +1076,28 @@ class AllBuildsPageTests(TestCase):
         default_build = Build.objects.create(**self.default_project_build_success)
 
         url = reverse('all-builds')
-        response = self.client.get(url, follow=True)
-        soup = BeautifulSoup(response.content)
+        response = self.client.get(url, {'format': 'json'}, follow=True)
+        data = json.loads(response.content)
+
+        # get the data row for the non-command-line builds project
+        other_project_row = self._get_row_for_build(data, build1.id)
+
+        # make sure there is some HTML
+        soup = BeautifulSoup(other_project_row['static:project'])
+        self.assertEqual(len(soup.select('a')), 1,
+                         'should be a project name link')
 
         # no help icon on non-default project name
-        result = soup.find('tr', attrs={'data-table-build-result': build1.id})
-        name = result.select('td.project-name')[0]
-        icons = name.select('i.get-help')
+        icons = soup.select('i.get-help')
         self.assertEqual(len(icons), 0,
                          'should not be a help icon for non-cli builds name')
 
+        # get the data row for the command-line builds project
+        default_project_row = self._get_row_for_build(data, default_build.id)
+
         # help icon on default project name
-        result = soup.find('tr', attrs={'data-table-build-result': default_build.id})
-        name = result.select('td.project-name')[0]
-        icons = name.select('i.get-help')
+        soup = BeautifulSoup(default_project_row['static:project'])
+        icons = soup.select('i.get-help')
         self.assertEqual(len(icons), 1,
                          'should be a help icon for cli builds name')
 
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [review-request][PATCH 23/23] toaster: toastergui: code formatting and clean-up
  2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
                   ` (22 preceding siblings ...)
  2016-01-15 11:43 ` [review-request][PATCH 22/23] toaster: tests: fix Django tests for new ToasterTable pages Elliot Smith
@ 2016-01-15 11:43 ` Elliot Smith
  23 siblings, 0 replies; 25+ messages in thread
From: Elliot Smith @ 2016-01-15 11:43 UTC (permalink / raw)
  To: toaster

Minor fixes to code formatting and small improvements from
code review.

Signed-off-by: Elliot Smith <elliot.smith@intel.com>
---
 bitbake/lib/toaster/orm/models.py        | 12 +++---------
 bitbake/lib/toaster/toastergui/tables.py |  4 +++-
 bitbake/lib/toaster/toastergui/views.py  |  5 ++---
 3 files changed, 8 insertions(+), 13 deletions(-)

diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py
index 3dc4d6d..ac2aa9e 100644
--- a/bitbake/lib/toaster/orm/models.py
+++ b/bitbake/lib/toaster/orm/models.py
@@ -477,15 +477,9 @@ class Build(models.Model):
         targets in this build
         """
         targets = self.target_set.all()
-        target_labels = []
-        target_label = None
-
-        for target in targets:
-            target_label = target.target
-            if target.task:
-                target_label = target_label + ':' + target.task
-            target_labels.append(target_label)
-
+        target_labels = [target.target +
+                         (':' + target.task if target.task else '')
+                         for target in targets]
         target_labels.sort()
 
         return target_labels
diff --git a/bitbake/lib/toaster/toastergui/tables.py b/bitbake/lib/toaster/toastergui/tables.py
index 2279731..e9f1fdc 100644
--- a/bitbake/lib/toaster/toastergui/tables.py
+++ b/bitbake/lib/toaster/toastergui/tables.py
@@ -688,7 +688,9 @@ class ProjectsTable(ToasterTable):
 
         self.queryset = queryset
 
-    # columns: last activity on (updated) - DEFAULT, project (name), release, machine, number of builds, last build outcome, recipe (name),  errors, warnings, image files
+    # columns: last activity on (updated) - DEFAULT, project (name), release,
+    # machine, number of builds, last build outcome, recipe (name),  errors,
+    # warnings, image files
     def setup_columns(self, *args, **kwargs):
         name_template = '''
         {% load project_url_tag %}
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 4f7b50f..59e16b2 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -462,7 +462,7 @@ def builddashboard( request, build_id ):
     for t in tgts:
         elem = { }
         elem[ 'target' ] = t
-        if ( t.is_image ):
+        if t.is_image:
             hasImages = True
         npkg = 0
         pkgsz = 0
@@ -481,8 +481,7 @@ def builddashboard( request, build_id ):
                 ndx = 0;
             f = i.file_name[ ndx + 1: ]
             imageFiles.append({ 'id': i.id, 'path': f, 'size' : i.file_size })
-        if ( t.is_image and
-             (( len( imageFiles ) <= 0 ) or ( len( t.license_manifest_path ) <= 0 ))):
+        if t.is_image and (len(imageFiles) <= 0 or len(t.license_manifest_path) <= 0):
             targetHasNoImages = True
         elem[ 'imageFiles' ] = imageFiles
         elem[ 'targetHasNoImages' ] = targetHasNoImages
-- 
Elliot Smith
Software Engineer
Intel OTC

---------------------------------------------------------------------
Intel Corporation (UK) Limited
Registered No. 1134945 (England)
Registered Office: Pipers Way, Swindon SN3 1RJ
VAT No: 860 2173 47

This e-mail and any attachments may contain confidential material for
the sole use of the intended recipient(s). Any review or distribution
by others is strictly prohibited. If you are not the intended
recipient, please contact the sender and delete all copies.



^ permalink raw reply related	[flat|nested] 25+ messages in thread

end of thread, other threads:[~2016-01-15 13:04 UTC | newest]

Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2016-01-15 11:43 [review-request][PATCH 00/23][V2] Convert projects and builds pages to ToasterTable Elliot Smith
2016-01-15 11:07 ` Ed Bartosh
2016-01-15 11:43 ` [review-request][PATCH 01/23] toaster: toastergui: use ToasterTable for projects page Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 02/23] toaster: move image file suffix list to model Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 03/23] toaster: check inferred file suffixes against list of known types Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 04/23] toaster: toastergui: switch projects/ view to ToasterTable Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 05/23] toaster: toastergui: use event delegates for hover help elements Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 06/23] toaster: toastergui: convert all builds page to ToasterTable Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 07/23] toaster: toastergui: refactor ToasterTable filtering Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 08/23] toaster: toastergui: switch off filter highlights when inactive Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 09/23] toaster: toastergui: show recent builds on all builds page Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 10/23] toaster: toastergui: implement date range filters for builds Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 11/23] toaster: toastergui: implement "today" and "yesterday" filters Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 12/23] toaster: toastergui: convert project builds page to ToasterTable Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 13/23] toaster: toastergui: don't hide all elements with .col class Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 14/23] toaster: toastergui: ensure filter_value updates Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 15/23] toaster: toastergui: streamline construction of filter objects Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 16/23] toaster: toastergui: serialise decimals correctly Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 17/23] toaster: toastergui: set default visible and hideable columns Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 18/23] toaster: toastergui: mute label for filter actions with no records Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 19/23] toaster: toastergui: make "Apply" button state depend on filter range Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 20/23] toaster: toastergui: fix error and warning counts for builds Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 21/23] toaster: toastergui: remove unused views and template code Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 22/23] toaster: tests: fix Django tests for new ToasterTable pages Elliot Smith
2016-01-15 11:43 ` [review-request][PATCH 23/23] toaster: toastergui: code formatting and clean-up Elliot Smith

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.