1
# -*- coding: utf-8 -*-
3
# Copyright (C) 2009 Canonical
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.
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
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
33
import xml.dom.minidom
35
import softwarecenter.distro
37
from softwarecenter.db.database import Application
38
from softwarecenter.utils import *
39
from softwarecenter.paths import *
41
class ReviewStats(object):
42
def __init__(self, app):
44
self.avg_rating = None
47
return "[ReviewStats '%s' rating='%s' nr_reviews='%s']" % (self.app, self.avg_rating, self.nr_reviews)
50
"""A individual review object """
51
def __init__(self, app):
52
# a softwarecenter.db.database.Application object
54
# the review items that the object fills in
59
self.package_version = None
64
return "[Review id=%s text='%s' person='%s']" % (self.id, self.text, self.person)
66
return """<review app_name="%s" package_name="%s" id="%s" language="%s"
67
data="%s" rating="%s" reviewer_name="%s">
68
<summary>%s</summary><text>%s</text></review>""" % (
69
self.app.appname, self.app.pkgname,
70
self.id, self.language, self.date, self.rating,
71
self.person, self.summary, self.text)
73
class ReviewLoader(object):
74
"""A loader that returns a review object list"""
76
# cache the ReviewStats
77
REVIEW_STATS_CACHE = {}
78
REVIEW_STATS_CACHE_FILE = SOFTWARE_CENTER_CACHE_DIR+"/review-stats.p"
80
def __init__(self, distro=None):
83
self.distro = softwarecenter.distro.get_distro()
84
if os.path.exists(self.REVIEW_STATS_CACHE_FILE):
86
self.REVIEW_STATS_CACHE = cPickle.load(open(self.REVIEW_STATS_CACHE_FILE))
88
logging.exception("review stats cache load failure")
89
os.rename(self.REVIEW_STATS_CACHE_FILE, self.REVIEW_STATS_CACHE_FILE+".fail")
91
def get_reviews(self, application, callback):
92
"""run callback f(app, review_list)
93
with list of review objects for the given
94
db.database.Application object
98
def get_review_stats(self, application):
99
"""return a ReviewStats (number of reviews, rating)
100
for a given application. this *must* be super-fast
101
as it is called a lot during tree view display
104
if application in self.REVIEW_STATS_CACHE:
105
return self.REVIEW_STATS_CACHE[application]
108
def refresh_review_stats(self, callback):
109
""" get the review statists and call callback when its there """
112
def save_review_stats_cache_file(self):
113
""" save review stats cache file in xdg cache dir """
114
cachedir = SOFTWARE_CENTER_CACHE_DIR
115
if not os.path.exists(cachedir):
116
os.makedirs(cachedir)
117
cPickle.dump(self.REVIEW_STATS_CACHE,
118
open(self.REVIEW_STATS_CACHE_FILE, "w"))
120
class ReviewLoaderXMLAsync(ReviewLoader):
121
""" get xml (or gzip compressed xml) """
123
def _gio_review_input_callback(self, source, result):
124
app = source.get_data("app")
125
callback = source.get_data("callback")
127
xml_str = source.read_finish(result)
128
except glib.GError, e:
129
# ignore read errors, most likely transient
130
return callback(app, [])
131
# check for gzip header
132
if xml_str.startswith("\37\213"):
133
gz=gzip.GzipFile(fileobj=StringIO.StringIO(xml_str))
135
dom = xml.dom.minidom.parseString(xml_str)
137
for review_xml in dom.getElementsByTagName("review"):
138
appname = review_xml.getAttribute("app_name")
139
pkgname = review_xml.getAttribute("package_name")
140
app = Application(appname, pkgname)
142
review.id = review_xml.getAttribute("id")
143
review.date = review_xml.getAttribute("date")
144
review.rating = review_xml.getAttribute("rating")
145
review.person = review_xml.getAttribute("reviewer_name")
146
review.language = review_xml.getAttribute("language")
147
summary_elements = review_xml.getElementsByTagName("summary")
148
if summary_elements and summary_elements[0].childNodes:
149
review.summary = summary_elements[0].childNodes[0].data
150
review_elements = review_xml.getElementsByTagName("text")
151
if review_elements and review_elements[0].childNodes:
152
review.text = review_elements[0].childNodes[0].data
153
reviews.append(review)
155
callback(app, reviews)
157
def _gio_review_read_callback(self, source, result):
158
app = source.get_data("app")
159
callback = source.get_data("callback")
161
stream=source.read_finish(result)
162
except glib.GError, e:
163
print e, source, result
164
# 404 means no review
166
return callback(app, [])
169
stream.set_data("app", app)
170
stream.set_data("callback", callback)
171
# FIXME: static size here as first argument sucks, but it seems
172
# like there is a bug in the python bindings, I can not pass
173
# -1 or anything like this
174
stream.read_async(128*1024, self._gio_review_input_callback)
176
def get_reviews(self, app, callback):
177
""" get a specific review and call callback when its available"""
178
url = self.distro.REVIEWS_URL % app.pkgname
180
url += "/%s" % app.appname
181
logging.debug("looking for review at '%s'" % url)
183
f.read_async(self._gio_review_read_callback)
184
f.set_data("app", app)
185
f.set_data("callback", callback)
188
def _gio_review_stats_input_callback(self, source, result):
189
callback = source.get_data("callback")
191
xml_str = source.read_finish(result)
192
except glib.GError, e:
193
# ignore read errors, most likely transient
195
# check for gzip header
196
if xml_str.startswith("\37\213"):
197
gz=gzip.GzipFile(fileobj=StringIO.StringIO(xml_str))
199
dom = xml.dom.minidom.parseString(xml_str)
201
# FIXME: look at root element like:
202
# "<review-statistics origin="ubuntu" distroseries="lucid" language="en">"
203
# to verify we got the data we expected
204
for review_stats_xml in dom.getElementsByTagName("review"):
205
appname = review_stats_xml.getAttribute("app_name")
206
pkgname = review_stats_xml.getAttribute("package_name")
207
app = Application(appname, pkgname)
208
stats = ReviewStats(app)
209
stats.nr_reviews = int(review_stats_xml.getAttribute("count"))
210
stats.avg_rating = float(review_stats_xml.getAttribute("average"))
211
review_stats[app] = stats
212
# update review_stats dict
213
self.REVIEW_STATS_CACHE = review_stats
214
self.save_review_stats_cache_file()
218
def _gio_review_stats_read_callback(self, source, result):
219
callback = source.get_data("callback")
221
stream=source.read_finish(result)
222
except glib.GError, e:
223
print e, source, result
225
stream.set_data("callback", callback)
226
# FIXME: static size here as first argument sucks, but it seems
227
# like there is a bug in the python bindings, I can not pass
228
# -1 or anything like this
229
stream.read_async(128*1024, self._gio_review_stats_input_callback)
231
def refresh_review_stats(self, callback):
232
""" get the review statists and call callback when its there """
233
url = self.distro.REVIEW_STATS_URL
235
f.set_data("callback", callback)
236
f.read_async(self._gio_review_stats_read_callback)
238
class ReviewLoaderIpsum(ReviewLoader):
239
""" a test review loader that does not do any network io
240
and returns random lorem ipsum review texts
242
#This text is under public domain
245
LOREM=u"""lorem ipsum "dolor" äöü sit amet consetetur sadipscing elitr sed diam nonumy
246
eirmod tempor invidunt ut labore et dolore magna aliquyam erat sed diam
247
voluptua at vero eos et accusam et justo duo dolores et ea rebum stet clita
248
kasd gubergren no sea takimata sanctus est lorem ipsum dolor sit amet lorem
249
ipsum dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod
250
tempor invidunt ut labore et dolore magna aliquyam erat sed diam voluptua at
251
vero eos et accusam et justo duo dolores et ea rebum stet clita kasd
252
gubergren no sea takimata sanctus est lorem ipsum dolor sit amet lorem ipsum
253
dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor
254
invidunt ut labore et dolore magna aliquyam erat sed diam voluptua at vero
255
eos et accusam et justo duo dolores et ea rebum stet clita kasd gubergren no
256
sea takimata sanctus est lorem ipsum dolor sit amet
258
duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
259
molestie consequat vel illum dolore eu feugiat nulla facilisis at vero eros
260
et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril
261
delenit augue duis dolore te feugait nulla facilisi lorem ipsum dolor sit
262
amet consectetuer adipiscing elit sed diam nonummy nibh euismod tincidunt ut
263
laoreet dolore magna aliquam erat volutpat
265
ut wisi enim ad minim veniam quis nostrud exerci tation ullamcorper suscipit
266
lobortis nisl ut aliquip ex ea commodo consequat duis autem vel eum iriure
267
dolor in hendrerit in vulputate velit esse molestie consequat vel illum
268
dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio
269
dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te
270
feugait nulla facilisi
272
nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet
273
doming id quod mazim placerat facer possim assum lorem ipsum dolor sit amet
274
consectetuer adipiscing elit sed diam nonummy nibh euismod tincidunt ut
275
laoreet dolore magna aliquam erat volutpat ut wisi enim ad minim veniam quis
276
nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea
279
duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
280
molestie consequat vel illum dolore eu feugiat nulla facilisis
282
at vero eos et accusam et justo duo dolores et ea rebum stet clita kasd
283
gubergren no sea takimata sanctus est lorem ipsum dolor sit amet lorem ipsum
284
dolor sit amet consetetur sadipscing elitr sed diam nonumy eirmod tempor
285
invidunt ut labore et dolore magna aliquyam erat sed diam voluptua at vero
286
eos et accusam et justo duo dolores et ea rebum stet clita kasd gubergren no
287
sea takimata sanctus est lorem ipsum dolor sit amet lorem ipsum dolor sit
288
amet consetetur sadipscing elitr at accusam aliquyam diam diam dolore
289
dolores duo eirmod eos erat et nonumy sed tempor et et invidunt justo labore
290
stet clita ea et gubergren kasd magna no rebum sanctus sea sed takimata ut
291
vero voluptua est lorem ipsum dolor sit amet lorem ipsum dolor sit amet
292
consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut labore
293
et dolore magna aliquyam erat
295
consetetur sadipscing elitr sed diam nonumy eirmod tempor invidunt ut labore
296
et dolore magna aliquyam erat sed diam voluptua at vero eos et accusam et
297
justo duo dolores et ea rebum stet clita kasd gubergren no sea takimata
298
sanctus est lorem ipsum dolor sit amet lorem ipsum dolor sit amet consetetur
299
sadipscing elitr sed diam nonumy eirmod tempor invidunt ut labore et dolore
300
magna aliquyam erat sed diam voluptua at vero eos et accusam et justo duo
301
dolores et ea rebum stet clita kasd gubergren no sea takimata sanctus est
302
lorem ipsum dolor sit amet lorem ipsum dolor sit amet consetetur sadipscing
303
elitr sed diam nonumy eirmod tempor invidunt ut labore et dolore magna
304
aliquyam erat sed diam voluptua at vero eos et accusam et justo duo dolores
305
et ea rebum stet clita kasd gubergren no sea takimata sanctus est lorem
306
ipsum dolor sit amet"""
307
USERS = ["Joe Doll", "John Foo", "Cat Lala", "Foo Grumpf", "Bar Tender", "Baz Lightyear"]
308
SUMMARIES = ["Cool", "Medium", "Bad", "Too difficult"]
309
def _random_person(self):
310
return random.choice(self.USERS)
311
def _random_text(self):
312
return random.choice(self.LOREM.split("\n\n"))
313
def _random_summary(self):
314
return random.choice(self.SUMMARIES)
315
def get_reviews(self, application, callback):
317
for i in range(0,random.randint(0,6)):
318
review = Review(application)
319
review.id = random.randint(1,500)
320
review.rating = random.randint(1,5)
321
review.summary = self._random_summary()
322
review.date = time.ctime(time.time())
323
review.person = self._random_person()
324
review.text = self._random_text().replace("\n","")
325
reviews.append(review)
326
callback(application, reviews)
327
def refresh_review_stats(self, callback):
329
callback(review_stats)
332
def get_review_loader():
334
factory that returns a reviews loader singelton
337
if not review_loader:
338
if "SOFTWARE_CENTER_IPSUM_REVIEWS" in os.environ:
339
review_loader = ReviewLoaderIpsum()
341
review_loader = ReviewLoaderXMLAsync()
344
if __name__ == "__main__":
345
def callback(app, reviews):
346
print "app callback:"
348
def stats_callback(stats):
351
from softwarecenter.db.database import Application
352
app = Application("7zip",None)
353
#loader = ReviewLoaderIpsum()
354
#print loader.get_reviews(app, callback)
355
#print loader.get_review_stats(app)
356
app = Application("totem","totem")
357
loader = ReviewLoaderXMLAsync()
358
loader.get_review_stats(stats_callback)
359
loader.get_reviews(app, callback)