← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/js-translation into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/js-translation/+merge/55309

This branch prepares the translation sharing status page for
"YUIfication" (on which Aaron is working).

The page shows texts like "Translation sharing configuration is
incomplete" / "Translation sharing with upstream is active" and
similar texts for the four configuration steps required to enabled
translation sharing.

The base idea for the YUIfication is to always include all text
variants, but to hide the "wrong" texts via CSS.

This is implemented by browser class properties like
packaging_incomplete_class, packaging_complete_class,
branch_incomplete_class etc.

If an upstream product series has no source code branch set, we
want a link to the branch configuration page showing the "add"
icon (the one with the "plus" symbol), while we want a link with
the "edit" icon (showing a pencil) for the same target, if a
branch is already defined.

The former link is shown together with the text "No source branch
exists for the upstream series"; the latter link is shown together
with the text "Upstream source branch is...". As already
mentioned, we want to render both variants, regardless, if a
branch is already configured or not. For two reasons, this cannot
be done with the usual
tal:replace="structure productseries/menu:overview/set_branch/fmt:icon"

1. The link lp.registry.browser.productseries.\
   ProductSeriesOverviewMenu.link_branch automatically uses
   the "add" icon, if no branch is defined, and it uses the
   "edit" icon, if a branch is configured. But we want the
   "edit" icon for the "Change branch" varaint, even if at rendering
   time no branch is defined.
2. We cannot use the link at all, if no packaging link is defined
   when the page is rendered, because its target is not yet known.

Hence the links are created by the browser class properties
new_branch_link and change_branch_link.

Similar changes are still missing for the links to configure
upstream translations and upstream translation synchronisation,
but the diff for this branch is already large enough, so I'll
add this in a follow-up branch.

test: ./bin/test translations -vvt test_sharing_details

no lint.

-- 
https://code.launchpad.net/~adeuring/launchpad/js-translation/+merge/55309
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/js-translation into lp:launchpad.
=== modified file 'lib/lp/translations/browser/sourcepackage.py'
--- lib/lp/translations/browser/sourcepackage.py	2011-03-24 20:48:35 +0000
+++ lib/lp/translations/browser/sourcepackage.py	2011-03-29 10:46:20 +0000
@@ -11,6 +11,8 @@
     'SourcePackageTranslationSharingStatus',
     ]
 
+import cgi
+
 from zope.publisher.interfaces import NotFound
 
 from canonical.launchpad.webapp import (
@@ -23,6 +25,7 @@
 from canonical.launchpad.webapp.menu import structured
 from canonical.launchpad.webapp.publisher import LaunchpadView
 from lp.app.enums import ServiceUsage
+from lp.registry.browser.productseries import ProductSeriesOverviewMenu
 from lp.registry.interfaces.sourcepackage import ISourcePackage
 from lp.services.features import getFeatureFlag
 from lp.translations.browser.poexportrequest import BaseExportView
@@ -139,6 +142,86 @@
                 'shared with the upstream project.')
 
     @property
