Code

Sending of PGP-Encrypted mail to all users or selected users (via roles)
[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 """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         f = file(os.path.join(self.config.DATABASE, 'backend_name'))
90         name = f.readline().strip()
91         f.close()
92         return name
94     def open(self, name=None):
95         # load the database schema
96         # we cannot skip this part even if self.optimize is set
97         # because the schema has security settings that must be
98         # applied to each database instance
99         backend = self.backend
100         vars = {
101             'Class': backend.Class,
102             'FileClass': backend.FileClass,
103             'IssueClass': backend.IssueClass,
104             'String': hyperdb.String,
105             'Password': hyperdb.Password,
106             'Date': hyperdb.Date,
107             'Link': hyperdb.Link,
108             'Multilink': hyperdb.Multilink,
109             'Interval': hyperdb.Interval,
110             'Boolean': hyperdb.Boolean,
111             'Number': hyperdb.Number,
112             'db': backend.Database(self.config, name)
113         }
115         libdir = os.path.join(self.tracker_home, 'lib')
116         if os.path.isdir(libdir):
117             sys.path.insert(1, libdir)
118         if self.optimize:
119             # execute preloaded schema object
120             exec(self.schema, vars)
121             if callable (self.schema_hook):
122                 self.schema_hook(**vars)
123             # use preloaded detectors
124             detectors = self.detectors
125         else:
126             # execute the schema file
127             self._load_python('schema.py', vars)
128             if callable (self.schema_hook):
129                 self.schema_hook(**vars)
130             # reload extensions and detectors
131             for extension in self.get_extensions('extensions'):
132                 extension(self)
133             detectors = self.get_extensions('detectors')
134         if libdir in sys.path:
135             sys.path.remove(libdir)
136         db = vars['db']
137         # apply the detectors
138         for detector in detectors:
139             detector(db)
140         # if we are running in debug mode
141         # or this is the first time the database is opened,
142         # do database upgrade checks
143         if not (self.optimize and self.db_open):
144             # As a consistency check, ensure that every link property is
145             # pointing at a defined class.  Otherwise, the schema is
146             # internally inconsistent.  This is an important safety
147             # measure as it protects against an accidental schema change
148             # dropping a table while there are still links to the table;
149             # once the table has been dropped, there is no way to get it
150             # back, so it is important to drop it only if we are as sure
151             # as possible that it is no longer needed.
152             classes = db.getclasses()
153             for classname in classes:
154                 cl = db.getclass(classname)
155                 for propname, prop in cl.getprops().iteritems():
156                     if not isinstance(prop, (hyperdb.Link,
157                                              hyperdb.Multilink)):
158                         continue
159                     linkto = prop.classname
160                     if linkto not in classes:
161                         raise ValueError, \
162                             ("property %s.%s links to non-existent class %s"
163                              % (classname, propname, linkto))
165             db.post_init()
166             self.db_open = 1
167         return db
169     def load_interfaces(self):
170         """load interfaces.py (if any), initialize Client and MailGW attrs"""
171         vars = {}
172         if os.path.isfile(os.path.join(self.tracker_home, 'interfaces.py')):
173             self._load_python('interfaces.py', vars)
174         self.Client = vars.get('Client', client.Client)
175         self.MailGW = vars.get('MailGW', mailgw.MailGW)
177     def get_extensions(self, dirname):
178         """Load python extensions
180         Parameters:
181             dirname:
182                 extension directory name relative to tracker home
184         Return value:
185             list of init() functions for each extension
187         """
188         extensions = []
189         dirpath = os.path.join(self.tracker_home, dirname)
190         if os.path.isdir(dirpath):
191             sys.path.insert(1, dirpath)
192             for name in os.listdir(dirpath):
193                 if not name.endswith('.py'):
194                     continue
195                 vars = {}
196                 self._load_python(os.path.join(dirname, name), vars)
197                 extensions.append(vars['init'])
198             sys.path.remove(dirpath)
199         return extensions
201     def init(self, adminpw):
202         db = self.open('admin')
203         self._load_python('initial_data.py', {'db': db, 'adminpw': adminpw,
204             'admin_email': self.config['ADMIN_EMAIL']})
205         db.commit()
206         db.close()
208     def exists(self):
209         return self.backend.db_exists(self.config)
211     def nuke(self):
212         self.backend.db_nuke(self.config)
214     def _load_python(self, file, vars):
215         file = os.path.join(self.tracker_home, file)
216         execfile(file, vars)
217         return vars
219     def registerAction(self, name, action):
221         # The logic here is this:
222         # * if `action` derives from actions.Action,
223         #   it is executable as a generic action.
224         # * if, moreover, it also derives from cgi.actions.Bridge,
225         #   it may in addition be called via CGI
226         # * in all other cases we register it as a CGI action, without
227         #   any check (for backward compatibility).
228         if issubclass(action, actions.Action):
229             self.actions[name] = action
230             if issubclass(action, cgi_actions.Bridge):
231                 self.cgi_actions[name] = action
232         else:
233             self.cgi_actions[name] = action
235     def registerUtil(self, name, function):
236         self.templating_utils[name] = function
238 class TrackerError(Exception):
239     pass
242 class OldStyleTrackers:
243     def __init__(self):
244         self.number = 0
245         self.trackers = {}
247     def open(self, tracker_home, optimize=0):
248         """Open the tracker.
250         Parameters:
251             tracker_home:
252                 tracker home directory
253             optimize:
254                 if set, precompile html templates
256         Raise ValueError if the tracker home doesn't exist.
258         """
259         import imp
260         # sanity check existence of tracker home
261         if not os.path.exists(tracker_home):
262             raise ValueError, 'no such directory: "%s"'%tracker_home
264         # sanity check tracker home contents
265         for reqd in 'config dbinit select_db interfaces'.split():
266             if not os.path.exists(os.path.join(tracker_home, '%s.py'%reqd)):
267                 raise TrackerError, 'File "%s.py" missing from tracker '\
268                     'home "%s"'%(reqd, tracker_home)
270         if self.trackers.has_key(tracker_home):
271             return imp.load_package(self.trackers[tracker_home],
272                 tracker_home)
273         # register all available backend modules
274         backends.list_backends()
275         self.number = self.number + 1
276         modname = '_roundup_tracker_%s'%self.number
277         self.trackers[tracker_home] = modname
279         # load the tracker
280         tracker = imp.load_package(modname, tracker_home)
282         # ensure the tracker has all the required bits
283         for required in 'open init Client MailGW'.split():
284             if not hasattr(tracker, required):
285                 raise TrackerError, \
286                     'Required tracker attribute "%s" missing'%required
288         # load and apply the config
289         tracker.config = configuration.CoreConfig(tracker_home)
290         tracker.dbinit.config = tracker.config
292         tracker.optimize = optimize
293         tracker.templates = templating.Templates(tracker.config["TEMPLATES"])
294         if optimize:
295             tracker.templates.precompileTemplates()
297         return tracker
299 OldStyleTrackers = OldStyleTrackers()
300 def open(tracker_home, optimize=0):
301     if os.path.exists(os.path.join(tracker_home, 'dbinit.py')):
302         # user should upgrade...
303         return OldStyleTrackers.open(tracker_home, optimize=optimize)
305     return Tracker(tracker_home, optimize=optimize)
307 # vim: set filetype=python sts=4 sw=4 et si :