Code

- add comment to clarify semantics if pytz is installed
[roundup.git] / roundup / instance.py
1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
19 """Top-level tracker interface.
21 Open a tracker with:
23     >>> from roundup import instance
24     >>> db = instance.open('path to tracker home')
26 The "db" handle you get back is the tracker's hyperdb which has the interface
27 described in `roundup.hyperdb.Database`.
28 """
29 __docformat__ = 'restructuredtext'
31 import os
32 import sys
33 from roundup import configuration, mailgw
34 from roundup import hyperdb, backends, actions
35 from roundup.cgi import client, templating
36 from roundup.cgi import actions as cgi_actions
38 class Vars:
39     def __init__(self, vars):
40         self.__dict__.update(vars)
42 class Tracker:
43     def __init__(self, tracker_home, optimize=0):
44         """New-style tracker instance constructor
46         Parameters:
47             tracker_home:
48                 tracker home directory
49             optimize:
50                 if set, precompile html templates
52         """
53         self.tracker_home = tracker_home
54         self.optimize = optimize
55         # if set, call schema_hook after executing schema.py will get
56         # same variables (in particular db) as schema.py main purpose is
57         # for regression tests
58         self.schema_hook = None
59         self.config = configuration.CoreConfig(tracker_home)
60         self.actions = {}
61         self.cgi_actions = {}
62         self.templating_utils = {}
63         self.load_interfaces()
64         self.templates = templating.Templates(self.config["TEMPLATES"])
65         self.backend = backends.get_backend(self.get_backend_name())
66         if self.optimize:
67             libdir = os.path.join(self.tracker_home, 'lib')
68             if os.path.isdir(libdir):
69                 sys.path.insert(1, libdir)
70             self.templates.precompileTemplates()
71             # initialize tracker extensions
72             for extension in self.get_extensions('extensions'):
73                 extension(self)
74             # load database schema
75             schemafilename = os.path.join(self.tracker_home, 'schema.py')
76             # Note: can't use built-in open()
77             #   because of the global function with the same name
78             schemafile = file(schemafilename, 'rt')
79             self.schema = compile(schemafile.read(), schemafilename, 'exec')
80             schemafile.close()
81             # load database detectors
82             self.detectors = self.get_extensions('detectors')
83             # db_open is set to True after first open()
84             self.db_open = 0
85             if libdir in sys.path:
86                 sys.path.remove(libdir)
88     def get_backend_name(self):
89         o = __builtins__['open']
90         f = o(os.path.join(self.config.DATABASE, 'backend_name'))
91         name = f.readline().strip()
92         f.close()
93         return name
95     def open(self, name=None):
96         # load the database schema
97         # we cannot skip this part even if self.optimize is set
98         # because the schema has security settings that must be
99         # applied to each database instance
100         backend = self.backend
101         vars = {
102             'Class': backend.Class,
103             'FileClass': backend.FileClass,
104             'IssueClass': backend.IssueClass,
105             'String': hyperdb.String,
106             'Password': hyperdb.Password,
107             'Date': hyperdb.Date,
108             'Link': hyperdb.Link,
109             'Multilink': hyperdb.Multilink,
110             'Interval': hyperdb.Interval,
111             'Boolean': hyperdb.Boolean,
112             'Number': hyperdb.Number,
113             'db': backend.Database(self.config, name)
114         }
116         if self.optimize:
117             # execute preloaded schema object
118             exec(self.schema, vars)
119             if callable (self.schema_hook):
120                 self.schema_hook(**vars)
121             # use preloaded detectors
122             detectors = self.detectors
123         else:
124             libdir = os.path.join(self.tracker_home, 'lib')
125             if os.path.isdir(libdir):
126                 sys.path.insert(1, libdir)
127             # execute the schema file
128             self._load_python('schema.py', vars)
129             if callable (self.schema_hook):
130                 self.schema_hook(**vars)
131             # reload extensions and detectors
132             for extension in self.get_extensions('extensions'):
133                 extension(self)
134             detectors = self.get_extensions('detectors')
135             if libdir in sys.path:
136                 sys.path.remove(libdir)
137         db = vars['db']
138         # apply the detectors
139         for detector in detectors:
140             detector(db)
141         # if we are running in debug mode
142         # or this is the first time the database is opened,
143         # do database upgrade checks
144         if not (self.optimize and self.db_open):
145             # As a consistency check, ensure that every link property is
146             # pointing at a defined class.  Otherwise, the schema is
147             # internally inconsistent.  This is an important safety
148             # measure as it protects against an accidental schema change
149             # dropping a table while there are still links to the table;
150             # once the table has been dropped, there is no way to get it
151             # back, so it is important to drop it only if we are as sure
152             # as possible that it is no longer needed.
153             classes = db.getclasses()
154             for classname in classes:
155                 cl = db.getclass(classname)
156                 for propname, prop in cl.getprops().iteritems():
157                     if not isinstance(prop, (hyperdb.Link,
158                                              hyperdb.Multilink)):
159                         continue
160                     linkto = prop.classname
161                     if linkto not in classes:
162                         raise ValueError, \
163                             ("property %s.%s links to non-existent class %s"
164                              % (classname, propname, linkto))
166             db.post_init()
167             self.db_open = 1
168         return db
170     def load_interfaces(self):
171         """load interfaces.py (if any), initialize Client and MailGW attrs"""
172         vars = {}
173         if os.path.isfile(os.path.join(self.tracker_home, 'interfaces.py')):
174             self._load_python('interfaces.py', vars)
175         self.Client = vars.get('Client', client.Client)
176         self.MailGW = vars.get('MailGW', mailgw.MailGW)
178     def get_extensions(self, dirname):
179         """Load python extensions
181         Parameters:
182             dirname:
183                 extension directory name relative to tracker home
185         Return value:
186             list of init() functions for each extension
188         """
189         extensions = []
190         dirpath = os.path.join(self.tracker_home, dirname)
191         if os.path.isdir(dirpath):
192             sys.path.insert(1, dirpath)
193             for name in os.listdir(dirpath):
194                 if not name.endswith('.py'):
195                     continue
196                 vars = {}
197                 self._load_python(os.path.join(dirname, name), vars)
198                 extensions.append(vars['init'])
199             sys.path.remove(dirpath)
200         return extensions
202     def init(self, adminpw):
203         db = self.open('admin')
204         self._load_python('initial_data.py', {'db': db, 'adminpw': adminpw,
205             'admin_email': self.config['ADMIN_EMAIL']})
206         db.commit()
207         db.close()
209     def exists(self):
210         return self.backend.db_exists(self.config)
212     def nuke(self):
213         self.backend.db_nuke(self.config)
215     def _load_python(self, file, vars):
216         file = os.path.join(self.tracker_home, file)
217         execfile(file, vars)
218         return vars
220     def registerAction(self, name, action):
222         # The logic here is this:
223         # * if `action` derives from actions.Action,
224         #   it is executable as a generic action.
225         # * if, moreover, it also derives from cgi.actions.Bridge,
226         #   it may in addition be called via CGI
227         # * in all other cases we register it as a CGI action, without
228         #   any check (for backward compatibility).
229         if issubclass(action, actions.Action):
230             self.actions[name] = action
231             if issubclass(action, cgi_actions.Bridge):
232                 self.cgi_actions[name] = action
233         else:
234             self.cgi_actions[name] = action
236     def registerUtil(self, name, function):
237         self.templating_utils[name] = function
239 class TrackerError(Exception):
240     pass
243 class OldStyleTrackers:
244     def __init__(self):
245         self.number = 0
246         self.trackers = {}
248     def open(self, tracker_home, optimize=0):
249         """Open the tracker.
251         Parameters:
252             tracker_home:
253                 tracker home directory
254             optimize:
255                 if set, precompile html templates
257         Raise ValueError if the tracker home doesn't exist.
259         """
260         import imp
261         # sanity check existence of tracker home
262         if not os.path.exists(tracker_home):
263             raise ValueError, 'no such directory: "%s"'%tracker_home
265         # sanity check tracker home contents
266         for reqd in 'config dbinit select_db interfaces'.split():
267             if not os.path.exists(os.path.join(tracker_home, '%s.py'%reqd)):
268                 raise TrackerError, 'File "%s.py" missing from tracker '\
269                     'home "%s"'%(reqd, tracker_home)
271         if self.trackers.has_key(tracker_home):
272             return imp.load_package(self.trackers[tracker_home],
273                 tracker_home)
274         # register all available backend modules
275         backends.list_backends()
276         self.number = self.number + 1
277         modname = '_roundup_tracker_%s'%self.number
278         self.trackers[tracker_home] = modname
280         # load the tracker
281         tracker = imp.load_package(modname, tracker_home)
283         # ensure the tracker has all the required bits
284         for required in 'open init Client MailGW'.split():
285             if not hasattr(tracker, required):
286                 raise TrackerError, \
287                     'Required tracker attribute "%s" missing'%required
289         # load and apply the config
290         tracker.config = configuration.CoreConfig(tracker_home)
291         tracker.dbinit.config = tracker.config
293         tracker.optimize = optimize
294         tracker.templates = templating.Templates(tracker.config["TEMPLATES"])
295         if optimize:
296             tracker.templates.precompileTemplates()
298         return tracker
300 OldStyleTrackers = OldStyleTrackers()
301 def open(tracker_home, optimize=0):
302     if os.path.exists(os.path.join(tracker_home, 'dbinit.py')):
303         # user should upgrade...
304         return OldStyleTrackers.open(tracker_home, optimize=optimize)
306     return Tracker(tracker_home, optimize=optimize)
308 # vim: set filetype=python sts=4 sw=4 et si :