[Dextrose] [PATCH sugar] Initial feedback implementation

Aleksey Lim alsroot at activitycentral.org
Fri Feb 11 21:36:56 EST 2011


---
 bin/sugar-session                      |    9 ++-
 data/sugar.schemas.in                  |   55 ++++++++
 extensions/deviceicon/Makefile.am      |    4 +-
 extensions/deviceicon/feedback.py      |  179 ++++++++++++++++++++++++++
 src/jarabe/model/Makefile.am           |    1 +
 src/jarabe/model/feedback_collector.py |  217 ++++++++++++++++++++++++++++++++
 src/jarabe/model/shell.py              |   16 +++
 src/jarabe/view/service.py             |    5 +
 8 files changed, 484 insertions(+), 2 deletions(-)
 create mode 100644 extensions/deviceicon/feedback.py
 create mode 100644 src/jarabe/model/feedback_collector.py

diff --git a/bin/sugar-session b/bin/sugar-session
index 0501311..fd63386 100755
--- a/bin/sugar-session
+++ b/bin/sugar-session
@@ -229,7 +229,7 @@ def main():
     gettext.textdomain('sugar')
 
     from jarabe.desktop import homewindow
-    from jarabe.model import sound
+    from jarabe.model import sound, feedback_collector
     from jarabe import intro
 
     logger.start('shell')
@@ -238,6 +238,13 @@ def main():
     client.set_string('/apps/metacity/general/mouse_button_modifier',
                       '<Super>')
 
+    if client.get_bool('/desktop/sugar/feedback/personalized_submit') or \
+            client.get_int('/desktop/sugar/feedback/anonymous_delay'):
+        feedback_collector.start(
+                client.get_string('/desktop/sugar/feedback/server_host'),
+                client.get_int('/desktop/sugar/feedback/server_port'),
+                client.get_int('/desktop/sugar/feedback/anonymous_delay'))
+
     timezone = client.get_string('/desktop/sugar/date/timezone')
     if timezone is not None and timezone:
         os.environ['TZ'] = timezone
diff --git a/data/sugar.schemas.in b/data/sugar.schemas.in
index 695ad59..841bc7f 100644
--- a/data/sugar.schemas.in
+++ b/data/sugar.schemas.in
@@ -2,6 +2,61 @@
 <gconfschemafile>
   <schemalist>
     <schema>
+      <key>/schemas/desktop/sugar/feedback/personalized_submit</key>
+      <applyto>/desktop/sugar/feedback/personalized_submit</applyto>
+      <owner>sugar</owner>
+      <type>bool</type>
+      <default>true</default>
+      <locale name="C">
+        <short>Enable personalized submit</short>
+        <long>Show device icon to let poeple submit text messages with all collected data including detialed information about sumbitter.</long>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/sugar/feedback/anonymous_delay</key>
+      <applyto>/desktop/sugar/feedback/anonymous_delay</applyto>
+      <owner>sugar</owner>
+      <type>int</type>
+      <default>0</default>
+      <locale name="C">
+        <short>Delay in seconds to send anonymous reports automatically</short>
+        <long>Submit will not contain any information about submiter, only anonymous data (but see anonymous_with_sn). Setting value to 0 will disable automatic submiting.</long>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/sugar/feedback/anonymous_with_sn</key>
+      <applyto>/desktop/sugar/feedback/anonymous_with_sn</applyto>
+      <owner>sugar</owner>
+      <type>bool</type>
+      <default>false</default>
+      <locale name="C">
+        <short>Add XO serial numbers to anonymous submits</short>
+        <long>Actually, setting this value to true will make anonymous sebmits not anonymous for XO laptops. Might be useful for Sugar deployments.</long>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/sugar/feedback/server_host</key>
+      <applyto>/desktop/sugar/feedback/server_host</applyto>
+      <owner>sugar</owner>
+      <type>string</type>
+      <default>feedback.sugarlabs.org</default>
+      <locale name="C">
+        <short>Server host to send reports to</short>
+        <long>Server that will handle reports sent via HTTPS POST requests.</long>
+      </locale>
+    </schema>
+    <schema>
+      <key>/schemas/desktop/sugar/feedback/server_port</key>
+      <applyto>/desktop/sugar/feedback/server_port</applyto>
+      <owner>sugar</owner>
+      <type>int</type>
+      <default>8080</default>
+      <locale name="C">
+        <short>Server port to send reports to</short>
+        <long>TCP port that will used to send HTTPS POST requests.</long>
+      </locale>
+    </schema>
+    <schema>
       <key>/schemas/desktop/sugar/user/nick</key>
       <applyto>/desktop/sugar/user/nick</applyto>
       <owner>sugar</owner>
