Merge lp:~glatzor/software-center/portability into lp:software-center

Proposed by Sebastian Heinlein
Status: Merged
Merged at revision: 2540
Proposed branch: lp:~glatzor/software-center/portability
Merge into: lp:software-center
Diff against target: 1637 lines (+778/-563)
16 files modified
data/ui/gtk3/SoftwareCenter.ui (+1/-1)
softwarecenter/backend/channel_impl/aptchannels.py (+3/-2)
softwarecenter/backend/oneconfhandler.py (+0/-206)
softwarecenter/backend/oneconfhandler/__init__.py (+32/-0)
softwarecenter/backend/oneconfhandler/core.py (+196/-0)
softwarecenter/backend/piston/rnrclient.py (+1/-1)
softwarecenter/backend/reviews/__init__.py (+41/-325)
softwarecenter/backend/reviews/rnr.py (+376/-0)
softwarecenter/db/application.py (+2/-1)
softwarecenter/db/update.py (+1/-1)
softwarecenter/distro/Debian.py (+69/-7)
softwarecenter/distro/Ubuntu.py (+6/-0)
softwarecenter/distro/__init__.py (+23/-9)
softwarecenter/ui/gtk3/app.py (+22/-7)
softwarecenter/ui/gtk3/panes/installedpane.py (+3/-2)
softwarecenter/ui/gtk3/views/appdetailsview_gtk.py (+2/-1)
To merge this branch: bzr merge lp:~glatzor/software-center/portability
Reviewer Review Type Date Requested Status
Michael Vogt Pending
Review via email: mp+80448@code.launchpad.net

Description of the change

This branch allows to run software-center on non-Ubuntu systems, e.g. Debian! It makes some components optionally (oneconf, ratings, reviews, purchases ...)

To post a comment you must log in.
2553. By Sebastian Heinlein

Fix a copy and paste error in the Debian supported query

2554. By Sebastian Heinlein

Store the lsb_info in the Distro class

2555. By Sebastian Heinlein

Add maintenance status information for Debian packages

It is based on the suite/archive and comp of the Release file

2556. By Sebastian Heinlein

Remove obsolete get_maintenace_status method

2557. By Sebastian Heinlein

Mention the DFSG in the license information

2558. By Sebastian Heinlein

