← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~thumper/launchpad/inline-ppa-chooser into lp:launchpad

 

Tim Penhey has proposed merging lp:~thumper/launchpad/inline-ppa-chooser into lp:launchpad with lp:~thumper/launchpad/recipe-inline-edit-owner as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #423149 Wacky error when using the inline edit for bug task project in Chromium
  https://bugs.launchpad.net/bugs/423149

For more details, see:
https://code.launchpad.net/~thumper/launchpad/inline-ppa-chooser/+merge/46983

The primary purpose of this branch is to add an inline picker
for the daily build PPA.  This brought around much refactoring
and simplification.

lib/canonical/widgets/lazrjs.py
 - cleaned up the docstring for the InlineEditPickerWidget
 - renamed some parameters and removed the jsonification of
   some where it was wrong
lib/canonical/widgets/templates/inline-picker.pt
 - changed the registration code to run on load
lib/canonical/widgets/tests/test_inlineeditpickerwidget.py
 - added tests for the InlineEditPickerWidget

lib/lp/registry/browser/webservice.py => lib/lp/app/browser/webservice.py
lib/lp/registry/browser/webservice.py
lib/lp/app/browser/webservice.py
 - moved the person_xhtml_representation to from lp.registry
   to lp.app and made more generic, and renamed to
   reference_xhtml_representation
 - moved the text_xhtml_representation to lp.app

lib/lp/app/browser/tales.py
 - tweaked format_link to raise if link isn't supported

lib/lp/app/javascript/picker.js
 - renamed non_searchable_vocabulary to show_search_box
 - fixed up the config value existance checks
 - fixed the code where it was checking to see of the content
   uri had changed to use the full_resource_uri

lib/lp/bugs/javascript/bugtask_index.js
 - Remove the webkit special casing for product editing on
   bugtask rows

lib/lp/code/browser/sourcepackagerecipe.py
 - show that daily builds that haven't specified a PPA will
   not get run.
 - add InlineEditPickerWidgets for person and archive

lib/lp/code/model/sourcepackagerecipebuild.py
 - have the daily build code handle the case where the recipe
   doesn't have an archive specified

lib/lp/code/model/tests/test_sourcepackagerecipebuild.py
 - and test it here

lib/lp/code/templates/sourcepackagerecipe-index.pt
 - clean up the page template now that we are using the widget

-- 
https://code.launchpad.net/~thumper/launchpad/inline-ppa-chooser/+merge/46983
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thumper/launchpad/inline-ppa-chooser into lp:launchpad.
=== modified file 'lib/canonical/widgets/lazrjs.py'
--- lib/canonical/widgets/lazrjs.py	2011-01-20 22:32:56 +0000
+++ lib/canonical/widgets/lazrjs.py	2011-01-20 22:33:30 +0000
@@ -18,6 +18,7 @@
 from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
 from zope.component import getUtility
 from zope.security.checker import canAccess, canWrite
+from zope.schema.interfaces import IVocabulary
 from zope.schema.vocabulary import getVocabularyRegistry
 
 from lazr.restful.interfaces import IWebServiceClientRequest
@@ -26,6 +27,7 @@
 from canonical.launchpad.webapp.interfaces import ILaunchBag
 from canonical.launchpad.webapp.publisher import canonical_url
 from canonical.launchpad.webapp.vocabulary import IHugeVocabulary
+from lp.services.propertycache import cachedproperty
 
 
 class TextLineEditorWidget:
@@ -260,21 +262,22 @@
     __call__ = ViewPageTemplateFile('templates/inline-picker.pt')
 
     def __init__(self, context, request, interface_attribute, default_html,
-                 id=None, header='Select an item', step_title='Search',
-                 remove_button_text='Remove', null_display_value='None'):
+                 content_box_id=None, header='Select an item',
+                 step_title='Search', remove_button_text='Remove',
+                 null_display_value='None'):
         """Create a widget wrapper.
 
         :param context: The object that is being edited.
         :param request: The request object.
-        :param interface_attribute: The attribute being edited.
+        :param interface_attribute: The attribute being edited. This should be
+            a field from an interface of the form ISomeInterface['fieldname']
         :param default_html: Default display of attribute.
-        :param id: The HTML id to use for this widget. Automatically
+        :param content_box_id: The HTML id to use for this widget. Automatically
             generated if this is not provided.
         :param header: The large text at the top of the picker.
         :param step_title: Smaller line of text below the header.
-        :param show_remove_button: Show remove button below search box.
-        :param show_assign_me_button: Show assign-me button below search box.
         :param remove_button_text: Override default button text: "Remove"
+        :param null_display_value: This will be shown for a missing value
         """
         self.context = context
         self.request = request