+    def branch_link(self):
+        if self.has_upstream_branch:
+            # Normally should use BranchFormatterAPI(branch).link(None), but
+            # on this page, that information is redundant.
+            title = cgi.escape(self.upstream_branch.unique_name)
+            url = canonical_url(self.upstream_branch)
+        else:
+            title = ''
+            url = '#'
+        return '<a class="sprite branch link" href="%s">%s</a>' % (url, title)
+
+    def makeConfigCompleteCSS(self, complete, disable, lowlight):
+        if complete:
+            classes = ['sprite', 'yes']
+        else:
+            classes = ['sprite', 'no']
+        if disable:
+            classes.append('unseen')
+        if lowlight:
+            classes.append("lowlight")
+        return ' '.join(classes)
+
+    @property
+    def configuration_complete_class(self):
+        if self.is_configuration_complete:
+            return ""
+        return "unseen"
+
+    @property
+    def configuration_incomplete_class(self):
+        if not self.is_configuration_complete:
+            return ""
+        return "unseen"
+
+    @property
+    def packaging_incomplete_class(self):
+        return self.makeConfigCompleteCSS(
+            False, self.is_packaging_configured, False)
+
+    @property
+    def packaging_complete_class(self):
+        return self.makeConfigCompleteCSS(
+            True, not self.is_packaging_configured, False)
+
+    @property
+    def branch_incomplete_class(self):
+        return self.makeConfigCompleteCSS(
+            False, self.has_upstream_branch, not self.is_packaging_configured)
+
+    @property
+    def branch_complete_class(self):
+        return self.makeConfigCompleteCSS(
+            True, not self.has_upstream_branch,
+            not self.is_packaging_configured)
+
+    @property
+    def translations_disabled_class(self):
+        return self.makeConfigCompleteCSS(
+            False, self.is_upstream_translations_enabled,
+            not self.is_packaging_configured)
+
+    @property
+    def translations_enabled_class(self):
+        return self.makeConfigCompleteCSS(
+            True, not self.is_upstream_translations_enabled,
+            not self.is_packaging_configured)
+
+    @property
+    def upstream_sync_disabled_class(self):
+        return self.makeConfigCompleteCSS(
+            False, self.is_upstream_synchronization_enabled,
+            not self.is_packaging_configured)
+
+    @property
+    def upstream_sync_enabled_class(self):
+        return self.makeConfigCompleteCSS(
+            True, not self.is_upstream_synchronization_enabled,
+            not self.is_packaging_configured)
+
+    @property
     def is_packaging_configured(self):
         """Is a packaging link defined for this branch?"""
         return self.context.direct_packaging is not None
@@ -153,11 +236,15 @@
             return css_class + " lowlight"
 
     @property
+    def upstream_branch(self):
+        if not self.is_packaging_configured:
+            return None
+        return self.context.direct_packaging.productseries.branch
+
+    @property
     def has_upstream_branch(self):
         """Does the upstream series have a source code branch?"""
-        if not self.is_packaging_configured:
-            return False
-        return self.context.direct_packaging.productseries.branch is not None
+        return self.upstream_branch is not None
 
     @property
     def is_upstream_translations_enabled(self):
@@ -236,3 +323,43 @@
                         }
         info = info.values()
         return sorted(info, key=lambda template: template['name'])
+
+    def icon_link(self, id, icon, url, text, hidden):
+        """The HTML link to a configuration page."""
+        if hidden:
+            css_class = 'sprite %s unseen' % icon
+        else:
+            css_class = 'sprite %s' % icon
+        return (
+            '<a id="%s" class="%s" href="%s">'
+            '<span class="invisible-link">%s</span></a>'
+            % (id, css_class, url, text))
+
+    def edit_branch_link(self, id, icon, text):
+        """The HTML link to define or edit a product series branch.
+
+        If a product is linked to the source package and if the current
+        user has the permission to define the branch, a real link is
+        returned, otherwise a hidden dummy link is returned.
+        """
+        packaging = self.context.direct_packaging
+        if packaging is not None:
+            productseries = self.context.direct_packaging.productseries
+            productseries_menu = ProductSeriesOverviewMenu(productseries)
+            branch_link = productseries_menu.link_branch()
+            url = '%s/%s' % (canonical_url(productseries), branch_link.target)
+            if branch_link.enabled:
+                return self.icon_link(id, icon, url, text, hidden=False)
+            else:
+                return self.icon_link(id, icon, url, text, hidden=True)
+        return self.icon_link(id, icon, '#', text, hidden=True)
+
+    @property
+    def new_branch_link(self):
+        """The HTML link to define a product series branch."""
+        return self.edit_branch_link('add-branch', 'add', 'Link to branch')
+
+    @property
+    def change_branch_link(self):
+        """The HTML link to change a product series branch."""
+        return self.edit_branch_link('change-branch', 'edit', 'Change branch')

