launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29321
[Merge] ~andrey-fedoseev/launchpad:bug-presense into launchpad:master
Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:bug-presense into launchpad:master.
Commit message:
Add `BugPresence` model
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/431710
It represents a range of versions or git commits in which the bug was present.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:bug-presense into launchpad:master.
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index 6eaa58c..89e0402 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -1113,4 +1113,29 @@
>
<allow interface="zope.schema.interfaces.IVocabularyFactory"/>
</securedutility>
+
+ <!-- BugPresence -->
+ <class
+ class="lp.bugs.model.bugpresence.BugPresence">
+ <require
+ permission="launchpad.View"
+ interface="lp.bugs.interfaces.bugpresence.IBugPresence"/>
+ <require
+ permission="launchpad.Edit"
+ set_schema="lp.bugs.interfaces.bugpresence.IBugPresence" />
+ </class>
+
+ <!-- BugPresenceSet -->
+ <class
+ class="lp.bugs.model.bugpresence.BugPresenceSet">
+ <allow
+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/>
+ </class>
+ <securedutility
+ class="lp.bugs.model.bugpresence.BugPresenceSet"
+ provides="lp.bugs.interfaces.bugpresence.IBugPresenceSet">
+ <allow
+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/>
+ </securedutility>
+
</configure>
diff --git a/lib/lp/bugs/interfaces/bugpresence.py b/lib/lp/bugs/interfaces/bugpresence.py
new file mode 100644
index 0000000..426a2d7
--- /dev/null
+++ b/lib/lp/bugs/interfaces/bugpresence.py
@@ -0,0 +1,85 @@
+from lazr.restful.fields import Reference
+from zope.interface import Attribute, Interface
+from zope.schema import Choice, Int, TextLine
+
+from lp import _
+from lp.services.fields import BugField
+
+__all__ = ["IBugPresence", "IBugPresenceSet"]
+
+
+class IBugPresence(Interface):
+ """
+ Represents a range of versions or git commits in which the bug was present.
+ """
+
+ id = Int()
+ bug = BugField(title=_("Bug"), readonly=True)
+ bug_id = Int()
+
+ project = Choice(title=_("Project"), required=False, vocabulary="Product")
+ project_id = Attribute("The product ID")
+ sourcepackagename = Choice(
+ title=_("Package"), required=False, vocabulary="SourcePackageName"
+ )
+ sourcepackagename_id = Attribute("The sourcepackagename ID")
+ distribution = Choice(
+ title=_("Distribution"), required=False, vocabulary="Distribution"
+ )
+ distribution_id = Attribute("The distribution ID")
+
+ broken_version = TextLine(
+ required=False, title=_("Version that introduced the bug")
+ )
+ fixed_version = TextLine(
+ required=False, title=_("Version that fixed the bug")
+ )
+
+ git_repository = Choice(
+ title=_("Git repository"), required=False, vocabulary="GitRepository"
+ )
+ git_repository_id = Attribute("The git repository ID")
+ broken_git_commit_sha1 = TextLine(
+ required=False,
+ title=_("Git commit that introduced the bug"),
+ max_length=40,
+ )
+ fixed_git_commit_sha1 = TextLine(
+ required=False, title=_("Git commit that fixed the bug"), max_length=40
+ )
+
+ target = Reference(
+ title=_("Target"),
+ required=True,
+ schema=Interface, # IBugTarget|IGitRepository
+ )
+
+
+class IBugPresenceSet(Interface):
+ def new(
+ bug,
+ target,
+ broken_version,
+ fixed_version,
+ broken_git_commit_sha1,
+ fixed_git_commit_sha1,
+ ):
+ """Create new BugPresence instance.
+ :param bug: a bug to create a bug presence for
+ :param target: a project, a distribution, a distribution package or
+ a git repository
+ :param broken_version: version in which the bug was introduced
+ :param fixed_version: version in which the bug was fixed
+ :param broken_git_commit_sha1: git commit in which the bug
+ was introduced (for git repository)
+ :param fixed_git_commit_sha1: git commit in which the bug
+ was fixed (for git repository)
+ """
+ pass
+
+ def getByBug(bug):
+ """Get all BugPresence instances for the given bug.
+ :param bug: a bug to get the bug presence instances from
+ :return: a collection of BugPresence instances
+ """
+ pass
diff --git a/lib/lp/bugs/model/bugpresence.py b/lib/lp/bugs/model/bugpresence.py
new file mode 100644
index 0000000..d4f718b
--- /dev/null
+++ b/lib/lp/bugs/model/bugpresence.py
@@ -0,0 +1,138 @@
+from storm.properties import Int, Unicode
+from storm.references import Reference
+from zope.interface import implementer
+
+from lp.bugs.interfaces.bugpresence import IBugPresence, IBugPresenceSet
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.registry.interfaces.distribution import IDistribution
+from lp.registry.interfaces.distributionsourcepackage import (
+ IDistributionSourcePackage,
+)
+from lp.registry.interfaces.product import IProduct
+from lp.services.database.interfaces import IStore
+from lp.services.database.stormbase import StormBase
+
+__all__ = ["BugPresence", "BugPresenceSet"]
+
+
+@implementer(IBugPresence)
+class BugPresence(StormBase):
+ """See `IBugPresence`."""
+
+ __storm_table__ = "BugPresence"
+
+ id = Int(primary=True)
+
+ bug_id = Int(name="bug", allow_none=False)
+ bug = Reference(bug_id, "Bug.id")
+
+ project_id = Int(name="project", allow_none=True)
+ project = Reference(project_id, "Product.id")
+
+ sourcepackagename_id = Int(name="sourcepackagename", allow_none=True)
+ sourcepackagename = Reference(sourcepackagename_id, "SourcePackageName.id")
+
+ distribution_id = Int(name="distribution", allow_none=True)
+ distribution = Reference(distribution_id, "Distribution.id")
+
+ broken_version = Unicode(name="broken_version", allow_none=True)
+ fixed_version = Unicode(name="fixed_version", allow_none=True)
+
+ git_repository_id = Int(name="git_repository", allow_none=True)
+ git_repository = Reference(git_repository_id, "GitRepository.id")
+
+ broken_git_commit_sha1 = Unicode(
+ name="broken_git_commit_sha1", allow_none=True
+ )
+ fixed_git_commit_sha1 = Unicode(
+ name="fixed_git_commit_sha1", allow_none=True
+ )
+
+ def __init__(
+ self,
+ bug,
+ target,
+ broken_version=None,
+ fixed_version=None,
+ broken_git_commit_sha1=None,
+ fixed_git_commit_sha1=None,
+ ):
+ self.bug = bug
+ self.target = target
+ self.broken_version = broken_version
+ self.fixed_version = fixed_version
+ self.broken_git_commit_sha1 = broken_git_commit_sha1
+ self.fixed_git_commit_sha1 = fixed_git_commit_sha1
+
+ @property
+ def target(self):
+ if self.project:
+ return self.project
+ elif self.distribution:
+ if self.sourcepackagename:
+ return self.distribution.getSourcePackage(
+ self.sourcepackagename
+ )
+ else:
+ return self.distribution
+ elif self.git_repository:
+ return self.git_repository
+ else:
+ raise AssertionError("Could not determine BugPresence target")
+
+ @target.setter
+ def target(self, target):
+ if IProduct.providedBy(target):
+ self.project = target
+ elif IDistribution.providedBy(target):
+ self.distribution = target
+ elif IDistributionSourcePackage.providedBy(target):
+ self.distribution = target.distribution
+ self.sourcepackagename = target.sourcepackagename
+ elif IGitRepository.providedBy(target):
+ self.git_repository = target
+ else:
+ raise AssertionError("Invalid BugPresence target.")
+
+
+@implementer(IBugPresenceSet)
+class BugPresenceSet:
+ """See `IBugPresenceSet`."""
+
+ def new(
+ self,
+ bug,
+ target,
+ broken_version=None,
+ fixed_version=None,
+ broken_git_commit_sha1=None,
+ fixed_git_commit_sha1=None,
+ ):
+
+ is_version = bool(broken_version or fixed_version)
+ is_git_range = bool(broken_git_commit_sha1 or fixed_git_commit_sha1)
+
+ if is_version == is_git_range:
+ raise ValueError(
+ "Either broken_version/fixed_version or "
+ "broken_git_commit_sha1/fixed_git_commit_sha1 "
+ "must be specified"
+ )
+
+ if target is None:
+ raise ValueError("target must be specified")
+
+ if is_git_range and not IGitRepository.providedBy(target):
+ raise ValueError("target must be a git repository")
+
+ return BugPresence(
+ bug=bug,
+ target=target,
+ broken_version=broken_version,
+ fixed_version=fixed_version,
+ broken_git_commit_sha1=broken_git_commit_sha1,
+ fixed_git_commit_sha1=fixed_git_commit_sha1,
+ )
+
+ def getByBug(self, bug):
+ return IStore(BugPresence).find(BugPresence, bug=bug)
diff --git a/lib/lp/bugs/security.py b/lib/lp/bugs/security.py
index 11d74e9..15ae315 100644
--- a/lib/lp/bugs/security.py
+++ b/lib/lp/bugs/security.py
@@ -17,6 +17,7 @@ from lp.bugs.interfaces.bug import IBug
from lp.bugs.interfaces.bugactivity import IBugActivity
from lp.bugs.interfaces.bugattachment import IBugAttachment
from lp.bugs.interfaces.bugnomination import IBugNomination
+from lp.bugs.interfaces.bugpresence import IBugPresence
from lp.bugs.interfaces.bugsubscription import IBugSubscription
from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
@@ -455,3 +456,35 @@ class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
or user.inTeam(self.obj.owner)
or user.in_admin
)
+
+
+class ViewBugPresence(DelegatedAuthorization):
+ """
+ A person that can view a Bug can also view a related BugPresence.
+ """
+
+ permission = "launchpad.View"
+ usedfor = IBugPresence
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.bug, "launchpad.View")
+
+ def checkAuthenticated(self, user):
+ r = super().checkAuthenticated(user)
+ return r
+
+ def checkUnauthenticated(self):
+ r = super().checkUnauthenticated()
+ return r
+
+
+class EditBugPresence(DelegatedAuthorization):
+ """
+ A person that can edit a Bug can also edit a related BugPresence.
+ """
+
+ permission = "launchpad.Edit"
+ usedfor = IBugPresence
+
+ def __init__(self, obj):
+ super().__init__(obj, obj.bug, "launchpad.Edit")
diff --git a/lib/lp/bugs/tests/test_bugpresence.py b/lib/lp/bugs/tests/test_bugpresence.py
new file mode 100644
index 0000000..ce03db9
--- /dev/null
+++ b/lib/lp/bugs/tests/test_bugpresence.py
@@ -0,0 +1,309 @@
+from psycopg2.errors import CheckViolation
+from zope.component import getUtility
+from zope.security import checkPermission
+
+from lp.app.enums import InformationType
+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
+from lp.bugs.model.bugpresence import BugPresence
+from lp.registry.model.distributionsourcepackage import (
+ DistributionSourcePackage,
+)
+from lp.services.database.interfaces import IStore
+from lp.testing import (
+ TestCaseWithFactory,
+ anonymous_logged_in,
+ person_logged_in,
+)
+from lp.testing.layers import DatabaseFunctionalLayer, ZopelessDatabaseLayer
+
+
+class TestBugPresence(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_target(self):
+ project = self.factory.makeProduct()
+ distribution = self.factory.makeDistribution()
+ spn = self.factory.makeSourcePackageName()
+
+ for target, attrs in (
+ (project, {"project": project}),
+ (distribution, {"distribution": distribution}),
+ (
+ DistributionSourcePackage(distribution, spn),
+ {"distribution": distribution, "sourcepackagename": spn},
+ ),
+ ):
+ bug_presence = BugPresence(
+ bug=self.factory.makeBug(),
+ target=target,
+ broken_version="1",
+ )
+ self.assertEqual(target, bug_presence.target)
+ for attr_name, value in attrs.items():
+ self.assertEqual(value, getattr(bug_presence, attr_name))
+
+ git_repository = self.factory.makeGitRepository()
+ bug_presence = BugPresence(
+ bug=self.factory.makeBug(),
+ target=git_repository,
+ broken_git_commit_sha1="1",
+ )
+ self.assertEqual(git_repository, bug_presence.target)
+ self.assertEqual(git_repository, bug_presence.git_repository)
+
+ def test_constraints(self):
+ project = self.factory.makeProduct()
+ distribution = self.factory.makeDistribution()
+ distro_package = self.factory.makeDistributionSourcePackage(
+ distribution=distribution
+ )
+ git_repository = self.factory.makeGitRepository()
+
+ store = IStore(BugPresence)
+ store.commit()
+
+ # no version range, nor git commit range
+ for target in project, distribution, distro_package, git_repository:
+ BugPresence(bug=self.factory.makeBug(), target=target)
+ self.assertRaises(CheckViolation, store.flush)
+ store.rollback()
+
+ # valid version range
+ for broken_version, fixed_version in (
+ ("1", None),
+ (None, "2"),
+ ("1", "2"),
+ ):
+ for target in project, distribution, distro_package:
+ bp = BugPresence(
+ bug=self.factory.makeBug(),
+ target=target,
+ broken_version=broken_version,
+ fixed_version=fixed_version,
+ )
+ store.commit()
+ bp = store.get(BugPresence, bp.id)
+ self.assertEqual(broken_version, bp.broken_version)
+ self.assertEqual(fixed_version, bp.fixed_version)
+
+ # valid git commit range
+ for broken_git_commit_sha1, fixed_git_commit_sha1 in (
+ ("1", None),
+ (None, "2"),
+ ("1", "2"),
+ ):
+ bp = BugPresence(
+ bug=self.factory.makeBug(),
+ target=git_repository,
+ broken_git_commit_sha1=broken_git_commit_sha1,
+ fixed_git_commit_sha1=fixed_git_commit_sha1,
+ )
+ store.commit()
+ bp = store.get(BugPresence, bp.id)
+ self.assertEqual(broken_git_commit_sha1, bp.broken_git_commit_sha1)
+ self.assertEqual(fixed_git_commit_sha1, bp.fixed_git_commit_sha1)
+
+
+class TestBugPresenceSecurity(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_view_permission(self):
+ project = self.factory.makeProduct()
+
+ # Anyone can view a public bug presence
+ public_bug = self.factory.makeBug(
+ target=project, information_type=InformationType.PUBLIC
+ )
+ bp = BugPresence(bug=public_bug, target=project, broken_version="1")
+ with anonymous_logged_in():
+ self.assertTrue(checkPermission("launchpad.View", bp))
+
+ # Anonymous and random users can't see a private bug presence
+ owner = self.factory.makePerson()
+ project = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY, owner=owner
+ )
+ with person_logged_in(owner):
+ private_bug = self.factory.makeBug(
+ target=project,
+ information_type=InformationType.PROPRIETARY,
+ owner=owner,
+ )
+ bp = BugPresence(bug=private_bug, target=project, broken_version="1")
+ with anonymous_logged_in():
+ self.assertFalse(checkPermission("launchpad.View", bp))
+ with person_logged_in(self.factory.makePerson()):
+ self.assertFalse(checkPermission("launchpad.View", bp))
+
+ # Private bug owners can see a private bug presence
+ with person_logged_in(owner):
+ self.assertTrue(checkPermission("launchpad.View", bp))
+
+ def test_edit_permission(self):
+ project = self.factory.makeProduct()
+
+ public_bug = self.factory.makeBug(
+ target=project, information_type=InformationType.PUBLIC
+ )
+ bp = BugPresence(bug=public_bug, target=project, broken_version="1")
+ # Anonymous can't edit a public bug presence
+ with anonymous_logged_in():
+ self.assertFalse(checkPermission("launchpad.Edit", bp))
+
+ # Any authenticated user can edit a public bug presence
+ with person_logged_in(self.factory.makePerson()):
+ self.assertTrue(checkPermission("launchpad.Edit", bp))
+
+ # Anonymous and random users can't edit a private bug presence
+ owner = self.factory.makePerson()
+ project = self.factory.makeProduct(
+ information_type=InformationType.PROPRIETARY, owner=owner
+ )
+ with person_logged_in(owner):
+ private_bug = self.factory.makeBug(
+ target=project,
+ information_type=InformationType.PROPRIETARY,
+ owner=owner,
+ )
+ bp = BugPresence(bug=private_bug, target=project, broken_version="1")
+ with anonymous_logged_in():
+ self.assertFalse(checkPermission("launchpad.Edit", bp))
+ with person_logged_in(self.factory.makePerson()):
+ self.assertFalse(checkPermission("launchpad.Edit", bp))
+
+ # Private bug owners can edit a private bug presence
+ with person_logged_in(owner):
+ self.assertTrue(checkPermission("launchpad.Edit", bp))
+
+
+class TestBugPresenceSet(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_new__version_range(self):
+ project = self.factory.makeProduct()
+ distribution = self.factory.makeDistribution()
+ distro_package = self.factory.makeDistributionSourcePackage(
+ distribution=distribution
+ )
+
+ bug_presence_set = getUtility(IBugPresenceSet)
+
+ # version range
+ for broken_version, fixed_version in (
+ ("1", None),
+ (None, "2"),
+ ("1", "2"),
+ ):
+ for target in project, distribution, distro_package:
+ bug = self.factory.makeBug()
+ bp = bug_presence_set.new(
+ bug=bug,
+ target=target,
+ broken_version=broken_version,
+ fixed_version=fixed_version,
+ )
+ self.assertEqual(bug, bp.bug)
+ self.assertEqual(target, bp.target)
+ self.assertEqual(broken_version, bp.broken_version)
+ self.assertEqual(fixed_version, bp.fixed_version)
+
+ def test_new__git_commit_range(self):
+ git_repository = self.factory.makeGitRepository()
+
+ bug_presence_set = getUtility(IBugPresenceSet)
+
+ # version range
+ for broken_git_commit_sha1, fixed_git_commit_sha1 in (
+ ("1", None),
+ (None, "2"),
+ ("1", "2"),
+ ):
+ bug = self.factory.makeBug()
+ bp = bug_presence_set.new(
+ bug=bug,
+ target=git_repository,
+ broken_git_commit_sha1=broken_git_commit_sha1,
+ fixed_git_commit_sha1=fixed_git_commit_sha1,
+ )
+ self.assertEqual(bug, bp.bug)
+ self.assertEqual(git_repository, bp.target)
+ self.assertEqual(broken_git_commit_sha1, bp.broken_git_commit_sha1)
+ self.assertEqual(fixed_git_commit_sha1, bp.fixed_git_commit_sha1)
+
+ def test_new__invalid_arguments(self):
+ project = self.factory.makeProduct()
+ bug = self.factory.makeBug()
+ bug_presence_set = getUtility(IBugPresenceSet)
+
+ # no version range nor git commit range is specified
+ self.assertRaises(
+ ValueError, bug_presence_set.new, bug=bug, target=project
+ )
+
+ # both version range and commit range is specified
+ self.assertRaises(
+ ValueError,
+ bug_presence_set.new,
+ bug=bug,
+ target=project,
+ broken_version="1",
+ fixed_git_commit_sha1="2",
+ )
+
+ # target is not a git repository for git commit range
+ self.assertRaises(
+ ValueError,
+ bug_presence_set.new,
+ bug=bug,
+ target=project,
+ fixed_git_commit_sha1="2",
+ )
+
+ # target is missing
+ self.assertRaises(
+ ValueError,
+ bug_presence_set.new,
+ bug=bug,
+ target=None,
+ broken_version="1",
+ )
+
+ def test_getByBug(self):
+ project = self.factory.makeProduct()
+ bug = self.factory.makeBug()
+ another_bug = self.factory.makeBug()
+ bug_presence_set = getUtility(IBugPresenceSet)
+
+ self.assertEqual([], list(bug_presence_set.getByBug(bug)))
+ self.assertEqual([], list(bug_presence_set.getByBug(another_bug)))
+
+ bp_1 = bug_presence_set.new(
+ bug=bug, target=project, broken_version="1"
+ )
+ bp_2 = bug_presence_set.new(bug=bug, target=project, fixed_version="2")
+ bp_3 = bug_presence_set.new(
+ bug=another_bug, target=project, broken_version="3"
+ )
+ self.assertEqual({bp_1, bp_2}, set(bug_presence_set.getByBug(bug)))
+ self.assertEqual({bp_3}, set(bug_presence_set.getByBug(another_bug)))
+
+
+class TestBugPresenceFactory(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_makeBugPresence__no_arguments(self):
+ bp = self.factory.makeBugPresence()
+ self.assertIsNotNone(bp.target)
+ self.assertIsNotNone(bp.broken_version)
+ self.assertIsNotNone(bp.fixed_version)
+
+ def test_makeBugPresence__git_repository(self):
+ git_repository = self.factory.makeGitRepository()
+ bp = self.factory.makeBugPresence(target=git_repository)
+ self.assertEqual(git_repository, bp.git_repository)
+ self.assertIsNotNone(bp.broken_git_commit_sha1)
+ self.assertIsNotNone(bp.fixed_git_commit_sha1)
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 7ecb210..7ec64e3 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -65,6 +65,7 @@ from lp.blueprints.interfaces.specification import ISpecificationSet
from lp.blueprints.interfaces.sprint import ISprintSet
from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource
from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
from lp.bugs.interfaces.bugtask import (
BugTaskImportance,
BugTaskStatus,
@@ -2636,6 +2637,38 @@ class LaunchpadObjectFactory(ObjectFactory):
subscriber, subscribed_by
)
+ def makeBugPresence(
+ self,
+ bug=None,
+ target=None,
+ broken_version=None,
+ fixed_version=None,
+ broken_git_commit_sha1=None,
+ fixed_git_commit_sha1=None,
+ ):
+ if bug is None:
+ bug = self.makeBug()
+ if target is None:
+ target = self.makeProduct()
+
+ if IGitRepository.providedBy(target):
+ if not broken_git_commit_sha1 and not fixed_git_commit_sha1:
+ broken_git_commit_sha1 = self.getUniqueHexString(40)
+ fixed_git_commit_sha1 = self.getUniqueHexString(40)
+
+ elif not broken_version and not fixed_version:
+ broken_version = self.getUniqueString("version")
+ fixed_version = self.getUniqueString("version")
+
+ return getUtility(IBugPresenceSet).new(
+ bug=bug,
+ target=target,
+ broken_version=broken_version,
+ fixed_version=fixed_version,
+ broken_git_commit_sha1=broken_git_commit_sha1,
+ fixed_git_commit_sha1=fixed_git_commit_sha1,
+ )
+
def makeSignedMessage(
self,
msgid=None,