=== 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 @@
             scheme=AUTHENTICATED_API_SCHEME)
 
     @oauth_protected
-    @validate('pkgname', str)
+    @validate_pattern('pkgname', '[^/]+')
     @validate_pattern('action', '\w{3,20}')
     @returns_json
     def implicit_feedback(self, pkgname, action):

=== 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 @@
                           GObject.TYPE_NONE,
                           (GObject.TYPE_PYOBJECT,),
                          ),
+        "submit-implicit-feedback-finished": (GObject.SIGNAL_RUN_LAST,
+                                              GObject.TYPE_NONE,
+                                              (GObject.TYPE_PYOBJECT,),
+                                             ),
         "error": (GObject.SIGNAL_RUN_LAST,
                   GObject.TYPE_NONE,
                   (str,),
@@ -215,6 +219,22 @@
         spawner.run_generic_piston_helper(
             "SoftwareCenterRecommenderAPI", "recommend_top")
 
+    def post_implicit_feedback(self, pkgname, action):
+        # build the command
+        LOG.debug("called post_implicit_feedback with %s, '%s'" %
+                (pkgname, action))
+        spawner = SpawnHelper()
+        spawner.parent_xid = self.xid
+        spawner.needs_auth = True
+        spawner.connect("data-available",
+                        self._on_submit_implicit_feedback_data)
+        spawner.connect("error", lambda spawner, err: self.emit("error", err))
+        spawner.run_generic_piston_helper(
+            "SoftwareCenterRecommenderAPI",
+            "implicit_feedback",
+            pkgname=pkgname,
+            action=action)
+
     def is_opted_in(self):
         """
         Return True is the user is currently opted-in to the recommender
@@ -244,7 +264,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)
 
     def _on_recommend_me_data(self, spawner, piston_me_apps):
         self.emit("recommend-me", piston_me_apps)
@@ -258,6 +278,11 @@
     def _on_recommend_top_data(self, spawner, piston_top_apps):
         self.emit("recommend-top", piston_top_apps)
 
+    def _on_submit_implicit_feedback_data(self, spawner,
+                                     piston_submit_implicit_feedback):
+        self.emit("submit-implicit-feedback-finished",
+                  piston_submit_implicit_feedback)
+
     def _generate_submit_profile_data(self, recommender_uuid, package_list):
         submit_profile_data = [{
             'uuid': recommender_uuid,

=== 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 @@
     PACKAGE = ","
 
 
+# action values for recommendations feedback
+class RecommenderFeedbackActions:
+    INSTALLED = "installed"
+
 # mouse event codes for back/forward buttons
 # TODO: consider whether we ought to get these values from gconf so that we
 #       can be sure to use the corresponding values used by Nautilus:

=== 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 @@
         self.subcategories_view.connect(
             "category-selected", self.on_subcategory_activated)
         self.subcategories_view.connect(
-            "application-activated", self.on_application_activated)
-        self.subcategories_view.connect(
             "show-category-applist", self.on_show_category_applist)
-        # FIXME: why do we have two application-{selected,activated] ?!?
-        self.subcategories_view.connect(
-            "application-selected", self.on_application_selected)
         self.subcategories_view.connect(
             "application-activated", self.on_application_activated)
         self.scroll_subcategories = Gtk.ScrolledWindow()
@@ -215,8 +210,6 @@
         self.cat_view.connect(
             "category-selected", self.on_category_activated)
         self.cat_view.connect(
-            "application-selected", self.on_application_selected)
-        self.cat_view.connect(
             "application-activated", self.on_application_activated)
 
         # details
@@ -761,9 +754,9 @@
         vm = get_viewmanager()
         vm.display_page(self, page, self.state)
 
-    def on_application_selected(self, appview, app):
-        """Callback for when an app is selected."""
-        super(AvailablePane, self).on_application_selected(appview, app)
+    def on_application_activated(self, appview, app):
+        """Callback for when an app is activated."""
+        super(AvailablePane, self).on_application_activated(appview, app)
         if self.state.subcategory:
             self.current_app_by_subcategory[self.state.subcategory] = app
         else:

=== 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 @@
 from softwarecenter.backend.weblive import get_weblive_backend
 from softwarecenter.ui.gtk3.dialogs import error
 
-# FIXME: this is needed for the recommendations but really should become
-#        a widget or something generic instead
-from softwarecenter.ui.gtk3.views.catview_gtk import CategoriesViewGtk
-
 from softwarecenter.ui.gtk3.em import StockEms, em
 from softwarecenter.ui.gtk3.drawing import color_to_hex
 from softwarecenter.ui.gtk3.session.appmanager import get_appmanager
@@ -75,6 +71,8 @@
 
 import softwarecenter.ui.gtk3.dialogs as dialogs
 
+from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper
+
 from softwarecenter.hw import get_hw_missing_long_description
 from softwarecenter.region import REGION_WARNING_STRING
 
@@ -822,6 +820,8 @@
         self.appdetails = None
         self.addons_to_install = []
         self.addons_to_remove = []
+        self.properties_helper = AppPropertiesHelper(
+                self.db, self.cache, self.icons)
         # reviews
         self.review_loader = get_review_loader(self.cache, self.db)
         self.review_loader.connect(
@@ -1301,9 +1301,9 @@
         vb.pack_start(self._hbars[2], False, False, 0)
 
         # recommendations
-        catview = CategoriesViewGtk(
-            self.datadir, None, self.cache, self.db, self.icons, None)
-        self.recommended_for_app_panel = RecommendationsPanelDetails(catview)
+        self.recommended_for_app_panel = RecommendationsPanelDetails(
+                self.db,
+                self.properties_helper)
         self.recommended_for_app_panel.connect(
             "application-activated",
             self._on_recommended_application_activated)

=== 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 @@
 from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper
 from softwarecenter.ui.gtk3.widgets.viewport import Viewport
 from softwarecenter.ui.gtk3.widgets.containers import (
-     FramedHeaderBox, FramedBox, FlowableGrid)
+                                        FramedHeaderBox,
+                                        FramedBox,
+                                        TileGrid)
 from softwarecenter.ui.gtk3.widgets.recommendations import (
                                         RecommendationsPanelLobby,
                                         RecommendationsPanelCategory)
 from softwarecenter.ui.gtk3.widgets.exhibits import (
                                         ExhibitBanner, FeaturedExhibit)
 from softwarecenter.ui.gtk3.widgets.buttons import (LabelTile,
-                                                    CategoryTile,
-                                                    FeaturedTile)
+                                                    CategoryTile)
 from softwarecenter.ui.gtk3.em import StockEms
 from softwarecenter.db.appfilter import AppFilter, get_global_filter
 from softwarecenter.db.enquire import AppEnquire
@@ -74,11 +75,6 @@
                               (GObject.TYPE_PYOBJECT, ),
                              ),
 
-        "application-selected": (GObject.SignalFlags.RUN_LAST,
-                                 None,
-                                 (GObject.TYPE_PYOBJECT, ),
-                                ),
-
         "application-activated": (GObject.SignalFlags.RUN_LAST,
                                   None,
                                   (GObject.TYPE_PYOBJECT, ),
@@ -154,19 +150,6 @@
         self.connect("size-allocate", self.on_size_allocate)
         return
 
-    def _add_tiles_to_flowgrid(self, docs, flowgrid, amount):
-        '''Adds application tiles to a FlowableGrid:
-           docs = xapian documents (apps)
-           flowgrid = the FlowableGrid to add tiles to
-           amount = number of tiles to add from start of doc range'''
-        amount = min(len(docs), amount)
-        for doc in docs[0:amount]:
-            tile = FeaturedTile(self.properties_helper, doc)
-            tile.connect('clicked', self.on_app_clicked,
-                         self.properties_helper.get_application(doc))
-            flowgrid.add_child(tile)
-        return
-
     def on_size_allocate(self, widget, _):
         a = widget.get_allocation()
         prev = self._prev_alloc
@@ -187,10 +170,11 @@
         assets["stipple"] = ptrn
         return assets
 
-    def on_app_clicked(self, btn, app):
-        """emit the category-selected signal when a category was clicked"""
+    def on_application_activated(self, btn, app):
+        """ pass the application-activated signal along when an application
+            is clicked-through in one of the tiles
+        """
         def timeout_emit():
-            self.emit("application-selected", app)
             self.emit("application-activated", app)
             return False
 
@@ -362,13 +346,16 @@
             self.categories, u"Top Rated")  # untranslated name
         if top_rated_cat:
             docs = top_rated_cat.get_documents(self.db)
-            self._add_tiles_to_flowgrid(docs, self.top_rated,
-                                        TOP_RATED_CAROUSEL_LIMIT)
+            self.top_rated.add_tiles(self.properties_helper,
+                                     docs,
+                                     TOP_RATED_CAROUSEL_LIMIT)
             self.top_rated.show_all()
         return top_rated_cat
 
     def _append_top_rated(self):
-        self.top_rated = FlowableGrid()
+        self.top_rated = TileGrid()
+        self.top_rated.connect("application-activated",
+                               self.on_application_activated)
         #~ self.top_rated.row_spacing = StockEms.SMALL
         self.top_rated_frame = FramedHeaderBox()
         self.top_rated_frame.set_header_label(_("Top Rated"))
@@ -380,7 +367,6 @@
             self.top_rated_frame.header_implements_more_button()
             self.top_rated_frame.more.connect('clicked',
                                self.on_category_clicked, top_rated_cat)
-        return
 
     def _update_whats_new_content(self):
         # remove any existing children from the grid widget
@@ -390,13 +376,16 @@
             self.categories, u"What\u2019s New")  # untranslated name
         if whats_new_cat:
             docs = whats_new_cat.get_documents(self.db)
-            self._add_tiles_to_flowgrid(docs, self.whats_new,
-                                        WHATS_NEW_CAROUSEL_LIMIT)
+            self.whats_new.add_tiles(self.properties_helper,
+                                     docs,
+                                     WHATS_NEW_CAROUSEL_LIMIT)
             self.whats_new.show_all()
         return whats_new_cat
 
     def _append_whats_new(self):
-        self.whats_new = FlowableGrid()
+        self.whats_new = TileGrid()
+        self.whats_new.connect("application-activated",
+                               self.on_application_activated)
         self.whats_new_frame = FramedHeaderBox()
         self.whats_new_frame.set_header_label(_(u"What\u2019s New"))
         self.whats_new_frame.add(self.whats_new)
@@ -412,13 +401,33 @@
     def _update_recommended_for_you_content(self):
         if (self.recommended_for_you_panel and
             self.recommended_for_you_panel.get_parent()):
+            # disconnect listeners
+            self.recommended_for_you_panel.disconnect_by_func(
+                    self.on_application_activated)
+            self.recommended_for_you_panel.disconnect_by_func(
+                    self.on_category_clicked)
+            # and remove the panel
             self.right_column.remove(self.recommended_for_you_panel)
-        self.recommended_for_you_panel = RecommendationsPanelLobby(self)
+        self.recommended_for_you_panel = RecommendationsPanelLobby(
+                self.db,
+                self.properties_helper)
+        self.recommended_for_you_panel.connect("application-activated",
+                                               self.on_application_activated)
+        self.recommended_for_you_panel.connect(
+                'more-button-clicked',
+                self.on_category_clicked)
         self.right_column.pack_start(self.recommended_for_you_panel,
                                     True, True, 0)
 
     def _append_recommended_for_you(self):
-        self.recommended_for_you_panel = RecommendationsPanelLobby(self)
+        self.recommended_for_you_panel = RecommendationsPanelLobby(
+                self.db,
+                self.properties_helper)
+        self.recommended_for_you_panel.connect("application-activated",
+                                               self.on_application_activated)
+        self.recommended_for_you_panel.connect(
+                'more-button-clicked',
+                self.on_category_clicked)
         self.right_column.pack_start(
             self.recommended_for_you_panel, True, True, 0)
 
@@ -519,12 +528,15 @@
             'category': GObject.markup_escape_text(self.header)}
         self.top_rated_frame.set_header_label(m)
         docs = self._get_sub_top_rated_content(category)
-        self._add_tiles_to_flowgrid(docs, self.top_rated,
-                                    TOP_RATED_CAROUSEL_LIMIT)
+        self.top_rated.add_tiles(self.properties_helper,
+                                 docs,
+                                 TOP_RATED_CAROUSEL_LIMIT)
         return
 
     def _append_sub_top_rated(self):
-        self.top_rated = FlowableGrid()
+        self.top_rated = TileGrid()
+        self.top_rated.connect("application-activated",
+                               self.on_application_activated)
         self.top_rated.set_row_spacing(6)
         self.top_rated.set_column_spacing(6)
         self.top_rated_frame = FramedHeaderBox()
@@ -535,10 +547,18 @@
     def _update_recommended_for_you_in_cat_content(self, category):
         if (self.recommended_for_you_in_cat and
             self.recommended_for_you_in_cat.get_parent()):
+            self.recommended_for_you_in_cat.disconnect_by_func(
+                    self.on_application_activated)
             self.vbox.remove(self.recommended_for_you_in_cat)
         self.recommended_for_you_in_cat = RecommendationsPanelCategory(
-                                                                    self,
-                                                                    category)
+                self.db,
+                self.properties_helper,
+                category)
+        self.recommended_for_you_in_cat.connect("application-activated",
+                                                self.on_application_activated)
+        self.recommended_for_you_in_cat.connect(
+                'more-button-clicked',
+                self.on_category_clicked)
         # only show the panel in the categories view when the user
         # is opted in to the recommender service
         # FIXME: this is needed vs. a simple hide() on the widget because
@@ -596,7 +616,7 @@
     def _append_subcat_departments(self):
         self.subcat_label = Gtk.Label()
         self.subcat_label.set_alignment(0, 0.5)
-        self.departments = FlowableGrid(paint_grid_pattern=False)
+        self.departments = TileGrid(paint_grid_pattern=False)
         self.departments.set_row_spacing(StockEms.SMALL)
         self.departments.set_column_spacing(StockEms.SMALL)
         self.departments_frame = FramedBox(spacing=StockEms.MEDIUM,

=== 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 @@
 PI_OVER_180 = PI / 180
 import softwarecenter.paths
 
-from gi.repository import Gtk, Gdk
+from gi.repository import Gtk, Gdk, GObject
 
 from buttons import MoreLink
 from softwarecenter.ui.gtk3.em import StockEms
 from softwarecenter.ui.gtk3.drawing import rounded_rect
 from softwarecenter.ui.gtk3.widgets.spinner import (SpinnerView,
                                                     SpinnerNotebook)
+from softwarecenter.ui.gtk3.widgets.buttons import FeaturedTile
 
 
 class FlowableGrid(Gtk.Fixed):
@@ -182,6 +183,39 @@
             self.remove(child)
 
 
+class TileGrid(FlowableGrid):
+    ''' a flowable layout widget that holds a collection of TileButtons
+    '''
+    __gsignals__ = {
+        "application-activated": (GObject.SignalFlags.RUN_LAST,
+                                  None,
+                                  (GObject.TYPE_PYOBJECT, ),
+                                 ),
+        }
+
+    def add_tiles(self, properties_helper, docs, amount):
+        '''Adds application tiles to an ApplicationTileGrid:
+                properties_help -- an instance of the PropertiesHelper object
+                docs -- xapian documents (apps)
+                amount -- number of tiles to add from start of doc range
+        '''
+        amount = min(len(docs), amount)
+        for doc in docs[0:amount]:
+            tile = FeaturedTile(properties_helper, doc)
+            tile.connect('clicked', self._on_app_clicked,
+                         properties_helper.get_application(doc))
+            self.add_child(tile)
+
+    # private
+    def _on_app_clicked(self, btn, app):
+        """emit signal when a tile is clicked"""
+        def timeout_emit():
+            self.emit("application-activated", app)
+            return False
+
+        GObject.timeout_add(50, timeout_emit)
+
+
 # first tier of caching, cache component assets from which frames are
 # rendered
 _frame_asset_cache = {}
@@ -454,6 +488,9 @@
         self.spinner_notebook = SpinnerNotebook(self.content_box,
                                                 spinner_size=SpinnerView.SMALL)
         self.box.add(self.spinner_notebook)
+        # make the "More" button, but don't add it to the header unless/until
+        # we get a header_implements_more_button
+        self.more = MoreLink()
 
     def on_draw(self, cr):
         a = self.get_allocation()
@@ -473,17 +510,6 @@
     def pack_end(self, *args, **kwargs):
         return self.content_box.pack_end(*args, **kwargs)
 
-    # XXX: non-functional with current code...
-    #~ def set_header_expand(self, expand):
-        #~ alignment = self.header_alignment
-        #~ if expand:
-            #~ expand = 1.0
-        #~ else:
-            #~ expand = 0.0
-        #~ alignment.set(alignment.get_property("xalign"),
-                      #~ alignment.get_property("yalign"),
-                      #~ expand, 1.0)
-
     def set_header_position(self, position):
         alignment = self.header_alignment
         alignment.set(position, 0.5,
@@ -502,18 +528,16 @@
         self.title.set_markup(self.MARKUP % label)
 
     def header_implements_more_button(self):
-        if not hasattr(self, "more"):
-            self.more = MoreLink()
+        if self.more not in self.header:
             self.header.pack_end(self.more, False, False, 0)
 
     def remove_more_button(self):
-        if hasattr(self, "more"):
+        if self.more in self.header:
             self.header.remove(self.more)
-            del self.more
 
     def render_header(self, cr, a, border_radius, assets):
 
-        if hasattr(self, "more"):
+        if self.more in self.header:
             context = self.get_style_context()
 
             # set the arrow fill color

=== 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 @@
 
 from softwarecenter.ui.gtk3.em import StockEms
 from softwarecenter.ui.gtk3.widgets.containers import (FramedHeaderBox,
-                                                       FlowableGrid)
+                                                       TileGrid)
 from softwarecenter.ui.gtk3.utils import get_parent_xid
 from softwarecenter.db.categories import (RecommendedForYouCategory,
                                           AppRecommendationsCategory)
+from softwarecenter.backend import get_install_backend
 from softwarecenter.backend.recagent import RecommenderAgent
 from softwarecenter.backend.login_sso import get_sso_backend
 from softwarecenter.backend.ubuntusso import get_ubuntu_sso_backend
@@ -35,6 +36,8 @@
     LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT,
     DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT,
     SOFTWARE_CENTER_NAME_KEYRING,
+    RecommenderFeedbackActions,
+    TransactionTypes,
     )
 from softwarecenter.utils import utf8
 from softwarecenter.netstatus import network_state_is_connected
@@ -54,18 +57,42 @@
                                  ),
         }
 
-    def __init__(self, catview):
+    def __init__(self):
         FramedHeaderBox.__init__(self)
-        # FIXME: we only need the catview for "add_titles_to_flowgrid"
-        #        and "on_category_clicked" so we should be able to
-        #        extract this to a "leaner" widget
-        self.catview = catview
-        self.catview.connect(
-                    "application-activated", self._on_application_activated)
         self.recommender_agent = RecommenderAgent()
+        # keep track of applications that have been viewed via a
+        # recommendation so that we can detect when a recommended app
+        # has been installed
+        self.recommended_apps_viewed = set()
+        self.backend = get_install_backend()
+        self.backend.connect("transaction-started",
+                             self._on_transaction_started)
+        self.backend.connect("transaction-finished",
+                             self._on_transaction_finished)
 
-    def _on_application_activated(self, catview, app):
+    def _on_application_activated(self, tile, app):
         self.emit("application-activated", app)
+        # we only track installed items if the user has opted-in to the
+        # recommendations service
+        if self.recommender_agent.is_opted_in():
+            self.recommended_apps_viewed.add(app.pkgname)
+
+    def _on_transaction_started(self, backend, pkgname, appname, trans_id,
+                                trans_type):
+        if (trans_type != TransactionTypes.INSTALL and
+            pkgname in self.recommended_apps_viewed):
+            # if the transaction is not an installation we don't want to
+            # track it as a recommended item
+            self.recommended_apps_viewed.remove(pkgname)
+
+    def _on_transaction_finished(self, backend, result):
+        if result.pkgname in self.recommended_apps_viewed:
+            self.recommended_apps_viewed.remove(result.pkgname)
+            if network_state_is_connected():
+                # no need to monitor for an error, etc., for this
+                self.recommender_agent.post_implicit_feedback(
+                        result.pkgname,
+                        RecommenderFeedbackActions.INSTALLED)
 
     def _on_recommender_agent_error(self, agent, msg):
         LOG.warn("Error while accessing the recommender agent: %s" % msg)
@@ -81,8 +108,18 @@
     Panel for use in the category view that displays recommended apps for
     the given category
     """
-    def __init__(self, catview, subcategory):
-        RecommendationsPanel.__init__(self, catview)
+
+    __gsignals__ = {
+        "more-button-clicked": (GObject.SignalFlags.RUN_LAST,
+                                None,
+                                (GObject.TYPE_PYOBJECT, ),
+                               ),
+        }
+
+    def __init__(self, db, properties_helper, subcategory):
+        RecommendationsPanel.__init__(self)
+        self.db = db
+        self.properties_helper = properties_helper
         self.subcategory = subcategory
         if self.subcategory:
             self.set_header_label(GObject.markup_escape_text(utf8(
@@ -98,7 +135,9 @@
         if self.recommended_for_you_content:
             self.recommended_for_you_content.destroy()
         # add the new stuff
-        self.recommended_for_you_content = FlowableGrid()
+        self.recommended_for_you_content = TileGrid()
+        self.recommended_for_you_content.connect(
+                    "application-activated", self._on_application_activated)
         self.add(self.recommended_for_you_content)
         self.spinner_notebook.show_spinner(_(u"Receiving recommendations…"))
         # get the recommendations from the recommender agent
@@ -112,23 +151,27 @@
 
     def _on_recommended_for_you_agent_refresh(self, cat):
         self.header_implements_more_button()
-        docs = cat.get_documents(self.catview.db)
+        self.more.connect("clicked",
+                          self._on_more_button_clicked,
+                          self.recommended_for_you_cat)
+        docs = cat.get_documents(self.db)
         # display the recommendedations
         if len(docs) > 0:
-            self.catview._add_tiles_to_flowgrid(
-                docs, self.recommended_for_you_content,
-                LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT)
+            self.recommended_for_you_content.add_tiles(
+                    self.properties_helper,
+                    docs,
+                    LOBBY_RECOMMENDATIONS_CAROUSEL_LIMIT)
             self.recommended_for_you_content.show_all()
             self.spinner_notebook.hide_spinner()
-            self.more.connect('clicked',
-                              self.catview.on_category_clicked,
-                              cat)
             self.header.queue_draw()
             self.show_all()
         else:
             # hide the panel if we have no recommendations to show
             self.hide()
 
+    def _on_more_button_clicked(self, btn, category):
+        self.emit("more-button-clicked", category)
+
 
 class RecommendationsPanelLobby(RecommendationsPanelCategory):
     """
@@ -155,8 +198,10 @@
     NO_NETWORK_RECOMMENDATIONS_TEXT = _(u"Recommendations will appear "
                                          "when next online.")
 
-    def __init__(self, catview):
-        RecommendationsPanel.__init__(self, catview)
+    def __init__(self, db, properties_helper):
+        RecommendationsPanel.__init__(self)
+        self.db = db
+        self.properties_helper = properties_helper
         self.subcategory = None
         self.set_header_label(_(u"Recommended For You"))
         self.recommended_for_you_content = None
@@ -325,7 +370,7 @@
                                   self._on_profile_submitted)
         self.recommender_agent.connect("error",
                                   self._on_profile_submitted_error)
-        self.recommender_agent.post_submit_profile(self.catview.db)
+        self.recommender_agent.post_submit_profile(self.db)
 
     def _on_profile_submitted(self, agent, profile):
         # after the user profile data has been uploaded, make the request
@@ -362,10 +407,14 @@
     Panel for use in the details view to display recommendations for a given
     application
     """
-    def __init__(self, catview):
-        RecommendationsPanel.__init__(self, catview)
+    def __init__(self, db, properties_helper):
+        RecommendationsPanel.__init__(self)
+        self.db = db
+        self.properties_helper = properties_helper
         self.set_header_label(_(u"People Also Installed"))
-        self.app_recommendations_content = FlowableGrid()
+        self.app_recommendations_content = TileGrid()
+        self.app_recommendations_content.connect(
+                    "application-activated", self._on_application_activated)
         self.add(self.app_recommendations_content)
 
     def set_pkgname(self, pkgname):
@@ -385,12 +434,13 @@
                                              self._on_recommender_agent_error)
 
     def _on_app_recommendations_agent_refresh(self, cat):
-        docs = cat.get_documents(self.catview.db)
+        docs = cat.get_documents(self.db)
         # display the recommendations
         if len(docs) > 0:
-            self.catview._add_tiles_to_flowgrid(
-                docs, self.app_recommendations_content,
-                DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT)
+            self.app_recommendations_content.add_tiles(
+                    self.properties_helper,
+                    docs,
+                    DETAILS_RECOMMENDATIONS_CAROUSEL_LIMIT)
             self.show_all()
             self.spinner_notebook.hide_spinner()
         else:

=== 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 @@
 
 from softwarecenter.db.appfilter import AppFilter
 from softwarecenter.db.database import StoreDatabase
-from softwarecenter.enums import SortMethods
+from softwarecenter.enums import (SortMethods,
+                                  TransactionTypes,
+                                  RecommenderFeedbackActions)
 from softwarecenter.ui.gtk3.views import catview_gtk
 from softwarecenter.ui.gtk3.widgets.containers import FramedHeaderBox
 from softwarecenter.ui.gtk3.widgets.spinner import SpinnerNotebook
@@ -35,6 +37,7 @@
 
     def setUp(self):
         self._cat = None
+        self._app = None
         self.win = get_test_window_catview(self.db)
         self.addCleanup(self.win.destroy)
         self.notebook = self.win.get_child()
@@ -121,6 +124,7 @@
            '.post_submit_profile')
     def setUp(self, mock_query, mock_recommender_is_opted_in, mock_sso):
         self._cat = None
+        self._app = None
         # patch the recommender to specify that we are not opted-in at
         # the start of each test
         mock_recommender_is_opted_in.return_value = False
@@ -162,11 +166,7 @@
                 mock_network_state_is_connected):
         # simulate no network available
         mock_network_state_is_connected.return_value = False
-        # click the opt-in button to initiate the process
-        self.rec_panel.opt_in_button.clicked()
-        do_events()
-        self.rec_panel._update_recommended_for_you_content()
-        do_events()
+        self._populate_recommended_for_you_panel()
         self.assertEqual(self.rec_panel.spinner_notebook.get_current_page(),
                          FramedHeaderBox.CONTENT)
         # ensure that we are showing the network not available view
@@ -176,14 +176,9 @@
                          self.rec_panel.NO_NETWORK_RECOMMENDATIONS_TEXT)
 
     def test_recommended_for_you_display_recommendations(self):
-        # click the opt-in button to initiate the process,
-        # this will show the spinner
-        self.rec_panel.opt_in_button.clicked()
-        do_events()
-        self.rec_panel._update_recommended_for_you_content()
-        do_events()
+        self._populate_recommended_for_you_panel()
         # we fake the callback from the agent here
-        for_you = self.lobby.recommended_for_you_panel.recommended_for_you_cat
+        for_you = self.rec_panel.recommended_for_you_cat
         for_you._recommend_me_result(None,
             make_recommender_agent_recommend_me_dict())
         self.assertNotEqual(for_you.get_documents(self.db), [])
@@ -192,7 +187,7 @@
         do_events()
         # test clicking recommended_for_you More button
         self.lobby.connect("category-selected", self._on_category_selected)
-        self.lobby.recommended_for_you_panel.more.clicked()
+        self.rec_panel.more.clicked()
         # this is delayed for some reason so we need to sleep here
         do_events_with_sleep()
         self.assertNotEqual(self._cat, None)
@@ -208,13 +203,8 @@
         self.assertFalse(visible)
 
     def test_recommended_for_you_display_recommendations_opted_in(self):
-        # click the opt-in button to initiate the process,
-        # this will show the spinner
-        self.rec_panel.opt_in_button.clicked()
-        do_events()
-        self.rec_panel._update_recommended_for_you_content()
-        do_events()
-        
+        self._populate_recommended_for_you_panel()
+
         # we want to work in the "subcat" view
         self.notebook.next_page()
 
