59
59
LOG = logging.getLogger(__name__)
61
62
class ReviewStats(object):
62
63
def __init__(self, app):
64
65
self.ratings_average = None
65
66
self.ratings_total = 0
66
self.rating_spread = [0,0,0,0,0]
67
self.rating_spread = [0, 0, 0, 0, 0]
67
68
self.dampened_rating = 3.00
68
70
def __repr__(self):
69
return ("<ReviewStats '%s' ratings_average='%s' ratings_total='%s'"
70
" rating_spread='%s' dampened_rating='%s'>" %
71
(self.app, self.ratings_average, self.ratings_total,
71
return ("<ReviewStats '%s' ratings_average='%s' ratings_total='%s'"
72
" rating_spread='%s' dampened_rating='%s'>" %
73
(self.app, self.ratings_average, self.ratings_total,
72
74
self.rating_spread, self.dampened_rating))
75
77
class UsefulnessCache(object):
77
79
USEFULNESS_CACHE = {}
79
81
def __init__(self, try_server=False):
80
82
fname = "usefulness.p"
81
83
self.USEFULNESS_CACHE_FILE = os.path.join(SOFTWARE_CENTER_CACHE_DIR,
84
86
self._retrieve_votes_from_cache()
85
#Only try to get votes from the server if required, otherwise just use cache
87
# Only try to get votes from the server if required, otherwise
87
90
self._retrieve_votes_from_server()
89
92
def _retrieve_votes_from_cache(self):
90
93
if os.path.exists(self.USEFULNESS_CACHE_FILE):
92
self.USEFULNESS_CACHE = pickle.load(open(self.USEFULNESS_CACHE_FILE))
95
self.USEFULNESS_CACHE = pickle.load(
96
open(self.USEFULNESS_CACHE_FILE))
94
98
LOG.exception("usefulness cache load fallback failure")
95
os.rename(self.USEFULNESS_CACHE_FILE, self.USEFULNESS_CACHE_FILE+".fail")
99
os.rename(self.USEFULNESS_CACHE_FILE,
100
self.USEFULNESS_CACHE_FILE + ".fail")
98
102
def _retrieve_votes_from_server(self):
99
103
LOG.debug("_retrieve_votes_from_server started")
100
104
user = get_person_from_config()
103
LOG.warn("Could not get usefulness from server, no username in config file")
107
LOG.warn("Could not get usefulness from server, no username "
106
111
# run the command and add watcher
107
112
spawn_helper = SpawnHelper()
108
113
spawn_helper.connect("data-available", self._on_usefulness_data)
116
121
for result in results:
117
122
self.USEFULNESS_CACHE[str(result['review_id'])] = result['useful']
118
123
if not self.save_usefulness_cache_file():
119
LOG.warn("Read usefulness results from server but failed to write to cache")
124
LOG.warn("Read usefulness results from server but failed to "
121
127
def save_usefulness_cache_file(self):
122
128
"""write the dict out to cache file"""
123
129
cachedir = SOFTWARE_CENTER_CACHE_DIR
133
139
def add_usefulness_vote(self, review_id, useful):
134
"""pass a review id and useful boolean vote and save it into the dict, then try to save to cache file"""
140
"""pass a review id and useful boolean vote and save it into the
141
dict, then try to save to cache file
135
143
self.USEFULNESS_CACHE[str(review_id)] = useful
136
144
if self.save_usefulness_cache_file():
140
148
def check_for_usefulness(self, review_id):
141
"""pass a review id and get a True/False useful back or None if the review_id is not in the dict"""
149
"""pass a review id and get a True/False useful back or None if the
150
review_id is not in the dict
142
152
return self.USEFULNESS_CACHE.get(str(review_id))
146
155
class Review(object):
147
156
"""A individual review object """
163
172
self.version = ""
164
173
self.usefulness_total = 0
165
174
self.usefulness_favorable = 0
166
# this will be set if tryint to submit usefulness for this review failed
175
# this will be set if tryint to submit usefulness for this review
167
177
self.usefulness_submit_error = False
168
178
self.delete_error = False
169
179
self.modify_error = False
170
181
def __repr__(self):
171
182
return "[Review id=%s review_text='%s' reviewer_username='%s']" % (
172
183
self.id, self.review_text, self.reviewer_username)
173
185
def __cmp__(self, other):
174
186
# first compare version, high version number first
175
187
vc = upstream_version_compare(self.version, other.version)
178
190
# then wilson score
179
uc = cmp(wilson_score(self.usefulness_favorable,
191
uc = cmp(wilson_score(self.usefulness_favorable,
180
192
self.usefulness_total),
181
193
wilson_score(other.usefulness_favorable,
182
194
other.usefulness_total))
209
222
setattr(review, k, v)
212
226
class ReviewLoader(GObject.GObject):
213
227
"""A loader that returns a review object list"""
216
"refresh-review-stats-finished" : (GObject.SIGNAL_RUN_LAST,
218
(GObject.TYPE_PYOBJECT,),
230
"refresh-review-stats-finished": (GObject.SIGNAL_RUN_LAST,
232
(GObject.TYPE_PYOBJECT,),
222
236
# cache the ReviewStats
236
250
self.REVIEW_STATS_CACHE_FILE = os.path.join(SOFTWARE_CENTER_CACHE_DIR,
238
252
self.REVIEW_STATS_BSDDB_FILE = "%s__%s.%s.db" % (
239
self.REVIEW_STATS_CACHE_FILE,
240
bdb.DB_VERSION_MAJOR,
253
self.REVIEW_STATS_CACHE_FILE,
254
bdb.DB_VERSION_MAJOR,
241
255
bdb.DB_VERSION_MINOR)
243
257
self.language = get_language()
244
258
if os.path.exists(self.REVIEW_STATS_CACHE_FILE):
246
self.REVIEW_STATS_CACHE = pickle.load(open(self.REVIEW_STATS_CACHE_FILE))
260
self.REVIEW_STATS_CACHE = pickle.load(
261
open(self.REVIEW_STATS_CACHE_FILE))
247
262
self._cache_version_old = self._missing_histogram_in_cache()
249
264
LOG.exception("review stats cache load failure")
250
os.rename(self.REVIEW_STATS_CACHE_FILE, self.REVIEW_STATS_CACHE_FILE+".fail")
265
os.rename(self.REVIEW_STATS_CACHE_FILE,
266
self.REVIEW_STATS_CACHE_FILE + ".fail")
252
268
def _missing_histogram_in_cache(self):
253
269
'''iterate through review stats to see if it has been fully reloaded
254
270
with new histogram data from server update'''
261
277
def get_reviews(self, application, callback, page=1, language=None,
262
278
sort=0, relaxed=False):
263
"""run callback f(app, review_list)
279
"""run callback f(app, review_list)
264
280
with list of review objects for the given
265
281
db.database.Application object
322
337
""" write out the full REVIEWS_STATS_CACHE as a pickle """
323
338
pickle.dump(self.REVIEW_STATS_CACHE,
324
339
open(self.REVIEW_STATS_CACHE_FILE, "w"))
326
341
def _dump_bsddbm_for_unity(self, outfile, outdir):
327
342
""" write out the subset that unity needs of the REVIEW_STATS_CACHE
328
343
as a C friendly (using struct) bsddb
330
345
env = bdb.DBEnv()
331
346
if not os.path.exists(outdir):
332
347
os.makedirs(outdir)
334
bdb.DB_CREATE | bdb.DB_INIT_CDB | bdb.DB_INIT_MPOOL |
335
bdb.DB_NOMMAP, # be gentle on e.g. nfs mounts
349
bdb.DB_CREATE | bdb.DB_INIT_CDB | bdb.DB_INIT_MPOOL |
350
bdb.DB_NOMMAP, # be gentle on e.g. nfs mounts
342
357
for (app, stats) in self.REVIEW_STATS_CACHE.iteritems():
343
358
# pkgname is ascii by policy, so its fine to use str() here
344
db[str(app.pkgname)] = struct.pack('iii',
359
db[str(app.pkgname)] = struct.pack('iii',
345
360
stats.ratings_average or 0,
346
361
stats.ratings_total,
347
362
stats.dampened_rating)
351
366
def get_top_rated_apps(self, quantity=12, category=None):
352
367
"""Returns a list of the packages with the highest 'rating' based on
353
368
the dampened rating calculated from the ReviewStats rating spread.
354
369
Also optionally takes a category (string) to filter by"""
356
371
cache = self.REVIEW_STATS_CACHE
359
374
applist = self._get_apps_for_category(category)
360
375
cache = self._filter_cache_with_applist(cache, applist)
362
377
#create a list of tuples with (Application,dampened_rating)
364
379
for item in cache.items():
365
if hasattr(item[1],'dampened_rating'):
380
if hasattr(item[1], 'dampened_rating'):
366
381
dr_list.append((item[0], item[1].dampened_rating))
368
383
dr_list.append((item[0], 3.00))
370
385
#sorted the list descending by dampened rating
371
386
sorted_dr_list = sorted(dr_list, key=operator.itemgetter(1),
374
389
#return the quantity requested or as much as we can
375
390
if quantity < len(sorted_dr_list):
376
391
return_qty = quantity
378
393
return_qty = len(sorted_dr_list)
381
for i in range (0,return_qty):
396
for i in range(0, return_qty):
382
397
top_rated.append(sorted_dr_list[i][0])
386
401
def _filter_cache_with_applist(self, cache, applist):
387
402
"""Take the review cache and filter it to only include the apps that
388
403
also appear in the applist passed in"""
391
406
if key.pkgname in applist:
392
407
filtered_cache[key] = cache[key]
393
408
return filtered_cache
395
410
def _get_apps_for_category(self, category):
396
411
query = get_query_for_category(self.db, category)
398
413
LOG.warn("_get_apps_for_category: received invalid category")
401
416
pathname = os.path.join(XAPIAN_BASE_PATH, "xapian")
402
417
db = StoreDatabase(pathname, self.cache)
404
419
docs = db.get_docs_from_query(query)
406
421
#from the db docs, return a list of pkgnames
409
424
applist.append(db.get_pkgname(doc))
412
def spawn_write_new_review_ui(self, translated_app, version, iconname,
427
def spawn_write_new_review_ui(self, translated_app, version, iconname,
413
428
origin, parent_xid, datadir, callback):
414
429
"""Spawn the UI for writing a new review and adds it automatically
415
430
to the reviews DB.
432
448
"""Spawn a helper to delete a review."""
435
def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):
451
def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id,
436
453
"""Spawn a helper to modify a review."""
440
457
class ReviewLoaderFake(ReviewLoader):
442
USERS = ["Joe Doll", "John Foo", "Cat Lala", "Foo Grumpf", "Bar Tender", "Baz Lightyear"]
459
USERS = ["Joe Doll", "John Foo", "Cat Lala", "Foo Grumpf", "Bar Tender",
443
461
SUMMARIES = ["Cool", "Medium", "Bad", "Too difficult"]
444
462
IPSUM = "no ipsum\n\nstill no ipsum"
447
465
ReviewLoader.__init__(self, cache, db)
448
466
self._review_stats_cache = {}
449
467
self._reviews_cache = {}
450
469
def _random_person(self):
451
470
return random.choice(self.USERS)
452
472
def _random_text(self):
453
473
return random.choice(self.LOREM.split("\n\n"))
454
475
def _random_summary(self):
455
476
return random.choice(self.SUMMARIES)
456
478
def get_reviews(self, application, callback, page=1, language=None,
457
479
sort=0, relaxed=False):
458
480
if not application in self._review_stats_cache:
463
485
for i in range(0, stats.ratings_total):
464
486
review = Review(application)
465
review.id = random.randint(1,50000)
487
review.id = random.randint(1, 50000)
466
488
# FIXME: instead of random, try to match the avg_rating
467
review.rating = random.randint(1,5)
489
review.rating = random.randint(1, 5)
468
490
review.summary = self._random_summary()
469
491
review.date_created = time.strftime("%Y-%m-%d %H:%M:%S")
470
492
review.reviewer_username = self._random_person()
471
review.review_text = self._random_text().replace("\n","")
493
review.review_text = self._random_text().replace("\n", "")
472
494
review.usefulness_total = random.randint(1, 20)
473
495
review.usefulness_favorable = random.randint(1, 20)
474
496
reviews.append(review)
475
497
self._reviews_cache[application] = reviews
476
498
reviews = self._reviews_cache[application]
477
499
callback(application, reviews)
478
501
def get_review_stats(self, application):
479
502
if not application in self._review_stats_cache:
480
503
stat = ReviewStats(application)
481
stat.ratings_average = random.randint(1,5)
482
stat.ratings_total = random.randint(1,20)
504
stat.ratings_average = random.randint(1, 5)
505
stat.ratings_total = random.randint(1, 20)
483
506
self._review_stats_cache[application] = stat
484
507
return self._review_stats_cache[application]
485
509
def refresh_review_stats(self, callback):
486
510
review_stats = []
487
511
callback(review_stats)
489
514
class ReviewLoaderFortune(ReviewLoaderFake):
490
515
def __init__(self, cache, db):
491
516
ReviewLoaderFake.__init__(self, cache, db)
493
518
for i in range(10):
494
out = subprocess.Popen(["fortune"], stdout=subprocess.PIPE).communicate()[0]
519
out = subprocess.Popen(["fortune"],
520
stdout=subprocess.PIPE).communicate()[0]
495
521
self.LOREM += "\n\n%s" % out
497
524
class ReviewLoaderTechspeak(ReviewLoaderFake):
498
525
""" a test review loader that does not do any network io
499
526
and returns random review texts
501
LOREM=u"""This package is using cloud based technology that will
528
LOREM = u"""This package is using cloud based technology that will
502
529
make it suitable in a distributed environment where soup and xml-rpc
503
530
are used. The backend is written in C++ but the frontend code will
504
531
utilize dynamic languages lika LUA to provide a execution environment
675
707
cache = get_pkg_info()
678
db = StoreDatabase(XAPIAN_BASE_PATH+"/xapian", cache)
710
db = StoreDatabase(XAPIAN_BASE_PATH + "/xapian", cache)
681
713
# rnrclient loader
682
714
app = Application("ACE", "unace")
683
715
#app = Application("", "2vcard")
685
from softwarecenter.backend.reviews.rnr import ReviewLoaderSpawningRNRClient
717
from softwarecenter.backend.reviews.rnr import (
718
ReviewLoaderSpawningRNRClient
686
720
loader = ReviewLoaderSpawningRNRClient(cache, db)
687
721
print loader.refresh_review_stats(stats_callback)
688
722
print loader.get_reviews(app, callback)