Module importexport
[hide private]
[frames] | no frames]

Source Code for Module importexport

   1  ### BITPIM 
   2  ### 
   3  ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 
   4  ### 
   5  ### This program is free software; you can redistribute it and/or modify 
   6  ### it under the terms of the BitPim license as detailed in the LICENSE file. 
   7  ### 
   8  ### $Id: importexport.py 4377 2007-08-27 04:58:33Z djpham $ 
   9   
  10  "Deals with importing and exporting stuff" 
  11   
  12  # System modules 
  13  from __future__ import with_statement 
  14  import contextlib 
  15  import string 
  16  import re 
  17  import StringIO 
  18  import os 
  19   
  20  # wxPython modules 
  21  import wx 
  22  import wx.grid 
  23  import wx.html 
  24   
  25  # Others 
  26  from thirdparty import DSV 
  27   
  28  # My modules 
  29  import common 
  30  import guihelper 
  31  import vcard 
  32  import phonenumber 
  33  import guiwidgets 
  34  import nameparser 
  35  import phonebook 
  36  import pubsub 
  37  import guihelper 
  38  import csv_calendar 
  39  import vcal_calendar 
  40  import ical_calendar 
  41  import gcal_calendar as gcal 
  42  import playlist 
  43  import wpl_file 
44 45 # control 46 -def GetPhonebookImports():
47 res=[] 48 # Calendar Wizard 49 res.append( (guihelper.ID_CALENDAR_WIZARD, 'Import Calendar Wizard...', 50 'Import Calendar Wizard', OnCalendarWizard) ) 51 res.append( (wx.NewId(), 'Calendar Import Preset...', 52 'Calendar Import Preset...', OnCalendarPreset) ) 53 res.append( (wx.NewId(), 'Auto Calendar Import', 54 'Auto Calendar Import', 55 ( (guihelper.ID_AUTOSYNCSETTINGS, 'Settings', 56 'Configure Auto Calendar Import', None), 57 (guihelper.ID_AUTOSYNCEXECUTE, 'Execute', 58 'Perform Auto Calendar Import', None)) 59 )) 60 # CSV - always possible 61 res.append( (guihelper.ID_IMPORT_CSV_CONTACTS,"CSV Contacts...", "Import a CSV file for the phonebook", OnFileImportCSVContacts) ) 62 res.append( (guihelper.ID_IMPORT_CSV_CALENDAR,"CSV Calendar...", "Import a CSV file for the calendar", OnFileImportCSVCalendar) ) 63 # Vcards - always possible 64 res.append( (guihelper.ID_IMPORT_VCARDS,"vCards...", "Import vCards for the phonebook", OnFileImportVCards) ) 65 # Vcal - always possible 66 res.append((guihelper.ID_IMPORT_VCALENDAR,'vCalendar...', 'Import vCalendar data for the calendar', OnFileImportVCal)) 67 # iCal - always possible 68 res.append((guihelper.ID_IMPORT_ICALENDAR, 'iCalendar...', 69 'Import iCalendar data for the calendar', 70 OnFileImportiCal)) 71 # Google Calendar - always possible 72 res.append((guihelper.ID_IMPORT_GCALENDAR, 'Google Calendar...', 73 'Import Google Calendar data for the calendar', 74 OnFileImportgCal)) 75 # Outlook 76 try: 77 import native.outlook 78 res.append( (guihelper.ID_IMPORT_OUTLOOK_CONTACTS,"Outlook Contacts...", "Import Outlook contacts for the phonebook", OnFileImportOutlookContacts) ) 79 res.append( (guihelper.ID_IMPORT_OUTLOOK_CALENDAR,"Outlook Calendar...", "Import Outlook calendar for the calendar", OnFileImportOutlookCalendar) ) 80 res.append( (guihelper.ID_IMPORT_OUTLOOK_NOTES,"Outlook Notes...", "Import Outlook notes for the memo", OnFileImportOutlookNotes) ) 81 res.append( (guihelper.ID_IMPORT_OUTLOOK_TASKS,"Outlook Tasks...", "Import Outlook tasks for the todo", OnFileImportOutlookTasks) ) 82 except: 83 pass 84 # Evolution 85 try: 86 import native.evolution 87 res.append( (guihelper.ID_IMPORT_EVO_CONTACTS,"Evolution Contacts...", "Import Evolution contacts for the phonebook", OnFileImportEvolutionContacts) ) 88 except ImportError: 89 pass 90 # Qtopia Desktop - always possible 91 res.append( (guihelper.ID_IMPORT_QTOPIA_CONTACTS,"Qtopia Desktop...", "Import Qtopia Desktop contacts for the phonebook", OnFileImportQtopiaDesktopContacts) ) 92 # eGroupware - always possible 93 res.append( (guihelper.ID_IMPORT_GROUPWARE_CONTACTS,"eGroupware...", "Import eGroupware contacts for the phonebook", OnFileImporteGroupwareContacts) ) 94 # WPL Playlist, always possible 95 res.append( (guihelper.ID_IMPORT_WPL, 'WPL Play List...', 96 'Import WPL Play List', 97 OnWPLImport)) 98 return res
99
100 -def GetCalenderAutoSyncImports():
101 res=[] 102 # CSV - always possible 103 res.append( ("CSV Calendar", AutoConfCSVCalender, AutoImportCSVCalendar) ) 104 # Vcal - always possible 105 res.append(('vCalendar', AutoConfVCal, AutoImportVCal)) 106 # Outlook 107 try: 108 import native.outlook 109 res.append( ("Outlook", AutoConfOutlookCalender, AutoImportOutlookCalendar) ) 110 except: 111 print "Failed to get outlook" 112 pass 113 # Evolution 114 115 return res
116
117 -def GetCalendarImports():
118 # return a list of calendar types data objects 119 res=[] 120 res.append({ 'type': 'CSV Calendar', 121 'source': csv_calendar.ImportDataSource, 122 'data': csv_calendar.CSVCalendarImportData }) 123 res.append({ 'type': 'vCalendar', 124 'source': vcal_calendar.ImportDataSource, 125 'data': vcal_calendar.VCalendarImportData }) 126 res.append({ 'type': 'iCalendar', 127 'source': ical_calendar.ImportDataSource, 128 'data': ical_calendar.iCalendarImportData }) 129 res.append({ 'type': 'Google Calendar', 130 'source': gcal.ImportDataSource, 131 'data': gcal.gCalendarImportData }) 132 try: 133 import native.outlook 134 import outlook_calendar 135 res.append({ 'type': 'Outlook Calendar', 136 'source': outlook_calendar.ImportDataSource, 137 'data': outlook_calendar.OutlookCalendarImportData }) 138 except: 139 pass 140 return res
141
142 -def TestOutlookIsInstalled():
143 import native.outlook 144 try: 145 native.outlook.getmapinamespace() 146 except: 147 guihelper.MessageDialog(None, 'Unable to initialise Outlook, Check that it is installed correctly.', 148 'Outlook Error', wx.OK|wx.ICON_ERROR) 149 return False 150 return True
151
152 -class PreviewGrid(wx.grid.Grid):
153
154 - def __init__(self, parent, id):
155 wx.grid.Grid.__init__(self, parent, id, style=wx.WANTS_CHARS) 156 wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self, self.OnLeftDClick)
157 158 # (Taken from the demo) I do this because I don't like the default 159 # behaviour of not starting the cell editor on double clicks, but 160 # only a second click.
161 - def OnLeftDClick(self, evt):
162 if self.CanEnableCellControl(): 163 self.EnableCellEditControl()
164
165 -class ImportDialog(wx.Dialog):
166 "The dialog for importing phonebook stuff" 167 168 169 # these are presented in the UI and are what the user can select. additional 170 # column names are available but not specified 171 possiblecolumns=["<ignore>", "First Name", "Last Name", "Middle Name", 172 "Name", "Nickname", "Email Address", "Web Page", "Fax", "Home Street", 173 "Home City", "Home Postal Code", "Home State", 174 "Home Country/Region", "Home Phone", "Home Fax", "Mobile Phone", "Home Web Page", 175 "Business Street", "Business City", "Business Postal Code", 176 "Business State", "Business Country/Region", "Business Web Page", 177 "Business Phone", "Business Fax", "Pager", "Company", "Notes", "Private", 178 "Category", "Categories"] 179 bp_columns=[ 180 # BitPim CSV fields 181 'names_title', 'names_first', 'names_middle', 'names_last', 182 'names_full', 'names_nickname', 183 'addresses_type', 'addresses_company', 'addresses_street', 184 'addresses_street2', 'addresses_city', 185 'addresses_state', 'addresses_postalcode', 186 'addresses_country', 187 'numbers_number', 'numbers_type', 'numbers_speeddial', 188 'emails_email', 'emails_type', 189 'urls_url', 'urls_type', 190 'categories_category', 191 'ringtones_ringtone', 'ringtones_use', 192 'wallpapers_wallpaper', 'wallpapers_use', 193 'memos_memo', 'flags_secret' 194 ] 195 196 # used for the filtering - any of the named columns have to be present for the data row 197 # to be considered to have that type of column 198 filternamecolumns=["First Name", "Last Name", "Middle Name", "Name", "Nickname"] 199 200 filternumbercolumns=["Home Phone", "Home Fax", "Mobile Phone", "Business Phone", 201 "Business Fax", "Pager", "Fax", "Phone"] 202 203 filterhomeaddresscolumns=["Home Street", "Home City", "Home Postal Code", "Home State", 204 "Home Country/Region"] 205 206 filterbusinessaddresscolumns=["Business Street", "Business City", 207 "Business Postal Code", "Business State", "Business Country/Region"] 208 209 filteraddresscolumns=filterhomeaddresscolumns+filterbusinessaddresscolumns+["Address"] 210 211 filteremailcolumns=["Email Address", "Email Addresses"] 212 213 # used in mapping column names above into bitpim phonebook fields 214 addressmap={ 215 'Street': 'street', 216 'City': 'city', 217 'Postal Code': 'postalcode', 218 'State': 'state', 219 'Country/Region': 'country', 220 } 221 222 namemap={ 223 'First Name': 'first', 224 'Last Name': 'last', 225 'Middle Name': 'middle', 226 'Name': 'full', 227 'Nickname': 'nickname' 228 } 229 230 numbermap={ 231 "Home Phone": 'home', 232 "Home Fax": 'fax', 233 "Mobile Phone": 'cell', 234 "Business Phone": 'office', 235 "Business Fax": 'fax', 236 "Pager": 'pager', 237 "Fax": 'fax' 238 } 239 240
241 - def __init__(self, parent, id, title, style=wx.CAPTION|wx.MAXIMIZE_BOX|\ 242 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER):
243 wx.Dialog.__init__(self, parent, id=id, title=title, style=style) 244 self.possiblecolumns+=self.bp_columns 245 self.merge=True 246 vbs=wx.BoxSizer(wx.VERTICAL) 247 t,sz=self.gethtmlhelp() 248 w=wx.html.HtmlWindow(self, -1, size=sz, style=wx.html.HW_SCROLLBAR_NEVER) 249 w.SetPage(t) 250 vbs.Add(w, 0, wx.EXPAND|wx.ALL,5) 251 252 self.getcontrols(vbs) 253 254 cfg=lambda key: wx.GetApp().config.ReadInt("importdialog/filter"+key, False) 255 256 257 # Only records with ... row 258 hbs=wx.BoxSizer(wx.HORIZONTAL) 259 hbs.Add(wx.StaticText(self, -1, "Only rows with "), 0, wx.ALL|wx.ALIGN_CENTRE,2) 260 self.wname=wx.CheckBox(self, wx.NewId(), "a name") 261 self.wname.SetValue(cfg("name")) 262 hbs.Add(self.wname, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7) 263 self.wnumber=wx.CheckBox(self, wx.NewId(), "a number") 264 self.wnumber.SetValue(cfg("phonenumber")) 265 hbs.Add(self.wnumber, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7) 266 self.waddress=wx.CheckBox(self, wx.NewId(), "an address") 267 self.waddress.SetValue(cfg("postaladdress")) 268 hbs.Add(self.waddress, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7) 269 self.wemail=wx.CheckBox(self, wx.NewId(), "an email") 270 self.wemail.SetValue(cfg("emailaddress")) 271 hbs.Add(self.wemail, 0, wx.LEFT|wx.ALIGN_CENTRE,7) 272 cats=wx.GetApp().config.Read("importdialog/filtercategories", "") 273 if len(cats): 274 self.categorieswanted=cats.split(";") 275 else: 276 self.categorieswanted=None 277 self.categoriesbutton=wx.Button(self, wx.NewId(), "Categories...") 278 hbs.Add(self.categoriesbutton, 0, wx.EXPAND|wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE, 10) 279 self.categorieslabel=wx.StaticText(self, -1, "") 280 if self.categorieswanted is None: 281 self.categorieslabel.SetLabel("*ANY*") 282 else: 283 self.categorieslabel.SetLabel("; ".join(self.categorieswanted)) 284 hbs.Add(self.categorieslabel, 1, wx.ALIGN_LEFT|wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, 5) 285 vbs.Add(hbs,0, wx.EXPAND|wx.ALL,5) 286 # Full name options: Full Name, First M Last, or Last, First M 287 self._name_option=wx.RadioBox(self, -1, 'Name Reformat', 288 choices=['No Reformat', 'First M Last', 'Last, First M']) 289 wx.EVT_RADIOBOX(self, self._name_option.GetId(), 290 self.DataNeedsUpdate) 291 vbs.Add(self._name_option, 0, wx.ALL, 5) 292 # Preview grid row 293 self.preview=PreviewGrid(self, wx.NewId()) 294 self.preview.CreateGrid(10,10) 295 self.preview.SetColLabelSize(0) 296 self.preview.SetRowLabelSize(0) 297 self.preview.SetMargins(1,0) 298 299 vbs.Add(self.preview, 1, wx.EXPAND|wx.ALL, 5) 300 # Static line and buttons 301 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) 302 _button_sizer=self.CreateButtonSizer(wx.CANCEL|wx.HELP) 303 _btn=wx.Button(self, -1, 'Merge') 304 _button_sizer.Add(_btn, 0, wx.ALIGN_CENTER|wx.ALL, 5) 305 wx.EVT_BUTTON(self, _btn.GetId(), self.OnOk) 306 _btn=wx.Button(self, -1, 'Replace All') 307 _button_sizer.Add(_btn, 0, wx.ALIGN_CENTER|wx.ALL, 5) 308 wx.EVT_BUTTON(self, _btn.GetId(), self.OnReplaceAll) 309 vbs.Add(_button_sizer, 0, wx.ALIGN_CENTER|wx.ALL, 5) 310 self.SetSizer(vbs) 311 for w in self.wname, self.wnumber, self.waddress, self.wemail: 312 wx.EVT_CHECKBOX(self, w.GetId(), self.DataNeedsUpdate) 313 314 wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) 315 wx.EVT_CLOSE(self, self.OnClose) 316 wx.EVT_BUTTON(self, self.categoriesbutton.GetId(), self.OnCategories) 317 318 guiwidgets.set_size("importdialog", self, 90) 319 320 self.DataNeedsUpdate()
321
322 - def DataNeedsUpdate(self, _=None):
323 "The preview data needs to be updated" 324 self.needsupdate=True 325 wx.CallAfter(self.UpdateData)
326
327 - def OnGridCellChanged(self, event):
328 "Called when the user has changed one of the columns" 329 self.columns[event.GetCol()]=self.preview.GetCellValue(0, event.GetCol()) 330 self.wcolumnsname.SetValue("Custom") 331 if self.wname.GetValue() or self.wnumber.GetValue() or self.waddress.GetValue() or self.wemail.GetValue(): 332 self.DataNeedsUpdate()
333
334 - def OnClose(self, event=None):
335 # save various config pieces 336 guiwidgets.save_size("importdialog", self.GetRect()) 337 cfg=lambda key, value: wx.GetApp().config.WriteInt("importdialog/filter"+key, value) 338 cfg("name", self.wname.GetValue()) 339 cfg("phonenumber", self.wnumber.GetValue()) 340 cfg("postaladdress", self.waddress.GetValue()) 341 cfg("emailaddress", self.wemail.GetValue()) 342 if self.categorieswanted is None: 343 cats="" 344 else: 345 cats=";".join(self.categorieswanted) 346 wx.GetApp().config.Write("importdialog/filtercategories", cats) 347 wx.GetApp().config.Flush() 348 if event is not None: 349 event.Skip()
350
351 - def OnOk(self,_):
352 "Ok button was pressed" 353 if self.preview.IsCellEditControlEnabled(): 354 self.preview.HideCellEditControl() 355 self.preview.SaveEditControlValue() 356 self.OnClose() # for some reason this isn't called automatically 357 self.EndModal(wx.ID_OK)
358
359 - def OnReplaceAll(self, evt):
360 "ReplaceAll button was pressed" 361 self.merge=False 362 self.OnOk(evt)
363
364 - def _reformat_name_firstmiddlelast(self, entry):
365 # reformat the full name to be First Middle Last 366 _name=entry.get('names', [None])[0] 367 if not _name: 368 return entry 369 _s=nameparser.formatsimplefirstlast(_name) 370 if _s: 371 _name['full']=_s 372 entry['names']=[_name] 373 return entry
374 - def _reformat_name_lastfirtsmiddle(self, entry):
375 # reformat the full name to be Last, First Middle 376 _name=entry.get('names', [None])[0] 377 if not _name: 378 return entry 379 _s=nameparser.formatsimplelastfirst(_name) 380 if _s: 381 _name['full']=_s 382 entry['names']=[_name] 383 return entry
384 _reformat_name_func=(lambda self, entry: entry, 385 _reformat_name_firstmiddlelast, 386 _reformat_name_lastfirtsmiddle)
387 - def __build_entry(self, rec):
388 entry={} 389 # emails 390 emails=[] 391 if rec.has_key('Email Address'): 392 for e in rec['Email Address']: 393 if isinstance(e, dict): 394 emails.append(e) 395 else: 396 emails.append({'email': e}) 397 del rec['Email Address'] 398 if rec.has_key("Email Addresses"): 399 for e in rec['Email Addresses']: 400 emails.append({'email': e}) 401 del rec["Email Addresses"] 402 if len(emails): 403 entry['emails']=emails 404 # addresses 405 for prefix,fields in \ 406 ( ("Home", self.filterhomeaddresscolumns), 407 ("Business", self.filterbusinessaddresscolumns) 408 ): 409 addr={} 410 for k in fields: 411 if k in rec: 412 # it has a field for this type 413 shortk=k[len(prefix)+1:] 414 addr['type']=prefix.lower() 415 addr[self.addressmap[shortk]]=rec[k] 416 del rec[k] 417 if len(addr): 418 if prefix=="Business" and rec.has_key("Company"): 419 # fill in company info 420 addr['type']=prefix.lower() 421 addr['company']=rec["Company"] 422 if not entry.has_key("addresses"): 423 entry["addresses"]=[] 424 entry["addresses"].append(addr) 425 # address (dict form of addresses) 426 if rec.has_key("Address"): 427 # ensure result key exists 428 if not entry.has_key("addresses"): 429 entry["addresses"]=[] 430 # find the company name 431 company=rec.get("Company", None) 432 for a in rec["Address"]: 433 if a["type"]=="business": a["company"]=company 434 addr={} 435 for k in ("type", "company", "street", "street2", "city", "state", "postalcode", "country"): 436 v=a.get(k, None) 437 if v is not None: addr[k]=v 438 entry["addresses"].append(addr) 439 del rec["Address"] 440 # numbers 441 numbers=[] 442 for field in self.filternumbercolumns: 443 if field!="Phone" and rec.has_key(field): 444 for val in rec[field]: 445 numbers.append({'type': self.numbermap[field], 'number': phonenumber.normalise(val)}) 446 del rec[field] 447 # phones (dict form of numbers) 448 if rec.has_key("Phone"): 449 mapping={"business": "office", "business fax": "fax", "home fax": "fax"} 450 for val in rec["Phone"]: 451 number={"type": mapping.get(val["type"], val["type"]), 452 "number": phonenumber.normalise(val["number"])} 453 sd=val.get('speeddial', None) 454 if sd is not None: 455 number.update({ 'speeddial': sd }) 456 numbers.append(number) 457 del rec["Phone"] 458 if len(numbers): 459 entry["numbers"]=numbers 460 461 # names 462 name={} 463 for field in self.filternamecolumns: 464 if field in rec: 465 name[self.namemap[field]]=rec[field] 466 del rec[field] 467 if len(name): 468 entry["names"]=[name] 469 # notes 470 if rec.has_key("Notes"): 471 notes=[] 472 for note in rec["Notes"]: 473 notes.append({'memo': note}) 474 del rec["Notes"] 475 entry["memos"]=notes 476 # web pages 477 urls=[] 478 for type, key in ( (None, "Web Page"), 479 ("home", "Home Web Page"), 480 ("business", "Business Web Page") 481 ): 482 if rec.has_key(key): 483 for url in rec[key]: 484 if isinstance(url, dict): 485 u=url 486 else: 487 u={'url': url} 488 if type is not None: 489 u['type']=type 490 urls.append(u) 491 del rec[key] 492 if len(urls): 493 entry["urls"]=urls 494 # categories 495 cats=[] 496 if rec.has_key("Category"): 497 cats=rec['Category'] 498 del rec["Category"] 499 if rec.has_key("Categories"): 500 # multiple entries in the field, semi-colon seperated 501 if isinstance(rec['Categories'], list): 502 cats+=rec['Categories'] 503 else: 504 for cat in rec['Categories'].split(';'): 505 cats.append(cat) 506 del rec['Categories'] 507 _cats=[] 508 if self.categorieswanted is not None: 509 for c in self.categorieswanted: 510 if c in cats: 511 _cats.append({'category': c }) 512 if _cats: 513 entry["categories"]=_cats 514 # wallpapers 515 l=[] 516 r=rec.get('Wallpapers', None) 517 if r is not None: 518 if isinstance(r, list): 519 l=[{'wallpaper': x, 'use': 'call' } for x in r] 520 else: 521 l=[{'wallpaper': x, 'use': 'call' } for x in r.split(';')] 522 del rec['Wallpapers'] 523 if len(l): 524 entry['wallpapers']=l 525 # ringtones 526 l=[] 527 r=rec.get('Ringtones', None) 528 if r is not None: 529 if isinstance(r, list): 530 l=[{'ringtone': x, 'use': 'call'} for x in r] 531 else: 532 l=[{'ringtone': x, 'use': 'call'} for x in r.split(';')] 533 del rec['Ringtones'] 534 if len(l): 535 entry['ringtones']=l 536 # flags 537 flags=[] 538 if rec.has_key("Private"): 539 private=True 540 # lets see how they have done false 541 if rec["Private"].lower() in ("false", "no", 0, "0"): 542 private=False 543 flags.append({'secret': private}) 544 del rec["Private"] 545 546 if len(flags): 547 entry["flags"]=flags 548 549 # unique serials 550 serial={} 551 for k in rec.keys(): 552 if k.startswith("UniqueSerial-"): 553 v=rec[k] 554 del rec[k] 555 k=k[len("UniqueSerial-"):] 556 serial[k]=v 557 if len(serial): 558 assert serial.has_key("sourcetype") 559 if len(serial)>1: # ie more than just sourcetype 560 entry["serials"]=[serial] 561 # Did we forget anything? 562 # Company is part of other fields 563 if rec.has_key("Company"): del rec["Company"] 564 if len(rec): 565 raise Exception( 566 "Internal conversion failed to complete.\nStill to do: %s" % rec) 567 return entry
568
569 - def __build_bp_entry(self, rec):
570 entry={} 571 for idx,col in enumerate(self.columns): 572 # build the entry from the colum data 573 key=col[:col.find('_')] 574 field=col[col.find('_')+1:] 575 v=rec[idx] 576 if not len(v): 577 v=None 578 if not entry.has_key(key): 579 entry[key]=[] 580 done=False 581 for field_idx,n in enumerate(entry[key]): 582 if not n.has_key(field): 583 entry[key][field_idx][field]=v 584 done=True 585 break 586 if not done: 587 entry[key].append({ field: v }) 588 # go through and delete all blanks fields/dicts 589 for k,e in entry.items(): 590 for i1,d in enumerate(e): 591 for k2,item in d.items(): 592 if item is None: 593 del entry[k][i1][k2] 594 else: 595 if k2=='speeddial': 596 d[k2]=int(item) 597 elif k2=='secret': 598 d[k2]=True 599 if item.lower() in ("false", "no", 0, "0"): 600 d[k2]=False 601 l=[x for x in entry[k] if len(x)] 602 if len(l): 603 entry[k]=l 604 else: 605 del entry[k] 606 return entry
607
608 - def GetFormattedData(self):
609 "Returns the data in BitPim phonebook format" 610 bp_csv=True 611 for c in self.columns: 612 if c=="<ignore>": 613 continue 614 if c not in self.bp_columns: 615 bp_csv=False 616 break 617 res={} 618 count=0 619 for record in self.data: 620 if bp_csv: 621 _entry=self.__build_bp_entry(record) 622 else: 623 # make a dict of the record 624 rec={} 625 for n in range(len(self.columns)): 626 c=self.columns[n] 627 if c=="<ignore>": 628 continue 629 if record[n] is None or len(record[n])==0: 630 continue 631 if c not in self.bp_columns: 632 bp_csv=False 633 if c in self.filternumbercolumns or c in \ 634 ["Category", "Notes", "Business Web Page", "Home Web Page", "Web Page", "Notes", "Phone", "Address", "Email Address"]: 635 # these are multivalued 636 if not rec.has_key(c): 637 rec[c]=[] 638 rec[c].append(record[n]) 639 else: 640 rec[c]=record[n] 641 # entry is what we are building. fields are removed from rec as we process them 642 _entry=self.__build_entry(rec) 643 res[count]=self._reformat_name_func[self._name_option.GetSelection()](self, 644 _entry) 645 count+=1 646 return res
647
649 res="" 650 for col,name in enumerate(self.columns): 651 if name=="Categories": 652 res+="_getpreviewformatted(row[%d], %s).split(';') + " % (col, `name`) 653 elif name=="Category": 654 res+="_getpreviewformatted(row[%d], %s) + " % (col, `name`) 655 res+="[]" 656 fn=compile(res, "_GetExtractCategoriesFunction_", 'eval') 657 return lambda row: eval(fn, globals(), {'row': row})
658 659
660 - def OnCategories(self, _):
661 # find all categories in current unfiltered data 662 savedcolumns,saveddata=self.columns, self.data 663 if self.categorieswanted is not None: 664 # we have to re-read the data if currently filtering categories! This is 665 # because it would contain only the currently selected categories. 666 self.ReReadData() 667 catfn=self.GetExtractCategoriesFunction() 668 cats=[] 669 for row in self.data: 670 for c in catfn(row): 671 if c not in cats: 672 cats.append(c) 673 cats.sort() 674 if len(cats) and cats[0]=="": 675 cats=cats[1:] 676 self.columns,self.data=savedcolumns, saveddata 677 with guihelper.WXDialogWrapper(CategorySelectorDialog(self, self.categorieswanted, cats), 678 True) as (dlg, retcode): 679 if retcode==wx.ID_OK: 680 self.categorieswanted=dlg.GetCategories() 681 if self.categorieswanted is None: 682 self.categorieslabel.SetLabel("*ALL*") 683 else: 684 self.categorieslabel.SetLabel("; ".join(self.categorieswanted)) 685 self.DataNeedsUpdate()
686 687 @guihelper.BusyWrapper
688 - def UpdateData(self):
689 "Actually update the preview data" 690 if not self.needsupdate: 691 return 692 self.needsupdate=False 693 # reread the data 694 self.ReReadData() 695 # category filtering 696 if self.categorieswanted is not None: 697 newdata=[] 698 catfn=self.GetExtractCategoriesFunction() 699 for row in self.data: 700 for cat in catfn(row): 701 if cat in self.categorieswanted: 702 newdata.append(row) 703 break 704 self.data=newdata 705 706 # name/number/address/email filtering 707 if self.wname.GetValue() or self.wnumber.GetValue() or self.waddress.GetValue() or self.wemail.GetValue(): 708 newdata=[] 709 for rownum in range(len(self.data)): 710 # generate a list of fields for which this row has data 711 fields=[] 712 # how many filters are required 713 req=0 714 # how many are present 715 present=0 716 for n in range(len(self.columns)): 717 v=self.data[rownum][n] 718 if v is not None and len(v): 719 fields.append(self.columns[n]) 720 for widget,filter in ( (self.wname, self.filternamecolumns), 721 (self.wnumber, self.filternumbercolumns), 722 (self.waddress, self.filteraddresscolumns), 723 (self.wemail, self.filteremailcolumns) 724 ): 725 if widget.GetValue(): 726 req+=1 727 for f in fields: 728 if f in filter: 729 present+=1 730 break 731 if req>present: 732 break 733 if present==req: 734 newdata.append(self.data[rownum]) 735 self.data=newdata 736 737 self.FillPreview()
738
739 - def _preview_format_name_none(self, row, col, names_col):
740 # no format needed 741 return row[col]
742 - def _preview_format_name_lastfirtmiddle(self, row, col, names_col):
743 # reformat to Last, First Middle 744 _last=names_col.get('Last Name', 745 names_col.get('names_last', None)) 746 _first=names_col.get('First Name', 747 names_col.get('names_first', None)) 748 _middle=names_col.get('Middle Name', 749 names_col.get('names_middle', None)) 750 _full=names_col.get('Name', 751 names_col.get('names_full', None)) 752 _name_dict={} 753 for _key,_value in (('full', _full), ('first', _first), 754 ('middle', _middle), ('last', _last)): 755 if _value is not None and row[_value]: 756 _name_dict[_key]=row[_value] 757 return nameparser.formatsimplelastfirst(_name_dict)
758 - def _preview_format_name_firstmiddlelast(self, row, col, names_col):
759 # reformat to First Middle Last 760 _last=names_col.get('Last Name', 761 names_col.get('names_last', None)) 762 _first=names_col.get('First Name', 763 names_col.get('names_first', None)) 764 _middle=names_col.get('Middle Name', 765 names_col.get('names_middle', None)) 766 _full=names_col.get('Name', 767 names_col.get('names_full', None)) 768 _name_dict={} 769 for _key,_value in (('full', _full), ('first', _first), 770 ('middle', _middle), ('last', _last)): 771 if _value is not None and row[_value]: 772 _name_dict[_key]=row[_value] 773 return nameparser.formatsimplefirstlast(_name_dict)
774 _preview_format_names_func=(_preview_format_name_none, 775 _preview_format_name_firstmiddlelast, 776 _preview_format_name_lastfirtmiddle) 777
778 - def FillPreview(self):
779 self.preview.BeginBatch() 780 if self.preview.GetNumberCols(): 781 self.preview.DeleteCols(0,self.preview.GetNumberCols()) 782 self.preview.DeleteRows(0,self.preview.GetNumberRows()) 783 self.preview.ClearGrid() 784 785 numrows=len(self.data) 786 if numrows: 787 numcols=max(map(lambda x: len(x), self.data)) 788 else: 789 numcols=len(self.columns) 790 # add header row 791 editor=wx.grid.GridCellChoiceEditor(self.possiblecolumns, False) 792 self.preview.AppendRows(1) 793 self.preview.AppendCols(numcols) 794 _names_col={} 795 for col in range(numcols): 796 if 'Name' in self.columns[col] or \ 797 'names_' in self.columns[col]: 798 _names_col[self.columns[col]]=col 799 self.preview.SetCellValue(0, col, self.columns[col]) 800 self.preview.SetCellEditor(0, col, editor) 801 attr=wx.grid.GridCellAttr() 802 attr.SetBackgroundColour(wx.GREEN) 803 attr.SetFont(wx.Font(10,wx.SWISS, wx.NORMAL, wx.BOLD)) 804 attr.SetReadOnly(not self.headerrowiseditable) 805 self.preview.SetRowAttr(0,attr) 806 # add each row 807 oddattr=wx.grid.GridCellAttr() 808 oddattr.SetBackgroundColour("OLDLACE") 809 oddattr.SetReadOnly(True) 810 evenattr=wx.grid.GridCellAttr() 811 evenattr.SetBackgroundColour("ALICE BLUE") 812 evenattr.SetReadOnly(True) 813 _format_name=self._preview_format_names_func[self._name_option.GetSelection()] 814 for row in range(numrows): 815 self.preview.AppendRows(1) 816 for col in range(numcols): 817 if self.columns[col] in ('Name', 'names_full'): 818 s=_format_name(self, self.data[row], col, _names_col) 819 else: 820 s=_getpreviewformatted(self.data[row][col], self.columns[col]) 821 if len(s): 822 self.preview.SetCellValue(row+1, col, s) 823 self.preview.SetRowAttr(row+1, (evenattr,oddattr)[row%2]) 824 self.preview.AutoSizeColumns() 825 self.preview.AutoSizeRows() 826 self.preview.EndBatch()
827
828 -def _getpreviewformatted(value, column):
829 if value is None: return "" 830 if isinstance(value, dict): 831 if column=="Email Address": 832 value="%s (%s)" %(value["email"], value["type"]) 833 elif column=="Web Page": 834 value="%s (%s)" %(value["url"], value["type"]) 835 elif column=="Phone": 836 value="%s (%s)" %(phonenumber.format(value["number"]), value["type"]) 837 elif column=="Address": 838 v=[] 839 for f in ("company", "pobox", "street", "street2", "city", "state", "postalcode", "country"): 840 vv=value.get(f, None) 841 if vv is not None: 842 v.append(vv) 843 assert len(v) 844 v[0]=v[0]+" (%s)" %(value['type'],) 845 value="\n".join(v) 846 else: 847 print "don't know how to convert dict",value,"for preview column",column 848 assert False 849 elif isinstance(value, list): 850 if column=="Email Addresses": 851 value="\n".join(value) 852 elif column=="Categories": 853 value=";".join(value) 854 else: 855 print "don't know how to convert list",value,"for preview column",column 856 assert False 857 return common.strorunicode(value)
858
859 860 -class CategorySelectorDialog(wx.Dialog):
861
862 - def __init__(self, parent, categorieswanted, categoriesavailable):
863 wx.Dialog.__init__(self, parent, title="Import Category Selector", style=wx.CAPTION|wx.MAXIMIZE_BOX|\ 864 wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) #, size=(640,480)) 865 vbs=wx.BoxSizer(wx.VERTICAL) 866 hbs=wx.BoxSizer(wx.HORIZONTAL) 867 self.selected=wx.RadioButton(self, wx.NewId(), "Selected Below", style=wx.RB_GROUP) 868 self.any=wx.RadioButton(self, wx.NewId(), "Any/All") 869 hbs.Add(self.selected, 0, wx.ALL, 5) 870 hbs.Add(self.any, 0, wx.ALL, 5) 871 _up=wx.BitmapButton(self, -1, 872 wx.ArtProvider.GetBitmap(guihelper.ART_ARROW_UP, wx.ART_TOOLBAR, 873 wx.Size(16, 16))) 874 _dn=wx.BitmapButton(self, -1, 875 wx.ArtProvider.GetBitmap(guihelper.ART_ARROW_DOWN, wx.ART_TOOLBAR, 876 wx.Size(16, 16))) 877 hbs.Add(_up, 0, wx.ALL, 5) 878 wx.EVT_BUTTON(self, _up.GetId(), self.OnMoveUp) 879 wx.EVT_BUTTON(self, _dn.GetId(), self.OnMoveDown) 880 hbs.Add(_dn, 0, wx.ALL, 5) 881 vbs.Add(hbs, 0, wx.ALL, 5) 882 883 self.categoriesavailable=categoriesavailable 884 self.cats=wx.CheckListBox(self, wx.NewId(), choices=categoriesavailable) 885 vbs.Add(self.cats, 1, wx.EXPAND|wx.ALL, 5) 886 vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) 887 vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5) 888 889 if categorieswanted is None: 890 self.any.SetValue(True) 891 self.selected.SetValue(False) 892 else: 893 self.any.SetValue(False) 894 self.selected.SetValue(True) 895 for c in categorieswanted: 896 try: 897 self.cats.Check(categoriesavailable.index(c)) 898 except ValueError: 899 pass # had one selected that wasn't in list 900 901 wx.EVT_CHECKLISTBOX(self, self.cats.GetId(), self.OnCatsList) 902 903 self.SetSizer(vbs) 904 vbs.Fit(self)
905
906 - def OnCatsList(self, _):
907 self.any.SetValue(False) 908 self.selected.SetValue(True)
909
910 - def GetCategories(self):
911 if self.any.GetValue(): 912 return None 913 return [self.cats.GetString(x) for x in range(len(self.categoriesavailable)) if self.cats.IsChecked(x)]
914
915 - def _populate(self):
916 _sel_str=self.cats.GetStringSelection() 917 _chk=self.GetCategories() 918 if _chk is None: 919 _chk=[] 920 self.cats.Clear() 921 for s in self.categoriesavailable: 922 i=self.cats.Append(s) 923 if s==_sel_str: 924 self.cats.SetSelection(i) 925 self.cats.Check(i, s in _chk)
926
927 - def OnMoveUp(self, _):
928 _sel_idx=self.cats.GetSelection() 929 if _sel_idx==wx.NOT_FOUND or not _sel_idx: 930 # no selection or top item 931 return 932 # move the selected item one up 933 self.categoriesavailable[_sel_idx], self.categoriesavailable[_sel_idx-1]=\ 934 self.categoriesavailable[_sel_idx-1], self.categoriesavailable[_sel_idx] 935 self._populate()
936
937 - def OnMoveDown(self, _):
938 _sel_idx=self.cats.GetSelection() 939 if _sel_idx==wx.NOT_FOUND or \ 940 _sel_idx==len(self.categoriesavailable)-1: 941 # no selection or bottom item 942 return 943 # move the selected item one up 944 self.categoriesavailable[_sel_idx], self.categoriesavailable[_sel_idx+1]=\ 945 self.categoriesavailable[_sel_idx+1], self.categoriesavailable[_sel_idx] 946 self._populate()
947
948 -class ImportCSVDialog(ImportDialog):
949 950 delimiternames={ 951 '\t': "Tab", 952 ' ': "Space", 953 ',': "Comma" 954 } 955
956 - def __init__(self, filename, parent, id, title):
957 self.headerrowiseditable=True 958 self.filename=filename 959 self.UpdatePredefinedColumns() 960 ImportDialog.__init__(self, parent, id, title)
961
962 - def gethtmlhelp(self):
963 "Returns tuple of help text and size" 964 bg=self.GetBackgroundColour() 965 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), \ 966 (600,100)
967
968 - def getcontrols(self, vbs):
969 data=common.opentextfile(self.filename).read() 970 # turn all EOL chars into \n and then ensure only one \n terminates each line 971 data=data.replace("\r", "\n") 972 oldlen=-1 973 while len(data)!=oldlen: 974 oldlen=len(data) 975 data=data.replace("\n\n", "\n") 976 977 self.rawdata=data 978 979 self.qualifier=DSV.guessTextQualifier(self.rawdata) 980 if self.qualifier is None or len(self.qualifier)==0: 981 self.qualifier='"' 982 self.data=DSV.organizeIntoLines(self.rawdata, textQualifier=self.qualifier) 983 self.delimiter=DSV.guessDelimiter(self.data) 984 # sometimes it picks the letter 'w' 985 if self.delimiter is not None and self.delimiter.lower() in "abcdefghijklmnopqrstuvwxyz": 986 self.delimiter=None 987 if self.delimiter is None: 988 if self.filename.lower().endswith("tsv"): 989 self.delimiter="\t" 990 else: 991 self.delimiter="," 992 # complete processing the data otherwise we can't guess if first row is headers 993 self.data=DSV.importDSV(self.data, delimiter=self.delimiter, textQualifier=self.qualifier, errorHandler=DSV.padRow) 994 # Delimter and Qualifier row 995 hbs=wx.BoxSizer(wx.HORIZONTAL) 996 hbs.Add(wx.StaticText(self, -1, "Delimiter"), 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE, 2) 997 self.wdelimiter=wx.ComboBox(self, wx.NewId(), self.PrettyDelimiter(self.delimiter), choices=self.delimiternames.values(), style=wx.CB_DROPDOWN|wx.WANTS_CHARS) 998 hbs.Add(self.wdelimiter, 1, wx.EXPAND|wx.ALL, 2) 999 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