launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18326
[Merge] lp:~cjwatson/launchpad/git-subscriptions-browser into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-subscriptions-browser into lp:launchpad.
Commit message:
Add web UI for Git subscriptions, fixing the webservice along the way.
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-browser/+merge/256900
Add web UI for Git subscriptions. This also has the effect of fixing the webservice, which was failing because GitSubscription objects didn't have a URL; I decided it was easiest to just finish the job here.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-subscriptions-browser into lp:launchpad.
=== 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"/>
<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>
Follow ups