1 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
2 # This module is free software, and you may redistribute it and/or modify
3 # under the same terms as Python, so long as this copyright message and
4 # disclaimer are retained in their original form.
5 #
6 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
7 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
8 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
9 # POSSIBILITY OF SUCH DAMAGE.
10 #
11 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
12 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
14 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16 #
17 # $Id: ZRoundup.py,v 1.16 2002-10-18 03:34:58 richard Exp $
18 #
19 ''' ZRoundup module - exposes the roundup web interface to Zope
21 This frontend works by providing a thin layer that sits between Zope and the
22 regular CGI interface of roundup, providing the web frontend with the minimum
23 of effort.
25 This means that the regular CGI interface does all authentication quite
26 independently of Zope. The roundup code is kept in memory though, and it
27 runs in the same server as all your other Zope stuff, so it does have _some_
28 advantages over regular CGI :)
30 It also means that any requests which specify :filter, :columns or :sort
31 _must_ be done using a GET, so that this interface can re-parse the
32 QUERY_STRING. Zope interprets the ':' as a special character, and the special
33 args are lost to it.
34 '''
36 import urlparse
38 from Globals import InitializeClass, HTMLFile
39 from OFS.SimpleItem import Item
40 from OFS.PropertyManager import PropertyManager
41 from Acquisition import Explicit, Implicit
42 from Persistence import Persistent
43 from AccessControl import ClassSecurityInfo
44 from AccessControl import ModuleSecurityInfo
45 modulesecurity = ModuleSecurityInfo()
47 import roundup.instance
48 from roundup.cgi.client import NotFound
50 modulesecurity.declareProtected('View management screens',
51 'manage_addZRoundupForm')
52 manage_addZRoundupForm = HTMLFile('dtml/manage_addZRoundupForm', globals())
54 modulesecurity.declareProtected('Add Z Roundups', 'manage_addZRoundup')
55 def manage_addZRoundup(self, id, instance_home, REQUEST):
56 """Add a ZRoundup product """
57 # validate the instance_home
58 roundup.instance.open(instance_home)
59 self._setObject(id, ZRoundup(id, instance_home))
60 return self.manage_main(self, REQUEST)
62 class RequestWrapper:
63 '''Make the Zope RESPONSE look like a BaseHTTPServer
64 '''
65 def __init__(self, RESPONSE):
66 self.RESPONSE = RESPONSE
67 self.wfile = self.RESPONSE
68 def send_response(self, status):
69 self.RESPONSE.setStatus(status)
70 def send_header(self, header, value):
71 self.RESPONSE.addHeader(header, value)
72 def end_headers(self):
73 # not needed - the RESPONSE object handles this internally on write()
74 pass
76 class FormItem:
77 '''Make a Zope form item look like a cgi.py one
78 '''
79 def __init__(self, value):
80 self.value = value
81 if hasattr(self.value, 'filename'):
82 self.filename = self.value.filename
83 self.file = self.value
85 class FormWrapper:
86 '''Make a Zope form dict look like a cgi.py one
87 '''
88 def __init__(self, form):
89 self.form = form
90 def __getitem__(self, item):
91 return FormItem(self.form[item])
92 def has_key(self, item):
93 return self.form.has_key(item)
94 def keys(self):
95 return self.form.keys()
97 class ZRoundup(Item, PropertyManager, Implicit, Persistent):
98 '''An instance of this class provides an interface between Zope and
99 roundup for one roundup instance
100 '''
101 meta_type = 'Z Roundup'
102 security = ClassSecurityInfo()
104 def __init__(self, id, instance_home):
105 self.id = id
106 self.instance_home = instance_home
108 # define the properties that define this object
109 _properties = (
110 {'id':'id', 'type': 'string', 'mode': 'w'},
111 {'id':'instance_home', 'type': 'string', 'mode': 'w'},
112 )
113 property_extensible_schema__ = 0
115 # define the tabs for the management interface
116 manage_options= PropertyManager.manage_options + (
117 {'label': 'View', 'action':'index_html'},
118 ) + Item.manage_options
120 icon = "misc_/ZRoundup/icon"
122 security.declarePrivate('roundup_opendb')
123 def roundup_opendb(self):
124 '''Open the roundup instance database for a transaction.
125 '''
126 instance = roundup.instance.open(self.instance_home)
127 request = RequestWrapper(self.REQUEST['RESPONSE'])
128 env = self.REQUEST.environ
130 # figure out the path components to set
131 url = urlparse.urlparse( self.absolute_url() )
132 path = url[2]
133 path_components = path.split( '/' )
135 # special case when roundup is '/' in this virtual host,
136 if path == "/" :
137 env['SCRIPT_NAME'] = "/"
138 env['TRACKER_NAME'] = ''
139 else :
140 # all but the last element is the path
141 env['SCRIPT_NAME'] = '/'.join( path_components[:-1] )
142 # the last element is the name
143 env['TRACKER_NAME'] = path_components[-1]
145 if env['REQUEST_METHOD'] == 'GET':
146 # force roundup to re-parse the request because Zope fiddles
147 # with it and we lose all the :filter, :columns, etc goodness
148 form = None
149 else:
150 # For some reason, CRs are embeded in multiline notes.
151 # It doesn't occur with apache/roundup.cgi, though.
152 form = FormWrapper(self.REQUEST.form)
154 print (env['SCRIPT_NAME'], env['PATH_INFO'])
155 return instance.Client(instance, request, env, form)
157 security.declareProtected('View', 'index_html')
158 def index_html(self):
159 '''Alias index_html to roundup's index
160 '''
161 # Redirect misdirected requests -- bugs 558867 , 565992
162 # PATH_INFO, as defined by the CGI spec, has the *real* request path
163 orig_path = self.REQUEST.environ[ 'PATH_INFO' ]
164 if orig_path[-1] != '/' :
165 url = urlparse.urlparse( self.absolute_url() )
166 url = list( url ) # make mutable
167 url[2] = url[2]+'/' # patch
168 url = urlparse.urlunparse( url ) # reassemble
169 RESPONSE = self.REQUEST.RESPONSE
170 RESPONSE.setStatus( "MovedPermanently" ) # 301
171 RESPONSE.setHeader( "Location" , url )
172 return RESPONSE
174 client = self.roundup_opendb()
175 # fake the path that roundup should use
176 client.split_path = ['index']
177 return client.main()
179 def __getitem__(self, item):
180 '''All other URL accesses are passed throuh to roundup
181 '''
182 return PathElement(self, item)
184 class PathElement(Item, Implicit, Persistent):
185 def __init__(self, parent, path):
186 self.parent = parent
187 self.path = path
189 def __getitem__(self, item):
190 ''' Get a subitem.
191 '''
192 return PathElement(self.path + '/' + item)
194 def __call__(self, *args, **kw):
195 ''' Actually call through to roundup to handle the request.
196 '''
197 print '*****', self.path
198 try:
199 client = self.parent.roundup_opendb()
200 # fake the path that roundup should use
201 client.path = self.path
202 # and call roundup to do something
203 client.main()
204 return ''
205 except NotFound:
206 raise 'NotFound', self.REQUEST.URL
207 pass
208 except:
209 import traceback
210 traceback.print_exc()
211 # all other exceptions in roundup are valid
212 raise
214 InitializeClass(ZRoundup)
215 modulesecurity.apply(globals())
218 # vim: set filetype=python ts=4 sw=4 et si