482
448
operation is complete it will call callback with the updated
485
cmd = [os.path.join(datadir, RNRApps.REPORT_REVIEW),
486
"--review-id", review_id,
487
"--parent-xid", "%s" % parent_xid,
488
"--datadir", datadir,
490
spawn_helper = SpawnHelper("json")
491
spawn_helper.connect("exited",
492
self._on_report_abuse_finished,
494
spawn_helper.run(cmd)
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)
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)
509
453
def spawn_submit_usefulness_ui(self, review_id, is_useful, parent_xid, datadir, callback):
510
cmd = [os.path.join(datadir, RNRApps.SUBMIT_USEFULNESS),
511
"--review-id", "%s" % review_id,
512
"--is-useful", "%s" % int(is_useful),
513
"--parent-xid", "%s" % parent_xid,
514
"--datadir", datadir,
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,
523
spawn_helper.run(cmd)
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)
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
544
review.usefulness_favorable = getattr(review, "usefulness_favorable", 0) + 1
545
callback(app, self._reviews[app], useful_votes, 'replace', review)
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)
454
"""Spawn a helper to submit a usefulness vote."""
558
457
def spawn_delete_review_ui(self, review_id, parent_xid, datadir, callback):
559
cmd = [os.path.join(datadir, RNRApps.DELETE_REVIEW),
560
"--review-id", "%s" % review_id,
561
"--parent-xid", "%s" % parent_xid,
562
"--datadir", datadir,
564
spawn_helper = SpawnHelper(format="none")
565
spawn_helper.connect("exited",
566
self._on_delete_review_finished,
568
spawn_helper.connect("error", self._on_delete_review_error,
570
spawn_helper.run(cmd)
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)
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)
458
"""Spawn a helper to delete a review."""
595
461
def spawn_modify_review_ui(self, parent_xid, iconname, datadir, review_id, callback):
596
""" this spawns the UI for writing a new review and
597
adds it automatically to the reviews DB """
598
cmd = [os.path.join(datadir, RNRApps.MODIFY_REVIEW),
599
"--parent-xid", "%s" % parent_xid,
600
"--iconname", iconname,
601
"--datadir", "%s" % datadir,
602
"--review-id", "%s" % review_id,
604
spawn_helper = SpawnHelper(format="json")
605
spawn_helper.connect("data-available",
606
self._on_modify_review_finished,
608
spawn_helper.connect("error", self._on_modify_review_error,
610
spawn_helper.run(cmd)
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)
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)
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)
645
class ReviewLoaderSpawningRNRClient(ReviewLoader):
646
""" loader that uses multiprocessing to call rnrclient and
647
a glib timeout watcher that polls periodically for the
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)
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()
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
669
# its fine to use the translated appname here, we only submit the
670
# pkgname to the server
672
self._update_rnrclient_offline_state()
673
sort_method = self._review_sort_methods[sort]
675
language = self.language
676
# gather args for the helper
678
origin = self.cache.get_origin(app.pkgname)
680
# this can happen if e.g. the app has multiple origins, this
681
# will be handled later
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
688
origin = "lp-ppa-%s" % ppa.replace("/", "-")
689
# if there is no origin, there is nothing to do
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,
698
"--distroseries", distroseries,
699
"--pkgname", str(app.pkgname), # ensure its str, not unicode
701
"--sort", sort_method,
703
spawn_helper = SpawnHelper()
704
spawn_helper.connect(
705
"data-available", self._on_reviews_helper_data, app, callback)
706
spawn_helper.run(cmd)
708
def _on_reviews_helper_data(self, spawn_helper, piston_reviews, app, callback):
709
# convert into our review objects
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])
719
def refresh_review_stats(self, callback):
720
""" public api, refresh the available statistics """
722
mtime = os.path.getmtime(self.REVIEW_STATS_CACHE_FILE)
723
days_delta = int((time.time() - mtime) // (24*60*60))
727
LOG.debug("refresh with days_delta: %s" % days_delta)
729
#distroseries = self.distro.get_codename()
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
736
#"--distroseries", distroseries,
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)
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
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)
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)
760
s.rating_spread = json.loads(r.histogram)
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()
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)
776
class ReviewLoaderJsonAsync(ReviewLoader):
777
""" get json (or gzip compressed json) """
779
def _gio_review_download_complete_callback(self, source, result):
780
app = source.get_data("app")
781
callback = source.get_data("callback")
783
(success, json_str, etag) = source.load_contents_finish(result)
784
except GObject.GError:
785
# ignore read errors, most likely transient
786
return callback(app, [])
787
# check for gzip header
788
if json_str.startswith("\37\213"):
789
gz=gzip.GzipFile(fileobj=StringIO(json_str))
791
reviews_json = json.loads(json_str)
793
for review_json in reviews_json:
794
review = Review.from_json(review_json)
795
reviews.append(review)
797
callback(app, reviews)
799
def get_reviews(self, app, callback, page=1, language=None, sort=0):
800
""" get a specific review and call callback when its available"""
801
# FIXME: get this from the app details
802
origin = self.cache.get_origin(app.pkgname)
803
distroseries = self.distro.get_codename()
805
appname = ";"+app.appname
809
sort_method = self._review_sort_methods[sort]
811
url = self.distro.REVIEWS_URL % { 'pkgname' : app.pkgname,
812
'appname' : quote_plus(appname.encode("utf-8")),
813
'language' : self.language,
815
'distroseries' : distroseries,
817
'sort' : sort_method,
819
LOG.debug("looking for review at '%s'" % url)
820
f=Gio.File.new_for_uri(url)
821
f.set_data("app", app)
822
f.set_data("callback", callback)
823
f.load_contents_async(self._gio_review_download_complete_callback)
826
def _gio_review_stats_download_finished_callback(self, source, result):
827
callback = source.get_data("callback")
829
(json_str, length, etag) = source.load_contents_finish(result)
830
except GObject.GError:
831
# ignore read errors, most likely transient
833
# check for gzip header
834
if json_str.startswith("\37\213"):
835
gz=gzip.GzipFile(fileobj=StringIO(json_str))
837
review_stats_json = json.loads(json_str)
839
for review_stat_json in review_stats_json:
840
#appname = review_stat_json["app_name"]
841
pkgname = review_stat_json["package_name"]
842
app = Application('', pkgname)
843
stats = ReviewStats(app)
844
stats.ratings_total = int(review_stat_json["ratings_total"])
845
stats.ratings_average = float(review_stat_json["ratings_average"])
846
review_stats[app] = stats
847
# update review_stats dict
848
self.REVIEW_STATS_CACHE = review_stats
849
self.save_review_stats_cache_file()
851
callback(review_stats)
853
def refresh_review_stats(self, callback):
854
""" get the review statists and call callback when its there """
855
f=Gio.File(self.distro.REVIEW_STATS_URL)
856
f.set_data("callback", callback)
857
f.load_contents_async(self._gio_review_stats_download_finished_callback)
462
"""Spawn a helper to modify a review."""
859
466
class ReviewLoaderFake(ReviewLoader):