46
46
# define additional entities for the unescape method, needed
47
47
# because only '&', '<', and '>' are included by default
48
ESCAPE_ENTITIES = {"'":"'",
48
ESCAPE_ENTITIES = {"'": "'",
51
51
LOG = logging.getLogger(__name__)
53
54
class UnimplementedError(Exception):
56
58
class ExecutionTime(object):
58
60
Helper that can be used in with statements to have a simple
63
65
def __init__(self, info="", with_traceback=False):
65
67
self.with_traceback = with_traceback
66
69
def __enter__(self):
67
70
self.now = time.time()
68
72
def __exit__(self, type, value, stack):
69
73
logger = logging.getLogger("softwarecenter.performance")
70
74
logger.debug("%s: %s" % (self.info, time.time() - self.now))
71
75
if self.with_traceback:
72
76
log_traceback("populate model from query: '%s' (threaded: %s)")
76
81
Takes a string or unicode object and returns a utf-8 encoded
99
104
def wrapper(*args, **kwargs):
101
# check if the cache is ready and
106
# check if the cache is ready and
103
108
if hasattr(self, "app_view"):
104
window = self.app_view.get_window()
109
window = self.app_view.get_window()
105
110
if not self.cache.ready:
107
112
window.set_cursor(self.busy_cursor)
183
190
all_h1 = root.findall(".//h1")
186
# we don't support any sub html in the h1 when
193
# we don't support any sub html in the h1 when
191
199
def htmlize_package_description(desc):
193
201
inside_li = False
194
202
for part in normalize_package_description(desc).split("\n"):
195
203
stripped_part = part.strip()
196
if not stripped_part: continue
204
if not stripped_part:
197
206
if stripped_part.startswith("* "):
198
207
if not inside_li:
250
261
authentication = "%s:%s@" % (user, password)
251
262
host = settings.get_string("host")
252
263
port = settings.get_int("port")
253
http_proxy = "http://%s%s:%s/" % (authentication, host, port)
264
http_proxy = "http://%s%s:%s/" % (authentication, host, port)
255
266
return http_proxy
256
267
except Exception:
257
268
logging.exception("failed to get proxy from gconf")
259
271
def encode_for_xml(unicode_data, encoding="ascii"):
260
272
""" encode a given string for xml """
261
273
return unicode_data.encode(encoding, 'xmlcharrefreplace')
263
276
def decode_xml_char_reference(s):
264
""" takes a string like
277
""" takes a string like
266
279
and converts it to
269
282
p = re.compile("\&\#x(\d\d\d\d);")
270
283
return p.sub(r"\u\1", s).decode("unicode-escape")
272
286
def unescape(text):
274
288
unescapes the given text
276
290
return xml.sax.saxutils.unescape(text, ESCAPE_ENTITIES)
278
293
def uri_to_filename(uri):
336
356
LOG.exception("could not check for Unity dbus service")
337
357
return unity_running
339
def get_icon_from_theme(icons, iconname=None, iconsize=Icons.APP_ICON_SIZE, missingicon=Icons.MISSING_APP):
360
def get_icon_from_theme(icons, iconname=None, iconsize=Icons.APP_ICON_SIZE,
361
missingicon=Icons.MISSING_APP):
341
363
return the icon in the theme that corresponds to the given iconname
344
366
iconname = missingicon
346
368
icon = icons.load_icon(iconname, iconsize, 0)
347
369
except Exception as e:
348
LOG.warning(utf8("could not load icon '%s', displaying missing icon instead: %s "
349
) % (utf8(iconname), utf8(e.message)))
370
LOG.warning(utf8("could not load icon '%s', displaying missing "\
371
"icon instead: %s ") % (
372
utf8(iconname), utf8(e.message)))
350
373
icon = icons.load_icon(missingicon, iconsize, 0)
353
def get_file_path_from_iconname(icons, iconname=None, iconsize=Icons.APP_ICON_SIZE):
377
def get_file_path_from_iconname(icons, iconname=None,
378
iconsize=Icons.APP_ICON_SIZE):
355
380
return the file path of the icon in the theme that corresponds to the
356
381
given iconname, or None if it cannot be determined
366
391
icon_file_path = icon_info.get_filename()
368
393
return icon_file_path
370
def convert_desktop_file_to_installed_location(app_install_data_file_path, pkgname):
396
def convert_desktop_file_to_installed_location(app_install_data_file_path,
371
398
""" returns the installed desktop file path that corresponds to the
372
399
given app-install-data file path, and will also check directly for
373
400
the desktop file that corresponds to a given pkgname.
375
402
if app_install_data_file_path and pkgname:
377
installed_desktop_file_path = app_install_data_file_path.replace("app-install/desktop/"
404
installed_desktop_file_path = app_install_data_file_path.replace(
405
"app-install/desktop/" + pkgname + ":", "applications/")
380
406
if os.path.exists(installed_desktop_file_path):
381
return installed_desktop_file_path
407
return installed_desktop_file_path
382
408
# next, try case where a subdirectory is encoded in the app-install
383
409
# desktop filename, e.g. kde4_soundkonverter.desktop
384
installed_desktop_file_path = installed_desktop_file_path.replace(APP_INSTALL_PATH_DELIMITER, "/")
410
installed_desktop_file_path = installed_desktop_file_path.replace(
411
APP_INSTALL_PATH_DELIMITER, "/")
385
412
if os.path.exists(installed_desktop_file_path):
386
413
return installed_desktop_file_path
387
# lastly, just try checking directly for the desktop file based on the pkgname itself
414
# lastly, just try checking directly for the desktop file based on the
389
installed_desktop_file_path = "/usr/share/applications/%s.desktop" % pkgname
417
installed_desktop_file_path = "/usr/share/applications/%s.desktop" %\
390
419
if os.path.exists(installed_desktop_file_path):
391
420
return installed_desktop_file_path
392
LOG.warn("Could not determine the installed desktop file path for app-install desktop file: '%s'" % app_install_data_file_path)
421
LOG.warn("Could not determine the installed desktop file path for "
422
"app-install desktop file: '%s'" % app_install_data_file_path)
395
426
def clear_token_from_ubuntu_sso(appname):
396
""" send a dbus signal to the com.ubuntu.sso service to clear
427
""" send a dbus signal to the com.ubuntu.sso service to clear
397
428
the credentials for the given appname, e.g. _("Ubuntu Software Center")
399
430
from ubuntu_sso import (
418
450
secs = dt.seconds
422
453
if secs < 120: # less than 2 minute ago
423
454
s = _('a few minutes ago') # dont be fussy
425
456
elif secs < 3600: # less than an hour ago
426
457
s = gettext.ngettext("%(min)i minute ago",
427
458
"%(min)i minutes ago",
428
(secs/60)) % { 'min' : (secs/60) }
459
(secs / 60)) % {'min': (secs / 60)}
430
461
else: # less than a day ago
431
462
s = gettext.ngettext("%(hours)i hour ago",
432
463
"%(hours)i hours ago",
433
(secs/3600)) % { 'hours' : (secs/3600) }
435
elif days <= 5: # less than a week ago
464
(secs / 3600)) % {'hours': (secs / 3600)}
465
elif days <= 5: # less than a week ago
436
466
s = gettext.ngettext("%(days)i day ago",
437
467
"%(days)i days ago",
438
days) % { 'days' : days }
468
days) % {'days': days}
440
469
else: # any timedelta greater than 5 days old
442
471
s = cur_t.isoformat().split('T')[0]
446
475
def _get_from_desktop_file(desktop_file, key):
447
476
import ConfigParser
448
477
config = ConfigParser.ConfigParser()
452
481
except ConfigParser.NoOptionError:
455
485
def get_exec_line_from_desktop(desktop_file):
456
486
return _get_from_desktop_file(desktop_file, "Exec")
458
489
def is_no_display_desktop_file(desktop_file):
459
nd = _get_from_desktop_file(desktop_file, "NoDisplay")
490
nd = _get_from_desktop_file(desktop_file, "NoDisplay")
460
491
# desktop spec says the booleans are always either "true" or "false
465
497
def get_nice_size(n_bytes):
466
nice_size = lambda s:[(s%1024**i and "%.1f"%(s/1024.0**i) or \
467
str(s/1024**i))+x.strip() for i,x in enumerate(' KMGTPEZY') \
468
if s<1024**(i+1) or i==8][0]
498
nice_size = lambda s: [(s % 1024 ** i and "%.1f" % (s / 1024.0 ** i) or \
499
str(s / 1024 ** i)) + x.strip() for i, x in enumerate(' KMGTPEZY') \
500
if s < 1024 ** (i + 1) or i == 8][0]
469
501
return nice_size(n_bytes)
471
504
def save_person_to_config(username):
472
505
""" save the specified username value for Ubuntu SSO to the config file
494
528
return cfg.get("reviews", "username")
497
532
def pnormaldist(qn):
498
'''Inverse normal distribution, based on the Ruby statistics2.pnormaldist'''
534
Inverse normal distribution, based on the Ruby statistics2.pnormaldist
499
536
b = [1.570796288, 0.03706987906, -0.8364353589e-3,
500
537
-0.2250947176e-3, 0.6841218299e-5, 0.5824238515e-5,
501
538
-0.104527497e-5, 0.8360937017e-7, -0.3231081277e-8,
502
539
0.3657763036e-10, 0.6936233982e-12]
504
541
if qn < 0 or qn > 1:
505
542
raise ValueError("qn must be between 0.0 and 1.0")
512
549
w3 = -math.log(4.0 * w1 * (1.0 - w1))
514
for i in range (1,11):
551
for i in range(1, 11):
515
552
w1 = w1 + (b[i] * math.pow(w3, i))
518
return math.sqrt(w1*w3)
555
return math.sqrt(w1 * w3)
520
return -math.sqrt(w1*w3)
557
return -math.sqrt(w1 * w3)
522
560
def wilson_score(pos, n, power=0.2):
525
z = pnormaldist(1-power/2)
563
z = pnormaldist(1 - power / 2)
526
564
phat = 1.0 * pos / n
527
return (phat + z*z/(2*n) - z * math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
565
return (phat + z * z / (2 * n) - z * math.sqrt(
566
(phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / n)
529
569
def calc_dr(ratings, power=0.1):
530
570
'''Calculate the dampened rating for an app given its collective ratings'''
531
571
if not len(ratings) == 5:
532
572
raise AttributeError('ratings argument must be a list of 5 integers')
535
for i in range (0,5):
575
for i in range(0, 5):
536
576
tot_ratings = ratings[i] + tot_ratings
539
for i in range (0,5):
579
for i in range(0, 5):
540
580
ws = wilson_score(ratings[i], tot_ratings, power)
541
sum_scores = sum_scores + float((i+1)-3) * ws
581
sum_scores = sum_scores + float((i + 1) - 3) * ws
543
582
return sum_scores + 3
545
585
# we need this because some iconnames have already split off the extension
546
586
# (the desktop file standard suggests this) but still have a "." in the name.
547
587
# From other sources we get icons with a full extension so a simple splitext()
548
588
# is not good enough
549
589
def split_icon_ext(iconname):
550
""" return the basename of a icon if it matches a known icon
590
""" return the basename of a icon if it matches a known icon
551
591
extenstion like tiff, gif, jpg, svg, png, xpm, ico
553
SUPPORTED_EXTENSIONS = [".tiff", ".tif", ".gif", ".jpg", ".jpeg", ".svg",
593
SUPPORTED_EXTENSIONS = [".tiff", ".tif", ".gif", ".jpg", ".jpeg", ".svg",
554
594
".png", ".xpm", ".ico"]
555
595
basename, ext = os.path.splitext(iconname)
556
596
if ext.lower() in SUPPORTED_EXTENSIONS:
563
603
# we are running in a local checkout, make life as easy as possible
565
605
if os.path.exists("./data/ui/gtk3/SoftwareCenter.ui"):
566
logging.getLogger("softwarecenter").info("Using data (UI, xapian) from current dir")
606
logging.getLogger("softwarecenter").info(
607
"Using data (UI, xapian) from current dir")
567
608
# set pythonpath for the various helpers
568
if os.environ.get("PYTHONPATH",""):
569
os.environ["PYTHONPATH"]=os.path.abspath(".") + ":" + os.environ.get("PYTHONPATH","")
609
if os.environ.get("PYTHONPATH", ""):
610
os.environ["PYTHONPATH"] = os.path.abspath(".") + ":" +\
611
os.environ.get("PYTHONPATH", "")
571
os.environ["PYTHONPATH"]=os.path.abspath(".")
613
os.environ["PYTHONPATH"] = os.path.abspath(".")
572
614
datadir = "./data"
573
615
xapian_base_path = datadir
574
616
# set new global datadir
575
617
softwarecenter.paths.datadir = datadir
576
618
# also alter the app-install path
577
path = "%s/desktop/software-center.menu" % softwarecenter.paths.APP_INSTALL_PATH
619
path = "%s/desktop/software-center.menu" % \
620
softwarecenter.paths.APP_INSTALL_PATH
578
621
if not os.path.exists(path):
579
622
softwarecenter.paths.APP_INSTALL_PATH = './build/share/app-install'
580
logging.warn("using local APP_INSTALL_PATH: %s" % softwarecenter.paths.APP_INSTALL_PATH)
623
logging.warn("using local APP_INSTALL_PATH: %s" %\
624
softwarecenter.paths.APP_INSTALL_PATH)
582
626
datadir = softwarecenter.paths.datadir
583
627
xapian_base_path = softwarecenter.paths.XAPIAN_BASE_PATH
584
628
return (datadir, xapian_base_path)
588
633
return str(uuid.uuid4())
593
638
LOG = logging.getLogger("softwarecenter.simplefiledownloader")
596
"file-url-reachable" : (GObject.SIGNAL_RUN_LAST,
600
"file-download-complete" : (GObject.SIGNAL_RUN_LAST,
604
"error" : (GObject.SIGNAL_RUN_LAST,
606
(GObject.TYPE_PYOBJECT,
607
GObject.TYPE_PYOBJECT,),),
641
"file-url-reachable": (GObject.SIGNAL_RUN_LAST,
645
"file-download-complete": (GObject.SIGNAL_RUN_LAST,
649
"error": (GObject.SIGNAL_RUN_LAST,
651
(GObject.TYPE_PYOBJECT,
652
GObject.TYPE_PYOBJECT,),),
610
655
def __init__(self):
615
660
def download_file(self, url, dest_file_path=None, use_cache=False,
616
661
simple_quoting_for_webkit=False):
617
""" Download a url and emit the file-download-complete
662
""" Download a url and emit the file-download-complete
618
663
once the file is there. Note that calling this twice
619
664
will cancel the previous pending operation.
620
665
If dest_file_path is given, download to that specific
670
715
f = Gio.File.new_for_uri(url)
671
716
# first check if the url is reachable
672
f.query_info_async(Gio.FILE_ATTRIBUTE_STANDARD_SIZE, 0, 0,
717
f.query_info_async(Gio.FILE_ATTRIBUTE_STANDARD_SIZE, 0, 0,
673
718
self._cancellable,
674
719
self._check_url_reachable_and_then_download_cb,
677
def _check_url_reachable_and_then_download_cb(self, f, result, user_data=None):
722
def _check_url_reachable_and_then_download_cb(self, f, result,
678
724
self.LOG.debug("_check_url_reachable_and_then_download_cb: %s" % f)
680
726
info = f.query_info_finish(result)
681
727
etag = info.get_etag()
682
728
self.emit('file-url-reachable', True)
683
729
self.LOG.debug("file reachable %s %s %s" % (self.url,
686
732
# url is reachable, now download the file
687
733
f.load_contents_async(
695
741
def _file_download_complete_cb(self, f, result, path=None):
696
742
self.LOG.debug("file download completed %s" % self.dest_file_path)
697
# The result from the download is actually a tuple with three
743
# The result from the download is actually a tuple with three
698
744
# elements (content, size, etag?)
699
745
# The first element is the actual content so let's grab that
740
786
from softwarecenter.db.pkginfo import get_pkg_info
741
787
# do not call here get_pkg_info, since package switch may not have been set
742
788
# instead use an anonymous function delay
743
upstream_version_compare = lambda v1, v2: get_pkg_info().upstream_version_compare(v1, v2)
789
upstream_version_compare = lambda v1, v2: \
790
get_pkg_info().upstream_version_compare(v1, v2)
744
791
upstream_version = lambda v: get_pkg_info().upstream_version(v)
745
792
version_compare = lambda v1, v2: get_pkg_info().version_compare(v1, v2)