Merge lp:~michael.nelson/launchpad/496862-ppa-installable-binaries into lp:launchpad

Proposed by Michael Nelson
Status: Rejected
Rejected by: Michael Nelson
Proposed branch: lp:~michael.nelson/launchpad/496862-ppa-installable-binaries
Merge into: lp:launchpad
Diff against target: 675 lines (+240/-89)
10 files modified
lib/lp/soyuz/browser/archive.py (+79/-40)
lib/lp/soyuz/browser/tests/archive-views.txt (+51/-37)
lib/lp/soyuz/browser/tests/test_archive_view.py (+59/-0)
lib/lp/soyuz/doc/archive.txt (+14/-4)
lib/lp/soyuz/interfaces/archive.py (+7/-0)
lib/lp/soyuz/model/archive.py (+23/-1)
lib/lp/soyuz/templates/archive-copy-packages.pt (+1/-1)
lib/lp/soyuz/templates/archive-delete-packages.pt (+1/-1)
lib/lp/soyuz/templates/archive-index.pt (+1/-1)
lib/lp/soyuz/templates/archive-macros.pt (+4/-4)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/496862-ppa-installable-binaries
Reviewer Review Type Date Requested Status
Jonathan Lange (community) Approve
Review via email: mp+21623@code.launchpad.net

Description of the change

This branch is the first of two to address bug 496862.

It adds an ArchiveArchFactory to create a vocabulary dynamically depending on the binaries published in the archive.

The ArchiveSourcePackageListViewBase class is refactored to ArchivePackagesViewBase so that it can be re-used for both source and binary packages.

Finally, the ArchiveView (for the PPA index page) is updated to present binary packages.

The next branch will switch the PPA index ui to present the binary packages.

To test:
bin/test -vv -t archive-views.txt -t archive.txt -t TestArchiveView

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :
Download full text (4.6 KiB)

On Thu, Mar 18, 2010 at 9:24 AM, Michael Nelson
<email address hidden> wrote:
> Michael Nelson has proposed merging lp:~michael.nelson/launchpad/496862-ppa-installable-binaries into lp:launchpad/devel.
>
> Requested reviews:
>  Canonical Launchpad Engineering (launchpad)
>
>
> This branch is the first of two to address bug 496862.
>
> It adds an ArchiveArchFactory to create a vocabulary dynamically depending on the binaries published in the archive.
>
> The ArchiveSourcePackageListViewBase class is refactored to ArchivePackagesViewBase so that it can be re-used for both source and binary packages.
>
> Finally, the ArchiveView (for the PPA index page) is updated to present binary packages.
>
> The next branch will switch the PPA index ui to present the binary packages.
>
> To test:
> bin/test -vv -t archive-views.txt -t archive.txt -t TestArchiveView
>

Good patch. I like the tests and the renames.

Some minor issues with method names and docstring formatting and a
couple of questions.

jml

> --
> https://code.launchpad.net/~michael.nelson/launchpad/496862-ppa-installable-binaries/+merge/21623
> Your team Launchpad code reviewers from Canonical is subscribed to branch lp:launchpad/devel.
>
> === modified file 'lib/lp/soyuz/browser/archive.py'
> --- lib/lp/soyuz/browser/archive.py     2010-03-10 12:50:18 +0000
> +++ lib/lp/soyuz/browser/archive.py     2010-03-18 09:24:24 +0000
> @@ -589,6 +589,26 @@
>         return SimpleVocabulary(series_terms)
>
>
> +class ArchiveArchVocabularyFactory:
> +    """A factory for generating vocabularies of an archive's arches in
> +    a given distroseries."""
> +
> +    implements(IContextSourceBinder)
> +
> +    def __call__(self, context):
> +        """Return a vocabulary created dynamically from the context archive.
> +
> +        :param context: The archive used to generating vocabulary.
> +        """
> +        arch_terms = []
> +        for arch in context.arches_with_binaries:
> +            term = SimpleTerm(
> +                arch, token=arch.architecturetag,
> +                title=arch.displayname)
> +            arch_terms.append(term)
> +        return SimpleVocabulary(arch_terms)
> +

Why is this a class and not a function? Is the implements() bit really
important?

> === added file 'lib/lp/soyuz/browser/tests/test_archive_view.py'
> --- lib/lp/soyuz/browser/tests/test_archive_view.py     1970-01-01 00:00:00 +0000
> +++ lib/lp/soyuz/browser/tests/test_archive_view.py     2010-03-18 09:24:24 +0000
> @@ -0,0 +1,59 @@
> +# Copyright 2010 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Test the main PPA (user) view functionality."""
> +
> +__metaclass__ = type
> +
> +import unittest
> +
> +from canonical.launchpad.ftests import login
> +from canonical.testing import LaunchpadFunctionalLayer
> +from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
> +from lp.testing import TestCaseWithFactory
> +from lp.testing.views import create_initialized_view
> +
> +
> +class TestArchiveView(TestCaseWithFactory):
> +
> +    layer = LaunchpadFunctionalLayer
> +

You need the librarian?

> +    def setUp(self)...

Read more...

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

