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 libdir = os.path.join(self.tracker_home, 'lib')
117 if os.path.isdir(libdir):
118 sys.path.insert(1, libdir)
119 if self.optimize:
120 # execute preloaded schema object
121 exec(self.schema, vars)
122 if callable (self.schema_hook):
123 self.schema_hook(**vars)
124 # use preloaded detectors
125 detectors = self.detectors
126 else:
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 :