# -*- coding: utf-8 -*-
# Copyright (C) 2009 Canonical
#
# Authors:
# Michael Vogt
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from gi.repository import GObject, Gtk, Gdk
import datetime
import gettext
import logging
import os
import json
import sys
import tempfile
import time
import threading
# py3
try:
from urllib.request import urlopen
urlopen # pyflakes
from queue import Queue
Queue # pyflakes
except ImportError:
# py2 fallbacks
from urllib import urlopen
from Queue import Queue
from gettext import gettext as _
from softwarecenter.backend.ubuntusso import get_ubuntu_sso_backend
import piston_mini_client
from softwarecenter.paths import SOFTWARE_CENTER_CONFIG_DIR
from softwarecenter.enums import Icons, SOFTWARE_CENTER_NAME_KEYRING
from softwarecenter.config import get_config
from softwarecenter.distro import get_distro, get_current_arch
from softwarecenter.backend.login_sso import get_sso_backend
from softwarecenter.backend.reviews import Review
from softwarecenter.db.database import Application
from softwarecenter.gwibber_helper import GwibberHelper, GwibberHelperMock
from softwarecenter.i18n import get_language
from softwarecenter.ui.gtk3.SimpleGtkbuilderApp import SimpleGtkbuilderApp
from softwarecenter.ui.gtk3.dialogs import SimpleGtkbuilderDialog
from softwarecenter.ui.gtk3.widgets.stars import ReactiveStar
from softwarecenter.utils import make_string_from_list, utf8
from softwarecenter.backend.piston.rnrclient import RatingsAndReviewsAPI
from softwarecenter.backend.piston.rnrclient_pristine import ReviewRequest
# get current distro and set default server root
distro = get_distro()
SERVER_ROOT = distro.REVIEWS_SERVER
# server status URL
SERVER_STATUS_URL = SERVER_ROOT + "/server-status/"
class UserCancelException(Exception):
""" user pressed cancel """
pass
TRANSMIT_STATE_NONE = "transmit-state-none"
TRANSMIT_STATE_INPROGRESS = "transmit-state-inprogress"
TRANSMIT_STATE_DONE = "transmit-state-done"
TRANSMIT_STATE_ERROR = "transmit-state-error"
class GRatingsAndReviews(GObject.GObject):
""" Access ratings&reviews API as a gobject """
__gsignals__ = {
# send when a transmit is started
"transmit-start": (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_PYOBJECT, ),
),
# send when a transmit was successful
"transmit-success": (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_PYOBJECT, ),
),
# send when a transmit failed
"transmit-failure": (GObject.SIGNAL_RUN_LAST,
GObject.TYPE_NONE,
(GObject.TYPE_PYOBJECT, str),
),
}
def __init__(self, token):
super(GRatingsAndReviews, self).__init__()
# piston worker thread
self.worker_thread = Worker(token)
self.worker_thread.start()
GObject.timeout_add(500,
self._check_thread_status,
None)
def submit_review(self, review):
self.emit("transmit-start", review)
self.worker_thread.pending_reviews.put(review)
def report_abuse(self, review_id, summary, text):
self.emit("transmit-start", review_id)
self.worker_thread.pending_reports.put((int(review_id), summary, text))
def submit_usefulness(self, review_id, is_useful):
self.emit("transmit-start", review_id)
self.worker_thread.pending_usefulness.put((int(review_id), is_useful))
def modify_review(self, review_id, review):
self.emit("transmit-start", review_id)
self.worker_thread.pending_modify.put((int(review_id), review))
def delete_review(self, review_id):
self.emit("transmit-start", review_id)
self.worker_thread.pending_delete.put(int(review_id))
def server_status(self):
self.worker_thread.pending_server_status()
def shutdown(self):
self.worker_thread.shutdown()
# internal
def _check_thread_status(self, data):
if self.worker_thread._transmit_state == TRANSMIT_STATE_DONE:
self.emit("transmit-success", "")
self.worker_thread._transmit_state = TRANSMIT_STATE_NONE
elif self.worker_thread._transmit_state == TRANSMIT_STATE_ERROR:
self.emit("transmit-failure", "",
self.worker_thread._transmit_error_str)
self.worker_thread._transmit_state = TRANSMIT_STATE_NONE
return True
class Worker(threading.Thread):
def __init__(self, token):
# init parent
threading.Thread.__init__(self)
self.pending_reviews = Queue()
self.pending_reports = Queue()
self.pending_usefulness = Queue()
self.pending_modify = Queue()
self.pending_delete = Queue()
self.pending_server_status = Queue()
self._shutdown = False
# FIXME: instead of a binary value we need the state associated
# with each request from the queue
self._transmit_state = TRANSMIT_STATE_NONE
self._transmit_error_str = ""
self.display_name = "No display name"
auth = piston_mini_client.auth.OAuthAuthorizer(token["token"],
token["token_secret"], token["consumer_key"],
token["consumer_secret"])
# change default server to the SSL one
distro = get_distro()
service_root = distro.REVIEWS_SERVER
self.rnrclient = RatingsAndReviewsAPI(service_root=service_root,
auth=auth)
def run(self):
"""Main thread run interface, logs into launchpad and waits
for commands
"""
logging.debug("worker thread run")
# loop
self._wait_for_commands()
def shutdown(self):
"""Request shutdown"""
self._shutdown = True
def _wait_for_commands(self):
"""internal helper that waits for commands"""
while True:
#logging.debug("worker: _wait_for_commands")
self._submit_reviews_if_pending()
self._submit_reports_if_pending()
self._submit_usefulness_if_pending()
self._submit_modify_if_pending()
self._submit_delete_if_pending()
time.sleep(0.2)
if (self._shutdown and
self.pending_reviews.empty() and
self.pending_usefulness.empty() and
self.pending_reports.empty() and
self.pending_modify.empty() and
self.pending_delete.empty()):
return
def _submit_usefulness_if_pending(self):
""" the actual usefulness function """
while not self.pending_usefulness.empty():
logging.debug("POST usefulness")
self._transmit_state = TRANSMIT_STATE_INPROGRESS
(review_id, is_useful) = self.pending_usefulness.get()
try:
res = self.rnrclient.submit_usefulness(
review_id=review_id, useful=str(is_useful))
self._transmit_state = TRANSMIT_STATE_DONE
sys.stdout.write(json.dumps(res))
except Exception as e:
logging.exception("submit_usefulness failed")
err_str = self._get_error_messages(e)
self._transmit_error_str = err_str
self._write_exception_html_log_if_needed(e)
self._transmit_state = TRANSMIT_STATE_ERROR
self.pending_usefulness.task_done()
def _submit_modify_if_pending(self):
""" the actual modify function """
while not self.pending_modify.empty():
logging.debug("_modify_review")
self._transmit_state = TRANSMIT_STATE_INPROGRESS
(review_id, review) = self.pending_modify.get()
summary = review['summary']
review_text = review['review_text']
rating = review['rating']
try:
res = self.rnrclient.modify_review(review_id=review_id,
summary=summary,
review_text=review_text,
rating=rating)
self._transmit_state = TRANSMIT_STATE_DONE
sys.stdout.write(json.dumps(vars(res)))
except Exception as e:
logging.exception("modify_review")
err_str = self._get_error_messages(e)
self._write_exception_html_log_if_needed(e)
self._transmit_state = TRANSMIT_STATE_ERROR
self._transmit_error_str = err_str
self.pending_modify.task_done()
def _submit_delete_if_pending(self):
""" the actual deletion """
while not self.pending_delete.empty():
logging.debug("POST delete")
self._transmit_state = TRANSMIT_STATE_INPROGRESS
review_id = self.pending_delete.get()
try:
res = self.rnrclient.delete_review(review_id=review_id)
self._transmit_state = TRANSMIT_STATE_DONE
sys.stdout.write(json.dumps(res))
except Exception as e:
logging.exception("delete_review failed")
self._write_exception_html_log_if_needed(e)
self._transmit_error_str = _("Failed to delete review")
self._transmit_state = TRANSMIT_STATE_ERROR
self.pending_delete.task_done()
def _submit_reports_if_pending(self):
""" the actual report function """
while not self.pending_reports.empty():
logging.debug("POST report")
self._transmit_state = TRANSMIT_STATE_INPROGRESS
(review_id, summary, text) = self.pending_reports.get()
try:
res = self.rnrclient.flag_review(review_id=review_id,
reason=summary,
text=text)
self._transmit_state = TRANSMIT_STATE_DONE
sys.stdout.write(json.dumps(res))
except Exception as e:
logging.exception("flag_review failed")
err_str = self._get_error_messages(e)
self._transmit_error_str = err_str
self._write_exception_html_log_if_needed(e)
self._transmit_state = TRANSMIT_STATE_ERROR
self.pending_reports.task_done()
def _write_exception_html_log_if_needed(self, e):
# write out a "oops.html"
if type(e) is piston_mini_client.APIError:
f = tempfile.NamedTemporaryFile(
prefix="sc_submit_oops_", suffix=".html", delete=False)
# new piston-mini-client has only the body of the returned data
# older just pushes it into a big string
if hasattr(e, "body") and e.body:
f.write(e.body)
else:
f.write(str(e))
# reviews
def queue_review(self, review):
""" queue a new review for sending to LP """
logging.debug("queue_review %s" % review)
self.pending_reviews.put(review)
def _submit_reviews_if_pending(self):
""" the actual submit function """
while not self.pending_reviews.empty():
logging.debug("_submit_review")
self._transmit_state = TRANSMIT_STATE_INPROGRESS
review = self.pending_reviews.get()
piston_review = ReviewRequest()
piston_review.package_name = review.app.pkgname
piston_review.app_name = review.app.appname
piston_review.summary = review.summary
piston_review.version = review.package_version
piston_review.review_text = review.text
piston_review.date = str(review.date)
piston_review.rating = review.rating
piston_review.language = review.language
piston_review.arch_tag = get_current_arch()
piston_review.origin = review.origin
piston_review.distroseries = distro.get_codename()
try:
res = self.rnrclient.submit_review(review=piston_review)
self._transmit_state = TRANSMIT_STATE_DONE
# output the resulting ReviewDetails object as json so
# that the parent can read it
sys.stdout.write(json.dumps(vars(res)))
except Exception as e:
logging.exception("submit_review")
err_str = self._get_error_messages(e)
self._write_exception_html_log_if_needed(e)
self._transmit_state = TRANSMIT_STATE_ERROR
self._transmit_error_str = err_str
self.pending_reviews.task_done()
def _get_error_messages(self, e):
if type(e) is piston_mini_client.APIError:
try:
logging.warning(e.body)
error_msg = json.loads(e.body)['errors']
errs = error_msg["__all__"]
err_str = _("Server's response was:")
for err in errs:
err_str = _("%s\n%s") % (err_str, err)
except:
err_str = _("Unknown error communicating with server. "
"Check your log and consider raising a bug report "
"if this problem persists")
logging.warning(e)
else:
err_str = _("Unknown error communicating with server. Check "
"your log and consider raising a bug report if this "
"problem persists")
logging.warning(e)
return err_str
def verify_server_status(self):
""" verify that the server we want to talk to can be reached
this method should be overriden if clients talk to a different
server than rnr
"""
try:
resp = urlopen(SERVER_STATUS_URL).read()
if resp != "ok":
return False
except Exception as e:
logging.error("exception from '%s': '%s'" % (SERVER_STATUS_URL, e))
return False
return True
class BaseApp(SimpleGtkbuilderApp):
def __init__(self, datadir, uifile):
SimpleGtkbuilderApp.__init__(
self, os.path.join(datadir, "ui/gtk3", uifile), "software-center")
# generic data
self.token = None
self.display_name = None
self._login_successful = False
self._whoami_token_reset_nr = 0
#persistent config
configfile = os.path.join(
SOFTWARE_CENTER_CONFIG_DIR, "submit_reviews.cfg")
self.config = get_config(configfile)
# status spinner
self.status_spinner = Gtk.Spinner()
self.status_spinner.set_size_request(32, 32)
self.login_spinner_vbox.pack_start(self.status_spinner, False, False,
0)
self.login_spinner_vbox.reorder_child(self.status_spinner, 0)
self.status_spinner.show()
#submit status spinner
self.submit_spinner = Gtk.Spinner()
self.submit_spinner.set_size_request(*Gtk.icon_size_lookup(
Gtk.IconSize.SMALL_TOOLBAR)[:2])
#submit error image
self.submit_error_img = Gtk.Image()
self.submit_error_img.set_from_stock(Gtk.STOCK_DIALOG_ERROR,
Gtk.IconSize.SMALL_TOOLBAR)
#submit success image
self.submit_success_img = Gtk.Image()
self.submit_success_img.set_from_stock(Gtk.STOCK_APPLY,
Gtk.IconSize.SMALL_TOOLBAR)
#submit warn image
self.submit_warn_img = Gtk.Image()
self.submit_warn_img.set_from_stock(Gtk.STOCK_DIALOG_INFO,
Gtk.IconSize.SMALL_TOOLBAR)
#label size to prevent image or spinner from resizing
self.label_transmit_status.set_size_request(-1,
Gtk.icon_size_lookup(Gtk.IconSize.SMALL_TOOLBAR)[1])
def _get_parent_xid_for_login_window(self):
# no get_xid() yet in gir world
#return self.submit_window.get_window().get_xid()
return ""
def run(self):
# initially display a 'Connecting...' page
self.main_notebook.set_current_page(0)
self.login_status_label.set_markup(_(u"Signing in\u2026"))
self.status_spinner.start()
self.submit_window.show()
# now run the loop
self.login()
def quit(self, exitcode=0):
sys.exit(exitcode)
def _add_spellcheck_to_textview(self, textview):
""" adds a spellchecker (if available) to the given Gtk.textview """
pass
#~ try:
#~ import gtkspell
#~ # mvo: gtkspell.get_from_text_view() is broken, so we use this
#~ # method instead, the second argument is the language to
#~ # use (that is directly passed to pspell)
#~ spell = gtkspell.Spell(textview, None)
#~ except:
#~ return
#~ return spell
def login(self, show_register=True):
logging.debug("login()")
login_window_xid = self._get_parent_xid_for_login_window()
help_text = _("To review software or to report abuse you need to "
"sign in to a Ubuntu Single Sign-On account.")
self.sso = get_sso_backend(login_window_xid,
SOFTWARE_CENTER_NAME_KEYRING, help_text)
self.sso.connect("login-successful", self._maybe_login_successful)
self.sso.connect("login-canceled", self._login_canceled)
if show_register:
self.sso.login_or_register()
else:
self.sso.login()
def _login_canceled(self, sso):
self.status_spinner.hide()
self.login_status_label.set_markup(
'%s' % _("Login was canceled"))
def _maybe_login_successful(self, sso, oauth_result):
"""called after we have the token, then we go and figure out our
name
"""
logging.debug("_maybe_login_successful")
self.token = oauth_result
self.ssoapi = get_ubuntu_sso_backend()
self.ssoapi.connect("whoami", self._whoami_done)
self.ssoapi.connect("error", self._whoami_error)
# this will automatically verify the token and retrigger login
# if its expired
self.ssoapi.whoami()
def _whoami_done(self, ssologin, result):
logging.debug("_whoami_done")
self.display_name = result["displayname"]
self._create_gratings_api()
self.login_successful(self.display_name)
def _whoami_error(self, ssologin, e):
logging.error("whoami error '%s'" % e)
# show error
self.status_spinner.hide()
self.login_status_label.set_markup(
'%s' % _("Failed to log in"))
def login_successful(self, display_name):
""" callback when the login was successful """
pass
def on_button_cancel_clicked(self, button=None):
# bring it down gracefully
if hasattr(self, "api"):
self.api.shutdown()
while Gtk.events_pending():
Gtk.main_iteration()
self.quit(1)
def _create_gratings_api(self):
self.api = GRatingsAndReviews(self.token)
self.api.connect("transmit-start", self.on_transmit_start)
self.api.connect("transmit-success", self.on_transmit_success)
self.api.connect("transmit-failure", self.on_transmit_failure)
def on_transmit_start(self, api, trans):
self.button_post.set_sensitive(False)
self.button_cancel.set_sensitive(False)
self._change_status("progress", _(self.SUBMIT_MESSAGE))
def on_transmit_success(self, api, trans):
self.api.shutdown()
self.quit()
def on_transmit_failure(self, api, trans, error):
self._change_status("fail", error)
self.button_post.set_sensitive(True)
self.button_cancel.set_sensitive(True)
def _change_status(self, type, message):
"""method to separate the updating of status icon/spinner and
message in the submit review window, takes a type (progress,
fail, success, clear, warning) as a string and a message
string then updates status area accordingly
"""
self._clear_status_imagery()
self.label_transmit_status.set_text("")
if type == "progress":
self.status_hbox.pack_start(self.submit_spinner, False, False, 0)
self.status_hbox.reorder_child(self.submit_spinner, 0)
self.submit_spinner.show()
self.submit_spinner.start()
self.label_transmit_status.set_text(message)
elif type == "fail":
self.status_hbox.pack_start(self.submit_error_img, False, False, 0)
self.status_hbox.reorder_child(self.submit_error_img, 0)
self.submit_error_img.show()
self.label_transmit_status.set_text(_(self.FAILURE_MESSAGE))
self.error_textview.get_buffer().set_text(_(message))
self.detail_expander.show()
elif type == "success":
self.status_hbox.pack_start(self.submit_success_img, False, False,
0)
self.status_hbox.reorder_child(self.submit_success_img, 0)
self.submit_success_img.show()
self.label_transmit_status.set_text(message)
elif type == "warning":
self.status_hbox.pack_start(self.submit_warn_img, False, False, 0)
self.status_hbox.reorder_child(self.submit_warn_img, 0)
self.submit_warn_img.show()
self.label_transmit_status.set_text(message)
def _clear_status_imagery(self):
self.detail_expander.hide()
self.detail_expander.set_expanded(False)
#clears spinner or error image from dialog submission label
# before trying to display one or the other
if self.submit_spinner.get_parent():
self.status_hbox.remove(self.submit_spinner)
if self.submit_error_img.get_window():
self.status_hbox.remove(self.submit_error_img)
if self.submit_success_img.get_window():
self.status_hbox.remove(self.submit_success_img)
if self.submit_warn_img.get_window():
self.status_hbox.remove(self.submit_warn_img)
class SubmitReviewsApp(BaseApp):
""" review a given application or package """
STAR_SIZE = (32, 32)
APP_ICON_SIZE = 48
#character limits for text boxes and hurdles for indicator changes
# (overall field maximum, limit to display warning, limit to change
# colour)
SUMMARY_CHAR_LIMITS = (80, 60, 70)
REVIEW_CHAR_LIMITS = (5000, 4900, 4950)
#alert colours for character warning labels
NORMAL_COLOUR = "000000"
ERROR_COLOUR = "FF0000"
SUBMIT_MESSAGE = _("Submitting Review")
FAILURE_MESSAGE = _("Failed to submit review")
SUCCESS_MESSAGE = _("Review submitted")
def __init__(self, app, version, iconname, origin, parent_xid, datadir,
action="submit", review_id=0):
BaseApp.__init__(self, datadir, "submit_review.ui")
self.datadir = datadir
# legal fineprint, do not change without consulting a lawyer
msg = _("By submitting this review, you agree not to include "
"anything defamatory, infringing, or illegal. Canonical "
"may, at its discretion, publish your name and review in "
"Ubuntu Software Center and elsewhere, and allow the "
"software or content author to publish it too.")
self.label_legal_fineprint.set_markup(
'%s' % msg)
# additional icons come from app-install-data
self.icons = Gtk.IconTheme.get_default()
self.icons.append_search_path("/usr/share/app-install/icons/")
self.submit_window.connect("destroy", self.on_button_cancel_clicked)
self._add_spellcheck_to_textview(self.textview_review)
self.star_rating = ReactiveStar()
alignment = Gtk.Alignment.new(0.0, 0.5, 1.0, 1.0)
alignment.set_padding(3, 3, 3, 3)
alignment.add(self.star_rating)
self.star_rating.set_size_as_pixel_value(36)
self.star_caption = Gtk.Label()
alignment.show_all()
self.rating_hbox.pack_start(alignment, True, True, 0)
self.rating_hbox.reorder_child(alignment, 0)
self.rating_hbox.pack_start(self.star_caption, False, False, 0)
self.rating_hbox.reorder_child(self.star_caption, 1)
self.review_buffer = self.textview_review.get_buffer()
self.detail_expander.hide()
self.retrieve_api = RatingsAndReviewsAPI()
# data
self.app = app
self.version = version
self.origin = origin
self.iconname = iconname
self.action = action
self.review_id = int(review_id)
# parent xid
#~ if parent_xid:
#~ win = Gdk.Window.foreign_new(int(parent_xid))
#~ wnck_get_xid_from_pid(os.getpid())
#~ win = ''
#~ self.review_buffer.set_text(str(win))
#~ if win:
#~ self.submit_window.realize()
#~ self.submit_window.get_window().set_transient_for(win)
self.submit_window.set_position(Gtk.WindowPosition.MOUSE)
self._confirm_cancel_yes_handler = 0
self._confirm_cancel_no_handler = 0
self._displaying_cancel_confirmation = False
self.submit_window.connect("key-press-event", self._on_key_press_event)
self.review_summary_entry.connect('changed',
self._on_mandatory_text_entry_changed)
self.star_rating.connect('changed', self._on_mandatory_fields_changed)
self.review_buffer.connect('changed', self._on_text_entry_changed)
# gwibber stuff
self.gwibber_combo = Gtk.ComboBoxText.new()
#cells = self.gwibber_combo.get_cells()
#cells[0].set_property("ellipsize", pango.ELLIPSIZE_END)
self.gwibber_hbox.pack_start(self.gwibber_combo, True, True, 0)
if "SOFTWARE_CENTER_GWIBBER_MOCK_USERS" in os.environ:
self.gwibber_helper = GwibberHelperMock()
else:
self.gwibber_helper = GwibberHelper()
# get a dict with a saved gwibber_send (boolean) and gwibber
# account_id for persistent state
self.gwibber_prefs = self._get_gwibber_prefs()
# gwibber stuff
self._setup_gwibber_gui()
#now setup rest of app based on whether submit or modify
if self.action == "submit":
self._init_submit()
elif self.action == "modify":
self._init_modify()
def _init_submit(self):
self.submit_window.set_title(_("Review %s") %
gettext.dgettext("app-install-data", self.app.name))
def _init_modify(self):
self._populate_review()
self.submit_window.set_title(_("Modify Your %(appname)s Review") % {
'appname': gettext.dgettext("app-install-data", self.app.name)})
self.button_post.set_label(_("Modify"))
self.SUBMIT_MESSAGE = _("Updating your review")
self.FAILURE_MESSAGE = _("Failed to edit review")
self.SUCCESS_MESSAGE = _("Review updated")
self._enable_or_disable_post_button()
def _populate_review(self):
try:
review_data = self.retrieve_api.get_review(
review_id=self.review_id)
app = Application(appname=review_data.app_name,
pkgname=review_data.package_name)
self.app = app
self.review_summary_entry.set_text(review_data.summary)
self.star_rating.set_rating(review_data.rating)
self.review_buffer.set_text(review_data.review_text)
# save original review field data, for comparison purposes when
# user makes changes to fields
self.orig_summary_text = review_data.summary
self.orig_star_rating = review_data.rating
self.orig_review_text = review_data.review_text
self.version = review_data.version
self.origin = review_data.origin
except piston_mini_client.APIError:
logging.warn(
'Unable to retrieve review id %s for editing. Exiting' %
self.review_id)
self.quit(2)
def _setup_details(self, widget, app, iconname, version, display_name):
# icon shazam
try:
icon = self.icons.load_icon(iconname, self.APP_ICON_SIZE, 0)
except:
icon = self.icons.load_icon(Icons.MISSING_APP, self.APP_ICON_SIZE,
0)
self.review_appicon.set_from_pixbuf(icon)
# title
app = utf8(gettext.dgettext("app-install-data", app.name))
version = utf8(version)
self.review_title.set_markup(
'%s\n%s' % (app, version))
# review label
self.review_label.set_markup(_('Review by: %s') %
display_name.encode('utf8'))
# review summary label
self.review_summary_label.set_markup(_('Summary:'))
#rating label
self.rating_label.set_markup(_('Rating:'))
#error detail link label
self.label_expander.set_markup('%s' %
(_('Error Details')))
def _has_user_started_reviewing(self):
summary_chars = self.review_summary_entry.get_text_length()
review_chars = self.review_buffer.get_char_count()
return summary_chars > 0 or review_chars > 0
def _on_mandatory_fields_changed(self, *args):
self._enable_or_disable_post_button()
def _on_mandatory_text_entry_changed(self, widget):
self._check_summary_character_count()
self._on_mandatory_fields_changed(widget)
def _on_text_entry_changed(self, widget):
self._check_review_character_count()
self._on_mandatory_fields_changed(widget)
def _enable_or_disable_post_button(self):
summary_chars = self.review_summary_entry.get_text_length()
review_chars = self.review_buffer.get_char_count()
if (summary_chars and summary_chars <= self.SUMMARY_CHAR_LIMITS[0] and
review_chars and review_chars <= self.REVIEW_CHAR_LIMITS[0] and
int(self.star_rating.get_rating()) > 0):
self.button_post.set_sensitive(True)
self._change_status("clear", "")
else:
self.button_post.set_sensitive(False)
self._change_status("clear", "")
# set post button insensitive, if review being modified is the same
# as what is currently in the UI fields checks if 'original' review
# attributes exist to avoid exceptions when this method has been
# called prior to review being retrieved
if self.action == 'modify' and hasattr(self, "orig_star_rating"):
if self._modify_review_is_the_same():
self.button_post.set_sensitive(False)
self._change_status("warning", _("Can't submit unmodified"))
else:
self._change_status("clear", "")
def _modify_review_is_the_same(self):
"""checks if review fields are the same as the review being modified
and returns True if so
"""
# perform an initial check on character counts to return False if any
# don't match, avoids doing unnecessary string comparisons
if (self.review_summary_entry.get_text_length() !=
len(self.orig_summary_text) or
self.review_buffer.get_char_count() != len(self.orig_review_text)):
return False
#compare rating
if self.star_rating.get_rating() != self.orig_star_rating:
return False
#compare summary text
if (self.review_summary_entry.get_text().decode('utf-8') !=
self.orig_summary_text):
return False
#compare review text
if (self.review_buffer.get_text(
self.review_buffer.get_start_iter(),
self.review_buffer.get_end_iter(),
include_hidden_chars=False).decode('utf-8') !=
self.orig_review_text):
return False
return True
def _check_summary_character_count(self):
summary_chars = self.review_summary_entry.get_text_length()
if summary_chars > self.SUMMARY_CHAR_LIMITS[1] - 1:
markup = self._get_fade_colour_markup(
self.NORMAL_COLOUR, self.ERROR_COLOUR,
self.SUMMARY_CHAR_LIMITS[2], self.SUMMARY_CHAR_LIMITS[0],
summary_chars)
self.summary_char_label.set_markup(markup)
else:
self.summary_char_label.set_text('')
def _check_review_character_count(self):
review_chars = self.review_buffer.get_char_count()
if review_chars > self.REVIEW_CHAR_LIMITS[1] - 1:
markup = self._get_fade_colour_markup(
self.NORMAL_COLOUR, self.ERROR_COLOUR,
self.REVIEW_CHAR_LIMITS[2], self.REVIEW_CHAR_LIMITS[0],
review_chars)
self.review_char_label.set_markup(markup)
else:
self.review_char_label.set_text('')
def _get_fade_colour_markup(self, full_col, empty_col, cmin, cmax, curr):
"""takes two colours as well as a minimum and maximum value then
fades one colour into the other based on the proportion of the
current value between the min and max
returns a pango color string
"""
markup = '%s'
if curr > cmax:
return markup % (empty_col, str(cmax - curr))
elif curr <= cmin: # saves division by 0 later if cmin == cmax
return markup % (full_col, str(cmax - curr))
else:
#distance between min and max values to fade colours
scale = cmax - cmin
#percentage to fade colour by, based on current number of chars
percentage = (curr - cmin) / float(scale)
full_rgb = self._convert_html_to_rgb(full_col)
empty_rgb = self._convert_html_to_rgb(empty_col)
#calc changes to each of the r g b values to get the faded colour
red_change = full_rgb[0] - empty_rgb[0]
green_change = full_rgb[1] - empty_rgb[1]
blue_change = full_rgb[2] - empty_rgb[2]
new_red = int(full_rgb[0] - (percentage * red_change))
new_green = int(full_rgb[1] - (percentage * green_change))
new_blue = int(full_rgb[2] - (percentage * blue_change))
return_color = self._convert_rgb_to_html(new_red, new_green,
new_blue)
return markup % (return_color, str(cmax - curr))
def _convert_html_to_rgb(self, html):
r = html[0:2]
g = html[2:4]
b = html[4:6]
return (int(r, 16), int(g, 16), int(b, 16))
def _convert_rgb_to_html(self, r, g, b):
return "%s%s%s" % ("%02X" % r,
"%02X" % g,
"%02X" % b)
def on_button_post_clicked(self, button):
logging.debug("enter_review ok button")
review = Review(self.app)
text_buffer = self.textview_review.get_buffer()
review.text = text_buffer.get_text(text_buffer.get_start_iter(),
text_buffer.get_end_iter(),
False) # include_hidden_chars
review.summary = self.review_summary_entry.get_text()
review.date = datetime.datetime.now()
review.language = get_language()
review.rating = int(self.star_rating.get_rating())
review.package_version = self.version
review.origin = self.origin
if self.action == "submit":
self.api.submit_review(review)
elif self.action == "modify":
changes = {'review_text': review.text,
'summary': review.summary,
'rating': review.rating}
self.api.modify_review(self.review_id, changes)
def login_successful(self, display_name):
self.main_notebook.set_current_page(1)
self._setup_details(self.submit_window, self.app,
self.iconname, self.version, display_name)
self.textview_review.grab_focus()
def _setup_gwibber_gui(self):
self.gwibber_accounts = self.gwibber_helper.accounts()
list_length = len(self.gwibber_accounts)
if list_length == 0:
self._on_no_gwibber_accounts()
elif list_length == 1:
self._on_one_gwibber_account()
else:
self._on_multiple_gwibber_accounts()
def _get_gwibber_prefs(self):
if self.config.has_option("reviews", "gwibber_send"):
send = self.config.getboolean("reviews", "gwibber_send")
else:
send = False
if self.config.has_option("reviews", "account_id"):
account_id = self.config.get("reviews", "account_id")
else:
account_id = False
return {
"gwibber_send": send,
"account_id": account_id
}
def _on_no_gwibber_accounts(self):
self.gwibber_hbox.hide()
self.gwibber_checkbutton.set_active(False)
def _on_one_gwibber_account(self):
account = self.gwibber_accounts[0]
self.gwibber_hbox.show()
self.gwibber_combo.hide()
from softwarecenter.utils import utf8
acct_text = utf8(_("Also post this review to %s (@%s)")) % (
utf8(account['service'].capitalize()), utf8(account['username']))
self.gwibber_checkbutton.set_label(acct_text)
# simplifies on_transmit_successful later
self.gwibber_combo.append_text(acct_text)
self.gwibber_combo.set_active(0)
# auto select submit via gwibber checkbutton if saved prefs say True
self.gwibber_checkbutton.set_active(self.gwibber_prefs['gwibber_send'])
def _on_multiple_gwibber_accounts(self):
self.gwibber_hbox.show()
self.gwibber_combo.show()
# setup accounts combo
self.gwibber_checkbutton.set_label(_("Also post this review to: "))
for account in self.gwibber_accounts:
acct_text = "%s (@%s)" % (
account['service'].capitalize(), account['username'])
self.gwibber_combo.append_text(acct_text)
# add "all" to both combo and accounts (the later is only pseudo)
self.gwibber_combo.append_text(_("All my Gwibber services"))
self.gwibber_accounts.append({"id": "pseudo-sc-all"})
# reapply preferences
self.gwibber_checkbutton.set_active(self.gwibber_prefs['gwibber_send'])
gwibber_active_account = 0
for account in self.gwibber_accounts:
if account['id'] == self.gwibber_prefs['account_id']:
gwibber_active_account = self.gwibber_accounts.index(account)
self.gwibber_combo.set_active(gwibber_active_account)
def _post_to_one_gwibber_account(self, msg, account):
""" little helper to facilitate posting message to twitter account
passed in
"""
status_text = _("Posting to %s") % utf8(
account['service'].capitalize())
self._change_status("progress", status_text)
return self.gwibber_helper.send_message(msg, account['id'])
def on_transmit_success(self, api, trans):
"""on successful submission of a review, try to send to gwibber as
well
"""
self._run_gwibber_submits(api, trans)
def _on_key_press_event(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
self._confirm_cancellation()
def _confirm_cancellation(self):
if (self._has_user_started_reviewing() and not
self._displaying_cancel_confirmation):
def do_cancel(widget):
self.submit_window.destroy()
self.quit()
def undo_cancel(widget):
self._displaying_cancel_confirmation = False
self.response_hbuttonbox.set_visible(True)
self.main_notebook.set_current_page(1)
self.response_hbuttonbox.set_visible(False)
self.confirm_cancel_yes.grab_focus()
self.main_notebook.set_current_page(2)
self._displaying_cancel_confirmation = True
if not self._confirm_cancel_yes_handler:
tag = self.confirm_cancel_yes.connect("clicked", do_cancel)
self._confirm_cancel_yes_handler = tag
if not self._confirm_cancel_no_handler:
tag = self.confirm_cancel_no.connect("clicked", undo_cancel)
self._confirm_cancel_no_handler = tag
else:
self.submit_window.destroy()
self.quit()
def _get_send_accounts(self, sel_index):
"""return the account referenced by the passed in index, or all
accounts if the index of the combo points to the pseudo-sc-all
string
"""
if self.gwibber_accounts[sel_index]["id"] == "pseudo-sc-all":
return self.gwibber_accounts
else:
return [self.gwibber_accounts[sel_index]]
def _submit_to_gwibber(self, msg, send_accounts):
"""for each send_account passed in, try to submit to gwibber
then return a list of accounts that failed to submit (empty list
if all succeeded)
"""
#list of gwibber accounts that failed to submit, used later to allow
# selective re-send if user desires
failed_accounts = []
for account in send_accounts:
if account["id"] != "pseudo-sc-all":
if not self._post_to_one_gwibber_account(msg, account):
failed_accounts.append(account)
return failed_accounts
def _run_gwibber_submits(self, api, trans):
"""check if gwibber send should occur and send via gwibber if so"""
gwibber_success = True
using_gwibber = self.gwibber_checkbutton.get_active()
if using_gwibber:
i = self.gwibber_combo.get_active()
msg = (self._gwibber_message())
send_accounts = self._get_send_accounts(i)
self._save_gwibber_state(True, self.gwibber_accounts[i]['id'])
#tries to send to gwibber, and gets back any failed accounts
failed_accounts = self._submit_to_gwibber(msg, send_accounts)
if len(failed_accounts) > 0:
gwibber_success = False
#FIXME: send an error string to this method instead of empty
# string
self._on_gwibber_fail(api, trans, failed_accounts, "")
else:
# prevent _save_gwibber_state from overwriting the account id
# in config if the checkbutton was not selected
self._save_gwibber_state(False, None)
# run parent handler on gwibber success, otherwise this will be dealt
# with in _on_gwibber_fail
if gwibber_success:
self._success_status()
BaseApp.on_transmit_success(self, api, trans)
def _gwibber_retry_some(self, api, trans, accounts):
""" perform selective retrying of gwibber posting, using only
accounts passed in
"""
gwibber_success = True
failed_accounts = []
msg = (self._gwibber_message())
for account in accounts:
if not self._post_to_one_gwibber_account(msg, account):
failed_accounts.append(account)
gwibber_success = False
if not gwibber_success:
#FIXME: send an error string to this method instead of empty string
self._on_gwibber_fail(api, trans, failed_accounts, "")
else:
self._success_status()
BaseApp.on_transmit_success(self, api, trans)
def _success_status(self):
"""Updates status area to show success for 2 seconds then allows
window to proceed
"""
self._change_status("success", _(self.SUCCESS_MESSAGE))
while Gtk.events_pending():
Gtk.main_iteration()
time.sleep(2)
def _on_gwibber_fail(self, api, trans, failed_accounts, error):
self._change_status("fail", _("Problems posting to Gwibber"))
#list to hold service strings in the format: "Service (@username)"
failed_services = []
for account in failed_accounts:
failed_services.append("%s (@%s)" % (
account['service'].capitalize(), account['username']))
glade_dialog = SimpleGtkbuilderDialog(self.datadir,
domain="software-center")
dialog = glade_dialog.dialog_gwibber_error
dialog.set_transient_for(self.submit_window)
# build the failure string
# TRANSLATORS: the part in %s can either be a single entry
# like "facebook" or a string like
# "factbook and twister"
error_str = gettext.ngettext(
"There was a problem posting this review to %s.",
"There was a problem posting this review to %s.",
len(failed_services))
error_str = make_string_from_list(error_str, failed_services)
dialog.set_markup(error_str)
dialog.format_secondary_text(error)
result = dialog.run()
dialog.destroy()
if result == Gtk.RESPONSE_ACCEPT:
self._gwibber_retry_some(api, trans, failed_accounts)
else:
BaseApp.on_transmit_success(self, api, trans)
def _save_gwibber_state(self, gwibber_send, account_id):
if not self.config.has_section("reviews"):
self.config.add_section("reviews")
self.config.set("reviews", "gwibber_send", str(gwibber_send))
if account_id:
self.config.set("reviews", "account_id", account_id)
self.config.write()
def _gwibber_message(self, max_len=140):
""" build a gwibber message of max_len"""
def _gwibber_message_string_from_data(appname, rating, summary, link):
""" helper so that we do not duplicate the "reviewed..." string """
return _("reviewed %(appname)s in Ubuntu: %(rating)s "
"%(summary)s %(link)s") % {
'appname': appname,
'rating': rating,
'summary': summary,
'link': link}
rating = self.star_rating.get_rating()
rating_string = ''
#fill star ratings for string
for i in range(1, 6):
if i <= rating:
rating_string = rating_string + u"\u2605"
else:
rating_string = rating_string + u"\u2606"
review_summary_text = self.review_summary_entry.get_text()
# FIXME: currently the link is not useful (at all) for most
# people not runnig ubuntu
#app_link = "http://apt.ubuntu.com/p/%s" % self.app.pkgname
app_link = ""
gwib_msg = _gwibber_message_string_from_data(
self.app.name, rating_string, review_summary_text, app_link)
#check char count and ellipsize review summary if larger than 140 chars
if len(gwib_msg) > max_len:
chars_to_reduce = len(gwib_msg) - (max_len - 1)
new_char_count = len(review_summary_text) - chars_to_reduce
review_summary_text = (review_summary_text[:new_char_count] +
u"\u2026")
gwib_msg = _gwibber_message_string_from_data(
self.app.name, rating_string, review_summary_text, app_link)
return gwib_msg
class ReportReviewApp(BaseApp):
""" report a given application or package """
APP_ICON_SIZE = 48
SUBMIT_MESSAGE = _(u"Sending report\u2026")
FAILURE_MESSAGE = _("Failed to submit report")
def __init__(self, review_id, parent_xid, datadir):
BaseApp.__init__(self, datadir, "report_abuse.ui")
# status
self._add_spellcheck_to_textview(self.textview_report)
## make button sensitive when textview has content
self.textview_report.get_buffer().connect(
"changed", self._enable_or_disable_report_button)
# data
self.review_id = review_id
# title
self.submit_window.set_title(_("Flag as Inappropriate"))
# parent xid
#if parent_xid:
# #win = Gtk.gdk.window_foreign_new(int(parent_xid))
# if win:
# self.submit_window.realize()
# self.submit_window.window.set_transient_for(win)
# mousepos
self.submit_window.set_position(Gtk.WindowPosition.MOUSE)
# simple APIs ftw!
self.combobox_report_summary = Gtk.ComboBoxText.new()
self.report_body_vbox.pack_start(self.combobox_report_summary, False,
False, 0)
self.report_body_vbox.reorder_child(self.combobox_report_summary, 2)
self.combobox_report_summary.show()
for term in [_(u"Please make a selection\u2026"),
# TRANSLATORS: The following is one entry in a combobox that is
# located directly beneath a label asking 'Why is this review
# inappropriate?'.
# This text refers to a possible reason for why the corresponding
# review is being flagged as inappropriate.
_("Offensive language"),
# TRANSLATORS: The following is one entry in a combobox that is
# located directly beneath a label asking 'Why is this review
# inappropriate?'.
# This text refers to a possible reason for why the corresponding
# review is being flagged as inappropriate.
_("Infringes copyright"),
# TRANSLATORS: The following is one entry in a combobox that is
# located directly beneath a label asking 'Why is this review
# inappropriate?'.
# This text refers to a possible reason for why the corresponding
# review is being flagged as inappropriate.
_("Contains inaccuracies"),
# TRANSLATORS: The following is one entry in a combobox that is
# located directly beneath a label asking 'Why is this review
# inappropriate?'.
# This text refers to a possible reason for why the corresponding
# review is being flagged as inappropriate.
_("Other")]:
self.combobox_report_summary.append_text(term)
self.combobox_report_summary.set_active(0)
self.combobox_report_summary.connect(
"changed", self._enable_or_disable_report_button)
def _enable_or_disable_report_button(self, widget):
if (self.textview_report.get_buffer().get_char_count() > 0 and
self.combobox_report_summary.get_active() != 0):
self.button_post.set_sensitive(True)
else:
self.button_post.set_sensitive(False)
def _setup_details(self, widget, display_name):
# report label
self.report_label.set_markup(_('Please give details:'))
# review summary label
self.report_summary_label.set_markup(
_('Why is this review inappropriate?'))
#error detail link label
self.label_expander.set_markup('%s'
% (_('Error Details')))
def on_button_post_clicked(self, button):
logging.debug("report_abuse ok button")
report_summary = self.combobox_report_summary.get_active_text()
text_buffer = self.textview_report.get_buffer()
report_text = text_buffer.get_text(text_buffer.get_start_iter(),
text_buffer.get_end_iter(),
include_hidden_chars=False)
self.api.report_abuse(self.review_id, report_summary, report_text)
def login_successful(self, display_name):
logging.debug("login_successful")
self.main_notebook.set_current_page(1)
#self.label_reporter.set_text(display_name)
self._setup_details(self.submit_window, display_name)
class SubmitUsefulnessApp(BaseApp):
SUBMIT_MESSAGE = _(u"Sending usefulness\u2026")
def __init__(self, review_id, parent_xid, is_useful, datadir):
BaseApp.__init__(self, datadir, "submit_usefulness.ui")
# data
self.review_id = review_id
self.is_useful = bool(is_useful)
# no UI except for error conditions
self.parent_xid = parent_xid
# override behavior of baseapp here as we don't actually
# have a UI by default
def _get_parent_xid_for_login_window(self):
return self.parent_xid
def login_successful(self, display_name):
logging.debug("submit usefulness")
self.main_notebook.set_current_page(1)
self.api.submit_usefulness(self.review_id, self.is_useful)
def on_transmit_failure(self, api, trans, error):
logging.warn("exiting - error: %s" % error)
self.api.shutdown()
self.quit(2)
# override parents run to only trigger login (and subsequent
# events) but no UI, if this is commented out, there is some
# stub ui that can be useful for testing
def run(self):
self.login()
# override UI update methods from BaseApp to prevent them
# causing errors if called when UI is hidden
def _clear_status_imagery(self):
pass
def _change_status(self, type, message):
pass
class DeleteReviewApp(BaseApp):
SUBMIT_MESSAGE = _(u"Deleting review\u2026")
FAILURE_MESSAGE = _("Failed to delete review")
def __init__(self, review_id, parent_xid, datadir):
# uses same UI as submit usefulness because
# (a) it isn't shown and (b) it's similar in usage
BaseApp.__init__(self, datadir, "submit_usefulness.ui")
# data
self.review_id = review_id
# no UI except for error conditions
self.parent_xid = parent_xid
# override behavior of baseapp here as we don't actually
# have a UI by default
def _get_parent_xid_for_login_window(self):
return self.parent_xid
def login_successful(self, display_name):
logging.debug("delete review")
self.main_notebook.set_current_page(1)
self.api.delete_review(self.review_id)
def on_transmit_failure(self, api, trans, error):
logging.warn("exiting - error: %s" % error)
self.api.shutdown()
self.quit(2)
# override parents run to only trigger login (and subsequent
# events) but no UI, if this is commented out, there is some
# stub ui that can be useful for testing
def run(self):
self.login()
# override UI update methods from BaseApp to prevent them
# causing errors if called when UI is hidden
def _clear_status_imagery(self):
pass
def _change_status(self, type, message):
pass