=== modified file 'lib/lp/translations/browser/tests/test_sharing_details.py'
--- lib/lp/translations/browser/tests/test_sharing_details.py	2011-03-23 16:28:51 +0000
+++ lib/lp/translations/browser/tests/test_sharing_details.py	2011-03-29 10:46:20 +0000
@@ -3,6 +3,13 @@
 
 __metaclass__ = type
 
+
+import re
+from soupmatchers import (
+    HTMLContains,
+    Tag,
+)
+
 from canonical.launchpad.testing.pages import (
     extract_text,
     find_tag_by_id,
@@ -328,54 +335,215 @@
         distroseries = self.factory.makeUbuntuDistroSeries()
         return self.factory.makeSourcePackage(distroseries=distroseries)
 
-    def _getSharingDetailsViewBrowser(self, sourcepackage):
+    def _getSharingDetailsViewBrowser(self, sourcepackage, user=None):
+        if user is None:
+            no_login = True
+        else:
+            no_login = False
         return self.getViewBrowser(
-            sourcepackage, no_login=True, rootsite="translations",
-            view_name="+sharing-details")
+            sourcepackage, no_login=no_login, rootsite="translations",
+            view_name="+sharing-details", user=user)
+
+    def assertUnseen(self, browser, html_id):
+        unseen_matcher = Tag(html_id, 'li', attrs={
+            'id': html_id,
+            'class': lambda v: v and 'unseen' in v.split(' ')})
+        self.assertThat(browser.contents, HTMLContains(unseen_matcher))
+
+    def assertSeen(self, browser, html_id, dimmed=False):
+        seen_matcher = Tag(html_id, 'li', attrs={
+            'id': html_id,
+            'class': lambda v: v and 'unseen' not in v.split(' ')})
+        self.assertThat(browser.contents, HTMLContains(seen_matcher))
+        if dimmed:
+            dimmed_matcher = Tag(html_id, 'li', attrs={
+            'id': html_id,
+            'class': lambda v: v and 'lowlight' in v.split(' ')})
+        else:
+            dimmed_matcher = Tag(html_id, 'li', attrs={
+            'id': html_id,
+            'class': lambda v: v and 'lowlight' not in v.split(' ')})
+        self.assertThat(browser.contents, HTMLContains(dimmed_matcher))
+
+    def assertStatusDisplayShowsIncomplete(self, browser):
+        seen_matcher = Tag(
+            'configuration-incomplete', 'span',
+            attrs={
+                'id': 'configuration-incomplete',
+                'class': '',
+                })
+        self.assertThat(browser.contents, HTMLContains(seen_matcher))
+        unseen_matcher = Tag(
+            'configuration-complete', 'span',
+            attrs={
+                'id': 'configuration-complete',
+                'class': 'unseen',
+                })
+        self.assertThat(browser.contents, HTMLContains(unseen_matcher))
+
+    def assertStatusDisplayShowsCompleted(self, browser):
+        seen_matcher = Tag(
+            'configuration-complete', 'span',
+            attrs={
+                'id': 'configuration-complete',
+                'class': '',
+                })
+        self.assertThat(browser.contents, HTMLContains(seen_matcher))
+        unseen_matcher = Tag(
+            'configuration-incomplete', 'span',
+            attrs={
+                'id': 'configuration-incomplete',
+                'class': 'unseen',
+                })
+        self.assertThat(browser.contents, HTMLContains(unseen_matcher))
+
+    def assertElementText(self, browser, id, expected):
+        node = find_tag_by_id(browser.contents, id)
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            expected, extract_text(node))
+
+    def assertContentComplete(self, browser):
+        # The HTML data contains always all text variants.
+        checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
+        self.assertIsNot(None, checklist)
+        self.assertTextMatchesExpressionIgnoreWhitespace("""
+            Translation sharing configuration is incomplete.
+            Translation sharing with upstream is active.
+            No upstream project series has been linked.
+            Set upstream link
+            Linked upstream series is .*
+            Change upstream link
+            Remove upstream link
+            No source branch exists for the upstream series.
+            Link to branch
+            Upstream source branch is.*
+            Change branch
+            Translations are not enabled on the upstream project.
+            Translations are enabled on the upstream project.
+            Automatic synchronization of translations is not enabled.
+            Automatic synchronization of translations is enabled.""",
+            extract_text(checklist))
+        self.assertElementText(
+            browser, 'packaging-incomplete',
+            'No upstream project series has been linked.')
+        self.assertElementText(
+            browser, 'packaging-complete', 'Linked upstream series is .*')
+        self.assertElementText(
+            browser, 'packaging-complete', 'Linked upstream series is .*')
+        self.assertElementText(
+            browser, 'branch-incomplete',
+            'No source branch exists for the upstream series.')
+        self.assertElementText(
+            browser, 'branch-complete', 'Upstream source branch is .*')
+        self.assertElementText(
+            browser, 'translation-incomplete',
+            'Translations are not enabled on the upstream project.')
+        self.assertElementText(
+            browser, 'translation-complete',
+            'Translations are enabled on the upstream project.')
 
     def test_checklist_unconfigured(self):
         # Without a packaging link, sharing is completely unconfigured
         sourcepackage = self._makeSourcePackage()
         browser = self._getSharingDetailsViewBrowser(sourcepackage)
