FW: new Auto Sync feature for calendar (Part 3 of 3)
"Simon C" <[email protected]>
| Newsgroups | gmane.comp.mobile.bitpim.devel |
|---|---|
| Message-ID | <001d01c5be78$b7d10910$6400a8c0@HOME> |
Part 3 of 3 of code changes Simon
importexport.py
(text/plain, 93.7 KB)
### BITPIM ### ### Copyright (C) 2003-2004 Roger Binns <[email protected]> ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the BitPim license as detailed in the LICENSE file. ### ### $Id: importexport.py,v 1.80 2005/05/05 04:15:22 djpham Exp $ "Deals with importing and exporting stuff" # System modules import string import re import StringIO import os # wxPython modules import wx import wx.grid import wx.html # Others from DSV import DSV # My modules import common import guihelper import vcard import phonenumber import guiwidgets import nameparser import phonebook # control def GetPhonebookImports(): res=[] # CSV - always possible res.append( ("CSV Contacts...", "Import a CSV file for the phonebook", OnFileImportCSVContacts) ) res.append( ("CSV Calendar...", "Import a CSV file for the calendar", OnFileImportCSVCalendar) ) # Vcards - always possible res.append( ("vCards...", "Import vCards for the phonebook", OnFileImportVCards) ) # Vcal - always possible res.append(('vCalendar...', 'Import vCalendar data for the calendar', OnFileImportVCal)) # Outlook try: import native.outlook res.append( ("Outlook Contacts...", "Import Outlook contacts for the phonebook", OnFileImportOutlookContacts) ) res.append( ("Outlook Calendar...", "Import Outlook calendar for the calendar", OnFileImportOutlookCalendar) ) except: pass # Evolution try: import native.evolution res.append( ("Evolution Contacts...", "Import Evolution contacts for the phonebook", OnFileImportEvolutionContacts) ) except ImportError: pass # Qtopia Desktop - always possible res.append( ("Qtopia Desktop...", "Import Qtopia Desktop contacts for the phonebook", OnFileImportQtopiaDesktopContacts) ) # eGroupware - always possible res.append( ("eGroupware...", "Import eGroupware contacts for the phonebook", OnFileImporteGroupwareContacts) ) return res def GetCalenderAutoSyncImports(): res=[] # CSV - always possible res.append( ("CSV Calendar", AutoConfCSVCalender, AutoImportCSVCalendar) ) # Vcal - always possible res.append(('vCalendar', AutoConfVCal, AutoImportVCal)) # Outlook try: import native.outlook res.append( ("Outlook", AutoConfOutlookCalender, AutoImportOutlookCalendar) ) except: print "Failed to get outlook" pass # Evolution return res class PreviewGrid(wx.grid.Grid): def __init__(self, parent, id): wx.grid.Grid.__init__(self, parent, id, style=wx.WANTS_CHARS) wx.grid.EVT_GRID_CELL_LEFT_DCLICK(self, self.OnLeftDClick) # (Taken from the demo) I do this because I don't like the default # behaviour of not starting the cell editor on double clicks, but # only a second click. def OnLeftDClick(self, evt): if self.CanEnableCellControl(): self.EnableCellEditControl() class ImportDialog(wx.Dialog): "The dialog for importing phonebook stuff" # these are presented in the UI and are what the user can select. additional # column names are available but not specified possiblecolumns=["<ignore>", "First Name", "Last Name", "Middle Name", "Name", "Nickname", "Email Address", "Web Page", "Fax", "Home Street", "Home City", "Home Postal Code", "Home State", "Home Country/Region", "Home Phone", "Home Fax", "Mobile Phone", "Home Web Page", "Business Street", "Business City", "Business Postal Code", "Business State", "Business Country/Region", "Business Web Page", "Business Phone", "Business Fax", "Pager", "Company", "Notes", "Private", "Category", "Categories"] bp_columns=[ # BitPim CSV fields 'names_title', 'names_first', 'names_middle', 'names_last', 'names_full', 'names_nickname', 'addresses_type', 'addresses_company', 'addresses_street', 'addresses_street2', 'addresses_city', 'addresses_state', 'addresses_postalcode', 'addresses_country', 'numbers_number', 'numbers_type', 'numbers_speeddial', 'emails_email', 'emails_type', 'urls_url', 'urls_type', 'categories_category', 'ringtones_ringtone', 'ringtones_use', 'wallpapers_wallpaper', 'wallpapers_use', 'memos_memo', 'flags_secret' ] # used for the filtering - any of the named columns have to be present for the data row # to be considered to have that type of column filternamecolumns=["First Name", "Last Name", "Middle Name", "Name", "Nickname"] filternumbercolumns=["Home Phone", "Home Fax", "Mobile Phone", "Business Phone", "Business Fax", "Pager", "Fax", "Phone"] filterhomeaddresscolumns=["Home Street", "Home City", "Home Postal Code", "Home State", "Home Country/Region"] filterbusinessaddresscolumns=["Business Street", "Business City", "Business Postal Code", "Business State", "Business Country/Region"] filteraddresscolumns=filterhomeaddresscolumns+filterbusinessaddresscolumns+["Address"] filteremailcolumns=["Email Address", "Email Addresses"] # used in mapping column names above into bitpim phonebook fields addressmap={ 'Street': 'street', 'City': 'city', 'Postal Code': 'postalcode', 'State': 'state', 'Country/Region': 'country', } namemap={ 'First Name': 'first', 'Last Name': 'last', 'Middle Name': 'middle', 'Name': 'full', 'Nickname': 'nickname' } numbermap={ "Home Phone": 'home', "Home Fax": 'fax', "Mobile Phone": 'cell', "Business Phone": 'office', "Business Fax": 'fax', "Pager": 'pager', "Fax": 'fax' } def __init__(self, parent, id, title, style=wx.CAPTION|wx.MAXIMIZE_BOX|\ wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER): wx.Dialog.__init__(self, parent, id=id, title=title, style=style) self.possiblecolumns+=self.bp_columns vbs=wx.BoxSizer(wx.VERTICAL) t,sz=self.gethtmlhelp() w=wx.html.HtmlWindow(self, -1, size=sz, style=wx.html.HW_SCROLLBAR_NEVER) w.SetPage(t) vbs.Add(w, 0, wx.EXPAND|wx.ALL,5) self.getcontrols(vbs) cfg=lambda key: wx.GetApp().config.ReadInt("importdialog/filter"+key, False) # Only records with ... row hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticText(self, -1, "Only rows with "), 0, wx.ALL|wx.ALIGN_CENTRE,2) self.wname=wx.CheckBox(self, wx.NewId(), "a name") self.wname.SetValue(cfg("name")) hbs.Add(self.wname, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7) self.wnumber=wx.CheckBox(self, wx.NewId(), "a number") self.wnumber.SetValue(cfg("phonenumber")) hbs.Add(self.wnumber, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7) self.waddress=wx.CheckBox(self, wx.NewId(), "an address") self.waddress.SetValue(cfg("postaladdress")) hbs.Add(self.waddress, 0, wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE,7) self.wemail=wx.CheckBox(self, wx.NewId(), "an email") self.wemail.SetValue(cfg("emailaddress")) hbs.Add(self.wemail, 0, wx.LEFT|wx.ALIGN_CENTRE,7) cats=wx.GetApp().config.Read("importdialog/filtercategories", "") if len(cats): self.categorieswanted=cats.split(";") else: self.categorieswanted=None self.categoriesbutton=wx.Button(self, wx.NewId(), "Categories...") hbs.Add(self.categoriesbutton, 0, wx.EXPAND|wx.LEFT|wx.RIGHT|wx.ALIGN_CENTRE, 10) self.categorieslabel=wx.StaticText(self, -1, "") if self.categorieswanted is None: self.categorieslabel.SetLabel("*ANY*") else: self.categorieslabel.SetLabel("; ".join(self.categorieswanted)) hbs.Add(self.categorieslabel, 1, wx.ALIGN_LEFT|wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, 5) vbs.Add(hbs,0, wx.EXPAND|wx.ALL,5) # Preview grid row self.preview=PreviewGrid(self, wx.NewId()) self.preview.CreateGrid(10,10) self.preview.SetColLabelSize(0) self.preview.SetRowLabelSize(0) self.preview.SetMargins(1,0) vbs.Add(self.preview, 1, wx.EXPAND|wx.ALL, 5) # Static line and buttons vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5) self.SetSizer(vbs) for w in self.wname, self.wnumber, self.waddress, self.wemail: wx.EVT_CHECKBOX(self, w.GetId(), self.DataNeedsUpdate) wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) wx.EVT_CLOSE(self, self.OnClose) wx.EVT_BUTTON(self, self.categoriesbutton.GetId(), self.OnCategories) guiwidgets.set_size("importdialog", self, 90) self.DataNeedsUpdate() def DataNeedsUpdate(self, _=None): "The preview data needs to be updated" self.needsupdate=True wx.CallAfter(self.UpdateData) def OnGridCellChanged(self, event): "Called when the user has changed one of the columns" self.columns[event.GetCol()]=self.preview.GetCellValue(0, event.GetCol()) self.wcolumnsname.SetValue("Custom") if self.wname.GetValue() or self.wnumber.GetValue() or self.waddress.GetValue() or self.wemail.GetValue(): self.DataNeedsUpdate() def OnClose(self, event=None): # save various config pieces guiwidgets.save_size("importdialog", self.GetRect()) cfg=lambda key, value: wx.GetApp().config.WriteInt("importdialog/filter"+key, value) cfg("name", self.wname.GetValue()) cfg("phonenumber", self.wnumber.GetValue()) cfg("postaladdress", self.waddress.GetValue()) cfg("emailaddress", self.wemail.GetValue()) if self.categorieswanted is None: cats="" else: cats=";".join(self.categorieswanted) wx.GetApp().config.Write("importdialog/filtercategories", cats) wx.GetApp().config.Flush() if event is not None: event.Skip() def OnOk(self,_): "Ok button was pressed" if self.preview.IsCellEditControlEnabled(): self.preview.HideCellEditControl() self.preview.SaveEditControlValue() self.OnClose() # for some reason this isn't called automatically self.EndModal(wx.ID_OK) def __build_entry(self, rec): entry={} # emails emails=[] if rec.has_key('Email Address'): for e in rec['Email Address']: if isinstance(e, dict): emails.append(e) else: emails.append({'email': e}) del rec['Email Address'] if rec.has_key("Email Addresses"): for e in rec['Email Addresses']: emails.append({'email': e}) del rec["Email Addresses"] if len(emails): entry['emails']=emails # addresses for prefix,fields in \ ( ("Home", self.filterhomeaddresscolumns), ("Business", self.filterbusinessaddresscolumns) ): addr={} for k in fields: if k in rec: # it has a field for this type shortk=k[len(prefix)+1:] addr['type']=prefix.lower() addr[self.addressmap[shortk]]=rec[k] del rec[k] if len(addr): if prefix=="Business" and rec.has_key("Company"): # fill in company info addr['type']=prefix.lower() addr['company']=rec["Company"] if not entry.has_key("addresses"): entry["addresses"]=[] entry["addresses"].append(addr) # address (dict form of addresses) if rec.has_key("Address"): # ensure result key exists if not entry.has_key("addresses"): entry["addresses"]=[] # find the company name company=rec.get("Company", None) for a in rec["Address"]: if a["type"]=="business": a["company"]=company addr={} for k in ("type", "company", "street", "street2", "city", "state", "postalcode", "country"): v=a.get(k, None) if v is not None: addr[k]=v entry["addresses"].append(addr) del rec["Address"] # numbers numbers=[] for field in self.filternumbercolumns: if field!="Phone" and rec.has_key(field): for val in rec[field]: numbers.append({'type': self.numbermap[field], 'number': phonenumber.normalise(val)}) del rec[field] # phones (dict form of numbers) if rec.has_key("Phone"): mapping={"business": "office", "business fax": "fax", "home fax": "fax"} for val in rec["Phone"]: number={"type": mapping.get(val["type"], val["type"]), "number": phonenumber.normalise(val["number"])} sd=val.get('speeddial', None) if sd is not None: number.update({ 'speeddial': sd }) numbers.append(number) del rec["Phone"] if len(numbers): entry["numbers"]=numbers # names name={} for field in self.filternamecolumns: if field in rec: name[self.namemap[field]]=rec[field] del rec[field] if len(name): entry["names"]=[name] # notes if rec.has_key("Notes"): notes=[] for note in rec["Notes"]: notes.append({'memo': note}) del rec["Notes"] entry["memos"]=notes # web pages urls=[] for type, key in ( (None, "Web Page"), ("home", "Home Web Page"), ("business", "Business Web Page") ): if rec.has_key(key): for url in rec[key]: if isinstance(url, dict): u=url else: u={'url': url} if type is not None: u['type']=type urls.append(u) del rec[key] if len(urls): entry["urls"]=urls # categories cats=[] if rec.has_key("Category"): for cat in rec['Category']: cats.append({'category': cat}) del rec["Category"] if rec.has_key("Categories"): # multiple entries in the field, semi-colon seperated if isinstance(rec['Categories'], list): for cat in rec['Categories']: cats.append({'category': cat}) else: for cat in rec['Categories'].split(';'): cats.append({'category': cat}) del rec['Categories'] if len(cats): entry["categories"]=cats # wallpapers l=[] r=rec.get('Wallpapers', None) if r is not None: if isinstance(r, list): l=[{'wallpaper': x, 'use': 'call' } for x in r] else: l=[{'wallpaper': x, 'use': 'call' } for x in r.split(';')] del rec['Wallpapers'] if len(l): entry['wallpapers']=l # ringtones l=[] r=rec.get('Ringtones', None) if r is not None: if isinstance(r, list): l=[{'ringtone': x, 'use': 'call'} for x in r] else: l=[{'ringtone': x, 'use': 'call'} for x in r.split(';')] del rec['Ringtones'] if len(l): entry['ringtones']=l # flags flags=[] if rec.has_key("Private"): private=True # lets see how they have done false if rec["Private"].lower() in ("false", "no", 0, "0"): private=False flags.append({'secret': private}) del rec["Private"] if len(flags): entry["flags"]=flags # unique serials serial={} for k in rec.keys(): if k.startswith("UniqueSerial-"): v=rec[k] del rec[k] k=k[len("UniqueSerial-"):] serial[k]=v if len(serial): assert serial.has_key("sourcetype") if len(serial)>1: # ie more than just sourcetype entry["serials"]=[serial] # Did we forget anything? # Company is part of other fields if rec.has_key("Company"): del rec["Company"] if len(rec): raise Exception( "Internal conversion failed to complete.\nStill to do: %s" % rec) return entry def __build_bp_entry(self, rec): entry={} for idx,col in enumerate(self.columns): # build the entry from the colum data key=col[:col.find('_')] field=col[col.find('_')+1:] v=rec[idx] if not len(v): v=None if not entry.has_key(key): entry[key]=[] done=False for field_idx,n in enumerate(entry[key]): if not n.has_key(field): entry[key][field_idx][field]=v done=True break if not done: entry[key].append({ field: v }) # go through and delete all blanks fields/dicts for k,e in entry.items(): for i1,d in enumerate(e): for k2,item in d.items(): if item is None: del entry[k][i1][k2] else: if k2=='speeddial': d[k2]=int(item) l=[x for x in entry[k] if len(x)] if len(l): entry[k]=l else: del entry[k] return entry def GetFormattedData(self): "Returns the data in BitPim phonebook format" bp_csv=True for c in self.columns: if c=="<ignore>": continue if c not in self.bp_columns: bp_csv=False break res={} count=0 for record in self.data: if bp_csv: res[count]=self.__build_bp_entry(record) else: # make a dict of the record rec={} for n in range(len(self.columns)): c=self.columns[n] if c=="<ignore>": continue if record[n] is None or len(record[n])==0: continue if c not in self.bp_columns: bp_csv=False if c in self.filternumbercolumns or c in \ ["Category", "Notes", "Business Web Page", "Home Web Page", "Web Page", "Notes", "Phone", "Address", "Email Address"]: # these are multivalued if not rec.has_key(c): rec[c]=[] rec[c].append(record[n]) else: rec[c]=record[n] # entry is what we are building. fields are removed from rec as we process them res[count]=self.__build_entry(rec) count+=1 return res def GetExtractCategoriesFunction(self): res="" for col,name in enumerate(self.columns): if name=="Categories": res+="_getpreviewformatted(row[%d], %s).split(';') + " % (col, `name`) elif name=="Category": res+="_getpreviewformatted(row[%d], %s) + " % (col, `name`) res+="[]" fn=compile(res, "_GetExtractCategoriesFunction_", 'eval') return lambda row: eval(fn, globals(), {'row': row}) def OnCategories(self, _): # find all categories in current unfiltered data savedcolumns,saveddata=self.columns, self.data if self.categorieswanted is not None: # we have to re-read the data if currently filtering categories! This is # because it would contain only the currently selected categories. self.ReReadData() catfn=self.GetExtractCategoriesFunction() cats=[] for row in self.data: for c in catfn(row): if c not in cats: cats.append(c) cats.sort() if len(cats) and cats[0]=="": cats=cats[1:] self.columns,self.data=savedcolumns, saveddata dlg=CategorySelectorDialog(self, self.categorieswanted, cats) if dlg.ShowModal()==wx.ID_OK: self.categorieswanted=dlg.GetCategories() if self.categorieswanted is None: self.categorieslabel.SetLabel("*ALL*") else: self.categorieslabel.SetLabel("; ".join(self.categorieswanted)) self.DataNeedsUpdate() dlg.Destroy() def UpdateData(self): "Actually update the preview data" if not self.needsupdate: return self.needsupdate=False # reread the data self.ReReadData() # category filtering if self.categorieswanted is not None: newdata=[] catfn=self.GetExtractCategoriesFunction() for row in self.data: for cat in catfn(row): if cat in self.categorieswanted: newdata.append(row) break self.data=newdata # name/number/address/email filtering if self.wname.GetValue() or self.wnumber.GetValue() or self.waddress.GetValue() or self.wemail.GetValue(): newdata=[] for rownum in range(len(self.data)): # generate a list of fields for which this row has data fields=[] # how many filters are required req=0 # how many are present present=0 for n in range(len(self.columns)): v=self.data[rownum][n] if v is not None and len(v): fields.append(self.columns[n]) for widget,filter in ( (self.wname, self.filternamecolumns), (self.wnumber, self.filternumbercolumns), (self.waddress, self.filteraddresscolumns), (self.wemail, self.filteremailcolumns) ): if widget.GetValue(): req+=1 for f in fields: if f in filter: present+=1 break if req>present: break if present==req: newdata.append(self.data[rownum]) self.data=newdata self.FillPreview() UpdateData=guihelper.BusyWrapper(UpdateData) def FillPreview(self): self.preview.BeginBatch() if self.preview.GetNumberCols(): self.preview.DeleteCols(0,self.preview.GetNumberCols()) self.preview.DeleteRows(0,self.preview.GetNumberRows()) self.preview.ClearGrid() numrows=len(self.data) if numrows: numcols=max(map(lambda x: len(x), self.data)) else: numcols=len(self.columns) # add header row editor=wx.grid.GridCellChoiceEditor(self.possiblecolumns, False) self.preview.AppendRows(1) self.preview.AppendCols(numcols) for col in range(numcols): self.preview.SetCellValue(0, col, self.columns[col]) self.preview.SetCellEditor(0, col, editor) attr=wx.grid.GridCellAttr() attr.SetBackgroundColour(wx.GREEN) attr.SetFont(wx.Font(10,wx.SWISS, wx.NORMAL, wx.BOLD)) attr.SetReadOnly(not self.headerrowiseditable) self.preview.SetRowAttr(0,attr) # add each row oddattr=wx.grid.GridCellAttr() oddattr.SetBackgroundColour("OLDLACE") oddattr.SetReadOnly(True) evenattr=wx.grid.GridCellAttr() evenattr.SetBackgroundColour("ALICE BLUE") evenattr.SetReadOnly(True) for row in range(numrows): self.preview.AppendRows(1) for col in range(numcols): s=_getpreviewformatted(self.data[row][col], self.columns[col]) if len(s): self.preview.SetCellValue(row+1, col, s) self.preview.SetRowAttr(row+1, (evenattr,oddattr)[row%2]) self.preview.AutoSizeColumns() self.preview.AutoSizeRows() self.preview.EndBatch() def _getpreviewformatted(value, column): if value is None: return "" if isinstance(value, dict): if column=="Email Address": value="%s (%s)" %(value["email"], value["type"]) elif column=="Web Page": value="%s (%s)" %(value["url"], value["type"]) elif column=="Phone": value="%s (%s)" %(phonenumber.format(value["number"]), value["type"]) elif column=="Address": v=[] for f in ("company", "pobox", "street", "street2", "city", "state", "postalcode", "country"): vv=value.get(f, None) if vv is not None: v.append(vv) assert len(v) v[0]=v[0]+" (%s)" %(value['type'],) value="\n".join(v) else: print "don't know how to convert dict",value,"for preview column",column assert False elif isinstance(value, list): if column=="Email Addresses": value="\n".join(value) elif column=="Categories": value=";".join(value) else: print "don't know how to convert list",value,"for preview column",column assert False return common.strorunicode(value) class CategorySelectorDialog(wx.Dialog): def __init__(self, parent, categorieswanted, categoriesavailable): wx.Dialog.__init__(self, parent, title="Import Category Selector", style=wx.CAPTION|wx.MAXIMIZE_BOX|\ wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER) #, size=(640,480)) vbs=wx.BoxSizer(wx.VERTICAL) hbs=wx.BoxSizer(wx.HORIZONTAL) self.selected=wx.RadioButton(self, wx.NewId(), "Selected Below", style=wx.RB_GROUP) self.any=wx.RadioButton(self, wx.NewId(), "Any/All") hbs.Add(self.selected, 0, wx.ALL, 5) hbs.Add(self.any, 0, wx.ALL, 5) vbs.Add(hbs, 0, wx.ALL, 5) self.categoriesavailable=categoriesavailable self.cats=wx.CheckListBox(self, wx.NewId(), choices=categoriesavailable) vbs.Add(self.cats, 1, wx.EXPAND|wx.ALL, 5) vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5) if categorieswanted is None: self.any.SetValue(True) self.selected.SetValue(False) else: self.any.SetValue(False) self.selected.SetValue(True) for c in categorieswanted: try: self.cats.Check(categoriesavailable.index(c)) except ValueError: pass # had one selected that wasn't in list wx.EVT_CHECKLISTBOX(self, self.cats.GetId(), self.OnCatsList) self.SetSizer(vbs) vbs.Fit(self) def OnCatsList(self, _): self.any.SetValue(False) self.selected.SetValue(True) def GetCategories(self): if self.any.GetValue(): return None return [self.categoriesavailable[x] for x in range(len(self.categoriesavailable)) if self.cats.IsChecked(x)] class ImportCSVDialog(ImportDialog): delimiternames={ '\t': "Tab", ' ': "Space", ',': "Comma" } def __init__(self, filename, parent, id, title): self.headerrowiseditable=True self.filename=filename self.UpdatePredefinedColumns() ImportDialog.__init__(self, parent, id, title) def gethtmlhelp(self): "Returns tuple of help text and size" bg=self.GetBackgroundColour() 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), \ (600,100) def getcontrols(self, vbs): data=common.opentextfile(self.filename).read() # turn all EOL chars into \n and then ensure only one \n terminates each line data=data.replace("\r", "\n") oldlen=-1 while len(data)!=oldlen: oldlen=len(data) data=data.replace("\n\n", "\n") self.rawdata=data self.qualifier=DSV.guessTextQualifier(self.rawdata) if self.qualifier is None or len(self.qualifier)==0: self.qualifier='"' self.data=DSV.organizeIntoLines(self.rawdata, textQualifier=self.qualifier) self.delimiter=DSV.guessDelimiter(self.data) # sometimes it picks the letter 'w' if self.delimiter is not None and self.delimiter.lower() in "abcdefghijklmnopqrstuvwxyz": self.delimiter=None if self.delimiter is None: if self.filename.lower().endswith("tsv"): self.delimiter="\t" else: self.delimiter="," # complete processing the data otherwise we can't guess if first row is headers self.data=DSV.importDSV(self.data, delimiter=self.delimiter, textQualifier=self.qualifier, errorHandler=DSV.padRow) # Delimter and Qualifier row hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticText(self, -1, "Delimiter"), 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE, 2) self.wdelimiter=wx.ComboBox(self, wx.NewId(), self.PrettyDelimiter(self.delimiter), choices=self.delimiternames.values(), style=wx.CB_DROPDOWN|wx.WANTS_CHARS) hbs.Add(self.wdelimiter, 1, wx.EXPAND|wx.ALL, 2) hbs.Add(wx.StaticText(self, -1, "Text Qualifier"), 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE,2) self.wqualifier=wx.ComboBox(self, wx.NewId(), self.qualifier, choices=['"', "'", "(None)"], style=wx.CB_DROPDOWN|wx.WANTS_CHARS) hbs.Add(self.wqualifier, 1, wx.EXPAND|wx.ALL, 2) vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) # Pre-set columns, save and header row hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticText(self, -1, "Columns"), 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE, 2) self.wcolumnsname=wx.ComboBox(self, wx.NewId(), "Custom", choices=self.predefinedcolumns+["Custom"], style=wx.CB_READONLY|wx.CB_DROPDOWN|wx.WANTS_CHARS) hbs.Add(self.wcolumnsname, 1, wx.EXPAND|wx.ALL, 2) self.wfirstisheader=wx.CheckBox(self, wx.NewId(), "First row is header") self.wfirstisheader.SetValue(DSV.guessHeaders(self.data)) hbs.Add(self.wfirstisheader, 0, wx.EXPAND|wx.ALL|wx.ALIGN_CENTRE, 5) vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) # event handlers wx.EVT_CHECKBOX(self, self.wfirstisheader.GetId(), self.OnHeaderToggle) wx.grid.EVT_GRID_CELL_CHANGE(self, self.OnGridCellChanged) wx.EVT_TEXT(self, self.wdelimiter.GetId(), self.OnDelimiterChanged) wx.EVT_TEXT(self, self.wqualifier.GetId(), self.OnQualifierChanged) wx.EVT_TEXT(self, self.wcolumnsname.GetId(), self.OnColumnsNameChanged) def PrettyDelimiter(self, delim): "Returns a pretty version of the delimiter (eg Tab, Space instead of \t, ' ')" assert delim is not None if delim in self.delimiternames: return self.delimiternames[delim] return delim def UpdatePredefinedColumns(self): """Updates the list of pre-defined column names. We look for files with an extension of .pdc in the resource directory. The first line of the file is the description, and each remaining line corresponds to a column""" self.predefinedcolumns=[] for i in guihelper.getresourcefiles("*.pdc"): f=common.opentextfile(i) self.predefinedcolumns.append(f.readline().strip()) f.close() def OnHeaderToggle(self, _): self.columns=None self.DataNeedsUpdate() def OnDelimiterChanged(self, _): "Called when the user has changed the delimiter" text=self.wdelimiter.GetValue() if hasattr(self, "lastwdelimitervalue") and self.lastwdelimitervalue==text: print "on delim changed ignored" return if len(text)!=1: if text in self.delimiternames.values(): for k in self.delimiternames: if self.delimiternames[k]==text: text=k else: if len(text)==0: text="Comma" else: text=text[-1] if text in self.delimiternames: text=self.delimiternames[text] self.wdelimiter.SetValue(text) self.delimiter=text self.columns=None self.DataNeedsUpdate() # these calls cause another OnDelimiterChanged callback to happen, so we have to stop the loop self.lastwdelimitervalue=self.wdelimiter.GetValue() wx.CallAfter(self.wdelimiter.SetInsertionPointEnd) wx.CallAfter(self.wdelimiter.SetMark, 0,len(self.wdelimiter.GetValue())) def OnQualifierChanged(self,_): "Called when the user has changed the qualifier" # Very similar to the above function text=self.wqualifier.GetValue() if hasattr(self, "lastwqualifiervalue") and self.lastwqualifiervalue==text: return if len(text)!=1: if text=='(None)': text=None else: if len(text)==0: self.wqualifier.SetValue('(None)') text=None else: text=text[-1] self.wqualifier.SetValue(text) self.qualifier=text self.columns=None self.DataNeedsUpdate() self.lastwqualifiervalue=self.wqualifier.GetValue() wx.CallAfter(self.wqualifier.SetInsertionPointEnd) wx.CallAfter(self.wqualifier.SetMark, 0,len(self.wqualifier.GetValue())) def OnColumnsNameChanged(self,_): if self.wcolumnsname.GetValue()=="Custom": return str=self.wcolumnsname.GetValue() for file in guihelper.getresourcefiles("*.pdc"): f=common.opentextfile(file) desc=f.readline().strip() if desc==str: self.columns=map(string.strip, f.readlines()) for i in range(len(self.columns)): if self.columns[i] not in self.possiblecolumns: print self.columns[i],"is not a valid column name!" self.columns[i]="<ignore>" self.DataNeedsUpdate() f.close() return f.close() print "didn't find pdc for",str def ReReadData(self): self.data=DSV.organizeIntoLines(self.rawdata, textQualifier=self.qualifier) self.data=DSV.importDSV(self.data, delimiter=self.delimiter, textQualifier=self.qualifier, errorHandler=DSV.padRow) self.FigureOutColumns() def FigureOutColumns(self): "Initialize the columns variable, using header row if there is one" numcols=max(map(lambda x: len(x), self.data)) # normalize number of columns for row in self.data: while len(row)<numcols: row.append('') guesscols=False if not hasattr(self, "columns") or self.columns is None: self.columns=["<ignore>"]*numcols guesscols=True while len(self.columns)<numcols: self.columns.append("<ignore>") self.columns=self.columns[:numcols] if not self.wfirstisheader.GetValue(): return headers=self.data[0] self.data=self.data[1:] if not guesscols: return mungedcolumns=[] for c in self.possiblecolumns: mungedcolumns.append("".join(filter(lambda x: x in "abcdefghijklmnopqrstuvwxyz0123456789", c.lower()))) # look for header in possible columns for col,header in zip(range(numcols), headers): if header in self.possiblecolumns: self.columns[col]=header continue h="".join(filter(lambda x: x in "abcdefghijklmnopqrstuvwxyz0123456789", header.lower())) if h in mungedcolumns: self.columns[col]=self.possiblecolumns[mungedcolumns.index(h)] continue # here is where we would do some mapping class ImportOutlookDialog(ImportDialog): # the order of this mapping matters .... importmapping=( # first column is field in Outlook # second column is field in dialog (ImportDialog.possiblecolumns) ('FirstName', "First Name" ), ('LastName', "Last Name"), ('MiddleName', "Middle Name"), # ('FullName', ), -- this includes the prefix (aka title in Outlook) and the suffix # ('Title', ), -- the prefix (eg Dr, Mr, Mrs) ('Subject', "Name"), # this is first middle last suffix - note no prefix! # ('Suffix', ), -- Jr, Sr, III etc ('NickName', "Nickname"), ('Email1Address', "Email Address"), ('Email2Address', "Email Address"), ('Email3Address', "Email Address"), # Outlook is seriously screwed over web pages. It treats the Business Home Page # and Web Page as the same field, so we can't really tell the difference. ('WebPage', "Web Page"), ('OtherFaxNumber', "Fax" ), ('HomeAddressStreet', "Home Street"), ('HomeAddressCity', "Home City" ), ('HomeAddressPostalCode',"Home Postal Code" ), ('HomeAddressState', "Home State"), ('HomeAddressCountry', "Home Country/Region" ), ('HomeTelephoneNumber', "Home Phone"), ('Home2TelephoneNumber', "Home Phone"), ('HomeFaxNumber', "Home Fax"), ('MobileTelephoneNumber',"Mobile Phone"), ('PersonalHomePage', "Home Web Page"), ('BusinessAddressStreet',"Business Street"), ('BusinessAddressCity', "Business City"), ('BusinessAddressPostalCode', "Business Postal Code"), ('BusinessAddressState', "Business State"), ('BusinessAddressCountry', "Business Country/Region"), # ('BusinessHomePage',), -- no use, see Web Page above ('BusinessTelephoneNumber', "Business Phone"), ('Business2TelephoneNumber',"Business Phone"), ('BusinessFaxNumber', "Business Fax"), ('PagerNumber', "Pager"), ('CompanyName', "Company"), ('Body', "Notes"), # yes, really ('Categories', "Categories"), ('EntryID', "UniqueSerial-EntryID"), ) # These are all the fields we do nothing about ## ('Anniversary', ), ## ('AssistantName', ), ## ('AssistantTelephoneNumber', ), ## ('Birthday', ), ## ('BusinessAddress', ), ## ('BusinessAddressPostOfficeBox', ), ## ('CallbackTelephoneNumber', ), ## ('CarTelephoneNumber', ), ## ('Children', ), ## ('Class', ), ## ('CompanyAndFullName', ), ## ('CompanyLastFirstNoSpace', ), ## ('CompanyLastFirstSpaceOnly', ), ## ('CompanyMainTelephoneNumber', ), ## ('ComputerNetworkName', ), ## ('ConversationIndex', ), ## ('ConversationTopic', ), ## ('CreationTime', ), ## ('CustomerID', ), ## ('Department', ), ## ('FTPSite', ), ## ('FileAs', ), ## ('FullNameAndCompany', ), ## ('Gender', ), ## ('GovernmentIDNumber', ), ## ('Hobby', ), ## ('HomeAddress', ), ## ('HomeAddressPostOfficeBox', ), ## ('ISDNNumber', ), ## ('Importance', ), ## ('Initials', ), ## ('InternetFreeBusyAddress', ), ## ('JobTitle', ), ## ('Journal', ), ## ('Language', ), ## ('LastFirstAndSuffix', ), ## ('LastFirstNoSpace', ), ## ('LastFirstNoSpaceCompany', ), ## ('LastFirstSpaceOnly', ), ## ('LastFirstSpaceOnlyCompany', ), ## ('LastModificationTime', ), ## ('LastNameAndFirstName', ), ## ('MAPIOBJECT', ), ## ('MailingAddress', ), ## ('MailingAddressCity', ), ## ('MailingAddressCountry', ), ## ('MailingAddressPostalCode', ), ## ('MailingAddressState', ), ## ('MailingAddressStreet', ), ## ('ManagerName', ), ## ('MessageClass', ), ## ('Mileage', ), ## ('NetMeetingAlias', ), ## ('NetMeetingServer', ), ## ('NoAging', ), ## ('OfficeLocation', ), ## ('OrganizationalIDNumber', ), ## ('OtherAddress', ), ## ('OtherAddressCity', ), ## ('OtherAddressCountry', ), ## ('OtherAddressPostOfficeBox', ), ## ('OtherAddressPostalCode', ), ## ('OtherAddressState', ), ## ('OtherAddressStreet', ), ## ('OtherTelephoneNumber', ), ## ('OutlookInternalVersion', ), ## ('OutlookVersion', ), ## ('Parent', ), ## ('PrimaryTelephoneNumber', ), ## ('Profession', ), ## ('RadioTelephoneNumber', ), ## ('ReferredBy', ), ## ('Saved', ), ## ('SelectedMailingAddress', ), ## ('Sensitivity', ), ## ('Size', ), ## ('Spouse', ), ## ('TTYTDDTelephoneNumber', ), ## ('TelexNumber', ), ## ('UnRead', ), ## ('User1', ), ## ('User2', ), ## ('User3', ), ## ('User4', ), importmappingdict={} for o,i in importmapping: importmappingdict[o]=i def __init__(self, parent, id, title, outlook): self.headerrowiseditable=False self.outlook=outlook ImportDialog.__init__(self, parent, id, title) def gethtmlhelp(self): "Returns tuple of help text and size" bg=self.GetBackgroundColour() 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()), \ (600,30) def getcontrols(self, vbs): hbs=wx.BoxSizer(wx.HORIZONTAL) # label hbs.Add(wx.StaticText(self, -1, "Folder"), 0, wx.ALL|wx.ALIGN_CENTRE, 2) # where the folder name goes self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2) # browse button self.folderbrowse=wx.Button(self, wx.NewId(), "Browse ...") hbs.Add(self.folderbrowse, 0, wx.EXPAND|wx.ALL, 2) vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) wx.EVT_BUTTON(self, self.folderbrowse.GetId(), self.OnBrowse) # sort out folder id=wx.GetApp().config.Read("outlook/contacts", "") self.folder=self.outlook.getfolderfromid(id, True) wx.GetApp().config.Write("outlook/contacts", self.outlook.getfolderid(self.folder)) self.folderctrl.SetValue(self.outlook.getfoldername(self.folder)) def OnBrowse(self, _): p=self.outlook.pickfolder() if p is None: return # user hit cancel self.folder=p wx.GetApp().config.Write("outlook/contacts", self.outlook.getfolderid(self.folder)) self.folderctrl.SetValue(self.outlook.getfoldername(self.folder)) self.DataNeedsUpdate() def ReReadData(self): # this can take a really long time if the user doesn't spot the dialog # asking for permission to access email addresses :-) items=self.outlook.getcontacts(self.folder, self.importmappingdict.keys()) # work out what keys are actually present keys={} for item in items: for k in item.keys(): keys[k]=1 # We now need to produce columns with BitPim names not the Outlook ones. # mappings are in self.importmapping want=[] for o,i in self.importmapping: if o in keys.keys(): want.append(o) # want now contains list of Outlook keys we want, and the order we want them in self.columns=[self.importmappingdict[k] for k in want] # deal with serials self.columns.append("UniqueSerial-FolderID") self.columns.append("UniqueSerial-sourcetype") moredata=[ self.outlook.getfolderid(self.folder), "outlook"] # build up data self.data=[] for item in items: row=[] for k in want: v=item.get(k, None) v=common.strorunicode(v) row.append(v) self.data.append(row+moredata) class ImportVCardDialog(ImportDialog): keymapper={ "name": "Name", "notes": "Notes", "uid": "UniqueSerial-uid", "last name": "Last Name", "first name": "First Name", "middle name": "Middle Name", "nickname": "Nickname", "categories": "Categories", "email": "Email Address", "url": "Web Page", "phone": "Phone", "address": "Address", "organisation": "Company", "wallpapers": "Wallpapers", "ringtones": "Ringtones" } def __init__(self, filename, parent, id, title): self.headerrowiseditable=False self.filename=filename self.vcardcolumns,self.vcarddata=None,None ImportDialog.__init__(self, parent, id, title) def gethtmlhelp(self): "Returns tuple of help text and size" bg=self.GetBackgroundColour() 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()), \ (600,30) def getcontrols(self, vbs): # no extra controls return def ReReadData(self): if self.vcardcolumns is None or self.vcarddata is None: self.vcardcolumns,self.vcarddata=self.parsevcards(common.opentextfile(self.filename)) self.columns=self.vcardcolumns self.data=self.vcarddata def parsevcards(self, file): # returns columns, data data=[] keys={} for vc in vcard.VCards(vcard.VFile(file)): v=vc.getdata() data.append(v) for k in v: keys[k]=1 keys=keys.keys() # sort them into a natural order self.sortkeys(keys) # remove the ones we have no mapping for if __debug__: for k in keys: if _getstringbase(k)[0] not in self.keymapper: print "vcard import: no map for key "+k keys=[k for k in keys if _getstringbase(k)[0] in self.keymapper] columns=[self.keymapper[_getstringbase(k)[0]] for k in keys] # build up defaults defaults=[] for c in columns: if c in self.possiblecolumns: defaults.append("") else: defaults.append(None) # deal with serial/UniqueId if len([c for c in columns if c.startswith("UniqueSerial-")]): columns.append("UniqueSerial-sourcetype") extra=["vcard"] else: extra=[] # do some data munging newdata=[] for row in data: line=[] for i,k in enumerate(keys): line.append(row.get(k, defaults[i])) newdata.append(line+extra) # return our hard work return columns, newdata # name parts, name, nick, emails, urls, phone, addresses, categories, memos # things we ignore: title, prefix, suffix, organisational unit _preferredorder=["first name", "middle name", "last name", "name", "nickname", "phone", "address", "email", "url", "organisation", "categories", "notes"] def sortkeys(self, keys): po=self._preferredorder def funkycmpfunc(x, y, po=po): x=_getstringbase(x) y=_getstringbase(y) if x==y: return 0 if x[0]==y[0]: # if the same base, use the number to compare return cmp(x[1], y[1]) # find them in the preferred order list # (for some bizarre reason python doesn't have a method corresponding to # string.find on lists or tuples, and you only get index on lists # which throws an exception on not finding the item try: pos1=po.index(x[0]) except ValueError: pos1=-1 try: pos2=po.index(y[0]) except ValueError: pos2=-1 if pos1<0 and pos2<0: return cmp(x[0], y[0]) if pos1<0 and pos2>=0: return 1 if pos2<0 and pos1>=0: return -1 assert pos1>=0 and pos2>=0 return cmp(pos1, pos2) keys.sort(funkycmpfunc) def _getstringbase(v): mo=re.match(r"^(.*?)(\d+)$", v) if mo is None: return (v,1) return mo.group(1), int(mo.group(2)) class ImportEvolutionDialog(ImportVCardDialog): def __init__(self, parent, id, title, evolution): self.headerrowiseditable=False self.evolution=evolution self.evocolumns=None self.evodata=None ImportDialog.__init__(self, parent, id, title) def gethtmlhelp(self): "Returns tuple of help text and size" bg=self.GetBackgroundColour() 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()), \ (600,30) def getcontrols(self, vbs): hbs=wx.BoxSizer(wx.HORIZONTAL) # label hbs.Add(wx.StaticText(self, -1, "Folder"), 0, wx.ALL|wx.ALIGN_CENTRE, 2) # where the folder name goes self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2) # browse button self.folderbrowse=wx.Button(self, wx.NewId(), "Browse ...") hbs.Add(self.folderbrowse, 0, wx.EXPAND|wx.ALL, 2) vbs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) wx.EVT_BUTTON(self, self.folderbrowse.GetId(), self.OnBrowse) # sort out folder id=wx.GetApp().config.Read("evolution/contacts", "") self.folder=self.evolution.getfolderfromid(id, True) print "folder is",self.folder wx.GetApp().config.Write("evolution/contacts", self.evolution.getfolderid(self.folder)) self.folderctrl.SetValue(self.evolution.getfoldername(self.folder)) def OnBrowse(self, _): p=self.evolution.pickfolder(self.folder) if p is None: return # user hit cancel self.folder=p wx.GetApp().config.Write("evolution/contacts", self.evolution.getfolderid(self.folder)) self.folderctrl.SetValue(self.evolution.getfoldername(self.folder)) self.evocolumns=None self.evodata=None self.DataNeedsUpdate() def ReReadData(self): if self.evocolumns is not None and self.evodata is not None: self.columns=self.evocolumns self.data=self.evodata return vcards="\r\n".join(self.evolution.getcontacts(self.folder)) columns,data=self.parsevcards(StringIO.StringIO(vcards)) columns.append("UniqueSerial-folderid") columns.append("UniqueSerial-sourcetype") moredata=[self.folder, "evolution"] for row in data: row.extend(moredata) self.evocolumns=self.columns=columns self.evodata=self.data=data class ImportQtopiaDesktopDialog(ImportDialog): # the order of this mapping matters .... importmapping=( # first column is field in Qtopia # second column is field in dialog (ImportDialog.possiblecolumns) ('FirstName', "First Name" ), ('LastName', "Last Name" ), ('MiddleName', "Middle Name"), ('Nickname', "Nickname"), ('Emails', "Email Addresses"), ('HomeStreet', "Home Street"), ('HomeCity', "Home City"), ('HomeZip', "Home Postal Code"), ('HomeState', "Home State" ), ('HomeCountry', "Home Country/Region" ), ('HomePhone', "Home Phone" ), ('HomeFax', "Home Fax" ), ('HomeMobile', "Mobile Phone" ), ('BusinessMobile', "Mobile Phone" ), ('HomeWebPage', "Home Web Page" ), ('BusinessStreet', "Business Street"), ('BusinessCity', "Business City" ), ('BusinessZip', "Business Postal Code" ), ('BusinessState', "Business State" ), ('BusinessCountry', "Business Country/Region", ), ('BusinessWebPage', "Business Web Page"), ('BusinessPhone', "Business Phone"), ('BusinessFax', "Business Fax" ), ('BusinessPager', "Pager" ), ('Company', "Company" ), ('Notes', "Notes" ), ('Categories', "Categories" ), ('Uid', "UniqueSerial-uid" ), ) ## # the fields we ignore ## ('Assistant', ) ## ('Children', ) ## ('DefaultEmail', ) ## ('Department', ) ## ('Dtmid', ) ## ('FileAs', ) ## ('Gender', ) ## ('JobTitle', ) ## ('Manager', ) ## ('Office', ) ## ('Profession', ) ## ('Spouse', ) ## ('Suffix', ) ## ('Title', ) importmappingdict={} for o,i in importmapping: importmappingdict[o]=i def __init__(self, parent, id, title): self.headerrowiseditable=False self.origcolumns=self.origdata=None ImportDialog.__init__(self, parent, id, title) def gethtmlhelp(self): "Returns tuple of help text and size" bg=self.GetBackgroundColour() return '<html><body BGCOLOR="#%02X%02X%02X">Importing Qtopia Desktop Contacts..</body></html>' % (bg.Red(), bg.Green(), bg.Blue()), \ (600,30) def getcontrols(self, vbs): pass def ReReadData(self): if self.origcolumns is not None and self.origdata is not None: self.columns=self.origcolumns self.data=self.origdata return import native.qtopiadesktop filename=native.qtopiadesktop.getfilename() if not os.path.isfile(filename): wx.MessageBox(filename+" not found.", "Qtopia file not found", wx.ICON_EXCLAMATION|wx.OK) self.data={} self.columns=[] return items=native.qtopiadesktop.getcontacts() # work out what keys are actually present keys={} for item in items: for k in item.keys(): keys[k]=1 # We now need to produce columns with BitPim names not the Qtopia ones. # mappings are in self.importmapping want=[] for o,i in self.importmapping: if o in keys.keys(): want.append(o) # want now contains list of Qtopia keys we want, and the order we want them in self.columns=[self.importmappingdict[k] for k in want] # deal with serials self.columns.append("UniqueSerial-sourcetype") moredata=[ "qtopiadesktop"] # build up data self.data=[] for item in items: row=[] for k in want: v=item.get(k, None) row.append(v) self.data.append(row+moredata) self.origdata=self.data self.origcolumns=self.columns # The eGroupware login handling is a bit of a mess. Feel free to fix it. class eGroupwareLoginDialog(wx.Dialog): __pwdsentinel="\x01\x02\x01\x09\x01\x01\x14\x15" def __init__(self, parent, module, title="Login to eGroupware"): wx.Dialog.__init__(self, parent, -1, title, size=(400,-1)) self.module=module gs=wx.GridBagSizer(5,5) for row,label in enumerate( ("URL", "Domain", "Username", "Password") ): gs.Add(wx.StaticText(self, -1, label), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL, pos=(row,0)) self.curl=wx.TextCtrl(self, -1) self.cdomain=wx.TextCtrl(self, -1) self.cuser=wx.TextCtrl(self, -1) self.cpassword=wx.TextCtrl(self, -1, style=wx.TE_PASSWORD) self.csavepassword=wx.CheckBox(self, -1, "Save") for row,widget in enumerate( (self.curl, self.cdomain, self.cuser) ): gs.Add(widget, flag=wx.EXPAND, pos=(row,1), span=(1,2)) gs.Add(self.cpassword, flag=wx.EXPAND, pos=(3,1)) gs.Add(self.csavepassword, flag=wx.ALIGN_CENTRE, pos=(3,2)) gs.AddGrowableCol(1) self.cmessage=wx.StaticText(self, -1, "Please enter your details") gs.Add(self.cmessage, flag=wx.EXPAND, pos=(4,0), span=(1,3)) vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(gs, 0, wx.EXPAND|wx.ALL,5) vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5) # set initial values cfg=wx.GetApp().config self.curl.SetValue(cfg.Read("egroupware/url", "http://server.example.com/egroupware")) self.cdomain.SetValue(cfg.Read("egroupware/domain", "default")) try: import getpass defuser=getpass.getuser() except: defuser="user" self.cuser.SetValue(cfg.Read("egroupware/user", defuser)) p=cfg.Read("egroupware/password", "") if len(p): self.csavepassword.SetValue(True) self.cpassword.SetValue(self.__pwdsentinel) self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) # make the window a decent width self.SetDimensions(-1, -1, 500, -1, wx.SIZE_USE_EXISTING) wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) self.session=None def OnOk(self, evt): try: self.session=self._GetSession() evt.Skip() # end modal self.OnClose() return except Exception,e: self.cmessage.SetLabel(str(e)) # go around again def GetSession(self, auto=False): """Returns the Session object from the eGroupware library @param auto: If true then the user interface doesn't have to be shown""" if auto: try: self.session=self._GetSession() return self.session except Exception,e: self.cmessage.SetLabel(str(e)) self.ShowModal() return self.session def _GetSession(self): # lets see if the user has given us sensible params if self.curl.GetValue()=="http://server.example.com/egroupware": raise Exception("You must set the URL for the server") if len(self.cpassword.GetValue())==0: raise Exception("You must fill in the password") password=self.cpassword.GetValue() if password==self.__pwdsentinel: password=common.obfus_decode(wx.GetApp().config.Read("egroupware/password", "")) try: return self.module.getsession(self.curl.GetValue(), self.cuser.GetValue(), password, self.cdomain.GetValue()) finally: del password def OnClose(self, event=None): cfg=wx.GetApp().config cfg.Write("egroupware/url", self.curl.GetValue()) cfg.Write("egroupware/domain", self.cdomain.GetValue()) cfg.Write("egroupware/user", self.cuser.GetValue()) if self.csavepassword.GetValue(): p=self.cpassword.GetValue() if p!=self.__pwdsentinel: cfg.Write("egroupware/password", common.obfus_encode(p)) else: cfg.DeleteEntry("egroupware/password") class ImporteGroupwareDialog(ImportDialog): ID_CHANGE=wx.NewId() def __init__(self, parent, id, title, module): self.headerrowiseditable=False self.module=module ImportDialog.__init__(self, parent, id, title) self.sp=None def gethtmlhelp(self): "Returns tuple of help text and size" bg=self.GetBackgroundColour() return '<html><body BGCOLOR="#%02X%02X%02X">Importing eGroupware Contacts..</body></html>' % (bg.Red(), bg.Green(), bg.Blue()), \ (600,30) def getcontrols(self, vbs): # need url, username, password and domain fields hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticText(self, -1, "URL"), 0, wx.ALIGN_CENTRE|wx.ALL,2) self.curl=wx.StaticText(self, -1) hbs.Add(self.curl, 3, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2) hbs.Add(wx.StaticText(self, -1, "Domain"), 0, wx.ALIGN_CENTRE|wx.ALL,2) self.cdomain=wx.StaticText(self, -1) hbs.Add(self.cdomain, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2) hbs.Add(wx.StaticText(self, -1, "User"), 0, wx.ALIGN_CENTRE|wx.ALL,2) self.cuser=wx.StaticText(self, -1) hbs.Add(self.cuser, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2) self.cchange=wx.Button(self, self.ID_CHANGE, "Change ...") hbs.Add(self.cchange, 0, wx.ALL, 2) vbs.Add(hbs,0,wx.ALL,5) wx.EVT_BUTTON(self, self.ID_CHANGE, self.OnChangeCreds) self.setcreds() def OnChangeCreds(self,_): dlg=eGroupwareLoginDialog(self, self.module) newsp=dlg.GetSession() if newsp is not None: self.sp=newsp self.setcreds() self.DataNeedsUpdate() def setcreds(self): cfg=wx.GetApp().config self.curl.SetLabel(cfg.Read("egroupware/url", "http://server.example.com/egroupware")) self.cdomain.SetLabel(cfg.Read("egroupware/domain", "default")) try: import getpass defuser=getpass.getuser() except: defuser="user" self.cuser.SetLabel(cfg.Read("egroupware/user", defuser)) _preferred=( "Name", "First Name", "Middle Name", "Last Name", "Address", "Address2", "Email Address", "Email Address2", "Home Phone", "Mobile Phone", "Business Fax", "Pager", "Business Phone", "Notes", "Business Web Page", "Categories" ) def ReReadData(self): if self.sp is None: self.sp=eGroupwareLoginDialog(self, self.module).GetSession(auto=True) self.setcreds() self.data=[] self.columns=[] if self.sp is None: self.EndModal(wx.ID_CANCEL) return # work out what columns we have entries=[i for i in self.sp.getcontactspbformat()] cols=[] for e in entries: for k in e: if k not in cols: cols.append(k) # now put columns in prefered order cols.sort() self.columns=[] for p in self._preferred: if p in cols: self.columns.append(p) cols=[c for c in cols if c not in self.columns] self.columns.extend(cols) # make data self.data=[] for e in entries: self.data.append([e.get(c,None) for c in self.columns]) # strip trailing 2 off names in columns for i,c in enumerate(self.columns): if c.endswith("2"): self.columns[i]=c[:-1] def OnFileImportCSVContacts(parent): dlg=wx.FileDialog(parent, "Import CSV file", wildcard="CSV files (*.csv)|*.csv|Tab Separated file (*.tsv)|*.tsv|All files|*", style=wx.OPEN|wx.HIDE_READONLY|wx.CHANGE_DIR) path=None if dlg.ShowModal()==wx.ID_OK: path=dlg.GetPath() dlg.Destroy() if path is None: return dlg=ImportCSVDialog(path, parent, -1, "Import CSV file") data=None if dlg.ShowModal()==wx.ID_OK: data=dlg.GetFormattedData() dlg.Destroy() if data is not None: parent.phonewidget.importdata(data, merge=True) def OnFileImportVCards(parent): dlg=wx.FileDialog(parent, "Import vCards file", wildcard="vCard files (*.vcf)|*.vcf|All files|*", style=wx.OPEN|wx.HIDE_READONLY|wx.CHANGE_DIR) path=None if dlg.ShowModal()==wx.ID_OK: path=dlg.GetPath() dlg.Destroy() if path is None: return dlg=ImportVCardDialog(path, parent, -1, "Import vCard file") data=None if dlg.ShowModal()==wx.ID_OK: data=dlg.GetFormattedData() dlg.Destroy() if data is not None: parent.phonewidget.importdata(data, merge=True) def OnFileImportQtopiaDesktopContacts(parent): dlg=ImportQtopiaDesktopDialog(parent, -1, "Import Qtopia Desktop Contacts") data=None if dlg.ShowModal()==wx.ID_OK: data=dlg.GetFormattedData() dlg.Destroy() if data is not None: parent.phonewidget.importdata(data, merge=True) def OnFileImportOutlookContacts(parent): import native.outlook dlg=ImportOutlookDialog(parent, -1, "Import Outlook Contacts", native.outlook) data=None if dlg.ShowModal()==wx.ID_OK: data=dlg.GetFormattedData() dlg.Destroy() native.outlook.releaseoutlook() if data is not None: parent.phonewidget.importdata(data, merge=True) def OnFileImportEvolutionContacts(parent): import native.evolution dlg=ImportEvolutionDialog(parent, -1, "Import Evolution Contacts", native.evolution) data=None if dlg.ShowModal()==wx.ID_OK: data=dlg.GetFormattedData() dlg.Destroy() if data is not None: parent.phonewidget.importdata(data, merge=True) def OnFileImporteGroupwareContacts(parent): import native.egroupware dlg=ImporteGroupwareDialog(parent, -1, "Import eGroupware Contacts", native.egroupware) data=None if dlg.ShowModal()==wx.ID_OK: data=dlg.GetFormattedData() dlg.Destroy() if data is not None: parent.phonewidget.importdata(data, merge=True) def OnFileImportOutlookCalendar(parent): import native.outlook import outlook_calendar import pubsub dlg=outlook_calendar.OutlookImportCalDialog(parent, -1, 'Import Outlook Calendar') res=dlg.ShowModal() if res==wx.ID_OK: # ask phonebook to merge our categories pubsub.publish(pubsub.MERGE_CATEGORIES, dlg.get_categories()[:]) # and save the new data calendar_r={ 'calendar': dlg.get() } parent.calendarwidget.populate(calendar_r) parent.calendarwidget.populatefs(calendar_r) elif res==outlook_calendar.OutlookImportCalDialog.ID_ADD: # ask phonebook to merge our categories pubsub.publish(pubsub.MERGE_CATEGORIES, dlg.get_categories()[:]) # get existing data calendar_r=parent.calendarwidget.getdata({}).get('calendar', {}) # and add the imported data calendar_r.update(dlg.get()) # and save it parent.calendarwidget.populate({ 'calendar': calendar_r } ) parent.calendarwidget.populatefs({ 'calendar': calendar_r } ) # all done dlg.Destroy() native.outlook.releaseoutlook() def OnFileImportVCal(parent): import vcal_calendar import pubsub dlg=vcal_calendar.VcalImportCalDialog(parent, -1, 'Import vCalendar') res=dlg.ShowModal() if res==wx.ID_OK: # ask phonebook to merge our categories pubsub.publish(pubsub.MERGE_CATEGORIES, dlg.get_categories()[:]) # and save the new data calendar_r={ 'calendar': dlg.get() } parent.calendarwidget.populate(calendar_r) parent.calendarwidget.populatefs(calendar_r) elif res==vcal_calendar.VcalImportCalDialog.ID_ADD: # ask phonebook to merge our categories pubsub.publish(pubsub.MERGE_CATEGORIES, dlg.get_categories()[:]) # get existing data calendar_r=parent.calendarwidget.getdata({}).get('calendar', {}) # and add the imported data calendar_r.update(dlg.get()) # and save it parent.calendarwidget.populate({ 'calendar': calendar_r } ) parent.calendarwidget.populatefs({ 'calendar': calendar_r } ) # all done dlg.Destroy() def OnFileImportCSVCalendar(parent): import csv_calendar import pubsub dlg=csv_calendar.CSVImportDialog(parent, -1, 'Import CSV Calendar') res=dlg.ShowModal() if res==wx.ID_OK: # ask phonebook to merge our categories pubsub.publish(pubsub.MERGE_CATEGORIES, dlg.get_categories()[:]) # and save the new data calendar_r={ 'calendar': dlg.get() } parent.calendarwidget.populate(calendar_r) parent.calendarwidget.populatefs(calendar_r) elif res==csv_calendar.CSVImportDialog.ID_ADD: # ask phonebook to merge our categories pubsub.publish(pubsub.MERGE_CATEGORIES, dlg.get_categories()[:]) # get existing data calendar_r=parent.calendarwidget.getdata({}).get('calendar', {}) # and add the imported data calendar_r.update(dlg.get()) # and save it parent.calendarwidget.populate({ 'calendar': calendar_r } ) parent.calendarwidget.populatefs({ 'calendar': calendar_r } ) # all done dlg.Destroy() ### ### AUTO_SYNC ### def AutoConfOutlookCalender(parent, folder, filters): import native.outlook import outlook_calendar config=() dlg=outlook_calendar.OutlookAutoConfCalDialog(parent, -1, 'Config Outlook Calendar AutoSync Settings', folder, filters) return AutoConfCommon(dlg) def AutoConfCSVCalender(parent, folder, filters): import csv_calendar config=() dlg=csv_calendar.CSVAutoConfCalDialog(parent, -1, 'Config CSV Calendar AutoSync Settings', folder, filters) return AutoConfCommon(dlg) def AutoConfVCal(parent, folder, filters): import vcal_calendar dlg=vcal_calendar.VCalAutoConfCalDialog(parent, -1, 'Config VCal AutoSync Settings', folder, filters) return AutoConfCommon(dlg) def AutoConfCommon(dlg): config=() res=dlg.ShowModal() if res==wx.ID_OK: config=(dlg.GetFolder(), dlg.GetFilter()) dlg.Destroy() return res, config def AutoImportOutlookCalendar(parent, folder, filters): import native.outlook import outlook_calendar calendar_r=outlook_calendar.ImportCal(folder, filters) return AutoImportCalCommon(parent, calendar_r) def AutoImportVCal(parent, folder, filters): import vcal_calendar calendar_r=vcal_calendar.ImportCal(folder, filters) return AutoImportCalCommon(parent, calendar_r) def AutoImportCSVCalendar(parent, folder, filters): import csv_calendar calendar_r=csv_calendar.ImportCal(folder, filters) return AutoImportCalCommon(parent, calendar_r) def AutoImportCalCommon(parent, calendar_r): parent.calendarwidget.populate(calendar_r) parent.calendarwidget.populatefs(calendar_r) res=1 return res ### ### EXPORTS ### def GetPhonebookExports(): res=[] # Vcards - always possible res.append( ("vCards...", "Export the phonebook to vCards", OnFileExportVCards) ) # eGroupware - always possible res.append( ("eGroupware...", "Export the phonebook to eGroupware", OnFileExporteGroupware) ) # CSV - always possible res.append( ('CSV Contacts...', 'Export the phonebook to CSV', OnFileExportCSV)) res.append( ('CSV Calendar...', 'Export the calendar to CSV', OnFileExportCSVCalendar) ) return res class BaseExportDialog(wx.Dialog): def __init__(self, parent, title, style=wx.CAPTION|wx.MAXIMIZE_BOX|\ wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE): wx.Dialog.__init__(self, parent, id=-1, title=title, style=style) self._phonebook_module=phonebook.thephonewidget def GetSelectionGui(self, parent): "Returns a sizer containing the gui for selecting which items and which fields are exported" hbs=wx.BoxSizer(wx.HORIZONTAL) lsel=len(self._phonebook_module.GetSelectedRows()) lall=len(self._phonebook_module._data) rbs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Rows"), wx.VERTICAL) self.rows_selected=wx.RadioButton(self, wx.NewId(), "Selected (%d)" % (lsel,), style=wx.RB_GROUP) self.rows_all=wx.RadioButton(self, wx.NewId(), "All (%d)" % (lall,)) rbs.Add(self.rows_selected, 0, wx.EXPAND|wx.ALL, 2) rbs.Add(self.rows_all, 0, wx.EXPAND|wx.ALL, 2) hbs.Add(rbs, 3, wx.EXPAND|wx.ALL, 5) self.rows_selected.SetValue(lsel>1) self.rows_all.SetValue(not lsel>1) vbs2=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Fields"), wx.VERTICAL) cb=[] for c in ("Everything", "Phone Numbers", "Addresses", "Email Addresses"): cb.append(wx.CheckBox(self, -1, c)) vbs2.Add(cb[-1], 0, wx.EXPAND|wx.ALL, 5) for c in cb: c.Enable(False) cb[0].SetValue(True) hbs.Add(vbs2, 4, wx.EXPAND|wx.ALL, 5) return hbs def GetPhoneBookItems(self, includecount=False): """Returns each item in the phonebook based on the settings for all vs selected. The fields are also trimmed to match what the user requested. @param includecount: If this is true then the return is a tuple of (item, number, max) and you can use number and max to update a progress bar. Note that some items may be skipped (eg if the user only wants records with phone numbers and some records don't have them) """ data=self._phonebook_module._data if self.rows_all.GetValue(): rowkeys=data.keys() else: rowkeys=self._phonebook_module.GetSelectedRowKeys() for i,k in enumerate(rowkeys[:]): # we use a dup of rowkeys since it can be altered while exporting # ::TODO:: look at fields setting if includecount: yield data[k],i,len(rowkeys) else: yield data[k] class ExportVCardDialog(BaseExportDialog): dialects=vcard.profiles.keys() dialects.sort() default_dialect='vcard2' def __init__(self, parent, title): BaseExportDialog.__init__(self, parent, title) # make the ui vbs=wx.BoxSizer(wx.VERTICAL) bs=wx.BoxSizer(wx.HORIZONTAL) bs.Add(wx.StaticText(self, -1, "File"), 0, wx.ALL|wx.ALIGN_CENTRE, 5) self.filenamectrl=wx.TextCtrl(self, -1, wx.GetApp().config.Read("vcard/export-file", "bitpim.vcf")) bs.Add(self.filenamectrl, 1, wx.ALL|wx.EXPAND, 5) self.browsectrl=wx.Button(self, wx.NewId(), "Browse...") bs.Add(self.browsectrl, 0, wx.ALL|wx.EXPAND, 5) wx.EVT_BUTTON(self, self.browsectrl.GetId(), self.OnBrowse) vbs.Add(bs, 0, wx.EXPAND|wx.ALL, 5) hbs2=wx.BoxSizer(wx.HORIZONTAL) # dialects hbs=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Dialect"), wx.VERTICAL) self.dialectctrl=wx.ListBox(self, -1, style=wx.LB_SINGLE, choices=[vcard.profiles[d]['description'] for d in self.dialects]) default=wx.GetApp().config.Read("vcard/export-format", self.default_dialect) if default not in self.dialects: default=self.default_dialect self.dialectctrl.SetSelection(self.dialects.index(default)) hbs.Add(self.dialectctrl, 1, wx.ALL|wx.EXPAND, 5) hbs2.Add(hbs, 2, wx.EXPAND|wx.ALL, 10) hbs2.Add(self.GetSelectionGui(self), 5, wx.EXPAND|wx.ALL, 5) vbs.Add(hbs2, 0, wx.EXPAND|wx.ALL, 5) vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5) self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) def OnBrowse(self, _): dlg=wx.FileDialog(self, defaultFile=self.filenamectrl.GetValue(), wildcard="vCard files (*.vcf)|*.vcf", style=wx.SAVE|wx.CHANGE_DIR) if dlg.ShowModal()==wx.ID_OK: self.filenamectrl.SetValue(os.path.join(dlg.GetDirectory(), dlg.GetFilename())) dlg.Destroy() def OnOk(self, _): # do export filename=self.filenamectrl.GetValue() dialect=None for k,v in vcard.profiles.items(): if v['description']==self.dialectctrl.GetStringSelection(): dialect=k break assert dialect is not None # ::TODO:: ask about overwriting existing file f=open(filename, "wt") for record in self.GetPhoneBookItems(): print >>f, vcard.output_entry(record, vcard.profiles[dialect]['profile']) f.close() # save settings since we were succesful wx.GetApp().config.Write("vcard/export-file", filename) wx.GetApp().config.Write("vcard/export-format", dialect) wx.GetApp().config.Flush() self.EndModal(wx.ID_OK) class ExportCSVDialog(BaseExportDialog): __pb_keys=( ('names', ('title', 'first', 'middle', 'last', 'full', 'nickname')), ('addresses', ('type', 'company', 'street', 'street2', 'city', 'state', 'postalcode', 'country')), ('numbers', ('number', 'type', 'speeddial')), ('emails', ('email', 'type')), ('urls', ('url', 'type')), ('categories', ('category',)), ('ringtones', ('ringtone', 'use')), ('wallpapers', ('wallpaper', 'use')), ('memos', ('memo',)), ('flags', ('secret',)) ) def __init__(self, parent, title): super(ExportCSVDialog, self).__init__(parent, title) # make the ui vbs=wx.BoxSizer(wx.VERTICAL) bs=wx.BoxSizer(wx.HORIZONTAL) bs.Add(wx.StaticText(self, -1, "File"), 0, wx.ALL|wx.ALIGN_CENTRE, 5) self.filenamectrl=wx.TextCtrl(self, -1, "bitpim.csv") bs.Add(self.filenamectrl, 1, wx.ALL|wx.EXPAND, 5) self.browsectrl=wx.Button(self, wx.NewId(), "Browse...") bs.Add(self.browsectrl, 0, wx.ALL|wx.EXPAND, 5) vbs.Add(bs, 0, wx.EXPAND|wx.ALL, 5) # selection GUI vbs.Add(self.GetSelectionGui(self), 5, wx.EXPAND|wx.ALL, 5) # the buttons vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5) # event handlers wx.EVT_BUTTON(self, self.browsectrl.GetId(), self.OnBrowse) wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) # all done self.SetSizer(vbs) self.SetAutoLayout(True) vbs.Fit(self) def OnBrowse(self, _): dlg=wx.FileDialog(self, defaultFile=self.filenamectrl.GetValue(), wildcard="CSV files (*.cvs)|*.csv", style=wx.SAVE|wx.CHANGE_DIR) if dlg.ShowModal()==wx.ID_OK: self.filenamectrl.SetValue(os.path.join(dlg.GetDirectory(), dlg.GetFilename())) dlg.Destroy() def OnOk(self, _): # do export filename=self.filenamectrl.GetValue() # find out the length of each key key_count={} for e in self.__pb_keys: key_count[e[0]]=0 for record in self.GetPhoneBookItems(): for k in record: if key_count.has_key(k): key_count[k]=max(key_count[k], len(record[k])) f=open(filename, 'wt') l=[] for e in self.__pb_keys: if key_count[e[0]]: ll=[e[0]+'_'+x for x in e[1]] l+=ll*key_count[e[0]] f.write(','.join(l)+'\n') for record in self.GetPhoneBookItems(): ll=[] for e in self.__pb_keys: key=e[0] if key_count[key]: for i in range(key_count[key]): try: entry=record[key][i] except (KeyError, IndexError): entry={} for field in e[1]: ll.append('"'+str(entry.get(field, '')).replace('"', '')+'"') f.write(','.join(ll)+'\n') f.flush() f.close() self.EndModal(wx.ID_OK) class ExporteGroupwareDialog(BaseExportDialog): ID_REFRESH=wx.NewId() ID_CHANGE=wx.NewId() _categorymessage="eGroupware doesn't create categories correctly via XML-RPC, so you should manually create them via the web interface. " \ "Click to see which ones should be manually created." def __init__(self, parent, title, module): BaseExportDialog.__init__(self, parent, title) self.module=module # make the ui vbs=wx.BoxSizer(wx.VERTICAL) hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.StaticText(self, -1, "URL"), 0, wx.ALIGN_CENTRE|wx.ALL,2) self.curl=wx.StaticText(self, -1) hbs.Add(self.curl, 3, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2) hbs.Add(wx.StaticText(self, -1, "Domain"), 0, wx.ALIGN_CENTRE|wx.ALL,5) self.cdomain=wx.StaticText(self, -1) hbs.Add(self.cdomain, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 2) hbs.Add(wx.StaticText(self, -1, "User"), 0, wx.ALIGN_CENTRE|wx.ALL,5) self.cuser=wx.StaticText(self, -1) hbs.Add(self.cuser, 1, wx.ALIGN_CENTRE_VERTICAL|wx.ALL, 5) self.cchange=wx.Button(self, self.ID_CHANGE, "Change ...") hbs.Add(self.cchange, 0, wx.ALL, 2) vbs.Add(hbs,0,wx.ALL,5) wx.EVT_BUTTON(self, self.ID_CHANGE, self.OnChangeCreds) self.sp=None self.setcreds() vbs.Add(self.GetSelectionGui(self), 0, wx.EXPAND|wx.ALL, 5) # category checker bs2=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Category Checker"), wx.HORIZONTAL) bs2.Add(wx.Button(self, self.ID_REFRESH, "Check"), 0, wx.ALL, 5) self.categoryinfo=wx.TextCtrl(self, -1, self._categorymessage, style=wx.TE_MULTILINE|wx.TE_READONLY) bs2.Add(self.categoryinfo, 1, wx.EXPAND|wx.ALL, 2) vbs.Add(bs2, 0, wx.EXPAND|wx.ALL, 5) vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) bs2=wx.StaticBoxSizer(wx.StaticBox(self, -1, "Export Progress"), wx.VERTICAL) self.progress=wx.Gauge(self, -1, style=wx.GA_HORIZONTAL|wx.GA_SMOOTH) self.progress_text=wx.StaticText(self, -1, "") bs2.Add(self.progress, 0, wx.EXPAND|wx.ALL, 5) bs2.Add(self.progress_text, 0, wx.EXPAND|wx.ALL, 5) vbs.Add(bs2, 0, wx.EXPAND|wx.ALL, 5) vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.CANCEL|wx.HELP), 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALL, 5) self.SetSizer(vbs) self.SetAutoLayout(True) # for some reason wx decides to make the dialog way taller # than needed. i can't figure out why. sometimes wx just # makes you go grrrrrrrr vbs.Fit(self) wx.EVT_BUTTON(self, self.ID_REFRESH, self.CategoryCheck) wx.EVT_BUTTON(self, wx.ID_OK, self.OnOk) def OnChangeCreds(self,_): dlg=eGroupwareLoginDialog(self, self.module) newsp=dlg.GetSession() if newsp is not None: self.sp=newsp self.setcreds() def setcreds(self): cfg=wx.GetApp().config self.curl.SetLabel(cfg.Read("egroupware/url", "http://server.example.com/egroupware")) self.cdomain.SetLabel(cfg.Read("egroupware/domain", "default")) try: import getpass defuser=getpass.getuser() except: defuser="user" self.cuser.SetLabel(cfg.Read("egroupware/user", defuser)) def CategoryCheck(self, evt=None): if evt is not None: evt.Skip() if self.sp is None: self.sp=eGroupwareLoginDialog(self, self.module).GetSession(auto=True) self.setcreds() self.FindWindowById(wx.ID_OK).Enable(self.sp is not None) self.FindWindowById(self.ID_REFRESH).Enable(self.sp is not None) # find which categories are missing cats=[] for e in self.GetPhoneBookItems(): for c in e.get("categories", []): cc=c["category"] if cc not in cats: cats.append(cc) cats.sort() egcats=[v['name'] for v in self.sp.getcategories()] nocats=[c for c in cats if c not in egcats] if len(nocats): self.categoryinfo.SetValue("eGroupware doesn't have the following categories, which you should manually add:\n\n"+", ".join(nocats)) else: self.categoryinfo.SetValue("eGroupware has all the categories you use") def OnOk(self, _): if self.sp is None: self.sp=eGroupwareLoginDialog(self, self.module).GetSession(auto=True) self.setcreds() if self.sp is None: return doesntexistaction=None catsmodified=True # load categories # write out each one setmax=-1 for record,pos,max in self.GetPhoneBookItems(includecount=True): if catsmodified: # get the list of categories cats=dict( [(v['name'], v['id']) for v in self.sp.getcategories()] ) if max!=setmax: setmax=max self.progress.SetRange(max) self.progress.SetValue(pos) self.progress_text.SetLabel(nameparser.formatsimplename(record.get("names", [{}])[0])) wx.SafeYield() catsmodified,rec=self.FormatRecord(record, cats) if rec['id']!=0: # we have an id, but the record could have been deleted on the eg server, so # we check if not self.sp.doescontactexist(rec['id']): if doesntexistaction is None: dlg=eGroupwareEntryGoneDlg(self, rec['fn']) dlg.ShowModal() action=dlg.GetAction() if dlg.ForAll(): doesntexistaction=action else: action=doesntexistaction if action==self._ACTION_RECREATE: rec['id']=0 elif action==self._ACTION_IGNORE: continue elif action==self._ACTION_DELETE: found=False for serial in record["serials"]: if serial["sourcetype"]=="bitpim": phonebook.thephonewidget.DeleteBySerial(serial) found=True break assert found continue newid=self.sp.writecontact(rec) found=False for serial in record["serials"]: if serial["sourcetype"]=="bitpim": phonebook.thephonewidget.UpdateSerial(serial, {"sourcetype": "egroupware", "id": newid}) found=True break assert found self.EndModal(wx.ID_OK) def FormatRecord(self, record, categories): """Convert record into egroupware fields We return a tuple of (egw formatted record, if we update categories) If the second part is True, the categories should be re-read from the server after writing the record.""" catsmodified=False # note that mappings must be carefully chosen to ensure that importing from egroupware # and then re-exporting doesn't change anything. res={'id': 0} # zero means create new record # find existing egroupware id for i in record.get("serials", []): if i['sourcetype']=='egroupware': res['id']=i['id'] break # name (nb we don't do prefix or suffix since bitpim doesn't know about them) res['n_given'],res['n_middle'],res['n_family']=nameparser.getparts(record.get("names", [{}])[0]) for nf in 'n_given', 'n_middle', 'n_family': if res[nf] is None: res[nf]="" # set None fields to blank string res['fn']=nameparser.formatsimplename(record.get("names", [{}])[0]) # addresses for t,prefix in ("business", "adr_one"), ("home", "adr_two"): a={} adr=record.get("addresses", []) for i in adr: if i['type']==t: for p2,k in ("_street", "street"), ("_locality", "city"), ("_region", "state"), \ ("_postalcode", "postalcode"), ("_countryname", "country"): res[prefix+p2]=i.get(k, "") if t=="business": res['org_name']=i.get("company","") break # email if "emails" in record: # this means we ignore emails without a type, but that can't be avoided with # how egroupware works for t,k in ("business", "email"), ("home", "email_home"): for i in record["emails"]: if i.get("type",None)==t: res[k]=i.get("email") res[k+"_type"]="INTERNET" break # categories cats={} for cat in record.get("categories", []): c=cat['category'] v=categories.get(c, None) if v is None: catsmodified=True for i in xrange(0,-999999,-1): if `i` not in cats: break else: i=`v` cats[i]=str(c) res['cat_id']=cats # phone numbers # t,k is bitpim type, egroupware type for t,k in ("home", "tel_home"), ("cell", "tel_cell"), ('fax','tel_fax'), \ ('pager', 'tel_pager'), ('office', 'tel_work'): if "numbers" in record: v="" for i in record['numbers']: if i['type']==t: v=i['number'] break res[k]=phonenumber.format(v) # miscellaneous others if "memos" in record: memos=record.get("memos", []) memos+=[{}] res['note']=memos[0].get("memo","") if "urls" in record: urls=record.get("urls", []) u="" for url in urls: if url.get("type", None)=="business": u=url["url"] break if len(u)==0: urls+=[{'url':""}] u=urls[0]["url"] res['url']=u # that should be everything return catsmodified,res _ACTION_RECREATE=1 _ACTION_IGNORE=2 _ACTION_DELETE=3 class eGroupwareEntryGoneDlg(wx.Dialog): choices=( ("Re-create entry on server", ExporteGroupwareDialog._ACTION_RECREATE), ("Delete the entry in BitPim", ExporteGroupwareDialog._ACTION_DELETE), ("Ignore this entry for now", ExporteGroupwareDialog._ACTION_IGNORE) ) def __init__(self, parent, details): wx.Dialog.__init__(self, parent, -1, title="Entry deleted on server") vbs=wx.BoxSizer(wx.VERTICAL) vbs.Add(wx.StaticText(self, -1, "The entry for \"%s\" has\nbeen deleted on the server." % (details,) ), 0, wx.EXPAND|wx.ALL, 5) self.rb=wx.RadioBox(self, -1, "Action to take", choices=[t for t,a in self.choices]) vbs.Add(self.rb, 0, wx.EXPAND|wx.ALL, 5) self.always=wx.CheckBox(self, -1, "Always take this action") vbs.Add(self.always, 0, wx.ALL|wx.ALIGN_CENTRE, 5) vbs.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL,5) vbs.Add(self.CreateButtonSizer(wx.OK|wx.HELP), 0, wx.ALIGN_CENTER|wx.ALL, 5) self.SetSizer(vbs) vbs.Fit(self) def GetAction(self): return self.choices[self.rb.GetSelection()][1] def ForAll(self): return self.always.GetValue() def OnFileExportVCards(parent): dlg=ExportVCardDialog(parent, "Export phonebook to vCards") dlg.ShowModal() dlg.Destroy() def OnFileExporteGroupware(parent): import native.egroupware dlg=ExporteGroupwareDialog(parent, "Export phonebook to eGroupware", native.egroupware) dlg.ShowModal() dlg.Destroy() def OnFileExportCSV(parent): dlg=ExportCSVDialog(parent, "Export phonebook to CSV") dlg.ShowModal() dlg.Destroy() def OnFileExportCSVCalendar(parent): import csv_calendar dlg=csv_calendar.ExportCSVDialog(parent, 'Export Calendar to CSV') dlg.ShowModal() dlg.Destroy()
importexport.py.diff
(application/octet-stream, 2.9 KB)
Index: importexport.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/importexport.py,v
retrieving revision 1.80
diff -u -r1.80 importexport.py
--- importexport.py 5 May 2005 04:15:22 -0000 1.80
+++ importexport.py 19 Sep 2005 15:42:26 -0000
@@ -62,6 +62,22 @@
return res
+def GetCalenderAutoSyncImports():
+ res=[]
+ # CSV - always possible
+ res.append( ("CSV Calendar", AutoConfCSVCalender, AutoImportCSVCalendar) )
+ # Vcal - always possible
+ res.append(('vCalendar', AutoConfVCal, AutoImportVCal))
+ # Outlook
+ try:
+ import native.outlook
+ res.append( ("Outlook", AutoConfOutlookCalender, AutoImportOutlookCalendar) )
+ except:
+ print "Failed to get outlook"
+ pass
+ # Evolution
+
+ return res
class PreviewGrid(wx.grid.Grid):
@@ -1807,6 +1823,64 @@
dlg.Destroy()
###
+### AUTO_SYNC
+###
+
+def AutoConfOutlookCalender(parent, folder, filters):
+ import native.outlook
+ import outlook_calendar
+ config=()
+ dlg=outlook_calendar.OutlookAutoConfCalDialog(parent, -1,
+ 'Config Outlook Calendar AutoSync Settings',
+ folder, filters)
+ return AutoConfCommon(dlg)
+
+def AutoConfCSVCalender(parent, folder, filters):
+ import csv_calendar
+ config=()
+ dlg=csv_calendar.CSVAutoConfCalDialog(parent, -1,
+ 'Config CSV Calendar AutoSync Settings',
+ folder, filters)
+ return AutoConfCommon(dlg)
+
+def AutoConfVCal(parent, folder, filters):
+ import vcal_calendar
+ dlg=vcal_calendar.VCalAutoConfCalDialog(parent, -1,
+ 'Config VCal AutoSync Settings',
+ folder, filters)
+ return AutoConfCommon(dlg)
+
+def AutoConfCommon(dlg):
+ config=()
+ res=dlg.ShowModal()
+ if res==wx.ID_OK:
+ config=(dlg.GetFolder(), dlg.GetFilter())
+ dlg.Destroy()
+ return res, config
+
+def AutoImportOutlookCalendar(parent, folder, filters):
+ import native.outlook
+ import outlook_calendar
+ calendar_r=outlook_calendar.ImportCal(folder, filters)
+ return AutoImportCalCommon(parent, calendar_r)
+
+def AutoImportVCal(parent, folder, filters):
+ import vcal_calendar
+ calendar_r=vcal_calendar.ImportCal(folder, filters)
+ return AutoImportCalCommon(parent, calendar_r)
+
+def AutoImportCSVCalendar(parent, folder, filters):
+ import csv_calendar
+ calendar_r=csv_calendar.ImportCal(folder, filters)
+ return AutoImportCalCommon(parent, calendar_r)
+
+def AutoImportCalCommon(parent, calendar_r):
+ parent.calendarwidget.populate(calendar_r)
+ parent.calendarwidget.populatefs(calendar_r)
+ res=1
+ return res
+
+###
### EXPORTS
###
outlook_calendar.py
(text/plain, 23.5 KB)
### BITPIM ### ### Copyright (C) 2004 Joe Pham <[email protected]> ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the BitPim license as detailed in the LICENSE file. ### ### $Id: outlook_calendar.py,v 1.12 2005/09/12 22:19:50 djpham Exp $ "Deals with Outlook calendar import stuff" # System modules import datetime import pywintypes import sys import time # wxPython modules import wx import wx.calendar import wx.lib.mixins.listctrl as listmix # Others # My modules import bpcalendar import common import common_calendar import guiwidgets import helpids import native.outlook # common convertor functions def to_bp_date(dict, v, oc): # convert a pyTime to (y, m, d, h, m) if not isinstance(v, pywintypes.TimeType): raise TypeError, 'illegal type' if v.year>common_calendar.no_end_date[0]: return common_calendar.no_end_date return (v.year, v.month, v.day, v.hour, v.minute) def bp_repeat_str(dict, v): if v is None: return '' elif v==OutlookCalendarImportData.olRecursDaily: return 'Daily' elif v==OutlookCalendarImportData.olRecursWeekly: return 'Weekly' elif v==OutlookCalendarImportData.olRecursMonthly or \ v==OutlookCalendarImportData.olRecursMonthNth: return 'Monthly' elif v==OutlookCalendarImportData.olRecursYearly: return 'Yearly' else: return '<Unknown Value>' def convert_categories(dict, v, oc): return [x.strip() for x in v.split(",") if len(x)] def set_recurrence(item, dict, oc): oc.update_display() if not dict['repeat']: # no reccurrence, ignore dict['repeat']=None return True # get the recurrence pattern and map it to BP Calendar return oc.process_repeat(item, dict) #------------------------------------------------------------------------------- class OutlookCalendarImportData: _non_auto_sync_calendar_keys=[ # (Outlook field, BP Calendar field, convertor function) ('Subject', 'description', None), ('Location', 'location', None), ('Start', 'start', to_bp_date), ('End', 'end', to_bp_date), ('Categories', 'categories', convert_categories), ('IsRecurring', 'repeat', None), ('ReminderSet', 'alarm', None), ('ReminderMinutesBeforeStart', 'alarm_value', None), ('Importance', 'priority', None), ('Body', 'notes', None), ('AllDayEvent', 'allday', None) ] # the auto_sync calender fields do not support the "body" of the appointment # accessing this field causes an outlook warning to appear which prevents automation _auto_sync_calendar_keys=[ # (Outlook field, BP Calendar field, convertor function) ('Subject', 'description', None), ('Location', 'location', None), ('Start', 'start', to_bp_date), ('End', 'end', to_bp_date), ('Categories', 'categories', convert_categories), ('IsRecurring', 'repeat', None), ('ReminderSet', 'alarm', None), ('ReminderMinutesBeforeStart', 'alarm_value', None), ('Importance', 'priority', None), ('AllDayEvent', 'allday', None) ] _recurrence_keys=[ # (Outlook field, BP Calendar field, convertor function) ('NoEndDate', 'NoEndDate', None), ('PatternStartDate', 'PatternStartDate', to_bp_date), ('PatternEndDate', 'PatternEndDate', to_bp_date), ('Instance', 'Instance', None), ('DayOfWeekMask', 'DayOfWeekMask', None), ('Interval', 'Interval', None), ('Occurrences', 'Occurrences', None), ('RecurrenceType', 'RecurrenceType', None) ] _exception_keys=[ # (Outlook field, BP Calendar field, convertor function) ('OriginalDate', 'exception_date', to_bp_date), ('Deleted', 'deleted', None) ] _default_filter={ 'start': None, 'end': None, 'categories': None, 'rpt_events': False, 'no_alarm': False, 'ringtone': None, 'alarm_override':False, 'vibrate':False, 'alarm_value':0 } # Outlook constants olRecursDaily = native.outlook.outlook_com.constants.olRecursDaily olRecursMonthNth = native.outlook.outlook_com.constants.olRecursMonthNth olRecursMonthly = native.outlook.outlook_com.constants.olRecursMonthly olRecursWeekly = native.outlook.outlook_com.constants.olRecursWeekly olRecursYearNth = native.outlook.outlook_com.constants.olRecursYearNth olRecursYearly = native.outlook.outlook_com.constants.olRecursYearly olImportanceHigh = native.outlook.outlook_com.constants.olImportanceHigh olImportanceLow = native.outlook.outlook_com.constants.olImportanceLow olImportanceNormal = native.outlook.outlook_com.constants.olImportanceNormal def __init__(self, outlook, auto_sync_only=0): self._outlook=outlook self._data=[] self._error_list=[] self._single_data=[] self._folder=None self._filter=self._default_filter self._total_count=0 self._current_count=0 self._update_dlg=None self._exception_list=[] if auto_sync_only: self._calendar_keys=self._auto_sync_calendar_keys else: self._calendar_keys=self._non_auto_sync_calendar_keys def _accept(self, entry): s_date=entry['start'][:3] e_date=entry['end'][:3] if entry.get('repeat', False): # repeat event, must not fall outside the range if self._filter['start'] is not None and \ e_date<self._filter['start'][:3]: return False if self._filter['end'] is not None and \ s_date>self._filter['end'][:3]: return False else: # non-repeat event, must fall within the range if self._filter['start'] is not None and \ e_date<self._filter['start'][:3]: return False if self._filter['end'] is not None and \ e_date>self._filter['end'][:3]: return False # check the catefory c=self._filter.get('categories', None) if c is None or not len(c): # no categories specified => all catefories allowed. return True if len([x for x in entry['categories'] if x in c]): return True return False def _populate_entry(self, e, ce): # populate an calendar entry with outlook data ce.description=e.get('description', None) ce.location=e.get('location', None) v=e.get('priority', None) if v is not None: if v==self.olImportanceNormal: ce.priority=ce.priority_normal elif v==self.olImportanceLow: ce.priority=ce.priority_low elif v==self.olImportanceHigh: ce.priority=ce.priority_high if not self._filter.get('no_alarm', False) and \ not self._filter.get('alarm_override', False) and \ e.get('alarm', False): ce.alarm=e.get('alarm_value', 0) ce.ringtone=self._filter.get('ringtone', "") ce.vibrate=self._filter.get('vibrate', False) elif not self._filter.get('no_alarm', False) and \ self._filter.get('alarm_override', False): ce.alarm=self._filter.get('alarm_value', 0) ce.ringtone=self._filter.get('ringtone', "") ce.vibrate=self._filter.get('vibrate', False) ce.allday=e.get('allday', False) ce.start=e['start'] ce.end=e['end'] ce.notes=e.get('notes', None) v=[] for k in e.get('categories', []): v.append({ 'category': k }) ce.categories=v # look at repeat events if not e.get('repeat', False): # not a repeat event, just return return rp=bpcalendar.RepeatEntry() rt=e['repeat_type'] r_interval=e.get('repeat_interval', 0) r_dow=e.get('repeat_dow', 0) if rt==self.olRecursDaily: rp.repeat_type=rp.daily elif rt==self.olRecursWeekly: if r_interval: # weekly event rp.repeat_type=rp.weekly else: # mon-fri event rp.repeat_type=rp.daily elif rt==self.olRecursMonthly or rt==self.olRecursMonthNth: rp.repeat_type=rp.monthly else: rp.repeat_type=rp.yearly if rp.repeat_type==rp.daily: rp.interval=r_interval elif rp.repeat_type==rp.weekly or rp.repeat_type==rp.monthly: rp.interval=r_interval rp.dow=r_dow # add the list of exceptions for k in e.get('exceptions', []): rp.add_suppressed(*k[:3]) ce.repeat=rp def _generate_repeat_events(self, e): # generate multiple single events from this repeat event ce=bpcalendar.CalendarEntry() self._populate_entry(e, ce) l=[] new_e=e.copy() new_e['repeat']=False for k in ('repeat_type', 'repeat_interval', 'repeat_dow'): if new_e.has_key(k): del new_e[k] s_date=datetime.datetime(*self._filter['start']) e_date=datetime.datetime(*self._filter['end']) one_day=datetime.timedelta(1) this_date=s_date while this_date<=e_date: date_l=(this_date.year, this_date.month, this_date.day) if ce.is_active(*date_l): new_e['start']=date_l+new_e['start'][3:] new_e['end']=date_l+new_e['end'][3:] l.append(new_e.copy()) this_date+=one_day return l def get(self): res={} single_rpt=self._filter.get('rpt_events', False) for k in self._data: if self._accept(k): if k.get('repeat', False) and single_rpt: d=self._generate_repeat_events(k) else: d=[k] for n in d: ce=bpcalendar.CalendarEntry() self._populate_entry(n, ce) res[ce.id]=ce return res def get_display_data(self): cnt=0 res={} single_rpt=self._filter.get('rpt_events', False) no_alarm=self._filter.get('no_alarm', False) for k in self._data: if self._accept(k): if k.get('repeat', False) and single_rpt: d=self._generate_repeat_events(k) else: d=[k.copy()] for n in d: if no_alarm: n['alarm']=False res[cnt]=n cnt+=1 return res def get_category_list(self): l=[] for e in self._data: l+=[x for x in e.get('categories', []) if x not in l] return l def pick_folder(self): return self._outlook.pickfolder() def set_folder(self, f): if f is None: # default folder self._folder=self._outlook.getfolderfromid('', True, 'calendar') else: self._folder=f def get_folder_id(self): return self._outlook.getfolderid(self._folder) def set_folder_id(self, id): if id is None or id=="": self.set_folder(None) else: self._folder=self._outlook.getfolderfromid(id) def set_filter(self, filter): self._filter=filter def get_filter(self): return self._filter def get_folder_name(self): if self._folder is None: return '<None>' return self._outlook.getfoldername(self._folder) def read(self, folder=None, update_dlg=None): # folder from which to read if folder is not None: self._folder=folder if self._folder is None: self._folder=self._outlook.getfolderfromid('', True, 'calendar') self._update_dlg=update_dlg self._total_count=self._folder.Items.Count self._current_count=0 self._exception_list=[] self._data, self._error_list=self._outlook.getdata(self._folder, self._calendar_keys, {}, self, set_recurrence) # add in the exception list, .. or shoule we keep it separate ?? self._data+=self._exception_list def _set_repeat_dates(self, dict, r): dict['start']=r['PatternStartDate'][:3]+dict['start'][3:] dict['end']=r['PatternEndDate'][:3]+dict['end'][3:] dict['repeat_type']=r['RecurrenceType'] def _is_daily_or_weekly(self, dict, r): if r['RecurrenceType']==self.olRecursDaily or \ r['RecurrenceType']==self.olRecursWeekly: self._set_repeat_dates(dict, r) dict['repeat_interval']=r['Interval'] dict['repeat_dow']=r['DayOfWeekMask'] return True return False def _is_monthly(self, dict, r): if r['RecurrenceType']==self.olRecursMonthly or \ r['RecurrenceType']==self.olRecursMonthNth and \ r['Interval']==1: self._set_repeat_dates(dict, r) if r['RecurrenceType']==self.olRecursMonthNth: dict['repeat_interval']=r['Instance'] dict['repeat_dow']=r['DayOfWeekMask'] return True return False def _is_yearly(self, dict, r): if r['RecurrenceType']==self.olRecursYearly and \ r['Interval']==12: self._set_repeat_dates(dict, r) return True return False def _process_exceptions(self, dict, r): # check for and process exceptions for this event r_ex=r.Exceptions if not r_ex.Count: # no exception, bail return for i in range(1, r_ex.Count+1): ex=self._outlook.getitemdata(r_ex.Item(i), {}, self._exception_keys, self) dict.setdefault('exceptions', []).append(ex['exception_date']) if not ex['deleted']: # if this instance has been changed, then need to get it appt=self._outlook.getitemdata(r_ex.Item(i).AppointmentItem, {}, self._calendar_keys, self) # by definition, this instance cannot be a repeat event appt['repeat']=False appt['end']=appt['start'][:3]+appt['end'][3:] # and add it to the exception list self._exception_list.append(appt) def process_repeat(self, item, dict): # get the recurrence info that we need. rec_pat=item.GetRecurrencePattern() r=self._outlook.getitemdata(rec_pat, {}, self._recurrence_keys, self) if self._is_daily_or_weekly(dict, r) or \ self._is_monthly(dict, r) or \ self._is_yearly(dict, r): self._process_exceptions(dict, rec_pat) return True # invalide repeat type, turn this event into a regular event dict['repeat']=False dict['end']=dict['start'][:3]+dict['end'][3:] dict['notes']+=' [BITPIM: Unrecognized repeat event, repeat event discarded]' return True def update_display(self): # update the progress dialog if specified self._current_count += 1 if self._update_dlg is not None: self._update_dlg.Update(100*self._current_count/self._total_count) def has_errors(self): return bool(self._error_list) def get_error_list(self): # return a list of strings of failed items res=[] for d in self._error_list: # the format is 'YY-MM-DD hh:mm Description' if available _start=d.get('start', None) s='' if _start: if len(_start)>4: s='%02d-%02d-%02d %02d:%02d '%_start[:5] elif len(_start)>2: s='%02d-%02d-%02d '%_start[:3] _desc=d.get('description', None) if _desc: s+=_desc if not s: s='<Unknown>' res.append(s) return res #------------------------------------------------------------------------------- class OutlookImportCalDialog(common_calendar.PreviewDialog): _column_labels=[ ('description', 'Description', 400, None), ('start', 'Start', 150, common_calendar.bp_date_str), ('end', 'End', 150, common_calendar.bp_date_str), ('repeat_type', 'Repeat', 80, bp_repeat_str), ('alarm', 'Alarm', 80, common_calendar.bp_alarm_str), ('categories', 'Category', 150, common_calendar.category_str) ] ID_ADD=wx.NewId() def __init__(self, parent, id, title): self._oc=OutlookCalendarImportData(native.outlook) self._oc.set_folder(None) common_calendar.PreviewDialog.__init__(self, parent, id, title, self._column_labels, self._oc.get_display_data(), config_name='import/calendar/outlookdialog') def getcontrols(self, main_bs): hbs=wx.BoxSizer(wx.HORIZONTAL) # label hbs.Add(wx.StaticText(self, -1, "Outlook Calendar Folder:"), 0, wx.ALL|wx.ALIGN_CENTRE, 2) # where the folder name goes self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) self.folderctrl.SetValue(self._oc.get_folder_name()) hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2) # browse button id_browse=wx.NewId() hbs.Add(wx.Button(self, id_browse, 'Browse ...'), 0, wx.EXPAND|wx.ALL, 2) main_bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) wx.EVT_BUTTON(self, id_browse, self.OnBrowseFolder) def getpostcontrols(self, main_bs): main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) hbs=wx.BoxSizer(wx.HORIZONTAL) id_import=wx.NewId() hbs.Add(wx.Button(self, id_import, 'Import'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_OK, 'Replace All'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, self.ID_ADD, 'Add'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_CANCEL, 'Cancel'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) id_filter=wx.NewId() hbs.Add(wx.Button(self, id_filter, 'Filter'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_HELP, 'Help'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) main_bs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.ALL, 5) wx.EVT_BUTTON(self, id_import, self.OnImport) wx.EVT_BUTTON(self, id_filter, self.OnFilter) wx.EVT_BUTTON(self, self.ID_ADD, self.OnAdd) wx.EVT_BUTTON(self, wx.ID_HELP, lambda *_: wx.GetApp().displayhelpid(helpids.ID_DLG_CALENDAR_IMPORT)) def OnImport(self, evt): wx.BeginBusyCursor() dlg=wx.ProgressDialog('Outlook Calendar Import', 'Importing Outlook Data, please wait ...\n(Please also watch out for the Outlook Permission Request dialog)', parent=self) self._oc.read(None, dlg) self.populate(self._oc.get_display_data()) dlg.Destroy() if self._oc.has_errors(): # display the list of failed items error_dlg=wx.SingleChoiceDialog(self, 'Outlook Calendar Items that failed to import:', 'Outlook Calendar Import Error', self._oc.get_error_list()) error_dlg.ShowModal() error_dlg.Destroy() wx.EndBusyCursor() def OnBrowseFolder(self, evt): f=self._oc.pick_folder() if f is None: return # user hit cancel self._oc.set_folder(f) self.folderctrl.SetValue(self._oc.get_folder_name()) def OnFilter(self, evt): cat_list=self._oc.get_category_list() dlg=common_calendar.FilterDialog(self, -1, 'Filtering Parameters', cat_list) dlg.set(self._oc.get_filter()) if dlg.ShowModal()==wx.ID_OK: self._oc.set_filter(dlg.get()) self.populate(self._oc.get_display_data()) def OnAdd(self, evt): self.EndModal(self.ID_ADD) def get(self): return self._oc.get() def get_categories(self): return self._oc.get_category_list() #------------------------------------------------------------------------------- def ImportCal(folder, filters): _oc=OutlookCalendarImportData(native.outlook, auto_sync_only=1) _oc.set_folder_id(folder) _oc.set_filter(filters) _oc.read() res={ 'calendar':_oc.get() } return res #------------------------------------------------------------------------------- class OutlookAutoConfCalDialog(wx.Dialog): def __init__(self, parent, id, title, folder, filters, style=wx.CAPTION|wx.MAXIMIZE_BOX| \ wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER): self._oc=OutlookCalendarImportData(native.outlook, auto_sync_only=1) self._oc.set_folder_id(folder) self._oc.set_filter(filters) self.__read=False wx.Dialog.__init__(self, parent, id=id, title=title, style=style) main_bs=wx.BoxSizer(wx.VERTICAL) hbs=wx.BoxSizer(wx.HORIZONTAL) # label hbs.Add(wx.StaticText(self, -1, "Outlook Calendar Folder:"), 0, wx.ALL|wx.ALIGN_CENTRE, 2) # where the folder name goes self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) self.folderctrl.SetValue(self._oc.get_folder_name()) hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2) # browse button id_browse=wx.NewId() hbs.Add(wx.Button(self, id_browse, 'Browse ...'), 0, wx.EXPAND|wx.ALL, 2) main_bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) wx.EVT_BUTTON(self, id_browse, self.OnBrowseFolder) hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.Button(self, wx.ID_OK, 'OK'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_CANCEL, 'Cancel'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) id_filter=wx.NewId() hbs.Add(wx.Button(self, id_filter, 'Filter'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_HELP, 'Help'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) main_bs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.ALL, 5) wx.EVT_BUTTON(self, id_filter, self.OnFilter) wx.EVT_BUTTON(self, wx.ID_HELP, lambda *_: wx.GetApp().displayhelpid(helpids.ID_DLG_CALENDAR_IMPORT)) self.SetSizer(main_bs) self.SetAutoLayout(True) main_bs.Fit(self) def OnBrowseFolder(self, evt): f=self._oc.pick_folder() if f is None: return # user hit cancel self._oc.set_folder(f) self.folderctrl.SetValue(self._oc.get_folder_name()) self.__read=False def OnFilter(self, evt): # read the calender to get the category list if not self.__read: self._oc.read() self.__read=True cat_list=self._oc.get_category_list() dlg=common_calendar.AutoSyncFilterDialog(self, -1, 'Filtering Parameters', cat_list) dlg.set(self._oc.get_filter()) if dlg.ShowModal()==wx.ID_OK: self._oc.set_filter(dlg.get()) def GetFolder(self): return self._oc.get_folder_id() def GetFilter(self): return self._oc.get_filter()
outlook_calendar.py.diff
(application/octet-stream, 7.5 KB)
Index: outlook_calendar.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/outlook_calendar.py,v
retrieving revision 1.12
diff -u -r1.12 outlook_calendar.py
--- outlook_calendar.py 12 Sep 2005 22:19:50 -0000 1.12
+++ outlook_calendar.py 19 Sep 2005 15:41:39 -0000
@@ -68,7 +68,7 @@
#-------------------------------------------------------------------------------
class OutlookCalendarImportData:
- _calendar_keys=[
+ _non_auto_sync_calendar_keys=[
# (Outlook field, BP Calendar field, convertor function)
('Subject', 'description', None),
('Location', 'location', None),
@@ -82,6 +82,21 @@
('Body', 'notes', None),
('AllDayEvent', 'allday', None)
]
+ # the auto_sync calender fields do not support the "body" of the appointment
+ # accessing this field causes an outlook warning to appear which prevents automation
+ _auto_sync_calendar_keys=[
+ # (Outlook field, BP Calendar field, convertor function)
+ ('Subject', 'description', None),
+ ('Location', 'location', None),
+ ('Start', 'start', to_bp_date),
+ ('End', 'end', to_bp_date),
+ ('Categories', 'categories', convert_categories),
+ ('IsRecurring', 'repeat', None),
+ ('ReminderSet', 'alarm', None),
+ ('ReminderMinutesBeforeStart', 'alarm_value', None),
+ ('Importance', 'priority', None),
+ ('AllDayEvent', 'allday', None)
+ ]
_recurrence_keys=[
# (Outlook field, BP Calendar field, convertor function)
('NoEndDate', 'NoEndDate', None),
@@ -103,7 +118,11 @@
'end': None,
'categories': None,
'rpt_events': False,
- 'no_alarm': False
+ 'no_alarm': False,
+ 'ringtone': None,
+ 'alarm_override':False,
+ 'vibrate':False,
+ 'alarm_value':0
}
# Outlook constants
@@ -117,7 +136,7 @@
olImportanceLow = native.outlook.outlook_com.constants.olImportanceLow
olImportanceNormal = native.outlook.outlook_com.constants.olImportanceNormal
- def __init__(self, outlook):
+ def __init__(self, outlook, auto_sync_only=0):
self._outlook=outlook
self._data=[]
self._error_list=[]
@@ -128,6 +147,10 @@
self._current_count=0
self._update_dlg=None
self._exception_list=[]
+ if auto_sync_only:
+ self._calendar_keys=self._auto_sync_calendar_keys
+ else:
+ self._calendar_keys=self._non_auto_sync_calendar_keys
def _accept(self, entry):
s_date=entry['start'][:3]
@@ -149,7 +172,7 @@
e_date>self._filter['end'][:3]:
return False
# check the catefory
- c=self._filter['categories']
+ c=self._filter.get('categories', None)
if c is None or not len(c):
# no categories specified => all catefories allowed.
return True
@@ -169,8 +192,17 @@
ce.priority=ce.priority_low
elif v==self.olImportanceHigh:
ce.priority=ce.priority_high
- if not self._filter.get('no_alarm', False) and e.get('alarm', False):
+ if not self._filter.get('no_alarm', False) and \
+ not self._filter.get('alarm_override', False) and \
+ e.get('alarm', False):
ce.alarm=e.get('alarm_value', 0)
+ ce.ringtone=self._filter.get('ringtone', "")
+ ce.vibrate=self._filter.get('vibrate', False)
+ elif not self._filter.get('no_alarm', False) and \
+ self._filter.get('alarm_override', False):
+ ce.alarm=self._filter.get('alarm_value', 0)
+ ce.ringtone=self._filter.get('ringtone', "")
+ ce.vibrate=self._filter.get('vibrate', False)
ce.allday=e.get('allday', False)
ce.start=e['start']
ce.end=e['end']
@@ -282,6 +314,15 @@
else:
self._folder=f
+ def get_folder_id(self):
+ return self._outlook.getfolderid(self._folder)
+
+ def set_folder_id(self, id):
+ if id is None or id=="":
+ self.set_folder(None)
+ else:
+ self._folder=self._outlook.getfolderfromid(id)
+
def set_filter(self, filter):
self._filter=filter
@@ -498,3 +539,74 @@
def get_categories(self):
return self._oc.get_category_list()
+
+#-------------------------------------------------------------------------------
+def ImportCal(folder, filters):
+ _oc=OutlookCalendarImportData(native.outlook, auto_sync_only=1)
+ _oc.set_folder_id(folder)
+ _oc.set_filter(filters)
+ _oc.read()
+ res={ 'calendar':_oc.get() }
+ return res
+
+#-------------------------------------------------------------------------------
+class OutlookAutoConfCalDialog(wx.Dialog):
+ def __init__(self, parent, id, title, folder, filters,
+ style=wx.CAPTION|wx.MAXIMIZE_BOX| \
+ wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER):
+ self._oc=OutlookCalendarImportData(native.outlook, auto_sync_only=1)
+ self._oc.set_folder_id(folder)
+ self._oc.set_filter(filters)
+ self.__read=False
+ wx.Dialog.__init__(self, parent, id=id, title=title, style=style)
+ main_bs=wx.BoxSizer(wx.VERTICAL)
+ hbs=wx.BoxSizer(wx.HORIZONTAL)
+ # label
+ hbs.Add(wx.StaticText(self, -1, "Outlook Calendar Folder:"), 0, wx.ALL|wx.ALIGN_CENTRE, 2)
+ # where the folder name goes
+ self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY)
+ self.folderctrl.SetValue(self._oc.get_folder_name())
+ hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2)
+ # browse button
+ id_browse=wx.NewId()
+ hbs.Add(wx.Button(self, id_browse, 'Browse ...'), 0, wx.EXPAND|wx.ALL, 2)
+ main_bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
+ main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5)
+ wx.EVT_BUTTON(self, id_browse, self.OnBrowseFolder)
+ hbs=wx.BoxSizer(wx.HORIZONTAL)
+ hbs.Add(wx.Button(self, wx.ID_OK, 'OK'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ hbs.Add(wx.Button(self, wx.ID_CANCEL, 'Cancel'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ id_filter=wx.NewId()
+ hbs.Add(wx.Button(self, id_filter, 'Filter'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ hbs.Add(wx.Button(self, wx.ID_HELP, 'Help'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ main_bs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ wx.EVT_BUTTON(self, id_filter, self.OnFilter)
+ wx.EVT_BUTTON(self, wx.ID_HELP, lambda *_: wx.GetApp().displayhelpid(helpids.ID_DLG_CALENDAR_IMPORT))
+ self.SetSizer(main_bs)
+ self.SetAutoLayout(True)
+ main_bs.Fit(self)
+
+ def OnBrowseFolder(self, evt):
+ f=self._oc.pick_folder()
+ if f is None:
+ return # user hit cancel
+ self._oc.set_folder(f)
+ self.folderctrl.SetValue(self._oc.get_folder_name())
+ self.__read=False
+
+ def OnFilter(self, evt):
+ # read the calender to get the category list
+ if not self.__read:
+ self._oc.read()
+ self.__read=True
+ cat_list=self._oc.get_category_list()
+ dlg=common_calendar.AutoSyncFilterDialog(self, -1, 'Filtering Parameters', cat_list)
+ dlg.set(self._oc.get_filter())
+ if dlg.ShowModal()==wx.ID_OK:
+ self._oc.set_filter(dlg.get())
+
+ def GetFolder(self):
+ return self._oc.get_folder_id()
+
+ def GetFilter(self):
+ return self._oc.get_filter()
vcal_calendar.py
(text/plain, 21.7 KB)
### BITPIM ### ### Copyright (C) 2004 Joe Pham <[email protected]> ### ### This program is free software; you can redistribute it and/or modify ### it under the terms of the BitPim license as detailed in the LICENSE file. ### ### $Id: vcal_calendar.py,v 1.14 2005/06/22 00:07:44 djpham Exp $ "Deals with vcard calendar import stuff" # system modules import copy import datetime # site modules import wx # local modules import bpcalendar import bptime import common_calendar import helpids import vcard module_debug=False #------------------------------------------------------------------------------- class vCalendarFile(object): def __init__(self, file_name=None): self.__data=[] self.__file_name=file_name def read(self, file_name=None): self.__data=[] if file_name is not None: self.__file_name=file_name if self.__file_name is None: # no file name specified return try: f=open(self.__file_name) vfile=vcard.VFile(f) has_data=False for n,l in vfile: if n[0]=='BEGIN' and l=='VEVENT': # start of an event, turn on data loggin d={} has_data=True elif n[0]=='END' and l=='VEVENT': self.__data.append(d) d={} has_data=False elif has_data: d[n[0]]={ 'value': l } f.close() except: pass def __get_data(self): return copy.deepcopy(self.__data) data=property(fget=__get_data) #------------------------------------------------------------------------------- class VCalendarImportData(object): __default_filter={ 'start': None, 'end': None, 'categories': None, 'rpt_events': False, 'no_alarm': False, 'ringtone': None, 'alarm_override':False, 'vibrate':False, 'alarm_value':0 } __rrule_dow={ 'SU': 0x01, 'MO': 0x02, 'TU': 0x04, 'WE': 0x08, 'TH': 0x10, 'FR': 0x20, 'SA': 0x40 } __rrule_weekday=__rrule_dow['MO']|__rrule_dow['TU']|\ __rrule_dow['WE']|__rrule_dow['TH']|\ __rrule_dow['FR'] def __init__(self, file_name=None): self.__calendar_keys=[ ('CATEGORIES', 'categories', self.__conv_cat), ('DESCRIPTION', 'notes', None), ('DTEND', 'end', self.__conv_date), ('LOCATION', 'location', None), ('PRIORITY', 'priority', self.__conv_priority), ('DTSTART', 'start', self.__conv_date), ('SUMMARY', 'description', None), ('AALARM', 'alarm', self.__conv_alarm), ('DALARM', 'alarm', self.__conv_alarm), ('RRULE', 'repeat', self.__conv_repeat), ('EXDATE', 'exceptions', self.__conv_exceptions), ] self.__file_name=file_name self.__data=[] self.__filter=self.__default_filter self.read() def __accept(self, entry): # start & end time within specified filter if self.__filter['start'] is not None and \ entry['start'][:3]<self.__filter['start'][:3]: return False if self.__filter['end'] is not None and \ entry['end'][:3]>self.__filter['end'][:3] and \ entry['end'][:3]!=common_calendar.no_end_date[:3]: return False # check the catefory c=self.__filter['categories'] if c is None or not len(c): # no categories specified => all catefories allowed. return True if len([x for x in entry['categories'] if x in c]): return True return False def __populate_repeat_entry(self, e, ce): # populate repeat entry data if not e.get('repeat', False) or e.get('repeat_type', None) is None: # not a repeat event return rp=bpcalendar.RepeatEntry() rp_type=e['repeat_type'] rp_interval=e.get('repeat_interval', 1) rp_end=e.get('repeat_end', None) rp_num=e.get('repeat_num', None) rp_dow=e.get('repeat_dow', 0) if rp_type==rp.daily: # daily event rp.repeat_type=rp.daily rp.interval=rp_interval elif rp_type==rp.weekly or rp_type==rp.monthly: rp.repeat_type=rp_type rp.interval=rp_interval rp.dow=rp_dow elif rp_type==rp.yearly: rp.repeat_type=rp.yearly else: # not yet supported return # setting the repeat duration/end-date of this event if rp_end is not None: # end date specified ce.end=rp_end[:3]+ce.end[3:] elif rp_num is not None and rp_num: # num of occurrences specified if rp_type=='daily': bp_t=bptime.BPTime(ce.start)+ \ datetime.timedelta(rp_interval*(rp_num-1)) ce.end=bp_t.get()[:3]+ce.end[3:] elif rp_type=='weekly': bp_t=bptime.BPTime(ce.start)+ \ datetime.timedelta(7*rp_interval*(rp_num-1)) ce.end=bp_t.get()[:3]+ce.end[3:] elif rp_type=='monthly': bp_t=bptime.BPTime(ce.start)+ \ datetime.timedelta(30*(rp_num-1)) ce.end=bp_t.get()[:2]+ce.end[2:] else: bp_t=bptime.BPTime(ce.start)+ \ datetime.timedelta(365*(rp_num-1)) ce.end=bp_t.get()[:1]+ce.end[1:] else: # forever duration ce.end=common_calendar.no_end_date[:3]+ce.end[3:] # add the list of exceptions for k in e.get('exceptions', []): rp.add_suppressed(*k[:3]) # all done ce.repeat=rp def __populate_entry(self, e, ce): # populate an calendar entry with data ce.description=e.get('description', None) ce.location=e.get('location', None) v=e.get('priority', None) if v is not None: ce.priority=v if not self._filter.get('no_alarm', False) and \ not self._filter.get('alarm_override', False) and \ e.get('alarm', False): ce.alarm=e.get('alarm_value', 0) ce.ringtone=self._filter.get('ringtone', "") ce.vibrate=self._filter.get('vibrate', False) elif not self._filter.get('no_alarm', False) and \ self._filter.get('alarm_override', False): ce.alarm=self._filter.get('alarm_value', 0) ce.ringtone=self._filter.get('ringtone', "") ce.vibrate=self._filter.get('vibrate', False) ce_start=e.get('start', None) ce_end=e.get('end', None) if ce_start is None and ce_end is None: raise ValueError, "No start or end datetime" if ce_start is not None: ce.start=ce_start if ce_end is not None: ce.end=ce_end if ce_start is None: ce.start=ce.end elif ce_end is None: ce.end=ce.start ce.notes=e.get('notes', None) v=[] for k in e.get('categories', []): v.append({ 'category': k }) ce.categories=v # look at repeat self.__populate_repeat_entry(e, ce) def __generate_repeat_events(self, e): # generate multiple single events from this repeat event ce=bpcalendar.CalendarEntry() self.__populate_entry(e, ce) l=[] new_e=e.copy() new_e['repeat']=False for k in ('repeat_type', 'repeat_interval', 'repeat_dow'): if new_e.has_key(k): del new_e[k] s_date=datetime.datetime(*self.__filter['start']) e_date=datetime.datetime(*self.__filter['end']) one_day=datetime.timedelta(1) this_date=s_date while this_date<=e_date: date_l=(this_date.year, this_date.month, this_date.day) if ce.is_active(*date_l): new_e['start']=date_l+new_e['start'][3:] new_e['end']=date_l+new_e['end'][3:] l.append(new_e.copy()) this_date+=one_day return l def get(self): res={} single_rpt=self.__filter.get('rpt_events', False) for k in self.__data: try: if self.__accept(k): if k.get('repeat', False) and single_rpt: d=self.__generate_repeat_events(k) else: d=[k] for n in d: ce=bpcalendar.CalendarEntry() self.__populate_entry(n, ce) res[ce.id]=ce except: if module_debug: raise return res def get_category_list(self): l=[] for e in self.__data: l+=[x for x in e.get('categories', []) if x not in l] return l def set_filter(self, filter): self.__filter=filter def get_filter(self): return self.__filter def __conv_cat(self, v, _): return [x.strip() for x in v['value'].split(",") if len(x)] def __conv_alarm(self, v, dd): try: alarm_date=bptime.BPTime(v['value'].split(';')[0]) start_date=bptime.BPTime(dd['start']) if alarm_date.get()<start_date.get(): dd['alarm_value']=(start_date-alarm_date).seconds/60 return True return False except: return False def __conv_date(self, v, _): return bptime.BPTime(v['value']).get() def __conv_priority(self, v, _): try: return int(v['value']) except: return None def __process_daily_rule(self, v, dd): # the rule is Dx #y or Dx YYYYMMDDTHHMM s=v['value'].split(' ') dd['repeat_interval']=int(s[0][1:]) if len(s)==1: # no duration/end date return True if s[1][0]=='#': # duration dd['repeat_num']=int(s[1][1:]) else: # end date dd['repeat_end']=bptime.BPTime(s[1]).get() dd['repeat_type']='daily' return True def __process_weekly_rule(self, v, dd): # the rule is Wx | Wx <#y|YYYYMMDDTHHMMSS> | Wx MO TU s=v['value'].split(' ') dd['repeat_interval']=int(s[0][1:]) dow=0 for i in range(1, len(s)): n=s[i] if n[0].isdigit(): dd['repeat_end']=bptime.BPTime(n).get() elif n[0]=='#': dd['repeat_num']=int(n[1:]) else: # day-of-week dow=dow|self.__rrule_dow.get(n, 0) if dow: dd['repeat_dow']=dow dd['repeat_type']='weekly' return True def __process_monthly_rule(self, v, dd): try: # acceptable format: MD1 <day number> <end date | #duration> # or MP1 <[1-4]+ | 1-> <SU-SA> <end date | #duration> s=v['value'].split(' ') if s[0]!='MD1' and s[0]!='MP1': return False if s[0]=='MP1': # every nth *day of every month n=s[1] if n in ['1+', '2+', '3+', '4+', '1-']: if n[1]=='-': dd['repeat_interval']=5 else: dd['repeat_interval']=int(n[0]) else: return False dd['repeat_dow']=self.__rrule_dow.get(s[2], 0) else: dd['repeat_interval']=dd['repeat_dow']=0 dd['repeat_type']='monthly' n=s[-1] if len(n)>7 and n[:8].isdigit(): # end date/time specified dd['repeat_end']=bptime.BPTime(n).get() elif n[0]=='#': dd['repeat_num']=int(n[1:]) return True except: if module_debug: raise return False def __process_yearly_rule(self, v, dd): try: # acceptable format YM1 <Month number> <end date | #duration> s=v['value'].split(' ') if s[0]!='YM1': return False n=s[-1] if len(n)>7 and n[:8].isdigit(): # end date/time specified dd['repeat_end']=bptime.BPTime(n).get() elif n[0]=='#': dd['repeat_num']=int(n[1:]) dd['repeat_type']='yearly' return True except: if module_debug: raise return False def __conv_repeat(self, v, dd): func_dict={ 'D': self.__process_daily_rule, 'W': self.__process_weekly_rule, 'M': self.__process_monthly_rule, 'Y': self.__process_yearly_rule } c=v['value'][0] return func_dict.get(c, lambda *arg: False)(v, dd) def __conv_exceptions(self, v, _): try: l=v['value'].split(';') r=[] for n in l: r.append(bptime.BPTime(n).get()) return r except: return [] def __convert(self, vcal, d): for i in vcal: try: dd={'start': None, 'end': None } for j in self.__calendar_keys: if i.has_key(j[0]): k=i[j[0]] if j[2] is not None: dd[j[1]]=j[2](k, dd) else: dd[j[1]]=k['value'] if dd['start'] is None and dd['end'] is None: # no start or end, drop this one continue if dd['start'] is None: dd['start']=dd['end'] elif dd['end'] is None: dd['end']=dd['start'] if module_debug: print dd d.append(dd) except: if module_debug: raise def get_display_data(self): cnt=0 res={} single_rpt=self.__filter.get('rpt_events', False) for k in self.__data: if self.__accept(k): if k.get('repeat', False) and single_rpt: d=self.__generate_repeat_events(k) else: d=[k.copy()] for n in d: if self.__filter.get('no_alarm', False): n['alarm']=False res[cnt]=n cnt+=1 return res def get_file_name(self): if self.__file_name is not None: return self.__file_name return '' def read(self, file_name=None): if file_name is not None: self.__file_name=file_name if self.__file_name is None: # no file name specified return v=vCalendarFile(self.__file_name) v.read() self.__convert(v.data, self.__data) #------------------------------------------------------------------------------- class VcalImportCalDialog(common_calendar.PreviewDialog): __column_labels=[ ('description', 'Description', 400, None), ('start', 'Start', 150, common_calendar.bp_date_str), ('end', 'End', 150, common_calendar.bp_date_str), ('repeat_type', 'Repeat', 80, common_calendar.bp_repeat_str), ('alarm', 'Alarm', 80, common_calendar.bp_alarm_str), ('categories', 'Category', 150, common_calendar.category_str) ] ID_ADD=wx.NewId() def __init__(self, parent, id, title): self.__oc=VCalendarImportData() common_calendar.PreviewDialog.__init__(self, parent, id, title, self.__column_labels, self.__oc.get_display_data(), config_name='import/calendar/vcaldialog') def getcontrols(self, main_bs): hbs=wx.BoxSizer(wx.HORIZONTAL) # label hbs.Add(wx.StaticText(self, -1, "VCalendar File:"), 0, wx.ALL|wx.ALIGN_CENTRE, 2) # where the folder name goes self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) self.folderctrl.SetValue(self.__oc.get_file_name()) hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2) # browse button id_browse=wx.NewId() hbs.Add(wx.Button(self, id_browse, 'Browse ...'), 0, wx.EXPAND|wx.ALL, 2) main_bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) wx.EVT_BUTTON(self, id_browse, self.OnBrowseFolder) def getpostcontrols(self, main_bs): main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) hbs=wx.BoxSizer(wx.HORIZONTAL) id_import=wx.NewId() hbs.Add(wx.Button(self, id_import, 'Import'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_OK, 'Replace All'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, self.ID_ADD, 'Add'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_CANCEL, 'Cancel'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) id_filter=wx.NewId() hbs.Add(wx.Button(self, id_filter, 'Filter'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_HELP, 'Help'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) main_bs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.ALL, 5) wx.EVT_BUTTON(self, id_import, self.OnImport) wx.EVT_BUTTON(self, id_filter, self.OnFilter) wx.EVT_BUTTON(self, self.ID_ADD, self.OnAdd) wx.EVT_BUTTON(self, wx.ID_HELP, lambda *_: wx.GetApp().displayhelpid(helpids.ID_DLG_CALENDAR_IMPORT)) def OnImport(self, evt): wx.BeginBusyCursor() dlg=wx.ProgressDialog('VCalendar Import', 'Importing vCalendar Data, please wait ...', parent=self) self.__oc.read(self.folderctrl.GetValue()) self.populate(self.__oc.get_display_data()) dlg.Destroy() wx.EndBusyCursor() def OnBrowseFolder(self, evt): dlg=wx.FileDialog(self, "Pick a VCalendar File", wildcard='*.vcs') id=dlg.ShowModal() if id==wx.ID_CANCEL: dlg.Destroy() return self.folderctrl.SetValue(dlg.GetPath()) dlg.Destroy() def OnFilter(self, evt): cat_list=self.__oc.get_category_list() dlg=common_calendar.FilterDialog(self, -1, 'Filtering Parameters', cat_list) dlg.set(self.__oc.get_filter()) if dlg.ShowModal()==wx.ID_OK: self.__oc.set_filter(dlg.get()) self.populate(self.__oc.get_display_data()) def OnAdd(self, evt): self.EndModal(self.ID_ADD) def get(self): return self.__oc.get() def get_categories(self): return self.__oc.get_category_list() #------------------------------------------------------------------------------- def ImportCal(folder, filters): _oc=VCalendarImportData(folder) _oc.set_filter(filters) _oc.read() res={ 'calendar':_oc.get() } return res #------------------------------------------------------------------------------- class VCalAutoConfCalDialog(wx.Dialog): def __init__(self, parent, id, title, folder, filters, style=wx.CAPTION|wx.MAXIMIZE_BOX| \ wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER): self._oc=VCalendarImportData() self._oc.set_filter(filters) self.__read=False wx.Dialog.__init__(self, parent, id=id, title=title, style=style) main_bs=wx.BoxSizer(wx.VERTICAL) hbs=wx.BoxSizer(wx.HORIZONTAL) # label hbs.Add(wx.StaticText(self, -1, "VCalendar File:"), 0, wx.ALL|wx.ALIGN_CENTRE, 2) # where the folder name goes self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY) self.folderctrl.SetValue(folder) hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2) # browse button id_browse=wx.NewId() hbs.Add(wx.Button(self, id_browse, 'Browse ...'), 0, wx.EXPAND|wx.ALL, 2) main_bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5) main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5) wx.EVT_BUTTON(self, id_browse, self.OnBrowseFolder) hbs=wx.BoxSizer(wx.HORIZONTAL) hbs.Add(wx.Button(self, wx.ID_OK, 'OK'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_CANCEL, 'Cancel'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) id_filter=wx.NewId() hbs.Add(wx.Button(self, id_filter, 'Filter'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) hbs.Add(wx.Button(self, wx.ID_HELP, 'Help'), 0, wx.ALIGN_CENTRE|wx.ALL, 5) main_bs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.ALL, 5) wx.EVT_BUTTON(self, id_filter, self.OnFilter) wx.EVT_BUTTON(self, wx.ID_HELP, lambda *_: wx.GetApp().displayhelpid(helpids.ID_DLG_CALENDAR_IMPORT)) self.SetSizer(main_bs) self.SetAutoLayout(True) main_bs.Fit(self) def OnBrowseFolder(self, evt): dlg=wx.FileDialog(self, "Pick a VCalendar File", wildcard='*.vcs') id=dlg.ShowModal() if id==wx.ID_CANCEL: dlg.Destroy() return self.folderctrl.SetValue(dlg.GetPath()) self.__read=False dlg.Destroy() def OnFilter(self, evt): # read the calender to get the category list if not self.__read: self._oc.read(self.folderctrl.GetValue()) self.__read=True cat_list=self._oc.get_category_list() dlg=common_calendar.AutoSyncFilterDialog(self, -1, 'Filtering Parameters', cat_list) dlg.set(self._oc.get_filter()) if dlg.ShowModal()==wx.ID_OK: self._oc.set_filter(dlg.get()) def GetFolder(self): return self.folderctrl.GetValue() def GetFilter(self): return self._oc.get_filter()
vcal_calendar.py.diff
(application/octet-stream, 4.7 KB)
Index: vcal_calendar.py
===================================================================
RCS file: /cvsroot/bitpim/bitpim/vcal_calendar.py,v
retrieving revision 1.14
diff -u -r1.14 vcal_calendar.py
--- vcal_calendar.py 22 Jun 2005 00:07:44 -0000 1.14
+++ vcal_calendar.py 19 Sep 2005 15:28:34 -0000
@@ -69,7 +69,11 @@
'end': None,
'categories': None,
'rpt_events': False,
- 'no_alarm': False
+ 'no_alarm': False,
+ 'ringtone': None,
+ 'alarm_override':False,
+ 'vibrate':False,
+ 'alarm_value':0
}
__rrule_dow={
'SU': 0x01, 'MO': 0x02, 'TU': 0x04, 'WE': 0x08, 'TH': 0x10,
@@ -177,8 +181,17 @@
v=e.get('priority', None)
if v is not None:
ce.priority=v
- if not self.__filter.get('no_alarm', False) and e.get('alarm', False):
+ if not self._filter.get('no_alarm', False) and \
+ not self._filter.get('alarm_override', False) and \
+ e.get('alarm', False):
ce.alarm=e.get('alarm_value', 0)
+ ce.ringtone=self._filter.get('ringtone', "")
+ ce.vibrate=self._filter.get('vibrate', False)
+ elif not self._filter.get('no_alarm', False) and \
+ self._filter.get('alarm_override', False):
+ ce.alarm=self._filter.get('alarm_value', 0)
+ ce.ringtone=self._filter.get('ringtone', "")
+ ce.vibrate=self._filter.get('vibrate', False)
ce_start=e.get('start', None)
ce_end=e.get('end', None)
if ce_start is None and ce_end is None:
@@ -518,3 +531,72 @@
return self.__oc.get_category_list()
#-------------------------------------------------------------------------------
+def ImportCal(folder, filters):
+ _oc=VCalendarImportData(folder)
+ _oc.set_filter(filters)
+ _oc.read()
+ res={ 'calendar':_oc.get() }
+ return res
+
+#-------------------------------------------------------------------------------
+class VCalAutoConfCalDialog(wx.Dialog):
+ def __init__(self, parent, id, title, folder, filters,
+ style=wx.CAPTION|wx.MAXIMIZE_BOX| \
+ wx.SYSTEM_MENU|wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER):
+ self._oc=VCalendarImportData()
+ self._oc.set_filter(filters)
+ self.__read=False
+ wx.Dialog.__init__(self, parent, id=id, title=title, style=style)
+ main_bs=wx.BoxSizer(wx.VERTICAL)
+ hbs=wx.BoxSizer(wx.HORIZONTAL)
+ # label
+ hbs.Add(wx.StaticText(self, -1, "VCalendar File:"), 0, wx.ALL|wx.ALIGN_CENTRE, 2)
+ # where the folder name goes
+ self.folderctrl=wx.TextCtrl(self, -1, "", style=wx.TE_READONLY)
+ self.folderctrl.SetValue(folder)
+ hbs.Add(self.folderctrl, 1, wx.EXPAND|wx.ALL, 2)
+ # browse button
+ id_browse=wx.NewId()
+ hbs.Add(wx.Button(self, id_browse, 'Browse ...'), 0, wx.EXPAND|wx.ALL, 2)
+ main_bs.Add(hbs, 0, wx.EXPAND|wx.ALL, 5)
+ main_bs.Add(wx.StaticLine(self, -1), 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5)
+ wx.EVT_BUTTON(self, id_browse, self.OnBrowseFolder)
+ hbs=wx.BoxSizer(wx.HORIZONTAL)
+ hbs.Add(wx.Button(self, wx.ID_OK, 'OK'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ hbs.Add(wx.Button(self, wx.ID_CANCEL, 'Cancel'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ id_filter=wx.NewId()
+ hbs.Add(wx.Button(self, id_filter, 'Filter'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ hbs.Add(wx.Button(self, wx.ID_HELP, 'Help'), 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ main_bs.Add(hbs, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
+ wx.EVT_BUTTON(self, id_filter, self.OnFilter)
+ wx.EVT_BUTTON(self, wx.ID_HELP, lambda *_: wx.GetApp().displayhelpid(helpids.ID_DLG_CALENDAR_IMPORT))
+ self.SetSizer(main_bs)
+ self.SetAutoLayout(True)
+ main_bs.Fit(self)
+
+ def OnBrowseFolder(self, evt):
+ dlg=wx.FileDialog(self, "Pick a VCalendar File", wildcard='*.vcs')
+ id=dlg.ShowModal()
+ if id==wx.ID_CANCEL:
+ dlg.Destroy()
+ return
+ self.folderctrl.SetValue(dlg.GetPath())
+ self.__read=False
+ dlg.Destroy()
+
+ def OnFilter(self, evt):
+ # read the calender to get the category list
+ if not self.__read:
+ self._oc.read(self.folderctrl.GetValue())
+ self.__read=True
+ cat_list=self._oc.get_category_list()
+ dlg=common_calendar.AutoSyncFilterDialog(self, -1, 'Filtering Parameters', cat_list)
+ dlg.set(self._oc.get_filter())
+ if dlg.ShowModal()==wx.ID_OK:
+ self._oc.set_filter(dlg.get())
+
+ def GetFolder(self):
+ return self.folderctrl.GetValue()
+
+ def GetFilter(self):
+ return self._oc.get_filter()