launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #26352
[Merge] ~pappacena/launchpad:snap-pillar-subscribe-ui into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:snap-pillar-subscribe-ui into launchpad:master with ~pappacena/launchpad:snap-pillar-subscribe-removal-job as a prerequisite.
Commit message:
UI workflow for users to subscribe to snaps
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/398319
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:snap-pillar-subscribe-ui into launchpad:master.
diff --git a/lib/lp/registry/tests/test_personmerge.py b/lib/lp/registry/tests/test_personmerge.py
index d080746..f1a86b6 100644
--- a/lib/lp/registry/tests/test_personmerge.py
+++ b/lib/lp/registry/tests/test_personmerge.py
@@ -680,22 +680,22 @@ class TestMergePeople(TestCaseWithFactory, KarmaTestMixin):
login_admin()
snap.subscribe(duplicate, snap.owner)
self.assertTrue(snap.visibleByUser(duplicate))
- self.assertThat(snap._getSubscription(duplicate), MatchesStructure(
+ self.assertThat(snap.getSubscription(duplicate), MatchesStructure(
snap=Equals(snap),
person=Equals(duplicate)
))
self.assertFalse(snap.visibleByUser(mergee))
- self.assertIsNone(snap._getSubscription(mergee))
+ self.assertIsNone(snap.getSubscription(mergee))
duplicate, mergee = self._do_merge(duplicate, mergee)
self.assertTrue(snap.visibleByUser(mergee))
- self.assertThat(snap._getSubscription(mergee), MatchesStructure(
+ self.assertThat(snap.getSubscription(mergee), MatchesStructure(
snap=Equals(snap),
person=Equals(mergee)
))
self.assertFalse(snap.visibleByUser(duplicate))
- self.assertIsNone(snap._getSubscription(duplicate))
+ self.assertIsNone(snap.getSubscription(duplicate))
def test_merge_moves_oci_recipes(self):
# When person/teams are merged, oci recipes owned by the from
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 35c8b8c..2e4739f 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Security policies for using content objects."""
@@ -214,6 +214,7 @@ from lp.snappy.interfaces.snappyseries import (
ISnappySeries,
ISnappySeriesSet,
)
+from lp.snappy.interfaces.snapsubscription import ISnapSubscription
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
@@ -3298,17 +3299,13 @@ class ViewSnap(AuthorizationBase):
permission = 'launchpad.View'
usedfor = ISnap
- def checkUnauthenticated(self):
- return not self.obj.private
-
def checkAuthenticated(self, user):
- if not self.obj.private:
+ if user.isOwner(self.obj) or user.in_commercial_admin or user.in_admin:
return True
+ return self.obj.visibleByUser(user.person)
- return (
- user.isOwner(self.obj) or
- user.in_commercial_admin or
- user.in_admin)
+ def checkUnauthenticated(self):
+ return self.obj.visibleByUser(None)
class EditSnap(AuthorizationBase):
@@ -3339,6 +3336,30 @@ class AdminSnap(AuthorizationBase):
and EditSnap(self.obj).checkAuthenticated(user))
+class SnapSubscriptionEdit(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = ISnapSubscription
+
+ def checkAuthenticated(self, user):
+ """Is the user able to edit a Snap recipe subscription?
+
+ Any team member can edit a Snap recipe subscription for their
+ team.
+ Launchpad Admins can also edit any Snap recipe subscription.
+ The owner of the subscribed Snap can edit the subscription. If
+ the Snap owner is a team, then members of the team can edit
+ the subscription.
+ """
+ return (user.inTeam(self.obj.snap.owner) or
+ user.inTeam(self.obj.person) or
+ user.inTeam(self.obj.subscribed_by) or
+ user.in_admin)
+
+
+class SnapSubscriptionView(SnapSubscriptionEdit):
+ permission = 'launchpad.View'
+
+
class ViewSnapBuildRequest(DelegatedAuthorization):
permission = 'launchpad.View'
usedfor = ISnapBuildRequest
diff --git a/lib/lp/snappy/browser/configure.zcml b/lib/lp/snappy/browser/configure.zcml
index 9da248a..b11f797 100644
--- a/lib/lp/snappy/browser/configure.zcml
+++ b/lib/lp/snappy/browser/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2020 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2021 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -37,6 +37,45 @@
name="+portlet-privacy"
template="../templates/snap-portlet-privacy.pt"/>
<browser:page
+ for="lp.snappy.interfaces.snap.ISnap"
+ permission="launchpad.View"
+ name="+portlet-subscribers"
+ template="../templates/snap-portlet-subscribers.pt"/>
+ <browser:page
+ for="lp.snappy.interfaces.snap.ISnap"
+ class="lp.snappy.browser.snapsubscription.SnapPortletSubscribersContent"
+ permission="launchpad.View"
+ name="+snap-portlet-subscriber-content"
+ template="../templates/snap-portlet-subscribers-content.pt"/>
+
+ <browser:defaultView
+ for="lp.snappy.interfaces.snapsubscription.ISnapSubscription"
+ name="+index"/>
+ <browser:page
+ for="lp.snappy.interfaces.snapsubscription.ISnapSubscription"
+ class="lp.snappy.browser.snapsubscription.SnapSubscriptionEditView"
+ permission="launchpad.Edit"
+ name="+index"
+ template="../templates/snapsubscription-edit.pt"/>
+ <browser:page
+ for="lp.snappy.interfaces.snap.ISnap"
+ class="lp.snappy.browser.snapsubscription.SnapSubscriptionAddView"
+ permission="launchpad.AnyPerson"
+ name="+subscribe"
+ template="../../app/templates/generic-edit.pt"/>
+ <browser:page
+ for="lp.snappy.interfaces.snap.ISnap"
+ class="lp.snappy.browser.snapsubscription.SnapSubscriptionAddOtherView"
+ permission="launchpad.AnyPerson"
+ name="+addsubscriber"
+ template="../../app/templates/generic-edit.pt"/>
+ <browser:url
+ for="lp.snappy.interfaces.snapsubscription.ISnapSubscription"
+ path_expression="string:+subscription/${person/name}"
+ attribute_to_parent="snap"
+ rootsite="code"/>
+
+ <browser:page
for="lp.code.interfaces.branch.IBranch"
class="lp.snappy.browser.snap.SnapAddView"
permission="launchpad.AnyPerson"
diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
index 7056519..5b388ee 100644
--- a/lib/lp/snappy/browser/snap.py
+++ b/lib/lp/snappy/browser/snap.py
@@ -64,6 +64,7 @@ from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.code.browser.widgets.gitref import GitRefWidget
from lp.code.interfaces.gitref import IGitRef
from lp.registry.enums import VCSType
+from lp.registry.interfaces.person import IPersonSet
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.services.features import getFeatureFlag
from lp.services.propertycache import cachedproperty
@@ -133,6 +134,13 @@ class SnapNavigation(WebhookTargetNavigationMixin, Navigation):
return None
return build
+ @stepthrough("+subscription")
+ def traverse_subscription(self, name):
+ """Traverses to an `ISnapSubscription`."""
+ person = getUtility(IPersonSet).getByName(name)
+ if person is not None:
+ return self.context.getSubscription(person)
+
class SnapBreadcrumb(NameBreadcrumb):
@@ -187,12 +195,31 @@ class SnapContextMenu(ContextMenu):
facet = 'overview'
- links = ('request_builds',)
+ links = ('request_builds', 'add_subscriber', 'subscription')
@enabled_with_permission('launchpad.Edit')
def request_builds(self):
return Link('+request-builds', 'Request builds', icon='add')
+ @enabled_with_permission("launchpad.AnyPerson")
+ def subscription(self):
+ if self.context.hasSubscription(self.user):
+ url = "+subscription/%s" % self.user.name
+ text = "Edit your subscription"
+ icon = "edit"
+ elif self.context.userCanBeSubscribed(self.user):
+ url = "+subscribe"
+ text = "Subscribe yourself"
+ icon = "add"
+ else:
+ return None
+ return Link(url, text, icon=icon)
+
+ @enabled_with_permission("launchpad.Edit")
+ def add_subscriber(self):
+ text = "Subscribe someone else"
+ return Link("+addsubscriber", text, icon="add")
+
class SnapView(LaunchpadView):
"""Default view of a Snap."""
diff --git a/lib/lp/snappy/browser/snapsubscription.py b/lib/lp/snappy/browser/snapsubscription.py
new file mode 100644
index 0000000..84c1597
--- /dev/null
+++ b/lib/lp/snappy/browser/snapsubscription.py
@@ -0,0 +1,173 @@
+# Copyright 2020-2021 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap subscription views."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'SnapPortletSubscribersContent'
+]
+
+from zope.component._api import getUtility
+from zope.formlib.form import action
+
+from lp.app.browser.launchpadform import (
+ LaunchpadEditFormView,
+ LaunchpadFormView,
+ )
+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.snappy.interfaces.snapsubscription import ISnapSubscription
+
+
+class SnapPortletSubscribersContent(LaunchpadView):
+ """View for the contents for the subscribers portlet."""
+
+ def subscriptions(self):
+ """Return a decorated list of Snap recipe 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.
+ subscriptions = list(self.context.subscriptions)
+ person_ids = [sub.person.id for sub in subscriptions]
+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+ person_ids, need_validity=True))
+ if self.user is not None:
+ subscribers = [
+ subscription.person for subscription in subscriptions]
+ precache_permission_for_objects(
+ self.request, "launchpad.LimitedView", subscribers)
+
+ visible_subscriptions = [
+ subscription for subscription in subscriptions
+ if check_permission("launchpad.LimitedView", subscription.person)]
+ return sorted(
+ visible_subscriptions,
+ key=lambda subscription: subscription.person.displayname)
+
+
+class RedirectToSnapMixin:
+ @property
+ def next_url(self):
+ url = canonical_url(self.snap)
+ # If the subscriber can no longer see the Snap recipe, redirect them
+ # away.
+ if not self.snap.visibleByUser(self.user):
+ url = canonical_url(self.snap.project)
+ return url
+
+ cancel_url = next_url
+
+
+class SnapSubscriptionEditView(RedirectToSnapMixin, LaunchpadEditFormView):
+ """The view for editing Snap recipe subscriptions."""
+ schema = ISnapSubscription
+ field_names = []
+
+ @property
+ def page_title(self):
+ return (
+ "Edit subscription to Snap recipe %s" %
+ self.snap.displayname)
+
+ @property
+ def label(self):
+ return (
+ "Edit subscription to Snap recipe for %s" %
+ self.person.displayname)
+
+ def initialize(self):
+ self.snap = self.context.snap
+ self.person = self.context.person
+ super(SnapSubscriptionEditView, self).initialize()
+
+ @action("Unsubscribe", name="unsubscribe")
+ def unsubscribe_action(self, action, data):
+ """Unsubscribe the team from the Snap recipe."""
+ self.snap.unsubscribe(self.person, self.user)
+ self.request.response.addNotification(
+ "%s has been unsubscribed from this Snap recipe."
+ % self.person.displayname)
+
+
+class _SnapSubscriptionCreationView(RedirectToSnapMixin, LaunchpadFormView):
+ """Contains the common functionality of the Add and Edit views."""
+
+ schema = ISnapSubscription
+ field_names = []
+
+ def initialize(self):
+ self.snap = self.context
+ super(_SnapSubscriptionCreationView, self).initialize()
+
+
+class SnapSubscriptionAddView(_SnapSubscriptionCreationView):
+
+ page_title = label = "Subscribe to Snap recipe"
+
+ @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 Snap recipe.")
+ else:
+ self.context.subscribe(self.user, self.user)
+
+ self.request.response.addNotification(
+ "You have subscribed to this Snap recipe.")
+
+
+class SnapSubscriptionAddOtherView(_SnapSubscriptionCreationView):
+ """View used to subscribe someone other than the current user."""
+
+ field_names = ["person"]
+ for_input = True
+
+ # Since we are subscribing other people, the current user
+ # is never considered subscribed.
+ user_is_subscribed = False
+
+ page_title = label = "Subscribe to Snap recipe"
+
+ 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 Snap recipes.")
+
+ @action("Subscribe", name="subscribe_action")
+ def subscribe_action(self, action, data):
+ """Subscribe the specified user to the Snap recipe.
+
+ The user must be a member of a team in order to subscribe that team
+ to the Snap recipe. Launchpad Admins are special and they can
+ subscribe any team.
+ """
+ person = data["person"]
+ subscription = self.context.getSubscription(person)
+ if subscription is None:
+ self.context.subscribe(person, self.user)
+ self.request.response.addNotification(
+ "%s has been subscribed to this Snap recipe." %
+ person.displayname)
+ else:
+ self.request.response.addNotification(
+ "%s was already subscribed to this Snap recipe with." %
+ person.displayname)
diff --git a/lib/lp/snappy/configure.zcml b/lib/lp/snappy/configure.zcml
index a16c664..05529f8 100644
--- a/lib/lp/snappy/configure.zcml
+++ b/lib/lp/snappy/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2021 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -42,6 +42,15 @@
<allow interface="lp.snappy.interfaces.snap.ISnapSet" />
</securedutility>
+ <!-- SnapSubscription -->
+
+ <class class="lp.snappy.model.snapsubscription.SnapSubscription">
+ <allow interface="lp.snappy.interfaces.snapsubscription.ISnapSubscription"/>
+ <require
+ permission="zope.Public"
+ set_schema="lp.snappy.interfaces.snapsubscription.ISnapSubscription"/>
+ </class>
+
<!-- SnapBuildRequest -->
<class class="lp.snappy.model.snap.SnapBuildRequest">
<require
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index 687a605..9da7dc2 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -570,10 +570,25 @@ class ISnapView(Interface):
# Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
value_type=Reference(schema=Interface), readonly=True)))
+ subscriptions = CollectionField(
+ title=_("SnapSubscriptions associated with this repository."),
+ readonly=True,
+ # Really IGitSubscription, patched in _schema_circular_imports.py.
+ value_type=Reference(Interface))
+
subscribers = CollectionField(
- title=_("Persons subscribed to this repository."),
+ title=_("Persons subscribed to this snap recipe."),
readonly=True, value_type=Reference(IPerson))
+ def getSubscription(person):
+ """Returns the person's snap subscription for this snap recipe."""
+
+ def hasSubscription(person):
+ """Is this person subscribed to the snap recipe?"""
+
+ def userCanBeSubscribed(person):
+ """Checks if the given person can be subscribed to this snap recipe."""
+
def visibleByUser(user):
"""Can the specified user see this snap recipe?"""
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index a1000ea..abfb293 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -1098,6 +1098,18 @@ class Snap(Storm, WebhookTargetMixin):
order_by = Desc(SnapBuild.id)
return self._getBuilds(filter_term, order_by)
+ @property
+ def subscriptions(self):
+ return Store.of(self).find(
+ SnapSubscription, SnapSubscription.snap == self)
+
+ @property
+ def subscribers(self):
+ return Store.of(self).find(
+ Person,
+ SnapSubscription.person_id == Person.id,
+ SnapSubscription.snap == self)
+
def visibleByUser(self, user):
"""See `IGitRepository`."""
if not self.private:
@@ -1115,7 +1127,11 @@ class Snap(Storm, WebhookTargetMixin):
Snap.id == self.id,
visibility_clause).is_empty()
- def _getSubscription(self, person):
+ def hasSubscription(self, person):
+ """See `ISnap`."""
+ return self.getSubscription(person) is not None
+
+ def getSubscription(self, person):
"""Returns person's subscription to this snap recipe, or None if no
subscription is available.
"""
@@ -1126,7 +1142,7 @@ class Snap(Storm, WebhookTargetMixin):
SnapSubscription.person == person,
SnapSubscription.snap == self).one()
- def _userCanBeSubscribed(self, person):
+ def userCanBeSubscribed(self, person):
"""Checks if the given person can subscribe to this snap recipe."""
return not (
self.private and
@@ -1142,11 +1158,11 @@ class Snap(Storm, WebhookTargetMixin):
def subscribe(self, person, subscribed_by):
"""See `ISnap`."""
- if not self._userCanBeSubscribed(person):
+ if not self.userCanBeSubscribed(person):
raise SubscriptionPrivacyViolation(
"Open and delegated teams cannot be subscribed to private "
"snap recipes.")
- subscription = self._getSubscription(person)
+ subscription = self.getSubscription(person)
if subscription is None:
subscription = SnapSubscription(
person=person, snap=self, subscribed_by=subscribed_by)
@@ -1162,7 +1178,7 @@ class Snap(Storm, WebhookTargetMixin):
service = getUtility(IService, "sharing")
service.revokeAccessGrants(
self.pillar, person, unsubscribed_by, snaps=[self])
- subscription = self._getSubscription(person)
+ subscription = self.getSubscription(person)
if (not ignore_permissions
and not subscription.canBeUnsubscribedByUser(unsubscribed_by)):
raise UserCannotUnsubscribePerson(
diff --git a/lib/lp/snappy/templates/snap-index.pt b/lib/lp/snappy/templates/snap-index.pt
index 7d3083e..09eea5c 100644
--- a/lib/lp/snappy/templates/snap-index.pt
+++ b/lib/lp/snappy/templates/snap-index.pt
@@ -32,6 +32,7 @@
<metal:side fill-slot="side">
<div tal:replace="structure context/@@+portlet-privacy" />
<div tal:replace="structure context/@@+global-actions"/>
+ <tal:subscribers replace="structure context/@@+portlet-subscribers" />
</metal:side>
<metal:heading fill-slot="heading">
diff --git a/lib/lp/snappy/templates/snap-portlet-subscribers-content.pt b/lib/lp/snappy/templates/snap-portlet-subscribers-content.pt
new file mode 100644
index 0000000..05495ff
--- /dev/null
+++ b/lib/lp/snappy/templates/snap-portlet-subscribers-content.pt
@@ -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 snap-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>
diff --git a/lib/lp/snappy/templates/snap-portlet-subscribers.pt b/lib/lp/snappy/templates/snap-portlet-subscribers.pt
new file mode 100644
index 0000000..5f0dd60
--- /dev/null
+++ b/lib/lp/snappy/templates/snap-portlet-subscribers.pt
@@ -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="snap-subscribers-outer">
+ <div tal:replace="structure context/@@+snap-portlet-subscriber-content" />
+ </div>
+ </div>
+</div>
diff --git a/lib/lp/snappy/templates/snapsubscription-edit.pt b/lib/lp/snappy/templates/snapsubscription-edit.pt
new file mode 100644
index 0000000..f2d9d8c
--- /dev/null
+++ b/lib/lp/snappy/templates/snapsubscription-edit.pt
@@ -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 snap recipe it will no longer show up on
+ your personal pages.
+ </p>
+ </metal:extra>
+ </div>
+
+</div>
+
+</body>
+</html>
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index d089a46..0eb242f 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -1350,9 +1350,6 @@ class TestSnapVisibility(TestCaseWithFactory):
AccessArtifactGrant.abstract_artifact_id == AccessArtifact.id,
*conditions)
- def getSnapSubscription(self, snap, person):
- return removeSecurityProxy(snap)._getSubscription(person)
-
def test_only_owner_can_grant_access(self):
owner = self.factory.makePerson()
pillar = self.factory.makeProduct(owner=owner)
@@ -1389,20 +1386,18 @@ class TestSnapVisibility(TestCaseWithFactory):
with person_logged_in(owner):
self.assertFalse(snap.visibleByUser(person))
snap.subscribe(person, snap.owner)
- self.assertThat(
- self.getSnapSubscription(snap, person),
- MatchesStructure(
- person=Equals(person),
- snap=Equals(snap),
- subscribed_by=Equals(snap.owner),
- date_created=IsInstance(datetime)))
+ self.assertThat(snap.getSubscription(person), MatchesStructure(
+ person=Equals(person),
+ snap=Equals(snap),
+ subscribed_by=Equals(snap.owner),
+ date_created=IsInstance(datetime)))
# Calling again should be a no-op.
snap.subscribe(person, snap.owner)
self.assertTrue(snap.visibleByUser(person))
snap.unsubscribe(person, snap.owner)
self.assertFalse(snap.visibleByUser(person))
- self.assertIsNone(self.getSnapSubscription(snap, person))
+ self.assertIsNone(snap.getSubscription(person))
def test_reconcile_set_public(self):
owner = self.factory.makePerson()
@@ -1413,7 +1408,7 @@ class TestSnapVisibility(TestCaseWithFactory):
snap.subscribe(another_user, snap.owner)
self.assertEqual(1, self.getSnapGrants(snap, another_user).count())
self.assertThat(
- self.getSnapSubscription(snap, another_user),
+ snap.getSubscription(another_user),
MatchesStructure(
person=Equals(another_user),
snap=Equals(snap),
@@ -1423,7 +1418,7 @@ class TestSnapVisibility(TestCaseWithFactory):
snap.information_type = InformationType.PUBLIC
self.assertEqual(0, self.getSnapGrants(snap, another_user).count())
self.assertThat(
- self.getSnapSubscription(snap, another_user),
+ snap.getSubscription(another_user),
MatchesStructure(
person=Equals(another_user),
snap=Equals(snap),
@@ -1448,7 +1443,7 @@ class TestSnapVisibility(TestCaseWithFactory):
self.assertTrue(snap.visibleByUser(another_person))
self.assertEqual(1, self.getSnapGrants(snap).count())
self.assertThat(
- self.getSnapSubscription(snap, another_person),
+ snap.getSubscription(another_person),
MatchesStructure(
person=Equals(another_person),
snap=Equals(snap),
@@ -1459,7 +1454,7 @@ class TestSnapVisibility(TestCaseWithFactory):
self.assertTrue(snap.visibleByUser(another_person))
self.assertEqual(1, self.getSnapGrants(snap).count())
self.assertThat(
- self.getSnapSubscription(snap, another_person),
+ snap.getSubscription(another_person),
MatchesStructure(
person=Equals(another_person),
snap=Equals(snap),
Follow ups