source: traclegosscript/anyrelease/traclegos/web.py

Last change on this file was 7441, checked in by Jeff Hammel, 14 years ago

enable LDAP auth

File size: 17.5 KB
Line 
1"""
2TTW view for project creation and serving trac
3"""
4
5import cgi
6import os
7import string
8import subprocess
9import sys
10import time
11import traceback
12
13from genshi.core import Markup
14from genshi.template import TemplateLoader
15from martini.config import ConfigMunger
16from trac.env import open_environment
17from trac.web.main import dispatch_request
18from trac.web.main import RequestDispatcher
19from traclegos.db import available_databases
20from traclegos.legos import traclegos_factory
21from traclegos.legos import TracLegos
22from traclegos.pastescript.string import PasteScriptStringTemplate
23from traclegos.pastescript.var import vars2dict, dict2vars
24from traclegos.project import project_dict
25from traclegos.repository import available_repositories
26from webob import Request, Response, exc
27
28from StringIO import StringIO
29
30template_directory = os.path.join(os.path.dirname(__file__), 'templates')
31
32### project creation steps
33
34class Step(object):
35    """base class for TTW project creation steps"""
36
37    def __init__(self, view):
38        self.view = view
39        self.template = self.name + '.html'
40
41class CreateProject(Step):
42    """project creation step + transition"""
43    name = 'create-project'
44    def data(self, project):
45        """data needed for template rendering"""
46        return { 'URL': project['base_url'],
47                 'projects': self.view.available_templates }
48
49    def display(self, project):
50        """whether to display this step"""
51        return True
52
53    def errors(self, project, input):
54        """check for errors"""
55        errors = []       
56        project_name = input.get('project')
57        if project_name:           
58            projects = self.view.projects.keys() + os.listdir(self.view.directory)
59            if project_name in projects:
60                errors.append("The project '%s' already exists" % project_name)
61        else:
62            errors.append('No project URL specified')
63
64
65        project_type = input.get('project_type')
66        assert project_type in self.view.available_templates
67
68        # get the project logo
69        logo = input['logo']
70        if logo:
71            if not logo.startswith('http://') or logo.startswith('https://'):
72                if not os.path.exists(logo):
73                    errors.append("Logo file not found: %s" % logo)
74        return errors
75
76    def transition(self, project, input):
77        """transition following this step"""
78        logo = input['logo']
79        logo_file = None
80        if logo and not (logo.startswith('http://') or logo.startswith('https://')):
81            logo_file = file(logo, 'rb')
82            logo_file_name = os.path.basename(logo)
83
84        project['type'] = input['project_type']
85        project['vars'] = self.view.legos.vars.copy()
86        project['vars'].update({'project': input['project'],
87                                'description': input.get('project_name').strip() or input['project'],
88                                'url': input.get('alternate_url')
89                                })
90        project['config'] = {}
91        project['config']['header_logo'] = {'link': input['alternate_url'] }
92
93        # get an uploaded logo
94        # note that uploaded logos will override logo files/links
95        # (should this be an error instead?)
96        uploaded_logo = input['logo_file']
97        if isinstance(uploaded_logo, cgi.FieldStorage):
98            logo_file_name = uploaded_logo.filename
99            logo_file = uploaded_logo.file
100
101        project['logo_file'] = logo_file
102        if logo_file:
103            logo = 'site/%s' % logo_file_name
104
105        project['config']['header_logo']['src'] = logo
106        project['vars']['logo'] = logo
107       
108        # TODO:  get the favicon from the alternate URL or create one from the logo
109
110        # get a list of TRAC_ADMINs
111        # XXX this is a hack, for now
112        project['admins'] = input.get('trac_admins', '').replace(',', ' ').split()
113       
114
115class ProjectDetails(Step):
116    """
117    second project creation step: project details
118    svn repo, mailing lists (TODO)
119    """
120    name = 'project-details'
121    def data(self, project):
122        """data needed for template rendering"""
123        project_name = project['vars']['project']
124        repositories = [ self.view.repositories[name] for name in self.view.available_repositories ]
125        excluded_fields = dict((key, value.keys()) for key, value in self.view.legos.repository_fields(project_name).items())
126        for name in self.view.available_repositories:
127            excluded_fields.setdefault(name, []).extend(project['vars'].keys())
128        data = {'project': project_name,
129                'repositories': repositories,
130                'excluded_fields': excluded_fields,
131                'databases': [ self.view.databases[name] for name in self.view.available_databases ] } 
132
133        # get the database strings
134        data['db_string'] = {}
135        for database in data['databases']:
136            dbstring = database.db_string()
137            dbstring = string.Template(dbstring).safe_substitute(**project['vars'])
138            template = PasteScriptStringTemplate(dbstring)
139            missing = template.missing()
140            if missing:
141                vars = vars2dict(None, *database.options)
142                missing = dict([(i, 
143                                 '<input type="text" name="%s-%s" value="%s"/>' % (database.name, i, getattr(vars.get(i), 'default', '')))
144                                for i in missing])
145                dbstring = string.Template(dbstring).substitute(**missing)
146                dbstring = Markup(dbstring)
147            data['db_string'][database.name] = dbstring
148        return data
149   
150    def display(self, project):
151        """display this step if there is something to do"""
152        project_details = [ self.view.available_repositories, self.view.available_databases ]
153        return not sum([len(i) for i in project_details]) == len(project_details)
154   
155    def errors(self, project, input):
156        # TODO
157        return []
158   
159    def transition(self, project, input):
160        """transition to the next step"""
161       
162        # repository information
163        project['repository'] = None
164        repository = input.get('repository')
165        if not repository:
166            # repository is specified by policy
167            assert len(self.view.available_repositories) == 1
168            repository = self.view.available_repositories[0]
169
170        if repository in self.view.available_repositories: # XXX musn't this be true?!?
171            args = dict((arg.split('%s_' % repository, 1)[1], value) 
172                        for arg, value in input.items()
173                        if arg.startswith('%s_' % repository))
174            project['repository'] = self.view.repositories[repository]
175            project['vars'].update(args)
176            project['vars'].update(self.view.legos.repository_fields(project['vars']['project']).get(repository, {}))
177
178        # database information
179        project['database'] = None
180        database = input.get('database')
181        if not database:
182            # database is specified by policy
183            assert len(self.view.available_databases) == 1
184            database = self.view.available_databases[0]
185        if database in self.view.available_databases: # XXX musn't this be true?!?
186            project['database'] = self.view.databases[database]
187
188class ProjectVariables(Step):
189    """final project creation step:  filling in the project variables"""
190    name = 'project-variables'
191    def data(self, project):
192        """data needed for template rendering"""
193        templates = self.templates(project)
194        options = templates.options()
195        for var in project['vars']:
196            options.pop(var, None)
197        return  {'project': project['vars']['project'],
198                 'options': options.values()}
199       
200    def display(self, project):
201        templates = self.templates(project)
202        options = templates.options()
203        for var in project['vars']: # could use set
204            options.pop(var, None)
205        return bool(options)
206   
207    def errors(self, project, input):
208        # TODO
209        return []
210   
211    def transition(self, project, input):
212        """create the project from TTW input"""
213
214        # add input (form) variables to the project variables
215        project['vars'].update(input)
216
217        # don't add the repository directory here so that we can
218        # sync asynchronously
219        # XXX hack
220        repository_dir = project['vars'].get('repository_dir')
221        project['vars']['repository_dir'] = ''
222
223        # create the project
224        self.view.legos.create_project(project['vars']['project'],
225                                       self.templates(project),
226                                       project['vars'],
227                                       database=project['database'],
228                                       repository=project['repository'])
229
230        project_dir = os.path.join(self.view.directory, project['vars']['project'])
231
232        # write the logo_file to its new location
233        logo_file = project['logo_file']
234        if logo_file:
235            logo_file_name = os.path.basename(project['vars']['logo'])
236            filename = os.path.join(project_dir, 'htdocs', logo_file_name)
237            logo = file(filename, 'wb')
238            logo.write(logo_file.read())
239            logo.close()
240           
241        # TODO: favicons from logo or alternate url
242
243        # TODO: add authenticated user to TRAC_ADMIN of the new site
244        # (and redirect to the admin panel?)
245        # XXX hack for now
246#        for admin in project['admins']:
247#            subprocess.call(['trac-admin', project_dir, 'permission', 'add', admin, 'TRAC_ADMIN'])
248
249        # XXX hack to sync the repository asynchronously
250        if repository_dir:
251            ini = os.path.join(project_dir, 'conf', 'trac.ini')
252            conf = ConfigMunger(ini, { 'trac': {'repository_dir': repository_dir}})
253            f = file(ini, 'w')
254            conf.write(f)
255            subprocess.Popen(['trac-admin', project_dir, 'resync'])
256
257
258    def templates(self, project):
259        templates = [project['type'], project['config']]
260        repository = project['repository']
261        if repository:
262            templates.append(repository.config())
263        return self.view.legos.project_templates(templates)
264
265class View(object):
266    """WebOb view which wraps trac and allows TTW project creations"""
267
268    def __init__(self, remote_user_name=None, **kw):
269
270        # trac project creator
271        argspec = traclegos_factory(kw.get('conf', ()),
272                                    kw,
273                                    kw.get('variables', {}))
274        self.legos = TracLegos(**argspec)
275        self.legos.interactive = False
276        self.directory = self.legos.directory # XXX needed?
277        self.available_templates = kw.get('available_templates') or project_dict().keys()
278        assert self.available_templates
279           
280        # genshi template loader
281        self.loader = TemplateLoader(template_directory, auto_reload=True)
282
283        # storage of intermittent projects
284        self.projects = {} 
285
286        # URL to redirect to after project creation
287        self.done = '/%(project)s'
288
289        # steps of project creation
290        self.steps = [ 'create-project', 'project-details', 'project-variables' ]
291        self.steps = [ (step.name, step(self)) for step in [ CreateProject, ProjectDetails, ProjectVariables ] ]
292
293        # available SCM repository types
294        self.repositories = available_repositories()
295        self.available_repositories = kw.get('available_repositories')
296        if self.available_repositories is None:
297            self.available_repositories = ['NoRepository'] + [ name for name in self.repositories.keys() if name is not 'NoRepository' ]
298           
299        else:
300            for name in self.repositories.keys():
301                if name not in self.available_repositories:
302                    del self.repositories[name]
303        if 'variables' in kw:
304            # set repository options defaults from input variables
305            for repository in self.repositories.values():           
306                for option in repository.options:
307                    option.default = kw['variables'].get(option.name, option.default)
308
309        # available database types
310        self.databases = available_databases()
311        self.available_databases = kw.get('available_databases')
312        if self.available_databases is None:
313            self.available_databases = [ 'SQLite' ] + [ name for name in self.databases.keys() if name is not 'SQLite' ]
314        else:
315            for name in self.databases.keys():
316                if name not in self.available_databases:
317                    del self.databases[name]
318
319        # enforce authentication
320        self.auth = 'auth' in kw
321
322        # index page for projects list
323        self.index = kw.get('index', os.path.join(template_directory, 'index.html'))
324
325        self.remote_user_name = remote_user_name
326
327    def trac_projects(self):
328        """returns existing Trac projects"""
329        proj = {}
330        for i in os.listdir(self.directory):
331            try:
332                env = open_environment(os.path.join(self.directory, i))
333            except:
334                continue
335            proj[i] = env
336        return proj
337
338    ### methods dealing with HTTP
339    def __call__(self, environ, start_response):
340
341        req = Request(environ)
342        step = req.path_info.strip('/')
343
344        try:
345
346            if step in [i[0] for i in self.steps]:
347                # determine which step we are on
348                index = [i[0] for i in self.steps].index(step)
349            else:
350                # delegate to Trac
351
352                environ['trac.env_parent_dir'] = self.directory
353                environ['trac.env_index_template'] = self.index
354
355                # data for index template
356                if req.remote_user and self.remote_user_name:
357                    # XXX fails if unicode
358                    req.remote_user = str(self.remote_user_name(req.remote_user))
359                data = { 'remote_user': req.remote_user or '',
360                         'auth': self.auth and 'yes' or ''
361                         }
362                environ['trac.template_vars'] = ','.join(["%s=%s" % (key, value) for key, value in data.items()])
363                return dispatch_request(environ, start_response)
364
365
366            # if self.auth, enforce remote_user to be set
367            if self.auth and not req.remote_user:
368                return exc.HTTPUnauthorized()(environ, start_response)
369
370            # if POST-ing, validate the request and store needed information
371            errors = []
372            name, step = self.steps[index]
373            base_url = req.url.rsplit(step.name, 1)[0]
374            project = req.params.get('project')
375            if req.method == 'POST':
376
377                # check for project existence
378                if not project and index:
379                    res = exc.HTTPSeeOther("No session found", location="create-project")
380                    return res(environ, start_response)
381                if index:
382                    if project not in self.projects:
383                        errors.append('Project not found')
384
385                project_data = self.projects.get(project)
386                errors = step.errors(project_data, req.POST)
387                if not index:
388                    project_data = self.projects[project] = {}
389
390                # set *after* error check so that `create-project` doesn't find itself
391                project_data['base_url'] = base_url
392           
393                if not errors: # success
394                    step.transition(project_data, req.POST)
395
396                    # find the next step and redirect to it
397                    while True:
398                        index += 1
399                   
400                        if index == len(self.steps):
401                            destination = self.done % self.projects[project]['vars']
402                            time.sleep(1) # XXX needed?
403                            self.projects.pop(project) # successful project creation
404                            break
405                        else:
406                            name, step = self.steps[index]
407                            if step.display(project_data):
408                                destination = '%s?project=%s' % (self.steps[index][0], project)       
409                                break
410                            else:
411                                step.transition(project_data, {})
412                    res = exc.HTTPSeeOther(destination, location=destination)
413                    return res(environ, start_response)
414
415            else: # GET
416                project_data = self.projects.get(project, {})
417                project_data['base_url'] = base_url
418                if index and project not in self.projects:
419                    res = exc.HTTPSeeOther("No session found", location="create-project")
420                    return res(environ, start_response)
421           
422            # render the template and return the response
423            data = step.data(project_data)
424            data['req'] = req
425            data['errors'] = errors
426            template = self.loader.load(step.template)
427            html = template.generate(**data).render('html', doctype='html')
428            res = Response(content_type='text/html', body=html)
429            return res(environ, start_response)
430
431        except:
432            # error handling
433            exceptionType, exceptionValue, exceptionTraceback = sys.exc_info()
434            buffer = StringIO()
435            traceback.print_exception(exceptionType, exceptionValue, exceptionTraceback,
436                                      limit=20, file=buffer)
437            res = exc.HTTPServerError(buffer.getvalue())
438            return res(environ, start_response)
Note: See TracBrowser for help on using the repository browser.