[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