PyXR

c:\projects\bitpim\src \ importexport.py



0001 ### BITPIM
0002 ###
0003 ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
0004 ###
0005 ### This program is free software; you can redistribute it and/or modify
0006 ### it under the terms of the BitPim license as detailed in the LICENSE file.
0007 ###
0008 ### $Id: importexport.py 4377 2007-08-27 04:58:33Z djpham $
0009 
0010 "Deals with importing and exporting stuff"
0011 
0012 # System modules
0013 from __future__ import with_statement
0014 import contextlib
0015 import string
0016 import re
0017 import StringIO
0018 import os
0019 
0020 # wxPython modules
0021 import wx
0022 import wx.grid
0023 import wx.html
0024 
0025 # Others
0026 from thirdparty import DSV
0027 
0028 # My modules
0029 import common
0030 import guihelper
0031 import vcard
0032 import phonenumber
0033 import guiwidgets
0034 import nameparser
0035 import phonebook
0036 import pubsub
0037 import guihelper
0038 import csv_calendar
0039 import vcal_calendar
0040 import ical_calendar
0041 import gcal_calendar as gcal
0042 import playlist
0043 import wpl_file
0044 
0045 # control
0046 def GetPhonebookImports():
0047     res=[]
0048     # Calendar Wizard
0049     res.append( (guihelper.ID_CALENDAR_WIZARD, 'Import Calendar Wizard...',
0050                  'Import Calendar Wizard', OnCalendarWizard) )
0051     res.append( (wx.NewId(), 'Calendar Import Preset...',
0052                 'Calendar Import Preset...', OnCalendarPreset) )
0053     res.append( (wx.NewId(), 'Auto Calendar Import',
0054                  'Auto Calendar Import',
0055                  ( (guihelper.ID_AUTOSYNCSETTINGS, 'Settings',
0056                     'Configure Auto Calendar Import', None),
0057                    (guihelper.ID_AUTOSYNCEXECUTE, 'Execute',
0058                     'Perform Auto Calendar Import', None))
0059                  ))
0060     # CSV - always possible
0061     res.append( (guihelper.ID_IMPORT_CSV_CONTACTS,"CSV Contacts...", "Import a CSV file for the phonebook", OnFileImportCSVContacts) )
0062     res.append( (guihelper.ID_IMPORT_CSV_CALENDAR,"CSV Calendar...", "Import a CSV file for the calendar", OnFileImportCSVCalendar) )
0063     # Vcards - always possible
0064     res.append( (guihelper.ID_IMPORT_VCARDS,"vCards...", "Import vCards for the phonebook", OnFileImportVCards) )
0065     # Vcal - always possible
0066     res.append((guihelper.ID_IMPORT_VCALENDAR,'vCalendar...', 'Import vCalendar data for the calendar', OnFileImportVCal))
0067     # iCal - always possible
0068     res.append((guihelper.ID_IMPORT_ICALENDAR, 'iCalendar...',
0069                 'Import iCalendar data for the calendar',
0070                 OnFileImportiCal))
0071     # Google Calendar - always possible
0072     res.append((guihelper.ID_IMPORT_GCALENDAR, 'Google Calendar...',
0073                 'Import Google Calendar data for the calendar',
0074                 OnFileImportgCal))
0075     # Outlook
0076     try:
0077         import native.outlook
0078         res.append( (guihelper.ID_IMPORT_OUTLOOK_CONTACTS,"Outlook Contacts...", "Import Outlook contacts for the phonebook", OnFileImportOutlookContacts) )
0079         res.append( (guihelper.ID_IMPORT_OUTLOOK_CALENDAR,"Outlook Calendar...", "Import Outlook calendar for the calendar", OnFileImportOutlookCalendar) )
0080         res.append( (guihelper.ID_IMPORT_OUTLOOK_NOTES,"Outlook Notes...", "Import Outlook notes for the memo", OnFileImportOutlookNotes) )
0081         res.append( (guihelper.ID_IMPORT_OUTLOOK_TASKS,"Outlook Tasks...", "Import Outlook tasks for the todo", OnFileImportOutlookTasks) )
0082     except:
0083         pass
0084     # Evolution
0085     try:
0086         import native.evolution
0087         res.append( (guihelper.ID_IMPORT_EVO_CONTACTS,"Evolution Contacts...", "Import Evolution contacts for the phonebook", OnFileImportEvolutionContacts) )
0088     except ImportError:
0089         pass
0090     # Qtopia Desktop - always possible
0091     res.append( (guihelper.ID_IMPORT_QTOPIA_CONTACTS,"Qtopia Desktop...", "Import Qtopia Desktop contacts for the phonebook", OnFileImportQtopiaDesktopContacts) )
0092     # eGroupware - always possible
0093     res.append( (guihelper.ID_IMPORT_GROUPWARE_CONTACTS,"eGroupware...", "Import eGroupware contacts for the phonebook", OnFileImporteGroupwareContacts) )
0094     # WPL Playlist, always possible
0095     res.append( (guihelper.ID_IMPORT_WPL, 'WPL Play List...',
0096                  'Import WPL Play List',
0097                  OnWPLImport))
0098     return res
0099     
0100 def GetCalenderAutoSyncImports():
0101     res=[]
0102     # CSV - always possible
0103     res.append( ("CSV Calendar", AutoConfCSVCalender, AutoImportCSVCalendar) )
0104     # Vcal - always possible
0105     res.append(('vCalendar', AutoConfVCal, AutoImportVCal))
0106     # Outlook
0107     try:
0108         import native.outlook
0109         res.append( ("Outlook", AutoConfOutlookCalender, AutoImportOutlookCalendar) )
0110     except:
0111         print "Failed to get outlook"
0112         pass
0113     # Evolution
0114     
0115     return res
0116 
0117 def GetCalendarImports():
0118     # return a list of calendar types data objects
0119     res=[]
0120     res.append({ 'type': 'CSV Calendar',
0121                  'source': csv_calendar.ImportDataSource,
0122                  'data': csv_calendar.CSVCalendarImportData })
0123     res.append({ 'type': 'vCalendar',
0124                  'source': vcal_calendar.ImportDataSource,
0125                  'data': vcal_calendar.VCalendarImportData })
0126     res.append({ 'type': 'iCalendar',
0127                  'source': ical_calendar.ImportDataSource,
0128                  'data': ical_calendar.iCalendarImportData })
0129     res.append({ 'type': 'Google Calendar',
0130                  'source': gcal.ImportDataSource,
0131                  'data': gcal.gCalendarImportData })
0132     try:
0133         import native.outlook
0134         import outlook_calendar
0135         res.append({ 'type': 'Outlook Calendar',
0136                      'source': outlook_calendar.ImportDataSource,
0137                      'data': outlook_calendar.OutlookCalendarImportData })
0138     except:
0139         pass
0140     return res
0141 
0142 def TestOutlookIsInstalled():
0143     import native.outlook
0144     try:
0145         native.outlook.getmapinamespace()
0146     except:
0147         guihelper.MessageDialog(None, 'Unable to initialise Outlook, Check that it is installed correctly.',
0148                                 'Outlook Error', wx.OK|wx.ICON_ERROR)
0149         return False
0150     return True
0151 
0152 class PreviewGrid(wx.grid.Grid):
0153 
0154     def __init__(self, parent, id):
0155         wx.grid.Grid.__init__(self, parent, id, style=wx.WANTS_CHARS)
0156         wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self, self.OnLeftDClick)
0157 
0158     # (Taken from the demo) I do this because I don't like the default
0159     # behaviour of not starting the cell editor on double clicks, but
0160     # only a second click.
0161     def OnLeftDClick(self, evt):
0162         if self.CanEnableCellControl():
0163             self.EnableCellEditControl()
0164 
0165 class ImportDialog(wx.Dialog):
0166     "The dialog for importing phonebook stuff"
0167 
0168 
0169     # these are presented in the UI and are what the user can select.  additional
0170     # column names are available but not specified 
0171     possiblecolumns=["<ignore>", "First Name", "Last Name", "Middle Name",
0172                      "Name", "Nickname", "Email Address", "Web Page", "Fax", "Home Street",
0173                      "Home City", "Home Postal Code", "Home State",
0174                      "Home Country/Region",  "Home Phone", "Home Fax", "Mobile Phone", "Home Web Page",
0175                      "Business Street", "Business City", "Business Postal Code",
0176                      "Business State", "Business Country/Region", "Business Web Page",
0177                      "Business Phone", "Business Fax", "Pager", "Company", "Notes", "Private",
0178                      "Category", "Categories"]
0179     bp_columns=[
0180                      # BitPim CSV fields
0181                      'names_title', 'names_first', 'names_middle', 'names_last',
0182                      'names_full', 'names_nickname',
0183                      'addresses_type', 'addresses_company', 'addresses_street',
0184                      'addresses_street2', 'addresses_city',
0185                      'addresses_state', 'addresses_postalcode',
0186                      'addresses_country',
0187                      'numbers_number', 'numbers_type', 'numbers_speeddial',
0188                      'emails_email', 'emails_type',
0189                      'urls_url', 'urls_type',
0190                      'categories_category',
0191                      'ringtones_ringtone', 'ringtones_use',
0192                      'wallpapers_wallpaper', 'wallpapers_use',
0193                      'memos_memo', 'flags_secret'
0194                      ]
0195     
0196     # used for the filtering - any of the named columns have to be present for the data row
0197     # to be considered to have that type of column
0198     filternamecolumns=["First Name", "Last Name", "Middle Name", "Name", "Nickname"]
0199     
0200     filternumbercolumns=["Home Phone", "Home Fax", "Mobile Phone", "Business Phone",
0201                          "Business Fax", "Pager", "Fax", "Phone"]
0202 
0203     filterhomeaddresscolumns=["Home Street", "Home City", "Home Postal Code", "Home State",
0204                           "Home Country/Region"]
0205 
0206     filterbusinessaddresscolumns=["Business Street", "Business City",
0207                                   "Business Postal Code", "Business State", "Business Country/Region"]
0208 
0209     filteraddresscolumns=filterhomeaddresscolumns+filterbusinessaddresscolumns+["Address"]
0210 
0211     filteremailcolumns=["Email Address", "Email Addresses"]
0212                           
0213     # used in mapping column names above into bitpim phonebook fields
0214     addressmap={
0215         'Street': 'street',
0216         'City':   'city',
0217         'Postal Code': 'postalcode',
0218         'State':      'state',
0219         'Country/Region': 'country',
0220        }
0221 
0222     namemap={
0223         'First Name': 'first',
0224         'Last Name': 'last',
0225         'Middle Name': 'middle',
0226         'Name': 'full',
0227         'Nickname': 'nickname'
0228         }
0229 
0230     numbermap={
0231         "Home Phone": 'home',
0232         "Home Fax":   'fax',
0233         "Mobile Phone": 'cell',
0234         "Business Phone": 'office',
0235         "Business Fax":  'fax',
0236         "Pager": 'pager',
0237         "Fax": 'fax'
0238         }
0239 
0240 
0241     def __init__(self, parent, id, title, style=wx.CAPTION|wx.MAXIMIZE_BOX|\
0242                  wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER):
0243         wx.Dialog.__init__(self, parent, id=id, title=title, style=style)
0244         self.possiblecolumns+=self.bp_columns
0245         self.merge=True
0246         vbs=wx.BoxSizer(wx.VERTICAL)
0247         t,sz=self.gethtmlhelp()
0248         w=wx.html.HtmlWindow(self, -1, size=sz, style=wx.html.HW_SCROLLBAR_NEVER)
0249         w.SetPage(t)
0250         vbs.Add(w, 0, wx.EXPAND|wx.ALL,5)
0251 
0252         self.getcontrols(vbs)
0253 
0254         cfg=lambda key: wx.GetApp().config.ReadInt("importdialog/filter"+key, False)
0255         
0256 
0257         # Only records with ... row
0258         hbs=wx.BoxSizer(wx.HORIZONTAL)
0259         hbs.Add(wx.StaticText(self, -1, "Only rows with "), 0, wx.ALL|wx.ALIGN_CENTRE,2)
0260         self.wname=wx.CheckBox(self, wx.NewId(), "a name")
0261         self.wname.SetValue(cfg("name"))
0262         hbs.Add(self.wname, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7)
0263         self.wnumber=wx.CheckBox(self, wx.NewId(), "a number")
0264         self.wnumber.SetValue(cfg("phonenumber"))
0265         hbs.Add(self.wnumber, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7)
0266         self.waddress=wx.CheckBox(self, wx.NewId(), "an address")
0267         self.waddress.SetValue(cfg("postaladdress"))
0268         hbs.Add(self.waddress, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7)
0269         self.wemail=wx.CheckBox(self, wx.NewId(), "an email")
0270         self.wemail.SetValue(cfg("emailaddress"))
0271         hbs.Add(self.wemail, 0, wx.LEFT|wx.ALIGN_CENTRE,7)
0272         cats=wx.GetApp().config.Read("importdialog/filtercategories", "")
0273         if len(cats):
0274             self.categorieswanted=cats.split(";")
0275         else:
0276             self.categorieswanted=None
0277         self.categoriesbutton=wx.Button(self, wx.NewId(), "Categories...")
0278         hbs.Add(self.categoriesbutton, 0, wx.EXPAND|wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE, 10)
0279         self.categorieslabel=wx.StaticText(self, -1, "")
0280         if self.categorieswanted is None:
0281             self.categorieslabel.SetLabel("*ANY*")
0282         else:
0283             self.categorieslabel.SetLabel("; ".join(self.categorieswanted))
0284         hbs.Add(self.categorieslabel, 1, wx.ALIGN_LEFT|wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, 5)
0285         vbs.Add(hbs,0, wx.EXPAND|wx.ALL,5)
0286         # Full name options: Full Name, First M Last, or Last, First M
0287         self._name_option=wx.RadioBox(self, -1, 'Name Reformat',
0288                                       choices=['No Reformat', 'First M Last', 'Last, First M'])
0289         wx.EVT_RADIOBOX(self, self._name_option.GetId(),
0290                         self.DataNeedsUpdate)
0291         vbs.Add(self._name_option, 0, wx.ALL, 5)
0292         # Preview grid row
0293         self.preview=PreviewGrid(self, wx.NewId())
0294         self.preview.CreateGrid(10,10)
0295         self.preview.SetColLabelSize(0)
0296         self.preview.SetRowLabelSize(0)
0297         self.preview.SetMargins(1,0)
0298 
0299         vbs.Add(self.preview, 1, wx.EXPAND|wx.ALL, 5)
0300         # Static line and buttons
0301         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5)
0302         _button_sizer=self.CreateButtonSizer(wx.CANCEL|wx.HELP)
0303         _btn=wx.Button(self, -1, 'Merge')
0304         _button_sizer.Add(_btn, 0, wx.ALIGN_CENTER|wx.ALL, 5)
0305         wx.EVT_BUTTON(self, _btn.GetId(), self.OnOk)
0306         _btn=wx.Button(self, -1, 'Replace All')
0307         _button_sizer.Add(_btn, 0, wx.ALIGN_CENTER|wx.ALL, 5)
0308         wx.EVT_BUTTON(self, _btn.GetId(), self.OnReplaceAll)
0309         vbs.Add(_button_sizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
0310         self.SetSizer(vbs)
0311         for w in self.wname, self.wnumber, self.waddress, self.wemail:
0312             wx.EVT_CHECKBOX(self, w.GetId(), self.DataNeedsUpdate)
0313 
0314         wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
0315         wx.EVT_CLOSE(self, self.OnClose)
0316         wx.EVT_BUTTON(self, self.categoriesbutton.GetId(), self.OnCategories)
0317 
0318         guiwidgets.set_size("importdialog", self, 90)
0319         
0320         self.DataNeedsUpdate()
0321 
0322     def DataNeedsUpdate(self, _=None):
0323         "The preview data needs to be updated"
0324         self.needsupdate=True
0325         wx.CallAfter(self.UpdateData)
0326 
0327     def OnGridCellChanged(self, event):
0328         "Called when the user has changed one of the columns"
0329         self.columns[event.GetCol()]=self.preview.GetCellValue(0, event.GetCol())
0330         self.wcolumnsname.SetValue("Custom")
0331         if self.wname.GetValue() or self.wnumber.GetValue() or self.waddress.GetValue() or self.wemail.GetValue():
0332             self.DataNeedsUpdate()
0333 
0334     def OnClose(self, event=None):
0335         # save various config pieces
0336         guiwidgets.save_size("importdialog", self.GetRect())
0337         cfg=lambda key, value: wx.GetApp().config.WriteInt("importdialog/filter"+key, value)
0338         cfg("name", self.wname.GetValue())
0339         cfg("phonenumber", self.wnumber.GetValue())
0340         cfg("postaladdress", self.waddress.GetValue())
0341         cfg("emailaddress", self.wemail.GetValue())
0342         if self.categorieswanted is None:
0343             cats=""
0344         else:
0345             cats=";".join(self.categorieswanted)
0346         wx.GetApp().config.Write("importdialog/filtercategories", cats)
0347         wx.GetApp().config.Flush()
0348         if event is not None:
0349             event.Skip()
0350 
0351     def OnOk(self,_):
0352         "Ok button was pressed"
0353         if self.preview.IsCellEditControlEnabled():
0354             self.preview.HideCellEditControl()
0355             self.preview.SaveEditControlValue()
0356         self.OnClose()  # for some reason this isn't called automatically
0357         self.EndModal(wx.ID_OK)
0358 
0359     def OnReplaceAll(self, evt):
0360         "ReplaceAll button was pressed"
0361         self.merge=False
0362         self.OnOk(evt)
0363 
0364     def _reformat_name_firstmiddlelast(self, entry):
0365         # reformat the full name to be First Middle Last
0366         _name=entry.get('names', [None])[0]
0367         if not _name:
0368             return entry
0369         _s=nameparser.formatsimplefirstlast(_name)
0370         if _s:
0371             _name['full']=_s
0372             entry['names']=[_name]
0373         return entry
0374     def _reformat_name_lastfirtsmiddle(self, entry):
0375         # reformat the full name to be Last, First Middle
0376         _name=entry.get('names', [None])[0]
0377         if not _name:
0378             return entry
0379         _s=nameparser.formatsimplelastfirst(_name)
0380         if _s:
0381             _name['full']=_s
0382             entry['names']=[_name]
0383         return entry
0384     _reformat_name_func=(lambda self, entry: entry,
0385                          _reformat_name_firstmiddlelast,
0386                          _reformat_name_lastfirtsmiddle)
0387     def __build_entry(self, rec):
0388         entry={}
0389         # emails
0390         emails=[]
0391         if rec.has_key('Email Address'):
0392             for e in rec['Email Address']:
0393                 if isinstance(e, dict):
0394                     emails.append(e)
0395                 else:
0396                     emails.append({'email': e})
0397             del rec['Email Address']
0398         if rec.has_key("Email Addresses"):
0399             for e in rec['Email Addresses']:
0400                 emails.append({'email': e})
0401             del rec["Email Addresses"]
0402         if len(emails):
0403             entry['emails']=emails
0404         # addresses
0405         for prefix,fields in \
0406                 ( ("Home", self.filterhomeaddresscolumns),
0407                   ("Business", self.filterbusinessaddresscolumns)
0408                   ):
0409             addr={}
0410             for k in fields:
0411                 if k in rec:
0412                     # it has a field for this type
0413                     shortk=k[len(prefix)+1:]
0414                     addr['type']=prefix.lower()
0415                     addr[self.addressmap[shortk]]=rec[k]
0416                     del rec[k]
0417             if len(addr):
0418                 if prefix=="Business" and rec.has_key("Company"):
0419                     # fill in company info
0420                     addr['type']=prefix.lower()
0421                     addr['company']=rec["Company"]
0422                 if not entry.has_key("addresses"):
0423                     entry["addresses"]=[]
0424                 entry["addresses"].append(addr)
0425         # address (dict form of addresses)
0426         if rec.has_key("Address"):
0427             # ensure result key exists
0428             if not entry.has_key("addresses"):
0429                 entry["addresses"]=[]
0430             # find the company name
0431             company=rec.get("Company", None)
0432             for a in rec["Address"]:
0433                 if a["type"]=="business": a["company"]=company
0434                 addr={}
0435                 for k in ("type", "company", "street", "street2", "city", "state", "postalcode", "country"):
0436                     v=a.get(k, None)
0437                     if v is not None: addr[k]=v
0438                 entry["addresses"].append(addr)
0439             del rec["Address"]
0440         # numbers
0441         numbers=[]
0442         for field in self.filternumbercolumns:
0443             if field!="Phone" and rec.has_key(field):
0444                 for val in rec[field]:
0445                     numbers.append({'type': self.numbermap[field], 'number': phonenumber.normalise(val)})
0446                 del rec[field]
0447         # phones (dict form of numbers)
0448         if rec.has_key("Phone"):
0449             mapping={"business": "office", "business fax": "fax", "home fax": "fax"}
0450             for val in rec["Phone"]:
0451                 number={"type": mapping.get(val["type"], val["type"]),
0452                         "number": phonenumber.normalise(val["number"])}
0453                 sd=val.get('speeddial', None)
0454                 if sd is not None:
0455                     number.update({ 'speeddial': sd })
0456                 numbers.append(number)
0457             del rec["Phone"]
0458         if len(numbers):
0459             entry["numbers"]=numbers
0460                 
0461         # names
0462         name={}
0463         for field in self.filternamecolumns:
0464             if field in rec:
0465                 name[self.namemap[field]]=rec[field]
0466                 del rec[field]
0467         if len(name):
0468             entry["names"]=[name]
0469         # notes
0470         if rec.has_key("Notes"):
0471             notes=[]
0472             for note in rec["Notes"]:
0473                 notes.append({'memo': note})
0474             del rec["Notes"]
0475             entry["memos"]=notes
0476         # web pages
0477         urls=[]
0478         for type, key in ( (None, "Web Page"),
0479                           ("home", "Home Web Page"),
0480                           ("business", "Business Web Page")
0481                           ):
0482             if rec.has_key(key):
0483                 for url in rec[key]:
0484                     if isinstance(url, dict):
0485                         u=url
0486                     else:
0487                         u={'url': url}
0488                         if type is not None:
0489                             u['type']=type
0490                     urls.append(u)
0491                 del rec[key]
0492         if len(urls):
0493             entry["urls"]=urls
0494         # categories
0495         cats=[]
0496         if rec.has_key("Category"):
0497             cats=rec['Category']
0498             del rec["Category"]
0499         if rec.has_key("Categories"):
0500             # multiple entries in the field, semi-colon seperated
0501             if isinstance(rec['Categories'], list):
0502                 cats+=rec['Categories']
0503             else:
0504                 for cat in rec['Categories'].split(';'):
0505                     cats.append(cat)
0506             del rec['Categories']
0507         _cats=[]
0508         if self.categorieswanted is not None:
0509             for c in self.categorieswanted:
0510                 if c in cats:
0511                     _cats.append({'category': c })
0512         if _cats:
0513             entry["categories"]=_cats
0514         # wallpapers
0515         l=[]
0516         r=rec.get('Wallpapers', None)
0517         if r is not None:
0518             if isinstance(r, list):
0519                 l=[{'wallpaper': x, 'use': 'call' } for x in r]
0520             else:
0521                 l=[{'wallpaper': x, 'use': 'call' } for x in r.split(';')]
0522             del rec['Wallpapers']
0523         if len(l):
0524             entry['wallpapers']=l
0525         # ringtones
0526         l=[]
0527         r=rec.get('Ringtones', None)
0528         if r is not None:
0529             if isinstance(r, list):
0530                 l=[{'ringtone': x, 'use': 'call'} for x in r]
0531             else:
0532                 l=[{'ringtone': x, 'use': 'call'} for x in r.split(';')]
0533             del rec['Ringtones']
0534         if len(l):
0535             entry['ringtones']=l
0536         # flags
0537         flags=[]
0538         if rec.has_key("Private"):
0539             private=True
0540             # lets see how they have done false
0541             if rec["Private"].lower() in ("false", "no", 0, "0"):
0542                 private=False
0543             flags.append({'secret': private})
0544             del rec["Private"]
0545             
0546         if len(flags):
0547             entry["flags"]=flags
0548 
0549         # unique serials
0550         serial={}
0551         for k in rec.keys():
0552             if k.startswith("UniqueSerial-"):
0553                 v=rec[k]
0554                 del rec[k]
0555                 k=k[len("UniqueSerial-"):]
0556                 serial[k]=v
0557         if len(serial):
0558             assert serial.has_key("sourcetype")
0559             if len(serial)>1: # ie more than just sourcetype
0560                 entry["serials"]=[serial]
0561         # Did we forget anything?
0562         # Company is part of other fields
0563         if rec.has_key("Company"): del rec["Company"]
0564         if len(rec):
0565             raise Exception(
0566                 "Internal conversion failed to complete.\nStill to do: %s" % rec)
0567         return entry
0568 
0569     def __build_bp_entry(self, rec):
0570         entry={}
0571         for idx,col in enumerate(self.columns):
0572             # build the entry from the colum data
0573             key=col[:col.find('_')]
0574             field=col[col.find('_')+1:]
0575             v=rec[idx]
0576             if not len(v):
0577                 v=None
0578             if not entry.has_key(key):
0579                 entry[key]=[]
0580             done=False
0581             for field_idx,n in enumerate(entry[key]):
0582                 if not n.has_key(field):
0583                     entry[key][field_idx][field]=v
0584                     done=True
0585                     break
0586             if not done:
0587                 entry[key].append({ field: v })
0588         # go through and delete all blanks fields/dicts
0589         for k,e in entry.items():
0590             for i1,d in enumerate(e):
0591                 for k2,item in d.items():
0592                     if item is None:
0593                         del entry[k][i1][k2]
0594                     else:
0595                         if k2=='speeddial':
0596                             d[k2]=int(item)
0597                         elif k2=='secret':
0598                             d[k2]=True
0599                             if item.lower() in ("false", "no", 0, "0"):
0600                                 d[k2]=False
0601             l=[x for x in entry[k] if len(x)]
0602             if len(l):
0603                 entry[k]=l
0604             else:
0605                 del entry[k]
0606         return entry
0607         
0608     def GetFormattedData(self):
0609         "Returns the data in BitPim phonebook format"
0610         bp_csv=True
0611         for c in self.columns:
0612             if c=="<ignore>":
0613                 continue
0614             if c not in self.bp_columns:
0615                 bp_csv=False
0616                 break
0617         res={}
0618         count=0
0619         for record in self.data:
0620             if bp_csv:
0621                 _entry=self.__build_bp_entry(record)
0622             else:                
0623                 # make a dict of the record
0624                 rec={}
0625                 for n in range(len(self.columns)):
0626                     c=self.columns[n]
0627                     if c=="<ignore>":
0628                         continue
0629                     if record[n] is None or len(record[n])==0:
0630                         continue
0631                     if c not in self.bp_columns:
0632                         bp_csv=False
0633                     if c in self.filternumbercolumns or c in \
0634                        ["Category", "Notes", "Business Web Page", "Home Web Page", "Web Page", "Notes", "Phone", "Address", "Email Address"]:
0635                         # these are multivalued
0636                         if not rec.has_key(c):
0637                             rec[c]=[]
0638                         rec[c].append(record[n])
0639                     else:
0640                         rec[c]=record[n]
0641                 # entry is what we are building.  fields are removed from rec as we process them
0642                 _entry=self.__build_entry(rec)
0643             res[count]=self._reformat_name_func[self._name_option.GetSelection()](self,
0644                                                                                   _entry)
0645             count+=1
0646         return res
0647 
0648     def GetExtractCategoriesFunction(self):
0649         res=""
0650         for col,name in enumerate(self.columns):
0651             if name=="Categories":
0652                 res+="_getpreviewformatted(row[%d], %s).split(';') + " % (col, `name`)
0653             elif name=="Category":
0654                 res+="_getpreviewformatted(row[%d], %s) + " % (col, `name`)
0655         res+="[]"
0656         fn=compile(res, "_GetExtractCategoriesFunction_", 'eval')
0657         return lambda row: eval(fn, globals(), {'row': row})
0658 
0659 
0660     def OnCategories(self, _):
0661         # find all categories in current unfiltered data
0662         savedcolumns,saveddata=self.columns, self.data
0663         if self.categorieswanted is not None:
0664             # we have to re-read the data if currently filtering categories!  This is
0665             # because it would contain only the currently selected categories.
0666             self.ReReadData()  
0667         catfn=self.GetExtractCategoriesFunction()
0668         cats=[]
0669         for row in self.data:
0670             for c in catfn(row):
0671                 if c not in cats:
0672                     cats.append(c)
0673         cats.sort()
0674         if len(cats) and cats[0]=="":
0675             cats=cats[1:]
0676         self.columns,self.data=savedcolumns, saveddata
0677         with guihelper.WXDialogWrapper(CategorySelectorDialog(self, self.categorieswanted, cats),
0678                                        True) as (dlg, retcode):
0679             if retcode==wx.ID_OK:
0680                 self.categorieswanted=dlg.GetCategories()
0681                 if self.categorieswanted is None:
0682                     self.categorieslabel.SetLabel("*ALL*")
0683                 else:
0684                     self.categorieslabel.SetLabel("; ".join(self.categorieswanted))
0685                 self.DataNeedsUpdate()
0686 
0687     @guihelper.BusyWrapper
0688     def UpdateData(self):
0689         "Actually update the preview data"
0690         if not self.needsupdate:
0691             return
0692         self.needsupdate=False
0693         # reread the data
0694         self.ReReadData()
0695         # category filtering
0696         if self.categorieswanted is not None:
0697             newdata=[]
0698             catfn=self.GetExtractCategoriesFunction()
0699             for row in self.data:
0700                 for cat in catfn(row):
0701                     if cat in self.categorieswanted:
0702                         newdata.append(row)
0703                         break
0704             self.data=newdata
0705 
0706         # name/number/address/email filtering
0707         if self.wname.GetValue() or self.wnumber.GetValue() or self.waddress.GetValue() or self.wemail.GetValue():
0708             newdata=[]
0709             for rownum in range(len(self.data)):
0710                 # generate a list of fields for which this row has data
0711                 fields=[]
0712                 # how many filters are required
0713                 req=0
0714                 # how many are present
0715                 present=0
0716                 for n in range(len(self.columns)):
0717                     v=self.data[rownum][n]
0718                     if v is not None and len(v):
0719                         fields.append(self.columns[n])
0720                 for widget,filter in ( (self.wname, self.filternamecolumns),
0721                                        (self.wnumber, self.filternumbercolumns),
0722                                        (self.waddress, self.filteraddresscolumns),
0723                                        (self.wemail, self.filteremailcolumns)
0724                                        ):
0725                     if widget.GetValue():
0726                         req+=1
0727                         for f in fields:
0728                             if f in filter:
0729                                 present+=1
0730                                 break
0731                     if req>present:
0732                         break
0733                 if present==req:
0734                     newdata.append(self.data[rownum])
0735             self.data=newdata
0736 
0737         self.FillPreview()
0738 
0739     def _preview_format_name_none(self, row, col, names_col):
0740         # no format needed
0741         return row[col]
0742     def _preview_format_name_lastfirtmiddle(self, row, col, names_col):
0743         # reformat to Last, First Middle
0744         _last=names_col.get('Last Name',
0745                             names_col.get('names_last', None))
0746         _first=names_col.get('First Name',
0747                              names_col.get('names_first', None))
0748         _middle=names_col.get('Middle Name',
0749                               names_col.get('names_middle', None))
0750         _full=names_col.get('Name',
0751                             names_col.get('names_full', None))
0752         _name_dict={}
0753         for _key,_value in (('full', _full), ('first', _first),
0754                             ('middle', _middle), ('last', _last)):
0755             if _value is not None and row[_value]:
0756                 _name_dict[_key]=row[_value]
0757         return nameparser.formatsimplelastfirst(_name_dict)
0758     def _preview_format_name_firstmiddlelast(self, row, col, names_col):
0759         # reformat to First Middle Last
0760         _last=names_col.get('Last Name',
0761                             names_col.get('names_last', None))
0762         _first=names_col.get('First Name',
0763                              names_col.get('names_first', None))
0764         _middle=names_col.get('Middle Name',
0765                               names_col.get('names_middle', None))
0766         _full=names_col.get('Name',
0767                             names_col.get('names_full', None))
0768         _name_dict={}
0769         for _key,_value in (('full', _full), ('first', _first),
0770                             ('middle', _middle), ('last', _last)):
0771             if _value is not None and row[_value]:
0772                 _name_dict[_key]=row[_value]
0773         return nameparser.formatsimplefirstlast(_name_dict)
0774     _preview_format_names_func=(_preview_format_name_none,
0775                                 _preview_format_name_firstmiddlelast,
0776                                 _preview_format_name_lastfirtmiddle)
0777         
0778     def FillPreview(self):
0779         self.preview.BeginBatch()
0780         if self.preview.GetNumberCols():
0781             self.preview.DeleteCols(0,self.preview.GetNumberCols())
0782         self.preview.DeleteRows(0,self.preview.GetNumberRows())
0783         self.preview.ClearGrid()
0784         
0785         numrows=len(self.data)
0786         if numrows:
0787             numcols=max(map(lambda x: len(x), self.data))
0788         else:
0789             numcols=len(self.columns)
0790         # add header row
0791         editor=wx.grid.GridCellChoiceEditor(self.possiblecolumns, False)
0792         self.preview.AppendRows(1)
0793         self.preview.AppendCols(numcols)
0794         _names_col={}
0795         for col in range(numcols):
0796             if 'Name' in self.columns[col] or \
0797                'names_' in self.columns[col]:
0798                 _names_col[self.columns[col]]=col
0799             self.preview.SetCellValue(0, col, self.columns[col])
0800             self.preview.SetCellEditor(0, col, editor)
0801         attr=wx.grid.GridCellAttr()
0802         attr.SetBackgroundColour(wx.GREEN)
0803         attr.SetFont(wx.Font(10,wx.SWISS, wx.NORMAL, wx.BOLD))
0804         attr.SetReadOnly(not self.headerrowiseditable)
0805         self.preview.SetRowAttr(0,attr)
0806         # add each row
0807         oddattr=wx.grid.GridCellAttr()
0808         oddattr.SetBackgroundColour("OLDLACE")
0809         oddattr.SetReadOnly(True)
0810         evenattr=wx.grid.GridCellAttr()
0811         evenattr.SetBackgroundColour("ALICE BLUE")
0812         evenattr.SetReadOnly(True)
0813         _format_name=self._preview_format_names_func[self._name_option.GetSelection()]
0814         for row in range(numrows):
0815             self.preview.AppendRows(1)
0816             for col in range(numcols):
0817                 if self.columns[col] in ('Name', 'names_full'):
0818                     s=_format_name(self, self.data[row], col, _names_col)
0819                 else:
0820                     s=_getpreviewformatted(self.data[row][col], self.columns[col])
0821                 if len(s):
0822                     self.preview.SetCellValue(row+1, col, s)
0823             self.preview.SetRowAttr(row+1, (evenattr,oddattr)[row%2])
0824         self.preview.AutoSizeColumns()
0825         self.preview.AutoSizeRows()
0826         self.preview.EndBatch()
0827 
0828 def _getpreviewformatted(value, column):
0829     if value is None: return ""
0830     if isinstance(value, dict):
0831         if column=="Email Address":
0832             value="%s (%s)" %(value["email"], value["type"])
0833         elif column=="Web Page":
0834             value="%s (%s)" %(value["url"], value["type"])
0835         elif column=="Phone":
0836             value="%s (%s)" %(phonenumber.format(value["number"]), value["type"])
0837         elif column=="Address":
0838             v=[]
0839             for f in ("company", "pobox", "street", "street2", "city", "state", "postalcode", "country"):
0840                 vv=value.get(f, None)
0841                 if vv is not None:
0842                     v.append(vv)
0843             assert len(v)
0844             v[0]=v[0]+"  (%s)" %(value['type'],)
0845             value="\n".join(v)
0846         else:
0847             print "don't know how to convert dict",value,"for preview column",column
0848             assert False
0849     elif isinstance(value, list):
0850         if column=="Email Addresses":
0851             value="\n".join(value)
0852         elif column=="Categories":
0853             value=";".join(value)
0854         else:
0855             print "don't know how to convert list",value,"for preview column",column
0856             assert False
0857     return common.strorunicode(value)
0858 
0859 
0860 class CategorySelectorDialog(wx.Dialog):
0861 
0862     def __init__(self, parent, categorieswanted, categoriesavailable):
0863         wx.Dialog.__init__(self, parent, title="Import Category Selector", style=wx.CAPTION|wx.MAXIMIZE_BOX|\
0864                  wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) #, size=(640,480))
0865         vbs=wx.BoxSizer(wx.VERTICAL)
0866         hbs=wx.BoxSizer(wx.HORIZONTAL)
0867         self.selected=wx.RadioButton(self, wx.NewId(), "Selected Below", style=wx.RB_GROUP)
0868         self.any=wx.RadioButton(self, wx.NewId(), "Any/All")
0869         hbs.Add(self.selected, 0, wx.ALL, 5)
0870         hbs.Add(self.any, 0, wx.ALL, 5)
0871         _up=wx.BitmapButton(self, -1,
0872                             wx.ArtProvider.GetBitmap(guihelper.ART_ARROW_UP, wx.ART_TOOLBAR,
0873                                                      wx.Size(16, 16)))
0874         _dn=wx.BitmapButton(self, -1,
0875                             wx.ArtProvider.GetBitmap(guihelper.ART_ARROW_DOWN, wx.ART_TOOLBAR,
0876                                                      wx.Size(16, 16)))
0877         hbs.Add(_up, 0, wx.ALL, 5)
0878         wx.EVT_BUTTON(self, _up.GetId(), self.OnMoveUp)
0879         wx.EVT_BUTTON(self, _dn.GetId(), self.OnMoveDown)
0880         hbs.Add(_dn, 0, wx.ALL, 5)
0881         vbs.Add(hbs, 0, wx.ALL, 5)
0882 
0883         self.categoriesavailable=categoriesavailable
0884         self.cats=wx.CheckListBox(self, wx.NewId(), choices=categoriesavailable)
0885         vbs.Add(self.cats, 1, wx.EXPAND|wx.ALL, 5)
0886         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5)
0887         vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5)
0888 
0889         if categorieswanted is None:
0890             self.any.SetValue(True)
0891             self.selected.SetValue(False)
0892         else:
0893             self.any.SetValue(False)
0894             self.selected.SetValue(True)
0895             for c in categorieswanted:
0896                 try:
0897                     self.cats.Check(categoriesavailable.index(c))
0898                 except ValueError:
0899                     pass # had one selected that wasn't in list
0900 
0901         wx.EVT_CHECKLISTBOX(self, self.cats.GetId(), self.OnCatsList)
0902 
0903         self.SetSizer(vbs)
0904         vbs.Fit(self)
0905 
0906     def OnCatsList(self, _):
0907         self.any.SetValue(False)
0908         self.selected.SetValue(True)
0909 
0910     def GetCategories(self):
0911         if self.any.GetValue():
0912             return None
0913         return [self.cats.GetString(x) for x in range(len(self.categoriesavailable)) if self.cats.IsChecked(x)]
0914 
0915     def _populate(self):
0916         _sel_str=self.cats.GetStringSelection()
0917         _chk=self.GetCategories()
0918         if _chk is None:
0919             _chk=[]
0920         self.cats.Clear()
0921         for s in self.categoriesavailable:
0922             i=self.cats.Append(s)
0923             if s==_sel_str:
0924                 self.cats.SetSelection(i)
0925             self.cats.Check(i, s in _chk)
0926 
0927     def OnMoveUp(self, _):
0928         _sel_idx=self.cats.GetSelection()
0929         if _sel_idx==wx.NOT_FOUND or not _sel_idx:
0930             # no selection or top item
0931             return
0932         # move the selected item one up
0933         self.categoriesavailable[_sel_idx], self.categoriesavailable[_sel_idx-1]=\
0934         self.categoriesavailable[_sel_idx-1], self.categoriesavailable[_sel_idx]
0935         self._populate()
0936 
0937     def OnMoveDown(self, _):
0938         _sel_idx=self.cats.GetSelection()
0939         if _sel_idx==wx.NOT_FOUND or \
0940            _sel_idx==len(self.categoriesavailable)-1:
0941             # no selection or bottom item
0942             return
0943         # move the selected item one up
0944         self.categoriesavailable[_sel_idx], self.categoriesavailable[_sel_idx+1]=\
0945         self.categoriesavailable[_sel_idx+1], self.categoriesavailable[_sel_idx]
0946         self._populate()
0947 
0948 class ImportCSVDialog(ImportDialog):
0949 
0950     delimiternames={
0951         '\t': "Tab",
0952         ' ': "Space",
0953         ',': "Comma"
0954         }
0955 
0956     def __init__(self, filename, parent, id, title):
0957         self.headerrowiseditable=True
0958         self.filename=filename
0959         self.UpdatePredefinedColumns()
0960         ImportDialog.__init__(self, parent, id, title)
0961 
0962     def gethtmlhelp(self):
0963         "Returns tuple of help text and size"
0964         bg=self.GetBackgroundColour()
0965         return '<html><body BGCOLOR="#%02X%02X%02X">Importing %s.  BitPim has guessed the delimiter seperating each column, and the text qualifier that quotes values.  You need to select what each column is by clicking in the top row, or select one of the predefined sets of columns.</body></html>' % (bg.Red(), bg.Green(), bg.Blue(), self.filename), \
0966                 (600,100)
0967 
0968     def getcontrols(self, vbs):
0969         data=common.opentextfile(self.filename).read()
0970         # turn all EOL chars into \n and then ensure only one \n terminates each line
0971         data=data.replace("\r", "\n")
0972         oldlen=-1
0973         while len(data)!=oldlen:
0974             oldlen=len(data)
0975             data=data.replace("\n\n", "\n")
0976             
0977         self.rawdata=data
0978 
0979         self.qualifier=DSV.guessTextQualifier(self.rawdata)
0980         if self.qualifier is None or len(self.qualifier)==0:
0981             self.qualifier='"'
0982         self.data=DSV.organizeIntoLines(self.rawdata, textQualifier=self.qualifier)
0983         self.delimiter=DSV.guessDelimiter(self.data)
0984         # sometimes it picks the letter 'w'
0985         if self.delimiter is not None and self.delimiter.lower() in "abcdefghijklmnopqrstuvwxyz":
0986             self.delimiter=None
0987         if self.delimiter is None:
0988             if self.filename.lower().endswith("tsv"):
0989                 self.delimiter="\t"
0990             else:
0991                 self.delimiter=","
0992         # complete processing the data otherwise we can't guess if first row is headers
0993         self.data=DSV.importDSV(self.data, delimiter=self.delimiter, textQualifier=self.qualifier, errorHandler=DSV.padRow)
0994         # Delimter and Qualifier row
0995         hbs=wx.BoxSizer(wx.HORIZONTAL)
0996         hbs.Add(wx.StaticText(self, -1, "Delimiter"), 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE, 2)
0997         self.wdelimiter=wx.ComboBox(self, wx.NewId(), self.PrettyDelimiter(self.delimiter), choices=self.delimiternames.values(), style=wx.CB_DROPDOWN|wx.WANTS_CHARS)
0998         hbs.Add(self.wdelimiter, 1, wx.EXPAND|wx.ALL, 2)
0999         hbs.Add(wx.StaticText(self, -1, "Text Qualifier"), 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE,2)
1000         self.wqualifier=wx.ComboBox(self, wx.NewId(), self.qualifier, choices=['"', "'", "(None)"], style=wx.CB_DROPDOWN|wx.WANTS_CHARS)
1001         hbs.Add(self.wqualifier, 1, wx.EXPAND|wx.ALL, 2)
1002         vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
1003         # Pre-set columns, save and header row
1004         hbs=wx.BoxSizer(wx.HORIZONTAL)
1005         hbs.Add(wx.StaticText(self, -1, "Columns"), 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE, 2)
1006         self.wcolumnsname=wx.ComboBox(self, wx.NewId(), "Custom", choices=self.predefinedcolumns+["Custom"], style=wx.CB_READONLY|wx.CB_DROPDOWN|wx.WANTS_CHARS)
1007         hbs.Add(self.wcolumnsname, 1, wx.EXPAND|wx.ALL, 2)
1008         self.wfirstisheader=wx.CheckBox(self, wx.NewId(), "First row is header")
1009         self.wfirstisheader.SetValue(DSV.guessHeaders(self.data))
1010         hbs.Add(self.wfirstisheader, 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE, 5)
1011         vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
1012 
1013         # event handlers
1014         wx.EVT_CHECKBOX(self, self.wfirstisheader.GetId(), self.OnHeaderToggle)
1015         wx.grid.EVT_GRID_CELL_CHANGE(self, self.OnGridCellChanged)
1016         wx.EVT_TEXT(self, self.wdelimiter.GetId(), self.OnDelimiterChanged)
1017         wx.EVT_TEXT(self, self.wqualifier.GetId(), self.OnQualifierChanged)
1018         wx.EVT_TEXT(self, self.wcolumnsname.GetId(), self.OnColumnsNameChanged)
1019 
1020     def PrettyDelimiter(self, delim):
1021         "Returns a pretty version of the delimiter (eg Tab, Space instead of \t, ' ')"
1022         assert delim is not None
1023         if delim in self.delimiternames:
1024             return self.delimiternames[delim]
1025         return delim
1026         
1027     def UpdatePredefinedColumns(self):
1028         """Updates the list of pre-defined column names.
1029 
1030         We look for files with an extension of .pdc in the resource directory.  The first
1031         line of the file is the description, and each remaining line corresponds to a
1032         column"""
1033         self.predefinedcolumns=[]
1034         for i in guihelper.getresourcefiles("*.pdc"):
1035             with contextlib.closing(common.opentextfile(i)) as f:
1036                 self.predefinedcolumns.append(f.readline().strip())
1037 
1038     def OnHeaderToggle(self, _):
1039         self.columns=None
1040         self.DataNeedsUpdate()
1041 
1042     def OnDelimiterChanged(self, _):
1043         "Called when the user has changed the delimiter"
1044         text=self.wdelimiter.GetValue()
1045         if hasattr(self, "lastwdelimitervalue") and self.lastwdelimitervalue==text:
1046             print "on delim changed ignored"
1047             return
1048 
1049         if len(text)!=1:
1050             if text in self.delimiternames.values():
1051                 for k in self.delimiternames:
1052                     if self.delimiternames[k]==text:
1053                         text=k
1054             else:
1055                 if len(text)==0:
1056                     text="Comma"
1057                 else:
1058                     text=text[-1]
1059                     if text in self.delimiternames:
1060                         text=self.delimiternames[text]
1061                 self.wdelimiter.SetValue(text)
1062         self.delimiter=text
1063         self.columns=None
1064         self.DataNeedsUpdate()
1065         # these calls cause another OnDelimiterChanged callback to happen, so we have to stop the loop
1066         self.lastwdelimitervalue=self.wdelimiter.GetValue()
1067         wx.CallAfter(self.wdelimiter.SetInsertionPointEnd)
1068         wx.CallAfter(self.wdelimiter.SetMark, 0,len(self.wdelimiter.GetValue()))
1069 
1070     def OnQualifierChanged(self,_):
1071         "Called when the user has changed the qualifier"
1072         # Very similar to the above function
1073         text=self.wqualifier.GetValue()
1074         if hasattr(self, "lastwqualifiervalue") and self.lastwqualifiervalue==text:
1075             return
1076         if len(text)!=1:
1077             if text=='(None)':
1078                 text=None
1079             else:
1080                 if len(text)==0:
1081                     self.wqualifier.SetValue('(None)')
1082                     text=None
1083                 else:
1084                     text=text[-1]
1085                     self.wqualifier.SetValue(text)
1086         self.qualifier=text
1087         self.columns=None
1088         self.DataNeedsUpdate()
1089         self.lastwqualifiervalue=self.wqualifier.GetValue()
1090         wx.CallAfter(self.wqualifier.SetInsertionPointEnd)
1091         wx.CallAfter(self.wqualifier.SetMark, 0,len(self.wqualifier.GetValue()))
1092         
1093     def OnColumnsNameChanged(self,_):
1094         if self.wcolumnsname.GetValue()=="Custom":
1095             return
1096         str=self.wcolumnsname.GetValue()
1097         for file in guihelper.getresourcefiles("*.pdc"):
1098             with contextlib.closing(common.opentextfile(file)) as f:
1099                 desc=f.readline().strip()
1100                 if desc==str:
1101                     self.columns=map(string.strip, f.readlines())
1102                     for i in range(len(self.columns)):
1103                         if self.columns[i] not in self.possiblecolumns:
1104                             print self.columns[i],"is not a valid column name!"
1105                             self.columns[i]="<ignore>"
1106                     self.DataNeedsUpdate()
1107                     return
1108         print "didn't find pdc for",str
1109 
1110     def ReReadData(self):
1111         self.data=DSV.organizeIntoLines(self.rawdata, textQualifier=self.qualifier)
1112         self.data=DSV.importDSV(self.data, delimiter=self.delimiter, textQualifier=self.qualifier, errorHandler=DSV.padRow)
1113         self.FigureOutColumns()
1114 
1115     def FigureOutColumns(self):
1116         "Initialize the columns variable, using header row if there is one"
1117         numcols=max(map(lambda x: len(x), self.data))
1118         # normalize number of columns
1119         for row in self.data:
1120             while len(row)<numcols:
1121                 row.append('')
1122         guesscols=False
1123         if not hasattr(self, "columns") or self.columns is None:
1124             self.columns=["<ignore>"]*numcols
1125             guesscols=True
1126         while len(self.columns)<numcols:
1127             self.columns.append("<ignore>")
1128         self.columns=self.columns[:numcols]
1129         if not self.wfirstisheader.GetValue():
1130             return
1131         headers=self.data[0]
1132         self.data=self.data[1:]
1133         if not guesscols:
1134             return
1135         mungedcolumns=[]
1136         for c in self.possiblecolumns:
1137             mungedcolumns.append("".join(filter(lambda x: x in "abcdefghijklmnopqrstuvwxyz0123456789", c.lower())))
1138         # look for header in possible columns
1139         for col,header in zip(range(numcols), headers):
1140             if header in self.possiblecolumns:
1141                 self.columns[col]=header
1142                 continue
1143             h="".join(filter(lambda x: x in "abcdefghijklmnopqrstuvwxyz0123456789", header.lower()))
1144             
1145             if h in mungedcolumns:
1146                 self.columns[col]=self.possiblecolumns[mungedcolumns.index(h)]
1147                 continue
1148             # here is where we would do some mapping
1149 
1150 class ImportOutlookDialog(ImportDialog):
1151     # the order of this mapping matters ....
1152     importmapping=(
1153         # first column is field in Outlook
1154         # second column is field in dialog (ImportDialog.possiblecolumns)
1155         ('FirstName',            "First Name" ),
1156         ('LastName',             "Last Name"),
1157         ('MiddleName',           "Middle Name"),
1158         # ('FullName',  ),       -- this includes the prefix (aka title in Outlook) and the suffix
1159         # ('Title',  ),          -- the prefix (eg Dr, Mr, Mrs)
1160         ('Subject',              "Name"),  # this is first middle last suffix - note no prefix!
1161         # ('Suffix',  ),         -- Jr, Sr, III etc
1162         ('NickName',             "Nickname"),
1163         ('Email1Address',        "Email Address"),
1164         ('Email2Address',        "Email Address"),
1165         ('Email3Address',        "Email Address"),
1166         # Outlook is seriously screwed over web pages.  It treats the Business Home Page
1167         # and Web Page as the same field, so we can't really tell the difference.
1168         ('WebPage',              "Web Page"),
1169         ('OtherFaxNumber',       "Fax"  ),
1170         ('HomeAddressStreet',    "Home Street"),
1171         ('HomeAddressCity',      "Home City" ),
1172         ('HomeAddressPostalCode',"Home Postal Code"  ),
1173         ('HomeAddressState',     "Home State"),
1174         ('HomeAddressCountry',   "Home Country/Region" ),
1175         ('HomeTelephoneNumber',  "Home Phone"),
1176         ('Home2TelephoneNumber', "Home Phone"),
1177         ('HomeFaxNumber',        "Home Fax"),
1178         ('MobileTelephoneNumber',"Mobile Phone"),
1179         ('PersonalHomePage',     "Home Web Page"),
1180 
1181         ('BusinessAddressStreet',"Business Street"),
1182         ('BusinessAddressCity',  "Business City"),
1183         ('BusinessAddressPostalCode', "Business Postal Code"),
1184         ('BusinessAddressState', "Business State"),
1185         ('BusinessAddressCountry', "Business Country/Region"),
1186         # ('BusinessHomePage',), -- no use, see Web Page above
1187         ('BusinessTelephoneNumber', "Business Phone"),        
1188         ('Business2TelephoneNumber',"Business Phone"),
1189         ('BusinessFaxNumber',    "Business Fax"),
1190         ('PagerNumber',          "Pager"),
1191         ('CompanyName',          "Company"),
1192         
1193         ('Body',                 "Notes"),  # yes, really
1194 
1195         ('Categories',           "Categories"),
1196 
1197 
1198         ('EntryID',              "UniqueSerial-EntryID"),
1199         
1200         )
1201 
1202     
1203     # These are all the fields we do nothing about
1204 ##           ('Anniversary',  ),
1205 ##           ('AssistantName',  ),
1206 ##           ('AssistantTelephoneNumber',  ),
1207 ##           ('Birthday',  ),
1208 ##           ('BusinessAddress',  ),
1209 ##           ('BusinessAddressPostOfficeBox',  ),
1210 ##           ('CallbackTelephoneNumber',  ),
1211 ##           ('CarTelephoneNumber',  ),
1212 ##           ('Children',  ),
1213 ##           ('Class',  ),
1214 ##           ('CompanyAndFullName',  ),
1215 ##           ('CompanyLastFirstNoSpace',  ),
1216 ##           ('CompanyLastFirstSpaceOnly',  ),
1217 ##           ('CompanyMainTelephoneNumber',  ),
1218 ##           ('ComputerNetworkName',  ),
1219 ##           ('ConversationIndex',  ),
1220 ##           ('ConversationTopic',  ),
1221 ##           ('CreationTime',  ),
1222 ##           ('CustomerID',  ),
1223 ##           ('Department',  ),
1224 ##           ('FTPSite',  ),
1225 ##           ('FileAs',  ),
1226 ##           ('FullNameAndCompany',  ),
1227 ##           ('Gender',  ),
1228 ##           ('GovernmentIDNumber',  ),
1229 ##           ('Hobby',  ),
1230 ##           ('HomeAddress',  ),
1231 ##           ('HomeAddressPostOfficeBox',  ),
1232 ##           ('ISDNNumber',  ),
1233 ##           ('Importance',  ),
1234 ##           ('Initials',  ),
1235 ##           ('InternetFreeBusyAddress',  ),
1236 ##           ('JobTitle',  ),
1237 ##           ('Journal',  ),
1238 ##           ('Language',  ),
1239 ##           ('LastFirstAndSuffix',  ),
1240 ##           ('LastFirstNoSpace',  ),
1241 ##           ('LastFirstNoSpaceCompany',  ),
1242 ##           ('LastFirstSpaceOnly',  ),
1243 ##           ('LastFirstSpaceOnlyCompany',  ),
1244 ##           ('LastModificationTime',  ),
1245 ##           ('LastNameAndFirstName',  ),
1246 ##           ('MAPIOBJECT',  ),
1247 ##           ('MailingAddress',  ),
1248 ##           ('MailingAddressCity',  ),
1249 ##           ('MailingAddressCountry',  ),
1250 ##           ('MailingAddressPostalCode',  ),
1251 ##           ('MailingAddressState',  ),
1252 ##           ('MailingAddressStreet',  ),
1253 ##           ('ManagerName',  ),
1254 ##           ('MessageClass',  ),
1255 ##           ('Mileage',  ),
1256 ##           ('NetMeetingAlias',  ),
1257 ##           ('NetMeetingServer',  ),
1258 ##           ('NoAging',  ),
1259 ##           ('OfficeLocation',  ),
1260 ##           ('OrganizationalIDNumber',  ),
1261 ##           ('OtherAddress',  ),
1262 ##           ('OtherAddressCity',  ),
1263 ##           ('OtherAddressCountry',  ),
1264 ##           ('OtherAddressPostOfficeBox',  ),
1265 ##           ('OtherAddressPostalCode',  ),
1266 ##           ('OtherAddressState',  ),
1267 ##           ('OtherAddressStreet',  ),
1268 ##           ('OtherTelephoneNumber',  ),
1269 ##           ('OutlookInternalVersion',  ),
1270 ##           ('OutlookVersion',  ),
1271 ##           ('Parent',  ),
1272 ##           ('PrimaryTelephoneNumber',  ),
1273 ##           ('Profession',  ),
1274 ##           ('RadioTelephoneNumber',  ),
1275 ##           ('ReferredBy',  ),
1276 ##           ('Saved',  ),
1277 ##           ('SelectedMailingAddress',  ),
1278 ##           ('Sensitivity',  ),
1279 ##           ('Size',  ),
1280 ##           ('Spouse',  ),
1281 ##           ('TTYTDDTelephoneNumber',  ),
1282 ##           ('TelexNumber',  ),
1283 ##           ('UnRead',  ),
1284 ##           ('User1',  ),
1285 ##           ('User2',  ),
1286 ##           ('User3',  ),
1287 ##           ('User4',  ),
1288  
1289     importmappingdict={}
1290     for o,i in importmapping: importmappingdict[o]=i
1291 
1292     def __init__(self, parent, id, title, outlook):
1293         self.headerrowiseditable=False
1294         self.outlook=outlook
1295         ImportDialog.__init__(self, parent, id, title)
1296 
1297     def gethtmlhelp(self):
1298         "Returns tuple of help text and size"
1299         bg=self.GetBackgroundColour()
1300         return '<html><body BGCOLOR="#%02X%02X%02X">Importing Outlook Contacts.  Select the folder to import, and do any filtering necessary.</body></html>' % (bg.Red(), bg.Green(), bg.Blue()), \
1301                 (600,30)
1302 
1303     def getcontrols(self, vbs):
1304         hbs=wx.BoxSizer(wx.HORIZONTAL)
1305         # label
1306         hbs.Add(wx.StaticText(self, -1, "Folder"), 0, wx.ALL|wx.ALIGN_CENTRE, 2)
1307         # where the folder name goes
1308         self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY)
1309         hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2)
1310         # browse button
1311         self.folderbrowse=wx.Button(self, wx.NewId(), "Browse ...")
1312         hbs.Add(self.folderbrowse, 0, wx.EXPAND|wx.ALL, 2)
1313         vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
1314         wx.EVT_BUTTON(self, self.folderbrowse.GetId(), self.OnBrowse)
1315 
1316         # sort out folder
1317         id=wx.GetApp().config.Read("outlook/contacts", "")
1318         self.folder=self.outlook.getfolderfromid(id, True)
1319         wx.GetApp().config.Write("outlook/contacts", self.outlook.getfolderid(self.folder))
1320         self.folderctrl.SetValue(self.outlook.getfoldername(self.folder))
1321 
1322     def OnBrowse(self, _):
1323         p=self.outlook.pickfolder()
1324         if p is None: return # user hit cancel
1325         self.folder=p
1326         wx.GetApp().config.Write("outlook/contacts", self.outlook.getfolderid(self.folder))
1327         self.folderctrl.SetValue(self.outlook.getfoldername(self.folder))
1328         self.DataNeedsUpdate()
1329 
1330     def ReReadData(self):
1331         # this can take a really long time if the user doesn't spot the dialog
1332         # asking for permission to access email addresses :-)
1333         items=self.outlook.getcontacts(self.folder, self.importmappingdict.keys())
1334 
1335         # work out what keys are actually present
1336         keys={}
1337         for item in items:
1338             for k in item.keys():
1339                 keys[k]=1
1340 
1341         # We now need to produce columns with BitPim names not the Outlook ones.
1342         # mappings are in self.importmapping
1343         want=[]
1344         for o,i in self.importmapping:
1345             if o in keys.keys():
1346                 want.append(o)
1347         # want now contains list of Outlook keys we want, and the order we want them in
1348         
1349         self.columns=[self.importmappingdict[k] for k in want]
1350         # deal with serials
1351         self.columns.append("UniqueSerial-FolderID")
1352         self.columns.append("UniqueSerial-sourcetype")
1353         moredata=[ self.outlook.getfolderid(self.folder), "outlook"]
1354 
1355         # build up data
1356         self.data=[]
1357         for item in items:
1358             row=[]
1359             for k in want:
1360                 v=item.get(k, None)
1361                 v=common.strorunicode(v)
1362                 row.append(v)
1363             self.data.append(row+moredata)
1364 
1365 
1366 class ImportVCardDialog(ImportDialog):
1367     keymapper={
1368         "name": "Name",
1369         "notes": "Notes",
1370         "uid": "UniqueSerial-uid",
1371         "last name": "Last Name",
1372         "first name": "First Name",
1373         "middle name": "Middle Name",
1374         "nickname": "Nickname",
1375         "categories": "Categories",
1376         "email": "Email Address",
1377         "url": "Web Page",
1378         "phone": "Phone",
1379         "address": "Address",
1380         "organisation": "Company",
1381         "wallpapers": "Wallpapers",
1382         "ringtones": "Ringtones"
1383         }
1384     def __init__(self, filename, parent, id, title):
1385         self.headerrowiseditable=False
1386         self.filename=filename
1387         self.vcardcolumns,self.vcarddata=None,None
1388         ImportDialog.__init__(self, parent, id, title)
1389 
1390     def gethtmlhelp(self):
1391         "Returns tuple of help text and size"
1392         bg=self.GetBackgroundColour()
1393         return '<html><body BGCOLOR="#%02X%02X%02X">Importing vCard Contacts.  Verify the data and perform any filtering necessary.</body></html>' % (bg.Red(), bg.Green(), bg.Blue()), \
1394                 (600,30)
1395 
1396     def getcontrols(self, vbs):
1397         # no extra controls
1398         return
1399 
1400     def ReReadData(self):
1401         if self.vcardcolumns is None or self.vcarddata is None:
1402                 self.vcardcolumns,self.vcarddata=self.parsevcards(common.opentextfile(self.filename))
1403         self.columns=self.vcardcolumns
1404         self.data=self.vcarddata
1405 
1406     def parsevcards(self, file):
1407         # returns columns, data
1408         data=[]
1409         keys={}
1410         for vc in vcard.VCards(vcard.VFile(file)):
1411             v=vc.getdata()
1412             data.append(v)
1413             for k in v: keys[k]=1
1414         keys=keys.keys()
1415         # sort them into a natural order
1416         self.sortkeys(keys)
1417         # remove the ones we have no mapping for
1418         if __debug__:
1419             for k in keys:
1420                 if _getstringbase(k)[0] not in self.keymapper:
1421                     print "vcard import: no map for key "+k
1422         keys=[k for k in keys if _getstringbase(k)[0] in self.keymapper]
1423         columns=[self.keymapper[_getstringbase(k)[0]] for k in keys]
1424         # build up defaults
1425         defaults=[]
1426         for c in columns:
1427             if c in self.possiblecolumns:
1428                 defaults.append("")
1429             else:
1430                 defaults.append(None)
1431         # deal with serial/UniqueId
1432         if len([c for c in columns if c.startswith("UniqueSerial-")]):
1433             columns.append("UniqueSerial-sourcetype")
1434             extra=["vcard"]
1435         else:
1436             extra=[]
1437         # do some data munging
1438         newdata=[]
1439         for row in data:
1440             line=[]
1441             for i,k in enumerate(keys):
1442                 line.append(row.get(k, defaults[i]))
1443             newdata.append(line+extra)
1444 
1445         # return our hard work
1446         return columns, newdata
1447 
1448     # name parts, name, nick, emails, urls, phone, addresses, categories, memos
1449     # things we ignore: title, prefix, suffix, organisational unit
1450     _preferredorder=["first name", "middle name", "last name", "name", "nickname",
1451                      "phone", "address", "email", "url", "organisation", "categories", "notes"]
1452 
1453     def sortkeys(self, keys):
1454         po=self._preferredorder
1455 
1456         def funkycmpfunc(x, y, po=po):
1457             x=_getstringbase(x)
1458             y=_getstringbase(y)
1459             if x==y: return 0
1460             if x[0]==y[0]: # if the same base, use the number to compare
1461                 return cmp(x[1], y[1])
1462 
1463             # find them in the preferred order list
1464             # (for some bizarre reason python doesn't have a method corresponding to
1465             # string.find on lists or tuples, and you only get index on lists
1466             # which throws an exception on not finding the item
1467             try:
1468                 pos1=po.index(x[0])
1469             except ValueError: pos1=-1
1470             try:
1471                 pos2=po.index(y[0])
1472             except ValueError: pos2=-1
1473 
1474             if pos1<0 and pos2<0:   return cmp(x[0], y[0])
1475             if pos1<0 and pos2>=0:  return 1
1476             if pos2<0 and pos1>=0:  return -1
1477             assert pos1>=0 and pos2>=0
1478             return cmp(pos1, pos2)
1479 
1480         keys.sort(funkycmpfunc)
1481 
1482 
1483 def _getstringbase(v):
1484     mo=re.match(r"^(.*?)(\d+)$", v)
1485     if mo is None: return (v,1)
1486     return mo.group(1), int(mo.group(2))
1487 
1488 class ImportEvolutionDialog(ImportVCardDialog):
1489     def __init__(self, parent, id, title, evolution):
1490         self.headerrowiseditable=False
1491         self.evolution=evolution
1492         self.evocolumns=None
1493         self.evodata=None
1494         ImportDialog.__init__(self, parent, id, title)
1495 
1496     def gethtmlhelp(self):
1497         "Returns tuple of help text and size"
1498         bg=self.GetBackgroundColour()
1499         return '<html><body BGCOLOR="#%02X%02X%02X">Importing Evolution Contacts.  Select the folder to import, and do any filtering necessary.</body></html>' % (bg.Red(), bg.Green(), bg.Blue()), \
1500                 (600,30)
1501 
1502     def getcontrols(self, vbs):
1503         hbs=wx.BoxSizer(wx.HORIZONTAL)
1504         # label
1505         hbs.Add(wx.StaticText(self, -1, "Folder"), 0, wx.ALL|wx.ALIGN_CENTRE, 2)
1506         # where the folder name goes
1507         self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY)
1508         hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2)
1509         # browse button
1510         self.folderbrowse=wx.Button(self, wx.NewId(), "Browse ...")
1511         hbs.Add(self.folderbrowse, 0, wx.EXPAND|wx.ALL, 2)
1512         vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
1513         wx.EVT_BUTTON(self, self.folderbrowse.GetId(), self.OnBrowse)
1514 
1515         # sort out folder
1516         id=wx.GetApp().config.Read("evolution/contacts", "")
1517         self.folder=self.evolution.getfolderfromid(id, True)
1518         print "folder is",self.folder
1519         wx.GetApp().config.Write("evolution/contacts", self.evolution.getfolderid(self.folder))
1520         self.folderctrl.SetValue(self.evolution.getfoldername(self.folder))
1521 
1522     def OnBrowse(self, _):
1523         p=self.evolution.pickfolder(self.folder)
1524         if p is None: return # user hit cancel
1525         self.folder=p
1526         wx.GetApp().config.Write("evolution/contacts", self.evolution.getfolderid(self.folder))
1527         self.folderctrl.SetValue(self.evolution.getfoldername(self.folder))
1528         self.evocolumns=None
1529         self.evodata=None
1530         self.DataNeedsUpdate()
1531 
1532     def ReReadData(self):
1533         if self.evocolumns is not None and self.evodata is not None:
1534             self.columns=self.evocolumns
1535             self.data=self.evodata
1536             return
1537 
1538         vcards="\r\n".join(self.evolution.getcontacts(self.folder))
1539 
1540         columns,data=self.parsevcards(StringIO.StringIO(vcards))
1541 
1542         columns.append("UniqueSerial-folderid")
1543         columns.append("UniqueSerial-sourcetype")
1544         moredata=[self.folder, "evolution"]
1545 
1546         for row in data:
1547             row.extend(moredata)
1548 
1549         self.evocolumns=self.columns=columns
1550         self.evodata=self.data=data
1551 
1552 class ImportQtopiaDesktopDialog(ImportDialog):
1553     # the order of this mapping matters ....
1554     importmapping=(
1555         # first column is field in Qtopia
1556         # second column is field in dialog (ImportDialog.possiblecolumns)
1557            ('FirstName', "First Name"  ),
1558            ('LastName',  "Last Name" ),
1559            ('MiddleName',  "Middle Name"),
1560            ('Nickname',   "Nickname"),
1561            ('Emails',   "Email Addresses"),
1562            ('HomeStreet',   "Home Street"),
1563            ('HomeCity',   "Home City"),
1564            ('HomeZip',   "Home Postal Code"),
1565            ('HomeState',  "Home State" ),
1566            ('HomeCountry',  "Home Country/Region" ),
1567            ('HomePhone',  "Home Phone" ),
1568            ('HomeFax',  "Home Fax" ),
1569            ('HomeMobile', "Mobile Phone"  ),
1570            ('BusinessMobile', "Mobile Phone"  ),
1571            ('HomeWebPage',  "Home Web Page" ),
1572            ('BusinessStreet',   "Business Street"),
1573            ('BusinessCity',  "Business City" ),
1574            ('BusinessZip',  "Business Postal Code" ),
1575            ('BusinessState',  "Business State" ),
1576            ('BusinessCountry',  "Business Country/Region", ),
1577            ('BusinessWebPage',   "Business Web Page"),
1578            ('BusinessPhone',   "Business Phone"),
1579            ('BusinessFax',  "Business Fax" ),
1580            ('BusinessPager', "Pager"  ),
1581            ('Company',  "Company" ),
1582            ('Notes',  "Notes" ),
1583            ('Categories',  "Categories" ),
1584            ('Uid',  "UniqueSerial-uid" ),
1585            
1586            )           
1587 
1588 ##    # the fields we ignore
1589         
1590 ##           ('Assistant',   )
1591 ##           ('Children',   )
1592 ##           ('DefaultEmail',   )
1593 ##           ('Department',   )
1594 ##           ('Dtmid',   )
1595 ##           ('FileAs',   )
1596 ##           ('Gender',   )
1597 ##           ('JobTitle',   )
1598 ##           ('Manager',   )
1599 ##           ('Office',   )
1600 ##           ('Profession',   )
1601 ##           ('Spouse',   )
1602 ##           ('Suffix',   )
1603 ##           ('Title',   )
1604 
1605     importmappingdict={}
1606     for o,i in importmapping: importmappingdict[o]=i
1607 
1608     def __init__(self, parent, id, title):
1609         self.headerrowiseditable=False
1610         self.origcolumns=self.origdata=None
1611         ImportDialog.__init__(self, parent, id, title)
1612 
1613     def gethtmlhelp(self):
1614         "Returns tuple of help text and size"
1615         bg=self.GetBackgroundColour()
1616         return '<html><body BGCOLOR="#%02X%02X%02X">Importing Qtopia Desktop Contacts..</body></html>' % (bg.Red(), bg.Green(), bg.Blue()), \
1617                 (600,30)
1618 
1619     def getcontrols(self, vbs):
1620         pass
1621 
1622     def ReReadData(self):
1623         if self.origcolumns is not None and self.origdata is not None:
1624             self.columns=self.origcolumns
1625             self.data=self.origdata
1626             return
1627 
1628         import native.qtopiadesktop
1629 
1630         filename=native.qtopiadesktop.getfilename()
1631         if not os.path.isfile(filename):
1632             wx.MessageBox(filename+" not found.", "Qtopia file not found", wx.ICON_EXCLAMATION|wx.OK)
1633             self.data={}
1634             self.columns=[]
1635             return
1636 
1637         items=native.qtopiadesktop.getcontacts()
1638         
1639         # work out what keys are actually present
1640         keys={}
1641         for item in items:
1642             for k in item.keys():
1643                 keys[k]=1
1644 
1645         # We now need to produce columns with BitPim names not the Qtopia ones.
1646         # mappings are in self.importmapping
1647         want=[]
1648         for o,i in self.importmapping:
1649             if o in keys.keys():
1650                 want.append(o)
1651         # want now contains list of Qtopia keys we want, and the order we want them in
1652         
1653         self.columns=[self.importmappingdict[k] for k in want]
1654         # deal with serials
1655         self.columns.append("UniqueSerial-sourcetype")
1656         moredata=[ "qtopiadesktop"]
1657 
1658         # build up data
1659         self.data=[]
1660         for item in items:
1661             row=[]
1662             for k in want:
1663                 v=item.get(k, None)
1664                 row.append(v)
1665             self.data.append(row+moredata)
1666 
1667         self.origdata=self.data
1668         self.origcolumns=self.columns
1669 
1670 # The eGroupware login handling is a bit of a mess.  Feel free to fix it.
1671 
1672 class eGroupwareLoginDialog(wx.Dialog):
1673 
1674     __pwdsentinel="\x01\x02\x01\x09\x01\x01\x14\x15"
1675 
1676 
1677     def __init__(self, parent, module, title="Login to eGroupware"):
1678         wx.Dialog.__init__(self, parent, -1,  title, size=(400,-1))
1679         self.module=module
1680         gs=wx.GridBagSizer(5,5)
1681         for row,label in enumerate( ("URL", "Domain", "Username", "Password") ):
1682             gs.Add(wx.StaticText(self, -1, label), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL, pos=(row,0))
1683         self.curl=wx.TextCtrl(self, -1)
1684         self.cdomain=wx.TextCtrl(self, -1)
1685         self.cuser=wx.TextCtrl(self, -1)
1686         self.cpassword=wx.TextCtrl(self, -1, style=wx.TE_PASSWORD)
1687         self.csavepassword=wx.CheckBox(self, -1, "Save")
1688         for row,widget in enumerate( (self.curl, self.cdomain, self.cuser) ):
1689             gs.Add(widget, flag=wx.EXPAND, pos=(row,1), span=(1,2))
1690         gs.Add(self.cpassword, flag=wx.EXPAND, pos=(3,1))
1691         gs.Add(self.csavepassword, flag=wx.ALIGN_CENTRE, pos=(3,2))
1692         gs.AddGrowableCol(1)
1693         self.cmessage=wx.StaticText(self, -1, "Please enter your details")
1694         gs.Add(self.cmessage, flag=wx.EXPAND, pos=(4,0), span=(1,3))
1695         vbs=wx.BoxSizer(wx.VERTICAL)
1696         vbs.Add(gs, 0, wx.EXPAND|wx.ALL,5)
1697         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5)
1698         vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5)
1699 
1700         # set initial values
1701         cfg=wx.GetApp().config
1702         self.curl.SetValue(cfg.Read("egroupware/url", "http://server.example.com/egroupware"))
1703         self.cdomain.SetValue(cfg.Read("egroupware/domain", "default"))
1704         try:
1705             import getpass
1706             defuser=getpass.getuser()
1707         except:
1708             defuser="user"
1709         self.cuser.SetValue(cfg.Read("egroupware/user", defuser))
1710         p=cfg.Read("egroupware/password", "")
1711         if len(p):
1712             self.csavepassword.SetValue(True)
1713             self.cpassword.SetValue(self.__pwdsentinel)
1714     
1715         self.SetSizer(vbs)
1716         self.SetAutoLayout(True)
1717         vbs.Fit(self)
1718         # make the window a decent width
1719         self.SetDimensions(-1, -1, 500, -1, wx.SIZE_USE_EXISTING)
1720         wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
1721         self.session=None
1722 
1723     def OnOk(self, evt):
1724         try:
1725             self.session=self._GetSession()
1726             evt.Skip() # end modal
1727             self.OnClose()
1728             return
1729         except Exception,e:
1730             self.cmessage.SetLabel(str(e))
1731             # go around again
1732 
1733     def GetSession(self, auto=False):
1734         """Returns the Session object from the eGroupware library
1735         @param auto: If true then the user interface doesn't have to be shown"""
1736 
1737         if auto:
1738             try:
1739                 self.session=self._GetSession()
1740                 return self.session
1741             except Exception,e:
1742                 self.cmessage.SetLabel(str(e))
1743 
1744         self.ShowModal()
1745         return self.session
1746 
1747     def _GetSession(self):
1748         # lets see if the user has given us sensible params
1749         if self.curl.GetValue()=="http://server.example.com/egroupware":
1750             raise Exception("You must set the URL for the server")
1751         if len(self.cpassword.GetValue())==0:
1752             raise Exception("You must fill in the password")
1753         password=self.cpassword.GetValue()
1754         if password==self.__pwdsentinel:
1755             password=common.obfus_decode(wx.GetApp().config.Read("egroupware/password", ""))
1756         try:
1757             return self.module.getsession(self.curl.GetValue(), self.cuser.GetValue(), password, self.cdomain.GetValue())
1758         finally:
1759             del password
1760 
1761     def OnClose(self, event=None):
1762         cfg=wx.GetApp().config
1763         cfg.Write("egroupware/url", self.curl.GetValue())
1764         cfg.Write("egroupware/domain", self.cdomain.GetValue())
1765         cfg.Write("egroupware/user", self.cuser.GetValue())
1766         if self.csavepassword.GetValue():
1767             p=self.cpassword.GetValue()
1768             if p!=self.__pwdsentinel:
1769                 cfg.Write("egroupware/password", common.obfus_encode(p))
1770         else:
1771             cfg.DeleteEntry("egroupware/password")
1772 
1773 
1774                 
1775 class ImporteGroupwareDialog(ImportDialog):
1776 
1777     ID_CHANGE=wx.NewId()
1778 
1779     def __init__(self, parent, id, title, module):
1780         self.headerrowiseditable=False
1781         self.module=module
1782         ImportDialog.__init__(self, parent, id, title)
1783         self.sp=None
1784 
1785     def gethtmlhelp(self):
1786         "Returns tuple of help text and size"
1787         bg=self.GetBackgroundColour()
1788         return '<html><body BGCOLOR="#%02X%02X%02X">Importing eGroupware Contacts..</body></html>' % (bg.Red(), bg.Green(), bg.Blue()), \
1789                 (600,30)
1790 
1791     def getcontrols(self, vbs):
1792         # need url, username, password and domain fields
1793         hbs=wx.BoxSizer(wx.HORIZONTAL)
1794         hbs.Add(wx.StaticText(self, -1, "URL"), 0, wx.ALIGN_CENTRE|wx.ALL,2)
1795         self.curl=wx.StaticText(self, -1)
1796         hbs.Add(self.curl, 3, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2)
1797         hbs.Add(wx.StaticText(self, -1, "Domain"), 0, wx.ALIGN_CENTRE|wx.ALL,2)
1798         self.cdomain=wx.StaticText(self, -1)
1799         hbs.Add(self.cdomain, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2)
1800         hbs.Add(wx.StaticText(self, -1, "User"), 0, wx.ALIGN_CENTRE|wx.ALL,2)
1801         self.cuser=wx.StaticText(self, -1)
1802         hbs.Add(self.cuser, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2)
1803         self.cchange=wx.Button(self, self.ID_CHANGE, "Change ...")
1804         hbs.Add(self.cchange, 0, wx.ALL, 2)
1805         vbs.Add(hbs,0,wx.ALL,5)
1806         wx.EVT_BUTTON(self, self.ID_CHANGE, self.OnChangeCreds)
1807         self.setcreds()
1808 
1809     def OnChangeCreds(self,_):
1810         dlg=eGroupwareLoginDialog(self, self.module)
1811         newsp=dlg.GetSession()
1812         if newsp is not None:
1813             self.sp=newsp
1814             self.setcreds()
1815             self.DataNeedsUpdate()
1816 
1817     def setcreds(self):
1818         cfg=wx.GetApp().config
1819         self.curl.SetLabel(cfg.Read("egroupware/url", "http://server.example.com/egroupware"))
1820         self.cdomain.SetLabel(cfg.Read("egroupware/domain", "default"))
1821         try:
1822             import getpass
1823             defuser=getpass.getuser()
1824         except:
1825             defuser="user"
1826         self.cuser.SetLabel(cfg.Read("egroupware/user", defuser))        
1827 
1828     _preferred=( "Name", "First Name", "Middle Name", "Last Name",
1829                  "Address", "Address2", "Email Address", "Email Address2",
1830                  "Home Phone", "Mobile Phone", "Business Fax", "Pager", "Business Phone",
1831                  "Notes", "Business Web Page", "Categories" )
1832 
1833     def ReReadData(self):
1834         if self.sp is None:
1835             self.sp=eGroupwareLoginDialog(self, self.module).GetSession(auto=True)
1836             self.setcreds()
1837         self.data=[]
1838         self.columns=[]
1839         if self.sp is None:
1840             self.EndModal(wx.ID_CANCEL)
1841             return
1842         # work out what columns we have
1843         entries=[i for i in self.sp.getcontactspbformat()]
1844         cols=[]
1845         for e in entries:
1846             for k in e:
1847                 if k not in cols:
1848                     cols.append(k)
1849         # now put columns in prefered order
1850         cols.sort()
1851         self.columns=[]
1852         for p in self._preferred:
1853             if p in cols:
1854                 self.columns.append(p)
1855         cols=[c for c in cols if c not in self.columns]
1856         self.columns.extend(cols)
1857         # make data
1858         self.data=[]
1859         for e in entries:
1860             self.data.append([e.get(c,None) for c in self.columns])
1861         # strip trailing 2 off names in columns
1862         for i,c in enumerate(self.columns):
1863             if c.endswith("2"):
1864                 self.columns[i]=c[:-1]
1865 
1866 
1867 def OnFileImportCSVContacts(parent):
1868     with guihelper.WXDialogWrapper(wx.FileDialog(parent, "Import CSV file",
1869                                                  wildcard="CSV files (*.csv)|*.csv|Tab Separated file (*.tsv)|*.tsv|All files|*",
1870                                                  style=wx.OPEN|wx.HIDE_READONLY|wx.CHANGE_DIR),
1871                                    True) as (dlg, retcode):
1872         if retcode==wx.ID_OK:
1873             path=dlg.GetPath()
1874         else:
1875             return
1876 
1877     with guihelper.WXDialogWrapper(ImportCSVDialog(path, parent, -1, "Import CSV file"),
1878                                    True) as (dlg, retcode):
1879         if retcode==wx.ID_OK:
1880             data=dlg.GetFormattedData()
1881             if data is not None:
1882                 parent.GetActivePhonebookWidget().importdata(data, merge=dlg.merge)
1883 
1884 def OnFileImportVCards(parent):
1885     with guihelper.WXDialogWrapper(wx.FileDialog(parent, "Import vCards file",
1886                                                  wildcard="vCard files (*.vcf)|*.vcf|All files|*",
1887                                                  style=wx.OPEN|wx.HIDE_READONLY|wx.CHANGE_DIR),
1888                                    True) as (dlg, retcode):
1889         if retcode==wx.ID_OK:
1890             path=dlg.GetPath()
1891         else:
1892             return
1893 
1894     with guihelper.WXDialogWrapper(ImportVCardDialog(path, parent, -1, "Import vCard file"),
1895                                    True) as (dlg, retcode):
1896         if retcode==wx.ID_OK:
1897             data=dlg.GetFormattedData()
1898             if data is not None:
1899                 parent.GetActivePhonebookWidget().importdata(data, merge=dlg.merge)
1900 
1901 def OnFileImportQtopiaDesktopContacts(parent):
1902     with guihelper.WXDialogWrapper(ImportQtopiaDesktopDialog(parent, -1, "Import Qtopia Desktop Contacts"),
1903                                    True) as (dlg, retcode):
1904         if retcode==wx.ID_OK:
1905             data=dlg.GetFormattedData()
1906             if data is not None:
1907                 parent.GetActivePhonebookWidget().importdata(data, merge=dlg.merge)
1908         
1909 def OnFileImportOutlookContacts(parent):
1910     import native.outlook
1911     if not TestOutlookIsInstalled():
1912         return
1913     with guihelper.WXDialogWrapper(ImportOutlookDialog(parent, -1, "Import Outlook Contacts", native.outlook),
1914                                    True) as (dlg, retcode):
1915         if retcode==wx.ID_OK:
1916             data=dlg.GetFormattedData()
1917             if data is not None:
1918                 parent.GetActivePhonebookWidget().importdata(data, merge=dlg.merge)
1919     native.outlook.releaseoutlook()
1920 
1921 def OnFileImportEvolutionContacts(parent):
1922     import native.evolution
1923     with guihelper.WXDialogWrapper(ImportEvolutionDialog(parent, -1, "Import Evolution Contacts", native.evolution),
1924                                    True) as (dlg, retcode):
1925         if retcode==wx.ID_OK:
1926             data=dlg.GetFormattedData()
1927             if data is not None:
1928                 parent.GetActivePhonebookWidget().importdata(data, merge=dlg.merge)
1929 
1930 def OnFileImporteGroupwareContacts(parent):
1931     import native.egroupware
1932     with guihelper.WXDialogWrapper(ImporteGroupwareDialog(parent, -1, "Import eGroupware Contacts", native.egroupware),
1933                                    True) as (dlg, retcode):
1934         if retcode==wx.ID_OK:
1935             data=dlg.GetFormattedData()
1936             if data is not None:
1937                 parent.GetActivePhonebookWidget().importdata(data, merge=dlg.merge)
1938 
1939 def OnFileImportCommon(parent, dlg_class, dlg_title, widget, dict_key):
1940     with guihelper.WXDialogWrapper(dlg_class(parent, -1, dlg_title),
1941                                    True) as (dlg, res):
1942         if res==wx.ID_OK:
1943             pubsub.publish(pubsub.MERGE_CATEGORIES,
1944                            dlg.get_categories()[:])
1945             # and save the new data
1946             data_dict={ dict_key: dlg.get() }
1947             widget.populate(data_dict)
1948             widget.populatefs(data_dict)
1949         elif res==dlg_class.ID_ADD:
1950             # ask phonebook to merge our categories
1951             pubsub.publish(pubsub.MERGE_CATEGORIES,
1952                            dlg.get_categories()[:])
1953             # get existing data
1954             data_res=widget.getdata({}).get(dict_key, {})
1955             # and add the new imported data
1956             data_res.update(dlg.get())
1957             data_dict={ dict_key: data_res }
1958             # and save it
1959             widget.populate(data_dict)
1960             widget.populatefs(data_dict)
1961         elif res==dlg_class.ID_MERGE:
1962             # ask phonebook to merge our categories
1963             pubsub.publish(pubsub.MERGE_CATEGORIES,
1964                            dlg.get_categories()[:])
1965             # and merge the data
1966             widget.mergedata({ dict_key: dlg.get() })
1967 
1968 def OnFileImportOutlookCalendar(parent):
1969     import native.outlook
1970     if not TestOutlookIsInstalled():
1971         return
1972     import outlook_calendar
1973     import pubsub
1974     OnFileImportCommon(parent, outlook_calendar.OutlookImportCalDialog,
1975                        'Import Outlook Calendar', parent.GetActiveCalendarWidget(),
1976                        'calendar')
1977     native.outlook.releaseoutlook()
1978 
1979 def OnCalendarWizard(parent):
1980     import imp_cal_wizard
1981     OnFileImportCommon(parent, imp_cal_wizard.ImportCalendarWizard,
1982                        'Import Calendar Wizard',
1983                        parent.GetActiveCalendarWidget(),
1984                        'calendar')
1985 
1986 def OnCalendarPreset(parent):
1987     import imp_cal_preset
1988     OnFileImportCommon(parent, imp_cal_preset.ImportCalendarPresetDialog,
1989                        'Import Calendar Preset',
1990                        parent.GetActiveCalendarWidget(), 'calendar')
1991 
1992 def OnFileImportOutlookNotes(parent):
1993     import native.outlook
1994     if not TestOutlookIsInstalled():
1995         return
1996     import outlook_notes
1997     OnFileImportCommon(parent, outlook_notes.OutlookImportNotesDialog,
1998                        'Import Outlook Notes', parent.GetActiveMemoWidget(),
1999                        'memo')
2000     native.outlook.releaseoutlook()
2001 
2002 def OnFileImportOutlookTasks(parent):
2003     import native.outlook
2004     if not TestOutlookIsInstalled():
2005         return
2006     import outlook_tasks
2007     OnFileImportCommon(parent, outlook_tasks.OutlookImportTasksDialog,
2008                        'Import Outlook Tasks', parent.GetActiveTodoWidget(),
2009                        'todo')
2010     native.outlook.releaseoutlook()
2011 
2012 def OnFileImportVCal(parent):
2013     OnFileImportCommon(parent, vcal_calendar.VcalImportCalDialog,
2014                        'Import vCalendar', parent.GetActiveCalendarWidget(),
2015                        'calendar')
2016 
2017 def OnFileImportiCal(parent):
2018     OnFileImportCommon(parent, ical_calendar.iCalImportCalDialog,
2019                        'Import iCalendar', parent.GetActiveCalendarWidget(),
2020                        'calendar')
2021 
2022 def OnFileImportgCal(parent):
2023     OnFileImportCommon(parent, gcal.gCalImportDialog,
2024                        'Import Google Calendar',
2025                        parent.GetActiveCalendarWidget(),
2026                        'calendar')
2027 
2028 def OnFileImportCSVCalendar(parent):
2029     OnFileImportCommon(parent, csv_calendar.CSVImportDialog,
2030                        'Import CSV Calendar', parent.GetActiveCalendarWidget(),
2031                        'calendar')
2032 
2033 ###
2034 ###   AUTO_SYNC
2035 ###
2036 
2037 def AutoConfOutlookCalender(parent, folder, filters):
2038     import native.outlook
2039     if not TestOutlookIsInstalled():
2040         return None, None
2041     import outlook_calendar
2042     config=()
2043     dlg=outlook_calendar.OutlookAutoConfCalDialog(parent, -1,
2044                                                  'Config Outlook Calendar AutoSync Settings',
2045                                                  folder, filters)
2046     return AutoConfCommon(dlg)
2047 
2048 def AutoConfCSVCalender(parent, folder, filters):
2049     import csv_calendar
2050     config=()
2051     dlg=csv_calendar.CSVAutoConfCalDialog(parent, -1,
2052                                                  'Config CSV Calendar AutoSync Settings',
2053                                                  folder, filters)
2054     return AutoConfCommon(dlg)
2055 
2056 def AutoConfVCal(parent, folder, filters):
2057     import vcal_calendar
2058     dlg=vcal_calendar.VCalAutoConfCalDialog(parent, -1,
2059                                                  'Config VCal AutoSync Settings',
2060                                                  folder, filters)
2061     return AutoConfCommon(dlg)
2062 
2063 def AutoConfCommon(dlg):
2064     with guihelper.WXDialogWrapper(dlg, True) as (dlg, res):
2065         if res==wx.ID_OK:
2066             config=(dlg.GetFolder(), dlg.GetFilter())
2067         else:
2068             config=()
2069     return res, config
2070 
2071 def AutoImportOutlookCalendar(parent, folder, filters):
2072     import native.outlook
2073     if not TestOutlookIsInstalled():
2074         return 0
2075     import outlook_calendar
2076     calendar_r=outlook_calendar.ImportCal(folder, filters)
2077     return AutoImportCalCommon(parent, calendar_r)
2078 
2079 def AutoImportVCal(parent, folder, filters):
2080     import vcal_calendar
2081     calendar_r=vcal_calendar.ImportCal(folder, filters)
2082     return AutoImportCalCommon(parent, calendar_r)
2083 
2084 def AutoImportCSVCalendar(parent, folder, filters):
2085     import csv_calendar
2086     calendar_r=csv_calendar.ImportCal(folder, filters)
2087     return AutoImportCalCommon(parent, calendar_r)
2088 
2089 def AutoImportCalCommon(parent, calendar_r):
2090     parent.calendarwidget.populate(calendar_r)
2091     parent.calendarwidget.populatefs(calendar_r)
2092     res=1
2093     return res
2094 
2095 def OnAutoCalImportSettings(parent):
2096     pass
2097 
2098 def OnAutoCalImportExecute(parent):
2099     pass
2100 
2101 # Play list
2102 def OnWPLImport(parent):
2103     # get the wpl file name
2104     with guihelper.WXDialogWrapper(wx.FileDialog(parent, "Import wpl file",
2105                                                  wildcard="wpl files (*.wpl)|*.wpl|All files|*",
2106                                                  style=wx.OPEN|wx.HIDE_READONLY|wx.CHANGE_DIR),
2107                                    True) as (_dlg, _retcode):
2108         # parse & retrieve the data
2109         if _retcode==wx.ID_OK:
2110             _wpl=wpl_file.WPL(filename=_dlg.GetPath())
2111             if not _wpl.title:
2112                 return
2113             _pl_entry=playlist.PlaylistEntry()
2114             _pl_entry.name=_wpl.title
2115             _pl_entry.songs=[common.basename(x) for x in _wpl.songs]
2116             # populate the new data
2117             _widget=parent.GetActivePlaylistWidget()
2118             _pl_data={}
2119             _widget.getdata(_pl_data)
2120             _pl_data[playlist.playlist_key].append(_pl_entry)
2121             _widget.populate(_pl_data)
2122             _widget.populatefs(_pl_data)
2123 
2124 ###
2125 ###   EXPORTS
2126 ###
2127 
2128 def GetPhonebookExports():
2129     res=[]
2130     # Vcards - always possible
2131     res.append( (guihelper.ID_EXPORT_VCARD_CONTACTS, "vCards...", "Export the phonebook to vCards", OnFileExportVCards) )
2132     # eGroupware - always possible
2133     res.append( (guihelper.ID_EXPORT_GROUPWARE_CONTACTS, "eGroupware...", "Export the phonebook to eGroupware", OnFileExporteGroupware) )
2134     # CSV - always possible
2135     res.append( (guihelper.ID_EXPORT_CSV_CONTACTS, 'CSV Contacts...', 'Export the phonebook to CSV', OnFileExportCSV))
2136     res.append( (guihelper.ID_EXPORT_CSV_CALENDAR, 'CSV Calendar...', 'Export the calendar to CSV', OnFileExportCSVCalendar) )
2137     # iCal
2138     res.append( (guihelper.ID_EXPORT_ICALENDAR,
2139                  'iCalendar...',
2140                  'Export the calendar to iCalendar',
2141                  OnFileExportiCalendar) )
2142     # SMS - always possible
2143     res.append( (guihelper.ID_EXPORT_SMS, 'SMS...', 'Export SMS Messages', OnFileExportSMS))
2144     # Call History - always possible
2145     res.append( (guihelper.ID_EXPORT_CSV_CALL_HISTORY, 'CSV Call History...', 'Export Call History to CSV',
2146                  OnFileExportCallHistory))
2147     # Media - always possible
2148     res.append( (guihelper.ID_EXPORT_MEDIA_TO_DIR, 'Media to Folder...', 'Export Media Files to a Folder on your computer',
2149                  OnFileExportMediaDir))
2150     res.append( (guihelper.ID_EXPORT_MEDIA_TO_ZIP, 'Media to Zip File...', 'Export Media Files to a Zip file',
2151                  OnFileExportMediaZip))
2152     return res
2153 
2154 class BaseExportDialog(wx.Dialog):
2155 
2156     def __init__(self, parent, title, style=wx.CAPTION|wx.MAXIMIZE_BOX|\
2157              wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE):
2158         wx.Dialog.__init__(self, parent, id=-1, title=title, style=style)
2159         self._phonebook_module=parent.GetActivePhonebookWidget()
2160 
2161     def GetSelectionGui(self, parent):
2162         "Returns a sizer containing the gui for selecting which items and which fields are exported"
2163         hbs=wx.BoxSizer(wx.HORIZONTAL)
2164 
2165         lsel=len(self._phonebook_module.GetSelectedRows())
2166         lall=len(self._phonebook_module._data)
2167         rbs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Rows"), wx.VERTICAL)
2168         self.rows_selected=wx.RadioButton(self, wx.NewId(), "Selected (%d)" % (lsel,), style=wx.RB_GROUP)
2169         self.rows_all=wx.RadioButton(self, wx.NewId(), "All (%d)" % (lall,))
2170         rbs.Add(self.rows_selected, 0, wx.EXPAND|wx.ALL, 2)
2171         rbs.Add(self.rows_all, 0, wx.EXPAND|wx.ALL, 2)
2172         hbs.Add(rbs, 3, wx.EXPAND|wx.ALL, 5)
2173         self.rows_selected.SetValue(lsel>1)
2174         self.rows_all.SetValue(not lsel>1)
2175 
2176         vbs2=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Fields"), wx.VERTICAL)
2177         cb=[]
2178         for c in ("Everything", "Phone Numbers", "Addresses", "Email Addresses"):
2179             cb.append(wx.CheckBox(self, -1, c))
2180             vbs2.Add(cb[-1], 0, wx.EXPAND|wx.ALL, 5)
2181 
2182         for c in cb:
2183             c.Enable(False)
2184         cb[0].SetValue(True)
2185 
2186         hbs.Add(vbs2, 4, wx.EXPAND|wx.ALL, 5)
2187 
2188         return hbs
2189 
2190     def GetPhoneBookItems(self, includecount=False):
2191         """Returns each item in the phonebook based on the settings
2192         for all vs selected.  The fields are also trimmed to match
2193         what the user requested.
2194 
2195         @param includecount:  If this is true then the return is
2196                  a tuple of (item, number, max) and you can use
2197                  number and max to update a progress bar.  Note
2198                  that some items may be skipped (eg if the user
2199                  only wants records with phone numbers and some
2200                  records don't have them)
2201         """
2202         
2203         data=self._phonebook_module._data
2204         if self.rows_all.GetValue():
2205             rowkeys=data.keys()
2206         else:
2207             rowkeys=self._phonebook_module.GetSelectedRowKeys()
2208         for i,k in enumerate(rowkeys[:]): # we use a dup of rowkeys since it can be altered while exporting
2209             # ::TODO:: look at fields setting
2210             if includecount:
2211                 yield data[k],i,len(rowkeys)
2212             else:
2213                 yield data[k]
2214 
2215 class ExportVCardDialog(BaseExportDialog):
2216 
2217     dialects=vcard.profiles.keys()
2218     dialects.sort()
2219     default_dialect='vcard2'
2220 
2221     def __init__(self, parent, title):
2222         BaseExportDialog.__init__(self, parent, title)
2223         # make the ui
2224         
2225         vbs=wx.BoxSizer(wx.VERTICAL)
2226 
2227         bs=wx.BoxSizer(wx.HORIZONTAL)
2228 
2229         bs.Add(wx.StaticText(self, -1, "File"), 0, wx.ALL|wx.ALIGN_CENTRE, 5)
2230         self.filenamectrl=wx.TextCtrl(self, -1, wx.GetApp().config.Read("vcard/export-file", "bitpim.vcf")) 
2231         bs.Add(self.filenamectrl, 1, wx.ALL|wx.EXPAND, 5)
2232         self.browsectrl=wx.Button(self, wx.NewId(), "Browse...")
2233         bs.Add(self.browsectrl, 0, wx.ALL|wx.EXPAND, 5)
2234         wx.EVT_BUTTON(self, self.browsectrl.GetId(), self.OnBrowse)
2235 
2236         vbs.Add(bs, 0, wx.EXPAND|wx.ALL, 5)
2237 
2238         hbs2=wx.BoxSizer(wx.HORIZONTAL)
2239 
2240         # dialects
2241         hbs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Dialect"), wx.VERTICAL)
2242         self.dialectctrl=wx.ListBox(self, -1, style=wx.LB_SINGLE, choices=[vcard.profiles[d]['description'] for d in self.dialects])
2243         default=wx.GetApp().config.Read("vcard/export-format", self.default_dialect)
2244         if default not in self.dialects: default=self.default_dialect
2245         self.dialectctrl.SetSelection(self.dialects.index(default))
2246         hbs.Add(self.dialectctrl, 1, wx.ALL|wx.EXPAND, 5)
2247 
2248         hbs2.Add(hbs, 2, wx.EXPAND|wx.ALL, 10)
2249         hbs2.Add(self.GetSelectionGui(self), 5, wx.EXPAND|wx.ALL, 5)
2250 
2251         vbs.Add(hbs2, 0, wx.EXPAND|wx.ALL, 5)
2252 
2253         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5)
2254         vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5)
2255         self.SetSizer(vbs)
2256         self.SetAutoLayout(True)
2257         vbs.Fit(self)
2258 
2259         wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
2260 
2261     def OnBrowse(self, _):
2262         with guihelper.WXDialogWrapper(wx.FileDialog(self, defaultFile=self.filenamectrl.GetValue(),
2263                                                      wildcard="vCard files (*.vcf)|*.vcf", style=wx.SAVE|wx.CHANGE_DIR),
2264                                        True) as (dlg, retcode):
2265             if retcode==wx.ID_OK:
2266                 self.filenamectrl.SetValue(os.path.join(dlg.GetDirectory(), dlg.GetFilename()))
2267 
2268     def OnOk(self, _):
2269         # do export
2270         filename=self.filenamectrl.GetValue()
2271 
2272         dialect=None
2273         for k,v in vcard.profiles.items():
2274             if v['description']==self.dialectctrl.GetStringSelection():
2275                 dialect=k
2276                 break
2277 
2278         assert dialect is not None
2279 
2280         # ::TODO:: ask about overwriting existing file
2281         with file(filename, "wt") as f:
2282             for record in self.GetPhoneBookItems():
2283                 print >>f, vcard.output_entry(record, vcard.profiles[dialect]['profile'])
2284         
2285         # save settings since we were succesful
2286         wx.GetApp().config.Write("vcard/export-file", filename)
2287         wx.GetApp().config.Write("vcard/export-format", dialect)
2288         wx.GetApp().config.Flush()
2289         self.EndModal(wx.ID_OK)
2290 
2291 class ExportCSVDialog(BaseExportDialog):
2292     __pb_keys=(
2293         ('names', ('title', 'first', 'middle', 'last', 'full', 'nickname')),
2294         ('addresses', ('type', 'company', 'street', 'street2', 'city',
2295                        'state', 'postalcode', 'country')),
2296         ('numbers', ('number', 'type', 'speeddial')),
2297         ('emails', ('email', 'type')),
2298         ('urls', ('url', 'type')),
2299         ('categories', ('category',)),
2300         ('ringtones', ('ringtone', 'use')),
2301         ('wallpapers', ('wallpaper', 'use')),
2302         ('memos', ('memo',)),
2303         ('flags', ('secret',))
2304         )
2305         
2306     def __init__(self, parent, title):
2307         super(ExportCSVDialog, self).__init__(parent, title)
2308         # make the ui
2309         vbs=wx.BoxSizer(wx.VERTICAL)
2310         bs=wx.BoxSizer(wx.HORIZONTAL)
2311         bs.Add(wx.StaticText(self, -1, "File"), 0, wx.ALL|wx.ALIGN_CENTRE, 5)
2312         self.filenamectrl=wx.TextCtrl(self, -1, "bitpim.csv")
2313         bs.Add(self.filenamectrl, 1, wx.ALL|wx.EXPAND, 5)
2314         self.browsectrl=wx.Button(self, wx.NewId(), "Browse...")
2315         bs.Add(self.browsectrl, 0, wx.ALL|wx.EXPAND, 5)
2316         vbs.Add(bs, 0, wx.EXPAND|wx.ALL, 5)
2317         # selection GUI
2318         vbs.Add(self.GetSelectionGui(self), 5, wx.EXPAND|wx.ALL, 5)
2319         # the buttons
2320         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5)
2321         vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5)
2322         # event handlers
2323         wx.EVT_BUTTON(self, self.browsectrl.GetId(), self.OnBrowse)
2324         wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
2325         # all done
2326         self.SetSizer(vbs)
2327         self.SetAutoLayout(True)
2328         vbs.Fit(self)
2329 
2330     def OnBrowse(self, _):
2331         with guihelper.WXDialogWrapper(wx.FileDialog(self, defaultFile=self.filenamectrl.GetValue(),
2332                                                      wildcard="CSV files (*.csv)|*.csv", style=wx.SAVE|wx.CHANGE_DIR),
2333                                        True) as (dlg, retcode):
2334             if retcode==wx.ID_OK:
2335                 self.filenamectrl.SetValue(os.path.join(dlg.GetDirectory(), dlg.GetFilename()))
2336     def OnOk(self, _):
2337         # do export
2338         filename=self.filenamectrl.GetValue()
2339         # find out the length of each key
2340         key_count={}
2341         for e in self.__pb_keys:
2342             key_count[e[0]]=0
2343         for record in self.GetPhoneBookItems():
2344             for k in record:
2345                 if key_count.has_key(k):
2346                     key_count[k]=max(key_count[k], len(record[k]))
2347         with file(filename, 'wt') as f:
2348             l=[]
2349             for e in self.__pb_keys:
2350                 if key_count[e[0]]:
2351                     ll=[e[0]+'_'+x for x in e[1]]
2352                     l+=ll*key_count[e[0]]
2353             f.write(','.join(l)+'\n')
2354             for record in self.GetPhoneBookItems():
2355                 ll=[]
2356                 for e in self.__pb_keys:
2357                     key=e[0]
2358                     if key_count[key]:
2359                         for i in range(key_count[key]):
2360                             try:
2361                                 entry=record[key][i]
2362                             except (KeyError, IndexError):
2363                                 entry={}
2364                             for field in e[1]:
2365                                 _v=entry.get(field, '')
2366                                 if isinstance(_v, unicode):
2367                                     _v=_v.encode('ascii', 'ignore')
2368                                 ll.append('"'+str(_v).replace('"', '')+'"')
2369                 f.write(','.join(ll)+'\n')
2370                 f.flush()
2371         self.EndModal(wx.ID_OK)
2372         
2373 class ExporteGroupwareDialog(BaseExportDialog):
2374 
2375     ID_REFRESH=wx.NewId()
2376     ID_CHANGE=wx.NewId()
2377 
2378     _categorymessage="eGroupware doesn't create categories correctly via XML-RPC, so you should manually create them via the web interface.  " \
2379            "Click to see which ones should be manually created." 
2380 
2381 
2382     def __init__(self, parent, title, module):
2383         BaseExportDialog.__init__(self, parent, title)
2384 
2385         self.module=module
2386         self.parent=parent
2387         
2388         # make the ui
2389         
2390         vbs=wx.BoxSizer(wx.VERTICAL)
2391 
2392         hbs=wx.BoxSizer(wx.HORIZONTAL)
2393         hbs.Add(wx.StaticText(self, -1, "URL"), 0, wx.ALIGN_CENTRE|wx.ALL,2)
2394         self.curl=wx.StaticText(self, -1)
2395         hbs.Add(self.curl, 3, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2)
2396         hbs.Add(wx.StaticText(self, -1, "Domain"), 0, wx.ALIGN_CENTRE|wx.ALL,5)
2397         self.cdomain=wx.StaticText(self, -1)
2398         hbs.Add(self.cdomain, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2)
2399         hbs.Add(wx.StaticText(self, -1, "User"), 0, wx.ALIGN_CENTRE|wx.ALL,5)
2400         self.cuser=wx.StaticText(self, -1)
2401         hbs.Add(self.cuser, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 5)
2402         self.cchange=wx.Button(self, self.ID_CHANGE, "Change ...")
2403         hbs.Add(self.cchange, 0, wx.ALL, 2)
2404         vbs.Add(hbs,0,wx.ALL,5)
2405         wx.EVT_BUTTON(self, self.ID_CHANGE, self.OnChangeCreds)
2406         self.sp=None
2407         self.setcreds()
2408 
2409         vbs.Add(self.GetSelectionGui(self), 0, wx.EXPAND|wx.ALL, 5)
2410 
2411 
2412         # category checker
2413         
2414         bs2=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Category Checker"), wx.HORIZONTAL)
2415         bs2.Add(wx.Button(self, self.ID_REFRESH, "Check"), 0, wx.ALL, 5)
2416         self.categoryinfo=wx.TextCtrl(self, -1, self._categorymessage, style=wx.TE_MULTILINE|wx.TE_READONLY)
2417         bs2.Add(self.categoryinfo, 1, wx.EXPAND|wx.ALL, 2)
2418 
2419         vbs.Add(bs2, 0, wx.EXPAND|wx.ALL, 5)
2420 
2421         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5)
2422 
2423         bs2=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Export Progress"), wx.VERTICAL)
2424         self.progress=wx.Gauge(self, -1, style=wx.GA_HORIZONTAL|wx.GA_SMOOTH)
2425         self.progress_text=wx.StaticText(self, -1, "")
2426         bs2.Add(self.progress, 0, wx.EXPAND|wx.ALL, 5)
2427         bs2.Add(self.progress_text, 0, wx.EXPAND|wx.ALL, 5)
2428         vbs.Add(bs2, 0, wx.EXPAND|wx.ALL, 5)
2429 
2430         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5)
2431         vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALL, 5)
2432         
2433         self.SetSizer(vbs)
2434         self.SetAutoLayout(True)
2435         
2436         # for some reason wx decides to make the dialog way taller
2437         # than needed.  i can't figure out why.  sometimes wx just
2438         # makes you go grrrrrrrr
2439         vbs.Fit(self)
2440 
2441         wx.EVT_BUTTON(self, self.ID_REFRESH, self.CategoryCheck)
2442         wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk)
2443         
2444     def OnChangeCreds(self,_):
2445         dlg=eGroupwareLoginDialog(self, self.module)
2446         newsp=dlg.GetSession()
2447         if newsp is not None:
2448             self.sp=newsp
2449             self.setcreds()
2450 
2451     def setcreds(self):
2452         cfg=wx.GetApp().config
2453         self.curl.SetLabel(cfg.Read("egroupware/url", "http://server.example.com/egroupware"))
2454         self.cdomain.SetLabel(cfg.Read("egroupware/domain", "default"))
2455         try:
2456             import getpass
2457             defuser=getpass.getuser()
2458         except:
2459             defuser="user"
2460         self.cuser.SetLabel(cfg.Read("egroupware/user", defuser))
2461 
2462     def CategoryCheck(self, evt=None):
2463         if evt is not None:
2464             evt.Skip()
2465         if self.sp is None:
2466             self.sp=eGroupwareLoginDialog(self, self.module).GetSession(auto=True)
2467             self.setcreds()
2468         self.FindWindowById(wx.ID_OK).Enable(self.sp is not None)
2469         self.FindWindowById(self.ID_REFRESH).Enable(self.sp is not None)
2470 
2471         # find which categories are missing
2472         cats=[]
2473         for e in self.GetPhoneBookItems():
2474             for c in e.get("categories", []):
2475                 cc=c["category"]
2476                 if cc not in cats:
2477                     cats.append(cc)
2478 
2479         cats.sort()
2480         egcats=[v['name'] for v in self.sp.getcategories()]
2481         nocats=[c for c in cats if c not in egcats]
2482 
2483         if len(nocats):
2484             self.categoryinfo.SetValue("eGroupware doesn't have the following categories, which you should manually add:\n\n"+", ".join(nocats))
2485         else:
2486             self.categoryinfo.SetValue("eGroupware has all the categories you use")
2487 
2488     def OnOk(self, _):
2489         if self.sp is None:
2490             self.sp=eGroupwareLoginDialog(self, self.module).GetSession(auto=True)
2491             self.setcreds()
2492         if self.sp is None:
2493             return
2494 
2495 
2496         doesntexistaction=None
2497         catsmodified=True # load categories 
2498         # write out each one
2499         setmax=-1
2500         for record,pos,max in self.GetPhoneBookItems(includecount=True):
2501             if catsmodified:
2502                 # get the list of categories
2503                 cats=dict( [(v['name'], v['id']) for v in self.sp.getcategories()] )
2504             if max!=setmax:
2505                 setmax=max
2506                 self.progress.SetRange(max)
2507             self.progress.SetValue(pos)
2508             self.progress_text.SetLabel(nameparser.formatsimplename(record.get("names", [{}])[0]))
2509             wx.SafeYield()
2510             catsmodified,rec=self.FormatRecord(record, cats)
2511             if rec['id']!=0:
2512                 # we have an id, but the record could have been deleted on the eg server, so
2513                 # we check
2514                 if not self.sp.doescontactexist(rec['id']):
2515                     if doesntexistaction is None:
2516                         with guihelper.WXDialogWrapper(eGroupwareEntryGoneDlg(self, rec['fn']),
2517                                                        True) as (dlg, _):
2518                             action=dlg.GetAction()
2519                             if dlg.ForAll():
2520                                 doesntexistaction=action
2521                     else: action=doesntexistaction
2522                     if action==self._ACTION_RECREATE:
2523                         rec['id']=0
2524                     elif action==self._ACTION_IGNORE:
2525                         continue
2526                     elif action==self._ACTION_DELETE:
2527                         found=False
2528                         for serial in record["serials"]:
2529                             if serial["sourcetype"]=="bitpim":
2530                                 self.parent.GetActivePhonebookWidget().DeleteBySerial(serial)
2531                                 found=True
2532                                 break
2533                         assert found
2534                         continue 
2535 
2536             newid=self.sp.writecontact(rec)
2537             found=False
2538             for serial in record["serials"]:
2539                 if serial["sourcetype"]=="bitpim":
2540                     self.parent.GetActivePhonebookWidget().UpdateSerial(serial, {"sourcetype": "egroupware", "id": newid})
2541                     found=True
2542                     break
2543             assert found
2544             
2545             
2546         self.EndModal(wx.ID_OK)
2547 
2548 
2549 
2550     def FormatRecord(self, record, categories):
2551         """Convert record into egroupware fields
2552 
2553         We return a tuple of  (egw formatted record, if we update categories)
2554 
2555         If the second part is True, the categories should be re-read from the server after writing the record."""
2556 
2557         catsmodified=False
2558 
2559         # note that mappings must be carefully chosen to ensure that importing from egroupware
2560         # and then re-exporting doesn't change anything.
2561         
2562         res={'id': 0} # zero means create new record
2563         # find existing egroupware id
2564         for i in record.get("serials", []):
2565             if i['sourcetype']=='egroupware':
2566                 res['id']=i['id']
2567                 break
2568         # name (nb we don't do prefix or suffix since bitpim doesn't know about them)
2569         res['n_given'],res['n_middle'],res['n_family']=nameparser.getparts(record.get("names", [{}])[0])
2570         for nf in 'n_given', 'n_middle', 'n_family':
2571             if res[nf] is None:
2572                 res[nf]="" # set None fields to blank string
2573         res['fn']=nameparser.formatsimplename(record.get("names", [{}])[0])
2574         # addresses
2575         for t,prefix in ("business", "adr_one"), ("home", "adr_two"):
2576             a={}
2577             adr=record.get("addresses", [])
2578             for i in adr:
2579                 if i['type']==t:
2580                     for p2,k in ("_street", "street"), ("_locality", "city"), ("_region", "state"), \
2581                         ("_postalcode", "postalcode"), ("_countryname", "country"):
2582                         res[prefix+p2]=i.get(k, "")
2583                     if t=="business":
2584                         res['org_name']=i.get("company","")
2585                     break
2586         # email
2587         if "emails" in record:
2588             # this means we ignore emails without a type, but that can't be avoided with
2589             # how egroupware works
2590             for t,k in ("business", "email"), ("home", "email_home"):
2591                 for i in record["emails"]:
2592                     if i.get("type",None)==t:
2593                         res[k]=i.get("email")
2594                         res[k+"_type"]="INTERNET"
2595                         break
2596 
2597         # categories
2598         cats={}
2599         for cat in record.get("categories", []):
2600             c=cat['category']
2601             v=categories.get(c, None)
2602             if v is None:
2603                 catsmodified=True
2604                 for i in xrange(0,-999999,-1):
2605                     if `i` not in cats:
2606                         break
2607             else:
2608                 i=`v`
2609             cats[i]=str(c)
2610             
2611         res['cat_id']=cats
2612 
2613         # phone numbers
2614         # t,k is bitpim type, egroupware type
2615         for t,k in ("home", "tel_home"), ("cell", "tel_cell"), ('fax','tel_fax'), \
2616                 ('pager', 'tel_pager'), ('office', 'tel_work'):
2617             if "numbers" in record:
2618                 v=""
2619                 for i in record['numbers']:
2620                     if i['type']==t:
2621                         v=i['number']
2622                         break
2623                 res[k]=phonenumber.format(v)
2624 
2625         # miscellaneous others
2626         if "memos" in record:
2627             memos=record.get("memos", [])
2628             memos+=[{}]
2629             res['note']=memos[0].get("memo","")
2630         if "urls" in record:
2631             urls=record.get("urls", [])
2632             u=""
2633             for url in urls:
2634                 if url.get("type", None)=="business":
2635                     u=url["url"]
2636                     break
2637             if len(u)==0:
2638                 urls+=[{'url':""}]
2639                 u=urls[0]["url"]
2640             res['url']=u
2641 
2642         # that should be everything
2643         return catsmodified,res
2644 
2645     _ACTION_RECREATE=1
2646     _ACTION_IGNORE=2
2647     _ACTION_DELETE=3
2648     
2649 class eGroupwareEntryGoneDlg(wx.Dialog):
2650 
2651     choices=( ("Re-create entry on server", ExporteGroupwareDialog._ACTION_RECREATE),
2652               ("Delete the entry in BitPim", ExporteGroupwareDialog._ACTION_DELETE),
2653               ("Ignore this entry for now", ExporteGroupwareDialog._ACTION_IGNORE)
2654               )
2655 
2656     def __init__(self, parent, details):
2657         wx.Dialog.__init__(self, parent, -1, title="Entry deleted on server")
2658         vbs=wx.BoxSizer(wx.VERTICAL)
2659         vbs.Add(wx.StaticText(self, -1, "The entry for \"%s\" has\nbeen deleted on the server." % (details,) ), 0, wx.EXPAND|wx.ALL, 5)
2660         self.rb=wx.RadioBox(self, -1, "Action to take", choices=[t for t,a in self.choices])
2661         vbs.Add(self.rb, 0, wx.EXPAND|wx.ALL, 5)
2662         self.always=wx.CheckBox(self, -1, "Always take this action")
2663         vbs.Add(self.always, 0, wx.ALL|wx.ALIGN_CENTRE, 5)
2664 
2665         vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5)
2666         vbs.Add(self.CreateButtonSizer(wx.OK|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5)
2667         self.SetSizer(vbs)
2668         vbs.Fit(self)
2669 
2670     def GetAction(self):
2671         return self.choices[self.rb.GetSelection()][1]
2672 
2673     def ForAll(self):
2674             return self.always.GetValue()
2675 
2676 def OnFileExportVCards(parent):
2677     with guihelper.WXDialogWrapper(ExportVCardDialog(parent, "Export phonebook to vCards"),
2678                                    True):
2679         pass
2680 
2681 def OnFileExporteGroupware(parent):
2682     import native.egroupware
2683     with guihelper.WXDialogWrapper(ExporteGroupwareDialog(parent, "Export phonebook to eGroupware", native.egroupware),
2684                                    True):
2685         pass
2686 
2687 def OnFileExportCSV(parent):
2688     with guihelper.WXDialogWrapper(ExportCSVDialog(parent, "Export phonebook to CSV"),
2689                                    True):
2690         pass
2691 
2692 def OnFileExportCSVCalendar(parent):
2693     import csv_calendar
2694     with guihelper.WXDialogWrapper(csv_calendar.ExportCSVDialog(parent, 'Export Calendar to CSV'),
2695                                    True):
2696         pass
2697 
2698 def OnFileExportiCalendar(parent):
2699     with guihelper.WXDialogWrapper(ical_calendar.ExportDialog(parent, 'Export Calendar to iCalendar'),
2700                                    True):
2701         pass
2702 
2703 def OnFileExportSMS(parent):
2704     import sms_imexport
2705     with guihelper.WXDialogWrapper(sms_imexport.ExportSMSDialog(parent, 'Export SMS'),
2706                                    True):
2707         pass
2708 
2709 def OnFileExportCallHistory(parent):
2710     import call_history_export
2711     with guihelper.WXDialogWrapper(call_history_export.ExportCallHistoryDialog(parent, 'Export Call History'),
2712                                    True):
2713         pass
2714 
2715 def OnFileExportMediaZip(parent):
2716     import media_root
2717     with guihelper.WXDialogWrapper(media_root.ExportMediaToZipDialog(parent, 'Export Media to Zip')) as dlg:
2718         dlg.DoDialog()
2719 
2720 def OnFileExportMediaDir(parent):
2721     import media_root
2722     with guihelper.WXDialogWrapper(media_root.ExportMediaToDirDialog(parent, 'Media will be copied to selected folder')) as dlg:
2723         dlg.DoDialog()
2724 

Generated by PyXR 0.9.4