launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05301
[Merge] lp:~wgrant/launchpad/bug-833384 into lp:launchpad
William Grant has proposed merging lp:~wgrant/launchpad/bug-833384 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #833384 in Launchpad itself: "API access to debian changelog"
https://bugs.launchpad.net/launchpad/+bug/833384
For more details, see:
https://code.launchpad.net/~wgrant/launchpad/bug-833384/+merge/80140
This branch exposes source package changelogs in the webapp traversal hierarchy, and provides references to them through the API.
I needed a fake request for tests, so I replaced three existing scattered identical implementations with one in lp.testing. There are also some lint fixes around the place, and I moved TestSPPHModel's lonely test into test_publishing_models with the rest.
--
https://code.launchpad.net/~wgrant/launchpad/bug-833384/+merge/80140
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/bug-833384 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/browser/tests/test_branchtraversal.py'
--- lib/canonical/launchpad/browser/tests/test_branchtraversal.py 2011-08-12 11:37:08 +0000
+++ lib/canonical/launchpad/browser/tests/test_branchtraversal.py 2011-10-23 06:44:24 +0000
@@ -3,13 +3,11 @@
"""Tests for branch traversal."""
-from lazr.restful.testing.webservice import FakeRequest
from zope.component import getUtility
from zope.publisher.interfaces import NotFound
from zope.security.proxy import removeSecurityProxy
from canonical.launchpad.webapp.publisher import canonical_url
-from canonical.launchpad.webapp.servers import StepsToGo
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.registry.browser.person import PersonNavigation
from lp.registry.browser.personproduct import PersonProductNavigation
@@ -17,17 +15,10 @@
IPersonProduct,
IPersonProductFactory,
)
-from lp.testing import TestCaseWithFactory
-
-
-class LocalFakeRequest(FakeRequest):
- @property
- def stepstogo(self):
- """See IBasicLaunchpadRequest.
-
- This method is called by traversal machinery.
- """
- return StepsToGo(self)
+from lp.testing import (
+ FakeLaunchpadRequest,
+ TestCaseWithFactory,
+ )
class TestPersonBranchTraversal(TestCaseWithFactory):
@@ -59,7 +50,7 @@
"""
stack = list(reversed(segments))
name = stack.pop()
- request = LocalFakeRequest(['~' + self.person.name], stack)
+ request = FakeLaunchpadRequest(['~' + self.person.name], stack)
traverser = PersonNavigation(self.person, request)
return traverser.publishTraverse(request, name)
@@ -141,7 +132,7 @@
"""
stack = list(reversed(segments))
name = stack.pop()
- request = LocalFakeRequest(
+ request = FakeLaunchpadRequest(
['~' + self.person.name, self.product.name], stack)
traverser = PersonProductNavigation(self.person_product, request)
return traverser.publishTraverse(request, name)
=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
--- lib/lp/blueprints/browser/tests/test_specification.py 2011-08-28 07:29:11 +0000
+++ lib/lp/blueprints/browser/tests/test_specification.py 2011-10-23 06:44:24 +0000
@@ -7,7 +7,6 @@
import doctest
import unittest
-from lazr.restful.testing.webservice import FakeRequest
import pytz
from testtools.matchers import (
DocTestMatches,
@@ -22,7 +21,6 @@
find_tag_by_id,
)
from canonical.launchpad.webapp.interfaces import BrowserNotificationLevel
-from canonical.launchpad.webapp.servers import StepsToGo
from canonical.testing.layers import DatabaseFunctionalLayer
from lp.app.browser.tales import format_link
from lp.blueprints.browser import specification
@@ -32,6 +30,7 @@
ISpecificationSet,
)
from lp.testing import (
+ FakeLaunchpadRequest,
login_person,
person_logged_in,
TestCaseWithFactory,
@@ -51,17 +50,6 @@
self.assertEqual([], view.errors)
-class LocalFakeRequest(FakeRequest):
-
- @property
- def stepstogo(self):
- """See IBasicLaunchpadRequest.
-
- This method is called by traversal machinery.
- """
- return StepsToGo(self)
-
-
class TestBranchTraversal(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
@@ -80,7 +68,7 @@
def traverse(self, segments):
stack = list(reversed(['+branch'] + segments))
name = stack.pop()
- request = LocalFakeRequest([], stack)
+ request = FakeLaunchpadRequest([], stack)
traverser = specification.SpecificationNavigation(
self.specification, request)
return traverser.publishTraverse(request, name)
=== modified file 'lib/lp/bugs/browser/tests/test_structuralsubscription.py'
--- lib/lp/bugs/browser/tests/test_structuralsubscription.py 2011-03-22 14:56:58 +0000
+++ lib/lp/bugs/browser/tests/test_structuralsubscription.py 2011-10-23 06:44:24 +0000
@@ -5,7 +5,6 @@
from urlparse import urlparse
-from lazr.restful.testing.webservice import FakeRequest
import transaction
from zope.publisher.interfaces import NotFound
@@ -14,7 +13,6 @@
logout,
)
from canonical.launchpad.webapp.publisher import canonical_url
-from canonical.launchpad.webapp.servers import StepsToGo
from canonical.testing.layers import (
AppServerLayer,
DatabaseFunctionalLayer,
@@ -29,6 +27,7 @@
from lp.registry.browser.productseries import ProductSeriesNavigation
from lp.registry.browser.project import ProjectNavigation
from lp.testing import (
+ FakeLaunchpadRequest,
person_logged_in,
TestCaseWithFactory,
ws_object,
@@ -36,14 +35,6 @@
from lp.testing.views import create_initialized_view
-class FakeLaunchpadRequest(FakeRequest):
-
- @property
- def stepstogo(self):
- """See `IBasicLaunchpadRequest`."""
- return StepsToGo(self)
-
-
class StructuralSubscriptionTraversalTestBase(TestCaseWithFactory):
"""Verify that we can reach a target's structural subscriptions."""
=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml 2011-09-23 07:49:54 +0000
+++ lib/lp/soyuz/browser/configure.zcml 2011-10-23 06:44:24 +0000
@@ -171,6 +171,9 @@
path_expression="string:+sourcepub"
attribute_to_parent="archive"
urldata="lp.soyuz.browser.publishing.SourcePublicationURL"/>
+ <browser:navigation
+ module="lp.soyuz.browser.publishing"
+ classes="SourcePackagePublishingHistoryNavigation" />
</facet>
<browser:defaultView
for="lp.soyuz.interfaces.distroseriessourcepackagerelease.IDistroSeriesSourcePackageRelease"
=== modified file 'lib/lp/soyuz/browser/publishing.py'
--- lib/lp/soyuz/browser/publishing.py 2011-07-18 09:23:10 +0000
+++ lib/lp/soyuz/browser/publishing.py 2011-10-23 06:44:24 +0000
@@ -17,7 +17,11 @@
from lazr.delegates import delegates
from zope.interface import implements
-from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
+from canonical.launchpad.browser.librarian import (
+ FileNavigationMixin,
+ ProxiedLibraryFileAlias,
+ )
+from canonical.launchpad.webapp import Navigation
from canonical.launchpad.webapp.authorization import check_permission
from canonical.launchpad.webapp.interfaces import ICanonicalUrlData
from canonical.launchpad.webapp.menu import structured
@@ -62,6 +66,11 @@
return u"+binarypub/%s" % self.context.id
+class SourcePackagePublishingHistoryNavigation(Navigation,
+ FileNavigationMixin):
+ usedfor = ISourcePackagePublishingHistory
+
+
class ProxiedPackageDiff:
"""A `PackageDiff` extension.
@@ -121,7 +130,6 @@
accessor = attrgetter(self.timestamp_map[self.context.status])
return accessor(self.context)
-
def wasDeleted(self):
"""Whether or not a publishing record deletion was requested.
@@ -157,17 +165,19 @@
archive disk once it pass through its quarantine period and it's not
referred by any other archive publishing record.
Archive removal represents the act of having its content purged from
- archive disk, such situation can be triggered for different status,
- each one representing a distinct step in the Soyuz publishing workflow:
+ archive disk, such situation can be triggered for different
+ status, each one representing a distinct step in the Soyuz
+ publishing workflow:
- * SUPERSEDED -> the publication is not necessary since there is already
- a newer/higher/modified version available
+ * SUPERSEDED -> the publication is not necessary since there is
+ already a newer/higher/modified version available
* DELETED -> the publishing was explicitly marked for removal by a
archive-administrator, it's not wanted in the archive.
- * OBSOLETE -> the publication has become obsolete because its targeted
- distroseries has become obsolete (not supported by its developers).
+ * OBSOLETE -> the publication has become obsolete because its
+ targeted distroseries has become obsolete (not supported by its
+ developers).
"""
return self.context.dateremoved is not None
@@ -303,8 +313,8 @@
packagename = package.binarypackagename.name
if packagename not in packagenames:
entry = {
- "binarypackagename" : packagename,
- "summary" : package.summary,
+ "binarypackagename": packagename,
+ "summary": package.summary,
}
results.append(entry)
packagenames.add(packagename)
@@ -331,7 +341,7 @@
return check_permission('launchpad.View', archive)
@property
- def recipe_build_details(self):
+ def recipe_build_details(self):
"""Return a linkified string containing details about a
SourcePackageRecipeBuild.
"""
@@ -393,4 +403,3 @@
return True
return False
-
=== modified file 'lib/lp/soyuz/browser/tests/test_publishing.py'
--- lib/lp/soyuz/browser/tests/test_publishing.py 2011-03-01 05:00:01 +0000
+++ lib/lp/soyuz/browser/tests/test_publishing.py 2011-10-23 06:44:24 +0000
@@ -6,17 +6,33 @@
__metaclass__ = type
import soupmatchers
+from testtools.matchers import (
+ Contains,
+ MatchesAll,
+ )
+from zope.app.testing.functional import HTTPCaller
from zope.component import getUtility
+from zope.publisher.interfaces import NotFound
+from zope.security.interfaces import Unauthorized
+from canonical.launchpad.ftests import logout
from canonical.testing.layers import LaunchpadFunctionalLayer
-from canonical.launchpad.webapp.publisher import canonical_url
-
+from canonical.launchpad.webapp.publisher import (
+ canonical_url,
+ RedirectionView,
+ )
from lp.registry.interfaces.person import IPersonSet
+from lp.soyuz.browser.publishing import (
+ SourcePackagePublishingHistoryNavigation,
+ )
from lp.soyuz.enums import PackagePublishingStatus
+from lp.soyuz.interfaces.archive import ArchivePurpose
from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
from lp.testing import (
BrowserTestCase,
+ FakeLaunchpadRequest,
person_logged_in,
+ TestCaseWithFactory,
)
from lp.testing.sampledata import ADMIN_EMAIL
@@ -77,7 +93,7 @@
archive=self.archive, status=PackagePublishingStatus.PUBLISHED)
browser = self.getViewBrowser(spph, '+listing-archive-extra')
self.assertNotIn('Built by recipe', browser.contents)
-
+
def test_view_with_deleted_source_package_recipe(self):
# If a SourcePackageRelease is linked to a deleted recipe, the text
# 'deleted recipe' is displayed, rather than a link.
@@ -103,3 +119,65 @@
browser = self.getViewBrowser(spph, '+listing-archive-extra')
self.assertThat(browser.contents, recipe_link_matches)
self.assertIn('deleted recipe', browser.contents)
+
+
+class TestSourcePackagePublishingHistoryNavigation(TestCaseWithFactory):
+ layer = LaunchpadFunctionalLayer
+
+ def traverse(self, spph, segments):
+ req = FakeLaunchpadRequest([], segments[1:])
+ nav = SourcePackagePublishingHistoryNavigation(spph, req)
+ return nav.publishTraverse(req, segments[0])
+
+ def makeSPPHWithChangelog(self, archive=None):
+ lfa = self.factory.makeLibraryFileAlias(
+ filename='changelog',
+ restricted=(archive is not None and archive.private))
+ spr = self.factory.makeSourcePackageRelease(changelog=lfa)
+ return self.factory.makeSourcePackagePublishingHistory(
+ archive=archive,
+ sourcepackagerelease=spr)
+
+ def test_changelog(self):
+ # SPPH.SPR.changelog is accessible at +files/changelog.
+ spph = self.makeSPPHWithChangelog()
+ view = self.traverse(spph, ['+files', 'changelog'])
+ self.assertIsInstance(view, RedirectionView)
+ self.assertEqual(
+ spph.sourcepackagerelease.changelog.http_url, view.target)
+
+ def test_private_changelog(self):
+ # Private changelogs are inaccessible to anonymous users.
+ archive = self.factory.makeArchive(
+ purpose=ArchivePurpose.PPA, private=True)
+ spph = self.makeSPPHWithChangelog(archive=archive)
+
+ # A normal user can't traverse to the changelog.
+ self.assertRaises(
+ Unauthorized, self.traverse, spph, ['+files', 'changelog'])
+
+ # But the archive owner gets a librarian URL with a token.
+ with person_logged_in(archive.owner):
+ view = self.traverse(spph, ['+files', 'changelog'])
+ self.assertThat(view.target, Contains('?token='))
+
+ def test_unhandled_name(self):
+ # Unhandled names raise a NotFound.
+ spph = self.factory.makeSourcePackagePublishingHistory()
+ self.assertRaises(
+ NotFound, self.traverse, spph, ['+files', 'not-changelog'])
+
+ def test_registered(self):
+ # The Navigation is registered and traversable over HTTP.
+ spph = self.makeSPPHWithChangelog()
+ lfa_url = spph.sourcepackagerelease.changelog.http_url
+ redir_url = (
+ canonical_url(spph, path_only_if_possible=True)
+ + '/+files/changelog')
+ logout()
+ response = str(HTTPCaller()("GET %s HTTP/1.1\n\n" % redir_url))
+ self.assertThat(
+ response,
+ MatchesAll(
+ Contains("HTTP/1.1 303 See Other"),
+ Contains("Location: %s" % lfa_url)))
=== modified file 'lib/lp/soyuz/interfaces/publishing.py'
--- lib/lp/soyuz/interfaces/publishing.py 2011-09-24 12:57:47 +0000
+++ lib/lp/soyuz/interfaces/publishing.py 2011-10-23 06:44:24 +0000
@@ -34,6 +34,7 @@
export_read_operation,
export_write_operation,
exported,
+ operation_for_version,
operation_parameters,
operation_returns_collection_of,
REQUEST_USER,
@@ -533,6 +534,12 @@
:return: a list of `IBuilds`.
"""
+ def getFileByName(name):
+ """Return the file with the specified name.
+
+ Only supports 'changelog' at present.
+ """
+
@export_read_operation()
def changesFileUrl():
"""The .changes file URL for this source publication.
@@ -540,6 +547,14 @@
:return: the .changes file URL for this source (a string).
"""
+ @export_read_operation()
+ @operation_for_version('devel')
+ def changelogUrl():
+ """The URL for this source package release's changelog.
+
+ :return: the changelog file URL for this source (a string).
+ """
+
def getUnpublishedBuilds(build_states=None):
"""Return a resultset of `IBuild` objects in this context that are
not published.
=== modified file 'lib/lp/soyuz/model/publishing.py'
--- lib/lp/soyuz/model/publishing.py 2011-10-18 09:18:05 +0000
+++ lib/lp/soyuz/model/publishing.py 2011-10-23 06:44:24 +0000
@@ -68,6 +68,7 @@
IStoreSelector,
MAIN_STORE,
)
+from lp.app.errors import NotFoundError
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.model.buildfarmjob import BuildFarmJob
from lp.buildmaster.model.packagebuild import PackageBuild
@@ -552,6 +553,13 @@
self, build_states)
return DecoratedResultSet(result_set, operator.itemgetter(1))
+ def getFileByName(self, name):
+ """See `ISourcePackagePublishingHistory`."""
+ changelog = self.sourcepackagerelease.changelog
+ if changelog is not None and name == changelog.filename:
+ return changelog
+ raise NotFoundError(name)
+
def changesFileUrl(self):
"""See `ISourcePackagePublishingHistory`."""
# We use getChangesFileLFA() as opposed to getChangesFilesForSources()
@@ -575,6 +583,13 @@
the_url = self._proxied_urls((changes_lfa,), self.archive)[0]
return the_url
+ def changelogUrl(self):
+ """See `ISourcePackagePublishingHistory`."""
+ lfa = self.sourcepackagerelease.changelog
+ if lfa is not None:
+ return self._proxied_urls((lfa,), self)[0]
+ return None
+
def _getAllowedArchitectures(self, available_archs):
"""Filter out any restricted architectures not specifically allowed
for an archive.
=== modified file 'lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt'
--- lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2011-09-26 06:30:07 +0000
+++ lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2011-10-23 06:44:24 +0000
@@ -402,6 +402,20 @@
[]
[u'http://launchpad.dev/~cprov/+archive/ppa/+files/testwebservice-bin_666_all.deb']
+changelogUrl() returns the URL of debian/changelog, if it's available in
+the librarian.
+
+ >>> from lp.testing import celebrity_logged_in
+ >>> with celebrity_logged_in('admin'):
+ ... spr = factory.makeSourcePackageRelease(
+ ... changelog=factory.makeLibraryFileAlias(filename='changelog'))
+ ... spph = factory.makeSourcePackagePublishingHistory(
+ ... sourcepackagerelease=spr)
+ ... spph_url = canonical_url(spph, path_only_if_possible=True)
+ >>> print webservice.named_get(
+ ... spph_url, 'changelogUrl', api_version='devel').jsonBody()
+ http://launchpad.dev/.../+sourcepub/.../+files/changelog
+
The debdiff to a particular version can also be retrieved using the
packageDiffUrl() method. It takes one parameter, 'to_version' which
specifies the version of the package you want a diff against. If there
=== modified file 'lib/lp/soyuz/tests/test_publishing.py'
--- lib/lp/soyuz/tests/test_publishing.py 2011-10-18 09:18:05 +0000
+++ lib/lp/soyuz/tests/test_publishing.py 2011-10-23 06:44:24 +0000
@@ -1458,19 +1458,6 @@
self.checkOtherPublications(foreign_bins[0], foreign_bins)
-class TestSPPHModel(TestCaseWithFactory):
- """Test parts of the SourcePackagePublishingHistory model."""
-
- layer = LaunchpadZopelessLayer
-
- def testAncestry(self):
- """Ancestry can be traversed."""
- ancestor = self.factory.makeSourcePackagePublishingHistory()
- spph = self.factory.makeSourcePackagePublishingHistory(
- ancestor=ancestor)
- self.assertEquals(spph.ancestor.displayname, ancestor.displayname)
-
-
class TestGetOtherPublicationsForSameSource(TestNativePublishingBase):
"""Test parts of the BinaryPackagePublishingHistory model.
=== modified file 'lib/lp/soyuz/tests/test_publishing_models.py'
--- lib/lp/soyuz/tests/test_publishing_models.py 2011-08-26 16:44:05 +0000
+++ lib/lp/soyuz/tests/test_publishing_models.py 2011-10-23 06:44:24 +0000
@@ -7,13 +7,19 @@
from zope.security.proxy import removeSecurityProxy
from canonical.database.constants import UTC_NOW
-from canonical.testing.layers import LaunchpadZopelessLayer
+from canonical.launchpad.webapp.publisher import canonical_url
+from canonical.testing.layers import (
+ LaunchpadFunctionalLayer,
+ LaunchpadZopelessLayer,
+ )
+from lp.app.errors import NotFoundError
from lp.buildmaster.enums import BuildStatus
from lp.soyuz.interfaces.publishing import (
IPublishingSet,
PackagePublishingStatus,
)
from lp.soyuz.tests.test_binarypackagebuild import BaseTestCaseWithThreeBuilds
+from lp.testing import TestCaseWithFactory
class TestPublishingSet(BaseTestCaseWithThreeBuilds):
@@ -45,7 +51,7 @@
def test_getUnpublishedBuildsForSources_one_published(self):
# If we publish a binary for a build, it is no longer returned.
bpr = self.publisher.uploadBinaryForBuild(self.builds[0], 'gedit')
- bpph = self.publisher.publishBinaryInArchive(
+ self.publisher.publishBinaryInArchive(
bpr, self.sources[0].archive,
status=PackagePublishingStatus.PUBLISHED)
@@ -86,3 +92,47 @@
self.assert_(urls[1].endswith('/96/firefox_666_source.changes'))
self.assert_(urls[2].endswith(
'/98/getting-things-gnome_666_source.changes'))
+
+
+class TestSourcePackagePublishingHistory(TestCaseWithFactory):
+
+ layer = LaunchpadFunctionalLayer
+
+ def test_ancestry(self):
+ """Ancestry can be traversed."""
+ ancestor = self.factory.makeSourcePackagePublishingHistory()
+ spph = self.factory.makeSourcePackagePublishingHistory(
+ ancestor=ancestor)
+ self.assertEquals(spph.ancestor.displayname, ancestor.displayname)
+
+ def test_changelogUrl_missing(self):
+ spr = self.factory.makeSourcePackageRelease(changelog=None)
+ spph = self.factory.makeSourcePackagePublishingHistory(
+ sourcepackagerelease=spr)
+ self.assertEqual(None, spph.changelogUrl())
+
+ def test_changelogUrl(self):
+ spr = self.factory.makeSourcePackageRelease(
+ changelog=self.factory.makeChangelog('foo', ['1.0']))
+ spph = self.factory.makeSourcePackagePublishingHistory(
+ sourcepackagerelease=spr)
+ self.assertEqual(
+ canonical_url(spph) + '/+files/%s' % spr.changelog.filename,
+ spph.changelogUrl())
+
+ def test_getFileByName_changelog(self):
+ spr = self.factory.makeSourcePackageRelease(
+ changelog=self.factory.makeLibraryFileAlias(filename='changelog'))
+ spph = self.factory.makeSourcePackagePublishingHistory(
+ sourcepackagerelease=spr)
+ self.assertEqual(spr.changelog, spph.getFileByName('changelog'))
+
+ def test_getFileByName_changelog_absent(self):
+ spr = self.factory.makeSourcePackageRelease(changelog=None)
+ spph = self.factory.makeSourcePackagePublishingHistory(
+ sourcepackagerelease=spr)
+ self.assertRaises(NotFoundError, spph.getFileByName, 'changelog')
+
+ def test_getFileByName_unhandled_name(self):
+ spph = self.factory.makeSourcePackagePublishingHistory()
+ self.assertRaises(NotFoundError, spph.getFileByName, 'not-changelog')
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2011-10-11 07:20:53 +0000
+++ lib/lp/testing/__init__.py 2011-10-23 06:44:24 +0000
@@ -17,6 +17,7 @@
'celebrity_logged_in',
'ExpectedException',
'extract_lp_cache',
+ 'FakeLaunchpadRequest',
'FakeTime',
'get_lsb_information',
'launchpadlib_credentials_for',
@@ -85,6 +86,7 @@
)
from bzrlib.transport import get_transport
import fixtures
+from lazr.restful.testing.webservice import FakeRequest
import oops_datedir_repo.serializer_rfc822
import pytz
import simplejson
@@ -122,6 +124,7 @@
from canonical.launchpad.webapp.interaction import ANONYMOUS
from canonical.launchpad.webapp.servers import (
LaunchpadTestRequest,
+ StepsToGo,
WebServiceTestRequest,
)
from lp.codehosting.vfs import (
@@ -1345,3 +1348,11 @@
result.write(next_char)
now = time.time()
return result.getvalue()
+
+
+class FakeLaunchpadRequest(FakeRequest):
+
+ @property
+ def stepstogo(self):
+ """See `IBasicLaunchpadRequest`."""
+ return StepsToGo(self)