From fe23113f46d8264d49acb1bd5214c03fb706189f Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 13 Nov 2009 20:11:34 +0000 Subject: Merge branch 'master' of git://git.sugarlabs.org/tutorius/mainline --- diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py index 6572a6a..1ed1fe0 100644 --- a/addons/bubblemessage.py +++ b/addons/bubblemessage.py @@ -48,7 +48,7 @@ class BubbleMessage(Action): self._bubble = None self._speaker = None - def do(self): + def do(self, **kwargs): """ Show the dialog """ diff --git a/addons/changecolor.py b/addons/changecolor.py new file mode 100644 index 0000000..460da32 --- /dev/null +++ b/addons/changecolor.py @@ -0,0 +1,127 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 time + +import gobject + +import gtk, gtk.gdk + +from sugar.tutorius.actions import Action +from sugar.tutorius.properties import TUAMProperty +from sugar.tutorius.gtkutils import find_widget + +from sugar import profile + +# for easy profile access +xo_line_color = profile.get_color().get_stroke_color() +xo_fill_color = profile.get_color().get_fill_color() + +class ChangeColor(Action): + """ + ChangeColorEvent + """ + # widget address property + widaddr = TUAMProperty("0") + + # set timeout + timeout = 500 + + def __init__(self, widaddr=None): + """Constructor - Change a widget color + @param widaddr: the widget for which you want to change the color (UAM) + """ + Action.__init__(self) + + if widaddr: self.widaddr = widaddr + + self.init_style = None + self._new_color = None + + self.wid = None + + self._handler_id = None + + def do(self, **kwargs): + """ + do. + Change the color of the widaddr widget with the chosen color + """ + + if not "activity" in kwargs: + raise TypeError("activity argument is Mandatory") + + # get widget instance + self.wid = find_widget(kwargs["activity"], self.widaddr, ignore_errors=False) + + if not self.wid: + raise NameError("widget not found") + + # we have to get the initial color in the sugar rc theme + self.init_style = self.wid.rc_get_style() + + # define new color + self._new_color = gtk.gdk.color_parse(xo_fill_color) + + # set change color timeout (flash) + self._handler_id = gobject.timeout_add(ChangeColor.timeout, self._timeout_cb) + + def undo(self): + """ + Remove timer and go back to the original color + """ + + if self._handler_id: + try: + #remove the timer + gobject.source_remove(self._handler_id) + except: + pass + + # modify bg color (go back to original color) + self.wid.modify_bg(gtk.STATE_NORMAL, self.init_style.bg[gtk.STATE_NORMAL]) + self.wid.modify_bg(gtk.STATE_PRELIGHT, self.init_style.bg[gtk.STATE_PRELIGHT]) + self.wid.modify_bg(gtk.STATE_ACTIVE, self.init_style.bg[gtk.STATE_ACTIVE]) + self.wid.modify_bg(gtk.STATE_INSENSITIVE, self.init_style.bg[gtk.STATE_INSENSITIVE]) + + def _timeout_cb(self): + """ + _timeout_cb triggers the eventfilter callback. + """ + + if self.wid.rc_get_style().bg[gtk.STATE_NORMAL] == self._new_color: + # modify bg color (go back to original color) + self.wid.modify_bg(gtk.STATE_NORMAL, self.init_style.bg[gtk.STATE_NORMAL]) + self.wid.modify_bg(gtk.STATE_PRELIGHT, self.init_style.bg[gtk.STATE_PRELIGHT]) + self.wid.modify_bg(gtk.STATE_ACTIVE, self.init_style.bg[gtk.STATE_ACTIVE]) + self.wid.modify_bg(gtk.STATE_INSENSITIVE, self.init_style.bg[gtk.STATE_INSENSITIVE]) + else: + # modify bg color (to new color) + self.wid.modify_bg(gtk.STATE_NORMAL, self._new_color) + self.wid.modify_bg(gtk.STATE_PRELIGHT, self._new_color) + self.wid.modify_bg(gtk.STATE_ACTIVE, self._new_color) + self.wid.modify_bg(gtk.STATE_INSENSITIVE, self._new_color) + + return True + +__action__ = { + "name" : "ChangeColor", + "display_name" : "Change widget color", + "icon" : "message-bubble", + "class" : ChangeColor, + "mandatory_props" : ["widaddr"] +} + diff --git a/addons/clickaction.py b/addons/clickaction.py index 88c5519..071af28 100644 --- a/addons/clickaction.py +++ b/addons/clickaction.py @@ -29,7 +29,7 @@ class ClickAction(Action): Action.__init__(self) self.widget = widget - def do(self): + def do(self, **kwargs): """ click the widget """ diff --git a/addons/dialogmessage.py b/addons/dialogmessage.py index 9250693..fad6d2c 100644 --- a/addons/dialogmessage.py +++ b/addons/dialogmessage.py @@ -36,7 +36,7 @@ class DialogMessage(Action): self.message = message if position: self.position = position - def do(self): + def do(self, **kwargs): """ Show the dialog """ diff --git a/addons/disablewidget.py b/addons/disablewidget.py index fd88303..b3d9ae6 100644 --- a/addons/disablewidget.py +++ b/addons/disablewidget.py @@ -30,7 +30,7 @@ class DisableWidgetAction(Action): self.target = target self._widget = None - def do(self): + def do(self, **kwargs): """Action do""" os = ObjectStore() if os.activity: diff --git a/addons/messagebuttonnext.py b/addons/messagebuttonnext.py new file mode 100644 index 0000000..74ce1bb --- /dev/null +++ b/addons/messagebuttonnext.py @@ -0,0 +1,171 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 gtk, gtk.gdk + +from sugar.tutorius.filters import EventFilter +from sugar.tutorius.properties import TStringProperty, TArrayProperty +from sugar.tutorius import overlayer + +from sugar import profile + +xo_line_color_profile = profile.get_color().get_stroke_color() +xo_fill_color_profile = profile.get_color().get_fill_color() + +class MessageButtonNext(EventFilter): + """ + MessageButtonNext + """ + # set message + message = TStringProperty("Message") + + # create the position as an array of fixed-size 2 + position = TArrayProperty((0,0), 2, 2) + + # set padding + padding = 40 + + def __init__(self, message=None, position=None, center_pos=False): + """Constructor. + + @param message message to display + @param position message position + """ + super(MessageButtonNext,self).__init__() + + if position: + self.position = position + else: + # TODO: to be removed when creator supports editing properties on events + self.position = (300, 200) + + if message: + self.message = message + + self.overlay = None + self.msgnext = None + + def install_handlers(self, callback, **kwargs): + """install_handlers creates the message button next and shows it""" + super(MessageButtonNext,self).install_handlers(callback, **kwargs) + + if not "activity" in kwargs: + raise TypeError("activity argument is Mandatory") + + # get activity instance + self.activity = kwargs["activity"] + + # get or inject overlayer + self.overlay = self.activity._overlayer + + if not self.overlay: + self.overlay = self.activity._overlayer + + btntext = "NEXT" + + self.msgnext = MsgNext(text=self.message,btntext=btntext) + self.msgnext._btnnext.connect("clicked", self.btnnext_clicked) + + # add space around minimum need size + wid_width, wid_height = self.msgnext.size_request() + self.msgnext.set_size_request(wid_width+MessageButtonNext.padding, wid_height+MessageButtonNext.padding) + + # set position + x, y = self.position + + self.msgnext.show() + self.overlay.put(self.msgnext, x, y) + self.overlay.queue_draw() + + def remove_handlers(self): + """remove handler removes the message button next""" + super(MessageButtonNext,self).remove_handlers() + + if self.msgnext: + self.msgnext.destroy() + self.msgnext = None + + def btnnext_clicked(self, widget): + self.do_callback() + +__event__ = { + "name" : "MessageButtonNext", + "display_name" : "Message button next", + "icon" : "message-bubble", + "class" : MessageButtonNext, + "mandatory_props" : ["message"] +} + +class MsgNext(gtk.EventBox): + """ + Create an EventBox + """ + def __init__(self, text, btntext): + """ + Creates an Event Box + """ + gtk.EventBox.__init__(self) + + self.message = text + self.btnmessage = btntext + + self.set_visible_window(True) + + # create a vbox + self.box = gtk.VBox() + + # create a label (set message to display) + self._label = gtk.Label() + self._text = "%s" % self.message + self._label.set_markup(self._text) + self._label.set_line_wrap(True) + + self._colortext = gtk.gdk.color_parse("white") + self._label.modify_fg(gtk.STATE_NORMAL, self._colortext) + self._label.modify_fg(gtk.STATE_PRELIGHT, self._colortext) + self._label.modify_fg(gtk.STATE_ACTIVE, self._colortext) + self._label.modify_fg(gtk.STATE_INSENSITIVE, self._colortext) + + self._label.show() + + # create a hbox (holding button) + self._hbox = gtk.HBox() + + # create a button inside hbox + self._btnnext = gtk.Button(self.btnmessage) + + self._colorbtn = gtk.gdk.color_parse(xo_fill_color_profile) + + self._btnnext.modify_bg(gtk.STATE_NORMAL, self._colorbtn) + self._btnnext.modify_bg(gtk.STATE_PRELIGHT, self._colorbtn) + self._btnnext.modify_bg(gtk.STATE_ACTIVE, self._colorbtn) + + self._btnnext.show() + + self._hbox.pack_end(self._btnnext, expand=False) + + self._hbox.show() + + self.box.pack_start(self._label, expand=True) + self.box.pack_start(self._hbox, expand=True) + + self.box.show() + + self.add(self.box) + + self._colormsgnext = gtk.gdk.color_parse(xo_fill_color_profile) + self.modify_bg(gtk.STATE_NORMAL, self._colormsgnext) + diff --git a/addons/oncewrapper.py b/addons/oncewrapper.py index 5db3b60..c404ae4 100644 --- a/addons/oncewrapper.py +++ b/addons/oncewrapper.py @@ -32,7 +32,7 @@ class OnceWrapper(Action): self._need_undo = False self.action = action - def do(self): + def do(self, **kwargs): """ Do the action only on the first time """ diff --git a/addons/readfile.py b/addons/readfile.py index 9fe2f81..4a6c54d 100644 --- a/addons/readfile.py +++ b/addons/readfile.py @@ -34,7 +34,7 @@ class ReadFile(Action): if filename: self.filename=filename - def do(self): + def do(self, **kwargs): """ Perform the action, call read_file on the activity """ diff --git a/addons/widgetidentifyaction.py b/addons/widgetidentifyaction.py index 3df244b..c44964b 100644 --- a/addons/widgetidentifyaction.py +++ b/addons/widgetidentifyaction.py @@ -24,7 +24,7 @@ class WidgetIdentifyAction(Action): self.activity = None self._dialog = None - def do(self): + def do(self, **kwargs): os = ObjectStore() if os.activity: self.activity = os.activity diff --git a/tests/ressources/icon.svg b/tests/ressources/icon.svg new file mode 100644 index 0000000..bb28f04 --- /dev/null +++ b/tests/ressources/icon.svg @@ -0,0 +1,21 @@ + + +]> + + + + + + + + image/svg+xml + + + + + + + + + \ No newline at end of file diff --git a/tests/storetests.py b/tests/storetests.py index 1752fe6..0c36973 100644 --- a/tests/storetests.py +++ b/tests/storetests.py @@ -15,6 +15,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import unittest +import uuid from tests.utils import skip, catch_unimplemented import random @@ -79,7 +80,6 @@ class StoreProxyLoginTest(unittest.TestCase): if session_id is not None: self.store.close_session() - @catch_unimplemented def test_get_session_id(self): @@ -99,13 +99,14 @@ class StoreProxyLoginTest(unittest.TestCase): 'name': 'newtut', 'summary': 'This is a tutorial', 'filename': 'test.xml', + 'guid': str(uuid.uuid1()), 'homepage': 'http://google.com', 'version': '1', 'cat1': '17', 'cat2': '18', 'cat3': '' } - assert self.store.publish('This should be a real tutorial...', tutorial_info) + assert self.store.publish('This should be a real tutorial...', tutorial_info) != -1 @catch_unimplemented def test_unpublish(self): diff --git a/tests/vaulttests.py b/tests/vaulttests.py index 6d69ca9..c6bd852 100644 --- a/tests/vaulttests.py +++ b/tests/vaulttests.py @@ -259,6 +259,52 @@ class VaultInterfaceTest(unittest.TestCase): # TODO : Compare the initial and reloaded metadata when vault.Query() will accept specifc argument # (so we can specifiy that we want only the metadata for this particular tutorial + + def test_add_delete_get_path_resource(self): + """ + This test verify that the vault interface function add_resource succesfully add resource in the vault + and return the new resource id. It also test the deletion of the resource. + """ + # Path of an image file in the test folder + test_path = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp') + if os.path.isdir(test_path) == True: + shutil.rmtree(os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp')) + os.makedirs(test_path) + file_path = os.path.join(test_path, 'test_resource.svg') + resource_file = open(file_path, 'wt') + resource_file.write('test') + resource_file.close() + #image_path = os.path.join(os.getcwd(), 'tests', 'resources', 'icon.svg') + #assert os.path.isfile(image_path), 'cannot find the test image file' + + # Create and save a tutorial + tutorial = Tutorial('test') + Vault.saveTutorial(tutorial, self.test_metadata_dict) + + bundler = TutorialBundler(self.save_test_guid) + tuto_path = bundler.get_tutorial_path(self.save_test_guid) + + # add the resource to the tutorial + resource_id = Vault.add_resource(self.save_test_guid, file_path) + + # Check that the image file is now in the vault + assert os.path.isfile(os.path.join(tuto_path, 'resources', resource_id)), 'image file not found in vault' + + # Check if get_resource_path Vault interface function is working + vault_path = Vault.get_resource_path(self.save_test_guid, resource_id) + + assert os.path.isfile(vault_path), 'path returned is not a file' + basename, extension = os.path.splitext(vault_path) + assert extension == '.svg', 'wrong file path have been returned' + + # Delete the resource + Vault.delete_resource(self.save_test_guid, resource_id) + + # Check that the resource is not in the vault anymore + assert os.path.isfile(os.path.join(tuto_path, 'resources', resource_id)) == False, 'image file found in vault when it should have been deleted.' + + + def tearDown(self): folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data'); for file in os.listdir(folder): @@ -409,7 +455,7 @@ class TutorialBundlerTests(unittest.TestCase): This module contains all the tests for the storage mecanisms for tutorials This mean testing saving and loading tutorial, .ini file management and - adding ressources to tutorial + adding resources to tutorial """ def setUp(self): @@ -450,4 +496,4 @@ class TutorialBundlerTests(unittest.TestCase): if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index f55547c..dbab86a 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -110,8 +110,8 @@ class TProbe(dbus.service.Object): if action._props: action._props.update(loaded_action._props) - action.do() - + action.do(activity=self._activity) + return address @dbus.service.method("org.tutorius.ProbeInterface", diff --git a/tutorius/apilib/__init__.pyc b/tutorius/apilib/__init__.pyc deleted file mode 100644 index bd4346b..0000000 --- a/tutorius/apilib/__init__.pyc +++ /dev/null Binary files differ diff --git a/tutorius/apilib/httplib2/__init__.pyc b/tutorius/apilib/httplib2/__init__.pyc deleted file mode 100644 index e5f8ebe..0000000 --- a/tutorius/apilib/httplib2/__init__.pyc +++ /dev/null Binary files differ diff --git a/tutorius/apilib/httplib2/iri2uri.pyc b/tutorius/apilib/httplib2/iri2uri.pyc deleted file mode 100644 index 879e719..0000000 --- a/tutorius/apilib/httplib2/iri2uri.pyc +++ /dev/null Binary files differ diff --git a/tutorius/apilib/mimeTypes.pyc b/tutorius/apilib/mimeTypes.pyc deleted file mode 100644 index 35ef2b2..0000000 --- a/tutorius/apilib/mimeTypes.pyc +++ /dev/null Binary files differ diff --git a/tutorius/apilib/restful_lib.pyc b/tutorius/apilib/restful_lib.pyc deleted file mode 100644 index 5b06765..0000000 --- a/tutorius/apilib/restful_lib.pyc +++ /dev/null Binary files differ diff --git a/tutorius/gtkutils.py b/tutorius/gtkutils.py index 1a9cb0f..c96a73f 100644 --- a/tutorius/gtkutils.py +++ b/tutorius/gtkutils.py @@ -33,7 +33,7 @@ def raddr_lookup(widget): return ".".join(name) -def find_widget(base, target_fqdn): +def find_widget(base, target_fqdn, ignore_errors=True): """Find a widget by digging into a parent widget's children tree @param base the parent widget @param target_fqdn fqdn-style target object name @@ -57,7 +57,9 @@ def find_widget(base, target_fqdn): try: obj = get_children(obj)[int(path.pop(0))] except: - break + if ignore_errors: + break + return None return obj diff --git a/tutorius/store.py b/tutorius/store.py index 81925ed..dc52c82 100644 --- a/tutorius/store.py +++ b/tutorius/store.py @@ -323,27 +323,31 @@ class StoreProxy(object): headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_post(request_url, None, None, None, headers) - if self.helper.iserror(response): - return False + return -1 - return True + return tutorial_store_id # Otherwise, we want to publish a new tutorial if tutorial_info == None: - return False + return -1 request_url = "/%s/publish/" % (self.tutorius_api) headers = { 'X-API-Auth' : self.api_auth_key } response = self.conn.request_post(request_url, tutorial_info, tutorial, tutorial_info['filename'], headers) - + if self.helper.iserror(response): - return False + return -1 + + xml_response = minidom.parseString(response['body']) + + id_node = xml_response.getElementsByTagName("id")[0] + + id = id_node.getAttribute('value') - return True - + return id def unpublish(self, tutorial_store_id): """ @@ -454,6 +458,7 @@ class StoreProxyHelper(object): tutorial = {} params = [ + 'id', 'name', 'summary', 'version', diff --git a/tutorius/vault.py b/tutorius/vault.py index d6b4720..7ec0a23 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -58,6 +58,7 @@ INI_XML_FSM_PROPERTY = "fsm_filename" INI_VERSION_PROPERTY = 'version' INI_FILENAME = "meta.ini" TUTORIAL_FILENAME = "tutorial.xml" +RESOURCES_FOLDER = 'resources' ###################################################################### # XML Tag names and attributes @@ -85,7 +86,7 @@ class Vault(object): given activity. @param activity_name the name of the activity associated with this tutorial. None means ALL activities - @param activity_vers the version number of the activity to find tutorail for. 0 means find for ANY version. If activity_name is None, version number is not used + @param activity_vers the version number of the activity to find tutorial for. 0 means find for ANY version. If activity_name is None, version number is not used @returns a map of tutorial {names : GUID}. """ # check both under the activity data and user installed folders @@ -249,6 +250,7 @@ class Vault(object): # Return tutorial list return tutorial_list + @staticmethod def loadTutorial(Guid): @@ -323,7 +325,7 @@ class Vault(object): @staticmethod - def deleteTutorial(Tutorial): + def deleteTutorial(Guid): """ Removes the tutorial from the Vault. It will unpublish the tutorial if need be, and it will also wipe it from the persistent storage. @@ -341,6 +343,75 @@ class Vault(object): return False + @staticmethod + def add_resource(tutorial_guid, file_path): + """ + Add given resource file in the vault and returns a unique name for this resource + composed from the original name of the file and a suffix to make it unique + ( ex: name_1.jpg ). + @param tutorial_guid The guid of the tutorial + @param file_path the file path of the resource to add + @returns the resource_id of the resource + """ + assert os.path.isfile(file_path) + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Get the file name + file_name = os.path.basename(file_path) + #fname_splitted = file_path.rsplit('/') + #file_name = fname_splitted[fname_splitted.__len__() - 1] + base_name, extension = os.path.splitext(file_name) + # Append unique name to file name + file_name_appended = base_name + '_' + str(uuid.uuid1()) + extension + # Check if the resource file already exists + new_file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, file_name_appended) + if os.path.isfile(new_file_path) == False: + # Copy the resource file in the vault + if os.path.isdir(os.path.join(tutorial_path, RESOURCES_FOLDER)) == False: + os.makedirs(os.path.join(tutorial_path, RESOURCES_FOLDER)) + shutil.copyfile(file_path, new_file_path) + + return file_name_appended + + + @staticmethod + def delete_resource(tutorial_guid, resource_id): + """ + Delete the resource from the resources of the tutorial. + @param tutorial_guid the guid of the tutorial + @param resource_id the resource id of the resource to delete + """ + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Check if the resource file exists + file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) + if os.path.isfile(file_path): + # Delete the resource + os.remove(file_path) + else: + logging.info('File not found, no delete took place') + + @staticmethod + def get_resource_path(tutorial_guid, resource_id): + """ + Returns the absolute file path to the resourceID + @param tutorial_guid the guid of the tutorial + @param resource_id the resource id of the resource to find the path for + @returns the absolute path of the resource file + """ + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Check if the resource file exists + file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) + if os.path.isfile(file_path): + return file_path + else: + return None + + class Serializer(object): """ Interface that provide serializing and deserializing of the FSM @@ -914,6 +985,6 @@ class TutorialBundler(object): @staticmethod def add_resources(typename, file): """ - Add ressources to metadata. + Add resources to metadata. """ raise NotImplementedError("add_resources not implemented") -- cgit v0.9.1