← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/js-translation-2 into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/js-translation-2 into lp:launchpad with lp:~abentley/launchpad/json-init as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/js-translation-2/+merge/56648

= Summary =
Ajaxify productseries and automatic imports on +sharing-details.

This is the second of three branches.

== Pre-implementation notes ==
None

== Implementation details ==
Extracted IOHandler from select_branch and reused it for setting productseries
and autoimport.

Implement visible_check_selector so that the correct version of the checklist
item flashes on success or failure.

Revise update so that it works on all items in tsconfig.all_items.  If all
entries in tsconfig.all_items are complete, set the "configuration" to
complete.

Provide replace_product and replace_productseries to handle all implications of
changing the product and productseries.

Handle "412 Precondition Failed" responses more clearly.

== Tests ==
bin/test --layer=WindmillLayer sharing_details
firefox lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.html

== Demo and Q/A ==
Go to a +sharing-details page and set the productseries and autoimports.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/testing/windmill/widgets.py
  lib/lp/app/javascript/lp.js
  lib/lp/translations/templates/sourcepackage-sharing-details.pt
  lib/lp/app/javascript/client.js
  lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js
  lib/lp/app/javascript/picker.js
  lib/lp/translations/browser/tests/test_sharing_details.py
  lib/lp/translations/windmill/tests/test_sourcepackage_sharing_details.py
  lib/lp/translations/javascript/sourcepackage_sharing_details.js
  lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.html
  lib/lp/translations/browser/sourcepackage.py

./lib/lp/testing/windmill/widgets.py
      29: E501 line too long (80 characters)
      35: E501 line too long (80 characters)
      36: E501 line too long (81 characters)
      95: E501 line too long (83 characters)
     122: E301 expected 1 blank line, found 0
      27: Line exceeds 78 characters.
      29: Line exceeds 78 characters.
      35: Line exceeds 78 characters.
      36: Line exceeds 78 characters.
      43: Line exceeds 78 characters.
      95: Line exceeds 78 characters.
./lib/lp/app/javascript/lp.js
      53: Expected '!==' and instead saw '!='.
     117: Expected '!==' and instead saw '!='.
     187: Expected '===' and instead saw '=='.
     290: Expected '===' and instead saw '=='.
     305: Expected '===' and instead saw '=='.
     306: Expected '===' and instead saw '=='.
     338: Move 'var' declarations to the top of the function.
     338: Stopping.  (76% scanned).
       0: JSLINT had a fatal error.
./lib/lp/app/javascript/client.js
      30: Expected '===' and instead saw '=='.
      33: Expected '===' and instead saw '=='.
      54: Move 'var' declarations to the top of the function.
      54: Stopping.  (5% scanned).
       0: JSLINT had a fatal error.
       4: Line exceeds 78 characters.
     191: Line exceeds 78 characters.
     570: Line exceeds 78 characters.
     618: Line exceeds 78 characters.
     745: Line exceeds 78 characters.
     747: Line exceeds 78 characters.
./lib/lp/app/javascript/picker.js
      43: Expected ';' and instead saw 'if'.
      92: Expected '!==' and instead saw '!='.
     223: Expected '!==' and instead saw '!='.
     224: Expected '{' and instead saw 'temp_spinner'.
     257: Expected '!==' and instead saw '!='.
     270: 'header' used out of scope.
     271: 'step_title' used out of scope.
     319: Move 'var' declarations to the top of the function.
     319: Stopping.  (85% scanned).
       0: JSLINT had a fatal error.
     194: Line exceeds 78 characters.
-- 
https://code.launchpad.net/~abentley/launchpad/js-translation-2/+merge/56648
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/js-translation-2 into lp:launchpad.
=== modified file 'lib/lp/app/javascript/client.js'
--- lib/lp/app/javascript/client.js	2011-04-06 20:37:30 +0000
+++ lib/lp/app/javascript/client.js	2011-04-06 20:37:31 +0000
@@ -748,6 +748,8 @@
                 }
                 self.showError(server_error);
             // Otherwise we send some sane text as an error
+            } else if (o.status == 412){
+                self.showError(o.status + ' ' + o.statusText);
             } else {
                 self.showError(o.responseText);
             }

=== modified file 'lib/lp/app/javascript/lp.js'
--- lib/lp/app/javascript/lp.js	2011-03-24 15:31:53 +0000
+++ lib/lp/app/javascript/lp.js	2011-04-06 20:37:31 +0000
@@ -262,7 +262,9 @@
             });
         });
     };
-
+    Y.lp.get_url_path = function(url) {
+         return Y.Node.create('<a>junk</a>').set('href', url).get('pathname');
+    };
 }, "0.1", {"requires":["cookie", "lazr.effects"]});
 
 

