Merge lp:~mvo/software-center/merge-prev-purchases-lp969273 into lp:software-center

Proposed by Michael Vogt on 2012-09-14
Status: Merged
Merged at revision: 3180
Proposed branch: lp:~mvo/software-center/merge-prev-purchases-lp969273
Merge into: lp:software-center
Diff against target: 1157 lines (+408/-286)
15 files modified
softwarecenter/backend/login.py (+6/-0)
softwarecenter/backend/login_sso.py (+24/-2)
softwarecenter/backend/scagent.py (+2/-1)
softwarecenter/backend/spawn_helper.py (+3/-0)
softwarecenter/backend/ubuntusso.py (+106/-9)
softwarecenter/db/database.py (+12/-0)
softwarecenter/db/update.py (+41/-50)
softwarecenter/enums.py (+0/-1)
softwarecenter/ui/gtk3/app.py (+54/-57)
tests/gtk3/test_purchase.py (+19/-16)
tests/test_database.py (+10/-2)
tests/test_reinstall_purchased.py (+112/-63)
tests/test_ubuntu_sso_api.py (+3/-3)
utils/piston-helpers/piston_generic_helper.py (+12/-82)
utils/update-software-center-agent (+4/-0)
To merge this branch: bzr merge lp:~mvo/software-center/merge-prev-purchases-lp969273
Reviewer Review Type Date Requested Status
Gary Lasker (community) 2012-09-14 Approve on 2012-09-17
Review via email: mp+124417@code.launchpad.net

Description of the change

This branch changes the way the reinstall previous purchases is done. It merges them on update of the
update-software-center-update tool into the per-user xapian database instead of adding them as a seperate
in-memory database. This fixes the double entries in the DB and has the nice side-effect that items already
purchased will be displayed as already purchased instead of showing up with a price.

To post a comment you must log in.
3187. By Michael Vogt on 2012-09-14

tests/gtk3/test_purchase.py: update test to latest code

3188. By Michael Vogt on 2012-09-14

trivial pep8 fixes

Michael Vogt (mvo) wrote :

Careful review appreciated, should be good, but there is quite a bit of churn.

Gary Lasker (gary-lasker) wrote :

I looked this over carefully and tested reinstall previous purchases under various sequences of steps. I like the new query for a purchased item, it's simpler and it's very fast. All unit test changes look good and all changed tests pass for me.

