[GNOME-ABRT PATCH] Display two sets of problems (My/System)

Jakub Filak jfilak at redhat.com
Fri Jun 28 16:49:38 UTC 2013


Closes rhbz#973125

Signed-off-by: Jakub Filak <jfilak at 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



More information about the Crash-catcher mailing list