=== modified file 'lib/lp/app/javascript/picker.js'
--- lib/lp/app/javascript/picker.js	2011-04-06 20:37:30 +0000
+++ lib/lp/app/javascript/picker.js	2011-04-06 20:37:31 +0000
@@ -5,10 +5,6 @@
 var BATCH_SIZE = 6;
 var MAX_BATCHES = 20;
 
-function get_pathname(url) {
-     return Y.Node.create('<a>junk</a>').set('href', url).get('pathname');
-}
-
 /* Add a picker widget which will PATCH a given attribute on
  * a given resource.
  *
@@ -344,7 +340,7 @@
 
         var uri = '';
         if (Y.Lang.isValue(config['context'])){
-            uri += get_pathname(config['context'].get('web_link')) + '/';
+            uri += Y.lp.get_url_path(config['context'].get('web_link')) + '/';
         }
         uri += '@@+huge-vocabulary?' + qs;
 

=== modified file 'lib/lp/testing/windmill/widgets.py'
--- lib/lp/testing/windmill/widgets.py	2011-03-29 07:10:40 +0000
+++ lib/lp/testing/windmill/widgets.py	2011-04-06 20:37:31 +0000
@@ -85,14 +85,15 @@
         """
         super(SearchPickerWidget, self).__init__(client, 'yui3-picker')
         self.search_input_xpath = (
-            self.xpath + "//input[@class='yui3-picker-search']")
+            self.visible_xpath + "//input[@class='yui3-picker-search']")
         self.search_button_xpath = (
-            self.xpath + "//div[@class='yui3-picker-search-box']/button")
+            self.visible_xpath +
+            "//div[@class='yui3-picker-search-box']/button")
 
     def _get_result_xpath_by_number(self, item_number):
         """Return the XPath for the given search result number."""
         item_xpath = "//ul[@class='yui3-picker-results']/li[%d]/span" % item_number
-        return self.xpath + item_xpath
+        return self.visible_xpath + item_xpath
 
     def do_search(self, text):
         """Enter some text in the search field and click the search button.

=== modified file 'lib/lp/translations/javascript/sourcepackage_sharing_details.js'
--- lib/lp/translations/javascript/sourcepackage_sharing_details.js	2011-04-06 20:37:30 +0000
+++ lib/lp/translations/javascript/sourcepackage_sharing_details.js	2011-04-06 20:37:31 +0000
@@ -70,6 +70,10 @@
     set_link: function(text, url){
         this.set('_text', text);
         this.set('_url', url);
+    },
+    clear_link: function(){
+        this.set('_text', null);
+        this.set('_url', null);
     }
 });
 
@@ -115,22 +119,76 @@
 Y.extend(TranslationSharingConfig, Y.Base, {
     initializer: function(){
         var product_series = new LinkCheckItem(
-            {identifier: 'product-series'});
+            {identifier: 'packaging'});
         this.set('product_series', product_series);
         var usage = new CheckItem(
-            {identifier: 'usage', dependency: product_series});
+            {identifier: 'translation', dependency: product_series});
         this.set('translations_usage', usage);
         var branch = new LinkCheckItem(
             {identifier: 'branch', dependency: this.get('product_series')});
         this.set('branch', branch);
         var autoimport = new CheckItem(
-            {identifier: 'autoimport', dependency: branch});
+            {identifier: 'upstream-sync', dependency: branch});
         this.set('autoimport', autoimport);
-        this.set('all_items', [product_series, usage, branch, autoimport]);
+        var configuration = new CheckItem(
+            {identifier: 'configuration'});
+        this.set('configuration', configuration);
+        this.set(
+            'all_items', [product_series, usage, branch, autoimport]);
     }
 });
 namespace.TranslationSharingConfig = TranslationSharingConfig;
 
+
+function IOHandler(flash_target){
+    that = this;
+    this.flash_target = flash_target;
+    this.error_handler = new Y.lp.client.ErrorHandler();
+    this.error_handler.showError = function(error_msg) {
+        Y.lp.app.errors.display_error(Y.one(that.flash_target), error_msg);
+    };
+}
+
+
+/**
+ * Return an LP client config using error_handler.
+ *
+ * @param next {Object} A callback to call on success.
+ */
+IOHandler.prototype.get_config = function(next){
+    var config = {
+        on:{
+            success: next,
+            failure: this.error_handler.getFailureHandler()
+        }
+    };
+    return config;
+};
+
+
+/**
+ * Return an LP client config that will call the specified callbacks
+ * in sequence, using error_handler.
+ *
+ * @param next {Object} A callback to call on success.
+ */
+IOHandler.prototype.chain_config = function(){
+    var last_config;
+    var i;
+    // Each callback is bound to the next, so we use reverse order.
+    for(i = arguments.length-1; i >= 0; i--){
+        if (i === arguments.length - 1) {
+            callback = arguments[i];
+        }
+        else {
+            callback = Y.bind(arguments[i], this, last_config);
+        }
+        last_config = this.get_config(callback);
+    }
+    return last_config;
+};
+
+
 /**
  * This class is the controller for updating the TranslationSharingConfig.
  * It handles updating the HTML and the DB model.
@@ -141,10 +199,11 @@
 }
 Y.extend(TranslationSharingController, Y.Base, {
     initializer: function(source_package){
-        this.set('source_package', source_package);
         this.set('tsconfig', new TranslationSharingConfig());
         this.set('productseries', null);
         this.set('branch', null);
+        this.set('source_package', null);
+        this.set('branch_picker_config', null);
     },
     /*
      * Select the specified branch as the translation branch.
@@ -152,18 +211,12 @@
      * @param branch_summary {Object} An object containing api_url, css,
      * description, value, title
      */
