| 1 |
""" |
|---|
| 2 |
TTW view for project creation and serving trac |
|---|
| 3 |
""" |
|---|
| 4 |
|
|---|
| 5 |
import cgi |
|---|
| 6 |
import os |
|---|
| 7 |
import string |
|---|
| 8 |
import sys |
|---|
| 9 |
import tempfile |
|---|
| 10 |
|
|---|
| 11 |
from genshi.core import Markup |
|---|
| 12 |
from genshi.template import TemplateLoader |
|---|
| 13 |
from trac.web.main import dispatch_request |
|---|
| 14 |
from traclegos.config import ConfigMunger |
|---|
| 15 |
from traclegos.db import available_databases |
|---|
| 16 |
from traclegos.legos import site_configuration |
|---|
| 17 |
from traclegos.legos import traclegos_factory |
|---|
| 18 |
from traclegos.legos import TracLegos |
|---|
| 19 |
from traclegos.pastescript.string import PasteScriptStringTemplate |
|---|
| 20 |
from traclegos.pastescript.var import vars2dict, dict2vars |
|---|
| 21 |
from traclegos.project import project_dict |
|---|
| 22 |
from traclegos.repository import available_repositories |
|---|
| 23 |
from webob import Request, Response, exc |
|---|
| 24 |
|
|---|
| 25 |
# TODO: better handling of errors (not very friendly, currently) |
|---|
| 26 |
|
|---|
| 27 |
template_directory = os.path.join(os.path.dirname(__file__), 'templates') |
|---|
| 28 |
|
|---|
| 29 |
class View(object): |
|---|
| 30 |
"""WebOb view which wraps trac and allows TTW project creations""" |
|---|
| 31 |
|
|---|
| 32 |
def __init__(self, **kw): |
|---|
| 33 |
|
|---|
| 34 |
# trac project creator |
|---|
| 35 |
argspec = traclegos_factory(kw.get('conf', ()), |
|---|
| 36 |
kw, |
|---|
| 37 |
kw.get('variables', {})) |
|---|
| 38 |
self.legos = TracLegos(**argspec) |
|---|
| 39 |
self.legos.interactive = False |
|---|
| 40 |
self.directory = self.legos.directory # XXX needed? |
|---|
| 41 |
|
|---|
| 42 |
# trac projects available |
|---|
| 43 |
self.available_templates = kw.get('available_templates') or project_dict().keys() |
|---|
| 44 |
assert self.available_templates |
|---|
| 45 |
|
|---|
| 46 |
# genshi template loader |
|---|
| 47 |
self.loader = TemplateLoader(template_directory, auto_reload=True) |
|---|
| 48 |
|
|---|
| 49 |
# storage of intermittent projects |
|---|
| 50 |
self.projects = {} |
|---|
| 51 |
|
|---|
| 52 |
# URL to redirect to after project creation |
|---|
| 53 |
self.done = '/%(project)s' |
|---|
| 54 |
|
|---|
| 55 |
# steps of project creation |
|---|
| 56 |
self.steps = [ 'create-project', 'project-details', 'project-variables' ] |
|---|
| 57 |
|
|---|
| 58 |
# available SCM repository types |
|---|
| 59 |
self.repositories = available_repositories() |
|---|
| 60 |
self.available_repositories = kw.get('available_repositories') |
|---|
| 61 |
if self.available_repositories is None: |
|---|
| 62 |
self.available_repositories = ['NoRepository'] + [ name for name in self.repositories.keys() if name is not 'NoRepository' ] |
|---|
| 63 |
|
|---|
| 64 |
else: |
|---|
| 65 |
for name in self.repositories.keys(): |
|---|
| 66 |
if name not in self.available_repositories: |
|---|
| 67 |
del self.repositories[name] |
|---|
| 68 |
|
|---|
| 69 |
# available database types |
|---|
| 70 |
self.databases = available_databases() |
|---|
| 71 |
self.available_databases = kw.get('available_databases') |
|---|
| 72 |
if self.available_databases is None: |
|---|
| 73 |
self.available_databases = [ 'SQLite' ] + [ name for name in self.databases.keys() if name is not 'SQLite' ] |
|---|
| 74 |
else: |
|---|
| 75 |
for name in self.databases.keys(): |
|---|
| 76 |
if name not in self.available_databases: |
|---|
| 77 |
del self.databases[name] |
|---|
| 78 |
|
|---|
| 79 |
# TODO: pop project-details if this is an empty step |
|---|
| 80 |
|
|---|
| 81 |
### methods dealing with HTTP |
|---|
| 82 |
def __call__(self, environ, start_response): |
|---|
| 83 |
|
|---|
| 84 |
req = Request(environ) |
|---|
| 85 |
|
|---|
| 86 |
step = req.path_info.strip('/') |
|---|
| 87 |
if step in self.steps: |
|---|
| 88 |
method = ''.join(index and token.title() or token |
|---|
| 89 |
for index, token in enumerate(step.split('-'))) |
|---|
| 90 |
|
|---|
| 91 |
# if POST-ing, validate the request and store needed information |
|---|
| 92 |
errors = None |
|---|
| 93 |
if req.method == 'POST': |
|---|
| 94 |
|
|---|
| 95 |
project = req.POST.get('project') |
|---|
| 96 |
if not project and step != 'create-project': |
|---|
| 97 |
res = exc.HTTPSeeOther("No session found", location="create-project") |
|---|
| 98 |
return res(environ, start_response) |
|---|
| 99 |
|
|---|
| 100 |
validator = getattr(self, 'validate' + method[0].upper() + method[1:]) |
|---|
| 101 |
errors = validator(req) |
|---|
| 102 |
if not errors: # success |
|---|
| 103 |
index = self.steps.index(step) |
|---|
| 104 |
if index == len(self.steps) - 1: |
|---|
| 105 |
destination = self.done % self.projects[project]['vars'] |
|---|
| 106 |
self.projects.pop(project) # successful project creation |
|---|
| 107 |
else: |
|---|
| 108 |
destination = '%s?project=%s' % (self.steps[index + 1], project) |
|---|
| 109 |
res = exc.HTTPSeeOther(destination, location=destination) |
|---|
| 110 |
return res(environ, start_response) |
|---|
| 111 |
else: |
|---|
| 112 |
if step != 'create-project': |
|---|
| 113 |
project = req.GET.get('project') |
|---|
| 114 |
if project in self.projects: |
|---|
| 115 |
# TODO: put this and the project data into data |
|---|
| 116 |
pass |
|---|
| 117 |
else: |
|---|
| 118 |
res = exc.HTTPSeeOther("No session found", location="create-project") |
|---|
| 119 |
return res(environ, start_response) |
|---|
| 120 |
|
|---|
| 121 |
|
|---|
| 122 |
data = getattr(self, method)(req, errors) |
|---|
| 123 |
data['errors'] = errors |
|---|
| 124 |
template = self.loader.load("%s.html" % step) |
|---|
| 125 |
html = template.generate(**data).render('html', doctype='html') |
|---|
| 126 |
res = self.get_response(html) |
|---|
| 127 |
return res(environ, start_response) |
|---|
| 128 |
|
|---|
| 129 |
environ['trac.env_parent_dir'] = self.directory |
|---|
| 130 |
|
|---|
| 131 |
# could otherwise take over the index.html serving ourselves |
|---|
| 132 |
environ['trac.env_index_template'] = os.path.join(template_directory, 'index.html') |
|---|
| 133 |
|
|---|
| 134 |
if not step: # site index |
|---|
| 135 |
pass # XXX needed? not for now |
|---|
| 136 |
# vars = {} |
|---|
| 137 |
# environ.setdefault('trac.template_vars', {}).update(vars) |
|---|
| 138 |
|
|---|
| 139 |
return dispatch_request(environ, start_response) |
|---|
| 140 |
|
|---|
| 141 |
def get_response(self, text, content_type='text/html'): |
|---|
| 142 |
"""returns a response object for HTML/text input""" |
|---|
| 143 |
res = Response(content_type=content_type, body=text) |
|---|
| 144 |
res.content_length = len(res.body) |
|---|
| 145 |
return res |
|---|
| 146 |
|
|---|
| 147 |
### methods for URIs |
|---|
| 148 |
def createProject(self, req, errors=None): |
|---|
| 149 |
"""first project creation step: initial project data""" |
|---|
| 150 |
data = {} |
|---|
| 151 |
data['URL'] = req.url.rsplit('create-project', 1)[0] |
|---|
| 152 |
data['projects'] = self.available_templates |
|---|
| 153 |
data['next'] = 'Project Details' |
|---|
| 154 |
return data |
|---|
| 155 |
|
|---|
| 156 |
def validateCreateProject(self, req): |
|---|
| 157 |
|
|---|
| 158 |
# check for errors |
|---|
| 159 |
errors = [] |
|---|
| 160 |
project = req.POST.get('project') |
|---|
| 161 |
if project: |
|---|
| 162 |
if project in self.projects: # TODO check for existing trac projects |
|---|
| 163 |
errors.append("The project '%s' already exists" % project) |
|---|
| 164 |
else: |
|---|
| 165 |
self.projects[project] = {} # new project |
|---|
| 166 |
else: |
|---|
| 167 |
errors.append('No project URL specified') |
|---|
| 168 |
|
|---|
| 169 |
|
|---|
| 170 |
project_type = req.POST.get('project_type') |
|---|
| 171 |
assert project_type in self.available_templates |
|---|
| 172 |
|
|---|
| 173 |
# get the project logo |
|---|
| 174 |
logo = req.POST['logo'] |
|---|
| 175 |
logo_file = None |
|---|
| 176 |
if logo: |
|---|
| 177 |
if not logo.startswith('http://') or logo.startswith('https://'): |
|---|
| 178 |
if os.path.exists(logo): |
|---|
| 179 |
logo_file = file(logo, 'rb') |
|---|
| 180 |
logo_file_name = os.path.basename(logo) |
|---|
| 181 |
else: |
|---|
| 182 |
errors.append("Logo file not found: %s" % logo) |
|---|
| 183 |
|
|---|
| 184 |
if errors: |
|---|
| 185 |
return errors |
|---|
| 186 |
|
|---|
| 187 |
# process the request and save necessary data |
|---|
| 188 |
project_data = self.projects[project] |
|---|
| 189 |
project_data['type'] = project_type |
|---|
| 190 |
project_data['vars'] = self.legos.vars.copy() |
|---|
| 191 |
project_data['vars'].update({'project': project, |
|---|
| 192 |
'description': req.POST.get('project_name').strip() or project, |
|---|
| 193 |
'url': req.POST.get('alternate_url') |
|---|
| 194 |
}) |
|---|
| 195 |
|
|---|
| 196 |
project_data['config'] = {} |
|---|
| 197 |
project_data['config']['header_logo'] = {'link': req.POST['alternate_url'] } |
|---|
| 198 |
|
|---|
| 199 |
# get an uploaded logo |
|---|
| 200 |
# note that uploaded logos will override logo files/links |
|---|
| 201 |
# (should this be an error instead?) |
|---|
| 202 |
uploaded_logo = req.POST['logo_file'] |
|---|
| 203 |
if isinstance(uploaded_logo, cgi.FieldStorage): |
|---|
| 204 |
logo_file_name = uploaded_logo.filename |
|---|
| 205 |
logo_file = uploaded_logo.file |
|---|
| 206 |
|
|---|
| 207 |
project_data['logo_file'] = logo_file |
|---|
| 208 |
if logo_file: |
|---|
| 209 |
logo = 'site/%s' % logo_file_name |
|---|
| 210 |
|
|---|
| 211 |
project_data['config']['header_logo']['src'] = logo |
|---|
| 212 |
project_data['vars']['logo'] = logo |
|---|
| 213 |
|
|---|
| 214 |
# TODO: get the favicon from the alternate URL or create one from the logo |
|---|
| 215 |
|
|---|
| 216 |
def projectDetails(self, req, errors=None): |
|---|
| 217 |
"""second project creation step: project details |
|---|
| 218 |
svn repo, mailing lists (TODO) |
|---|
| 219 |
""" |
|---|
| 220 |
project = req.GET['project'] |
|---|
| 221 |
data = {'project': project, |
|---|
| 222 |
'repositories': [ self.repositories[name] for name in self.available_repositories ], |
|---|
| 223 |
'excluded_fields': dict((key, value.keys()) for key, value in self.legos.repository_fields(project).items()), |
|---|
| 224 |
'databases': [ self.databases[name] for name in self.available_databases ] } |
|---|
| 225 |
|
|---|
| 226 |
# get the database strings |
|---|
| 227 |
data['db_string'] = {} |
|---|
| 228 |
for database in data['databases']: |
|---|
| 229 |
dbstring = database.db_string() |
|---|
| 230 |
dbstring = string.Template(dbstring).safe_substitute(**self.projects[project]['vars']) |
|---|
| 231 |
template = PasteScriptStringTemplate(dbstring) |
|---|
| 232 |
missing = template.missing() |
|---|
| 233 |
if missing: |
|---|
| 234 |
vars = vars2dict(None, *database.options) |
|---|
| 235 |
missing = dict([(i, |
|---|
| 236 |
'<input type="text" name="%s-%s" value="%s"/>' % (database.name, i, getattr(vars.get(i), 'default', ''))) |
|---|
| 237 |
for i in missing]) |
|---|
| 238 |
dbstring = string.Template(dbstring).substitute(**missing) |
|---|
| 239 |
dbstring = Markup(dbstring) |
|---|
| 240 |
data['db_string'][database.name] = dbstring |
|---|
| 241 |
return data |
|---|
| 242 |
|
|---|
| 243 |
def validateProjectDetails(self, req): |
|---|
| 244 |
|
|---|
| 245 |
# check for errors |
|---|
| 246 |
errors = [] |
|---|
| 247 |
project = req.POST.get('project') |
|---|
| 248 |
project_data = self.projects.get(project) |
|---|
| 249 |
if project_data is None: |
|---|
| 250 |
errors.append('Project not found') |
|---|
| 251 |
|
|---|
| 252 |
# repository information |
|---|
| 253 |
project_data['repository'] = None |
|---|
| 254 |
repository = req.POST.get('repository') |
|---|
| 255 |
if repository in self.available_repositories: |
|---|
| 256 |
args = dict((arg.split('%s_' % repository, 1)[1], value) |
|---|
| 257 |
for arg, value in req.POST.items() |
|---|
| 258 |
if arg.startswith('%s_' % repository)) |
|---|
| 259 |
project_data['repository'] = self.repositories[repository] |
|---|
| 260 |
project_data['vars'].update(args) |
|---|
| 261 |
project_data['vars'].update(self.legos.repository_fields(project).get(repository, {})) |
|---|
| 262 |
|
|---|
| 263 |
# database information |
|---|
| 264 |
project_data['database'] = None |
|---|
| 265 |
database = req.POST.get('database') |
|---|
| 266 |
if database in self.available_databases: |
|---|
| 267 |
project_data['database'] = self.databases[database] |
|---|
| 268 |
|
|---|
| 269 |
return errors |
|---|
| 270 |
|
|---|
| 271 |
def projectVariables(self, req, errors=None): |
|---|
| 272 |
"""final project creation step: filling in the project variables""" |
|---|
| 273 |
|
|---|
| 274 |
project = req.GET.get('project') |
|---|
| 275 |
project_data = self.projects[project] |
|---|
| 276 |
templates = [project_data['type'], project_data['config']] |
|---|
| 277 |
repository = project_data['repository'] |
|---|
| 278 |
if repository: |
|---|
| 279 |
templates.append(repository.config()) |
|---|
| 280 |
templates = self.legos.project_templates(templates) |
|---|
| 281 |
options = templates.options() |
|---|
| 282 |
for var in project_data['vars']: |
|---|
| 283 |
options.pop(var, None) |
|---|
| 284 |
project_data['templates'] = templates |
|---|
| 285 |
data = {'project': project, |
|---|
| 286 |
'options': options.values()} |
|---|
| 287 |
|
|---|
| 288 |
return data |
|---|
| 289 |
|
|---|
| 290 |
def validateProjectVariables(self, req): |
|---|
| 291 |
errors = [] |
|---|
| 292 |
|
|---|
| 293 |
# XXX to deprecate ? |
|---|
| 294 |
project = req.POST.get('project') |
|---|
| 295 |
if project not in self.projects: |
|---|
| 296 |
errors.append('Project not found') |
|---|
| 297 |
|
|---|
| 298 |
if errors: |
|---|
| 299 |
return errors |
|---|
| 300 |
|
|---|
| 301 |
project_data = self.projects[project] |
|---|
| 302 |
project_data['vars'].update(req.POST) |
|---|
| 303 |
|
|---|
| 304 |
# create the project |
|---|
| 305 |
self.legos.create_project(project, |
|---|
| 306 |
project_data['templates'], |
|---|
| 307 |
project_data['vars'], |
|---|
| 308 |
repository=project_data['repository']) |
|---|
| 309 |
|
|---|
| 310 |
# write the logo_file to its new location |
|---|
| 311 |
logo_file = project_data['logo_file'] |
|---|
| 312 |
if logo_file: |
|---|
| 313 |
logo_file_name = os.path.basename(project_data['vars']['logo']) |
|---|
| 314 |
filename = os.path.join(self.directory, project, 'htdocs', logo_file_name) |
|---|
| 315 |
logo = file(filename, 'wb') |
|---|
| 316 |
logo.write(logo_file.read()) |
|---|
| 317 |
logo.close() |
|---|
| 318 |
|
|---|
| 319 |
# TODO: favicons from logo or alternate url |
|---|
| 320 |
|
|---|
| 321 |
# TODO: add authenticated user to TRAC_ADMIN of the new site |
|---|
| 322 |
# (and redirect to the admin panel?) |
|---|
| 323 |
|
|---|
| 324 |
|
|---|