← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~cjwatson/launchpad/git-subscriptions-browser into lp:launchpad

 

Review: Approve code

The permissions are a bit dodgy, but probably not fatal.

Diff comments:

> === modified file 'lib/lp/code/browser/configure.zcml'
> --- lib/lp/code/browser/configure.zcml	2015-04-19 12:56:32 +0000
> +++ lib/lp/code/browser/configure.zcml	2015-04-21 10:09:18 +0000
> @@ -768,6 +768,35 @@
>              name="++repository-management"
>              template="../templates/gitrepository-management.pt"/>
>      </browser:pages>
> +    <browser:page
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        permission="zope.Public"
> +        name="+portlet-subscribers"
> +        template="../templates/gitrepository-portlet-subscribers.pt"/>
> +    <browser:page
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        class="lp.code.browser.gitsubscription.GitRepositoryPortletSubscribersContent"
> +        permission="zope.Public"
> +        name="+repository-portlet-subscriber-content"
> +        template="../templates/gitrepository-portlet-subscribers-content.pt"/>
> +    <browser:page
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        class="lp.code.browser.gitsubscription.GitSubscriptionAddView"
> +        permission="launchpad.AnyPerson"
> +        name="+subscribe"
> +        template="../../app/templates/generic-edit.pt"/>
> +    <browser:page
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        class="lp.code.browser.gitsubscription.GitSubscriptionAddOtherView"
> +        permission="launchpad.AnyPerson"
> +        name="+addsubscriber"
> +        template="../../app/templates/generic-edit.pt"/>
> +    <browser:page
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        class="lp.code.browser.gitsubscription.GitSubscriptionEditOwnView"
> +        permission="launchpad.AnyPerson"
> +        name="+edit-subscription"
> +        template="../templates/gitrepository-edit-subscription.pt"/>

All of these should really require launchpad.View, but launchpad.AnyPerson makes it a bit awkward.