-    configure: function(config){
-        this.set_productseries(config.productseries);
-        var autoimport_mode = namespace.autoimport_modes.no_import;
-        var translations_usage = namespace.usage.unknown;
-        if (!Y.Lang.isNull(config.productseries)){
-            autoimport_mode = config.productseries.get(
-                'translations_autoimport_mode');
-            translations_usage = config.product.get(
-                'translations_usage');
-        }
-        this.set_autoimport_mode(autoimport_mode);
-        this.set_translations_usage(translations_usage);
+    configure: function(config, branch_picker_config, import_overlay){
+        this.set('branch_picker_config', branch_picker_config);
+        this.set('source_package', config.context);
+        this.set('import_overlay', import_overlay);
+        this.replace_productseries(config.productseries);
+        this.replace_product(config.product);
         this.set_branch(config.upstream_branch);
     },
     set_productseries: function(productseries) {
@@ -173,15 +226,43 @@
                 productseries.get('title'), productseries.get('web_link'));
         }
     },
+    replace_productseries: function(productseries) {
+        this.set_productseries(productseries);
+        var autoimport_mode = namespace.autoimport_modes.no_import;
+        if (!Y.Lang.isNull(productseries)){
+            autoimport_mode = productseries.get(
+                'translations_autoimport_mode');
+            var import_url = productseries.get('web_link');
+            import_url = Y.lp.get_url_path(import_url);
+            import_url += '/+translations-settings/++form++';
+            var import_overlay = this.get('import_overlay');
+            import_overlay.loadFormContentAndRender(import_url);
+            import_overlay.render();
+        }
+        this.set_autoimport_mode(autoimport_mode);
+    },
+    replace_product: function(product){
+        this.get('branch_picker_config').context = product;
+        var translations_usage = namespace.usage.unknown;
+        if (!Y.Lang.isNull(product)){
+            translations_usage = product.get('translations_usage');
+        }
+        this.set_translations_usage(translations_usage);
+    },
     set_branch: function(branch){
         this.set('branch', branch);
+        var check = this.get('tsconfig').get('branch');
         if (Y.Lang.isValue(branch)){
-            this.get('tsconfig').get('branch').set_link(
+            check.set_link(
                 branch.get('unique_name'), branch.get('web_link'));
         }
+        else {
+            check.clear_link();
+        }
     },
     set_autoimport_mode: function(mode){
-        complete = (mode === namespace.autoimport_modes.import_translations);
+        var complete = (
+            mode === namespace.autoimport_modes.import_translations);
         this.get('tsconfig').get('autoimport').set('complete', complete);
     },
     set_translations_usage: function(usage){
@@ -191,48 +272,48 @@
         var usage_check = this.get('tsconfig').get('translations_usage');
         usage_check.set('complete', complete);
     },
+    select_productseries: function(productseries_summary){
+        var that = this;
+        var productseries_check = that.get('tsconfig').get('product_series');
+        var lp_client = new Y.lp.client.Launchpad();
+        function save_productseries(config){
+            var source_package = that.get('source_package');
+            config.parameters = {
+                productseries: productseries_summary.api_uri};
+            source_package.named_post('setPackaging', config);
+        }
+        function get_productseries(config){
+            lp_client.get(productseries_summary.api_uri, config);
+        }
+        function cache_productseries(config, productseries){
+            that.replace_productseries(productseries);
+            var branch_link = productseries.get('branch_link');
+            if (branch_link === null){
+                config.on.success(null);
+            }
+            else {
+                lp_client.get(branch_link, config);
+            }
+        }
+        function cache_branch(config, branch){
+            that.set_branch(branch);
+            project_link = that.get('productseries').get('project_link');
+            lp_client.get(project_link, config);
+        }
+        function set_usage(product){
+            that.replace_product(product);
+            that.update();
+            that.flash_check_green(productseries_check);
+        }
+        var css_selector = this.visible_check_selector(productseries_check);
+        var io_handler = new IOHandler(css_selector);
+        save_productseries(io_handler.chain_config(
+            get_productseries, cache_productseries, cache_branch, set_usage));
+    },
     select_branch: function(branch_summary){
         var that = this;
         var lp_client = new Y.lp.client.Launchpad();
-        var error_handler = new Y.lp.client.ErrorHandler();
-        error_handler.showError = function(error_msg) {
-            Y.lp.app.errors.display_error(Y.one('#branch'), error_msg);
-        };
-        /**
-         * Return an LP client config using error_handler.
-         *
-         * @param next {Object} A callback to call on success.
-         */
-        function get_config(next){
-            var config = {
-                on:{
-                    success: next,
-                    failure: error_handler.getFailureHandler()
-                }
-            };
-            return config;
-        }
-        /**
-         * Return an LP client config that will call the specified callbacks
-         * in sequence, using error_handler.
-         *
-         * @param next {Object} A callback to call on success.
-         */
-        function chain_config(){
-            var last_config;
-            var i;
-            // Each callback is bound to the next, so we use reverse order.
-            for(i = arguments.length-1; i >= 0; i--){
-                if (i === arguments.length - 1) {
-                    callback = arguments[i];
-                }
-                else {
-                    callback = Y.bind(arguments[i], this, last_config);
-                }
-                last_config = get_config(callback);
-            }
-            return last_config;
-        }
+        var branch_check = that.get('tsconfig').get('branch');
 
         /* Here begin a series of methods which each represent a step in
          * setting the branch.  They each take a config to use in an lp_client
@@ -253,36 +334,66 @@
         function set_link(branch){
             that.set_branch(branch);
             that.update();
-            var check = that.get('tsconfig').get('branch');
-            var element = Y.one(that.check_selector(check, true));
-            var anim = Y.lazr.anim.green_flash({node: element});
-            anim.run();
+            that.flash_check_green(branch_check);
         }
-        save_branch(chain_config(get_branch, set_link));
+        css_selector = that.visible_check_selector(branch_check);
+        var io_handler = new IOHandler(css_selector);
+        save_branch(io_handler.chain_config(get_branch, set_link));
     },
     /**
      * Update the display of all checklist items.
      */
     update: function(){
-        var branch = this.get('tsconfig').get('branch');
-        this.update_check(branch);
+        var all_items = this.get('tsconfig').get('all_items');
+        var overall = this.get('tsconfig').get('configuration');
+        var i;
+        overall.set('complete', true);
+        for (i = 0; i < all_items.length; i++){
+            this.update_check(all_items[i]);
+            if (!all_items[i].get('complete')){
+                overall.set('complete', false);
+            }
+        }
+        this.update_check(overall);
     },
     check_selector: function(check, complete){
         var completion = complete ? '-complete' : '-incomplete';
         return '#' + check.get('identifier') + completion;
     },
+    visible_check_selector: function(check){
+        return this.check_selector(check, check.get('complete'));
+    },
+    picker_selector: function(check, complete){
+        return this.check_selector(check, complete) + '-picker a';
+    },
     /**
      * Update the display of a single checklist item.
      */
     update_check: function(check){
         var complete = Y.one(this.check_selector(check, true));
-        var link = complete.one('a.link');
-        link.set('href', check.get('url'));
-        link.set('text', check.get('text'));
+        var link = complete.one('.link a');
+        if (link !== null){
+            link.set('href', check.get('url'));
+            link.set('text', check.get('text'));
+        }
         complete.toggleClass('unseen', !check.get('complete'));
+        complete.toggleClass('lowlight', !check.get('enabled'));
+        var complete_picker = Y.one(this.picker_selector(check, true));
+        if (complete_picker !== null) {
+            complete_picker.toggleClass('unseen', !check.get('enabled'));
+        }
         var incomplete = Y.one(this.check_selector(check, false));
         incomplete.toggleClass('unseen', check.get('complete'));
         incomplete.toggleClass('lowlight', !check.get('enabled'));
+        var incomplete_picker = Y.one(this.picker_selector(check, false));
+        if (incomplete_picker !== null) {
+            incomplete_picker.toggleClass('unseen', !check.get('enabled'));
+        }
+    },
+    flash_check_green: function(check){
+        var element = Y.one(this.visible_check_selector(check));
+        var anim = Y.lazr.anim.green_flash({node: element});
+        anim.run();
     }
 });
 namespace.TranslationSharingController = TranslationSharingController;
