Commit | Line | Data |
---|---|---|
c638d827 CR |
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 | # | |
18 | ||
19 | """Tracker handling (open tracker). | |
20 | ||
21 | Backwards compatibility for the old-style "imported" trackers. | |
22 | """ | |
23 | __docformat__ = 'restructuredtext' | |
24 | ||
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 | |
31 | ||
32 | class Vars: | |
33 | def __init__(self, vars): | |
34 | self.__dict__.update(vars) | |
35 | ||
36 | class Tracker: | |
37 | def __init__(self, tracker_home, optimize=0): | |
38 | """New-style tracker instance constructor | |
39 | ||
40 | Parameters: | |
41 | tracker_home: | |
42 | tracker home directory | |
43 | optimize: | |
44 | if set, precompile html templates | |
45 | ||
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) | |
81 | ||
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 | |
88 | ||
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 | } | |
109 | ||
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)) | |
159 | ||
160 | db.post_init() | |
161 | self.db_open = 1 | |
162 | return db | |
163 | ||
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) | |
171 | ||
172 | def get_extensions(self, dirname): | |
173 | """Load python extensions | |
174 | ||
175 | Parameters: | |
176 | dirname: | |
177 | extension directory name relative to tracker home | |
178 | ||
179 | Return value: | |
180 | list of init() functions for each extension | |
181 | ||
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 | |
195 | ||
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() | |
202 | ||
203 | def exists(self): | |
204 | return self.backend.db_exists(self.config) | |
205 | ||
206 | def nuke(self): | |
207 | self.backend.db_nuke(self.config) | |
208 | ||
209 | def _load_python(self, file, vars): | |
210 | file = os.path.join(self.tracker_home, file) | |
211 | execfile(file, vars) | |
212 | return vars | |
213 | ||
214 | def registerAction(self, name, action): | |
215 | ||
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 | |
229 | ||
230 | def registerUtil(self, name, function): | |
231 | self.templating_utils[name] = function | |
232 | ||
233 | class TrackerError(Exception): | |
234 | pass | |
235 | ||
236 | ||
237 | class OldStyleTrackers: | |
238 | def __init__(self): | |
239 | self.number = 0 | |
240 | self.trackers = {} | |
241 | ||
242 | def open(self, tracker_home, optimize=0): | |
243 | """Open the tracker. | |
244 | ||
245 | Parameters: | |
246 | tracker_home: | |
247 | tracker home directory | |
248 | optimize: | |
249 | if set, precompile html templates | |
250 | ||
251 | Raise ValueError if the tracker home doesn't exist. | |
252 | ||
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 | |
258 | ||
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) | |
264 | ||
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 | |
273 | ||
274 | # load the tracker | |
275 | tracker = imp.load_package(modname, tracker_home) | |
276 | ||
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 | |
282 | ||
283 | # load and apply the config | |
284 | tracker.config = configuration.CoreConfig(tracker_home) | |
285 | tracker.dbinit.config = tracker.config | |
286 | ||
287 | tracker.optimize = optimize | |
288 | tracker.templates = templating.Templates(tracker.config["TEMPLATES"]) | |
289 | if optimize: | |
290 | tracker.templates.precompileTemplates() | |
291 | ||
292 | return tracker | |
293 | ||
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) | |
299 | ||
300 | return Tracker(tracker_home, optimize=optimize) | |
301 | ||
302 | # vim: set filetype=python sts=4 sw=4 et si : |