-        checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
-        self.assertIsNot(None, checklist)
-        self.assertTextMatchesExpressionIgnoreWhitespace("""
-            Translation sharing configuration is incomplete.
-            No upstream project series has been linked. Change upstream link
-            No source branch exists for the upstream series.
-            Translations are not enabled on the upstream series.
-            Automatic synchronization of translations is not enabled.""",
-            extract_text(checklist))
+        self.assertContentComplete(browser)
+        self.assertStatusDisplayShowsIncomplete(browser)
+        self.assertUnseen(browser, 'packaging-complete')
+        self.assertSeen(browser, 'branch-incomplete', dimmed=True)
+        self.assertUnseen(browser, 'branch-complete')
+        self.assertSeen(browser, 'translation-incomplete', dimmed=True)
+        self.assertUnseen(browser, 'translation-complete')
+        self.assertSeen(browser, 'upstream-sync-incomplete', dimmed=True)
+        self.assertUnseen(browser, 'upstream-sync-complete')
 
-    def test_checklist_partly_configured(self):
+    def test_checklist_packaging_configured(self):
         # Linking a source package takes care of one item.
-        packaging = self.factory.makePackagingLink(in_ubuntu=True)
-        browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
-        checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
-        self.assertIsNot(None, checklist)
-        self.assertTextMatchesExpressionIgnoreWhitespace("""
-            Translation sharing configuration is incomplete.
-            Linked upstream series is .+ trunk series.
-                Change upstream link Remove upstream link
-            No source branch exists for the upstream series.
-            Translations are not enabled on the upstream series.
-            Automatic synchronization of translations is not enabled.""",
-            extract_text(checklist))
+        # The other configuration elements are not dimmed.
+        packaging = self.factory.makePackagingLink(in_ubuntu=True)
+        browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
+        self.assertContentComplete(browser)
+        self.assertStatusDisplayShowsIncomplete(browser)
+        self.assertUnseen(browser, 'packaging-incomplete')
+        self.assertSeen(browser, 'packaging-complete')
+        self.assertSeen(browser, 'branch-incomplete')
+        self.assertUnseen(browser, 'branch-complete')
+        self.assertSeen(browser, 'translation-incomplete')
+        self.assertUnseen(browser, 'translation-complete')
+        self.assertSeen(browser, 'upstream-sync-incomplete')
+        self.assertUnseen(browser, 'upstream-sync-complete')
+
+    def test_checklist_packaging_and_branch_configured(self):
+        # Linking a source package and and setting an upstream branch
+        # changes the text displayed for the branch configuration.
+        packaging = self.factory.makePackagingLink(in_ubuntu=True)
+        self.configureUpstreamProject(
+            productseries=packaging.productseries, set_upstream_branch=True)
+        browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
+        self.assertContentComplete(browser)
+        self.assertStatusDisplayShowsIncomplete(browser)
+        self.assertUnseen(browser, 'packaging-incomplete')
+        self.assertSeen(browser, 'packaging-complete')
+        self.assertUnseen(browser, 'branch-incomplete')
+        self.assertSeen(browser, 'branch-complete')
+        self.assertSeen(browser, 'translation-incomplete')
+        self.assertUnseen(browser, 'translation-complete')
+        self.assertSeen(browser, 'upstream-sync-incomplete')
+        self.assertUnseen(browser, 'upstream-sync-complete')
+
+    def test_checklist_packaging_and_translations_enabled(self):
+        # Linking a source package and and setting an upstream branch
+        # changes the text displayed for the translation setting.
+        packaging = self.factory.makePackagingLink(in_ubuntu=True)
+        self.configureUpstreamProject(
+            productseries=packaging.productseries,
+            translations_usage=ServiceUsage.LAUNCHPAD)
+        browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
+        self.assertContentComplete(browser)
+        self.assertStatusDisplayShowsIncomplete(browser)
+        self.assertUnseen(browser, 'packaging-incomplete')
+        self.assertSeen(browser, 'packaging-complete')
+        self.assertSeen(browser, 'branch-incomplete')
+        self.assertUnseen(browser, 'branch-complete')
+        self.assertUnseen(browser, 'translation-incomplete')
+        self.assertSeen(browser, 'translation-complete')
+        self.assertSeen(browser, 'upstream-sync-incomplete')
+        self.assertUnseen(browser, 'upstream-sync-complete')
+
+    def test_checklist_packaging_and_upstream_sync_enabled(self):
+        # Linking a source package and enabling upstream translation
+        # synchronisation changes the text displayed for the
+        # translation sync setting.
+        packaging = self.factory.makePackagingLink(in_ubuntu=True)
+        self.configureUpstreamProject(
+            productseries=packaging.productseries,
+            translation_import_mode=(
+                TranslationsBranchImportMode.IMPORT_TRANSLATIONS))
+        browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
+        self.assertContentComplete(browser)
+        self.assertStatusDisplayShowsIncomplete(browser)
+        self.assertUnseen(browser, 'packaging-incomplete')
+        self.assertSeen(browser, 'packaging-complete')
+        self.assertSeen(browser, 'branch-incomplete')
+        self.assertUnseen(browser, 'branch-complete')
+        self.assertSeen(browser, 'translation-incomplete')
+        self.assertUnseen(browser, 'translation-complete')
+        self.assertUnseen(browser, 'upstream-sync-incomplete')
+        self.assertSeen(browser, 'upstream-sync-complete')
 
     def test_checklist_fully_configured(self):
         # A fully configured sharing setup.
         sourcepackage = self.makeFullyConfiguredSharing()[0]
         browser = self._getSharingDetailsViewBrowser(sourcepackage)