@@ -291,30 +402,77 @@
 /**
  * Method to prepare the AJAX translation sharing config functionality.
  */
-namespace.prepare = function(source_package, cache){
-    var sharing_controller = new namespace.TranslationSharingController(
-        source_package);
+namespace.prepare = function(cache){
+    var sharing_controller = new namespace.TranslationSharingController();
     var lp_client = new Y.lp.client.Launchpad();
     cache = namespace.convert_cache(lp_client, cache);
-    sharing_controller.configure(cache);
-    sharing_controller.update();
-    var config = {
+    var branch_picker_config = {
         picker_activator: '#branch-incomplete-picker a',
         header : 'Select translation branch',
         step_title: 'Search',
         save: Y.bind('select_branch', sharing_controller),
         context: cache.product
     };
-    var picker = Y.lp.app.picker.create('BranchRestrictedOnProduct', config);
-    var element = Y.one('#branch-complete-picker a');
-    /* Copy-pasted because picker can't normally be activated by two different
-     * elements. */
-    element.on('click', function(e) {
-        e.halt();
-        this.show();
-    }, picker);
-    element.addClass(picker.get('picker_activator_css_class'));
-
+    var picker = Y.lp.app.picker.create(
+        'BranchRestrictedOnProduct', branch_picker_config);
+    function add_activator(picker, selector){
+        var element = Y.one(selector);
+        /* Copy-pasted because picker can't normally be activated by two
+         * different elements. */
+        element.on('click', function(e) {
+            e.halt();
+            this.show();
+        }, picker);
+        element.addClass(picker.get('picker_activator_css_class'));
+    }
+    add_activator(picker, '#branch-complete-picker a');
+    var productseries_picker_config = {
+        picker_activator: '#packaging-complete-picker a',
+        header : 'Select productseries',
+        step_title: 'Search',
+        save: Y.bind('select_productseries', sharing_controller),
+        context: cache.product
+    };
+    var productseries_picker = Y.lp.app.picker.create(
+        'ProductSeries', productseries_picker_config);
+    add_activator(productseries_picker, '#packaging-incomplete-picker a');
+    var import_overlay = new Y.lazr.FormOverlay({
+        headerContent: '<h2>Import settings<h2>',
+        centered: true,
+        visible: false
+    });
+    import_overlay.set('form_submit_callback', function(form_data){
+        Y.log(form_data['field.translations_autoimport_mode']);
+        var key = form_data['field.translations_autoimport_mode'][0];
+        Y.log(key);
+        var mode = namespace.autoimport_modes[key.toLowerCase()];
+        Y.log(mode);
+        import_overlay.hide();
+        var product_series = sharing_controller.get('productseries');
+        product_series.set('translations_autoimport_mode', mode);
+        var autoimport_check = sharing_controller.get(
+            'tsconfig').get('autoimport');
+        var css_selector = sharing_controller.visible_check_selector(
+            autoimport_check);
+        handler = new IOHandler(css_selector);
+        function update_controller(){
+            sharing_controller.set_autoimport_mode(mode);
+            sharing_controller.update();
+            sharing_controller.flash_check_green(autoimport_check);
+        }
+        /* XXX: AaronBentley 2011-04-04 bug=369293: Avoid 412 on repeated
+         * changes.  This does not increase the risk of changing from a
+         * stale value, because the staleness check is not reasonable.
+         * The user is changing from the default shown in the form, not
+         * the value stored in productseries.
+         */
+        product_series.removeAttr('http_etag');
+        product_series.lp_save(handler.chain_config(update_controller));
+    });
+    add_activator(import_overlay, '#upstream-sync-complete-picker a');
+    add_activator(import_overlay, '#upstream-sync-incomplete-picker a');
+    sharing_controller.configure(cache, branch_picker_config, import_overlay);
+    sharing_controller.update();
 };
-}, "0.1", {"requires": ['lp.app.errors', 'lp.app.picker', 'oop', 'lp.client']}
-);
+}, "0.1", {"requires": [
+    'lp', 'lp.app.errors', 'lp.app.picker', 'oop', 'lp.client']});

