← Back to team overview

launchpad-reviewers team mailing list archive

lp:~allenap/launchpad/localpackagediffs-filter-by-package-set-bug-809786-overlay into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/localpackagediffs-filter-by-package-set-bug-809786-overlay into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/launchpad/localpackagediffs-filter-by-package-set-bug-809786-overlay/+merge/68647

This adds an overlay into DistroSeries:+localpackagediffs that allows
the user to only show packages which are within a given set of
packagesets.

- When the new JavaScript runs it changes the "Package-sets" table
  header into a clickable js-action that pops up a form overlay
  containing a single widget.

- When this overlay is dismissed, nothing happens.

- When this overlay is okayed, the search form (above the table) is
  changed to include field.packageset fields, then the form is
  submitted.

It's somewhat/fairly hacky right now, and I have firm plans to improve
this, but I want to get this out there and seek feedback. The page
it's on it feature flagged so we're not making this generally
available yet.

Note: the changes lib/lp/app/javascript/formoverlay/formoverlay.js are
all lint fixes. I happened to be passing and couldn't resist :-/

-- 
https://code.launchpad.net/~allenap/launchpad/localpackagediffs-filter-by-package-set-bug-809786-overlay/+merge/68647
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/localpackagediffs-filter-by-package-set-bug-809786-overlay into lp:launchpad.
=== modified file 'lib/lp/app/javascript/formoverlay/formoverlay.js'
--- lib/lp/app/javascript/formoverlay/formoverlay.js	2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/formoverlay/formoverlay.js	2011-07-21 09:36:36 +0000
@@ -84,8 +84,8 @@
  * @type string
  * @static
  */
-FormOverlay.FORM_HEADER_TEMPLATE = '<div class="' + FormOverlay.C_FORM_HEADER +
-    '" />';
+FormOverlay.FORM_HEADER_TEMPLATE =
+    '<div class="' + FormOverlay.C_FORM_HEADER + '" />';
 
 /**
  * Static html template to use for creating the form element.
@@ -307,7 +307,8 @@
         body_node.appendChild(this.form_header_node);
         body_node.appendChild(this.form_node);
 
-        this.setStdModContent(Y.WidgetStdMod.BODY, body_node, Y.WidgetStdMod.REPLACE);
+        this.setStdModContent(
+            Y.WidgetStdMod.BODY, body_node, Y.WidgetStdMod.REPLACE);
     },
 
     /**
@@ -405,7 +406,8 @@
                                 ++select_idx) {
                                 option = elem.options[select_idx];
                                 if (option.selected) {
-                                    addData(elem.name, getOptionValue(option));
+                                    addData(elem.name,
+                                            getOptionValue(option));
                                 }
                             }
                         }
@@ -443,7 +445,7 @@
      * @method showError
      */
     showError: function(error_msgs){
-        if (typeof(error_msgs) == "string"){
+        if (Y.Lang.isString(error_msgs)) {
             error_msgs = [error_msgs];
         }
         var error_html = "The following errors were encountered: <ul>";
@@ -494,8 +496,8 @@
         }
         var cfg = {
             on: {success: on_success, failure: on_failure},
-            arguments: this
-            }
+            "arguments": this
+            };
         io_provider.io(url, cfg);
     }
 });

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py	2011-07-18 18:28:27 +0000
+++ lib/lp/registry/browser/distroseries.py	2011-07-21 09:36:36 +0000
@@ -93,9 +93,7 @@
     DistroSeriesDifferenceStatus,
     DistroSeriesDifferenceType,
     )
-from lp.registry.interfaces.distroseries import (
-    IDistroSeries,
-    )
+from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.registry.interfaces.distroseriesdifference import (
     IDistroSeriesDifferenceSource,
     )
@@ -112,6 +110,7 @@
     IDistroSeriesDifferenceJobSource,
     )
 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
