changeset 107:5ae370c71803

CS: message sending is now working - xmltools/xmlui: buttons can now send fields of the ui when used - xmltools/xmlui: new textbox element, to write a large text (used for messages in CS plugin) - xmltools/xmlui: list have now an attribute multi for selecting several options - xmltools/xmlui: window title can now be specified in the xml (attribute title) - CS_plugin: message sending interface & management
author Goffi <goffi@goffi.org>
date Mon, 28 Jun 2010 15:18:59 +0800
parents 138d82f64b6f
children e24e080e6b16
files frontends/wix/xmlui.py plugins/plugin_misc_cs.py sat.tac tools/xml_tools.py
diffstat 4 files changed, 136 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/wix/xmlui.py	Sat Jun 26 15:33:16 2010 +0800
+++ b/frontends/wix/xmlui.py	Mon Jun 28 15:18:59 2010 +0800
@@ -38,7 +38,7 @@
         self.host = host
         self.options = options
         self.misc = misc
-        self.ctl_list = []  # usefull to access ctrl
+        self.ctrl_list = {}  # usefull to access ctrl
 
         self.sizer = wx.BoxSizer(wx.VERTICAL)
         self.SetSizer(self.sizer)
@@ -78,20 +78,25 @@
                 ctrl = wx.StaticText(parent, -1, value+": ")
             elif type=="string":
                 ctrl = wx.TextCtrl(parent, -1, value)
-                self.ctl_list.append({'name':name, 'type':type, 'control':ctrl})
+                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
                 _proportion = 1
             elif type=="password":
                 ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_PASSWORD)
-                self.ctl_list.append({'name':name, 'type':type, 'control':ctrl})
+                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
+                _proportion = 1 
+            elif type=="textbox":
+                ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_MULTILINE)
+                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
                 _proportion = 1
             elif type=="list":
-                ctrl = wx.ListBox(parent, -1, choices=[option.getAttribute("value") for option in elem.getElementsByTagName("option")], style=wx.LB_SINGLE)
-                self.ctl_list.append({'name':name, 'type':type, 'control':ctrl})
+                style=wx.LB_MULTIPLE if elem.getAttribute("multi")=='yes' else wx.LB_SINGLE
+                ctrl = wx.ListBox(parent, -1, choices=[option.getAttribute("value") for option in elem.getElementsByTagName("option")], style=style)
+                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
                 _proportion = 1
             elif type=="button":
                 callback_id = elem.getAttribute("callback_id")
                 ctrl = wx.Button(parent, -1, value)
-                ctrl.param_id = callback_id
+                ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")])
                 parent.Bind(wx.EVT_BUTTON, self.onButtonClicked, ctrl)
             else:
                 error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type)  #FIXME !
@@ -155,6 +160,9 @@
         cat_dom = minidom.parseString(xml_data.encode('utf-8'))
         top= cat_dom.documentElement
         self.type = top.getAttribute("type")
+        self.title = top .getAttribute("title")
+        if self.title:
+            self.SetTitle(self.title)
         if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']:
             raise Exception("Invalid XMLUI") #TODO: make a custom exception
 
@@ -182,8 +190,15 @@
 
     def onButtonClicked(self, event):
         """Called when a button is pushed"""
-        callback_id = event.GetEventObject().param_id
+        callback_id, fields = event.GetEventObject().param_id
         data = {"callback_id":callback_id}
+        for field in fields:
+            ctrl = self.ctrl_list[field]
+            if isinstance(ctrl['control'], wx.ListBox):
+                data[field] = '\t'.join([ctrl['control'].GetString(idx) for idx in ctrl['control'].GetSelections()])
+            else:
+                data[field] = ctrl['control'].GetValue()
+
         id = self.host.bridge.launchAction("button", data)
         self.host.current_action_ids.add(id)
         event.Skip()
@@ -192,11 +207,12 @@
         """Called when submit button is clicked"""
         debug(_("Submitting form"))
         data = []
-        for ctrl in self.ctl_list:
+        for ctrl_name in self.ctrl_list:
+            ctrl = self.ctrl_list[ctrl_name]
             if isinstance(ctrl['control'], wx.ListBox):
-                data.append((ctrl['name'], ctrl['control'].GetStringSelection()))
+                data.append((ctrl_name, ctrl['control'].GetStringSelection()))
             else:
-                data.append((ctrl["name"], ctrl["control"].GetValue()))
+                data.append((ctrl_name, ctrl['control'].GetValue()))
         if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned
             id = self.misc['action_back']("SUBMIT",self.misc['target'], data)
             self.host.current_action_ids.add(id)
--- a/plugins/plugin_misc_cs.py	Sat Jun 26 15:33:16 2010 +0800
+++ b/plugins/plugin_misc_cs.py	Mon Jun 28 15:18:59 2010 +0800
@@ -74,9 +74,11 @@
         #menu
         host.importMenu(_("Plugin"), "CouchSurfing", self.menuSelected, help_string = _("Launch CoushSurfing mangement interface"))
         self.data=self.host.memory.getPrivate('plugin_cs_data') or {} #TODO: delete cookies/data after a while
+        self.host.registerGeneralCB("plugin_CS_sendMessage", self.sendMessage)
 
     def erroCB(self, e, id):
         """Called when something is going wrong when contacting CS website"""
+        #pdb.set_trace()
         message_data={"reason": "connection error", "message":_(u"Impossible to contact CS website, please check your login/password, connection or try again later")}
         self.host.bridge.actionResult("ERROR", id, message_data)
 
@@ -101,13 +103,13 @@
 
    
         #tmp
-        f = open('/home/goffi/tmp/CS_principale.html','r')
+        """f = open('/home/goffi/tmp/CS_principale.html','r')
         html = f.read()
-        self.__connectionCB(html, id, profile)
+        self.__connectionCB(html, id, profile)"""
 
-        """d = getPage('http://www.couchsurfing.org/login.html', method='POST', postdata=post_data, headers={'Content-Type':'application/x-www-form-urlencoded'} , agent=AGENT, cookies=self.data[profile]['cookies'])
+        d = getPage('http://www.couchsurfing.org/login.html', method='POST', postdata=post_data, headers={'Content-Type':'application/x-www-form-urlencoded'} , agent=AGENT, cookies=self.data[profile]['cookies'])
         d.addCallback(self.__connectionCB, id, profile)
-        d.addErrback(self.erroCB, id)"""
+        d.addErrback(self.erroCB, id)
 
 
     #self.host.bridge.actionResult("SUPPRESS", id, {})
@@ -159,10 +161,18 @@
         unread_CR_mess = data['unread_CR_messages']
         friends_list = data['friends'].keys()
         friends_list.sort()
-        interface = XMLUI('window','tabs')
+        interface = XMLUI('window','tabs', title='CouchSurfing management')
         interface.addCategory(_("Messages"), "vertical")
-        interface.addText(_("G'day %(name)s, you have %(nb_message)i unread message%(plural_mess)s and %(unread_CR_mess)s unread couch request message%(plural_CR)s") % {'name':user_name, 'nb_message':unread_mess, 'plural_mess':'s' if unread_mess>1 else '', 'unread_CR_mess': unread_CR_mess, 'plural_CR':'s' if unread_CR_mess>1 else ''})
-        interface.addList(friends_list, 'friends')
+        interface.addText(_("G'day %(name)s, you have %(nb_message)i unread message%(plural_mess)s and %(unread_CR_mess)s unread couch request message%(plural_CR)s\nIf you want to send a message, select the recipient(s) in the list below") % {'name':user_name, 'nb_message':unread_mess, 'plural_mess':'s' if unread_mess>1 else '', 'unread_CR_mess': unread_CR_mess, 'plural_CR':'s' if unread_CR_mess>1 else ''})
+        interface.addList(friends_list, 'friends', style=['multi'])
+        interface.changeLayout('pairs')
+        interface.addLabel(_("Subject"))
+        interface.addString('subject')
+        interface.changeLayout('vertical')
+        interface.addLabel(_("Message"))
+        interface.addText("(use %name% for contact name and %firstname% for guessed first name)")
+        interface.addTextBox('message')
+        interface.addButton('plugin_CS_sendMessage', 'sendMessage', _('send'), fields_back=['friends','subject','message'])
         interface.addCategory(_("Events"), "vertical")
         interface.addCategory(_("Couch search"), "vertical")
         return interface.toXml()
@@ -173,7 +183,7 @@
     def __friendsPageCB(self, html, id, profile):
         """Called when the friends list page has been received"""
         self.savePage('friends',html)