=== modified file 'lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.html'
--- lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.html	2011-04-06 20:37:30 +0000
+++ lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.html	2011-04-06 20:37:31 +0000
@@ -14,6 +14,7 @@
     <link rel="stylesheet"
       href="../../../../canonical/launchpad/javascript/test.css" />
     <script type="text/javascript" src="../../../app/javascript/client.js"></script>
+    <script type="text/javascript" src="../../../app/javascript/lp.js"></script>
 
     <!-- The module under test -->
     <script type="text/javascript" src="../sourcepackage_sharing_details.js"></script>
@@ -25,9 +26,45 @@
 
     <!-- The example markup required by the script to run -->
     <div id="expected-id">
-      <div id="branch-complete">Branch selected: <a href="#" class="link"></a>
+      <div id="configuration-incomplete">
+        Translation sharing configuration is incomplete.
+      </div>
+      <div id="configuration-complete">
+        Translation sharing with upstream is active.
+      </div>
+      <div id="packaging-incomplete">
+        No upstream project series has been linked.
+        <a href="#picker" />
+      </div>
+      <div id="packaging-complete">
+          Linked upstream series is
+          <a href="#upstream-link" class="link">
+            Gimp trunk</a>.
+          <a href="#edit-link" ></a>
+          <a href="#remove-link" ></a>
+      </div>
+      <div id="branch-complete">Branch selected: <span class="link"><a
+          href="#" class="link"></a></span>
+          <span id="branch-complete-picker"><a href="#"></a></span>
       </div>
       <div id="branch-incomplete">No branch selected.</div>