diff --git a/extensions/deviceicon/Makefile.am b/extensions/deviceicon/Makefile.am
index 3a74053..b38cbb3 100644
--- a/extensions/deviceicon/Makefile.am
+++ b/extensions/deviceicon/Makefile.am
@@ -8,4 +8,6 @@ sugar_PYTHON = 		\
 	speaker.py	\
 	touchpad.py	\
 	virtualkeyboard.py	\
-	volume.py
+	volume.py	\
+	feedback.py
+	
diff --git a/extensions/deviceicon/feedback.py b/extensions/deviceicon/feedback.py
new file mode 100644
index 0000000..3cc6296
--- /dev/null
+++ b/extensions/deviceicon/feedback.py
@@ -0,0 +1,179 @@
+# Copyright (C) Mukesh Gupta <mukeshgupta.2006 at gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+import logging
+from gettext import gettext as _
+
+import gconf
+import gtk
+
+from sugar import profile
+from sugar.graphics import style
+from sugar.graphics.icon import Icon
+from sugar.graphics.tray import TrayIcon
+from sugar.graphics.palette import Palette
+from sugar.graphics.menuitem import MenuItem
+from sugar.graphics.toolbutton import ToolButton
+
+from jarabe.model import feedback_collector
+
+
+_ICON_NAME = 'feedback-icon'
+
+
+class DeviceView(TrayIcon):
+
+    FRAME_POSITION_RELATIVE = 500
+
+    def __init__(self):
+        TrayIcon.__init__(self, icon_name=_ICON_NAME,
+                xo_color=profile.get_color())
+        self.create_palette()
+
+    def create_palette(self):
+        logging.debug('palette created')
+        self.palette = _Palette(_('Feedback'))
+        self.palette.set_group_id('frame')
+        return self.palette
+
+
+class _Palette(Palette):
+
+    def __init__(self, primary_text):
+        Palette.__init__(self, primary_text)
+
+        icon = Icon()
+        icon.set_from_icon_name('emblem-favorite', gtk.ICON_SIZE_MENU)
+        icon.props.xo_color = profile.get_color()
+
+        personalized = MenuItem(_('Personalized submit...'))
+        personalized.set_image(icon)
+        personalized.connect('activate', self.__personalized_activate_cb)
+        personalized.show()
+        self.menu.append(personalized)
+
+        self._anonymous = MenuItem(_('Anonymous submit'), 'emblem-favorite')
+        self._anonymous.connect('activate', self.__anonymous_activate_cb)
+        self._anonymous.show()
+        self.menu.append(self._anonymous)
+
+    def popup(self, immediate=False, state=None):
+        self._anonymous.set_sensitive(not feedback_collector.is_empty())
+        Palette.popup(self, immediate=immediate, state=state)
+
+    def __anonymous_activate_cb(self, button):
+        feedback_collector.anonymous_submit()
+
+    def __personalized_activate_cb(self, button):
+        window = _Window()
+        window.show()
+
+
+class _Window(gtk.Window):
+
+    __gtype_name__ = 'FeedbackWindow'
+
+    def __init__(self):
+        gtk.Window.__init__(self)
+
+        self.set_border_width(style.LINE_WIDTH)
+        offset = style.GRID_CELL_SIZE
+        width = gtk.gdk.screen_width() - offset * 2
+        height = gtk.gdk.screen_height() - offset * 2
+        self.set_size_request(width, height)
+        self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+        self.set_decorated(False)
+        self.set_resizable(False)
+        self.set_modal(True)
+
+        canvas = gtk.VBox()
+        self.add(canvas)
+
+        self._toolbar = gtk.Toolbar()
+        canvas.pack_start(self._toolbar, False)
+
+        icon = Icon()
+        icon.set_from_icon_name('emblem-favorite', gtk.ICON_SIZE_LARGE_TOOLBAR)
+        icon.props.xo_color = profile.get_color()
+        self._add_widget(icon)
+
+        self._add_separator(False)
+
+        title = gtk.Label(_('Submit feedback with contact infromation'))
+        self._add_widget(title)
+
+        self._add_separator(True)
+
+        submit = ToolButton('dialog-ok', tooltip=_('Submit'))
+        submit.connect('clicked', lambda button: self._submit())
+        self._toolbar.insert(submit, -1)
+
+        cancel = ToolButton('dialog-cancel', tooltip=_('Cancel'))
+        cancel.connect('clicked', lambda button: self.destroy())
+        self._toolbar.insert(cancel, -1)
+
+        bg = gtk.EventBox()
+        bg.modify_bg(gtk.STATE_NORMAL, style.COLOR_WHITE.get_gdk_color())
+        canvas.pack_start(bg)
+
+        self._message = gtk.TextView()
+        scrolled = gtk.ScrolledWindow()
+        scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        scrolled.set_border_width(style.DEFAULT_PADDING)
+        scrolled.add(self._message)
+        bg.add(scrolled)
+
+        self.show_all()
+        self.set_focus(self._message)
+
+        self.connect("realize", self.__realize_cb)
+
+    def do_key_press_event(self, event):
+        if event.keyval == gtk.keysyms.Escape:
+            self.destroy()
+        elif event.keyval == gtk.keysyms.Return and \
+                event.state & gtk.gdk.CONTROL_MASK:
+            self._submit()
+        else:
+            gtk.Window.do_key_press_event(self, event)
+
+    def _add_widget(self, widget):
+        tool_item = gtk.ToolItem()
+        tool_item.add(widget)
+        self._toolbar.insert(tool_item, -1)
+
+    def _add_separator(self, expand):
+        separator = gtk.SeparatorToolItem()
+        separator.props.draw = False
+        if expand:
+            separator.set_expand(True)
+        else:
+            separator.set_size_request(style.DEFAULT_SPACING, -1)
+        self._toolbar.insert(separator, -1)
+
+    def _submit(self):
+        feedback_collector.submit(self._message.props.buffer.props.text)
+        self.destroy()
+
+    def __realize_cb(self, widget):
+        self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+        self.window.set_accept_focus(True)
+
+
+def setup(tray):
+    client = gconf.client_get_default()
+    if client.get_bool('/desktop/sugar/feedback/personalized_submit'):
+        tray.add_device(DeviceView())
diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am
index 374d6fc..c221e86 100644
--- a/src/jarabe/model/Makefile.am
+++ b/src/jarabe/model/Makefile.am
@@ -19,4 +19,5 @@ sugar_PYTHON =			\
         session.py		\
 	sound.py		\
 	processmanagement.py	\
+	feedback_collector.py	\
 	virtualkeyboard.py
diff --git a/src/jarabe/model/feedback_collector.py b/src/jarabe/model/feedback_collector.py
new file mode 100644
index 0000000..3f3b9a8
--- /dev/null
+++ b/src/jarabe/model/feedback_collector.py
@@ -0,0 +1,217 @@
+# Copyright (C) 2011, Aleksey Lim
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import time
+import httplib
+import logging
+import tarfile
+import threading
+from cStringIO import StringIO
+from os.path import join, exists, basename
+from email.mime.multipart import MIMEMultipart
+from email.mime.application import MIMEApplication
+from email.generator import Generator
+from email.encoders import encode_noop
+from gettext import gettext as _
+
+import gconf
+import gobject
+import simplejson
+
+from sugar import logger, feedback, util
+from jarabe import frame
+
+
+_reports = {}
+_logs = set()
+_host = None
+_port = None
+
+
+def start(host, port, auto_submit_delay):
+    global _host
+    global _port
+
+    _host = host
+    _port = port
+
+    if auto_submit_delay > 0:
+        logging.debug('Feedback auto submit with %ss delay', auto_submit_delay)
+        gobject.timeout_add_seconds(auto_submit_delay, _auto_submit_cb)
+
+
+def update(bundle_id, report, log_file):
+    if bundle_id not in _reports:
+        _reports[bundle_id] = {}
+    stat = _reports[bundle_id]
+
+    for key, count in report.items():
+        if key not in stat:
+            stat[key] = 0
+        stat[key] += count
+
+    if log_file:
+        _logs.add(log_file)
+
+
+def is_empty():
+    report, shell_log = feedback.flush()
+    if report:
+        if shell_log:
+            shell_log = join(logger.get_logs_dir(), 'shell.log')
+        update('shell', report, shell_log)
+
+    return not _reports
+
+
+def submit(message):
+    from jarabe.journal import misc
+
+    client = gconf.client_get_default()
+    jabber = client.get_string('/desktop/sugar/collaboration/jabber_server')
+    nick = client.get_string("/desktop/sugar/user/nick")
+
+    data = {'message': message,
+            'serial_number': misc.get_xo_serial(),
+            'nick': nick,
+            'jabber_server': jabber,
+            }
+    _reports.update(data)
+    _submit(False)
+
+
+def anonymous_submit(implicit=False):
+    if is_empty():
+        return
+
+    from jarabe.journal import misc
+
+    client = gconf.client_get_default()
+    if client.get_bool('/desktop/sugar/feedback/anonymous_with_sn'):
+        _reports['serial_number'] = misc.get_xo_serial()
+    _submit(implicit)
+
+
+def _auto_submit_cb():
+    anonymous_submit()
+    return True
+
+
+def _submit(implicit):
+    logging.debug('Sending feedback report: %r', _reports)
+
+    report = simplejson.dumps(_reports)
+    _reports.clear()
+
+    tar_file = util.TempFilePath()
+    tar = tarfile.open(tar_file, 'w:gz')
+
+    while _logs:
+        log_file = _logs.pop()
+        if exists(log_file):
+            tar.add(log_file, arcname=basename(log_file))
+
+    report_file = tarfile.TarInfo('report')
+    report_file.mode = 0644
+    report_file.mtime = int(time.time())
+    report_file.size = len(report)
+    tar.addfile(report_file, StringIO(report))
+
+    tar.close()
+
+    _SubmitThread(tar_file, implicit).run()
+
+
+class _SubmitThread(threading.Thread):
+
+    def __init__(self, tar_file, implicit):
+        threading.Thread.__init__(self)
+        self._tar_file = tar_file
+        self._implicit = implicit
+
+    def run(self):
+        try:
+            message = _FormData()
+            attachment = MIMEApplication(file(self._tar_file).read(),
+                    _encoder=encode_noop)
+            message.attach_file(attachment,
+                    name='report', filename='report.tar.gz')
+            body, headers = message.get_request_data()
+
+            conn = httplib.HTTPSConnection(_host, _port)
+            conn.request('POST', '/', body, headers)
+            response = conn.getresponse()
+
+            if response.status != 200:
+                raise Exception('Incorrect feedback submit: %s, %s',
+                        response.status, response.read())
+
+        except Exception:
+            title = _('Cannot submit feedback')
+            msg = _('Feedback was not sent to %s:%s.') % (_host, _port)
+            if not self._implicit:
+                gobject.idle_add(lambda:
+                        frame.get_view().add_message(summary=title, body=msg))
+            logging.exception('%s: %s', title, msg)
+        finally:
+            os.unlink(self._tar_file)
+            self._tar_file = None
+
+
+class _FormData(MIMEMultipart):
+    '''A simple RFC2388 multipart/form-data implementation.
+
+    A snippet from http://bugs.python.org/issue3244
+
+    '''
+
+    def __init__(self, boundary=None, _subparts=None, **kwargs):
+        MIMEMultipart.__init__(self, _subtype='form-data',
+                boundary=boundary, _subparts=_subparts, **kwargs)
+
+    def attach(self, subpart):
+        if 'MIME-Version' in subpart:
+            if subpart['MIME-Version'] != self['MIME-Version']:
+                raise ValueError('subpart has incompatible MIME-Version')
+            # Note: This isn't strictly necessary, but there is no point in
+            # including a MIME-Version header in each subpart.
+            del subpart['MIME-Version']
+        MIMEMultipart.attach(self, subpart)
+
+    def attach_file(self, subpart, name, filename):
+        '''
+        Attach a subpart, setting it's Content-Disposition header to "file".
+        '''
+        name = name.replace('"', '\\"')
+        filename = filename.replace('"', '\\"')
+        subpart['Content-Disposition'] = \
+                'form-data; name="%s"; filename="%s"' % (name, filename)
+        self.attach(subpart)
+
+    def get_request_data(self, trailing_newline=True):
+        '''Return the encoded message body.'''
+        f = StringIO()
+        generator = Generator(f, mangle_from_=False)
+        # pylint: disable-msg=W0212
+        generator._dispatch(self)
+        # HTTP needs a trailing newline.  Since our return value is likely to
+        # be passed directly to an HTTP connection, we might as well add it
+        # here.
+        if trailing_newline:
+            f.write('\n')
+        body = f.getvalue()
+        headers = dict(self)
+        return body, headers
diff --git a/src/jarabe/model/shell.py b/src/jarabe/model/shell.py
index 366f06a..f99af2d 100644
--- a/src/jarabe/model/shell.py
+++ b/src/jarabe/model/shell.py
@@ -23,12 +23,14 @@ import wnck
 import gobject
 import gtk
 import dbus