@@ -282,10 +285,10 @@
         self.interface_attribute = interface_attribute
         self.attribute_name = interface_attribute.__name__
 
-        if id is None:
-            self.id = self._generate_id()
+        if content_box_id is None:
+            self.content_box_id = self._generate_id()
         else:
-            self.id = id
+            self.content_box_id = content_box_id
 
         self.header = header
         self.step_title = step_title
@@ -293,12 +296,11 @@
         self.null_display_value = null_display_value
 
         # JSON encoded attributes.
-        self.json_id = simplejson.dumps(self.id)
+        self.json_content_box_id = simplejson.dumps(self.content_box_id)
         self.json_attribute = simplejson.dumps(self.attribute_name + '_link')
-        self.vocabulary_name = simplejson.dumps(
+        self.json_vocabulary_name = simplejson.dumps(
             self.interface_attribute.vocabularyName)
-        self.show_remove_button = simplejson.dumps(
-            not self.interface_attribute.required)
+        self.show_remove_button = not self.interface_attribute.required
 
     @property
     def config(self):
@@ -307,16 +309,25 @@
                  remove_button_text=self.remove_button_text,
                  null_display_value=self.null_display_value,
                  show_remove_button=self.show_remove_button,
-                 show_assign_me_button=self.show_assign_me_button))
+                 show_assign_me_button=self.show_assign_me_button,
+                 show_search_box=self.show_search_box))
+
+    @cachedproperty
+    def vocabulary(self):
+        registry = getVocabularyRegistry()
+        return registry.get(
+            IVocabulary, self.interface_attribute.vocabularyName)
+
+    @property
+    def show_search_box(self):
+        return IHugeVocabulary.providedBy(self.vocabulary)
 
     @property
     def show_assign_me_button(self):
         # show_assign_me_button is true if user is in the vocabulary.
-        registry = getVocabularyRegistry()
-        vocabulary = registry.get(
-            IHugeVocabulary, self.interface_attribute.vocabularyName)
+        vocabulary = self.vocabulary
         user = getUtility(ILaunchBag).user
-        return simplejson.dumps(user and user in vocabulary)
+        return user and user in vocabulary
 
     @classmethod
     def _generate_id(cls):
@@ -325,7 +336,7 @@
         return 'inline-picker-activator-id-%d' % cls.last_id
 
     @property
-    def resource_uri(self):
+    def json_resource_uri(self):
         return simplejson.dumps(
             canonical_url(
                 self.context, request=IWebServiceClientRequest(self.request),

=== modified file 'lib/canonical/widgets/templates/inline-picker.pt'
--- lib/canonical/widgets/templates/inline-picker.pt	2011-01-20 22:32:56 +0000
+++ lib/canonical/widgets/templates/inline-picker.pt	2011-01-20 22:33:30 +0000
@@ -1,4 +1,4 @@
-<span tal:attributes="id view/id">
+<span tal:attributes="id view/content_box_id">
     <span class="yui3-activator-data-box">
     <tal:attribute replace="structure view/default_html"/>
     </span>
@@ -14,11 +14,13 @@
         return;
     }
 
-    Y.lp.app.picker.addPickerPatcher(
-        ${view/vocabulary_name},
-        ${view/resource_uri},
+    Y.on('load', function(e) {
+      Y.lp.app.picker.addPickerPatcher(
+        ${view/json_vocabulary_name},
+        ${view/json_resource_uri},
         ${view/json_attribute},
-        ${view/json_id},
+        ${view/json_content_box_id},
         ${view/config});
+      }, window);
 });
 "/>

