Code

use config.DATABASE in cases where 'db' was still hard-coded
[roundup.git] / roundup / instance.py
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 :