root/trunk/dabo/dApp.py

Revision 4742, 51.1 kB (checked in by ed, 2 days ago)

Added handlers in the UI layer for some of wxPython's AppleEvent? handler methods. These catch the methods and propagate them to the dApp layer, where they can be overridden in an app subclass.

  • Property svn:eol-style set to native
Line 
1 # -*- coding: utf-8 -*-
2 import sys
3 import os
4 import locale
5 import warnings
6 import glob
7 import tempfile
8 import imp
9 import ConfigParser
10 import inspect
11 import datetime
12 import urllib2
13 import shutil
14 import logging
15 import dabo
16 import dabo.ui
17 import dabo.db
18 import dabo.dLocalize as dLocalize
19 import dabo.dException as dException
20 from dabo.dLocalize import _
21 from dabo.lib.connParser import importConnections
22 from dabo import dSecurityManager
23 from dabo.lib.SimpleCrypt import SimpleCrypt
24 from dabo.dObject import dObject
25 from dabo import dUserSettingProvider
26 from dabo.lib.RemoteConnector import RemoteConnector
27
28
29
30 class Collection(list):
31     """ Collection : Base class for the various collection
32     classes used in the app object.
33     """
34     def __init__(self):
35         list.__init__(self)
36
37
38     def add(self, objRef):
39         """Add the object reference to the collection."""
40         self.append(objRef)
41        
42
43     def remove(self, objRef):
44         """Delete the object reference from the collection."""
45         try:
46             index = self.index(objRef)
47         except ValueError:
48             index = None
49         if index is not None:
50             del self[index]
51
52
53
54 class TempFileHolder(object):
55     """Utility class to get temporary file names and to make sure they are
56     deleted when the Python session ends.
57     """
58     def __init__(self):
59         self._tempFiles = []
60
61
62     def __del__(self):
63         self._eraseTempFiles()
64        
65        
66     def _eraseTempFiles(self):
67         # Try to erase all temp files created during life.
68         # Need to re-import the os module here for some reason.
69         try:
70             import os
71             for f in self._tempFiles:
72                 if not os.path.exists(f):
73                     continue
74                 try:
75                     os.remove(f)
76                 except OSError, e:
77                     if not f.endswith(".pyc"):
78                         # Don't worry about the .pyc files, since they may not be there
79                         print "Could not delete %s: %s" % (f, e)
80         except StandardError, e:
81             # In these rare cases, Python has already 'gone away', so just bail
82             pass
83    
84    
85     def release(self):
86         self._eraseTempFiles()     
87
88
89     def append(self, f):
90         self._tempFiles.append(f)
91
92
93     def getTempFile(self, ext=None, badChars=None, directory=None):
94         if ext is None:
95             ext = "py"
96         if badChars is None:
97             badChars = "-:"
98         fname = ""
99         suffix = ".%s" % ext
100         while not fname:
101             if directory is None:
102                 fd, tmpname = tempfile.mkstemp(suffix=suffix)
103             else:
104                 fd, tmpname = tempfile.mkstemp(suffix=suffix, dir=directory)
105             os.close(fd)
106             bad = [ch for ch in badChars if ch in os.path.split(tmpname)[1]]
107             if not bad:
108                 fname = tmpname
109         self.append(fname)
110         if fname.endswith(".py"):
111             # Track the .pyc file, too.
112             self.append(fname + "c")
113         return fname
114
115
116
117 class dApp(dObject):
118     """The containing object for the entire application.
119
120     All Dabo objects have an Application property which refers to the dApp
121     instance. Instantiate your dApp object from your main script, like so:
122
123     >>> import dabo
124     >>> app = dabo.dApp
125     >>> app.start()
126
127     Normally, dApp gets instantiated from the client app's main Python script,
128     and lives through the life of the application.
129
130         -- set up an empty data connections object which holds
131         -- connectInfo objects connected to pretty names. If there
132         -- is a file named 'default.cnxml' present, it will import the
133         -- connection definitions contained in that. If no file of that
134         -- name exists, it will import any .cnxml file it finds. If there
135         -- are no such files, it will then revert to the old behavior
136         -- of importing a file in the current directory called
137         -- 'dbConnectionDefs.py', which contains connection
138         -- definitions in python code format instead of XML.
139
140         -- Set up a DB Connection manager, that is basically a dictionary
141         -- of dConnection objects. This allows connections to be shared
142         -- application-wide.
143
144         -- decide which ui to use (wx) and gets that ball rolling
145
146         -- look for a MainForm in an expected place, otherwise use default dabo
147         -- dMainForm, and instantiate that.
148
149         -- maintain a forms collection and provide interfaces for
150         -- opening dForms, closing them, and iterating through them.
151
152         -- start the main app event loop.
153
154         -- clean up and exit gracefully
155
156     """
157     _call_beforeInit, _call_afterInit, _call_initProperties = False, False, True
158     # Behaviors which are normal in the framework may need to
159     # be modified when run as the Designer. This flag will
160     # distinguish between the two states.
161     isDesigner = False
162
163    
164     def __init__(self, selfStart=False, properties=None, *args, **kwargs):
165         if dabo.settings.loadUserLocale:
166             locale.setlocale(locale.LC_ALL, '')
167
168         self._uiAlreadySet = False
169         dabo.dAppRef = self
170         self._beforeInit()
171        
172         # If we are displaying a splash screen, these attributes control
173         # its appearance. Extract them before the super call.
174         self.showSplashScreen = self._extractKey(kwargs, "showSplashScreen", False)
175         basepath = dabo.frameworkPath
176         img = os.path.join(basepath, "icons", "daboSplashName.png")
177         self.splashImage = self._extractKey(kwargs, "splashImage", img)
178         self.splashMaskColor = self._extractKey(kwargs, "splashMaskColor", None)
179         self.splashTimeout = self._extractKey(kwargs, "splashTimeout", 5000)
180        
181         super(dApp, self).__init__(properties, *args, **kwargs)
182         # egl: added the option of keeping the main form hidden
183         # initially. The default behavior is for it to be shown, as usual.
184         self.showMainFormOnStart = True
185         self._wasSetup = False
186         # Track names of menus whose MRUs need to be persisted. Set
187         # the key for each entry to the menu caption, and the value to
188         # the bound function.
189         self._persistentMRUs = {}
190         # Create the temp file handlers.
191         self._tempFileHolder = TempFileHolder()
192         self.getTempFile = self._tempFileHolder.getTempFile
193         # Create the framework-level preference manager
194         self._frameworkPrefs = dabo.dPref(key="dabo_framework")
195         # Hold a reference to the bizobj and connection, if any, controlling
196         # the current database transaction
197         self._transactionTokens = {}
198         # Holds update check times in case of errors.
199         self._lastCheckInfo = []
200         # Location and Name of the project; used for Web Update
201         self._projectInfo = (None, None)
202         self._setProjInfo()
203         # Other Web Update values
204         self.projectAbbrevs = {"dabo": "frm",
205                 "class designer": "cds",
206                 "cxn editor": "cxe",
207                 "editor": "edt",
208                 "menu designer": "mds",
209                 "preference editor": "prf",
210                 "report designer": "rds",
211                 "wizards": "wiz",
212                 "dabodemo": "dem"}
213         self.webUpdateDirs = {"dabo": "dabo",
214                 "class designer": "ide",
215                 "cxn editor": "ide",
216                 "editor": "ide",
217                 "menu designer": "ide",
218                 "preference editor": "ide",
219                 "report designer": "ide",
220                 "wizards": "ide",
221                 "dabodemo": "demo"}
222
223         # List of form classes to open on App Startup
224         self.formsToOpen = [] 
225         # Form to open if no forms were passed as a parameter
226         self.default_form = None
227         # Dict of "Last-Modified" values for dynamic web resources
228         self._sourceLastModified = {}
229        
230         # For simple UI apps, this allows the app object to be created
231         # and started in one step. It also suppresses the display of
232         # the main form.
233         if selfStart:
234             self.showMainFormOnStart = False
235             self.setup()
236
237         self._initDB()
238
239         # If running as a web app, sync the files
240         rp = self._RemoteProxy
241         if rp:
242             try:
243                 rp.syncFiles()
244             except urllib2.URLError, e:
245                 code, msg = e.reason
246                 if code == 61:
247                     # Connection refused; server's down
248                     print _("""
249
250
251 The connection was refused by the server. Most likely this means that
252 the server is not running. Please have that problem corrected, and
253 try again when it is running.
254
255 """)
256                     sys.exit(61)
257
258         self._afterInit()
259         self.autoBindEvents()
260        
261
262     def __del__(self):
263         """Make sure that temp files are removed"""
264         self._tempFileHolder.release()
265        
266
267     def setup(self, initUI=True):
268         """Set up the application object."""
269         # dabo is going to want to import various things from the Home Directory
270         if self.HomeDirectory not in sys.path:
271             sys.path.append(self.HomeDirectory)
272        
273         def initAppInfo(item, default):
274             if not self.getAppInfo(item):
275                 self.setAppInfo(item, default)
276
277         initAppInfo("appName", "Dabo Application")
278         initAppInfo("appShortName", self.getAppInfo("appName").replace(" ", ""))
279         initAppInfo("appVersion", "")
280         initAppInfo("vendorName", "")
281
282         # If there's a locale directory for the app and it looks valid, install it:
283         localeDir = os.path.join(self.HomeDirectory, "locale")
284         localeDomain = self.getAppInfo("appShortName").replace(" ", "_").lower()
285         if os.path.isdir(localeDir) and dLocalize.isValidDomain(localeDomain, localeDir):
286             lang = getattr(self, "_language", None)
287             charset = getattr(self, "_charset", None)
288             dLocalize.install(localeDomain, localeDir)
289             dLocalize.setLanguage(lang, charset)
290
291         self._initModuleNames()
292         self._initDB()
293
294         if initUI:
295             self._initUI()
296             if self.UI is not None:
297                 if self.showSplashScreen:
298                     #self.uiApp = dabo.ui.uiApp(self, callback=self.initUIApp)
299                     self.uiApp = dabo.ui.getUiApp(self, callback=self.initUIApp)
300                 else:
301                     #self.uiApp = dabo.ui.uiApp(self, callback=None)
302                     self.uiApp = dabo.ui.getUiApp(self, callback=None)
303                     self.initUIApp()
304         else:
305             self.uiApp = None
306         # Flip the flag
307         self._wasSetup = True
308         # Call the afterSetup hook
309         self.afterSetup()
310
311
312     def afterSetup(self):
313         """Hook method that is called after the app's setup code has run, and the
314         database, UI and module references have all been established.
315         """
316         pass
317
318
319     def startupForms(self):
320         """Open one or more of the defined forms. The default one is specified
321         in .default_form. If form names were passed on the command line,
322         they will be opened instead of the default one as long as they exist.
323         """
324         form_names = [class_name[3:] for class_name in dir(self.ui) if class_name[:3] == "Frm"]
325         for arg in sys.argv[1:]:
326             arg = arg.lower()
327             for form_name in form_names:
328                 if arg == form_name.lower():
329                     self.formsToOpen.append(getattr(self.ui, "Frm%s" % form_name))
330         if not self.formsToOpen:
331             self.formsToOpen.append(self.default_form)
332         for frm in self.formsToOpen:
333             frm(self.MainForm).show()
334
335     def initUIApp(self):
336         """Callback from the initial app setup. Used to allow the
337         splash screen, if any, to be shown quickly.
338         """
339         self.uiApp.setup()
340
341
342     def start(self):
343         """Start the application event loop."""
344         if not self._wasSetup:
345             # Convenience; if you don't need to customize setup(), just
346             # call start()
347             self.setup()
348
349         self._finished = False
350         if (not self.SecurityManager or not self.SecurityManager.RequireAppLogin
351             or self.SecurityManager.login()):
352            
353             userName = self.getUserCaption()
354             if userName:
355                 userName = " (%s)" % userName
356             else:
357                 userName = ""
358            
359             self._retrieveMRUs()
360             self.uiApp.start(self)
361         if not self._finished:
362             self.finish()
363    
364    
365     def finish(self):
366         """Called when the application event loop has ended. You may also
367         call this explicitly to exit the application event loop.
368         """
369         self.uiApp.exit()
370         self._persistMRU()
371         self.uiApp.finish()
372         self.closeConnections()
373         self._tempFileHolder.release()
374         dabo.infoLog.write(_("Application finished."))
375         self._finished = True
376         self.afterFinish()
377
378
379     def afterFinish(self):
380         """Stub method. When this is called, the app has already terminated, and you have
381         one last chance to execute code by overriding this method.
382         """
383         pass
384
385
386     def _setProjInfo(self):
387         """Create a 2-tuple containing the project location and project name, if any.
388         The location is always the directory containing the initial startup program; the
389         name is either None (default), or, if this is a Web Update-able app, the
390         descriptive name.
391         """
392         relpth = inspect.stack()[-1][1]
393         op = os.path
394         pth, fnm = op.split(op.normpath(op.join(os.getcwd(), relpth)))
395         projnames = {"ClassDesigner.py": "Class Designer",
396                 "CxnEditor.py": "Cxn Editor",
397                 "Editor.py": "Editor",
398                 "MenuDesigner.py": "Menu Designer",
399                 "PrefEditor.py": "Preference Editor",
400                 "ReportDesigner.py": "Report Designer",
401                 "DaboDemo.py": "DaboDemo"}
402         nm = projnames.get(fnm, None)
403         if nm is None:
404             if "wizards" in pth:
405                 nm ="Wizards"
406         self._projectInfo = (pth, nm)
407        
408        
409     def getLoginInfo(self, message=None):
410         """Return a tuple of (user, password) to dSecurityManager.login(). The default is to display the standard login dialog, and return the user/password as entered by the user, but subclasses can override to get the information from whereever is appropriate. You can customize the default dialog by adding your own code to the loginDialogHook() method, which will receive a reference to the login dialog.
411
412         Return a tuple of (user, pass).
413         """
414         import dabo.ui.dialogs.login as login
415         ld = login.Login(self.MainForm)
416         ld.setMessage(message)
417         # Allow the developer to customize the default login
418         self.loginDialogHook(ld)
419         ld.show()
420         user, password = ld.user, ld.password
421         return user, password
422    
423    
424     def loginDialogHook(self, dlg):
425         """Hook method; modify the dialog as needed."""
426         pass
427    
428    
429     def _persistMRU(self):
430         """Persist any MRU lists to disk."""
431         base = "MRU.%s" % self.getAppInfo("appName")
432         self.deleteAllUserSettings(base)       
433         for cap in self._persistentMRUs.keys():
434             mruList = self.uiApp.getMRUListForMenu(cap)
435             setName = ".".join((base, cap))
436             self.setUserSetting(setName, mruList)
437    
438    
439     def _retrieveMRUs(self):
440         """Retrieve any saved MRU lists."""
441         base = "MRU.%s" % self.getAppInfo("appName")
442         for cap, fcn in self._persistentMRUs.items():
443             itms = self.getUserSetting(".".join((base, cap)))
444             if itms:
445                 # Should be a list of items. Add 'em in reverse order
446                 for itm in itms:
447                     self.uiApp.addToMRU(cap, itm, fcn)
448        
449
450     def getAppInfo(self, item):
451         """Look up the item, and return the value."""
452         try:
453             retVal = self._appInfo[item]
454         except KeyError:
455             retVal = None
456         return retVal
457
458
459     def setAppInfo(self, item, value):
460         """Set item to value in the appinfo table."""
461         self._appInfo[item] = value
462
463
464     def _currentUpdateVersion(self, proj):
465         if proj == "Dabo":
466             localVers = dabo.version["file_revision"]
467             try:
468                 localVers = localVers.split(":")[1]
469             except IndexError:
470                 # Not a mixed version
471                 pass
472             ret = int("".join([ch for ch in localVers if ch.isdigit()]))
473         else:
474             ret = self.PreferenceManager.getValue("current_version")
475             if ret is None:
476                 ret = 0         
477         return ret
478
479    
480     def _resetWebUpdateCheck(self):
481         """Sets the time that Web Update was last checked to the passed value. Used
482         in cases where errors prevent an update from succeeding.
483         """
484         for setter, val in self._lastCheckInfo:
485             setter("last_check", val)
486
487
488     def checkForUpdates(self, evt=None):
489         """Public interface to the web updates mechanism. Returns a 2-tuple
490         consisting of a boolean and a list of projects available for update. The boolean
491         indicates whether this is the first time that the framework is being run and the list
492         contains the updatable project names; e.g., if both project 'Foo' and the framework
493         have updates, it will return ["Dabo", "Foo"]; if only the framework has updates, it will
494         return ["Dabo"]. If there are no updates available, an empty list will be returned.
495         """
496         return self.uiApp.checkForUpdates(force=True)
497
498
499     def _checkForUpdates(self, force=False):
500         """This is the actual code that checks if a) we are using Web Update; b) if we are
501         due for a check; and then c) returns the status of the available updates, if any.
502         """
503         frameloc = dabo.frameworkPath
504         pth, projectName = self._projectInfo
505         if not force:
506             # Check for cases where we absolutely will not Web Update.
507             update = dabo.settings.checkForWebUpdates
508             if update:
509                 # If they are running Subversion, don't update.
510                 if pth is None:
511                     pth = frameloc
512                 if os.path.isdir(os.path.join(pth, ".svn")):
513                     update = False
514                 # Frozen App:
515                 if hasattr(sys, "frozen") and inspect.stack()[-1][1] != "daborun.py":
516                     update = False
517
518             if not update:
519                 self._setWebUpdate(False)
520                 return (False, [])
521
522         # First check the framework. If it has updates available, return that info. If not,
523         # see if this is an updatable project. If so, check if it has updates available, and
524         # return that info.
525         prefs = [("Dabo", self._frameworkPrefs)]
526         if projectName is not None:
527             prefs.insert(0, (projectName, self.PreferenceManager))
528         retFirstTime = False
529         retUpdateNames = []
530         self._lastCheckInfo = []
531         for nm, prf in prefs:
532             val = prf.getValue
533             abbrev = self.projectAbbrevs.get(nm.lower())
534             retFirstTime = (nm == "Dabo") and not prf.hasKey("web_update")
535             retAvailable = False
536             lastcheck = val("last_check")
537             # Store the pref setter and the time so that we can reset the value
538             # in case the update fails later.
539             self._lastCheckInfo.append((prf.setValue, lastcheck))
540             if not retFirstTime:
541                 runCheck = force
542                 now = datetime.datetime.now()
543                 if not force:
544                     webUpdate = val("web_update")
545                     if webUpdate:
546                         checkInterval = val("update_interval")
547                         if checkInterval is None:
548                             # Default to one day
549                             checkInterval = 24 * 60
550                         mins = datetime.timedelta(minutes=checkInterval)
551                         if lastcheck is None:
552                             lastcheck = datetime.datetime(1900, 1, 1)
553                         runCheck = (now > (lastcheck + mins))
554                 if runCheck:
555                     # See if there is a later version
556                     url = "http://dabodev.com/frameworkVersions/latest?project=%s" % abbrev
557                     try:
558                         vers = int(urllib2.urlopen(url).read())
559                     except ValueError:
560                         vers = -1
561                     except StandardError, e:
562                         dabo.errorLog.write(_("Failed to open URL '%(url)s'. Error: %(e)s") % locals())
563                     localVers = self._currentUpdateVersion(nm)
564                     retAvailable = (localVers < vers)
565                 prf.setValue("last_check", now)
566                 if retFirstTime or retAvailable:
567                     retUpdateNames.append(nm)
568         return (retFirstTime, retUpdateNames)
569
570
571     def _updateFramework(self, projNames=None):
572         """Get any changed files from the dabodev.com server, and replace
573         the local copies with them. Return the new revision number"""
574         fileurl = "http://dabodev.com/frameworkVersions/changedFiles/%s/%s"
575         for pn in projNames:
576             currvers = self._currentUpdateVersion(pn)
577             if pn == "Dabo":
578                 localBasePath = dabo.frameworkPath
579             else:
580                 localBasePath = self._projectInfo[0]
581             abbrev = self.projectAbbrevs[pn.lower()]
582             webpath = self.webUpdateDirs[pn.lower()]
583             try:
584                 resp = urllib2.urlopen(fileurl % (