launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32778
Re: [Merge] ~enriqueesanchz/launchpad:add-external-package-url into launchpad:master
Added a few comments and questions.
I had never tried adding this sort of new URL traverse, great job finding all of these out. It all makes sense even if at times it feels a little over complicated - I think it's all due to how these things are set up in such a flexible way
Diff comments:
> diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
> index 9ec5f1a..68d90f1 100644
> --- a/lib/lp/bugs/browser/bugtask.py
> +++ b/lib/lp/bugs/browser/bugtask.py
> @@ -287,6 +288,27 @@ def get_visible_comments(comments, user=None):
> return visible_comments
>
>
> +@implementer(ICanonicalUrlData)
> +class BugTaskURL:
> + """External package URL creation rules."""
Afaics this isn't just for ExternalPackages - I'd update this to say it's a general bugtask URL creation rules, with an extra sentence mentioning that for external packages we do X
> +
> + rootsite = "bugs"
> +
> + def __init__(self, context):
> + self.context = context
> +
> + @property
> + def inside(self):
> + return self.context.target
> +
> + @property
> + def path(self):
> + if IExternalPackage.providedBy(self.context.target):
> + return f"+bug/{self.context.bug.id}/+bugtask/{self.context.id}"
> +
> + return "+bug/%s" % self.context.bug.id
> +
> +
> class BugTargetTraversalMixin:
> """Mix-in in class that provides .../+bug/NNN traversal."""
>
> @@ -320,29 +342,24 @@ class BugTargetTraversalMixin:
> # anonymous user is presented with a login screen at the correct URL,
> # rather than making it look as though this task was "not found",
> # because it was filtered out by privacy-aware code.
> -
> - externalpackage_bugtask = None
> + is_external_package = IExternalPackage.providedBy(context)
>
> for bugtask in bug.bugtasks:
> - if bugtask.target == context:
> - # Security proxy this object on the way out.
> - return getUtility(IBugTaskSet).get(bugtask.id)
> -
> + target = bugtask.target
> if (
> - externalpackage_bugtask is None
> - and IExternalPackage.providedBy(bugtask.target)
> + target == context
> + or is_external_package
> + and IExternalPackage.providedBy(target)
> + and target.sourcepackagename == context.sourcepackagename
> ):
> - # enriqueesanchz 2025-07-15 TODO: set +external urls for
> - # ExternalPackages. Currently we select the first
> - # ExternalPackage that appears if we don't have other match
> - externalpackage_bugtask = getUtility(IBugTaskSet).get(
> - bugtask.id
> - )
> + # for ExternalPackage we select the first package whose name is
Why? It feels like we might as well return if `target == context` else return the general one. This seems to be adding complexity without a real purpose. Can you explain why this would be needed?
> + # equal to the one from the context and later we will target
> + # other bugtask if required
>
> - # If we've come this far, there's no task for the requested context.
> - if externalpackage_bugtask:
> - return externalpackage_bugtask
> + # Security proxy this object on the way out.
> + return getUtility(IBugTaskSet).get(bugtask.id)
>
> + # If we've come this far, there's no task for the requested context.
> # If we are attempting to navigate past the non-existent bugtask,
> # we raise NotFound error. eg +delete or +edit etc.
> # Otherwise we are simply navigating to a non-existent task and so we
> @@ -365,6 +382,18 @@ class BugTaskNavigation(Navigation):
>
> usedfor = IBugTask
>
> + @stepthrough("+bugtask")
> + def traverse_bugtask(self, id):
> + bugtask = getUtility(IBugTaskSet).get(int(id))
> + # Jumping to another bug is not allowed
> + if bugtask.bug.id != self.context.bug.id:
> + return
> + # Jumping to another sourcepackagename is not allowed
> + if bugtask.sourcepackagename != self.context.sourcepackagename:
What about distribution?
> + return
> +
> + return bugtask
> +
> @stepthrough("attachments")
> def traverse_attachments(self, name):
> """traverse to an attachment by id."""
> diff --git a/lib/lp/bugs/browser/tests/test_buglisting.py b/lib/lp/bugs/browser/tests/test_buglisting.py
> index f11f586..8f0d5fb 100644
> --- a/lib/lp/bugs/browser/tests/test_buglisting.py
> +++ b/lib/lp/bugs/browser/tests/test_buglisting.py
> @@ -50,6 +54,23 @@ class TestBugTaskSearchListingPage(BrowserTestCase):
> extract_text(top_portlet[0]),
> )
>
> + def test_externalpackage_unknown_bugtracker_message(self):
Can you add a quick comment in each test explaining what it's testing?
> + ep = self._makeExternalPackage()
> + url = canonical_url(ep, rootsite="bugs")
> + browser = self.getUserBrowser(url)
> + top_portlet = find_tags_by_class(browser.contents, "top-portlet")
> + self.assertTrue(
> + len(top_portlet) > 0, "Tag with class=top-portlet not found"
> + )
> + # An external package from url will use unknown type
> + self.assertTextMatchesExpressionIgnoreWhitespace(
> + """
> + ep - Unknown in Ep-distro
> + does not use Launchpad for bug tracking.
> + Getting started with bug tracking in Launchpad.""",
> + extract_text(top_portlet[0]),
> + )
> +
> def test_distributionsourcepackage_unknown_bugtracker_no_button(self):
> # A DistributionSourcePackage whose Distro does not use
> # Launchpad for bug tracking should not show the "Report a bug"
> diff --git a/lib/lp/bugs/browser/tests/test_bugtask.py b/lib/lp/bugs/browser/tests/test_bugtask.py
> index 8d665f0..6a3ed4b 100644
> --- a/lib/lp/bugs/browser/tests/test_bugtask.py
> +++ b/lib/lp/bugs/browser/tests/test_bugtask.py
> @@ -873,6 +875,25 @@ class TestBugTasksTableView(TestCaseWithFactory):
> foo_ociproject = self.factory.makeOCIProject(pillar=foo)
> barix_ociproject = self.factory.makeOCIProject(pillar=barix)
>
> + foo_ep_snap = self.factory.makeExternalPackage(foo_spn)
I'd generally rather we use kwargs when it's not obvious what the args are, makes it quicker to read what the test is doing IMO.
So I'd use
```
foo_ep_snap = self.factory.makeExternalPackage(sourcepackagename=foo_spn)
```
Same below
> + foo_ep_charm = self.factory.makeExternalPackage(
> + foo_spn, packagetype=ExternalPackageType.CHARM
> + )
> + foo_ep_charm_candidate = self.factory.makeExternalPackage(
> + foo_spn,
> + packagetype=ExternalPackageType.CHARM,
> + channel=("12.1", "candidate"),
> + )
> + bar_ep_snap = self.factory.makeExternalPackage(bar_spn)
> + bar_ep_rock = self.factory.makeExternalPackage(
> + bar_spn, packagetype=ExternalPackageType.ROCK
> + )
> + bar_ep_rock_candidate = self.factory.makeExternalPackage(
> + bar_spn,
> + packagetype=ExternalPackageType.ROCK,
> + channel=("12.1", "candidate"),
> + )
> +
> expected_targets = [
> bar,
> bar.getSeries("0.0"),
> diff --git a/lib/lp/bugs/browser/tests/test_bugtask_navigation.py b/lib/lp/bugs/browser/tests/test_bugtask_navigation.py
> index 356f05d..7eb1f6b 100644
> --- a/lib/lp/bugs/browser/tests/test_bugtask_navigation.py
> +++ b/lib/lp/bugs/browser/tests/test_bugtask_navigation.py
> @@ -58,3 +58,87 @@ class TestBugTaskTraversal(TestCaseWithFactory):
> "http://api.launchpad.test/1.0/%s/+bug/%d"
> % (bug.default_bugtask.target.name, bug.default_bugtask.bug.id),
> )
> +
> + def test_traversal_to_external_package_bugtask(self):
> + # Test that traversal using +bugtask/id works
> + bug = self.factory.makeBug()
> + ep = self.factory.makeExternalPackage()
> + bugtask = self.factory.makeBugTask(bug=bug, target=ep)
> + bugtask_url = canonical_url(bugtask)
> + ep_2 = self.factory.makeExternalPackage()
> + bugtask_2 = self.factory.makeBugTask(bug=bug, target=ep_2)
> + bugtask_url_2 = canonical_url(bugtask_2)
> + self.assertEqual(
> + bugtask_url,
> + "http://bugs.launchpad.test/%s/+external/%s/+bug/%d/+bugtask/%s"
Could we ensure that if a bug contains multiple external packages with the same spn and distribution, if we try to target the last one we don't go to the first external package in the list?
> + % (
> + removeSecurityProxy(bugtask).distribution.name,
> + removeSecurityProxy(bugtask).target.name,
> + removeSecurityProxy(bugtask).bug.id,
> + removeSecurityProxy(bugtask).id,
> + ),
> + )
> + self.assertEqual(
> + bugtask_url_2,
> + "http://bugs.launchpad.test/%s/+external/%s/+bug/%d/+bugtask/%s"
> + % (
> + removeSecurityProxy(bugtask_2).distribution.name,
> + removeSecurityProxy(bugtask_2).target.name,
> + removeSecurityProxy(bugtask_2).bug.id,
> + removeSecurityProxy(bugtask_2).id,
> + ),
> + )
> + obj, _, _ = test_traverse(bugtask_url)
> + obj_2, _, _ = test_traverse(bugtask_url_2)
> + self.assertEqual(bugtask, obj)
> + self.assertEqual(bugtask_2, obj_2)
> + self.assertEqual(ep, obj.target)
> + self.assertEqual(ep_2, obj_2.target)
> +
> + def test_traversal_to_default_external_package_bugtask(self):
> + # Test that a traversing to a bug with an external package as default
> + # bugtask redirects to the bug's default bugtask using +bugtask/id.
> + ep = self.factory.makeExternalPackage()
> + bug = self.factory.makeBug(target=ep)
> + bug_url = canonical_url(bug, rootsite="bugs")
> + obj, view, request = test_traverse(bug_url)
> + view()
> + naked_view = removeSecurityProxy(view)
> + self.assertEqual(303, request.response.getStatus())
> + self.assertEqual(
> + naked_view.target,
> + canonical_url(bug.default_bugtask, rootsite="bugs"),
> + )
> + self.assertEqual(
> + removeSecurityProxy(view).target,
> + "http://bugs.launchpad.test/%s/+external/%s/+bug/%d/+bugtask/%s"
> + % (
> + bug.default_bugtask.distribution.name,
> + bug.default_bugtask.target.name,
> + bug.default_bugtask.bug.id,
> + bug.default_bugtask.id,
> + ),
> + )
> +
> + def test_traversal_to_default_external_package_bugtask_on_api(self):
> + # Traversing to a bug with an external package as default task
> + # redirects to the +bugtask/id also in the API.
> + ep = self.factory.makeExternalPackage()
> + bug = self.factory.makeBug(target=ep)
> + obj, view, request = test_traverse(
> + "http://api.launchpad.test/1.0/%s/+bug/%d"
> + % (
> + removeSecurityProxy(ep).distribution.name,
> + bug.default_bugtask.bug.id,
> + )
> + )
> + self.assertEqual(
> + removeSecurityProxy(view).target,
> + "http://api.launchpad.test/1.0/%s/+external/%s/+bug/%d/+bugtask/%s"
> + % (
> + bug.default_bugtask.distribution.name,
> + bug.default_bugtask.target.name,
> + bug.default_bugtask.bug.id,
> + bug.default_bugtask.id,
> + ),
> + )
> diff --git a/lib/lp/registry/browser/externalpackage.py b/lib/lp/registry/browser/externalpackage.py
> new file mode 100644
> index 0000000..36842ef
> --- /dev/null
> +++ b/lib/lp/registry/browser/externalpackage.py
> @@ -0,0 +1,92 @@
> +# Copyright 2009-2025 Canonical Ltd. This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__all__ = [
> + "ExternalPackageBreadcrumb",
> + "ExternalPackageNavigation",
> + "ExternalPackageURL",
> + "ExternalPackageFacets",
> +]
> +
> +
> +from zope.interface import implementer
> +
> +from lp.app.interfaces.headings import IHeadingBreadcrumb
> +from lp.bugs.browser.bugtask import BugTargetTraversalMixin
> +from lp.bugs.browser.structuralsubscription import (
> + StructuralSubscriptionTargetTraversalMixin,
> +)
> +from lp.code.browser.vcslisting import TargetDefaultVCSNavigationMixin
> +from lp.registry.interfaces.externalpackage import IExternalPackage
> +from lp.services.webapp import Navigation, StandardLaunchpadFacets, redirection
> +from lp.services.webapp.breadcrumb import Breadcrumb
> +from lp.services.webapp.interfaces import (
> + ICanonicalUrlData,
> + IMultiFacetedBreadcrumb,
> +)
> +from lp.services.webapp.menu import Link
> +from lp.services.webhooks.browser import WebhookTargetNavigationMixin
> +from lp.translations.browser.customlanguagecode import (
> + HasCustomLanguageCodesTraversalMixin,
> +)
> +
> +
> +@implementer(ICanonicalUrlData)
> +class ExternalPackageURL:
> + """External package URL creation rules."""
> +
> + rootsite = None
> +
> + def __init__(self, context):
> + self.context = context
> +
> + @property
> + def inside(self):
> + return self.context.distribution
> +
> + @property
> + def path(self):
> + return "+external/%s" % self.context.name
> +
> +
> +@implementer(IHeadingBreadcrumb, IMultiFacetedBreadcrumb)
> +class ExternalPackageBreadcrumb(Breadcrumb):
> + """Builds a breadcrumb for an `IExternalPackage`."""
> +
> + rootsite = "bugs"
> +
> + @property
> + def text(self):
> + return "%s external package" % self.context.sourcepackagename.name
> +
> +
> +class ExternalPackageFacets(StandardLaunchpadFacets):
> + usedfor = IExternalPackage
> + enable_only = [
> + "bugs",
> + ]
> +
> +
> +class ExternalPackageLinksMixin:
> + def new_bugs(self):
> + base_path = "+bugs"
> + get_data = "?field.status:list=NEW"
> + return Link(base_path + get_data, "New bugs", site="bugs")
> +
> +
> +class ExternalPackageNavigation(
> + Navigation,
> + BugTargetTraversalMixin,
> + HasCustomLanguageCodesTraversalMixin,
> + TargetDefaultVCSNavigationMixin,
> + StructuralSubscriptionTargetTraversalMixin,
> + WebhookTargetNavigationMixin,
> +):
> + usedfor = IExternalPackage
> +
> + @redirection("+editbugcontact")
> + def redirect_editbugcontact(self):
> + return "+subscribe"
> +
> + def traverse(self, name):
> + return None
What does this do? Does it mean that from external package page we can't go into a subpage?
> diff --git a/lib/lp/registry/model/externalpackage.py b/lib/lp/registry/model/externalpackage.py
> index d97390d..f7bfaa8 100644
> --- a/lib/lp/registry/model/externalpackage.py
> +++ b/lib/lp/registry/model/externalpackage.py
> @@ -147,6 +147,23 @@ class ExternalPackage(
> """See `IBugTarget`."""
> return self.distribution
>
> + @property
> + def bug_reporting_guidelines(self):
> + return
> +
> + @property
> + def content_templates(self):
> + return
> +
> + @property
> + def bug_reported_acknowledgement(self):
> + """See `IBugTarget`."""
> + return self.distribution.bug_reported_acknowledgement
> +
> def _getOfficialTagClause(self):
> """See `IBugTarget`."""
> return self.distribution._getOfficialTagClause()
> +
> + def _customizeSearchParams(self, search_params):
> + """Customize `search_params` for this distribution source package."""
Replace `source` with `external`
> + search_params.setExternalPackage(self)
--
https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/489379
Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-external-package-url into launchpad:master.
References