Merge lp:~gary-lasker/software-center/recommended-installed-feedback into lp:software-center

Proposed by Gary Lasker
Status: Merged
Merged at revision: 3153
Proposed branch: lp:~gary-lasker/software-center/recommended-installed-feedback
Merge into: lp:software-center
Prerequisite: lp:~gary-lasker/software-center/update-to-latest-recommender-client
Diff against target: 942 lines (+356/-133)
11 files modified
softwarecenter/backend/piston/sreclient_pristine.py (+1/-1)
softwarecenter/backend/recagent.py (+26/-1)
softwarecenter/enums.py (+4/-0)
softwarecenter/ui/gtk3/panes/availablepane.py (+3/-10)
softwarecenter/ui/gtk3/views/appdetailsview.py (+7/-7)
softwarecenter/ui/gtk3/views/catview_gtk.py (+59/-39)
softwarecenter/ui/gtk3/widgets/containers.py (+41/-17)
softwarecenter/ui/gtk3/widgets/recommendations.py (+79/-29)
tests/gtk3/test_catview.py (+117/-21)
tests/gtk3/windows.py (+9/-8)
tests/test_recagent.py (+10/-0)
To merge this branch: bzr merge lp:~gary-lasker/software-center/recommended-installed-feedback
Reviewer Review Type Date Requested Status
Michael Vogt Approve
Review via email: mp+122154@code.launchpad.net

This proposal supersedes a proposal from 2012-08-28.

Commit message

