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 f = file(os.path.join(self.config.DATABASE, 'backend_name'))
90 name = f.readline().strip()
91 f.close()
92 return name
94 def open(self, name=None):
95 # load the database schema
96 # we cannot skip this part even if self.optimize is set
97 # because the schema has security settings that must be
98 # applied to each database instance
99 backend = self.backend
100 vars = {
101 'Class': backend.Class,
102 'FileClass': backend.FileClass,
103 'IssueClass': backend.IssueClass,
104 'String': hyperdb.String,
105 'Password': hyperdb.Password,
106 'Date': hyperdb.Date,
107 'Link': hyperdb.Link,
108 'Multilink': hyperdb.Multilink,
109 'Interval': hyperdb.Interval,
110 'Boolean': hyperdb.Boolean,
111 'Number': hyperdb.Number,
112 'db': backend.Database(self.config, name)
113 }
115 libdir = os.path.join(self.tracker_home, 'lib')
116 if os.path.isdir(libdir):
117 sys.path.insert(1, libdir)
118 if self.optimize:
119 # execute preloaded schema object
120 exec(self.schema, vars)
121 if callable (self.schema_hook):
122 self.schema_hook(**vars)
123 # use preloaded detectors
124 detectors = self.detectors
125 else:
126 # execute the schema file
127 self._load_python('schema.py', vars)
128 if callable (self.schema_hook):
129 self.schema_hook(**vars)
130 # reload extensions and detectors
131 for extension in self.get_extensions('extensions'):
132 extension(self)
133 detectors = self.get_extensions('detectors')
134 if libdir in sys.path:
135 sys.path.remove(libdir)
136 db = vars['db']
137 # apply the detectors
138 for detector in detectors:
139 detector(db)
140 # if we are running in debug mode
141 # or this is the first time the database is opened,
142 # do database upgrade checks
143 if not (self.optimize and self.db_open):
144 # As a consistency check, ensure that every link property is
145 # pointing at a defined class. Otherwise, the schema is
146 # internally inconsistent. This is an important safety
147 # measure as it protects against an accidental schema change
148 # dropping a table while there are still links to the table;
149 # once the table has been dropped, there is no way to get it
150 # back, so it is important to drop it only if we are as sure
151 # as possible that it is no longer needed.
152 classes = db.getclasses()
153 for classname in classes:
154 cl = db.getclass(classname)
155 for propname, prop in cl.getprops().iteritems():
156 if not isinstance(prop, (hyperdb.Link,
157 hyperdb.Multilink)):
158 continue
159 linkto = prop.classname
160 if linkto not in classes:
161 raise ValueError, \
162 ("property %s.%s links to non-existent class %s"
163 % (classname, propname, linkto))
165 db.post_init()
166 self.db_open = 1
167 return db
169 def load_interfaces(self):
170 """load interfaces.py (if any), initialize Client and MailGW attrs"""
171 vars = {}
172 if os.path.isfile(os.path.join(self.tracker_home, 'interfaces.py')):
173 self._load_python('interfaces.py', vars)
174 self.Client = vars.get('Client', client.Client)
175 self.MailGW = vars.get('MailGW', mailgw.MailGW)
177 def get_extensions(self, dirname):
178 """Load python extensions
180 Parameters:
181 dirname:
182 extension directory name relative to tracker home
184 Return value:
185 list of init() functions for each extension
187 """
188 extensions = []
189 dirpath = os.path.join(self.tracker_home, dirname)
190 if os.path.isdir(dirpath):
191 sys.path.insert(1, dirpath)
192 for name in os.listdir(dirpath):
193 if not name.endswith('.py'):
194 continue
195 vars = {}
196 self._load_python(os.path.join(dirname, name), vars)
197 extensions.append(vars['init'])
198 sys.path.remove(dirpath)
199 return extensions
201 def init(self, adminpw):
202 db = self.open('admin')
203 self._load_python('initial_data.py', {'db': db, 'adminpw': adminpw,
204 'admin_email': self.config['ADMIN_EMAIL']})
205 db.commit()
206 db.close()
208 def exists(self):
209 return self.backend.db_exists(self.config)
211 def nuke(self):
212 self.backend.db_nuke(self.config)
214 def _load_python(self, file, vars):
215 file = os.path.join(self.tracker_home, file)
216 execfile(file, vars)
217 return vars
219 def registerAction(self, name, action):
221 # The logic here is this:
222 # * if `action` derives from actions.Action,
223 # it is executable as a generic action.
224 # * if, moreover, it also derives from cgi.actions.Bridge,
225 # it may in addition be called via CGI
226 # * in all other cases we register it as a CGI action, without
227 # any check (for backward compatibility).
228 if issubclass(action, actions.Action):
229 self.actions[name] = action
230 if issubclass(action, cgi_actions.Bridge):
231 self.cgi_actions[name] = action
232 else:
233 self.cgi_actions[name] = action
235 def registerUtil(self, name, function):
236 self.templating_utils[name] = function
238 class TrackerError(Exception):
239 pass
242 class OldStyleTrackers:
243 def __init__(self):
244 self.number = 0
245 self.trackers = {}
247 def open(self, tracker_home, optimize=0):
248 """Open the tracker.
250 Parameters:
251 tracker_home:
252 tracker home directory
253 optimize:
254 if set, precompile html templates
256 Raise ValueError if the tracker home doesn't exist.
258 """
259 import imp
260 # sanity check existence of tracker home
261 if not os.path.exists(tracker_home):
262 raise ValueError, 'no such directory: "%s"'%tracker_home
264 # sanity check tracker home contents
265 for reqd in 'config dbinit select_db interfaces'.split():
266 if not os.path.exists(os.path.join(tracker_home, '%s.py'%reqd)):
267 raise TrackerError, 'File "%s.py" missing from tracker '\
268 'home "%s"'%(reqd, tracker_home)
270 if self.trackers.has_key(tracker_home):
271 return imp.load_package(self.trackers[tracker_home],
272 tracker_home)
273 # register all available backend modules
274 backends.list_backends()
275 self.number = self.number + 1
276 modname = '_roundup_tracker_%s'%self.number
277 self.trackers[tracker_home] = modname
279 # load the tracker
280 tracker = imp.load_package(modname, tracker_home)
282 # ensure the tracker has all the required bits
283 for required in 'open init Client MailGW'.split():
284 if not hasattr(tracker, required):
285 raise TrackerError, \
286 'Required tracker attribute "%s" missing'%required
288 # load and apply the config
289 tracker.config = configuration.CoreConfig(tracker_home)
290 tracker.dbinit.config = tracker.config
292 tracker.optimize = optimize
293 tracker.templates = templating.Templates(tracker.config["TEMPLATES"])
294 if optimize:
295 tracker.templates.precompileTemplates()
297 return tracker
299 OldStyleTrackers = OldStyleTrackers()
300 def open(tracker_home, optimize=0):
301 if os.path.exists(os.path.join(tracker_home, 'dbinit.py')):
302 # user should upgrade...
303 return OldStyleTrackers.open(tracker_home, optimize=optimize)
305 return Tracker(tracker_home, optimize=optimize)
307 # vim: set filetype=python sts=4 sw=4 et si :