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 import __builtin__
34 from roundup import configuration, mailgw
35 from roundup import hyperdb, backends, actions
36 from roundup.cgi import client, templating
37 from roundup.cgi import actions as cgi_actions
39 class Vars:
40 def __init__(self, vars):
41 self.__dict__.update(vars)
43 class Tracker:
44 def __init__(self, tracker_home, optimize=0):
45 """New-style tracker instance constructor
47 Parameters:
48 tracker_home:
49 tracker home directory
50 optimize:
51 if set, precompile html templates
53 """
54 self.tracker_home = tracker_home
55 self.optimize = optimize
56 # if set, call schema_hook after executing schema.py will get
57 # same variables (in particular db) as schema.py main purpose is
58 # for regression tests
59 self.schema_hook = None
60 self.config = configuration.CoreConfig(tracker_home)
61 self.actions = {}
62 self.cgi_actions = {}
63 self.templating_utils = {}
64 self.load_interfaces()
65 self.templates = templating.Templates(self.config["TEMPLATES"])
66 self.backend = backends.get_backend(self.get_backend_name())
67 if self.optimize:
68 libdir = os.path.join(self.tracker_home, 'lib')
69 if os.path.isdir(libdir):
70 sys.path.insert(1, libdir)
71 self.templates.precompileTemplates()
72 # initialize tracker extensions
73 for extension in self.get_extensions('extensions'):
74 extension(self)
75 # load database schema
76 schemafilename = os.path.join(self.tracker_home, 'schema.py')
77 # Note: can't use built-in open()
78 # because of the global function with the same name
79 schemafile = file(schemafilename, 'rt')
80 self.schema = compile(schemafile.read(), schemafilename, 'exec')
81 schemafile.close()
82 # load database detectors
83 self.detectors = self.get_extensions('detectors')
84 # db_open is set to True after first open()
85 self.db_open = 0
86 if libdir in sys.path:
87 sys.path.remove(libdir)
89 def get_backend_name(self):
90 f = file(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 :