* lp:~gary-lasker/software-center/recommended-installed-feedback:
   - signal the recommender service when a recommended item has been
     successfully installed, refactor and clean up surrounding code,
     new unit tests for the feature (LP: #944060, LP: #1044107)

Description of the change

This branch implements the "implicit" recommender feedback feature. It detects the case where the user successfully installs a recommended item and fires a "submit_implicit_feedback" call to notify the recommender of the installed package.

This is the client-side implementation for bug 944060.

Note that this branch depends on the lp:~gary-lasker/software-center/update-to-latest-recommender-client branch as that branch adds the new recommender client code from lp:~canonical-ca-hackers/ubuntu-recommender/ubuntu-recommender-client. Also note that I made one small change to sreclient to fix the validate decorator for the pkgname argument to implicit_feedback so that it matches the validator for the other calls that use pkgname as an argument.

Finally, I added a unit test for the new call in test_recagent.py, however like a few of the other tests there, the test is not currently working with the server. Therefore, it remains disabled as the other tests are. I'll look at this tomorrow to try to see what the issue is with these tests.

Meanwhile, everything here should be working. To test, just select any recommended item in the lobby or category views and install it. Once the installation is complete, the corresponding implicit_feedback call will be fired.

Many thanks for your review!

To post a comment you must log in.
Revision history for this message
Michael Vogt (mvo) wrote : Posted in a previous version of this proposal
Download full text (5.3 KiB)

On Tue, Aug 28, 2012 at 05:46:23AM -0000, Gary Lasker wrote:
> Gary Lasker has proposed merging lp:~gary-lasker/software-center/recommended-installed-feedback into lp:software-center with lp:~gary-lasker/software-center/update-to-latest-recommender-client as a prerequisite.

Thanks for working on this branch. I'm very happy to see this feature
coming.

[..]
> Note that this branch depends on the lp:~gary-lasker/software-center/update-to-latest-recommender-client branch as that branch adds the new recommender client code from lp:~canonical-ca-hackers/ubuntu-recommender/ubuntu-recommender-client. Also note that I made one small change to sreclient to fix the validate decorator for the pkgname argument to implicit_feedback so that it matches the validator for the other calls that use pkgname as an argument.

Thanks for this, if you fixed the sreclient_pristine.py, please also
send the diff to the server team (if you haven't done already) so that
they add it to their
lp:~canonical-ca-hackers/ubuntu-recommender/ubuntu-recommender-client
branch.

> Finally, I added a unit test for the new call in test_recagent.py, however like a few of the other tests there, the test is not currently working with the server. Therefore, it remains disabled as the other tests are. I'll look at this tomorrow to try to see what the issue is with these tests.
[..]

Maybe the staging server had problems. I just tested enabling them and
its working, but that may well be that its now fixed. While looking at
the test code I noticed that there is a bit of duplication in
there, I pushed a branch at
lp:~mvo/software-center/recagent-test-cleanup that removes most of
that.

I also noticed that the branch is missing a test for the new
functionality, it would be nice to add one to
tests/gtk3/test_recommendations_widget.py. Something like activate a
app, trigger a install transaction and verify that on finish the
self.recommender_agent.post_implicit_feedback() call was done.

> @@ -244,7 +262,7 @@
>
> def _on_submit_anon_profile_data(self, spawner,
> piston_submit_anon_profile):
> - self.emit("submit-anon_profile", piston_submit_anon_profile)
> + self.emit("submit-anon-profile-finished", piston_submit_anon_profile)

Nice that this is more consistent now (plus that it will actually work).

> === modified file 'softwarecenter/enums.py'
> --- softwarecenter/enums.py 2012-08-23 14:37:28 +0000
> +++ softwarecenter/enums.py 2012-08-28 05:45:23 +0000
> @@ -299,6 +299,9 @@
> LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT = 9
> DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT = 4
>
> +# action values for recommendations implicit feedback
> +RECOMMENDATIONS_FEEDBACK_INSTALLED = "installed"
> +

That looks like we could use:

class RecommenderFeedback:
      INSTALLED = "installed"

(so that later "REMOVED" can be added there as well and its more
consitent with the rest of enums.py).

[..]
> def _on_application_activated(self, catview, app):
> self.emit("application-activated", app)
> + # we only track installed items of the user has opted-in to the
> + # recommendations service
> + if self.recommender_agent.i...

Read more...

Revision history for this message
Gary Lasker (gary-lasker) wrote : Posted in a previous version of this proposal

Just a note that I have a branch for review with the recommender-client changes that I mentioned above (and that have been made to sreclient_pristine.py). The merge proposal is here:

  https://code.launchpad.net/~gary-lasker/ubuntu-recommender/ubuntu-recommender-client-implicit-feedback-tweaks/+merge/121730

Revision history for this message
Gary Lasker (gary-lasker) wrote : Posted in a previous version of this proposal
Download full text (7.3 KiB)

> On Tue, Aug 28, 2012 at 05:46:23AM -0000, Gary Lasker wrote:
> > Gary Lasker has proposed merging lp:~gary-lasker/software-center
> /recommended-installed-feedback into lp:software-center with lp:~gary-lasker
> /software-center/update-to-latest-recommender-client as a prerequisite.
>
> Thanks for working on this branch. I'm very happy to see this feature
> coming.
>
> [..]
> > Note that this branch depends on the lp:~gary-lasker/software-center/update-
> to-latest-recommender-client branch as that branch adds the new recommender
> client code from lp:~canonical-ca-hackers/ubuntu-recommender/ubuntu-
> recommender-client. Also note that I made one small change to sreclient to fix
> the validate decorator for the pkgname argument to implicit_feedback so that
> it matches the validator for the other calls that use pkgname as an argument.
>
> Thanks for this, if you fixed the sreclient_pristine.py, please also
> send the diff to the server team (if you haven't done already) so that
> they add it to their
> lp:~canonical-ca-hackers/ubuntu-recommender/ubuntu-recommender-client
> branch.

Yep, I proposed a branch for this and it has been merged by Lukasz.

>
> > Finally, I added a unit test for the new call in test_recagent.py, however
> like a few of the other tests there, the test is not currently working with
> the server. Therefore, it remains disabled as the other tests are. I'll look
> at this tomorrow to try to see what the issue is with these tests.
> [..]
>
> Maybe the staging server had problems. I just tested enabling them and
> its working, but that may well be that its now fixed. While looking at
> the test code I noticed that there is a bit of duplication in
> there, I pushed a branch at
> lp:~mvo/software-center/recagent-test-cleanup that removes most of
> that.
>
> I also noticed that the branch is missing a test for the new
> functionality, it would be nice to add one to
> tests/gtk3/test_recommendations_widget.py. Something like activate a
> app, trigger a install transaction and verify that on finish the
> self.recommender_agent.post_implicit_feedback() call was done.
>

I added a thorough test for this functionality in test_catview.py (this is where the other recommendations panel tests are, and imo it's consistent because the tests for the other lobby panels (What's New and Top Rated) are also located here. While in test_catview.py I did some cleanup and consolidation of duplicated code as well.

> > @@ -244,7 +262,7 @@
> >
> > def _on_submit_anon_profile_data(self, spawner,
> > piston_submit_anon_profile):
> > - self.emit("submit-anon_profile", piston_submit_anon_profile)
> > + self.emit("submit-anon-profile-finished",
> piston_submit_anon_profile)
>
> Nice that this is more consistent now (plus that it will actually work).
>
> > === modified file 'softwarecenter/enums.py'
> > --- softwarecenter/enums.py 2012-08-23 14:37:28 +0000
> > +++ softwarecenter/enums.py 2012-08-28 05:45:23 +0000
> > @@ -299,6 +299,9 @@
> > LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT = 9
> > DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT = 4
> >
> > +# action values for recommendations implicit f...

Read more...

Revision history for this message
Gary Lasker (gary-lasker) wrote : Posted in a previous version of this proposal

Oops, I noticed a typo in my last comment:

"...when a person clicks on a recommended item, we remember that they did that for that item for as long as the current USC session is active (it is now, however, persisted between sessions, I think would be a mistake)."

Should be (see the "*not*"):

"...when a person clicks on a recommended item, we remember that they did that for that item for as long as the current USC session is active (it is *not*, however, persisted between sessions, I think would be a mistake)."

Though maybe this was obvious, but I wanted to make sure that I'm clear.

Thanks again!

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

What the heck? Guess I won't "resubmit" a MP again. :/

Revision history for this message
Michael Vogt (mvo) wrote : Posted in a previous version of this proposal
Download full text (7.0 KiB)

On Thu, Aug 30, 2012 at 09:25:21PM -0000, Gary Lasker wrote:
> > On Tue, Aug 28, 2012 at 05:46:23AM -0000, Gary Lasker wrote:
> > > Gary Lasker has proposed merging lp:~gary-lasker/software-center
> > /recommended-installed-feedback into lp:software-center with lp:~gary-lasker
> > /software-center/update-to-latest-recommender-client as a prerequisite.
> >
> > Thanks for working on this branch. I'm very happy to see this feature
> > coming.
> >
> > [..]
> > > Note that this branch depends on the lp:~gary-lasker/software-center/update-
> > to-latest-recommender-client branch as that branch adds the new recommender
> > client code from lp:~canonical-ca-hackers/ubuntu-recommender/ubuntu-
> > recommender-client. Also note that I made one small change to sreclient to fix
> > the validate decorator for the pkgname argument to implicit_feedback so that
> > it matches the validator for the other calls that use pkgname as an argument.
> >
> > Thanks for this, if you fixed the sreclient_pristine.py, please also
> > send the diff to the server team (if you haven't done already) so that
> > they add it to their
> > lp:~canonical-ca-hackers/ubuntu-recommender/ubuntu-recommender-client
> > branch.
>
> Yep, I proposed a branch for this and it has been merged by Lukasz.

Great, thanks for this.

> > > Finally, I added a unit test for the new call in test_recagent.py, however
> > like a few of the other tests there, the test is not currently working with
> > the server. Therefore, it remains disabled as the other tests are. I'll look
> > at this tomorrow to try to see what the issue is with these tests.
> > [..]
> >
> > Maybe the staging server had problems. I just tested enabling them and
> > its working, but that may well be that its now fixed. While looking at
> > the test code I noticed that there is a bit of duplication in
> > there, I pushed a branch at
> > lp:~mvo/software-center/recagent-test-cleanup that removes most of
> > that.
> >
> > I also noticed that the branch is missing a test for the new
> > functionality, it would be nice to add one to
> > tests/gtk3/test_recommendations_widget.py. Something like activate a
> > app, trigger a install transaction and verify that on finish the
> > self.recommender_agent.post_implicit_feedback() call was done.
> >
>
> I added a thorough test for this functionality in test_catview.py (this is where the other recommendations panel tests are, and imo it's consistent because the tests for the other lobby panels (What's New and Top Rated) are also located here. While in test_catview.py I did some cleanup and consolidation of duplicated code as well.

Thanks for adding the test and for the cleanup there, that looks very
good!

[..]
> > Should self.recommended_apps_viewed we cleared when we get new data,
> > e.g on _on_app_recommendations_agent_refresh (and the equivalent
> > function for the categories). Or should this be persistent? I'm don't
> > know the details of the spec well enough :) I guess it comes down to
> > if we should consider it a recommendation that are reached
> > "indirectly", e.g. click on it "app A", move back to main view search
> > for something else and then reach "app A" again late...

Read more...

3094. By Gary Lasker on 2012-08-31

add a test to insure that no recommender feedback call occurs in the case where the user is *not* opted in to the recommender service

3095. By Gary Lasker on 2012-08-31

merge with trunk

3096. By Gary Lasker on 2012-09-02

RED: add a failing test to show that the implicit recommender call is incorrectly being made from the top rated panel

3097. By Gary Lasker on 2012-09-03

the fix for the failing test case is to do the refactoring that we know is needed in CategoriesViewGtk to move the add_tiles_to_flowgrid and corresponding on_application_selected methods to where they belong, specifically, to the FlowableGrid class itself, this commit contains the brunt of this refactor, but there are some additional pieces needed to finish it up

3098. By Gary Lasker on 2012-09-04

GREEN: essential refactor complete and signals wired up, all test cases now working; removed the unused (with FIXME) 'application-selected' signal from catview as it is not used there (we only 'activate' apps from this widget), fix single-line cut-and-paste error in the test case

3099. By Gary Lasker on 2012-09-05

REFACTOR: make a new TileGrid subclass of FlowableGrid so that FlowableGrid remains generic, wire up the TileGrid for the various panels that it is used

3100. By Gary Lasker on 2012-09-05

REFACTOR: remove all need for the catview in the recommendations code

3101. By Gary Lasker on 2012-09-05

don't use hasattr in the FramedHeaderBox so that we can attach listeners to the more button when we need to, connect listeners for this, add a signal to the RecommenderPanelCategory (and subclass RecommenderPanelLobby) so that we can get their correct categories when clicking 'More' in those

3102. By Gary Lasker on 2012-09-05

pep8 fixes

3103. By Gary Lasker on 2012-09-05

merge with trunk

3104. By Gary Lasker on 2012-09-05

clean up get_test_window_recommendations in windows.py per the new factoring

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

Ok, I've added more unit test cases, done a lot of refactoring and cleanup around this area of the code (lots of FIXMEs cleared) and at this point the branch should be ready for review again. You can review the changes in detail by looking at the checkin comments starting at rev 3094.

Many thanks for your review, as always!!

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

Hi Gary, thanks for this updated branch.

I just reviewed it and it looks very nice now. Some comments below:

I like that the recommendations widgets do no longer get a catview, giving them the db and propertieshelper
looks much nicer now.

395 class FlowableGrid(Gtk.Fixed):
396
397 MIN_HEIGHT = 100
398
399 + __gsignals__ = {
400 + "application-activated": (GObject.SignalFlags.RUN_LAST,
401 + None,
402 + (GObject.TYPE_PYOBJECT, ),
403 + ),
404 + }
405 +

It seems that the FlowableGrid is pretty generic (i.e. can hold other items than application tiles too) so this signal probably is better put into the TileGrid class? Or am I missing something here? I can do this during the merge if you agree with moving it.

I think its great that you added the docstring:
416 + def add_tiles(self, properties_helper, docs, amount):
417 + '''Adds application tiles to an ApplicationTileGrid:
418 + properties_help = an instance of the PropertiesHelper object
419 + docs = xapian documents (apps)
420 + amount = number of tiles to add from start of doc range'''
here! It might be worth taking a quick look http://www.python.org/dev/peps/pep-0257/ but there seems to be no real convention that everyone in the python community is following, so to a good extend its a matter of taste :) I personally like using a "--" as the keyword seperator there, e.g. "doc -- xapian docuemnts" better than using the "=" as initially my brain read it as code until I noticed the surrounding '''. But thats just a tiny detail and its good to have the docstring.

I like the way the "more" button is now handled, much nicer than in the previous code.

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

> Hi Gary, thanks for this updated branch.
>
> I just reviewed it and it looks very nice now. Some comments below:
>
>
> I like that the recommendations widgets do no longer get a catview, giving
> them the db and propertieshelper
> looks much nicer now.
>
> 395 class FlowableGrid(Gtk.Fixed):
> 396
> 397 MIN_HEIGHT = 100
> 398
> 399 + __gsignals__ = {
> 400 + "application-activated": (GObject.SignalFlags.RUN_LAST,
> 401 + None,
> 402 + (GObject.TYPE_PYOBJECT, ),
> 403 + ),
> 404 + }
> 405 +
>
> It seems that the FlowableGrid is pretty generic (i.e. can hold other items
> than application tiles too) so this signal probably is better put into the
> TileGrid class? Or am I missing something here? I can do this during the merge
> if you agree with moving it.

Yes! Thank you for noticing that. I just overlooked moving it down to the new subclass, but you are correct, that's where it belongs.

>
> I think its great that you added the docstring:
> 416 + def add_tiles(self, properties_helper, docs, amount):
> 417 + '''Adds application tiles to an ApplicationTileGrid:
> 418 + properties_help = an instance of the PropertiesHelper object
> 419 + docs = xapian documents (apps)
> 420 + amount = number of tiles to add from start of doc range'''
> here! It might be worth taking a quick look
> http://www.python.org/dev/peps/pep-0257/ but there seems to be no real
> convention that everyone in the python community is following, so to a good
> extend its a matter of taste :) I personally like using a "--" as the keyword
> seperator there, e.g. "doc -- xapian docuemnts" better than using the "=" as
> initially my brain read it as code until I noticed the surrounding '''. But
> thats just a tiny detail and its good to have the docstring.
>

I am with you completely. I just cut and pasted from another docstring and didn't notice the "=" signs. I would much prefer something different, and "--" seems just fine to me.

> I like the way the "more" button is now handled, much nicer than in the
> previous code.

Thanks! I like all of this better too. :) I'll make the updates now.

3105. By Gary Lasker on 2012-09-06

move the application-activated signal into the TileGrid subclass where it belongs

3106. By Gary Lasker on 2012-09-06

make a docstring more better per review comment

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

Remaining small updates are done in the branch, now we just need the FFe! Bug 1044107. Hopefully very soon now.

3107. By Gary Lasker on 2012-09-06

merge with trunk, don't wanna cruft

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'softwarecenter/backend/piston/sreclient_pristine.py'
--- softwarecenter/backend/piston/sreclient_pristine.py 2012-08-28 18:16:47 +0000
+++ softwarecenter/backend/piston/sreclient_pristine.py 2012-09-06 15:40:27 +0000
@@ -84,7 +84,7 @@
84 scheme=AUTHENTICATED_API_SCHEME)84 scheme=AUTHENTICATED_API_SCHEME)
8585
86 @oauth_protected86 @oauth_protected
87 @validate('pkgname', str)87 @validate_pattern('pkgname', '[^/]+')
88 @validate_pattern('action', '\w{3,20}')88 @validate_pattern('action', '\w{3,20}')
89 @returns_json89 @returns_json
90 def implicit_feedback(self, pkgname, action):90 def implicit_feedback(self, pkgname, action):
9191
=== modified file 'softwarecenter/backend/recagent.py'
--- softwarecenter/backend/recagent.py 2012-08-29 18:05:52 +0000
+++ softwarecenter/backend/recagent.py 2012-09-06 15:40:27 +0000
@@ -68,6 +68,10 @@
68 GObject.TYPE_NONE,68 GObject.TYPE_NONE,
69 (GObject.TYPE_PYOBJECT,),69 (GObject.TYPE_PYOBJECT,),
70 ),70 ),
71 "submit-implicit-feedback-finished": (GObject.SIGNAL_RUN_LAST,
72 GObject.TYPE_NONE,
73 (GObject.TYPE_PYOBJECT,),
74 ),
71 "error": (GObject.SIGNAL_RUN_LAST,75 "error": (GObject.SIGNAL_RUN_LAST,
72 GObject.TYPE_NONE,76 GObject.TYPE_NONE,
73 (str,),77 (str,),
@@ -215,6 +219,22 @@
215 spawner.run_generic_piston_helper(219 spawner.run_generic_piston_helper(
216 "SoftwareCenterRecommenderAPI", "recommend_top")220 "SoftwareCenterRecommenderAPI", "recommend_top")
217221
222 def post_implicit_feedback(self, pkgname, action):
223 # build the command
224 LOG.debug("called post_implicit_feedback with %s, '%s'" %
225 (pkgname, action))
226 spawner = SpawnHelper()
227 spawner.parent_xid = self.xid
228 spawner.needs_auth = True
229 spawner.connect("data-available",
230 self._on_submit_implicit_feedback_data)
231 spawner.connect("error", lambda spawner, err: self.emit("error", err))
232 spawner.run_generic_piston_helper(
233 "SoftwareCenterRecommenderAPI",
234 "implicit_feedback",
235 pkgname=pkgname,
236 action=action)
237
218 def is_opted_in(self):238 def is_opted_in(self):
219 """239 """
220 Return True is the user is currently opted-in to the recommender240 Return True is the user is currently opted-in to the recommender
@@ -244,7 +264,7 @@
244264
245 def _on_submit_anon_profile_data(self, spawner,265 def _on_submit_anon_profile_data(self, spawner,
246 piston_submit_anon_profile):266 piston_submit_anon_profile):
247 self.emit("submit-anon_profile", piston_submit_anon_profile)267 self.emit("submit-anon-profile-finished", piston_submit_anon_profile)
248268
249 def _on_recommend_me_data(self, spawner, piston_me_apps):269 def _on_recommend_me_data(self, spawner, piston_me_apps):
250 self.emit("recommend-me", piston_me_apps)270 self.emit("recommend-me", piston_me_apps)
@@ -258,6 +278,11 @@
258 def _on_recommend_top_data(self, spawner, piston_top_apps):278 def _on_recommend_top_data(self, spawner, piston_top_apps):
259 self.emit("recommend-top", piston_top_apps)279 self.emit("recommend-top", piston_top_apps)
260280
281 def _on_submit_implicit_feedback_data(self, spawner,
282 piston_submit_implicit_feedback):
283 self.emit("submit-implicit-feedback-finished",
284 piston_submit_implicit_feedback)
285
261 def _generate_submit_profile_data(self, recommender_uuid, package_list):286 def _generate_submit_profile_data(self, recommender_uuid, package_list):
262 submit_profile_data = [{287 submit_profile_data = [{
263 'uuid': recommender_uuid,288 'uuid': recommender_uuid,
264289
=== modified file 'softwarecenter/enums.py'
--- softwarecenter/enums.py 2012-08-23 14:37:28 +0000
+++ softwarecenter/enums.py 2012-09-06 15:40:27 +0000
@@ -282,6 +282,10 @@
282 PACKAGE = ","282 PACKAGE = ","
283283
284284
285# action values for recommendations feedback
286class RecommenderFeedbackActions:
287 INSTALLED = "installed"
288
285# mouse event codes for back/forward buttons289# mouse event codes for back/forward buttons
286# TODO: consider whether we ought to get these values from gconf so that we290# TODO: consider whether we ought to get these values from gconf so that we
287# can be sure to use the corresponding values used by Nautilus:291# can be sure to use the corresponding values used by Nautilus:
288292
=== modified file 'softwarecenter/ui/gtk3/panes/availablepane.py'
--- softwarecenter/ui/gtk3/panes/availablepane.py 2012-08-21 11:54:39 +0000
+++ softwarecenter/ui/gtk3/panes/availablepane.py 2012-09-06 15:40:27 +0000
@@ -193,12 +193,7 @@
193 self.subcategories_view.connect(193 self.subcategories_view.connect(
194 "category-selected", self.on_subcategory_activated)194 "category-selected", self.on_subcategory_activated)
195 self.subcategories_view.connect(195 self.subcategories_view.connect(
196 "application-activated", self.on_application_activated)
197 self.subcategories_view.connect(
198 "show-category-applist", self.on_show_category_applist)196 "show-category-applist", self.on_show_category_applist)
199 # FIXME: why do we have two application-{selected,activated] ?!?
200 self.subcategories_view.connect(
201 "application-selected", self.on_application_selected)
202 self.subcategories_view.connect(197 self.subcategories_view.connect(
203 "application-activated", self.on_application_activated)198 "application-activated", self.on_application_activated)
204 self.scroll_subcategories = Gtk.ScrolledWindow()199 self.scroll_subcategories = Gtk.ScrolledWindow()
@@ -215,8 +210,6 @@
215 self.cat_view.connect(210 self.cat_view.connect(
216 "category-selected", self.on_category_activated)211 "category-selected", self.on_category_activated)
217 self.cat_view.connect(212 self.cat_view.connect(
218 "application-selected", self.on_application_selected)
219 self.cat_view.connect(
220 "application-activated", self.on_application_activated)213 "application-activated", self.on_application_activated)
221214
222 # details215 # details
@@ -761,9 +754,9 @@
761 vm = get_viewmanager()754 vm = get_viewmanager()
762 vm.display_page(self, page, self.state)755 vm.display_page(self, page, self.state)
763756
764 def on_application_selected(self, appview, app):757 def on_application_activated(self, appview, app):
765 """Callback for when an app is selected."""758 """Callback for when an app is activated."""
766 super(AvailablePane, self).on_application_selected(appview, app)759 super(AvailablePane, self).on_application_activated(appview, app)
767 if self.state.subcategory:760 if self.state.subcategory:
768 self.current_app_by_subcategory[self.state.subcategory] = app761 self.current_app_by_subcategory[self.state.subcategory] = app
769 else:762 else:
770763
=== modified file 'softwarecenter/ui/gtk3/views/appdetailsview.py'
--- softwarecenter/ui/gtk3/views/appdetailsview.py 2012-08-30 09:04:39 +0000
+++ softwarecenter/ui/gtk3/views/appdetailsview.py 2012-09-06 15:40:27 +0000
@@ -51,10 +51,6 @@
51from softwarecenter.backend.weblive import get_weblive_backend51from softwarecenter.backend.weblive import get_weblive_backend
52from softwarecenter.ui.gtk3.dialogs import error52from softwarecenter.ui.gtk3.dialogs import error
5353
54# FIXME: this is needed for the recommendations but really should become
55# a widget or something generic instead
56from softwarecenter.ui.gtk3.views.catview_gtk import CategoriesViewGtk
57
58from softwarecenter.ui.gtk3.em import StockEms, em54from softwarecenter.ui.gtk3.em import StockEms, em
59from softwarecenter.ui.gtk3.drawing import color_to_hex55from softwarecenter.ui.gtk3.drawing import color_to_hex
60from softwarecenter.ui.gtk3.session.appmanager import get_appmanager56from softwarecenter.ui.gtk3.session.appmanager import get_appmanager
@@ -75,6 +71,8 @@
7571
76import softwarecenter.ui.gtk3.dialogs as dialogs72import softwarecenter.ui.gtk3.dialogs as dialogs
7773
74from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper
75
78from softwarecenter.hw import get_hw_missing_long_description76from softwarecenter.hw import get_hw_missing_long_description
79from softwarecenter.region import REGION_WARNING_STRING77from softwarecenter.region import REGION_WARNING_STRING
8078
@@ -822,6 +820,8 @@
822 self.appdetails = None820 self.appdetails = None
823 self.addons_to_install = []821 self.addons_to_install = []
824 self.addons_to_remove = []822 self.addons_to_remove = []
823 self.properties_helper = AppPropertiesHelper(
824 self.db, self.cache, self.icons)
825 # reviews825 # reviews
826 self.review_loader = get_review_loader(self.cache, self.db)826 self.review_loader = get_review_loader(self.cache, self.db)
827 self.review_loader.connect(827 self.review_loader.connect(
@@ -1301,9 +1301,9 @@
1301 vb.pack_start(self._hbars[2], False, False, 0)1301 vb.pack_start(self._hbars[2], False, False, 0)
13021302
1303 # recommendations1303 # recommendations
1304 catview = CategoriesViewGtk(1304 self.recommended_for_app_panel = RecommendationsPanelDetails(
1305 self.datadir, None, self.cache, self.db, self.icons, None)1305 self.db,
1306 self.recommended_for_app_panel = RecommendationsPanelDetails(catview)1306 self.properties_helper)
1307 self.recommended_for_app_panel.connect(1307 self.recommended_for_app_panel.connect(
1308 "application-activated",1308 "application-activated",
1309 self._on_recommended_application_activated)1309 self._on_recommended_application_activated)
13101310
=== modified file 'softwarecenter/ui/gtk3/views/catview_gtk.py'
--- softwarecenter/ui/gtk3/views/catview_gtk.py 2012-08-22 10:21:06 +0000
+++ softwarecenter/ui/gtk3/views/catview_gtk.py 2012-09-06 15:40:27 +0000
@@ -39,15 +39,16 @@
39from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper39from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper
40from softwarecenter.ui.gtk3.widgets.viewport import Viewport40from softwarecenter.ui.gtk3.widgets.viewport import Viewport
41from softwarecenter.ui.gtk3.widgets.containers import (41from softwarecenter.ui.gtk3.widgets.containers import (
42 FramedHeaderBox, FramedBox, FlowableGrid)42 FramedHeaderBox,
43 FramedBox,
44 TileGrid)
43from softwarecenter.ui.gtk3.widgets.recommendations import (45from softwarecenter.ui.gtk3.widgets.recommendations import (
44 RecommendationsPanelLobby,46 RecommendationsPanelLobby,
45 RecommendationsPanelCategory)47 RecommendationsPanelCategory)
46from softwarecenter.ui.gtk3.widgets.exhibits import (48from softwarecenter.ui.gtk3.widgets.exhibits import (
47 ExhibitBanner, FeaturedExhibit)49 ExhibitBanner, FeaturedExhibit)
48from softwarecenter.ui.gtk3.widgets.buttons import (LabelTile,50from softwarecenter.ui.gtk3.widgets.buttons import (LabelTile,
49 CategoryTile,51 CategoryTile)
50 FeaturedTile)
51from softwarecenter.ui.gtk3.em import StockEms52from softwarecenter.ui.gtk3.em import StockEms
52from softwarecenter.db.appfilter import AppFilter, get_global_filter53from softwarecenter.db.appfilter import AppFilter, get_global_filter
53from softwarecenter.db.enquire import AppEnquire54from softwarecenter.db.enquire import AppEnquire
@@ -74,11 +75,6 @@
74 (GObject.TYPE_PYOBJECT, ),75 (GObject.TYPE_PYOBJECT, ),
75 ),76 ),
7677
77 "application-selected": (GObject.SignalFlags.RUN_LAST,
78 None,
79 (GObject.TYPE_PYOBJECT, ),
80 ),
81
82 "application-activated": (GObject.SignalFlags.RUN_LAST,78 "application-activated": (GObject.SignalFlags.RUN_LAST,
83 None,79 None,
84 (GObject.TYPE_PYOBJECT, ),80 (GObject.TYPE_PYOBJECT, ),
@@ -154,19 +150,6 @@
154 self.connect("size-allocate", self.on_size_allocate)150 self.connect("size-allocate", self.on_size_allocate)
155 return151 return
156152
157 def _add_tiles_to_flowgrid(self, docs, flowgrid, amount):
158 '''Adds application tiles to a FlowableGrid:
159 docs = xapian documents (apps)
160 flowgrid = the FlowableGrid to add tiles to
161 amount = number of tiles to add from start of doc range'''
162 amount = min(len(docs), amount)
163 for doc in docs[0:amount]:
164 tile = FeaturedTile(self.properties_helper, doc)
165 tile.connect('clicked', self.on_app_clicked,
166 self.properties_helper.get_application(doc))
167 flowgrid.add_child(tile)
168 return
169
170 def on_size_allocate(self, widget, _):153 def on_size_allocate(self, widget, _):
171 a = widget.get_allocation()154 a = widget.get_allocation()
172 prev = self._prev_alloc155 prev = self._prev_alloc
@@ -187,10 +170,11 @@
187 assets["stipple"] = ptrn170 assets["stipple"] = ptrn
188 return assets171 return assets
189172
190 def on_app_clicked(self, btn, app):173 def on_application_activated(self, btn, app):
191 """emit the category-selected signal when a category was clicked"""174 """ pass the application-activated signal along when an application
175 is clicked-through in one of the tiles
176 """
192 def timeout_emit():177 def timeout_emit():
193 self.emit("application-selected", app)
194 self.emit("application-activated", app)178 self.emit("application-activated", app)
195 return False179 return False
196180
@@ -362,13 +346,16 @@
362 self.categories, u"Top Rated") # untranslated name346 self.categories, u"Top Rated") # untranslated name
363 if top_rated_cat:347 if top_rated_cat:
364 docs = top_rated_cat.get_documents(self.db)348 docs = top_rated_cat.get_documents(self.db)
365 self._add_tiles_to_flowgrid(docs, self.top_rated,349 self.top_rated.add_tiles(self.properties_helper,
366 TOP_RATED_CAROUSEL_LIMIT)350 docs,
351 TOP_RATED_CAROUSEL_LIMIT)
367 self.top_rated.show_all()352 self.top_rated.show_all()
368 return top_rated_cat353 return top_rated_cat
369354
370 def _append_top_rated(self):355 def _append_top_rated(self):
371 self.top_rated = FlowableGrid()356 self.top_rated = TileGrid()
357 self.top_rated.connect("application-activated",
358 self.on_application_activated)
372 #~ self.top_rated.row_spacing = StockEms.SMALL359 #~ self.top_rated.row_spacing = StockEms.SMALL
373 self.top_rated_frame = FramedHeaderBox()360 self.top_rated_frame = FramedHeaderBox()
374 self.top_rated_frame.set_header_label(_("Top Rated"))361 self.top_rated_frame.set_header_label(_("Top Rated"))
@@ -380,7 +367,6 @@
380 self.top_rated_frame.header_implements_more_button()367 self.top_rated_frame.header_implements_more_button()
381 self.top_rated_frame.more.connect('clicked',368 self.top_rated_frame.more.connect('clicked',
382 self.on_category_clicked, top_rated_cat)369 self.on_category_clicked, top_rated_cat)
383 return
384370
385 def _update_whats_new_content(self):371 def _update_whats_new_content(self):
386 # remove any existing children from the grid widget372 # remove any existing children from the grid widget
@@ -390,13 +376,16 @@
390 self.categories, u"What\u2019s New") # untranslated name376 self.categories, u"What\u2019s New") # untranslated name
391 if whats_new_cat:377 if whats_new_cat:
392 docs = whats_new_cat.get_documents(self.db)378 docs = whats_new_cat.get_documents(self.db)
393 self._add_tiles_to_flowgrid(docs, self.whats_new,379 self.whats_new.add_tiles(self.properties_helper,
394 WHATS_NEW_CAROUSEL_LIMIT)380 docs,
381 WHATS_NEW_CAROUSEL_LIMIT)
395 self.whats_new.show_all()382 self.whats_new.show_all()
396 return whats_new_cat383 return whats_new_cat
397384
398 def _append_whats_new(self):385 def _append_whats_new(self):
399 self.whats_new = FlowableGrid()386 self.whats_new = TileGrid()
387 self.whats_new.connect("application-activated",
388 self.on_application_activated)
400 self.whats_new_frame = FramedHeaderBox()389 self.whats_new_frame = FramedHeaderBox()
401 self.whats_new_frame.set_header_label(_(u"What\u2019s New"))390 self.whats_new_frame.set_header_label(_(u"What\u2019s New"))
402 self.whats_new_frame.add(self.whats_new)391 self.whats_new_frame.add(self.whats_new)
@@ -412,13 +401,33 @@
412 def _update_recommended_for_you_content(self):401 def _update_recommended_for_you_content(self):
413 if (self.recommended_for_you_panel and402 if (self.recommended_for_you_panel and
414 self.recommended_for_you_panel.get_parent()):403 self.recommended_for_you_panel.get_parent()):
404 # disconnect listeners
405 self.recommended_for_you_panel.disconnect_by_func(
406 self.on_application_activated)
407 self.recommended_for_you_panel.disconnect_by_func(
408 self.on_category_clicked)
409 # and remove the panel
415 self.right_column.remove(self.recommended_for_you_panel)410 self.right_column.remove(self.recommended_for_you_panel)
416 self.recommended_for_you_panel = RecommendationsPanelLobby(self)411 self.recommended_for_you_panel = RecommendationsPanelLobby(
412 self.db,
413 self.properties_helper)
414 self.recommended_for_you_panel.connect("application-activated",
415 self.on_application_activated)
416 self.recommended_for_you_panel.connect(
417 'more-button-clicked',
418 self.on_category_clicked)
417 self.right_column.pack_start(self.recommended_for_you_panel,419 self.right_column.pack_start(self.recommended_for_you_panel,
418 True, True, 0)420 True, True, 0)
419421
420 def _append_recommended_for_you(self):422 def _append_recommended_for_you(self):
421 self.recommended_for_you_panel = RecommendationsPanelLobby(self)423 self.recommended_for_you_panel = RecommendationsPanelLobby(
424 self.db,
425 self.properties_helper)
426 self.recommended_for_you_panel.connect("application-activated",
427 self.on_application_activated)
428 self.recommended_for_you_panel.connect(
429 'more-button-clicked',
430 self.on_category_clicked)
422 self.right_column.pack_start(431 self.right_column.pack_start(
423 self.recommended_for_you_panel, True, True, 0)432 self.recommended_for_you_panel, True, True, 0)
424433
@@ -519,12 +528,15 @@
519 'category': GObject.markup_escape_text(self.header)}528 'category': GObject.markup_escape_text(self.header)}
520 self.top_rated_frame.set_header_label(m)529 self.top_rated_frame.set_header_label(m)
521 docs = self._get_sub_top_rated_content(category)530 docs = self._get_sub_top_rated_content(category)
522 self._add_tiles_to_flowgrid(docs, self.top_rated,531 self.top_rated.add_tiles(self.properties_helper,
523 TOP_RATED_CAROUSEL_LIMIT)532 docs,
533 TOP_RATED_CAROUSEL_LIMIT)
524 return534 return
525535
526 def _append_sub_top_rated(self):536 def _append_sub_top_rated(self):
527 self.top_rated = FlowableGrid()537 self.top_rated = TileGrid()
538 self.top_rated.connect("application-activated",
539 self.on_application_activated)
528 self.top_rated.set_row_spacing(6)540 self.top_rated.set_row_spacing(6)
529 self.top_rated.set_column_spacing(6)541 self.top_rated.set_column_spacing(6)
530 self.top_rated_frame = FramedHeaderBox()542 self.top_rated_frame = FramedHeaderBox()
@@ -535,10 +547,18 @@
535 def _update_recommended_for_you_in_cat_content(self, category):547 def _update_recommended_for_you_in_cat_content(self, category):
536 if (self.recommended_for_you_in_cat and548 if (self.recommended_for_you_in_cat and
537 self.recommended_for_you_in_cat.get_parent()):549 self.recommended_for_you_in_cat.get_parent()):
550 self.recommended_for_you_in_cat.disconnect_by_func(
551 self.on_application_activated)
538 self.vbox.remove(self.recommended_for_you_in_cat)552 self.vbox.remove(self.recommended_for_you_in_cat)
539 self.recommended_for_you_in_cat = RecommendationsPanelCategory(553 self.recommended_for_you_in_cat = RecommendationsPanelCategory(
540 self,554 self.db,
541 category)555 self.properties_helper,
556 category)
557 self.recommended_for_you_in_cat.connect("application-activated",
558 self.on_application_activated)
559 self.recommended_for_you_in_cat.connect(
560 'more-button-clicked',
561 self.on_category_clicked)
542 # only show the panel in the categories view when the user562 # only show the panel in the categories view when the user
543 # is opted in to the recommender service563 # is opted in to the recommender service
544 # FIXME: this is needed vs. a simple hide() on the widget because564 # FIXME: this is needed vs. a simple hide() on the widget because
@@ -596,7 +616,7 @@
596 def _append_subcat_departments(self):616 def _append_subcat_departments(self):
597 self.subcat_label = Gtk.Label()617 self.subcat_label = Gtk.Label()
598 self.subcat_label.set_alignment(0, 0.5)618 self.subcat_label.set_alignment(0, 0.5)
599 self.departments = FlowableGrid(paint_grid_pattern=False)619 self.departments = TileGrid(paint_grid_pattern=False)
600 self.departments.set_row_spacing(StockEms.SMALL)620 self.departments.set_row_spacing(StockEms.SMALL)
601 self.departments.set_column_spacing(StockEms.SMALL)621 self.departments.set_column_spacing(StockEms.SMALL)
602 self.departments_frame = FramedBox(spacing=StockEms.MEDIUM,622 self.departments_frame = FramedBox(spacing=StockEms.MEDIUM,
603623
=== modified file 'softwarecenter/ui/gtk3/widgets/containers.py'
--- softwarecenter/ui/gtk3/widgets/containers.py 2012-08-27 20:24:10 +0000
+++ softwarecenter/ui/gtk3/widgets/containers.py 2012-09-06 15:40:27 +0000
@@ -4,13 +4,14 @@
4PI_OVER_180 = PI / 1804PI_OVER_180 = PI / 180
5import softwarecenter.paths5import softwarecenter.paths
66
7from gi.repository import Gtk, Gdk7from gi.repository import Gtk, Gdk, GObject
88
9from buttons import MoreLink9from buttons import MoreLink
10from softwarecenter.ui.gtk3.em import StockEms10from softwarecenter.ui.gtk3.em import StockEms
11from softwarecenter.ui.gtk3.drawing import rounded_rect11from softwarecenter.ui.gtk3.drawing import rounded_rect
12from softwarecenter.ui.gtk3.widgets.spinner import (SpinnerView,12from softwarecenter.ui.gtk3.widgets.spinner import (SpinnerView,
13 SpinnerNotebook)13 SpinnerNotebook)
14from softwarecenter.ui.gtk3.widgets.buttons import FeaturedTile
1415
1516
16class FlowableGrid(Gtk.Fixed):17class FlowableGrid(Gtk.Fixed):
@@ -182,6 +183,39 @@
182 self.remove(child)183 self.remove(child)
183184
184185
186class TileGrid(FlowableGrid):
187 ''' a flowable layout widget that holds a collection of TileButtons
188 '''
189 __gsignals__ = {
190 "application-activated": (GObject.SignalFlags.RUN_LAST,
191 None,
192 (GObject.TYPE_PYOBJECT, ),
193 ),
194 }
195
196 def add_tiles(self, properties_helper, docs, amount):
197 '''Adds application tiles to an ApplicationTileGrid:
198 properties_help -- an instance of the PropertiesHelper object
199 docs -- xapian documents (apps)
200 amount -- number of tiles to add from start of doc range
201 '''
202 amount = min(len(docs), amount)
203 for doc in docs[0:amount]:
204 tile = FeaturedTile(properties_helper, doc)
205 tile.connect('clicked', self._on_app_clicked,
206 properties_helper.get_application(doc))
207 self.add_child(tile)
208
209 # private
210 def _on_app_clicked(self, btn, app):
211 """emit signal when a tile is clicked"""
212 def timeout_emit():
213 self.emit("application-activated", app)
214 return False
215
216 GObject.timeout_add(50, timeout_emit)
217
218
185# first tier of caching, cache component assets from which frames are219# first tier of caching, cache component assets from which frames are
186# rendered220# rendered
187_frame_asset_cache = {}221_frame_asset_cache = {}
@@ -454,6 +488,9 @@
454 self.spinner_notebook = SpinnerNotebook(self.content_box,488 self.spinner_notebook = SpinnerNotebook(self.content_box,
455 spinner_size=SpinnerView.SMALL)489 spinner_size=SpinnerView.SMALL)
456 self.box.add(self.spinner_notebook)490 self.box.add(self.spinner_notebook)
491 # make the "More" button, but don't add it to the header unless/until
492 # we get a header_implements_more_button
493 self.more = MoreLink()
457494
458 def on_draw(self, cr):495 def on_draw(self, cr):
459 a = self.get_allocation()496 a = self.get_allocation()
@@ -473,17 +510,6 @@
473 def pack_end(self, *args, **kwargs):510 def pack_end(self, *args, **kwargs):
474 return self.content_box.pack_end(*args, **kwargs)511 return self.content_box.pack_end(*args, **kwargs)
475512
476 # XXX: non-functional with current code...
477 #~ def set_header_expand(self, expand):
478 #~ alignment = self.header_alignment
479 #~ if expand:
480 #~ expand = 1.0
481 #~ else:
482 #~ expand = 0.0
483 #~ alignment.set(alignment.get_property("xalign"),
484 #~ alignment.get_property("yalign"),
485 #~ expand, 1.0)
486
487 def set_header_position(self, position):513 def set_header_position(self, position):
488 alignment = self.header_alignment514 alignment = self.header_alignment
489 alignment.set(position, 0.5,515 alignment.set(position, 0.5,
@@ -502,18 +528,16 @@
502 self.title.set_markup(self.MARKUP % label)528 self.title.set_markup(self.MARKUP % label)
503529
504 def header_implements_more_button(self):530 def header_implements_more_button(self):
505 if not hasattr(self, "more"):531 if self.more not in self.header:
506 self.more = MoreLink()
507 self.header.pack_end(self.more, False, False, 0)532 self.header.pack_end(self.more, False, False, 0)
508533
509 def remove_more_button(self):534 def remove_more_button(self):
510 if hasattr(self, "more"):535 if self.more in self.header:
511 self.header.remove(self.more)536 self.header.remove(self.more)
512 del self.more
513537
514 def render_header(self, cr, a, border_radius, assets):538 def render_header(self, cr, a, border_radius, assets):
515539
516 if hasattr(self, "more"):540 if self.more in self.header:
517 context = self.get_style_context()541 context = self.get_style_context()
518542
519 # set the arrow fill color543 # set the arrow fill color
520544
=== modified file 'softwarecenter/ui/gtk3/widgets/recommendations.py'
--- softwarecenter/ui/gtk3/widgets/recommendations.py 2012-08-22 08:16:30 +0000
+++ softwarecenter/ui/gtk3/widgets/recommendations.py 2012-09-06 15:40:27 +0000
@@ -24,10 +24,11 @@
2424
25from softwarecenter.ui.gtk3.em import StockEms25from softwarecenter.ui.gtk3.em import StockEms
26from softwarecenter.ui.gtk3.widgets.containers import (FramedHeaderBox,26from softwarecenter.ui.gtk3.widgets.containers import (FramedHeaderBox,
27 FlowableGrid)27 TileGrid)
28from softwarecenter.ui.gtk3.utils import get_parent_xid28from softwarecenter.ui.gtk3.utils import get_parent_xid
29from softwarecenter.db.categories import (RecommendedForYouCategory,29from softwarecenter.db.categories import (RecommendedForYouCategory,
30 AppRecommendationsCategory)30 AppRecommendationsCategory)
31from softwarecenter.backend import get_install_backend
31from softwarecenter.backend.recagent import RecommenderAgent32from softwarecenter.backend.recagent import RecommenderAgent
32from softwarecenter.backend.login_sso import get_sso_backend33from softwarecenter.backend.login_sso import get_sso_backend
33from softwarecenter.backend.ubuntusso import get_ubuntu_sso_backend34from softwarecenter.backend.ubuntusso import get_ubuntu_sso_backend
@@ -35,6 +36,8 @@
35 LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT,36 LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT,
36 DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT,37 DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT,
37 SOFTWARE_CENTER_NAME_KEYRING,38 SOFTWARE_CENTER_NAME_KEYRING,
39 RecommenderFeedbackActions,
40 TransactionTypes,
38 )41 )
39from softwarecenter.utils import utf842from softwarecenter.utils import utf8
40from softwarecenter.netstatus import network_state_is_connected43from softwarecenter.netstatus import network_state_is_connected
@@ -54,18 +57,42 @@
54 ),57 ),
55 }58 }
5659
57 def __init__(self, catview):60 def __init__(self):
58 FramedHeaderBox.__init__(self)61 FramedHeaderBox.__init__(self)
59 # FIXME: we only need the catview for "add_titles_to_flowgrid"
60 # and "on_category_clicked" so we should be able to
61 # extract this to a "leaner" widget
62 self.catview = catview
63 self.catview.connect(
64 "application-activated", self._on_application_activated)
65 self.recommender_agent = RecommenderAgent()62 self.recommender_agent = RecommenderAgent()
63 # keep track of applications that have been viewed via a
64 # recommendation so that we can detect when a recommended app
65 # has been installed
66 self.recommended_apps_viewed = set()
67 self.backend = get_install_backend()
68 self.backend.connect("transaction-started",
69 self._on_transaction_started)
70 self.backend.connect("transaction-finished",
71 self._on_transaction_finished)
6672
67 def _on_application_activated(self, catview, app):73 def _on_application_activated(self, tile, app):
68 self.emit("application-activated", app)74 self.emit("application-activated", app)
75 # we only track installed items if the user has opted-in to the
76 # recommendations service
77 if self.recommender_agent.is_opted_in():
78 self.recommended_apps_viewed.add(app.pkgname)
79
80 def _on_transaction_started(self, backend, pkgname, appname, trans_id,
81 trans_type):
82 if (trans_type != TransactionTypes.INSTALL and
83 pkgname in self.recommended_apps_viewed):
84 # if the transaction is not an installation we don't want to
85 # track it as a recommended item
86 self.recommended_apps_viewed.remove(pkgname)
87
88 def _on_transaction_finished(self, backend, result):
89 if result.pkgname in self.recommended_apps_viewed:
90 self.recommended_apps_viewed.remove(result.pkgname)
91 if network_state_is_connected():
92 # no need to monitor for an error, etc., for this
93 self.recommender_agent.post_implicit_feedback(
94 result.pkgname,
95 RecommenderFeedbackActions.INSTALLED)
6996
70 def _on_recommender_agent_error(self, agent, msg):97 def _on_recommender_agent_error(self, agent, msg):
71 LOG.warn("Error while accessing the recommender agent: %s" % msg)98 LOG.warn("Error while accessing the recommender agent: %s" % msg)
@@ -81,8 +108,18 @@
81 Panel for use in the category view that displays recommended apps for108 Panel for use in the category view that displays recommended apps for
82 the given category109 the given category
83 """110 """
84 def __init__(self, catview, subcategory):111
85 RecommendationsPanel.__init__(self, catview)112 __gsignals__ = {
113 "more-button-clicked": (GObject.SignalFlags.RUN_LAST,
114 None,
115 (GObject.TYPE_PYOBJECT, ),
116 ),
117 }
118
119 def __init__(self, db, properties_helper, subcategory):
120 RecommendationsPanel.__init__(self)
121 self.db = db
122 self.properties_helper = properties_helper
86 self.subcategory = subcategory123 self.subcategory = subcategory
87 if self.subcategory:124 if self.subcategory:
88 self.set_header_label(GObject.markup_escape_text(utf8(125 self.set_header_label(GObject.markup_escape_text(utf8(
@@ -98,7 +135,9 @@
98 if self.recommended_for_you_content:135 if self.recommended_for_you_content:
99 self.recommended_for_you_content.destroy()136 self.recommended_for_you_content.destroy()
100 # add the new stuff137 # add the new stuff
101 self.recommended_for_you_content = FlowableGrid()138 self.recommended_for_you_content = TileGrid()
139 self.recommended_for_you_content.connect(
140 "application-activated", self._on_application_activated)
102 self.add(self.recommended_for_you_content)141 self.add(self.recommended_for_you_content)
103 self.spinner_notebook.show_spinner(_(u"Receiving recommendations…"))142 self.spinner_notebook.show_spinner(_(u"Receiving recommendations…"))
104 # get the recommendations from the recommender agent143 # get the recommendations from the recommender agent
@@ -112,23 +151,27 @@
112151
113 def _on_recommended_for_you_agent_refresh(self, cat):152 def _on_recommended_for_you_agent_refresh(self, cat):
114 self.header_implements_more_button()153 self.header_implements_more_button()
115 docs = cat.get_documents(self.catview.db)154 self.more.connect("clicked",
155 self._on_more_button_clicked,
156 self.recommended_for_you_cat)
157 docs = cat.get_documents(self.db)
116 # display the recommendedations158 # display the recommendedations
117 if len(docs) > 0:159 if len(docs) > 0:
118 self.catview._add_tiles_to_flowgrid(160 self.recommended_for_you_content.add_tiles(
119 docs, self.recommended_for_you_content,161 self.properties_helper,
120 LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT)162 docs,
163 LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT)
121 self.recommended_for_you_content.show_all()164 self.recommended_for_you_content.show_all()
122 self.spinner_notebook.hide_spinner()165 self.spinner_notebook.hide_spinner()
123 self.more.connect('clicked',
124 self.catview.on_category_clicked,
125 cat)
126 self.header.queue_draw()166 self.header.queue_draw()
127 self.show_all()167 self.show_all()
128 else:168 else:
129 # hide the panel if we have no recommendations to show169 # hide the panel if we have no recommendations to show
130 self.hide()170 self.hide()
131171
172 def _on_more_button_clicked(self, btn, category):
173 self.emit("more-button-clicked", category)
174
132175
133class RecommendationsPanelLobby(RecommendationsPanelCategory):176class RecommendationsPanelLobby(RecommendationsPanelCategory):
134 """177 """
@@ -155,8 +198,10 @@
155 NO_NETWORK_RECOMMENDATIONS_TEXT = _(u"Recommendations will appear "198 NO_NETWORK_RECOMMENDATIONS_TEXT = _(u"Recommendations will appear "
156 "when next online.")199 "when next online.")
157200
158 def __init__(self, catview):201 def __init__(self, db, properties_helper):
159 RecommendationsPanel.__init__(self, catview)202 RecommendationsPanel.__init__(self)
203 self.db = db
204 self.properties_helper = properties_helper
160 self.subcategory = None205 self.subcategory = None
161 self.set_header_label(_(u"Recommended For You"))206 self.set_header_label(_(u"Recommended For You"))
162 self.recommended_for_you_content = None207 self.recommended_for_you_content = None
@@ -325,7 +370,7 @@
325 self._on_profile_submitted)370 self._on_profile_submitted)
326 self.recommender_agent.connect("error",371 self.recommender_agent.connect("error",
327 self._on_profile_submitted_error)372 self._on_profile_submitted_error)
328 self.recommender_agent.post_submit_profile(self.catview.db)373 self.recommender_agent.post_submit_profile(self.db)
329374
330 def _on_profile_submitted(self, agent, profile):375 def _on_profile_submitted(self, agent, profile):
331 # after the user profile data has been uploaded, make the request376 # after the user profile data has been uploaded, make the request
@@ -362,10 +407,14 @@
362 Panel for use in the details view to display recommendations for a given407 Panel for use in the details view to display recommendations for a given
363 application408 application
364 """409 """
365 def __init__(self, catview):410 def __init__(self, db, properties_helper):
366 RecommendationsPanel.__init__(self, catview)411 RecommendationsPanel.__init__(self)
412 self.db = db
413 self.properties_helper = properties_helper
367 self.set_header_label(_(u"People Also Installed"))414 self.set_header_label(_(u"People Also Installed"))
368 self.app_recommendations_content = FlowableGrid()415 self.app_recommendations_content = TileGrid()
416 self.app_recommendations_content.connect(
417 "application-activated", self._on_application_activated)
369 self.add(self.app_recommendations_content)418 self.add(self.app_recommendations_content)
370419
371 def set_pkgname(self, pkgname):420 def set_pkgname(self, pkgname):
@@ -385,12 +434,13 @@
385 self._on_recommender_agent_error)434 self._on_recommender_agent_error)
386435
387 def _on_app_recommendations_agent_refresh(self, cat):436 def _on_app_recommendations_agent_refresh(self, cat):
388 docs = cat.get_documents(self.catview.db)437 docs = cat.get_documents(self.db)
389 # display the recommendations438 # display the recommendations
390 if len(docs) > 0:439 if len(docs) > 0:
391 self.catview._add_tiles_to_flowgrid(440 self.app_recommendations_content.add_tiles(
392 docs, self.app_recommendations_content,441 self.properties_helper,
393 DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT)442 docs,
443 DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT)
394 self.show_all()444 self.show_all()
395 self.spinner_notebook.hide_spinner()445 self.spinner_notebook.hide_spinner()
396 else:446 else:
397447
=== modified file 'tests/gtk3/test_catview.py'
--- tests/gtk3/test_catview.py 2012-08-28 21:20:40 +0000
+++ tests/gtk3/test_catview.py 2012-09-06 15:40:27 +0000
@@ -20,7 +20,9 @@
2020
21from softwarecenter.db.appfilter import AppFilter21from softwarecenter.db.appfilter import AppFilter
22from softwarecenter.db.database import StoreDatabase22from softwarecenter.db.database import StoreDatabase
23from softwarecenter.enums import SortMethods23from softwarecenter.enums import (SortMethods,
24 TransactionTypes,
25 RecommenderFeedbackActions)
24from softwarecenter.ui.gtk3.views import catview_gtk26from softwarecenter.ui.gtk3.views import catview_gtk
25from softwarecenter.ui.gtk3.widgets.containers import FramedHeaderBox27from softwarecenter.ui.gtk3.widgets.containers import FramedHeaderBox
26from softwarecenter.ui.gtk3.widgets.spinner import SpinnerNotebook28from softwarecenter.ui.gtk3.widgets.spinner import SpinnerNotebook
@@ -35,6 +37,7 @@
3537
36 def setUp(self):38 def setUp(self):
37 self._cat = None39 self._cat = None
40 self._app = None
38 self.win = get_test_window_catview(self.db)41 self.win = get_test_window_catview(self.db)
39 self.addCleanup(self.win.destroy)42 self.addCleanup(self.win.destroy)
40 self.notebook = self.win.get_child()43 self.notebook = self.win.get_child()
@@ -121,6 +124,7 @@
121 '.post_submit_profile')124 '.post_submit_profile')
122 def setUp(self, mock_query, mock_recommender_is_opted_in, mock_sso):125 def setUp(self, mock_query, mock_recommender_is_opted_in, mock_sso):
123 self._cat = None126 self._cat = None
127 self._app = None
124 # patch the recommender to specify that we are not opted-in at128 # patch the recommender to specify that we are not opted-in at
125 # the start of each test129 # the start of each test
126 mock_recommender_is_opted_in.return_value = False130 mock_recommender_is_opted_in.return_value = False
@@ -162,11 +166,7 @@
162 mock_network_state_is_connected):166 mock_network_state_is_connected):
163 # simulate no network available167 # simulate no network available
164 mock_network_state_is_connected.return_value = False168 mock_network_state_is_connected.return_value = False
165 # click the opt-in button to initiate the process169 self._populate_recommended_for_you_panel()
166 self.rec_panel.opt_in_button.clicked()
167 do_events()
168 self.rec_panel._update_recommended_for_you_content()
169 do_events()
170 self.assertEqual(self.rec_panel.spinner_notebook.get_current_page(),170 self.assertEqual(self.rec_panel.spinner_notebook.get_current_page(),
171 FramedHeaderBox.CONTENT)171 FramedHeaderBox.CONTENT)
172 # ensure that we are showing the network not available view172 # ensure that we are showing the network not available view
@@ -176,14 +176,9 @@
176 self.rec_panel.NO_NETWORK_RECOMMENDATIONS_TEXT)176 self.rec_panel.NO_NETWORK_RECOMMENDATIONS_TEXT)
177177
178 def test_recommended_for_you_display_recommendations(self):178 def test_recommended_for_you_display_recommendations(self):
179 # click the opt-in button to initiate the process,179 self._populate_recommended_for_you_panel()
180 # this will show the spinner
181 self.rec_panel.opt_in_button.clicked()
182 do_events()
183 self.rec_panel._update_recommended_for_you_content()
184 do_events()
185 # we fake the callback from the agent here180 # we fake the callback from the agent here
186 for_you = self.lobby.recommended_for_you_panel.recommended_for_you_cat181 for_you = self.rec_panel.recommended_for_you_cat
187 for_you._recommend_me_result(None,182 for_you._recommend_me_result(None,
188 make_recommender_agent_recommend_me_dict())183 make_recommender_agent_recommend_me_dict())
189 self.assertNotEqual(for_you.get_documents(self.db), [])184 self.assertNotEqual(for_you.get_documents(self.db), [])
@@ -192,7 +187,7 @@
192 do_events()187 do_events()
193 # test clicking recommended_for_you More button188 # test clicking recommended_for_you More button
194 self.lobby.connect("category-selected", self._on_category_selected)189 self.lobby.connect("category-selected", self._on_category_selected)
195 self.lobby.recommended_for_you_panel.more.clicked()190 self.rec_panel.more.clicked()
196 # this is delayed for some reason so we need to sleep here191 # this is delayed for some reason so we need to sleep here
197 do_events_with_sleep()192 do_events_with_sleep()
198 self.assertNotEqual(self._cat, None)193 self.assertNotEqual(self._cat, None)
@@ -208,13 +203,8 @@
208 self.assertFalse(visible)203 self.assertFalse(visible)
209204
210 def test_recommended_for_you_display_recommendations_opted_in(self):205 def test_recommended_for_you_display_recommendations_opted_in(self):
211 # click the opt-in button to initiate the process,206 self._populate_recommended_for_you_panel()
212 # this will show the spinner207
213 self.rec_panel.opt_in_button.clicked()
214 do_events()
215 self.rec_panel._update_recommended_for_you_content()
216 do_events()
217
218 # we want to work in the "subcat" view208 # we want to work in the "subcat" view
219 self.notebook.next_page()209 self.notebook.next_page()
220210
@@ -248,6 +238,112 @@
248 do_events_with_sleep()238 do_events_with_sleep()
249 self.assertNotEqual(self._cat, None)239 self.assertNotEqual(self._cat, None)
250 self.assertEqual(self._cat.name, "Recommended For You in Internet")240 self.assertEqual(self._cat.name, "Recommended For You in Internet")
241
242 def test_implicit_recommender_feedback(self):
243 self._populate_recommended_for_you_panel()
244 # we fake the callback from the agent here
245 for_you = self.rec_panel.recommended_for_you_cat
246 for_you._recommend_me_result(None,
247 make_recommender_agent_recommend_me_dict())
248 do_events()
249
250 post_implicit_feedback_fn = ('softwarecenter.ui.gtk3.widgets'
251 '.recommendations.RecommenderAgent'
252 '.post_implicit_feedback')
253 with patch(post_implicit_feedback_fn) as mock_post_implicit_feedback:
254 # we want to grab the app that is activated, it will be in
255 # self._app after the app is activated on the tile click
256 self.rec_panel.recommended_for_you_content.connect(
257 "application-activated",
258 self._on_application_activated)
259 # click a recommendation in the lobby
260 self._click_first_tile_in_panel(self.rec_panel)
261 # simulate installing the application
262 self._simulate_install_events(self._app)
263 # and verify that after the install has completed we have fired
264 # the implicit feedback call to the recommender with the correct
265 # arguments
266 mock_post_implicit_feedback.assert_called_with(
267 self._app.pkgname,
268 RecommenderFeedbackActions.INSTALLED)
269 # finally, make sure that we have cleared the application
270 # from the recommended_apps_viewed set
271 self.assertFalse(self._app.pkgname in
272 self.rec_panel.recommended_apps_viewed)
273
274 def test_implicit_recommender_feedback_recommendations_panel_only(self):
275 # this test insures that we only provide feedback when installing
276 # items clicked to via the recommendations panel itself, and not
277 # via the What's New or Top Rated panels
278 self._populate_recommended_for_you_panel()
279 # we fake the callback from the agent here
280 for_you = self.rec_panel.recommended_for_you_cat
281 for_you._recommend_me_result(None,
282 make_recommender_agent_recommend_me_dict())
283 do_events()
284
285 post_implicit_feedback_fn = ('softwarecenter.ui.gtk3.widgets'
286 '.recommendations.RecommenderAgent'
287 '.post_implicit_feedback')
288 with patch(post_implicit_feedback_fn) as mock_post_implicit_feedback:
289 # we want to grab the app that is activated, it will be in
290 # self._app after the app is activated on the tile click
291 self.lobby.top_rated.connect("application-activated",
292 self._on_application_activated)
293 # click a tile in the Top Rated section of the lobby
294 self._click_first_tile_in_panel(self.lobby.top_rated_frame)
295 # simulate installing the application
296 self._simulate_install_events(self._app)
297 # and verify that after the install has completed we have *not*
298 # fired the implicit feedback call to the recommender service
299 self.assertFalse(mock_post_implicit_feedback.called)
300
301 def test_implicit_recommender_feedback_not_opted_in(self):
302 # this test verifies that we do *not* send feedback to the
303 # recommender service if the user has not opted-in to it
304 post_implicit_feedback_fn = ('softwarecenter.ui.gtk3.widgets'
305 '.recommendations.RecommenderAgent'
306 '.post_implicit_feedback')
307 with patch(post_implicit_feedback_fn) as mock_post_implicit_feedback:
308 # we are not opted-in
309 from softwarecenter.db.application import Application
310 app = Application("Calculator", "gcalctool")
311 # simulate installing the application
312 self._simulate_install_events(app)
313 # and verify that after the install has completed we have *not*
314 # fired the implicit feedback call to the recommender
315 self.assertFalse(mock_post_implicit_feedback.called)
316
317 def _populate_recommended_for_you_panel(self):
318 # click the opt-in button to initiate the process
319 self.rec_panel.opt_in_button.clicked()
320 do_events()
321 # and update the recommended for you panel to load it up
322 self.rec_panel._update_recommended_for_you_content()
323 do_events()
324
325 def _on_application_activated(self, catview, app):
326 self._app = app
327
328 def _click_first_tile_in_panel(self, framed_header_box):
329 first_tile = (framed_header_box.content_box.
330 get_children()[0].get_children()[0])
331 first_tile.clicked()
332 do_events_with_sleep()
333
334 def _simulate_install_events(self, app):
335 # pretend we started an install
336 self.rec_panel.backend.emit("transaction-started",
337 app.pkgname, app.appname,
338 "testid101",
339 TransactionTypes.INSTALL)
340 do_events_with_sleep()
341 # send the signal to complete the install
342 mock_result = Mock()
343 mock_result.pkgname = app.pkgname
344 self.rec_panel.backend.emit("transaction-finished",
345 mock_result)
346 do_events()
251347
252348
253class ExhibitsTestCase(unittest.TestCase):349class ExhibitsTestCase(unittest.TestCase):
254350
=== modified file 'tests/gtk3/windows.py'
--- tests/gtk3/windows.py 2012-08-28 21:34:36 +0000
+++ tests/gtk3/windows.py 2012-09-06 15:40:27 +0000
@@ -368,21 +368,22 @@
368368
369369
370def get_test_window_recommendations(panel_type="lobby"):370def get_test_window_recommendations(panel_type="lobby"):
371 # this is *way* too complicated we should *not* need a CatView
372 # here! see FIXME in RecommendationsPanel.__init__()
373 cache = get_test_pkg_info()371 cache = get_test_pkg_info()
374 db = get_test_db()372 db = get_test_db()
375 icons = get_test_gtk3_icon_cache()373 icons = get_test_gtk3_icon_cache()
376 catview = catview_gtk.CategoriesViewGtk(softwarecenter.paths.datadir,374 from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper
377 softwarecenter.paths.APP_INSTALL_PATH, cache, db, icons)375 properties_helper = AppPropertiesHelper(db, cache, icons)
378376
379 if panel_type is "lobby":377 if panel_type is "lobby":
380 view = recommendations.RecommendationsPanelLobby(catview)378 view = recommendations.RecommendationsPanelLobby(
379 db, properties_helper)
381 elif panel_type is "category":380 elif panel_type is "category":
382 cats = get_test_categories(db)381 cats = get_test_categories(db)
383 view = recommendations.RecommendationsPanelCategory(catview, cats[0])382 view = recommendations.RecommendationsPanelCategory(
383 db, properties_helper, cats[0])
384 else: # panel_type is "details":384 else: # panel_type is "details":
385 view = recommendations.RecommendationsPanelDetails(catview)385 view = recommendations.RecommendationsPanelDetails(
386 db, properties_helper)
386387
387 win = get_test_window(child=view)388 win = get_test_window(child=view)
388 win.set_data("rec_panel", view)389 win.set_data("rec_panel", view)
389390
=== modified file 'tests/test_recagent.py'
--- tests/test_recagent.py 2012-08-29 14:46:51 +0000
+++ tests/test_recagent.py 2012-09-06 15:40:27 +0000
@@ -129,6 +129,16 @@
129 self.loop.run()129 self.loop.run()
130 self.assertTrue(self.error)130 self.assertTrue(self.error)
131131
132 @unittest.skip("server returns 401")
133 def test_recagent_post_implicit_feedback(self):
134 self.recommender_agent.connect("submit-implicit-feedback-finished",
135 self.on_query_done)
136 from softwarecenter.enums import RecommenderFeedbackActions
137 self.recommender_agent.post_implicit_feedback(
138 "bluefish",
139 RecommenderFeedbackActions.INSTALLED)
140 self.assertServerReturnsWithNoError()
141
132142
133if __name__ == "__main__":143if __name__ == "__main__":
134 #import logging144 #import logging

Subscribers

People subscribed via source and target branches