-        soup = BeautifulSoup(html.replace('"formtable width="400','"formtable" width="400"'))
+        soup = BeautifulSoup(html.replace('"formtable width="400','"formtable" width="400"')) #CS html fix #TODO: report the bug to CS dev team
         friends = self.data[profile]['friends']
         for _tr in soup.findAll('tr', {'class':re.compile("^msgRow*")}): #we parse the row with friends infos
             _nobr = _tr.find('nobr')  #contain the friend name
@@ -196,8 +206,61 @@
             self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":self.__buildUI(self.data[profile])})
             #and save the data
             self.host.memory.setPrivate('plugin_cs_data', self.data)
-            
+
+    def __sendMessage(self, answer, subject, message, data, recipient_list, id, profile):
+        """Send actually the message
+        @param subject: subject of the message
+        @param message: body of the message
+        @param data: data of the profile
+        @param recipient_list: list of friends names, names are removed once message is sent
+        @param id: id of the action
+        @param profile: profile who launched the action
+        """
+        if answer:
+            if not 'Here is a copy of the email that was sent' in answer:
+                error(_("INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has been modified ?"))
+                #TODO: throw a warning to the frontend, saying that maybe the message has not been sent and to contact dev of this plugin
+            #debug(_('HTML answer: %s') % answer)
+        if recipient_list:
+            recipient = recipient_list.pop()
+            try:
+                friend_id = data['friends'][recipient]['id']
+            except KeyError:
+                error('INTERNAL ERROR: unknown friend')
+                return  #send an error to the frontend
+            mess = message.replace('%name%',recipient).replace('%firstname%',recipient.split(' ')[0])
+            info(_('Sending message to %s') % recipient)
+            debug(_("\nsubject: %(subject)s\nmessage: \n---\n%(message)s\n---\n\n") % {'subject':subject,'message':mess})
+            post_data = urllib.urlencode({'email[subject]':subject.encode('utf-8'),'email[body]':mess.encode('utf-8'),'email[id]':friend_id,'email[action]':'Send Message','email[replied_id]':'','email[couchsurf]':'','email[directions_to_add]':''})
+            d = getPage("http://www.couchsurfing.org/send_message.html", method='POST', postdata=post_data, headers={'Content-Type':'application/x-www-form-urlencoded'} , agent=AGENT, cookies=data['cookies'])
+            d.addCallback(self.__sendMessage, subject, message, data, recipient_list, id, profile)
+            d.addErrback(self.erroCB, id)
+        else:
+            interface = XMLUI('window', title=_('Message sent')) #TODO: create particular actionResult for alerts ?
+            interface.addText(_('The message has been sent to every recipients'))
+            self.host.bridge.actionResult("XMLUI", id, {"type":"window", "xml":interface.toXml()})
 
 
 
 
+
+    def sendMessage(self, id, data, profile):
+        """Called to send a message to a friend
+        @param data: dict with the following keys:
+            friend: name of the recipient
+            subject: subject of the message
+            message: body of the message, with the following special keywords:
+                - %name%: name of the friend
+                - %firstname%: guessed first name of the friend (currently the first part of the name)
+        """
+        if not data['friends']:
+            message_data={"reason": "bad data", "message":_(u"There is not recipient selected for this message !")}
+            self.host.bridge.actionResult("ERROR", id, message_data)
+            return
+        friends = data['friends'].split('\t')
+        subject = data['subject']
+        message = data['message']
+        print "send message \o/ :) :) :)"
+        info(_("sending message to %(friends)s with subject [%(subject)s]" % {'friends':friends, 'subject':subject}))
+        self.__sendMessage(None, subject, message, self.data[profile], friends, id, profile)
+       
--- a/sat.tac	Sat Jun 26 15:33:16 2010 +0800
+++ b/sat.tac	Mon Jun 28 15:18:59 2010 +0800
@@ -476,7 +476,7 @@
         
         return next_id 
 
-    def registerNewAccountCB(self, id, data):
+    def registerNewAccountCB(self, id, data, profile):
         #FIXME: gof: profile not managed here !
         user = jid.parse(self.memory.getParamA("JabberID", "Connection"))[0]
         password = self.memory.getParamA("Password", "Connection")
@@ -558,13 +558,17 @@
             return False
         return self.profiles[profile].isConnected()
 
-    def launchAction(self, type, data):
+    def launchAction(self, type, data, profile_key='@DEFAULT@'):
         """Launch a specific action asked by client
         @param type: action type (button)
         @param data: needed data to launch the action
 
         @return: action id for result, or empty string in case or error
         """