+      <span id="branch-incomplete-picker"><a href="#"></a></span>
+      <div id="translation-incomplete">
+        Translations are not enabled on the upstream project.
+        <a href="#enable-translations"></a>
+      </div>
+      <div id="translation-complete">
+        Translations are enabled on the upstream project.
+        <a href="#enable-translations"></a>
+      </div>
+      <div  id="upstream-sync-incomplete">
+        Automatic synchronization of translations is not enabled.
+        <a href="#enable-sync"></a>
+      </div>
+      <div  id="upstream-sync-complete">
+        Automatic synchronization of translations is enabled.
+        <a href="#enable-sync"></a>
+      </div>
     </div>
     <!-- The test output -->
     <div id="log"></div>

=== modified file 'lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js'
--- lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js	2011-04-06 20:37:30 +0000
+++ lib/lp/translations/javascript/tests/test_sourcepackage_sharing_details.js	2011-04-06 20:37:31 +0000
@@ -51,6 +51,13 @@
             Y.Assert.areEqual('mytext', lci.get('text'));
             Y.Assert.areEqual('http://example.com', lci.get('url'));
         },
+        test_LinkCheckItem_clear: function() {
+            var lci = new LinkCheckItem();
+            lci.set_link('mytext', 'http://example.com');
+            lci.clear_link();
+            Y.Assert.isNull(lci.get('text'));
+            Y.Assert.isNull(lci.get('url'));
+        },
         test_LinkCheckItem_complete: function() {
             var lci = new LinkCheckItem();
             Y.Assert.isFalse(lci.get('complete'));
@@ -73,12 +80,13 @@
             Y.Assert.areEqual('id1', ci.get('identifier'));
         },
         test_configure_empty: function(){
-            var ctrl = new TranslationSharingController({});
+            var ctrl = new TranslationSharingController();
             var cache = {
                 productseries: null,
+                product: null,
                 upstream_branch: null
             };
-            ctrl.configure(cache);
+            ctrl.configure(cache, {});
         },
         test_configure: function(){
             var cache = {
@@ -97,12 +105,19 @@
                     unique_name: 'title2',
                     web_link: 'http://web2',
                     resource_type_link: 'branch'
+                },
+                context: {
+                    resource_type_link: 'http://sourcepackage'
                 }
             };
-            var ctrl = new TranslationSharingController({});
+            var ctrl = new TranslationSharingController();
             var lp_client = new Y.lp.client.Launchpad();
             cache = test_ns.convert_cache(lp_client, cache);
-            ctrl.configure(cache);
+            var import_overlay = {
+                loadFormContentAndRender: function(){},
+                render: function(){}
+            };
+            ctrl.configure(cache, {}, import_overlay);
             var tsconfig = ctrl.get('tsconfig');
             Y.Assert.areEqual(
                 tsconfig.get('product_series').get('text'), 'title1');
@@ -113,6 +128,7 @@
             Y.Assert.isTrue(tsconfig.get('autoimport').get('complete'));
             Y.Assert.isTrue(
                 tsconfig.get('translations_usage').get('complete'));