@@ -248,6 +238,112 @@
         do_events_with_sleep()
         self.assertNotEqual(self._cat, None)
         self.assertEqual(self._cat.name, "Recommended For You in Internet")
+        
+    def test_implicit_recommender_feedback(self):
+        self._populate_recommended_for_you_panel()
+        # we fake the callback from the agent here
+        for_you = self.rec_panel.recommended_for_you_cat
+        for_you._recommend_me_result(None,
+            make_recommender_agent_recommend_me_dict())
+        do_events()
+        
+        post_implicit_feedback_fn = ('softwarecenter.ui.gtk3.widgets'
+                                     '.recommendations.RecommenderAgent'
+                                     '.post_implicit_feedback')
+        with patch(post_implicit_feedback_fn) as mock_post_implicit_feedback:
+            # we want to grab the app that is activated, it will be in
+            # self._app after the app is activated on the tile click
+            self.rec_panel.recommended_for_you_content.connect(
+                                            "application-activated",
+                                            self._on_application_activated)
+            # click a recommendation in the lobby
+            self._click_first_tile_in_panel(self.rec_panel)
+            # simulate installing the application
+            self._simulate_install_events(self._app)
+            # and verify that after the install has completed we have fired
+            # the implicit feedback call to the recommender with the correct
+            # arguments
+            mock_post_implicit_feedback.assert_called_with(
+                    self._app.pkgname,
+                    RecommenderFeedbackActions.INSTALLED)
+            # finally, make sure that we have cleared the application
+            # from the recommended_apps_viewed set
+            self.assertFalse(self._app.pkgname in
+                    self.rec_panel.recommended_apps_viewed)
+                    
+    def test_implicit_recommender_feedback_recommendations_panel_only(self):
+        # this test insures that we only provide feedback when installing
+        # items clicked to via the recommendations panel itself, and not
+        # via the What's New or Top Rated panels
+        self._populate_recommended_for_you_panel()
+        # we fake the callback from the agent here
+        for_you = self.rec_panel.recommended_for_you_cat
+        for_you._recommend_me_result(None,
+            make_recommender_agent_recommend_me_dict())
+        do_events()
+        
+        post_implicit_feedback_fn = ('softwarecenter.ui.gtk3.widgets'
+                                     '.recommendations.RecommenderAgent'
+                                     '.post_implicit_feedback')
+        with patch(post_implicit_feedback_fn) as mock_post_implicit_feedback:
+            # we want to grab the app that is activated, it will be in
+            # self._app after the app is activated on the tile click
+            self.lobby.top_rated.connect("application-activated",
+                                          self._on_application_activated)
+            # click a tile in the Top Rated section of the lobby
+            self._click_first_tile_in_panel(self.lobby.top_rated_frame)
+            # simulate installing the application
+            self._simulate_install_events(self._app)
+            # and verify that after the install has completed we have *not*
+            # fired the implicit feedback call to the recommender service
+            self.assertFalse(mock_post_implicit_feedback.called)
+                    
+    def test_implicit_recommender_feedback_not_opted_in(self):
+        # this test verifies that we do *not* send feedback to the
+        # recommender service if the user has not opted-in to it
+        post_implicit_feedback_fn = ('softwarecenter.ui.gtk3.widgets'
+                                     '.recommendations.RecommenderAgent'
+                                     '.post_implicit_feedback')
+        with patch(post_implicit_feedback_fn) as mock_post_implicit_feedback:
+            # we are not opted-in
+            from softwarecenter.db.application import Application
+            app = Application("Calculator", "gcalctool")
+            # simulate installing the application
+            self._simulate_install_events(app)
+            # and verify that after the install has completed we have *not*
+            # fired the implicit feedback call to the recommender
+            self.assertFalse(mock_post_implicit_feedback.called)
+
+    def _populate_recommended_for_you_panel(self):
+        # click the opt-in button to initiate the process
+        self.rec_panel.opt_in_button.clicked()
+        do_events()
+        # and update the recommended for you panel to load it up
+        self.rec_panel._update_recommended_for_you_content()
+        do_events()
+
+    def _on_application_activated(self, catview, app):
+        self._app = app
+
+    def _click_first_tile_in_panel(self, framed_header_box):
+        first_tile = (framed_header_box.content_box.
+                        get_children()[0].get_children()[0])
+        first_tile.clicked()
+        do_events_with_sleep()
+
+    def _simulate_install_events(self, app):
+        # pretend we started an install
+        self.rec_panel.backend.emit("transaction-started",
+                                    app.pkgname, app.appname,
+                                    "testid101",
+                                    TransactionTypes.INSTALL)
+        do_events_with_sleep()
+        # send the signal to complete the install
+        mock_result = Mock()
+        mock_result.pkgname = app.pkgname
+        self.rec_panel.backend.emit("transaction-finished",
+                                    mock_result)
+        do_events()
 
 
 class ExhibitsTestCase(unittest.TestCase):

