Merge lp:~gary-lasker/software-center/recommends-ui-lobby into lp:software-center

Proposed by Gary Lasker
Status: Merged
Merged at revision: 2711
Proposed branch: lp:~gary-lasker/software-center/recommends-ui-lobby
Merge into: lp:software-center
Diff against target: 1123 lines (+490/-208)
15 files modified
data/featured.menu.in (+21/-21)
po/POTFILES.in (+1/-1)
setup.cfg (+1/-1)
softwarecenter/backend/piston/sreclient_pristine.py (+1/-1)
softwarecenter/backend/reviews/__init__.py (+2/-14)
softwarecenter/db/categories.py (+78/-4)
softwarecenter/db/enquire.py (+3/-1)
softwarecenter/enums.py (+2/-1)
softwarecenter/testutils.py (+39/-0)
softwarecenter/ui/gtk3/views/catview_gtk.py (+151/-149)
softwarecenter/ui/gtk3/widgets/containers.py (+35/-3)
test/gtk3/test_catview.py (+45/-8)
test/test_categories.py (+39/-3)
test/test_recagent.py (+64/-0)
utils/piston-helpers/piston_generic_helper.py (+8/-1)
To merge this branch: bzr merge lp:~gary-lasker/software-center/recommends-ui-lobby
Reviewer Review Type Date Requested Status
software-store-developers Pending
Review via email: mp+91201@code.launchpad.net

Description of the change

This branch implements the recommendations display piece of the home page UI for the lobby view's "Recommended for You" pane (spec at https://wiki.ubuntu.com/SoftwareCenter/Recommendations#Displaying). Note that the corresponding functionality on the server side is not yet deployed, so for testing we will need to rely on the included unit tests for now.

Also, you'll see that I've located the "Recommended for You" pane at the bottom of the view rather than alongside the categories list as specified in the spec. This is only temporary as I've set things up to not display the pane at all when a server is not available or when actual recommendations are not yet available from the server. Later, when everything is ready, we can simply swap the locations of the "Top Rated" and the "Recommended for You" panes.

You can use the following to specify the staging recommender agent:

  SOFTWARE_CENTER_RECOMMENDER_HOST=https://rec.staging.ubuntu.com/ PYTHONPATH=. python software-center

