Thanks for reviewing! Addressed some of the comments, will finish it tomorrow.
Diff comments: > diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py > index 1714f52..da41350 100644 > --- a/lib/lp/app/browser/tales.py > +++ b/lib/lp/app/browser/tales.py > @@ -733,6 +734,9 @@ class ObjectImageDisplayAPI: > sprite_string = "distribution" > elif IDistributionSourcePackage.providedBy(context): > sprite_string = "package-source" > + elif IExternalPackage.providedBy(context): > + # TODO: create a new sprite for ExternalPackages? Adding name + date to this comment! > + sprite_string = "package-source" > elif ISprint.providedBy(context): > sprite_string = "meeting" > elif IBug.providedBy(context): > diff --git a/lib/lp/app/widgets/tests/test_launchpadtarget.py > b/lib/lp/app/widgets/tests/test_launchpadtarget.py > index 40350b8..16f5f3e 100644 > --- a/lib/lp/app/widgets/tests/test_launchpadtarget.py > +++ b/lib/lp/app/widgets/tests/test_launchpadtarget.py > @@ -61,6 +61,9 @@ class LaunchpadTargetWidgetTestCase(TestCaseWithFactory): > self.package = self.factory.makeDSPCache( > distroseries=distroseries, sourcepackagename="snarf" > ) > + self.externalpackage = self.factory.makeExternalPackage( > + distribution=self.distribution, sourcepackagename="snarf" Done :) > + ) > self.project = self.factory.makeProduct("pting") > field = Reference(__name__="target", schema=Interface, > title="target") > field = field.bind(Thing()) > diff --git a/lib/lp/bugs/browser/bugtask.py b/lib/lp/bugs/browser/bugtask.py > index 8fcb135..69f7f0c 100644 > --- a/lib/lp/bugs/browser/bugtask.py > +++ b/lib/lp/bugs/browser/bugtask.py > @@ -320,7 +321,12 @@ class BugTargetTraversalMixin: > # rather than making it look as though this task was "not found", > # because it was filtered out by privacy-aware code. > for bugtask in bug.bugtasks: > - if bugtask.target == context: > + if bugtask.target == context or IExternalPackage.providedBy( Agree, I did work on this for the next MP but I agree to change the workaround for this one! I've changed it. I think we can live with this new workaround for now until I deploy the next MP as we don't have any way to create an ExternalPackage in production yet. > + bugtask.target > + ): > + # TODO: set +external urls for ExternalPackages > + # actually we select the first ExternalPackage that appears Done! > + > # Security proxy this object on the way out. > return getUtility(IBugTaskSet).get(bugtask.id) > > diff --git a/lib/lp/bugs/browser/structuralsubscription.py > b/lib/lp/bugs/browser/structuralsubscription.py > index ec278f7..96f2687 100644 > --- a/lib/lp/bugs/browser/structuralsubscription.py > +++ b/lib/lp/bugs/browser/structuralsubscription.py > @@ -284,9 +285,11 @@ class StructuralSubscriptionView(LaunchpadFormView): > def userIsDriver(self): > """Has the current user driver permissions?""" > # We only want to look at this if the target is a > - # distribution source package, in order to maintain > + # distribution or external package, in order to maintain done :) > # compatibility with the obsolete bug contacts feature. > - if IDistributionSourcePackage.providedBy(self.context): > + if IDistributionSourcePackage.providedBy( > + self.context > + ) or IExternalPackage.providedBy(self.context): > return check_permission( > "launchpad.Driver", self.context.distribution > ) > diff --git a/lib/lp/bugs/model/bugtask.py b/lib/lp/bugs/model/bugtask.py > index d6116ce..5e9af48 100644 > --- a/lib/lp/bugs/model/bugtask.py > +++ b/lib/lp/bugs/model/bugtask.py > @@ -371,6 +389,9 @@ def validate_target( > ) > except NotFoundError as e: > raise IllegalTarget(e.args[0]) > + elif IExternalPackage.providedBy(target): > + # TODO: Check with store/soss that package exists done :) > + pass > > legal_types = target.pillar.getAllowedBugInformationTypes() > new_pillar = target.pillar not in bug.affected_pillars > diff --git a/lib/lp/bugs/model/structuralsubscription.py > b/lib/lp/bugs/model/structuralsubscription.py > index 37e8be2..debb06e 100644 > --- a/lib/lp/bugs/model/structuralsubscription.py > +++ b/lib/lp/bugs/model/structuralsubscription.py > @@ -607,7 +608,12 @@ def get_structural_subscriptions_for_bug(bug, > person=None): > # This is here because of a circular import. > from lp.registry.model.person import Person > > - bugtasks = bug.bugtasks > + bugtasks = [] > + # TODO: support bug subscriptions done > + for bugtask in bug.bugtasks: > + if not IExternalPackage.providedBy(bugtask.target): > + bugtasks.append(bugtask) > + > if not bugtasks: > return EmptyResultSet() > conditions = [] > diff --git a/lib/lp/bugs/tests/test_structuralsubscription.py > b/lib/lp/bugs/tests/test_structuralsubscription.py > index ce1f712..a22e0d6 100644 > --- a/lib/lp/bugs/tests/test_structuralsubscription.py > +++ b/lib/lp/bugs/tests/test_structuralsubscription.py > @@ -548,6 +548,22 @@ class > TestGetStructuralSubscriptionTargets(TestCaseWithFactory): > }, > ) > > + def test_externalpackage_target(self): > + actor = self.factory.makePerson() > + login_person(actor) > + externalpackage = self.factory.makeExternalPackage() > + product = self.factory.makeProduct() > + bug = self.factory.makeBug(target=product) > + bug.addTask(actor, externalpackage) > + product_bugtask = bug.bugtasks[0] > + result = get_structural_subscription_targets(bug.bugtasks) > + self.assertEqual( > + set(result), > + { > + (product_bugtask, product), good catch, done! > + }, > + ) > + > def test_product_with_project_group(self): > # get_structural_subscription_targets() will yield both a > # product and its parent project group if it has one. > diff --git a/lib/lp/registry/interfaces/externalpackage.py > b/lib/lp/registry/interfaces/externalpackage.py > new file mode 100644 > index 0000000..f2dccf9 > --- /dev/null > +++ b/lib/lp/registry/interfaces/externalpackage.py > @@ -0,0 +1,148 @@ > +# Copyright 2009, 2025 Canonical Ltd. This software is licensed under the > +# GNU Affero General Public License version 3 (see the file LICENSE). > + > +"""External package interfaces.""" > + > +__all__ = [ > + "IExternalPackage", > + "ExternalPackageType", > +] > + > +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 > +from zope.schema import TextLine > + > +from lp import _ > +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.role import IHasDrivers > + > + > +@exported_as_webservice_entry(as_of="beta") > +class IExternalPackageView( > + IHeadingContext, > + IBugTarget, > + IHasOfficialBugTags, > + IHasDrivers, > +): > + """`IExternalPackage` attributes that require launchpad.View.""" > + > + packagetype = Attribute("The package type") > + > + channel = Attribute("The package channel") > + > + display_channel = TextLine(title=_("Display channel name"), > readonly=True) > + > + distribution = exported( > + Reference(IDistribution, title=_("The distribution.")) > + ) > + sourcepackagename = Attribute("The source package name.") > + > + name = exported( > + TextLine(title=_("The source package name as text"), readonly=True) > + ) > + display_name = exported( > + TextLine(title=_("Display name for this package."), readonly=True) > + ) > + displayname = Attribute("Display name (deprecated)") > + title = exported( > + TextLine(title=_("Title for this package."), readonly=True) > + ) > + > + drivers = Attribute("The drivers for the distribution.") > + > + def __eq__(other): > + """IExternalPackage comparison method. > + > + Distro sourcepackages compare equal only if their fields compare > equal. > + """ > + > + def __ne__(other): > + """IExternalPackage comparison method. > + > + External packages compare not equal if either of their > + fields compare not equal. > + """ > + > + > +@exported_as_webservice_entry(as_of="beta") > +class IExternalPackage( > + IExternalPackageView, > +): > + """Represents an ExternalPackage in a distribution. > + > + Create IExternalPackage by invoking `IDistribution.getExternalPackage()`. > + """ > + > + > +class ExternalPackageType(DBEnumeratedType): > + """Bug Task Status > + > + The various possible states for a bugfix in a specific place. > + """ > + > + SNAP = DBItem( I would say that we will need the Unknown package type = 0, so I'll add that one > + 1, > + """ > + Snap > + > + Snap external package > + """, > + ) > + > + CHARM = DBItem( > + 2, > + """ > + Charm > + > + Charm external package > + """, > + ) > + > + ROCK = DBItem( > + 3, > + """ > + Rock > + > + Rock external package > + """, > + ) > + > + PYTHON = DBItem( > + 4, > + """ > + Python > + > + Python external package > + """, > + ) > + > + CONDA = DBItem( > + 5, > + """ > + Conda > + > + Conda external package > + """, > + ) > + > + CARGO = DBItem( > + 6, > + """ > + Cargo > + > + Cargo external package > + """, > + ) > + > + MAVEN = DBItem( > + 7, > + """ > + Maven > + > + Maven external package > + """, > + ) -- https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/488673 Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-external-package into launchpad:master. _______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp