Closes rhbz#973125
Signed-off-by: Jakub Filak <jfilak(a)redhat.com>
---
src/gnome-abrt | 24 ++-
src/gnome_abrt/controller.py.in | 18 +-
src/gnome_abrt/dbus_problems.py | 207 ++++++++++++++-----
src/gnome_abrt/errors.py | 4 +-
src/gnome_abrt/oops.glade | 79 ++++++--
src/gnome_abrt/problems.py | 80 +++++---
src/gnome_abrt/views.py | 428 ++++++++++++++++++++++++++++------------
7 files changed, 595 insertions(+), 245 deletions(-)
diff --git a/src/gnome-abrt b/src/gnome-abrt
index 65db955..b9be029 100755
--- a/src/gnome-abrt
+++ b/src/gnome-abrt
@@ -22,6 +22,7 @@ import os
import sys
import logging
import signal
+import traceback
from argparse import ArgumentParser
# pygobject
@@ -40,7 +41,8 @@ from gnome_abrt.controller import Controller
from gnome_abrt.signals import glib_sigchld_signal_handler
from gnome_abrt.problems import MultipleSources
from gnome_abrt.directory_problems import DirectoryProblemSource
-from gnome_abrt.dbus_problems import DBusProblemSource
+from gnome_abrt.dbus_problems import (get_standard_problems_source,
+ get_foreign_problems_source)
from gnome_abrt.errors import UnavailableSource
from gnome_abrt.l10n import _
from gnome_abrt.wrappers import show_events_list_dialog
@@ -157,7 +159,7 @@ class OopsApplication(Gtk.Application):
self.connect("command-line", self._parse_command_line)
self.set_flags(Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.gcontext = None
- self.source = None
+ self.all_sources = None
self._url_pool = GetURLTitleSourcePool(GNOME_ABRT_URL_POOL_CAPACITY)
self._url_cache = GetURLTitleSourceCache(self._url_pool)
gnome_abrt.url.set_async_worker(self._url_cache.get_url_title_async)
@@ -188,26 +190,28 @@ class OopsApplication(Gtk.Application):
sources = []
try:
- sources.append(DBusProblemSource(self._dbus_srv.mainloop))
- except UnavailableSource as ex:
- logging.warning(ex)
+ sources.append(
+ get_standard_problems_source(self._dbus_srv.mainloop))
+ except UnavailableSource:
+ logging.warning(traceback.format_exc())
self.gcontext = GLib.main_context_default()
try:
path = os.path.join(GLib.get_user_cache_dir(), "abrt/spool")
dps = DirectoryProblemSource(path, context=self.gcontext)
sources.append(dps)
- except UnavailableSource as ex:
- logging.warning(ex)
+ except UnavailableSource:
+ logging.warning(traceback.format_exc())
if len(sources) == 0:
raise UnavailableSource("No available problem source.")
- self.source = MultipleSources(sources)
+ self.all_sources = [(_("My"), MultipleSources(sources)),
+ (_("System"), get_foreign_problems_source())]
- controller = Controller(self.source,
+ controller = Controller(self.all_sources,
glib_sigchld_signal_handler)
- main_window = OopsWindow(self, self.source, controller)
+ main_window = OopsWindow(self, self.all_sources, controller)
main_window.show_all()
self.add_window (main_window)
#pylint: disable=W0703
diff --git a/src/gnome_abrt/controller.py.in b/src/gnome_abrt/controller.py.in
index f48e26d..0dad426 100644
--- a/src/gnome_abrt/controller.py.in
+++ b/src/gnome_abrt/controller.py.in
@@ -18,11 +18,15 @@
import logging
import os
import sys
+import traceback
+
+# gnome-abrt
+from gnome_abrt import errors
class Controller(object):
- def __init__(self, source, sigchld_assign=None):
- self.source = source
+ def __init__(self, sources, sigchld_assign=None):
+ self.sources = sources
self.run_event_fn = self._first_event_run
self._sigchld_assign = sigchld_assign
@@ -47,9 +51,17 @@ class Controller(object):
self.run_event_fn("open-gui", problem)
+ def _refresh_sources(self):
+ for name, src in self.sources:
+ try:
+ src.refresh()
+ except errors.UnavailableSource:
+ logging.debug("Cannot refresh problem source {0}: {1}"
+ .format(name, traceback.format_exc()))
+
def _first_event_run(self, event, problem):
if self._sigchld_assign is not None:
- self._sigchld_assign(self.source.refresh)
+ self._sigchld_assign(self._refresh_sources)
self.run_event_fn = self._run_event_on_problem
self.run_event_fn(event, problem)
diff --git a/src/gnome_abrt/dbus_problems.py b/src/gnome_abrt/dbus_problems.py
index c035606..1c964d5 100644
--- a/src/gnome_abrt/dbus_problems.py
+++ b/src/gnome_abrt/dbus_problems.py
@@ -17,6 +17,7 @@
import os
import dbus
+import traceback
from dbus.mainloop.glib import DBusGMainLoop
import logging
@@ -33,9 +34,42 @@ BUS_IFACE = 'org.freedesktop.problems'
ABRTD_DBUS_PATH = '/org/freedesktop/problems'
ABRTD_DBUS_SIGNAL = 'Crash'
+def get_standard_problems_source(mainloop=None):
+ return DBusProblemSource(StandardProblems, mainloop)
+
+def get_foreign_problems_source(mainloop=None):
+ return DBusProblemSource(ForeignProblems, mainloop)
+
class DBusProblemSource(problems.CachedSource):
- def __init__(self, mainloop=None):
+ class Driver(object):
+ """Handles differences in behaviour while working with different sets
+ of problems obtained from D-Bus service.
+ """
+
+ def __init__(self, source):
+ self._source = source
+
+ @property
+ def get_problems_method(self):
+ """Gets an instance of a method used for obtaining problems from
+ D-Bus service.
+ """
+
+ raise NotImplementedError()
+
+ def on_new_problem(self, *args):
+ """Process a notification about detected problem."""
+
+ raise NotImplementedError()
+
+ def on_dbus_exception(self, problem_id, ex):
+ """Process D-Bus errors."""
+
+ raise NotImplementedError()
+
+
+ def __init__(self, driverclass, mainloop=None):
super(DBusProblemSource, self).__init__()
self._mainloop = mainloop
@@ -55,17 +89,7 @@ class DBusProblemSource(problems.CachedSource):
_("Can't add receiver of signal '{0}'on DBus system path '{1}': {2}")
.format(ABRTD_DBUS_SIGNAL, ABRTD_DBUS_PATH, ex))
- class ConfigObserver():
- def __init__(self, source):
- self.source = source
-
- #pylint: disable=W0613
- def option_updated(self, conf, option):
- if option == "all_problems":
- self.source.refresh()
-
- conf = config.get_configuration()
- conf.set_watch("all_problems", ConfigObserver(self))
+ self._driver = driverclass(self)
def _connect_to_problems_bus(self):
# I can't find any description of raised exceptions
@@ -74,14 +98,14 @@ class DBusProblemSource(problems.CachedSource):
try:
self._proxy = self._bus.get_object(BUS_NAME, BUS_PATH)
except dbus.exceptions.DBusException as ex:
- raise errors.UnavailableSource(
+ raise errors.UnavailableSource(self, False,
_("Can't connect to DBus system bus '{0}' path '{1}': {2}")
.format(BUS_NAME, BUS_PATH, ex))
try:
self._interface = dbus.Interface(self._proxy, BUS_IFACE)
except dbus.exceptions.DBusException as ex:
- raise errors.UnavailableSource(
+ raise errors.UnavailableSource(self, False,
_("Can't get interface '{0}' on path '{1}' in bus '{2}': {3}")
.format(BUS_IFACE, BUS_PATH, BUS_NAME, ex))
@@ -111,21 +135,9 @@ class DBusProblemSource(problems.CachedSource):
.format(args))
return
- conf = config.get_configuration()
- if (len(args) > 2
- and int(args[2]) != os.getuid()
- and not conf['all_problems']):
- logging.debug("Received the new problem signal with different uid "
- "'{0}' ('{1}') and the all problems option is not configured"
- .format(args[2], os.getuid()))
- return
-
- if len(args) == 2 and not conf['all_problems']:
- logging.debug("Received the new problem signal without the uid "
- "argument and the all problems option is not configured")
- return
-
- self.process_new_problem_id(str(args[1]))
+ prblm_id = self._driver.on_new_problem(*args)
+ if prblm_id is not None:
+ self.process_new_problem_id(prblm_id)
def chown_problem(self, problem_id):
try:
@@ -150,12 +162,7 @@ class DBusProblemSource(problems.CachedSource):
lambda iface, *params: iface.GetInfo(*params),
problem_id, args)
except dbus.exceptions.DBusException as ex:
- if ex.get_dbus_name() in [
- "org.freedesktop.problems.AuthFailure",
- "org.freedesktop.problems.InvalidProblemDir"]:
- self._remove_from_cache(problem_id)
- raise errors.InvalidProblem(problem_id, ex)
-
+ self._driver.on_dbus_exception(problem_id, ex)
logging.warning(
_("Can't get problem data from DBus service: {0!s}")
.format(ex))
@@ -163,19 +170,9 @@ class DBusProblemSource(problems.CachedSource):
return info
def _get_problems(self):
- conf = config.get_configuration()
-
prblms = []
-
try:
- if conf['all_problems']:
- prblms = self._send_dbus_message(
- #pylint: disable=W0142
- lambda iface, *args: iface.GetAllProblems(*args))
- else:
- prblms = self._send_dbus_message(
- #pylint: disable=W0142
- lambda iface, *args: self._interface.GetProblems(*args))
+ prblms = self._send_dbus_message(self._driver.get_problems_method)
except dbus.exceptions.DBusException as ex:
logging.warning(
_("Can't get list of problems from DBus service: {0!s}")
@@ -190,12 +187,120 @@ class DBusProblemSource(problems.CachedSource):
lambda iface, *args: iface.DeleteProblem(*args), [problem_id])
return True
except dbus.exceptions.DBusException as ex:
- if ex.get_dbus_name() in ["org.freedesktop.problems.AuthFailure",
- "org.freedesktop.problems.InvalidProblemDir"]:
- self._remove_from_cache(problem_id)
- raise errors.InvalidProblem(problem_id, ex)
-
+ self._driver.on_dbus_exception(problem_id, ex)
logging.warning(
_("Can't delete problem over DBus service: {0!s}")
.format(ex))
return False
+
+
+class StandardProblems(DBusProblemSource.Driver):
+ """The old behaviour."""
+
+ def __init__(self, source):
+ super(StandardProblems, self).__init__(source)
+
+ class ConfigObserver():
+ def __init__(self, source):
+ self._source = source
+
+ #pylint: disable=W0613
+ def option_updated(self, conf, option):
+ if option == "all_problems":
+ self._source.refresh()
+
+ conf = config.get_configuration()
+ conf.set_watch("all_problems", ConfigObserver(self._source))
+
+ @property
+ def get_problems_method(self):
+ conf = config.get_configuration()
+ if conf['all_problems']:
+ #pylint: disable=W0142
+ return lambda iface, *args: iface.GetAllProblems(*args)
+ else:
+ #pylint: disable=W0142
+ return lambda iface, *args: iface.GetProblems(*args)
+
+ def on_new_problem(self, *args):
+ """Accepts foreign problems only if the all_problems option is enabled
+ """
+
+ conf = config.get_configuration()
+ try:
+ if (len(args) > 2
+ and int(args[2]) != os.getuid()
+ and not conf['all_problems']):
+ logging.debug("Received the new problem signal with different "
+ "uid '{0}' ('{1}') and the all problems option "
+ "is not configured" .format(args[2], os.getuid()))
+ return None
+ except ValueError:
+ logging.debug(traceback.format_exc())
+ return None
+
+ if len(args) == 2 and not conf['all_problems']:
+ logging.debug("Received the new problem signal without the uid "
+ "argument and the all problems option is not configured")
+ return None
+
+ return str(args[1])
+
+ def on_dbus_exception(self, problem_id, ex):
+ """Process AuthFailure error in same way as InvalidProblemDir because
+ this sort of problem source provides all kinds of problems. The user
+ problems which should be always accessible and the foreign problems
+ which may be inaccessible when a user cancels the authentication
+ dialogue. So, an AuthFailure error in this context means that a one
+ of many problems is unavailable (something like an invalid problem).
+ """
+
+ if ex.get_dbus_name() in ["org.freedesktop.problems.AuthFailure",
+ "org.freedesktop.problems.InvalidProblemDir"]:
+ self._source._remove_from_cache(problem_id)
+ raise errors.InvalidProblem(problem_id, ex)
+
+
+class ForeignProblems(DBusProblemSource.Driver):
+
+ def __init__(self, source):
+ super(ForeignProblems, self).__init__(source)
+
+ @property
+ def get_problems_method(self):
+ #pylint: disable=W0142
+ return lambda iface, *args: iface.GetForeignProblems(*args)
+
+ def on_new_problem(self, *args):
+ """Accepts only foreign problems."""
+
+ args_len = len(args)
+ try:
+ if args_len == 2 or (args_len > 2 and int(args[2]) != os.getuid()):
+ return str(args[1])
+ except ValueError:
+ logging.debug(traceback.format_exc())
+ return None
+
+ logging.debug("Received the new problem signal with current user's uid "
+ "'{0}' ('{1}') in ForeignPorblems driver"
+ .format(args[2], os.getuid()))
+
+ return None
+
+ def on_dbus_exception(self, problem_id, ex):
+ """If the authentication fails, a foreign problems source become
+ temporary unavailable because we expect that the failure was caused by
+ dismissing of the authentication dialog and all problems provided by
+ this kind of source require authentication (none of them is available
+ now). So, it means that a view can try to query the temporary
+ unavailable source later.
+ """
+
+ if ex.get_dbus_name() == "org.freedesktop.problems.AuthFailure":
+ logging.debug("User dismissed D-Bus authentication")
+ raise errors.UnavailableSource(self._source, True)
+
+ if ex.get_dbus_name() == "org.freedesktop.problems.InvalidProblemDir":
+ self._source._remove_from_cache(problem_id)
+ raise errors.InvalidProblem(problem_id, ex)
diff --git a/src/gnome_abrt/errors.py b/src/gnome_abrt/errors.py
index fffaf5e..777b85e 100644
--- a/src/gnome_abrt/errors.py
+++ b/src/gnome_abrt/errors.py
@@ -29,5 +29,7 @@ class InvalidProblem(GnomeAbrtError):
class UnavailableSource(GnomeAbrtError):
- def __init__(self, message=None):
+ def __init__(self, source, temporary, message=None):
super(UnavailableSource, self).__init__(message)
+ self.source = source
+ self.temporary = temporary
diff --git a/src/gnome_abrt/oops.glade b/src/gnome_abrt/oops.glade
index b9b4b1a..497c386 100644
--- a/src/gnome_abrt/oops.glade
+++ b/src/gnome_abrt/oops.glade
@@ -433,9 +433,6 @@
</packing>
</child>
<child>
- <placeholder/>
- </child>
- <child>
<object class="GtkLabel" id="lbl_reported">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -469,6 +466,12 @@
<property name="height">1</property>
</packing>
</child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
</object>
<packing>
<property name="left_attach">1</property>
@@ -560,6 +563,39 @@
<property name="tab_fill">False</property>
</packing>
</child>
+ <child>
+ <object class="GtkBox" id="vbx_no_source_page">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="lbl_no_source">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">No source selected!</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="lbl_page_no_source">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">No source</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
</object>
<packing>
<property name="left_attach">1</property>
@@ -575,6 +611,26 @@
<property name="margin_top">10</property>
<property name="orientation">vertical</property>
<child>
+ <object class="GtkBox" id="hbox_source_btns">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
<object class="GtkScrolledWindow" id="sw_problems">
<property name="visible">True</property>
<property name="can_focus">True</property>
@@ -630,23 +686,6 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkCheckButton" id="chb_all_problems">
- <property name="related_action">gac_opt_all_problems</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="margin_top">10</property>
- <property name="xalign">0</property>
- <property name="draw_indicator">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
diff --git a/src/gnome_abrt/problems.py b/src/gnome_abrt/problems.py
index 314dfcf..96a34ee 100644
--- a/src/gnome_abrt/problems.py
+++ b/src/gnome_abrt/problems.py
@@ -22,8 +22,7 @@ import logging
import gnome_abrt.url
from gnome_abrt.application import find_application
from gnome_abrt.errors import (InvalidProblem,
- UnavailableSource,
- GnomeAbrtError)
+ UnavailableSource)
from gnome_abrt.l10n import _
class ProblemSource(object):
@@ -112,7 +111,7 @@ class Problem:
self.source = source
self.app = None
self.submission = None
- self.data = self._get_initial_data(self.source)
+ self.data = None
self._deleted = False
def __str__(self):
@@ -142,6 +141,9 @@ class Problem:
return items
def __getitem__(self, item, cached=True):
+ if self.data is None:
+ self.data = self._get_initial_data(self.source)
+
if item == 'date':
return datetime.datetime.fromtimestamp(float(self['time']))
if item == 'date_last':
@@ -197,17 +199,8 @@ class Problem:
self.source.notify(ProblemSource.CHANGED_PROBLEM, self)
def delete(self):
- # TODO : weird?? the assignemt can be moved
+ self.source.delete_problem(self.problem_id)
self._deleted = True
- try:
- self.source.delete_problem(self.problem_id)
- except GnomeAbrtError as ex:
- logging.warning(_("Can't delete problem '{0}': '{1}'")
- .format(self.problem_id, ex))
- self._deleted = False
- except Exception as ex:
- self._deleted = False
- raise
def is_reported(self):
return not self['reported_to'] is None
@@ -297,13 +290,34 @@ class MultipleSources(ProblemSource):
self._disable_notify = False
+ def __eq__(self, other):
+ if other is None:
+ return False
+
+ return (super(MultipleSources, self) == other
+ or other in self.sources)
+
+ def _pop_source(self, index):
+ self.sources.pop(index)
+ if not self.sources:
+ raise UnavailableSource()
+
def get_items(self, problem_id, *args):
pass
def get_problems(self):
result = []
- for src in self.sources:
- result.extend(src.get_problems())
+ i = 0
+ while i != len(self.sources):
+ try:
+ result.extend(self.sources[i].get_problems())
+ i += 1
+ except UnavailableSource as ex:
+ logging.debug("{0}".format(str(ex)))
+ if not ex.temporary:
+ self._pop_source(i)
+ else:
+ i += 1
return result
@@ -323,8 +337,17 @@ class MultipleSources(ProblemSource):
self._disable_notify = True
try:
- for src in self.sources:
- src.refresh()
+ i = 0
+ while i != len(self.sources):
+ try:
+ self.sources[i].refresh()
+ i += 1
+ except UnavailableSource as ex:
+ logging.debug("{0}".format(str(ex)))
+ if not ex.temporary:
+ self._pop_source(i)
+ else:
+ i += 1
finally:
self._disable_notify = False
@@ -345,9 +368,7 @@ class CachedSource(ProblemSource):
try:
self._cache.append(self._create_new_problem(prblmid))
except InvalidProblem as ex:
- logging.warning(ex)
- except UnavailableSource as ex:
- logging.warning(ex)
+ logging.warning(str(ex))
return self._cache if self._cache else []
@@ -390,19 +411,14 @@ class CachedSource(ProblemSource):
self._cache = None
self.notify()
-
def _create_new_problem(self, problem_id):
return Problem(problem_id, self)
def process_new_problem_id(self, problem_id):
- try:
- if self._problem_is_in_cache(problem_id):
- prblm = self._cache[self._cache.index(problem_id)]
- prblm.refresh()
- else:
- prblm = self._create_new_problem(problem_id)
- self._insert_to_cache(prblm)
- self.notify(ProblemSource.NEW_PROBLEM, prblm)
- except UnavailableSource as ex:
- logging.warning(_("Source failed on processing of '{0}': {1}")
- .format(problem_id, ex))
+ if self._problem_is_in_cache(problem_id):
+ prblm = self._cache[self._cache.index(problem_id)]
+ prblm.refresh()
+ else:
+ prblm = self._create_new_problem(problem_id)
+ self._insert_to_cache(prblm)
+ self.notify(ProblemSource.NEW_PROBLEM, prblm)
diff --git a/src/gnome_abrt/views.py b/src/gnome_abrt/views.py
index ebc8908..6fa512b 100644
--- a/src/gnome_abrt/views.py
+++ b/src/gnome_abrt/views.py
@@ -22,6 +22,7 @@ import time
import logging
import subprocess
import locale
+import traceback
#pygobject
#pylint: disable=E0611
@@ -97,6 +98,27 @@ def time_sort_func(model, first, second, view):
logging.debug(ex)
return 0
+def format_button_source_name(name, source):
+ return "{0} ({1})".format(name, len(source.get_problems()))
+
+def handle_problem_and_source_errors(func):
+ """Wraps repetitive exception handling."""
+
+ def wrapper_for_instance_function(oops_wnd, *args):
+ try:
+ return func(oops_wnd, *args)
+ except errors.InvalidProblem as ex:
+ logging.debug(traceback.format_exc())
+ oops_wnd._remove_problem_from_storage(ex.problem_id)
+ except errors.UnavailableSource as ex:
+ logging.debug(traceback.format_exc())
+ oops_wnd._disable_source(ex.source, ex.temporary)
+
+ return None
+
+ return wrapper_for_instance_function
+
+
#pylint: disable=R0902
class OopsWindow(Gtk.ApplicationWindow):
class OopsGtkBuilder(object):
@@ -154,11 +176,54 @@ class OopsWindow(Gtk.ApplicationWindow):
return obj
- def __init__(self, application, source, controller):
+ class SourceObserver:
+ def __init__(self, wnd):
+ self.wnd = wnd
+ self._enabled = True
+
+ def enable(self):
+ self._enabled = True
+
+ def disable(self):
+ self._enabled = False
+
+ def changed(self, source, change_type=None, problem=None):
+ if not self._enabled:
+ return
+
+ try:
+ if source == self.wnd._source:
+ if not change_type:
+ self.wnd._reload_problems(source)
+ elif change_type == problems.ProblemSource.NEW_PROBLEM:
+ self.wnd._add_problem_to_storage(problem)
+ elif change_type == problems.ProblemSource.DELETED_PROBLEM:
+ self.wnd._remove_problem_from_storage(problem)
+ elif change_type == problems.ProblemSource.CHANGED_PROBLEM:
+ self.wnd._update_problem_in_storage(problem)
+
+ self.wnd._update_source_button(source)
+ except errors.UnavailableSource as ex:
+ self.wnd._disable_source(ex.source, ex.temporary)
+
+
+ class OptionsObserver:
+ def __init__(self, wnd):
+ self.wnd = wnd
+
+ def option_updated(self, conf, option):
+ if option == 'problemid' and conf[option]:
+ self.wnd._select_problem_by_id(conf[option])
+
+
+ def __init__(self, application, sources, controller):
Gtk.ApplicationWindow.__init__(self,
title=_('Automatic Bug Reporting Tool'),
application=application)
+ if not sources:
+ raise ValueError("The source list cannot be empty!")
+
self._builder = OopsWindow.OopsGtkBuilder()
self.set_default_size(*self._builder.wnd_main.get_size())
self._builder.wnd_main.remove(self._builder.gr_main_layout)
@@ -176,60 +241,150 @@ class OopsWindow(Gtk.ApplicationWindow):
stl_ctx.add_provider_for_screen(stl_ctx.get_screen(), css_prv, 6000)
self._builder.connect_signals(self)
- self.selected_problem = None
- self._source = source
+ self._source_observer = OopsWindow.SourceObserver(self)
+ self._source_observer.disable()
+
self._reloading = False
self._controller = controller
+ self.selected_problem = None
+ self._all_sources = []
+ self._source = None
+ self._handling_source_click = False
+ for name, src in sources:
+ self._all_sources.append(src)
+ src.attach(self._source_observer)
+
+ label = None
+ try:
+ label = format_button_source_name(name, src)
+ except errors.UnavailableSource:
+ logging.debug("Unavailable source: {0}".format(name))
+ continue
+
+ src_btn = Gtk.ToggleButton(label)
+ src_btn.set_visible(True)
+ # add an extra member source (I don't like it but it so easy)
+ src_btn.source = src
+ self._builder.hbox_source_btns.pack_start(src_btn,
+ True, True, 0)
+
+ # add an extra member name (I don't like it but it so easy)
+ src.name = name
+ # add an extra member button (I don't like it but it so easy)
+ src.button = src_btn
+ src_btn.connect("clicked", self._on_source_btn_clicked, src)
+
+ self._source = self._all_sources[0]
+ self._set_button_toggled(self._source.button, True)
+
self._builder.ls_problems.set_sort_column_id(0, Gtk.SortType.DESCENDING)
self._builder.ls_problems.set_sort_func(0, time_sort_func, self)
self._filter = ProblemsFilter(self, self._builder.tv_problems)
- class SourceObserver:
- def __init__(self, wnd):
- self.wnd = wnd
-
- def changed(self, source, change_type=None, problem=None):
- if not change_type:
- self.wnd._reload_problems(source)
- elif change_type == problems.ProblemSource.NEW_PROBLEM:
- self.wnd._add_problem_to_storage(problem)
- elif change_type == problems.ProblemSource.DELETED_PROBLEM:
- self.wnd._remove_problem_from_storage(problem)
- elif change_type == problems.ProblemSource.CHANGED_PROBLEM:
- self.wnd._update_problem_in_storage(problem)
-
-
- self._source_observer = SourceObserver(self)
- self._source.attach(self._source_observer)
-
self._builder.tv_problems.grab_focus()
- self._reload_problems(self._source)
-
- class OptionsObserver:
- def __init__(self, wnd):
- self.wnd = wnd
-
- def option_updated(self, conf, option):
- if option == 'problemid' and conf[option]:
- self.wnd._select_problem_by_id(conf[option])
+ try:
+ self._reload_problems(self._source)
+ except errors.UnavailableSource as ex:
+ self._disable_source(ex.source, ex.temporary)
- self._options_observer = OptionsObserver(self)
+ self._options_observer = OopsWindow.OptionsObserver(self)
conf = config.get_configuration()
conf.set_watch('problemid', self._options_observer)
self._options_observer.option_updated(conf, 'problemid')
self._builder.btn_detail.set_visible(conf['expert'])
self._builder.mi_detail.set_visible(conf['expert'])
+ # enable observer
+ self._source_observer.enable()
+
+ def _update_source_button(self, source):
+ name = format_button_source_name(source.name, source)
+ source.button.set_label(name)
+
+ def _set_button_toggled(self, button, state):
+ # set_active() triggers the clicked signal
+ # and if we set the active in program,
+ # we don't want do any action in the clicked handler
+ self._handling_source_click = True
+ try:
+ button.set_active(state)
+ finally:
+ self._handling_source_click = False
+
+ def _on_source_btn_clicked(self, btn, args):
+ # If True, then button's state was not changed by click
+ # and we don't want to switch source
+ if self._handling_source_click:
+ return
+
+ res, old_source = self._switch_source(btn.source)
+ if not res:
+ self._set_button_toggled(btn, False)
+ elif old_source is not None:
+ self._set_button_toggled(old_source.button, False)
+
+ def _switch_source(self, source):
+ """Sets the passed source as the selected source."""
+
+ result = True
+ old_source = None
+ if source != self._source:
+ try:
+ self._reload_problems(source)
+ old_source = self._source
+ self._source = source
+ except errors.UnavailableSource as ex:
+ self._disable_source(source, ex.temporary)
+ result = False
+
+ return (result, old_source)
+
+ def _disable_source(self, source, temporary):
+ if self._source is None or not self._all_sources:
+ return
+
+ # Some sources can be components of other sources.
+ # Problems are connected directly to the component sources, therefore
+ # exception's source is a component source, thus we have to find an
+ # instance of composite source which the unavailable component source
+ # belongs.
+ source_index = self._all_sources.index(source)
+
+ if source_index != -1:
+ real_source = self._all_sources[source_index]
+ self._set_button_toggled(real_source.button, False)
+ if not temporary:
+ logging.debug("Disabling source")
+ real_source.button.set_sensitive(False)
+ self._all_sources.pop(source_index)
+
+ if source != self._source:
+ return
+
+ # We just disabled the currently selected source. So, we should select
+ # some other source. The simplest way is to select the first source
+ # but only if it is not the disabled source.
+ # If the disabled source is completely unavailable (not temporary) we
+ # can always select the source at index 0 because the disabled
+ # source was removed from the _all_sources list.
+ if (not temporary or source_index != 0) and self._all_sources:
+ self._source = self._all_sources[0]
+ self._set_button_toggled(self._source.button, True)
+ else:
+ self._source = None
+
+ try:
+ self._reload_problems(self._source)
+ except errors.UnavailableSource as ex:
+ self._disable_source(ex.source, ex.temporary)
+
+ @handle_problem_and_source_errors
def _find_problem_iter(self, problem, model):
pit = model.get_iter_first()
while pit:
- try:
- if model[pit][2] == problem:
- return pit
- except errors.InvalidProblem as ex:
- self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
+ if model[pit][2] == problem:
+ return pit
pit = model.iter_next(pit)
@@ -245,8 +400,8 @@ class OopsWindow(Gtk.ApplicationWindow):
self._builder.ls_problems.append(problem_to_storage_values(problem))
return True
except errors.InvalidProblem as ex:
+ logging.debug("Exception: {0}".format(traceback.format_exc()))
self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
return False
def _remove_problem_from_storage(self, problem):
@@ -262,35 +417,46 @@ class OopsWindow(Gtk.ApplicationWindow):
if pit:
try:
values = problem_to_storage_values(problem)
- for i in xrange(0, len(values)-1):
- self._builder.ls_problems.set_value(pit, i, values[i])
except errors.InvalidProblem as ex:
+ logging.debug("Exception: {0}".format(traceback.format_exc()))
self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
return
- try:
- if problem in self._get_selected(self._builder.tvs_problems):
- self._set_problem(problem)
- except errors.InvalidProblem as ex:
- self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
+ for i in xrange(0, len(values)-1):
+ self._builder.ls_problems.set_value(pit, i, values[i])
+
+ if problem in self._get_selected(self._builder.tvs_problems):
+ self._set_problem(problem)
def _reload_problems(self, source):
- self._reloading = True
+ # Try to load and prepare the list of selected problems before we
+ # clear the view. So, we can gracefully handle UnavailableSource
+ # exception. If the reloaded source is unavailable the old list
+ # of problems remains untouched.
+ storage_problems = []
+ if source is not None:
+ prblms = source.get_problems()
+ for p in prblms:
+ try:
+ storage_problems.append(problem_to_storage_values(p))
+ except errors.InvalidProblem:
+ logging.debug("Exception: {0}"
+ .format(traceback.format_exc()))
+
old_selection = self._get_selected(self._builder.tvs_problems)
- canselect = False
+
+ self._reloading = True
+ old_selection = None
try:
self._builder.ls_problems.clear()
- prblms = source.get_problems()
- # Can select a problem only if at least one problem was added to
- # the storage
- for p in prblms:
- canselect |= self._add_problem_to_storage(p)
+
+ if storage_problems:
+ for p in storage_problems:
+ self._builder.ls_problems.append(p)
finally:
self._reloading = False
- if canselect:
+ if storage_problems:
model = self._builder.tv_problems.get_model()
pit = None
if old_selection:
@@ -306,8 +472,22 @@ class OopsWindow(Gtk.ApplicationWindow):
self._set_problem(None)
def _select_problem_by_id(self, problem_id):
+ # The problem could come from a different source than the currently
+ # loaded source. If so, try to switch to problem's origin source and
+ # select the problem after that.
+ if (self._source is not None and
+ not problem_id in self._source.get_problems()):
+ for source in self._all_sources:
+ if problem_id in source.get_problems():
+ res, old_source = self._switch_source(source)
+ if res:
+ self._set_button_toggled(old_source.button, False)
+ self._set_button_toggled(source.button, True)
+ break
+
pit = self._find_problem_iter(problem_id,
self._builder.tv_problems.get_model())
+
if pit:
self._select_problem_iter(pit)
else:
@@ -358,58 +538,58 @@ class OopsWindow(Gtk.ApplicationWindow):
self._builder.vbx_problem_messages.pack_start(msg, False, True, 0)
+ @handle_problem_and_source_errors
def _set_problem(self, problem):
- try:
- self.selected_problem = problem
-
- sensitive_btn = problem is not None
- self._builder.btn_delete.set_sensitive(sensitive_btn)
- self._builder.btn_report.set_sensitive(
- sensitive_btn and not problem['not-reportable'] )
- self._builder.btn_detail.set_sensitive(sensitive_btn)
- self._builder.vbx_links.foreach(
- lambda w, u: w.destroy(), None)
- self._builder.vbx_problem_messages.foreach(
- lambda w, u: w.destroy(), None)
-
- if problem:
- self._builder.nb_problem_layout.set_current_page(0)
- app = problem['application']
- self._builder.lbl_summary.set_text(problem['reason'] or "")
- self._builder.lbl_app_name_value.set_text(app.name or _("N/A"))
- self._builder.lbl_reason.set_text("{0} {1}".format(
- app.name or _("N/A"), _(' crashed').strip()))
- self._builder.lbl_app_version_value.set_text(
- problem['package'] or "")
- self._builder.lbl_detected_value.set_text(
- problem['date'].strftime(
- locale.nl_langinfo(locale.D_FMT)))
-
- if app.icon:
- self._builder.img_app_icon.set_from_pixbuf(app.icon)
- else:
- self._builder.img_app_icon.clear()
-
- if problem['is_reported']:
- self._builder.lbl_reported_value.set_text(_('yes'))
- self._show_problem_links(problem['submission'])
- else:
- self._builder.lbl_reported_value.set_text(_('no'))
-
- if problem['not-reportable']:
- self._show_problem_message(problem['not-reportable'])
- elif (not problem['is_reported']
- or not any((s.name == "Bugzilla"
- for s in problem['submission']))):
- self._show_problem_message(
- _("This problem hasn't been reported to <i>Bugzilla</i> yet. "
- "Our developers may need more information to fix the problem.\n"
- "Please consider <b>reporting it</b> - you may help them. Thank you."))
+ self.selected_problem = problem
+
+ sensitive_btn = problem is not None
+ self._builder.btn_delete.set_sensitive(sensitive_btn)
+ self._builder.btn_report.set_sensitive(
+ sensitive_btn and not problem['not-reportable'] )
+ self._builder.btn_detail.set_sensitive(sensitive_btn)
+ self._builder.vbx_links.foreach(
+ lambda w, u: w.destroy(), None)
+ self._builder.vbx_problem_messages.foreach(
+ lambda w, u: w.destroy(), None)
+
+ if problem:
+ self._builder.nb_problem_layout.set_current_page(0)
+ app = problem['application']
+ self._builder.lbl_summary.set_text(problem['reason'] or "")
+ self._builder.lbl_app_name_value.set_text(app.name or _("N/A"))
+ self._builder.lbl_reason.set_text("{0} {1}".format(
+ app.name or _("N/A"), _(' crashed').strip()))
+ self._builder.lbl_app_version_value.set_text(
+ problem['package'] or "")
+ self._builder.lbl_detected_value.set_text(
+ problem['date'].strftime(
+ locale.nl_langinfo(locale.D_FMT)))
+
+ if app.icon:
+ self._builder.img_app_icon.set_from_pixbuf(app.icon)
+ else:
+ self._builder.img_app_icon.clear()
+
+ if problem['is_reported']:
+ self._builder.lbl_reported_value.set_text(_('yes'))
+ self._show_problem_links(problem['submission'])
else:
+ self._builder.lbl_reported_value.set_text(_('no'))
+
+ if problem['not-reportable']:
+ self._show_problem_message(problem['not-reportable'])
+ elif (not problem['is_reported']
+ or not any((s.name == "Bugzilla"
+ for s in problem['submission']))):
+ self._show_problem_message(
+_("This problem hasn't been reported to <i>Bugzilla</i> yet. "
+ "Our developers may need more information to fix the problem.\n"
+ "Please consider <b>reporting it</b> - you may help them. Thank you."))
+ else:
+ if self._source is not None:
self._builder.nb_problem_layout.set_current_page(1)
- except errors.InvalidProblem as ex:
- self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
+ else:
+ self._builder.nb_problem_layout.set_current_page(2)
def _get_selected(self, selection):
model, rows = selection.get_selected_rows()
@@ -434,38 +614,30 @@ class OopsWindow(Gtk.ApplicationWindow):
# Clear window because of empty list of problems!
self._set_problem(None)
+ @handle_problem_and_source_errors
def on_gac_delete_activate(self, action):
- try:
- for prblm in self._get_selected(self._builder.tvs_problems):
+ for prblm in self._get_selected(self._builder.tvs_problems):
+ try:
self._controller.delete(prblm)
- except errors.InvalidProblem as ex:
- self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
+ except errors.InvalidProblem as ex:
+ logging.debug(traceback.format_exc())
+ self._remove_problem_from_storage(ex.problem_id)
+ @handle_problem_and_source_errors
def on_gac_detail_activate(self, action):
- try:
- selected = self._get_selected(self._builder.tvs_problems)
- if selected:
- self._controller.detail(selected[0])
- except errors.InvalidProblem as ex:
- self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
+ selected = self._get_selected(self._builder.tvs_problems)
+ if selected:
+ self._controller.detail(selected[0])
+ @handle_problem_and_source_errors
def on_gac_report_activate(self, action):
- try:
- selected = self._get_selected(self._builder.tvs_problems)
- if selected and not selected[0]['not-reportable']:
- self._controller.report(selected[0])
- except errors.InvalidProblem as ex:
- self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
+ selected = self._get_selected(self._builder.tvs_problems)
+ if selected and not selected[0]['not-reportable']:
+ self._controller.report(selected[0])
+ @handle_problem_and_source_errors
def on_te_search_changed(self, entry):
- try:
- self._filter.set_pattern(entry.get_text())
- except errors.InvalidProblem as ex:
- self._remove_problem_from_storage(ex.problem_id)
- logging.debug(ex)
+ self._filter.set_pattern(entry.get_text())
def on_gac_opt_all_problems_activate(self, action):
conf = config.get_configuration()
--
1.8.2.1