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 on 2012-04-02

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

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
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 on 2012-04-02

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
1=== modified file 'softwarecenter/backend/recagent.py'
2--- softwarecenter/backend/recagent.py 2012-03-19 17:08:56 +0000
3+++ softwarecenter/backend/recagent.py 2012-04-02 16:58:19 +0000
4@@ -21,6 +21,7 @@
5
6 from gi.repository import GObject
7 import logging
8+import hashlib
9
10 import softwarecenter.paths
11 from spawn_helper import SpawnHelper
12@@ -45,7 +46,7 @@
13 ),
14 "submit-profile-finished": (GObject.SIGNAL_RUN_LAST,
15 GObject.TYPE_NONE,
16- (GObject.TYPE_PYOBJECT, str),
17+ (GObject.TYPE_PYOBJECT,),
18 ),
19 "submit-anon-profile-finished": (GObject.SIGNAL_RUN_LAST,
20 GObject.TYPE_NONE,
21@@ -76,7 +77,6 @@
22 def __init__(self, xid=None):
23 GObject.GObject.__init__(self)
24 self.xid = xid
25- self.recommender_uuid = self._get_recommender_uuid()
26
27 def query_server_status(self):
28 # build the command
29@@ -88,29 +88,76 @@
30 spawner.run_generic_piston_helper(
31 "SoftwareCenterRecommenderAPI", "server_status")
32
33+ def _calc_profile_id(self, profile):
34+ """ Return a profile id (md5 hash of a profile) for the given profile
35+ """
36+ return hashlib.md5(str(profile)).hexdigest()
37+
38+ @property
39+ def recommender_uuid(self):
40+ config = get_config()
41+ if config.has_option("general", "recommender_uuid"):
42+ recommender_uuid = config.get("general",
43+ "recommender_uuid")
44+ else:
45+ recommender_uuid = ""
46+ return recommender_uuid
47+
48+ @property
49+ def recommender_profile_id(self):
50+ config = get_config()
51+ if config.has_option("general", "recommender_profile_id"):
52+ recommender_profile_id = config.get("general",
53+ "recommender_profile_id")
54+ else:
55+ recommender_profile_id = ""
56+ return recommender_profile_id
57+
58+ def _set_recommender_profile_id(self, profile_id):
59+ config = get_config()
60+ if not config.has_section("general"):
61+ config.add_section("general")
62+ config.set("general", "recommender_profile_id", profile_id)
63+
64+ def _set_recommender_uuid(self, uuid):
65+ config = get_config()
66+ if not config.has_section("general"):
67+ config.add_section("general")
68+ config.set("general", "recommender_uuid", uuid)
69+
70 def post_submit_profile(self, db):
71 """ This will post the users profile to the recommender server
72 and also generate the UUID for the user if that is not
73 there yet
74 """
75- # if we have not already set a recommender UUID, now is the time
76- # to do it
77- if not self.recommender_uuid:
78- self.recommender_uuid = get_uuid()
79+ recommender_uuid = self.recommender_uuid
80+ if not recommender_uuid:
81+ # generate a new uuid, but do not save it yet, this will
82+ # be done later in _on_submit_profile_data
83+ recommender_uuid = get_uuid()
84 installed_pkglist = [app.pkgname
85 for app in get_installed_apps_list(db)]
86- data = self._generate_submit_profile_data(self.recommender_uuid,
87- installed_pkglist)
88- # build the command
89- spawner = SpawnHelper()
90- spawner.parent_xid = self.xid
91- spawner.needs_auth = True
92- spawner.connect("data-available", self._on_submit_profile_data)
93- spawner.connect("error", lambda spawner, err: self.emit("error", err))
94- spawner.run_generic_piston_helper(
95- "SoftwareCenterRecommenderAPI",
96- "submit_profile",
97- data=data)
98+ profile = self._generate_submit_profile_data(recommender_uuid,
99+ installed_pkglist)
100+
101+ # compare profiles to see if there has been a change, and if there
102+ # has, do the profile update
103+ current_recommender_profile_id = self._calc_profile_id(profile)
104+ if current_recommender_profile_id != self.recommender_profile_id:
105+ LOG.info("Submitting recommendations profile to the server")
106+ self._set_recommender_profile_id(current_recommender_profile_id)
107+ # build the command and upload the profile
108+ spawner = SpawnHelper()
109+ spawner.parent_xid = self.xid
110+ spawner.needs_auth = True
111+ spawner.connect("data-available", self._on_submit_profile_data,
112+ recommender_uuid)
113+ spawner.connect(
114+ "error", lambda spawner, err: self.emit("error", err))
115+ spawner.run_generic_piston_helper(
116+ "SoftwareCenterRecommenderAPI",
117+ "submit_profile",
118+ data=profile)
119
120 def post_submit_anon_profile(self, uuid, installed_packages, extra):
121 # build the command
122@@ -188,7 +235,9 @@
123 return False
124
125 def opt_out(self):
126- self.recommender_uuid = ""
127+ config = get_config()
128+ config.set("general", "recommender_uuid", "")
129+ config.set("general", "recommender_profile_id", "")
130
131 def _on_server_status_data(self, spawner, piston_server_status):
132 self.emit("server-status", piston_server_status)
133@@ -196,13 +245,14 @@
134 def _on_profile_data(self, spawner, piston_profile):
135 self.emit("profile", piston_profile)
136
137- def _on_submit_profile_data(self, spawner, piston_submit_profile):
138+ def _on_submit_profile_data(self, spawner, piston_submit_profile,
139+ recommender_uuid):
140+ self._set_recommender_uuid(recommender_uuid)
141 self.emit("submit-profile-finished",
142- piston_submit_profile,
143- self.recommender_uuid)
144+ piston_submit_profile)
145
146 def _on_submit_anon_profile_data(self, spawner,
147- piston_submit_anon_profile):
148+ piston_submit_anon_profile):
149 self.emit("submit-anon_profile", piston_submit_anon_profile)
150
151 def _on_recommend_me_data(self, spawner, piston_me_apps):
152@@ -217,18 +267,6 @@
153 def _on_recommend_top_data(self, spawner, piston_top_apps):
154 self.emit("recommend-top", piston_top_apps)
155
156- def _get_recommender_uuid(self):
157- """ returns the recommender UUID value, which can be empty if it
158- has not yet been set (indicating that the user has not yet
159- opted-in to the recommender service)
160- """
161- config = get_config()
162- if config.has_option("general", "recommender_uuid"):
163- recommender_uuid = config.get("general", "recommender_uuid")
164- if recommender_uuid:
165- return recommender_uuid
166- return ""
167-
168 def _generate_submit_profile_data(self, recommender_uuid, package_list):
169 submit_profile_data = [{
170 'uuid': recommender_uuid,
171
172=== modified file 'softwarecenter/ui/gtk3/app.py'
173--- softwarecenter/ui/gtk3/app.py 2012-03-23 09:45:35 +0000
174+++ softwarecenter/ui/gtk3/app.py 2012-04-02 16:58:19 +0000
175@@ -97,6 +97,7 @@
176 from softwarecenter.config import get_config
177 from softwarecenter.backend import get_install_backend
178 from softwarecenter.backend.login_sso import get_sso_backend
179+from softwarecenter.backend.recagent import RecommenderAgent
180
181 from softwarecenter.backend.channel import AllInstalledChannel
182 from softwarecenter.backend.reviews import get_review_loader, UsefulnessCache
183@@ -269,7 +270,6 @@
184 self.scagent = None
185 self.sso = None
186 self.available_for_me_query = None
187- self.recommender_uuid = ""
188
189 Gtk.Window.set_default_icon_name("softwarecenter")
190
191@@ -414,6 +414,10 @@
192 # keep the cache clean
193 GObject.timeout_add_seconds(15, self._run_expunge_cache_helper)
194
195+ # check to see if a new recommendations profile upload is
196+ # needed and upload if necessary
197+ GObject.timeout_add_seconds(45, self._upload_recommendations_profile)
198+
199 # TODO: Remove the following two lines once we have remove repository
200 # support in aptdaemon (see LP: #723911)
201 self.menu_file.remove(self.menuitem_deauthorize_computer)
202@@ -493,9 +497,8 @@
203
204 def on_available_pane_created(self, widget):
205 self.available_pane.searchentry.grab_focus()
206- rec_panel = self.available_pane.cat_view.recommended_for_you_panel
207 self._update_recommendations_menuitem(
208- opted_in=rec_panel.recommender_agent.is_opted_in())
209+ opted_in=self._get_recommender_agent().is_opted_in())
210 # connect a signal to monitor the recommendations opt-in state and
211 # persist the recommendations uuid on an opt-in
212 self.available_pane.cat_view.recommended_for_you_panel.connect(
213@@ -509,14 +512,10 @@
214 #~ def on_installed_pane_created(self, widget):
215 #~ pass
216
217- def _on_recommendations_opt_in(self, rec_panel, recommender_uuid):
218- self.recommender_uuid = recommender_uuid
219+ def _on_recommendations_opt_in(self, rec_panel):
220 self._update_recommendations_menuitem(opted_in=True)
221
222 def _on_recommendations_opt_out(self, rec_panel):
223- # if the user opts back out of the recommender service, we
224- # reset the recommender UUID to indicate it
225- self.recommender_uuid = ""
226 self._update_recommendations_menuitem(opted_in=False)
227
228 def _update_recommendations_menuitem(self, opted_in):
229@@ -527,6 +526,16 @@
230 self.menuitem_recommendations.set_label(
231 _(u"Turn On Recommendations…"))
232
233+ def _upload_recommendations_profile(self):
234+ recommender_agent = self._get_recommender_agent()
235+ if recommender_agent.is_opted_in():
236+ recommender_agent.post_submit_profile(self.db)
237+
238+ def _get_recommender_agent(self):
239+ if not hasattr(self, "_recommender_agent"):
240+ self._recommender_agent = RecommenderAgent()
241+ return self._recommender_agent
242+
243 def _on_update_software_center_agent_finished(self, pid, condition):
244 LOG.info("software-center-agent finished with status %i" %
245 os.WEXITSTATUS(condition))
246@@ -780,7 +789,7 @@
247
248 def on_menuitem_recommendations_activate(self, menu_item):
249 rec_panel = self.available_pane.cat_view.recommended_for_you_panel
250- if rec_panel.recommender_agent.is_opted_in():
251+ if self._get_recommender_agent().is_opted_in():
252 rec_panel.opt_out_of_recommendations_service()
253 else:
254 # build and show the opt-in dialog
255@@ -1294,9 +1303,6 @@
256 else:
257 # initial default state is to add to launcher, per spec
258 self.available_pane.add_to_launcher_enabled = True
259- if self.config.has_option("general", "recommender_uuid"):
260- self.recommender_uuid = self.config.get("general",
261- "recommender_uuid")
262
263 def save_state(self):
264 LOG.debug("save_state")
265@@ -1318,9 +1324,13 @@
266 self.config.set("general", "add_to_launcher", "True")
267 else:
268 self.config.set("general", "add_to_launcher", "False")
269+ # store the recommender values
270 self.config.set("general",
271 "recommender_uuid",
272- self.recommender_uuid)
273+ self._get_recommender_agent().recommender_uuid)
274+ self.config.set("general",
275+ "recommender_profile_id",
276+ self._get_recommender_agent().recommender_profile_id)
277 self.config.write()
278
279 def run(self, args):
280
281=== modified file 'softwarecenter/ui/gtk3/widgets/recommendations.py'
282--- softwarecenter/ui/gtk3/widgets/recommendations.py 2012-03-20 10:28:51 +0000
283+++ softwarecenter/ui/gtk3/widgets/recommendations.py 2012-04-02 16:58:19 +0000
284@@ -129,7 +129,7 @@
285 __gsignals__ = {
286 "recommendations-opt-in": (GObject.SIGNAL_RUN_LAST,
287 GObject.TYPE_NONE,
288- (GObject.TYPE_STRING,),
289+ (),
290 ),
291 "recommendations-opt-out": (GObject.SIGNAL_RUN_LAST,
292 GObject.TYPE_NONE,
293@@ -202,13 +202,7 @@
294 self.remove_more_button()
295 self.show_all()
296 self.emit("recommendations-opt-out")
297- try:
298- self.recommender_agent.disconnect_by_func(
299- self._on_profile_submitted)
300- self.recommender_agent.disconnect_by_func(
301- self._on_profile_submitted_error)
302- except TypeError:
303- pass
304+ self._disconnect_recommender_listeners()
305
306 def _upload_user_profile_and_get_recommendations(self):
307 # initiate upload of the user profile here
308@@ -222,20 +216,35 @@
309 self._on_profile_submitted_error)
310 self.recommender_agent.post_submit_profile(self.catview.db)
311
312- def _on_profile_submitted(self, agent, profile, recommender_uuid):
313+ def _on_profile_submitted(self, agent, profile):
314 # after the user profile data has been uploaded, make the request
315 # and load the the recommended_for_you content
316- LOG.debug("The recommendations profile has been successfully "
317- "submitted to the recommender agent")
318- self.emit("recommendations-opt-in", recommender_uuid)
319+ LOG.debug("The updated profile was successfully submitted to the "
320+ "recommender service")
321+ # only detect the very first profile upload as that indicates
322+ # the user's initial opt-in
323 self._update_recommended_for_you_content()
324+ self._disconnect_recommender_listeners()
325+ self.emit("recommendations-opt-in")
326
327 def _on_profile_submitted_error(self, agent, msg):
328 LOG.warn("Error while submitting the recommendations profile to the "
329 "recommender agent: %s" % msg)
330 # TODO: handle this! display an error message in the panel
331+ # detect the very first profile upload as that indicates
332+ # the user's initial opt-in
333+ self._disconnect_recommender_listeners()
334 self._hide_recommended_for_you_panel()
335
336+ def _disconnect_recommender_listeners(self):
337+ try:
338+ self.recommender_agent.disconnect_by_func(
339+ self._on_profile_submitted)
340+ self.recommender_agent.disconnect_by_func(
341+ self._on_profile_submitted_error)
342+ except TypeError:
343+ pass
344+
345
346 class RecommendationsPanelDetails(RecommendationsPanel):
347 """
348
349=== modified file 'test/test_recagent.py'
350--- test/test_recagent.py 2012-03-20 09:27:18 +0000
351+++ test/test_recagent.py 2012-04-02 16:58:19 +0000
352@@ -3,7 +3,6 @@
353 from gi.repository import GObject
354 import unittest
355 import os
356-import uuid
357
358 from mock import patch
359
360@@ -40,20 +39,19 @@
361 def _patched_on_submit_profile_data(*args, **kwargs):
362 piston_submit_profile = {}
363 recommender_agent.emit("submit-profile-finished",
364- piston_submit_profile,
365- uuid.uuid1())
366+ piston_submit_profile)
367 mock_spawn_helper_run.side_effect = _patched_on_submit_profile_data
368 recommender_agent = RecommenderAgent()
369 recommender_agent.connect("submit-profile-finished", self.on_query_done)
370 recommender_agent.connect("error", self.on_query_error)
371+ recommender_agent._calc_profile_id = lambda profile: "i-am-random"
372 db = get_test_db()
373 recommender_agent.post_submit_profile(db)
374 self.assertFalse(self.error)
375 args, kwargs = mock_spawn_helper_run.call_args
376- self.assertNotEqual(kwargs['data'][0]['uuid'], None)
377 self.assertNotEqual(kwargs['data'][0]['package_list'], [])
378
379- def on_query_done(self, recagent, data, uuid=""):
380+ def on_query_done(self, recagent, data):
381 print "query done, data: '%s'" % data
382 self.loop.quit()
383

Subscribers

People subscribed via source and target branches