+from lp.soyuz.interfaces.packageset import IPackagesetSet
 from lp.soyuz.interfaces.queue import IPackageUploadSet
 from lp.soyuz.model.queue import PackageUploadQueue
 from lp.translations.browser.distroseries import (
@@ -1028,6 +1027,20 @@
             return None
 
     @property
+    def specified_packagesets_filter(self):
+        """If specified, return Packagesets given in the GET form data."""
+        packageset_ids = (
+            self.request.query_string_params.get("field.packageset", []))
+        packageset_ids = set(
+            int(packageset_id) for packageset_id in packageset_ids
+            if packageset_id.isdigit())
+        packagesets = getUtility(IPackagesetSet).getBySeries(self.context)
+        packagesets = set(
+            packageset for packageset in packagesets
+            if packageset.id in packageset_ids)
+        return None if len(packagesets) == 0 else packagesets
+
+    @property
     def specified_package_type(self):
         """If specified, return the package type filter from the GET form
         data.
@@ -1059,7 +1072,8 @@
             IDistroSeriesDifferenceSource).getForDistroSeries(
                 self.context, difference_type=self.differences_type,
                 name_filter=self.specified_name_filter,
-                status=status, child_version_higher=child_version_higher)
+                status=status, child_version_higher=child_version_higher,
+                packagesets=self.specified_packagesets_filter)
         return BatchNavigator(differences, self.request)
 
     @cachedproperty

=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py	2011-07-07 20:07:51 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py	2011-07-21 09:36:36 +0000
@@ -596,10 +596,11 @@
     def test_submit_sets_previous_series(self):
         # Creating a new series when one already exists should set the
         # previous_series.
-        old_series = self.factory.makeDistroSeries(self.distribution,
-                                                   version='11.10')
-        older_series = self.factory.makeDistroSeries(self.distribution,
-                                                     version='11.04')
+        old_series = self.factory.makeDistroSeries(
+            self.distribution, version='11.10')
+        # A yet older series.
+        self.factory.makeDistroSeries(
+            self.distribution, version='11.04')
         old_time = utc_now() - timedelta(days=5)
         removeSecurityProxy(old_series).datereleased = old_time
         distroseries = self.createNewDistroseries()
@@ -2079,6 +2080,59 @@
             'http://127.0.0.1?start=1&batch=1',
             view.action_url)
 
+    def test_specified_packagesets_filter_none_specified(self):
+        # specified_packagesets_filter is None when there are no
+        # field.packageset parameters in the query.
+        set_derived_series_ui_feature_flag(self)
+        dsd = self.factory.makeDistroSeriesDifference()
+        person = dsd.derived_series.owner
+        with person_logged_in(person):
+            view = create_initialized_view(
+                dsd.derived_series, '+localpackagediffs', method='GET',
+                query_string='')
+            self.assertIs(None, view.specified_packagesets_filter)
+
+    def test_specified_packagesets_filter_specified(self):
+        # specified_packagesets_filter returns a collection of Packagesets
+        # when there are field.packageset query parameters.
+        set_derived_series_ui_feature_flag(self)
+        dsd = self.factory.makeDistroSeriesDifference()
+        person = dsd.derived_series.owner
+        packageset1 = self.factory.makePackageset(
+            distroseries=dsd.derived_series)
+        packageset2 = self.factory.makePackageset(
+            distroseries=dsd.derived_series)
+        with person_logged_in(person):
+            view = create_initialized_view(
+                dsd.derived_series, '+localpackagediffs', method='GET',
+                query_string='field.packageset=%d&field.packageset=%d' % (
+                    packageset1.id, packageset2.id))
+            self.assertContentEqual(
+                [packageset1, packageset2],
+                view.specified_packagesets_filter)
+
+    def test_search_for_packagesets(self):
+        # If packagesets are supplied in the query the resulting batch will
+        # only contain packages in the given packagesets.
+        set_derived_series_ui_feature_flag(self)
+        dsd = self.factory.makeDistroSeriesDifference()
+        person = dsd.derived_series.owner
+        packageset = self.factory.makePackageset(
+            owner=person, distroseries=dsd.derived_series)
+        # The package is not in the packageset so the batch will be empty.
+        with person_logged_in(person):
+            view = create_initialized_view(
+                dsd.derived_series, '+localpackagediffs', method='GET',
+                query_string='field.packageset=%d' % packageset.id)
+            self.assertEqual(0, len(view.cached_differences.batch))
+            # The batch will contain the package once it has been added to the
+            # packageset.
+            packageset.add((dsd.source_package_name,))
+            view = create_initialized_view(
+                dsd.derived_series, '+localpackagediffs', method='GET',
+                query_string='field.packageset=%d' % packageset.id)
+            self.assertEqual(1, len(view.cached_differences.batch))
+
 
 class TestDistroSeriesNeedsPackagesView(TestCaseWithFactory):
     """Test the distroseries +needs-packaging view."""

=== added file 'lib/lp/registry/javascript/distroseries/differences.js'
--- lib/lp/registry/javascript/distroseries/differences.js	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/javascript/distroseries/differences.js	2011-07-21 09:36:36 +0000
@@ -0,0 +1,181 @@
+/**
+ * Copyright 2011 Canonical Ltd. This software is licensed under the
+ * GNU Affero General Public License version 3 (see the file LICENSE).
+ *
+ * DistroSeries Initialization.
+ *
+ * @module lp.registry.distroseries
+ * @submodule differences
+ */
+
+YUI.add('lp.registry.distroseries.differences', function(Y) {
+
+Y.log('loading lp.registry.distroseries.differences');
+
+var namespace = Y.namespace('lp.registry.distroseries.differences'),
+    formwidgets = Y.lp.app.formwidgets,
+    widgets = Y.lp.registry.distroseries.widgets;
+
+var PACKAGESET_FIELD = "field.packageset",
+    PACKAGESET_FIELD_SELECTOR = "input[name=" + PACKAGESET_FIELD + "]";
+
+
+/**
+ * Return an array of the packageset IDs that are configured in the
+ * current window's query string.
+ *
+ * @param {String} qs The query string, typically obtained from
+ *     window.location.search.
+ */
+var get_packagesets_in_query = function(qs) {
+    var query = Y.QueryString.parse(qs.replace(/^[?]/, ""));
+    /* Y.QueryString.parse() tries to be helpful and convert
+       numeral strings into numbers... but we don't want that,
+       so we have to convert back again. */
+    var packagesets = query[PACKAGESET_FIELD];
+    var n2s = function(n) { return n.toString(10); };
+    /* Y.QueryString.parse() tries to be even more helpful by
+       returning multiple values in an array but single values *not*
+       in an array... I wonder why I'm using Y.QueryString.parse() at
+       all. */
+    if (Y.Lang.isArray(packagesets)) {
+        return packagesets.map(n2s);
+    }
+    else if (Y.Lang.isValue(packagesets)) {
+        return [n2s(packagesets)];
+    }
+    else {
+        return [];
+    }
+};
+
+
+/**
+ * Wire up a packageset picker that updates the given form.
+ *
+ * @param {Y.Node} origin The node that, when clicked, should activate
+ *     the picker.
+ * @param {Y.Node} form The form that the picker should update.
+ */
+var connect_packageset_picker = function(origin, form) {
+    /* XXX: GavinPanella 2011-07-19 bug=???: PackagesetPickerWidget
+       needs to render into a container. This should not be
+       required. */
+    var picker_table =
+        Y.Node.create("<table><tbody /></table>");
+    var picker =
+        new widgets.PackagesetPickerWidget()
+            .set("name", "packagesets")
+            .set("size", 5)
+            .set("multiple", true)
+            .render(picker_table.one("tbody"));
+
+    /* XXX: GavinPanella 2011-07-19 bug=???:
+       PackagesetPickerWidget.add_distroseries() accepts only this
+       odd-looking object. It would be more convenient and less
+       surprising if it also accepted an lp.client.Entry. */
+    picker.add_distroseries({
+        api_uri: LP.cache.context.self_link,
+        title: LP.cache.context.title,
+        value: LP.cache.context.self_link
+    });
+
+    /* Buttons */
+    var submit_button = Y.Node.create(
+        '<button type="submit" class="lazr-pos lazr-btn" />')
+           .set("text", "OK");
+    var cancel_button = Y.Node.create(
+        '<button type="button" class="lazr-neg lazr-btn" />')
+           .set("text", "Cancel");
+
+    /* When the form overlay is submitted the search filter form is
+       modified and submitted. */
+    var submit_callback = function(data) {
+        // Remove all packagesets information previously recorded.
+        form.all(PACKAGESET_FIELD_SELECTOR).remove();
+        if (data.packagesets !== undefined) {
+            Y.each(data.packagesets, function(packageset) {
+                form.append(
+                    Y.Node.create('<input type="hidden" />')
+                        .set("name", PACKAGESET_FIELD)
+                        .set("value", packageset));
+            });
+        }
+        form.submit();
+    };
+
+    /* Form overlay. */
+    var overlay = new Y.lazr.FormOverlay({
+        align: {
+            /* Align the centre of the overlay with the centre of the
+               origin node. */
+            node: origin,
+            points: [
+                Y.WidgetPositionAlign.CC,
+                Y.WidgetPositionAlign.CC
+            ]
+        },
+        headerContent: "<h2>Select package sets</h2>",
+        form_content: picker_table,
+        form_submit_button: submit_button,
+        form_cancel_button: cancel_button,
+        form_submit_callback: submit_callback,
+        visible: false
+    });
+    overlay.render();
+
+    var reposition_overlay = function() {
+        /* Trigger alignment and constrain to the viewport. Should
+           these not be automatic? Perhaps a bad interaction between
+           widget-position-align and widget-position-constrain? Only
+           reposition when overlay is visible. */
+        if (overlay.get("visible")) {
+            overlay.set("align", overlay.get("align"));
+            overlay.constrain(null, true);
+        }
+    };
+    overlay.after("visibleChange", reposition_overlay);
+    Y.on("windowresize", reposition_overlay);
+
+    var packagesets_in_query =
+        get_packagesets_in_query(window.location.search);
+    var initialize_picker = function() {
+        /* Set the current selection from the query string. Only
+           initialize when overlay is visible. */
+        if (overlay.get("visible")) {
+            picker.set("choice", packagesets_in_query);
+        }
+    };
+    /* XXX: GavinPanella 2011-07-20 bug=???: We should be able to
+       listen to choicesChange events from the picker widget but
+       they're not fired consistently. Instead we initialize when
+       showing the overlay, which is prone to a race condition (it may
+       update the selection before the packageset picker has been
+       populated with choices. */
+    overlay.after("visibleChange", initialize_picker);
+
+    /* Convert the content of the origin into a js-action link, and
+       show the overlay when it's clicked. I would rather not do this
+       with innerHTML, but I can't figure out how to get YUI3 to wrap
+       the contents of a node with another node, *including text
+       nodes*. Also, node.get("text") is broken; given
+       <a>foo<b/>bar</a>, a.get("text") will return "foobar". */
+    var link = Y.Node.create('<a class="js-action sprite edit" />');
+    link.set("innerHTML", origin.get("innerHTML"));
+    link.on("click", function() { overlay.show(); });
+    origin.empty().append(link);
+};
+
+
+// Exports.
+namespace.connect_packageset_picker = connect_packageset_picker;
+
+
+}, "0.1", {
+    "requires": [
+        "node",
+        "querystring-parse",
+        "lazr.formoverlay",
+        "lp.app.formwidgets",
+        "lp.registry.distroseries.widgets"
+    ]});

=== modified file 'lib/lp/registry/javascript/distroseries/widgets.js'
--- lib/lp/registry/javascript/distroseries/widgets.js	2011-07-18 10:50:01 +0000
+++ lib/lp/registry/javascript/distroseries/widgets.js	2011-07-21 09:36:36 +0000
@@ -367,8 +367,8 @@
      * distroseries to the possible choices.
      *
      * @method add_distroseries
-     * @param {Object} The distroseries to add ({value:distroseries_id),
-     *     api_uri:distroseries_uri}).
+     * @param {Object} distroseries The distroseries to add
+     *     ({value:distroseries_id, api_uri:distroseries_uri}).
      */
     add_distroseries: function(distroseries) {
         var path = distroseries.api_uri + "/architectures";
@@ -468,6 +468,9 @@
          * @property distroSeries
          */
         distroSeries: {
+            /* XXX: GavinPanella 2011-07-20 bug=???: This widget
+               handles multiple distroseries so this attribute is
+               defunct. */
             setter: function(value, name) {
                 var distro_series_uri = Y.lp.client.get_absolute_uri(value);
                 var on = {
@@ -495,7 +498,12 @@
      * Add a distroseries: add its packagesets to the packageset picker.
      *
      * @method add_distroseries
-     * @param {Object} distroseries The distroseries object.
+     * @param {Object} distroseries An object describing a
+     *     distroseries, containing three properties: api_uri, title
+     *     and value. The first two are what you might expect. The
+     *     last is simply a unique term for referencing the
+     *     distroseries which is required when calling
+     *     remove_distroseries.
      */
     add_distroseries: function(distroseries) {
         var distro_series_uri = Y.lp.client.get_absolute_uri(
@@ -522,6 +530,14 @@
      * @method clean_display
      */
     clean_display: function() {
+        /* XXX: GavinPanella 2011-07-20 bug=???: The list of choices
+           should be set via this.set("choices", ...) and the setter
+           should ensure that the selection is maintained. Doing it
+           via the back door means that the choicesChange event is not
+           fired. Rather than fire it explicity we should DTRT. */
+        /* XXX: GavinPanella 2011-07-18 bug=???: This does not update
+           this._distroseries and can thus leave the widget in an
+           invalid state. */
         this.fieldNode.empty();
         this.fieldNode.append(Y.Node.create("<div />")
             .set('text', '[No package sets to select from yet!]'));
@@ -534,6 +550,9 @@
      * @method init_select
      */
     init_select: function() {
+        /* XXX: GavinPanella 2011-07-20 bug=???: This method will not
+           be necessary once this.set("choices", ...) is used
+           consistently. */
         var select = this.fieldNode.one('select');
         if (select === null) {
             select = Y.Node.create("<select />");
@@ -555,6 +574,11 @@
      *     ({text: choice_text, value: choice_value, data: choice_data}).
      */
     add_choice: function(choice) {
+        /* XXX: GavinPanella 2011-07-20 bug=???: The list of choices
+           should be set via this.set("choices", ...) and the setter
+           should ensure that the selection is maintained. Doing it
+           via the back door means that the choicesChange event is not
+           fired. Rather than fire it explicity we should DTRT. */
         var select = this.init_select();
         var option = Y.Node.create("<option />");
         option.set("value", choice.value)
@@ -612,6 +636,12 @@
     remove_distroseries: function(distroseries_id) {
         this._packagesets[distroseries_id].forEach(
             function(packageset) {
+                /* XXX: GavinPanella 2011-07-20 bug=???: The list of
+                   choices should be set via this.set("choices", ...)
+                   and the setter should ensure that the selection is
+                   maintained. Doing it via the back door means that
+                   the choicesChange event is not fired. Rather than
+                   fire it explicity we should DTRT. */
                 this.fieldNode.one(
                     'option[value="' + packageset.get("id") + '"]').remove();
             }, this);

=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
--- lib/lp/registry/templates/distroseries-localdifferences.pt	2011-06-13 19:41:40 +0000
+++ lib/lp/registry/templates/distroseries-localdifferences.pt	2011-07-21 09:36:36 +0000
@@ -38,7 +38,7 @@
 
       <div metal:use-macro="context/@@launchpad_form/form">
 
-      <div tal:condition="differences/batch" metal:fill-slot="widgets">
+      <div metal:fill-slot="widgets">
         <tal:navigation_top
           replace="structure differences/@@+navigation-links-upper" />
           <table class="listing">
@@ -61,10 +61,12 @@
                 <a tal:attributes="href context/fmt:url"
                    tal:content="series_name">Deriwarty</a> version
               </th>
-              <th tal:condition="view/show_parent_packagesets">
+              <th tal:condition="view/show_parent_packagesets"
+                  class="parent-package-sets">
                 Parent package-sets
               </th>
-              <th tal:condition="view/show_packagesets">
+              <th tal:condition="view/show_packagesets"
+                  class="package-sets">
                 Package-sets
               </th>
                <th>Last uploaded</th>
@@ -202,6 +204,18 @@
   Y.on('domready', diff_module.setup_expandable_rows);
 });
 </script>
+<script type="text/javascript">
+  LPS.use("lp.registry.distroseries.differences", function(Y) {
+    Y.on("domready", function() {
+      var form = Y.one("form#distroseries-localdiff-search-filter");
+      var differences = Y.lp.registry.distroseries.differences;
+      // Probably only one, but check for more just in case.
+      Y.all("th.package-sets").each(function(header) {
+        differences.connect_packageset_picker(header, form);
+      });
+    });
+  });
+</script>
 
     </div>