launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18291
[Merge] lp:~cjwatson/launchpad/git-subscriptions into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-subscriptions into lp:launchpad.
Commit message:
Add Git repository subscriptions.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1444591 in Launchpad itself: "Allow Git repository subscriptions"
https://bugs.launchpad.net/launchpad/+bug/1444591
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-subscriptions/+merge/256381
Add Git repository subscriptions. There are lots of tentacles into personmerge and sharing, mostly cleaning up the sites of previous XXX comments now that we can.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-subscriptions into lp:launchpad.
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py 2015-04-13 19:02:15 +0000
+++ lib/lp/_schema_circular_imports.py 2015-04-15 18:39:22 +0000
@@ -71,6 +71,7 @@
from lp.code.interfaces.diff import IPreviewDiff
from lp.code.interfaces.gitref import IGitRef
from lp.code.interfaces.gitrepository import IGitRepository
+from lp.code.interfaces.gitsubscription import IGitSubscription
from lp.code.interfaces.hasbranches import (
IHasBranches,
IHasCodeImports,
@@ -567,6 +568,9 @@
# IGitRepository
patch_collection_property(IGitRepository, 'branches', IGitRef)
patch_collection_property(IGitRepository, 'refs', IGitRef)
+patch_collection_property(IGitRepository, 'subscriptions', IGitSubscription)
+patch_entry_return_type(IGitRepository, 'subscribe', IGitSubscription)
+patch_entry_return_type(IGitRepository, 'getSubscription', IGitSubscription)
# ILiveFSFile
patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild)
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-03-12 15:21:27 +0000
+++ lib/lp/code/configure.zcml 2015-04-15 18:39:22 +0000
@@ -839,6 +839,15 @@
<allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
</securedutility>
+ <!-- GitSubscription -->
+
+ <class class="lp.code.model.gitsubscription.GitSubscription">
+ <allow interface="lp.code.interfaces.gitsubscription.IGitSubscription"/>
+ <require
+ permission="zope.Public"
+ set_schema="lp.code.interfaces.gitsubscription.IGitSubscription"/>
+ </class>
+
<!-- GitNamespace -->
<class class="lp.code.model.gitnamespace.PackageGitNamespace">
=== modified file 'lib/lp/code/interfaces/branchsubscription.py'
--- lib/lp/code/interfaces/branchsubscription.py 2013-02-26 03:20:44 +0000
+++ lib/lp/code/interfaces/branchsubscription.py 2015-04-15 18:39:22 +0000
@@ -1,7 +1,7 @@
# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Bug subscription interfaces."""
+"""Bazaar branch subscription interfaces."""
__metaclass__ = type
=== modified file 'lib/lp/code/interfaces/gitcollection.py'
--- lib/lp/code/interfaces/gitcollection.py 2015-02-23 15:58:36 +0000
+++ lib/lp/code/interfaces/gitcollection.py 2015-04-15 18:39:22 +0000
@@ -113,6 +113,10 @@
:return: A `ResultSet` of repositories that matched.
"""
+ def subscribedBy(person):
+ """Restrict the collection to repositories subscribed to by
+ 'person'."""
+
def visibleByUser(person):
"""Restrict the collection to repositories that person is allowed to
see."""
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2015-04-13 19:02:15 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2015-04-15 18:39:22 +0000
@@ -55,6 +55,11 @@
from lp import _
from lp.app.enums import InformationType
from lp.app.validators import LaunchpadValidationError
+from lp.code.enums import (
+ BranchSubscriptionDiffSize,
+ BranchSubscriptionNotificationLevel,
+ CodeReviewNotificationLevel,
+ )
from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
from lp.registry.interfaces.distributionsourcepackage import (
@@ -201,6 +206,16 @@
# Really IGitRef, patched in _schema_circular_imports.py.
value_type=Reference(Interface)))
+ subscriptions = exported(CollectionField(
+ title=_("GitSubscriptions associated with this repository."),
+ readonly=True,
+ # Really IGitSubscription, patched in _schema_circular_imports.py.
+ value_type=Reference(Interface)))
+
+ subscribers = exported(CollectionField(
+ title=_("Persons subscribed to this repository."),
+ readonly=True, value_type=Reference(IPerson)))
+
def getRefByPath(path):
"""Look up a single reference in this repository by path.
@@ -335,6 +350,73 @@
where the context object is the repository itself.
"""
+ def userCanBeSubscribed(person):
+ """Return True if the `IPerson` can be subscribed to the repository."""
+
+ @operation_parameters(
+ person=Reference(title=_("The person to subscribe."), schema=IPerson),
+ notification_level=Choice(
+ title=_("The level of notification to subscribe to."),
+ vocabulary=BranchSubscriptionNotificationLevel),
+ max_diff_lines=Choice(
+ title=_("The max number of lines for diff email."),
+ vocabulary=BranchSubscriptionDiffSize),
+ code_review_level=Choice(
+ title=_("The level of code review notification emails."),
+ vocabulary=CodeReviewNotificationLevel))
+ # Really IGitSubscription, patched in _schema_circular_imports.py.
+ @operation_returns_entry(Interface)
+ @call_with(subscribed_by=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def subscribe(person, notification_level, max_diff_lines,
+ code_review_level, subscribed_by):
+ """Subscribe this person to the repository.
+
+ :param person: The `Person` to subscribe.
+ :param notification_level: The kinds of repository changes that
+ cause notification.
+ :param max_diff_lines: The maximum number of lines of diff that may
+ appear in a notification.
+ :param code_review_level: The kinds of code review activity that
+ cause notification.
+ :param subscribed_by: The person who is subscribing the subscriber.
+ Most often the subscriber themselves.
+ :return: A new or existing `GitSubscription`.
+ """
+
+ @operation_parameters(
+ person=Reference(title=_("The person to unsubscribe"), schema=IPerson))
+ # Really IGitSubscription, patched in _schema_circular_imports.py.
+ @operation_returns_entry(Interface)
+ @export_read_operation()
+ @operation_for_version("devel")
+ def getSubscription(person):
+ """Return the `GitSubscription` for this person."""
+
+ def hasSubscription(person):
+ """Is this person subscribed to the repository?"""
+
+ @operation_parameters(
+ person=Reference(title=_("The person to unsubscribe"), schema=IPerson))
+ @call_with(unsubscribed_by=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def unsubscribe(person, unsubscribed_by):
+ """Remove the person's subscription to this repository.
+
+ :param person: The person or team to unsubscribe from the repository.
+ :param unsubscribed_by: The person doing the unsubscribing.
+ """
+
+ def getSubscriptionsByLevel(notification_levels):
+ """Return the subscriptions that are at the given notification levels.
+
+ :param notification_levels: An iterable of
+ `BranchSubscriptionNotificationLevel`s.
+ :return: A `ResultSet`.
+ """
+
class IGitRepositoryModerateAttributes(Interface):
"""IGitRepository attributes that can be edited by more than one community.
=== added file 'lib/lp/code/interfaces/gitsubscription.py'
--- lib/lp/code/interfaces/gitsubscription.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/gitsubscription.py 2015-04-15 18:39:22 +0000
@@ -0,0 +1,99 @@
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Git repository subscription interfaces."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'IGitSubscription',
+ ]
+
+from lazr.restful.declarations import (
+ call_with,
+ export_as_webservice_entry,
+ export_read_operation,
+ exported,
+ operation_for_version,
+ REQUEST_USER,
+ )
+from lazr.restful.fields import Reference
+from zope.interface import Interface
+from zope.schema import (
+ Choice,
+ Int,
+ )
+
+from lp import _
+from lp.code.enums import (
+ BranchSubscriptionDiffSize,
+ BranchSubscriptionNotificationLevel,
+ CodeReviewNotificationLevel,
+ )
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.services.fields import PersonChoice
+
+
+class IGitSubscription(Interface):
+ """The relationship between a person and a Git repository."""
+
+ # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
+ # generation working. Individual attributes must set their version to
+ # "devel".
+ export_as_webservice_entry(as_of="beta")
+
+ id = Int(title=_("ID"), readonly=True, required=True)
+ personID = Int(title=_("Person ID"), required=True, readonly=True)
+ person = exported(
+ PersonChoice(
+ title=_("Person"), required=True, vocabulary="ValidPersonOrTeam",
+ readonly=True,
+ description=_(
+ 'Enter the launchpad id, or email address of the person you '
+ 'wish to subscribe to this repository. If you are unsure, use '
+ 'the "Choose..." option to find the person in Launchpad. You '
+ 'can only subscribe someone who is a registered user of the '
+ 'system.')))
+ repository = exported(
+ Reference(
+ title=_("Repository ID"), required=True, readonly=True,
+ schema=IGitRepository))
+ notification_level = exported(
+ Choice(
+ title=_("Notification Level"), required=True,
+ vocabulary=BranchSubscriptionNotificationLevel,
+ default=BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
+ description=_(
+ "Attribute notifications are sent when repository details are "
+ "changed such as lifecycle status and name. Revision "
+ "notifications are generated when new revisions are found.")))
+ max_diff_lines = exported(
+ Choice(
+ title=_("Generated Diff Size Limit"), required=True,
+ vocabulary=BranchSubscriptionDiffSize,
+ default=BranchSubscriptionDiffSize.ONEKLINES,
+ description=_(
+ "Diffs greater than the specified number of lines will not "
+ "be sent to the subscriber. The subscriber will still "
+ "receive an email with the new revision details even if the "
+ "diff is larger than the specified number of lines.")))
+ review_level = exported(
+ Choice(
+ title=_("Code review Level"), required=True,
+ vocabulary=CodeReviewNotificationLevel,
+ default=CodeReviewNotificationLevel.FULL,
+ description=_(
+ "Control the kind of review activity that triggers "
+ "notifications."
+ )))
+
+ subscribed_by = exported(PersonChoice(
+ title=_("Subscribed by"), required=True,
+ vocabulary="ValidPersonOrTeam", readonly=True,
+ description=_("The person who created this subscription.")))
+
+ @call_with(user=REQUEST_USER)
+ @export_read_operation()
+ @operation_for_version("devel")
+ def canBeUnsubscribedByUser(user):
+ """Can the user unsubscribe the subscriber from the repository?"""
=== modified file 'lib/lp/code/interfaces/webservice.py'
--- lib/lp/code/interfaces/webservice.py 2015-04-13 19:02:15 +0000
+++ lib/lp/code/interfaces/webservice.py 2015-04-15 18:39:22 +0000
@@ -28,6 +28,7 @@
'IGitRef',
'IGitRepository',
'IGitRepositorySet',
+ 'IGitSubscription',
'IHasGitRepositories',
'IPreviewDiff',
'ISourcePackageRecipe',
@@ -66,6 +67,7 @@
IGitRepository,
IGitRepositorySet,
)
+from lp.code.interfaces.gitsubscription import IGitSubscription
from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
from lp.code.interfaces.sourcepackagerecipebuild import (
=== modified file 'lib/lp/code/model/branchnamespace.py'
--- lib/lp/code/model/branchnamespace.py 2015-02-11 11:48:49 +0000
+++ lib/lp/code/model/branchnamespace.py 2015-04-15 18:39:22 +0000
@@ -158,10 +158,10 @@
control_format=control_format, distroseries=distroseries,
sourcepackagename=sourcepackagename)
- # The registrant of the branch should also be automatically subscribed
- # in order for them to get code review notifications. The implicit
- # registrant subscription does not cause email to be sent about
- # attribute changes, just merge proposals and code review comments.
+ # The owner of the branch should also be automatically subscribed in
+ # order for them to get code review notifications. The implicit
+ # owner subscription does not cause email to be sent about attribute
+ # changes, just merge proposals and code review comments.
branch.subscribe(
self.owner,
BranchSubscriptionNotificationLevel.NOEMAIL,
=== modified file 'lib/lp/code/model/gitcollection.py'
--- lib/lp/code/model/gitcollection.py 2015-02-23 15:58:36 +0000
+++ lib/lp/code/model/gitcollection.py 2015-04-15 18:39:22 +0000
@@ -35,6 +35,7 @@
GitRepository,
get_git_repository_privacy_filter,
)
+from lp.code.model.gitsubscription import GitSubscription
from lp.registry.enums import EXCLUSIVE_TEAM_POLICY
from lp.registry.model.person import Person
from lp.registry.model.product import Product
@@ -267,6 +268,14 @@
return collection.getRepositories(eager_load=False).order_by(
GitRepository.name, GitRepository.id)
+ def subscribedBy(self, person):
+ """See `IGitCollection`."""
+ return self._filterBy(
+ [GitSubscription.person == person],
+ table=GitSubscription,
+ join=Join(GitSubscription,
+ GitSubscription.repository == GitRepository.id))
+
def visibleByUser(self, person):
"""See `IGitCollection`."""
if (person == LAUNCHPAD_SERVICES or
=== modified file 'lib/lp/code/model/gitnamespace.py'
--- lib/lp/code/model/gitnamespace.py 2015-03-17 16:05:54 +0000
+++ lib/lp/code/model/gitnamespace.py 2015-04-15 18:39:22 +0000
@@ -25,6 +25,11 @@
PUBLIC_INFORMATION_TYPES,
)
from lp.app.interfaces.services import IService
+from lp.code.enums import (
+ BranchSubscriptionDiffSize,
+ BranchSubscriptionNotificationLevel,
+ CodeReviewNotificationLevel,
+ )
from lp.code.errors import (
GitRepositoryCreationForbidden,
GitRepositoryCreatorNotMemberOfOwnerTeam,
@@ -74,6 +79,18 @@
repository = GitRepository(
registrant, self.owner, self.target, name, information_type,
date_created, description=description)
+
+ # The owner of the repository should also be automatically subscribed
+ # in order for them to get code review notifications. The implicit
+ # owner subscription does not cause email to be sent about attribute
+ # changes, just merge proposals and code review comments.
+ repository.subscribe(
+ self.owner,
+ BranchSubscriptionNotificationLevel.NOEMAIL,
+ BranchSubscriptionDiffSize.NODIFF,
+ CodeReviewNotificationLevel.FULL,
+ registrant)
+
repository._reconcileAccess()
notify(ObjectCreatedEvent(repository))
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-03-30 09:46:03 +0000
+++ lib/lp/code/model/gitrepository.py 2015-04-15 18:39:22 +0000
@@ -45,6 +45,10 @@
PRIVATE_INFORMATION_TYPES,
PUBLIC_INFORMATION_TYPES,
)
+from lp.app.errors import (
+ SubscriptionPrivacyViolation,
+ UserCannotUnsubscribePerson,
+ )
from lp.app.interfaces.informationtype import IInformationType
from lp.app.interfaces.launchpad import IPrivacy
from lp.app.interfaces.services import IService
@@ -72,9 +76,11 @@
)
from lp.code.interfaces.revision import IRevisionSet
from lp.code.model.gitref import GitRef
+from lp.code.model.gitsubscription import GitSubscription
from lp.registry.enums import PersonVisibility
from lp.registry.errors import CannotChangeInformationType
from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactGrantSource,
IAccessArtifactSource,
IAccessPolicySource,
)
@@ -91,6 +97,7 @@
AccessPolicyGrant,
reconcile_access_for_artifact,
)
+from lp.registry.model.person import Person
from lp.registry.model.teammembership import TeamParticipation
from lp.services.config import config
from lp.services.database import bulk
@@ -560,15 +567,11 @@
raise CannotChangeInformationType("Forbidden by project policy.")
self.information_type = information_type
self._reconcileAccess()
- # XXX cjwatson 2015-02-05: Once we have repository subscribers, we
- # need to grant them access if necessary. For now, treat the owner
- # as always subscribed, which is just about enough to make the
- # GitCollection tests pass.
- if information_type in PRIVATE_INFORMATION_TYPES:
+ if information_type in PRIVATE_INFORMATION_TYPES and self.subscribers:
# Grant the subscriber access if they can't see the repository.
service = getUtility(IService, "sharing")
blind_subscribers = service.getPeopleWithoutAccess(
- self, [self.owner])
+ self, self.subscribers)
if len(blind_subscribers):
service.ensureAccessGrants(
blind_subscribers, user, gitrepositories=[self],
@@ -583,6 +586,101 @@
new_namespace = get_git_namespace(self.target, new_owner)
new_namespace.moveRepository(self, user, rename_if_necessary=True)
+ @property
+ def subscriptions(self):
+ return Store.of(self).find(
+ GitSubscription,
+ GitSubscription.repository == self)
+
+ @property
+ def subscribers(self):
+ return Store.of(self).find(
+ Person,
+ GitSubscription.person_id == Person.id,
+ GitSubscription.repository == self)
+
+ def userCanBeSubscribed(self, person):
+ """See `IGitRepository`."""
+ return not (
+ person.is_team and
+ self.information_type in PRIVATE_INFORMATION_TYPES and
+ person.anyone_can_join())
+
+ def subscribe(self, person, notification_level, max_diff_lines,
+ code_review_level, subscribed_by):
+ """See `IGitRepository`."""
+ if not self.userCanBeSubscribed(person):
+ raise SubscriptionPrivacyViolation(
+ "Open and delegated teams cannot be subscribed to private "
+ "repositories.")
+ # If the person is already subscribed, update the subscription with
+ # the specified notification details.
+ subscription = self.getSubscription(person)
+ if subscription is None:
+ subscription = GitSubscription(
+ person=person, repository=self,
+ notification_level=notification_level,
+ max_diff_lines=max_diff_lines, review_level=code_review_level,
+ subscribed_by=subscribed_by)
+ Store.of(subscription).flush()
+ else:
+ subscription.notification_level = notification_level
+ subscription.max_diff_lines = max_diff_lines
+ subscription.review_level = code_review_level
+ # Grant the subscriber access if they can't see the repository.
+ service = getUtility(IService, "sharing")
+ _, _, repositories, _ = service.getVisibleArtifacts(
+ person, gitrepositories=[self], ignore_permissions=True)
+ if not repositories:
+ service.ensureAccessGrants(
+ [person], subscribed_by, gitrepositories=[self],
+ ignore_permissions=True)
+ return subscription
+
+ def getSubscription(self, person):
+ """See `IGitRepository`."""
+ if person is None:
+ return None
+ return Store.of(self).find(
+ GitSubscription,
+ GitSubscription.person == person,
+ GitSubscription.repository == self).one()
+
+ def getSubscriptionsByLevel(self, notification_levels):
+ """See `IGitRepository`."""
+ # XXX: JonathanLange 2009-05-07 bug=373026: This is only used by real
+ # code to determine whether there are any subscribers at the given
+ # notification levels. The only code that cares about the actual
+ # object is in a test:
+ # test_only_nodiff_subscribers_means_no_diff_generated.
+ return Store.of(self).find(
+ GitSubscription,
+ GitSubscription.repository == self,
+ GitSubscription.notification_level.is_in(notification_levels))
+
+ def hasSubscription(self, person):
+ """See `IGitRepository`."""
+ return self.getSubscription(person) is not None
+
+ def unsubscribe(self, person, unsubscribed_by, ignore_permissions=False):
+ """See `IGitRepository`."""
+ subscription = self.getSubscription(person)
+ if subscription is None:
+ # Silent success seems order of the day (like bugs).
+ return
+ if (not ignore_permissions
+ and not subscription.canBeUnsubscribedByUser(unsubscribed_by)):
+ raise UserCannotUnsubscribePerson(
+ '%s does not have permission to unsubscribe %s.' % (
+ unsubscribed_by.displayname,
+ person.displayname))
+ store = Store.of(subscription)
+ store.remove(subscription)
+ artifact = getUtility(IAccessArtifactSource).find([self])
+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(
+ artifact, [person])
+ store.flush()
+
def destroySelf(self):
raise NotImplementedError
=== added file 'lib/lp/code/model/gitsubscription.py'
--- lib/lp/code/model/gitsubscription.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/gitsubscription.py 2015-04-15 18:39:22 +0000
@@ -0,0 +1,71 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+ 'GitSubscription',
+ ]
+
+from storm.locals import (
+ Int,
+ Reference,
+ )
+from zope.interface import implements
+
+from lp.code.enums import (
+ BranchSubscriptionDiffSize,
+ BranchSubscriptionNotificationLevel,
+ CodeReviewNotificationLevel,
+ )
+from lp.code.interfaces.gitsubscription import IGitSubscription
+from lp.code.security import GitSubscriptionEdit
+from lp.registry.interfaces.person import validate_person
+from lp.registry.interfaces.role import IPersonRoles
+from lp.services.database.constants import DEFAULT
+from lp.services.database.enumcol import EnumCol
+from lp.services.database.stormbase import StormBase
+
+
+class GitSubscription(StormBase):
+ """A relationship between a person and a Git repository."""
+
+ __storm_table__ = 'GitSubscription'
+
+ implements(IGitSubscription)
+
+ id = Int(primary=True)
+
+ person_id = Int(name='person', allow_none=False, validator=validate_person)
+ person = Reference(person_id, 'Person.id')
+
+ repository_id = Int(name='repository', allow_none=False)
+ repository = Reference(repository_id, 'GitRepository.id')
+
+ notification_level = EnumCol(
+ enum=BranchSubscriptionNotificationLevel, notNull=True,
+ default=DEFAULT)
+ max_diff_lines = EnumCol(
+ enum=BranchSubscriptionDiffSize, notNull=False, default=DEFAULT)
+ review_level = EnumCol(
+ enum=CodeReviewNotificationLevel, notNull=True, default=DEFAULT)
+
+ subscribed_by_id = Int(
+ name='subscribed_by', allow_none=False, validator=validate_person)
+ subscribed_by = Reference(subscribed_by_id, 'Person.id')
+
+ def __init__(self, person, repository, notification_level, max_diff_lines,
+ review_level, subscribed_by):
+ super(GitSubscription, self).__init__()
+ self.person = person
+ self.repository = repository
+ self.notification_level = notification_level
+ self.max_diff_lines = max_diff_lines
+ self.review_level = review_level
+ self.subscribed_by = subscribed_by
+
+ def canBeUnsubscribedByUser(self, user):
+ """See `IBranchSubscription`."""
+ if user is None:
+ return False
+ permission_check = GitSubscriptionEdit(self)
+ return permission_check.checkAuthenticated(IPersonRoles(user))
=== modified file 'lib/lp/code/model/tests/test_branchsubscription.py'
--- lib/lp/code/model/tests/test_branchsubscription.py 2015-02-16 13:01:34 +0000
+++ lib/lp/code/model/tests/test_branchsubscription.py 2015-04-15 18:39:22 +0000
@@ -1,7 +1,7 @@
# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-"""Tests for the BranchSubscrptions model object.."""
+"""Tests for the BranchSubscription model object."""
__metaclass__ = type
=== modified file 'lib/lp/code/model/tests/test_gitcollection.py'
--- lib/lp/code/model/tests/test_gitcollection.py 2015-03-05 14:13:16 +0000
+++ lib/lp/code/model/tests/test_gitcollection.py 2015-04-15 18:39:22 +0000
@@ -12,6 +12,11 @@
from lp.app.enums import InformationType
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.interfaces.services import IService
+from lp.code.enums import (
+ BranchSubscriptionDiffSize,
+ BranchSubscriptionNotificationLevel,
+ CodeReviewNotificationLevel,
+ )
from lp.code.interfaces.codehosting import LAUNCHPAD_SERVICES
from lp.code.interfaces.gitcollection import (
IAllGitRepositories,
@@ -388,6 +393,19 @@
collection = self.all_repositories.registeredBy(registrant)
self.assertEqual([repository], list(collection.getRepositories()))
+ def test_subscribedBy(self):
+ # 'subscribedBy' returns a new collection that only has repositories
+ # that the given user is subscribed to.
+ repository = self.factory.makeGitRepository()
+ subscriber = self.factory.makePerson()
+ repository.subscribe(
+ subscriber, BranchSubscriptionNotificationLevel.NOEMAIL,
+ BranchSubscriptionDiffSize.NODIFF,
+ CodeReviewNotificationLevel.NOEMAIL,
+ subscriber)
+ collection = self.all_repositories.subscribedBy(subscriber)
+ self.assertEqual([repository], list(collection.getRepositories()))
+
class TestGenericGitCollectionVisibleFilter(TestCaseWithFactory):
@@ -457,6 +475,40 @@
sorted(self.all_repositories.getRepositories()),
sorted(repositories.getRepositories()))
+ def test_subscribers_can_see_repositories(self):
+ # A person subscribed to a repository can see it, even if it's
+ # private.
+ subscriber = self.factory.makePerson()
+ removeSecurityProxy(self.private_repository).subscribe(
+ subscriber, BranchSubscriptionNotificationLevel.NOEMAIL,
+ BranchSubscriptionDiffSize.NODIFF,
+ CodeReviewNotificationLevel.NOEMAIL,
+ subscriber)
+ repositories = self.all_repositories.visibleByUser(subscriber)
+ self.assertEqual(
+ sorted([self.public_repository, self.private_repository]),
+ sorted(repositories.getRepositories()))
+
+ def test_subscribed_team_members_can_see_repositories(self):
+ # A person in a team that is subscribed to a repository can see that
+ # repository, even if it's private.
+ team_owner = self.factory.makePerson()
+ team = self.factory.makeTeam(
+ membership_policy=TeamMembershipPolicy.MODERATED,
+ owner=team_owner)
+ # Subscribe the team.
+ removeSecurityProxy(self.private_repository).subscribe(
+ team, BranchSubscriptionNotificationLevel.NOEMAIL,
+ BranchSubscriptionDiffSize.NODIFF,
+ CodeReviewNotificationLevel.NOEMAIL,
+ team_owner)
+ # Members of the team can see the private repository that the team
+ # is subscribed to.
+ repositories = self.all_repositories.visibleByUser(team_owner)
+ self.assertEqual(
+ sorted([self.public_repository, self.private_repository]),
+ sorted(repositories.getRepositories()))
+
def test_private_teams_see_own_private_personal_repositories(self):
# Private teams are given an access grant to see their private
# personal repositories.
@@ -473,9 +525,7 @@
# they are the owner. We want to unsubscribe them so that they
# lose access conferred via subscription and rely instead on the
# APG.
- # XXX cjwatson 2015-02-05: Uncomment this once
- # GitRepositorySubscriptions exist.
- #personal_repository.unsubscribe(team, team_owner, True)
+ personal_repository.unsubscribe(team, team_owner, True)
# Make another personal repository the team can't see.
other_person = self.factory.makePerson()
self.factory.makeGitRepository(
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2015-03-20 14:54:23 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2015-04-15 18:39:22 +0000
@@ -970,7 +970,7 @@
owner = self.factory.makeTeam(visibility=PersonVisibility.PRIVATE)
with person_logged_in(owner):
repository = self.factory.makeGitRepository(
- owner=owner, target=owner,
+ owner=owner,
information_type=InformationType.USERDATA)
repository.setTarget(target=owner, user=owner)
self.assertEqual(
=== added file 'lib/lp/code/model/tests/test_gitsubscription.py'
--- lib/lp/code/model/tests/test_gitsubscription.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitsubscription.py 2015-04-15 18:39:22 +0000
@@ -0,0 +1,201 @@
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the GitSubscription model object."""
+
+__metaclass__ = type
+
+
+from lp.app.enums import InformationType
+from lp.app.errors import (
+ SubscriptionPrivacyViolation,
+ UserCannotUnsubscribePerson,
+ )
+from lp.code.enums import (
+ BranchSubscriptionNotificationLevel,
+ CodeReviewNotificationLevel,
+ )
+from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
+from lp.services.features.testing import FeatureFixture
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestGitSubscriptions(TestCaseWithFactory):
+ """Tests relating to Git repository subscriptions in general."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestGitSubscriptions, self).setUp()
+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
+
+ def test_owner_subscribed(self):
+ # The owner of a repository is subscribed to the repository.
+ repository = self.factory.makeGitRepository()
+ [subscription] = list(repository.subscriptions)
+ self.assertEqual(repository.owner, subscription.person)
+
+ def test_subscribed_by_set(self):
+ # The user subscribing is recorded along with the subscriber.
+ subscriber = self.factory.makePerson()
+ subscribed_by = self.factory.makePerson()
+ repository = self.factory.makeGitRepository()
+ subscription = repository.subscribe(
+ subscriber, BranchSubscriptionNotificationLevel.NOEMAIL, None,
+ CodeReviewNotificationLevel.NOEMAIL, subscribed_by)
+ self.assertEqual(subscriber, subscription.person)
+ self.assertEqual(subscribed_by, subscription.subscribed_by)
+
+ def test_unsubscribe(self):
+ # Test unsubscribing by the subscriber.
+ subscription = self.factory.makeGitSubscription()
+ subscriber = subscription.person
+ repository = subscription.repository
+ repository.unsubscribe(subscriber, subscriber)
+ self.assertFalse(repository.hasSubscription(subscriber))
+
+ def test_unsubscribe_by_subscriber(self):
+ # Test unsubscribing by the person who subscribed the user.
+ subscribed_by = self.factory.makePerson()
+ subscription = self.factory.makeGitSubscription(
+ subscribed_by=subscribed_by)
+ subscriber = subscription.person
+ repository = subscription.repository
+ repository.unsubscribe(subscriber, subscribed_by)
+ self.assertFalse(repository.hasSubscription(subscriber))
+
+ def test_unsubscribe_by_unauthorized(self):
+ # Test unsubscribing someone you shouldn't be able to.
+ subscription = self.factory.makeGitSubscription()
+ repository = subscription.repository
+ self.assertRaises(
+ UserCannotUnsubscribePerson,
+ repository.unsubscribe,
+ subscription.person,
+ self.factory.makePerson())
+
+ def test_cannot_subscribe_open_team_to_private_repository(self):
+ # It is forbidden to subscribe a open team to a private repository.
+ owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ information_type=InformationType.USERDATA, owner=owner)
+ team = self.factory.makeTeam()
+ with person_logged_in(owner):
+ self.assertRaises(
+ SubscriptionPrivacyViolation, repository.subscribe, team, None,
+ None, None, owner)
+
+ def test_subscribe_gives_access(self):
+ # Subscribing a user to a repository gives them access.
+ owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ information_type=InformationType.USERDATA, owner=owner)
+ subscribee = self.factory.makePerson()
+ with person_logged_in(owner):
+ self.assertFalse(repository.visibleByUser(subscribee))
+ repository.subscribe(
+ subscribee, BranchSubscriptionNotificationLevel.NOEMAIL,
+ None, CodeReviewNotificationLevel.NOEMAIL, owner)
+ self.assertTrue(repository.visibleByUser(subscribee))
+
+ def test_unsubscribe_removes_access(self):
+ # Unsubscribing a user from a repository removes their access.
+ owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(
+ information_type=InformationType.USERDATA, owner=owner)
+ subscribee = self.factory.makePerson()
+ with person_logged_in(owner):
+ repository.subscribe(
+ subscribee, BranchSubscriptionNotificationLevel.NOEMAIL,
+ None, CodeReviewNotificationLevel.NOEMAIL, owner)
+ self.assertTrue(repository.visibleByUser(subscribee))
+ repository.unsubscribe(subscribee, owner)
+ self.assertFalse(repository.visibleByUser(subscribee))
+
+
+class TestGitSubscriptionCanBeUnsubscribedbyUser(TestCaseWithFactory):
+ """Tests for GitSubscription.canBeUnsubscribedByUser."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestGitSubscriptionCanBeUnsubscribedbyUser, self).setUp()
+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
+
+ def test_none(self):
+ # None for a user always returns False.
+ subscription = self.factory.makeGitSubscription()
+ self.assertFalse(subscription.canBeUnsubscribedByUser(None))
+
+ def test_self_subscriber(self):
+ # The subscriber has permission to unsubscribe.
+ subscription = self.factory.makeGitSubscription()
+ self.assertTrue(
+ subscription.canBeUnsubscribedByUser(subscription.person))
+
+ def test_non_subscriber_fails(self):
+ # An unrelated person can't unsubscribe a user.
+ subscription = self.factory.makeGitSubscription()
+ editor = self.factory.makePerson()
+ self.assertFalse(subscription.canBeUnsubscribedByUser(editor))
+
+ def test_subscribed_by(self):
+ # If a user subscribes someone else, the user can unsubscribe.
+ subscribed_by = self.factory.makePerson()
+ subscriber = self.factory.makePerson()
+ subscription = self.factory.makeGitSubscription(
+ person=subscriber, subscribed_by=subscribed_by)
+ self.assertTrue(subscription.canBeUnsubscribedByUser(subscribed_by))
+
+ def test_team_member_can_unsubscribe(self):
+ # Any team member can unsubscribe the team from a repository.
+ team = self.factory.makeTeam()
+ member = self.factory.makePerson()
+ with person_logged_in(team.teamowner):
+ team.addMember(member, team.teamowner)
+ subscription = self.factory.makeGitSubscription(
+ person=team, subscribed_by=team.teamowner)
+ self.assertTrue(subscription.canBeUnsubscribedByUser(member))
+
+ def test_team_subscriber_can_unsubscribe(self):
+ # A team can be unsubscribed by the subscriber even if they are not
+ # a member.
+ team = self.factory.makeTeam()
+ subscribed_by = self.factory.makePerson()
+ subscription = self.factory.makeGitSubscription(
+ person=team, subscribed_by=subscribed_by)
+ self.assertTrue(subscription.canBeUnsubscribedByUser(subscribed_by))
+
+ def test_repository_person_owner_can_unsubscribe(self):
+ # The repository owner can unsubscribe someone from the repository.
+ repository_owner = self.factory.makePerson()
+ repository = self.factory.makeGitRepository(owner=repository_owner)
+ subscribed_by = self.factory.makePerson()
+ subscriber = self.factory.makePerson()
+ subscription = self.factory.makeGitSubscription(
+ repository=repository, person=subscriber,
+ subscribed_by=subscribed_by)
+ self.assertTrue(subscription.canBeUnsubscribedByUser(repository_owner))
+
+ def test_repository_team_owner_can_unsubscribe(self):
+ # The repository team owner can unsubscribe someone from the
+ # repository.
+ #
+ # If the owner of a repository is a team, then the team members can
+ # unsubscribe someone.
+ team_owner = self.factory.makePerson()
+ team_member = self.factory.makePerson()
+ repository_owner = self.factory.makeTeam(
+ owner=team_owner, members=[team_member])
+ repository = self.factory.makeGitRepository(owner=repository_owner)
+ subscribed_by = self.factory.makePerson()
+ subscriber = self.factory.makePerson()
+ subscription = self.factory.makeGitSubscription(
+ repository=repository, person=subscriber,
+ subscribed_by=subscribed_by)
+ self.assertTrue(subscription.canBeUnsubscribedByUser(team_owner))
+ self.assertTrue(subscription.canBeUnsubscribedByUser(team_member))
=== modified file 'lib/lp/code/security.py'
--- lib/lp/code/security.py 2011-07-13 06:06:53 +0000
+++ lib/lp/code/security.py 2015-04-15 18:39:22 +0000
@@ -7,10 +7,13 @@
__all__ = [
'BranchSubscriptionEdit',
'BranchSubscriptionView',
+ 'GitSubscriptionEdit',
+ 'GitSubscriptionView',
]
from lp.app.security import AuthorizationBase
from lp.code.interfaces.branchsubscription import IBranchSubscription
+from lp.code.interfaces.gitsubscription import IGitSubscription
class BranchSubscriptionEdit(AuthorizationBase):
@@ -34,3 +37,27 @@
class BranchSubscriptionView(BranchSubscriptionEdit):
permission = 'launchpad.View'
+
+
+class GitSubscriptionEdit(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = IGitSubscription
+
+ def checkAuthenticated(self, user):
+ """Is the user able to edit a Git repository subscription?
+
+ Any team member can edit a Git repository subscription for their
+ team.
+ Launchpad Admins can also edit any Git repository subscription.
+ The owner of the subscribed repository can edit the subscription. If
+ the repository owner is a team, then members of the team can edit
+ the subscription.
+ """
+ return (user.inTeam(self.obj.repository.owner) or
+ user.inTeam(self.obj.person) or
+ user.inTeam(self.obj.subscribed_by) or
+ user.in_admin)
+
+
+class GitSubscriptionView(GitSubscriptionEdit):
+ permission = 'launchpad.View'
=== modified file 'lib/lp/registry/browser/tests/private-team-creation-views.txt'
--- lib/lp/registry/browser/tests/private-team-creation-views.txt 2014-01-08 07:33:25 +0000
+++ lib/lp/registry/browser/tests/private-team-creation-views.txt 2015-04-15 18:39:22 +0000
@@ -230,14 +230,14 @@
Public teams can be made private if the only artifacts they have are
those permitted by private teams.
+ >>> from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
+ >>> from lp.services.features.testing import FeatureFixture
>>> def createTeamArtifacts(team, team_owner):
... # A bug subscription.
... bug = factory.makeBug()
... bug.subscribe(team, team_owner)
... bugtask = bug.default_bugtask
... bugtask.transitionToAssignee(team)
- ... # A branch.
- ... branch = factory.makeBranch(owner=team, registrant=team_owner)
... # A branch subscription.
... from lp.code.enums import (
... BranchSubscriptionDiffSize,
@@ -249,6 +249,14 @@
... BranchSubscriptionNotificationLevel.DIFFSONLY,
... BranchSubscriptionDiffSize.WHOLEDIFF,
... CodeReviewNotificationLevel.STATUS, team_owner)
+ ... # A Git repository subscription.
+ ... with FeatureFixture({GIT_FEATURE_FLAG: 'on'}):
+ ... repository = factory.makeGitRepository()
+ ... repository.subscribe(
+ ... team,
+ ... BranchSubscriptionNotificationLevel.DIFFSONLY,
+ ... BranchSubscriptionDiffSize.WHOLEDIFF,
+ ... CodeReviewNotificationLevel.STATUS, team_owner)
... # A PPA.
... from lp.soyuz.enums import ArchivePurpose
... from lp.soyuz.interfaces.archive import IArchiveSet
=== modified file 'lib/lp/registry/doc/private-team-roles.txt'
--- lib/lp/registry/doc/private-team-roles.txt 2012-10-08 14:05:57 +0000
+++ lib/lp/registry/doc/private-team-roles.txt 2015-04-15 18:39:22 +0000
@@ -60,7 +60,7 @@
Branch ownership
----------------
-Private teams can be assigned as the owner of a branch
+Private teams can be assigned as the owner of a branch.
>>> branch = factory.makeBranch()
>>> branch.setOwner(priv_team, user=admin_user)
@@ -84,6 +84,36 @@
private-team
+Git repositories
+================
+
+Git repository ownership
+------------------------
+
+Private teams can be assigned as the owner of a Git repository.
+
+ >>> from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
+ >>> from lp.services.features.testing import FeatureFixture
+ >>> with FeatureFixture({GIT_FEATURE_FLAG: 'on'}):
+ ... repository = factory.makeGitRepository()
+ >>> repository.setOwner(priv_team, user=admin_user)
+
+Git repository subscriptions
+----------------------------
+
+Private teams can subscribe to Git repositories.
+
+ >>> with FeatureFixture({GIT_FEATURE_FLAG: 'on'}):
+ ... repository = factory.makeGitRepository()
+ >>> subscription = repository.subscribe(
+ ... priv_team,
+ ... BranchSubscriptionNotificationLevel.DIFFSONLY,
+ ... BranchSubscriptionDiffSize.WHOLEDIFF,
+ ... CodeReviewNotificationLevel.STATUS, team_owner)
+ >>> print subscription.person.name
+ private-team
+
+
PPAs
====
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2015-03-10 22:07:32 +0000
+++ lib/lp/registry/model/person.py 2015-04-15 18:39:22 +0000
@@ -2275,6 +2275,7 @@
# Nuke all subscriptions of this person.
removals = [
('BranchSubscription', 'person'),
+ ('GitSubscription', 'person'),
('BugSubscription', 'person'),
('QuestionSubscription', 'person'),
('SpecificationSubscription', 'person'),
@@ -2343,6 +2344,8 @@
('bugsummary', 'viewed_by'),
('bugtask', 'assignee'),
('emailaddress', 'person'),
+ ('gitrepository', 'owner'),
+ ('gitsubscription', 'person'),
('gpgkey', 'owner'),
('ircid', 'person'),
('jabberid', 'person'),
=== modified file 'lib/lp/registry/model/sharingjob.py'
--- lib/lp/registry/model/sharingjob.py 2015-02-16 13:08:52 +0000
+++ lib/lp/registry/model/sharingjob.py 2015-04-15 18:39:22 +0000
@@ -63,7 +63,11 @@
get_branch_privacy_filter,
)
from lp.code.model.branchsubscription import BranchSubscription
-from lp.code.model.gitrepository import GitRepository
+from lp.code.model.gitrepository import (
+ get_git_repository_privacy_filter,
+ GitRepository,
+ )
+from lp.code.model.gitsubscription import GitSubscription
from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.sharingjob import (
@@ -421,8 +425,11 @@
Select(
TeamParticipation.personID,
where=TeamParticipation.team == self.grantee)))
- # XXX cjwatson 2015-02-05: Fill this in once we have
- # GitRepositorySubscription.
+ gitrepository_filters.append(
+ In(GitSubscription.person_id,
+ Select(
+ TeamParticipation.personID,
+ where=TeamParticipation.team == self.grantee)))
specification_filters.append(
In(SpecificationSubscription.personID,
Select(
@@ -452,8 +459,20 @@
for sub in branch_subscriptions:
sub.branch.unsubscribe(
sub.person, self.requestor, ignore_permissions=True)
- # XXX cjwatson 2015-02-05: Fill this in once we have
- # GitRepositorySubscription.
+ if gitrepository_filters:
+ gitrepository_filters.append(Not(
+ Or(*get_git_repository_privacy_filter(
+ GitSubscription.person_id))))
+ gitrepository_subscriptions = IStore(GitSubscription).using(
+ GitSubscription,
+ Join(
+ GitRepository,
+ GitRepository.id == GitSubscription.repository_id)
+ ).find(GitSubscription, *gitrepository_filters).config(
+ distinct=True)
+ for sub in gitrepository_subscriptions:
+ sub.repository.unsubscribe(
+ sub.person, self.requestor, ignore_permissions=True)
if specification_filters:
specification_filters.append(Not(*get_specification_privacy_filter(
SpecificationSubscription.personID)))
=== modified file 'lib/lp/registry/personmerge.py'
--- lib/lp/registry/personmerge.py 2015-02-23 19:47:01 +0000
+++ lib/lp/registry/personmerge.py 2015-04-15 18:39:22 +0000
@@ -208,6 +208,24 @@
''' % vars())
+def _mergeGitSubscription(cur, from_id, to_id):
+ # Update only the GitSubscription that will not conflict.
+ cur.execute('''
+ UPDATE GitSubscription
+ SET person=%(to_id)d
+ WHERE person=%(from_id)d AND repository NOT IN
+ (
+ SELECT repository
+ FROM GitSubscription
+ WHERE person = %(to_id)d
+ )
+ ''' % vars())
+ # and delete those left over.
+ cur.execute('''
+ DELETE FROM GitSubscription WHERE person=%(from_id)d
+ ''' % vars())
+
+
def _mergeBugAffectsPerson(cur, from_id, to_id):
# Update only the BugAffectsPerson that will not conflict
cur.execute('''
@@ -763,6 +781,9 @@
_mergeBranchSubscription(cur, from_id, to_id)
skip.append(('branchsubscription', 'person'))
+ _mergeGitSubscription(cur, from_id, to_id)
+ skip.append(('gitsubscription', 'person'))
+
_mergeBugAffectsPerson(cur, from_id, to_id)
skip.append(('bugaffectsperson', 'person'))
=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py 2015-03-05 16:23:26 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py 2015-04-15 18:39:22 +0000
@@ -1165,9 +1165,7 @@
self._assert_revokeTeamAccessGrants(
product, None, [branch], None, None)
- # XXX cjwatson 2015-02-05: Enable this once GitRepositorySubscription is
- # implemented.
- def disabled_test_revokeTeamAccessGrantsGitRepositories(self):
+ def test_revokeTeamAccessGrantsGitRepositories(self):
# Users with launchpad.Edit can delete all access for a grantee.
owner = self.factory.makePerson()
product = self.factory.makeProduct(owner=owner)
=== modified file 'lib/lp/registry/tests/test_sharingjob.py'
--- lib/lp/registry/tests/test_sharingjob.py 2015-03-04 18:22:06 +0000
+++ lib/lp/registry/tests/test_sharingjob.py 2015-04-15 18:39:22 +0000
@@ -333,8 +333,9 @@
branch.subscribe(artifact_indirect_grantee,
BranchSubscriptionNotificationLevel.NOEMAIL, None,
CodeReviewNotificationLevel.NOEMAIL, owner)
- # XXX cjwatson 2015-02-05: Fill this in once we have
- # GitRepositorySubscription.
+ gitrepository.subscribe(artifact_indirect_grantee,
+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
+ CodeReviewNotificationLevel.NOEMAIL, owner)
# Subscribing somebody to a specification does not automatically
# create an artifact grant.
spec_artifact = self.factory.makeAccessArtifact(specification)
@@ -379,8 +380,7 @@
self.assertIn(artifact_team_grantee, subscribers)
self.assertIn(artifact_indirect_grantee, bug.getDirectSubscribers())
self.assertIn(artifact_indirect_grantee, branch.subscribers)
- # XXX cjwatson 2015-02-05: Fill this in once we have
- # GitRepositorySubscription.
+ self.assertIn(artifact_indirect_grantee, gitrepository.subscribers)
self.assertIn(artifact_indirect_grantee,
removeSecurityProxy(specification).subscribers)
@@ -437,7 +437,7 @@
def _assert_gitrepository_change_unsubscribes(self, change_callback):
def get_pillars(concrete_artifact):
- return [concrete_artifact.product]
+ return [concrete_artifact.target]
def get_subscribers(concrete_artifact):
return concrete_artifact.subscribers
@@ -446,14 +446,22 @@
policy_team_grantee, policy_indirect_grantee,
artifact_team_grantee, owner):
concrete_artifact = gitrepository
- # XXX cjwatson 2015-02-05: Fill this in once we have
- # GitRepositorySubscription.
+ gitrepository.subscribe(
+ policy_team_grantee,
+ BranchSubscriptionNotificationLevel.NOEMAIL,
+ None, CodeReviewNotificationLevel.NOEMAIL, owner)
+ gitrepository.subscribe(
+ policy_indirect_grantee,
+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
+ CodeReviewNotificationLevel.NOEMAIL, owner)
+ gitrepository.subscribe(
+ artifact_team_grantee,
+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
+ CodeReviewNotificationLevel.NOEMAIL, owner)
return concrete_artifact, get_pillars, get_subscribers
- # XXX cjwatson 2015-02-05: Uncomment once we have
- # GitRepositorySubscription.
- #self._assert_artifact_change_unsubscribes(
- # change_callback, configure_test)
+ self._assert_artifact_change_unsubscribes(
+ change_callback, configure_test)
def _assert_specification_change_unsubscribes(self, change_callback):
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2015-04-02 01:14:22 +0000
+++ lib/lp/testing/factory.py 2015-04-15 18:39:22 +0000
@@ -1688,6 +1688,19 @@
information_type, registrant, verify_policy=False)
return repository
+ def makeGitSubscription(self, repository=None, person=None,
+ subscribed_by=None):
+ """Create a GitSubscription."""
+ if repository is None:
+ repository = self.makeGitRepository()
+ if person is None:
+ person = self.makePerson()
+ if subscribed_by is None:
+ subscribed_by = person
+ return repository.subscribe(removeSecurityProxy(person),
+ BranchSubscriptionNotificationLevel.NOEMAIL, None,
+ CodeReviewNotificationLevel.NOEMAIL, subscribed_by)
+
def makeGitRefs(self, repository=None, paths=None):
"""Create and return a list of new, arbitrary GitRefs."""
if repository is None:
Follow ups