-        checklist = find_tag_by_id(browser.contents, 'sharing-checklist')
-        self.assertIsNot(None, checklist)
-        self.assertTextMatchesExpressionIgnoreWhitespace("""
-            Translation sharing with upstream is active.
-            Linked upstream series is .+ trunk series.
-                Change upstream link Remove upstream link
-            Upstream source branch is .+[.]
-            Translations are enabled on the upstream project.
-            Automatic synchronization of translations is enabled.""",
-            extract_text(checklist))
+        self.assertContentComplete(browser)
+        self.assertStatusDisplayShowsCompleted(browser)
+        self.assertUnseen(browser, 'packaging-incomplete')
+        self.assertSeen(browser, 'packaging-complete')
+        self.assertUnseen(browser, 'branch-incomplete')
+        self.assertSeen(browser, 'branch-complete')
+        self.assertUnseen(browser, 'translation-incomplete')
+        self.assertSeen(browser, 'translation-complete')
+        self.assertUnseen(browser, 'upstream-sync-incomplete')
+        self.assertSeen(browser, 'upstream-sync-complete')
 
     def test_potlist_only_ubuntu(self):
         # Without a packaging link, only Ubuntu templates are listed.
@@ -439,6 +607,78 @@
             foo-template  linking""",
             extract_text(tbody))
 
+    def assertBranchLinks(self, contents, real_links, enabled):
+        if real_links:
+            match = (
+                r'^http://translations.launchpad.dev/.*/trunk/\+linkbranch$')
+            def link_matcher(url):
+                return re.search(match, url)
+        else:
+            link_matcher = '#'
+        if enabled:
+            css_class = 'sprite add'
+        else:
+            css_class = 'sprite add unseen'
+        matcher = Tag('add-branch', 'a', attrs={
+            'id': 'add-branch',
+            'href': link_matcher,
+            'class': css_class})
+        self.assertThat(contents, HTMLContains(matcher))
+        if enabled:
+            css_class = 'sprite edit'
+        else:
+            css_class = 'sprite edit unseen'
+        matcher = Tag('change-branch', 'a', attrs={
+            'id': 'change-branch',
+            'href': link_matcher,
+            'class': css_class})
+        self.assertThat(contents, HTMLContains(matcher))
+
+    def test_edit_branch_links__no_packaging_link(self):
+        # If no packaging link exists, new_branch_link and edit_branch_link
+        # return hidden dummy links.
+        sourcepackage = self._makeSourcePackage()
+        browser = self._getSharingDetailsViewBrowser(sourcepackage)
+        self.assertBranchLinks(
+            browser.contents, real_links=False, enabled=False)
+
+    def test_edit_branch_links__with_packaging_link__anon_user(self):
+        # If a packaging link exists, new_branch_link and edit_branch_link
+        # return hidden links which point to the product series
+        # branch configuration page for anonymous users.
+        packaging = self.factory.makePackagingLink(in_ubuntu=True)
+        self.configureUpstreamProject(
+            productseries=packaging.productseries)
+        browser = self._getSharingDetailsViewBrowser(packaging.sourcepackage)
+        self.assertBranchLinks(
+            browser.contents, real_links=True, enabled=False)
+
+    def test_edit_branch_links__with_packaging_link__unprivileged_user(self):
+        # If a packaging link exists, new_branch_link and edit_branch_link
+        # return hidden links which point to the product series
+        # branch configuration page for users which cannot change the
+        # branch of the product series.
+        packaging = self.factory.makePackagingLink(in_ubuntu=True)
+        self.configureUpstreamProject(
+            productseries=packaging.productseries)
+        browser = self._getSharingDetailsViewBrowser(
+            packaging.sourcepackage, user=self.factory.makePerson())
+        self.assertBranchLinks(
+            browser.contents, real_links=True, enabled=False)
+
+    def test_edit_branch_links__with_packaging_link__privileged_user(self):
+        # If a packaging link exists, new_branch_link and edit_branch_link
+        # return links which point to the product series
+        # branch configuration page for users which can change the
+        # branch of the product series.
+        packaging = self.factory.makePackagingLink(in_ubuntu=True)
+        self.configureUpstreamProject(
+            productseries=packaging.productseries)
+        browser = self._getSharingDetailsViewBrowser(
+            packaging.sourcepackage, user=packaging.productseries.owner)
+        self.assertBranchLinks(
+            browser.contents, real_links=True, enabled=True)
+
 
 class TestTranslationSharingDetailsViewNotifications(TestCaseWithFactory,
                                                      ConfigureScenarioMixin):

=== modified file 'lib/lp/translations/templates/sourcepackage-sharing-details.pt'
--- lib/lp/translations/templates/sourcepackage-sharing-details.pt	2011-03-23 16:28:51 +0000
+++ lib/lp/translations/templates/sourcepackage-sharing-details.pt	2011-03-29 10:46:20 +0000
@@ -12,62 +12,65 @@
     </div>
     <div metal:fill-slot="main">
       <dl id="sharing-checklist">
-        <dt><span tal:condition="not:view/is_configuration_complete">
+        <dt><span tal:attributes="class view/configuration_incomplete_class"
+                  id="configuration-incomplete">
             Translation sharing configuration is incomplete.
           </span>
-          <span tal:condition="view/is_configuration_complete">
+          <span tal:attributes="class view/configuration_complete_class"
+                id="configuration-complete">
             Translation sharing with upstream is active.
           </span>
         </dt>
         <dd>
           <ul>
-            <li class="sprite no"
-                tal:condition="not:view/is_packaging_configured">
+            <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/edit_packaging/fmt:icon" />
+                <a tal:replace="structure context/menu:overview/set_upstream/fmt:icon" />
             </li>
-            <li class="sprite yes"
-                tal:condition="view/is_packaging_configured">
+            <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" />
               <a tal:replace="structure context/menu:overview/remove_packaging/fmt:icon" />
             </li>
-            <li tal:attributes="class view/no_item_class"
-                tal:condition="not:view/has_upstream_branch">
+            <li tal:attributes="class view/branch_incomplete_class"
+                id="branch-incomplete">
               No source branch exists for the upstream series.
-              <a tal:condition="view/is_packaging_configured"
-                 tal:replace="structure context/productseries/menu:overview/set_branch/fmt:icon" />
-            </li>
-            <li class="sprite yes"
-                tal:condition="view/has_upstream_branch">
-              Upstream source branch is
-              <a tal:replace="structure context/productseries/branch/fmt:link">
-                lp:gimp</a>.
-              <a tal:condition="view/is_packaging_configured"
-                 tal:replace="structure context/productseries/menu:overview/set_branch/fmt:icon" />
-            </li>
-            <li tal:attributes="class view/no_item_class"
-                tal:condition="not:view/is_upstream_translations_enabled">
-              Translations are not enabled on the upstream series.
+              <span id="branch-incomplete-picker">
+                <a tal:replace="structure view/new_branch_link" />
+              </span>
+            </li>
+            <li tal:attributes="class view/branch_complete_class"
+                id="branch-complete">
+            Upstream source branch is
+              <span id="branch-complete-picker">
+                <a tal:replace="structure view/branch_link">lp:gimp</a>
+                <a tal:replace="structure view/change_branch_link" />
+              </span>
+            </li>
+            <li tal:attributes="class view/translations_disabled_class"
+                id="translation-incomplete">
+              Translations are not enabled on the upstream project.
               <a tal:condition="view/is_packaging_configured"
                  tal:replace="structure context/productseries/product/menu:translations/settings/fmt:icon" />
             </li>
-            <li class="sprite yes"
-                tal:condition="view/is_upstream_translations_enabled">
+            <li tal:attributes="class view/translations_enabled_class"
+                id="translation-complete">
               Translations are enabled on the upstream project.
               <a tal:condition="view/is_packaging_configured"
                  tal:replace="structure context/productseries/product/menu:translations/settings/fmt:icon" />
             </li>
-            <li tal:attributes="class view/no_item_class"
-                tal:condition="not:view/is_upstream_synchronization_enabled">
+            <li tal:attributes="class view/upstream_sync_disabled_class"
+                id="upstream-sync-incomplete">
               Automatic synchronization of translations is not enabled.
               <a tal:condition="view/is_packaging_configured"
                  tal:replace="structure context/productseries/menu:translations/settings/fmt:icon" />
             </li>
-            <li class="sprite yes"
-                tal:condition="view/is_upstream_synchronization_enabled">
+            <li tal:attributes="class view/upstream_sync_enabled_class"
+                id="upstream-sync-complete">
               Automatic synchronization of translations is enabled.
               <a tal:condition="view/is_packaging_configured"
                  tal:replace="structure context/productseries/menu:translations/settings/fmt:icon" />