+        profile = self.memory.getProfileName(profile_key)
+        if not profile:
+            error (_('trying to launch action with a non-existant profile'))
+            raise Exception  #TODO: raise a proper exception
         if type=="button":
             try:
                 cb_name = data['callback_id']
@@ -572,7 +576,7 @@
                 error (_("Incomplete data"))
                 return ""
             id = sat_next_id()
-            self.callGeneralCB(cb_name, id, data)
+            self.callGeneralCB(cb_name, id, data, profile = profile)
             return id
         else:
             error (_("Unknown action type"))
@@ -741,7 +745,7 @@
         try:
             return self.__general_cb_map[name](*args, **kwargs)
         except KeyError:
-            error(_("Trying to call unknown function"))
+            error(_("Trying to call unknown function (%s)") % name)
             return None
 
     #Menus management
--- a/tools/xml_tools.py	Sat Jun 26 15:33:16 2010 +0800
+++ b/tools/xml_tools.py	Mon Jun 28 15:18:59 2010 +0800
@@ -103,17 +103,19 @@
 class XMLUI:
     """This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
 
-    def __init__(self, panel_type, layout="vertical"):
+    def __init__(self, panel_type, layout="vertical", title=None):
         """Init SàT XML Panel
         @param panel_type: one of
             - window (new window)
             - form (formulaire, depend of the frontend, usually a panel with cancel/submit buttons)
             - param (parameters, presentatio depend of the frontend)
         @param layout: disposition of elements, one of:
-            - VERTICAL: elements are disposed up to bottom
-            - HORIZONTAL: elements are disposed left to right
-            - PAIRS: elements come on two aligned columns
+            - vertical: elements are disposed up to bottom
+            - horizontal: elements are disposed left to right
+            - pairs: elements come on two aligned columns
               (usually one for a label, the next for the element)
+            - tabs: elemens are in categories with tabs (notebook)
+        @param title: title or default if None
         """
         if not panel_type in ['window', 'form', 'param']:
             error(_("Unknown panel type [%s]") % panel_type)
@@ -124,6 +126,8 @@
         self.doc = impl.createDocument(None, "sat_xmlui", None)
         top_element = self.doc.documentElement
         top_element.setAttribute("type", panel_type)
+        if title:
+            top_element.setAttribute("title", title)
         self.parentTabsLayout = None #used only we have 'tabs' layout
         self.currentCategory = None #used only we have 'tabs' layout
         self.changeLayout(layout)
@@ -195,23 +199,38 @@
         if value:
             elem.setAttribute('value', value)
 
-    def addList(self, options, name=None, value=None):
+    def addTextBox(self, name=None, value=None):
+        """Add a string box"""
+        elem = self.__createElem('textbox', name, self.currentLayout)
+        if value:
+            elem.setAttribute('value', value)
+    
+    def addList(self, options, name=None, value=None, style=set()):
         """Add a list of choices"""
+        styles = set(style)
         assert (options)
+        assert (styles.issubset(['multi'])) 
         elem = self.__createElem('list', name, self.currentLayout)
         self.addOptions(options, elem) 
         if value:
             elem.setAttribute('value', value)
+        for style in styles:
+            elem.setAttribute(style, 'yes')
 
-    def addButton(self, callback_id, name, value):
+    def addButton(self, callback_id, name, value, fields_back=[]):
         """Add a button
         @param callback: callback which will be called if button is pressed
         @param name: name
-        @param value: label of the button"""
+        @param value: label of the button
+        @fields_back: list of names of field to give back when pushing the button"""
         elem = self.__createElem('button', name, self.currentLayout)
         elem.setAttribute('callback_id', callback_id)
-        if value:
-            elem.setAttribute('value', value)
+        elem.setAttribute('value', value)
+        for field in fields_back:
+            fback_el = self.doc.createElement('field_back')
+            fback_el.setAttribute('name', field)
+            elem.appendChild(fback_el)
+
 
     
     def addElement(self, type, name = None, content = None, value = None, options = None, callback_id = None):
@@ -227,6 +246,8 @@
             self.addString(name, value)
         elif type == 'password':
             self.addPassword(name, value)
+        elif type == 'textbox':
+            self.addTextBox(name, value)
         elif type == 'list':
             self.addList(options, name, value)
         elif type == 'button':