Merge lp:~michael.nelson/launchpad/652838-select-diffs-for-syncing into lp:launchpad/db-devel

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Nelson
Approved revision: no longer in the source branch.
Merged at revision: 9863
Proposed branch: lp:~michael.nelson/launchpad/652838-select-diffs-for-syncing
Merge into: lp:launchpad/db-devel
Diff against target: 489 lines (+264/-51)
4 files modified
lib/canonical/launchpad/templates/launchpad-form.pt (+4/-3)
lib/lp/registry/browser/distroseries.py (+76/-2)
lib/lp/registry/browser/tests/test_series_views.py (+161/-43)
lib/lp/registry/templates/distroseries-localdifferences.pt (+23/-3)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/652838-select-diffs-for-syncing
Reviewer Review Type Date Requested Status
Curtis Hovey (community) ui Approve
Guilherme Salgado (community) ui* Approve
Jeroen T. Vermeulen (community) code Approve
Review via email: mp+37572@code.launchpad.net

Commit message

Allow multiple differences to be selected for syncing.

Description of the change

Overview
========
This branch fixes bug 652838 by adding (non-js) check boxes and the sync button for syncing differences.

Note: it does not add the actual sync operation itself, but just a stub action. Julian will be organising the actual operation at at later point (obviously before the feature is enabled).

UI demo
=======
http://people.canonical.com/~michaeln/tmp/652838-select-diffs.ogv

mockup:
https://dev.launchpad.net/LEP/DerivativeDistributions?action=AttachFile&do=get&target=derived-series-diffs_uiround2.png

To demo locally:
Run http://pastebin.ubuntu.com/494656/ in bin/iharness and then open
https://launchpad.dev/ubuntu/hoary/+localpackagediffs

Details
=======
I had 2 issues, and one ongoing:

1) There doesn't seem to be a natural way to have a dynamic name for the action as required by the prototype at:

https://dev.launchpad.net/LEP/DerivativeDistributions?action=AttachFile&do=get&target=derived-series-diffs_uiround2.png

You'll see the solution in the view's initialize() method, but any better options would be gratefully accepted.

2) There doesn't seem to be a natural way to have dynamic vocabularies based on the view. I've looked at various ways that we've done this in the past, and none are ideal.

In translations a vocabulary factory is used (see lp.translations.browser.translationimportqueue.TranslationImportQueueView), but vocabulary factories are designed to be based on the context, not the view (they implement IContextSourceBinder), so translations adds an __init__ kwarg, which means the vocabulary factory can't be instantiated in the interface description as it normally would, which means no LaunchpadFormView with its schema etc.

In soyuz an extra field and widget is added to the form during view initialisation (ie. one that is not on the schema, see lp.soyuz.browser.archive.ArchiveSourceSelectionFormView)

I decided instead to include the field on the schema with an empty vocabulary, and then update the vocabulary during the view's setUpFields() call. But again, any better ideas are welcome.

3) Finally, two tests where I render the view and check tags are failing, it seems, because the traversal request/lp:person is returning None:
http://pastebin.ubuntu.com/506335/

I'm still looking into why this is. So far, tracked it down to the fact that, even within the person_logged_in context manager, the tests view.request.principal is zope.principalregistry.principalregistry.UnauthenticatedPrincipal.

To test
=======

bin/test -vvm test_series_views

I'll upload a quick screencast soon.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

It's just a crying shame that you have to go to these lengths to include dynamic information in your view. I looked for alternatives but didn't find any.

You mentioned that you'd talk to the zope people; I think that's a good idea. If only Zope's @action accepted a callable as an alternative to a string!

review: Approve (code)
Revision history for this message
Guilherme Salgado (salgado) wrote :

The UI changes look good to me. I think it'd be nice to have an extra checkbox for selecting all/none versions, though.

review: Approve (ui*)
Revision history for this message
Michael Nelson (michael.nelson) wrote :

