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 """Tracker handling (open tracker).
21 Backwards compatibility for the old-style "imported" trackers.
22 """
23 __docformat__ = 'restructuredtext'
25 import os
26 import sys
27 from roundup import configuration, mailgw
28 from roundup import hyperdb, backends, actions
29 from roundup.cgi import client, templating
30 from roundup.cgi import actions as cgi_actions
32 class Vars:
33 def __init__(self, vars):
34 self.__dict__.update(vars)
36 class Tracker:
37 def __init__(self, tracker_home, optimize=0):
38 """New-style tracker instance constructor
40 Parameters:
41 tracker_home:
42 tracker home directory
43 optimize:
44 if set, precompile html templates
46 """
47 self.tracker_home = tracker_home
48 self.optimize = optimize
49 # if set, call schema_hook after executing schema.py will get
50 # same variables (in particular db) as schema.py main purpose is
51 # for regression tests
52 self.schema_hook = None
53 self.config = configuration.CoreConfig(tracker_home)
54 self.actions = {}
55 self.cgi_actions = {}
56 self.templating_utils = {}
57 self.load_interfaces()
58 self.templates = templating.Templates(self.config["TEMPLATES"])
59 self.backend = backends.get_backend(self.get_backend_name())
60 if self.optimize:
61 libdir = os.path.join(self.tracker_home, 'lib')
62 if os.path.isdir(libdir):
63 sys.path.insert(1, libdir)
64 self.templates.precompileTemplates()
65 # initialize tracker extensions
66 for extension in self.get_extensions('extensions'):
67 extension(self)
68 # load database schema
69 schemafilename = os.path.join(self.tracker_home, 'schema.py')
70 # Note: can't use built-in open()
71 # because of the global function with the same name
72 schemafile = file(schemafilename, 'rt')
73 self.schema = compile(schemafile.read(), schemafilename, 'exec')
74 schemafile.close()
75 # load database detectors
76 self.detectors = self.get_extensions('detectors')
77 # db_open is set to True after first open()
78 self.db_open = 0
79 if libdir in sys.path:
80 sys.path.remove(libdir)
82 def get_backend_name(self):
83 o = __builtins__['open']
84 f = o(os.path.join(self.config.DATABASE, 'backend_name'))
85 name = f.readline().strip()
86 f.close()
87 return name
89 def open(self, name=None):
90 # load the database schema
91 # we cannot skip this part even if self.optimize is set
92 # because the schema has security settings that must be
93 # applied to each database instance
94 backend = self.backend
95 vars = {
96 'Class': backend.Class,
97 'FileClass': backend.FileClass,
98 'IssueClass': backend.IssueClass,
99 'String': hyperdb.String,
100 'Password': hyperdb.Password,
101 'Date': hyperdb.Date,
102 'Link': hyperdb.Link,
103 'Multilink': hyperdb.Multilink,
104 'Interval': hyperdb.Interval,
105 'Boolean': hyperdb.Boolean,
106 'Number': hyperdb.Number,
107 'db': backend.Database(self.config, name)
108 }
110 if self.optimize:
111 # execute preloaded schema object
112 exec(self.schema, vars)
113 if callable (self.schema_hook):
114 self.schema_hook(**vars)
115 # use preloaded detectors
116 detectors = self.detectors
117 else:
118 libdir = os.path.join(self.tracker_home, 'lib')
119 if os.path.isdir(libdir):
120 sys.path.insert(1, libdir)
121 # execute the schema file
122 self._load_python('schema.py', vars)
123 if callable (self.schema_hook):
124 self.schema_hook(**vars)
125 # reload extensions and detectors
126 for extension in self.get_extensions('extensions'):
127 extension(self)
128 detectors = self.get_extensions('detectors')
129 if libdir in sys.path:
130 sys.path.remove(libdir)
131 db = vars['db']
132 # apply the detectors
133 for detector in detectors:
134 detector(db)
135 # if we are running in debug mode
136 # or this is the first time the database is opened,
137 # do database upgrade checks
138 if not (self.optimize and self.db_open):
139 # As a consistency check, ensure that every link property is
140 # pointing at a defined class. Otherwise, the schema is
141 # internally inconsistent. This is an important safety
142 # measure as it protects against an accidental schema change
143 # dropping a table while there are still links to the table;
144 # once the table has been dropped, there is no way to get it
145 # back, so it is important to drop it only if we are as sure
146 # as possible that it is no longer needed.
147 classes = db.getclasses()
148 for classname in classes:
149 cl = db.getclass(classname)
150 for propname, prop in cl.getprops().iteritems():
151 if not isinstance(prop, (hyperdb.Link,
152 hyperdb.Multilink)):
153 continue
154 linkto = prop.classname
155 if linkto not in classes:
156 raise ValueError, \
157 ("property %s.%s links to non-existent class %s"
158 % (classname, propname, linkto))
160 db.post_init()
161 self.db_open = 1
162 return db
164 def load_interfaces(self):
165 """load interfaces.py (if any), initialize Client and MailGW attrs"""
166 vars = {}
167 if os.path.isfile(os.path.join(self.tracker_home, 'interfaces.py')):
168 self._load_python('interfaces.py', vars)
169 self.Client = vars.get('Client', client.Client)
170 self.MailGW = vars.get('MailGW', mailgw.MailGW)
172 def get_extensions(self, dirname):
173 """Load python extensions
175 Parameters:
176 dirname:
177 extension directory name relative to tracker home
179 Return value:
180 list of init() functions for each extension
182 """
183 extensions = []
184 dirpath = os.path.join(self.tracker_home, dirname)
185 if os.path.isdir(dirpath):
186 sys.path.insert(1, dirpath)
187 for name in os.listdir(dirpath):
188 if not name.endswith('.py'):
189 continue
190 vars = {}
191 self._load_python(os.path.join(dirname, name), vars)
192 extensions.append(vars['init'])
193 sys.path.remove(dirpath)
194 return extensions
196 def init(self, adminpw):
197 db = self.open('admin')
198 self._load_python('initial_data.py', {'db': db, 'adminpw': adminpw,
199 'admin_email': self.config['ADMIN_EMAIL']})
200 db.commit()
201 db.close()
203 def exists(self):
204 return self.backend.db_exists(self.config)
206 def nuke(self):
207 self.backend.db_nuke(self.config)
209 def _load_python(self, file, vars):
210 file = os.path.join(self.tracker_home, file)
211 execfile(file, vars)
212 return vars
214 def registerAction(self, name, action):
216 # The logic here is this:
217 # * if `action` derives from actions.Action,
218 # it is executable as a generic action.
219 # * if, moreover, it also derives from cgi.actions.Bridge,
220 # it may in addition be called via CGI
221 # * in all other cases we register it as a CGI action, without
222 # any check (for backward compatibility).
223 if issubclass(action, actions.Action):
224 self.actions[name] = action
225 if issubclass(action, cgi_actions.Bridge):
226 self.cgi_actions[name] = action
227 else:
228 self.cgi_actions[name] = action
230 def registerUtil(self, name, function):
231 self.templating_utils[name] = function
233 class TrackerError(Exception):
234 pass
237 class OldStyleTrackers:
238 def __init__(self):
239 self.number = 0
240 self.trackers = {}
242 def open(self, tracker_home, optimize=0):
243 """Open the tracker.
245 Parameters:
246 tracker_home:
247 tracker home directory
248 optimize:
249 if set, precompile html templates
251 Raise ValueError if the tracker home doesn't exist.
253 """
254 import imp
255 # sanity check existence of tracker home
256 if not os.path.exists(tracker_home):
257 raise ValueError, 'no such directory: "%s"'%tracker_home
259 # sanity check tracker home contents
260 for reqd in 'config dbinit select_db interfaces'.split():
261 if not os.path.exists(os.path.join(tracker_home, '%s.py'%reqd)):
262 raise TrackerError, 'File "%s.py" missing from tracker '\
263 'home "%s"'%(reqd, tracker_home)
265 if self.trackers.has_key(tracker_home):
266 return imp.load_package(self.trackers[tracker_home],
267 tracker_home)
268 # register all available backend modules
269 backends.list_backends()
270 self.number = self.number + 1
271 modname = '_roundup_tracker_%s'%self.number
272 self.trackers[tracker_home] = modname
274 # load the tracker
275 tracker = imp.load_package(modname, tracker_home)
277 # ensure the tracker has all the required bits
278 for required in 'open init Client MailGW'.split():
279 if not hasattr(tracker, required):
280 raise TrackerError, \
281 'Required tracker attribute "%s" missing'%required
283 # load and apply the config
284 tracker.config = configuration.CoreConfig(tracker_home)
285 tracker.dbinit.config = tracker.config
287 tracker.optimize = optimize
288 tracker.templates = templating.Templates(tracker.config["TEMPLATES"])
289 if optimize:
290 tracker.templates.precompileTemplates()
292 return tracker
294 OldStyleTrackers = OldStyleTrackers()
295 def open(tracker_home, optimize=0):
296 if os.path.exists(os.path.join(tracker_home, 'dbinit.py')):
297 # user should upgrade...
298 return OldStyleTrackers.open(tracker_home, optimize=optimize)
300 return Tracker(tracker_home, optimize=optimize)
302 # vim: set filetype=python sts=4 sw=4 et si :