= Archive View Classes and Pages = Let's use Celso's PPA for the tests. >>> from lp.registry.interfaces.person import IPersonSet >>> cprov = getUtility(IPersonSet).getByName('cprov') == ArchiveView == The ArchiveView includes a few helper methods that make it easier to display different types of archives (copy archives, ppas). First let's create a copy archive: >>> from lp.registry.interfaces.distribution import ( ... IDistributionSet) >>> from lp.testing.factory import ( ... remove_security_proxy_and_shout_at_engineer) >>> ubuntu = getUtility(IDistributionSet)['ubuntu'] >>> copy_location = factory.makeCopyArchiveLocation( ... distribution=ubuntu, ... name="intrepid-security-rebuild") >>> copy_archive = remove_security_proxy_and_shout_at_engineer( ... copy_location).archive And let's create two views to compare: >>> ppa_archive_view = create_initialized_view(cprov.archive, ... name="+index") >>> copy_archive_view = create_initialized_view(copy_archive, ... name="+index") The ArchiveView includes an archive_url property that will return the archive url if it is available (ie. an active archive that is not a copy) and None otherwise: >>> print ppa_archive_view.archive_url http://ppa.launchpad.dev/cprov/ppa/ubuntu >>> print copy_archive_view.archive_url None The ArchiveView includes an archive_label property that returns either the string 'PPA' or 'archive' depending on whether the archive is a PPA (this is mainly for branding purposes): >>> print ppa_archive_view.archive_label PPA >>> print copy_archive_view.archive_label archive The ArchiveView provides the html for the inline description editing widget. >>> print ppa_archive_view.archive_description_html.title PPA description For convenience the ArchiveView also includes a build_counters property that returns a dict of the build count summary for the archive: >>> print ppa_archive_view.build_counters {'failed': 1L, 'superseded': 0, 'total': 4L, ... An ArchiveView also includes an easy way to get any IPackageCopyRequest's associated with an archive: >>> len(ppa_archive_view.package_copy_requests) 0 # Create a copy-request to Celso's PPA. >>> naked_copy_location = remove_security_proxy_and_shout_at_engineer( ... copy_location) >>> package_copy_request = ubuntu.main_archive.requestPackageCopy( ... naked_copy_location, copy_archive.owner) >>> len(copy_archive_view.package_copy_requests) 1 An ArchiveView inherits the status-filter widget for filtering packages by status. >>> for term in ppa_archive_view.widgets['status_filter'].vocabulary: ... print term.title Published Superseded An ArchiveView inherits the series-filter widget for filtering packages by series. >>> for term in ppa_archive_view.widgets['series_filter'].vocabulary: ... print term.title Breezy Badger Autotest Warty An ArchiveView provides a helper property which returns repository usage details in a dictionary containing: * Number of sources and binaries published with their appropriate labels; * Number of bytes used and permitted (quota); * Percentage of the used quota (with 2 degrees of precision). We will use a helper function for printing the returned dictionary contents. >>> def print_repository_usage(repository_usage): ... for key, value in sorted(repository_usage.iteritems()): ... print '%s: %s' % (key, value) Celso PPA has some packages, but still below the quota. >>> ppa_repository_usage = ppa_archive_view.repository_usage >>> print_repository_usage(ppa_repository_usage) binaries_size: 3 binary_label: 3 binary packages quota: 1073741824 source_label: 3 source packages sources_size: 9923399 used: 9929546 used_css_class: green used_percentage: 0.92 Reducing the quota and making Celso's PPA usage above it. The quota value is updated, percentage is limited to 100 % and the CSS class has changed. >>> login('foo.bar@canonical.com') >>> cprov.archive.authorized_size = 1 >>> login(ANONYMOUS) >>> fresh_view = create_initialized_view( ... cprov.archive, name="+index") >>> print_repository_usage(fresh_view.repository_usage) binaries_size: 3 binary_label: 3 binary packages quota: 1048576 source_label: 3 source packages sources_size: 9923399 used: 9929546 used_css_class: red used_percentage: 100.00 The COPY archive has no packages. >>> copy_repository_usage = copy_archive_view.repository_usage >>> print_repository_usage(copy_repository_usage) binaries_size: 0 binary_label: 0 binary packages quota: 2147483648 source_label: 0 source packages sources_size: 0 used: 0 used_css_class: green used_percentage: 0.00 Mark's PPA has a single source, thus the package labels are adjusted for their singular form. >>> mark = getUtility(IPersonSet).getByName('mark') >>> mark_archive_view = create_initialized_view( ... mark.archive, name="+index") >>> mark_repository_usage = mark_archive_view.repository_usage >>> print_repository_usage(mark_repository_usage) binaries_size: 0 binary_label: 1 binary package quota: 1073741824 source_label: 1 source package sources_size: 9922683 used: 9924731 used_css_class: green used_percentage: 0.92 An ArchiveView provides a batched_sources property that can be used to get the current batch of publishing records for an archive: >>> for publishing in ppa_archive_view.batched_sources: ... print publishing.source_package_name cdrkit iceweasel pmount The batched_sources property will also be filtered by distroseries when appropriate: >>> filtered_view = create_initialized_view( ... cprov.archive, ... name="+index", ... method='GET', ... query_string='field.series_filter=warty') >>> for publishing in filtered_view.batched_sources: ... print publishing.source_package_name iceweasel pmount The context archive dependencies access is also encapsulated in `ArchiveView` with the following aspects: * 'dependencies': cached `list` of `self.context.dependencies`. * 'show_dependencies': whether or not the dependencies section in the UI should be presented. * 'has_disabled_dependencies': whether or not the context archive uses disabled archives as dependencies. >>> view = create_initialized_view(cprov.archive, name="+index") >>> print view.dependencies [] >>> print view.show_dependencies False >>> print view.has_disabled_dependencies False 'show_dependencies' is True for the PPA users, since the link for adding new dependencies is part of the section controlled by this flag. >>> login('celso.providelo@canonical.com') >>> view = create_initialized_view(cprov.archive, name="+index") >>> print view.dependencies [] >>> print view.show_dependencies True >>> print view.has_disabled_dependencies False When there are any dependencies, 'show_dependencies' becomes True also for anonymous requests, since the dependencies are relevant to any user. # Create a new PPA and add it as dependency of Celso's PPA. >>> login('foo.bar@canonical.com') >>> testing_person = factory.makePerson(name='zoing') >>> testing_ppa = factory.makeArchive( ... distribution=ubuntu, name='ppa', owner=testing_person) >>> from lp.soyuz.interfaces.publishing import PackagePublishingPocket >>> unused = cprov.archive.addArchiveDependency( ... testing_ppa, PackagePublishingPocket.RELEASE) >>> login(ANONYMOUS) >>> view = create_initialized_view(cprov.archive, name="+index") >>> for archive_dependency in view.dependencies: ... print archive_dependency.dependency.displayname PPA for Zoing >>> print view.show_dependencies True >>> print view.has_disabled_dependencies False When a dependency is disabled, the 'has_disabled_dependencies' flag becomes True, but only if the viewer has permission to edit the PPA. # Disable the just created testing PPA. >>> login('foo.bar@canonical.com') >>> testing_ppa.disable() >>> login(ANONYMOUS) >>> view = create_initialized_view(cprov.archive, name="+index") >>> for archive_dependency in view.dependencies: ... print archive_dependency.dependency.displayname PPA for Zoing >>> print view.show_dependencies True >>> print view.has_disabled_dependencies False >>> login('celso.providelo@canonical.com') >>> view = create_initialized_view(cprov.archive, name="+index") >>> for archive_dependency in view.dependencies: ... print archive_dependency.dependency.displayname PPA for Zoing >>> print view.show_dependencies True >>> print view.has_disabled_dependencies True Remove the testing PPA dependency to not influence subsequent tests. >>> login('foo.bar@canonical.com') >>> cprov.archive.removeArchiveDependency(testing_ppa) >>> login(ANONYMOUS) The ArchiveView also provides the latest updates ordered by the date they were published. We include any relevant builds for failures. >>> def print_latest_updates(latest_updates): ... for update in latest_updates: ... arch_tags = [build.arch_tag for build in update['builds']] ... print "%s - %s %s" % ( ... update['title'], ... update['status'], ... " ".join(arch_tags), ... ) >>> print_latest_updates(view.latest_updates) cdrkit - Failed to build: i386 iceweasel - Successfully built pmount - Successfully built Let's now update the datepublished for iceweasel to show that the ordering is from most recent. The view's latest_updates property is cached so we need to reload the view. >>> login('celso.providelo@canonical.com') >>> view = create_initialized_view(cprov.archive, name="+index") >>> from canonical.database.constants import UTC_NOW >>> login('foo.bar@canonical.com') >>> view.filtered_sources[1].datepublished = UTC_NOW >>> login(ANONYMOUS) >>> print_latest_updates(view.latest_updates) iceweasel - Successfully built cdrkit - Failed to build: i386 pmount - Successfully built The ArchiveView also includes a helper method to return the number of updates over the past month (by default). >>> view.num_updates_over_last_days() 0 If we update the datecreated for some of the publishing records, those created within the last 30 days will be included in the count, but others will not. >>> from datetime import datetime, timedelta >>> import pytz >>> thirtyone_days_ago = datetime.now(tz=pytz.UTC) - timedelta(31) >>> login('foo.bar@canonical.com') >>> view.filtered_sources[0].datecreated = UTC_NOW >>> view.filtered_sources[1].datecreated = UTC_NOW >>> view.filtered_sources[2].datecreated = thirtyone_days_ago >>> login(ANONYMOUS) >>> view.num_updates_over_last_days() 2 We can optionally pass the number of days. >>> view.num_updates_over_last_days(33) 3 The ArchiveView includes a helper to return the number of packages that are building as well as the number of packages waiting to build. >>> print view.num_pkgs_building {'building': 0, 'waiting': 0, 'total': 0} Let's set some builds appropriately to see the results. >>> from lp.buildmaster.enums import BuildStatus >>> from lp.soyuz.interfaces.binarypackagebuild import ( ... IBinaryPackageBuildSet) >>> warty_hppa = getUtility(IDistributionSet)['ubuntu']['warty']['hppa'] >>> source = view.filtered_sources[0] >>> ignore = source.sourcepackagerelease.createBuild( ... distro_arch_series=warty_hppa, archive=view.context, ... pocket=source.pocket) >>> builds = getUtility(IBinaryPackageBuildSet).getBuildsForArchive( ... view.context) >>> for build in builds: ... print build.title hppa build of cdrkit 1.0 in ubuntu warty RELEASE hppa build of mozilla-firefox 0.9 in ubuntu warty RELEASE i386 build of pmount 0.1-1 in ubuntu warty RELEASE i386 build of iceweasel 1.0 in ubuntu warty RELEASE i386 build of cdrkit 1.0 in ubuntu breezy-autotest RELEASE >>> login('foo.bar@canonical.com') >>> builds[0].status = BuildStatus.NEEDSBUILD >>> builds[1].status = BuildStatus.BUILDING >>> builds[2].status = BuildStatus.BUILDING >>> login(ANONYMOUS) >>> view.num_pkgs_building {'building': 2, 'waiting': 1, 'total': 3} Adding a second waiting build for the cdrkit does not add to the number of packages that are currently building. >>> login('foo.bar@canonical.com') >>> builds[4].status = BuildStatus.NEEDSBUILD >>> login(ANONYMOUS) >>> view.num_pkgs_building {'building': 2, 'waiting': 1, 'total': 3} But as soon as one of cdrkit's builds start, the package is considered to be building: >>> login('foo.bar@canonical.com') >>> builds[4].status = BuildStatus.BUILDING >>> login(ANONYMOUS) >>> view.num_pkgs_building {'building': 3, 'waiting': 0, 'total': 3} The archive index view overrides the default series filter to use the distroseries from the browser's user-agent, when applicable. >>> print view.default_series_filter None >>> view_warty = create_view( ... cprov.archive, name="+index", ... HTTP_USER_AGENT='Mozilla/5.0 ' ... '(X11; U; Linux i686; en-US; rv:1.9.0.10) ' ... 'Gecko/2009042523 Ubuntu/4.10 (whatever) ' ... 'Firefox/3.0.10') >>> view_warty.initialize() >>> print view_warty.default_series_filter.name warty The archive index view also inherits the getSelectedFilterValue() method which can be used to find the currently selected value for both filters. >>> print view_warty.getSelectedFilterValue('series_filter').name warty >>> for status in view_warty.getSelectedFilterValue('status_filter'): ... print status.name PENDING PUBLISHED To enable the inline editing of the archive displayname, ArchiveView also provides a custom widget, displayname_edit_widget. >>> print view.displayname_edit_widget.title Edit the displayname The view provides the is_probationary_ppa property. The archive's description is not linkified when the owner is a probationary user to prevent spammers from using PPAs. >>> login('admin@canonical.com') >>> cprov.archive.description = 'http://example.dom/' >>> login(ANONYMOUS) >>> cprov.is_probationary True >>> print view.archive_description_html.value
http://example.dom/
The description is HTML escaped, and not linkified even when it contains HTML tags. >>> login('admin@canonical.com') >>> cprov.archive.description = ( ... 'http://example.com/') >>> login(ANONYMOUS) >>> print view.archive_description_html.value<a href="http://example.com/">http://example.com/</a>
The PPA description is linked when the user has made a contribution. >>> from lp.registry.interfaces.person import IPersonSet >>> login('admin@canonical.com') >>> contributor = getUtility(IPersonSet).getByName('name12') >>> contributor_ppa = factory.makeArchive( ... distribution=ubuntu, name='ppa', owner=contributor) >>> contributor_ppa.description = 'http://example.dom/' >>> login(ANONYMOUS) >>> contributor_view = create_initialized_view( ... contributor_ppa, name="+index") >>> contributor.is_probationary False >>> print contributor_view.archive_description_html.valuehttp://...example...
== ArchivePackageView ==
This view displays detailed information about the archive packages that
is not so relevant for the PPA index page, such as a summary of build
statuses, repository usage, full publishing details and access to
copy/delete packages where appropriate.
And let's create two views to compare:
>>> ppa_archive_view = create_initialized_view(
... cprov.archive, name="+packages")
>>> copy_archive_view = create_initialized_view(
... copy_archive, name="+packages")
>>> print ppa_archive_view.page_title
Packages in ...PPA for Celso Providelo...
>>> print copy_archive_view.page_title
Packages in ...Copy archive intrepid-security-rebuild...
This view inherits from ArchiveViewBase and has all the
corresponding properties such as archive_url, build_counters etc.
(see ArchiveView above).
Additionally, ArchivePackageView can display a string representation
of the series supported by this archive.
>>> print ppa_archive_view.series_list_string
Breezy Badger Autotest and Warty
>>> copy_archive_view.series_list_string
''
The view also has a page_title property and can indicate whether the context
is a copy archive.
>>> print copy_archive_view.page_title
Packages in ...Copy archive intrepid-security-rebuild...
>>> copy_archive_view.is_copy
True
== ArchivePackageDeletionView ==
We use ArchivePackageDeletionView to provide the mechnisms used to
delete packages from a PPA via the UI.
This view is only accessible by users with 'launchpad.Edit' permission
in the archive, that would be only the PPA owner (or administrators of
the Team owning the PPA) and Launchpad administrators. See further
tests in pagetests/xx-delete-packages.txt.
We will use the PPA owner, Celso user, to satisfy the references
required for deleting packages.
>>> login('celso.providelo@canonical.com')
Issuing a empty request we can inspect the internal attributes used to
build the page.
>>> view = create_initialized_view(
... cprov.archive, name="+delete-packages")
We query the available PUBLISHED sources and use them to build the
'selected_sources' widget.
>>> [pub.id for pub in view.batched_sources]
[27, 28, 29]
>>> view.has_sources_for_display
True
>>> len(view.widgets.get('selected_sources').vocabulary)
3
This view also provides package filtering by source package name, so
the user can refine the available options presented. By default all
available sources are presented with empty filter.
>>> for pub in view.batched_sources:
... print pub.displayname
cdrkit 1.0 in breezy-autotest
iceweasel 1.0 in warty
pmount 0.1-1 in warty
Whatever is passed as 'name_filter' results in a corresponding set of
filtered results.
>>> view = create_initialized_view(
... cprov.archive, name="+delete-packages",
... query_string="field.name_filter=pmount")
>>> for pub in view.batched_sources:
... print pub.displayname
pmount 0.1-1 in warty
The 'name_filter' is decoded as UTF-8 before futher processing. If it
did not, the storm query compiler would raise an error, because it can
only deal with unicode variables.
>>> view = create_initialized_view(
... cprov.archive, name="+delete-packages",
... query_string="field.name_filter=%C3%A7")
>>> len(list(view.batched_sources))
0
Similarly, the sources can be filtered by series:
>>> view = create_initialized_view(
... cprov.archive, name="+delete-packages",
... query_string="field.series_filter=warty")
>>> for pub in view.batched_sources:
... print pub.displayname
iceweasel 1.0 in warty
pmount 0.1-1 in warty
The page also uses all the built in batching features:
>>> view = create_initialized_view(
... cprov.archive, name="+delete-packages",
... query_string="field.series_filter=warty",
... form={'batch': '1', 'start': '1'})
>>> for pub in view.batched_sources:
... print pub.displayname
pmount 0.1-1 in warty
When submitted, deletions immediately take effect resulting in a page
which the available options already exclude the deleted items.
>>> view = create_initialized_view(
... cprov.archive, name="+delete-packages",
... form={
... 'field.actions.delete': 'Delete Packages',
... 'field.name_filter': '',
... 'field.deletion_comment': 'Go away',
... 'field.selected_sources': ['27', '28', '29'],
... 'field.selected_sources-empty-marker': 1,
... })
>>> view.has_sources_for_display
False
>>> import transaction
>>> transaction.commit()
If by any chance, the form containing already deleted items, is
re-POSTed to the page, the code is able to identify such invalid
situation and ignore it. See bug #185922 for reference.
>>> view = create_initialized_view(
... cprov.archive, name="+delete-packages",
... form={
... 'field.actions.delete': 'Delete Packages',
... 'field.name_filter': '',
... 'field.deletion_comment': 'Go away',
... 'field.selected_sources': ['27', '28', '29'],
... 'field.selected_sources-empty-marker': 1,
... })
>>> view.has_sources_for_display
False
>>> len(view.errors)
2
== ArchiveEditDependenciesView ==
We use ArchiveEditDependenciesView to provide the mechnisms used to
add and/or remove archive dependencies for a PPA via the UI.
This view is only accessible by users with 'launchpad.Edit' permission
in the archive, that would be only the PPA owner (or administrators of
the Team owning the PPA) and Launchpad administrators. See further
tests in pagetests/xx-edit-dependencies.txt.
We will use the PPA owner, Celso user, to play with edit-dependencies
corner-cases.
>>> login('celso.providelo@canonical.com')
Issuing a empty request we can inspect the internal attributes used to
build the page.
>>> view = create_initialized_view(
... cprov.archive, name="+edit-dependencies")
The view's h1 heading and leaf breadcrumb are equivalent.
>>> print view.label
Edit PPA dependencies
>>> print view.page_title
Edit PPA dependencies
There is a property indicating whether or not the context PPA has
recorded dependencies.
>>> view.has_dependencies
False
Also the 'selected_dependencies' form field is present, even if it is empty.
>>> len(view.widgets.get('selected_dependencies').vocabulary)
0
When there is no dependencies the form focus is set to the
'dependency_candidate' input field. Where the user can directly type
the owner of the PPA he wants to mark as dependency.
>>> print view.focusedElementScript()
Let's emulate a dependency addition. Note that the form contains, a
empty 'selected_dependencies' (as it was rendered in the empty
request) and 'dependency_candidate' contains a valid PPA owner name.
Validation checks are documented in
pagetests/ppa/xx-edit-dependencies.txt.
>>> view = create_initialized_view(
... cprov.archive, name="+edit-dependencies",
... form={
... 'field.selected_dependencies': [],
... 'field.dependency_candidate': 'mark/ppa',
... 'field.primary_dependencies': 'UPDATES',
... 'field.primary_components': 'ALL_COMPONENTS',
... 'field.actions.save': 'Save',
... })
>>> transaction.commit()
After processing the POST the view will redirect to itself.
>>> view.next_url is not None
True
Let's refresh the view class as it would be done in browsers.
>>> view = create_initialized_view(
... cprov.archive, name="+edit-dependencies")
Now we can see that the view properties correctly indicate the
presence of a PPA dependency.
>>> view.has_dependencies
True
The 'selected_dependencies' widget has one element representing a PPA
dependency. Each element has:
* value: dependency IArchive,
* token: dependency IArchive.owner,
* title: link to the dependency IArchive in Launchpad redered as the
dependency title.
>>> [dependency] = view.widgets.get('selected_dependencies').vocabulary
>>> print dependency.value.displayname
PPA for Mark Shuttleworth
>>> print dependency.token
mark/ppa
>>> print dependency.title.escapedtext
PPA for Mark
Shuttleworth
The form focus, now that we have a recorded dependencies, is set to the
first listed dependency.
>>> print view.focusedElementScript()
The PPA dependency element 'title' is only linkified if the viewer can
view the target PPA. If Mark's PPA gets disabled, Celso can't view it
anymore, so it's not rendered as a link.
# Disable Mark's PPA.
>>> login('foo.bar@canonical.com')
>>> mark.archive.disable()
>>> login('celso.providelo@canonical.com')
>>> view = create_initialized_view(
... cprov.archive, name="+edit-dependencies")
>>> [dependency] = view.widgets.get('selected_dependencies').vocabulary
>>> print dependency.value.displayname
PPA for Mark Shuttleworth
>>> print dependency.token
mark/ppa
>>> print dependency.title
PPA for Mark Shuttleworth
If we remove the just-added dependency, the view gets back to its
initial/empty state.
>>> view = create_initialized_view(
... cprov.archive, name="+edit-dependencies",
... form={
... 'field.selected_dependencies': ['mark/ppa'],
... 'field.dependency_candidate': '',
... 'field.primary_dependencies': 'UPDATES',
... 'field.primary_components': 'ALL_COMPONENTS',
... 'field.actions.save': 'Save',
... })
After processing the POST the view will redirect to itself.
>>> view.next_url is not None
True
Again, the view would be refreshed by browsers.
>>> view = create_initialized_view(
... cprov.archive, name="+edit-dependencies")
Now all the updated fields can be inspected.
>>> view.has_dependencies
False
>>> print view.focusedElementScript()
Primary dependencies can be adjusted in the same form according to a
set of pre-defined options. By default all PPAs use the dependencies
for UPDATES pocket (see archive-dependencies.txt for more information).
>>> primary_dependencies = view.widgets.get(
... 'primary_dependencies').vocabulary
>>> for dependency in primary_dependencies:
... print dependency.value
Release
Security
Updates
Proposed
Backports
>>> view.widgets.get('primary_dependencies')._getCurrentValue()