launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #33176
[Merge] ~enriqueesanchz/launchpad:add-channel-url into launchpad:master
Enrique Sánchez has proposed merging ~enriqueesanchz/launchpad:add-channel-url into launchpad:master.
Commit message:
Add channel to external package bugtask url
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/494829
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-channel-url into launchpad:master.
diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py
index 270167d..9c65b9d 100644
--- a/lib/lp/bugs/browser/bugtask.py
+++ b/lib/lp/bugs/browser/bugtask.py
@@ -127,10 +127,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
)
from lp.registry.interfaces.distroseries import IDistroSeries, IDistroSeriesSet
-from lp.registry.interfaces.externalpackage import (
- IExternalPackage,
- IExternalURL,
-)
+from lp.registry.interfaces.externalpackage import IExternalPackage
from lp.registry.interfaces.externalpackageseries import IExternalPackageSeries
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.person import IPersonSet
@@ -307,9 +304,6 @@ class BugTaskURL:
@property
def path(self):
- if IExternalURL.providedBy(self.context.target):
- return f"+bug/{self.context.bug.id}/+bugtask/{self.context.id}"
-
return "+bug/%s" % self.context.bug.id
@@ -347,17 +341,9 @@ class BugTargetTraversalMixin:
# rather than making it look as though this task was "not found",
# because it was filtered out by privacy-aware code.
- # Check if it uses +external url
- is_external = IExternalURL.providedBy(context)
-
for bugtask in bug.bugtasks:
target = bugtask.target
-
- if is_external:
- if context.isMatching(target):
- # Security proxy the object on the way out
- return getUtility(IBugTaskSet).get(bugtask.id)
- elif target == context:
+ if target == context:
return getUtility(IBugTaskSet).get(bugtask.id)
# If we've come this far, there's no task for the requested context.
@@ -383,23 +369,6 @@ class BugTaskNavigation(Navigation):
usedfor = IBugTask
- @stepthrough("+bugtask")
- def traverse_bugtask(self, id):
- bugtask = getUtility(IBugTaskSet).get(int(id))
- # Jumping to a not matching bugtask is not allowed
- if bugtask.bug.id != self.context.bug.id:
- return
- if bugtask.sourcepackagename != self.context.sourcepackagename:
- return
- if bugtask.distribution != self.context.distribution:
- return
- if bugtask.distroseries != self.context.distroseries:
- return
- if bugtask.packagetype != self.context.packagetype:
- 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_bugtask_navigation.py b/lib/lp/bugs/browser/tests/test_bugtask_navigation.py
index 8cf2537..a208eeb 100644
--- a/lib/lp/bugs/browser/tests/test_bugtask_navigation.py
+++ b/lib/lp/bugs/browser/tests/test_bugtask_navigation.py
@@ -73,7 +73,6 @@ class TestBugTaskTraversal(TestCaseWithFactory):
)
def test_traversal_to_external_bugtask(self):
- # Test that traversal using +bugtask/id works
# Test that we can differ between bugtasks with same packagename and
# distribution/distroseries, but different packagetype or channel
bug = self.factory.makeBug()
@@ -91,31 +90,31 @@ class TestBugTaskTraversal(TestCaseWithFactory):
distribution=distribution,
sourcepackagename=spn,
packagetype=ExternalPackageType.SNAP,
- channel=("11", "stable"),
+ channel=("11", "stable", None),
),
self.factory.makeExternalPackage(
distribution=distribution,
sourcepackagename=spn,
packagetype=ExternalPackageType.SNAP,
- channel=("11", "edge"),
+ channel=("11", "edge", "branch1"),
),
self.factory.makeExternalPackage(
distribution=distribution,
sourcepackagename=spn,
packagetype=ExternalPackageType.CHARM,
- channel=("11", "stable"),
+ channel=("11", "stable", None),
),
self.factory.makeExternalPackageSeries(
distroseries=distroseries_1,
sourcepackagename=spn,
packagetype=ExternalPackageType.CHARM,
- channel=("11", "stable"),
+ channel=("11", "stable", None),
),
self.factory.makeExternalPackageSeries(
distroseries=distroseries_2,
sourcepackagename=spn,
packagetype=ExternalPackageType.CHARM,
- channel=("11", "stable"),
+ channel=("11", "stable", None),
),
)
@@ -132,36 +131,40 @@ class TestBugTaskTraversal(TestCaseWithFactory):
# Check externalpackage urls
for bugtask in bugtasks[:3]:
- self.assertEqual(
- canonical_url(bugtask),
- "http://bugs.launchpad.test/%s/+%s/%s/+bug/%d/"
- "+bugtask/%s"
- % (
- bugtask.distribution.name,
- bugtask.target.packagetype.name.lower(),
- bugtask.target.name,
- bugtask.bug.id,
- bugtask.id,
- ),
+ target = bugtask.target
+ track, risk, branch = target.channel
+ # Helper to build the channel URL part
+ channel_url = f"/+track/{track}/+risk/{risk}"
+ if branch is not None:
+ channel_url += f"/+branch/{branch}"
+
+ expected_url = (
+ f"http://bugs.launchpad.test/{target.distribution.name}"
+ f"/+{target.packagetype.name.lower()}/{target.name}"
+ f"{channel_url}/+bug/{bugtask.bug.id}"
)
+ self.assertEqual(expected_url, canonical_url(bugtask))
+
obj, _, _ = test_traverse(canonical_url(bugtask))
self.assertEqual(bugtask, obj)
# Check externalpackageseries urls
for bugtask in bugtasks[3:]:
- self.assertEqual(
- canonical_url(bugtask),
- "http://bugs.launchpad.test/%s/%s/+%s/%s/+bug/%d/"
- "+bugtask/%s"
- % (
- bugtask.target.distribution.name,
- bugtask.distroseries.name,
- bugtask.target.packagetype.name.lower(),
- bugtask.target.name,
- bugtask.bug.id,
- bugtask.id,
- ),
+ target = bugtask.target
+ track, risk, branch = target.channel
+ # Helper to build the channel URL part
+ channel_url = f"/+track/{track}/+risk/{risk}"
+ if branch is not None:
+ channel_url += f"/+branch/{branch}"
+
+ expected_url = (
+ f"http://bugs.launchpad.test/{target.distribution.name}"
+ f"/{target.distroseries.name}"
+ f"/+{target.packagetype.name.lower()}/{target.name}"
+ f"{channel_url}/+bug/{bugtask.bug.id}"
)
+ self.assertEqual(expected_url, canonical_url(bugtask))
+
obj, _, _ = test_traverse(canonical_url(bugtask))
self.assertEqual(bugtask, obj)
@@ -178,16 +181,20 @@ class TestBugTaskTraversal(TestCaseWithFactory):
naked_view.target,
canonical_url(bug.default_bugtask, rootsite="bugs"),
)
+
+ bugtask = bug.default_bugtask
+ distro_name = bugtask.distribution.name
+ packagetype_name = bugtask.target.packagetype.name.lower()
+ target_name = bugtask.target.name
+ bug_id = bugtask.bug.id
+ track = bugtask.channel[0]
+ risk = bugtask.channel[1]
+ # branch is None so we don't add it
+
self.assertEqual(
removeSecurityProxy(view).target,
- "http://bugs.launchpad.test/%s/+%s/%s/+bug/%d/+bugtask/%s"
- % (
- bug.default_bugtask.distribution.name,
- bug.default_bugtask.target.packagetype.name.lower(),
- bug.default_bugtask.target.name,
- bug.default_bugtask.bug.id,
- bug.default_bugtask.id,
- ),
+ f"http://bugs.launchpad.test/{distro_name}/+{packagetype_name}/"
+ f"{target_name}/+track/{track}/+risk/{risk}/+bug/{bug_id}",
)
def test_traversal_to_default_external_package_series_bugtask(self):
@@ -214,17 +221,20 @@ class TestBugTaskTraversal(TestCaseWithFactory):
naked_view.target,
canonical_url(bug.default_bugtask, rootsite="bugs"),
)
+ distro_name = bug.default_bugtask.target.distribution.name
+ distroseries_name = bug.default_bugtask.distroseries.name
+ packagetype_name = bug.default_bugtask.target.packagetype.name.lower()
+ target_name = bug.default_bugtask.target.name
+ bug_id = bug.default_bugtask.bug.id
+ track = bug.default_bugtask.channel[0]
+ risk = bug.default_bugtask.channel[1]
+ # branch is None so we don't add it
+
self.assertEqual(
removeSecurityProxy(view).target,
- "http://bugs.launchpad.test/%s/%s/+%s/%s/+bug/%d/+bugtask/%s"
- % (
- bug.default_bugtask.target.distribution.name,
- bug.default_bugtask.distroseries.name,
- bug.default_bugtask.target.packagetype.name.lower(),
- bug.default_bugtask.target.name,
- bug.default_bugtask.bug.id,
- bug.default_bugtask.id,
- ),
+ f"http://bugs.launchpad.test/{distro_name}/{distroseries_name}/"
+ f"+{packagetype_name}/{target_name}/+track/{track}/+risk/{risk}/"
+ f"+bug/{bug_id}",
)
def test_traversal_to_default_external_package_bugtask_on_api(self):
@@ -238,16 +248,19 @@ class TestBugTaskTraversal(TestCaseWithFactory):
bug.default_bugtask.bug.id,
)
)
+ bugtask = bug.default_bugtask
+ distro_name = bugtask.distribution.name
+ packagetype_name = bugtask.target.packagetype.name.lower()
+ target_name = bugtask.target.name
+ bug_id = bugtask.bug.id
+ track = bugtask.channel[0]
+ risk = bugtask.channel[1]
+ # branch is None so we don't add it
+
self.assertEqual(
removeSecurityProxy(view).target,
- "http://api.launchpad.test/1.0/%s/+%s/%s/+bug/%d/+bugtask/%s"
- % (
- bug.default_bugtask.distribution.name,
- bug.default_bugtask.target.packagetype.name.lower(),
- bug.default_bugtask.target.name,
- bug.default_bugtask.bug.id,
- bug.default_bugtask.id,
- ),
+ f"http://api.launchpad.test/1.0/{distro_name}/+{packagetype_name}/"
+ f"{target_name}/+track/{track}/+risk/{risk}/+bug/{bug_id}",
)
def test_traversal_to_default_external_package_series_bugtask_on_api(self):
@@ -270,16 +283,18 @@ class TestBugTaskTraversal(TestCaseWithFactory):
bug.default_bugtask.bug.id,
)
)
+ distro_name = bug.default_bugtask.target.distribution.name
+ distroseries_name = bug.default_bugtask.distroseries.name
+ packagetype_name = bug.default_bugtask.target.packagetype.name.lower()
+ target_name = bug.default_bugtask.target.name
+ bug_id = bug.default_bugtask.bug.id
+ track = bug.default_bugtask.channel[0]
+ risk = bug.default_bugtask.channel[1]
+ # branch is None so we don't add it
+
self.assertEqual(
removeSecurityProxy(view).target,
- "http://api.launchpad.test/1.0/%s/%s/+%s/%s/+bug/%d/"
- "+bugtask/%s"
- % (
- bug.default_bugtask.target.distribution.name,
- bug.default_bugtask.distroseries.name,
- bug.default_bugtask.target.packagetype.name.lower(),
- bug.default_bugtask.target.name,
- bug.default_bugtask.bug.id,
- bug.default_bugtask.id,
- ),
+ f"http://api.launchpad.test/1.0/{distro_name}/{distroseries_name}/"
+ f"+{packagetype_name}/{target_name}/+track/{track}/+risk/{risk}/"
+ f"+bug/{bug_id}",
)
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index ea98516..8ebd8c2 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -209,6 +209,7 @@
product_id
productseries_id
packagetype
+ channel
sourcepackagename_id
task_age
bug_subscribers
diff --git a/lib/lp/bugs/interfaces/bugtasksearch.py b/lib/lp/bugs/interfaces/bugtasksearch.py
index 55e69ae..6a27fec 100644
--- a/lib/lp/bugs/interfaces/bugtasksearch.py
+++ b/lib/lp/bugs/interfaces/bugtasksearch.py
@@ -156,6 +156,7 @@ class BugTaskSearchParams:
assignee=None,
sourcepackagename=None,
packagetype=None,
+ channel=None,
owner=None,
attachmenttype=None,
orderby=None,
@@ -202,6 +203,7 @@ class BugTaskSearchParams:
self.assignee = assignee
self.sourcepackagename = sourcepackagename
self.packagetype = packagetype
+ self.channel = channel
self.owner = owner
self.attachmenttype = attachmenttype
self.user = user
@@ -315,14 +317,14 @@ class BugTaskSearchParams:
def setExternalPackage(self, externalpackage):
"""Set the externalpackage context on which to filter the search."""
self.distribution = externalpackage.distribution
- # Currently we are not filtering by channel
+ self.channel = externalpackage.channel
self.packagetype = externalpackage.packagetype.value
self.sourcepackagename = externalpackage.sourcepackagename
def setExternalPackageSeries(self, externalpackageseries):
"""Set the externalpackage context on which to filter the search."""
self.distroseries = externalpackageseries.distroseries
- # Currently we are not filtering by channel
+ self.channel = externalpackageseries.channel
self.packagetype = externalpackageseries.packagetype.value
self.sourcepackagename = externalpackageseries.sourcepackagename
@@ -349,6 +351,9 @@ class BugTaskSearchParams:
)
from lp.registry.interfaces.distroseries import IDistroSeries
from lp.registry.interfaces.externalpackage import IExternalPackage
+ from lp.registry.interfaces.externalpackageseries import (
+ IExternalPackageSeries,
+ )
from lp.registry.interfaces.milestone import IMilestone
from lp.registry.interfaces.ociproject import IOCIProject
from lp.registry.interfaces.product import IProduct
@@ -379,6 +384,8 @@ class BugTaskSearchParams:
self.setSourcePackage(target)
elif IExternalPackage.providedBy(instance):
self.setExternalPackage(target)
+ elif IExternalPackageSeries.providedBy(instance):
+ self.setExternalPackageSeries(target)
elif IProjectGroup.providedBy(instance):
self.setProjectGroup(target)
elif IOCIProject.providedBy(instance):
diff --git a/lib/lp/bugs/model/bugnomination.py b/lib/lp/bugs/model/bugnomination.py
index 50d1b0a..3e883e6 100644
--- a/lib/lp/bugs/model/bugnomination.py
+++ b/lib/lp/bugs/model/bugnomination.py
@@ -27,6 +27,7 @@ from lp.bugs.interfaces.bugnomination import (
)
from lp.bugs.interfaces.bugtask import IBugTaskSet
from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.externalpackageseries import IExternalPackageSeries
from lp.registry.interfaces.person import validate_public_person
from lp.registry.interfaces.productseries import IProductSeries
from lp.registry.interfaces.sourcepackage import ISourcePackage
@@ -240,9 +241,10 @@ class BugNominationSet:
filter_args = dict(productseries_id=target.id)
elif ISourcePackage.providedBy(target):
filter_args = dict(distroseries_id=target.series.id)
+ elif IExternalPackageSeries.providedBy(target):
+ filter_args = dict(distroseries_id=target.series.id)
else:
return None
- # IExternalPackageSeries does not support bug nominations
store = IStore(BugNomination)
return store.find(BugNomination, bug=bug, **filter_args).one()
diff --git a/lib/lp/bugs/model/bugtask.py b/lib/lp/bugs/model/bugtask.py
index c3fb364..a903637 100644
--- a/lib/lp/bugs/model/bugtask.py
+++ b/lib/lp/bugs/model/bugtask.py
@@ -907,6 +907,8 @@ class BugTask(StormBase):
if (
bugtask.distroseries is not None
and bugtask.sourcepackagename == self.sourcepackagename
+ and bugtask.packagetype == self.packagetype
+ and bugtask.channel == self.channel
)
]
# Return early, so that we don't have to get currentseries,
@@ -933,6 +935,7 @@ class BugTask(StormBase):
and conjoined_primary.status in self._NON_CONJOINED_STATUSES
):
conjoined_primary = None
+
return conjoined_primary
def _get_shortlisted_bugtasks(self):
@@ -956,6 +959,8 @@ class BugTask(StormBase):
if (
bugtask.distribution == distribution
and bugtask.sourcepackagename == self.sourcepackagename
+ and bugtask.packagetype == self.packagetype
+ and bugtask.channel == self.channel
):
conjoined_replica = bugtask
break
diff --git a/lib/lp/bugs/model/bugtaskflat.py b/lib/lp/bugs/model/bugtaskflat.py
index ea2304b..a500512 100644
--- a/lib/lp/bugs/model/bugtaskflat.py
+++ b/lib/lp/bugs/model/bugtaskflat.py
@@ -1,6 +1,7 @@
# Copyright 2012-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
+from storm.databases.postgres import JSON
from storm.locals import Bool, DateTime, Int, List, Reference
from lp.app.enums import InformationType
@@ -41,6 +42,7 @@ class BugTaskFlat(StormBase):
sourcepackagename_id = Int(name="sourcepackagename")
sourcepackagename = Reference(sourcepackagename_id, "SourcePackageName.id")
packagetype = Int(name="packagetype")
+ channel = JSON(name="channel")
ociproject_id = Int(name="ociproject")
ociproject = Reference(ociproject_id, "OCIProject.id")
status = DBEnum(enum=(BugTaskStatus, BugTaskStatusSearch))
diff --git a/lib/lp/bugs/model/bugtasksearch.py b/lib/lp/bugs/model/bugtasksearch.py
index 2af1ee7..6e9b98d 100644
--- a/lib/lp/bugs/model/bugtasksearch.py
+++ b/lib/lp/bugs/model/bugtasksearch.py
@@ -349,6 +349,9 @@ def _build_query(params):
if where_cond is not None:
extra_clauses.append(where_cond)
+ if params.channel is not None:
+ extra_clauses.append(BugTaskFlat.channel == params.channel)
+
if params.status is not None:
extra_clauses.append(
_build_status_clause(BugTaskFlat.status, params.status)
diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
index 54a39e2..61901e6 100644
--- a/lib/lp/bugs/model/tests/test_bugtask.py
+++ b/lib/lp/bugs/model/tests/test_bugtask.py
@@ -57,6 +57,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
)
from lp.registry.interfaces.distroseries import IDistroSeriesSet
+from lp.registry.interfaces.externalpackage import ExternalPackageType
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProductSet
from lp.registry.interfaces.projectgroup import IProjectGroupSet
@@ -2169,6 +2170,99 @@ class TestConjoinedBugTasks(TestCaseWithFactory):
self.assertIsNone(gentoo_netapplet_task.conjoined_primary)
self.assertIsNone(gentoo_netapplet_task.conjoined_replica)
+ def test_conjoined_external_package(self):
+ """Test conjoined primary/replica logic for external packages.
+
+ Conjoined tasks must match on sourcepackagename, packagetype,
+ and channel, all within the same distribution.
+ """
+ login("foo.bar@xxxxxxxxxxxxx")
+ launchbag = getUtility(ILaunchBag)
+ user = launchbag.user
+ task_set = getUtility(IBugTaskSet)
+
+ series_older = self.factory.makeDistroSeries(name="older")
+ distribution = removeSecurityProxy(series_older.distribution)
+ series_newer = self.factory.makeDistroSeries(
+ name="newer", distribution=distribution
+ )
+
+ pkg_name = self.factory.makeSourcePackageName()
+ pkg_type_rock = ExternalPackageType.ROCK
+ pkg_type_snap = ExternalPackageType.SNAP
+ channel_stable = (None, "stable", None)
+ channel_edge = (None, "edge", None)
+
+ # 1. Create the bug with a distribution-level task
+ dist_target = distribution.getExternalPackage(
+ pkg_name, pkg_type_rock, channel_stable
+ )
+ params = CreateBugParams(
+ owner=user, title="conjoined bug", comment="test"
+ )
+ bug = dist_target.createBug(params)
+ dist_task = bug.bugtasks[0]
+
+ # Initially, no conjoined tasks
+ self.assertIsNone(dist_task.conjoined_primary)
+ self.assertIsNone(dist_task.conjoined_replica)
+
+ # 2. Add "primary" series task
+ # Adding a task for the newest series. This will become the primary.
+ primary_target = series_newer.getExternalPackageSeries(
+ pkg_name, pkg_type_rock, channel_stable
+ )
+ primary_task = task_set.createTask(bug, user, primary_target)
+
+ # The new series task becomes primary
+ self.assertIsNone(primary_task.conjoined_primary)
+ self.assertEqual(dist_task, primary_task.conjoined_replica)
+ # The original dist task now points to the new primary
+ self.assertEqual(primary_task, dist_task.conjoined_primary)
+ self.assertIsNone(dist_task.conjoined_replica)
+
+ # 3. Test: Negative case (older series)
+ older_target = series_older.getExternalPackageSeries(
+ pkg_name, pkg_type_rock, channel_stable
+ )
+ older_task = task_set.createTask(bug, user, older_target)
+
+ # A series task conjoined_primary is always None
+ self.assertIsNone(older_task.conjoined_primary)
+ # A series task only gets a conjoined_replica if it's the currentseries
+ self.assertIsNone(older_task.conjoined_replica)
+
+ # 4. Test: Negative case (different channel)
+ # This task has a different channel and should NOT be conjoined.
+ edge_target = series_newer.getExternalPackageSeries(
+ pkg_name, pkg_type_rock, channel_edge
+ )
+ edge_task = task_set.createTask(bug, user, edge_target)
+
+ self.assertIsNone(edge_task.conjoined_primary)
+ self.assertIsNone(edge_task.conjoined_replica)
+
+ # 5. Test: Negative case (different packagetype)
+ # This task has a different package type and should NOT be conjoined.
+ snap_target = series_newer.getExternalPackageSeries(
+ pkg_name, pkg_type_snap, channel_stable
+ )
+ snap_task = task_set.createTask(bug, user, snap_target)
+
+ self.assertIsNone(snap_task.conjoined_primary)
+ self.assertIsNone(snap_task.conjoined_replica)
+
+ # 6. Test: Negative case (different distribution)
+ # This task is on a different distribution and should NOT be conjoined.
+ other_distro = self.factory.makeDistribution(name="other")
+ other_target = other_distro.getExternalPackage(
+ pkg_name, pkg_type_rock, channel_stable
+ )
+ other_task = task_set.createTask(bug, user, other_target)
+
+ self.assertIsNone(other_task.conjoined_primary)
+ self.assertIsNone(other_task.conjoined_replica)
+
def test_conjoined_broken_relationship(self):
"""A conjoined relationship can be broken, though.
diff --git a/lib/lp/bugs/model/tests/test_bugtasksearch.py b/lib/lp/bugs/model/tests/test_bugtasksearch.py
index 3dc70a7..75bd9aa 100644
--- a/lib/lp/bugs/model/tests/test_bugtasksearch.py
+++ b/lib/lp/bugs/model/tests/test_bugtasksearch.py
@@ -1667,6 +1667,63 @@ class UpstreamFilterTests:
self.assertSearchFinds(params, self.bugtasks[:1])
+class ExternalPackageSeriesTarget(BugTargetTestBase):
+ """Use an ExternalPackageSeries as the bug target."""
+
+ def setUp(self):
+ super().setUp()
+ # ExternalPackageSeries does not support aggregate search
+ # It requires BugSummary to be implemented
+ self.group_on = None
+ self.searchtarget = removeSecurityProxy(
+ self.factory.makeExternalPackageSeries()
+ )
+ self.owner = self.searchtarget.distroseries.owner
+ self.makeBugTasks()
+
+ def setUpTarget2(self):
+ self.searchtarget2 = removeSecurityProxy(
+ self.factory.makeExternalPackageSeries(
+ distroseries=self.searchtarget.distroseries
+ )
+ )
+ self.bugtasks2 = []
+ self.makeBugTasks(
+ bugtarget=self.searchtarget2,
+ bugtasks=self.bugtasks2,
+ owner=self.searchtarget2.distroseries.owner,
+ )
+ self.makeBugTasks(
+ bugtarget=self.searchtarget2,
+ bugtasks=self.bugtasks2,
+ owner=self.searchtarget2.distroseries.owner,
+ )
+
+ def setBugParamsTarget(self, params, target):
+ params.setExternalPackageSeries(target)
+
+ def subscribeToTarget(self, subscriber):
+ # Subscribe the given person to the search target.
+ # ExternalPackageSeries do not support structural subscriptions,
+ # so we subscribe to the distro series instead.
+ with person_logged_in(subscriber):
+ self.searchtarget.distroseries.addSubscription(
+ subscriber, subscribed_by=subscriber
+ )
+
+ def setUpMilestoneSorting(self):
+ with person_logged_in(self.owner):
+ milestone_1 = self.factory.makeMilestone(
+ distribution=self.searchtarget.distribution, name="1.0"
+ )
+ milestone_2 = self.factory.makeMilestone(
+ distribution=self.searchtarget.distribution, name="2.0"
+ )
+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
+ return self.bugtasks[1:] + self.bugtasks[:1]
+
+
class SourcePackageTarget(BugTargetTestBase, UpstreamFilterTests):
"""Use a source package as the bug target."""
@@ -1721,6 +1778,71 @@ class SourcePackageTarget(BugTargetTestBase, UpstreamFilterTests):
return self.bugtasks[1:] + self.bugtasks[:1]
+class ExternalPackageTarget(
+ BugTargetTestBase,
+ BugTargetWithBugSuperVisor,
+):
+ """Use an ExternalPackage as the bug target."""
+
+ def setUp(self):
+ super().setUp()
+ # ExternalPackage does not support aggregate search
+ # It requires BugSummary to be implemented
+ self.group_on = None
+ self.searchtarget = removeSecurityProxy(
+ self.factory.makeExternalPackage()
+ )
+ self.owner = self.searchtarget.distribution.owner
+ self.makeBugTasks()
+
+ def setUpTarget2(self):
+ self.searchtarget2 = removeSecurityProxy(
+ self.factory.makeExternalPackage(
+ distribution=self.searchtarget.distribution
+ )
+ )
+ self.bugtasks2 = []
+ self.makeBugTasks(
+ bugtarget=self.searchtarget2,
+ bugtasks=self.bugtasks2,
+ owner=self.searchtarget2.distribution.owner,
+ )
+ self.makeBugTasks(
+ bugtarget=self.searchtarget2,
+ bugtasks=self.bugtasks2,
+ owner=self.searchtarget2.distribution.owner,
+ )
+
+ def setBugParamsTarget(self, params, target):
+ params.setExternalPackage(target)
+
+ def subscribeToTarget(self, subscriber):
+ # Subscribe the given person to the search target.
+ # ExternalPackage do not support structural subscriptions,
+ # so we subscribe to the distro instead.
+ with person_logged_in(subscriber):
+ self.searchtarget.distribution.addSubscription(
+ subscriber, subscribed_by=subscriber
+ )
+
+ def setSupervisor(self, supervisor):
+ """Set the bug supervisor for the bug task target."""
+ with person_logged_in(self.owner):
+ self.searchtarget.distribution.bug_supervisor = supervisor
+
+ def setUpMilestoneSorting(self):
+ with person_logged_in(self.owner):
+ milestone_1 = self.factory.makeMilestone(
+ distribution=self.searchtarget.distribution, name="1.0"
+ )
+ milestone_2 = self.factory.makeMilestone(
+ distribution=self.searchtarget.distribution, name="2.0"
+ )
+ self.bugtasks[1].transitionToMilestone(milestone_1, self.owner)
+ self.bugtasks[2].transitionToMilestone(milestone_2, self.owner)
+ return self.bugtasks[1:] + self.bugtasks[:1]
+
+
class DistributionSourcePackageTarget(
BugTargetTestBase, BugTargetWithBugSuperVisor, UpstreamFilterTests
):
@@ -1782,6 +1904,8 @@ bug_targets_mixins = (
ProductTarget,
ProjectGroupTarget,
SourcePackageTarget,
+ ExternalPackageTarget,
+ ExternalPackageSeriesTarget,
)
diff --git a/lib/lp/bugs/tests/test_bugnomination.py b/lib/lp/bugs/tests/test_bugnomination.py
index 19c0a15..8174a30 100644
--- a/lib/lp/bugs/tests/test_bugnomination.py
+++ b/lib/lp/bugs/tests/test_bugnomination.py
@@ -468,3 +468,22 @@ class BugNominationSetTestCase(TestCaseWithFactory):
def test_get_none(self):
bug_nomination_set = getUtility(IBugNominationSet)
self.assertRaises(NotFoundError, bug_nomination_set.get, -1)
+
+ def test_getByBugTarget(self):
+ pkg1 = self.factory.makeExternalPackageSeries()
+ pkg2 = self.factory.makeExternalPackageSeries()
+
+ with person_logged_in(pkg1.distroseries.owner):
+ nomination = self.factory.makeBugNomination(target=pkg1)
+ with person_logged_in(pkg2.distroseries.owner):
+ self.factory.makeBugNomination(target=pkg2)
+
+ bug_nomination_set = getUtility(IBugNominationSet)
+ self.assertEqual(
+ nomination,
+ bug_nomination_set.getByBugTarget(nomination.bug, pkg1),
+ )
+ self.assertNotEqual(
+ nomination,
+ bug_nomination_set.getByBugTarget(nomination.bug, pkg2),
+ )
diff --git a/lib/lp/registry/browser/externalpackage.py b/lib/lp/registry/browser/externalpackage.py
index 6ec7657..1b398d8 100644
--- a/lib/lp/registry/browser/externalpackage.py
+++ b/lib/lp/registry/browser/externalpackage.py
@@ -6,10 +6,12 @@ __all__ = [
"ExternalPackageNavigation",
"ExternalPackageFacets",
"ExternalPackageURL",
+ "ExternalPackageNavigationMixin",
]
from zope.interface import implementer
+from zope.publisher.interfaces import NotFound
from lp.app.interfaces.headings import IHeadingBreadcrumb
from lp.bugs.browser.bugtask import BugTargetTraversalMixin
@@ -17,7 +19,12 @@ from lp.bugs.browser.structuralsubscription import (
StructuralSubscriptionTargetTraversalMixin,
)
from lp.registry.interfaces.externalpackage import IExternalPackage
-from lp.services.webapp import Navigation, StandardLaunchpadFacets, redirection
+from lp.services.webapp import (
+ Navigation,
+ StandardLaunchpadFacets,
+ redirection,
+ stepthrough,
+)
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.interfaces import (
ICanonicalUrlData,
@@ -46,10 +53,56 @@ class ExternalPackageFacets(StandardLaunchpadFacets):
]
+class ExternalPackageNavigationMixin:
+ """Provides traversal for +track, +risk, and +branch."""
+
+ def _get_package_for_channel(self, channel):
+ """
+ Get the external package or package series for a given channel.
+
+ Subclasses must implement this.
+ """
+ raise NotImplementedError
+
+ @stepthrough("+track")
+ def traverse_track(self, track):
+ stack = self.request.getTraversalStack()
+ if len(stack) >= 2 and stack[-1] == "+risk":
+ risk = stack[-2]
+ else:
+ # If no risk provided, default to stable
+ risk = "stable"
+
+ channel = (track, risk)
+ return self._get_package_for_channel(channel)
+
+ @stepthrough("+risk")
+ def traverse_risk(self, risk):
+ if self.context.channel:
+ channel = (self.context.channel[0], risk)
+ else:
+ channel = risk
+
+ return self._get_package_for_channel(channel)
+
+ @stepthrough("+branch")
+ def traverse_branch(self, branch):
+ if self.context.channel:
+ track, risk = self.context.channel[0], self.context.channel[1]
+ else:
+ # If no track/risk provided, default to None/stable
+ track = None
+ risk = "stable"
+
+ channel = (track, risk, branch)
+ return self._get_package_for_channel(channel)
+
+
class ExternalPackageNavigation(
Navigation,
BugTargetTraversalMixin,
StructuralSubscriptionTargetTraversalMixin,
+ ExternalPackageNavigationMixin,
):
usedfor = IExternalPackage
@@ -57,6 +110,14 @@ class ExternalPackageNavigation(
def redirect_editbugcontact(self):
return "+subscribe"
+ def _get_package_for_channel(self, channel):
+ try:
+ return self.context.distribution.getExternalPackage(
+ self.context.name, self.context.packagetype, channel
+ )
+ except ValueError:
+ raise NotFound(self.context, channel)
+
@implementer(ICanonicalUrlData)
class ExternalPackageURL:
@@ -74,4 +135,15 @@ class ExternalPackageURL:
@property
def path(self):
packagetype = self.context.packagetype.name.lower()
- return f"+{packagetype}/{self.context.name}"
+
+ track, risk, branch = self.context.channel or (None, None, None)
+
+ channel_url = ""
+ if track:
+ channel_url = f"/+track/{track}"
+ if risk:
+ channel_url = channel_url + f"/+risk/{risk}"
+ if branch:
+ channel_url = channel_url + f"/+branch/{branch}"
+
+ return f"+{packagetype}/{self.context.name}{channel_url}"
diff --git a/lib/lp/registry/browser/externalpackageseries.py b/lib/lp/registry/browser/externalpackageseries.py
index 4221ef8..a9a54ae 100644
--- a/lib/lp/registry/browser/externalpackageseries.py
+++ b/lib/lp/registry/browser/externalpackageseries.py
@@ -10,12 +10,14 @@ __all__ = [
from zope.interface import implementer
+from zope.publisher.interfaces import NotFound
from lp.app.interfaces.headings import IHeadingBreadcrumb
from lp.bugs.browser.bugtask import BugTargetTraversalMixin
from lp.bugs.browser.structuralsubscription import (
StructuralSubscriptionTargetTraversalMixin,
)
+from lp.registry.browser.externalpackage import ExternalPackageNavigationMixin
from lp.registry.interfaces.externalpackageseries import IExternalPackageSeries
from lp.services.webapp import (
Navigation,
@@ -57,6 +59,7 @@ class ExternalPackageSeriesNavigation(
Navigation,
BugTargetTraversalMixin,
StructuralSubscriptionTargetTraversalMixin,
+ ExternalPackageNavigationMixin,
):
usedfor = IExternalPackageSeries
@@ -74,6 +77,14 @@ class ExternalPackageSeriesNavigation(
redirection_url += "?no-redirect"
return self.redirectSubTree(redirection_url, status=303)
+ def _get_package_for_channel(self, channel):
+ try:
+ return self.context.distroseries.getExternalPackageSeries(
+ self.context.name, self.context.packagetype, channel
+ )
+ except ValueError:
+ raise NotFound(self.context, channel)
+
@implementer(ICanonicalUrlData)
class ExternalPackageSeriesURL:
@@ -91,4 +102,15 @@ class ExternalPackageSeriesURL:
@property
def path(self):
packagetype = self.context.packagetype.name.lower()
- return f"+{packagetype}/{self.context.name}"
+
+ track, risk, branch = self.context.channel or (None, None, None)
+
+ channel_url = ""
+ if track:
+ channel_url = f"/+track/{track}"
+ if risk:
+ channel_url = channel_url + f"/+risk/{risk}"
+ if branch:
+ channel_url = channel_url + f"/+branch/{branch}"
+
+ return f"+{packagetype}/{self.context.name}{channel_url}"
diff --git a/lib/lp/registry/interfaces/externalpackage.py b/lib/lp/registry/interfaces/externalpackage.py
index d0116f7..80ec0a6 100644
--- a/lib/lp/registry/interfaces/externalpackage.py
+++ b/lib/lp/registry/interfaces/externalpackage.py
@@ -4,7 +4,6 @@
"""External package interfaces."""
__all__ = [
- "IExternalURL",
"IExternalPackage",
"ExternalPackageType",
]
@@ -12,7 +11,7 @@ __all__ = [
from lazr.enum import DBEnumeratedType, DBItem
from lazr.restful.declarations import exported, exported_as_webservice_entry
from lazr.restful.fields import Reference
-from zope.interface import Attribute, Interface
+from zope.interface import Attribute
from zope.schema import TextLine
from lp import _
@@ -22,23 +21,12 @@ from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.role import IHasDrivers
-class IExternalURL(Interface):
- """Uses +external url"""
-
- def isMatching(other):
- """Returns if it matches the other object.
- +external url lacks necessary data, so we only match the necessary
- attributes.
- """
-
-
@exported_as_webservice_entry(as_of="beta")
class IExternalPackageView(
IHeadingContext,
IBugTarget,
IHasOfficialBugTags,
IHasDrivers,
- IExternalURL,
):
"""`IExternalPackage` attributes that require launchpad.View."""
@@ -66,9 +54,6 @@ class IExternalPackageView(
drivers = Attribute("The drivers for the distribution.")
- def isMatching(other):
- """See `IExternalURL`."""
-
def __eq__(other):
"""IExternalPackage comparison method.
diff --git a/lib/lp/registry/interfaces/externalpackageseries.py b/lib/lp/registry/interfaces/externalpackageseries.py
index 63e8d32..c7d5e9b 100644
--- a/lib/lp/registry/interfaces/externalpackageseries.py
+++ b/lib/lp/registry/interfaces/externalpackageseries.py
@@ -17,7 +17,6 @@ from lp.app.interfaces.launchpad import IHeadingContext
from lp.bugs.interfaces.bugtarget import IBugTarget, IHasOfficialBugTags
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.distroseries import IDistroSeries
-from lp.registry.interfaces.externalpackage import IExternalURL
from lp.registry.interfaces.role import IHasDrivers
@@ -27,7 +26,6 @@ class IExternalPackageSeriesView(
IBugTarget,
IHasOfficialBugTags,
IHasDrivers,
- IExternalURL,
):
"""`IExternalPackageSeries` attributes that require launchpad.View."""
@@ -62,9 +60,6 @@ class IExternalPackageSeriesView(
drivers = Attribute("The drivers for the distroseries.")
- def isMatching(other):
- """See `IExternalURL`."""
-
def __eq__(other):
"""IExternalPackageSeries comparison method.
diff --git a/lib/lp/registry/model/externalpackage.py b/lib/lp/registry/model/externalpackage.py
index 8327349..c1efca8 100644
--- a/lib/lp/registry/model/externalpackage.py
+++ b/lib/lp/registry/model/externalpackage.py
@@ -111,15 +111,6 @@ class ExternalPackage(
"""See `IExternalPackage`."""
return self.display_name
- def isMatching(self, other) -> bool:
- """See `IExternalURL`."""
- return (
- IExternalPackage.providedBy(other)
- and self.sourcepackagename.id == other.sourcepackagename.id
- and self.distribution.id == other.distribution.id
- and self.packagetype == other.packagetype
- )
-
def __eq__(self, other: "ExternalPackage") -> str:
"""See `IExternalPackage`."""
return (
diff --git a/lib/lp/registry/model/externalpackageseries.py b/lib/lp/registry/model/externalpackageseries.py
index c6c08c7..1fd2695 100644
--- a/lib/lp/registry/model/externalpackageseries.py
+++ b/lib/lp/registry/model/externalpackageseries.py
@@ -133,15 +133,6 @@ class ExternalPackageSeries(
"""See `IExternalPackageSeries`."""
return self.display_name
- def isMatching(self, other) -> bool:
- """See `IExternalURL`."""
- return (
- IExternalPackageSeries.providedBy(other)
- and self.sourcepackagename.id == other.sourcepackagename.id
- and self.distroseries.id == other.distroseries.id
- and self.packagetype == other.packagetype
- )
-
def __eq__(self, other: "ExternalPackageSeries") -> str:
"""See `IExternalPackageSeries`."""
return (
diff --git a/lib/lp/registry/tests/test_externalpackage.py b/lib/lp/registry/tests/test_externalpackage.py
index ab1f715..1fe375e 100644
--- a/lib/lp/registry/tests/test_externalpackage.py
+++ b/lib/lp/registry/tests/test_externalpackage.py
@@ -139,31 +139,6 @@ class TestExternalPackage(TestCaseWithFactory):
self.externalpackage.display_name,
)
- def test_matches(self):
- """Test if two externalpackages matches in sourcepackagename,
- distribution and packagetype.
- """
- self.assertTrue(
- self.externalpackage.isMatching(self.externalpackage_matching)
- )
-
- self.assertFalse(
- self.externalpackage.isMatching(self.externalpackage_maven)
- )
-
- other_spn = self.factory.makeSourcePackageName()
- other_ep_1 = self.factory.makeExternalPackage(
- sourcepackagename=other_spn,
- distribution=self.distribution,
- )
- self.assertFalse(self.externalpackage.isMatching(other_ep_1))
-
- other_distro = self.factory.makeDistribution()
- other_ep_2 = self.factory.makeExternalPackage(
- sourcepackagename=self.sourcepackagename, distribution=other_distro
- )
- self.assertFalse(self.externalpackage.isMatching(other_ep_2))
-
def test_compare(self):
"""Test __eq__ and __neq__"""
self.assertEqual(self.externalpackage, self.externalpackage_copy)
diff --git a/lib/lp/registry/tests/test_externalpackageseries.py b/lib/lp/registry/tests/test_externalpackageseries.py
index 03539d8..1a8dc62 100644
--- a/lib/lp/registry/tests/test_externalpackageseries.py
+++ b/lib/lp/registry/tests/test_externalpackageseries.py
@@ -178,36 +178,6 @@ class TestExternalPackageSeries(TestCaseWithFactory):
)
self.assertEqual(expected, self.externalpackageseries.bugtarget_parent)
- def test_matches(self):
- """Test if two externalpackageseries matches in sourcepackagename,
- distroseries and packagetype.
- """
- self.assertTrue(
- self.externalpackageseries.isMatching(
- self.externalpackageseries_matching
- )
- )
-
- self.assertFalse(
- self.externalpackageseries.isMatching(
- self.externalpackageseries_maven
- )
- )
-
- other_spn = self.factory.makeSourcePackageName()
- other_eps_1 = self.factory.makeExternalPackageSeries(
- sourcepackagename=other_spn,
- distroseries=self.distroseries,
- )
- self.assertFalse(self.externalpackageseries.isMatching(other_eps_1))
-
- other_distroseries = self.factory.makeDistroSeries()
- other_eps_2 = self.factory.makeExternalPackageSeries(
- sourcepackagename=self.sourcepackagename,
- distroseries=other_distroseries,
- )
- self.assertFalse(self.externalpackageseries.isMatching(other_eps_2))
-
def test_compare(self):
"""Test __eq__ and __neq__"""
self.assertEqual(
diff --git a/lib/lp/services/channels.py b/lib/lp/services/channels.py
index 418cd02..e828190 100644
--- a/lib/lp/services/channels.py
+++ b/lib/lp/services/channels.py
@@ -40,7 +40,7 @@ def channel_string_to_list(channel):
# Only 1, 2, or 3 components are allowed
if len(components) > 3:
- raise ValueError("Invalid channel name: %r" % channel)
+ raise ValueError(f"Invalid channel name: {channel}")
track = None
risk = None
@@ -54,16 +54,16 @@ def channel_string_to_list(channel):
elif _is_risk(components[1]):
track, risk = components
else:
- raise ValueError("No valid risk provided: %r" % channel)
+ raise ValueError(f"No valid risk provided: {channel}")
elif len(components) == 1:
risk = components[0]
# Validate risk and branch names
if not _is_risk(risk):
- raise ValueError("No valid risk provided: %r" % channel)
+ raise ValueError(f"No valid risk provided: {channel}")
if branch and _is_risk(branch):
- raise ValueError("Branch name cannot match a risk name: %r" % channel)
+ raise ValueError(f"Branch name cannot match a risk name: {channel}")
return track, risk, branch
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 95ae12e..78bd52b 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -180,6 +180,7 @@ from lp.registry.interfaces.distroseriesdifferencecomment import (
)
from lp.registry.interfaces.distroseriesparent import IDistroSeriesParentSet
from lp.registry.interfaces.externalpackage import ExternalPackageType
+from lp.registry.interfaces.externalpackageseries import IExternalPackageSeries
from lp.registry.interfaces.gpg import IGPGKeySet
from lp.registry.interfaces.mailinglist import (
IMailingListSet,
@@ -2450,7 +2451,9 @@ class LaunchpadObjectFactory(ObjectFactory):
:param target: The `IProductSeries`, `IDistroSeries` or
`ISourcePackage` to nominate for.
"""
- if ISourcePackage.providedBy(target):
+ if ISourcePackage.providedBy(
+ target
+ ) or IExternalPackageSeries.providedBy(target):
non_series = target.distribution_sourcepackage
series = target.distroseries
else:
@@ -5654,8 +5657,10 @@ class LaunchpadObjectFactory(ObjectFactory):
if channel is None:
channel = ("12.1", "stable", None)
- return distribution.getExternalPackage(
- sourcepackagename, packagetype, channel
+ return ProxyFactory(
+ distribution.getExternalPackage(
+ sourcepackagename, packagetype, channel
+ )
)
def makeExternalPackageSeries(
@@ -5676,8 +5681,10 @@ class LaunchpadObjectFactory(ObjectFactory):
if channel is None:
channel = ("12.1", "stable", None)
- return distroseries.getExternalPackageSeries(
- sourcepackagename, packagetype, channel
+ return ProxyFactory(
+ distroseries.getExternalPackageSeries(
+ sourcepackagename, packagetype, channel
+ )
)
def makeEmailMessage(
@@ -5968,7 +5975,7 @@ class LaunchpadObjectFactory(ObjectFactory):
"""Create a new CVE record."""
if sequence is None:
sequence = "2000-%04i" % self.getUniqueInteger()
-
+
if description is None:
description = self.getUniqueUnicode()
Follow ups