+            Y.Assert.areSame(ctrl.get('source_package'), cache.context);
         },
         test_convert_cache: function(){
             var cache = {
@@ -135,7 +151,7 @@
             Y.Assert.areNotEqual('http:///', link.get('href'));
             Y.Assert.isFalse(complete.hasClass('unseen'));
             Y.Assert.isFalse(incomplete.hasClass('unseen'));
-            var ctrl = new TranslationSharingController({});
+            var ctrl = new TranslationSharingController();
             ctrl.update();
             Y.Assert.isTrue(complete.hasClass('unseen'));
             Y.Assert.isFalse(incomplete.hasClass('unseen'));
@@ -147,9 +163,32 @@
             Y.Assert.areEqual('a', link.get('text'));
             Y.Assert.areEqual('http:///', link.get('href'));
         },
+        test_update_all: function(){
+            var ctrl = new TranslationSharingController();
+            var config = ctrl.get('tsconfig');
+            ctrl.update();
+            var config_incomplete = Y.one('#configuration-incomplete');
+            Y.Assert.isFalse(config_incomplete.hasClass('unseen'));
+            var pack_incomplete = Y.one('#packaging-incomplete');
+            Y.Assert.isFalse(pack_incomplete.hasClass('unseen'));
+            var usage_incomplete = Y.one('#translation-incomplete');
+            Y.Assert.isFalse(usage_incomplete.hasClass('unseen'));
+            var sync_incomplete = Y.one('#upstream-sync-incomplete');
+            Y.Assert.isFalse(sync_incomplete.hasClass('unseen'));
+            config.get('configuration').set('complete', true);
+            config.get('product_series').set_link('a', 'http:///');
+            config.get('branch').set_link('a', 'http:///');
+            config.get('translations_usage').set('complete', true);
+            config.get('autoimport').set('complete', true);
+            ctrl.update();
+            Y.Assert.isTrue(config_incomplete.hasClass('unseen'));
+            Y.Assert.isTrue(pack_incomplete.hasClass('unseen'));
+            Y.Assert.isTrue(usage_incomplete.hasClass('unseen'));
+            Y.Assert.isTrue(sync_incomplete.hasClass('unseen'));
+        },
         test_update_check_disabled: function(){
             var incomplete = Y.one('#branch-incomplete');
-            var ctrl = new TranslationSharingController({});
+            var ctrl = new TranslationSharingController();
             var branch = ctrl.get('tsconfig').get('branch');
             ctrl.update_check(branch);
             Y.Assert.isTrue(incomplete.hasClass('lowlight'));
@@ -159,7 +198,7 @@
             Y.Assert.isFalse(incomplete.hasClass('lowlight'));
         },
         test_set_autoimport_mode: function(){
-            var ctrl = new TranslationSharingController({});
+            var ctrl = new TranslationSharingController();
             var check = ctrl.get('tsconfig').get('autoimport');
             Y.Assert.isFalse(check.get('complete'));
             ctrl.set_autoimport_mode('Import template and translation files');
@@ -168,7 +207,7 @@
             Y.Assert.isFalse(check.get('complete'));
         },
         test_set_translations_usage: function(){
-            var ctrl = new TranslationSharingController({});
+            var ctrl = new TranslationSharingController();
             var check = ctrl.get('tsconfig').get('translations_usage');
             ctrl.set_translations_usage('Unknown');
             Y.Assert.isFalse(check.get('complete'));

=== modified file 'lib/lp/translations/templates/sourcepackage-sharing-details.pt'
--- lib/lp/translations/templates/sourcepackage-sharing-details.pt	2011-04-06 20:37:30 +0000
+++ lib/lp/translations/templates/sourcepackage-sharing-details.pt	2011-04-06 20:37:31 +0000
@@ -12,7 +12,7 @@
         LPS.use('lp.translations.sourcepackage_sharing_details', function(Y) {
           Y.on('domready', function() {
               Y.lp.translations.sourcepackage_sharing_details.prepare(
-                LP.cache['context'], LP.cache);
+                LP.cache);
           });
         });
       </script>
@@ -36,14 +36,21 @@
             <li tal:attributes="class view/packaging_incomplete_class"
                 id="packaging-incomplete">
                 No upstream project series has been linked.
-                <a tal:replace="structure context/menu:overview/set_upstream/fmt:icon" />
+                <span id="packaging-incomplete-picker">
+                  <a tal:replace="structure context/menu:overview/set_upstream/fmt:icon" />
+                </span>
             </li>
             <li tal:attributes="class view/packaging_complete_class"
                 id="packaging-complete">
               Linked upstream series is
-              <a tal:replace="structure context/productseries/fmt:link">
-                Gimp trunk</a>.
-              <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
+              <span class="link">
+                  <a tal:replace="structure context/productseries/fmt:link">
+                      Gimp trunk</a>
+                  <a tal:condition="not:context/productseries" href="#"></a>
+              </span>.
+              <span id="packaging-complete-picker">
+                <a tal:replace="structure context/menu:overview/edit_packaging/fmt:icon" />
+              </span>
               <a tal:replace="structure context/menu:overview/remove_packaging/fmt:icon" />
             </li>
             <li tal:attributes="class view/branch_incomplete_class"
@@ -56,8 +63,10 @@
             <li tal:attributes="class view/branch_complete_class"
                 id="branch-complete">
             Upstream source branch is
+            <span class="link">
+                <a tal:replace="structure view/branch_link/escapedtext">lp:gimp</a>
+</span>
               <span id="branch-complete-picker">
-                <a tal:replace="structure view/branch_link/escapedtext">lp:gimp</a>
                 <a tal:replace="structure view/change_branch_link/escapedtext" />
               </span>
             </li>
@@ -68,18 +77,24 @@
             </li>
             <li tal:attributes="class view/translations_enabled_class"
                 id="translation-complete">
-              Translations are enabled on the upstream project.
+                Translations are enabled on the upstream project.
+              <span id="translation-complete-picker">
               <a tal:replace="structure view/configure_translations_link_configured/escapedtext" />
+             </span>
             </li>
             <li tal:attributes="class view/upstream_sync_disabled_class"
                 id="upstream-sync-incomplete">
               Automatic synchronization of translations is not enabled.
+              <span id="upstream-sync-incomplete-picker">
               <a tal:replace="structure view/translation_sync_link_unconfigured/escapedtext" />
+              </span>
             </li>
             <li tal:attributes="class view/upstream_sync_enabled_class"
                 id="upstream-sync-complete">
               Automatic synchronization of translations is enabled.
+              <span id="upstream-sync-complete-picker">
               <a tal:replace="structure view/translation_sync_link_configured/escapedtext" />
+              </span>
             </li>
           </ul>
         </dd>

=== modified file 'lib/lp/translations/windmill/tests/test_sourcepackage_sharing_details.py'
--- lib/lp/translations/windmill/tests/test_sourcepackage_sharing_details.py	2011-04-06 20:37:30 +0000
+++ lib/lp/translations/windmill/tests/test_sourcepackage_sharing_details.py	2011-04-06 20:37:31 +0000
@@ -23,6 +23,9 @@
 from lp.testing.windmill.widgets import (
     search_and_select_picker_widget,
 )
+from lp.translations.interfaces.translations import (
+    TranslationsBranchImportMode,
+)
 from lp.translations.windmill.testing import (
     TranslationsWindmillLayer,
 )
@@ -33,22 +36,38 @@
     layer = TranslationsWindmillLayer
 
     def test_set_branch(self):
-        packaging = self.factory.makePackagingLink()
+        sourcepackage = self.factory.makeSourcePackage()
+        productseries = self.factory.makeProductSeries(name='my-ps-name')
         branch = self.factory.makeProductBranch(
-            product=packaging.productseries.product, name='product-branch')
+            product=productseries.product, name='product-branch')
         self.useContext(feature_flags())
         set_feature_flag(u'translations.sharing_information.enabled', u'on')
         transaction.commit()