> The UI changes look good to me. I think it'd be nice to have an extra
> checkbox for selecting all/none versions, though.

salgado: thanks. And yes, the all/none checkbox is included on the mockup, I hope to add it in a later branch, but it's not a priority right now.

Revision history for this message
Curtis Hovey (sinzui) wrote :

The behaviour is nice. Buttons should be in title case (except for subordinating conjunctions and commonly used prepositions):
    Sync selected %s versions into => Sync Selected %s Versions into %s

This may look odd since the interpolated names may not be title case, but the user did that, not us.

review: Approve (ui)
Revision history for this message
Leonard Richardson (leonardr) wrote :

FYI, I just did a branch with dynamically-named buttons:

https://code.edge.launchpad.net/~leonardr/launchpad/rename-grant-permissions/+merge/37590

Basically, in the action code I created my own Actions object and copied in new Action objects. My Actions object was based on a class Actions object containing generic actions.

Revision history for this message
Gary Poster (gary) wrote :

I asked Leonard to comment on your (1), which he did, above. formlib actions are unpleasantly limited.

For (2), context-based vocabularies are what I expected you to use, but I see they are not sufficient for some reason. Without looking at the code more carefully, I don't know why. In their original usage, views are expected to be only a presentation of the context/model, so the context should be sufficient. I'm not sure why views are more than a presentation here, but would be interested to hear why. For the short term though, unless there's a nice way to refactor to actually use the context, hacks are the order of the day. :-(

Revision history for this message
Michael Nelson (michael.nelson) wrote :

On Tue, Oct 5, 2010 at 6:18 PM, Gary Poster <email address hidden> wrote:
> I asked Leonard to comment on your (1), which he did, above.  formlib actions are unpleasantly limited.

Thanks Gary and Leonard.

>
> For (2), context-based vocabularies are what I expected you to use, but I see they are not sufficient for some reason.  Without looking at the code more carefully, I don't know why.  In their original usage, views are expected to be only a presentation of the context/model, so the context should be sufficient.  I'm not sure why views are more than a presentation here, but would be interested to hear why.  For the short term though, unless there's a nice way to refactor to actually use the context, hacks are the order of the day. :-(

In this case, the context is a distro series, the view is displaying a
batched (and soon to be filtered) list of items related to that
distroseries, which can be selected for an operation. Another similar
situation that comes to mind is copying packages for an archive. The
view's context is the archive, but the view lists the related batched
(and filtered) packages.

In both cases we've manually created a field with a vocabulary
matching the items listed on screen so that validation and the action
can only operate on the current batch of items. Does that make sense,
or is there a better way?

