diff --git a/sulfur/Makefile b/sulfur/Makefile index 50946764d..7a962d651 100644 --- a/sulfur/Makefile +++ b/sulfur/Makefile @@ -38,6 +38,7 @@ install: install -m755 $(MISCDIR)/sulfur-uri-handler $(DESTDIR)$(BINDIR)/. install -m644 $(MISCDIR)/entropy-handler.schemas $(DESTDIR)/etc/gconf/schemas/. install -m644 $(PIXDIR)/*.png $(DESTDIR)/usr/share/pixmaps/sulfur/. + install -m644 $(PIXDIR)/*.gif $(DESTDIR)/usr/share/pixmaps/sulfur/. install -m644 $(PIXDIR)/packages/*.png $(DESTDIR)/usr/share/pixmaps/sulfur/packages/. install -m644 $(PIXDIR)/ugc/*.png $(DESTDIR)/usr/share/pixmaps/sulfur/ugc/. install -m644 $(MISCDIR)/kde_x-sulfur.desktop $(DESTDIR)/usr/share/mimelnk/application/. diff --git a/sulfur/src/sulfur/__init__.py b/sulfur/src/sulfur/__init__.py index 0c3b04539..ec9226b11 100644 --- a/sulfur/src/sulfur/__init__.py +++ b/sulfur/src/sulfur/__init__.py @@ -43,13 +43,14 @@ from entropy.services.exceptions import EntropyServicesError import entropy.tools from entropy.const import etpConst, const_get_stringtype, \ initconfig_entropy_constants, const_convert_to_unicode, \ - const_drop_privileges, const_regain_privileges + const_debug_write from entropy.i18n import _ from entropy.misc import ParallelTask from entropy.cache import EntropyCacher, MtimePingus from entropy.output import print_generic from entropy.db.exceptions import ProgrammingError, OperationalError from entropy.core.settings.base import SystemSettings +from entropy.services.client import WebService # Sulfur Imports import gtk, gobject @@ -59,7 +60,7 @@ from sulfur.setup import SulfurConf, const, fakeoutfile, fakeinfile, \ cleanMarkupString from sulfur.widgets import SulfurConsole from sulfur.core import UI, Controller, FORK_PIDS, fork_function, \ - busy_cursor, normal_cursor + busy_cursor, normal_cursor, get_entropy_webservice, Privileges from sulfur.views import * from sulfur.filters import Filter from sulfur.dialogs import * @@ -67,59 +68,14 @@ from sulfur.progress import Base as BaseProgress from sulfur.events import SulfurApplicationEventsMixin from sulfur.event import SulfurSignals - class SulfurApplication(Controller, SulfurApplicationEventsMixin): - class Privileges: - - def __init__(self, drop_privs = True): - self.__drop_privs = drop_privs - self.__drop_privs_lock = threading.RLock() - self.__with_stmt = 0 - - def __enter__(self): - """ - Hold the lock. - """ - self.__drop_privs_lock.acquire() - if self.__with_stmt < 1: - self.regain() - self.__with_stmt += 1 - - def __exit__(self, exc_type, exc_value, traceback): - """ - Drop the lock. - """ - if self.__with_stmt == 1: - self.drop() - self.__with_stmt -= 1 - self.__drop_privs_lock.release() - - def drop(self): - """ - Drop process privileges. Setting unpriv_gid to etpConst['entropygid'] - makes Entropy UGC/Data cache handling working. - """ - if self.__drop_privs: - with self.__drop_privs_lock: - const_drop_privileges(unpriv_gid = etpConst['entropygid']) - const.setup() - - def regain(self): - """ - Regain previously dropped process privileges. - """ - if self.__drop_privs: - with self.__drop_privs_lock: - const_regain_privileges() - const.setup() - - def __init__(self, drop_privs = True): + def __init__(self): self.do_debug = False self._ugc_status = "--nougc" not in sys.argv - self._privileges = self.Privileges(drop_privs = drop_privs) + self._privileges = Privileges() # Use this lock when you want to make sure that no other asynchronous # executions are being run. self._async_event_execution_lock = threading.Lock() @@ -127,6 +83,7 @@ class SulfurApplication(Controller, SulfurApplicationEventsMixin): self._entropy = Equo() self._cacher = EntropyCacher() self._settings = SystemSettings() + self._webserv_map = {} # support for packages installation on startup packages_install, atoms_install, do_fetch = \ @@ -187,6 +144,40 @@ class SulfurApplication(Controller, SulfurApplicationEventsMixin): # init the Controller Class to connect signals. Controller.__init__( self, ui ) + def _get_webservice(self, repository_id): + webserv = self._webserv_map.get(repository_id) + if webserv == -1: + # not available + return None + if webserv is not None: + return webserv + + with self._privileges: + # so that the correct auth store file is fetched + # NOTE: other areas don't do the same, be careful + try: + webserv = get_entropy_webservice(self._entropy, repository_id) + except WebService.UnsupportedService as err: + webserv = None + + if webserv is None: + self._webserv_map[repository_id] = -1 + # not available + return + + try: + available = webserv.service_available() + except WebService.WebServiceException: + available = False + + if not available: + self._webserv_map[repository_id] = -1 + # not available + return + + self._webserv_map[repository_id] = webserv + return webserv + def __scan_packages_install(self): packages_install = os.environ.get("SULFUR_PACKAGES", '').split(";") atoms_install = [] @@ -896,23 +887,38 @@ class SulfurApplication(Controller, SulfurApplicationEventsMixin): if self.do_debug: print_generic("entering UGC") - if self._entropy.UGC is None: - return - if self._spawning_ugc or self.disable_ugc: return if not force: - eapi3_repos = [] + webserv_repos = [] for repoid in self._entropy.repositories(): - aware = self._entropy.UGC.is_repository_eapi3_aware(repoid) + webserv = self._get_webservice(repoid) + if webserv is None: + continue + try: + aware = webserv.service_available() + except WebService.WebServiceException: + continue if aware: - eapi3_repos.append(repoid) + webserv_repos.append((webserv, repoid)) cache_available = True - for repoid in eapi3_repos: - if not self._entropy.is_ugc_cached(repoid): + for webserv, repoid in webserv_repos: + cache_available = True + try: + webserv.get_available_votes(cache = True, cached = True) + except WebService.CacheMiss: cache_available = False + if cache_available: + # also check downloads + try: + webserv.get_available_downloads(cache = True, + cached = True) + except WebService.CacheMiss: + cache_available = False + if not cache_available: + break pingus_id = "sulfur_ugc_content_spawn" # check if at least 2 hours are passed since last check @@ -936,9 +942,11 @@ class SulfurApplication(Controller, SulfurApplicationEventsMixin): self._ugc_update() self._cacher.sync() print_generic("UGC child process done") + emit_ugc_update() self._spawning_ugc = True - fork_function(do_ugc_sync, emit_ugc_update) + th = ParallelTask(do_ugc_sync) + th.start() if self.do_debug: print_generic("quitting UGC") @@ -947,15 +955,46 @@ class SulfurApplication(Controller, SulfurApplicationEventsMixin): def _ugc_update(self): + def _update_available_cache(repository_id): + webserv = self._get_webservice(repository_id) + if webserv is None: + return + + # fetch vote cache first + try: + webserv.get_available_votes(cache = False) + except WebService.WebServiceException as err: + const_debug_write(__name__, + "_ugc_update.get_available_votes: ouch %s" % (err,)) + return + # drop get_votes cache completely + # this enforces EntropyPackage.vote* to use available_votes cache + webserv._drop_cached("get_votes") + + try: + webserv.get_available_downloads(cache = False) + except WebService.WebServiceException as err: + const_debug_write(__name__, + "_ugc_update.get_available_downloads: ouch %s" % (err,)) + return + # drop get_downloads cache completely + # this enforces EntropyPackage.down* to use available_downloads + # cache + webserv._drop_cached("get_downloads") + + for repo in self._entropy.repositories(): - if self.do_debug: - t1 = time.time() - print_generic("working UGC update for", repo) - self._entropy.update_ugc_cache(repo) - if self.do_debug: - t2 = time.time() - td = t2 - t1 - print_generic("completed UGC update for", repo, "took", td) + t1 = time.time() + const_debug_write(__name__, + "working UGC update for %s" % (repo,)) + + _update_available_cache(repo) + + t2 = time.time() + td = t2 - t1 + const_debug_write(__name__, + "completed UGC update for %s, took %s" % ( + repo, td,)) def fill_pref_db_backup_page(self): self.dbBackupStore.clear() @@ -1030,22 +1069,31 @@ class SulfurApplication(Controller, SulfurApplicationEventsMixin): obj = model.get_value( myiter, 0 ) if obj: t = "%s" % (_("Not logged in"),) - if self._entropy.UGC != None: - logged_data = self._entropy.UGC.read_login(obj['repoid']) - if logged_data != None: - t = "%s" % (logged_data[0],) + webserv = self._get_webservice(obj['repoid']) + if webserv is None: + t = "%s" % (_("Not available"),) + else: + with self._privileges: + username = webserv.get_credentials() + if username is not None: + t = "%s" % (username,) cell.set_property('markup', t) def get_ugc_status_pix( column, cell, model, myiter ): - if self._entropy.UGC == None: - cell.set_property('icon-name', 'gtk-cancel') - return if not self._ugc_status: cell.set_property('icon-name', 'gtk-cancel') return obj = model.get_value( myiter, 0 ) if obj: - if self._entropy.UGC.is_repository_eapi3_aware(obj['repoid']): + webserv = self._get_webservice(obj['repoid']) + if webserv is None: + cell.set_property( 'icon-name', 'gtk-cancel' ) + return + try: + available = webserv.service_available() + except WebService.WebServiceException: + available = False + if available: cell.set_property( 'icon-name', 'gtk-apply' ) else: cell.set_property( 'icon-name', 'gtk-cancel' ) @@ -1436,11 +1484,20 @@ class SulfurApplication(Controller, SulfurApplicationEventsMixin): for repo in repos: # inform UGC that we are syncing this repo - if self._entropy.UGC is not None: - try: - self._entropy.UGC.add_download_stats(repo, [repo]) - except EntropyServicesError: - continue + webserv = self._get_webservice(repository) + if webserv is None: + continue + try: + available = webserv.service_available() + except WebService.WebServiceException: + continue + if not available: + continue + try: + webserv.add_downloads([repo]) + except WebService.WebServiceException as err: + const_debug_write(__name__, repr(err)) + continue if repoConn.sync_errors or (rc != 0): self.progress.set_mainLabel(_('Errors updating repositories.')) diff --git a/sulfur/src/sulfur/core.py b/sulfur/src/sulfur/core.py index 114611cf6..dffda77ac 100644 --- a/sulfur/src/sulfur/core.py +++ b/sulfur/src/sulfur/core.py @@ -19,12 +19,16 @@ import os import sys import shutil +import threading import gtk import gtk.glade import gobject -from entropy.const import const_debug_write +from entropy.const import const_debug_write, const_drop_privileges, \ + const_regain_privileges, etpConst from entropy.misc import ParallelTask +from entropy.core import Singleton +from sulfur.setup import const FORK_PIDS = [] @@ -164,6 +168,26 @@ def resize_image(max_width, image_path, new_image_path): del img_buf del img +def resize_image_height(max_height, image_path, new_image_path): + shutil.copy2(image_path, new_image_path) + img = gtk.Image() + img.set_from_file(new_image_path) + img_buf = img.get_pixbuf() + w, h = img_buf.get_width(), img_buf.get_height() + if h > max_height: + # resize pix + new_h = max_height + new_w = new_h*w/h + img_buf = img_buf.scale_simple(int(new_w), + int(new_h), gtk.gdk.INTERP_BILINEAR) + try: + img_buf.save(new_image_path, "png") + except gobject.GError: + # libpng issue? try jpeg + img_buf.save(new_image_path, "jpeg") + del img_buf + del img + def load_url(url): xdg_open = '/usr/bin/xdg-open' if os.access(xdg_open, os.X_OK): @@ -172,3 +196,66 @@ def load_url(url): # child os.execv(xdg_open, [xdg_open, url]) os._exit(0) + +def get_entropy_webservice(entropy_client, repository_id, tx_cb = None): + """ + Get Entropy Web Services service object (ClientWebService). + + @param entropy_client: Entropy Client interface + @type entropy_client: entropy.client.interfaces.Client + @param repository_id: repository identifier + @type repository_id: string + @return: the ClientWebService instance + @rtype: entropy.client.services.interfaces.ClientWebService + @raise WebService.UnsupportedService: if service is unsupported by + repository + """ + factory = entropy_client.WebServices() + webserv = factory.new(repository_id) + if tx_cb is not None: + webserv._set_transfer_callback(tx_cb) + return webserv + +class Privileges(Singleton): + + def init_singleton(self): + self.__drop_privs = True + self.__drop_privs_lock = threading.RLock() + self.__with_stmt = 0 + + def __enter__(self): + """ + Hold the lock. + """ + self.__drop_privs_lock.acquire() + if self.__with_stmt < 1: + self.regain() + self.__with_stmt += 1 + + def __exit__(self, exc_type, exc_value, traceback): + """ + Drop the lock. + """ + if self.__with_stmt == 1: + self.drop() + self.__with_stmt -= 1 + self.__drop_privs_lock.release() + + def drop(self): + """ + Drop process privileges. Setting unpriv_gid to etpConst['entropygid'] + makes Entropy UGC/Data cache handling working. + """ + if self.__drop_privs: + with self.__drop_privs_lock: + const_drop_privileges(unpriv_gid = etpConst['entropygid']) + const.setup() + + def regain(self): + """ + Regain previously dropped process privileges. + """ + if self.__drop_privs: + with self.__drop_privs_lock: + const_regain_privileges() + const.setup() diff --git a/sulfur/src/sulfur/dialogs.py b/sulfur/src/sulfur/dialogs.py index 0c4127d33..d86d97961 100644 --- a/sulfur/src/sulfur/dialogs.py +++ b/sulfur/src/sulfur/dialogs.py @@ -31,12 +31,17 @@ from entropy.exceptions import * from entropy.services.exceptions import TimeoutError from entropy.const import * from entropy.misc import ParallelTask +from entropy.services.client import WebService +from entropy.client.services.interfaces import Document, DocumentList, \ + DocumentFactory, ClientWebService + import entropy.dep import entropy.tools from sulfur.event import SulfurSignals from sulfur.core import UI, busy_cursor, normal_cursor, fork_function, \ - resize_image, load_url + resize_image, resize_image_height, load_url, get_entropy_webservice, \ + Privileges from sulfur.setup import const, cleanMarkupString, SulfurConf class MenuSkel: @@ -548,6 +553,7 @@ class PkgInfoMenu(MenuSkel): self.star_selected_pixmap = const.star_selected_pixmap self.star_empty_pixmap = const.star_empty_pixmap + self._ugc_row_height = 48 self._ugc_icon_store_path = None self.ugc_update_event_handler_id = None self.loading_pix = gtk.image_new_from_file(const.loading_pix) @@ -559,6 +565,7 @@ class PkgInfoMenu(MenuSkel): self.Entropy = Entropy self.repository = None self.pkgkey = None + self.__webserv = None self._page_tabs_map = { 1: "details", @@ -602,6 +609,42 @@ class PkgInfoMenu(MenuSkel): SulfurSignals.connect('pkg_properties__ugc_tab_clicked', self._ugc_tab_clicked) + @property + def _webserv(self): + if self.repository is None: + # cannot load ! + return None + + if self.__webserv == -1: + # unsupported + return None + if self.__webserv is not None: + return self.__webserv + + with Privileges(): + try: + webserv = get_entropy_webservice(self.Entropy, self.repository) + except WebService.UnsupportedService as err: + webserv = None + + if webserv is None: + self.__webserv = -1 + # not available + return None + + try: + available = webserv.service_available() + except WebService.WebServiceException: + available = False + + if not available: + self.__webserv = -1 + # not available + return + + self.__webserv = webserv + return webserv + def _set_pixbuf_to_cell(self, cell, path): try: pixbuf = gtk.gdk.pixbuf_new_from_file(path) @@ -614,68 +657,74 @@ class PkgInfoMenu(MenuSkel): if isinstance(obj, dict): if obj.get('preview_path') is not None: self._set_pixbuf_to_cell(cell, obj['preview_path']) - else: + elif obj['image_path']: self._set_pixbuf_to_cell(cell, obj['image_path']) self._set_colors_to_cell(cell, obj) - def ugc_content( self, column, cell, model, myiter ): - obj = model.get_value( myiter, 0 ) - if isinstance(obj, dict): - self._set_colors_to_cell(cell, obj) + def ugc_content(self, column, cell, model, myiter): + obj = model.get_value(myiter, 0) + if obj is None: + return - if 'is_cat' in obj: - cell.set_property('markup', "%s\n%s" % ( - obj['parent_desc'], _("Expand to browse"),)) - else: + self._set_colors_to_cell(cell, obj) - title = _("N/A") - if obj['title']: - title = const_convert_to_unicode(obj['title']) - description = _("N/A") + if 'is_cat' in obj: + cell.set_property('markup', "%s\n%s" % ( + obj['parent_desc'], _("Expand to browse"),)) + else: - if obj['description']: - description = obj['description'] + document_id = obj.document_id() + document_type = obj.document_type() - if obj['iddoctype'] in ( - etpConst['ugc_doctypes']['comments'], - etpConst['ugc_doctypes']['bbcode_doc'],): + title = _("N/A") + title_s = obj.get(Document.DOCUMENT_TITLE_ID) + if title_s: + title = const_convert_to_unicode(title_s).rstrip() - myddata = obj['ddata'] - if not isinstance(obj['ddata'], const_get_stringtype()): - myddata = myddata.tostring() - description = const_convert_to_unicode(myddata) - if len(description) > 100: - description = description[:100].strip()+"..." + description = _("N/A") + description_s = obj.get(Document.DOCUMENT_DESCRIPTION_ID) + if description_s: + description = description_s - first_txt = "%s: %s, %s: %s\n" % ( - _("Identifier"), - obj['iddoc'], - _("Size"), - entropy.tools.bytes_into_human(obj['size']), - ) - second_txt = "%s: %s, %s\n" % ( - _("Author"), - obj['username'], - obj['ts'], - ) - third_txt = "%s: %s\n%s: %s\n" % ( - _("Title"), - title, - _("Description"), - description, - ) - fourth_txt = "%s: %s" % ( + desc_t = _("Description") + ": " + if obj.is_comment(): + description = obj.document_data() + description = cleanMarkupString( + const_convert_to_unicode(description)) + desc_t = "\n" + + first_txt = "" + username = obj.get(DocumentFactory.DOCUMENT_USERNAME_ID) or _("N/A") + ts = obj.document_timestamp() + ts = entropy.tools.convert_unix_time_to_human_time(ts) + second_txt = "%s: %s, %s\n" % ( + _("Author"), + username, + ts, + ) + third_txt = "%s: %s\n%s%s" % ( + _("Title"), + title, + desc_t, + description, + ) + doc_keywords = obj.document_keywords().strip() + if doc_keywords: + keywords = doc_keywords.split() + fourth_txt = "\n%s: %s" % ( _("Keywords"), - ', '.join(obj['keywords']), + ', '.join(keywords), ) + else: + fourth_txt = "" - mytxt = first_txt + second_txt + third_txt + fourth_txt - cell.set_property('markup', mytxt) + mytxt = first_txt + second_txt + third_txt + fourth_txt + cell.set_property('markup', mytxt) def _set_colors_to_cell(self, cell, obj): odd = 0 if 'counter' in obj: - odd = obj['counter']%2 + odd = obj['counter'] % 2 if 'background' in obj: cell.set_property('cell-background', obj['background'][odd]) else: @@ -704,12 +753,12 @@ class PkgInfoMenu(MenuSkel): def on_closeInfo_clicked(self, widget): self.disconnect_event_signals() - self.reset_ugc_data() + self.ugc_data = None self.pkginfo_ui.pkgInfo.hide() def on_pkgInfo_delete_event(self, widget, path): self.disconnect_event_signals() - self.reset_ugc_data() + self.ugc_data = None self.pkginfo_ui.pkgInfo.hide() if isinstance(self._pkg_meta, dict): self._pkg_meta.clear() @@ -719,54 +768,62 @@ class PkgInfoMenu(MenuSkel): self.spawn_ugc_load(force = force) def on_ugcRemoveButton_released(self, widget): - if self.Entropy.UGC is None: - return if self.repository is None: return + if self._webserv is None: + return model, myiter = self.ugcView.get_selection().get_selected() if myiter is None: return - obj = model.get_value( myiter, 0 ) - if not isinstance(obj, dict): - return - if 'is_cat' in obj: + doc = model.get_value( myiter, 0 ) + if not isinstance(doc, Document): return + + document_id = doc.document_id() self.show_loading() - self.Entropy.UGC.remove_document_autosense( - self.repository, int(obj['iddoc']), obj['iddoctype']) - self.hide_loading() - self.reset_ugc_data() - self.spawn_ugc_load(force = True) - SulfurSignals.emit('ugc_cache_clear') - self.refresh_ugc_view() + with Privileges(): + try: + # requires authentication + removed = self._webserv.remove_document(document_id) + except WebService.AuthenticationRequired: + gobject.idle_add(self._ugc_login) + removed = False + except WebService.WebServiceException: + removed = False + finally: + self.hide_loading() + + if removed: + self.ugc_data = None + self.spawn_ugc_load(force = True) + SulfurSignals.emit('ugc_cache_clear') + self.refresh_ugc_view() def on_ugc_doubleclick(self, widget, path, view): self.on_ugcShowButton_clicked(widget) def on_ugcShowButton_clicked(self, widget): - if self.Entropy.UGC is None: - return if self.repository is None: return + if self._webserv is None: + return model, myiter = self.ugcView.get_selection().get_selected() if myiter is None: return obj = model.get_value( myiter, 0 ) - if not isinstance(obj, dict): + if not isinstance(obj, Document): return - if 'is_cat' in obj: - return - my = UGCInfoMenu(self.Entropy, obj, self.repository, + my = UGCInfoMenu(self.Entropy, obj, self.pkgkey, self.repository, self.pkginfo_ui.pkgInfo) my.load() def on_ugcAddButton_clicked(self, widget): - if self.Entropy.UGC is None: - return if self.repository is None: return if self.pkgkey is None: return + if self._webserv is None: + return my = UGCAddMenu(self.Entropy, self.pkgkey, self.repository, self.pkginfo_ui.pkgInfo, self.refresh_view_cb) my.load() @@ -775,10 +832,6 @@ class PkgInfoMenu(MenuSkel): self.spawn_ugc_load(force = True) SulfurSignals.emit('ugc_cache_clear') - def reset_ugc_data(self): - del self.ugc_data - self.ugc_data = None - def show_loading(self): self.pkginfo_ui.ugcButtonBox.hide() self.pkginfo_ui.loadUgcButton.set_sensitive(False) @@ -797,132 +850,159 @@ class PkgInfoMenu(MenuSkel): self.pkginfo_ui.loadUgcButton.set_sensitive(True) self.pkginfo_ui.ugcButtonBox.show_all() - def ugc_metadata_fetch(self, force = False): - if self.Entropy.UGC is None: - return + def __get_docs_loop(self, force): + docs = [] + docs_offset = 0 + with Privileges(): + while True: + try: + docs_list = self._webserv.get_documents([self.pkgkey], + cache = not force, cached = not force, + offset = docs_offset)[self.pkgkey] + except WebService.CacheMiss: + return None + except WebService.WebServiceException: + return None + if docs_list is None: + # wtf! + return None + docs.extend(docs_list) + if not docs_list.has_more(): + break + docs_offset += len(docs_list) + return docs - docs_data = None + def ugc_metadata_fetch(self, force = False): + if self._webserv is None: + return [] + + docs = None if not force: - docs_data = self.Entropy.UGC.UGCCache.get_alldocs_cache( - self.pkgkey, self.repository) - if docs_data is None: - docs_data, err_msg = self.Entropy.UGC.get_docs( - self.repository, self.pkgkey) - return docs_data + docs = self.__get_docs_loop(False) + if docs is None: + docs = self.__get_docs_loop(True) + return docs def spawn_ugc_load(self, force = False): if (self.ugc_data != None) and (not force): return - if self.Entropy.UGC is None: - return if not (self.pkgkey and self.repository): return self.show_loading() - try: - docs_data = self.ugc_metadata_fetch(force = force) - except TimeoutError: - dialog_title = _("Timeout Error") - err_msg = _("Connection timed out, sorry!") - docs_data = None - okDialog(self.window, err_msg, title = dialog_title) + docs = self.ugc_metadata_fetch(force = force) + if docs is None: + self.ugc_data = None + else: + doc_map = {} + for doc in docs: + doc_type = doc.document_type() + obj = doc_map.setdefault(doc_type, []) + obj.append(doc) + self.ugc_data = doc_map - if not isinstance(docs_data, (list, tuple)): - self.ugc_data = {} - else: - self.ugc_data = self.digest_remote_docs_data(docs_data) - - self.refresh_ugc_view() - self.setup_ugc_icon() - self.hide_loading() - - def digest_remote_docs_data(self, data): - newdata = {} - for mydict in data: - if not mydict: - continue - if mydict['iddoctype'] not in newdata: - newdata[mydict['iddoctype']] = [] - newdata[mydict['iddoctype']].append(mydict) - return newdata + self.refresh_ugc_view() + th = ParallelTask(self.setup_ugc_icon) + th.start() + finally: + self.hide_loading() def refresh_ugc_view(self): self.ugcModel.clear() if self.ugc_data is None: return self.populate_ugc_view() - #self.ugcView.expand_all() def prefetch_ugc_icon(self): - def update_ugc_icon(): - self.setup_ugc_icon() - return False def do_ugc_fetch(): - try: - self.ugc_metadata_fetch() - except TimeoutError: - return - fork_function(do_ugc_fetch, update_ugc_icon) + self.ugc_metadata_fetch() + self.setup_ugc_icon() + th = ParallelTask(do_ugc_fetch) + th.start() def setup_ugc_icon(self): """ Setup UGC icon for package if UGC data has been already downloaded and prepared by this class. """ - - if self.Entropy.UGC is None: + if not (self.pkgkey and self.repository): return - const_debug_write(__name__, "setup_ugc_icon, UGC available") - icon_doc = self.Entropy.UGC.UGCCache.get_icon_cache( - self.pkgkey, self.repository) - if icon_doc is None: + webserv = self._webserv + const_debug_write(__name__, + "setup_ugc_icon, WebService available: %s" % (webserv,)) + + # sorry web service, we need data this way + with Privileges(): + try: + icon_docs = webserv.get_icons([self.pkgkey], + cache = True, cached = True)[self.pkgkey] + except WebService.CacheMiss: + icon_docs = None + + if icon_docs is None: + # not in cache return + const_debug_write(__name__, "setup_ugc_icon, UGC metadata available in cache for (%s, %s): %s" % ( - self.pkgkey, self.repository, icon_doc,)) + self.pkgkey, self.repository, icon_docs,)) + + # get the first available in cache + store_path = None + store_doc = None + for icon_doc in icon_docs: + local_path = icon_doc.local_document() + if local_path is None: + continue + store_path = local_path + store_doc = icon_doc + # found a good one + break - store_path = self.Entropy.UGC.UGCCache.get_stored_document( - icon_doc['iddoc'], self.repository, icon_doc['store_url']) const_debug_write(__name__, "setup_ugc_icon, UGC store path for (%s, %s): %s" % ( self.pkgkey, self.repository, store_path,)) if store_path is None: + # cannot get any valid store path + return - # fetch from WWW if not available - self.Entropy.UGC.UGCCache.store_document(icon_doc['iddoc'], - self.repository, icon_doc['store_url']) - store_path = self.Entropy.UGC.UGCCache.get_stored_document( - icon_doc['iddoc'], self.repository, icon_doc['store_url']) + if not (os.path.isfile(store_path) and os.access(store_path, os.R_OK)): + # fetch from WWW + with Privileges(): + try: + store_path = webserv.get_document_url(store_doc) + except ClientWebService.DocumentError: + store_path = None const_debug_write(__name__, "setup_ugc_icon, UGC store path fetched for (%s, %s): %s" % ( self.pkgkey, self.repository, store_path,)) + else: + const_debug_write(__name__, + "setup_ugc_icon, UGC store path " + "available in cache for (%s, %s)" % ( + self.pkgkey, self.repository,)) if store_path is None: - # cannot get store path + # meh! + return + if self._ugc_icon_store_path == store_path: + # meh! 2 return - if not (os.access(store_path, os.R_OK) and os.path.isfile(store_path)): - return - - const_debug_write(__name__, - "setup_ugc_icon, UGC store path available in cache for (%s, %s)" % ( - self.pkgkey, self.repository,)) - - if self._ugc_icon_store_path != store_path: - self._ugc_icon_store_path = store_path - # if image has __icon__ as title, use as real icon - icon_path = store_path + ".sulfur_icon" - if not (os.path.isfile(icon_path) and os.access(icon_path, os.R_OK)): - try: - resize_image(48.0, store_path, icon_path) - except (ValueError, OSError,): - # image should be a GdkPixbuf or empty - return # wtf! - self.pkginfo_ui.pkgImage.set_from_file(icon_path) + self._ugc_icon_store_path = store_path + # if image has __icon__ as title, use as real icon + icon_path = store_path + ".sulfur_icon" + if not (os.path.isfile(icon_path) and os.access(icon_path, os.R_OK)): + try: + resize_image(48.0, store_path, icon_path) + except (ValueError, OSError,): + # image should be a GdkPixbuf or empty + return # wtf! + gobject.idle_add(self.pkginfo_ui.pkgImage.set_from_file, icon_path) def spawn_docs_fetch(self): @@ -930,36 +1010,40 @@ class PkgInfoMenu(MenuSkel): return if self.repository is None: return + if self._webserv is None: + return - for doc_type in self.ugc_data: - if int(doc_type) not in (etpConst['ugc_doctypes']['image'],): + try: + docs_map = self.ugc_data.copy() + except AttributeError: + # must be concurrency-free + return + + for doc_type, docs in docs_map.items(): + + if doc_type not in (Document.IMAGE_TYPE_ID, Document.ICON_TYPE_ID): continue - for mydoc in self.ugc_data[doc_type]: - if 'store_url' not in mydoc: - continue - if not mydoc['store_url']: - continue - store_path = self.Entropy.UGC.UGCCache.get_stored_document( - mydoc['iddoc'], self.repository, mydoc['store_url']) + for mydoc in docs: + with Privileges(): + try: + store_path = self._webserv.get_document_url(mydoc) + except ClientWebService.DocumentError: + store_path = None if store_path is None: - try: - self.Entropy.UGC.UGCCache.store_document(mydoc['iddoc'], - self.repository, mydoc['store_url']) - except PermissionDenied: - # not enough privileges - continue - store_path = self.Entropy.UGC.UGCCache.get_stored_document( - mydoc['iddoc'], self.repository, mydoc['store_url']) + continue + if not (os.path.isfile(store_path) and \ + os.access(store_path, os.R_OK)): + continue - if (store_path != None) and os.access(store_path, os.R_OK): - preview_path = store_path+".sulfur_preview" - try: - resize_image(64.0, store_path, preview_path) - mydoc['preview_path'] = preview_path - except (ValueError, OSError,): - continue + preview_path = store_path+".sulfur_preview_height" + try: + resize_image_height(self._ugc_row_height - 4, store_path, + preview_path) + except (ValueError, OSError,): + continue + mydoc['preview_path'] = preview_path def populate_ugc_view(self): @@ -969,58 +1053,59 @@ class PkgInfoMenu(MenuSkel): spawn_fetch = False doc_types = list(self.ugc_data.keys()) doc_type_image_map = { - 1: const.ugc_text_pix, - 2: const.ugc_text_pix, - 3: const.ugc_image_pix, - 4: const.ugc_generic_pix, - 5: const.ugc_video_pix, + Document.COMMENT_TYPE_ID: const.ugc_text_pix, + Document.IMAGE_TYPE_ID: const.ugc_image_pix, + Document.FILE_TYPE_ID: const.ugc_generic_pix, + Document.VIDEO_TYPE_ID: const.ugc_video_pix, + Document.ICON_TYPE_ID: const.ugc_image_pix, } doc_type_background_map = { - 1: ('#67AB6F', '#599360'), - 2: ('#67AB6F', '#599360'), - 3: ('#AB8158', '#CA9968'), - 4: ('#BBD5B0', '#99AE90'), - 5: ('#A5C0D5', '#8EA5B7'), + Document.COMMENT_TYPE_ID: ('#67AB6F', '#599360'), + Document.IMAGE_TYPE_ID: ('#AB8158', '#CA9968'), + Document.FILE_TYPE_ID: ('#BBD5B0', '#99AE90'), + Document.VIDEO_TYPE_ID: ('#A5C0D5', '#8EA5B7'), + Document.ICON_TYPE_ID: ('#AB8158', '#CA9968'), } doc_type_foreground_map = { - 1: '#FFFFFF', - 2: '#FFFFFF', - 3: '#FFFFFF', - 4: '#FFFFFF', - 5: '#FFFFFF', + Document.COMMENT_TYPE_ID: '#FFFFFF', + Document.IMAGE_TYPE_ID: '#FFFFFF', + Document.FILE_TYPE_ID: '#FFFFFF', + Document.VIDEO_TYPE_ID: '#FFFFFF', + Document.ICON_TYPE_ID: '#FFFFFF', } counter = 1 for doc_type in doc_types: spawn_fetch = True - image_path = doc_type_image_map.get(int(doc_type)) + image_path = doc_type_image_map.get(doc_type) cat_dict = { 'is_cat': True, 'image_path': image_path, 'parent_desc': "%s (%s)" % ( - etpConst['ugc_doctypes_description'].get(int(doc_type)), + Document.DESCRIPTION_PLURAL.get(doc_type, _("N/A")), len(self.ugc_data[doc_type]),), - 'foreground': doc_type_foreground_map.get(int(doc_type)), - 'background': doc_type_background_map.get(int(doc_type)), + 'foreground': doc_type_foreground_map.get(doc_type), + 'background': doc_type_background_map.get(doc_type), } parent = self.ugcModel.append( None, (cat_dict,) ) docs_dates = {} for mydoc in self.ugc_data[doc_type]: ts = mydoc['ts'] - if ts not in docs_dates: - docs_dates[ts] = [] - docs_dates[ts].append(mydoc) + lst = docs_dates.setdefault(ts, []) + lst.append(mydoc) + sorted_dates = sorted(docs_dates.keys()) for ts in sorted_dates: for mydoc in docs_dates[ts]: mydoc['image_path'] = const.ugc_pixmap_small - mydoc['foreground'] = doc_type_foreground_map.get(int(doc_type)) - mydoc['background'] = doc_type_background_map.get(int(doc_type)) + mydoc['foreground'] = doc_type_foreground_map.get(doc_type) + mydoc['background'] = doc_type_background_map.get(doc_type) mydoc['counter'] = counter self.ugcModel.append( parent, (mydoc,) ) counter += 1 if spawn_fetch: - gobject.idle_add(self.spawn_docs_fetch) + th = ParallelTask(self.spawn_docs_fetch) + th.start() self.ugcView.set_property('headers-visible', True) self.ugcView.set_property('enable-search', True) @@ -1232,21 +1317,89 @@ class PkgInfoMenu(MenuSkel): def on_starEvent1_button_release_event(self, widget, event): self.vote_click(1) + def _ugc_login(self): + if self.repository is None: + return False + if self._webserv is None: + return False + + def fake_callback(*args, **kwargs): + return True + + # use input box to read login + input_params = [ + ('username', _('Username'), fake_callback, False), + ('password', _('Password'), fake_callback, True) + ] + login_data = self.Entropy.input_box( + "%s %s %s" % ( + _('Please login against'), self.repository, _('repository'),), + input_params, + cancel_button = True + ) + if not login_data: + return False + + username, password = login_data['username'], login_data['password'] + with Privileges(): + self._webserv.add_credentials(username, password) + try: + self._webserv.validate_credentials() + except WebService.AuthenticationFailed: + okDialog(self.window, + _("Authentication error. Not logged in.")) + return False + + okDialog(self.window, + _("Successfully logged in.")) + return True + def vote_click(self, vote): - if self.Entropy.UGC is None: - return if not (self.repository and self.pkgkey): return - if not self.Entropy.UGC.is_repository_eapi3_aware(self.repository): - return - status, err_msg = self.Entropy.UGC.add_vote(self.repository, self.pkgkey, vote) - if status: - self.set_stars_from_repository() - msg = "%s: %s" % (SulfurConf.color_good, _("Vote registered successfully"), vote,) - else: - msg = "%s: %s" % (SulfurConf.color_error, _("Error registering vote"), err_msg,) - self.pkginfo_ui.ugcMessageBox.set_markup(msg) + def _do_vote(): + if self._webserv is None: + return + with Privileges(): + try: + voted = self._webserv.add_vote(self.pkgkey, vote) + except WebService.AuthenticationRequired: + def _do_login(): + logged_in = self._ugc_login() + if logged_in: + # call myself again + gobject.idle_add(self.vote_click, vote) + return False + gobject.idle_add(_do_login) + return + + if voted: + # inform user first + msg = "%s: %s" % ( + SulfurConf.color_good, + _("Vote registered successfully"), vote,) + else: + msg = "%s: %s" % ( + SulfurConf.color_error, _("Error"), _("Already voted"),) + gobject.idle_add(self.pkginfo_ui.ugcMessageBox.set_markup, msg) + if voted: + with Privileges(): + # need to refill local cache + done = True + try: + self._webserv.get_votes([self.pkgkey], cache = False) + except WebService.WebServiceException as err: + # ouch! drop everything completely + self._webserv._drop_cached("get_votes") + done = False + + if done: + gobject.idle_add(self.set_stars_from_repository) + + th = ParallelTask(_do_vote) + th.start() + def star_enter(self, widget, event, number): self.set_stars(number, hover = True) @@ -1347,7 +1500,7 @@ class PkgInfoMenu(MenuSkel): # Setup image column cell = gtk.CellRendererPixbuf() - cell.set_property('height', 78) + cell.set_property('height', self._ugc_row_height) column = gtk.TreeViewColumn( _("Type"), cell ) # Document Type column.set_cell_data_func( cell, self.ugc_pixbuf ) column.set_sizing( gtk.TREE_VIEW_COLUMN_FIXED ) @@ -1356,15 +1509,17 @@ class PkgInfoMenu(MenuSkel): self.ugcView.append_column( column ) cell = gtk.CellRendererText() - column = gtk.TreeViewColumn( _( "Content" ), cell ) - column.set_resizable( True ) - column.set_cell_data_func( cell, self.ugc_content ) - column.set_sizing( gtk.TREE_VIEW_COLUMN_FIXED ) - column.set_fixed_width( 350 ) + cell.set_property('wrap-mode', gtk.WRAP_WORD) + cell.set_property('wrap-width', 340) + column = gtk.TreeViewColumn(_( "Content" ), cell) + column.set_resizable(True) + column.set_cell_data_func(cell, self.ugc_content) + column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + column.set_fixed_width(350) column.set_expand(True) - column.set_sort_column_id( -1 ) - self.ugcView.append_column( column ) - self.ugcView.set_model( self.ugcModel ) + column.set_sort_column_id(-1) + self.ugcView.append_column(column) + self.ugcView.set_model(self.ugcModel) self.ugcView.set_property('headers-visible', True) self.ugcView.set_property('enable-search', True) @@ -1436,24 +1591,17 @@ class PkgInfoMenu(MenuSkel): # set package image pkgatom = self._pkg_meta['atom'] - self.vote = int(pkg.vote) + self.vote = int(pkg.votefloat) self.repository = pkg.repoid self.pkgkey = entropy.dep.dep_getkey(pkgatom) self.set_stars_from_repository() self.pkginfo_ui.pkgImage.set_from_file(self.pkg_pixmap) self.pkginfo_ui.ugcSmallIcon.set_from_file(self.ugc_small_pixmap) - self.pkginfo_ui.ugcIcon.set_from_file(self.ugc_pixmap) - self.pkginfo_ui.refreshImage.set_from_file(self.refresh_pixmap) self.pkginfo_ui.labelAtom.set_markup("%s" % ( cleanMarkupString(pkgatom),)) self.pkginfo_ui.labelDescription.set_markup("%s" % ( cleanMarkupString(self._pkg_meta['description']),)) - self.pkginfo_ui.ugcDescriptionLabel.set_markup("%s\n%s" % ( - _("Share your opinion, your documents, your screenshots!"), - _("Be part of our Community!") - ) - ) self.set_download_numbers_from_repository() bold_items = [ self.pkginfo_ui.locationLabel, @@ -1475,7 +1623,6 @@ class PkgInfoMenu(MenuSkel): self.pkginfo_ui.maskedLabel, self.pkginfo_ui.triggerLabel, self.pkginfo_ui.configProtectLabel, - self.pkginfo_ui.ugcTitleLabel, self.pkginfo_ui.changeLogLabel ] for item in bold_items: @@ -1718,14 +1865,13 @@ class SecurityAdvisoryMenu(MenuSkel): class UGCInfoMenu(MenuSkel): - def __init__(self, Entropy, obj, repository, window): + def __init__(self, Entropy, obj, package_name, repository, window): - import subprocess - self.subprocess = subprocess + self.package_name = package_name self.repository = repository self.window = window self.Entropy = Entropy - self.ugc_data = obj.copy() + self.ugc_doc = obj self.ugcinfo_ui = UI( const.GLADE_FILE, 'ugcInfo', 'entropy' ) self.ugcinfo_ui.signal_autoconnect(self._getAllMethods()) self.ugcinfo_ui.ugcInfo.set_transient_for(self.window) @@ -1736,49 +1882,62 @@ class UGCInfoMenu(MenuSkel): return True def on_getButton_clicked(self, widget): - if self.ugc_data['store_url'] != None: - load_url(self.ugc_data['store_url']) + url = self.ugc_doc.document_url() + if url is not None: + load_url(url) def load(self): - pix_path = self.ugc_data['image_path'] - if self.ugc_data.get('preview_path') is not None: - if os.path.isfile(self.ugc_data['preview_path']) and \ - os.access(self.ugc_data['preview_path'], os.R_OK): - pix_path = self.ugc_data['preview_path'] + pix_path = self.ugc_doc['image_path'] + if self.ugc_doc.get('preview_path') is not None: + if os.path.isfile(self.ugc_doc['preview_path']) and \ + os.access(self.ugc_doc['preview_path'], os.R_OK): + pix_path = self.ugc_doc['preview_path'] self.ugcinfo_ui.ugcImage.set_from_file(pix_path) self.ugcinfo_ui.labelKey.set_markup("%s" % ( - self.ugc_data['pkgkey'],)) - doc_type_desc = etpConst['ugc_doctypes_description_singular'].get( - int(self.ugc_data['iddoctype'])) + self.package_name,)) + doc_type = self.ugc_doc.document_type() + doc_id = self.ugc_doc.document_id() + + doc_type_desc = Document.DESCRIPTION_SINGULAR.get(doc_type, _("N/A")) self.ugcinfo_ui.labelTypedesc.set_markup( "[%s:%d] %s" % ( _("Id"), - int(self.ugc_data['iddoc']), + doc_id, doc_type_desc, ) ) + title = self.ugc_doc.get(Document.DOCUMENT_TITLE_ID, _("N/A")) + description = self.ugc_doc.get(Document.DOCUMENT_DESCRIPTION_ID, + _("N/A")) + username = self.ugc_doc.get(DocumentFactory.DOCUMENT_USERNAME_ID, + _("N/A")) + keywords = self.ugc_doc.document_keywords().strip().split() + ts = self.ugc_doc.document_timestamp() + ts = entropy.tools.convert_unix_time_to_human_time(ts) + self.ugcinfo_ui.titleContent.set_markup("%s" % ( - const_convert_to_unicode(self.ugc_data['title']),)) + cleanMarkupString(const_convert_to_unicode(title)),)) self.ugcinfo_ui.descriptionContent.set_markup("%s" % ( - const_convert_to_unicode(self.ugc_data['description']),)) + cleanMarkupString(const_convert_to_unicode(description)),)) self.ugcinfo_ui.authorContent.set_markup("%s" % ( - const_convert_to_unicode(self.ugc_data['username']),)) + cleanMarkupString(const_convert_to_unicode(username)),)) self.ugcinfo_ui.dateContent.set_markup( - "%s" % (self.ugc_data['ts'],)) - self.ugcinfo_ui.keywordsContent.set_markup("%s" % ( - const_convert_to_unicode(', '.join(self.ugc_data['keywords'])),)) - self.ugcinfo_ui.sizeContent.set_markup("%s" % ( - entropy.tools.bytes_into_human(self.ugc_data['size']),)) + "%s" % (cleanMarkupString(ts),)) + if keywords: + keywords_str = ", ".join(keywords) + self.ugcinfo_ui.keywordsContent.set_markup("%s" % ( + cleanMarkupString(const_convert_to_unicode(keywords_str)),)) + else: + self.ugcinfo_ui.keywordsContent.set_markup(_("No keywords")) bold_items = [ self.ugcinfo_ui.titleLabel, self.ugcinfo_ui.descLabel, self.ugcinfo_ui.authorLabel, self.ugcinfo_ui.dateLabel, - self.ugcinfo_ui.keywordsLabel, - self.ugcinfo_ui.sizeLabel + self.ugcinfo_ui.keywordsLabel ] for item in bold_items: t = item.get_text() @@ -1790,17 +1949,13 @@ class UGCInfoMenu(MenuSkel): self.ugcinfo_ui.descriptionContent, self.ugcinfo_ui.authorContent, self.ugcinfo_ui.dateContent, - self.ugcinfo_ui.keywordsContent, - self.ugcinfo_ui.sizeContent + self.ugcinfo_ui.keywordsContent ] for item in small_items: t = item.get_label() item.set_markup("%s" % (t,)) - if self.ugc_data['iddoctype'] in ( - etpConst['ugc_doctypes']['comments'], - etpConst['ugc_doctypes']['bbcode_doc'],): - + if self.ugc_doc.is_comment(): self.ugcinfo_ui.ugcTable.remove(self.ugcinfo_ui.descLabel) self.ugcinfo_ui.ugcTable.remove(self.ugcinfo_ui.descriptionContent) self.ugcinfo_ui.buttonBox.hide() @@ -1808,13 +1963,9 @@ class UGCInfoMenu(MenuSkel): ### # we need to properly handle raw data coming from ddata dict key ### - if const_isunicode(self.ugc_data['ddata']): - buf_text = self.ugc_data['ddata'] - elif const_israwstring(self.ugc_data['ddata']): - buf_text = const_convert_to_unicode(self.ugc_data['ddata']) - else: # mysql shitty type? - buf_text = const_convert_to_unicode(self.ugc_data['ddata'].tostring()) - mybuf.set_text(buf_text) + comment = self.ugc_doc.document_data() + comment = const_convert_to_unicode(comment) + mybuf.set_text(comment) self.ugcinfo_ui.textContent.set_buffer(mybuf) else: self.ugcinfo_ui.textFrame.hide() @@ -1824,8 +1975,6 @@ class UGCInfoMenu(MenuSkel): class UGCAddMenu(MenuSkel): - DOC_TYPES_LIST = sorted(etpConst['ugc_doctypes_description_singular']) - def __init__(self, Entropy, pkgkey, repository, window, refresh_cb): self.loading_pix = gtk.image_new_from_file(const.loading_pix) @@ -1842,7 +1991,7 @@ class UGCAddMenu(MenuSkel): self.ugcadd_ui.ugcAdd.add_events(gtk.gdk.BUTTON_PRESS_MASK) self.store = None self.refresh_cb = refresh_cb - self.text_types = (etpConst['ugc_doctypes']['comments'], etpConst['ugc_doctypes']['bbcode_doc'],) + self.text_types = [Document.COMMENT_TYPE_ID] self.file_selected = None self._combo_doc_types = [] @@ -1854,16 +2003,50 @@ class UGCAddMenu(MenuSkel): myiter = widget.get_active_iter() idx = self.store.get_value( myiter, 0 ) if idx in self.text_types: - txt = "%s %s" % (_("Write your"), etpConst['ugc_doctypes_description_singular'][idx],) # write your - self.setup_text_insert() + desc = Document.DESCRIPTION_SINGULAR[idx] + txt = "%s %s" % (_("Write your"), desc,) + self.setup_text_insert(txt) else: - txt = "%s %s" % (_("Select your"), etpConst['ugc_doctypes_description_singular'][idx],) # select your + desc = Document.DESCRIPTION_SINGULAR[idx] + txt = "%s %s" % (_("Select your"), desc,) self.setup_file_insert(txt) def on_ugcAddFileChooser_file_set(self, widget): self.file_selected = widget.get_filename() + def _ugc_login(self, webserv): + + def fake_callback(*args, **kwargs): + return True + + # use input box to read login + input_params = [ + ('username', _('Username'), fake_callback, False), + ('password', _('Password'), fake_callback, True) + ] + login_data = self.Entropy.input_box( + "%s %s %s" % ( + _('Please login against'), self.repository, _('repository'),), + input_params, + cancel_button = True + ) + if not login_data: + return False + + username, password = login_data['username'], login_data['password'] + with Privileges(): + webserv.add_credentials(username, password) + try: + webserv.validate_credentials() + except WebService.AuthenticationFailed: + okDialog(self.window, + _("Authentication error. Not logged in.")) + return False + + return True + def on_submitButton_clicked(self, widget): + dialog_title = _("Submit issue") myiter = self.ugcadd_ui.ugcAddTypeCombo.get_active_iter() doc_type = self.store.get_value( myiter, 0 ) @@ -1877,20 +2060,24 @@ class UGCAddMenu(MenuSkel): end_iter = mybuf.get_end_iter() description = mybuf.get_text(start_iter, end_iter) if not description: - okDialog(self.window, _("Empty Document"), title = dialog_title) + okDialog(self.window, _("Empty Document"), + title = dialog_title) return False else: if not description: - okDialog(self.window, _("Invalid Description"), title = dialog_title) + okDialog(self.window, _("Invalid Description"), + title = dialog_title) return False doc_path = self.file_selected # checking if doc_type is None: - okDialog(self.window, _("Invalid Document Type"), title = dialog_title) + okDialog(self.window, _("Invalid Document Type"), + title = dialog_title) return False if not title: - okDialog(self.window, _("Invalid Title"), title = dialog_title) + okDialog(self.window, _("Invalid Title"), + title = dialog_title) return False # confirm ? @@ -1900,68 +2087,125 @@ class UGCAddMenu(MenuSkel): self.show_loading() - old_show_progress = self.Entropy.UGC.show_progress - self.Entropy.UGC.show_progress = True - bck_output = self.Entropy.output - self.Entropy.output = self.do_label_update_progress try: - t = ParallelTask(self.do_send_document_autosense, doc_type, doc_path, title, description, keywords_text) - t.start() - while True: - if not t.isAlive(): break - while gtk.events_pending(): - gtk.main_iteration() - time.sleep(0.06) - rslt, data = t.get_rc() - finally: - self.Entropy.UGC.show_progress = old_show_progress - self.Entropy.output = bck_output - - - self.hide_loading() - if not rslt: - txt = "%s: %s | %s" % (SulfurConf.color_error, _("UGC Error"), rslt, data,) - self.ugcadd_ui.ugcAddStatusLabel.set_markup(txt) + webserv = get_entropy_webservice(self.Entropy, self.repository) + except WebService.UnsupportedService as err: + okDialog(self.window, _("Unsupported Service"), + title = dialog_title) return False - else: - okDialog(self.ugcadd_ui.ugcAdd, _("Document added successfully. Thank you"), title = _("Success!")) - self.on_closeAdd_clicked(None, None) - self.refresh_cb() - return True - def do_label_update_progress(self, *myargs, **mykwargs): - - count = mykwargs.get("count") - percent = mykwargs.get("percent") - text = myargs[0].encode('utf-8') - - count_str = "" - if count: - if len(count) > 1: - if percent: - count_str = " ("+str(round((float(count[0])/count[1])*100, 1))+"%) " - else: - count_str = " (%s/%s) " % (str(count[0]), str(count[1]),) - - txt = count_str+text - txt = decolorize(txt) - self.ugcadd_ui.ugcAddStatusLabel.set_markup(cleanMarkupString(txt)) - - def do_send_document_autosense(self, doc_type, doc_path, title, description, keywords_text): try: - rslt, data = self.Entropy.UGC.send_document_autosense( - self.repository, - str(self.pkgkey), - doc_type, - doc_path, - title, - description, - keywords_text - ) - except Exception as e: - rslt = False - data = e - return rslt, data + available = webserv.service_available() + except WebService.WebServiceException: + okDialog(self.window, _("Service is unavailable"), + title = dialog_title) + return False + if not available: + okDialog(self.window, _("Service is unavailable"), + title = dialog_title) + return False + + with Privileges(): + # ask user to login + need_login = False + if webserv.credentials_available(): + try: + webserv.validate_credentials() + except WebService.AuthenticationFailed: + need_login = True + else: + need_login = True + + if need_login: + logged_in = self._ugc_login(webserv) + if not logged_in: + return False + + # can't be None + username = webserv.get_credentials() + webserv._set_transfer_callback(self.do_label_update_progress) + + # Build up the Document object + doc_factory = DocumentFactory(self.repository) + + doc_f = None + try: + if doc_type == Document.COMMENT_TYPE_ID: + doc = doc_factory.comment(username, description, + title, keywords_text) + elif doc_type == Document.ICON_TYPE_ID: + doc_f = open(doc_path, "rb") + doc = doc_factory.icon(username, doc_f, title, + description, keywords_text) + elif doc_type == Document.FILE_TYPE_ID: + doc_f = open(doc_path, "rb") + doc = doc_factory.file(username, doc_f, title, + description, keywords_text) + elif doc_type == Document.IMAGE_TYPE_ID: + doc_f = open(doc_path, "rb") + doc = doc_factory.image(username, doc_f, title, + description, keywords_text) + elif doc_type == Document.VIDEO_TYPE_ID: + doc_f = open(doc_path, "rb") + doc = doc_factory.video(username, doc_f, title, + description, keywords_text) + else: + # WTF + raise AssertionError("invalid document type") + except AssertionError as err: + okDialog(self.window, "%s: %s" % (_("Invalid fields"), err), + title = dialog_title) + if doc_f is not None: + doc_f.close() + doc_f = None + return False + + def _do_add_document(doc, doc_f): + err_msg = None + try: + new_doc = webserv.add_document(self.pkgkey, doc) + except WebService.WebServiceException as err: + new_doc = None + err_msg = str(err) + finally: + if doc_f is not None: + doc_f.close() + gobject.idle_add(self.hide_loading) + + if new_doc is not None: + def _happy_ending(): + okDialog(self.ugcadd_ui.ugcAdd, + _("Document added successfully. Thank you"), + title = _("Success!")) + self.on_closeAdd_clicked(None, None) + self.refresh_cb() + return False + gobject.idle_add(_happy_ending) + else: + def _unhappy_ending(): + txt = "%s: %s" % ( + SulfurConf.color_error, _("UGC Error"), err_msg,) + self.ugcadd_ui.ugcAddStatusLabel.set_markup(txt) + return False + gobject.idle_add(_unhappy_ending) + + th = ParallelTask(_do_add_document, doc, doc_f) + th.start() + return False + + def do_label_update_progress(self, transfered, total, download): + + if download: + action = _("Downloading") + else: + action = _("Uploading") + + percent = 100 + if (total > 0) and (transfered <= total): + percent = int(round((float(transfered)/total) * 100, 1)) + + msg = cleanMarkupString("[%s%s] %s ..." % (percent, "%", action)) + gobject.idle_add(self.ugcadd_ui.ugcAddStatusLabel.set_markup, msg) def show_loading(self): self.ugcadd_ui.ugcAddButtonBox.hide() @@ -1995,13 +2239,15 @@ class UGCAddMenu(MenuSkel): self.ugcadd_ui.ugcAddInsertLabel.set_markup(txt) def prepare_image_insert(self, pkg_name, image_path, as_icon = False): - image_doctype = etpConst['ugc_doctypes']['image'] + doc_type = Document.IMAGE_TYPE_ID + if as_icon: + doc_type = Document.ICON_TYPE_ID self.ugcadd_ui.ugcAddTypeCombo.set_active( - self._combo_doc_types.index(image_doctype)) + self._combo_doc_types.index(doc_type)) self.ugcadd_ui.ugcAddFileChooser.set_filename(image_path) self.file_selected = image_path if as_icon: - self.ugcadd_ui.ugcAddTitleEntry.set_text("__icon__") + self.ugcadd_ui.ugcAddTitleEntry.set_text(pkg_name + " icon") self.ugcadd_ui.ugcAddDescEntry.set_text("This is the %s icon" % ( pkg_name,)) else: @@ -2011,9 +2257,8 @@ class UGCAddMenu(MenuSkel): pkg_name,)) def prepare_file_insert(self, pkg_name, file_path): - file_doctype = etpConst['ugc_doctypes']['generic_file'] self.ugcadd_ui.ugcAddTypeCombo.set_active( - self._combo_doc_types.index(file_doctype)) + self._combo_doc_types.index(Document.FILE_TYPE_ID)) self.ugcadd_ui.ugcAddFileChooser.set_filename(file_path) self.file_selected = file_path self.ugcadd_ui.ugcAddTitleEntry.set_text("%s document" % ( @@ -2029,19 +2274,15 @@ class UGCAddMenu(MenuSkel): _("On repository"), self.repository,)) # add types to combo - doc_types_list = UGCAddMenu.DOC_TYPES_LIST self.store = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING) self.ugcadd_ui.ugcAddTypeCombo.set_model(self.store) cell = gtk.CellRendererText() self.ugcadd_ui.ugcAddTypeCombo.pack_start(cell, True) self.ugcadd_ui.ugcAddTypeCombo.add_attribute(cell, 'text', 1) del self._combo_doc_types[:] - for idx in doc_types_list: - # disable bbcode for now - if idx == etpConst['ugc_doctypes']['bbcode_doc']: - continue + for idx in Document.SUPPORTED_TYPES: self._combo_doc_types.append(idx) - self.store.append( (idx, etpConst['ugc_doctypes_description_singular'][idx],) ) + self.store.append((idx, Document.DESCRIPTION_SINGULAR[idx],)) self.ugcadd_ui.ugcAddTypeCombo.set_active(0) # hide file chooser @@ -2063,7 +2304,7 @@ class UGCAddMenu(MenuSkel): class MaskedPackagesDialog(MenuSkel): - def __init__( self, Entropy, etpbase, parent, pkgs, top_text = None, sub_text = None ): + def __init__( self, Entropy, etpbase, parent, pkgs, top_text = None, sub_text = None): self.pkgs = pkgs self.pkgcount = 0 diff --git a/sulfur/src/sulfur/entropyapi.py b/sulfur/src/sulfur/entropyapi.py index 91ead590e..c953ea766 100644 --- a/sulfur/src/sulfur/entropyapi.py +++ b/sulfur/src/sulfur/entropyapi.py @@ -25,10 +25,11 @@ except ImportError: import _thread as thread from sulfur.setup import const, SulfurConf from sulfur.dialogs import LicenseDialog, okDialog, choiceDialog, inputDialog +from sulfur.core import get_entropy_webservice import gobject # Entropy Imports -from entropy.const import etpConst, etpUi +from entropy.const import etpConst, etpUi, const_debug_write from entropy.output import print_generic, nocolor, decolorize from entropy.client.interfaces import Client from entropy.fetchers import UrlFetcher @@ -37,6 +38,7 @@ from entropy.misc import ParallelTask from entropy.client.mirrors import StatusInterface from entropy.exceptions import RepositoryError from entropy.db.exceptions import OperationalError, IntegrityError +from entropy.services.client import WebService import entropy.dep import entropy.tools @@ -193,14 +195,22 @@ class QueueExecutor: if not download_sources: def spawn_ugc(): - try: - if self._entropy.UGC != None: - for myrepo in mykeys: - mypkgkeys = sorted(mykeys[myrepo]) - self._entropy.UGC.add_download_stats(myrepo, - mypkgkeys) - except: - pass + for myrepo, mykeys in mykeys.items(): + try: + webserv = get_entropy_webservice(self._entropy, + myrepo) + except WebService.UnsupportedService: + continue + try: + avail = webserv.service_available() + except WebService.WebServiceException: + avail = False + if avail: + try: + webserv.add_downloads(sorted(mykeys)) + except WebService.WebServiceException as err: + const_debug_write(__name__, repr(err)) + continue spawn_ugc() def do_skip_hide(): @@ -315,8 +325,6 @@ class Equo(Client): Client.init_singleton(self, *args, **kwargs) self._progress_divider = 1 self.xcache = True # force xcache enabling - if etpUi['debug']: - self.UGC.quiet = False self._mthread_rc = { 'ask_question': {}, 'input_box': {}, diff --git a/sulfur/src/sulfur/events.py b/sulfur/src/sulfur/events.py index b7c95f4c2..c41728c28 100644 --- a/sulfur/src/sulfur/events.py +++ b/sulfur/src/sulfur/events.py @@ -35,6 +35,7 @@ from entropy.output import print_generic from entropy.i18n import _ from entropy.misc import ParallelTask from entropy.client.mirrors import StatusInterface +from entropy.services.client import WebService import entropy.dump from sulfur.filters import Filter @@ -563,7 +564,8 @@ class SulfurApplicationEventsMixin: do_clear_filter_bar = False if action != self.lastPkgPB: - do_clear_filter_bar = True + # disabled for now, users are complaining + do_clear_filter_bar = False if action == "updates": self.ui.updatesButtonbox.show() @@ -813,46 +815,72 @@ class SulfurApplicationEventsMixin: self.load_color_settings() def on_ugcLoginButton_clicked(self, widget): - if self._entropy.UGC == None: return + if not self._ugc_status: + return model, myiter = self.ugcRepositoriesView.get_selection().get_selected() - if (myiter == None) or (model == None): return + if (myiter == None) or (model == None): + return obj = model.get_value( myiter, 0 ) if obj: - #logged_data = self._entropy.UGC.read_login(obj['repoid']) - self._entropy.UGC.login(obj['repoid'], force = True) + webserv = self._get_webservice(obj['repoid']) + if webserv is None: + return + + def fake_callback(*args, **kwargs): + return True + + # use input box to read login + input_params = [ + ('username', _('Username'), fake_callback, False), + ('password', _('Password'), fake_callback, True) + ] + login_data = self._entropy.input_box( + "%s %s %s" % ( + _('Please login against'), obj['repoid'], _('repository'),), + input_params, + cancel_button = True + ) + if not login_data: + okDialog(self.ui.main, _("Login aborted. Not logged in.")) + return + + username, password = login_data['username'], login_data['password'] + with self._privileges: + webserv.add_credentials(username, password) + try: + webserv.validate_credentials() + except WebService.AuthenticationFailed: + okDialog(self.ui.main, + _("Authentication error. Not logged in.")) + return self.load_ugc_repositories() def on_ugcClearLoginButton_clicked(self, widget): - if self._entropy.UGC == None: return + if not self._ugc_status: + return model, myiter = self.ugcRepositoriesView.get_selection().get_selected() - if (myiter == None) or (model == None): return + if (myiter == None) or (model == None): + return obj = model.get_value( myiter, 0 ) if obj: - if not self._entropy.UGC.is_repository_eapi3_aware(obj['repoid']): + webserv = self._get_webservice(obj['repoid']) + if webserv is None: return - logged_data = self._entropy.UGC.read_login(obj['repoid']) - if logged_data: self._entropy.UGC.remove_login(obj['repoid']) + with self._privileges: + webserv.remove_credentials() self.load_ugc_repositories() - def on_ugcClearCacheButton_clicked(self, widget): - if self._entropy.UGC == None: return - repo_excluded = self._settings['repositories']['excluded'] - avail_repos = self._settings['repositories']['available'] - for repoid in list(set(list(avail_repos.keys())+list(repo_excluded.keys()))): - self._entropy.UGC.UGCCache.clear_cache(repoid) - self.set_status_ticker("%s: %s ..." % (_("Cleaning UGC cache of"), repoid,)) - self.set_status_ticker("%s" % (_("UGC cache cleared"),)) - def on_ugcClearCredentialsButton_clicked(self, widget): - if self._entropy.UGC == None: + if not self._ugc_status: return repo_excluded = self._settings['repositories']['excluded'] avail_repos = self._settings['repositories']['available'] for repoid in list(set(list(avail_repos.keys())+list(repo_excluded.keys()))): - if not self._entropy.UGC.is_repository_eapi3_aware(repoid): + webserv = self._get_webservice(repoid) + if webserv is None: continue - logged_data = self._entropy.UGC.read_login(repoid) - if logged_data: self._entropy.UGC.remove_login(repoid) + with self._privileges: + webserv.remove_credentials() self.load_ugc_repositories() self.set_status_ticker("%s" % (_("UGC credentials cleared"),)) diff --git a/sulfur/src/sulfur/package.py b/sulfur/src/sulfur/package.py index f14197aa2..f3d56be0e 100644 --- a/sulfur/src/sulfur/package.py +++ b/sulfur/src/sulfur/package.py @@ -14,10 +14,15 @@ from entropy.const import * from entropy.i18n import _ import entropy.dep import entropy.tools +from entropy.services.client import WebService from sulfur.entropyapi import Equo from sulfur.setup import cleanMarkupString, SulfurConf +from sulfur.core import get_entropy_webservice + + ENTROPY = Equo() +WEBSERV_MAP = {} class DummyEntropyPackage: @@ -130,6 +135,25 @@ class EntropyPackage: self.matched_atom = matched_atom self.installed_match = None + def _get_webservice(self): + + repository_id = self.get_repository_clean() + webserv = WEBSERV_MAP.get(repository_id) + + if webserv == -1: + return None # not available + if webserv is not None: + return webserv + + try: + webserv = get_entropy_webservice(ENTROPY, repository_id) + except WebService.UnsupportedService: + WEBSERV_MAP[repository_id] = -1 + return None + + WEBSERV_MAP[repository_id] = webserv + return webserv + # for debugging purposes, sample method """ def __setattr__(self, name, value): @@ -263,8 +287,7 @@ class EntropyPackage: if atom is None: # wtf! return 'N/A' - key = entropy.dep.dep_getkey(atom) - downloads = ENTROPY.UGC.UGCCache.get_package_downloads(repo_clean, key) + downloads = self.get_ugc_package_downloads() ugc_string = '[%s|%s] ' % ( downloads, SulfurConf.color_title2, repo_clean,) if ver_str: @@ -577,14 +600,40 @@ class EntropyPackage: return ENTROPY.Settings()['repositories']['branch'] return self.dbconn.retrieveBranch(self.matched_id) + def _get_vote_raw(self, pkg_key): + webserv = self._get_webservice() + if webserv is None: + return None + + # try to get vote for the single package first, if it fails, + # we'll search the info inside the available vote data. + got_miss = False + try: + vote = webserv.get_votes([pkg_key], cache = True, + cached = True)[pkg_key] + except WebService.CacheMiss: + vote = None + got_miss = True + + # found it? + if vote is not None: + return vote + + # fallback to available cache + try: + return webserv.get_available_votes(cache = True, + cached = True).get(pkg_key) + except WebService.CacheMiss: + return None + def get_ugc_package_vote(self): if self.pkgset: return -1 atom = self.get_name() if not atom: return None - return ENTROPY.UGC.UGCCache.get_package_vote( - self.get_repository_clean(), entropy.dep.dep_getkey(atom)) + pkg_key = entropy.dep.dep_getkey(atom) + return self._get_vote_raw(pkg_key) def get_ugc_package_vote_int(self): if self.pkgset: @@ -592,9 +641,10 @@ class EntropyPackage: atom = self.get_name() if not atom: return 0 - vote = ENTROPY.UGC.UGCCache.get_package_vote( - self.get_repository_clean(), entropy.dep.dep_getkey(atom)) - if not isinstance(vote, float): + + pkg_key = entropy.dep.dep_getkey(atom) + vote = self._get_vote_raw(pkg_key) + if vote is None: return 0 return int(vote) @@ -604,9 +654,10 @@ class EntropyPackage: atom = self.get_name() if not atom: return 0.0 - vote = ENTROPY.UGC.UGCCache.get_package_vote( - self.get_repository_clean(), entropy.dep.dep_getkey(atom)) - if not isinstance(vote, float): + + pkg_key = entropy.dep.dep_getkey(atom) + vote = self._get_vote_raw(pkg_key) + if vote is None: return 0.0 return vote @@ -619,9 +670,17 @@ class EntropyPackage: atom = self.get_name() if not atom: return 0 - key = entropy.dep.dep_getkey(atom) - return ENTROPY.UGC.UGCCache.get_package_downloads( - self.get_repository_clean(), key) + + webserv = self._get_webservice() + if webserv is None: + return 0 + pkg_key = entropy.dep.dep_getkey(atom) + try: + downloads = webserv.get_available_downloads(cache = True, + cached = True).get(pkg_key, 0) + except WebService.CacheMiss: + return 0 + return downloads def get_attribute(self, attr): cached = self.__cache.get(('get_attribute', attr,)) diff --git a/sulfur/src/sulfur/sulfur.glade b/sulfur/src/sulfur/sulfur.glade index 0173974ce..b71c935ac 100644 --- a/sulfur/src/sulfur/sulfur.glade +++ b/sulfur/src/sulfur/sulfur.glade @@ -124,9 +124,9 @@ True 2 False + - True @@ -2740,8 +2740,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -2783,8 +2783,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -2869,8 +2869,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -2891,8 +2891,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -2913,8 +2913,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -2998,8 +2998,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -3020,8 +3020,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -3042,8 +3042,8 @@ Relaxed True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + True @@ -3441,34 +3441,6 @@ Relaxed 0 - - - True - True - True - Clear all cache - - - - True - - - True - gtk-harddisk - - - 0 - - - - - - - False - False - 1 - - False @@ -6998,8 +6970,8 @@ gz True True True - + True @@ -7988,90 +7960,6 @@ gz True vertical - - - True - - - True - gtk-dnd - 0 - 6 - - - 3 - 0 - - - - - True - vertical - - - True - 0 - 1 - User Generated Content - True - - - False - 3 - 0 - - - - - True - 0 - 0 - User Generated Content description - - - 1 - - - - - 1 - - - - - True - 20 - 6 - - - True - True - True - Load User Generated Content - - - - True - gtk-refresh - 6 - - - - - - - False - False - 2 - - - - - False - 6 - 0 - - True @@ -8195,7 +8083,6 @@ gz True - 6 end @@ -8238,6 +8125,29 @@ gz 1 + + + True + True + True + True + Load User Generated Content + + + + True + 22 + view-refresh + 3 + + + + + False + False + 2 + + False @@ -8247,7 +8157,7 @@ gz - 1 + 0 @@ -9282,6 +9192,7 @@ Please read them carefully and <b>make your choice</b>. True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + vertical True @@ -9359,8 +9270,8 @@ Please read them carefully and <b>make your choice</b>. True True True - + True @@ -9385,37 +9296,13 @@ Please read them carefully and <b>make your choice</b>. True + vertical True - 6 + 5 2 3 - - - True - 0 - this is the size - True - - - 1 - 2 - 5 - 6 - - - - - True - 0 - Size - - - 5 - 6 - - True @@ -11393,8 +11280,8 @@ Please read them carefully and <b>make your choice</b>. True - + True diff --git a/sulfur/src/sulfur/views.py b/sulfur/src/sulfur/views.py index 24f207c7b..716c391e1 100644 --- a/sulfur/src/sulfur/views.py +++ b/sulfur/src/sulfur/views.py @@ -25,16 +25,16 @@ import tempfile from entropy.exceptions import RepositoryError, InvalidPackageSet from entropy.const import etpConst, etpSys, initconfig_entropy_constants, \ - const_debug_write + const_debug_write, const_debug_enabled, const_get_stringtype from entropy.misc import ParallelTask, TimeScheduled from entropy.i18n import _, _LOCALE from entropy.tools import print_traceback from entropy.dep import dep_getkey -from entropy.const import const_get_stringtype from sulfur.setup import const, cleanMarkupString, SulfurConf from sulfur.core import UI, busy_cursor, normal_cursor, \ - STATUS_BAR_CONTEXT_IDS, resize_image, fork_function + STATUS_BAR_CONTEXT_IDS, resize_image, fork_function, \ + get_entropy_webservice, Privileges from sulfur.widgets import CellRendererStars from sulfur.package import DummyEntropyPackage, EntropyPackage from sulfur.entropyapi import Equo @@ -44,6 +44,8 @@ from sulfur.event import SulfurSignals from entropy.db.exceptions import ProgrammingError, Error as DbError, \ OperationalError, DatabaseError +from entropy.services.client import WebService +from entropy.client.services.interfaces import ClientWebService class EntropyPackageViewModelInjector: @@ -494,6 +496,7 @@ class EntropyPackageView: self.selected_objs = [] self.last_row = None self.loaded_reinstallables = [] + self._webserv_map = {} self._pixbuf_map = {} self.loaded_event = None self.do_refresh_view = False @@ -689,15 +692,14 @@ class EntropyPackageView: self.queue_full_exception = Full self.queue_empty_exception = Empty + self.__pkg_ugc_icon_local_path_cache = {} self.__pkg_ugc_icon_cache = {} - self.__pkg_ugc_icon_call_cache = {} - # max 64 items at time, don't lower it too much, can cause - # request starvation with the current very low priority - self._ugc_load_queue = queue_class(64) - self._ugc_load_thread = TimeScheduled(3, self._ugc_queue_run) + # avoid DoS, lol + self._ugc_icon_load_queue = queue_class(64) + self._ugc_icon_thread = TimeScheduled(3, self._ugc_icon_queue_run) if self._ugc_status: # deferred loading, speedup UI init - gobject.timeout_add_seconds(15, self._ugc_load_thread.start) + gobject.timeout_add_seconds(15, self._ugc_icon_thread.start) def __update_ugc_event(self, event): self.view.queue_draw() @@ -864,7 +866,7 @@ class EntropyPackageView: self.loaded_event = event del self.loaded_reinstallables[:] - if objs == None: + if objs is None: objs = self.collect_selected_items(widget) event_x = event.x @@ -909,16 +911,16 @@ class EntropyPackageView: return True - elif len(objs) == 1: - obj = objs[0] - distance = 0 - for col in self.view.get_columns()[0:2]: - distance += col.get_width() - if (event_x > distance) and (event_x < (distance+self.stars_col_size)): - vote = int((event_x - distance)/self.stars_col_size*5)+1 - # this is required to fix idiotic GTK behaviour + elif column.get_title() == self.pkgcolumn_text_rating: + def _do_vote(x, widget): + rel_distance = x + valid_votes = ClientWebService.VALID_VOTES + vote = int(float(rel_distance) / self.stars_col_size * \ + len(valid_votes)) + 1 self.delayed_vote_submit(widget, vote) - return True + return False + gobject.idle_add(_do_vote, x, widget) + return True return False @@ -952,6 +954,7 @@ class EntropyPackageView: def run_properties_menu(self, menu_item): objs = self.collect_selected_items() + for obj in objs: if obj.is_group or obj.is_pkgset_cat: continue @@ -1141,7 +1144,8 @@ class EntropyPackageView: busy_cursor(self.main_window) objs = self.selected_objs oldmask = self.etpbase.unmaskingPackages.copy() - mydialog = MaskedPackagesDialog(self._entropy, self.etpbase, self.ui.main, objs) + mydialog = MaskedPackagesDialog(self._entropy, self.etpbase, + self.ui.main, objs) result = mydialog.run() if result != -5: self.etpbase.unmaskingPackages = oldmask.copy() @@ -1734,25 +1738,116 @@ class EntropyPackageView: return store + def _get_webservice(self, repository_id): + webserv = self._webserv_map.get(repository_id) + if webserv == -1: + # not available + return None + if webserv is not None: + return webserv + + with Privileges(): + try: + webserv = get_entropy_webservice(self._entropy, repository_id) + except WebService.UnsupportedService as err: + webserv = None + + if webserv is None: + self._webserv_map[repository_id] = -1 + # not available + return + + try: + available = webserv.service_available() + except WebService.WebServiceException: + available = False + + if not available: + self._webserv_map[repository_id] = -1 + # not available + return + + self._webserv_map[repository_id] = webserv + return webserv + + def _ugc_available(self, repository_id): + webserv = self._get_webservice(repository_id) + if webserv is None: + return False + return True + + def _ugc_cache_icons(self, repository_id, package_names, cache): + + webserv = self._get_webservice(repository_id) + if webserv is None: + return + + for package_name in package_names: + with Privileges(): + # sorry web service, we need data this way + try: + icon_docs = webserv.get_icons([package_name], + cache = cache)[package_name] + except WebService.WebServiceException as err: + return + + # get document urls, store to local cache + for icon_doc in icon_docs: + cache_key = package_name, icon_doc.repository_id() + if cache_key in self.__pkg_ugc_icon_local_path_cache: + const_debug_write(__name__, + "_ugc_cache_icons: already in cache: %s" % ( + cache_key,)) + continue + with Privileges(): + try: + local_path = webserv.get_document_url(icon_doc) + except ClientWebService.DocumentError as err: + const_debug_write(__name__, + "_ugc_cache_icons: document error: %s" % ( + err,)) + continue + + try: + self.__pkg_ugc_icon_cache.pop(cache_key) + except KeyError: + pass + self.__pkg_ugc_icon_local_path_cache[cache_key] = local_path + const_debug_write(__name__, + "_ugc_cache_icons: pushed to cache: %s, %s" % ( + cache_key, local_path,)) + break + def __ugc_dnd_updates_clear_cache(self, *args): const_debug_write(__name__, "EntropyPackageView, " "__ugc_dnd_updates_clear_cache called") - while True: - try: - key, repoid = self._ugc_dnd_cache_taint.pop() - except KeyError: - break - self._entropy.UGC.get_docs(repoid, key) + def _do_parallel(): - self.__pkg_ugc_icon_cache.clear() - self.__pkg_ugc_icon_call_cache.clear() - self._ugc_pixbuf_map.clear() - self._ugc_metadata_sync_exec_cache.clear() - self._emit_ugc_update() + fetch_map = {} + while True: + try: + key, repoid = self._ugc_dnd_cache_taint.pop() + except KeyError: + break + obj = fetch_map.setdefault(repoid, set()) + obj.add(key) - def _ugc_drag_data_received(self, view, context, x, y, selection, drop_id, etime): + for repository_id, keys in fetch_map.items(): + self._ugc_cache_icons(repository_id, keys, False) + + self.__pkg_ugc_icon_cache.clear() + self._ugc_pixbuf_map.clear() + self._ugc_metadata_sync_exec_cache.clear() + self._emit_ugc_update() + self.__pkg_ugc_icon_local_path_cache.clear() + + th = ParallelTask(_do_parallel) + th.start() + + def _ugc_drag_data_received(self, view, context, x, y, selection, drop_id, + etime): """ Callback function for drag_data_received event used for UGC interaction on Drag and Drop. @@ -1857,7 +1952,7 @@ class EntropyPackageView: obj.voted = 0.0 return repository = obj.repoid - if not self._entropy.UGC.is_repository_eapi3_aware(repository): + if not self._ugc_available(repository): obj.voted = 0.0 return atom = obj.name @@ -1868,8 +1963,80 @@ class EntropyPackageView: t = ParallelTask(self.vote_submit_thread, repository, key, obj) t.start() + def _ugc_login(self, webserv, repository): + + def fake_callback(*args, **kwargs): + return True + + # use input box to read login + input_params = [ + ('username', _('Username'), fake_callback, False), + ('password', _('Password'), fake_callback, True) + ] + login_data = self._entropy.input_box( + "%s %s %s" % ( + _('Please login against'), repository, _('repository'),), + input_params, + cancel_button = True + ) + if not login_data: + return False + + username, password = login_data['username'], login_data['password'] + with Privileges(): + webserv.add_credentials(username, password) + try: + webserv.validate_credentials() + except WebService.AuthenticationFailed: + okDialog(self.ui.main, + _("Authentication error. Not logged in.")) + return False + okDialog(self.ui.main, + _("Successfully logged in.")) + return True + def vote_submit_thread(self, repository, key, obj): - status, err_msg = self._entropy.UGC.add_vote(repository, key, int(obj.voted)) + + status = True + err_msg = None + + webserv = self._get_webservice(repository) + if webserv is None: + err_msg = _("Unsupported Service") + status = False + + if status: + + with Privileges(): + try: + status = webserv.add_vote(key, int(obj.voted)) + except WebService.AuthenticationRequired: + def _do_login(): + logged_in = self._ugc_login(webserv, repository) + if logged_in: + # call myself again + gobject.idle_add(self.vote_submit_thread, + repository, key, obj) + else: + t = ParallelTask(self.refresh_vote_info, obj) + t.start() + return False + gobject.idle_add(_do_login) + return False + except WebService.WebServiceException as err: + err_msg = repr(err) + status = False + + if status: + with Privileges(): + # need to refill local cache + done = True + try: + webserv.get_votes([key], cache = False) + except WebService.WebServiceException as err: + # ouch! drop everything completely + webserv._drop_cached("get_votes") + done = False if status: color = SulfurConf.color_good @@ -1878,19 +2045,16 @@ class EntropyPackageView: else: color = SulfurConf.color_error txt1 = _("Error registering vote") - txt2 = err_msg + txt2 = err_msg or _("Already voted") msg = "%s: %s" % ( color, txt1, txt2,) def do_refresh(msg): - self.ui.status.push(STATUS_BAR_CONTEXT_IDS['UGC'], - "%s: %s" % (txt1, txt2,)) self.ui.UGCMessageLabel.show() self.ui.UGCMessageLabel.set_markup(msg) return False def remove_ugc_sts(): - self.ui.status.pop(STATUS_BAR_CONTEXT_IDS['UGC']) self.ui.UGCMessageLabel.set_markup("") self.ui.UGCMessageLabel.hide() return False @@ -1899,6 +2063,7 @@ class EntropyPackageView: gobject.timeout_add_seconds(20, remove_ugc_sts) t = ParallelTask(self.refresh_vote_info, obj) t.start() + return False def refresh_vote_info(self, obj): time.sleep(5) @@ -2045,12 +2210,12 @@ class EntropyPackageView: else: cell.set_property('foreground', None) - def _ugc_queue_run(self): + def _ugc_icon_queue_run(self): - const_debug_write(__name__, "_ugc_queue_run called") + const_debug_write(__name__, "_ugc_icon_queue_run called") pkgs = set() - queue = self._ugc_load_queue + queue = self._ugc_icon_load_queue while queue.qsize(): try: @@ -2062,40 +2227,24 @@ class EntropyPackageView: if pkgs: const_debug_write(__name__, - "_ugc_queue_run, spawning fetch of: %s" % (pkgs,)) - self._spawn_ugc_metadata_fetch(pkgs) + "_ugc_icon_queue_run, spawning fetch of: %s" % (pkgs,)) + self._spawn_ugc_icon_docs_fetch(pkgs) - def _spawn_ugc_metadata_fetch(self, pkgs): + def _spawn_ugc_icon_docs_fetch(self, pkgs): def do_ugc_sync(): + pkg_map = {} for key, repoid in pkgs: - const_debug_write(__name__, - "_spawn_ugc_metadata_fetch, checking: %s, %s" % ( - key, repoid,)) - if self._entropy.UGC.UGCCache.is_alldocs_cached(key, repoid): - const_debug_write(__name__, - "_spawn_ugc_metadata_fetch, skipping !!: %s, %s" % ( - key, repoid,)) - continue - outcome = self._entropy.UGC.get_docs(repoid, key) - const_debug_write(__name__, - "_spawn_ugc_metadata_fetch, get_docs() called: %s" % ( - outcome,)) - self.__pkg_ugc_icon_call_cache.clear() + obj = pkg_map.setdefault(repoid, []) + obj.append(key) - def do_fork(): - fork_function(do_ugc_sync, self._emit_ugc_update) + for repoid, keys in pkg_map.items(): + self._ugc_cache_icons(repoid, keys, True) + self._emit_ugc_update() - gobject.idle_add(do_fork, priority = gobject.PRIORITY_LOW) - - def _spawn_ugc_icon_fetch(self, icon_doc, repoid): - - def do_icon_fetch(): - self._entropy.UGC.UGCCache.store_document(icon_doc['iddoc'], - repoid, icon_doc['store_url']) - - fork_function(do_icon_fetch, self._emit_ugc_update) + th = ParallelTask(do_ugc_sync) + th.start() def _get_cached_pkg_ugc_icon(self, pkg): @@ -2109,9 +2258,12 @@ class EntropyPackageView: cache_key = (key, repoid,) cached = self.__pkg_ugc_icon_cache.get(cache_key) + if cached == -1: + # unavailable + return None if cached is not None: - const_debug_write(__name__, "_get_cached_pkg_ugc_icon %s in RAM" % ( - cache_key,)) + #const_debug_write(__name__, "_get_cached_pkg_ugc_icon %s in RAM" % ( + # cache_key,)) return cached # validate variables... @@ -2120,38 +2272,41 @@ class EntropyPackageView: if repoid is None: return - icon_doc = self._entropy.UGC.UGCCache.get_icon_cache(key, repoid) - if icon_doc is None: - const_debug_write(__name__, - "_get_cached_pkg_ugc_icon %s NOT on disk" % ( - cache_key,)) - # not cached - return - - const_debug_write(__name__, - "_get_cached_pkg_ugc_icon %s on disk?" % ( + webserv = self._get_webservice(repoid) + if webserv is None: + const_debug_write(__name__, "_get_cached_pkg_ugc_icon %s unsup." % ( cache_key,)) + return - store_path = self._entropy.UGC.UGCCache.get_stored_document( - icon_doc['iddoc'], repoid, icon_doc['store_url']) - + store_path = self.__pkg_ugc_icon_local_path_cache.get(cache_key) if store_path is None: - # not cached - called = self.__pkg_ugc_icon_call_cache.get(cache_key) - if not called: - self._spawn_ugc_icon_fetch(icon_doc, repoid) - self.__pkg_ugc_icon_call_cache[cache_key] = True - const_debug_write(__name__, - "_get_cached_pkg_ugc_icon %s not cached on disk" % ( - cache_key,)) - return - if not (os.access(store_path, os.R_OK) and os.path.isfile(store_path)): - # not cached - const_debug_write(__name__, - "_get_cached_pkg_ugc_icon %s path not available" % ( - cache_key,)) - return + with Privileges(): + # sorry web service, we need data this way + try: + icon_docs = webserv.get_icons([key], cache = True, + cached = True)[key] + except WebService.CacheMiss as err: + #const_debug_write(__name__, + # "_get_cached_pkg_ugc_icon %s NOT cached yet" % ( + # cache_key,)) + self.__pkg_ugc_icon_cache[cache_key] = -1 + return + + # get document urls, store to local cache + for icon_doc in icon_docs: + local_path = icon_doc.local_document() + if local_path is None: + continue + store_path = local_path + break + + if store_path is None: + #const_debug_write(__name__, + # "_get_cached_pkg_ugc_icon %s NOT cached yet (2)" % ( + # cache_key,)) + self.__pkg_ugc_icon_cache[cache_key] = -1 + return icon_path = store_path + ".sulfur_icon_small" pixbuf = self._ugc_pixbuf_map.get(icon_path) @@ -2165,7 +2320,8 @@ class EntropyPackageView: if not (os.path.isfile(icon_path) and \ os.access(icon_path, os.R_OK)): try: - resize_image(self._get_row_height(), store_path, + # keep some margin... -5 + resize_image(self._get_row_height() - 5, store_path, icon_path) except (ValueError, OSError, IOError, gobject.GError): # OSError = source file moved while copying @@ -2191,52 +2347,63 @@ class EntropyPackageView: def __new_ugc_pixbuf_stash_fetch(self, pkg): # stash to queue for loading from WWW if required - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch called: %s" % ( - pkg,)) + #if const_debug_enabled(): + # const_debug_write(__name__, + # "__new_ugc_pixbuf_stash_fetch called: %s" % ( + # pkg,)) if not self._ugc_status: - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch UGC STATUS FALSE!") + if const_debug_enabled(): + const_debug_write(__name__, + "__new_ugc_pixbuf_stash_fetch UGC STATUS FALSE!") return - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch going on for: %s" % ( - pkg,)) + #if const_debug_enabled(): + # const_debug_write(__name__, + # "__new_ugc_pixbuf_stash_fetch going on for: %s" % ( + # pkg,)) try: repoid = pkg.repoid sync_item = (pkg.key, repoid) except (ProgrammingError, OperationalError) as err: - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch, ouch: %s" % ( - repr(err),)) + if const_debug_enabled(): + const_debug_write(__name__, + "__new_ugc_pixbuf_stash_fetch, ouch: %s" % ( + repr(err),)) return if sync_item in self._ugc_metadata_sync_exec_cache: - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch: already in cache: %s" % ( - sync_item,)) + #if const_debug_enabled(): + # const_debug_write(__name__, + # "__new_ugc_pixbuf_stash_fetch: already in cache: %s" % ( + # sync_item,)) return self._ugc_metadata_sync_exec_cache.add(sync_item) - if not self._entropy.UGC.is_repository_eapi3_aware(repoid): - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch: repository not EAPI3 aware: %s" % ( - repoid,)) + if not self._ugc_available(repoid): + if const_debug_enabled(): + const_debug_write(__name__, + "__new_ugc_pixbuf_stash_fetch: " + "repository not Web Services aware: %s" % ( + repoid,)) return - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch: enqueue %s" % (sync_item,)) + if const_debug_enabled(): + const_debug_write(__name__, + "__new_ugc_pixbuf_stash_fetch: enqueue %s" % (sync_item,)) try: - self._ugc_load_queue.put_nowait(sync_item) - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch: enqueued!! %s" % (sync_item,)) + self._ugc_icon_load_queue.put_nowait(sync_item) + if const_debug_enabled(): + const_debug_write(__name__, + "__new_ugc_pixbuf_stash_fetch: enqueued!! %s" % ( + sync_item,)) except self.queue_full_exception as err: # argh! queue full! - const_debug_write(__name__, - "__new_ugc_pixbuf_stash_fetch: ARGH QUEUE FULL %s" % ( - sync_item,)) + if const_debug_enabled(): + const_debug_write(__name__, + "__new_ugc_pixbuf_stash_fetch: ARGH QUEUE FULL %s" % ( + sync_item,)) pass def new_ugc_pixbuf(self, column, cell, model, myiter):