Don't hardcode Ubuntu for the component detection

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/ui/gtk3/SoftwareCenter.ui'
2--- data/ui/gtk3/SoftwareCenter.ui 2011-10-08 17:15:20 +0000
3+++ data/ui/gtk3/SoftwareCenter.ui 2011-10-27 07:59:24 +0000
4@@ -470,7 +470,7 @@
5 </object>
6 </child>
7 <child>
8- <object class="GtkSeparatorMenuItem" id="menuitem3">
9+ <object class="GtkSeparatorMenuItem" id="separator_developer">
10 <property name="visible">True</property>
11 <property name="can_focus">False</property>
12 </object>
13
14=== modified file 'softwarecenter/backend/channel_impl/aptchannels.py'
15--- softwarecenter/backend/channel_impl/aptchannels.py 2011-10-11 18:44:41 +0000
16+++ softwarecenter/backend/channel_impl/aptchannels.py 2011-10-27 07:59:24 +0000
17@@ -266,7 +266,7 @@
18 installed_only=installed_only))
19
20 # always display the partner channel, even if its source is not enabled
21- if not partner_channel:
22+ if not partner_channel and distro_channel_name == "Ubuntu":
23 partner_channel = SoftwareChannel("Partner archive",
24 "Canonical",
25 "partner",
26@@ -285,7 +285,8 @@
27 channels.append(dist_channel)
28 if partner_channel is not None:
29 channels.append(partner_channel)
30- channels.append(for_purchase_channel)
31+ if get_distro().PURCHASE_APP_URL:
32+ channels.append(for_purchase_channel)
33 if new_apps_channel is not None:
34 channels.append(new_apps_channel)
35 channels.extend(ppa_channels)
36
37=== added directory 'softwarecenter/backend/oneconfhandler'
38=== removed file 'softwarecenter/backend/oneconfhandler.py'
39--- softwarecenter/backend/oneconfhandler.py 2011-09-26 14:34:05 +0000
40+++ softwarecenter/backend/oneconfhandler.py 1970-01-01 00:00:00 +0000
41@@ -1,206 +0,0 @@
42-# -*- coding: utf-8 -*-
43-# Copyright (C) 2011 Canonical
44-#
45-# Authors:
46-# Didier Roche
47-#
48-# This program is free software; you can redistribute it and/or modify it under
49-# the terms of the GNU General Public License as published by the Free Software
50-# Foundation; version 3.
51-#
52-# This program is distributed in the hope that it will be useful, but WITHOUT
53-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
54-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
55-# details.
56-#
57-# You should have received a copy of the GNU General Public License along with
58-# this program; if not, write to the Free Software Foundation, Inc.,
59-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
60-
61-
62-from oneconf.dbusconnect import DbusConnect
63-from oneconf.enums import MIN_TIME_WITHOUT_ACTIVITY
64-
65-from softwarecenter.backend.login_sso import get_sso_backend
66-from softwarecenter.backend.restfulclient import get_ubuntu_sso_backend
67-from softwarecenter.utils import clear_token_from_ubuntu_sso
68-
69-import datetime
70-from gi.repository import GObject
71-import logging
72-
73-from gettext import gettext as _
74-
75-LOG = logging.getLogger(__name__)
76-
77-class OneConfHandler(GObject.GObject):
78-
79- __gsignals__ = {
80- "show-oneconf-changed" : (GObject.SIGNAL_RUN_LAST,
81- GObject.TYPE_NONE,
82- (GObject.TYPE_PYOBJECT,),
83- ),
84- "last-time-sync-changed" : (GObject.SIGNAL_RUN_LAST,
85- GObject.TYPE_NONE,
86- (GObject.TYPE_PYOBJECT,),
87- ),
88- }
89-
90-
91- def __init__(self, oneconfviewpickler):
92- '''Controller of the installed pane'''
93-
94- LOG.debug("OneConf Handler init")
95- super(OneConfHandler, self).__init__()
96-
97- # FIXME: should be an enum common to OneConf and here
98- self.appname = "Ubuntu Software Center"
99-
100- # OneConf stuff
101- self.oneconf = DbusConnect()
102- self.oneconf.hosts_dbus_object.connect_to_signal('hostlist_changed',
103- self.refresh_hosts)
104- self.oneconf.hosts_dbus_object.connect_to_signal('packagelist_changed',
105- self._on_store_packagelist_changed)
106- self.oneconf.hosts_dbus_object.connect_to_signal('latestsync_changed',
107- self.on_new_latest_oneconf_sync_timestamp)
108- self.already_registered_hostids = []
109- self.is_current_registered = False
110-
111- self.oneconfviewpickler = oneconfviewpickler
112-
113- # refresh host list
114- self._refreshing_hosts = False
115- GObject.timeout_add_seconds(MIN_TIME_WITHOUT_ACTIVITY, self.get_latest_oneconf_sync)
116- GObject.idle_add(self.refresh_hosts)
117-
118- def refresh_hosts(self):
119- """refresh hosts list in the panel view"""
120- LOG.debug('oneconf: refresh hosts')
121-
122- # this function can be called in different threads
123- if self._refreshing_hosts:
124- return
125- self._refreshing_hosts = True
126-
127- #view_switcher = self.app.view_switcher
128- #model = view_switcher.get_model()
129- #previous_iter = model.installed_iter
130-
131- all_hosts = self.oneconf.get_all_hosts()
132- for hostid in all_hosts:
133- current, hostname, share_inventory = all_hosts[hostid]
134- if not hostid in self.already_registered_hostids and not current:
135- self.oneconfviewpickler.register_computer(hostid, hostname)
136- self.already_registered_hostids.append(hostid)
137- if current:
138- is_current_registered = share_inventory
139-
140- # ensure we are logged to ubuntu sso to activate the view
141- if self.is_current_registered != is_current_registered:
142- self.sync_between_computers(is_current_registered)
143-
144- self._refreshing_hosts = False
145-
146- def get_latest_oneconf_sync(self):
147- '''Get latest sync state in OneConf.
148-
149- This function is also the "ping" letting OneConf service alive'''
150- LOG.debug("get latest sync state")
151- timestamp = self.oneconf.get_last_sync_date()
152- self.on_new_latest_oneconf_sync_timestamp(timestamp)
153- return True
154-
155- def on_new_latest_oneconf_sync_timestamp(self, timestamp):
156- '''Callback computing the right message for latest sync time'''
157- try:
158- last_sync = datetime.datetime.fromtimestamp(float(timestamp))
159- today = datetime.datetime.strptime(str(datetime.date.today()), '%Y-%m-%d')
160- the_daybefore = today - datetime.timedelta(days=1)
161-
162- if last_sync > today:
163- msg = _("Last sync %s") % last_sync.strftime('%H:%M')
164- elif last_sync < today and last_sync > the_daybefore:
165- msg = _("Last sync yesterday %s") % last_sync.strftime('%H:%M')
166- else:
167- msg = _("Last sync %s") % last_sync.strftime('%Y-%m-%d %H:%M')
168- except (TypeError, ValueError):
169- msg = _("To sync with another computer, choose “Sync Between Computers” from that computer.")
170- self.emit("last-time-sync-changed", msg)
171-
172- def _share_inventory(self, share_inventory):
173- '''set oneconf state and emit signal for installed view to show or not oneconf'''
174-
175- if share_inventory == self.is_current_registered:
176- return
177- self.is_current_registered = share_inventory
178- LOG.debug("change share inventory state to %s", share_inventory)
179- self.oneconf.set_share_inventory(share_inventory)
180- self.get_latest_oneconf_sync()
181- self.emit("show-oneconf-changed", share_inventory)
182-
183- def sync_between_computers(self, sync_on):
184- '''toggle the sync on and off if needed between computers'''
185- LOG.debug("Toggle sync between computers: %s", sync_on)
186-
187- if sync_on:
188- self._try_login()
189- else:
190- self._share_inventory(False)
191-
192- def _on_store_packagelist_changed(self, hostid):
193- '''pass the message to the view controller'''
194- self.oneconfviewpickler.store_packagelist_changed(hostid)
195-
196-
197- # SSO login part
198-
199- def _try_login(self):
200- '''Try to get the credential or login on ubuntu sso'''
201- logging.debug("OneConf login()")
202- help_text = _("With multiple Ubuntu computers, you can publish their inventories online to compare the software installed on each\n"
203- "No-one else will be able to see what you have installed.")
204- self.sso = get_sso_backend(0,
205- self.appname, help_text)
206- self.sso.connect("login-successful", self._maybe_login_successful)
207- self.sso.connect("login-canceled", self._login_canceled)
208- self.sso.login_or_register()
209-
210- def _login_canceled(self, sso):
211- self._share_inventory(False)
212-
213- def _maybe_login_successful(self, sso, oauth_result):
214- """ called after we have the token, then we go and figure out our name """
215- logging.debug("_maybe_login_successful")
216- token = oauth_result
217- self.ssoapi = get_ubuntu_sso_backend(token)
218- self.ssoapi.connect("whoami", self._whoami_done)
219- self.ssoapi.connect("error", self._whoami_error)
220- self.ssoapi.whoami()
221-
222- def _whoami_done(self, ssologin, result):
223- logging.debug("_whoami_done")
224- self._share_inventory(True)
225-
226- def _whoami_error(self, ssologin, e):
227- logging.error("whoami error '%s'" % e)
228- # HACK: clear the token from the keyring assuming that it expired
229- # or got deauthorized by the user on the website
230- # this really should be done by ubuntu-sso-client itself
231- import lazr.restfulclient.errors
232- errortype = lazr.restfulclient.errors.HTTPError
233- if (type(e) == errortype):
234- LOG.warn("authentication error, resetting token and retrying")
235- clear_token_from_ubuntu_sso(self.appname)
236- self._share_inventory(False)
237- return
238-
239-
240-# singleton
241-oneconf_handler = None
242-def get_oneconf_handler(oneconfviewpickler = None):
243- global oneconf_handler
244- if oneconf_handler is None and oneconfviewpickler:
245- oneconf_handler = OneConfHandler(oneconfviewpickler)
246- return oneconf_handler
247-
248
249=== added file 'softwarecenter/backend/oneconfhandler/__init__.py'
250--- softwarecenter/backend/oneconfhandler/__init__.py 1970-01-01 00:00:00 +0000
251+++ softwarecenter/backend/oneconfhandler/__init__.py 2011-10-27 07:59:24 +0000
252@@ -0,0 +1,32 @@
253+# -*- coding: utf-8 -*-
254+# Copyright (C) 2011 Canonical
255+#
256+# Authors:
257+# Didier Roche
258+#
259+# This program is free software; you can redistribute it and/or modify it under
260+# the terms of the GNU General Public License as published by the Free Software
261+# Foundation; version 3.
262+#
263+# This program is distributed in the hope that it will be useful, but WITHOUT
264+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
265+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
266+# details.
267+#
268+# You should have received a copy of the GNU General Public License along with
269+# this program; if not, write to the Free Software Foundation, Inc.,
270+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
271+
272+
273+# singleton
274+oneconf_handler = None
275+def get_oneconf_handler(oneconfviewpickler = None):
276+ global oneconf_handler
277+ try:
278+ from softwarecenter.backend.oneconfhandler.core import OnceConfHandler
279+ except ImportError:
280+ return None
281+ if oneconf_handler is None and oneconfviewpickler:
282+ oneconf_handler = OneConfHandler(oneconfviewpickler)
283+ return oneconf_handler
284+
285
286=== added file 'softwarecenter/backend/oneconfhandler/core.py'
287--- softwarecenter/backend/oneconfhandler/core.py 1970-01-01 00:00:00 +0000
288+++ softwarecenter/backend/oneconfhandler/core.py 2011-10-27 07:59:24 +0000
289@@ -0,0 +1,196 @@
290+# -*- coding: utf-8 -*-
291+# Copyright (C) 2011 Canonical
292+#
293+# Authors:
294+# Didier Roche
295+#
296+# This program is free software; you can redistribute it and/or modify it under
297+# the terms of the GNU General Public License as published by the Free Software
298+# Foundation; version 3.
299+#
300+# This program is distributed in the hope that it will be useful, but WITHOUT
301+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
302+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
303+# details.
304+#
305+# You should have received a copy of the GNU General Public License along with
306+# this program; if not, write to the Free Software Foundation, Inc.,
307+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
308+
309+
310+from oneconf.dbusconnect import DbusConnect
311+from oneconf.enums import MIN_TIME_WITHOUT_ACTIVITY
312+
313+from softwarecenter.backend.login_sso import get_sso_backend
314+from softwarecenter.backend.restfulclient import get_ubuntu_sso_backend
315+from softwarecenter.utils import clear_token_from_ubuntu_sso
316+
317+import datetime
318+from gi.repository import GObject
319+import logging
320+
321+from gettext import gettext as _
322+
323+LOG = logging.getLogger(__name__)
324+
325+class OneConfHandler(GObject.GObject):
326+
327+ __gsignals__ = {
328+ "show-oneconf-changed" : (GObject.SIGNAL_RUN_LAST,
329+ GObject.TYPE_NONE,
330+ (GObject.TYPE_PYOBJECT,),
331+ ),
332+ "last-time-sync-changed" : (GObject.SIGNAL_RUN_LAST,
333+ GObject.TYPE_NONE,
334+ (GObject.TYPE_PYOBJECT,),
335+ ),
336+ }
337+
338+
339+ def __init__(self, oneconfviewpickler):
340+ '''Controller of the installed pane'''
341+
342+ LOG.debug("OneConf Handler init")
343+ super(OneConfHandler, self).__init__()
344+
345+ # FIXME: should be an enum common to OneConf and here
346+ self.appname = "Ubuntu Software Center"
347+
348+ # OneConf stuff
349+ self.oneconf = DbusConnect()
350+ self.oneconf.hosts_dbus_object.connect_to_signal('hostlist_changed',
351+ self.refresh_hosts)
352+ self.oneconf.hosts_dbus_object.connect_to_signal('packagelist_changed',
353+ self._on_store_packagelist_changed)
354+ self.oneconf.hosts_dbus_object.connect_to_signal('latestsync_changed',
355+ self.on_new_latest_oneconf_sync_timestamp)
356+ self.already_registered_hostids = []
357+ self.is_current_registered = False
358+
359+ self.oneconfviewpickler = oneconfviewpickler
360+
361+ # refresh host list
362+ self._refreshing_hosts = False
363+ GObject.timeout_add_seconds(MIN_TIME_WITHOUT_ACTIVITY, self.get_latest_oneconf_sync)
364+ GObject.idle_add(self.refresh_hosts)
365+
366+ def refresh_hosts(self):
367+ """refresh hosts list in the panel view"""
368+ LOG.debug('oneconf: refresh hosts')
369+
370+ # this function can be called in different threads
371+ if self._refreshing_hosts:
372+ return
373+ self._refreshing_hosts = True
374+
375+ #view_switcher = self.app.view_switcher
376+ #model = view_switcher.get_model()
377+ #previous_iter = model.installed_iter
378+
379+ all_hosts = self.oneconf.get_all_hosts()
380+ for hostid in all_hosts:
381+ current, hostname, share_inventory = all_hosts[hostid]
382+ if not hostid in self.already_registered_hostids and not current:
383+ self.oneconfviewpickler.register_computer(hostid, hostname)
384+ self.already_registered_hostids.append(hostid)
385+ if current:
386+ is_current_registered = share_inventory
387+
388+ # ensure we are logged to ubuntu sso to activate the view
389+ if self.is_current_registered != is_current_registered:
390+ self.sync_between_computers(is_current_registered)
391+
392+ self._refreshing_hosts = False
393+
394+ def get_latest_oneconf_sync(self):
395+ '''Get latest sync state in OneConf.
396+
397+ This function is also the "ping" letting OneConf service alive'''
398+ LOG.debug("get latest sync state")
399+ timestamp = self.oneconf.get_last_sync_date()
400+ self.on_new_latest_oneconf_sync_timestamp(timestamp)
401+ return True
402+
403+ def on_new_latest_oneconf_sync_timestamp(self, timestamp):
404+ '''Callback computing the right message for latest sync time'''
405+ try:
406+ last_sync = datetime.datetime.fromtimestamp(float(timestamp))
407+ today = datetime.datetime.strptime(str(datetime.date.today()), '%Y-%m-%d')
408+ the_daybefore = today - datetime.timedelta(days=1)
409+
410+ if last_sync > today:
411+ msg = _("Last sync %s") % last_sync.strftime('%H:%M')
412+ elif last_sync < today and last_sync > the_daybefore:
413+ msg = _("Last sync yesterday %s") % last_sync.strftime('%H:%M')
414+ else:
415+ msg = _("Last sync %s") % last_sync.strftime('%Y-%m-%d %H:%M')
416+ except (TypeError, ValueError):
417+ msg = _("To sync with another computer, choose “Sync Between Computers” from that computer.")
418+ self.emit("last-time-sync-changed", msg)
419+
420+ def _share_inventory(self, share_inventory):
421+ '''set oneconf state and emit signal for installed view to show or not oneconf'''
422+
423+ if share_inventory == self.is_current_registered:
424+ return
425+ self.is_current_registered = share_inventory
426+ LOG.debug("change share inventory state to %s", share_inventory)
427+ self.oneconf.set_share_inventory(share_inventory)
428+ self.get_latest_oneconf_sync()
429+ self.emit("show-oneconf-changed", share_inventory)
430+
431+ def sync_between_computers(self, sync_on):
432+ '''toggle the sync on and off if needed between computers'''
433+ LOG.debug("Toggle sync between computers: %s", sync_on)
434+
435+ if sync_on:
436+ self._try_login()
437+ else:
438+ self._share_inventory(False)
439+
440+ def _on_store_packagelist_changed(self, hostid):
441+ '''pass the message to the view controller'''
442+ self.oneconfviewpickler.store_packagelist_changed(hostid)
443+
444+
445+ # SSO login part
446+
447+ def _try_login(self):
448+ '''Try to get the credential or login on ubuntu sso'''
449+ logging.debug("OneConf login()")
450+ help_text = _("With multiple Ubuntu computers, you can publish their inventories online to compare the software installed on each\n"
451+ "No-one else will be able to see what you have installed.")
452+ self.sso = get_sso_backend(0,
453+ self.appname, help_text)
454+ self.sso.connect("login-successful", self._maybe_login_successful)
455+ self.sso.connect("login-canceled", self._login_canceled)
456+ self.sso.login_or_register()
457+
458+ def _login_canceled(self, sso):
459+ self._share_inventory(False)
460+
461+ def _maybe_login_successful(self, sso, oauth_result):
462+ """ called after we have the token, then we go and figure out our name """
463+ logging.debug("_maybe_login_successful")
464+ token = oauth_result
465+ self.ssoapi = get_ubuntu_sso_backend(token)
466+ self.ssoapi.connect("whoami", self._whoami_done)
467+ self.ssoapi.connect("error", self._whoami_error)
468+ self.ssoapi.whoami()
469+
470+ def _whoami_done(self, ssologin, result):
471+ logging.debug("_whoami_done")
472+ self._share_inventory(True)
473+
474+ def _whoami_error(self, ssologin, e):
475+ logging.error("whoami error '%s'" % e)
476+ # HACK: clear the token from the keyring assuming that it expired
477+ # or got deauthorized by the user on the website
478+ # this really should be done by ubuntu-sso-client itself
479+ import lazr.restfulclient.errors
480+ errortype = lazr.restfulclient.errors.HTTPError
481+ if (type(e) == errortype):
482+ LOG.warn("authentication error, resetting token and retrying")
483+ clear_token_from_ubuntu_sso(self.appname)
484+ self._share_inventory(False)
485+ return
486
487=== modified file 'softwarecenter/backend/piston/rnrclient.py'
488--- softwarecenter/backend/piston/rnrclient.py 2011-08-09 08:47:43 +0000
489+++ softwarecenter/backend/piston/rnrclient.py 2011-10-27 07:59:24 +0000
490@@ -52,7 +52,7 @@
491 LOG.error("need python-piston-mini client\n"
492 "available in natty or from:\n"
493 " ppa:software-store-developers/daily-build ")
494- sys.exit(1)
495+ raise
496
497
498 if __name__ == "__main__":
499
500=== added directory 'softwarecenter/backend/reviews'
501=== renamed file 'softwarecenter/backend/reviews.py' => 'softwarecenter/backend/reviews/__init__.py'
502--- softwarecenter/backend/reviews.py 2011-10-13 13:39:21 +0000
503+++ softwarecenter/backend/reviews/__init__.py 2011-10-27 07:59:24 +0000
504@@ -53,8 +53,6 @@
505 from StringIO import StringIO
506 from urllib import quote_plus
507
508-from softwarecenter.backend.piston.rnrclient import RatingsAndReviewsAPI
509-from softwarecenter.backend.piston.rnrclient_pristine import ReviewDetails
510 from softwarecenter.db.categories import CategoriesParser
511 from softwarecenter.db.database import Application, StoreDatabase
512 import softwarecenter.distro
513@@ -77,7 +75,7 @@
514
515 from softwarecenter.netstatus import network_state_is_connected
516
517-from spawn_helper import SpawnHelper
518+from softwarecenter.backend.spawn_helper import SpawnHelper
519
520 LOG = logging.getLogger(__name__)
521
522@@ -100,7 +98,7 @@
523 USEFULNESS_CACHE = {}
524
525 def __init__(self, try_server=False):
526- self.rnrclient = RatingsAndReviewsAPI()
527+ #self.rnrclient = RatingsAndReviewsAPI()
528 fname = "usefulness.p"
529 self.USEFULNESS_CACHE_FILE = os.path.join(SOFTWARE_CENTER_CACHE_DIR,
530 fname)
531@@ -438,43 +436,12 @@
532 applist.append(db.get_pkgname(doc))
533 return applist
534
535- # writing new reviews spawns external helper
536- # FIXME: instead of the callback we should add proper gobject signals
537 def spawn_write_new_review_ui(self, translated_app, version, iconname,
538 origin, parent_xid, datadir, callback):
539- """ this spawns the UI for writing a new review and
540- adds it automatically to the reviews DB """
541- app = translated_app.get_untranslated_app(self.db)
542- cmd = [os.path.join(datadir, RNRApps.SUBMIT_REVIEW),
543- "--pkgname", app.pkgname,
544- "--iconname", iconname,
545- "--parent-xid", "%s" % parent_xid,
546- "--version", version,
547- "--origin", origin,
548- "--datadir", datadir,
549- ]
550- if app.appname:
551- # needs to be (utf8 encoded) str, otherwise call fails
552- cmd += ["--appname", utf8(app.appname)]
553- spawn_helper = SpawnHelper(format="json")
554- spawn_helper.connect(
555- "data-available", self._on_submit_review_data, app, callback)
556- spawn_helper.run(cmd)
557-
558- def _on_submit_review_data(self, spawn_helper, review_json, app, callback):
559- """ called when submit_review finished, when the review was send
560- successfully the callback is triggered with the new reviews
561+ """Spawn the UI for writing a new review and adds it automatically
562+ to the reviews DB.
563 """
564- LOG.debug("_on_submit_review_data")
565- # read stdout from submit_review
566- review = ReviewDetails.from_dict(review_json)
567- # FIXME: ideally this would be stored in ubuntu-sso-client
568- # but it dosn't so we store it here
569- save_person_to_config(review.reviewer_username)
570- if not app in self._reviews:
571- self._reviews[app] = []
572- self._reviews[app].insert(0, Review.from_piston_mini_client(review))
573- callback(app, self._reviews[app])
574+ pass
575
576 def spawn_report_abuse_ui(self, review_id, parent_xid, datadir, callback):
577 """ this spawns the UI for reporting a review as inappropriate
578@@ -482,296 +449,20 @@
579 operation is complete it will call callback with the updated
580 review list
581 """
582- cmd = [os.path.join(datadir, RNRApps.REPORT_REVIEW),
583- "--review-id", review_id,
584- "--parent-xid", "%s" % parent_xid,
585- "--datadir", datadir,
586- ]
587- spawn_helper = SpawnHelper("json")
588- spawn_helper.connect("exited",
589- self._on_report_abuse_finished,
590- review_id, callback)
591- spawn_helper.run(cmd)
592-
593- def _on_report_abuse_finished(self, spawn_helper, exitcode, review_id, callback):
594- """ called when report_absuse finished """
595- LOG.debug("hide id %s " % review_id)
596- if exitcode == 0:
597- for (app, reviews) in self._reviews.items():
598- for review in reviews:
599- if str(review.id) == str(review_id):
600- # remove the one we don't want to see anymore
601- self._reviews[app].remove(review)
602- callback(app, self._reviews[app], None, 'remove', review)
603- break
604-
605+ pass
606
607 def spawn_submit_usefulness_ui(self, review_id, is_useful, parent_xid, datadir, callback):
608- cmd = [os.path.join(datadir, RNRApps.SUBMIT_USEFULNESS),
609- "--review-id", "%s" % review_id,
610- "--is-useful", "%s" % int(is_useful),
611- "--parent-xid", "%s" % parent_xid,
612- "--datadir", datadir,
613- ]
614- spawn_helper = SpawnHelper(format="none")
615- spawn_helper.connect("exited",
616- self._on_submit_usefulness_finished,
617- review_id, is_useful, callback)
618- spawn_helper.connect("error",
619- self._on_submit_usefulness_error,
620- review_id, callback)
621- spawn_helper.run(cmd)
622-
623- def _on_submit_usefulness_finished(self, spawn_helper, res, review_id, is_useful, callback):
624- """ called when report_usefulness finished """
625- # "Created", "Updated", "Not modified" -
626- # once lp:~mvo/rnr-server/submit-usefulness-result-strings makes it
627- response = spawn_helper._stdout
628- if response == '"Not modified"':
629- self._on_submit_usefulness_error(spawn_helper, response, review_id, callback)
630- return
631-
632- LOG.debug("usefulness id %s " % review_id)
633- useful_votes = UsefulnessCache()
634- useful_votes.add_usefulness_vote(review_id, is_useful)
635- for (app, reviews) in self._reviews.items():
636- for review in reviews:
637- if str(review.id) == str(review_id):
638- # update usefulness, older servers do not send
639- # usefulness_{total,favorable} so we use getattr
640- review.usefulness_total = getattr(review, "usefulness_total", 0) + 1
641- if is_useful:
642- review.usefulness_favorable = getattr(review, "usefulness_favorable", 0) + 1
643- callback(app, self._reviews[app], useful_votes, 'replace', review)
644- break
645-
646- def _on_submit_usefulness_error(self, spawn_helper, error_str, review_id, callback):
647- LOG.warn("submit usefulness id=%s failed with error: %s" %
648- (review_id, error_str))
649- for (app, reviews) in self._reviews.items():
650- for review in reviews:
651- if str(review.id) == str(review_id):
652- review.usefulness_submit_error = True
653- callback(app, self._reviews[app], None, 'replace', review)
654- break
655+ """Spawn a helper to submit a usefulness vote."""
656+ pass
657
658 def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback):
659- cmd = [os.path.join(datadir, RNRApps.DELETE_REVIEW),
660- "--review-id", "%s" % review_id,
661- "--parent-xid", "%s" % parent_xid,
662- "--datadir", datadir,
663- ]
664- spawn_helper = SpawnHelper(format="none")
665- spawn_helper.connect("exited",
666- self._on_delete_review_finished,
667- review_id, callback)
668- spawn_helper.connect("error", self._on_delete_review_error,
669- review_id, callback)
670- spawn_helper.run(cmd)
671-
672- def _on_delete_review_finished(self, spawn_helper, res, review_id, callback):
673- """ called when delete_review finished"""
674- LOG.debug("delete id %s " % review_id)
675- for (app, reviews) in self._reviews.items():
676- for review in reviews:
677- if str(review.id) == str(review_id):
678- # remove the one we don't want to see anymore
679- self._reviews[app].remove(review)
680- callback(app, self._reviews[app], None, 'remove', review)
681- break
682-
683- def _on_delete_review_error(self, spawn_helper, error_str, review_id, callback):
684- """called if delete review errors"""
685- LOG.warn("delete review id=%s failed with error: %s" % (review_id, error_str))
686- for (app, reviews) in self._reviews.items():
687- for review in reviews:
688- if str(review.id) == str(review_id):
689- review.delete_error = True
690- callback(app, self._reviews[app], action='replace',
691- single_review=review)
692- break
693-
694-
695+ """Spawn a helper to delete a review."""
696+ pass
697+
698 def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):
699- """ this spawns the UI for writing a new review and
700- adds it automatically to the reviews DB """
701- cmd = [os.path.join(datadir, RNRApps.MODIFY_REVIEW),
702- "--parent-xid", "%s" % parent_xid,
703- "--iconname", iconname,
704- "--datadir", "%s" % datadir,
705- "--review-id", "%s" % review_id,
706- ]
707- spawn_helper = SpawnHelper(format="json")
708- spawn_helper.connect("data-available",
709- self._on_modify_review_finished,
710- review_id, callback)
711- spawn_helper.connect("error", self._on_modify_review_error,
712- review_id, callback)
713- spawn_helper.run(cmd)
714-
715- def _on_modify_review_finished(self, spawn_helper, review_json, review_id, callback):
716- """called when modify_review finished"""
717- LOG.debug("_on_modify_review_finished")
718- #review_json = spawn_helper._stdout
719- mod_review = ReviewDetails.from_dict(review_json)
720- for (app, reviews) in self._reviews.items():
721- for review in reviews:
722- if str(review.id) == str(review_id):
723- # remove the one we don't want to see anymore
724- self._reviews[app].remove(review)
725- new_review = Review.from_piston_mini_client(mod_review)
726- self._reviews[app].insert(0, new_review)
727- callback(app, self._reviews[app], action='replace',
728- single_review=new_review)
729- break
730-
731- def _on_modify_review_error(self, spawn_helper, error_str, review_id, callback):
732- """called if modify review errors"""
733- LOG.debug("modify review id=%s failed with error: %s" % (review_id, error_str))
734- for (app, reviews) in self._reviews.items():
735- for review in reviews:
736- if str(review.id) == str(review_id):
737- review.modify_error = True
738- callback(app, self._reviews[app], action='replace',
739- single_review=review)
740- break
741-
742-
743-# this code had several incernations:
744-# - python threads, slow and full of latency (GIL)
745-# - python multiprocesing, crashed when accessibility was turned on,
746-# does not work in the quest session (#743020)
747-# - GObject.spawn_async() looks good so far (using the SpawnHelper code)
748-class ReviewLoaderSpawningRNRClient(ReviewLoader):
749- """ loader that uses multiprocessing to call rnrclient and
750- a glib timeout watcher that polls periodically for the
751- data
752- """
753-
754- def __init__(self, cache, db, distro=None):
755- super(ReviewLoaderSpawningRNRClient, self).__init__(cache, db, distro)
756- cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
757- self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
758- cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
759- self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
760- self._reviews = {}
761-
762- def _update_rnrclient_offline_state(self):
763- # this needs the lp:~mvo/piston-mini-client/offline-mode branch
764- self.rnrclient._offline_mode = not network_state_is_connected()
765-
766- # reviews
767- def get_reviews(self, translated_app, callback, page=1,
768- language=None, sort=0):
769- """ public api, triggers fetching a review and calls callback
770- when its ready
771- """
772- # its fine to use the translated appname here, we only submit the
773- # pkgname to the server
774- app = translated_app
775- self._update_rnrclient_offline_state()
776- sort_method = self._review_sort_methods[sort]
777- if language is None:
778- language = self.language
779- # gather args for the helper
780- try:
781- origin = self.cache.get_origin(app.pkgname)
782- except:
783- # this can happen if e.g. the app has multiple origins, this
784- # will be handled later
785- origin = None
786- # special case for not-enabled PPAs
787- if not origin and self.db:
788- details = app.get_details(self.db)
789- ppa = details.ppaname
790- if ppa:
791- origin = "lp-ppa-%s" % ppa.replace("/", "-")
792- # if there is no origin, there is nothing to do
793- if not origin:
794- callback(app, [])
795- return
796- distroseries = self.distro.get_codename()
797- # run the command and add watcher
798- cmd = [os.path.join(softwarecenter.paths.datadir, PistonHelpers.GET_REVIEWS),
799- "--language", language,
800- "--origin", origin,
801- "--distroseries", distroseries,
802- "--pkgname", str(app.pkgname), # ensure its str, not unicode
803- "--page", str(page),
804- "--sort", sort_method,
805- ]
806- spawn_helper = SpawnHelper()
807- spawn_helper.connect(
808- "data-available", self._on_reviews_helper_data, app, callback)
809- spawn_helper.run(cmd)
810-
811- def _on_reviews_helper_data(self, spawn_helper, piston_reviews, app, callback):
812- # convert into our review objects
813- reviews = []
814- for r in piston_reviews:
815- reviews.append(Review.from_piston_mini_client(r))
816- # add to our dicts and run callback
817- self._reviews[app] = reviews
818- callback(app, self._reviews[app])
819- return False
820-
821- # stats
822- def refresh_review_stats(self, callback):
823- """ public api, refresh the available statistics """
824- try:
825- mtime = os.path.getmtime(self.REVIEW_STATS_CACHE_FILE)
826- days_delta = int((time.time() - mtime) // (24*60*60))
827- days_delta += 1
828- except OSError:
829- days_delta = 0
830- LOG.debug("refresh with days_delta: %s" % days_delta)
831- #origin = "any"
832- #distroseries = self.distro.get_codename()
833- cmd = [os.path.join(
834- softwarecenter.paths.datadir, PistonHelpers.GET_REVIEW_STATS),
835- # FIXME: the server currently has bug (#757695) so we
836- # can not turn this on just yet and need to use
837- # the old "catch-all" review-stats for now
838- #"--origin", origin,
839- #"--distroseries", distroseries,
840- ]
841- if days_delta:
842- cmd += ["--days-delta", str(days_delta)]
843- spawn_helper = SpawnHelper()
844- spawn_helper.connect("data-available", self._on_review_stats_data, callback)
845- spawn_helper.run(cmd)
846-
847- def _on_review_stats_data(self, spawn_helper, piston_review_stats, callback):
848- """ process stdout from the helper """
849- review_stats = self.REVIEW_STATS_CACHE
850-
851- if self._cache_version_old and self._server_has_histogram(piston_review_stats):
852- self.REVIEW_STATS_CACHE = {}
853- self.save_review_stats_cache_file()
854- self.refresh_review_stats(callback)
855- return
856-
857- # convert to the format that s-c uses
858- for r in piston_review_stats:
859- s = ReviewStats(Application("", r.package_name))
860- s.ratings_average = float(r.ratings_average)
861- s.ratings_total = float(r.ratings_total)
862- if r.histogram:
863- s.rating_spread = json.loads(r.histogram)
864- else:
865- s.rating_spread = [0,0,0,0,0]
866- s.dampened_rating = calc_dr(s.rating_spread)
867- review_stats[s.app] = s
868- self.REVIEW_STATS_CACHE = review_stats
869- callback(review_stats)
870- self.save_review_stats_cache_file()
871-
872- def _server_has_histogram(self, piston_review_stats):
873- '''check response from server to see if histogram is supported'''
874- supported = getattr(piston_review_stats[0], "histogram", False)
875- if not supported:
876- return False
877- return True
878+ """Spawn a helper to modify a review."""
879+ pass
880+
881
882 class ReviewLoaderJsonAsync(ReviewLoader):
883 """ get json (or gzip compressed json) """
884@@ -871,7 +562,7 @@
885 return random.choice(self.LOREM.split("\n\n"))
886 def _random_summary(self):
887 return random.choice(self.SUMMARIES)
888- def get_reviews(self, application, callback, page=1, language=None):
889+ def get_reviews(self, application, callback, page=1, language=None, sort=0):
890 if not application in self._review_stats_cache:
891 self.get_review_stats(application)
892 stats = self._review_stats_cache[application]
893@@ -1034,6 +725,26 @@
894 et ea rebum stet clita kasd gubergren no sea takimata sanctus est lorem
895 ipsum dolor sit amet"""
896
897+
898+class ReviewLoaderNull(ReviewLoader):
899+
900+ """A dummy review loader which just returns empty results."""
901+
902+ def __init__(self, cache, db):
903+ self._review_stats_cache = {}
904+ self._reviews_cache = {}
905+
906+ def get_reviews(self, application, callback, page=1, language=None, sort=0):
907+ callback(application, [])
908+
909+ def get_review_stats(self, application):
910+ return None
911+
912+ def refresh_review_stats(self, callback):
913+ review_stats = []
914+ callback(review_stats)
915+
916+
917 review_loader = None
918 def get_review_loader(cache, db=None):
919 """
920@@ -1050,7 +761,12 @@
921 elif "SOFTWARE_CENTER_GIO_REVIEWS" in os.environ:
922 review_loader = ReviewLoaderJsonAsync(cache, db)
923 else:
924- review_loader = ReviewLoaderSpawningRNRClient(cache, db)
925+ try:
926+ from softwarecenter.backend.reviews.rnr import ReviewLoaderSpawningRNRClient
927+ except ImportError:
928+ review_loader = ReviewLoaderNull(cache, db)
929+ else:
930+ review_loader = ReviewLoaderSpawningRNRClient(cache, db)
931 return review_loader
932
933 if __name__ == "__main__":
934
935=== added file 'softwarecenter/backend/reviews/rnr.py'
936--- softwarecenter/backend/reviews/rnr.py 1970-01-01 00:00:00 +0000
937+++ softwarecenter/backend/reviews/rnr.py 2011-10-27 07:59:24 +0000
938@@ -0,0 +1,376 @@
939+# -*- coding: utf-8 -*-
940+
941+# Copyright (C) 2009 Canonical
942+#
943+# Authors:
944+# Michael Vogt
945+#
946+# This program is free software; you can redistribute it and/or modify it under
947+# the terms of the GNU General Public License as published by the Free Software
948+# Foundation; version 3.
949+#
950+# This program is distributed in the hope that it will be useful, but WITHOUT
951+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
952+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
953+# details.
954+#
955+# You should have received a copy of the GNU General Public License along with
956+# this program; if not, write to the Free Software Foundation, Inc.,
957+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
958+
959+import gzip
960+import logging
961+import os
962+import json
963+import time
964+
965+from softwarecenter.backend.spawn_helper import SpawnHelper
966+from softwarecenter.backend.reviews import ReviewLoader, Review, ReviewStats
967+from softwarecenter.backend.piston.rnrclient import RatingsAndReviewsAPI
968+from softwarecenter.backend.piston.rnrclient_pristine import ReviewDetails
969+from softwarecenter.db.database import Application
970+import softwarecenter.distro
971+from softwarecenter.netstatus import network_state_is_connected
972+from softwarecenter.paths import (SOFTWARE_CENTER_CACHE_DIR,
973+ PistonHelpers,
974+ RNRApps,
975+ )
976+from softwarecenter.utils import calc_dr
977+
978+LOG = logging.getLogger(__name__)
979+
980+
981+# this code had several incernations:
982+# - python threads, slow and full of latency (GIL)
983+# - python multiprocesing, crashed when accessibility was turned on,
984+# does not work in the quest session (#743020)
985+# - GObject.spawn_async() looks good so far (using the SpawnHelper code)
986+class ReviewLoaderSpawningRNRClient(ReviewLoader):
987+ """ loader that uses multiprocessing to call rnrclient and
988+ a glib timeout watcher that polls periodically for the
989+ data
990+ """
991+
992+ def __init__(self, cache, db, distro=None):
993+ super(ReviewLoaderSpawningRNRClient, self).__init__(cache, db, distro)
994+ cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
995+ self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
996+ cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
997+ self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
998+ self._reviews = {}
999+
1000+ def _update_rnrclient_offline_state(self):
1001+ # this needs the lp:~mvo/piston-mini-client/offline-mode branch
1002+ self.rnrclient._offline_mode = not network_state_is_connected()
1003+
1004+ # reviews
1005+ def get_reviews(self, translated_app, callback, page=1,
1006+ language=None, sort=0):
1007+ """ public api, triggers fetching a review and calls callback
1008+ when its ready
1009+ """
1010+ # its fine to use the translated appname here, we only submit the
1011+ # pkgname to the server
1012+ app = translated_app
1013+ self._update_rnrclient_offline_state()
1014+ sort_method = self._review_sort_methods[sort]
1015+ if language is None:
1016+ language = self.language
1017+ # gather args for the helper
1018+ try:
1019+ origin = self.cache.get_origin(app.pkgname)
1020+ except:
1021+ # this can happen if e.g. the app has multiple origins, this
1022+ # will be handled later
1023+ origin = None
1024+ # special case for not-enabled PPAs
1025+ if not origin and self.db:
1026+ details = app.get_details(self.db)
1027+ ppa = details.ppaname
1028+ if ppa:
1029+ origin = "lp-ppa-%s" % ppa.replace("/", "-")
1030+ # if there is no origin, there is nothing to do
1031+ if not origin:
1032+ callback(app, [])
1033+ return
1034+ distroseries = self.distro.get_codename()
1035+ # run the command and add watcher
1036+ cmd = [os.path.join(softwarecenter.paths.datadir, PistonHelpers.GET_REVIEWS),
1037+ "--language", language,
1038+ "--origin", origin,
1039+ "--distroseries", distroseries,
1040+ "--pkgname", str(app.pkgname), # ensure its str, not unicode
1041+ "--page", str(page),
1042+ "--sort", sort_method,
1043+ ]
1044+ spawn_helper = SpawnHelper()
1045+ spawn_helper.connect(
1046+ "data-available", self._on_reviews_helper_data, app, callback)
1047+ spawn_helper.run(cmd)
1048+
1049+ def _on_reviews_helper_data(self, spawn_helper, piston_reviews, app, callback):
1050+ # convert into our review objects
1051+ reviews = []
1052+ for r in piston_reviews:
1053+ reviews.append(Review.from_piston_mini_client(r))
1054+ # add to our dicts and run callback
1055+ self._reviews[app] = reviews
1056+ callback(app, self._reviews[app])
1057+ return False
1058+
1059+ # stats
1060+ def refresh_review_stats(self, callback):
1061+ """ public api, refresh the available statistics """
1062+ try:
1063+ mtime = os.path.getmtime(self.REVIEW_STATS_CACHE_FILE)
1064+ days_delta = int((time.time() - mtime) // (24*60*60))
1065+ days_delta += 1
1066+ except OSError:
1067+ days_delta = 0
1068+ LOG.debug("refresh with days_delta: %s" % days_delta)
1069+ #origin = "any"
1070+ #distroseries = self.distro.get_codename()
1071+ cmd = [os.path.join(
1072+ softwarecenter.paths.datadir, PistonHelpers.GET_REVIEW_STATS),
1073+ # FIXME: the server currently has bug (#757695) so we
1074+ # can not turn this on just yet and need to use
1075+ # the old "catch-all" review-stats for now
1076+ #"--origin", origin,
1077+ #"--distroseries", distroseries,
1078+ ]
1079+ if days_delta:
1080+ cmd += ["--days-delta", str(days_delta)]
1081+ spawn_helper = SpawnHelper()
1082+ spawn_helper.connect("data-available", self._on_review_stats_data, callback)
1083+ spawn_helper.run(cmd)
1084+
1085+ def _on_review_stats_data(self, spawn_helper, piston_review_stats, callback):
1086+ """ process stdout from the helper """
1087+ review_stats = self.REVIEW_STATS_CACHE
1088+
1089+ if self._cache_version_old and self._server_has_histogram(piston_review_stats):
1090+ self.REVIEW_STATS_CACHE = {}
1091+ self.save_review_stats_cache_file()
1092+ self.refresh_review_stats(callback)
1093+ return
1094+
1095+ # convert to the format that s-c uses
1096+ for r in piston_review_stats:
1097+ s = ReviewStats(Application("", r.package_name))
1098+ s.ratings_average = float(r.ratings_average)
1099+ s.ratings_total = float(r.ratings_total)
1100+ if r.histogram:
1101+ s.rating_spread = json.loads(r.histogram)
1102+ else:
1103+ s.rating_spread = [0,0,0,0,0]
1104+ s.dampened_rating = calc_dr(s.rating_spread)
1105+ review_stats[s.app] = s
1106+ self.REVIEW_STATS_CACHE = review_stats
1107+ callback(review_stats)
1108+ self.save_review_stats_cache_file()
1109+
1110+ def _server_has_histogram(self, piston_review_stats):
1111+ '''check response from server to see if histogram is supported'''
1112+ supported = getattr(piston_review_stats[0], "histogram", False)
1113+ if not supported:
1114+ return False
1115+ return True
1116+
1117+ # writing new reviews spawns external helper
1118+ # FIXME: instead of the callback we should add proper gobject signals
1119+ def spawn_write_new_review_ui(self, translated_app, version, iconname,
1120+ origin, parent_xid, datadir, callback):
1121+ """ this spawns the UI for writing a new review and
1122+ adds it automatically to the reviews DB """
1123+ app = translated_app.get_untranslated_app(self.db)
1124+ cmd = [os.path.join(datadir, RNRApps.SUBMIT_REVIEW),
1125+ "--pkgname", app.pkgname,
1126+ "--iconname", iconname,
1127+ "--parent-xid", "%s" % parent_xid,
1128+ "--version", version,
1129+ "--origin", origin,
1130+ "--datadir", datadir,
1131+ ]
1132+ if app.appname:
1133+ # needs to be (utf8 encoded) str, otherwise call fails
1134+ cmd += ["--appname", utf8(app.appname)]
1135+ spawn_helper = SpawnHelper(format="json")
1136+ spawn_helper.connect(
1137+ "data-available", self._on_submit_review_data, app, callback)
1138+ spawn_helper.run(cmd)
1139+
1140+ def _on_submit_review_data(self, spawn_helper, review_json, app, callback):
1141+ """ called when submit_review finished, when the review was send
1142+ successfully the callback is triggered with the new reviews
1143+ """
1144+ LOG.debug("_on_submit_review_data")
1145+ return
1146+ # read stdout from submit_review
1147+ review = ReviewDetails.from_dict(review_json)
1148+ # FIXME: ideally this would be stored in ubuntu-sso-client
1149+ # but it dosn't so we store it here
1150+ save_person_to_config(review.reviewer_username)
1151+ if not app in self._reviews:
1152+ self._reviews[app] = []
1153+ self._reviews[app].insert(0, Review.from_piston_mini_client(review))
1154+ callback(app, self._reviews[app])
1155+
1156+ def spawn_report_abuse_ui(self, review_id, parent_xid, datadir, callback):
1157+ """ this spawns the UI for reporting a review as inappropriate
1158+ and adds the review-id to the internal hide list. once the
1159+ operation is complete it will call callback with the updated
1160+ review list
1161+ """
1162+ cmd = [os.path.join(datadir, RNRApps.REPORT_REVIEW),
1163+ "--review-id", review_id,
1164+ "--parent-xid", "%s" % parent_xid,
1165+ "--datadir", datadir,
1166+ ]
1167+ spawn_helper = SpawnHelper("json")
1168+ spawn_helper.connect("exited",
1169+ self._on_report_abuse_finished,
1170+ review_id, callback)
1171+ spawn_helper.run(cmd)
1172+
1173+ def _on_report_abuse_finished(self, spawn_helper, exitcode, review_id, callback):
1174+ """ called when report_absuse finished """
1175+ LOG.debug("hide id %s " % review_id)
1176+ if exitcode == 0:
1177+ for (app, reviews) in self._reviews.items():
1178+ for review in reviews:
1179+ if str(review.id) == str(review_id):
1180+ # remove the one we don't want to see anymore
1181+ self._reviews[app].remove(review)
1182+ callback(app, self._reviews[app], None, 'remove', review)
1183+ break
1184+
1185+
1186+ def spawn_submit_usefulness_ui(self, review_id, is_useful, parent_xid, datadir, callback):
1187+ cmd = [os.path.join(datadir, RNRApps.SUBMIT_USEFULNESS),
1188+ "--review-id", "%s" % review_id,
1189+ "--is-useful", "%s" % int(is_useful),
1190+ "--parent-xid", "%s" % parent_xid,
1191+ "--datadir", datadir,
1192+ ]
1193+ spawn_helper = SpawnHelper(format="none")
1194+ spawn_helper.connect("exited",
1195+ self._on_submit_usefulness_finished,
1196+ review_id, is_useful, callback)
1197+ spawn_helper.connect("error",
1198+ self._on_submit_usefulness_error,
1199+ review_id, callback)
1200+ spawn_helper.run(cmd)
1201+
1202+ def _on_submit_usefulness_finished(self, spawn_helper, res, review_id, is_useful, callback):
1203+ """ called when report_usefulness finished """
1204+ # "Created", "Updated", "Not modified" -
1205+ # once lp:~mvo/rnr-server/submit-usefulness-result-strings makes it
1206+ response = spawn_helper._stdout
1207+ if response == '"Not modified"':
1208+ self._on_submit_usefulness_error(spawn_helper, response, review_id, callback)
1209+ return
1210+
1211+ LOG.debug("usefulness id %s " % review_id)
1212+ useful_votes = UsefulnessCache()
1213+ useful_votes.add_usefulness_vote(review_id, is_useful)
1214+ for (app, reviews) in self._reviews.items():
1215+ for review in reviews:
1216+ if str(review.id) == str(review_id):
1217+ # update usefulness, older servers do not send
1218+ # usefulness_{total,favorable} so we use getattr
1219+ review.usefulness_total = getattr(review, "usefulness_total", 0) + 1
1220+ if is_useful:
1221+ review.usefulness_favorable = getattr(review, "usefulness_favorable", 0) + 1
1222+ callback(app, self._reviews[app], useful_votes, 'replace', review)
1223+ break
1224+
1225+ def _on_submit_usefulness_error(self, spawn_helper, error_str, review_id, callback):
1226+ LOG.warn("submit usefulness id=%s failed with error: %s" %
1227+ (review_id, error_str))
1228+ for (app, reviews) in self._reviews.items():
1229+ for review in reviews:
1230+ if str(review.id) == str(review_id):
1231+ review.usefulness_submit_error = True
1232+ callback(app, self._reviews[app], None, 'replace', review)
1233+ break
1234+
1235+ def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback):
1236+ cmd = [os.path.join(datadir, RNRApps.DELETE_REVIEW),
1237+ "--review-id", "%s" % review_id,
1238+ "--parent-xid", "%s" % parent_xid,
1239+ "--datadir", datadir,
1240+ ]
1241+ spawn_helper = SpawnHelper(format="none")
1242+ spawn_helper.connect("exited",
1243+ self._on_delete_review_finished,
1244+ review_id, callback)
1245+ spawn_helper.connect("error", self._on_delete_review_error,
1246+ review_id, callback)
1247+ spawn_helper.run(cmd)
1248+
1249+ def _on_delete_review_finished(self, spawn_helper, res, review_id, callback):
1250+ """ called when delete_review finished"""
1251+ LOG.debug("delete id %s " % review_id)
1252+ for (app, reviews) in self._reviews.items():
1253+ for review in reviews:
1254+ if str(review.id) == str(review_id):
1255+ # remove the one we don't want to see anymore
1256+ self._reviews[app].remove(review)
1257+ callback(app, self._reviews[app], None, 'remove', review)
1258+ break
1259+
1260+ def _on_delete_review_error(self, spawn_helper, error_str, review_id, callback):
1261+ """called if delete review errors"""
1262+ LOG.warn("delete review id=%s failed with error: %s" % (review_id, error_str))
1263+ for (app, reviews) in self._reviews.items():
1264+ for review in reviews:
1265+ if str(review.id) == str(review_id):
1266+ review.delete_error = True
1267+ callback(app, self._reviews[app], action='replace',
1268+ single_review=review)
1269+ break
1270+
1271+ def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):
1272+ """ this spawns the UI for writing a new review and
1273+ adds it automatically to the reviews DB """
1274+ cmd = [os.path.join(datadir, RNRApps.MODIFY_REVIEW),
1275+ "--parent-xid", "%s" % parent_xid,
1276+ "--iconname", iconname,
1277+ "--datadir", "%s" % datadir,
1278+ "--review-id", "%s" % review_id,
1279+ ]
1280+ spawn_helper = SpawnHelper(format="json")
1281+ spawn_helper.connect("data-available",
1282+ self._on_modify_review_finished,
1283+ review_id, callback)
1284+ spawn_helper.connect("error", self._on_modify_review_error,
1285+ review_id, callback)
1286+ spawn_helper.run(cmd)
1287+
1288+ def _on_modify_review_finished(self, spawn_helper, review_json, review_id, callback):
1289+ """called when modify_review finished"""
1290+ LOG.debug("_on_modify_review_finished")
1291+ return
1292+ #review_json = spawn_helper._stdout
1293+ mod_review = ReviewDetails.from_dict(review_json)
1294+ for (app, reviews) in self._reviews.items():
1295+ for review in reviews:
1296+ if str(review.id) == str(review_id):
1297+ # remove the one we don't want to see anymore
1298+ self._reviews[app].remove(review)
1299+ new_review = Review.from_piston_mini_client(mod_review)
1300+ self._reviews[app].insert(0, new_review)
1301+ callback(app, self._reviews[app], action='replace',
1302+ single_review=new_review)
1303+ break
1304+
1305+ def _on_modify_review_error(self, spawn_helper, error_str, review_id, callback):
1306+ """called if modify review errors"""
1307+ LOG.debug("modify review id=%s failed with error: %s" % (review_id, error_str))
1308+ for (app, reviews) in self._reviews.items():
1309+ for review in reviews:
1310+ if str(review.id) == str(review_id):
1311+ review.modify_error = True
1312+ callback(app, self._reviews[app], action='replace',
1313+ single_review=review)
1314+ break
1315
1316=== modified file 'softwarecenter/db/application.py'
1317--- softwarecenter/db/application.py 2011-10-25 18:38:08 +0000
1318+++ softwarecenter/db/application.py 2011-10-27 07:59:24 +0000
1319@@ -252,7 +252,8 @@
1320 # try apt first
1321 if self._pkg:
1322 for origin in self._pkg.candidate.origins:
1323- if (origin.origin == "Ubuntu" and origin.trusted and origin.component):
1324+ if (origin.origin == get_distro().get_distro_channel_name() and
1325+ origin.trusted and origin.component):
1326 return origin.component
1327 # then xapian
1328 elif self._doc:
1329
1330=== modified file 'softwarecenter/db/update.py'
1331--- softwarecenter/db/update.py 2011-10-18 00:48:28 +0000
1332+++ softwarecenter/db/update.py 2011-10-27 07:59:24 +0000
1333@@ -401,7 +401,7 @@
1334 except ImportError:
1335 return False
1336 if not listsdir:
1337- listsdir = apt_pkg.Config.find_dir("Dir::State::lists")
1338+ listsdir = apt_pkg.config.find_dir("Dir::State::lists")
1339 context = GObject.main_context_default()
1340 for appinfo in glob("%s/*AppInfo" % listsdir):
1341 LOG.debug("processing %s" % appinfo)
1342
1343=== modified file 'softwarecenter/distro/Debian.py'
1344--- softwarecenter/distro/Debian.py 2011-09-21 15:15:28 +0000
1345+++ softwarecenter/distro/Debian.py 2011-10-27 07:59:24 +0000
1346@@ -31,6 +31,10 @@
1347 SCREENSHOT_THUMB_URL = "http://screenshots.debian.net/thumbnail/%(pkgname)s"
1348 SCREENSHOT_LARGE_URL = "http://screenshots.debian.net/screenshot/%(pkgname)s"
1349
1350+ REVIEWS_SERVER = ""
1351+
1352+ DEVELOPER_URL = "http://www.debian.org/devel/"
1353+
1354 def get_distro_channel_name(self):
1355 """ The name in the Release file """
1356 return "Debian"
1357@@ -68,19 +72,20 @@
1358
1359 def get_license_text(self, component):
1360 if component in ("main",):
1361- return _("Open source")
1362+ return _("Meets the Debian Free Software Guidelines")
1363 elif component == "contrib":
1364- return _("Open source, with proprietary parts")
1365- elif component == "restricted":
1366- return _("Proprietary")
1367-
1368- def get_maintenance_status(self, cache, appname, pkgname, component, channel):
1369- return ""
1370+ return _("Meets the Debian Free Software Guidelines itself "
1371+ "but requires additional non-free software to work")
1372+ elif component == "non-free":
1373+ return _("Non-free since it is either restricted "
1374+ "in use, redistribution or modification.")
1375
1376 def get_architecture(self):
1377 return apt.apt_pkg.config.find("Apt::Architecture")
1378
1379 def get_foreign_architectures(self):
1380+ return []
1381+ # Not yet in unstable
1382 import subprocess
1383 out = subprocess.Popen(['dpkg', '--print-foreign-architectures'],
1384 stdout=subprocess.PIPE).communicate()[0].rstrip('\n')
1385@@ -88,6 +93,63 @@
1386 return out.split(' ')
1387 return []
1388
1389+ def is_supported(self, cache, doc, pkgname):
1390+ # the doc does not by definition contain correct data regarding the
1391+ # section. Looking up in the cache seems just as fast/slow.
1392+ if pkgname in cache and cache[pkgname].candidate:
1393+ for origin in cache[pkgname].candidate.origins:
1394+ if (origin.origin == "Debian" and
1395+ origin.trusted and
1396+ origin.component == "main"):
1397+ return True
1398+ return False
1399+
1400+ def get_maintenance_status(self, cache, appname, pkgname, component, channelname):
1401+ """Return the maintenance status of a package."""
1402+ if not hasattr(cache, '_cache') or not pkgname:
1403+ return
1404+ try:
1405+ origins = cache[pkgname].candidate.origins
1406+ except KeyError, AttributeError:
1407+ return
1408+ else:
1409+ for origin in origins:
1410+ if (origin.origin == "Debian" and origin.trusted):
1411+ pkg_comp = origin.component
1412+ pkg_archive = origin.archive
1413+ break
1414+ else:
1415+ return
1416+ if pkg_comp in ("contrib", "non-free"):
1417+ if pkg_archive == "oldstable":
1418+ return _("Debian does not provide critical updates.")
1419+ else:
1420+ return _("Debian does not provide critical updates. "
1421+ "Some updates may be provided by the developers "
1422+ "of %s and redistributed by Debian.") % appname
1423+ elif pkg_comp == "main":
1424+ if pkg_archive == "stable":
1425+ return _("Debian provides critical updates for %s.") % appname
1426+ elif pkg_archive == "oldstable":
1427+ return _("Debian only provides updates for %s during "
1428+ "a transition phase. "
1429+ "Please consider upgrading to a later stable "
1430+ "release of Debian.") % appname
1431+ elif pkg_archive == "testing":
1432+ return _("Debian provides critical updates for %s. But "
1433+ "updates could be delayed or skipped.") % appname
1434+ elif pkg_archive == "unstable":
1435+ return _("Debian doens't provides critical updates "
1436+ "for %s") % appname
1437+ return
1438+
1439+ def get_supported_query(self):
1440+ import xapian
1441+ query1 = xapian.Query("XOL"+"Debian")
1442+ query2 = xapian.Query("XOC"+"main")
1443+ return xapian.Query(xapian.Query.OP_AND, query1, query2)
1444+
1445+
1446 if __name__ == "__main__":
1447 cache = apt.Cache()
1448 print(cache.get_maintenance_status(cache, "synaptic app", "synaptic", "main", None))
1449
1450=== modified file 'softwarecenter/distro/Ubuntu.py'
1451--- softwarecenter/distro/Ubuntu.py 2011-10-05 00:58:57 +0000
1452+++ softwarecenter/distro/Ubuntu.py 2011-10-27 07:59:24 +0000
1453@@ -56,6 +56,9 @@
1454 # FIXME: does that make sense?!?
1455 REVIEW_STATS_URL = REVIEWS_SERVER+"/review-stats"
1456
1457+ # Starting point for Ubuntu app developers
1458+ DEVELOPER_URL = "http://developer.ubuntu.com/"
1459+
1460 def get_app_name(self):
1461 return _("Ubuntu Software Center")
1462
1463@@ -128,6 +131,9 @@
1464 query2 = xapian.Query(xapian.Query.OP_OR, query2a, query2b)
1465 return xapian.Query(xapian.Query.OP_AND, query1, query2)
1466
1467+ def get_supported_filter_name(self):
1468+ return _("Canonical-Maintained Software")
1469+
1470 def get_maintenance_status(self, cache, appname, pkgname, component, channelname):
1471 # try to figure out the support dates of the release and make
1472 # sure to look only for stuff in "Ubuntu" and "distro_codename"
1473
1474=== modified file 'softwarecenter/distro/__init__.py'
1475--- softwarecenter/distro/__init__.py 2011-10-25 18:38:08 +0000
1476+++ softwarecenter/distro/__init__.py 2011-10-27 07:59:24 +0000
1477@@ -19,12 +19,15 @@
1478
1479 import logging
1480 import os
1481-import subprocess
1482
1483 from gettext import gettext as _
1484+import lsb_release
1485
1486 from softwarecenter.utils import UnimplementedError, utf8
1487
1488+log = logging.getLogger(__name__)
1489+
1490+
1491 class Distro(object):
1492 """ abstract base class for a distribution """
1493
1494@@ -37,6 +40,16 @@
1495 REVIEW_SUMMARY_STARS_BASE_PATH = "/usr/share/software-center/images/review-summary"
1496 REVIEWS_SERVER = os.environ.get("SOFTWARE_CENTER_REVIEWS_HOST") or "http://localhost:8000"
1497
1498+ # You need to set this var to enable purchases
1499+ PURCHASE_APP_URL = ""
1500+
1501+ # Point developers to a web page
1502+ DEVELOPER_URL = ""
1503+
1504+ def __init__(self, lsb_info):
1505+ """Return a new generic Distro instance."""
1506+ self.lsb_info = lsb_info
1507+
1508 def get_app_name(self):
1509 """
1510 The name of the application (as displayed in the main window and
1511@@ -66,9 +79,8 @@
1512 return os.environ["SOFTWARE_CENTER_DISTRO_CODENAME"]
1513 # normal behavior
1514 if not hasattr(self, "_distro_code_name"):
1515- self._distro_code_name = subprocess.Popen(
1516- ["lsb_release","-c","-s"],
1517- stdout=subprocess.PIPE).communicate()[0].strip()
1518+ self._distro_code_name = \
1519+ lsb_release.get_distro_information()["CODENAME"]
1520 return self._distro_code_name
1521
1522 def get_maintenance_status(self, cache, appname, pkgname, component, channelname):
1523@@ -89,6 +101,9 @@
1524 import xapian
1525 return xapian.Query()
1526
1527+ def get_supported_filter_name(self):
1528+ return _("Supported Software")
1529+
1530 def get_install_warning_text(self, cache, pkg, appname, depends):
1531 primary = utf8(_("To install %s, these items must be removed:")) % utf8(appname)
1532 button_text = _("Install Anyway")
1533@@ -140,15 +155,14 @@
1534
1535
1536 def _get_distro():
1537- distro_id = subprocess.Popen(["lsb_release","-i","-s"],
1538- stdout=subprocess.PIPE).communicate()[0]
1539- distro_id = distro_id.strip().replace(' ', '')
1540- logging.getLogger("softwarecenter.distro").debug("get_distro: '%s'" % distro_id)
1541+ lsb_info = lsb_release.get_distro_information()
1542+ distro_id = lsb_info["ID"]
1543+ log.debug("get_distro: '%s'", distro_id)
1544 # start with a import, this gives us only a softwarecenter module
1545 module = __import__(distro_id, globals(), locals(), [], -1)
1546 # get the right class and instanciate it
1547 distro_class = getattr(module, distro_id)
1548- instance = distro_class()
1549+ instance = distro_class(lsb_info)
1550 return instance
1551
1552 def get_distro():
1553
1554=== modified file 'softwarecenter/ui/gtk3/app.py'
1555--- softwarecenter/ui/gtk3/app.py 2011-10-20 01:52:42 +0000
1556+++ softwarecenter/ui/gtk3/app.py 2011-10-27 07:59:24 +0000
1557@@ -342,10 +342,28 @@
1558 self.config = get_config()
1559 self.restore_state()
1560
1561+ # Adapt menu entries
1562+ supported_menuitem = self.builder.get_object("menuitem_view_supported_only")
1563+ supported_menuitem.set_label(self.distro.get_supported_filter_name())
1564+ file_menu = self.builder.get_object("menu1")
1565+
1566+ if not self.distro.DEVELOPER_URL:
1567+ help_menu = self.builder.get_object("menu_help")
1568+ developer_separator = self.builder.get_object("separator_developer")
1569+ help_menu.remove(developer_separator)
1570+ developer_menuitem = self.builder.get_object("menuitem_developer")
1571+ help_menu.remove(developer_menuitem)
1572+
1573+ # Check if oneconf is available
1574+ och = get_oneconf_handler()
1575+ if not och:
1576+ file_menu.remove(self.builder.get_object("menuitem_sync_between_computers"))
1577+
1578 # run s-c-agent update
1579- if options.disable_buy:
1580- file_menu = self.builder.get_object("menu1")
1581+ if options.disable_buy or not self.distro.PURCHASE_APP_URL:
1582 file_menu.remove(self.builder.get_object("menuitem_reinstall_purchases"))
1583+ if not (options.enable_lp or och):
1584+ file_menu.remove(self.builder.get_object("separator_login"))
1585 else:
1586 sc_agent_update = os.path.join(
1587 self.datadir, "update-software-center-agent")
1588@@ -355,9 +373,6 @@
1589 GObject.child_watch_add(
1590 pid, self._on_update_software_center_agent_finished)
1591
1592- if options.disable_buy and not options.enable_lp:
1593- file_menu.remove(self.builder.get_object("separator_login"))
1594-
1595 # TODO: Remove the following two lines once we have remove repository
1596 # support in aptdaemon (see LP: #723911)
1597 file_menu = self.builder.get_object("menu1")
1598@@ -1031,8 +1046,8 @@
1599 GObject.timeout_add_seconds(1, lambda p: p.poll() == None, p)
1600
1601 def on_menuitem_developer_activate(self, menuitem):
1602- webbrowser.open("http://developer.ubuntu.com/")
1603-
1604+ webbrowser.open(self.distro.DEVELOPER_URL)
1605+
1606 def _ask_and_repair_broken_cache(self):
1607 # wait until the window window is available
1608 if self.window_main.props.visible == False:
1609
1610=== modified file 'softwarecenter/ui/gtk3/panes/installedpane.py'
1611--- softwarecenter/ui/gtk3/panes/installedpane.py 2011-10-06 02:35:27 +0000
1612+++ softwarecenter/ui/gtk3/panes/installedpane.py 2011-10-27 07:59:24 +0000
1613@@ -132,8 +132,9 @@
1614
1615 # Start OneConf
1616 self.oneconf_handler = get_oneconf_handler(self.oneconf_viewpickler)
1617- self.oneconf_handler.connect('show-oneconf-changed', self._show_oneconf_changed)
1618- self.oneconf_handler.connect('last-time-sync-changed', self._last_time_sync_oneconf_changed)
1619+ if self.oneconf_handler:
1620+ self.oneconf_handler.connect('show-oneconf-changed', self._show_oneconf_changed)
1621+ self.oneconf_handler.connect('last-time-sync-changed', self._last_time_sync_oneconf_changed)
1622
1623 # OneConf pane
1624 self.computerpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
1625
1626=== modified file 'softwarecenter/ui/gtk3/views/appdetailsview_gtk.py'
1627--- softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-10-20 16:08:23 +0000
1628+++ softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-10-27 07:59:24 +0000
1629@@ -1051,7 +1051,8 @@
1630 self.reviews.connect("more-reviews-clicked", self._on_more_reviews_clicked)
1631 self.reviews.connect("different-review-language-clicked", self._on_reviews_in_different_language_clicked)
1632 self.reviews.connect("review-sort-changed", self._on_review_sort_method_changed)
1633- vb.pack_start(self.reviews, False, False, 0)
1634+ if get_distro().REVIEWS_SERVER:
1635+ vb.pack_start(self.reviews, False, False, 0)
1636
1637 self.show_all()
1638