=== 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 @@
 
 
 def get_test_window_recommendations(panel_type="lobby"):
-    # this is *way* too complicated we should *not* need a CatView
-    # here! see FIXME in RecommendationsPanel.__init__()
     cache = get_test_pkg_info()
     db = get_test_db()
     icons = get_test_gtk3_icon_cache()
-    catview = catview_gtk.CategoriesViewGtk(softwarecenter.paths.datadir,
-        softwarecenter.paths.APP_INSTALL_PATH, cache, db, icons)
-
+    from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper
+    properties_helper = AppPropertiesHelper(db, cache, icons)
+            
     if panel_type is "lobby":
-        view = recommendations.RecommendationsPanelLobby(catview)
+        view = recommendations.RecommendationsPanelLobby(
+                db, properties_helper)
     elif panel_type is "category":
         cats = get_test_categories(db)
-        view = recommendations.RecommendationsPanelCategory(catview, cats[0])
+        view = recommendations.RecommendationsPanelCategory(
+                db, properties_helper, cats[0])
     else:  # panel_type is "details":
-        view = recommendations.RecommendationsPanelDetails(catview)
+        view = recommendations.RecommendationsPanelDetails(
+                db, properties_helper)
 
     win = get_test_window(child=view)
     win.set_data("rec_panel", view)

=== 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 @@
         self.loop.run()
         self.assertTrue(self.error)
 
+    @unittest.skip("server returns 401")
+    def test_recagent_post_implicit_feedback(self):
+        self.recommender_agent.connect("submit-implicit-feedback-finished",
+                                  self.on_query_done)
+        from softwarecenter.enums import RecommenderFeedbackActions
+        self.recommender_agent.post_implicit_feedback(
+                "bluefish",
+                RecommenderFeedbackActions.INSTALLED)
+        self.assertServerReturnsWithNoError()
+
 
 if __name__ == "__main__":
     #import logging