Thanks, Michael!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'softwarecenter/backend/login.py'
2--- softwarecenter/backend/login.py 2012-03-19 14:20:55 +0000
3+++ softwarecenter/backend/login.py 2012-09-14 13:44:26 +0000
4@@ -49,5 +49,11 @@
5 def login(self, username=None, password=None):
6 raise NotImplemented
7
8+ def login_or_register(self):
9+ raise NotImplemented
10+
11+ def find_credentials(self):
12+ raise NotImplemented
13+
14 def cancel_login(self):
15 self.emit("login-canceled")
16
17=== modified file 'softwarecenter/backend/login_sso.py'
18--- softwarecenter/backend/login_sso.py 2012-07-09 13:33:51 +0000
19+++ softwarecenter/backend/login_sso.py 2012-09-14 13:44:26 +0000
20@@ -31,6 +31,12 @@
21 from softwarecenter.utils import utf8
22 from login import LoginBackend
23
24+from ubuntu_sso import (
25+ DBUS_BUS_NAME,
26+ DBUS_CREDENTIALS_IFACE,
27+ DBUS_CREDENTIALS_PATH,
28+ )
29+
30 # mostly for testing
31 from fake_review_settings import FakeReviewSettings, network_delay
32
33@@ -44,10 +50,15 @@
34 self.appname = appname
35 self.help_text = help_text
36 self.bus = dbus.SessionBus()
37- self.proxy = self.bus.get_object(
38- 'com.ubuntu.sso', '/com/ubuntu/sso/credentials')
39+ obj = self.bus.get_object(bus_name=DBUS_BUS_NAME,
40+ object_path=DBUS_CREDENTIALS_PATH,
41+ follow_name_owner_changes=True)
42+ self.proxy = dbus.Interface(object=obj,
43+ dbus_interface=DBUS_CREDENTIALS_IFACE)
44 self.proxy.connect_to_signal("CredentialsFound",
45 self._on_credentials_found)
46+ self.proxy.connect_to_signal("CredentialsNotFound",
47+ self._on_credentials_not_found)
48 self.proxy.connect_to_signal("CredentialsError",
49 self._on_credentials_error)
50 self.proxy.connect_to_signal("AuthorizationDenied",
51@@ -64,6 +75,11 @@
52 p['window_id'] = self._window_id
53 return p
54
55+ def find_credentials(self):
56+ LOG.debug("find_crendentials()")
57+ self._credentials = None
58+ self.proxy.find_credentials(self.appname, self._get_params())
59+
60 def login(self, username=None, password=None):
61 LOG.debug("login()")
62 self._credentials = None
63@@ -74,6 +90,12 @@
64 self._credentials = None
65 self.proxy.register(self.appname, self._get_params())
66
67+ def _on_credentials_not_found(self, app_name):
68+ LOG.debug("_on_credentials_found for '%s'" % app_name)
69+ if app_name != self.appname:
70+ return
71+ self.emit("login-failed")
72+
73 def _on_credentials_found(self, app_name, credentials):
74 LOG.debug("_on_credentials_found for '%s'" % app_name)
75 if app_name != self.appname:
76
77=== modified file 'softwarecenter/backend/scagent.py'
78--- softwarecenter/backend/scagent.py 2012-08-29 12:07:14 +0000
79+++ softwarecenter/backend/scagent.py 2012-09-14 13:44:26 +0000
80@@ -93,13 +93,14 @@
81 def _on_query_available_data(self, spawner, piston_available):
82 self.emit("available", piston_available)
83
84- def query_available_for_me(self):
85+ def query_available_for_me(self, no_relogin=False):
86 spawner = SpawnHelper()
87 spawner.parent_xid = self.xid
88 spawner.ignore_cache = self.ignore_cache
89 spawner.connect("data-available", self._on_query_available_for_me_data)
90 spawner.connect("error", lambda spawner, err: self.emit("error", err))
91 spawner.needs_auth = True
92+ spawner.no_relogin = no_relogin
93 spawner.run_generic_piston_helper(
94 "SoftwareCenterAgentAPI", "subscriptions_for_me",
95 complete_only=True)
96
97=== modified file 'softwarecenter/backend/spawn_helper.py'
98--- softwarecenter/backend/spawn_helper.py 2012-05-16 15:52:07 +0000
99+++ softwarecenter/backend/spawn_helper.py 2012-09-14 13:44:26 +0000
100@@ -64,6 +64,7 @@
101 self._child_watch = None
102 self._cmd = None
103 self.needs_auth = False
104+ self.no_relogin = False
105 self.ignore_cache = False
106 self.parent_xid = None
107
108@@ -76,6 +77,8 @@
109 cmd.append("--needs-auth")
110 if self.ignore_cache:
111 cmd.append("--ignore-cache")
112+ if self.no_relogin:
113+ cmd.append("--no-relogin")
114 if self.parent_xid:
115 cmd.append("--parent-xid")
116 cmd.append(str(self.parent_xid))
117
118=== modified file 'softwarecenter/backend/ubuntusso.py'
119--- softwarecenter/backend/ubuntusso.py 2012-08-30 08:57:29 +0000
120+++ softwarecenter/backend/ubuntusso.py 2012-09-14 13:44:26 +0000
121@@ -21,22 +21,44 @@
122
123
124 from gi.repository import GObject
125+from gettext import gettext as _
126
127 import logging
128 import os
129
130+import piston_mini_client.auth
131+import piston_mini_client.failhandlers
132+
133+
134 import softwarecenter.paths
135
136 # mostly for testing
137 from fake_review_settings import FakeReviewSettings, network_delay
138 from spawn_helper import SpawnHelper
139 from softwarecenter.config import get_config
140+from softwarecenter.backend.login_sso import get_sso_backend
141+
142+from softwarecenter.backend.piston.ubuntusso_pristine import (
143+ UbuntuSsoAPI as PristineUbuntuSsoAPI,
144+ )
145+# patch default_service_root to the one we use
146+from softwarecenter.enums import UBUNTU_SSO_SERVICE
147+# *Don't* append /api/1.0, as it's already included in UBUNTU_SSO_SERVICE
148+PristineUbuntuSsoAPI.default_service_root = UBUNTU_SSO_SERVICE
149+
150+from softwarecenter.enums import (SOFTWARE_CENTER_NAME_KEYRING,
151+ SOFTWARE_CENTER_SSO_DESCRIPTION,
152+ )
153+from softwarecenter.utils import clear_token_from_ubuntu_sso_sync
154
155 LOG = logging.getLogger(__name__)
156
157
158-class UbuntuSSOAPI(GObject.GObject):
159- """ Ubuntu SSO interface using the oauth token from the keyring """
160+class UbuntuSSO(GObject.GObject):
161+ """ Ubuntu SSO interface using the oauth token from the keyring
162+
163+ The methods that work syncronous are suffixed with _sync()
164+ """
165
166 __gsignals__ = {
167 "whoami": (GObject.SIGNAL_RUN_LAST,
168@@ -49,8 +71,11 @@
169 ),
170 }
171
172- def __init__(self):
173+ def __init__(self, xid=0):
174 GObject.GObject.__init__(self)
175+ self.oauth = None
176+ self.xid = xid
177+ self.loop = GObject.MainLoop(GObject.main_context_default())
178
179 def _on_whoami_data(self, spawner, piston_whoami):
180 # once we have data, make sure to save it
181@@ -71,11 +96,83 @@
182 spawner.needs_auth = True
183 spawner.run_generic_piston_helper("UbuntuSsoAPI", "whoami")
184
185-
186-class UbuntuSSOAPIFake(UbuntuSSOAPI):
187+ def _login_successful(self, sso_backend, oauth_result):
188+ LOG.debug("_login_successful")
189+ self.oauth = oauth_result
190+ self.loop.quit()
191+
192+ # sync calls
193+ def verify_token_sync(self, token):
194+ """ Verify that the token is valid
195+
196+ Note that this may raise httplib2 exceptions if the server
197+ is not reachable
198+ """
199+ LOG.debug("verify_token")
200+ auth = piston_mini_client.auth.OAuthAuthorizer(
201+ token["token"], token["token_secret"],
202+ token["consumer_key"], token["consumer_secret"])
203+ api = PristineUbuntuSsoAPI(auth=auth)
204+ try:
205+ res = api.whoami()
206+ except piston_mini_client.failhandlers.APIError as e:
207+ LOG.exception("api.whoami failed with APIError: '%s'" % e)
208+ return False
209+ return len(res) > 0
210+
211+ def clear_token(self):
212+ clear_token_from_ubuntu_sso_sync(SOFTWARE_CENTER_NAME_KEYRING)
213+
214+ def _get_sso_backend_and_connect(self):
215+ sso = get_sso_backend(
216+ self.xid,
217+ SOFTWARE_CENTER_NAME_KEYRING,
218+ _(SOFTWARE_CENTER_SSO_DESCRIPTION))
219+ sso.connect("login-successful", self._login_successful)
220+ sso.connect("login-failed", lambda s: self.loop.quit())
221+ sso.connect("login-canceled", lambda s: self.loop.quit())
222+ return sso
223+
224+ def find_oauth_token_sync(self):
225+ self.oauth = None
226+ sso = self. _get_sso_backend_and_connect()
227+ sso.find_credentials()
228+ self.loop.run()
229+ return self.oauth
230+
231+ def get_oauth_token_sync(self):
232+ self.oauth = None
233+ sso = self. _get_sso_backend_and_connect()
234+ sso.login_or_register()
235+ self.loop.run()
236+ return self.oauth
237+
238+ def get_oauth_token_and_verify_sync(self, no_relogin=False):
239+ token = self.get_oauth_token_sync()
240+ # check if the token is valid and reset it if it is not
241+ if token:
242+ # verify token will return false if there is a API error,
243+ # but there maybe httplib2 errors if there is no network,
244+ # so ignore them
245+ try:
246+ if not self.verify_token_sync(token):
247+ attempt_relogin = not no_relogin
248+ if attempt_relogin:
249+ self.clear_token()
250+ # re-trigger login once
251+ token = self.get_oauth_token_sync()
252+ else:
253+ return None
254+ except Exception as e:
255+ LOG.warn(
256+ "token could not be verified (network problem?): %s" % e)
257+ return token
258+
259+
260+class UbuntuSSOAPIFake(UbuntuSSO):
261
262 def __init__(self):
263- UbuntuSSOAPI.__init__(self)
264+ UbuntuSSO.__init__(self)
265 self._fake_settings = FakeReviewSettings()
266
267 @network_delay
268@@ -110,7 +207,7 @@
269 ubuntu_sso_class = UbuntuSSOAPIFake()
270 LOG.warn('Using fake Ubuntu SSO API. Only meant for testing purposes')
271 else:
272- ubuntu_sso_class = UbuntuSSOAPI()
273+ ubuntu_sso_class = UbuntuSSO()
274 return ubuntu_sso_class
275
276
277@@ -133,6 +230,7 @@
278 password = sys.stdin.readline().strip()
279 sso.login(user, password)
280
281+
282 # interactive test code
283 if __name__ == "__main__":
284 def _whoami(sso, result):
285@@ -145,7 +243,7 @@
286
287 def _dbus_maybe_login_successful(ssologin, oauth_result):
288 print "got token, verify it now"
289- sso = UbuntuSSOAPI()
290+ sso = UbuntuSSO()
291 sso.connect("whoami", _whoami)
292 sso.connect("error", _error)
293 sso.whoami()
294@@ -154,7 +252,6 @@
295 logging.basicConfig(level=logging.DEBUG)
296 softwarecenter.paths.datadir = "./data"
297
298- from login_sso import get_sso_backend
299 backend = get_sso_backend("", "appname", "help_text")
300 backend.connect("login-successful", _dbus_maybe_login_successful)
301 backend.login_or_register()
302
303=== modified file 'softwarecenter/db/database.py'
304--- softwarecenter/db/database.py 2012-09-12 12:41:05 +0000
305+++ softwarecenter/db/database.py 2012-09-14 13:44:26 +0000
306@@ -43,6 +43,18 @@
307 LOG = logging.getLogger(__name__)
308
309
310+def get_reinstall_previous_purchases_query():
311+ """Return a query to get applications purchased
312+
313+ :return: a xapian query to get all the apps that are purchaed
314+ """
315+ # this query will give us all documents that have a purchase date != ""
316+ query = xapian.Query(xapian.Query.OP_VALUE_GE,
317+ XapianValues.PURCHASED_DATE,
318+ "1")
319+ return query
320+
321+
322 def parse_axi_values_file(filename="/var/lib/apt-xapian-index/values"):
323 """ parse the apt-xapian-index "values" file and provide the
324 information in the self._axi_values dict
325
326=== modified file 'softwarecenter/db/update.py'
327--- softwarecenter/db/update.py 2012-09-10 09:40:48 +0000
328+++ softwarecenter/db/update.py 2012-09-14 13:44:26 +0000
329@@ -30,9 +30,13 @@
330 from gi.repository import GObject
331 from piston_mini_client import PistonResponseObject
332
333+from softwarecenter.backend.scagent import SoftwareCenterAgent
334+from softwarecenter.backend.ubuntusso import UbuntuSSO
335 from softwarecenter.distro import get_distro
336 from softwarecenter.utils import utf8
337
338+from gettext import gettext as _
339+
340 # py3 compat
341 try:
342 from configparser import RawConfigParser, NoOptionError
343@@ -49,7 +53,6 @@
344 import pickle
345
346
347-from gettext import gettext as _
348 from glob import glob
349 from urlparse import urlparse
350
351@@ -59,7 +62,6 @@
352 AppInfoFields,
353 AVAILABLE_FOR_PURCHASE_MAGIC_CHANNEL_NAME,
354 DB_SCHEMA_VERSION,
355- PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME,
356 XapianValues,
357 )
358 from softwarecenter.db.database import parse_axi_values_file
359@@ -672,8 +674,6 @@
360 # gets confused about (appname, pkgname) duplication
361 self.sca_application.name = utf8(_("%s (already purchased)")) % utf8(
362 self.sca_application.name)
363- self.sca_application.channel = (
364- PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME)
365 for attr_name in ('license_key', 'license_key_path'):
366 attr = getattr(self.sca_subscription, attr_name, self.NOT_DEFINED)
367 if attr is self.NOT_DEFINED:
368@@ -1007,44 +1007,6 @@
369 return True
370
371
372-def add_from_purchased_but_needs_reinstall_data(
373- purchased_but_may_need_reinstall_list, db, cache):
374- """Add application that have been purchased but may require a reinstall
375-
376- This adds a inmemory database to the main db with the special
377- PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME channel prefix
378-
379- :return: a xapian query to get all the apps that need reinstall
380- """
381- # magic
382- db_purchased = xapian.inmemory_open()
383- # go over the items we have
384- for item in purchased_but_may_need_reinstall_list:
385- # FIXME: what to do with duplicated entries? we will end
386- # up with two xapian.Document, one for the for-pay
387- # and one for the availalbe one from s-c-agent
388- #try:
389- # db.get_xapian_document(item.name,
390- # item.package_name)
391- #except IndexError:
392- # # item is not in the xapian db
393- # pass
394- #else:
395- # # ignore items we already have in the db, ignore
396- # continue
397- # index the item
398- try:
399- parser = SCAPurchasedApplicationParser(item)
400- parser.index_app_info(db_purchased, cache)
401- except:
402- LOG.exception("error processing: %r", item)
403- # add new in memory db to the main db
404- db.add_database(db_purchased)
405- # return a query
406- query = xapian.Query("AH" + PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME)
407- return query
408-
409-
410 def update_from_software_center_agent(db, cache, ignore_cache=False,
411 include_sca_qa=False):
412 """Update the index based on the software-center-agent data."""
413@@ -1056,31 +1018,60 @@
414 sca.good_data = True
415 loop.quit()
416
417+ def _available_for_me_cb(sca, available_for_me):
418+ LOG.debug("update_from_software_center_agent: available_for_me: %r",
419+ available_for_me)
420+ sca.available_for_me = available_for_me
421+ loop.quit()
422+
423 def _error_cb(sca, error):
424 LOG.warn("update_from_software_center_agent: error: %r", error)
425- sca.available = []
426 sca.good_data = False
427 loop.quit()
428
429- # use the anonymous interface to s-c-agent, scales much better and is
430- # much cache friendlier
431- from softwarecenter.backend.scagent import SoftwareCenterAgent
432- # FIXME: honor ignore_etag here somehow with the new piston based API
433+ context = GObject.main_context_default()
434+ loop = GObject.MainLoop(context)
435+
436 sca = SoftwareCenterAgent(ignore_cache)
437 sca.connect("available", _available_cb)
438+ sca.connect("available-for-me", _available_for_me_cb)
439 sca.connect("error", _error_cb)
440- sca.available = None
441+ sca.available = []
442+ sca.available_for_me = []
443+
444+ # query what is available for me first
445+ available_for_me_pkgnames = set()
446+ # this will ensure we do not trigger a login dialog
447+ helper = UbuntuSSO()
448+ token = helper.find_oauth_token_sync()
449+ if token:
450+ sca.query_available_for_me(no_relogin=True)
451+ loop.run()
452+ for item in sca.available_for_me:
453+ try:
454+ parser = SCAPurchasedApplicationParser(item)
455+ parser.index_app_info(db, cache)
456+ available_for_me_pkgnames.add(item.application["package_name"])
457+ except:
458+ LOG.exception("error processing: %r", item)
459+
460+ # ... now query all that is available
461 if include_sca_qa:
462 sca.query_available_qa()
463 else:
464 sca.query_available()
465+
466 # create event loop and run it until data is available
467 # (the _available_cb and _error_cb will quit it)
468- context = GObject.main_context_default()
469- loop = GObject.MainLoop(context)
470 loop.run()
471+
472 # process data
473 for entry in sca.available:
474+
475+ # do not add stuff here thats already purchased to avoid duplication
476+ if entry.package_name in available_for_me_pkgnames:
477+ continue
478+
479 # process events
480 while context.pending():
481 context.iteration()
482
483=== modified file 'softwarecenter/enums.py'
484--- softwarecenter/enums.py 2012-09-11 00:02:18 +0000
485+++ softwarecenter/enums.py 2012-09-14 13:44:26 +0000
486@@ -208,7 +208,6 @@
487
488
489 # fake channels
490-PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME = "for-pay-needs-reinstall"
491 AVAILABLE_FOR_PURCHASE_MAGIC_CHANNEL_NAME = "available-for-pay"
492
493
494
495=== modified file 'softwarecenter/ui/gtk3/app.py'
496--- softwarecenter/ui/gtk3/app.py 2012-09-11 13:31:09 +0000
497+++ softwarecenter/ui/gtk3/app.py 2012-09-14 13:44:26 +0000
498@@ -47,6 +47,8 @@
499 # make pyflakes shut up
500 softwarecenter.netstatus.NETWORK_STATE
501
502+from softwarecenter.backend.ubuntusso import UbuntuSSO
503+
504 # db imports
505 from softwarecenter.db.application import Application
506 from softwarecenter.db import (
507@@ -84,7 +86,10 @@
508 init_sc_css_provider,
509 )
510 from softwarecenter.version import VERSION
511-from softwarecenter.db.database import StoreDatabase
512+from softwarecenter.db.database import (
513+ StoreDatabase,
514+ get_reinstall_previous_purchases_query,
515+ )
516 try:
517 from aptd_gtk3 import InstallBackendUI
518 InstallBackendUI # pyflakes
519@@ -336,7 +341,7 @@
520 # for use when viewing previous purchases
521 self.scagent = None
522 self.sso = None
523- self.available_for_me_query = None
524+ self.available_for_me_query = get_reinstall_previous_purchases_query()
525
526 Gtk.Window.set_default_icon_name("softwarecenter")
527
528@@ -739,25 +744,9 @@
529 channel_manager.feed_in_private_sources_list_entries(
530 private_archives)
531
532- def _on_sso_login(self, sso, oauth_result):
533- self._sso_login_successful = True
534- # appmanager needs to know about the oauth token for the reinstall
535- # previous purchases add_license_key call
536- self.app_manager.oauth_token = oauth_result
537- self.scagent.query_available_for_me()
538-
539 def _on_style_updated(self, widget, init_css_callback, *args):
540 init_css_callback(widget, *args)
541
542- def _available_for_me_result(self, scagent, result_list):
543- #print "available_for_me_result", result_list
544- from softwarecenter.db.update import (
545- add_from_purchased_but_needs_reinstall_data)
546- available = add_from_purchased_but_needs_reinstall_data(result_list,
547- self.db, self.cache)
548- self.available_for_me_query = available
549- self.available_pane.on_previous_purchases_activated(available)
550-
551 def get_icon_filename(self, iconname, iconsize):
552 iconinfo = self.icons.lookup_icon(iconname, iconsize, 0)
553 if not iconinfo:
554@@ -838,30 +827,53 @@
555 d = LoginDialog(self.glaunchpad, self.datadir, parent=self.window_main)
556 d.login()
557
558- def _create_dbus_sso(self):
559- # see bug #773214 for the rationale, do not translate the appname
560- #appname = _("Ubuntu Software Center")
561- appname = SOFTWARE_CENTER_NAME_KEYRING
562- help_text = _("To reinstall previous purchases, sign in to the "
563- "Ubuntu Single Sign-On account you used to pay for them.")
564- #window = self.window_main.get_window()
565- #xid = self.get_window().xid
566- xid = 0
567- self.sso = get_sso_backend(xid,
568- appname,
569- help_text)
570- self.sso.connect("login-successful", self._on_sso_login)
571-
572- def _login_via_dbus_sso(self):
573- self._create_dbus_sso()
574- self.sso.login()
575-
576- def _create_scagent_if_needed(self):
577- if not self.scagent:
578- from softwarecenter.backend.scagent import SoftwareCenterAgent
579- self.scagent = SoftwareCenterAgent()
580- self.scagent.connect("available-for-me",
581- self._available_for_me_result)
582+ def _on_reinstall_purchased_login(self, sso, oauth_result):
583+ self._sso_login_successful = True
584+ # appmanager needs to know about the oauth token for the reinstall
585+ # previous purchases add_license_key call
586+ self.app_manager.oauth_token = oauth_result
587+
588+ # the software-center-agent will ensure the previous-purchases
589+ # get merged in
590+ self._run_software_center_agent()
591+
592+ # show spinner as this may take some time, the spinner will
593+ # automatically go away when a DB refreshes
594+ self.available_pane.show_appview_spinner()
595+
596+ # show previous purchased
597+ self.available_pane.on_previous_purchases_activated(
598+ self.available_for_me_query)
599+
600+ def on_menuitem_reinstall_purchases_activate(self, menuitem):
601+ self.view_manager.set_active_view(ViewPages.AVAILABLE)
602+ self.view_manager.search_entry.clear_with_no_signal()
603+ # its ok to use the sync version here to get the token, this is
604+ # very quick
605+ helper = UbuntuSSO()
606+ token = helper.find_oauth_token_sync()
607+ if token:
608+ # trigger a software-center-agent run to ensure the merged in
609+ # purchases are fresh
610+ self._run_software_center_agent()
611+ # we already have the list of available items, so just show it
612+ # (no need for spinner here)
613+ self.available_pane.on_previous_purchases_activated(
614+ self.available_for_me_query)
615+ else:
616+ # see bug #773214 for the rationale, do not translate the appname
617+ #appname = _("Ubuntu Software Center")
618+ appname = SOFTWARE_CENTER_NAME_KEYRING
619+ help_text = _(
620+ "To reinstall previous purchases, sign in to the "
621+ "Ubuntu Single Sign-On account you used to pay for them.")
622+ #window = self.window_main.get_window()
623+ #xid = self.get_window().xid
624+ xid = 0
625+ self.sso = get_sso_backend(xid, appname, help_text)
626+ self.sso.connect(
627+ "login-successful", self._on_reinstall_purchased_login)
628+ self.sso.login()
629
630 def on_menuitem_recommendations_activate(self, menu_item):
631 rec_panel = self.available_pane.cat_view.recommended_for_you_panel
632@@ -875,21 +887,6 @@
633 if res == Gtk.ResponseType.YES:
634 rec_panel.opt_in_to_recommendations_service()
635
636- def on_menuitem_reinstall_purchases_activate(self, menuitem):
637- self.view_manager.set_active_view(ViewPages.AVAILABLE)
638- self.view_manager.search_entry.clear_with_no_signal()
639- if self.available_for_me_query:
640- # we already have the list of available items, so just show it
641- # (no need for spinner here)
642- self.available_pane.on_previous_purchases_activated(
643- self.available_for_me_query)
644- else:
645- # show spinner as this may take some time
646- self.available_pane.show_appview_spinner()
647- # fetch the list of available items and show it
648- self._create_scagent_if_needed()
649- self._login_via_dbus_sso()
650-
651 def on_menuitem_deauthorize_computer_activate(self, menuitem):
652
653 # FIXME: need Ubuntu SSO username here
654
655=== modified file 'tests/gtk3/test_purchase.py'
656--- tests/gtk3/test_purchase.py 2012-08-21 08:46:26 +0000
657+++ tests/gtk3/test_purchase.py 2012-09-14 13:44:26 +0000
658@@ -71,7 +71,13 @@
659 do_events_with_sleep()
660 self.assertTrue(signal_mock.called)
661
662- def test_reinstall_previous_purchase_display(self):
663+
664+class PreviousPurchasesTestCase(unittest.TestCase):
665+
666+ @patch("softwarecenter.backend.ubuntusso.UbuntuSSO"
667+ ".find_oauth_token_sync")
668+ def test_reinstall_previous_purchase_display(self, mock_find_token):
669+ mock_find_token.return_value = { 'not': 'important' }
670 mock_options = get_mock_options()
671 xapiandb = "/var/cache/software-center/"
672 app = SoftwareCenterAppGtk3(
673@@ -79,22 +85,19 @@
674 self.addCleanup(app.destroy)
675 # real app opens cache async
676 app.cache.open()
677- # no real sso
678- with patch.object(app, '_login_via_dbus_sso',
679- lambda: app._available_for_me_result(None, [])):
680- # show it
681- app.window_main.show_all()
682- app.available_pane.init_view()
683+ # .. and now pretend we clicked on the menu item
684+ app.window_main.show_all()
685+ app.available_pane.init_view()
686+ do_events_with_sleep()
687+ app.on_menuitem_reinstall_purchases_activate(None)
688+ # it can take a bit until the sso client is ready
689+ for i in range(10):
690+ if (app.available_pane.get_current_page() ==
691+ AvailablePane.Pages.LIST):
692+ break
693 do_events_with_sleep()
694- app.on_menuitem_reinstall_purchases_activate(None)
695- # it can take a bit until the sso client is ready
696- for i in range(100):
697- if (app.available_pane.get_current_page() ==
698- AvailablePane.Pages.LIST):
699- break
700- do_events_with_sleep()
701- self.assertEqual(app.available_pane.get_current_page(),
702- AvailablePane.Pages.LIST)
703+ self.assertEqual(app.available_pane.get_current_page(),
704+ AvailablePane.Pages.LIST)
705
706
707 if __name__ == "__main__":
708
709=== modified file 'tests/test_database.py'
710--- tests/test_database.py 2012-09-14 07:31:06 +0000
711+++ tests/test_database.py 2012-09-14 13:44:26 +0000
712@@ -166,7 +166,11 @@
713 self.assertTrue(res)
714 self.assertEqual(db.get_doccount(), 1)
715
716- def test_build_from_software_center_agent(self):
717+ @patch("softwarecenter.backend.ubuntusso.UbuntuSSO"
718+ ".find_oauth_token_sync")
719+ def test_build_from_software_center_agent(self, mock_find_oauth):
720+ # pretend we have no token
721+ mock_find_oauth.return_value = None
722 db = xapian.inmemory_open()
723 cache = apt.Cache()
724 # monkey patch distro to ensure we get data
725@@ -365,7 +369,11 @@
726 doc.get_value(value_time) >= last_time
727 last_time = doc.get_value(value_time)
728
729- def test_for_purchase_apps_date_published(self):
730+ @patch("softwarecenter.backend.ubuntusso.UbuntuSSO"
731+ ".find_oauth_token_sync")
732+ def test_for_purchase_apps_date_published(self, mock_find_oauth):
733+ # pretend we have no token
734+ mock_find_oauth.return_value = None
735 #os.environ["SOFTWARE_CENTER_DEBUG_HTTP"] = "1"
736 #os.environ["SOFTWARE_CENTER_AGENT_HOST"] = "http://sc.staging.ubuntu.com/"
737 # staging does not have a valid cert
738
739=== modified file 'tests/test_reinstall_purchased.py'
740--- tests/test_reinstall_purchased.py 2012-06-25 17:40:17 +0000
741+++ tests/test_reinstall_purchased.py 2012-09-14 13:44:26 +0000
742@@ -1,30 +1,29 @@
743-import apt_pkg
744-import apt
745 import json
746 import platform
747-import os
748 import unittest
749 import xapian
750
751+from gi.repository import GObject
752+
753 from mock import patch
754 from piston_mini_client import PistonResponseObject
755 from tests.utils import (
756- DATA_DIR,
757+ get_test_pkg_info,
758 setup_test_env,
759+ ObjectWithSignals,
760 )
761 setup_test_env()
762
763 from softwarecenter.enums import (
764 AppInfoFields,
765 AVAILABLE_FOR_PURCHASE_MAGIC_CHANNEL_NAME,
766- PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME,
767 XapianValues,
768 )
769-from softwarecenter.db.database import StoreDatabase
770+from softwarecenter.db.database import get_reinstall_previous_purchases_query
771 from softwarecenter.db.update import (
772- add_from_purchased_but_needs_reinstall_data,
773 SCAPurchasedApplicationParser,
774 SCAApplicationParser,
775+ update_from_software_center_agent,
776 )
777
778 # Example taken from running:
779@@ -98,62 +97,45 @@
780 "description": "Play DVD-Videos\\r\\n\\r\\nFluendo DVD Player is a software application specially designed to\\r\\nreproduce DVD on Linux/Unix platforms, which provides end users with\\r\\nhigh quality standards.\\r\\n\\r\\nThe following features are provided:\\r\\n* Full DVD Playback\\r\\n* DVD Menu support\\r\\n* Fullscreen support\\r\\n* Dolby Digital pass-through\\r\\n* Dolby Digital 5.1 output and stereo downmixing support\\r\\n* Resume from last position support\\r\\n* Subtitle support\\r\\n* Audio selection support\\r\\n* Multiple Angles support\\r\\n* Support for encrypted discs\\r\\n* Multiregion, works in all regions\\r\\n* Multiple video deinterlacing algorithms",
781 "website": null,
782 "version": "1.2.1"
783- }
784+ },
785+ {
786+ "website": "",
787+ "package_name": "photobomb",
788+ "video_embedded_html_urls": [ ],
789+ "demo": null,
790+ "keywords": "photos, pictures, editing, gwibber, twitter, facebook, drawing",
791+ "video_urls": [ ],
792+ "screenshot_url": "http://software-center.ubuntu.com/site_media/screenshots/2011/08/Screenshot-45.png",
793+ "id": 83,
794+ "archive_id": "commercial-ppa-uploaders/photobomb",
795+ "support_url": "http://launchpad.net/photobomb",
796+ "icon_url": "http://software-center.ubuntu.com/site_media/icons/2011/08/logo_64.png",
797+ "binary_filesize": null,
798+ "version": "",
799+ "company_name": "",
800+ "department": [
801+ "Graphics"
802+ ],
803+ "tos_url": "",
804+ "channel": "For Purchase",
805+ "status": "Published",
806+ "signing_key_id": "1024R/75254D99",
807+ "description": "Easy and Social Image Editor\\nPhotobomb give you easy access to images in your social networking feeds, pictures on your computer and peripherals, and pictures on the web, and let's you draw, write, crop, combine, and generally have a blast mashing 'em all up. Then you can save off your photobomb, or tweet your creation right back to your social network.",
808+ "price": "2.99",
809+ "debtags": [ ],
810+ "date_published": "2011-12-05 18:43:20.794802",
811+ "categories": "Graphics",
812+ "name": "Photobomb",
813+ "license": "GNU GPL v3",
814+ "screenshot_urls": [
815+ "http://software-center.ubuntu.com/site_media/screenshots/2011/08/Screenshot-45.png"
816+ ],
817+ "archive_root": "https://private-ppa.launchpad.net/"
818+ }
819 ]
820 """
821
822
823-class TestPurchased(unittest.TestCase):
824- """ tests the store database """
825-
826- def _make_available_for_me_list(self):
827- my_subscriptions = json.loads(SUBSCRIPTIONS_FOR_ME_JSON)
828- return list(
829- PistonResponseObject.from_dict(subs) for subs in my_subscriptions)
830-
831- def setUp(self):
832- # use fixture apt data
833- apt_pkg.config.set("APT::Architecture", "i386")
834- apt_pkg.config.set("Dir::State::status",
835- os.path.join(DATA_DIR, "appdetails", "var", "lib", "dpkg", "status"))
836- # create mocks
837- self.available_to_me = self._make_available_for_me_list()
838- self.cache = apt.Cache()
839-
840- def test_reinstall_purchased_mock(self):
841- # test if the mocks are ok
842- self.assertEqual(len(self.available_to_me), 1)
843- self.assertEqual(
844- self.available_to_me[0].application['package_name'], "photobomb")
845-
846- def test_reinstall_purchased_xapian(self):
847- db = StoreDatabase("/var/cache/software-center/xapian", self.cache)
848- db.open(use_axi=False)
849- # now create purchased debs xapian index (in memory because
850- # we store the repository passwords in here)
851- old_db_len = len(db)
852- query = add_from_purchased_but_needs_reinstall_data(
853- self.available_to_me, db, self.cache)
854- # ensure we have a new item (the available for reinstall one)
855- self.assertEqual(len(db), old_db_len+1)
856- # query
857- enquire = xapian.Enquire(db.xapiandb)
858- enquire.set_query(query)
859- matches = enquire.get_mset(0, len(db))
860- self.assertEqual(len(matches), 1)
861- distroseries = platform.dist()[2]
862- for m in matches:
863- doc = db.xapiandb.get_document(m.docid)
864- self.assertEqual(doc.get_value(XapianValues.PKGNAME), "photobomb")
865- self.assertEqual(
866- doc.get_value(XapianValues.ARCHIVE_SIGNING_KEY_ID),
867- "1024R/75254D99")
868- self.assertEqual(doc.get_value(XapianValues.ARCHIVE_DEB_LINE),
869- "deb https://username:random3atoken@"
870- "private-ppa.launchpad.net/commercial-ppa-uploaders"
871- "/photobomb/ubuntu %s main" % distroseries)
872-
873-
874 class SCAApplicationParserTestCase(unittest.TestCase):
875
876 def _make_application_parser(self, piston_application=None):
877@@ -313,12 +295,11 @@
878 for key in (AppInfoFields.LICENSE_KEY, AppInfoFields.LICENSE_KEY_PATH):
879 self.assertIsNone(parser.get_value(key))
880
881- def test_magic_channel(self):
882+ def test_purchase_date(self):
883 parser = self._make_application_parser()
884-
885 self.assertEqual(
886- PURCHASED_NEEDS_REINSTALL_MAGIC_CHANNEL_NAME,
887- parser.get_value(AppInfoFields.CHANNEL))
888+ "2011-09-16 06:37:52",
889+ parser.get_value(AppInfoFields.PURCHASED_DATE))
890
891 def test_will_handle_supported_distros_when_available(self):
892 # When the fix for bug 917109 reaches production, we will be
893@@ -367,5 +348,73 @@
894 SCAPurchasedApplicationParser.update_debline(orig_debline))
895
896
897+class TestAvailableForMeMerging(unittest.TestCase):
898+
899+ def setUp(self):
900+ self.available_for_me = self._make_available_for_me_list()
901+ self.available = self._make_available_list()
902+
903+ def _make_available_for_me_list(self):
904+ my_subscriptions = json.loads(SUBSCRIPTIONS_FOR_ME_JSON)
905+ return list(
906+ PistonResponseObject.from_dict(subs) for subs in my_subscriptions)
907+
908+ def _make_available_list(self):
909+ available_apps = json.loads(AVAILABLE_APPS_JSON)
910+ return list(
911+ PistonResponseObject.from_dict(subs) for subs in available_apps)
912+
913+ def _make_fake_scagent(self, available_data, available_for_me_data):
914+ sca = ObjectWithSignals()
915+ sca.query_available = lambda **kwargs: GObject.timeout_add(
916+ 100, lambda: sca.emit('available', sca, available_data))
917+ sca.query_available_for_me = lambda **kwargs: GObject.timeout_add(
918+ 100, lambda: sca.emit('available-for-me',
919+ sca, available_for_me_data))
920+ return sca
921+
922+ def test_reinstall_purchased_mock(self):
923+ # test if the mocks are ok
924+ self.assertEqual(len(self.available_for_me), 1)
925+ self.assertEqual(
926+ self.available_for_me[0].application['package_name'], "photobomb")
927+
928+ @patch("softwarecenter.db.update.SoftwareCenterAgent")
929+ @patch("softwarecenter.db.update.UbuntuSSO")
930+ def test_reinstall_purchased_xapian(self, mock_helper, mock_agent):
931+ small_available = [ self.available[0] ]
932+ mock_agent.return_value = self._make_fake_scagent(
933+ small_available, self.available_for_me)
934+
935+ db = xapian.inmemory_open()
936+ cache = get_test_pkg_info()
937+
938+ # now create purchased debs xapian index (in memory because
939+ # we store the repository passwords in here)
940+ old_db_len = db.get_doccount()
941+ update_from_software_center_agent(db, cache)
942+ # ensure we have the new item
943+ self.assertEqual(db.get_doccount(), old_db_len+2)
944+ # query
945+ query = get_reinstall_previous_purchases_query()
946+ enquire = xapian.Enquire(db)
947+ enquire.set_query(query)
948+ matches = enquire.get_mset(0, db.get_doccount())
949+ self.assertEqual(len(matches), 1)
950+ distroseries = platform.dist()[2]
951+ for m in matches:
952+ doc = db.get_document(m.docid)
953+ self.assertEqual(doc.get_value(XapianValues.PKGNAME), "photobomb")
954+ self.assertEqual(
955+ doc.get_value(XapianValues.ARCHIVE_SIGNING_KEY_ID),
956+ "1024R/75254D99")
957+ self.assertEqual(doc.get_value(XapianValues.ARCHIVE_DEB_LINE),
958+ "deb https://username:random3atoken@"
959+ "private-ppa.launchpad.net/commercial-ppa-uploaders"
960+ "/photobomb/ubuntu %s main" % distroseries)
961+
962+
963 if __name__ == "__main__":
964+ import logging
965+ logging.basicConfig(level=logging.DEBUG)
966 unittest.main()
967
968=== modified file 'tests/test_ubuntu_sso_api.py'
969--- tests/test_ubuntu_sso_api.py 2012-05-30 18:39:55 +0000
970+++ tests/test_ubuntu_sso_api.py 2012-09-14 13:44:26 +0000
971@@ -6,7 +6,7 @@
972 )
973 setup_test_env()
974 from softwarecenter.backend.ubuntusso import (UbuntuSSOAPIFake,
975- UbuntuSSOAPI,
976+ UbuntuSSO,
977 get_ubuntu_sso_backend,
978 )
979
980@@ -15,7 +15,7 @@
981
982 def test_fake_and_real_provide_similar_methods(self):
983 """ test if the real and fake sso provide the same functions """
984- sso_real = UbuntuSSOAPI
985+ sso_real = UbuntuSSO
986 sso_fake = UbuntuSSOAPIFake
987 # ensure that both fake and real implement the same methods
988 self.assertEqual(
989@@ -25,7 +25,7 @@
990 def test_get_ubuntu_backend(self):
991 # test that we get the real one
992 self.assertEqual(type(get_ubuntu_sso_backend()),
993- UbuntuSSOAPI)
994+ UbuntuSSO)
995 # test that we get the fake one
996 os.environ["SOFTWARE_CENTER_FAKE_REVIEW_API"] = "1"
997 self.assertEqual(type(get_ubuntu_sso_backend()),
998
999=== modified file 'utils/piston-helpers/piston_generic_helper.py'
1000--- utils/piston-helpers/piston_generic_helper.py 2012-08-15 08:36:50 +0000
1001+++ utils/piston-helpers/piston_generic_helper.py 2012-09-14 13:44:26 +0000
1002@@ -25,7 +25,6 @@
1003 import pickle
1004 import sys
1005
1006-from gi.repository import GObject
1007
1008 # useful for debugging
1009 if "SOFTWARE_CENTER_DEBUG_HTTP" in os.environ:
1010@@ -45,13 +44,8 @@
1011
1012 import softwarecenter.paths
1013 from softwarecenter.paths import SOFTWARE_CENTER_CACHE_DIR
1014-from softwarecenter.backend.login_sso import get_sso_backend
1015
1016-from softwarecenter.enums import (SOFTWARE_CENTER_NAME_KEYRING,
1017- SOFTWARE_CENTER_SSO_DESCRIPTION,
1018- )
1019-
1020-from softwarecenter.utils import clear_token_from_ubuntu_sso_sync
1021+from softwarecenter.backend.ubuntusso import UbuntuSSO
1022
1023 # the piston import
1024 from softwarecenter.backend.piston.ubuntusso_pristine import UbuntuSsoAPI
1025@@ -60,90 +54,23 @@
1026 from softwarecenter.backend.piston.sreclient_pristine import (
1027 SoftwareCenterRecommenderAPI)
1028
1029+
1030+from softwarecenter.enums import RECOMMENDER_HOST
1031+SoftwareCenterRecommenderAPI.default_service_root = \
1032+ RECOMMENDER_HOST + "/api/1.0"
1033+
1034+
1035 # patch default_service_root to the one we use
1036 from softwarecenter.enums import UBUNTU_SSO_SERVICE
1037 # *Don't* append /api/1.0, as it's already included in UBUNTU_SSO_SERVICE
1038 UbuntuSsoAPI.default_service_root = UBUNTU_SSO_SERVICE
1039
1040-from softwarecenter.enums import RECOMMENDER_HOST
1041-SoftwareCenterRecommenderAPI.default_service_root = \
1042- RECOMMENDER_HOST + "/api/1.0"
1043-
1044
1045 RatingsAndReviewsAPI # pyflakes
1046 UbuntuSsoAPI # pyflakes
1047 SoftwareCenterAgentAPI # pyflakes
1048 SoftwareCenterRecommenderAPI # pyflakes
1049
1050-from gettext import gettext as _
1051-
1052-
1053-# helper that is only used to verify that the token is ok
1054-# and trigger cleanup if not
1055-class SSOLoginHelper(object):
1056-
1057- def __init__(self, xid=0):
1058- self.oauth = None
1059- self.xid = xid
1060- self.loop = GObject.MainLoop(GObject.main_context_default())
1061-
1062- def _login_successful(self, sso_backend, oauth_result):
1063- LOG.debug("_login_successful")
1064- self.oauth = oauth_result
1065- # FIXME: actually verify the token against ubuntu SSO
1066- self.loop.quit()
1067-
1068- def verify_token_sync(self, token):
1069- """ Verify that the token is valid
1070-
1071- Note that this may raise httplib2 exceptions if the server
1072- is not reachable
1073- """
1074- LOG.debug("verify_token")
1075- auth = piston_mini_client.auth.OAuthAuthorizer(
1076- token["token"], token["token_secret"],
1077- token["consumer_key"], token["consumer_secret"])
1078- api = UbuntuSsoAPI(auth=auth)
1079- try:
1080- res = api.whoami()
1081- except piston_mini_client.failhandlers.APIError as e:
1082- LOG.exception("api.whoami failed with APIError: '%s'" % e)
1083- return False
1084- return len(res) > 0
1085-
1086- def clear_token(self):
1087- clear_token_from_ubuntu_sso_sync(SOFTWARE_CENTER_NAME_KEYRING)
1088-
1089- def get_oauth_token_sync(self):
1090- self.oauth = None
1091- sso = get_sso_backend(
1092- self.xid,
1093- SOFTWARE_CENTER_NAME_KEYRING,
1094- _(SOFTWARE_CENTER_SSO_DESCRIPTION))
1095- sso.connect("login-successful", self._login_successful)
1096- sso.connect("login-failed", lambda s: self.loop.quit())
1097- sso.connect("login-canceled", lambda s: self.loop.quit())
1098- sso.login_or_register()
1099- self.loop.run()
1100- return self.oauth
1101-
1102- def get_oauth_token_and_verify_sync(self):
1103- token = self.get_oauth_token_sync()
1104- # check if the token is valid and reset it if it is not
1105- if token:
1106- # verify token will return false if there is a API error,
1107- # but there maybe httplib2 errors if there is no network,
1108- # so ignore them
1109- try:
1110- if not self.verify_token_sync(token):
1111- self.clear_token()
1112- # re-trigger login once
1113- token = self.get_oauth_token_sync()
1114- except Exception as e:
1115- LOG.warn(
1116- "token could not be verified (network problem?): %s" % e)
1117- return token
1118-
1119
1120 LOG = logging.getLogger(__name__)
1121
1122@@ -164,6 +91,8 @@
1123 help="force disable offline mode")
1124 parser.add_argument("--needs-auth", default=False, action="store_true",
1125 help="need oauth credentials")
1126+ parser.add_argument("--no-relogin", default=False, action="store_true",
1127+ help="do not attempt relogin if token is invalid")
1128 parser.add_argument("--output", default="pickle",
1129 help="output result as [pickle|json|text]")
1130 parser.add_argument("--parent-xid", default=0,
1131@@ -191,8 +120,9 @@
1132 softwarecenter.paths.datadir = args.datadir
1133
1134 if args.needs_auth:
1135- helper = SSOLoginHelper(args.parent_xid)
1136- token = helper.get_oauth_token_and_verify_sync()
1137+ helper = UbuntuSSO(args.parent_xid)
1138+ token = helper.get_oauth_token_and_verify_sync(
1139+ no_relogin=args.no_relogin)
1140 # if we don't have a token, error here
1141 if not token:
1142 # it may happen that the parent is closed already so the pipe
1143
1144=== modified file 'utils/update-software-center-agent'
1145--- utils/update-software-center-agent 2012-03-16 11:09:25 +0000
1146+++ utils/update-software-center-agent 2012-09-14 13:44:26 +0000
1147@@ -96,6 +96,10 @@
1148 "a write lock on %s" % pathname)
1149 sys.exit(1)
1150
1151+ # ensure permissions are 0700 as this may contain repo passwords from
1152+ # the reinstall-previous-purchase repos
1153+ os.chmod(pathname, 0o700)
1154+
1155 # the following requires a http connection, so we do it in a
1156 # seperate database
1157 include_sca_qa = "SOFTWARE_CENTER_AGENT_INCLUDE_QA" in os.environ

Subscribers

People subscribed via source and target branches