>      <adapter
>          provides="lp.services.webapp.interfaces.IBreadcrumb"
>          for="lp.code.interfaces.gitrepository.IGitRepository"
> @@ -804,6 +833,21 @@
>          name="+ref-listing"
>          template="../templates/gitref-listing.pt"/>
>  
> +    <browser:defaultView
> +        for="lp.code.interfaces.gitsubscription.IGitSubscription"
> +        name="+index"/>
> +    <browser:page
> +        for="lp.code.interfaces.gitsubscription.IGitSubscription"
> +        class="lp.code.browser.gitsubscription.GitSubscriptionEditView"
> +        permission="launchpad.Edit"
> +        name="+index"
> +        template="../templates/gitsubscription-edit.pt"/>
> +    <browser:url
> +        for="lp.code.interfaces.gitsubscription.IGitSubscription"
> +        path_expression="string:+subscription/${person/name}"
> +        attribute_to_parent="repository"
> +        rootsite="code"/>
> +
>      <browser:menus
>          classes="ProductBranchesMenu"
>          module="lp.code.browser.branchlisting"/>
> 
> === modified file 'lib/lp/code/browser/gitrepository.py'
> --- lib/lp/code/browser/gitrepository.py	2015-03-24 13:10:52 +0000
> +++ lib/lp/code/browser/gitrepository.py	2015-04-21 10:09:18 +0000
> @@ -23,6 +23,7 @@
>  from lp.services.config import config
>  from lp.services.webapp import (
>      ContextMenu,
> +    enabled_with_permission,
>      LaunchpadView,
>      Link,
>      Navigation,
> @@ -83,7 +84,24 @@
>  
>      usedfor = IGitRepository
>      facet = "branches"
> -    links = ["source"]
> +    links = ["add_subscriber", "source", "subscription"]
> +
> +    @enabled_with_permission("launchpad.AnyPerson")
> +    def subscription(self):
> +        if self.context.hasSubscription(self.user):
> +            url = "+edit-subscription"
> +            text = "Edit your subscription"
> +            icon = "edit"
> +        else:
> +            url = "+subscribe"
> +            text = "Subscribe yourself"
> +            icon = "add"
> +        return Link(url, text, icon=icon)
> +
> +    @enabled_with_permission("launchpad.AnyPerson")
> +    def add_subscriber(self):
> +        text = "Subscribe someone else"
> +        return Link("+addsubscriber", text, icon="add")
>  
>      def source(self):
>          """Return a link to the branch's browsing interface."""
> 
> === added file 'lib/lp/code/browser/gitsubscription.py'
> --- lib/lp/code/browser/gitsubscription.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/browser/gitsubscription.py	2015-04-21 10:09:18 +0000
> @@ -0,0 +1,293 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +__metaclass__ = type
> +
> +__all__ = [
> +    'GitRepositoryPortletSubscribersContent',
> +    'GitSubscriptionAddOtherView',
> +    'GitSubscriptionAddView',
> +    'GitSubscriptionEditOwnView',
> +    'GitSubscriptionEditView',
> +    ]
> +
> +from zope.component import getUtility
> +
> +from lp.app.browser.launchpadform import (
> +    action,
> +    LaunchpadEditFormView,
> +    LaunchpadFormView,
> +    )
> +from lp.app.interfaces.services import IService
> +from lp.code.enums import BranchSubscriptionNotificationLevel
> +from lp.code.interfaces.gitsubscription import IGitSubscription
> +from lp.registry.interfaces.person import IPersonSet
> +from lp.services.webapp import (
> +    canonical_url,
> +    LaunchpadView,
> +    )
> +from lp.services.webapp.authorization import (
> +    check_permission,
> +    precache_permission_for_objects,
> +    )
> +from lp.services.webapp.escaping import structured
> +
> +
> +class GitRepositoryPortletSubscribersContent(LaunchpadView):
> +    """View for the contents for the subscribers portlet."""
> +
> +    def subscriptions(self):
> +        """Return a decorated list of Git repository subscriptions."""
> +
> +        # Cache permissions so private subscribers can be rendered.
> +        # The security adaptor will do the job also but we don't want or
> +        # need the expense of running several complex SQL queries.
> +        person_ids = [sub.person_id for sub in self.context.subscriptions]
> +        list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
> +            person_ids, need_validity=True))
> +        if self.user is not None:
> +            subscribers = [
> +                subscription.person
> +                for subscription in self.context.subscriptions]
> +            precache_permission_for_objects(
> +                self.request, "launchpad.LimitedView", subscribers)
> +
> +        visible_subscriptions = [
> +            subscription for subscription in self.context.subscriptions
> +            if check_permission("launchpad.LimitedView", subscription.person)]
> +        return sorted(
> +            visible_subscriptions,
> +            key=lambda subscription: subscription.person.displayname)
> +
> +
> +class _GitSubscriptionView(LaunchpadFormView):
> +    """Contains the common functionality of the Add and Edit views."""
> +
> +    schema = IGitSubscription
> +    field_names = ['notification_level', 'max_diff_lines', 'review_level']
> +
> +    LEVELS_REQUIRING_LINES_SPECIFICATION = (
> +        BranchSubscriptionNotificationLevel.DIFFSONLY,
> +        BranchSubscriptionNotificationLevel.FULL)
> +
> +    @property
> +    def user_is_subscribed(self):
> +        # Since it is technically possible to get to this page when
> +        # the user is not subscribed by hacking the URL, we should
> +        # handle the case nicely.
> +        return self.context.getSubscription(self.user) is not None
> +
> +    @property
> +    def next_url(self):
> +        return canonical_url(self.context)
> +
> +    cancel_url = next_url
> +
> +    def add_notification_message(self, initial, notification_level,
> +                                 max_diff_lines, review_level):
> +        if notification_level in self.LEVELS_REQUIRING_LINES_SPECIFICATION:
> +            lines_message = "<li>%s</li>" % max_diff_lines.description
> +        else:
> +            lines_message = ""
> +
> +        format_str = "%%s<ul><li>%%s</li>%s<li>%%s</li></ul>" % lines_message
> +        message = structured(
> +            format_str, initial, notification_level.description,
> +            review_level.description)
> +        self.request.response.addNotification(message)
> +
> +    def optional_max_diff_lines(self, notification_level, max_diff_lines):
> +        if notification_level in self.LEVELS_REQUIRING_LINES_SPECIFICATION:
> +            return max_diff_lines
> +        else:
> +            return None
> +
> +
> +class GitSubscriptionAddView(_GitSubscriptionView):
> +
> +    subscribing_self = True
> +
> +    page_title = label = "Subscribe to repository"
> +
> +    @action("Subscribe")
> +    def subscribe(self, action, data):
> +        # To catch the stale post problem, check that the user is not
> +        # subscribed before continuing.
> +        if self.context.hasSubscription(self.user):
> +            self.request.response.addNotification(
> +                "You are already subscribed to this repository.")
> +        else:
> +            notification_level = data["notification_level"]
> +            max_diff_lines = self.optional_max_diff_lines(
> +                notification_level, data["max_diff_lines"])
> +            review_level = data["review_level"]
> +
> +            self.context.subscribe(
> +                self.user, notification_level, max_diff_lines, review_level,
> +                self.user)
> +
> +            self.add_notification_message(
> +                "You have subscribed to this repository with: ",
> +                notification_level, max_diff_lines, review_level)
> +
> +
> +class GitSubscriptionEditOwnView(_GitSubscriptionView):
> +
> +    @property
> +    def label(self):
> +        return "Edit subscription to repository"
> +
> +    @property
> +    def page_title(self):
> +        return "Edit subscription to repository %s" % self.context.displayname
> +
> +    @property
> +    def initial_values(self):
> +        subscription = self.context.getSubscription(self.user)
> +        if subscription is None:
> +            # This is the case of URL hacking or stale page.
> +            return {}
> +        else:
> +            return {"notification_level": subscription.notification_level,
> +                    "max_diff_lines": subscription.max_diff_lines,
> +                    "review_level": subscription.review_level}
> +
> +    @action("Change")
> +    def change_details(self, action, data):
> +        # Be proactive in the checking to catch the stale post problem.
> +        if self.context.hasSubscription(self.user):
> +            subscription = self.context.getSubscription(self.user)
> +            subscription.notification_level = data["notification_level"]
> +            subscription.max_diff_lines = self.optional_max_diff_lines(
> +                subscription.notification_level,
> +                data["max_diff_lines"])
> +            subscription.review_level = data["review_level"]
> +
> +            self.add_notification_message(
> +                "Subscription updated to: ",
> +                subscription.notification_level,
> +                subscription.max_diff_lines,
> +                subscription.review_level)
> +        else:
> +            self.request.response.addNotification(
> +                "You are not subscribed to this repository.")
> +
> +    @action("Unsubscribe")
> +    def unsubscribe(self, action, data):
> +        # Be proactive in the checking to catch the stale post problem.
> +        if self.context.hasSubscription(self.user):
> +            self.context.unsubscribe(self.user, self.user)
> +            self.request.response.addNotification(
> +                "You have unsubscribed from this repository.")
> +        else:
> +            self.request.response.addNotification(
> +                "You are not subscribed to this repository.")
> +
> +
> +class GitSubscriptionAddOtherView(_GitSubscriptionView):
> +    """View used to subscribe someone other than the current user."""
> +
> +    field_names = [
> +        "person", "notification_level", "max_diff_lines", "review_level"]
> +    for_input = True
> +
> +    # Since we are subscribing other people, the current user
> +    # is never considered subscribed.
> +    user_is_subscribed = False
> +    subscribing_self = False
> +
> +    page_title = label = "Subscribe to repository"
> +
> +    def validate(self, data):
> +        if "person" in data:
> +            person = data["person"]
> +            subscription = self.context.getSubscription(person)
> +            if subscription is None and not self.context.userCanBeSubscribed(
> +                person):
> +                self.setFieldError(
> +                    "person",
> +                    "Open and delegated teams cannot be subscribed to "
> +                    "private repositories.")
> +
> +    @action("Subscribe", name="subscribe_action")
> +    def subscribe_action(self, action, data):
> +        """Subscribe the specified user to the repository.
> +
> +        The user must be a member of a team in order to subscribe that team
> +        to the repository.  Launchpad Admins are special and they can
> +        subscribe any team.
> +        """
> +        notification_level = data["notification_level"]
> +        max_diff_lines = self.optional_max_diff_lines(
> +            notification_level, data["max_diff_lines"])
> +        review_level = data["review_level"]
> +        person = data["person"]
> +        subscription = self.context.getSubscription(person)
> +        if subscription is None:
> +            self.context.subscribe(
> +                person, notification_level, max_diff_lines, review_level,
> +                self.user)
> +            self.add_notification_message(
> +                "%s has been subscribed to this repository with: "
> +                % person.displayname, notification_level, max_diff_lines,
> +                review_level)
> +        else:
> +            self.add_notification_message(
> +                "%s was already subscribed to this repository with: "
> +                % person.displayname,
> +                subscription.notification_level, subscription.max_diff_lines,
> +                review_level)
> +
> +
> +class GitSubscriptionEditView(LaunchpadEditFormView):
> +    """The view for editing repository subscriptions.
> +
> +    Used when traversed to the repository subscription itself rather than
> +    through the repository action item to edit the user's own subscription.
> +    This is the only current way to edit a team repository subscription.
> +    """
> +    schema = IGitSubscription
> +    field_names = ["notification_level", "max_diff_lines", "review_level"]
> +
> +    @property
> +    def page_title(self):
> +        return (
> +            "Edit subscription to repository %s" % self.repository.displayname)
> +
> +    @property
> +    def label(self):
> +        return (
> +            "Edit subscription to repository for %s" % self.person.displayname)
> +
> +    def initialize(self):
> +        self.repository = self.context.repository
> +        self.person = self.context.person
> +        super(GitSubscriptionEditView, self).initialize()
> +
> +    @action("Change", name="change")
> +    def change_action(self, action, data):
> +        """Update the repository subscription."""
> +        self.updateContextFromData(data)
> +
> +    @action("Unsubscribe", name="unsubscribe")
> +    def unsubscribe_action(self, action, data):
> +        """Unsubscribe the team from the repository."""
> +        self.repository.unsubscribe(self.person, self.user)
> +        self.request.response.addNotification(
> +            "%s has been unsubscribed from this repository."
> +            % self.person.displayname)
> +
> +    @property
> +    def next_url(self):
> +        url = canonical_url(self.repository)
> +        # If the subscriber can no longer see the repository, redirect them
> +        # away.
> +        service = getUtility(IService, "sharing")
> +        _, _, repositories, _ = service.getVisibleArtifacts(
> +            self.person, gitrepositories=[self.repository],
> +            ignore_permissions=True)
> +        if not repositories:
> +            url = canonical_url(self.repository.target)
> +        return url
> +
> +    cancel_url = next_url
> 
> === added file 'lib/lp/code/browser/tests/test_gitsubscription.py'
> --- lib/lp/code/browser/tests/test_gitsubscription.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/browser/tests/test_gitsubscription.py	2015-04-21 10:09:18 +0000
> @@ -0,0 +1,58 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Unit tests for GitSubscriptions."""
> +
> +__metaclass__ = type
> +
> +from lp.app.enums import InformationType
> +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
> +from lp.testing.views import create_initialized_view
> +
> +
> +class TestGitSubscriptionAddOtherView(TestCaseWithFactory):
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def setUp(self):
> +        super(TestGitSubscriptionAddOtherView, self).setUp()
> +        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
> +
> +    def test_cannot_subscribe_open_team_to_private_repository(self):
> +        owner = self.factory.makePerson()
> +        repository = self.factory.makeGitRepository(
> +            information_type=InformationType.USERDATA, owner=owner)
> +        team = self.factory.makeTeam()
> +        form = {
> +            'field.person': team.name,
> +            'field.notification_level': 'NOEMAIL',
> +            'field.max_diff_lines': 'NODIFF',
> +            'field.review_level': 'NOEMAIL',
> +            'field.actions.subscribe_action': 'Subscribe'}
> +        with person_logged_in(owner):
> +            view = create_initialized_view(
> +                repository, '+addsubscriber', pricipal=owner, form=form)
> +            self.assertContentEqual(
> +                ['Open and delegated teams cannot be subscribed to private '
> +                 'repositories.'], view.errors)
> +
> +    def test_can_subscribe_open_team_to_public_repository(self):
> +        owner = self.factory.makePerson()
> +        repository = self.factory.makeGitRepository(owner=owner)
> +        team = self.factory.makeTeam()
> +        form = {
> +            'field.person': team.name,
> +            'field.notification_level': 'NOEMAIL',
> +            'field.max_diff_lines': 'NODIFF',
> +            'field.review_level': 'NOEMAIL',
> +            'field.actions.subscribe_action': 'Subscribe'}
> +        with person_logged_in(owner):
> +            view = create_initialized_view(
> +                repository, '+addsubscriber', pricipal=owner, form=form)
> +            self.assertContentEqual([], view.errors)
> 
> === modified file 'lib/lp/code/interfaces/gitsubscription.py'
> --- lib/lp/code/interfaces/gitsubscription.py	2015-04-15 18:34:25 +0000
> +++ lib/lp/code/interfaces/gitsubscription.py	2015-04-21 10:09:18 +0000
> @@ -43,7 +43,7 @@
>      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_id = Int(title=_("Person ID"), required=True, readonly=True)
>      person = exported(
>          PersonChoice(
>              title=_("Person"), required=True, vocabulary="ValidPersonOrTeam",
> 
> === modified file 'lib/lp/code/model/gitrepository.py'
> --- lib/lp/code/model/gitrepository.py	2015-04-17 00:01:15 +0000
> +++ lib/lp/code/model/gitrepository.py	2015-04-21 10:09:18 +0000
> @@ -139,8 +139,9 @@
>      This method is registered as a subscriber to `IObjectModifiedEvent`
>      events on Git repositories.
>      """
> -    repository.date_last_modified = UTC_NOW
> -    send_git_repository_modified_notifications(repository, event)
> +    if event.edited_fields:
> +        repository.date_last_modified = UTC_NOW
> +        send_git_repository_modified_notifications(repository, event)
>  
>  
>  class GitRepository(StormBase, GitIdentityMixin):
> 
> === modified file 'lib/lp/code/model/tests/test_gitrepository.py'
> --- lib/lp/code/model/tests/test_gitrepository.py	2015-04-17 00:01:15 +0000
> +++ lib/lp/code/model/tests/test_gitrepository.py	2015-04-21 10:09:18 +0000
> @@ -13,6 +13,7 @@
>  from lazr.lifecycle.event import ObjectModifiedEvent
>  import pytz
>  from testtools.matchers import (
> +    EndsWith,
>      MatchesSetwise,
>      MatchesStructure,
>      )
> @@ -26,7 +27,12 @@
>      PUBLIC_INFORMATION_TYPES,
>      )
>  from lp.app.interfaces.launchpad import ILaunchpadCelebrities
> -from lp.code.enums import GitObjectType
> +from lp.code.enums import (
> +    BranchSubscriptionDiffSize,
> +    BranchSubscriptionNotificationLevel,
> +    CodeReviewNotificationLevel,
> +    GitObjectType,
> +    )
>  from lp.code.errors import (
>      GitFeatureDisabled,
>      GitRepositoryCreatorNotMemberOfOwnerTeam,
> @@ -1423,3 +1429,107 @@
>          self.assertEqual(401, response.status)
>          with person_logged_in(ANONYMOUS):
>              self.assertEqual(owner_db, repository_db.owner)
> +
> +    def test_subscribe(self):
> +        # A user can subscribe to a repository.
> +        repository_db = self.factory.makeGitRepository()
> +        subscriber_db = self.factory.makePerson()
> +        webservice = webservice_for_person(
> +            subscriber_db, permission=OAuthPermission.WRITE_PUBLIC)
> +        webservice.default_api_version = "devel"
> +        with person_logged_in(ANONYMOUS):
> +            repository_url = api_url(repository_db)
> +            subscriber_url = api_url(subscriber_db)
> +        response = webservice.named_post(
> +            repository_url, "subscribe", person=subscriber_url,
> +            notification_level=u"Branch attribute notifications only",
> +            max_diff_lines=u"Don't send diffs", code_review_level=u"No email")
> +        self.assertEqual(200, response.status)
> +        with person_logged_in(ANONYMOUS):
> +            subscription_db = repository_db.getSubscription(subscriber_db)
> +            self.assertIsNotNone(subscription_db)
> +            self.assertThat(
> +                response.jsonBody()["self_link"],
> +                EndsWith(api_url(subscription_db)))
> +
> +    def _makeSubscription(self, repository, subscriber):
> +        with person_logged_in(subscriber):
> +            return repository.subscribe(
> +                person=subscriber,
> +                notification_level=(
> +                    BranchSubscriptionNotificationLevel.ATTRIBUTEONLY),
> +                max_diff_lines=BranchSubscriptionDiffSize.NODIFF,
> +                code_review_level=CodeReviewNotificationLevel.NOEMAIL,
> +                subscribed_by=subscriber)
> +
> +    def test_getSubscription(self):
> +        # It is possible to get a single subscription via the webservice.
> +        repository_db = self.factory.makeGitRepository()
> +        subscriber_db = self.factory.makePerson()
> +        subscription_db = self._makeSubscription(repository_db, subscriber_db)
> +        with person_logged_in(subscriber_db):
> +            repository_url = api_url(repository_db)
> +            subscriber_url = api_url(subscriber_db)
> +            subscription_url = api_url(subscription_db)
> +        webservice = webservice_for_person(
> +            subscriber_db, permission=OAuthPermission.WRITE_PUBLIC)
> +        webservice.default_api_version = "devel"
> +        response = webservice.named_get(
> +            repository_url, "getSubscription", person=subscriber_url)
> +        self.assertEqual(200, response.status)
> +        self.assertThat(
> +            response.jsonBody()["self_link"], EndsWith(subscription_url))
> +
> +    def test_edit_subscription(self):
> +        # An existing subscription can be edited via the webservice, by
> +        # subscribing the same person again with different details.
> +        repository_db = self.factory.makeGitRepository()
> +        subscriber_db = self.factory.makePerson()
> +        self._makeSubscription(repository_db, subscriber_db)
> +        with person_logged_in(subscriber_db):
> +            repository_url = api_url(repository_db)
> +            subscriber_url = api_url(subscriber_db)
> +        webservice = webservice_for_person(
> +            subscriber_db, permission=OAuthPermission.WRITE_PUBLIC)
> +        webservice.default_api_version = "devel"
> +        response = webservice.named_post(
> +            repository_url, "subscribe", person=subscriber_url,
> +            notification_level=u"No email",
> +            max_diff_lines=u"Send entire diff",
> +            code_review_level=u"Status changes only")
> +        self.assertEqual(200, response.status)
> +        with person_logged_in(subscriber_db):
> +            self.assertThat(
> +                repository_db.getSubscription(subscriber_db),
> +                MatchesStructure.byEquality(
> +                    person=subscriber_db,
> +                    notification_level=(
> +                        BranchSubscriptionNotificationLevel.NOEMAIL),
> +                    max_diff_lines=BranchSubscriptionDiffSize.WHOLEDIFF,
> +                    review_level=CodeReviewNotificationLevel.STATUS,
> +                    ))
> +        repository = webservice.get(repository_url).jsonBody()
> +        subscribers = webservice.get(
> +            repository["subscribers_collection_link"]).jsonBody()
> +        self.assertEqual(2, len(subscribers["entries"]))
> +        with person_logged_in(subscriber_db):
> +            self.assertContentEqual(
> +                [repository_db.owner.name, subscriber_db.name],
> +                [subscriber["name"] for subscriber in subscribers["entries"]])
> +
> +    def test_unsubscribe(self):
> +        # It is possible to unsubscribe via the webservice.
> +        repository_db = self.factory.makeGitRepository()
> +        subscriber_db = self.factory.makePerson()
> +        self._makeSubscription(repository_db, subscriber_db)
> +        with person_logged_in(subscriber_db):
> +            repository_url = api_url(repository_db)
> +            subscriber_url = api_url(subscriber_db)
> +        webservice = webservice_for_person(
> +            subscriber_db, permission=OAuthPermission.WRITE_PUBLIC)
> +        webservice.default_api_version = "devel"
> +        response = webservice.named_post(
> +            repository_url, "unsubscribe", person=subscriber_url)
> +        self.assertEqual(200, response.status)
> +        with person_logged_in(subscriber_db):
> +            self.assertNotIn(subscriber_db, repository_db.subscribers)
> 
> === added file 'lib/lp/code/templates/gitrepository-edit-subscription.pt'
> --- lib/lp/code/templates/gitrepository-edit-subscription.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitrepository-edit-subscription.pt	2015-04-21 10:09:18 +0000
> @@ -0,0 +1,45 @@
> +<html
> +  xmlns="http://www.w3.org/1999/xhtml";
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +  metal:use-macro="view/macro:page/main_only"
> +  i18n:domain="launchpad">
> +
> +  <body>
> +
> +<div metal:fill-slot="main">
> +
> +<tal:subscribed condition="view/user_is_subscribed">
> +
> +  <div metal:use-macro="context/@@launchpad_form/form">
> +    <metal:extra fill-slot="extra_info">
> +      <p class="documentDescription">
> +        If you unsubscribe from a repository it will no longer show up on
> +        your personal pages.
> +      </p>
> +    </metal:extra>
> +  </div>
> +
> +</tal:subscribed>
> +
> +<tal:not_subscribed condition="not: view/user_is_subscribed">
> +
> +  <tal:comment condition="nothing">
> +    This occurs if the user is hacking the URLs,
> +    and should never be linked to from a valid page.
> +
> +    It could occur from a stale page as well if the user
> +    is using a tabbed browser and hasn't refreshed the page.
> +  </tal:comment>
> +
> +  <p class="documentDescription">
> +    You are not currently subscribed to this repository.
> +  </p>
> +
> +</tal:not_subscribed>
> +
> +</div>
> +
> +</body>
> +</html>
> 
> === modified file 'lib/lp/code/templates/gitrepository-index.pt'
> --- lib/lp/code/templates/gitrepository-index.pt	2015-03-20 14:54:23 +0000
> +++ lib/lp/code/templates/gitrepository-index.pt	2015-04-21 10:09:18 +0000
> @@ -19,6 +19,7 @@
>  
>  <metal:side fill-slot="side">
>    <div tal:replace="structure context/@@+global-actions" />
> +  <tal:subscribers replace="structure context/@@+portlet-subscribers" />
>  </metal:side>
>  
>  <tal:registering metal:fill-slot="registering">
> 
> === added file 'lib/lp/code/templates/gitrepository-portlet-subscribers-content.pt'
> --- lib/lp/code/templates/gitrepository-portlet-subscribers-content.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitrepository-portlet-subscribers-content.pt	2015-04-21 10:09:18 +0000
> @@ -0,0 +1,31 @@
> +<div
> +  tal:omit-tag=""
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";>
> +  <div class="section repository-subscribers">
> +    <div
> +      tal:condition="view/subscriptions"
> +      tal:repeat="subscription view/subscriptions"
> +      tal:attributes="id string:subscriber-${subscription/person/name}">
> +        <a tal:condition="subscription/person/name|nothing"
> +           tal:attributes="href subscription/person/fmt:url">
> +
> +          <tal:block replace="structure subscription/person/fmt:icon" />
> +          <tal:block replace="subscription/person/fmt:displayname/fmt:shorten/20" />
> +        </a>
> +
> +        <a tal:condition="subscription/required:launchpad.Edit"
> +           tal:attributes="
> +             href subscription/fmt:url;
> +             title string:Edit subscription ${subscription/person/fmt:displayname};
> +             id string:editsubscription-${subscription/person/name}">
> +          <img class="editsub-icon" src="/@@/edit"
> +            tal:attributes="id string:editsubscription-icon-${subscription/person/name}" />
> +        </a>
> +    </div>
> +    <div id="none-subscribers" tal:condition="not:view/subscriptions">
> +      No subscribers.
> +    </div>
> +  </div>
> +</div>
> 
> === added file 'lib/lp/code/templates/gitrepository-portlet-subscribers.pt'
> --- lib/lp/code/templates/gitrepository-portlet-subscribers.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitrepository-portlet-subscribers.pt	2015-04-21 10:09:18 +0000
> @@ -0,0 +1,29 @@
> +<div
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +  class="portlet" id="portlet-subscribers">
> +  <div tal:define="context_menu view/context/menu:context">
> +    <div>
> +      <div class="section">
> +        <div
> +          tal:define="link context_menu/subscription"
> +          tal:condition="link/enabled"
> +          id="selfsubscriptioncontainer">
> +          <a class="sprite add subscribe-self"
> +             tal:attributes="href link/url"
> +             tal:content="link/text" />
> +        </div>
> +        <div
> +          tal:define="link context_menu/add_subscriber"
> +          tal:condition="link/enabled"
> +          tal:content="structure link/render" />
> +      </div>
> +    </div>
> +
> +    <h2>Subscribers</h2>
> +    <div id="repository-subscribers-outer">
> +      <div tal:replace="structure context/@@+repository-portlet-subscriber-content" />
> +    </div>
> +  </div>
> +</div>
> 
> === added file 'lib/lp/code/templates/gitsubscription-edit.pt'
> --- lib/lp/code/templates/gitsubscription-edit.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitsubscription-edit.pt	2015-04-21 10:09:18 +0000
> @@ -0,0 +1,25 @@
> +<html
> +  xmlns="http://www.w3.org/1999/xhtml";
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +  metal:use-macro="view/macro:page/main_only"
> +  i18n:domain="launchpad"
> +>
> +  <body>
> +
> +<div metal:fill-slot="main">
> +
> +  <div metal:use-macro="context/@@launchpad_form/form" >
> +    <metal:extra fill-slot="extra_info">
> +      <p class="documentDescription">
> +        If you unsubscribe from a repository it will no longer show up on
> +        your personal pages.
> +      </p>
> +    </metal:extra>
> +  </div>
> +
> +</div>
> +
> +</body>
> +</html>
> 


-- 
https://code.launchpad.net/~cjwatson/launchpad/git-subscriptions-browser/+merge/256900
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References