-
         client, start_url = self.getClientFor(
-            packaging.sourcepackage, user=lpuser.TRANSLATIONS_ADMIN,
+            sourcepackage, user=lpuser.TRANSLATIONS_ADMIN,
             view_name='+sharing-details')
         client.waits.forElement(
             id='branch-incomplete', timeout=FOR_ELEMENT)
+        client.click(xpath='//*[@id="packaging-incomplete-picker"]/a')
+        search_and_select_picker_widget(self.client, 'my-ps-name', 1)
+        client.waits.forElementProperty(
+            id='packaging-incomplete', option='className|sprite no unseen',
+            timeout=FOR_ELEMENT)
         client.click(xpath='//*[@id="branch-incomplete-picker"]/a')
-        search_and_select_picker_widget(client, 'product-branch', 1)
-        client.waits.forElementProperty(
-            id='branch-incomplete', option='className|sprite no unseen',
-            timeout=FOR_ELEMENT)
+        search_and_select_picker_widget(self.client, 'product-branch', 1)
+        client.waits.forElementProperty(
+            xpath='//*[@id="upstream-sync-incomplete-picker"]/a',
+            option='className|sprite edit', timeout=FOR_ELEMENT)
+        client.click(
+            xpath='//*[@id="upstream-sync-incomplete-picker"]/a')
+        client.click(id='field.translations_autoimport_mode.2')
+        client.click(xpath='//input[@value="Submit"]')
+        client.waits.forElementProperty(
+            id='upstream-sync-incomplete',
+            option='className|sprite no unseen', timeout=FOR_ELEMENT)
         transaction.commit()
-        self.assertEqual(branch, packaging.productseries.branch)
+        self.assertEqual(sourcepackage.productseries, productseries)
+        self.assertEqual(branch, productseries.branch)
+        self.assertEqual(
+            TranslationsBranchImportMode.IMPORT_TRANSLATIONS,
+            productseries.translations_autoimport_mode)