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
=== modified file 'data/ui/gtk3/SoftwareCenter.ui'
--- data/ui/gtk3/SoftwareCenter.ui 2011-10-08 17:15:20 +0000
+++ data/ui/gtk3/SoftwareCenter.ui 2011-10-27 07:59:24 +0000
@@ -470,7 +470,7 @@
470 </object>470 </object>
471 </child>471 </child>
472 <child>472 <child>
473 <object class="GtkSeparatorMenuItem" id="menuitem3">473 <object class="GtkSeparatorMenuItem" id="separator_developer">
474 <property name="visible">True</property>474 <property name="visible">True</property>
475 <property name="can_focus">False</property>475 <property name="can_focus">False</property>
476 </object>476 </object>
477477
=== modified file 'softwarecenter/backend/channel_impl/aptchannels.py'
--- softwarecenter/backend/channel_impl/aptchannels.py 2011-10-11 18:44:41 +0000
+++ softwarecenter/backend/channel_impl/aptchannels.py 2011-10-27 07:59:24 +0000
@@ -266,7 +266,7 @@
266 installed_only=installed_only))266 installed_only=installed_only))
267267
268 # always display the partner channel, even if its source is not enabled 268 # always display the partner channel, even if its source is not enabled
269 if not partner_channel:269 if not partner_channel and distro_channel_name == "Ubuntu":
270 partner_channel = SoftwareChannel("Partner archive",270 partner_channel = SoftwareChannel("Partner archive",
271 "Canonical",271 "Canonical",
272 "partner", 272 "partner",
@@ -285,7 +285,8 @@
285 channels.append(dist_channel)285 channels.append(dist_channel)
286 if partner_channel is not None:286 if partner_channel is not None:
287 channels.append(partner_channel)287 channels.append(partner_channel)
288 channels.append(for_purchase_channel)288 if get_distro().PURCHASE_APP_URL:
289 channels.append(for_purchase_channel)
289 if new_apps_channel is not None:290 if new_apps_channel is not None:
290 channels.append(new_apps_channel)291 channels.append(new_apps_channel)
291 channels.extend(ppa_channels)292 channels.extend(ppa_channels)
292293
=== added directory 'softwarecenter/backend/oneconfhandler'
=== removed file 'softwarecenter/backend/oneconfhandler.py'
--- softwarecenter/backend/oneconfhandler.py 2011-09-26 14:34:05 +0000
+++ softwarecenter/backend/oneconfhandler.py 1970-01-01 00:00:00 +0000
@@ -1,206 +0,0 @@
1# -*- coding: utf-8 -*-
2# Copyright (C) 2011 Canonical
3#
4# Authors:
5# Didier Roche
6#
7# This program is free software; you can redistribute it and/or modify it under
8# the terms of the GNU General Public License as published by the Free Software
9# Foundation; version 3.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20
21from oneconf.dbusconnect import DbusConnect
22from oneconf.enums import MIN_TIME_WITHOUT_ACTIVITY
23
24from softwarecenter.backend.login_sso import get_sso_backend
25from softwarecenter.backend.restfulclient import get_ubuntu_sso_backend
26from softwarecenter.utils import clear_token_from_ubuntu_sso
27
28import datetime
29from gi.repository import GObject
30import logging
31
32from gettext import gettext as _
33
34LOG = logging.getLogger(__name__)
35
36class OneConfHandler(GObject.GObject):
37
38 __gsignals__ = {
39 "show-oneconf-changed" : (GObject.SIGNAL_RUN_LAST,
40 GObject.TYPE_NONE,
41 (GObject.TYPE_PYOBJECT,),
42 ),
43 "last-time-sync-changed" : (GObject.SIGNAL_RUN_LAST,
44 GObject.TYPE_NONE,
45 (GObject.TYPE_PYOBJECT,),
46 ),
47 }
48
49
50 def __init__(self, oneconfviewpickler):
51 '''Controller of the installed pane'''
52
53 LOG.debug("OneConf Handler init")
54 super(OneConfHandler, self).__init__()
55
56 # FIXME: should be an enum common to OneConf and here
57 self.appname = "Ubuntu Software Center"
58
59 # OneConf stuff
60 self.oneconf = DbusConnect()
61 self.oneconf.hosts_dbus_object.connect_to_signal('hostlist_changed',
62 self.refresh_hosts)
63 self.oneconf.hosts_dbus_object.connect_to_signal('packagelist_changed',
64 self._on_store_packagelist_changed)
65 self.oneconf.hosts_dbus_object.connect_to_signal('latestsync_changed',
66 self.on_new_latest_oneconf_sync_timestamp)
67 self.already_registered_hostids = []
68 self.is_current_registered = False
69
70 self.oneconfviewpickler = oneconfviewpickler
71
72 # refresh host list
73 self._refreshing_hosts = False
74 GObject.timeout_add_seconds(MIN_TIME_WITHOUT_ACTIVITY, self.get_latest_oneconf_sync)
75 GObject.idle_add(self.refresh_hosts)
76
77 def refresh_hosts(self):
78 """refresh hosts list in the panel view"""
79 LOG.debug('oneconf: refresh hosts')
80
81 # this function can be called in different threads
82 if self._refreshing_hosts:
83 return
84 self._refreshing_hosts = True
85
86 #view_switcher = self.app.view_switcher
87 #model = view_switcher.get_model()
88 #previous_iter = model.installed_iter
89
90 all_hosts = self.oneconf.get_all_hosts()
91 for hostid in all_hosts:
92 current, hostname, share_inventory = all_hosts[hostid]
93 if not hostid in self.already_registered_hostids and not current:
94 self.oneconfviewpickler.register_computer(hostid, hostname)
95 self.already_registered_hostids.append(hostid)
96 if current:
97 is_current_registered = share_inventory
98
99 # ensure we are logged to ubuntu sso to activate the view
100 if self.is_current_registered != is_current_registered:
101 self.sync_between_computers(is_current_registered)
102
103 self._refreshing_hosts = False
104
105 def get_latest_oneconf_sync(self):
106 '''Get latest sync state in OneConf.
107
108 This function is also the "ping" letting OneConf service alive'''
109 LOG.debug("get latest sync state")
110 timestamp = self.oneconf.get_last_sync_date()
111 self.on_new_latest_oneconf_sync_timestamp(timestamp)
112 return True
113
114 def on_new_latest_oneconf_sync_timestamp(self, timestamp):
115 '''Callback computing the right message for latest sync time'''
116 try:
117 last_sync = datetime.datetime.fromtimestamp(float(timestamp))
118 today = datetime.datetime.strptime(str(datetime.date.today()), '%Y-%m-%d')
119 the_daybefore = today - datetime.timedelta(days=1)
120
121 if last_sync > today:
122 msg = _("Last sync %s") % last_sync.strftime('%H:%M')
123 elif last_sync < today and last_sync > the_daybefore:
124 msg = _("Last sync yesterday %s") % last_sync.strftime('%H:%M')
125 else:
126 msg = _("Last sync %s") % last_sync.strftime('%Y-%m-%d %H:%M')
127 except (TypeError, ValueError):
128 msg = _("To sync with another computer, choose “Sync Between Computers” from that computer.")
129 self.emit("last-time-sync-changed", msg)
130
131 def _share_inventory(self, share_inventory):
132 '''set oneconf state and emit signal for installed view to show or not oneconf'''
133
134 if share_inventory == self.is_current_registered:
135 return
136 self.is_current_registered = share_inventory
137 LOG.debug("change share inventory state to %s", share_inventory)
138 self.oneconf.set_share_inventory(share_inventory)
139 self.get_latest_oneconf_sync()
140 self.emit("show-oneconf-changed", share_inventory)
141
142 def sync_between_computers(self, sync_on):
143 '''toggle the sync on and off if needed between computers'''
144 LOG.debug("Toggle sync between computers: %s", sync_on)
145
146 if sync_on:
147 self._try_login()
148 else:
149 self._share_inventory(False)
150
151 def _on_store_packagelist_changed(self, hostid):
152 '''pass the message to the view controller'''
153 self.oneconfviewpickler.store_packagelist_changed(hostid)
154
155
156 # SSO login part
157
158 def _try_login(self):
159 '''Try to get the credential or login on ubuntu sso'''
160 logging.debug("OneConf login()")
161 help_text = _("With multiple Ubuntu computers, you can publish their inventories online to compare the software installed on each\n"
162 "No-one else will be able to see what you have installed.")
163 self.sso = get_sso_backend(0,
164 self.appname, help_text)
165 self.sso.connect("login-successful", self._maybe_login_successful)
166 self.sso.connect("login-canceled", self._login_canceled)
167 self.sso.login_or_register()
168
169 def _login_canceled(self, sso):
170 self._share_inventory(False)
171
172 def _maybe_login_successful(self, sso, oauth_result):
173 """ called after we have the token, then we go and figure out our name """
174 logging.debug("_maybe_login_successful")
175 token = oauth_result
176 self.ssoapi = get_ubuntu_sso_backend(token)
177 self.ssoapi.connect("whoami", self._whoami_done)
178 self.ssoapi.connect("error", self._whoami_error)
179 self.ssoapi.whoami()
180
181 def _whoami_done(self, ssologin, result):
182 logging.debug("_whoami_done")
183 self._share_inventory(True)
184
185 def _whoami_error(self, ssologin, e):
186 logging.error("whoami error '%s'" % e)
187 # HACK: clear the token from the keyring assuming that it expired
188 # or got deauthorized by the user on the website
189 # this really should be done by ubuntu-sso-client itself
190 import lazr.restfulclient.errors
191 errortype = lazr.restfulclient.errors.HTTPError
192 if (type(e) == errortype):
193 LOG.warn("authentication error, resetting token and retrying")
194 clear_token_from_ubuntu_sso(self.appname)
195 self._share_inventory(False)
196 return
197
198
199# singleton
200oneconf_handler = None
201def get_oneconf_handler(oneconfviewpickler = None):
202 global oneconf_handler
203 if oneconf_handler is None and oneconfviewpickler:
204 oneconf_handler = OneConfHandler(oneconfviewpickler)
205 return oneconf_handler
206
2070
=== added file 'softwarecenter/backend/oneconfhandler/__init__.py'
--- softwarecenter/backend/oneconfhandler/__init__.py 1970-01-01 00:00:00 +0000
+++ softwarecenter/backend/oneconfhandler/__init__.py 2011-10-27 07:59:24 +0000
@@ -0,0 +1,32 @@
1# -*- coding: utf-8 -*-
2# Copyright (C) 2011 Canonical
3#
4# Authors:
5# Didier Roche
6#
7# This program is free software; you can redistribute it and/or modify it under
8# the terms of the GNU General Public License as published by the Free Software
9# Foundation; version 3.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20
21# singleton
22oneconf_handler = None
23def get_oneconf_handler(oneconfviewpickler = None):
24 global oneconf_handler
25 try:
26 from softwarecenter.backend.oneconfhandler.core import OnceConfHandler
27 except ImportError:
28 return None
29 if oneconf_handler is None and oneconfviewpickler:
30 oneconf_handler = OneConfHandler(oneconfviewpickler)
31 return oneconf_handler
32
033
=== added file 'softwarecenter/backend/oneconfhandler/core.py'
--- softwarecenter/backend/oneconfhandler/core.py 1970-01-01 00:00:00 +0000
+++ softwarecenter/backend/oneconfhandler/core.py 2011-10-27 07:59:24 +0000
@@ -0,0 +1,196 @@
1# -*- coding: utf-8 -*-
2# Copyright (C) 2011 Canonical
3#
4# Authors:
5# Didier Roche
6#
7# This program is free software; you can redistribute it and/or modify it under
8# the terms of the GNU General Public License as published by the Free Software
9# Foundation; version 3.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20
21from oneconf.dbusconnect import DbusConnect
22from oneconf.enums import MIN_TIME_WITHOUT_ACTIVITY
23
24from softwarecenter.backend.login_sso import get_sso_backend
25from softwarecenter.backend.restfulclient import get_ubuntu_sso_backend
26from softwarecenter.utils import clear_token_from_ubuntu_sso
27
28import datetime
29from gi.repository import GObject
30import logging
31
32from gettext import gettext as _
33
34LOG = logging.getLogger(__name__)
35
36class OneConfHandler(GObject.GObject):
37
38 __gsignals__ = {
39 "show-oneconf-changed" : (GObject.SIGNAL_RUN_LAST,
40 GObject.TYPE_NONE,
41 (GObject.TYPE_PYOBJECT,),
42 ),
43 "last-time-sync-changed" : (GObject.SIGNAL_RUN_LAST,
44 GObject.TYPE_NONE,
45 (GObject.TYPE_PYOBJECT,),
46 ),
47 }
48
49
50 def __init__(self, oneconfviewpickler):
51 '''Controller of the installed pane'''
52
53 LOG.debug("OneConf Handler init")
54 super(OneConfHandler, self).__init__()
55
56 # FIXME: should be an enum common to OneConf and here
57 self.appname = "Ubuntu Software Center"
58
59 # OneConf stuff
60 self.oneconf = DbusConnect()
61 self.oneconf.hosts_dbus_object.connect_to_signal('hostlist_changed',
62 self.refresh_hosts)
63 self.oneconf.hosts_dbus_object.connect_to_signal('packagelist_changed',
64 self._on_store_packagelist_changed)
65 self.oneconf.hosts_dbus_object.connect_to_signal('latestsync_changed',
66 self.on_new_latest_oneconf_sync_timestamp)
67 self.already_registered_hostids = []
68 self.is_current_registered = False
69
70 self.oneconfviewpickler = oneconfviewpickler
71
72 # refresh host list
73 self._refreshing_hosts = False
74 GObject.timeout_add_seconds(MIN_TIME_WITHOUT_ACTIVITY, self.get_latest_oneconf_sync)
75 GObject.idle_add(self.refresh_hosts)
76
77 def refresh_hosts(self):
78 """refresh hosts list in the panel view"""
79 LOG.debug('oneconf: refresh hosts')
80
81 # this function can be called in different threads
82 if self._refreshing_hosts:
83 return
84 self._refreshing_hosts = True
85
86 #view_switcher = self.app.view_switcher
87 #model = view_switcher.get_model()
88 #previous_iter = model.installed_iter
89
90 all_hosts = self.oneconf.get_all_hosts()
91 for hostid in all_hosts:
92 current, hostname, share_inventory = all_hosts[hostid]
93 if not hostid in self.already_registered_hostids and not current:
94 self.oneconfviewpickler.register_computer(hostid, hostname)
95 self.already_registered_hostids.append(hostid)
96 if current:
97 is_current_registered = share_inventory
98
99 # ensure we are logged to ubuntu sso to activate the view
100 if self.is_current_registered != is_current_registered:
101 self.sync_between_computers(is_current_registered)
102
103 self._refreshing_hosts = False
104
105 def get_latest_oneconf_sync(self):
106 '''Get latest sync state in OneConf.
107
108 This function is also the "ping" letting OneConf service alive'''
109 LOG.debug("get latest sync state")
110 timestamp = self.oneconf.get_last_sync_date()
111 self.on_new_latest_oneconf_sync_timestamp(timestamp)
112 return True
113
114 def on_new_latest_oneconf_sync_timestamp(self, timestamp):
115 '''Callback computing the right message for latest sync time'''
116 try:
117 last_sync = datetime.datetime.fromtimestamp(float(timestamp))
118 today = datetime.datetime.strptime(str(datetime.date.today()), '%Y-%m-%d')
119 the_daybefore = today - datetime.timedelta(days=1)
120
121 if last_sync > today:
122 msg = _("Last sync %s") % last_sync.strftime('%H:%M')
123 elif last_sync < today and last_sync > the_daybefore:
124 msg = _("Last sync yesterday %s") % last_sync.strftime('%H:%M')
125 else:
126 msg = _("Last sync %s") % last_sync.strftime('%Y-%m-%d %H:%M')
127 except (TypeError, ValueError):
128 msg = _("To sync with another computer, choose “Sync Between Computers” from that computer.")
129 self.emit("last-time-sync-changed", msg)
130
131 def _share_inventory(self, share_inventory):
132 '''set oneconf state and emit signal for installed view to show or not oneconf'''
133
134 if share_inventory == self.is_current_registered:
135 return
136 self.is_current_registered = share_inventory
137 LOG.debug("change share inventory state to %s", share_inventory)
138 self.oneconf.set_share_inventory(share_inventory)
139 self.get_latest_oneconf_sync()
140 self.emit("show-oneconf-changed", share_inventory)
141
142 def sync_between_computers(self, sync_on):
143 '''toggle the sync on and off if needed between computers'''
144 LOG.debug("Toggle sync between computers: %s", sync_on)
145
146 if sync_on:
147 self._try_login()
148 else:
149 self._share_inventory(False)
150
151 def _on_store_packagelist_changed(self, hostid):
152 '''pass the message to the view controller'''
153 self.oneconfviewpickler.store_packagelist_changed(hostid)
154
155
156 # SSO login part
157
158 def _try_login(self):
159 '''Try to get the credential or login on ubuntu sso'''
160 logging.debug("OneConf login()")
161 help_text = _("With multiple Ubuntu computers, you can publish their inventories online to compare the software installed on each\n"
162 "No-one else will be able to see what you have installed.")
163 self.sso = get_sso_backend(0,
164 self.appname, help_text)
165 self.sso.connect("login-successful", self._maybe_login_successful)
166 self.sso.connect("login-canceled", self._login_canceled)
167 self.sso.login_or_register()
168
169 def _login_canceled(self, sso):
170 self._share_inventory(False)
171
172 def _maybe_login_successful(self, sso, oauth_result):
173 """ called after we have the token, then we go and figure out our name """
174 logging.debug("_maybe_login_successful")
175 token = oauth_result
176 self.ssoapi = get_ubuntu_sso_backend(token)
177 self.ssoapi.connect("whoami", self._whoami_done)
178 self.ssoapi.connect("error", self._whoami_error)
179 self.ssoapi.whoami()
180
181 def _whoami_done(self, ssologin, result):
182 logging.debug("_whoami_done")
183 self._share_inventory(True)
184
185 def _whoami_error(self, ssologin, e):
186 logging.error("whoami error '%s'" % e)
187 # HACK: clear the token from the keyring assuming that it expired
188 # or got deauthorized by the user on the website
189 # this really should be done by ubuntu-sso-client itself
190 import lazr.restfulclient.errors
191 errortype = lazr.restfulclient.errors.HTTPError
192 if (type(e) == errortype):
193 LOG.warn("authentication error, resetting token and retrying")
194 clear_token_from_ubuntu_sso(self.appname)
195 self._share_inventory(False)
196 return
0197
=== modified file 'softwarecenter/backend/piston/rnrclient.py'
--- softwarecenter/backend/piston/rnrclient.py 2011-08-09 08:47:43 +0000
+++ softwarecenter/backend/piston/rnrclient.py 2011-10-27 07:59:24 +0000
@@ -52,7 +52,7 @@
52 LOG.error("need python-piston-mini client\n"52 LOG.error("need python-piston-mini client\n"
53 "available in natty or from:\n"53 "available in natty or from:\n"
54 " ppa:software-store-developers/daily-build ")54 " ppa:software-store-developers/daily-build ")
55 sys.exit(1)55 raise
5656
5757
58if __name__ == "__main__":58if __name__ == "__main__":
5959
=== added directory 'softwarecenter/backend/reviews'
=== renamed file 'softwarecenter/backend/reviews.py' => 'softwarecenter/backend/reviews/__init__.py'
--- softwarecenter/backend/reviews.py 2011-10-13 13:39:21 +0000
+++ softwarecenter/backend/reviews/__init__.py 2011-10-27 07:59:24 +0000
@@ -53,8 +53,6 @@
53 from StringIO import StringIO53 from StringIO import StringIO
54 from urllib import quote_plus54 from urllib import quote_plus
5555
56from softwarecenter.backend.piston.rnrclient import RatingsAndReviewsAPI
57from softwarecenter.backend.piston.rnrclient_pristine import ReviewDetails
58from softwarecenter.db.categories import CategoriesParser56from softwarecenter.db.categories import CategoriesParser
59from softwarecenter.db.database import Application, StoreDatabase57from softwarecenter.db.database import Application, StoreDatabase
60import softwarecenter.distro58import softwarecenter.distro
@@ -77,7 +75,7 @@
7775
78from softwarecenter.netstatus import network_state_is_connected76from softwarecenter.netstatus import network_state_is_connected
7977
80from spawn_helper import SpawnHelper78from softwarecenter.backend.spawn_helper import SpawnHelper
8179
82LOG = logging.getLogger(__name__)80LOG = logging.getLogger(__name__)
8381
@@ -100,7 +98,7 @@
100 USEFULNESS_CACHE = {}98 USEFULNESS_CACHE = {}
101 99
102 def __init__(self, try_server=False):100 def __init__(self, try_server=False):
103 self.rnrclient = RatingsAndReviewsAPI()101 #self.rnrclient = RatingsAndReviewsAPI()
104 fname = "usefulness.p"102 fname = "usefulness.p"
105 self.USEFULNESS_CACHE_FILE = os.path.join(SOFTWARE_CENTER_CACHE_DIR,103 self.USEFULNESS_CACHE_FILE = os.path.join(SOFTWARE_CENTER_CACHE_DIR,
106 fname)104 fname)
@@ -438,43 +436,12 @@
438 applist.append(db.get_pkgname(doc))436 applist.append(db.get_pkgname(doc))
439 return applist437 return applist
440438
441 # writing new reviews spawns external helper
442 # FIXME: instead of the callback we should add proper gobject signals
443 def spawn_write_new_review_ui(self, translated_app, version, iconname, 439 def spawn_write_new_review_ui(self, translated_app, version, iconname,
444 origin, parent_xid, datadir, callback):440 origin, parent_xid, datadir, callback):
445 """ this spawns the UI for writing a new review and441 """Spawn the UI for writing a new review and adds it automatically
446 adds it automatically to the reviews DB """442 to the reviews DB.
447 app = translated_app.get_untranslated_app(self.db)
448 cmd = [os.path.join(datadir, RNRApps.SUBMIT_REVIEW),
449 "--pkgname", app.pkgname,
450 "--iconname", iconname,
451 "--parent-xid", "%s" % parent_xid,
452 "--version", version,
453 "--origin", origin,
454 "--datadir", datadir,
455 ]
456 if app.appname:
457 # needs to be (utf8 encoded) str, otherwise call fails
458 cmd += ["--appname", utf8(app.appname)]
459 spawn_helper = SpawnHelper(format="json")
460 spawn_helper.connect(
461 "data-available", self._on_submit_review_data, app, callback)
462 spawn_helper.run(cmd)
463
464 def _on_submit_review_data(self, spawn_helper, review_json, app, callback):
465 """ called when submit_review finished, when the review was send
466 successfully the callback is triggered with the new reviews
467 """443 """
468 LOG.debug("_on_submit_review_data")444 pass
469 # read stdout from submit_review
470 review = ReviewDetails.from_dict(review_json)
471 # FIXME: ideally this would be stored in ubuntu-sso-client
472 # but it dosn't so we store it here
473 save_person_to_config(review.reviewer_username)
474 if not app in self._reviews:
475 self._reviews[app] = []
476 self._reviews[app].insert(0, Review.from_piston_mini_client(review))
477 callback(app, self._reviews[app])
478445
479 def spawn_report_abuse_ui(self, review_id, parent_xid, datadir, callback):446 def spawn_report_abuse_ui(self, review_id, parent_xid, datadir, callback):
480 """ this spawns the UI for reporting a review as inappropriate447 """ this spawns the UI for reporting a review as inappropriate
@@ -482,296 +449,20 @@
482 operation is complete it will call callback with the updated449 operation is complete it will call callback with the updated
483 review list450 review list
484 """451 """
485 cmd = [os.path.join(datadir, RNRApps.REPORT_REVIEW), 452 pass
486 "--review-id", review_id,
487 "--parent-xid", "%s" % parent_xid,
488 "--datadir", datadir,
489 ]
490 spawn_helper = SpawnHelper("json")
491 spawn_helper.connect("exited",
492 self._on_report_abuse_finished,
493 review_id, callback)
494 spawn_helper.run(cmd)
495
496 def _on_report_abuse_finished(self, spawn_helper, exitcode, review_id, callback):
497 """ called when report_absuse finished """
498 LOG.debug("hide id %s " % review_id)
499 if exitcode == 0:
500 for (app, reviews) in self._reviews.items():
501 for review in reviews:
502 if str(review.id) == str(review_id):
503 # remove the one we don't want to see anymore
504 self._reviews[app].remove(review)
505 callback(app, self._reviews[app], None, 'remove', review)
506 break
507
508453
509 def spawn_submit_usefulness_ui(self, review_id, is_useful, parent_xid, datadir, callback):454 def spawn_submit_usefulness_ui(self, review_id, is_useful, parent_xid, datadir, callback):
510 cmd = [os.path.join(datadir, RNRApps.SUBMIT_USEFULNESS), 455 """Spawn a helper to submit a usefulness vote."""
511 "--review-id", "%s" % review_id,456 pass
512 "--is-useful", "%s" % int(is_useful),
513 "--parent-xid", "%s" % parent_xid,
514 "--datadir", datadir,
515 ]
516 spawn_helper = SpawnHelper(format="none")
517 spawn_helper.connect("exited",
518 self._on_submit_usefulness_finished,
519 review_id, is_useful, callback)
520 spawn_helper.connect("error",
521 self._on_submit_usefulness_error,
522 review_id, callback)
523 spawn_helper.run(cmd)
524
525 def _on_submit_usefulness_finished(self, spawn_helper, res, review_id, is_useful, callback):
526 """ called when report_usefulness finished """
527 # "Created", "Updated", "Not modified" -
528 # once lp:~mvo/rnr-server/submit-usefulness-result-strings makes it
529 response = spawn_helper._stdout
530 if response == '"Not modified"':
531 self._on_submit_usefulness_error(spawn_helper, response, review_id, callback)
532 return
533
534 LOG.debug("usefulness id %s " % review_id)
535 useful_votes = UsefulnessCache()
536 useful_votes.add_usefulness_vote(review_id, is_useful)
537 for (app, reviews) in self._reviews.items():
538 for review in reviews:
539 if str(review.id) == str(review_id):
540 # update usefulness, older servers do not send
541 # usefulness_{total,favorable} so we use getattr
542 review.usefulness_total = getattr(review, "usefulness_total", 0) + 1
543 if is_useful:
544 review.usefulness_favorable = getattr(review, "usefulness_favorable", 0) + 1
545 callback(app, self._reviews[app], useful_votes, 'replace', review)
546 break
547
548 def _on_submit_usefulness_error(self, spawn_helper, error_str, review_id, callback):
549 LOG.warn("submit usefulness id=%s failed with error: %s" %
550 (review_id, error_str))
551 for (app, reviews) in self._reviews.items():
552 for review in reviews:
553 if str(review.id) == str(review_id):
554 review.usefulness_submit_error = True
555 callback(app, self._reviews[app], None, 'replace', review)
556 break
557457
558 def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback):458 def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback):
559 cmd = [os.path.join(datadir, RNRApps.DELETE_REVIEW), 459 """Spawn a helper to delete a review."""
560 "--review-id", "%s" % review_id,460 pass
561 "--parent-xid", "%s" % parent_xid,461
562 "--datadir", datadir,
563 ]
564 spawn_helper = SpawnHelper(format="none")
565 spawn_helper.connect("exited",
566 self._on_delete_review_finished,
567 review_id, callback)
568 spawn_helper.connect("error", self._on_delete_review_error,
569 review_id, callback)
570 spawn_helper.run(cmd)
571
572 def _on_delete_review_finished(self, spawn_helper, res, review_id, callback):
573 """ called when delete_review finished"""
574 LOG.debug("delete id %s " % review_id)
575 for (app, reviews) in self._reviews.items():
576 for review in reviews:
577 if str(review.id) == str(review_id):
578 # remove the one we don't want to see anymore
579 self._reviews[app].remove(review)
580 callback(app, self._reviews[app], None, 'remove', review)
581 break
582
583 def _on_delete_review_error(self, spawn_helper, error_str, review_id, callback):
584 """called if delete review errors"""
585 LOG.warn("delete review id=%s failed with error: %s" % (review_id, error_str))
586 for (app, reviews) in self._reviews.items():
587 for review in reviews:
588 if str(review.id) == str(review_id):
589 review.delete_error = True
590 callback(app, self._reviews[app], action='replace',
591 single_review=review)
592 break
593
594
595 def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):462 def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):
596 """ this spawns the UI for writing a new review and463 """Spawn a helper to modify a review."""
597 adds it automatically to the reviews DB """464 pass
598 cmd = [os.path.join(datadir, RNRApps.MODIFY_REVIEW), 465
599 "--parent-xid", "%s" % parent_xid,
600 "--iconname", iconname,
601 "--datadir", "%s" % datadir,
602 "--review-id", "%s" % review_id,
603 ]
604 spawn_helper = SpawnHelper(format="json")
605 spawn_helper.connect("data-available",
606 self._on_modify_review_finished,
607 review_id, callback)
608 spawn_helper.connect("error", self._on_modify_review_error,
609 review_id, callback)
610 spawn_helper.run(cmd)
611
612 def _on_modify_review_finished(self, spawn_helper, review_json, review_id, callback):
613 """called when modify_review finished"""
614 LOG.debug("_on_modify_review_finished")
615 #review_json = spawn_helper._stdout
616 mod_review = ReviewDetails.from_dict(review_json)
617 for (app, reviews) in self._reviews.items():
618 for review in reviews:
619 if str(review.id) == str(review_id):
620 # remove the one we don't want to see anymore
621 self._reviews[app].remove(review)
622 new_review = Review.from_piston_mini_client(mod_review)
623 self._reviews[app].insert(0, new_review)
624 callback(app, self._reviews[app], action='replace',
625 single_review=new_review)
626 break
627
628 def _on_modify_review_error(self, spawn_helper, error_str, review_id, callback):
629 """called if modify review errors"""
630 LOG.debug("modify review id=%s failed with error: %s" % (review_id, error_str))
631 for (app, reviews) in self._reviews.items():
632 for review in reviews:
633 if str(review.id) == str(review_id):
634 review.modify_error = True
635 callback(app, self._reviews[app], action='replace',
636 single_review=review)
637 break
638
639
640# this code had several incernations:
641# - python threads, slow and full of latency (GIL)
642# - python multiprocesing, crashed when accessibility was turned on,
643# does not work in the quest session (#743020)
644# - GObject.spawn_async() looks good so far (using the SpawnHelper code)
645class ReviewLoaderSpawningRNRClient(ReviewLoader):
646 """ loader that uses multiprocessing to call rnrclient and
647 a glib timeout watcher that polls periodically for the
648 data
649 """
650
651 def __init__(self, cache, db, distro=None):
652 super(ReviewLoaderSpawningRNRClient, self).__init__(cache, db, distro)
653 cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
654 self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
655 cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
656 self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
657 self._reviews = {}
658
659 def _update_rnrclient_offline_state(self):
660 # this needs the lp:~mvo/piston-mini-client/offline-mode branch
661 self.rnrclient._offline_mode = not network_state_is_connected()
662
663 # reviews
664 def get_reviews(self, translated_app, callback, page=1,
665 language=None, sort=0):
666 """ public api, triggers fetching a review and calls callback
667 when its ready
668 """
669 # its fine to use the translated appname here, we only submit the
670 # pkgname to the server
671 app = translated_app
672 self._update_rnrclient_offline_state()
673 sort_method = self._review_sort_methods[sort]
674 if language is None:
675 language = self.language
676 # gather args for the helper
677 try:
678 origin = self.cache.get_origin(app.pkgname)
679 except:
680 # this can happen if e.g. the app has multiple origins, this
681 # will be handled later
682 origin = None
683 # special case for not-enabled PPAs
684 if not origin and self.db:
685 details = app.get_details(self.db)
686 ppa = details.ppaname
687 if ppa:
688 origin = "lp-ppa-%s" % ppa.replace("/", "-")
689 # if there is no origin, there is nothing to do
690 if not origin:
691 callback(app, [])
692 return
693 distroseries = self.distro.get_codename()
694 # run the command and add watcher
695 cmd = [os.path.join(softwarecenter.paths.datadir, PistonHelpers.GET_REVIEWS),
696 "--language", language,
697 "--origin", origin,
698 "--distroseries", distroseries,
699 "--pkgname", str(app.pkgname), # ensure its str, not unicode
700 "--page", str(page),
701 "--sort", sort_method,
702 ]
703 spawn_helper = SpawnHelper()
704 spawn_helper.connect(
705 "data-available", self._on_reviews_helper_data, app, callback)
706 spawn_helper.run(cmd)
707
708 def _on_reviews_helper_data(self, spawn_helper, piston_reviews, app, callback):
709 # convert into our review objects
710 reviews = []
711 for r in piston_reviews:
712 reviews.append(Review.from_piston_mini_client(r))
713 # add to our dicts and run callback
714 self._reviews[app] = reviews
715 callback(app, self._reviews[app])
716 return False
717
718 # stats
719 def refresh_review_stats(self, callback):
720 """ public api, refresh the available statistics """
721 try:
722 mtime = os.path.getmtime(self.REVIEW_STATS_CACHE_FILE)
723 days_delta = int((time.time() - mtime) // (24*60*60))
724 days_delta += 1
725 except OSError:
726 days_delta = 0
727 LOG.debug("refresh with days_delta: %s" % days_delta)
728 #origin = "any"
729 #distroseries = self.distro.get_codename()
730 cmd = [os.path.join(
731 softwarecenter.paths.datadir, PistonHelpers.GET_REVIEW_STATS),
732 # FIXME: the server currently has bug (#757695) so we
733 # can not turn this on just yet and need to use
734 # the old "catch-all" review-stats for now
735 #"--origin", origin,
736 #"--distroseries", distroseries,
737 ]
738 if days_delta:
739 cmd += ["--days-delta", str(days_delta)]
740 spawn_helper = SpawnHelper()
741 spawn_helper.connect("data-available", self._on_review_stats_data, callback)
742 spawn_helper.run(cmd)
743
744 def _on_review_stats_data(self, spawn_helper, piston_review_stats, callback):
745 """ process stdout from the helper """
746 review_stats = self.REVIEW_STATS_CACHE
747
748 if self._cache_version_old and self._server_has_histogram(piston_review_stats):
749 self.REVIEW_STATS_CACHE = {}
750 self.save_review_stats_cache_file()
751 self.refresh_review_stats(callback)
752 return
753
754 # convert to the format that s-c uses
755 for r in piston_review_stats:
756 s = ReviewStats(Application("", r.package_name))
757 s.ratings_average = float(r.ratings_average)
758 s.ratings_total = float(r.ratings_total)
759 if r.histogram:
760 s.rating_spread = json.loads(r.histogram)
761 else:
762 s.rating_spread = [0,0,0,0,0]
763 s.dampened_rating = calc_dr(s.rating_spread)
764 review_stats[s.app] = s
765 self.REVIEW_STATS_CACHE = review_stats
766 callback(review_stats)
767 self.save_review_stats_cache_file()
768
769 def _server_has_histogram(self, piston_review_stats):
770 '''check response from server to see if histogram is supported'''
771 supported = getattr(piston_review_stats[0], "histogram", False)
772 if not supported:
773 return False
774 return True
775466
776class ReviewLoaderJsonAsync(ReviewLoader):467class ReviewLoaderJsonAsync(ReviewLoader):
777 """ get json (or gzip compressed json) """468 """ get json (or gzip compressed json) """
@@ -871,7 +562,7 @@
871 return random.choice(self.LOREM.split("\n\n"))562 return random.choice(self.LOREM.split("\n\n"))
872 def _random_summary(self):563 def _random_summary(self):
873 return random.choice(self.SUMMARIES)564 return random.choice(self.SUMMARIES)
874 def get_reviews(self, application, callback, page=1, language=None):565 def get_reviews(self, application, callback, page=1, language=None, sort=0):
875 if not application in self._review_stats_cache:566 if not application in self._review_stats_cache:
876 self.get_review_stats(application)567 self.get_review_stats(application)
877 stats = self._review_stats_cache[application]568 stats = self._review_stats_cache[application]
@@ -1034,6 +725,26 @@
1034et ea rebum stet clita kasd gubergren no sea takimata sanctus est lorem725et ea rebum stet clita kasd gubergren no sea takimata sanctus est lorem
1035ipsum dolor sit amet"""726ipsum dolor sit amet"""
1036727
728
729class ReviewLoaderNull(ReviewLoader):
730
731 """A dummy review loader which just returns empty results."""
732
733 def __init__(self, cache, db):
734 self._review_stats_cache = {}
735 self._reviews_cache = {}
736
737 def get_reviews(self, application, callback, page=1, language=None, sort=0):
738 callback(application, [])
739
740 def get_review_stats(self, application):
741 return None
742
743 def refresh_review_stats(self, callback):
744 review_stats = []
745 callback(review_stats)
746
747
1037review_loader = None748review_loader = None
1038def get_review_loader(cache, db=None):749def get_review_loader(cache, db=None):
1039 """ 750 """
@@ -1050,7 +761,12 @@
1050 elif "SOFTWARE_CENTER_GIO_REVIEWS" in os.environ:761 elif "SOFTWARE_CENTER_GIO_REVIEWS" in os.environ:
1051 review_loader = ReviewLoaderJsonAsync(cache, db)762 review_loader = ReviewLoaderJsonAsync(cache, db)
1052 else:763 else:
1053 review_loader = ReviewLoaderSpawningRNRClient(cache, db)764 try:
765 from softwarecenter.backend.reviews.rnr import ReviewLoaderSpawningRNRClient
766 except ImportError:
767 review_loader = ReviewLoaderNull(cache, db)
768 else:
769 review_loader = ReviewLoaderSpawningRNRClient(cache, db)
1054 return review_loader770 return review_loader
1055771
1056if __name__ == "__main__":772if __name__ == "__main__":
1057773
=== added file 'softwarecenter/backend/reviews/rnr.py'
--- softwarecenter/backend/reviews/rnr.py 1970-01-01 00:00:00 +0000
+++ softwarecenter/backend/reviews/rnr.py 2011-10-27 07:59:24 +0000
@@ -0,0 +1,376 @@
1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2009 Canonical
4#
5# Authors:
6# Michael Vogt
7#
8# This program is free software; you can redistribute it and/or modify it under
9# the terms of the GNU General Public License as published by the Free Software
10# Foundation; version 3.
11#
12# This program is distributed in the hope that it will be useful, but WITHOUT
13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15# details.
16#
17# You should have received a copy of the GNU General Public License along with
18# this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20
21import gzip
22import logging
23import os
24import json
25import time
26
27from softwarecenter.backend.spawn_helper import SpawnHelper
28from softwarecenter.backend.reviews import ReviewLoader, Review, ReviewStats
29from softwarecenter.backend.piston.rnrclient import RatingsAndReviewsAPI
30from softwarecenter.backend.piston.rnrclient_pristine import ReviewDetails
31from softwarecenter.db.database import Application
32import softwarecenter.distro
33from softwarecenter.netstatus import network_state_is_connected
34from softwarecenter.paths import (SOFTWARE_CENTER_CACHE_DIR,
35 PistonHelpers,
36 RNRApps,
37 )
38from softwarecenter.utils import calc_dr
39
40LOG = logging.getLogger(__name__)
41
42
43# this code had several incernations:
44# - python threads, slow and full of latency (GIL)
45# - python multiprocesing, crashed when accessibility was turned on,
46# does not work in the quest session (#743020)
47# - GObject.spawn_async() looks good so far (using the SpawnHelper code)
48class ReviewLoaderSpawningRNRClient(ReviewLoader):
49 """ loader that uses multiprocessing to call rnrclient and
50 a glib timeout watcher that polls periodically for the
51 data
52 """
53
54 def __init__(self, cache, db, distro=None):
55 super(ReviewLoaderSpawningRNRClient, self).__init__(cache, db, distro)
56 cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
57 self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
58 cachedir = os.path.join(SOFTWARE_CENTER_CACHE_DIR, "rnrclient")
59 self.rnrclient = RatingsAndReviewsAPI(cachedir=cachedir)
60 self._reviews = {}
61
62 def _update_rnrclient_offline_state(self):
63 # this needs the lp:~mvo/piston-mini-client/offline-mode branch
64 self.rnrclient._offline_mode = not network_state_is_connected()
65
66 # reviews
67 def get_reviews(self, translated_app, callback, page=1,
68 language=None, sort=0):
69 """ public api, triggers fetching a review and calls callback
70 when its ready
71 """
72 # its fine to use the translated appname here, we only submit the
73 # pkgname to the server
74 app = translated_app
75 self._update_rnrclient_offline_state()
76 sort_method = self._review_sort_methods[sort]
77 if language is None:
78 language = self.language
79 # gather args for the helper
80 try:
81 origin = self.cache.get_origin(app.pkgname)
82 except:
83 # this can happen if e.g. the app has multiple origins, this
84 # will be handled later
85 origin = None
86 # special case for not-enabled PPAs
87 if not origin and self.db:
88 details = app.get_details(self.db)
89 ppa = details.ppaname
90 if ppa:
91 origin = "lp-ppa-%s" % ppa.replace("/", "-")
92 # if there is no origin, there is nothing to do
93 if not origin:
94 callback(app, [])
95 return
96 distroseries = self.distro.get_codename()
97 # run the command and add watcher
98 cmd = [os.path.join(softwarecenter.paths.datadir, PistonHelpers.GET_REVIEWS),
99 "--language", language,
100 "--origin", origin,
101 "--distroseries", distroseries,
102 "--pkgname", str(app.pkgname), # ensure its str, not unicode
103 "--page", str(page),
104 "--sort", sort_method,
105 ]
106 spawn_helper = SpawnHelper()
107 spawn_helper.connect(
108 "data-available", self._on_reviews_helper_data, app, callback)
109 spawn_helper.run(cmd)
110
111 def _on_reviews_helper_data(self, spawn_helper, piston_reviews, app, callback):
112 # convert into our review objects
113 reviews = []
114 for r in piston_reviews:
115 reviews.append(Review.from_piston_mini_client(r))
116 # add to our dicts and run callback
117 self._reviews[app] = reviews
118 callback(app, self._reviews[app])
119 return False
120
121 # stats
122 def refresh_review_stats(self, callback):
123 """ public api, refresh the available statistics """
124 try:
125 mtime = os.path.getmtime(self.REVIEW_STATS_CACHE_FILE)
126 days_delta = int((time.time() - mtime) // (24*60*60))
127 days_delta += 1
128 except OSError:
129 days_delta = 0
130 LOG.debug("refresh with days_delta: %s" % days_delta)
131 #origin = "any"
132 #distroseries = self.distro.get_codename()
133 cmd = [os.path.join(
134 softwarecenter.paths.datadir, PistonHelpers.GET_REVIEW_STATS),
135 # FIXME: the server currently has bug (#757695) so we
136 # can not turn this on just yet and need to use
137 # the old "catch-all" review-stats for now
138 #"--origin", origin,
139 #"--distroseries", distroseries,
140 ]
141 if days_delta:
142 cmd += ["--days-delta", str(days_delta)]
143 spawn_helper = SpawnHelper()
144 spawn_helper.connect("data-available", self._on_review_stats_data, callback)
145 spawn_helper.run(cmd)
146
147 def _on_review_stats_data(self, spawn_helper, piston_review_stats, callback):
148 """ process stdout from the helper """
149 review_stats = self.REVIEW_STATS_CACHE
150
151 if self._cache_version_old and self._server_has_histogram(piston_review_stats):
152 self.REVIEW_STATS_CACHE = {}
153 self.save_review_stats_cache_file()
154 self.refresh_review_stats(callback)
155 return
156
157 # convert to the format that s-c uses
158 for r in piston_review_stats:
159 s = ReviewStats(Application("", r.package_name))
160 s.ratings_average = float(r.ratings_average)
161 s.ratings_total = float(r.ratings_total)
162 if r.histogram:
163 s.rating_spread = json.loads(r.histogram)
164 else:
165 s.rating_spread = [0,0,0,0,0]
166 s.dampened_rating = calc_dr(s.rating_spread)
167 review_stats[s.app] = s
168 self.REVIEW_STATS_CACHE = review_stats
169 callback(review_stats)
170 self.save_review_stats_cache_file()
171
172 def _server_has_histogram(self, piston_review_stats):
173 '''check response from server to see if histogram is supported'''
174 supported = getattr(piston_review_stats[0], "histogram", False)
175 if not supported:
176 return False
177 return True
178
179 # writing new reviews spawns external helper
180 # FIXME: instead of the callback we should add proper gobject signals
181 def spawn_write_new_review_ui(self, translated_app, version, iconname,
182 origin, parent_xid, datadir, callback):
183 """ this spawns the UI for writing a new review and
184 adds it automatically to the reviews DB """
185 app = translated_app.get_untranslated_app(self.db)
186 cmd = [os.path.join(datadir, RNRApps.SUBMIT_REVIEW),
187 "--pkgname", app.pkgname,
188 "--iconname", iconname,
189 "--parent-xid", "%s" % parent_xid,
190 "--version", version,
191 "--origin", origin,
192 "--datadir", datadir,
193 ]
194 if app.appname:
195 # needs to be (utf8 encoded) str, otherwise call fails
196 cmd += ["--appname", utf8(app.appname)]
197 spawn_helper = SpawnHelper(format="json")
198 spawn_helper.connect(
199 "data-available", self._on_submit_review_data, app, callback)
200 spawn_helper.run(cmd)
201
202 def _on_submit_review_data(self, spawn_helper, review_json, app, callback):
203 """ called when submit_review finished, when the review was send
204 successfully the callback is triggered with the new reviews
205 """
206 LOG.debug("_on_submit_review_data")
207 return
208 # read stdout from submit_review
209 review = ReviewDetails.from_dict(review_json)
210 # FIXME: ideally this would be stored in ubuntu-sso-client
211 # but it dosn't so we store it here
212 save_person_to_config(review.reviewer_username)
213 if not app in self._reviews:
214 self._reviews[app] = []
215 self._reviews[app].insert(0, Review.from_piston_mini_client(review))
216 callback(app, self._reviews[app])
217
218 def spawn_report_abuse_ui(self, review_id, parent_xid, datadir, callback):
219 """ this spawns the UI for reporting a review as inappropriate
220 and adds the review-id to the internal hide list. once the
221 operation is complete it will call callback with the updated
222 review list
223 """
224 cmd = [os.path.join(datadir, RNRApps.REPORT_REVIEW),
225 "--review-id", review_id,
226 "--parent-xid", "%s" % parent_xid,
227 "--datadir", datadir,
228 ]
229 spawn_helper = SpawnHelper("json")
230 spawn_helper.connect("exited",
231 self._on_report_abuse_finished,
232 review_id, callback)
233 spawn_helper.run(cmd)
234
235 def _on_report_abuse_finished(self, spawn_helper, exitcode, review_id, callback):
236 """ called when report_absuse finished """
237 LOG.debug("hide id %s " % review_id)
238 if exitcode == 0:
239 for (app, reviews) in self._reviews.items():
240 for review in reviews:
241 if str(review.id) == str(review_id):
242 # remove the one we don't want to see anymore
243 self._reviews[app].remove(review)
244 callback(app, self._reviews[app], None, 'remove', review)
245 break
246
247
248 def spawn_submit_usefulness_ui(self, review_id, is_useful, parent_xid, datadir, callback):
249 cmd = [os.path.join(datadir, RNRApps.SUBMIT_USEFULNESS),
250 "--review-id", "%s" % review_id,
251 "--is-useful", "%s" % int(is_useful),
252 "--parent-xid", "%s" % parent_xid,
253 "--datadir", datadir,
254 ]
255 spawn_helper = SpawnHelper(format="none")
256 spawn_helper.connect("exited",
257 self._on_submit_usefulness_finished,
258 review_id, is_useful, callback)
259 spawn_helper.connect("error",
260 self._on_submit_usefulness_error,
261 review_id, callback)
262 spawn_helper.run(cmd)
263
264 def _on_submit_usefulness_finished(self, spawn_helper, res, review_id, is_useful, callback):
265 """ called when report_usefulness finished """
266 # "Created", "Updated", "Not modified" -
267 # once lp:~mvo/rnr-server/submit-usefulness-result-strings makes it
268 response = spawn_helper._stdout
269 if response == '"Not modified"':
270 self._on_submit_usefulness_error(spawn_helper, response, review_id, callback)
271 return
272
273 LOG.debug("usefulness id %s " % review_id)
274 useful_votes = UsefulnessCache()
275 useful_votes.add_usefulness_vote(review_id, is_useful)
276 for (app, reviews) in self._reviews.items():
277 for review in reviews:
278 if str(review.id) == str(review_id):
279 # update usefulness, older servers do not send
280 # usefulness_{total,favorable} so we use getattr
281 review.usefulness_total = getattr(review, "usefulness_total", 0) + 1
282 if is_useful:
283 review.usefulness_favorable = getattr(review, "usefulness_favorable", 0) + 1
284 callback(app, self._reviews[app], useful_votes, 'replace', review)
285 break
286
287 def _on_submit_usefulness_error(self, spawn_helper, error_str, review_id, callback):
288 LOG.warn("submit usefulness id=%s failed with error: %s" %
289 (review_id, error_str))
290 for (app, reviews) in self._reviews.items():
291 for review in reviews:
292 if str(review.id) == str(review_id):
293 review.usefulness_submit_error = True
294 callback(app, self._reviews[app], None, 'replace', review)
295 break
296
297 def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback):
298 cmd = [os.path.join(datadir, RNRApps.DELETE_REVIEW),
299 "--review-id", "%s" % review_id,
300 "--parent-xid", "%s" % parent_xid,
301 "--datadir", datadir,
302 ]
303 spawn_helper = SpawnHelper(format="none")
304 spawn_helper.connect("exited",
305 self._on_delete_review_finished,
306 review_id, callback)
307 spawn_helper.connect("error", self._on_delete_review_error,
308 review_id, callback)
309 spawn_helper.run(cmd)
310
311 def _on_delete_review_finished(self, spawn_helper, res, review_id, callback):
312 """ called when delete_review finished"""
313 LOG.debug("delete id %s " % review_id)
314 for (app, reviews) in self._reviews.items():
315 for review in reviews:
316 if str(review.id) == str(review_id):
317 # remove the one we don't want to see anymore
318 self._reviews[app].remove(review)
319 callback(app, self._reviews[app], None, 'remove', review)
320 break
321
322 def _on_delete_review_error(self, spawn_helper, error_str, review_id, callback):
323 """called if delete review errors"""
324 LOG.warn("delete review id=%s failed with error: %s" % (review_id, error_str))
325 for (app, reviews) in self._reviews.items():
326 for review in reviews:
327 if str(review.id) == str(review_id):
328 review.delete_error = True
329 callback(app, self._reviews[app], action='replace',
330 single_review=review)
331 break
332
333 def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):
334 """ this spawns the UI for writing a new review and
335 adds it automatically to the reviews DB """
336 cmd = [os.path.join(datadir, RNRApps.MODIFY_REVIEW),
337 "--parent-xid", "%s" % parent_xid,
338 "--iconname", iconname,
339 "--datadir", "%s" % datadir,
340 "--review-id", "%s" % review_id,
341 ]
342 spawn_helper = SpawnHelper(format="json")
343 spawn_helper.connect("data-available",
344 self._on_modify_review_finished,
345 review_id, callback)
346 spawn_helper.connect("error", self._on_modify_review_error,
347 review_id, callback)
348 spawn_helper.run(cmd)
349
350 def _on_modify_review_finished(self, spawn_helper, review_json, review_id, callback):
351 """called when modify_review finished"""
352 LOG.debug("_on_modify_review_finished")
353 return
354 #review_json = spawn_helper._stdout
355 mod_review = ReviewDetails.from_dict(review_json)
356 for (app, reviews) in self._reviews.items():
357 for review in reviews:
358 if str(review.id) == str(review_id):
359 # remove the one we don't want to see anymore
360 self._reviews[app].remove(review)
361 new_review = Review.from_piston_mini_client(mod_review)
362 self._reviews[app].insert(0, new_review)
363 callback(app, self._reviews[app], action='replace',
364 single_review=new_review)
365 break
366
367 def _on_modify_review_error(self, spawn_helper, error_str, review_id, callback):
368 """called if modify review errors"""
369 LOG.debug("modify review id=%s failed with error: %s" % (review_id, error_str))
370 for (app, reviews) in self._reviews.items():
371 for review in reviews:
372 if str(review.id) == str(review_id):
373 review.modify_error = True
374 callback(app, self._reviews[app], action='replace',
375 single_review=review)
376 break
0377
=== modified file 'softwarecenter/db/application.py'
--- softwarecenter/db/application.py 2011-10-25 18:38:08 +0000
+++ softwarecenter/db/application.py 2011-10-27 07:59:24 +0000
@@ -252,7 +252,8 @@
252 # try apt first252 # try apt first
253 if self._pkg:253 if self._pkg:
254 for origin in self._pkg.candidate.origins:254 for origin in self._pkg.candidate.origins:
255 if (origin.origin == "Ubuntu" and origin.trusted and origin.component):255 if (origin.origin == get_distro().get_distro_channel_name() and
256 origin.trusted and origin.component):
256 return origin.component257 return origin.component
257 # then xapian258 # then xapian
258 elif self._doc:259 elif self._doc:
259260
=== modified file 'softwarecenter/db/update.py'
--- softwarecenter/db/update.py 2011-10-18 00:48:28 +0000
+++ softwarecenter/db/update.py 2011-10-27 07:59:24 +0000
@@ -401,7 +401,7 @@
401 except ImportError:401 except ImportError:
402 return False402 return False
403 if not listsdir:403 if not listsdir:
404 listsdir = apt_pkg.Config.find_dir("Dir::State::lists")404 listsdir = apt_pkg.config.find_dir("Dir::State::lists")
405 context = GObject.main_context_default()405 context = GObject.main_context_default()
406 for appinfo in glob("%s/*AppInfo" % listsdir):406 for appinfo in glob("%s/*AppInfo" % listsdir):
407 LOG.debug("processing %s" % appinfo)407 LOG.debug("processing %s" % appinfo)
408408
=== modified file 'softwarecenter/distro/Debian.py'
--- softwarecenter/distro/Debian.py 2011-09-21 15:15:28 +0000
+++ softwarecenter/distro/Debian.py 2011-10-27 07:59:24 +0000
@@ -31,6 +31,10 @@
31 SCREENSHOT_THUMB_URL = "http://screenshots.debian.net/thumbnail/%(pkgname)s"31 SCREENSHOT_THUMB_URL = "http://screenshots.debian.net/thumbnail/%(pkgname)s"
32 SCREENSHOT_LARGE_URL = "http://screenshots.debian.net/screenshot/%(pkgname)s"32 SCREENSHOT_LARGE_URL = "http://screenshots.debian.net/screenshot/%(pkgname)s"
3333
34 REVIEWS_SERVER = ""
35
36 DEVELOPER_URL = "http://www.debian.org/devel/"
37
34 def get_distro_channel_name(self):38 def get_distro_channel_name(self):
35 """ The name in the Release file """39 """ The name in the Release file """
36 return "Debian"40 return "Debian"
@@ -68,19 +72,20 @@
68 72
69 def get_license_text(self, component):73 def get_license_text(self, component):
70 if component in ("main",):74 if component in ("main",):
71 return _("Open source")75 return _("Meets the Debian Free Software Guidelines")
72 elif component == "contrib":76 elif component == "contrib":
73 return _("Open source, with proprietary parts")77 return _("Meets the Debian Free Software Guidelines itself "
74 elif component == "restricted":78 "but requires additional non-free software to work")
75 return _("Proprietary")79 elif component == "non-free":
7680 return _("Non-free since it is either restricted "
77 def get_maintenance_status(self, cache, appname, pkgname, component, channel):81 "in use, redistribution or modification.")
78 return ""
7982
80 def get_architecture(self):83 def get_architecture(self):
81 return apt.apt_pkg.config.find("Apt::Architecture")84 return apt.apt_pkg.config.find("Apt::Architecture")
8285
83 def get_foreign_architectures(self):86 def get_foreign_architectures(self):
87 return []
88 # Not yet in unstable
84 import subprocess89 import subprocess
85 out = subprocess.Popen(['dpkg', '--print-foreign-architectures'],90 out = subprocess.Popen(['dpkg', '--print-foreign-architectures'],
86 stdout=subprocess.PIPE).communicate()[0].rstrip('\n')91 stdout=subprocess.PIPE).communicate()[0].rstrip('\n')
@@ -88,6 +93,63 @@
88 return out.split(' ')93 return out.split(' ')
89 return []94 return []
9095
96 def is_supported(self, cache, doc, pkgname):
97 # the doc does not by definition contain correct data regarding the
98 # section. Looking up in the cache seems just as fast/slow.
99 if pkgname in cache and cache[pkgname].candidate:
100 for origin in cache[pkgname].candidate.origins:
101 if (origin.origin == "Debian" and
102 origin.trusted and
103 origin.component == "main"):
104 return True
105 return False
106
107 def get_maintenance_status(self, cache, appname, pkgname, component, channelname):
108 """Return the maintenance status of a package."""
109 if not hasattr(cache, '_cache') or not pkgname:
110 return
111 try:
112 origins = cache[pkgname].candidate.origins
113 except KeyError, AttributeError:
114 return
115 else:
116 for origin in origins:
117 if (origin.origin == "Debian" and origin.trusted):
118 pkg_comp = origin.component
119 pkg_archive = origin.archive
120 break
121 else:
122 return
123 if pkg_comp in ("contrib", "non-free"):
124 if pkg_archive == "oldstable":
125 return _("Debian does not provide critical updates.")
126 else:
127 return _("Debian does not provide critical updates. "
128 "Some updates may be provided by the developers "
129 "of %s and redistributed by Debian.") % appname
130 elif pkg_comp == "main":
131 if pkg_archive == "stable":
132 return _("Debian provides critical updates for %s.") % appname
133 elif pkg_archive == "oldstable":
134 return _("Debian only provides updates for %s during "
135 "a transition phase. "
136 "Please consider upgrading to a later stable "
137 "release of Debian.") % appname
138 elif pkg_archive == "testing":
139 return _("Debian provides critical updates for %s. But "
140 "updates could be delayed or skipped.") % appname
141 elif pkg_archive == "unstable":
142 return _("Debian doens't provides critical updates "
143 "for %s") % appname
144 return
145
146 def get_supported_query(self):
147 import xapian
148 query1 = xapian.Query("XOL"+"Debian")
149 query2 = xapian.Query("XOC"+"main")
150 return xapian.Query(xapian.Query.OP_AND, query1, query2)
151
152
91if __name__ == "__main__":153if __name__ == "__main__":
92 cache = apt.Cache()154 cache = apt.Cache()
93 print(cache.get_maintenance_status(cache, "synaptic app", "synaptic", "main", None))155 print(cache.get_maintenance_status(cache, "synaptic app", "synaptic", "main", None))
94156
=== modified file 'softwarecenter/distro/Ubuntu.py'
--- softwarecenter/distro/Ubuntu.py 2011-10-05 00:58:57 +0000
+++ softwarecenter/distro/Ubuntu.py 2011-10-27 07:59:24 +0000
@@ -56,6 +56,9 @@
56 # FIXME: does that make sense?!?56 # FIXME: does that make sense?!?
57 REVIEW_STATS_URL = REVIEWS_SERVER+"/review-stats"57 REVIEW_STATS_URL = REVIEWS_SERVER+"/review-stats"
5858
59 # Starting point for Ubuntu app developers
60 DEVELOPER_URL = "http://developer.ubuntu.com/"
61
59 def get_app_name(self):62 def get_app_name(self):
60 return _("Ubuntu Software Center")63 return _("Ubuntu Software Center")
6164
@@ -128,6 +131,9 @@
128 query2 = xapian.Query(xapian.Query.OP_OR, query2a, query2b)131 query2 = xapian.Query(xapian.Query.OP_OR, query2a, query2b)
129 return xapian.Query(xapian.Query.OP_AND, query1, query2)132 return xapian.Query(xapian.Query.OP_AND, query1, query2)
130133
134 def get_supported_filter_name(self):
135 return _("Canonical-Maintained Software")
136
131 def get_maintenance_status(self, cache, appname, pkgname, component, channelname):137 def get_maintenance_status(self, cache, appname, pkgname, component, channelname):
132 # try to figure out the support dates of the release and make138 # try to figure out the support dates of the release and make
133 # sure to look only for stuff in "Ubuntu" and "distro_codename"139 # sure to look only for stuff in "Ubuntu" and "distro_codename"
134140
=== modified file 'softwarecenter/distro/__init__.py'
--- softwarecenter/distro/__init__.py 2011-10-25 18:38:08 +0000
+++ softwarecenter/distro/__init__.py 2011-10-27 07:59:24 +0000
@@ -19,12 +19,15 @@
1919
20import logging20import logging
21import os21import os
22import subprocess
2322
24from gettext import gettext as _23from gettext import gettext as _
24import lsb_release
2525
26from softwarecenter.utils import UnimplementedError, utf826from softwarecenter.utils import UnimplementedError, utf8
2727
28log = logging.getLogger(__name__)
29
30
28class Distro(object):31class Distro(object):
29 """ abstract base class for a distribution """32 """ abstract base class for a distribution """
3033
@@ -37,6 +40,16 @@
37 REVIEW_SUMMARY_STARS_BASE_PATH = "/usr/share/software-center/images/review-summary"40 REVIEW_SUMMARY_STARS_BASE_PATH = "/usr/share/software-center/images/review-summary"
38 REVIEWS_SERVER = os.environ.get("SOFTWARE_CENTER_REVIEWS_HOST") or "http://localhost:8000"41 REVIEWS_SERVER = os.environ.get("SOFTWARE_CENTER_REVIEWS_HOST") or "http://localhost:8000"
3942
43 # You need to set this var to enable purchases
44 PURCHASE_APP_URL = ""
45
46 # Point developers to a web page
47 DEVELOPER_URL = ""
48
49 def __init__(self, lsb_info):
50 """Return a new generic Distro instance."""
51 self.lsb_info = lsb_info
52
40 def get_app_name(self):53 def get_app_name(self):
41 """ 54 """
42 The name of the application (as displayed in the main window and 55 The name of the application (as displayed in the main window and
@@ -66,9 +79,8 @@
66 return os.environ["SOFTWARE_CENTER_DISTRO_CODENAME"]79 return os.environ["SOFTWARE_CENTER_DISTRO_CODENAME"]
67 # normal behavior80 # normal behavior
68 if not hasattr(self, "_distro_code_name"):81 if not hasattr(self, "_distro_code_name"):
69 self._distro_code_name = subprocess.Popen(82 self._distro_code_name = \
70 ["lsb_release","-c","-s"], 83 lsb_release.get_distro_information()["CODENAME"]
71 stdout=subprocess.PIPE).communicate()[0].strip()
72 return self._distro_code_name84 return self._distro_code_name
7385
74 def get_maintenance_status(self, cache, appname, pkgname, component, channelname):86 def get_maintenance_status(self, cache, appname, pkgname, component, channelname):
@@ -89,6 +101,9 @@
89 import xapian101 import xapian
90 return xapian.Query()102 return xapian.Query()
91103
104 def get_supported_filter_name(self):
105 return _("Supported Software")
106
92 def get_install_warning_text(self, cache, pkg, appname, depends):107 def get_install_warning_text(self, cache, pkg, appname, depends):
93 primary = utf8(_("To install %s, these items must be removed:")) % utf8(appname)108 primary = utf8(_("To install %s, these items must be removed:")) % utf8(appname)
94 button_text = _("Install Anyway")109 button_text = _("Install Anyway")
@@ -140,15 +155,14 @@
140155
141156
142def _get_distro():157def _get_distro():
143 distro_id = subprocess.Popen(["lsb_release","-i","-s"], 158 lsb_info = lsb_release.get_distro_information()
144 stdout=subprocess.PIPE).communicate()[0]159 distro_id = lsb_info["ID"]
145 distro_id = distro_id.strip().replace(' ', '')160 log.debug("get_distro: '%s'", distro_id)
146 logging.getLogger("softwarecenter.distro").debug("get_distro: '%s'" % distro_id)
147 # start with a import, this gives us only a softwarecenter module161 # start with a import, this gives us only a softwarecenter module
148 module = __import__(distro_id, globals(), locals(), [], -1)162 module = __import__(distro_id, globals(), locals(), [], -1)
149 # get the right class and instanciate it163 # get the right class and instanciate it
150 distro_class = getattr(module, distro_id)164 distro_class = getattr(module, distro_id)
151 instance = distro_class()165 instance = distro_class(lsb_info)
152 return instance166 return instance
153167
154def get_distro():168def get_distro():
155169
=== modified file 'softwarecenter/ui/gtk3/app.py'
--- softwarecenter/ui/gtk3/app.py 2011-10-20 01:52:42 +0000
+++ softwarecenter/ui/gtk3/app.py 2011-10-27 07:59:24 +0000
@@ -342,10 +342,28 @@
342 self.config = get_config()342 self.config = get_config()
343 self.restore_state()343 self.restore_state()
344344
345 # Adapt menu entries
346 supported_menuitem = self.builder.get_object("menuitem_view_supported_only")
347 supported_menuitem.set_label(self.distro.get_supported_filter_name())
348 file_menu = self.builder.get_object("menu1")
349
350 if not self.distro.DEVELOPER_URL:
351 help_menu = self.builder.get_object("menu_help")
352 developer_separator = self.builder.get_object("separator_developer")
353 help_menu.remove(developer_separator)
354 developer_menuitem = self.builder.get_object("menuitem_developer")
355 help_menu.remove(developer_menuitem)
356
357 # Check if oneconf is available
358 och = get_oneconf_handler()
359 if not och:
360 file_menu.remove(self.builder.get_object("menuitem_sync_between_computers"))
361
345 # run s-c-agent update362 # run s-c-agent update
346 if options.disable_buy:363 if options.disable_buy or not self.distro.PURCHASE_APP_URL:
347 file_menu = self.builder.get_object("menu1")
348 file_menu.remove(self.builder.get_object("menuitem_reinstall_purchases"))364 file_menu.remove(self.builder.get_object("menuitem_reinstall_purchases"))
365 if not (options.enable_lp or och):
366 file_menu.remove(self.builder.get_object("separator_login"))
349 else:367 else:
350 sc_agent_update = os.path.join(368 sc_agent_update = os.path.join(
351 self.datadir, "update-software-center-agent")369 self.datadir, "update-software-center-agent")
@@ -355,9 +373,6 @@
355 GObject.child_watch_add(373 GObject.child_watch_add(
356 pid, self._on_update_software_center_agent_finished)374 pid, self._on_update_software_center_agent_finished)
357375
358 if options.disable_buy and not options.enable_lp:
359 file_menu.remove(self.builder.get_object("separator_login"))
360
361 # TODO: Remove the following two lines once we have remove repository376 # TODO: Remove the following two lines once we have remove repository
362 # support in aptdaemon (see LP: #723911)377 # support in aptdaemon (see LP: #723911)
363 file_menu = self.builder.get_object("menu1")378 file_menu = self.builder.get_object("menu1")
@@ -1031,8 +1046,8 @@
1031 GObject.timeout_add_seconds(1, lambda p: p.poll() == None, p)1046 GObject.timeout_add_seconds(1, lambda p: p.poll() == None, p)
10321047
1033 def on_menuitem_developer_activate(self, menuitem):1048 def on_menuitem_developer_activate(self, menuitem):
1034 webbrowser.open("http://developer.ubuntu.com/")1049 webbrowser.open(self.distro.DEVELOPER_URL)
1035 1050
1036 def _ask_and_repair_broken_cache(self):1051 def _ask_and_repair_broken_cache(self):
1037 # wait until the window window is available1052 # wait until the window window is available
1038 if self.window_main.props.visible == False:1053 if self.window_main.props.visible == False:
10391054
=== modified file 'softwarecenter/ui/gtk3/panes/installedpane.py'
--- softwarecenter/ui/gtk3/panes/installedpane.py 2011-10-06 02:35:27 +0000
+++ softwarecenter/ui/gtk3/panes/installedpane.py 2011-10-27 07:59:24 +0000
@@ -132,8 +132,9 @@
132 132
133 # Start OneConf133 # Start OneConf
134 self.oneconf_handler = get_oneconf_handler(self.oneconf_viewpickler)134 self.oneconf_handler = get_oneconf_handler(self.oneconf_viewpickler)
135 self.oneconf_handler.connect('show-oneconf-changed', self._show_oneconf_changed)135 if self.oneconf_handler:
136 self.oneconf_handler.connect('last-time-sync-changed', self._last_time_sync_oneconf_changed)136 self.oneconf_handler.connect('show-oneconf-changed', self._show_oneconf_changed)
137 self.oneconf_handler.connect('last-time-sync-changed', self._last_time_sync_oneconf_changed)
137 138
138 # OneConf pane139 # OneConf pane
139 self.computerpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)140 self.computerpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
140141
=== modified file 'softwarecenter/ui/gtk3/views/appdetailsview_gtk.py'
--- softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-10-20 16:08:23 +0000
+++ softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-10-27 07:59:24 +0000
@@ -1051,7 +1051,8 @@
1051 self.reviews.connect("more-reviews-clicked", self._on_more_reviews_clicked)1051 self.reviews.connect("more-reviews-clicked", self._on_more_reviews_clicked)
1052 self.reviews.connect("different-review-language-clicked", self._on_reviews_in_different_language_clicked)1052 self.reviews.connect("different-review-language-clicked", self._on_reviews_in_different_language_clicked)
1053 self.reviews.connect("review-sort-changed", self._on_review_sort_method_changed)1053 self.reviews.connect("review-sort-changed", self._on_review_sort_method_changed)
1054 vb.pack_start(self.reviews, False, False, 0)1054 if get_distro().REVIEWS_SERVER:
1055 vb.pack_start(self.reviews, False, False, 0)
10551056
1056 self.show_all()1057 self.show_all()
10571058