Merge lp:~allenap/launchpad/initseries-for-one-night-only-bug-793620 into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 13200
Proposed branch: lp:~allenap/launchpad/initseries-for-one-night-only-bug-793620
Merge into: lp:launchpad
Diff against target: 587 lines (+199/-52)
11 files modified
lib/lp/registry/browser/distroseries.py (+23/-0)
lib/lp/registry/browser/tests/test_distroseries.py (+52/-16)
lib/lp/registry/interfaces/distroseries.py (+14/-11)
lib/lp/registry/model/distroseries.py (+11/-7)
lib/lp/registry/templates/distroseries-index.pt (+2/-1)
lib/lp/registry/templates/distroseries-initialize.pt (+19/-3)
lib/lp/registry/templates/distroseries-portlet-derivation.pt (+2/-2)
lib/lp/registry/tests/test_distroseries.py (+17/-8)
lib/lp/testing/__init__.py (+1/-1)
lib/lp/testing/matchers.py (+24/-3)
lib/lp/testing/tests/test_matchers.py (+34/-0)
To merge this branch: bzr merge lp:~allenap/launchpad/initseries-for-one-night-only-bug-793620
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Steve Kowalik (community) code Needs Fixing
Review via email: mp+63756@code.launchpad.net

Commit message

[r=julian-edwards][bug=793620] Show a message on DistroSeries:+initseries, and prevent attempts to use the page, when the distroseries is in the process of being initialized or when the distroseries has already been initialized.

Description of the change

Show a message on +initseries, and prevent attempts to use the page,
when the distroseries is in the process of being derived or when the
distroseries has already been derived.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) wrote :

I think you're conflating things were they don't need to be. For +initseries, you only really care if the distroseries has been initialised, not if it is a derived series.

The with featureflag(): section reads as a little messy -- I thought we had FeatureFixture for that which makes it easier?

Thank you for cleaning up the imports, but I don't think you can import DSDJ from lp.soyuz.model? Or if you can, why are you importing the whole module?

review: Needs Fixing (code)
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks for the review!

> I think you're conflating things were they don't need to be. For
> +initseries, you only really care if the distroseries has been
> initialised, not if it is a derived series.

I have completely lost track of what this all means.

DistroSeries.is_derived_series checked if there are any DSPs, which
are, iirc, created by InitializeDistroSeries. As I understand it, they
are a marker that the distroseries has been initialized, that it is
now a derived distroseries. If initializing and deriving are very
different things that I'm conflating then I pity the users!

> The with featureflag(): section reads as a little messy -- I thought
> we had FeatureFixture for that which makes it easier?

I cargocult-n-pasted that. I've changed it to use FeatureFixture().

> Thank you for cleaning up the imports, but I don't think you can import DSDJ
> from lp.soyuz.model? Or if you can, why are you importing the whole module?

I didn't clean up the imports, utilities/format-imports took care of
that (by way of utilities/format-new-and-modified-imports). The
machine overlord has made its decision.

Revision history for this message
Gavin Panella (allenap) wrote :

Okay, Julian explained it to me. The follow-up is quite large, so I'll
explain what I've done:

- Introduced a new matched, EqualsIgnoringWhitespace. Yes, there are
  other ways to do this, but this seems to be a common test that was
  worth cementing. I suppose DocTestMatches is the closest thing to
  it, but this is simpler to grok.

- Removes DistroSeries.is_initialising. As Julian pointed out, this
  hits the database so would be better as a method...

- Introduce DistroSeries.isInitializing() and .isInitialized(). The
  former replaces .is_initialising and the latter reports if there are
  any published sources in the series' main archive.

- These are then used in the +initseries template.

- I also fixed some lint and imports.