Thanks!

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/featured.menu.in'
2--- data/featured.menu.in 2011-06-23 14:32:13 +0000
3+++ data/featured.menu.in 2012-02-02 05:45:26 +0000
4@@ -7,27 +7,27 @@
5 </Flags>
6 <Include>
7 <Or>
8- <SCPkgname>armagetronad</SCPkgname>
9- <SCPkgname>calibre</SCPkgname>
10- <SCPkgname>cheese</SCPkgname>
11- <SCPkgname>homebank</SCPkgname>
12- <SCPkgname>stellarium</SCPkgname>
13- <SCPkgname>gimp</SCPkgname>
14- <SCPkgname>inkscape</SCPkgname>
15- <SCPkgname>blender</SCPkgname>
16- <SCPkgname>audacity</SCPkgname>
17- <SCPkgname>gufw</SCPkgname>
18- <SCPkgname>frozen-bubble</SCPkgname>
19- <SCPkgname>fretsonfire</SCPkgname>
20- <SCPkgname>moovida</SCPkgname>
21- <SCPkgname>liferea</SCPkgname>
22- <SCPkgname>arista</SCPkgname>
23- <SCPkgname>gtg</SCPkgname>
24- <SCPkgname>freeciv-client-gtk</SCPkgname>
25- <SCPkgname>supertuxkart</SCPkgname>
26- <SCPkgname>tumiki-fighters</SCPkgname>
27- <SCPkgname>tuxpaint</SCPkgname>
28- <SCPkgname>webservice-office-zoho</SCPkgname>
29+ <SCPkgname>armagetronad</SCPkgname>
30+ <SCPkgname>calibre</SCPkgname>
31+ <SCPkgname>cheese</SCPkgname>
32+ <SCPkgname>homebank</SCPkgname>
33+ <SCPkgname>stellarium</SCPkgname>
34+ <SCPkgname>gimp</SCPkgname>
35+ <SCPkgname>inkscape</SCPkgname>
36+ <SCPkgname>blender</SCPkgname>
37+ <SCPkgname>audacity</SCPkgname>
38+ <SCPkgname>gufw</SCPkgname>
39+ <SCPkgname>frozen-bubble</SCPkgname>
40+ <SCPkgname>fretsonfire</SCPkgname>
41+ <SCPkgname>moovida</SCPkgname>
42+ <SCPkgname>liferea</SCPkgname>
43+ <SCPkgname>arista</SCPkgname>
44+ <SCPkgname>gtg</SCPkgname>
45+ <SCPkgname>freeciv-client-gtk</SCPkgname>
46+ <SCPkgname>supertuxkart</SCPkgname>
47+ <SCPkgname>tumiki-fighters</SCPkgname>
48+ <SCPkgname>tuxpaint</SCPkgname>
49+ <SCPkgname>webservice-office-zoho</SCPkgname>
50 </Or>
51 </Include>
52 </Menu>
53
54=== renamed file 'data/new.menu.in' => 'data/whats_new.menu.in'
55=== modified file 'po/POTFILES.in'
56--- po/POTFILES.in 2012-01-19 12:48:54 +0000
57+++ po/POTFILES.in 2012-02-02 05:45:26 +0000
58@@ -4,7 +4,7 @@
59 data/ubuntu-software-center.desktop.in
60 data/unbranded-software-center.desktop.in
61 [type: gettext/xml] data/featured.menu.in
62-[type: gettext/xml] data/new.menu.in
63+[type: gettext/xml] data/whats_new.menu.in
64 [type: gettext/xml] data/software-center.menu.in
65 softwarecenter/backend/channel.py
66 softwarecenter/backend/login_sso.py
67
68=== modified file 'setup.cfg'
69--- setup.cfg 2011-07-21 15:28:53 +0000
70+++ setup.cfg 2012-02-02 05:45:26 +0000
71@@ -14,7 +14,7 @@
72 ),
73 ("share/app-install/menu.d/",
74 ("data/featured.menu.in",
75- "data/new.menu.in",
76+ "data/whats_new.menu.in",
77 "data/top-rated.menu.in",
78 )
79 ),
80
81=== modified file 'softwarecenter/backend/piston/sreclient_pristine.py'
82--- softwarecenter/backend/piston/sreclient_pristine.py 2012-01-06 09:11:38 +0000
83+++ softwarecenter/backend/piston/sreclient_pristine.py 2012-02-02 05:45:26 +0000
84@@ -7,7 +7,7 @@
85 AUTHENTICATED_API_SCHEME = 'https'
86
87 class SoftwareCenterRecommenderAPI(PistonAPI):
88- default_service_root = 'http://localhost:8000/api/2.0'
89+ default_service_root = 'http://localhost:8000/api/1.0'
90
91 @returns_json
92 def server_status(self):
93
94=== modified file 'softwarecenter/backend/reviews/__init__.py'
95--- softwarecenter/backend/reviews/__init__.py 2012-01-17 21:25:58 +0000
96+++ softwarecenter/backend/reviews/__init__.py 2012-02-02 05:45:26 +0000
97@@ -40,8 +40,7 @@
98 except ImportError:
99 import pickle
100
101-
102-from softwarecenter.db.categories import CategoriesParser
103+from softwarecenter.db.categories import get_query_for_category
104 from softwarecenter.db.database import Application, StoreDatabase
105 import softwarecenter.distro
106 from softwarecenter.i18n import get_language
107@@ -51,7 +50,6 @@
108 wilson_score,
109 )
110 from softwarecenter.paths import (SOFTWARE_CENTER_CACHE_DIR,
111- APP_INSTALL_PATH,
112 XAPIAN_BASE_PATH,
113 )
114 from softwarecenter.enums import ReviewSortMethods
115@@ -384,20 +382,10 @@
116 for key in cache.keys():
117 if key.pkgname in applist:
118 filtered_cache[key] = cache[key]
119-
120 return filtered_cache
121-
122- def _get_query_for_category(self, category):
123- cat_parser = CategoriesParser(self.db)
124- categories = cat_parser.parse_applications_menu(APP_INSTALL_PATH)
125- for c in categories:
126- if category == c.untranslated_name:
127- query = c.query
128- return query
129- return False
130
131 def _get_apps_for_category(self, category):
132- query = self._get_query_for_category(category)
133+ query = get_query_for_category(self.db, category)
134 if not query:
135 LOG.warn("_get_apps_for_category: received invalid category")
136 return []
137
138=== modified file 'softwarecenter/db/categories.py'
139--- softwarecenter/db/categories.py 2012-01-31 10:51:32 +0000
140+++ softwarecenter/db/categories.py 2012-02-02 05:45:26 +0000
141@@ -16,6 +16,7 @@
142 # this program; if not, write to the Free Software Foundation, Inc.,
143 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
144
145+from gi.repository import GObject
146 import gettext
147 import glob
148 import locale
149@@ -27,7 +28,15 @@
150 from xml.sax.saxutils import escape as xml_escape
151 from xml.sax.saxutils import unescape as xml_unescape
152
153-from softwarecenter.enums import SortMethods
154+from softwarecenter.enums import (
155+ SortMethods, NonAppVisibility)
156+from softwarecenter.backend.recommends import RecommenderAgent
157+from softwarecenter.db.appfilter import AppFilter
158+from softwarecenter.db.enquire import AppEnquire
159+from softwarecenter.db.utils import get_query_for_pkgnames
160+from softwarecenter.paths import APP_INSTALL_PATH
161+
162+from gettext import gettext as _
163
164 # not possible not use local logger
165 LOG = logging.getLogger(__name__)
166@@ -55,14 +64,24 @@
167 sorted_cats.append(cat)
168 break
169 return sorted_cats
170-
171-
172-class Category(object):
173+
174+def get_query_for_category(db, untranslated_category_name):
175+ cat_parser = CategoriesParser(db)
176+ categories = cat_parser.parse_applications_menu(APP_INSTALL_PATH)
177+ for c in categories:
178+ if untranslated_category_name == c.untranslated_name:
179+ query = c.query
180+ return query
181+ return False
182+
183+
184+class Category(GObject.GObject):
185 """represents a menu category"""
186 def __init__(self, untranslated_name, name, iconname, query,
187 only_unallocated=True, dont_display=False, flags=[],
188 subcategories=[], sortmode=SortMethods.BY_ALPHABET,
189 item_limit=0):
190+ GObject.GObject.__init__(self)
191 if type(name) == str:
192 self.name = unicode(name, 'utf8').encode('utf8')
193 else:
194@@ -83,11 +102,65 @@
195 def is_forced_sort_mode(self):
196 return (self.sortmode != SortMethods.BY_ALPHABET)
197
198+ def get_documents(self, db):
199+ """ return the database docids for the given category """
200+ enq = AppEnquire(db._aptcache, db)
201+ app_filter = AppFilter(db, db._aptcache)
202+ if "available-only" in self.flags:
203+ app_filter.set_available_only(True)
204+ if "not-installed-only" in self.flags:
205+ app_filter.set_not_installed_only(True)
206+ enq.set_query(self.query,
207+ limit=self.item_limit,
208+ filter=app_filter,
209+ sortmode=self.sortmode,
210+ nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE,
211+ nonblocking_load=False)
212+ return enq.get_documents()
213+
214 def __str__(self):
215 return "<Category: name='%s', sortmode='%s', "\
216 "item_limit='%s'>" % (
217 self.name, self.sortmode, self.item_limit)
218
219+
220+class RecommendedForYouCategory(Category):
221+
222+ __gsignals__ = {
223+ "needs-refresh" : (GObject.SIGNAL_RUN_LAST,
224+ GObject.TYPE_NONE,
225+ (),
226+ ),
227+ "recommender-agent-error" : (GObject.SIGNAL_RUN_LAST,
228+ GObject.TYPE_NONE,
229+ (GObject.TYPE_STRING,),
230+ ),
231+ }
232+
233+ def __init__(self):
234+ super(RecommendedForYouCategory, self).__init__(
235+ u"Recommended for You", _("Recommended for You"), None,
236+ xapian.Query(),flags=['available-only', 'not-installed-only'],
237+ item_limit=60)
238+ self.recommender_agent = RecommenderAgent()
239+ self.recommender_agent.connect(
240+ "recommend-top", self._recommend_top_result)
241+ self.recommender_agent.connect(
242+ "error", self._recommender_agent_error)
243+ self.recommender_agent.query_recommend_top()
244+
245+ def _recommend_top_result(self, recommender_agent, result_list):
246+ pkgs = []
247+ for item in result_list['recommendations']:
248+ pkgs.append(item['package_name'])
249+ self.query = get_query_for_pkgnames(pkgs)
250+ self.emit("needs-refresh")
251+
252+ def _recommender_agent_error(self, recommender_agent, msg):
253+ LOG.warn("Error while accessing the recommender service: %s"
254+ % msg)
255+ self.emit("recommender-agent-error", msg)
256+
257 class CategoriesParser(object):
258 """
259 Parser that is able to read the categories from a menu file
260@@ -328,6 +401,7 @@
261 #print cat_unalloc.name, cat_unalloc.query
262 return
263
264+
265 # static category mapping for the tiles
266
267 category_cat = {
268
269=== modified file 'softwarecenter/db/enquire.py'
270--- softwarecenter/db/enquire.py 2012-01-06 16:39:13 +0000
271+++ softwarecenter/db/enquire.py 2012-02-02 05:45:26 +0000
272@@ -28,7 +28,6 @@
273 XapianValues,
274 NonAppVisibility,
275 DEFAULT_SEARCH_LIMIT)
276-from softwarecenter.backend.reviews import get_review_loader
277 from softwarecenter.db.database import (
278 SearchQuery, LocaleSorter, TopRatedSorter)
279 from softwarecenter.distro import get_distro
280@@ -186,6 +185,7 @@
281 else:
282 LOG.warning("no catelogedtime in axi")
283 elif self.sortmode == SortMethods.BY_TOP_RATED:
284+ from softwarecenter.backend.reviews import get_review_loader
285 review_loader = get_review_loader(self.cache, self.db)
286 sorter = TopRatedSorter(self.db, review_loader)
287 enquire.set_sort_by_key(sorter, reverse=True)
288@@ -339,3 +339,5 @@
289 """ get the xapian.Document objects of the current matches """
290 xdb = self.db.xapiandb
291 return [xdb.get_document(m.docid) for m in self._matches]
292+
293+
294
295=== modified file 'softwarecenter/enums.py'
296--- softwarecenter/enums.py 2012-01-20 10:24:51 +0000
297+++ softwarecenter/enums.py 2012-02-02 05:45:26 +0000
298@@ -35,7 +35,8 @@
299 BUY_SOMETHING_HOST_ANONYMOUS = os.environ.get("SOFTWARE_CENTER_AGENT_HOST") or os.environ.get("SOFTWARE_CENTER_BUY_HOST") or "http://software-center.ubuntu.com"
300
301 # recommender
302-RECOMMENDER_HOST = os.environ.get("SOFTWARE_CENTER_RECOMMENDER_HOST") or "https://recommender.software-center.ubuntu.com"
303+RECOMMENDER_HOST = os.environ.get("SOFTWARE_CENTER_RECOMMENDER_HOST") or "https://recommender.ubuntu.com"
304+#RECOMMENDER_HOST = os.environ.get("SOFTWARE_CENTER_RECOMMENDER_HOST") or "https://rec.staging.ubuntu.com"
305
306 # for the sso login
307 UBUNTU_SSO_SERVICE = os.environ.get(
308
309=== modified file 'softwarecenter/testutils.py'
310--- softwarecenter/testutils.py 2012-01-27 09:05:40 +0000
311+++ softwarecenter/testutils.py 2012-02-02 05:45:26 +0000
312@@ -190,3 +190,42 @@
313 u'state': u'Complete',
314 }
315 return subscription_dict
316+
317+def make_recommender_agent_recommend_top_dict():
318+ # best to have a list of likely not-installed items
319+ app_dict = {
320+ u'recommendations': [
321+ {
322+ u'package_name': u'clementine'
323+ },
324+ {
325+ u'package_name': u'hedgewars'
326+ },
327+ {
328+ u'package_name': u'gelemental'
329+ },
330+ {
331+ u'package_name': u'nexuiz'
332+ },
333+ {
334+ u'package_name': u'fgo'
335+ },
336+ {
337+ u'package_name': u'musique'
338+ },
339+ {
340+ u'package_name': u'pybik'
341+ },
342+ {
343+ u'package_name': u'radiotray'
344+ },
345+ {
346+ u'package_name': u'cherrytree'
347+ },
348+ {
349+ u'package_name': u'phlipple'
350+ }
351+ ]
352+}
353+ return app_dict
354+
355
356=== modified file 'softwarecenter/ui/gtk3/views/catview_gtk.py'
357--- softwarecenter/ui/gtk3/views/catview_gtk.py 2012-01-04 10:24:54 +0000
358+++ softwarecenter/ui/gtk3/views/catview_gtk.py 2012-02-02 05:45:26 +0000
359@@ -47,6 +47,7 @@
360 from softwarecenter.db.enquire import AppEnquire
361 from softwarecenter.db.categories import (Category,
362 CategoriesParser,
363+ RecommendedForYouCategory,
364 get_category_by_name,
365 categories_sorted_by_name)
366 from softwarecenter.db.utils import get_query_for_pkgnames
367@@ -108,6 +109,8 @@
368 self.cache = cache
369 self.db = db
370 self.icons = icons
371+ self.properties_helper = AppPropertiesHelper(
372+ self.db, self.cache, self.icons)
373 self.section = None
374
375 Viewport.__init__(self)
376@@ -152,9 +155,9 @@
377 amount = number of tiles to add from start of doc range'''
378 amount = min(len(docs), amount)
379 for doc in docs[0:amount]:
380- tile = FeaturedTile(self.helper, doc)
381+ tile = FeaturedTile(self.properties_helper, doc)
382 tile.connect('clicked', self.on_app_clicked,
383- self.helper.get_application(doc))
384+ self.properties_helper.get_application(doc))
385 flowgrid.add_child(tile)
386 return
387
388@@ -248,10 +251,9 @@
389 self.right_column = Gtk.Box.new(Gtk.Orientation.VERTICAL, self.SPACING)
390 self.top_hbox.pack_start(self.right_column, True, True, 0)
391
392- self._append_new()
393- #~ self._append_recommendations()
394+ self._append_whats_new()
395 self._append_top_rated()
396-
397+ self._append_recommended_for_you()
398 self._append_appcount()
399
400 #self._append_video_clips()
401@@ -363,129 +365,127 @@
402 cat_vbox.pack_start(label, False, False, 0)
403 return
404
405- def _get_toprated_category_content(self):
406- toprated_cat = get_category_by_name(self.categories,
407- u"Top Rated") # unstranslated name
408- if toprated_cat is None:
409- LOG.warn("No 'toprated' category found!!")
410- return None, []
411-
412- enq = AppEnquire(self.cache, self.db)
413- app_filter = AppFilter(self.db, self.cache)
414- enq.set_query(toprated_cat.query,
415- limit=TOP_RATED_CAROUSEL_LIMIT,
416- sortmode=toprated_cat.sortmode,
417- filter=app_filter,
418- nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE,
419- nonblocking_load=False)
420-
421- if not hasattr(self, "helper"):
422- self.helper = AppPropertiesHelper(self.db,
423- self.cache,
424- self.icons)
425-
426- return toprated_cat, enq.get_documents()
427-
428- def _update_toprated_content(self):
429+ # FIXME: _update_{top_rated,whats_new,recommended_for_you}_conent()
430+ # duplicates a lot of code
431+ def _update_top_rated_content(self):
432 # remove any existing children from the grid widget
433- self.toprated.remove_all()
434- # get toprated category and docs
435- toprated_cat, docs = self._get_toprated_category_content()
436- # display docs
437- self._add_tiles_to_flowgrid(docs, self.toprated,
438- TOP_RATED_CAROUSEL_LIMIT)
439- self.toprated.show_all()
440- return toprated_cat
441+ self.top_rated.remove_all()
442+ # get top_rated category and docs
443+ top_rated_cat = get_category_by_name(
444+ self.categories, u"Top Rated") # untranslated name
445+ if top_rated_cat:
446+ docs = top_rated_cat.get_documents(self.db)
447+ self._add_tiles_to_flowgrid(docs, self.top_rated,
448+ TOP_RATED_CAROUSEL_LIMIT)
449+ self.top_rated.show_all()
450+ return top_rated_cat
451
452 def _append_top_rated(self):
453- self.toprated = FlowableGrid()
454- #~ self.featured.row_spacing = StockEms.SMALL
455- frame = FramedHeaderBox()
456- frame.set_header_label(_("Top Rated"))
457- frame.add(self.toprated)
458- self.toprated_frame = frame
459- self.right_column.pack_start(frame, True, True, 0)
460-
461- toprated_cat = self._update_toprated_content()
462- # only display the 'More' LinkButton if we have toprated content
463- if toprated_cat is not None:
464- frame.header_implements_more_button()
465- frame.more.connect('clicked', self.on_category_clicked, toprated_cat)
466+ self.top_rated = FlowableGrid()
467+ #~ self.top_rated.row_spacing = StockEms.SMALL
468+ self.top_rated_frame = FramedHeaderBox()
469+ self.top_rated_frame.set_header_label(_("Top Rated"))
470+ self.top_rated_frame.add(self.top_rated)
471+ self.right_column.pack_start(self.top_rated_frame, True, True, 0)
472+ top_rated_cat = self._update_top_rated_content()
473+ # only display the 'More' LinkButton if we have top_rated content
474+ if top_rated_cat is not None:
475+ self.top_rated_frame.header_implements_more_button()
476+ self.top_rated_frame.more.connect('clicked',
477+ self.on_category_clicked, top_rated_cat)
478 return
479
480- def _get_new_category_content(self):
481- whatsnew_cat = get_category_by_name(self.categories,
482- u"What\u2019s New") # unstranslated name
483- if whatsnew_cat is None:
484- LOG.warn("No 'new' category found!!")
485- return None, []
486-
487- enq = AppEnquire(self.cache, self.db)
488- app_filter = AppFilter(self.db, self.cache)
489- app_filter.set_available_only(True)
490- app_filter.set_not_installed_only(True)
491- enq.set_query(whatsnew_cat.query,
492- limit=8,
493- filter=app_filter,
494- sortmode=SortMethods.BY_CATALOGED_TIME,
495- nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE,
496- nonblocking_load=False)
497-
498- if not hasattr(self, "helper"):
499- self.helper = AppPropertiesHelper(self.db,
500- self.cache,
501- self.icons)
502-
503- return whatsnew_cat, enq.get_documents()
504-
505- def _update_new_content(self):
506+ def _update_whats_new_content(self):
507 # remove any existing children from the grid widget
508- self.featured.remove_all()
509- # get toprated category and docs
510- whatsnew_cat, docs = self._get_new_category_content()
511- # display docs
512- self._add_tiles_to_flowgrid(docs, self.featured, 8)
513- self.featured.show_all()
514- return whatsnew_cat
515-
516- def _append_new(self):
517- self.featured = FlowableGrid()
518- frame = FramedHeaderBox()
519- frame.set_header_label(_(u"What\u2019s New"))
520- frame.add(self.featured)
521- self.new_frame = frame
522-
523- whatsnew_cat = self._update_new_content()
524- if whatsnew_cat is not None:
525+ self.whats_new.remove_all()
526+ # get top_rated category and docs
527+ whats_new_cat = get_category_by_name(
528+ self.categories, u"What\u2019s New") # untranslated name
529+ if whats_new_cat:
530+ docs = whats_new_cat.get_documents(self.db)
531+ self._add_tiles_to_flowgrid(docs, self.whats_new, 8)
532+ self.whats_new.show_all()
533+ return whats_new_cat
534+
535+ def _append_whats_new(self):
536+ self.whats_new = FlowableGrid()
537+ self.whats_new_frame = FramedHeaderBox()
538+ self.whats_new_frame.set_header_label(_(u"What\u2019s New"))
539+ self.whats_new_frame.add(self.whats_new)
540+
541+ whats_new_cat = self._update_whats_new_content()
542+ if whats_new_cat is not None:
543 # only add to the visible right_frame if we actually have it
544- self.right_column.pack_start(frame, True, True, 0)
545- frame.header_implements_more_button()
546- frame.more.connect('clicked', self.on_category_clicked, whatsnew_cat)
547- return
548-
549- #~ def _append_recommendations(self):
550- #~ featured_cat = get_category_by_name(self.categories,
551- #~ u"Featured") # unstranslated name
552-#~
553- #~ enq = AppEnquire(self.cache, self.db)
554- #~ app_filter = AppFilter(self.db, self.cache)
555- #~ enq.set_query(featured_cat.query,
556- #~ limit=12,
557- #~ filter=app_filter,
558- #~ nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE,
559- #~ nonblocking_load=False)
560-#~
561- #~ self.featured = FlowableGrid()
562- #~ frame = FramedHeaderBox(Gtk.Orientation.VERTICAL)
563- #~ frame.add(self.featured)
564- #~ frame.set_header_label(_("Recommended For You"))
565- #~ frame.header_implements_more_button()
566- #~ self.right_column.pack_start(frame, True, True, 0)
567-#~
568- #~ self.helper = AppPropertiesHelper(self.db, self.cache, self.icons)
569- #~ docs = enq.get_documents()
570- #~ self._add_tiles_to_flowgrid(docs, self.featured, 12)
571- #~ return
572+ self.right_column.pack_start(self.whats_new_frame, True, True, 0)
573+ self.whats_new_frame.header_implements_more_button()
574+ self.whats_new_frame.more.connect(
575+ 'clicked', self.on_category_clicked, whats_new_cat)
576+ return
577+
578+ def _on_recommended_for_you_refresh(self, cat):
579+ docs = cat.get_documents(self.db)
580+ # display the recommendedations
581+ if len(docs) > 0:
582+ self._add_tiles_to_flowgrid(docs, self.recommended_for_you, 8)
583+ self.recommended_for_you.show_all()
584+ self.recommended_for_you_frame.hide_spinner()
585+ self.recommended_for_you_frame.more.connect(
586+ 'clicked',
587+ self.on_category_clicked,
588+ cat)
589+ else:
590+ # TODO: this test for zero docs is temporary and will not be
591+ # needed once the recommendation agent is up and running
592+ self._hide_recommended_for_you()
593+ return
594+
595+ def _on_recommender_agent_error(self, agent, msg):
596+ LOG.warn("Error while accessing the recommender agent: %s"
597+ % msg)
598+ # TODO: temporary, instead we will display cached recommendations here
599+ self._hide_recommended_for_you()
600+
601+ def _update_recommended_for_you_content(self):
602+ # remove any existing children from the grid widget
603+ self.recommended_for_you.remove_all()
604+ self.recommended_for_you_frame.show_spinner()
605+ # get the recommendations from the recommender agent
606+ self.recommended_for_you_cat = RecommendedForYouCategory()
607+ self.recommended_for_you_cat.connect(
608+ 'needs-refresh',
609+ self._on_recommended_for_you_refresh)
610+ self.recommended_for_you_cat.connect('recommender-agent-error',
611+ self._on_recommender_agent_error)
612+
613+ def _append_recommended_for_you(self):
614+ # TODO: This space will initially contain an opt-in screen, and this
615+ # will update to the tile view of recommended apps when ready
616+ # see https://wiki.ubuntu.com/SoftwareCenter#Home_screen
617+ self.bottom_hbox = Gtk.HBox(spacing=StockEms.SMALL)
618+ bottom_hbox_alignment = Gtk.Alignment()
619+ bottom_hbox_alignment.set_padding(0, 0, StockEms.MEDIUM-2, StockEms.MEDIUM-2)
620+ bottom_hbox_alignment.add(self.bottom_hbox)
621+ self.vbox.pack_start(bottom_hbox_alignment, False, False, 0)
622+
623+ # TODO: During development, place the "Recommended for You" panel
624+ # at the bottom, but swap this with the Top Rated panel once
625+ # the recommended for you pieces are done and deployed
626+ # see https://wiki.ubuntu.com/SoftwareCenter#Home_screen
627+ self.recommended_for_you = FlowableGrid()
628+ self.recommended_for_you_frame = FramedHeaderBox()
629+ self.recommended_for_you_frame.set_header_label(
630+ _(u"Recommended for You"))
631+ self.recommended_for_you_frame.add(self.recommended_for_you)
632+ self.recommended_for_you_frame.header_implements_more_button()
633+ self.bottom_hbox.pack_start(self.recommended_for_you_frame,
634+ True, True, 0)
635+
636+ # get the recommendations from the recommender agent
637+ self._update_recommended_for_you_content()
638+
639+ def _hide_recommended_for_you(self):
640+ # and hide the pane
641+ self.recommended_for_you_frame.hide()
642
643 def _update_appcount(self):
644 enq = AppEnquire(self.cache, self.db)
645@@ -523,8 +523,9 @@
646 return
647 self._supported_only = supported_only
648
649- self._update_toprated_content()
650- self._update_new_content()
651+ self._update_top_rated_content()
652+ self._update_whats_new_content()
653+ self._update_recommended_for_you_content()
654 self._update_appcount()
655 return
656
657@@ -547,14 +548,14 @@
658 # data
659 self.root_category = root_category
660 self.enquire = AppEnquire(self.cache, self.db)
661- self.helper = AppPropertiesHelper(self.db,
662- self.cache,
663- self.icons)
664+ self.properties_helper = AppPropertiesHelper(
665+ self.db, self.cache, self.icons)
666
667 # sections
668 self.current_category = None
669 self.departments = None
670- self.toprated = None
671+ self.top_rated = None
672+ self.recommended_for_you = None
673 self.appcount = None
674
675 # widgetry
676@@ -563,7 +564,7 @@
677 self.vbox.set_margin_top(StockEms.MEDIUM)
678 return
679
680- def _get_sub_toprated_content(self, category):
681+ def _get_sub_top_rated_content(self, category):
682 app_filter = AppFilter(self.db, self.cache)
683 self.enquire.set_query(category.query,
684 limit=TOP_RATED_CAROUSEL_LIMIT,
685@@ -574,23 +575,24 @@
686 return self.enquire.get_documents()
687
688 @wait_for_apt_cache_ready # be consistent with new apps
689- def _update_sub_toprated_content(self, category):
690- self.toprated.remove_all()
691+ def _update_sub_top_rated_content(self, category):
692+ self.top_rated.remove_all()
693 # FIXME: should this be m = "%s %s" % (_(gettext text), header text) ??
694 # TRANSLATORS: %s is a category name, like Internet or Development Tools
695 m = _('Top Rated %(category)s') % { 'category' : GObject.markup_escape_text(self.header)}
696- self.toprated_frame.set_header_label(m)
697- docs = self._get_sub_toprated_content(category)
698- self._add_tiles_to_flowgrid(docs, self.toprated, TOP_RATED_CAROUSEL_LIMIT)
699+ self.top_rated_frame.set_header_label(m)
700+ docs = self._get_sub_top_rated_content(category)
701+ self._add_tiles_to_flowgrid(docs, self.top_rated,
702+ TOP_RATED_CAROUSEL_LIMIT)
703 return
704
705- def _append_sub_toprated(self):
706- self.toprated = FlowableGrid()
707- self.toprated.set_row_spacing(6)
708- self.toprated.set_column_spacing(6)
709- self.toprated_frame = FramedHeaderBox()
710- self.toprated_frame.pack_start(self.toprated, True, True, 0)
711- self.vbox.pack_start(self.toprated_frame, False, True, 0)
712+ def _append_sub_top_rated(self):
713+ self.top_rated = FlowableGrid()
714+ self.top_rated.set_row_spacing(6)
715+ self.top_rated.set_column_spacing(6)
716+ self.top_rated_frame = FramedHeaderBox()
717+ self.top_rated_frame.pack_start(self.top_rated, True, True, 0)
718+ self.vbox.pack_start(self.top_rated_frame, False, True, 0)
719 return
720
721 def _update_subcat_departments(self, category, num_items):
722@@ -637,14 +639,14 @@
723 self.departments = FlowableGrid(paint_grid_pattern=False)
724 self.departments.set_row_spacing(StockEms.SMALL)
725 self.departments.set_column_spacing(StockEms.SMALL)
726- frame = FramedBox(spacing=StockEms.MEDIUM,
727- padding=StockEms.MEDIUM)
728+ self.departments_frame = FramedBox(spacing=StockEms.MEDIUM,
729+ padding=StockEms.MEDIUM)
730 # set x/y-alignment and x/y-expand
731- frame.set(0.5, 0.0, 1.0, 1.0)
732- frame.pack_start(self.subcat_label, False, False, 0)
733- frame.pack_start(self.departments, True, True, 0)
734+ self.departments_frame.set(0.5, 0.0, 1.0, 1.0)
735+ self.departments_frame.pack_start(self.subcat_label, False, False, 0)
736+ self.departments_frame.pack_start(self.departments, True, True, 0)
737 # append the departments section to the page
738- self.vbox.pack_start(frame, False, True, 0)
739+ self.vbox.pack_start(self.departments_frame, False, True, 0)
740 return
741
742 def _update_appcount(self, appcount):
743@@ -666,14 +668,14 @@
744 # these methods add sections to the page
745 # changing order of methods changes order that they appear in the page
746 self._append_subcat_departments()
747- self._append_sub_toprated()
748+ self._append_sub_top_rated()
749 self._append_appcount()
750 self._built = True
751 return
752
753 def _update_subcat_view(self, category, num_items=0):
754 num_items = self._update_subcat_departments(category, num_items)
755- self._update_sub_toprated_content(category)
756+ self._update_sub_top_rated_content(category)
757 self._update_appcount(num_items)
758 self.show_all()
759 return
760@@ -768,7 +770,7 @@
761 n.append_page(scroll, Gtk.Label(label="Subcats"))
762
763 win.add(n)
764- win.set_size_request(800,600)
765+ win.set_size_request(800,800)
766 win.show_all()
767 win.connect('destroy', Gtk.main_quit)
768 return win
769
770=== modified file 'softwarecenter/ui/gtk3/widgets/containers.py'
771--- softwarecenter/ui/gtk3/widgets/containers.py 2011-10-27 20:48:15 +0000
772+++ softwarecenter/ui/gtk3/widgets/containers.py 2012-02-02 05:45:26 +0000
773@@ -495,15 +495,37 @@
774 class FramedHeaderBox(FramedBox):
775
776 MARKUP = '<b>%s</b>'
777+
778+ # pages for the spinner notebook
779+ (CONTENT,
780+ SPINNER) = range(2)
781
782- def __init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=0, padding=0):
783+ def __init__(self, orientation=Gtk.Orientation.VERTICAL,
784+ spacing=0,
785+ padding=0,
786+ show_spinner=False):
787 FramedBox.__init__(self, Gtk.Orientation.VERTICAL, spacing, padding)
788+ # make the header
789 self.header = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing)
790 self.header_alignment = Gtk.Alignment()
791 self.header_alignment.add(self.header)
792 self.box.pack_start(self.header_alignment, False, False, 0)
793+ # make the content box
794 self.content_box = Gtk.Box.new(orientation, spacing)
795- self.box.add(self.content_box)
796+ # and a spinner (to be used only if needed)
797+ # TODO: cosmetic tweak needed - draw a line at the top of the spinner
798+ # area so that the header has a proper border
799+ self.spinner = Gtk.Spinner()
800+ self.spinner.set_size_request(32, 32)
801+ self.spinner.set_valign(Gtk.Align.CENTER)
802+ self.spinner.set_halign(Gtk.Align.CENTER)
803+ # finally, a notebook for the spinner and the content box to share
804+ self.spinner_notebook = Gtk.Notebook()
805+ self.spinner_notebook.set_show_tabs(False)
806+ self.spinner_notebook.set_show_border(False)
807+ self.spinner_notebook.append_page(self.content_box, None)
808+ self.spinner_notebook.append_page(self.spinner, None)
809+ self.box.add(self.spinner_notebook)
810 return
811
812 def on_draw(self, cr):
813@@ -523,6 +545,16 @@
814
815 def pack_end(self, *args, **kwargs):
816 return self.content_box.pack_end(*args, **kwargs)
817+
818+ def show_spinner(self):
819+ self.spinner.start()
820+ self.spinner.show()
821+ self.spinner_notebook.set_current_page(self.SPINNER)
822+
823+ def hide_spinner(self):
824+ self.spinner.stop()
825+ self.spinner.hide()
826+ self.spinner_notebook.set_current_page(self.CONTENT)
827
828 # XXX: non-functional with current code...
829 #~ def set_header_expand(self, expand):
830@@ -553,7 +585,7 @@
831 self.title.set_markup(self.MARKUP % label)
832 return
833
834- def header_implements_more_button(self, callback=None):
835+ def header_implements_more_button(self):
836 if not hasattr(self, "more"):
837 self.more = MoreLink()
838 self.header.pack_end(self.more, False, False, 0)
839
840=== modified file 'test/gtk3/test_catview.py'
841--- test/gtk3/test_catview.py 2012-01-16 14:42:49 +0000
842+++ test/gtk3/test_catview.py 2012-02-02 05:45:26 +0000
843@@ -1,46 +1,61 @@
844 from gi.repository import Gtk
845 import time
846 import unittest
847+from mock import patch
848
849 from testutils import setup_test_env
850 setup_test_env()
851
852 from softwarecenter.enums import SortMethods
853+from softwarecenter.testutils import (get_test_db,
854+ make_recommender_agent_recommend_top_dict)
855
856 class TestCatView(unittest.TestCase):
857
858+ def setUp(self):
859+ self.db = get_test_db()
860+
861 def _on_category_selected(self, subcatview, category):
862 #print "**************", subcatview, category
863 self._cat = category
864
865- def test_subcatview_toprated(self):
866+ # patch out the agent query method to avoid making the actual server call
867+ @patch('softwarecenter.backend.recommends.RecommenderAgent'
868+ '.query_recommend_top')
869+ def test_subcatview_top_rated(self, mock_query_recommend_top):
870 from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview
871 # get the widgets we need
872 win = get_test_window_catview()
873 lobby = win.get_data("lobby")
874- # test clicking toprated
875+ # test clicking top_rated
876 lobby.connect("category-selected", self._on_category_selected)
877- lobby.toprated_frame.more.clicked()
878+ lobby.top_rated_frame.more.clicked()
879 self._p()
880 self.assertNotEqual(self._cat, None)
881 self.assertEqual(self._cat.name, "Top Rated")
882 self.assertEqual(self._cat.sortmode, SortMethods.BY_TOP_RATED)
883
884- def test_subcatview_new(self):
885+ # patch out the agent query method to avoid making the actual server call
886+ @patch('softwarecenter.backend.recommends.RecommenderAgent'
887+ '.query_recommend_top')
888+ def test_subcatview_new(self, mock_query_recommend_top):
889 from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview
890 # get the widgets we need
891 win = get_test_window_catview()
892 lobby = win.get_data("lobby")
893 # test clicking new
894 lobby.connect("category-selected", self._on_category_selected)
895- lobby.new_frame.more.clicked()
896+ lobby.whats_new_frame.more.clicked()
897 self._p()
898 self.assertNotEqual(self._cat, None)
899 # encoding is utf-8 (since r2218, see category.py)
900 self.assertEqual(self._cat.name, 'What\xe2\x80\x99s New')
901 self.assertEqual(self._cat.sortmode, SortMethods.BY_CATALOGED_TIME)
902
903- def test_subcatview_new_no_sort_info_yet(self):
904+ # patch out the agent query method to avoid making the actual server call
905+ @patch('softwarecenter.backend.recommends.RecommenderAgent'
906+ '.query_recommend_top')
907+ def test_subcatview_new_no_sort_info_yet(self, mock_query_recommend_top):
908 # ensure that we don't show a empty "whats new" category
909 # see LP: #865985
910 from softwarecenter.testutils import get_test_db
911@@ -74,8 +89,30 @@
912 win.show()
913 # test visibility
914 self._p()
915- self.assertFalse(view.new_frame.get_property("visible"))
916- self._p()
917+ self.assertFalse(view.whats_new_frame.get_property("visible"))
918+ self._p()
919+
920+ # patch out the agent query method to avoid making the actual server call
921+ @patch('softwarecenter.backend.recommends.RecommenderAgent'
922+ '.query_recommend_top')
923+ def test_subcatview_recommended_for_me(self, mock_query_recommend_top):
924+ from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview
925+ # get the widgets we need
926+ win = get_test_window_catview()
927+ lobby = win.get_data("lobby")
928+ # we fake the callback from the agent here
929+ lobby.recommended_for_you_cat._recommend_top_result(
930+ None,
931+ make_recommender_agent_recommend_top_dict())
932+ self.assertNotEqual(
933+ lobby.recommended_for_you_cat.get_documents(self.db), [])
934+ self._p()
935+ # test clicking recommended_for_you More button
936+ lobby.connect("category-selected", self._on_category_selected)
937+ lobby.recommended_for_you_frame.more.clicked()
938+ self._p()
939+ self.assertNotEqual(self._cat, None)
940+ self.assertEqual(self._cat.name, "Recommended for You")
941
942 def _p(self):
943 for i in range(5):
944
945=== renamed file 'test/test_cat_parsing.py' => 'test/test_categories.py'
946--- test/test_cat_parsing.py 2012-01-16 14:42:49 +0000
947+++ test/test_categories.py 2012-02-02 05:45:26 +0000
948@@ -2,6 +2,8 @@
949
950 import os
951 import unittest
952+import xapian
953+from mock import patch
954
955 from testutils import setup_test_env
956 setup_test_env()
957@@ -10,8 +12,34 @@
958
959 from softwarecenter.db.database import StoreDatabase
960 from softwarecenter.db.pkginfo import get_pkg_info
961-from softwarecenter.db.categories import CategoriesParser, get_category_by_name
962-
963+from softwarecenter.db.categories import (
964+ CategoriesParser, RecommendedForYouCategory,
965+ get_category_by_name, get_query_for_category)
966+from softwarecenter.testutils import (get_test_db,
967+ make_recommender_agent_recommend_top_dict)
968+
969+class TestCategories(unittest.TestCase):
970+
971+ def setUp(self):
972+ self.db = get_test_db()
973+
974+ @patch('softwarecenter.db.categories.RecommenderAgent')
975+ def test_recommends_category(self, AgentMockCls):
976+ # ensure we use the same instance in test and code
977+ agent_mock_instance = AgentMockCls.return_value
978+ recommends_cat = RecommendedForYouCategory()
979+ docids = recommends_cat.get_documents(self.db)
980+ self.assertEqual(docids, [])
981+ self.assertTrue(agent_mock_instance.query_recommend_top.called)
982+ # ensure we get a query when the callback is called
983+ recommends_cat._recommend_top_result(
984+ None,
985+ make_recommender_agent_recommend_top_dict())
986+ self.assertNotEqual(recommends_cat.get_documents(self.db), [])
987+
988+ def test_get_query(self):
989+ query = get_query_for_category(self.db, "Education")
990+ self.assertNotEqual(query, None)
991
992 class TestCatParsing(unittest.TestCase):
993 """ tests the "where is it in the menu" code """
994@@ -25,7 +53,8 @@
995 self.db.open()
996 self.catview = CategoriesParser(self.db)
997 self.catview.db = self.db
998- self.cats = self.catview.parse_applications_menu('/usr/share/app-install')
999+ self.cats = self.catview.parse_applications_menu(
1000+ '/usr/share/app-install')
1001
1002 def test_get_cat_by_name(self):
1003 cat = get_category_by_name(self.cats, 'Games')
1004@@ -37,6 +66,13 @@
1005 cat = get_category_by_name(self.cats, 'Featured')
1006 self.assertEqual(cat.flags[0], 'carousel-only')
1007
1008+ def test_get_documents(self):
1009+ cat = get_category_by_name(self.cats, 'Featured')
1010+ docs = cat.get_documents(self.db)
1011+ self.assertNotEqual(docs, [])
1012+ for doc in docs:
1013+ self.assertEqual(type(doc), xapian.Document)
1014+
1015
1016 if __name__ == "__main__":
1017 import logging
1018
1019=== added file 'test/test_recagent.py'
1020--- test/test_recagent.py 1970-01-01 00:00:00 +0000
1021+++ test/test_recagent.py 2012-02-02 05:45:26 +0000
1022@@ -0,0 +1,64 @@
1023+#!/usr/bin/python
1024+
1025+from gi.repository import GObject
1026+import unittest
1027+import os
1028+
1029+from testutils import setup_test_env
1030+setup_test_env()
1031+
1032+from softwarecenter.backend.recommends import RecommenderAgent
1033+
1034+class TestRecommenderAgent(unittest.TestCase):
1035+ """ tests the recommender agent """
1036+
1037+ def setUp(self):
1038+ self.loop = GObject.MainLoop(GObject.main_context_default())
1039+ self.error = False
1040+
1041+ def on_query_done(self, recagent, data):
1042+ print "query done, data: '%s'" % data
1043+ self.loop.quit()
1044+
1045+ def on_query_error(self, recagent, error):
1046+ self.loop.quit()
1047+ self.error = True
1048+
1049+ def test_recagent_query_recommend_top(self):
1050+ # NOTE: This requires a working recommender host that is reachable
1051+ os.environ["SOFTWARE_CENTER_RECOMMENDER_HOST"] = "https://rec.staging.ubuntu.com/"
1052+ recommender_agent = RecommenderAgent()
1053+ recommender_agent.connect("recommend-top", self.on_query_done)
1054+ recommender_agent.connect("error", self.on_query_error)
1055+ recommender_agent.query_recommend_top()
1056+ self.loop.run()
1057+ self.assertFalse(self.error)
1058+ del os.environ["SOFTWARE_CENTER_RECOMMENDER_HOST"]
1059+
1060+# def test_recagent_query_recommend_me(self):
1061+# os.environ["SOFTWARE_CENTER_RECOMMENDER_HOST"] = "https://rec.staging.ubuntu.com/"
1062+# recommender_agent = RecommenderAgent()
1063+# recommender_agent.connect("recommend-me", self.on_query_done)
1064+# recommender_agent.connect("error", self.on_query_error)
1065+# recommender_agent.query_recommend_me()
1066+# self.loop.run()
1067+# self.assertFalse(self.error)
1068+# del os.environ["SOFTWARE_CENTER_RECOMMENDER_HOST"]
1069+
1070+ def test_recagent_query_error(self):
1071+ # there definitely ain't no server here
1072+ os.environ["SOFTWARE_CENTER_RECOMMENDER_HOST"] = "https://orange.staging.ubuntu.com/"
1073+ recommender_agent = RecommenderAgent()
1074+ recommender_agent.connect("recommend-top", self.on_query_done)
1075+ recommender_agent.connect("error", self.on_query_error)
1076+ recommender_agent.query_recommend_top()
1077+ self.loop.run()
1078+ self.assertTrue(self.error)
1079+
1080+ del os.environ["SOFTWARE_CENTER_RECOMMENDER_HOST"]
1081+
1082+
1083+if __name__ == "__main__":
1084+ import logging
1085+ logging.basicConfig(level=logging.DEBUG)
1086+ unittest.main()
1087
1088=== modified file 'utils/piston-helpers/piston_generic_helper.py'
1089--- utils/piston-helpers/piston_generic_helper.py 2012-01-31 08:34:58 +0000
1090+++ utils/piston-helpers/piston_generic_helper.py 2012-02-02 05:45:26 +0000
1091@@ -17,6 +17,7 @@
1092 # this program; if not, write to the Free Software Foundation, Inc.,
1093 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1094
1095+import httplib2
1096 import argparse
1097 import logging
1098 import os
1099@@ -28,10 +29,10 @@
1100
1101 # useful for debugging
1102 if "SOFTWARE_CENTER_DEBUG_HTTP" in os.environ:
1103- import httplib2
1104 httplib2.debuglevel = 1
1105
1106 import piston_mini_client.auth
1107+from piston_mini_client.failhandlers import APIError
1108
1109 try:
1110 import softwarecenter
1111@@ -191,6 +192,12 @@
1112 f = getattr(api, func)
1113 try:
1114 piston_reply = f(**kwargs)
1115+ except httplib2.ServerNotFoundError as e:
1116+ LOG.warn(e)
1117+ sys.exit(1)
1118+ except APIError as e:
1119+ LOG.warn(e)
1120+ sys.exit(1)
1121 except:
1122 LOG.exception("urclient_apps")
1123 sys.exit(1)

Subscribers

People subscribed via source and target branches