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