On Thu, Mar 18, 2010 at 12:02 PM, Jonathan Lange <email address hidden> wrote:
...
>> +    def make_binaries_readable(self, binaries):
>
> Methods should be camelCase :( I hate that rule.
>

Per our recent discussion, this is comment is correct and the method
needs to change...

>> +    def test_batched_packages(self):
>
> test_batchedPackages :(
>

But this comment is not. The test can remain the same.

>> +    def test_batched_packages_filter_arch(self):
>
> testBatchedPackagesFilterArch
>

Likewise here.

jml

Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (4.2 KiB)

On Thu, Mar 18, 2010 at 12:03 PM, Jonathan Lange <email address hidden> wrote:
> On Thu, Mar 18, 2010 at 9:24 AM, Michael Nelson
...
>> === modified file 'lib/lp/soyuz/browser/archive.py'
>> --- lib/lp/soyuz/browser/archive.py     2010-03-10 12:50:18 +0000
>> +++ lib/lp/soyuz/browser/archive.py     2010-03-18 09:24:24 +0000
>> @@ -589,6 +589,26 @@
>>         return SimpleVocabulary(series_terms)
>>
>>
>> +class ArchiveArchVocabularyFactory:
>> +    """A factory for generating vocabularies of an archive's arches in
>> +    a given distroseries."""
>> +
>> +    implements(IContextSourceBinder)
>> +
>> +    def __call__(self, context):
>> +        """Return a vocabulary created dynamically from the context archive.
>> +
>> +        :param context: The archive used to generating vocabulary.
>> +        """
>> +        arch_terms = []
>> +        for arch in context.arches_with_binaries:
>> +            term = SimpleTerm(
>> +                arch, token=arch.architecturetag,
>> +                title=arch.displayname)
>> +            arch_terms.append(term)
>> +        return SimpleVocabulary(arch_terms)
>> +
>
> Why is this a class and not a function? Is the implements() bit really
> important?
>

Yep, but I used the zope.interface.directlyProvides as you mentioned,
and it worked well :)

>> === added file 'lib/lp/soyuz/browser/tests/test_archive_view.py'
>> --- lib/lp/soyuz/browser/tests/test_archive_view.py     1970-01-01 00:00:00 +0000
>> +++ lib/lp/soyuz/browser/tests/test_archive_view.py     2010-03-18 09:24:24 +0000
>> @@ -0,0 +1,59 @@
>> +# Copyright 2010 Canonical Ltd.  This software is licensed under the
>> +# GNU Affero General Public License version 3 (see the file LICENSE).
>> +
>> +"""Test the main PPA (user) view functionality."""
>> +
>> +__metaclass__ = type
>> +
>> +import unittest
>> +
>> +from canonical.launchpad.ftests import login
>> +from canonical.testing import LaunchpadFunctionalLayer
>> +from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
>> +from lp.testing import TestCaseWithFactory
>> +from lp.testing.views import create_initialized_view
>> +
>> +
>> +class TestArchiveView(TestCaseWithFactory):
>> +
>> +    layer = LaunchpadFunctionalLayer
>> +
>
> You need the librarian?

Yes, publishing test data with the SoyuzTestPublisher adds librarian
files for the tests.

>
>> +    def setUp(self):
>> +        """Create a ppa with some publishings for different arches
>> +        and in different distroseries."""
>
> Closing triple quotes on newline please.

Done.

>
>> +    def make_binaries_readable(self, binaries):
>
> Methods should be camelCase :( I hate that rule.

Done.

>
>> +    def test_batched_packages(self):
>
> test_batchedPackages :(

Not done, as your subsequent email.

>
>> +    def test_batched_packages_filter_arch(self):
>
> testBatchedPackagesFilterArch

And again.

>
>> === modified file 'lib/lp/soyuz/interfaces/archive.py'
>> --- lib/lp/soyuz/interfaces/archive.py  2010-03-17 16:45:38 +0000
>> +++ lib/lp/soyuz/interfaces/archive.py  2010-03-18 09:24:24 +0000
>> @@ -47,6 +47,7 @@
>>  from canonical.launchpad.interfaces.launchpad import IPrivacy
>>  from lp.registry.interfaces.role import IHasOwner
>> ...

Read more...

=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2010-03-18 09:08:28 +0000
+++ lib/lp/soyuz/browser/archive.py 2010-03-18 17:01:59 +0000
@@ -33,7 +33,7 @@
33from zope.app.form.browser import TextAreaWidget33from zope.app.form.browser import TextAreaWidget
34from zope.component import getUtility34from zope.component import getUtility
35from zope.formlib import form35from zope.formlib import form
36from zope.interface import implements, Interface36from zope.interface import directlyProvides, implements, Interface
37from zope.security.proxy import removeSecurityProxy37from zope.security.proxy import removeSecurityProxy
38from zope.schema import Choice, List, TextLine38from zope.schema import Choice, List, TextLine
39from zope.schema.interfaces import IContextSourceBinder39from zope.schema.interfaces import IContextSourceBinder
@@ -568,45 +568,22 @@
568 return list(copy_requests)568 return list(copy_requests)
569569
570570
571class ArchiveSeriesVocabularyFactory:571def archive_series_vocabulary_factory(context):
572 """A factory for generating vocabularies of an archive's series."""572 """Return a vocabulary created dynamically from the context archive.
573573
574 implements(IContextSourceBinder)574 :param context: The context used to generate the vocabulary. This
575575 is passed automatically by the zope machinery. Therefore
576 def __call__(self, context):576 this factory can only be used in a class where the context is
577 """Return a vocabulary created dynamically from the context archive.577 an IArchive.
578578 """
579 :param context: The context used to generate the vocabulary. This579 series_terms = []
580 is passed automatically by the zope machinery. Therefore580 for distroseries in context.series_with_sources:
581 this factory can only be used in a class where the context is581 series_terms.append(
582 an IArchive.582 SimpleTerm(distroseries, token=distroseries.name,
583 """583 title=distroseries.displayname))
584 series_terms = []584 return SimpleVocabulary(series_terms)
585 for distroseries in context.series_with_sources:585
586 series_terms.append(586directlyProvides(archive_series_vocabulary_factory, IContextSourceBinder)
587 SimpleTerm(distroseries, token=distroseries.name,
588 title=distroseries.displayname))
589 return SimpleVocabulary(series_terms)
590
591
592class ArchiveArchVocabularyFactory:
593 """A factory for generating vocabularies of an archive's arches in
594 a given distroseries."""
595
596 implements(IContextSourceBinder)
597
598 def __call__(self, context):
599 """Return a vocabulary created dynamically from the context archive.
600
601 :param context: The archive used to generating vocabulary.
602 """
603 arch_terms = []
604 for arch in context.arches_with_binaries:
605 term = SimpleTerm(
606 arch, token=arch.architecturetag,
607 title=arch.displayname)
608 arch_terms.append(term)
609 return SimpleVocabulary(arch_terms)
610587
611588
612class SeriesFilterWidget(LaunchpadDropdownWidget):589class SeriesFilterWidget(LaunchpadDropdownWidget):
@@ -625,7 +602,7 @@
625 title=_("Package name contains"), required=False)602 title=_("Package name contains"), required=False)
626603
627 series_filter = Choice(604 series_filter = Choice(
628 source=ArchiveSeriesVocabularyFactory(), required=False)605 source=archive_series_vocabulary_factory, required=False)
629606
630 status_filter = Choice(vocabulary=SimpleVocabulary((607 status_filter = Choice(vocabulary=SimpleVocabulary((
631 SimpleTerm(active_publishing_status, 'published', 'Published'),608 SimpleTerm(active_publishing_status, 'published', 'Published'),
@@ -633,10 +610,26 @@
633 )), required=False)610 )), required=False)
634611
635612
613def archive_arch_vocabulary_factory(context):
614 """Return a vocabulary created dynamically from the context archive.
615
616 :param context: The archive used to generating vocabulary.
617 """
618 arch_terms = []
619 for arch in context.arches_with_binaries:
620 term = SimpleTerm(
621 arch, token=arch.architecturetag,
622 title=arch.displayname)
623 arch_terms.append(term)
624 return SimpleVocabulary(arch_terms)
625
626directlyProvides(archive_arch_vocabulary_factory, IContextSourceBinder)
627
628
636class IArchiveBinaryPackageFilter(IPPAPackageFilter):629class IArchiveBinaryPackageFilter(IPPAPackageFilter):
637 """The interface used for filtering binary packages."""630 """The interface used for filtering binary packages."""
638 arch_filter = Choice(631 arch_filter = Choice(
639 source=ArchiveArchVocabularyFactory(), required=False)632 source=archive_arch_vocabulary_factory, required=False)
640633
641634
642class ArchivePackagesViewBase(ArchiveViewBase, LaunchpadFormView):635class ArchivePackagesViewBase(ArchiveViewBase, LaunchpadFormView):
643636
=== modified file 'lib/lp/soyuz/browser/tests/test_archive_view.py'
--- lib/lp/soyuz/browser/tests/test_archive_view.py 2010-03-18 09:06:00 +0000
+++ lib/lp/soyuz/browser/tests/test_archive_view.py 2010-03-18 17:14:42 +0000
@@ -29,7 +29,7 @@
2929
30 test_publisher.getPubBinaries(archive=self.ppa)30 test_publisher.getPubBinaries(archive=self.ppa)
3131
32 def make_binaries_readable(self, binaries):32 def makeBinariesReadable(self, binaries):
33 """Return a string representation of the binaries."""33 """Return a string representation of the binaries."""
34 return ", ".join([34 return ", ".join([
35 "%s in %s" % (35 "%s in %s" % (
@@ -45,7 +45,7 @@
45 self.assertEqual(45 self.assertEqual(
46 "foo-bin in ubuntutest Breezy Badger Autotest i386, "46 "foo-bin in ubuntutest Breezy Badger Autotest i386, "
47 "foo-bin in ubuntutest Breezy Badger Autotest hppa",47 "foo-bin in ubuntutest Breezy Badger Autotest hppa",
48 self.make_binaries_readable(view.batched_packages))48 self.makeBinariesReadable(view.batched_packages))
4949
50 def test_batched_packages_filter_arch(self):50 def test_batched_packages_filter_arch(self):
51 view = create_initialized_view(51 view = create_initialized_view(
@@ -53,7 +53,7 @@
53 query_string="field.arch_filter=i386")53 query_string="field.arch_filter=i386")
54 self.assertEqual(54 self.assertEqual(
55 "foo-bin in ubuntutest Breezy Badger Autotest i386",55 "foo-bin in ubuntutest Breezy Badger Autotest i386",
56 self.make_binaries_readable(view.batched_packages))56 self.makeBinariesReadable(view.batched_packages))
5757
58def test_suite():58def test_suite():
59 return unittest.TestLoader().loadTestsFromName(__name__)59 return unittest.TestLoader().loadTestsFromName(__name__)
10544. By Michael Nelson

Refactored dynamic vocabularies to functions rather than classes.

10545. By Michael Nelson

test style method renames.

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

All good :)

review: Approve

Unmerged revisions

10545. By Michael Nelson

test style method renames.

10544. By Michael Nelson

Refactored dynamic vocabularies to functions rather than classes.

10543. By Michael Nelson

Removed branch todos as they're done.

10542. By Michael Nelson

Removed lint

10541. By Michael Nelson

Added unit tests for ArchiveView.batched_packages.

10540. By Michael Nelson

Added filtered binary packages to the ArchiveView.

10539. By Michael Nelson

Adds IArchive.arches_with_binaries.

10538. By Michael Nelson

Added failing test for IArchive.arches_with_binaries.

10537. By Michael Nelson

A few more s/sources/packages renaming.

10536. By Michael Nelson

Initial renaming to re-use ArchivePackagesViewBase for both source and binary
package listings.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2010-03-10 12:50:18 +0000
+++ lib/lp/soyuz/browser/archive.py 2010-03-22 11:43:06 +0000
@@ -33,7 +33,7 @@
33from zope.app.form.browser import TextAreaWidget33from zope.app.form.browser import TextAreaWidget
34from zope.component import getUtility34from zope.component import getUtility
35from zope.formlib import form35from zope.formlib import form
36from zope.interface import implements, Interface36from zope.interface import directlyProvides, implements, Interface
37from zope.security.proxy import removeSecurityProxy37from zope.security.proxy import removeSecurityProxy
38from zope.schema import Choice, List, TextLine38from zope.schema import Choice, List, TextLine
39from zope.schema.interfaces import IContextSourceBinder39from zope.schema.interfaces import IContextSourceBinder
@@ -568,25 +568,22 @@
568 return list(copy_requests)568 return list(copy_requests)
569569
570570
571class ArchiveSeriesVocabularyFactory:571def archive_series_vocabulary_factory(context):
572 """A factory for generating vocabularies of an archive's series."""572 """Return a vocabulary created dynamically from the context archive.
573573
574 implements(IContextSourceBinder)574 :param context: The context used to generate the vocabulary. This
575575 is passed automatically by the zope machinery. Therefore
576 def __call__(self, context):576 this factory can only be used in a class where the context is
577 """Return a vocabulary created dynamically from the context archive.577 an IArchive.
578578 """
579 :param context: The context used to generate the vocabulary. This579 series_terms = []
580 is passed automatically by the zope machinery. Therefore580 for distroseries in context.series_with_sources:
581 this factory can only be used in a class where the context is581 series_terms.append(
582 an IArchive.582 SimpleTerm(distroseries, token=distroseries.name,
583 """583 title=distroseries.displayname))
584 series_terms = []584 return SimpleVocabulary(series_terms)
585 for distroseries in context.series_with_sources:585
586 series_terms.append(586directlyProvides(archive_series_vocabulary_factory, IContextSourceBinder)
587 SimpleTerm(distroseries, token=distroseries.name,
588 title=distroseries.displayname))
589 return SimpleVocabulary(series_terms)
590587
591588
592class SeriesFilterWidget(LaunchpadDropdownWidget):589class SeriesFilterWidget(LaunchpadDropdownWidget):
@@ -605,7 +602,7 @@
605 title=_("Package name contains"), required=False)602 title=_("Package name contains"), required=False)
606603
607 series_filter = Choice(604 series_filter = Choice(
608 source=ArchiveSeriesVocabularyFactory(), required=False)605 source=archive_series_vocabulary_factory, required=False)
609606
610 status_filter = Choice(vocabulary=SimpleVocabulary((607 status_filter = Choice(vocabulary=SimpleVocabulary((
611 SimpleTerm(active_publishing_status, 'published', 'Published'),608 SimpleTerm(active_publishing_status, 'published', 'Published'),
@@ -613,8 +610,30 @@
613 )), required=False)610 )), required=False)
614611
615612
616class ArchiveSourcePackageListViewBase(ArchiveViewBase, LaunchpadFormView):613def archive_arch_vocabulary_factory(context):
617 """A Form view for filtering and batching source packages."""614 """Return a vocabulary created dynamically from the context archive.
615
616 :param context: The archive used to generating vocabulary.
617 """
618 arch_terms = []
619 for arch in context.arches_with_binaries:
620 term = SimpleTerm(
621 arch, token=arch.architecturetag,
622 title=arch.displayname)
623 arch_terms.append(term)
624 return SimpleVocabulary(arch_terms)
625
626directlyProvides(archive_arch_vocabulary_factory, IContextSourceBinder)
627
628
629class IArchiveBinaryPackageFilter(IPPAPackageFilter):
630 """The interface used for filtering binary packages."""
631 arch_filter = Choice(
632 source=archive_arch_vocabulary_factory, required=False)
633
634
635class ArchivePackagesViewBase(ArchiveViewBase, LaunchpadFormView):
636 """A form view for filtering and batching source or binary packages."""
618637
619 schema = IPPAPackageFilter638 schema = IPPAPackageFilter
620 custom_widget('series_filter', SeriesFilterWidget)639 custom_widget('series_filter', SeriesFilterWidget)
@@ -622,7 +641,7 @@
622641
623 # By default this view will not display the sources with selectable642 # By default this view will not display the sources with selectable
624 # checkboxes, but subclasses can override as needed.643 # checkboxes, but subclasses can override as needed.
625 selectable_sources = False644 selectable_packages = False
626645
627 @cachedproperty646 @cachedproperty
628 def series_with_sources(self):647 def series_with_sources(self):
@@ -680,7 +699,7 @@
680 self.getSelectedFilterValue('series_filter'))699 self.getSelectedFilterValue('series_filter'))
681700
682 @property701 @property
683 def filtered_sources(self):702 def filtered_packages(self):
684 """Return the source results for display after filtering."""703 """Return the source results for display after filtering."""
685 return self.context.getPublishedSources(704 return self.context.getPublishedSources(
686 name=self.specified_name_filter,705 name=self.specified_name_filter,
@@ -707,31 +726,33 @@
707 @cachedproperty726 @cachedproperty
708 def batchnav(self):727 def batchnav(self):
709 """Return a batch navigator of the filtered sources."""728 """Return a batch navigator of the filtered sources."""
710 return BatchNavigator(self.filtered_sources, self.request)729 return BatchNavigator(self.filtered_packages, self.request)
711730
712 @cachedproperty731 @cachedproperty
713 def batched_sources(self):732 def batched_packages(self):
714 """Return the current batch of archive source publications."""733 """Return the current batch of archive source publications."""
715 results = list(self.batchnav.currentBatch())734 results = list(self.batchnav.currentBatch())
716 return ArchiveSourcePublications(results)735 return ArchiveSourcePublications(results)
717736
718 @cachedproperty737 @cachedproperty
719 def has_sources_for_display(self):738 def has_packages_for_display(self):
720 """Whether or not the PPA has any source packages for display.739 """Whether or not the PPA has any source packages for display.
721740
722 This is after any filtering or overriding of the sources() method.741 This is after any filtering or overriding of the sources() method.
723 """742 """
724 # XXX cprov 20080708 bug=246200: use bool() when it gets fixed743 # XXX cprov 20080708 bug=246200: use bool() when it gets fixed
725 # in storm.744 # in storm.
726 return self.filtered_sources.count() > 0745 return self.filtered_packages.count() > 0
727746
728747
729class ArchiveView(ArchiveSourcePackageListViewBase):748class ArchiveView(ArchivePackagesViewBase):
730 """Default Archive view class.749 """Default Archive view class.
731750
732 Implements useful actions and collects useful sets for the page template.751 Implements useful actions and collects useful sets for the page template.
733 """752 """
734753
754 schema = IArchiveBinaryPackageFilter
755
735 __used_for__ = IArchive756 __used_for__ = IArchive
736 implements(IArchiveIndexActionsMenu)757 implements(IArchiveIndexActionsMenu)
737758
@@ -744,6 +765,19 @@
744 super(ArchiveView, self).initialize()765 super(ArchiveView, self).initialize()
745766
746 @property767 @property
768 def filtered_packages(self):
769 """Return the binary results for display after filtering."""
770 return self.context.getAllPublishedBinaries(
771 name=self.specified_name_filter,
772 status=self.getSelectedFilterValue('status_filter'),
773 distroarchseries=self.getSelectedFilterValue('arch_filter'))
774
775 @cachedproperty
776 def batched_packages(self):
777 """Return the current batch of archive binary publications."""
778 return self.batchnav.currentBatch()
779
780 @property
747 def displayname_edit_widget(self):781 def displayname_edit_widget(self):
748 widget = TextLineEditorWidget(782 widget = TextLineEditorWidget(
749 self.context, 'displayname',783 self.context, 'displayname',
@@ -777,6 +811,11 @@
777 return None811 return None
778812
779 @property813 @property
814 def default_arch_filter(self):
815 """Return the architecture identified by the user-agent."""
816 return None
817
818 @property
780 def archive_description_html(self):819 def archive_description_html(self):
781 """The archive's description as HTML."""820 """The archive's description as HTML."""
782 formatter = FormattersAPI821 formatter = FormattersAPI
@@ -889,7 +928,7 @@
889 }928 }
890929
891930
892class ArchivePackagesView(ArchiveSourcePackageListViewBase):931class ArchivePackagesView(ArchivePackagesViewBase):
893 """Detailed packages view for an archive."""932 """Detailed packages view for an archive."""
894 implements(IArchivePackagesActionMenu)933 implements(IArchivePackagesActionMenu)
895934
@@ -915,12 +954,12 @@
915 return self.context.is_copy954 return self.context.is_copy
916955
917956
918class ArchiveSourceSelectionFormView(ArchiveSourcePackageListViewBase):957class ArchiveSourceSelectionFormView(ArchivePackagesViewBase):
919 """Base class to implement a source selection widget for PPAs."""958 """Base class to implement a source selection widget for PPAs."""
920959
921 custom_widget('selected_sources', LabeledMultiCheckBoxWidget)960 custom_widget('selected_sources', LabeledMultiCheckBoxWidget)
922961
923 selectable_sources = True962 selectable_packages = True
924963
925 def setNextURL(self):964 def setNextURL(self):
926 """Set self.next_url based on current context.965 """Set self.next_url based on current context.
@@ -938,7 +977,7 @@
938 """977 """
939 # To create the selected sources field, we need to define a978 # To create the selected sources field, we need to define a
940 # vocabulary based on the currently selected sources (using self979 # vocabulary based on the currently selected sources (using self
941 # batched_sources) but this itself requires the current values of980 # batched_packages) but this itself requires the current values of
942 # the filtering widgets. So we setup the widgets, then add the981 # the filtering widgets. So we setup the widgets, then add the
943 # extra field and create its widget too.982 # extra field and create its widget too.
944 super(ArchiveSourceSelectionFormView, self).setUpWidgets()983 super(ArchiveSourceSelectionFormView, self).setUpWidgets()
@@ -955,7 +994,7 @@
955994
956 Ensure focus is only set if there are sources actually presented.995 Ensure focus is only set if there are sources actually presented.
957 """996 """
958 if not self.has_sources_for_display:997 if not self.has_packages_for_display:
959 return ''998 return ''
960 return LaunchpadFormView.focusedElementScript(self)999 return LaunchpadFormView.focusedElementScript(self)
9611000
@@ -968,7 +1007,7 @@
968 """1007 """
969 terms = []1008 terms = []
9701009
971 for pub in self.batched_sources:1010 for pub in self.batched_packages:
972 terms.append(SimpleTerm(pub, str(pub.id), pub.displayname))1011 terms.append(SimpleTerm(pub, str(pub.id), pub.displayname))
973 return form.Fields(1012 return form.Fields(
974 List(__name__='selected_sources',1013 List(__name__='selected_sources',
@@ -1010,10 +1049,10 @@
1010 return None1049 return None
10111050
1012 @cachedproperty1051 @cachedproperty
1013 def filtered_sources(self):1052 def filtered_packages(self):
1014 """Return the filtered results of publishing records for deletion.1053 """Return the filtered results of publishing records for deletion.
10151054
1016 This overrides ArchiveViewBase.filtered_sources to use a1055 This overrides ArchiveViewBase.filtered_packages to use a
1017 different method on the context specific to deletion records.1056 different method on the context specific to deletion records.
1018 """1057 """
1019 return self.context.getSourcesForDeletion(1058 return self.context.getSourcesForDeletion(
10201059
=== modified file 'lib/lp/soyuz/browser/tests/archive-views.txt'
--- lib/lp/soyuz/browser/tests/archive-views.txt 2010-03-06 04:57:40 +0000
+++ lib/lp/soyuz/browser/tests/archive-views.txt 2010-03-22 11:43:06 +0000
@@ -165,27 +165,17 @@
165 used_css_class: green165 used_css_class: green
166 used_percentage: 0.92166 used_percentage: 0.92
167167
168An ArchiveView provides a batched_sources property that can be used168An ArchiveView provides a batched_packages property that can be used
169to get the current batch of publishing records for an archive:169to get the current batch of installable binaries published for an archive:
170170
171 >>> for publishing in ppa_archive_view.batched_sources:171 >>> for pub in ppa_archive_view.batched_packages:
172 ... print publishing.source_package_name172 ... print "%s in %s" % (
173 cdrkit173 ... pub.binary_package_name,
174 iceweasel174 ... pub.distroarchseries.displayname)
175 pmount175 mozilla-firefox in Ubuntu Warty hppa
176176 mozilla-firefox in Ubuntu Warty i386
177The batched_sources property will also be filtered by distroseries when177 pmount in Ubuntu Warty hppa
178appropriate:178 pmount in Ubuntu Warty i386
179
180 >>> filtered_view = create_initialized_view(
181 ... cprov.archive,
182 ... name="+index",
183 ... method='GET',
184 ... query_string='field.series_filter=warty')
185 >>> for publishing in filtered_view.batched_sources:
186 ... print publishing.source_package_name
187 iceweasel
188 pmount
189179
190The context archive dependencies access is also encapsulated in180The context archive dependencies access is also encapsulated in
191`ArchiveView` with the following aspects:181`ArchiveView` with the following aspects:
@@ -312,7 +302,8 @@
312302
313 >>> from canonical.database.constants import UTC_NOW303 >>> from canonical.database.constants import UTC_NOW
314 >>> login('foo.bar@canonical.com')304 >>> login('foo.bar@canonical.com')
315 >>> view.filtered_sources[1].datepublished = UTC_NOW305 >>> iceweasel = view.context.getPublishedSources(name="iceweasel")[0]
306 >>> iceweasel.datepublished = UTC_NOW
316 >>> login(ANONYMOUS)307 >>> login(ANONYMOUS)
317 >>> print_latest_updates(view.latest_updates)308 >>> print_latest_updates(view.latest_updates)
318 iceweasel - Successfully built309 iceweasel - Successfully built
@@ -333,9 +324,10 @@
333 >>> import pytz324 >>> import pytz
334 >>> thirtyone_days_ago = datetime.now(tz=pytz.UTC) - timedelta(31)325 >>> thirtyone_days_ago = datetime.now(tz=pytz.UTC) - timedelta(31)
335 >>> login('foo.bar@canonical.com')326 >>> login('foo.bar@canonical.com')
336 >>> view.filtered_sources[0].datecreated = UTC_NOW327 >>> sources = view.context.getPublishedSources()
337 >>> view.filtered_sources[1].datecreated = UTC_NOW328 >>> sources[0].datecreated = UTC_NOW
338 >>> view.filtered_sources[2].datecreated = thirtyone_days_ago329 >>> sources[1].datecreated = UTC_NOW
330 >>> sources[2].datecreated = thirtyone_days_ago
339 >>> login(ANONYMOUS)331 >>> login(ANONYMOUS)
340332
341 >>> view.num_updates_over_last_days()333 >>> view.num_updates_over_last_days()
@@ -358,7 +350,7 @@
358 >>> from lp.buildmaster.interfaces.buildbase import BuildStatus350 >>> from lp.buildmaster.interfaces.buildbase import BuildStatus
359 >>> from lp.soyuz.interfaces.build import IBuildSet351 >>> from lp.soyuz.interfaces.build import IBuildSet
360 >>> warty_hppa = getUtility(IDistributionSet)['ubuntu']['warty']['hppa']352 >>> warty_hppa = getUtility(IDistributionSet)['ubuntu']['warty']['hppa']
361 >>> source = view.filtered_sources[0]353 >>> source = sources[0]
362 >>> ignore = source.sourcepackagerelease.createBuild(354 >>> ignore = source.sourcepackagerelease.createBuild(
363 ... distroarchseries=warty_hppa, archive=view.context,355 ... distroarchseries=warty_hppa, archive=view.context,
364 ... pocket=source.pocket)356 ... pocket=source.pocket)
@@ -487,6 +479,28 @@
487corresponding properties such as archive_url, build_counters etc.479corresponding properties such as archive_url, build_counters etc.
488(see ArchiveView above).480(see ArchiveView above).
489481
482An ArchiveView provides a batched_packages property that can be used
483to get the current batch of publishing records for an archive:
484
485 >>> for publishing in ppa_archive_view.batched_packages:
486 ... print publishing.source_package_name
487 cdrkit
488 iceweasel
489 pmount
490
491The batched_packages property will also be filtered by distroseries when
492appropriate:
493
494 >>> filtered_view = create_initialized_view(
495 ... cprov.archive,
496 ... name="+packages",
497 ... method='GET',
498 ... query_string='field.series_filter=warty')
499 >>> for publishing in filtered_view.batched_packages:
500 ... print publishing.source_package_name
501 iceweasel
502 pmount
503
490Additionally, ArchivePackageView can display a string representation504Additionally, ArchivePackageView can display a string representation
491of the series supported by this archive.505of the series supported by this archive.
492506
@@ -530,10 +544,10 @@
530We query the available PUBLISHED sources and use them to build the544We query the available PUBLISHED sources and use them to build the
531'selected_sources' widget.545'selected_sources' widget.
532546
533 >>> [pub.id for pub in view.batched_sources]547 >>> [pub.id for pub in view.batched_packages]
534 [27, 28, 29]548 [27, 28, 29]
535549
536 >>> view.has_sources_for_display550 >>> view.has_packages_for_display
537 True551 True
538552
539 >>> len(view.widgets.get('selected_sources').vocabulary)553 >>> len(view.widgets.get('selected_sources').vocabulary)
@@ -543,7 +557,7 @@
543the user can refine the available options presented. By default all557the user can refine the available options presented. By default all
544available sources are presented with empty filter.558available sources are presented with empty filter.
545559
546 >>> for pub in view.batched_sources:560 >>> for pub in view.batched_packages:
547 ... print pub.displayname561 ... print pub.displayname
548 cdrkit 1.0 in breezy-autotest562 cdrkit 1.0 in breezy-autotest
549 iceweasel 1.0 in warty563 iceweasel 1.0 in warty
@@ -556,7 +570,7 @@
556 ... cprov.archive, name="+delete-packages",570 ... cprov.archive, name="+delete-packages",
557 ... query_string="field.name_filter=pmount")571 ... query_string="field.name_filter=pmount")
558572
559 >>> for pub in view.batched_sources:573 >>> for pub in view.batched_packages:
560 ... print pub.displayname574 ... print pub.displayname
561 pmount 0.1-1 in warty575 pmount 0.1-1 in warty
562576
@@ -568,7 +582,7 @@
568 ... cprov.archive, name="+delete-packages",582 ... cprov.archive, name="+delete-packages",
569 ... query_string="field.name_filter=%C3%A7")583 ... query_string="field.name_filter=%C3%A7")
570584
571 >>> len(list(view.batched_sources))585 >>> len(list(view.batched_packages))
572 0586 0
573587
574Similarly, the sources can be filtered by series:588Similarly, the sources can be filtered by series:
@@ -577,7 +591,7 @@
577 ... cprov.archive, name="+delete-packages",591 ... cprov.archive, name="+delete-packages",
578 ... query_string="field.series_filter=warty")592 ... query_string="field.series_filter=warty")
579593
580 >>> for pub in view.batched_sources:594 >>> for pub in view.batched_packages:
581 ... print pub.displayname595 ... print pub.displayname
582 iceweasel 1.0 in warty596 iceweasel 1.0 in warty
583 pmount 0.1-1 in warty597 pmount 0.1-1 in warty
@@ -589,7 +603,7 @@
589 ... query_string="field.series_filter=warty",603 ... query_string="field.series_filter=warty",
590 ... form={'batch': '1', 'start': '1'})604 ... form={'batch': '1', 'start': '1'})
591605
592 >>> for pub in view.batched_sources:606 >>> for pub in view.batched_packages:
593 ... print pub.displayname607 ... print pub.displayname
594 pmount 0.1-1 in warty608 pmount 0.1-1 in warty
595609
@@ -606,7 +620,7 @@
606 ... 'field.selected_sources-empty-marker': 1,620 ... 'field.selected_sources-empty-marker': 1,
607 ... })621 ... })
608622
609 >>> view.has_sources_for_display623 >>> view.has_packages_for_display
610 False624 False
611625
612 >>> import transaction626 >>> import transaction
@@ -626,7 +640,7 @@
626 ... 'field.selected_sources-empty-marker': 1,640 ... 'field.selected_sources-empty-marker': 1,
627 ... })641 ... })
628642
629 >>> view.has_sources_for_display643 >>> view.has_packages_for_display
630 False644 False
631645
632 >>> len(view.errors)646 >>> len(view.errors)
@@ -1036,7 +1050,7 @@
1036different default status filter (only published sources are presented1050different default status filter (only published sources are presented
1037by default).1051by default).
10381052
1039 >>> view.has_sources_for_display1053 >>> view.has_packages_for_display
1040 False1054 False
10411055
1042In this case, the template can use the has_sources1056In this case, the template can use the has_sources
@@ -1054,7 +1068,7 @@
1054 ... cprov.archive, name="+copy-packages",1068 ... cprov.archive, name="+copy-packages",
1055 ... query_string="field.status_filter=")1069 ... query_string="field.status_filter=")
10561070
1057 >>> [pub.status.name for pub in view.batched_sources]1071 >>> [pub.status.name for pub in view.batched_packages]
1058 ['DELETED', 'DELETED', 'DELETED']1072 ['DELETED', 'DELETED', 'DELETED']
10591073
1060This view contains three properties. The first is a list of the PPAs1074This view contains three properties. The first is a list of the PPAs
10611075
=== added file 'lib/lp/soyuz/browser/tests/test_archive_view.py'
--- lib/lp/soyuz/browser/tests/test_archive_view.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/browser/tests/test_archive_view.py 2010-03-22 11:43:06 +0000
@@ -0,0 +1,59 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test the main PPA (user) view functionality."""
5
6__metaclass__ = type
7
8import unittest
9
10from canonical.launchpad.ftests import login
11from canonical.testing import LaunchpadFunctionalLayer
12from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
13from lp.testing import TestCaseWithFactory
14from lp.testing.views import create_initialized_view
15
16
17class TestArchiveView(TestCaseWithFactory):
18
19 layer = LaunchpadFunctionalLayer
20
21 def setUp(self):
22 """Create a ppa with some publishings for different arches
23 and in different distroseries."""
24 super(TestArchiveView, self).setUp()
25 self.ppa = self.factory.makeArchive()
26 login('admin@canonical.com')
27 test_publisher = SoyuzTestPublisher()
28 test_publisher.prepareBreezyAutotest()
29
30 test_publisher.getPubBinaries(archive=self.ppa)
31
32 def makeBinariesReadable(self, binaries):
33 """Return a string representation of the binaries."""
34 return ", ".join([
35 "%s in %s" % (
36 bin.binary_package_name,
37 bin.distroarchseries.displayname
38 ) for bin in binaries])
39
40 def test_batched_packages(self):
41 # The batched_packages property of ArchiveView returns
42 # a batch of binary publishings.
43 view = create_initialized_view(
44 self.ppa, name="+index", method="GET")
45 self.assertEqual(
46 "foo-bin in ubuntutest Breezy Badger Autotest i386, "
47 "foo-bin in ubuntutest Breezy Badger Autotest hppa",
48 self.makeBinariesReadable(view.batched_packages))
49
50 def test_batched_packages_filter_arch(self):
51 view = create_initialized_view(
52 self.ppa, name="+index", method="GET",
53 query_string="field.arch_filter=i386")
54 self.assertEqual(
55 "foo-bin in ubuntutest Breezy Badger Autotest i386",
56 self.makeBinariesReadable(view.batched_packages))
57
58def test_suite():
59 return unittest.TestLoader().loadTestsFromName(__name__)
060
=== modified file 'lib/lp/soyuz/doc/archive.txt'
--- lib/lp/soyuz/doc/archive.txt 2010-03-13 00:32:40 +0000
+++ lib/lp/soyuz/doc/archive.txt 2010-03-22 11:43:06 +0000
@@ -158,10 +158,20 @@
158 >>> print cprov_archive.archive_url158 >>> print cprov_archive.archive_url
159 http://ppa.launchpad.dev/cprov/ppa/ubuntu159 http://ppa.launchpad.dev/cprov/ppa/ubuntu
160160
161Inquire what Distribution Series this archive has published sources to:161We can find out to which Distribution Series this archive has published
162162sources:
163 >>> [s.name for s in cprov_archive.series_with_sources]163
164 [u'breezy-autotest', u'warty']164 >>> for series in cprov_archive.series_with_sources:
165 ... print series.name
166 breezy-autotest
167 warty
168
169Or the DistroArchSeries to which this archive has published binaries:
170
171 >>> for arch in cprov_archive.arches_with_binaries:
172 ... print arch.architecturetag
173 hppa
174 i386
165175
166'purpose' is a read-only attribute, it can't and shouldn't be modified176'purpose' is a read-only attribute, it can't and shouldn't be modified
167once a IArchive is created. Changing those values would affect the way177once a IArchive is created. Changing those values would affect the way
168178
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py 2010-03-17 16:45:38 +0000
+++ lib/lp/soyuz/interfaces/archive.py 2010-03-22 11:43:06 +0000
@@ -47,6 +47,7 @@
47from canonical.launchpad.interfaces.launchpad import IPrivacy47from canonical.launchpad.interfaces.launchpad import IPrivacy
48from lp.registry.interfaces.role import IHasOwner48from lp.registry.interfaces.role import IHasOwner
49from lp.soyuz.interfaces.buildrecords import IHasBuildRecords49from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
50from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
50from lp.registry.interfaces.gpg import IGPGKey51from lp.registry.interfaces.gpg import IGPGKey
51from lp.registry.interfaces.person import IPerson52from lp.registry.interfaces.person import IPerson
52from canonical.launchpad.validators.name import name_validator53from canonical.launchpad.validators.name import name_validator
@@ -223,6 +224,12 @@
223224
224 series_with_sources = Attribute(225 series_with_sources = Attribute(
225 "DistroSeries to which this archive has published sources")226 "DistroSeries to which this archive has published sources")
227 arches_with_binaries = CollectionField(
228 title=_(
229 "DistroArchSeries to which this archive has published "
230 "binaries."),
231 value_type=Reference(schema=IDistroArchSeries),
232 readonly=True)
226 number_of_sources = Attribute(233 number_of_sources = Attribute(
227 'The number of sources published in the context archive.')234 'The number of sources published in the context archive.')
228 number_of_binaries = Attribute(235 number_of_binaries = Attribute(
229236
=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2010-03-17 04:05:42 +0000
+++ lib/lp/soyuz/model/archive.py 2010-03-22 11:43:06 +0000
@@ -205,7 +205,7 @@
205 for (family, archive_arch) in restricted_families:205 for (family, archive_arch) in restricted_families:
206 if family == arm:206 if family == arm:
207 return (archive_arch is not None)207 return (archive_arch is not None)
208 # ARM doesn't exist or isn't restricted. Either way, there is no 208 # ARM doesn't exist or isn't restricted. Either way, there is no
209 # need for an explicit association.209 # need for an explicit association.
210 return False210 return False
211211
@@ -286,6 +286,28 @@
286 distro_series, key=lambda a: Version(a.version), reverse=True)286 distro_series, key=lambda a: Version(a.version), reverse=True)
287287
288 @property288 @property
289 def arches_with_binaries(self):
290 """See `IArchive`."""
291 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
292
293 # Import DistroArchSeries here to avoid circular imports.
294 from lp.soyuz.model.distroarchseries import DistroArchSeries
295
296 distro_arch_series = store.find(
297 DistroArchSeries,
298 (BinaryPackagePublishingHistory.distroarchseries ==
299 DistroArchSeries.id),
300 BinaryPackagePublishingHistory.archive == self,
301 BinaryPackagePublishingHistory.status.is_in(
302 active_publishing_status))
303
304 # Ensure the ordering is the same as presented by
305 # DistroSeries.enabled_architectures.
306 distro_arch_series.config(distinct=True)
307 return distro_arch_series.order_by(
308 DistroArchSeries.architecturetag)
309
310 @property
289 def dependencies(self):311 def dependencies(self):
290 query = """312 query = """
291 ArchiveDependency.dependency = Archive.id AND313 ArchiveDependency.dependency = Archive.id AND
292314
=== modified file 'lib/lp/soyuz/templates/archive-copy-packages.pt'
--- lib/lp/soyuz/templates/archive-copy-packages.pt 2009-09-10 11:53:18 +0000
+++ lib/lp/soyuz/templates/archive-copy-packages.pt 2010-03-22 11:43:06 +0000
@@ -68,7 +68,7 @@
68 </tal:selectable_sources_end>68 </tal:selectable_sources_end>
6969
70 <table style="margin-top: 1em;"70 <table style="margin-top: 1em;"
71 tal:condition="view/has_sources_for_display">71 tal:condition="view/has_packages_for_display">
7272
73 <tal:archive73 <tal:archive
74 define="widget nocall:view/widgets/destination_archive|nothing"74 define="widget nocall:view/widgets/destination_archive|nothing"
7575
=== modified file 'lib/lp/soyuz/templates/archive-delete-packages.pt'
--- lib/lp/soyuz/templates/archive-delete-packages.pt 2009-09-09 17:54:13 +0000
+++ lib/lp/soyuz/templates/archive-delete-packages.pt 2010-03-22 11:43:06 +0000
@@ -52,7 +52,7 @@
52 tal:content="structure error">Error message</div>52 tal:content="structure error">Error message</div>
53 </tal:selectable_sources_end>53 </tal:selectable_sources_end>
5454
55 <tal:actions condition="view/has_sources_for_display"55 <tal:actions condition="view/has_packages_for_display"
56 define="widget nocall:view/widgets/deletion_comment;56 define="widget nocall:view/widgets/deletion_comment;
57 field_name widget/context/__name__;57 field_name widget/context/__name__;
58 error python:view.getFieldError(field_name);">58 error python:view.getFieldError(field_name);">
5959
=== modified file 'lib/lp/soyuz/templates/archive-index.pt'
--- lib/lp/soyuz/templates/archive-index.pt 2010-02-25 10:47:50 +0000
+++ lib/lp/soyuz/templates/archive-index.pt 2010-03-22 11:43:06 +0000
@@ -242,7 +242,7 @@
242 </form>242 </form>
243 </h2>243 </h2>
244244
245 <div style="padding-top: 1em;" tal:define="batch view/batched_sources">245 <div style="padding-top: 1em;" tal:define="batch view/batched_packages">
246246
247 <tal:navigation_top247 <tal:navigation_top
248 replace="structure view/batchnav/@@+navigation-links-upper" />248 replace="structure view/batchnav/@@+navigation-links-upper" />
249249
=== modified file 'lib/lp/soyuz/templates/archive-macros.pt'
--- lib/lp/soyuz/templates/archive-macros.pt 2009-12-03 18:33:22 +0000
+++ lib/lp/soyuz/templates/archive-macros.pt 2010-03-22 11:43:06 +0000
@@ -154,7 +154,7 @@
154 archive.154 archive.
155 </tal:comment>155 </tal:comment>
156156
157 <div style="padding-top: 1em;" tal:define="batch view/batched_sources">157 <div style="padding-top: 1em;" tal:define="batch view/batched_packages">
158158
159 <tal:navigation_top159 <tal:navigation_top
160 replace="structure view/batchnav/@@+navigation-links-upper" />160 replace="structure view/batchnav/@@+navigation-links-upper" />
@@ -175,9 +175,9 @@
175 <tbody>175 <tbody>
176 <tal:non_selectable repeat="publishing batch"176 <tal:non_selectable repeat="publishing batch"
177 replace="structure publishing/@@+listing-archive-detailed"177 replace="structure publishing/@@+listing-archive-detailed"
178 condition="not:view/selectable_sources">178 condition="not:view/selectable_packages">
179 </tal:non_selectable>179 </tal:non_selectable>
180 <tal:selectable condition="view/selectable_sources">180 <tal:selectable condition="view/selectable_packages">
181 <tal:each_term define="widget nocall:view/widgets/selected_sources"181 <tal:each_term define="widget nocall:view/widgets/selected_sources"
182 repeat="term widget/vocabulary">182 repeat="term widget/vocabulary">
183 <tal:block183 <tal:block