=== renamed directory 'lib/canonical/widgets/ftests' => 'lib/canonical/widgets/tests'
=== added file 'lib/canonical/widgets/tests/test_inlineeditpickerwidget.py'
--- lib/canonical/widgets/tests/test_inlineeditpickerwidget.py	1970-01-01 00:00:00 +0000
+++ lib/canonical/widgets/tests/test_inlineeditpickerwidget.py	2011-01-20 22:33:30 +0000
@@ -0,0 +1,57 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the InlineEditPickerWidget."""
+
+__metaclass__ = type
+
+from zope.interface import Interface
+from zope.schema import Choice
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.widgets.lazrjs import InlineEditPickerWidget
+from lp.testing import (
+    login_person,
+    TestCaseWithFactory,
+    )
+
+
+class TestInlineEditPickerWidget(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def getWidget(self, **kwargs):
+        class ITest(Interface):
+            test_field = Choice(**kwargs)
+        return InlineEditPickerWidget(None, None, ITest['test_field'], None)
+
+    def test_huge_vocabulary_is_searchable(self):
+        # Make sure that when given a field for a huge vocabulary, the picker
+        # is set to show the search box.
+        widget = self.getWidget(vocabulary='ValidPersonOrTeam')
+        self.assertTrue(widget.show_search_box)
+
+    def test_normal_vocabulary_is_not_searchable(self):
+        # Make sure that when given a field for a normal vocabulary, the picker
+        # is set to show the search box.
+        widget = self.getWidget(vocabulary='UserTeamsParticipation')
+        self.assertFalse(widget.show_search_box)
+
+    def test_required_fields_dont_have_a_remove_link(self):
+        widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
+        self.assertFalse(widget.show_remove_button)
+
+    def test_optional_fields_do_have_a_remove_link(self):
+        widget = self.getWidget(
+            vocabulary='ValidPersonOrTeam', required=False)
+        self.assertTrue(widget.show_remove_button)
+
+    def test_assign_me_exists_if_user_in_vocabulary(self):
+        widget = self.getWidget(vocabulary='ValidPersonOrTeam', required=True)
+        login_person(self.factory.makePerson())
+        self.assertTrue(widget.show_assign_me_button)
+
+    def test_assign_me_not_shown_if_user_not_in_vocabulary(self):
+        widget = self.getWidget(vocabulary='TargetPPAs', required=True)
+        login_person(self.factory.makePerson())
+        self.assertFalse(widget.show_assign_me_button)

=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml	2011-01-06 01:36:25 +0000
+++ lib/lp/app/browser/configure.zcml	2011-01-20 22:33:30 +0000
@@ -556,4 +556,10 @@
   <adapter
       factory="lp.app.browser.tales.LaunchpadLayerToMainTemplateAdapter"
       />
+
+  <adapter
+      factory="lp.app.browser.webservice.reference_xhtml_representation"/>
+  <adapter
+      factory="lp.app.browser.webservice.text_xhtml_representation"/>
+
 </configure>

=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py	2011-01-14 10:06:25 +0000
+++ lib/lp/app/browser/tales.py	2011-01-20 22:33:30 +0000
@@ -78,7 +78,10 @@
 def format_link(obj, view_name=None):
     """Return the equivalent of obj/fmt:link as a string."""
     adapter = queryAdapter(obj, IPathAdapter, 'fmt')
-    return adapter.link(view_name)
+    link = getattr(adapter, 'link', None)
+    if link is None:
+        raise NotImplementedError("Missing link function on adapter.")
+    return link(view_name)
 
 
 class MenuLinksDict(dict):

=== renamed file 'lib/lp/registry/browser/webservice.py' => 'lib/lp/app/browser/webservice.py'
--- lib/lp/registry/browser/webservice.py	2011-01-20 22:32:56 +0000
+++ lib/lp/app/browser/webservice.py	2011-01-20 22:33:30 +0000
@@ -8,6 +8,7 @@
 
 from lazr.restful.interfaces import (
     IFieldHTMLRenderer,
+    IReference,
     IWebServiceClientRequest,
     )
 from zope import component
@@ -18,22 +19,24 @@
 from zope.schema.interfaces import IText
 
 from lp.app.browser.stringformatter import FormattersAPI
-from lp.app.browser.tales import PersonFormatterAPI
-from lp.services.fields import IPersonChoice
-
-
-@component.adapter(Interface, IPersonChoice, IWebServiceClientRequest)
+from lp.app.browser.tales import format_link
+
+
+@component.adapter(Interface, IReference, IWebServiceClientRequest)
 @implementer(IFieldHTMLRenderer)
-def person_xhtml_representation(context, field, request):
-    """Render a person as a link to the person."""
+def reference_xhtml_representation(context, field, request):
+    """Render an object as a link to the object."""
 
     def render(value):
-        # The value is a webservice link to a person.
-        person = getattr(context, field.__name__, None)
-        if person is None:
+        # The value is a webservice link to the object, we want field value.
+        obj = getattr(context, field.__name__, None)
+        if obj is None:
             return ''
         else:
-            return PersonFormatterAPI(person).link(None)
+            try:
+                return format_link(obj)
+            except NotImplementedError:
+                return value
     return render
 
 

=== modified file 'lib/lp/app/javascript/picker.js'
--- lib/lp/app/javascript/picker.js	2011-01-20 22:32:56 +0000
+++ lib/lp/app/javascript/picker.js	2011-01-20 22:33:30 +0000
@@ -23,10 +23,8 @@
  *         Defaults to false, should be a boolean.
  *     config.show_assign_me_botton: Should the 'assign me' button be shown?
  *         Defaults to false, should be a boolean.
- *     config.non_searchable_vocabulary: No search bar is shown, and the
- *         vocabulary is required to not implement IHugeVocabulary.  The
- *         vocabularies values are then offered in a batched way for
- *         selection.  Defaults to false.
+ *     config.show_search_box: Should the search box be shown.
+ *         Vocabularies that are not huge should not have a search box.
  */
 namespace.addPickerPatcher = function (
     vocabulary, resource_uri, attribute_name,
@@ -40,30 +38,30 @@
     var show_assign_me_button = false;
     var remove_button_text = 'Remove';
     var null_display_value = 'None';
-    var non_searchable_vocabulary = false;
+    var show_search_box = true;
     var full_resource_uri = LP.client.get_absolute_uri(resource_uri);
     var current_context_uri = LP.client.cache['context']['self_link'];
     var editing_main_context = (full_resource_uri == current_context_uri);
 
     if (config !== undefined) {
-        if (config.remove_button_text) {
+        if (config.remove_button_text !== undefined) {
             remove_button_text = config.remove_button_text;
         }
 
-        if (config.null_display_value) {
+        if (config.null_display_value !== undefined) {
             null_display_value = config.null_display_value;
         }
 
-        if (config.show_remove_button) {
+        if (config.show_remove_button !== undefined) {
             show_remove_button = config.show_remove_button;
         }
 
-        if (config.show_assign_me_button) {
+        if (config.show_assign_me_button !== undefined) {
             show_assign_me_button = config.show_assign_me_button;
         }
 
-        if (config.non_searchable_vocabulary) {
-            non_searchable_vocabulary = config.non_searchable_vocabulary;
+        if (config.show_search_box !== undefined) {
+            show_search_box = config.show_search_box;
         }
     }
 
@@ -125,7 +123,7 @@
                     } else if (current_field == 'self_link') {
                         picker._resource_uri = element.get('innerHTML');
                         content_uri_has_changed = (
-                            resource_uri != picker._resource_uri);
+                            full_resource_uri != picker._resource_uri);
                     }
                 }
             });
@@ -139,7 +137,7 @@
               // fix this.
               var new_url = picker._resource_uri.replace('/api/devel', '');
               window.location = new_url;
-            }
+          }
         };
 
         var patch_payload = {};
@@ -215,7 +213,7 @@
     picker.set('footer_slot', extra_buttons);
 
     activator.subscribe('act', function (e) {
-        if (non_searchable_vocabulary) {
+        if (!show_search_box) {
           picker.set('min_search_chars', 0);
           picker.fire('search', '');
           picker.get('contentBox').one('.yui3-picker-search-box').addClass('unseen');

=== modified file 'lib/lp/bugs/javascript/bugtask_index.js'
--- lib/lp/bugs/javascript/bugtask_index.js	2011-01-20 22:32:56 +0000
+++ lib/lp/bugs/javascript/bugtask_index.js	2011-01-20 22:33:30 +0000
@@ -1352,21 +1352,13 @@
     if ((LP.client.links.me !== undefined) && (LP.client.links.me !== null))  {
         if (Y.Lang.isValue(bugtarget_content)) {
             if (conf.target_is_product) {
-                if  (Y.UA.webkit) {
-                    bugtarget_content.replaceChild(
-                        Y.DOM.create(
-                            '<a href="+editstatus" ' +
-                            '   class="sprite edit yui3-activator-act" />'),
-                        bugtarget_content.one('.yui3-activator-act'));
-                } else {
-                    var bugtarget_picker = Y.lp.app.picker.addPickerPatcher(
+              var bugtarget_picker = Y.lp.app.picker.addPickerPatcher(
                         'Product',
                         conf.bugtask_path,
                         "target_link",
                         bugtarget_content.get('id'),
                         {"step_title": "Search products",
                          "header": "Change product"});
-                 }
             }
         }
 

=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py	2011-01-18 18:44:11 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py	2011-01-20 22:33:30 +0000
@@ -58,11 +58,12 @@
     )
 from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
-from canonical.widgets.suggestion import RecipeOwnerWidget
 from canonical.widgets.itemswidgets import (
     LabeledMultiCheckBoxWidget,
     LaunchpadRadioWidget,
     )
+from canonical.widgets.lazrjs import InlineEditPickerWidget
+from canonical.widgets.suggestion import RecipeOwnerWidget
 from lp.app.browser.launchpadform import (
     action,
     custom_widget,
@@ -182,7 +183,12 @@
         super(SourcePackageRecipeView, self).initialize()
         self.request.response.addWarningNotification(RECIPE_BETA_MESSAGE)
         recipe = self.context
-        if self.dailyBuildWithoutUploadPermission():
+        if recipe.build_daily and recipe.daily_build_archive is None:
+            self.request.response.addWarningNotification(
+                structured(
+                    "Daily builds for this recipe will <strong>not</strong> "
+                    "occur.<br/><br/>There is no PPA."))
+        elif self.dailyBuildWithoutUploadPermission():
             self.request.response.addWarningNotification(
                 structured(
                     "Daily builds for this recipe will <strong>not</strong> "
@@ -226,6 +232,30 @@
             return not has_upload
         return False
 
+    @property
+    def person_picker(self):
+        return InlineEditPickerWidget(
+            self.context, self.request, ISourcePackageRecipe['owner'],
+            format_link(self.context.owner),
+            content_box_id='recipe-owner',
+            header='Change owner',
+            step_title='Select a new owner')
+
+    @property
+    def archive_picker(self):
+        ppa = self.context.daily_build_archive
+        if ppa is None:
+            initial_html = 'None'
+        else:
+            initial_html = format_link(ppa)
+        return InlineEditPickerWidget(
+            self.context, self.request,
+            ISourcePackageAddSchema['daily_build_archive'],
+            initial_html,
+            content_box_id='recipe-ppa',
+            header='Change daily build archive',
+            step_title='Select a PPA')
+
 
 class SourcePackageRecipeRequestBuildsView(LaunchpadFormView):
     """A view for requesting builds of a SourcePackageRecipe."""

=== modified file 'lib/lp/code/model/sourcepackagerecipebuild.py'
--- lib/lp/code/model/sourcepackagerecipebuild.py	2011-01-15 06:32:40 +0000
+++ lib/lp/code/model/sourcepackagerecipebuild.py	2011-01-20 22:33:30 +0000
@@ -194,8 +194,12 @@
             logger = logging.getLogger()
         builds = []
         for recipe in recipes:
+            recipe.is_stale = False
             logger.debug(
                 'Recipe %s/%s is stale', recipe.owner.name, recipe.name)
+            if recipe.daily_build_archive is None:
+                logger.debug(' - No daily build archive specified.')
+                continue
             for distroseries in recipe.distroseries:
                 series_name = distroseries.named_version
                 try:
@@ -215,7 +219,6 @@
                 else:
                     logger.debug(' - build requested for %s', series_name)
                     builds.append(build)
-            recipe.is_stale = False
         return builds
 
     def _unqueueBuild(self):

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2011-01-10 03:22:42 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipebuild.py	2011-01-20 22:33:30 +0000
@@ -235,13 +235,21 @@
         self.assertIs(None, build.manifest)
 
     def test_makeDailyBuilds(self):
-        self.assertEqual([],
-            SourcePackageRecipeBuild.makeDailyBuilds())
+        self.assertEqual([], SourcePackageRecipeBuild.makeDailyBuilds())
         recipe = self.factory.makeSourcePackageRecipe(build_daily=True)
-        build = SourcePackageRecipeBuild.makeDailyBuilds()[0]
+        [build] = SourcePackageRecipeBuild.makeDailyBuilds()
         self.assertEqual(recipe, build.recipe)
         self.assertEqual(list(recipe.distroseries), [build.distroseries])
 
+    def test_makeDailyBuilds_skips_missing_archive(self):
+        """When creating daily builds, skip ones that are already pending."""
+        recipe = self.factory.makeSourcePackageRecipe(
+            build_daily=True, is_stale=True)
+        with person_logged_in(recipe.owner):
+            recipe.daily_build_archive = None
+        builds = SourcePackageRecipeBuild.makeDailyBuilds()
+        self.assertEqual([], builds)
+
     def test_makeDailyBuilds_logs_builds(self):
         # If a logger is passed into the makeDailyBuilds method, each recipe
         # that a build is requested for gets logged.

=== modified file 'lib/lp/code/templates/sourcepackagerecipe-index.pt'
--- lib/lp/code/templates/sourcepackagerecipe-index.pt	2011-01-20 22:32:56 +0000
+++ lib/lp/code/templates/sourcepackagerecipe-index.pt	2011-01-20 22:33:30 +0000
@@ -13,27 +13,6 @@
       padding-left: 2em;
     }
   </style>
-  <script type="text/javascript"
-          tal:content="string:
-    LPS.use('lp.app.picker', function(Y) {
-        Y.on('load', function(e) {
-
-           var config = {
-              header: 'Change owner',
-              step_title: 'Select a new owner',
-              non_searchable_vocabulary: true
-           };
-
-           Y.lp.app.picker.addPickerPatcher(
-               'UserTeamsParticipationPlusSelf',
-               LP.client.cache['context']['self_link'],
-               'owner_link',
-               'recipe-owner',
-               config);
-
-        }, window);
-    });
-  "/>
 </metal:block>
 
 <body>
@@ -76,17 +55,7 @@
 
           <dl id="owner">
             <dt>Owner:</dt>
-            <dd>
-              <span id="recipe-owner">
-                <span class="yui3-activator-data-box">
-                  <tal:owner replace="structure context/owner/fmt:link" />
-                </span>
-                <button class="lazr-btn yui3-activator-act yui3-activator-hidden"
-                        tal:condition="context/required:launchpad.Edit">
-                  Edit
-                </button>
-                <div class="yui3-activator-message-box yui3-activator-hidden" />
-              </span>
+            <dd tal:content="structure view/person_picker"/>
           </dl>
           <dl id="base-branch">
             <dt>Base branch:</dt>
@@ -98,10 +67,7 @@
           </dl>
           <dl id="daily_build_archive">
             <dt>Daily build archive:</dt>
-            <dd tal:content="structure context/daily_build_archive/fmt:link"
-                tal:condition="context/daily_build_archive">
-            </dd>
-            <dd tal:condition="not: context/daily_build_archive">None</dd>
+            <dd tal:content="structure view/archive_picker"/>
           </dl>
 
           <dl id="distros">

=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2011-01-19 22:53:32 +0000
+++ lib/lp/registry/browser/configure.zcml	2011-01-20 22:33:30 +0000
@@ -2328,9 +2328,4 @@
         rootsite="api"
         attribute_to_parent="owner" />
 
-    <adapter
-        factory="lp.registry.browser.webservice.person_xhtml_representation"/>
-    <adapter
-        factory="lp.registry.browser.webservice.text_xhtml_representation"/>
-
 </configure>

=== modified file 'versions.cfg'
--- versions.cfg	2011-01-20 22:32:56 +0000
+++ versions.cfg	2011-01-20 22:33:30 +0000
@@ -33,7 +33,7 @@
 lazr.delegates = 1.2.0
 lazr.enum = 1.1.2
 lazr.lifecycle = 1.1
-lazr.restful = 0.15.1
+lazr.restful = 0.15.2
 lazr.restfulclient = 0.11.1
 lazr.smtptest = 1.1
 lazr.testing = 0.1.1