=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py 2011-06-07 19:49:53 +0000
+++ lib/lp/registry/browser/distroseries.py 2011-06-09 12:04:01 +0000
@@ -654,20 +654,20 @@
654 def show_derivation_form(self):654 def show_derivation_form(self):
655 return (655 return (
656 self.is_derived_series_feature_enabled and656 self.is_derived_series_feature_enabled and
657 not self.context.is_derived_series and657 not self.context.isInitializing() and
658 not self.context.is_initialising)658 not self.context.isInitialized())
659659
660 @property660 @property
661 def show_already_derived_message(self):661 def show_already_initialized_message(self):
662 return (662 return (
663 self.is_derived_series_feature_enabled and663 self.is_derived_series_feature_enabled and
664 self.context.is_derived_series)664 self.context.isInitialized())
665665
666 @property666 @property
667 def show_already_initializing_message(self):667 def show_already_initializing_message(self):
668 return (668 return (
669 self.is_derived_series_feature_enabled and669 self.is_derived_series_feature_enabled and
670 self.context.is_initialising)670 self.context.isInitializing())
671671
672 @property672 @property
673 def next_url(self):673 def next_url(self):
674674
=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py 2011-06-08 08:22:07 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py 2011-06-09 12:05:09 +0000
@@ -85,7 +85,10 @@
85 with_celebrity_logged_in,85 with_celebrity_logged_in,
86 )86 )
87from lp.testing.fakemethod import FakeMethod87from lp.testing.fakemethod import FakeMethod
88from lp.testing.matchers import HasQueryCount88from lp.testing.matchers import (
89 EqualsIgnoringWhitespace,
90 HasQueryCount,
91 )
89from lp.testing.views import create_initialized_view92from lp.testing.views import create_initialized_view
9093
9194
@@ -321,7 +324,7 @@
321 view.request.features = get_relevant_feature_controller()324 view.request.features = get_relevant_feature_controller()
322 html_content = view()325 html_content = view()
323326
324 self.assertTrue(derived_series.is_initialising)327 self.assertTrue(derived_series.isInitializing())
325 self.assertThat(html_content, portlet_display)328 self.assertThat(html_content, portlet_display)
326329
327330
@@ -455,9 +458,10 @@
455458
456 def test_form_hidden_when_distroseries_is_initialized(self):459 def test_form_hidden_when_distroseries_is_initialized(self):
457 # The form is hidden when the feature flag is set but the series has460 # The form is hidden when the feature flag is set but the series has
458 # already been derived.461 # already been initialized.
459 distroseries = self.factory.makeDistroSeries()462 distroseries = self.factory.makeDistroSeries()
460 self.factory.makeDistroSeriesParent(derived_series=distroseries)463 self.factory.makeSourcePackagePublishingHistory(
464 distroseries=distroseries, archive=distroseries.main_archive)
461 view = create_initialized_view(distroseries, "+initseries")465 view = create_initialized_view(distroseries, "+initseries")
462 flags = {u"soyuz.derived_series_ui.enabled": u"true"}466 flags = {u"soyuz.derived_series_ui.enabled": u"true"}
463 with FeatureFixture(flags):467 with FeatureFixture(flags):
@@ -466,9 +470,10 @@
466 [], root.cssselect("#initseries-form-container"))470 [], root.cssselect("#initseries-form-container"))
467 # Instead an explanatory message is shown.471 # Instead an explanatory message is shown.
468 [message] = root.cssselect("p.error.message")472 [message] = root.cssselect("p.error.message")
469 self.assertEqual(473 self.assertThat(
470 u"This series has already been derived.",474 message.text.strip(), EqualsIgnoringWhitespace(
471 message.text.strip())475 u"This series already contains source packages "
476 u"and cannot be initialized again."))
472477
473 def test_form_hidden_when_distroseries_is_being_initialized(self):478 def test_form_hidden_when_distroseries_is_being_initialized(self):
474 # The form is hidden when the feature flag is set but the series has479 # The form is hidden when the feature flag is set but the series has
@@ -484,9 +489,10 @@
484 [], root.cssselect("#initseries-form-container"))489 [], root.cssselect("#initseries-form-container"))
485 # Instead an explanatory message is shown.490 # Instead an explanatory message is shown.
486 [message] = root.cssselect("p.error.message")491 [message] = root.cssselect("p.error.message")
487 self.assertEqual(492 self.assertThat(
488 u"This series is already being initialized.",493 message.text.strip(),
489 message.text.strip())494 EqualsIgnoringWhitespace(
495 u"This series is already being initialized."))
490496
491497
492class DistroSeriesDifferenceMixin:498class DistroSeriesDifferenceMixin:
493499
=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py 2011-05-31 15:45:19 +0000
+++ lib/lp/registry/interfaces/distroseries.py 2011-06-09 12:07:12 +0000
@@ -213,7 +213,7 @@
213 description=_("The version string for this series.")))213 description=_("The version string for this series.")))
214 distribution = exported(214 distribution = exported(
215 Reference(215 Reference(
216 Interface, # Really IDistribution, see circular import fix below.216 Interface, # Really IDistribution, see circular import fix below.
217 title=_("Distribution"), required=True,217 title=_("Distribution"), required=True,
218 description=_("The distribution for which this is a series.")))218 description=_("The distribution for which this is a series.")))
219 distributionID = Attribute('The distribution ID.')219 distributionID = Attribute('The distribution ID.')
@@ -239,16 +239,13 @@
239 is_derived_series = Bool(239 is_derived_series = Bool(
240 title=u'Is this series a derived series?', readonly=True,240 title=u'Is this series a derived series?', readonly=True,
241 description=(u"Whether or not this series is a derived series."))241 description=(u"Whether or not this series is a derived series."))
242 is_initialising = Bool(
243 title=u'Is this series initialising?', readonly=True,
244 description=(u"Whether or not this series is initialising."))
245 datereleased = exported(242 datereleased = exported(
246 Datetime(title=_("Date released")))243 Datetime(title=_("Date released")))
247 previous_series = exported(244 previous_series = exported(
248 ReferenceChoice(245 ReferenceChoice(
249 title=_("Parent series"),246 title=_("Parent series"),
250 description=_("The series from which this one was branched."),247 description=_("The series from which this one was branched."),
251 required=True, schema=Interface, # Really IDistroSeries, see below248 required=True, schema=Interface, # Really IDistroSeries, see below
252 vocabulary='DistroSeries'),249 vocabulary='DistroSeries'),
253 ("devel", dict(exported_as="previous_series")),250 ("devel", dict(exported_as="previous_series")),
254 ("1.0", dict(exported_as="parent_series")),251 ("1.0", dict(exported_as="parent_series")),
@@ -370,7 +367,7 @@
370367
371 main_archive = exported(368 main_archive = exported(
372 Reference(369 Reference(
373 Interface, # Really IArchive, see below for circular import fix.370 Interface, # Really IArchive, see below for circular import fix.
374 title=_('Distribution Main Archive')))371 title=_('Distribution Main Archive')))
375372
376 supported = exported(373 supported = exported(
@@ -418,7 +415,7 @@
418 architectures = exported(415 architectures = exported(
419 CollectionField(416 CollectionField(
420 title=_("All architectures in this series."),417 title=_("All architectures in this series."),
421 value_type=Reference(schema=Interface), # IDistroArchSeries.418 value_type=Reference(schema=Interface), # IDistroArchSeries.
422 readonly=True))419 readonly=True))
423420
424 enabled_architectures = Attribute(421 enabled_architectures = Attribute(
@@ -848,18 +845,18 @@
848845
849 @operation_parameters(846 @operation_parameters(
850 parent_series=Reference(847 parent_series=Reference(
851 schema=Interface, # IDistroSeries848 schema=Interface, # IDistroSeries
852 title=_("The parent series to consider."),849 title=_("The parent series to consider."),
853 required=False),850 required=False),
854 difference_type=Choice(851 difference_type=Choice(
855 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceType852 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceType
856 title=_("Only return differences of this type."), required=False),853 title=_("Only return differences of this type."), required=False),
857 source_package_name_filter=TextLine(854 source_package_name_filter=TextLine(
858 title=_("Only return differences for packages matching this "855 title=_("Only return differences for packages matching this "
859 "name."),856 "name."),
860 required=False),857 required=False),
861 status=Choice(858 status=Choice(
862 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceStatus859 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceStatus
863 title=_("Only return differences of this status."),860 title=_("Only return differences of this status."),
864 required=False),861 required=False),
865 child_version_higher=Bool(862 child_version_higher=Bool(
@@ -886,6 +883,12 @@
886 child's version is higher than the parent's version.883 child's version is higher than the parent's version.
887 """884 """
888885
886 def isInitializing(self):
887 """Is this series initializing?"""
888
889 def isInitialized(self):
890 """Has this series been initialized?"""
891
889892
890class IDistroSeriesEditRestricted(Interface):893class IDistroSeriesEditRestricted(Interface):
891 """IDistroSeries properties which require launchpad.Edit."""894 """IDistroSeries properties which require launchpad.Edit."""
@@ -1032,7 +1035,7 @@
10321035
1033class DerivationError(Exception):1036class DerivationError(Exception):
1034 """Raised when there is a problem deriving a distroseries."""1037 """Raised when there is a problem deriving a distroseries."""
1035 webservice_error(400) # Bad Request1038 webservice_error(400) # Bad Request
1036 _message_prefix = "Error deriving distro series"1039 _message_prefix = "Error deriving distro series"
10371040
10381041
10391042
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2011-05-31 15:45:19 +0000
+++ lib/lp/registry/model/distroseries.py 2011-06-09 11:58:23 +0000
@@ -801,13 +801,6 @@
801 return not self.getParentSeries() == []801 return not self.getParentSeries() == []
802802
803 @property803 @property
804 def is_initialising(self):
805 """See `IDistroSeries`."""
806 return not getUtility(
807 IInitialiseDistroSeriesJobSource).getPendingJobsForDistroseries(
808 self).is_empty()
809
810 @property
811 def bugtargetname(self):804 def bugtargetname(self):
812 """See IBugTarget."""805 """See IBugTarget."""
813 # XXX mpt 2007-07-10 bugs 113258, 113262:806 # XXX mpt 2007-07-10 bugs 113258, 113262:
@@ -2023,6 +2016,17 @@
2023 status=status,2016 status=status,
2024 child_version_higher=child_version_higher)2017 child_version_higher=child_version_higher)
20252018
2019 def isInitializing(self):
2020 """See `IDistroSeries`."""
2021 job_source = getUtility(IInitialiseDistroSeriesJobSource)
2022 pending_jobs = job_source.getPendingJobsForDistroseries(self)
2023 return not pending_jobs.is_empty()
2024
2025 def isInitialized(self):
2026 """See `IDistroSeries`."""
2027 published = self.main_archive.getPublishedSources(distroseries=self)
2028 return not published.is_empty()
2029
20262030
2027class DistroSeriesSet:2031class DistroSeriesSet:
2028 implements(IDistroSeriesSet)2032 implements(IDistroSeriesSet)
20292033
=== modified file 'lib/lp/registry/templates/distroseries-index.pt'
--- lib/lp/registry/templates/distroseries-index.pt 2011-05-24 10:08:33 +0000
+++ lib/lp/registry/templates/distroseries-index.pt 2011-06-09 12:00:16 +0000
@@ -68,7 +68,8 @@
68 <tal:derivation68 <tal:derivation
69 tal:condition="request/features/soyuz.derived_series_ui.enabled">69 tal:condition="request/features/soyuz.derived_series_ui.enabled">
70 <div class="yui-u"70 <div class="yui-u"
71 tal:condition="python:context.is_derived_series or context.is_initialising">71 tal:condition="python: context.is_derived_series or
72 context.isInitializing()">
72 <div tal:replace="structure context/@@+portlet-derivation" />73 <div tal:replace="structure context/@@+portlet-derivation" />
73 </div>74 </div>
74 </tal:derivation>75 </tal:derivation>
7576
=== modified file 'lib/lp/registry/templates/distroseries-initialize.pt'
--- lib/lp/registry/templates/distroseries-initialize.pt 2011-06-07 19:49:53 +0000
+++ lib/lp/registry/templates/distroseries-initialize.pt 2011-06-08 12:57:58 +0000
@@ -50,11 +50,12 @@
50 </p>50 </p>
51 </tal:disabled>51 </tal:disabled>
5252
53 <tal:already-derived condition="view/show_already_derived_message">53 <tal:already-initialized condition="view/show_already_initialized_message">
54 <p class="error message">54 <p class="error message">
55 This series has already been derived.55 This series already contains source packages and cannot be
56 initialized again.
56 </p>57 </p>
57 </tal:already-derived>58 </tal:already-initialized>
5859
59 <tal:already-initializing60 <tal:already-initializing
60 condition="view/show_already_initializing_message">61 condition="view/show_already_initializing_message">
6162
=== modified file 'lib/lp/registry/templates/distroseries-portlet-derivation.pt'
--- lib/lp/registry/templates/distroseries-portlet-derivation.pt 2011-05-31 11:55:38 +0000
+++ lib/lp/registry/templates/distroseries-portlet-derivation.pt 2011-06-09 11:59:46 +0000
@@ -5,7 +5,7 @@
5 id="series-derivation" class="portlet"5 id="series-derivation" class="portlet"
6 tal:define="overview_menu context/menu:overview">6 tal:define="overview_menu context/menu:overview">
7 <tal:is_derived condition="context/is_derived_series">7 <tal:is_derived condition="context/is_derived_series">
8 <tal:is_initialised condition="not: context/is_initialising">8 <tal:is_initialised condition="not: context/isInitializing()">
9 <tal:one_parent condition="view/has_unique_parent">9 <tal:one_parent condition="view/has_unique_parent">
10 <h2>Derived from <tal:name replace="view/unique_parent/displayname"/></h2>10 <h2>Derived from <tal:name replace="view/unique_parent/displayname"/></h2>
11 </tal:one_parent>11 </tal:one_parent>
@@ -61,7 +61,7 @@
61 </tal:diffs>61 </tal:diffs>
62 </tal:is_initialised>62 </tal:is_initialised>
63 </tal:is_derived>63 </tal:is_derived>
64 <tal:is_initialising condition="context/is_initialising">64 <tal:is_initialising condition="context/isInitializing">
65 <h2>Series initialisation in progress</h2>65 <h2>Series initialisation in progress</h2>
66 This series is initialising.66 This series is initialising.
67 </tal:is_initialising>67 </tal:is_initialising>
6868
=== modified file 'lib/lp/registry/tests/test_distroseries.py'
--- lib/lp/registry/tests/test_distroseries.py 2011-05-31 15:40:10 +0000
+++ lib/lp/registry/tests/test_distroseries.py 2011-06-09 11:57:05 +0000
@@ -222,22 +222,31 @@
222 self.assertEquals(registrant, distroseries.registrant)222 self.assertEquals(registrant, distroseries.registrant)
223 self.assertNotEqual(distroseries.registrant, distroseries.owner)223 self.assertNotEqual(distroseries.registrant, distroseries.owner)
224224
225 def test_is_initialising(self):225 def test_isInitializing(self):
226 # The series is_initialising only if there is an initialisation226 # The series method isInitializing() returns True only if there is an
227 # job with a pending status attached to this series.227 # initialisation job with a pending status attached to this series.
228 distroseries = self.factory.makeDistroSeries()228 distroseries = self.factory.makeDistroSeries()
229 parent_distroseries = self.factory.makeDistroSeries()229 parent_distroseries = self.factory.makeDistroSeries()
230 self.assertEquals(False, distroseries.is_initialising)230 self.assertFalse(distroseries.isInitializing())
231 job_source = getUtility(IInitialiseDistroSeriesJobSource)231 job_source = getUtility(IInitialiseDistroSeriesJobSource)
232 job = job_source.create(distroseries, [parent_distroseries.id])232 job = job_source.create(distroseries, [parent_distroseries.id])
233 self.assertEquals(True, distroseries.is_initialising)233 self.assertTrue(distroseries.isInitializing())
234 job.start()234 job.start()
235 self.assertEquals(True, distroseries.is_initialising)235 self.assertTrue(distroseries.isInitializing())
236 job.queue()236 job.queue()
237 self.assertEquals(True, distroseries.is_initialising)237 self.assertTrue(distroseries.isInitializing())
238 job.start()238 job.start()
239 job.complete()239 job.complete()
240 self.assertEquals(False, distroseries.is_initialising)240 self.assertFalse(distroseries.isInitializing())
241
242 def test_isInitialized(self):
243 # The series method isInitialized() returns True once the series has
244 # been initialized.
245 distroseries = self.factory.makeDistroSeries()
246 self.assertFalse(distroseries.isInitialized())
247 self.factory.makeSourcePackagePublishingHistory(
248 distroseries=distroseries, archive=distroseries.main_archive)
249 self.assertTrue(distroseries.isInitialized())
241250
242251
243class TestDistroSeriesPackaging(TestCaseWithFactory):252class TestDistroSeriesPackaging(TestCaseWithFactory):
244253
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2011-05-19 15:15:16 +0000
+++ lib/lp/testing/__init__.py 2011-06-08 13:19:10 +0000
@@ -163,7 +163,6 @@
163 )163 )
164from lp.testing.fixture import ZopeEventHandlerFixture164from lp.testing.fixture import ZopeEventHandlerFixture
165from lp.testing.karma import KarmaRecorder165from lp.testing.karma import KarmaRecorder
166from lp.testing.matchers import Provides
167from lp.testing.windmill import (166from lp.testing.windmill import (
168 constants,167 constants,
169 lpuser,168 lpuser,
@@ -390,6 +389,7 @@
390389
391 def assertProvides(self, obj, interface):390 def assertProvides(self, obj, interface):
392 """Assert 'obj' correctly provides 'interface'."""391 """Assert 'obj' correctly provides 'interface'."""
392 from lp.testing.matchers import Provides
393 self.assertThat(obj, Provides(interface))393 self.assertThat(obj, Provides(interface))
394394
395 def assertClassImplements(self, cls, interface):395 def assertClassImplements(self, cls, interface):
396396
=== modified file 'lib/lp/testing/matchers.py'
--- lib/lp/testing/matchers.py 2011-03-02 23:54:25 +0000
+++ lib/lp/testing/matchers.py 2011-06-08 13:33:03 +0000
@@ -8,6 +8,7 @@
8 'DocTestMatches',8 'DocTestMatches',
9 'DoesNotCorrectlyProvide',9 'DoesNotCorrectlyProvide',
10 'DoesNotProvide',10 'DoesNotProvide',
11 'EqualsIgnoringWhitespace',
11 'HasQueryCount',12 'HasQueryCount',
12 'IsNotProxied',13 'IsNotProxied',
13 'IsProxied',14 'IsProxied',
@@ -20,17 +21,17 @@
20 ]21 ]
2122
22from lazr.lifecycle.snapshot import Snapshot23from lazr.lifecycle.snapshot import Snapshot
24from testtools import matchers
23from testtools.content import Content25from testtools.content import Content
24from testtools.content_type import UTF8_TEXT26from testtools.content_type import UTF8_TEXT
25from testtools.matchers import (27from testtools.matchers import (
28 DocTestMatches as OriginalDocTestMatches,
26 Equals,29 Equals,
27 DocTestMatches as OriginalDocTestMatches,
28 LessThan,30 LessThan,
29 Matcher,31 Matcher,
30 Mismatch,32 Mismatch,
31 MismatchesAll,33 MismatchesAll,
32 )34 )
33from testtools import matchers
34from zope.interface.exceptions import (35from zope.interface.exceptions import (
35 BrokenImplementation,36 BrokenImplementation,
36 BrokenMethodImplementation,37 BrokenMethodImplementation,
@@ -44,6 +45,7 @@
4445
45from canonical.launchpad.webapp import canonical_url46from canonical.launchpad.webapp import canonical_url
46from canonical.launchpad.webapp.batching import BatchNavigator47from canonical.launchpad.webapp.batching import BatchNavigator
48from lp.testing import normalize_whitespace
47from lp.testing._login import person_logged_in49from lp.testing._login import person_logged_in
48from lp.testing._webservice import QueryCollector50from lp.testing._webservice import QueryCollector
4951
@@ -290,7 +292,8 @@
290292
291 :param singular: The singular header the batch should be using.293 :param singular: The singular header the batch should be using.
292 :param plural: The plural header the batch should be using.294 :param plural: The plural header the batch should be using.
293 :param batch_size: The batch size that should be configured by default.295 :param batch_size: The batch size that should be configured by
296 default.
294 """297 """
295 self._single = Equals(singular)298 self._single = Equals(singular)
296 self._plural = Equals(plural)299 self._plural = Equals(plural)
@@ -438,3 +441,21 @@
438 text = widget.findAll(attrs={'class': 'yui3-activator-data-box'})[0]441 text = widget.findAll(attrs={'class': 'yui3-activator-data-box'})[0]
439 text_matcher = DocTestMatches(extract_text(text))442 text_matcher = DocTestMatches(extract_text(text))
440 return text_matcher.match(matchee)443 return text_matcher.match(matchee)
444
445
446class EqualsIgnoringWhitespace(Equals):
447 """Compare equality, ignoring whitespace in strings.
448
449 Whitespace in strings is normalized before comparison. All other objected
450 are compared as they come.
451 """
452
453 def __init__(self, expected):
454 if isinstance(expected, (str, unicode)):
455 expected = normalize_whitespace(expected)
456 super(EqualsIgnoringWhitespace, self).__init__(expected)
457
458 def match(self, observed):
459 if isinstance(observed, (str, unicode)):
460 observed = normalize_whitespace(observed)
461 return super(EqualsIgnoringWhitespace, self).match(observed)
441462
=== modified file 'lib/lp/testing/tests/test_matchers.py'
--- lib/lp/testing/tests/test_matchers.py 2011-02-25 07:15:06 +0000
+++ lib/lp/testing/tests/test_matchers.py 2011-06-08 13:31:16 +0000
@@ -29,6 +29,7 @@
29 DoesNotContain,29 DoesNotContain,
30 DoesNotCorrectlyProvide,30 DoesNotCorrectlyProvide,
31 DoesNotProvide,31 DoesNotProvide,
32 EqualsIgnoringWhitespace,
32 HasQueryCount,33 HasQueryCount,
33 IsNotProxied,34 IsNotProxied,
34 IsProxied,35 IsProxied,
@@ -274,3 +275,36 @@
274 matcher = Contains("bar")275 matcher = Contains("bar")
275 mismatch = matcher.match("foo")276 mismatch = matcher.match("foo")
276 self.assertEqual("bar", mismatch.expected)277 self.assertEqual("bar", mismatch.expected)
278
279
280class EqualsIgnoringWhitespaceTests(TestCase):
281
282 def test_str(self):
283 matcher = EqualsIgnoringWhitespace("abc")
284 self.assertEqual("EqualsIgnoringWhitespace('abc')", str(matcher))
285
286 def test_match_str(self):
287 matcher = EqualsIgnoringWhitespace("one \t two \n three")
288 self.assertIs(None, matcher.match(" one \r two three "))
289
290 def test_mismatch_str(self):
291 matcher = EqualsIgnoringWhitespace("one \t two \n three")
292 mismatch = matcher.match(" one \r three ")
293 self.assertEqual(
294 "'one two three' != 'one three'",
295 mismatch.describe())
296
297 def test_match_unicode(self):
298 matcher = EqualsIgnoringWhitespace(u"one \t two \n \u1234 ")
299 self.assertIs(None, matcher.match(u" one \r two \u1234 "))
300
301 def test_mismatch_unicode(self):
302 matcher = EqualsIgnoringWhitespace(u"one \t two \n \u1234 ")
303 mismatch = matcher.match(u" one \r \u1234 ")
304 self.assertEqual(
305 u"u'one two \\u1234' != u'one \\u1234'",
306 mismatch.describe())
307
308 def test_match_non_string(self):
309 matcher = EqualsIgnoringWhitespace(1234)
310 self.assertIs(None, matcher.match(1234))
Revision history for this message
Jonathan Lange (jml) wrote :

`EqualsIgnoringWhitespace` could be written as:

  EqualsIgnoringWhitespace = lambda s: AfterPreprocessing(normalize_whitespace, Equals(s))

This wouldn't have the (str, unicode) type guard, but I'm not really sure that adds much anyway.

Revision history for this message
Gavin Panella (allenap) wrote :

I hadn't seen AfterPreprocessing before, neat :)

However, AfterPreprocessing only processes the matchee, not the
matcher, so that lambda doesn't ignore whitespace in the same way as
EqualsIgnoringWhitespace does. In that respect I think
EqualsIgnoringWhitespace is less surprising.

Having said that, EqualsIgnoringWhitespace doesn't actually *ignore*
whitespace, it just treats any run of whitespace as equal in matcher
and matchee. It's misnamed, but I don't think people are going to be
surprised by its behaviour anyway.

The reason for the (str, unicode) guard is so that the assertion
doesn't fail with an AttributeError (i.e. '...' object has no
attribute 'split') when the matcher or, more significantly, the
matchee are None or some other unexpected result.

Revision history for this message
Jonathan Lange (jml) wrote :

On Thu, Jun 9, 2011 at 3:43 PM, Gavin Panella
<email address hidden> wrote:
> I hadn't seen AfterPreprocessing before, neat :)
>
> However, AfterPreprocessing only processes the matchee, not the
> matcher, so that lambda doesn't ignore whitespace in the same way as
> EqualsIgnoringWhitespace does. In that respect I think
> EqualsIgnoringWhitespace is less surprising.
>

Ahh, OK. I guess testtools could use a combinator like
AfterPreprocessing that maps both the matcher and the matchee.
Shouldn't have to write a class to do this.

jml

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Lovely!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/distroseries.py'
--- lib/lp/registry/browser/distroseries.py 2011-06-09 10:34:37 +0000
+++ lib/lp/registry/browser/distroseries.py 2011-06-10 15:18:35 +0000
@@ -647,6 +647,29 @@
647 return getFeatureFlag("soyuz.derived_series_ui.enabled") is not None647 return getFeatureFlag("soyuz.derived_series_ui.enabled") is not None
648648
649 @property649 @property
650 def show_derivation_not_yet_available(self):
651 return not self.is_derived_series_feature_enabled
652
653 @property
654 def show_derivation_form(self):
655 return (
656 self.is_derived_series_feature_enabled and
657 not self.context.isInitializing() and
658 not self.context.isInitialized())
659
660 @property
661 def show_already_initialized_message(self):
662 return (
663 self.is_derived_series_feature_enabled and
664 self.context.isInitialized())
665
666 @property
667 def show_already_initializing_message(self):
668 return (
669 self.is_derived_series_feature_enabled and
670 self.context.isInitializing())
671
672 @property
650 def rebuilding_allowed(self):673 def rebuilding_allowed(self):
651 """If the distribution has got any initialized series already,674 """If the distribution has got any initialized series already,
652 rebuilding is not allowed.675 rebuilding is not allowed.
653676
=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
--- lib/lp/registry/browser/tests/test_distroseries.py 2011-06-09 10:34:37 +0000
+++ lib/lp/registry/browser/tests/test_distroseries.py 2011-06-10 15:18:35 +0000
@@ -40,11 +40,11 @@
40 LaunchpadFunctionalLayer,40 LaunchpadFunctionalLayer,
41 LaunchpadZopelessLayer,41 LaunchpadZopelessLayer,
42 )42 )
43from lp.app.interfaces.launchpad import ILaunchpadCelebrities
43from lp.archivepublisher.debversion import Version44from lp.archivepublisher.debversion import Version
44from lp.app.interfaces.launchpad import ILaunchpadCelebrities
45from lp.registry.browser.distroseries import (45from lp.registry.browser.distroseries import (
46 HIGHER_VERSION_THAN_PARENT,
46 IGNORED,47 IGNORED,
47 HIGHER_VERSION_THAN_PARENT,
48 NON_IGNORED,48 NON_IGNORED,
49 RESOLVED,49 RESOLVED,
50 )50 )
@@ -73,22 +73,23 @@
73from lp.soyuz.interfaces.sourcepackageformat import (73from lp.soyuz.interfaces.sourcepackageformat import (
74 ISourcePackageFormatSelectionSet,74 ISourcePackageFormatSelectionSet,
75 )75 )
76from lp.soyuz.model import distroseriesdifferencejob
76from lp.soyuz.model.archivepermission import ArchivePermission77from lp.soyuz.model.archivepermission import ArchivePermission
77from lp.soyuz.model.packagecopyjob import PlainPackageCopyJob78from lp.soyuz.model.packagecopyjob import PlainPackageCopyJob
78from lp.soyuz.model import distroseriesdifferencejob
79from lp.testing import (79from lp.testing import (
80 anonymous_logged_in,80 anonymous_logged_in,
81 celebrity_logged_in,81 celebrity_logged_in,
82 feature_flags,
83 login_person,82 login_person,
84 person_logged_in,83 person_logged_in,
85 set_feature_flag,
86 StormStatementRecorder,84 StormStatementRecorder,
87 TestCaseWithFactory,85 TestCaseWithFactory,
88 with_celebrity_logged_in,86 with_celebrity_logged_in,
89 )87 )
90from lp.testing.fakemethod import FakeMethod88from lp.testing.fakemethod import FakeMethod
91from lp.testing.matchers import HasQueryCount89from lp.testing.matchers import (
90 EqualsIgnoringWhitespace,
91 HasQueryCount,
92 )
92from lp.testing.views import create_initialized_view93from lp.testing.views import create_initialized_view
9394
9495
@@ -324,7 +325,7 @@
324 view.request.features = get_relevant_feature_controller()325 view.request.features = get_relevant_feature_controller()
325 html_content = view()326 html_content = view()
326327
327 self.assertTrue(derived_series.is_initialising)328 self.assertTrue(derived_series.isInitializing())
328 self.assertThat(html_content, portlet_display)329 self.assertThat(html_content, portlet_display)
329330
330331
@@ -417,17 +418,17 @@
417 # the soyuz.derived_series_ui.enabled flag.418 # the soyuz.derived_series_ui.enabled flag.
418 distroseries = self.factory.makeDistroSeries()419 distroseries = self.factory.makeDistroSeries()
419 view = create_initialized_view(distroseries, "+initseries")420 view = create_initialized_view(distroseries, "+initseries")
420 with feature_flags():421 with FeatureFixture({}):
421 self.assertFalse(view.is_derived_series_feature_enabled)422 self.assertFalse(view.is_derived_series_feature_enabled)
422 with feature_flags():423 flags = {u"soyuz.derived_series_ui.enabled": u"true"}
423 set_feature_flag(u"soyuz.derived_series_ui.enabled", u"true")424 with FeatureFixture(flags):
424 self.assertTrue(view.is_derived_series_feature_enabled)425 self.assertTrue(view.is_derived_series_feature_enabled)
425426
426 def test_form_hidden_when_derived_series_feature_disabled(self):427 def test_form_hidden_when_derived_series_feature_disabled(self):
427 # The form is hidden when the feature flag is not set.428 # The form is hidden when the feature flag is not set.
428 distroseries = self.factory.makeDistroSeries()429 distroseries = self.factory.makeDistroSeries()
429 view = create_initialized_view(distroseries, "+initseries")430 view = create_initialized_view(distroseries, "+initseries")
430 with feature_flags():431 with FeatureFixture({}):
431 root = html.fromstring(view())432 root = html.fromstring(view())
432 self.assertEqual(433 self.assertEqual(
433 [], root.cssselect("#initseries-form-container"))434 [], root.cssselect("#initseries-form-container"))
@@ -441,8 +442,8 @@
441 # The form is shown when the feature flag is set.442 # The form is shown when the feature flag is set.
442 distroseries = self.factory.makeDistroSeries()443 distroseries = self.factory.makeDistroSeries()
443 view = create_initialized_view(distroseries, "+initseries")444 view = create_initialized_view(distroseries, "+initseries")
444 with feature_flags():445 flags = {u"soyuz.derived_series_ui.enabled": u"true"}
445 set_feature_flag(u"soyuz.derived_series_ui.enabled", u"true")446 with FeatureFixture(flags):
446 root = html.fromstring(view())447 root = html.fromstring(view())
447 self.assertNotEqual(448 self.assertNotEqual(
448 [], root.cssselect("#initseries-form-container"))449 [], root.cssselect("#initseries-form-container"))
@@ -462,7 +463,6 @@
462 self.factory.makeDistroSeries(463 self.factory.makeDistroSeries(
463 distribution=distroseries.distribution)464 distribution=distroseries.distribution)
464 view = create_initialized_view(distroseries, "+initseries")465 view = create_initialized_view(distroseries, "+initseries")
465
466 self.assertTrue(view.rebuilding_allowed)466 self.assertTrue(view.rebuilding_allowed)
467467
468 def test_rebuilding_not_allowed(self):468 def test_rebuilding_not_allowed(self):
@@ -473,9 +473,45 @@
473 self.factory.makeSourcePackagePublishingHistory(473 self.factory.makeSourcePackagePublishingHistory(
474 distroseries=another_distroseries)474 distroseries=another_distroseries)
475 view = create_initialized_view(distroseries, "+initseries")475 view = create_initialized_view(distroseries, "+initseries")
476
477 self.assertFalse(view.rebuilding_allowed)476 self.assertFalse(view.rebuilding_allowed)
478477
478 def test_form_hidden_when_distroseries_is_initialized(self):
479 # The form is hidden when the feature flag is set but the series has
480 # already been initialized.
481 distroseries = self.factory.makeDistroSeries()
482 self.factory.makeSourcePackagePublishingHistory(
483 distroseries=distroseries, archive=distroseries.main_archive)
484 view = create_initialized_view(distroseries, "+initseries")
485 flags = {u"soyuz.derived_series_ui.enabled": u"true"}
486 with FeatureFixture(flags):
487 root = html.fromstring(view())
488 self.assertEqual(
489 [], root.cssselect("#initseries-form-container"))
490 # Instead an explanatory message is shown.
491 [message] = root.cssselect("p.error.message")
492 self.assertThat(
493 message.text, EqualsIgnoringWhitespace(
494 u"This series already contains source packages "
495 u"and cannot be initialized again."))
496
497 def test_form_hidden_when_distroseries_is_being_initialized(self):
498 # The form is hidden when the feature flag is set but the series has
499 # already been derived.
500 distroseries = self.factory.makeDistroSeries()
501 getUtility(IInitialiseDistroSeriesJobSource).create(
502 distroseries, [self.factory.makeDistroSeries().id])
503 view = create_initialized_view(distroseries, "+initseries")
504 flags = {u"soyuz.derived_series_ui.enabled": u"true"}
505 with FeatureFixture(flags):
506 root = html.fromstring(view())
507 self.assertEqual(
508 [], root.cssselect("#initseries-form-container"))
509 # Instead an explanatory message is shown.
510 [message] = root.cssselect("p.error.message")
511 self.assertThat(
512 message.text, EqualsIgnoringWhitespace(
513 u"This series is already being initialized."))
514
479515
480class DistroSeriesDifferenceMixin:516class DistroSeriesDifferenceMixin:
481 """A helper class for testing differences pages"""517 """A helper class for testing differences pages"""
@@ -1701,7 +1737,7 @@
1701 person, sp_name)1737 person, sp_name)
1702 self._syncAndGetView(1738 self._syncAndGetView(
1703 derived_series, person, [diff_id])1739 derived_series, person, [diff_id])
1704 parent_pub = parent_series.main_archive.getPublishedSources(1740 parent_series.main_archive.getPublishedSources(
1705 name='my-src-name', version=versions['parent'],1741 name='my-src-name', version=versions['parent'],
1706 distroseries=parent_series).one()1742 distroseries=parent_series).one()
17071743
17081744
=== modified file 'lib/lp/registry/interfaces/distroseries.py'
--- lib/lp/registry/interfaces/distroseries.py 2011-06-08 11:06:04 +0000
+++ lib/lp/registry/interfaces/distroseries.py 2011-06-10 15:18:35 +0000
@@ -213,7 +213,7 @@
213 description=_("The version string for this series.")))213 description=_("The version string for this series.")))
214 distribution = exported(214 distribution = exported(
215 Reference(215 Reference(
216 Interface, # Really IDistribution, see circular import fix below.216 Interface, # Really IDistribution, see circular import fix below.
217 title=_("Distribution"), required=True,217 title=_("Distribution"), required=True,
218 description=_("The distribution for which this is a series.")))218 description=_("The distribution for which this is a series.")))
219 distributionID = Attribute('The distribution ID.')219 distributionID = Attribute('The distribution ID.')
@@ -239,16 +239,13 @@
239 is_derived_series = Bool(239 is_derived_series = Bool(
240 title=u'Is this series a derived series?', readonly=True,240 title=u'Is this series a derived series?', readonly=True,
241 description=(u"Whether or not this series is a derived series."))241 description=(u"Whether or not this series is a derived series."))
242 is_initialising = Bool(
243 title=u'Is this series initialising?', readonly=True,
244 description=(u"Whether or not this series is initialising."))
245 datereleased = exported(242 datereleased = exported(
246 Datetime(title=_("Date released")))243 Datetime(title=_("Date released")))
247 previous_series = exported(244 previous_series = exported(
248 ReferenceChoice(245 ReferenceChoice(
249 title=_("Parent series"),246 title=_("Parent series"),
250 description=_("The series from which this one was branched."),247 description=_("The series from which this one was branched."),
251 required=True, schema=Interface, # Really IDistroSeries, see below248 required=True, schema=Interface, # Really IDistroSeries
252 vocabulary='DistroSeries'),249 vocabulary='DistroSeries'),
253 ("devel", dict(exported_as="previous_series")),250 ("devel", dict(exported_as="previous_series")),
254 ("1.0", dict(exported_as="parent_series")),251 ("1.0", dict(exported_as="parent_series")),
@@ -370,7 +367,7 @@
370367
371 main_archive = exported(368 main_archive = exported(
372 Reference(369 Reference(
373 Interface, # Really IArchive, see below for circular import fix.370 Interface, # Really IArchive, see below for circular import fix.
374 title=_('Distribution Main Archive')))371 title=_('Distribution Main Archive')))
375372
376 supported = exported(373 supported = exported(
@@ -418,7 +415,7 @@
418 architectures = exported(415 architectures = exported(
419 CollectionField(416 CollectionField(
420 title=_("All architectures in this series."),417 title=_("All architectures in this series."),
421 value_type=Reference(schema=Interface), # IDistroArchSeries.418 value_type=Reference(schema=Interface), # IDistroArchSeries.
422 readonly=True))419 readonly=True))
423420
424 enabled_architectures = Attribute(421 enabled_architectures = Attribute(
@@ -848,18 +845,18 @@
848845
849 @operation_parameters(846 @operation_parameters(
850 parent_series=Reference(847 parent_series=Reference(
851 schema=Interface, # IDistroSeries848 schema=Interface, # IDistroSeries
852 title=_("The parent series to consider."),849 title=_("The parent series to consider."),
853 required=False),850 required=False),
854 difference_type=Choice(851 difference_type=Choice(
855 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceType852 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceType
856 title=_("Only return differences of this type."), required=False),853 title=_("Only return differences of this type."), required=False),
857 source_package_name_filter=TextLine(854 source_package_name_filter=TextLine(
858 title=_("Only return differences for packages matching this "855 title=_("Only return differences for packages matching this "
859 "name."),856 "name."),
860 required=False),857 required=False),
861 status=Choice(858 status=Choice(
862 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceStatus859 vocabulary=DBEnumeratedType, # DistroSeriesDifferenceStatus
863 title=_("Only return differences of this status."),860 title=_("Only return differences of this status."),
864 required=False),861 required=False),
865 child_version_higher=Bool(862 child_version_higher=Bool(
@@ -886,6 +883,12 @@
886 child's version is higher than the parent's version.883 child's version is higher than the parent's version.
887 """884 """
888885
886 def isInitializing():
887 """Is this series initializing?"""
888
889 def isInitialized():
890 """Has this series been initialized?"""
891
889892
890class IDistroSeriesEditRestricted(Interface):893class IDistroSeriesEditRestricted(Interface):
891 """IDistroSeries properties which require launchpad.Edit."""894 """IDistroSeries properties which require launchpad.Edit."""
@@ -1032,7 +1035,7 @@
10321035
1033class DerivationError(Exception):1036class DerivationError(Exception):
1034 """Raised when there is a problem deriving a distroseries."""1037 """Raised when there is a problem deriving a distroseries."""
1035 webservice_error(400) # Bad Request1038 webservice_error(400) # Bad Request
1036 _message_prefix = "Error deriving distro series"1039 _message_prefix = "Error deriving distro series"
10371040
10381041
10391042
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2011-06-08 15:27:40 +0000
+++ lib/lp/registry/model/distroseries.py 2011-06-10 15:18:35 +0000
@@ -802,13 +802,6 @@
802 return not self.getParentSeries() == []802 return not self.getParentSeries() == []
803803
804 @property804 @property
805 def is_initialising(self):
806 """See `IDistroSeries`."""
807 return not getUtility(
808 IInitialiseDistroSeriesJobSource).getPendingJobsForDistroseries(
809 self).is_empty()
810
811 @property
812 def bugtargetname(self):805 def bugtargetname(self):
813 """See IBugTarget."""806 """See IBugTarget."""
814 # XXX mpt 2007-07-10 bugs 113258, 113262:807 # XXX mpt 2007-07-10 bugs 113258, 113262:
@@ -2037,6 +2030,17 @@
2037 status=status,2030 status=status,
2038 child_version_higher=child_version_higher)2031 child_version_higher=child_version_higher)
20392032
2033 def isInitializing(self):
2034 """See `IDistroSeries`."""
2035 job_source = getUtility(IInitialiseDistroSeriesJobSource)
2036 pending_jobs = job_source.getPendingJobsForDistroseries(self)
2037 return not pending_jobs.is_empty()
2038
2039 def isInitialized(self):
2040 """See `IDistroSeries`."""
2041 published = self.main_archive.getPublishedSources(distroseries=self)
2042 return not published.is_empty()
2043
20402044
2041class DistroSeriesSet:2045class DistroSeriesSet:
2042 implements(IDistroSeriesSet)2046 implements(IDistroSeriesSet)
20432047
=== modified file 'lib/lp/registry/templates/distroseries-index.pt'
--- lib/lp/registry/templates/distroseries-index.pt 2011-05-24 10:08:33 +0000
+++ lib/lp/registry/templates/distroseries-index.pt 2011-06-10 15:18:35 +0000
@@ -68,7 +68,8 @@
68 <tal:derivation68 <tal:derivation
69 tal:condition="request/features/soyuz.derived_series_ui.enabled">69 tal:condition="request/features/soyuz.derived_series_ui.enabled">
70 <div class="yui-u"70 <div class="yui-u"
71 tal:condition="python:context.is_derived_series or context.is_initialising">71 tal:condition="python: context.is_derived_series or
72 context.isInitializing()">
72 <div tal:replace="structure context/@@+portlet-derivation" />73 <div tal:replace="structure context/@@+portlet-derivation" />
73 </div>74 </div>
74 </tal:derivation>75 </tal:derivation>
7576
=== modified file 'lib/lp/registry/templates/distroseries-initialize.pt'
--- lib/lp/registry/templates/distroseries-initialize.pt 2011-06-06 09:32:14 +0000
+++ lib/lp/registry/templates/distroseries-initialize.pt 2011-06-10 15:18:35 +0000
@@ -12,13 +12,13 @@
12 </metal:head-epilogue>12 </metal:head-epilogue>
13 <body>13 <body>
14 <div metal:fill-slot="main">14 <div metal:fill-slot="main">
15 <tal:enabled condition="view/is_derived_series_feature_enabled">15
16 <tal:enabled condition="view/show_derivation_form">
16 <div class="top-portlet">17 <div class="top-portlet">
17 This page allows you to initialize a distribution series.18 This page allows you to initialize a distribution series.
18 <a href="/+help/init-series-title-help.html"19 <a href="/+help/init-series-title-help.html"
19 target="help" class="sprite maybe">&nbsp;20 target="help" class="sprite maybe">&nbsp;
20 <span class="invisible-link">Initialization help</span></a>21 <span class="invisible-link">Initialization help</span></a>
21
22 </div>22 </div>
23 <p class="error message javascript-disabled">23 <p class="error message javascript-disabled">
24 Javascript is required to use this page. Please enable24 Javascript is required to use this page. Please enable
@@ -42,7 +42,8 @@
42 many thousands of packages is likely to take hours to complete.42 many thousands of packages is likely to take hours to complete.
43 </p>43 </p>
44 </tal:enabled>44 </tal:enabled>
45 <tal:disabled condition="not:view/is_derived_series_feature_enabled">45
46 <tal:disabled condition="view/show_derivation_not_yet_available">
46 <p class="error message">47 <p class="error message">
47 The Derivative Distributions feature is under development48 The Derivative Distributions feature is under development
48 and is not yet generally available. You can read more about49 and is not yet generally available. You can read more about
@@ -51,6 +52,21 @@
51 page</a>.52 page</a>.
52 </p>53 </p>
53 </tal:disabled>54 </tal:disabled>
55
56 <tal:already-initialized condition="view/show_already_initialized_message">
57 <p class="error message">
58 This series already contains source packages and cannot be
59 initialized again.
60 </p>
61 </tal:already-initialized>
62
63 <tal:already-initializing
64 condition="view/show_already_initializing_message">
65 <p class="error message">
66 This series is already being initialized.
67 </p>
68 </tal:already-initializing>
69
54 </div>70 </div>
55 </body>71 </body>
56</html>72</html>
5773
=== modified file 'lib/lp/registry/templates/distroseries-portlet-derivation.pt'
--- lib/lp/registry/templates/distroseries-portlet-derivation.pt 2011-05-31 11:55:38 +0000
+++ lib/lp/registry/templates/distroseries-portlet-derivation.pt 2011-06-10 15:18:35 +0000
@@ -5,7 +5,7 @@
5 id="series-derivation" class="portlet"5 id="series-derivation" class="portlet"
6 tal:define="overview_menu context/menu:overview">6 tal:define="overview_menu context/menu:overview">
7 <tal:is_derived condition="context/is_derived_series">7 <tal:is_derived condition="context/is_derived_series">
8 <tal:is_initialised condition="not: context/is_initialising">8 <tal:is_initialised condition="not: context/isInitializing">
9 <tal:one_parent condition="view/has_unique_parent">9 <tal:one_parent condition="view/has_unique_parent">
10 <h2>Derived from <tal:name replace="view/unique_parent/displayname"/></h2>10 <h2>Derived from <tal:name replace="view/unique_parent/displayname"/></h2>
11 </tal:one_parent>11 </tal:one_parent>
@@ -61,7 +61,7 @@
61 </tal:diffs>61 </tal:diffs>
62 </tal:is_initialised>62 </tal:is_initialised>
63 </tal:is_derived>63 </tal:is_derived>
64 <tal:is_initialising condition="context/is_initialising">64 <tal:is_initialising condition="context/isInitializing">
65 <h2>Series initialisation in progress</h2>65 <h2>Series initialisation in progress</h2>
66 This series is initialising.66 This series is initialising.
67 </tal:is_initialising>67 </tal:is_initialising>
6868
=== modified file 'lib/lp/registry/tests/test_distroseries.py'
--- lib/lp/registry/tests/test_distroseries.py 2011-05-31 15:40:10 +0000
+++ lib/lp/registry/tests/test_distroseries.py 2011-06-10 15:18:35 +0000
@@ -222,22 +222,31 @@
222 self.assertEquals(registrant, distroseries.registrant)222 self.assertEquals(registrant, distroseries.registrant)
223 self.assertNotEqual(distroseries.registrant, distroseries.owner)223 self.assertNotEqual(distroseries.registrant, distroseries.owner)
224224
225 def test_is_initialising(self):225 def test_isInitializing(self):
226 # The series is_initialising only if there is an initialisation226 # The series method isInitializing() returns True only if there is an
227 # job with a pending status attached to this series.227 # initialisation job with a pending status attached to this series.
228 distroseries = self.factory.makeDistroSeries()228 distroseries = self.factory.makeDistroSeries()
229 parent_distroseries = self.factory.makeDistroSeries()229 parent_distroseries = self.factory.makeDistroSeries()
230 self.assertEquals(False, distroseries.is_initialising)230 self.assertFalse(distroseries.isInitializing())
231 job_source = getUtility(IInitialiseDistroSeriesJobSource)231 job_source = getUtility(IInitialiseDistroSeriesJobSource)
232 job = job_source.create(distroseries, [parent_distroseries.id])232 job = job_source.create(distroseries, [parent_distroseries.id])
233 self.assertEquals(True, distroseries.is_initialising)233 self.assertTrue(distroseries.isInitializing())
234 job.start()234 job.start()
235 self.assertEquals(True, distroseries.is_initialising)235 self.assertTrue(distroseries.isInitializing())
236 job.queue()236 job.queue()
237 self.assertEquals(True, distroseries.is_initialising)237 self.assertTrue(distroseries.isInitializing())
238 job.start()238 job.start()
239 job.complete()239 job.complete()
240 self.assertEquals(False, distroseries.is_initialising)240 self.assertFalse(distroseries.isInitializing())
241
242 def test_isInitialized(self):
243 # The series method isInitialized() returns True once the series has
244 # been initialized.
245 distroseries = self.factory.makeDistroSeries()
246 self.assertFalse(distroseries.isInitialized())
247 self.factory.makeSourcePackagePublishingHistory(
248 distroseries=distroseries, archive=distroseries.main_archive)
249 self.assertTrue(distroseries.isInitialized())
241250
242251
243class TestDistroSeriesPackaging(TestCaseWithFactory):252class TestDistroSeriesPackaging(TestCaseWithFactory):
244253
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2011-05-19 15:15:16 +0000
+++ lib/lp/testing/__init__.py 2011-06-10 15:18:35 +0000
@@ -163,7 +163,6 @@
163 )163 )
164from lp.testing.fixture import ZopeEventHandlerFixture164from lp.testing.fixture import ZopeEventHandlerFixture
165from lp.testing.karma import KarmaRecorder165from lp.testing.karma import KarmaRecorder
166from lp.testing.matchers import Provides
167from lp.testing.windmill import (166from lp.testing.windmill import (
168 constants,167 constants,
169 lpuser,168 lpuser,
@@ -390,6 +389,7 @@
390389
391 def assertProvides(self, obj, interface):390 def assertProvides(self, obj, interface):
392 """Assert 'obj' correctly provides 'interface'."""391 """Assert 'obj' correctly provides 'interface'."""
392 from lp.testing.matchers import Provides
393 self.assertThat(obj, Provides(interface))393 self.assertThat(obj, Provides(interface))
394394
395 def assertClassImplements(self, cls, interface):395 def assertClassImplements(self, cls, interface):
396396
=== modified file 'lib/lp/testing/matchers.py'
--- lib/lp/testing/matchers.py 2011-03-02 23:54:25 +0000
+++ lib/lp/testing/matchers.py 2011-06-10 15:18:35 +0000
@@ -8,6 +8,7 @@
8 'DocTestMatches',8 'DocTestMatches',
9 'DoesNotCorrectlyProvide',9 'DoesNotCorrectlyProvide',
10 'DoesNotProvide',10 'DoesNotProvide',
11 'EqualsIgnoringWhitespace',
11 'HasQueryCount',12 'HasQueryCount',
12 'IsNotProxied',13 'IsNotProxied',
13 'IsProxied',14 'IsProxied',
@@ -20,17 +21,17 @@
20 ]21 ]
2122
22from lazr.lifecycle.snapshot import Snapshot23from lazr.lifecycle.snapshot import Snapshot
24from testtools import matchers
23from testtools.content import Content25from testtools.content import Content
24from testtools.content_type import UTF8_TEXT26from testtools.content_type import UTF8_TEXT
25from testtools.matchers import (27from testtools.matchers import (
28 DocTestMatches as OriginalDocTestMatches,
26 Equals,29 Equals,
27 DocTestMatches as OriginalDocTestMatches,
28 LessThan,30 LessThan,
29 Matcher,31 Matcher,
30 Mismatch,32 Mismatch,
31 MismatchesAll,33 MismatchesAll,
32 )34 )
33from testtools import matchers
34from zope.interface.exceptions import (35from zope.interface.exceptions import (
35 BrokenImplementation,36 BrokenImplementation,
36 BrokenMethodImplementation,37 BrokenMethodImplementation,
@@ -44,6 +45,7 @@
4445
45from canonical.launchpad.webapp import canonical_url46from canonical.launchpad.webapp import canonical_url
46from canonical.launchpad.webapp.batching import BatchNavigator47from canonical.launchpad.webapp.batching import BatchNavigator
48from lp.testing import normalize_whitespace
47from lp.testing._login import person_logged_in49from lp.testing._login import person_logged_in
48from lp.testing._webservice import QueryCollector50from lp.testing._webservice import QueryCollector
4951
@@ -290,7 +292,8 @@
290292
291 :param singular: The singular header the batch should be using.293 :param singular: The singular header the batch should be using.
292 :param plural: The plural header the batch should be using.294 :param plural: The plural header the batch should be using.
293 :param batch_size: The batch size that should be configured by default.295 :param batch_size: The batch size that should be configured by
296 default.
294 """297 """
295 self._single = Equals(singular)298 self._single = Equals(singular)
296 self._plural = Equals(plural)299 self._plural = Equals(plural)
@@ -438,3 +441,21 @@
438 text = widget.findAll(attrs={'class': 'yui3-activator-data-box'})[0]441 text = widget.findAll(attrs={'class': 'yui3-activator-data-box'})[0]
439 text_matcher = DocTestMatches(extract_text(text))442 text_matcher = DocTestMatches(extract_text(text))
440 return text_matcher.match(matchee)443 return text_matcher.match(matchee)
444
445
446class EqualsIgnoringWhitespace(Equals):
447 """Compare equality, ignoring whitespace in strings.
448
449 Whitespace in strings is normalized before comparison. All other objects
450 are compared as they come.
451 """
452
453 def __init__(self, expected):
454 if isinstance(expected, (str, unicode)):
455 expected = normalize_whitespace(expected)
456 super(EqualsIgnoringWhitespace, self).__init__(expected)
457
458 def match(self, observed):
459 if isinstance(observed, (str, unicode)):
460 observed = normalize_whitespace(observed)
461 return super(EqualsIgnoringWhitespace, self).match(observed)
441462
=== modified file 'lib/lp/testing/tests/test_matchers.py'
--- lib/lp/testing/tests/test_matchers.py 2011-02-25 07:15:06 +0000
+++ lib/lp/testing/tests/test_matchers.py 2011-06-10 15:18:35 +0000
@@ -29,6 +29,7 @@
29 DoesNotContain,29 DoesNotContain,
30 DoesNotCorrectlyProvide,30 DoesNotCorrectlyProvide,
31 DoesNotProvide,31 DoesNotProvide,
32 EqualsIgnoringWhitespace,
32 HasQueryCount,33 HasQueryCount,
33 IsNotProxied,34 IsNotProxied,
34 IsProxied,35 IsProxied,
@@ -274,3 +275,36 @@
274 matcher = Contains("bar")275 matcher = Contains("bar")
275 mismatch = matcher.match("foo")276 mismatch = matcher.match("foo")
276 self.assertEqual("bar", mismatch.expected)277 self.assertEqual("bar", mismatch.expected)
278
279
280class EqualsIgnoringWhitespaceTests(TestCase):
281
282 def test_str(self):
283 matcher = EqualsIgnoringWhitespace("abc")
284 self.assertEqual("EqualsIgnoringWhitespace('abc')", str(matcher))
285
286 def test_match_str(self):
287 matcher = EqualsIgnoringWhitespace("one \t two \n three")
288 self.assertIs(None, matcher.match(" one \r two three "))
289
290 def test_mismatch_str(self):
291 matcher = EqualsIgnoringWhitespace("one \t two \n three")
292 mismatch = matcher.match(" one \r three ")
293 self.assertEqual(
294 "'one two three' != 'one three'",
295 mismatch.describe())
296
297 def test_match_unicode(self):
298 matcher = EqualsIgnoringWhitespace(u"one \t two \n \u1234 ")
299 self.assertIs(None, matcher.match(u" one \r two \u1234 "))
300
301 def test_mismatch_unicode(self):
302 matcher = EqualsIgnoringWhitespace(u"one \t two \n \u1234 ")
303 mismatch = matcher.match(u" one \r \u1234 ")
304 self.assertEqual(
305 u"u'one two \\u1234' != u'one \\u1234'",
306 mismatch.describe())
307
308 def test_match_non_string(self):
309 matcher = EqualsIgnoringWhitespace(1234)
310 self.assertIs(None, matcher.match(1234))