+import simplejson
 
 from sugar import wm
 from sugar import dispatch
 from sugar.graphics.xocolor import XoColor
 from sugar.presence import presenceservice
 
+from jarabe.model import feedback_collector
 from jarabe.model.bundleregistry import get_registry
 
 _SERVICE_NAME = "org.laptop.Activity"
@@ -220,6 +222,12 @@ class Activity(gobject.GObject):
         else:
             return self._activity_info.get_path()
 
+    def get_bundle_id(self):
+        if self._activity_info is None:
+            return None
+        else:
+            return self._activity_info.get_bundle_id()
+
     def get_activity_name(self):
         """Returns the activity's bundle name"""
         if self._activity_info is None:
@@ -617,6 +625,14 @@ class ShellModel(gobject.GObject):
             logging.error('Model for activity id %s does not exist.',
                 activity_id)
 
+    def notify_feedback(self, activity_id, report, log_file):
+        home_activity = self.get_activity_by_id(activity_id)
+        if home_activity is not None:
+            feedback_collector.update(home_activity.get_bundle_id(),
+                    simplejson.loads(report), log_file)
+        else:
+            logging.error('No %s activity for sending feedback', activity_id)
+
     def _check_activity_launched(self, activity_id):
         home_activity = self.get_activity_by_id(activity_id)
 
diff --git a/src/jarabe/view/service.py b/src/jarabe/view/service.py
index bb71694..a576eb9 100644
--- a/src/jarabe/view/service.py
+++ b/src/jarabe/view/service.py
@@ -97,6 +97,11 @@ class UIService(dbus.service.Object):
     def NotifyLaunchFailure(self, activity_id):
         shell.get_model().notify_launch_failed(activity_id)
 
+    @dbus.service.method(_DBUS_SHELL_IFACE,
+                         in_signature="sss", out_signature="")
+    def Feedback(self, activity_id, report, log_file):
+        shell.get_model().notify_feedback(activity_id, report, log_file)
+
     @dbus.service.signal(_DBUS_OWNER_IFACE, signature="s")
     def ColorChanged(self, color):
         pass
-- 
1.7.3.4



More information about the Dextrose mailing list