> --
> https://code.launchpad.net/~michael.nelson/launchpad/652838-select-diffs-for-syncing/+merge/37572
> You are the owner of lp:~michael.nelson/launchpad/652838-select-diffs-for-syncing.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/templates/launchpad-form.pt'
2--- lib/canonical/launchpad/templates/launchpad-form.pt 2010-06-22 17:12:23 +0000
3+++ lib/canonical/launchpad/templates/launchpad-form.pt 2010-10-05 15:06:16 +0000
4@@ -76,9 +76,10 @@
5 </div>
6 <div class="actions" id="launchpad-form-actions"
7 metal:define-slot="buttons">
8- <input tal:repeat="action view/actions"
9- tal:replace="structure action/render"
10- />
11+ <tal:actions repeat="action view/actions">
12+ <input tal:replace="structure action/render"
13+ tal:condition="action/available"/>
14+ </tal:actions>
15 <tal:has-cancel-link condition="view/cancel_url">
16 <tal:or condition="view/has_available_actions">or&nbsp;</tal:or>
17 <a tal:attributes="href view/cancel_url">Cancel</a>
18
19=== modified file 'lib/lp/registry/browser/distroseries.py'
20--- lib/lp/registry/browser/distroseries.py 2010-09-21 08:42:29 +0000
21+++ lib/lp/registry/browser/distroseries.py 2010-10-05 15:06:16 +0000
22@@ -21,8 +21,12 @@
23 from zope.component import getUtility
24 from zope.event import notify
25 from zope.formlib import form
26+from zope.interface import Interface
27 from zope.lifecycleevent import ObjectCreatedEvent
28-from zope.schema import Choice
29+from zope.schema import (
30+ Choice,
31+ List,
32+ )
33 from zope.schema.vocabulary import (
34 SimpleTerm,
35 SimpleVocabulary,
36@@ -59,6 +63,7 @@
37 stepthrough,
38 stepto,
39 )
40+from canonical.widgets import LabeledMultiCheckBoxWidget
41 from canonical.widgets.itemswidgets import LaunchpadDropdownWidget
42 from lp.app.errors import NotFoundError
43 from lp.blueprints.browser.specificationtarget import (
44@@ -536,8 +541,18 @@
45 return navigator
46
47
48-class DistroSeriesLocalDifferences(LaunchpadView):
49+class IDifferencesFormSchema(Interface):
50+ selected_differences = List(
51+ title=_('Selected differences'),
52+ value_type=Choice(vocabulary=SimpleVocabulary([])),
53+ description=_("Select the differences for syncing."),
54+ required=True)
55+
56+
57+class DistroSeriesLocalDifferences(LaunchpadFormView):
58 """Present differences between a derived series and its parent."""
59+ schema = IDifferencesFormSchema
60+ custom_widget('selected_differences', LabeledMultiCheckBoxWidget)
61
62 page_title = 'Local package differences'
63
64@@ -546,6 +561,13 @@
65 if not getFeatureFlag('soyuz.derived-series-ui.enabled'):
66 self.request.response.redirect(canonical_url(self.context))
67 return
68+
69+ # Update the label for sync action.
70+ self.__class__.actions.byname['actions.sync'].label = (
71+ "Sync Selected %s Versions into %s" % (
72+ self.context.parent_series.displayname,
73+ self.context.displayname,
74+ ))
75 super(DistroSeriesLocalDifferences, self).initialize()
76
77 @property
78@@ -563,3 +585,55 @@
79 utility = getUtility(IDistroSeriesDifferenceSource)
80 differences = utility.getForDistroSeries(self.context)
81 return BatchNavigator(differences, self.request)
82+
83+ def setUpFields(self):
84+ """Add the selected differences field.
85+
86+ As this field depends on other search/filtering field values
87+ for its own vocabulary, we set it up after all the others.
88+ """
89+ super(DistroSeriesLocalDifferences, self).setUpFields()
90+ has_edit = check_permission('launchpad.Edit', self.context)
91+
92+ terms = [
93+ SimpleTerm(diff, diff.source_package_name.name,
94+ diff.source_package_name.name)
95+ for diff in self.cached_differences.batch]
96+ diffs_vocabulary = SimpleVocabulary(terms)
97+ choice = self.form_fields['selected_differences'].field.value_type
98+ choice.vocabulary = diffs_vocabulary
99+
100+ @action(_("Sync Sources"), name="sync", validator='validate_sync',
101+ condition='canPerformSync')
102+ def sync_sources(self, action, data):
103+ """Mark the diffs as syncing and request the sync.
104+
105+ Currently this is a stub operation, the details of which will
106+ be implemented later.
107+ """
108+ selected_differences = data['selected_differences']
109+ diffs = [
110+ diff.source_package_name.name
111+ for diff in selected_differences]
112+
113+ self.request.response.addNotification(
114+ "The following sources would have been synced if this "
115+ "wasn't just a stub operation: " + ", ".join(diffs))
116+
117+ self.next_url = self.request.URL
118+
119+ def validate_sync(self, action, data):
120+ """Validate selected differences."""
121+ form.getWidgetsData(self.widgets, self.prefix, data)
122+
123+ if len(data.get('selected_differences', [])) == 0:
124+ self.setFieldError(
125+ 'selected_differences', 'No differences selected.')
126+
127+ def canPerformSync(self, *args):
128+ """Return whether a sync can be performed.
129+
130+ This method is used as a condition for the above sync action, as
131+ well as directly in the template.
132+ """
133+ return check_permission('launchpad.Edit', self.context)
134
135=== modified file 'lib/lp/registry/browser/tests/test_series_views.py'
136--- lib/lp/registry/browser/tests/test_series_views.py 2010-09-21 15:52:01 +0000
137+++ lib/lp/registry/browser/tests/test_series_views.py 2010-10-05 15:06:16 +0000
138@@ -1,6 +1,8 @@
139 # Copyright 2010 Canonical Ltd. This software is licensed under the
140 # GNU Affero General Public License version 3 (see the file LICENSE).
141
142+from __future__ import with_statement
143+
144 __metaclass__ = type
145
146 from BeautifulSoup import BeautifulSoup
147@@ -14,7 +16,10 @@
148 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
149 from canonical.launchpad.webapp.batching import BatchNavigator
150 from canonical.launchpad.webapp.publisher import canonical_url
151-from canonical.testing import LaunchpadZopelessLayer
152+from canonical.testing import (
153+ LaunchpadZopelessLayer,
154+ LaunchpadFunctionalLayer,
155+ )
156 from lp.registry.enum import (
157 DistroSeriesDifferenceStatus,
158 DistroSeriesDifferenceType,
159@@ -28,7 +33,10 @@
160 getFeatureFlag,
161 per_thread,
162 )
163-from lp.testing import TestCaseWithFactory
164+from lp.testing import (
165+ TestCaseWithFactory,
166+ person_logged_in,
167+ )
168 from lp.testing.views import create_initialized_view
169
170
171@@ -61,40 +69,35 @@
172 self.assertEqual(view.needs_linking, None)
173
174
175+def set_derived_series_ui_feature_flag(test_case):
176+ # Helper to set the feature flag enabling the derived series ui.
177+ ignore = getFeatureStore().add(FeatureFlag(
178+ scope=u'default', flag=u'soyuz.derived-series-ui.enabled',
179+ value=u'on', priority=1))
180+
181+ # XXX Michael Nelson 2010-09-21 bug=631884
182+ # Currently LaunchpadTestRequest doesn't set per-thread
183+ # features.
184+ def in_scope(value):
185+ return True
186+ per_thread.features = FeatureController(in_scope)
187+
188+ def reset_per_thread_features():
189+ per_thread.features = None
190+ test_case.addCleanup(reset_per_thread_features)
191+
192+
193 class DistroSeriesLocalPackageDiffsTestCase(TestCaseWithFactory):
194 """Test the distroseries +localpackagediffs view."""
195
196 layer = LaunchpadZopelessLayer
197
198- def makeDerivedSeries(self, derived_name=None, parent_name=None):
199- # Helper that creates a derived distro series.
200- parent = self.factory.makeDistroSeries(name=parent_name)
201- derived_series = self.factory.makeDistroSeries(
202- name=derived_name, parent_series=parent)
203- return derived_series
204-
205- def setDerivedSeriesUIFeatureFlag(self):
206- # Helper to set the feature flag enabling the derived series ui.
207- ignore = getFeatureStore().add(FeatureFlag(
208- scope=u'default', flag=u'soyuz.derived-series-ui.enabled',
209- value=u'on', priority=1))
210-
211- # XXX Michael Nelson 2010-09-21 bug=631884
212- # Currently LaunchpadTestRequest doesn't set per-thread
213- # features.
214- def in_scope(value):
215- return True
216- per_thread.features = FeatureController(in_scope)
217-
218- def reset_per_thread_features():
219- per_thread.features = None
220- self.addCleanup(reset_per_thread_features)
221-
222 def test_view_redirects_without_feature_flag(self):
223 # If the feature flag soyuz.derived-series-ui.enabled is not set the
224 # view simply redirects to the derived series.
225- derived_series = self.makeDerivedSeries(
226- parent_name='lucid', derived_name='derilucid')
227+ derived_series = self.factory.makeDistroSeries(
228+ name='derilucid', parent_series=self.factory.makeDistroSeries(
229+ name='lucid'))
230
231 self.assertIs(
232 None, getFeatureFlag('soyuz.derived-series-ui.enabled'))
233@@ -108,8 +111,9 @@
234
235 def test_label(self):
236 # The view label includes the names of both series.
237- derived_series = self.makeDerivedSeries(
238- parent_name='lucid', derived_name='derilucid')
239+ derived_series = self.factory.makeDistroSeries(
240+ name='derilucid', parent_series=self.factory.makeDistroSeries(
241+ name='lucid'))
242
243 view = create_initialized_view(
244 derived_series, '+localpackagediffs')
245@@ -122,8 +126,9 @@
246 def test_batch_includes_needing_attention_only(self):
247 # The differences attribute includes differences needing
248 # attention only.
249- derived_series = self.makeDerivedSeries(
250- parent_name='lucid', derived_name='derilucid')
251+ derived_series = self.factory.makeDistroSeries(
252+ name='derilucid', parent_series=self.factory.makeDistroSeries(
253+ name='lucid'))
254 current_difference = self.factory.makeDistroSeriesDifference(
255 derived_series=derived_series)
256 old_difference = self.factory.makeDistroSeriesDifference(
257@@ -138,8 +143,9 @@
258
259 def test_batch_includes_different_versions_only(self):
260 # The view contains differences of type DIFFERENT_VERSIONS only.
261- derived_series = self.makeDerivedSeries(
262- parent_name='lucid', derived_name='derilucid')
263+ derived_series = self.factory.makeDistroSeries(
264+ name='derilucid', parent_series=self.factory.makeDistroSeries(
265+ name='lucid'))
266 different_versions_diff = self.factory.makeDistroSeriesDifference(
267 derived_series=derived_series)
268 unique_diff = self.factory.makeDistroSeriesDifference(
269@@ -155,10 +161,11 @@
270
271 def test_template_includes_help_link(self):
272 # The help link for popup help is included.
273- derived_series = self.makeDerivedSeries(
274- parent_name='lucid', derived_name='derilucid')
275+ derived_series = self.factory.makeDistroSeries(
276+ name='derilucid', parent_series=self.factory.makeDistroSeries(
277+ name='lucid'))
278
279- self.setDerivedSeriesUIFeatureFlag()
280+ set_derived_series_ui_feature_flag(self)
281 view = create_initialized_view(
282 derived_series, '+localpackagediffs')
283
284@@ -169,14 +176,15 @@
285
286 def test_diff_row_includes_last_comment_only(self):
287 # The most recent comment is rendered for each difference.
288- derived_series = self.makeDerivedSeries(
289- parent_name='lucid', derived_name='derilucid')
290+ derived_series = self.factory.makeDistroSeries(
291+ name='derilucid', parent_series=self.factory.makeDistroSeries(
292+ name='lucid'))
293 difference = self.factory.makeDistroSeriesDifference(
294 derived_series=derived_series)
295 difference.addComment(difference.owner, "Earlier comment")
296 difference.addComment(difference.owner, "Latest comment")
297
298- self.setDerivedSeriesUIFeatureFlag()
299+ set_derived_series_ui_feature_flag(self)
300 view = create_initialized_view(
301 derived_series, '+localpackagediffs')
302
303@@ -192,12 +200,13 @@
304
305 def test_diff_row_links_to_extra_details(self):
306 # The source package name links to the difference details.
307- derived_series = self.makeDerivedSeries(
308- parent_name='lucid', derived_name='derilucid')
309+ derived_series = self.factory.makeDistroSeries(
310+ name='derilucid', parent_series=self.factory.makeDistroSeries(
311+ name='lucid'))
312 difference = self.factory.makeDistroSeriesDifference(
313 derived_series=derived_series)
314
315- self.setDerivedSeriesUIFeatureFlag()
316+ set_derived_series_ui_feature_flag(self)
317 view = create_initialized_view(
318 derived_series, '+localpackagediffs')
319 soup = BeautifulSoup(view())
320@@ -210,6 +219,115 @@
321 self.assertEqual(difference.source_package_name.name, links[0].string)
322
323
324+class DistroSeriesLocalPackageDiffsFunctionalTestCase(TestCaseWithFactory):
325+
326+ layer = LaunchpadFunctionalLayer
327+
328+ def test_canPerformSync_non_editor(self):
329+ # Non-editors do not see options to sync.
330+ derived_series = self.factory.makeDistroSeries(
331+ name='derilucid', parent_series=self.factory.makeDistroSeries(
332+ name='lucid'))
333+ difference = self.factory.makeDistroSeriesDifference(
334+ derived_series=derived_series)
335+
336+ set_derived_series_ui_feature_flag(self)
337+ with person_logged_in(self.factory.makePerson()):
338+ view = create_initialized_view(
339+ derived_series, '+localpackagediffs')
340+
341+ self.assertFalse(view.canPerformSync())
342+
343+ def test_canPerformSync_editor(self):
344+ # Editors are presented with options to perform syncs.
345+ derived_series = self.factory.makeDistroSeries(
346+ name='derilucid', parent_series=self.factory.makeDistroSeries(
347+ name='lucid'))
348+ difference = self.factory.makeDistroSeriesDifference(
349+ derived_series=derived_series)
350+
351+ set_derived_series_ui_feature_flag(self)
352+ with person_logged_in(derived_series.owner):
353+ view = create_initialized_view(
354+ derived_series, '+localpackagediffs')
355+ self.assertTrue(view.canPerformSync())
356+
357+ def test_sync_notification_on_success(self):
358+ # Syncing one or more diffs results in a stub notification.
359+ derived_series = self.factory.makeDistroSeries(
360+ name='derilucid', parent_series=self.factory.makeDistroSeries(
361+ name='lucid'))
362+ difference = self.factory.makeDistroSeriesDifference(
363+ source_package_name_str='my-src-name',
364+ derived_series=derived_series)
365+
366+ set_derived_series_ui_feature_flag(self)
367+ with person_logged_in(derived_series.owner):
368+ view = create_initialized_view(
369+ derived_series, '+localpackagediffs',
370+ method='POST', form={
371+ 'field.selected_differences': [
372+ difference.source_package_name.name,
373+ ],
374+ 'field.actions.sync': 'Sync',
375+ })
376+
377+ self.assertEqual(0, len(view.errors))
378+ notifications = view.request.response.notifications
379+ self.assertEqual(1, len(notifications))
380+ self.assertEqual(
381+ "The following sources would have been synced if this wasn't "
382+ "just a stub operation: my-src-name",
383+ notifications[0].message)
384+ self.assertEqual(302, view.request.response.getStatus())
385+
386+ def test_sync_error_nothing_selected(self):
387+ # An error is raised when a sync is requested without any selection.
388+ derived_series = self.factory.makeDistroSeries(
389+ name='derilucid', parent_series=self.factory.makeDistroSeries(
390+ name='lucid'))
391+ difference = self.factory.makeDistroSeriesDifference(
392+ source_package_name_str='my-src-name',
393+ derived_series=derived_series)
394+
395+ set_derived_series_ui_feature_flag(self)
396+ with person_logged_in(derived_series.owner):
397+ view = create_initialized_view(
398+ derived_series, '+localpackagediffs',
399+ method='POST', form={
400+ 'field.selected_differences': [],
401+ 'field.actions.sync': 'Sync',
402+ })
403+
404+ self.assertEqual(1, len(view.errors))
405+ self.assertEqual(
406+ 'No differences selected.', view.errors[0])
407+
408+ def test_sync_error_invalid_selection(self):
409+ # An error is raised when an invalid difference is selected.
410+ derived_series = self.factory.makeDistroSeries(
411+ name='derilucid', parent_series=self.factory.makeDistroSeries(
412+ name='lucid'))
413+ difference = self.factory.makeDistroSeriesDifference(
414+ source_package_name_str='my-src-name',
415+ derived_series=derived_series)
416+
417+ set_derived_series_ui_feature_flag(self)
418+ with person_logged_in(derived_series.owner):
419+ view = create_initialized_view(
420+ derived_series, '+localpackagediffs',
421+ method='POST', form={
422+ 'field.selected_differences': ['some-other-name'],
423+ 'field.actions.sync': 'Sync',
424+ })
425+
426+ self.assertEqual(2, len(view.errors))
427+ self.assertEqual(
428+ 'No differences selected.', view.errors[0])
429+ self.assertEqual(
430+ 'Invalid value', view.errors[1].error_name)
431+
432+
433 class TestMilestoneBatchNavigatorAttribute(TestCaseWithFactory):
434 """Test the series.milestone_batch_navigator attribute."""
435
436
437=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
438--- lib/lp/registry/templates/distroseries-localdifferences.pt 2010-09-28 14:42:41 +0000
439+++ lib/lp/registry/templates/distroseries-localdifferences.pt 2010-10-05 15:06:16 +0000
440@@ -27,7 +27,9 @@
441 more about syncing from the parent series</a>).
442 </p>
443
444- <div tal:condition="differences/batch">
445+ <div metal:use-macro="context/@@launchpad_form/form">
446+
447+ <div tal:condition="differences/batch" metal:fill-slot="widgets">
448 <tal:navigation_top
449 replace="structure differences/@@+navigation-links-upper" />
450 <table class="listing">
451@@ -44,10 +46,18 @@
452 <tal:difference repeat="difference differences/batch">
453 <tr tal:define="parent_source_pub difference/parent_source_pub;
454 source_pub difference/source_pub;
455+ src_name difference/source_package_name/name;
456 signer source_pub/sourcepackagerelease/uploader/fmt:link|nothing;"
457 tal:attributes="class parent_source_pub/source_package_name">
458- <td><a tal:attributes="href difference/fmt:url" class="toggle-extra"
459- tal:content="parent_source_pub/source_package_name">Foo</a>
460+ <td>
461+ <input tal:condition="view/canPerformSync"
462+ name="field.selected_differences" type="checkbox"
463+ tal:attributes="
464+ value src_name;
465+ id string:field.selected_differences.${src_name}"/>
466+
467+ <a tal:attributes="href difference/fmt:url" class="toggle-extra"
468+ tal:content="parent_source_pub/source_package_name">Foo</a>
469 </td>
470 <td><a tal:attributes="href parent_source_pub/sourcepackagerelease/fmt:url">
471 <tal:replace
472@@ -80,7 +90,17 @@
473 </tal:difference>
474 </tbody>
475 </table>
476+ <tal:selectable_differences_end
477+ define="widget nocall:view/widgets/selected_differences;
478+ field_name widget/context/__name__;
479+ error python:view.getFieldError(field_name);">
480+ <input tal:attributes="name string:${widget/name}-empty-marker"
481+ type="hidden" value="1" />
482+ <div class="error message" tal:condition="error"
483+ tal:content="structure error">Error message</div>
484+ </tal:selectable_differences_end>
485 </div>
486+ </div>
487 <script type="text/javascript">
488 LPS.use('lp.registry.distroseriesdifferences_details', function(Y) {
489 diff_module = Y.lp.registry.distroseriesdifferences_details

Subscribers

People subscribed via source and target branches

to status/vote changes: