Merge lp:~gary-lasker/software-center/recommender-profile-uploads-lp944693 into lp:software-center

Proposed by Gary Lasker
Status: Merged
Merged at revision: 2939
Proposed branch: lp:~gary-lasker/software-center/recommender-profile-uploads-lp944693
Merge into: lp:software-center
Diff against target: 382 lines (+120/-65)
4 files modified
softwarecenter/backend/recagent.py (+73/-35)
softwarecenter/ui/gtk3/app.py (+23/-13)
softwarecenter/ui/gtk3/widgets/recommendations.py (+21/-12)
test/test_recagent.py (+3/-5)
To merge this branch: bzr merge lp:~gary-lasker/software-center/recommender-profile-uploads-lp944693
Reviewer Review Type Date Requested Status
Michael Vogt Approve
Review via email: mp+100360@code.launchpad.net

Description of the change

This branch implements the fix for bug 944693. A timed check is added that is set to run 45 seconds after each startup of Software Center (this purpose of the delay is to avoid impacting startup time). The current profile (list of installed apps) is compared to the previous uploaded profile, and if they differ the new profile data is uploaded to the recommender service. If the profile is found to be unchanged since the previous upload, the server upload is skipped.

To post a comment you must log in.
2920. By Gary Lasker

update unit test for the changed submit-profile-finished call

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks, that looks good and works nicely!

Reading the comment about that we should use a singleton made me think a bit and I would like to
propose some tweaks where we might get away without having to use one. But I will ask you for review
on this approach once I added code for it.

review: Approve
Revision history for this message
Gary Lasker (gary-lasker) wrote :

Thanks Michael, that sounds great! I definitely also prefer we only use a single agent object instance, even if we don't enforce it as a singleton (which really should not be necessary). I appreciate your help with this and look forward to seeing your branch!

2921. By Gary Lasker

merged lp:~mvo/software-center/recagent-almost-singleton, very nice improvements, many thanks Michael

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'softwarecenter/backend/recagent.py'
--- softwarecenter/backend/recagent.py 2012-03-19 17:08:56 +0000
+++ softwarecenter/backend/recagent.py 2012-04-02 16:58:19 +0000
@@ -21,6 +21,7 @@
2121
22from gi.repository import GObject22from gi.repository import GObject
23import logging23import logging
24import hashlib
2425
25import softwarecenter.paths26import softwarecenter.paths
26from spawn_helper import SpawnHelper27from spawn_helper import SpawnHelper
@@ -45,7 +46,7 @@
45 ),46 ),
46 "submit-profile-finished": (GObject.SIGNAL_RUN_LAST,47 "submit-profile-finished": (GObject.SIGNAL_RUN_LAST,
47 GObject.TYPE_NONE,48 GObject.TYPE_NONE,
48 (GObject.TYPE_PYOBJECT, str),49 (GObject.TYPE_PYOBJECT,),
49 ),50 ),
50 "submit-anon-profile-finished": (GObject.SIGNAL_RUN_LAST,51 "submit-anon-profile-finished": (GObject.SIGNAL_RUN_LAST,
51 GObject.TYPE_NONE,52 GObject.TYPE_NONE,
@@ -76,7 +77,6 @@
76 def __init__(self, xid=None):77 def __init__(self, xid=None):
77 GObject.GObject.__init__(self)78 GObject.GObject.__init__(self)
78 self.xid = xid79 self.xid = xid
79 self.recommender_uuid = self._get_recommender_uuid()
8080
81 def query_server_status(self):81 def query_server_status(self):
82 # build the command82 # build the command
@@ -88,29 +88,76 @@
88 spawner.run_generic_piston_helper(88 spawner.run_generic_piston_helper(
89 "SoftwareCenterRecommenderAPI", "server_status")89 "SoftwareCenterRecommenderAPI", "server_status")
9090
91 def _calc_profile_id(self, profile):
92 """ Return a profile id (md5 hash of a profile) for the given profile
93 """
94 return hashlib.md5(str(profile)).hexdigest()
95
96 @property
97 def recommender_uuid(self):
98 config = get_config()
99 if config.has_option("general", "recommender_uuid"):
100 recommender_uuid = config.get("general",
101 "recommender_uuid")
102 else:
103 recommender_uuid = ""
104 return recommender_uuid
105
106 @property
107 def recommender_profile_id(self):
108 config = get_config()
109 if config.has_option("general", "recommender_profile_id"):
110 recommender_profile_id = config.get("general",
111 "recommender_profile_id")
112 else:
113 recommender_profile_id = ""
114 return recommender_profile_id
115
116 def _set_recommender_profile_id(self, profile_id):
117 config = get_config()
118 if not config.has_section("general"):
119 config.add_section("general")
120 config.set("general", "recommender_profile_id", profile_id)
121
122 def _set_recommender_uuid(self, uuid):
123 config = get_config()
124 if not config.has_section("general"):
125 config.add_section("general")
126 config.set("general", "recommender_uuid", uuid)
127
91 def post_submit_profile(self, db):128 def post_submit_profile(self, db):
92 """ This will post the users profile to the recommender server129 """ This will post the users profile to the recommender server
93 and also generate the UUID for the user if that is not130 and also generate the UUID for the user if that is not
94 there yet131 there yet
95 """132 """
96 # if we have not already set a recommender UUID, now is the time133 recommender_uuid = self.recommender_uuid
97 # to do it134 if not recommender_uuid:
98 if not self.recommender_uuid:135 # generate a new uuid, but do not save it yet, this will
99 self.recommender_uuid = get_uuid()136 # be done later in _on_submit_profile_data
137 recommender_uuid = get_uuid()
100 installed_pkglist = [app.pkgname138 installed_pkglist = [app.pkgname
101 for app in get_installed_apps_list(db)]139 for app in get_installed_apps_list(db)]
102 data = self._generate_submit_profile_data(self.recommender_uuid,140 profile = self._generate_submit_profile_data(recommender_uuid,
103 installed_pkglist)141 installed_pkglist)
104 # build the command142
105 spawner = SpawnHelper()143 # compare profiles to see if there has been a change, and if there
106 spawner.parent_xid = self.xid144 # has, do the profile update
107 spawner.needs_auth = True145 current_recommender_profile_id = self._calc_profile_id(profile)
108 spawner.connect("data-available", self._on_submit_profile_data)146 if current_recommender_profile_id != self.recommender_profile_id:
109 spawner.connect("error", lambda spawner, err: self.emit("error", err))147 LOG.info("Submitting recommendations profile to the server")
110 spawner.run_generic_piston_helper(148 self._set_recommender_profile_id(current_recommender_profile_id)
111 "SoftwareCenterRecommenderAPI",149 # build the command and upload the profile
112 "submit_profile",150 spawner = SpawnHelper()
113 data=data)151 spawner.parent_xid = self.xid
152 spawner.needs_auth = True
153 spawner.connect("data-available", self._on_submit_profile_data,
154 recommender_uuid)
155 spawner.connect(
156 "error", lambda spawner, err: self.emit("error", err))
157 spawner.run_generic_piston_helper(
158 "SoftwareCenterRecommenderAPI",
159 "submit_profile",
160 data=profile)
114161
115 def post_submit_anon_profile(self, uuid, installed_packages, extra):162 def post_submit_anon_profile(self, uuid, installed_packages, extra):
116 # build the command163 # build the command
@@ -188,7 +235,9 @@
188 return False235 return False
189236
190 def opt_out(self):237 def opt_out(self):
191 self.recommender_uuid = ""238 config = get_config()
239 config.set("general", "recommender_uuid", "")
240 config.set("general", "recommender_profile_id", "")
192241
193 def _on_server_status_data(self, spawner, piston_server_status):242 def _on_server_status_data(self, spawner, piston_server_status):
194 self.emit("server-status", piston_server_status)243 self.emit("server-status", piston_server_status)
@@ -196,13 +245,14 @@
196 def _on_profile_data(self, spawner, piston_profile):245 def _on_profile_data(self, spawner, piston_profile):
197 self.emit("profile", piston_profile)246 self.emit("profile", piston_profile)
198247
199 def _on_submit_profile_data(self, spawner, piston_submit_profile):248 def _on_submit_profile_data(self, spawner, piston_submit_profile,
249 recommender_uuid):
250 self._set_recommender_uuid(recommender_uuid)
200 self.emit("submit-profile-finished",251 self.emit("submit-profile-finished",
201 piston_submit_profile,252 piston_submit_profile)
202 self.recommender_uuid)
203253
204 def _on_submit_anon_profile_data(self, spawner,254 def _on_submit_anon_profile_data(self, spawner,
205 piston_submit_anon_profile):255 piston_submit_anon_profile):
206 self.emit("submit-anon_profile", piston_submit_anon_profile)256 self.emit("submit-anon_profile", piston_submit_anon_profile)
207257
208 def _on_recommend_me_data(self, spawner, piston_me_apps):258 def _on_recommend_me_data(self, spawner, piston_me_apps):
@@ -217,18 +267,6 @@
217 def _on_recommend_top_data(self, spawner, piston_top_apps):267 def _on_recommend_top_data(self, spawner, piston_top_apps):
218 self.emit("recommend-top", piston_top_apps)268 self.emit("recommend-top", piston_top_apps)
219269
220 def _get_recommender_uuid(self):
221 """ returns the recommender UUID value, which can be empty if it
222 has not yet been set (indicating that the user has not yet
223 opted-in to the recommender service)
224 """
225 config = get_config()
226 if config.has_option("general", "recommender_uuid"):
227 recommender_uuid = config.get("general", "recommender_uuid")
228 if recommender_uuid:
229 return recommender_uuid
230 return ""
231
232 def _generate_submit_profile_data(self, recommender_uuid, package_list):270 def _generate_submit_profile_data(self, recommender_uuid, package_list):
233 submit_profile_data = [{271 submit_profile_data = [{
234 'uuid': recommender_uuid,272 'uuid': recommender_uuid,
235273
=== modified file 'softwarecenter/ui/gtk3/app.py'
--- softwarecenter/ui/gtk3/app.py 2012-03-23 09:45:35 +0000
+++ softwarecenter/ui/gtk3/app.py 2012-04-02 16:58:19 +0000
@@ -97,6 +97,7 @@
97from softwarecenter.config import get_config97from softwarecenter.config import get_config
98from softwarecenter.backend import get_install_backend98from softwarecenter.backend import get_install_backend
99from softwarecenter.backend.login_sso import get_sso_backend99from softwarecenter.backend.login_sso import get_sso_backend
100from softwarecenter.backend.recagent import RecommenderAgent
100101
101from softwarecenter.backend.channel import AllInstalledChannel102from softwarecenter.backend.channel import AllInstalledChannel
102from softwarecenter.backend.reviews import get_review_loader, UsefulnessCache103from softwarecenter.backend.reviews import get_review_loader, UsefulnessCache
@@ -269,7 +270,6 @@
269 self.scagent = None270 self.scagent = None
270 self.sso = None271 self.sso = None
271 self.available_for_me_query = None272 self.available_for_me_query = None
272 self.recommender_uuid = ""
273273
274 Gtk.Window.set_default_icon_name("softwarecenter")274 Gtk.Window.set_default_icon_name("softwarecenter")
275275
@@ -414,6 +414,10 @@
414 # keep the cache clean414 # keep the cache clean
415 GObject.timeout_add_seconds(15, self._run_expunge_cache_helper)415 GObject.timeout_add_seconds(15, self._run_expunge_cache_helper)
416416
417 # check to see if a new recommendations profile upload is
418 # needed and upload if necessary
419 GObject.timeout_add_seconds(45, self._upload_recommendations_profile)
420
417 # TODO: Remove the following two lines once we have remove repository421 # TODO: Remove the following two lines once we have remove repository
418 # support in aptdaemon (see LP: #723911)422 # support in aptdaemon (see LP: #723911)
419 self.menu_file.remove(self.menuitem_deauthorize_computer)423 self.menu_file.remove(self.menuitem_deauthorize_computer)
@@ -493,9 +497,8 @@
493497
494 def on_available_pane_created(self, widget):498 def on_available_pane_created(self, widget):
495 self.available_pane.searchentry.grab_focus()499 self.available_pane.searchentry.grab_focus()
496 rec_panel = self.available_pane.cat_view.recommended_for_you_panel
497 self._update_recommendations_menuitem(500 self._update_recommendations_menuitem(
498 opted_in=rec_panel.recommender_agent.is_opted_in())501 opted_in=self._get_recommender_agent().is_opted_in())
499 # connect a signal to monitor the recommendations opt-in state and502 # connect a signal to monitor the recommendations opt-in state and
500 # persist the recommendations uuid on an opt-in503 # persist the recommendations uuid on an opt-in
501 self.available_pane.cat_view.recommended_for_you_panel.connect(504 self.available_pane.cat_view.recommended_for_you_panel.connect(
@@ -509,14 +512,10 @@
509 #~ def on_installed_pane_created(self, widget):512 #~ def on_installed_pane_created(self, widget):
510 #~ pass513 #~ pass
511514
512 def _on_recommendations_opt_in(self, rec_panel, recommender_uuid):515 def _on_recommendations_opt_in(self, rec_panel):
513 self.recommender_uuid = recommender_uuid
514 self._update_recommendations_menuitem(opted_in=True)516 self._update_recommendations_menuitem(opted_in=True)
515517
516 def _on_recommendations_opt_out(self, rec_panel):518 def _on_recommendations_opt_out(self, rec_panel):
517 # if the user opts back out of the recommender service, we
518 # reset the recommender UUID to indicate it
519 self.recommender_uuid = ""
520 self._update_recommendations_menuitem(opted_in=False)519 self._update_recommendations_menuitem(opted_in=False)
521520
522 def _update_recommendations_menuitem(self, opted_in):521 def _update_recommendations_menuitem(self, opted_in):
@@ -527,6 +526,16 @@
527 self.menuitem_recommendations.set_label(526 self.menuitem_recommendations.set_label(
528 _(u"Turn On Recommendations…"))527 _(u"Turn On Recommendations…"))
529528
529 def _upload_recommendations_profile(self):
530 recommender_agent = self._get_recommender_agent()
531 if recommender_agent.is_opted_in():
532 recommender_agent.post_submit_profile(self.db)
533
534 def _get_recommender_agent(self):
535 if not hasattr(self, "_recommender_agent"):
536 self._recommender_agent = RecommenderAgent()
537 return self._recommender_agent
538
530 def _on_update_software_center_agent_finished(self, pid, condition):539 def _on_update_software_center_agent_finished(self, pid, condition):
531 LOG.info("software-center-agent finished with status %i" %540 LOG.info("software-center-agent finished with status %i" %
532 os.WEXITSTATUS(condition))541 os.WEXITSTATUS(condition))
@@ -780,7 +789,7 @@
780789
781 def on_menuitem_recommendations_activate(self, menu_item):790 def on_menuitem_recommendations_activate(self, menu_item):
782 rec_panel = self.available_pane.cat_view.recommended_for_you_panel791 rec_panel = self.available_pane.cat_view.recommended_for_you_panel
783 if rec_panel.recommender_agent.is_opted_in():792 if self._get_recommender_agent().is_opted_in():
784 rec_panel.opt_out_of_recommendations_service()793 rec_panel.opt_out_of_recommendations_service()
785 else:794 else:
786 # build and show the opt-in dialog795 # build and show the opt-in dialog
@@ -1294,9 +1303,6 @@
1294 else:1303 else:
1295 # initial default state is to add to launcher, per spec1304 # initial default state is to add to launcher, per spec
1296 self.available_pane.add_to_launcher_enabled = True1305 self.available_pane.add_to_launcher_enabled = True
1297 if self.config.has_option("general", "recommender_uuid"):
1298 self.recommender_uuid = self.config.get("general",
1299 "recommender_uuid")
13001306
1301 def save_state(self):1307 def save_state(self):
1302 LOG.debug("save_state")1308 LOG.debug("save_state")
@@ -1318,9 +1324,13 @@
1318 self.config.set("general", "add_to_launcher", "True")1324 self.config.set("general", "add_to_launcher", "True")
1319 else:1325 else:
1320 self.config.set("general", "add_to_launcher", "False")1326 self.config.set("general", "add_to_launcher", "False")
1327 # store the recommender values
1321 self.config.set("general",1328 self.config.set("general",
1322 "recommender_uuid",1329 "recommender_uuid",
1323 self.recommender_uuid)1330 self._get_recommender_agent().recommender_uuid)
1331 self.config.set("general",
1332 "recommender_profile_id",
1333 self._get_recommender_agent().recommender_profile_id)
1324 self.config.write()1334 self.config.write()
13251335
1326 def run(self, args):1336 def run(self, args):
13271337
=== modified file 'softwarecenter/ui/gtk3/widgets/recommendations.py'
--- softwarecenter/ui/gtk3/widgets/recommendations.py 2012-03-20 10:28:51 +0000
+++ softwarecenter/ui/gtk3/widgets/recommendations.py 2012-04-02 16:58:19 +0000
@@ -129,7 +129,7 @@
129 __gsignals__ = {129 __gsignals__ = {
130 "recommendations-opt-in": (GObject.SIGNAL_RUN_LAST,130 "recommendations-opt-in": (GObject.SIGNAL_RUN_LAST,
131 GObject.TYPE_NONE,131 GObject.TYPE_NONE,
132 (GObject.TYPE_STRING,),132 (),
133 ),133 ),
134 "recommendations-opt-out": (GObject.SIGNAL_RUN_LAST,134 "recommendations-opt-out": (GObject.SIGNAL_RUN_LAST,
135 GObject.TYPE_NONE,135 GObject.TYPE_NONE,
@@ -202,13 +202,7 @@
202 self.remove_more_button()202 self.remove_more_button()
203 self.show_all()203 self.show_all()
204 self.emit("recommendations-opt-out")204 self.emit("recommendations-opt-out")
205 try:205 self._disconnect_recommender_listeners()
206 self.recommender_agent.disconnect_by_func(
207 self._on_profile_submitted)
208 self.recommender_agent.disconnect_by_func(
209 self._on_profile_submitted_error)
210 except TypeError:
211 pass
212206
213 def _upload_user_profile_and_get_recommendations(self):207 def _upload_user_profile_and_get_recommendations(self):
214 # initiate upload of the user profile here208 # initiate upload of the user profile here
@@ -222,20 +216,35 @@
222 self._on_profile_submitted_error)216 self._on_profile_submitted_error)
223 self.recommender_agent.post_submit_profile(self.catview.db)217 self.recommender_agent.post_submit_profile(self.catview.db)
224218
225 def _on_profile_submitted(self, agent, profile, recommender_uuid):219 def _on_profile_submitted(self, agent, profile):
226 # after the user profile data has been uploaded, make the request220 # after the user profile data has been uploaded, make the request
227 # and load the the recommended_for_you content221 # and load the the recommended_for_you content
228 LOG.debug("The recommendations profile has been successfully "222 LOG.debug("The updated profile was successfully submitted to the "
229 "submitted to the recommender agent")223 "recommender service")
230 self.emit("recommendations-opt-in", recommender_uuid)224 # only detect the very first profile upload as that indicates
225 # the user's initial opt-in
231 self._update_recommended_for_you_content()226 self._update_recommended_for_you_content()
227 self._disconnect_recommender_listeners()
228 self.emit("recommendations-opt-in")
232229
233 def _on_profile_submitted_error(self, agent, msg):230 def _on_profile_submitted_error(self, agent, msg):
234 LOG.warn("Error while submitting the recommendations profile to the "231 LOG.warn("Error while submitting the recommendations profile to the "
235 "recommender agent: %s" % msg)232 "recommender agent: %s" % msg)
236 # TODO: handle this! display an error message in the panel233 # TODO: handle this! display an error message in the panel
234 # detect the very first profile upload as that indicates
235 # the user's initial opt-in
236 self._disconnect_recommender_listeners()
237 self._hide_recommended_for_you_panel()237 self._hide_recommended_for_you_panel()
238238
239 def _disconnect_recommender_listeners(self):
240 try:
241 self.recommender_agent.disconnect_by_func(
242 self._on_profile_submitted)
243 self.recommender_agent.disconnect_by_func(
244 self._on_profile_submitted_error)
245 except TypeError:
246 pass
247
239248
240class RecommendationsPanelDetails(RecommendationsPanel):249class RecommendationsPanelDetails(RecommendationsPanel):
241 """250 """
242251
=== modified file 'test/test_recagent.py'
--- test/test_recagent.py 2012-03-20 09:27:18 +0000
+++ test/test_recagent.py 2012-04-02 16:58:19 +0000
@@ -3,7 +3,6 @@
3from gi.repository import GObject3from gi.repository import GObject
4import unittest4import unittest
5import os5import os
6import uuid
76
8from mock import patch7from mock import patch
98
@@ -40,20 +39,19 @@
40 def _patched_on_submit_profile_data(*args, **kwargs):39 def _patched_on_submit_profile_data(*args, **kwargs):
41 piston_submit_profile = {}40 piston_submit_profile = {}
42 recommender_agent.emit("submit-profile-finished", 41 recommender_agent.emit("submit-profile-finished",
43 piston_submit_profile, 42 piston_submit_profile)
44 uuid.uuid1())
45 mock_spawn_helper_run.side_effect = _patched_on_submit_profile_data43 mock_spawn_helper_run.side_effect = _patched_on_submit_profile_data
46 recommender_agent = RecommenderAgent()44 recommender_agent = RecommenderAgent()
47 recommender_agent.connect("submit-profile-finished", self.on_query_done)45 recommender_agent.connect("submit-profile-finished", self.on_query_done)
48 recommender_agent.connect("error", self.on_query_error)46 recommender_agent.connect("error", self.on_query_error)
47 recommender_agent._calc_profile_id = lambda profile: "i-am-random"
49 db = get_test_db()48 db = get_test_db()
50 recommender_agent.post_submit_profile(db)49 recommender_agent.post_submit_profile(db)
51 self.assertFalse(self.error)50 self.assertFalse(self.error)
52 args, kwargs = mock_spawn_helper_run.call_args51 args, kwargs = mock_spawn_helper_run.call_args
53 self.assertNotEqual(kwargs['data'][0]['uuid'], None)
54 self.assertNotEqual(kwargs['data'][0]['package_list'], [])52 self.assertNotEqual(kwargs['data'][0]['package_list'], [])
5553
56 def on_query_done(self, recagent, data, uuid=""):54 def on_query_done(self, recagent, data):
57 print "query done, data: '%s'" % data55 print "query done, data: '%s'" % data
58 self.loop.quit()56 self.loop.quit()
59 57

Subscribers

People subscribed via source and target branches