pyflake and pylint cleanups.

pull/7/head
Ferry Boender 10 years ago
parent 340b28a8fb
commit 24f9a25f54
  1. 48
      src/daemon.py
  2. 41
      src/formconfig.py
  3. 65
      src/formdefinition.py
  4. 185
      src/formrender.py
  5. 49
      src/scriptform.py
  6. 176
      src/webapp.py

@ -1,3 +1,7 @@
"""
Provide daemon capabilities via the Daemon class.
"""
import logging
import os
import sys
@ -8,10 +12,13 @@ import atexit
class DaemonError(Exception):
"""
Default error for Daemon class.
"""
pass
class Daemon: # pragma: no cover
class Daemon(object): # pragma: no cover
"""
Daemonize the current process (detach it from the console).
"""
@ -27,17 +34,24 @@ class Daemon: # pragma: no cover
self.log_file = log_file
self.foreground = foreground
log_fmt = '%(asctime)s:%(name)s:%(levelname)s:%(message)s'
logging.basicConfig(level=log_level,
format='%(asctime)s:%(name)s:%(levelname)s:%(message)s',
format=log_fmt,
filename=self.log_file,
filemode='a')
self.log = logging.getLogger('DAEMON')
self.shutdown_cb = None
self.shutdown_callback = None
def register_shutdown_cb(self, cb):
self.shutdown_cb = cb
def register_shutdown_callback(self, callback):
"""
Register a callback to be executed when the daemon is stopped.
"""
self.shutdown_callback = callback
def start(self):
"""
Start the daemon. Raises a DaemonError if it's already running.
"""
self.log.info("Starting")
if self.is_running():
self.log.error('Already running')
@ -46,6 +60,10 @@ class Daemon: # pragma: no cover
self._fork()
def stop(self):
"""
Stop the daemon. Raises a DaemonError if the daemon is ot running,
which is determined by examaning the PID file.
"""
if not self.is_running():
raise DaemonError("Not running")
@ -101,6 +119,11 @@ class Daemon: # pragma: no cover
return True
def _fork(self):
"""
Fork the current process daemon-style. Forks twice, closes file
descriptors, etc. A signal handler is also registered to be called if
the daemon received a SIGTERM signal.
"""
# Fork a child and end the parent (detach from parent)
pid = os.fork()
if pid > 0:
@ -114,9 +137,9 @@ class Daemon: # pragma: no cover
pid = os.fork()
if pid > 0:
self.log.info("PID = {0}".format(pid))
f = file(self.pid_file, 'w')
f.write(str(pid))
f.close()
pidfile = file(self.pid_file, 'w')
pidfile.write(str(pid))
pidfile.close()
sys.exit(0) # End parent
atexit.register(self._cleanup)
@ -124,9 +147,9 @@ class Daemon: # pragma: no cover
# Close STDIN, STDOUT and STDERR so we don't tie up the controlling
# terminal
for fd in (0, 1, 2):
for fdescriptor in (0, 1, 2):
try:
os.close(fd)
os.close(fdescriptor)
except OSError:
pass
@ -139,7 +162,10 @@ class Daemon: # pragma: no cover
return pid
def _cleanup(self, sig=None):
"""
Remvoe pid files and call registered shutodnw callbacks.
"""
self.log.info("Received signal {0}".format(sig))
if os.path.exists(self.pid_file):
os.unlink(self.pid_file)
self.shutdown_cb()
self.shutdown_callback()

@ -1,3 +1,9 @@
"""
FormConfig is the in-memory representation of a form configuration JSON file.
It holds information (title, users, the form definitions) on the form
configuration being served by this instance of ScriptForm.
"""
import logging
import stat
import os
@ -5,16 +11,20 @@ import subprocess
class FormConfigError(Exception):
"""
Default error for FormConfig errors
"""
pass
class FormConfig:
class FormConfig(object):
"""
FormConfig is the in-memory representation of a form configuration JSON
file. It holds information (title, users, the form definitions) on the
form configuration being served by this instance of ScriptForm.
"""
def __init__(self, title, forms, users=None, static_dir=None, custom_css=None):
def __init__(self, title, forms, users=None, static_dir=None,
custom_css=None):
self.title = title
self.users = {}
if users is not None:
@ -27,7 +37,8 @@ class FormConfig:
# Validate scripts
for form_def in self.forms:
if not stat.S_IXUSR & os.stat(form_def.script)[stat.ST_MODE]:
raise FormConfigError("{0} is not executable".format(form_def.script))
msg = "{0} is not executable".format(form_def.script)
raise FormConfigError(msg)
def get_form_def(self, form_name):
"""
@ -70,29 +81,33 @@ class FormConfig:
# Validate params
if form.output == 'raw' and (stdout is None or stderr is None):
raise ValueError('stdout and stderr cannot be None if script output is \'raw\'')
msg = 'stdout and stderr cannot be none if script output ' \
'is \'raw\''
raise ValueError(msg)
# Pass form values to the script through the environment as strings.
env = os.environ.copy()
for k, v in form_values.items():
env[k] = str(v)
for key, value in form_values.items():
env[key] = str(value)
# If the form output type is 'raw', we directly stream the output to
# the browser. Otherwise we store it for later displaying.
if form.output == 'raw':
p = subprocess.Popen(form.script, shell=True,
proc = subprocess.Popen(form.script, shell=True,
stdout=stdout,
stderr=stderr,
env=env)
stdout, stderr = p.communicate(input)
return p.returncode
stdout, stderr = proc.communicate(input)
return proc.returncode
else:
p = subprocess.Popen(form.script, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
proc = subprocess.Popen(form.script, shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env)
stdout, stderr = p.communicate()
stdout, stderr = proc.communicate()
return {
'stdout': stdout,
'stderr': stderr,
'exitcode': p.returncode
'exitcode': proc.returncode
}

@ -1,12 +1,18 @@
"""
FormDefinition holds information about a single form and provides methods for
validation of the form values.
"""
import os
import datetime
class ValidationError(Exception):
"""Default exception for Validation errors"""
pass
class FormDefinition:
class FormDefinition(object):
"""
FormDefinition holds information about a single form and provides methods
for validation of the form values.
@ -25,6 +31,9 @@ class FormDefinition:
self.allowed_users = allowed_users
def get_field_def(self, field_name):
"""
Return the field definition for `field_name`.
"""
for field in self.fields:
if field['name'] == field_name:
return field
@ -40,9 +49,11 @@ class FormDefinition:
# First make sure all required fields are there
for field in self.fields:
if 'required' in field and \
field['required'] is True and \
(field['name'] not in form_values or form_values[field['name']] == ''):
field_required = ('required' in field and
field['required'] is True)
field_missing = (field['name'] not in form_values or
form_values[field['name']] == '')
if field_required and field_missing:
errors.setdefault(field['name'], []).append(
"This field is required"
)
@ -51,14 +62,15 @@ class FormDefinition:
for field in self.fields:
field_name = field['name']
if field_name in errors:
# Skip fields that are required but missing, since they can't be validated
# Skip fields that are required but missing, since they can't
# be validated
continue
try:
v = self._field_validate(field_name, form_values)
if v is not None:
values[field_name] = v
except ValidationError, e:
errors.setdefault(field_name, []).append(str(e))
value = self._field_validate(field_name, form_values)
if value is not None:
values[field_name] = value
except ValidationError, err:
errors.setdefault(field_name, []).append(str(err))
return (errors, values)
@ -75,6 +87,9 @@ class FormDefinition:
return validate_cb(field_def, form_values)
def validate_string(self, field_def, form_values):
"""
Validate a form field of type 'string'.
"""
value = form_values[field_def['name']]
maxlen = field_def.get('maxlen', None)
minlen = field_def.get('minlen', None)
@ -87,6 +102,9 @@ class FormDefinition:
return value
def validate_integer(self, field_def, form_values):
"""
Validate a form field of type 'integer'.
"""
value = form_values[field_def['name']]
maxval = field_def.get('max', None)
minval = field_def.get('min', None)
@ -104,6 +122,9 @@ class FormDefinition:
return int(value)
def validate_float(self, field_def, form_values):
"""
Validate a form field of type 'float'.
"""
value = form_values[field_def['name']]
maxval = field_def.get('max', None)
minval = field_def.get('min', None)
@ -121,6 +142,9 @@ class FormDefinition:
return float(value)
def validate_date(self, field_def, form_values):
"""
Validate a form field of type 'date'.
"""
value = form_values[field_def['name']]
maxval = field_def.get('max', None)
minval = field_def.get('min', None)
@ -140,6 +164,9 @@ class FormDefinition:
return value
def validate_radio(self, field_def, form_values):
"""
Validate a form field of type 'radio'.
"""
value = form_values[field_def['name']]
if not value in [o[0] for o in field_def['options']]:
raise ValidationError(
@ -147,6 +174,9 @@ class FormDefinition:
return value
def validate_select(self, field_def, form_values):
"""
Validate a form field of type 'select'.
"""
value = form_values[field_def['name']]
if not value in [o[0] for o in field_def['options']]:
raise ValidationError(
@ -154,6 +184,9 @@ class FormDefinition:
return value
def validate_checkbox(self, field_def, form_values):
"""
Validate a form field of type 'checkbox'.
"""
value = form_values.get(field_def['name'], 'off')
if not value in ['on', 'off']:
raise ValidationError(
@ -161,6 +194,9 @@ class FormDefinition:
return value
def validate_text(self, field_def, form_values):
"""
Validate a form field of type 'text'.
"""
value = form_values[field_def['name']]
minlen = field_def.get('minlen', None)
maxlen = field_def.get('maxlen', None)
@ -174,6 +210,9 @@ class FormDefinition:
return value
def validate_password(self, field_def, form_values):
"""
Validate a form field of type 'password'.
"""
value = form_values[field_def['name']]
minlen = field_def.get('minlen', None)
@ -183,6 +222,9 @@ class FormDefinition:
return value
def validate_file(self, field_def, form_values):
"""
Validate a form field of type 'file'.
"""
value = form_values[field_def['name']]
field_name = field_def['name']
upload_fname = form_values[u'{0}__name'.format(field_name)]
@ -190,6 +232,7 @@ class FormDefinition:
extensions = field_def.get('extensions', None)
if extensions is not None and upload_fname_ext not in extensions:
raise ValidationError("Only file types allowed: {0}".format(u','.join(extensions)))
msg = "Only file types allowed: {0}".format(u','.join(extensions))
raise ValidationError(msg)
return value

@ -1,37 +1,74 @@
html_field = u'''
"""
FormRender takes care of the rendering of forms to HTML.
"""
HTML_FIELD = u'''
<li class="{classes}">
<p class="form-field-title">{title}</p>
<p class="form-field-input">{h_input} <span class="error">{errors}</span></p>
<p class="form-field-input">
{h_input}
<span class="error">{errors}</span>
</p>
</li>
'''
html_field_checkbox = u'''
HTML_FIELD_CHECKBOX = u'''
<li class="checkbox {classes}">
<p class="form-field-input">{h_input} <p class="form-field-title">{title}</p><span class="error">{errors}</span></p>
<p class="form-field-input">
{h_input}
<p class="form-field-title">{title}</p>
<span class="error">{errors}</span>
</p>
</li>
'''
class FormRender():
class FormRender(object):
"""
FormRender takes care of the rendering of forms to HTML.
"""
field_tpl = {
"string": u'<input {required} type="text" name="{name}" value="{value}" size="{size}" class="{classes}" style="{style}" />',
"number": u'<input {required} type="number" min="{minval}" max="{maxval}" name="{name}" value="{value}" class="{classes}" style="{style}" />',
"integer": u'<input {required} type="number" min="{minval}" max="{maxval}" name="{name}" value="{value}" class="{classes}" style="{style}" />',
"float": u'<input {required} type="number" min="{minval}" max="{maxval}" step="any" name="{name}" value="{value}" class="{classes}" style="{style}" />',
"date": u'<input {required} type="date" name="{name}" value="{value}" class="{classes}" style="{style}" />',
"file": u'<input {required} type="file" name="{name}" class="{classes}" style="{style}" />',
"password": u'<input {required} type="password" min="{minval}" name="{name}" value="{value}" class="{classes}" style="{style}" />',
"text": u'<textarea {required} name="{name}" rows="{rows}" cols="{cols}" style="{style}" class="{classes}">{value}</textarea>',
"radio_option": u'<input {checked} type="radio" name="{name}" value="{value}" class="{classes} style="{style}"">{label}<br/>',
"select_option": u'<option value="{value}" style="{style}" {selected}>{label}</option>',
"select": u'<select name="{name}" class="{classes}" style="{style}">{select_elems}</select>',
"checkbox": u'<input {checked} type="checkbox" name="{name}" value="on" class="{classes} style="{style}"" />',
"string": u'<input {required} type="text" name="{name}" '
u'value="{value}" size="{size}" '
u'class="{classes}" style="{style}" />',
"number": u'<input {required} type="number" min="{minval}" '
u'max="{maxval}" name="{name}" value="{value}" '
u'class="{classes}" style="{style}" />',
"integer": u'<input {required} type="number" min="{minval}" '
u'max="{maxval}" name="{name}" value="{value}" '
u'class="{classes}" style="{style}" />',
"float": u'<input {required} type="number" min="{minval}" '
u'max="{maxval}" step="any" name="{name}" '
u'value="{value}" class="{classes}" style="{style}" />',
"date": u'<input {required} type="date" name="{name}" value="{value}" '
u'class="{classes}" style="{style}" />',
"file": u'<input {required} type="file" name="{name}" '
u'class="{classes}" style="{style}" />',
"password": u'<input {required} type="password" min="{minval}" '
u'name="{name}" value="{value}" class="{classes}" '
u'style="{style}" />',
"text": u'<textarea {required} name="{name}" rows="{rows}" '
u'cols="{cols}" style="{style}" '
u'class="{classes}">{value}</textarea>',
"radio_option": u'<input {checked} type="radio" name="{name}" '
u'value="{value}" class="{classes} '
u'style="{style}"">{label}<br/>',
"select_option": u'<option value="{value}" style="{style}" '
u'{selected}>{label}</option>',
"select": u'<select name="{name}" class="{classes}" '
u'style="{style}">{select_elems}</select>',
"checkbox": u'<input {checked} type="checkbox" name="{name}" '
u'value="on" class="{classes} style="{style}"" />',
}
def __init__(self, form_def):
self.form_def = form_def
def cast_params(self, params):
"""
Casts values in `params` dictionary to the correct types and values for
use in the form rendering.
"""
new_params = params.copy()
if 'required' in new_params:
@ -52,60 +89,105 @@ class FormRender():
return new_params
def r_field(self, field_type, **kwargs):
"""
Render a generic field to HTML.
"""
params = self.cast_params(kwargs)
method_name = 'r_field_{0}'.format(field_type)
method = getattr(self, method_name, None)
return method(**params)
def r_field_string(self, name, value, size=50, required=False, classes=None, style=""):
def r_field_string(self, name, value, size=50, required=False,
classes=None, style=""):
"""
Render a string field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['string']
return tpl.format(name=name, value=value, size=size, required=required, classes=classes, style=style)
def r_field_number(self, name, value, minval=None, maxval=None, required=False, classes=None, style=""):
return tpl.format(name=name, value=value, size=size, required=required,
classes=classes, style=style)
def r_field_number(self, name, value, minval=None, maxval=None,
required=False, classes=None, style=""):
"""
Render a number field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['number']
return tpl.format(name=name, value=value, minval=minval, maxval=maxval, required=required, classes=classes, style=style)
def r_field_integer(self, name, value, minval=None, maxval=None, required=False, classes=None, style=""):
return tpl.format(name=name, value=value, minval=minval, maxval=maxval,
required=required, classes=classes, style=style)
def r_field_integer(self, name, value, minval=None, maxval=None,
required=False, classes=None, style=""):
"""
Render a integer field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['integer']
return tpl.format(name=name, value=value, minval=minval, maxval=maxval, required=required, classes=classes, style=style)
def r_field_float(self, name, value, minval=None, maxval=None, required=False, classes=None, style=""):
return tpl.format(name=name, value=value, minval=minval, maxval=maxval,
required=required, classes=classes, style=style)
def r_field_float(self, name, value, minval=None, maxval=None,
required=False, classes=None, style=""):
"""
Render a float field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['integer']
return tpl.format(name=name, value=value, minval=minval, maxval=maxval, required=required, classes=classes, style=style)
def r_field_date(self, name, value, required=False, classes=None, style=""):
return tpl.format(name=name, value=value, minval=minval, maxval=maxval,
required=required, classes=classes, style=style)
def r_field_date(self, name, value, required=False, classes=None,
style=""):
"""
Render a date field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['date']
return tpl.format(name=name, value=value, required=required, classes=classes, style=style)
return tpl.format(name=name, value=value, required=required,
classes=classes, style=style)
def r_field_file(self, name, required=False, classes=None, style=""):
"""
Render a file field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['file']
return tpl.format(name=name, required=required, classes=classes, style=style)
def r_field_password(self, name, value, minval=None, required=False, classes=None, style=""):
return tpl.format(name=name, required=required, classes=classes,
style=style)
def r_field_password(self, name, value, minval=None, required=False,
classes=None, style=""):
"""
Render a password field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['password']
return tpl.format(name=name, value=value, minval=minval, required=required, classes=classes, style=style)
def r_field_text(self, name, value, rows=4, cols=80, required=False, classes=None, style=""):
return tpl.format(name=name, value=value, minval=minval,
required=required, classes=classes, style=style)
def r_field_text(self, name, value, rows=4, cols=80, required=False,
classes=None, style=""):
"""
Render a text field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['text']
return tpl.format(name=name, value=value, rows=rows, cols=cols, required=required, classes=classes, style=style)
return tpl.format(name=name, value=value, rows=rows, cols=cols,
required=required, classes=classes, style=style)
def r_field_radio(self, name, value, options, classes=None, style=""):
"""
Render a radio field to HTML.
"""
if classes is None:
classes = []
tpl_option = self.field_tpl['radio_option']
@ -114,16 +196,25 @@ class FormRender():
checked = ''
if o_value == value:
checked = 'checked'
radio_elems.append(tpl_option.format(name=name, value=value, checked=checked, label=o_label, classes=classes, style=style))
radio_elems.append(tpl_option.format(name=name, value=value,
checked=checked, label=o_label, classes=classes,
style=style))
return u''.join(radio_elems)
def r_field_checkbox(self, name, checked, classes='', style=""):
"""
Render a checkbox field to HTML.
"""
if classes is None:
classes = []
tpl = self.field_tpl['checkbox']
return tpl.format(name=name, checked=checked, classes=classes, style=style)
return tpl.format(name=name, checked=checked, classes=classes,
style=style)
def r_field_select(self, name, value, options, classes=None, style=""):
"""
Render a select field to HTML.
"""
if classes is None:
classes = []
tpl_option = self.field_tpl['select_option']
@ -132,16 +223,22 @@ class FormRender():
selected = ''
if o_value == value:
selected = 'selected'
select_elems.append(tpl_option.format(value=o_value, selected=selected, label=o_label, style=style))
select_elems.append(tpl_option.format(value=o_value,
selected=selected, label=o_label,
style=style))
tpl = self.field_tpl['select']
return tpl.format(name=name, select_elems=''.join(select_elems), classes=classes, style=style)
return tpl.format(name=name, select_elems=''.join(select_elems),
classes=classes, style=style)
def r_form_line(self, field_type, title, h_input, classes, errors):
"""
Render a line (label + input) to HTML.
"""
if field_type == 'checkbox':
html = html_field_checkbox
html = HTML_FIELD_CHECKBOX
else:
html = html_field
html = HTML_FIELD
return (html.format(classes=' '.join(classes),
title=title,

@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Scriptform roughly works like this:
#
# 1. Instantiate a ScriptForm class. This takes care of loading the form config
@ -18,9 +18,9 @@
# 5. Depending on the request, a method is called on ScriptFormWebApp. These
# methods render HTML to as a response.
# 6. If a form is submitted, its fields are validated and the script callback
# is called. Depending on the output type, the output of the script is either
# captured and displayed as HTML to the user or directly streamed to the
# browser.
# is called. Depending on the output type, the output of the script is
# either captured and displayed as HTML to the user or directly streamed to
# the browser.
# 7. GOTO 4.
# 8. Upon receiving an OS signal (kill, etc) the daemon calls the shutdown
# callback.
@ -28,6 +28,10 @@
# until the next request) to stop the server.
# 10. The program exits.
"""
Main ScriptForm program
"""
import sys
import optparse
import os
@ -43,10 +47,13 @@ from webapp import ThreadedHTTPServer, ScriptFormWebApp
class ScriptFormError(Exception):
"""
Default exception thrown by ScriptForm errors.
"""
pass
class ScriptForm:
class ScriptForm(object):
"""
'Main' class that orchestrates parsing the Form configurations and running
the webserver.
@ -60,7 +67,8 @@ class ScriptForm:
self.running = False
self.httpd = None
self.get_form_config() # Init form config so it can raise errors about problems.
# Init form config so it can raise errors about kproblems.
self.get_form_config()
def get_form_config(self):
"""
@ -122,7 +130,8 @@ class ScriptForm:
is called or something like SystemExit is raised in a handler.
"""
ScriptFormWebApp.scriptform = self
self.httpd = ThreadedHTTPServer((listen_addr, listen_port), ScriptFormWebApp)
self.httpd = ThreadedHTTPServer((listen_addr, listen_port),
ScriptFormWebApp)
self.httpd.daemon_threads = True
self.log.info("Listening on {0}:{1}".format(listen_addr, listen_port))
self.running = True
@ -136,10 +145,14 @@ class ScriptForm:
"""
self.log.info("Attempting server shutdown")
def t_shutdown(sf):
sf.log.info(self.websrv)
sf.httpd.socket.close() # Undocumented requirement to shut the server
sf.httpd.shutdown()
def t_shutdown(scriptform_instance):
"""
Callback for when the server is shutdown.
"""
scriptform_instance.log.info(self.websrv)
# Undocumented feature to shutdow the server.
scriptform_instance.httpd.socket.close()
scriptform_instance.httpd.shutdown()
# We need to spawn a new thread in which the server is shut down,
# because doing it from the main thread blocks, since the server is
@ -148,6 +161,9 @@ class ScriptForm:
def main(): # pragma: no cover
"""
main method
"""
usage = [
sys.argv[0] + " [option] (--start|--stop) <form_definition.json>",
" " + sys.argv[0] + " --generate-pw",
@ -205,15 +221,16 @@ def main(): # pragma: no cover
log = logging.getLogger('MAIN')
try:
if options.action_start:
sf = ScriptForm(args[0], cache=not options.reload)
daemon.register_shutdown_cb(sf.shutdown)
cache = not options.reload
scriptform_instance = ScriptForm(args[0], cache=cache)
daemon.register_shutdown_callback(scriptform_instance.shutdown)
daemon.start()
sf.run(listen_port=options.port)
scriptform_instance.run(listen_port=options.port)
elif options.action_stop:
daemon.stop()
sys.exit(0)
except Exception, e:
log.exception(e)
except Exception, err:
log.exception(err)
raise
if __name__ == "__main__": # pragma: no cover

@ -1,3 +1,8 @@
"""
The webapp part of Scriptform, which takes care of serving requests and
handling them.
"""
from SocketServer import ThreadingMixIn
import BaseHTTPServer
from BaseHTTPServer import BaseHTTPRequestHandler
@ -12,7 +17,7 @@ import hashlib
from formrender import FormRender
html_header = u'''<html>
HTML_HEADER = u'''<html>
<head>
<meta charset="UTF-8">
<style>
@ -49,7 +54,8 @@ html_header = u'''<html>
div.form li.hidden {{ display: none; }}
div.form p.form-field-title {{ margin-bottom: 0px; }}
div.form p.form-field-input {{ margin-top: 0px; }}
div.form li.checkbox p.form-field-input {{ float: left; margin-right: 8px; }}
div.form li.checkbox p.form-field-input {{ float: left;
margin-right: 8px; }}
select,
textarea,
input[type=text],
@ -82,14 +88,18 @@ html_header = u'''<html>
<div class="page">
'''
html_footer = u'''
<div class="about">Powered by <a href="https://github.com/fboender/scriptform">Scriptform</a> v%%VERSION%%</div>
HTML_FOOTER = u'''
<div class="about">
Powered by
<a href="https://github.com/fboender/scriptform">Scriptform</a>
v%%VERSION%%
</div>
</div>
</body>
</html>
'''
html_list = u''''
HTML_LIST = u''''
{header}
<div class="list">
{form_list}
@ -97,18 +107,23 @@ html_list = u''''
{footer}
'''
html_form = u'''
HTML_FORM = u'''
{header}
<div class="form">
<h2 class="form-title">{title}</h2>
<p class="form-description">{description}</p>
<form id="{name}" action="submit" method="post" enctype="multipart/form-data">
<form id="{name}" action="submit" method="post"
enctype="multipart/form-data">
<input type="hidden" name="form_name" value="{name}" />
<ul>
{fields}
<li class="submit">
<input type="submit" class="btn btn-act" value="{submit_title}" />
<a href="."><button type="button" class="btn btn-lnk" value="Back">Back to the list</button></a>
<a href=".">
<button type="button" class="btn btn-lnk" value="Back">
Back to the list
</button>
</a>
</li>
</ul>
</form>
@ -116,7 +131,17 @@ html_form = u'''
{footer}
'''
html_submit_response = u'''
HTML_FORM_LIST = u'''
<li>
<h2 class="form-title">{title}</h2>
<p class="form-description">{description}</p>
<a class="form-link btn btn-act" href="./form?form_name={name}">
{title}
</a>
</li>
'''
HTML_SUBMIT_RESPONSE = u'''
{header}
<div class="result">
<h2 class="result-title">{title}</h2>
@ -136,6 +161,10 @@ html_submit_response = u'''
class HTTPError(Exception):
"""
HTTPError may be thrown by routes to indicate HTTP errors such as 404, 301,
etc. They are caught by the 'framework' and sent to the client's browser.
"""
def __init__(self, status_code, msg, headers=None):
if headers is None:
headers = {}
@ -146,6 +175,9 @@ class HTTPError(Exception):
class ThreadedHTTPServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
"""
Base class for multithreaded HTTP servers.
"""
pass
@ -163,9 +195,15 @@ class WebAppHandler(BaseHTTPRequestHandler):
self.scriptform.log.info(fmt.format(self.address_string(), args))
def do_GET(self):
"""
Handle a GET request.
"""
self._call(*self._parse(self.path))
def do_POST(self):
"""
Handle a POST request.
"""
form_values = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
@ -173,12 +211,15 @@ class WebAppHandler(BaseHTTPRequestHandler):
self._call(self.path.strip('/'), params={'form_values': form_values})
def _parse(self, reqinfo):
"""
Parse information from a request.
"""
url_comp = urlparse.urlsplit(reqinfo)
path = url_comp.path
qs = urlparse.parse_qs(url_comp.query)
query_vars = urlparse.parse_qs(url_comp.query)
# Only return the first value of each query var. E.g. for
# "?foo=1&foo=2" return '1'.
var_values = dict([(k, v[0]) for k, v in qs.items()])
var_values = dict([(k, v[0]) for k, v in query_vars.items()])
return (path.strip('/'), var_values)
def _call(self, path, params):
@ -206,21 +247,21 @@ class WebAppHandler(BaseHTTPRequestHandler):
else:
raise HTTPError(404, "Not found")
method_cb(**params)
except HTTPError, e:
except HTTPError, err:
# HTTP erors are generally thrown by the webapp on purpose. Send
# error to the browser.
if e.status_code not in (401, ):
self.scriptform.log.exception(e)
self.send_response(e.status_code)
for header_k, header_v in e.headers.items():
if err.status_code not in (401, ):
self.scriptform.log.exception(err)
self.send_response(err.status_code)
for header_k, header_v in err.headers.items():
self.send_header(header_k, header_v)
self.end_headers()
self.wfile.write("Error {}: {}".format(e.status_code,
e.msg))
self.wfile.write("Error {}: {}".format(err.status_code,
err.msg))
self.wfile.flush()
return False
except Exception, e:
self.scriptform.log.exception(e)
except Exception, err:
self.scriptform.log.exception(err)
self.send_error(500, "Internal server error")
raise
@ -236,7 +277,8 @@ class ScriptFormWebApp(WebAppHandler):
"""
form_config = self.scriptform.get_form_config()
visible_forms = form_config.get_visible_forms(getattr(self, 'username', None))
username = getattr(self, 'username', None)
visible_forms = form_config.get_visible_forms(username)
if len(visible_forms) == 1:
first_form = visible_forms[0]
return self.h_form(first_form.name)
@ -282,24 +324,20 @@ class ScriptFormWebApp(WebAppHandler):
form_config = self.scriptform.get_form_config()
h_form_list = []
for form_def in form_config.get_visible_forms(getattr(self, 'username', None)):
h_form_list.append(u'''
<li>
<h2 class="form-title">{title}</h2>
<p class="form-description">{description}</p>
<a class="form-link btn btn-act" href="./form?form_name={name}">
{title}
</a>
</li>
'''.format(title=form_def.title,
username = getattr(self, 'username', None)
for form_def in form_config.get_visible_forms(username):
h_form_list.append(
HTML_FORM_LIST.format(
title=form_def.title,
description=form_def.description,
name=form_def.name)
name=form_def.name
)
)
output = html_list.format(
header=html_header.format(title=form_config.title,
output = HTML_LIST.format(
header=HTML_HEADER.format(title=form_config.title,
custom_css=form_config.custom_css),
footer=html_footer,
footer=HTML_FOOTER,
form_list=u''.join(h_form_list)
)
self.send_response(200)
@ -317,9 +355,12 @@ class ScriptFormWebApp(WebAppHandler):
return
form_config = self.scriptform.get_form_config()
fr = FormRender(None)
fr_inst = FormRender(None)
def render_field(field, errors):
"""
Render a HTML field.
"""
params = {
'name': field['name'],
'classes': [],
@ -336,7 +377,7 @@ class ScriptFormWebApp(WebAppHandler):
if field['type'] not in ('radio', 'checkbox', 'select'):
params['required'] = field.get('required', False)
if field['type'] in ('string'):
if field['type'] == 'string':
params['size'] = field.get('size', '')
if field['type'] in ('number', 'integer', 'float', 'password'):
@ -345,7 +386,7 @@ class ScriptFormWebApp(WebAppHandler):
if field['type'] in ('number', 'integer', 'float'):
params['maxval'] = field.get("max", '')
if field['type'] in ('text'):
if field['type'] == 'text':
params['rows'] = field.get("rows", '')
params['cols'] = field.get("cols", '')
@ -359,12 +400,13 @@ class ScriptFormWebApp(WebAppHandler):
if field['type'] == 'checkbox':
params['checked'] = False
if field['name'] in form_values and form_values[field['name']] == 'on':
if field['name'] in form_values and \
form_values[field['name']] == 'on':
params['checked'] = True
h_input = fr.r_field(field['type'], **params)
h_input = fr_inst.r_field(field['type'], **params)
return fr.r_form_line(field['type'], field['title'],
return fr_inst.r_form_line(field['type'], field['title'],
h_input, params['classes'], errors)
# Make sure the user is allowed to access this form.
@ -380,15 +422,18 @@ class ScriptFormWebApp(WebAppHandler):
html_errors += u'<li class="error">{0}</li>'.format(error)
html_errors += u'</ul>'
output = html_form.format(
header=html_header.format(title=form_config.title,
output = HTML_FORM.format(
header=HTML_HEADER.format(title=form_config.title,
custom_css=form_config.custom_css),
footer=html_footer,
footer=HTML_FOOTER,
title=form_def.title,
description=form_def.description,
errors=html_errors,
name=form_def.name,
fields=u''.join([render_field(f, errors.get(f['name'], [])) for f in form_def.fields]),
fields=u''.join(
[render_field(f, errors.get(f['name'], []))
for f in form_def.fields]
),
submit_title=form_def.submit_title
)
self.send_response(200)
@ -427,18 +472,18 @@ class ScriptFormWebApp(WebAppHandler):
# something was actually uploaded
if field.filename == '':
continue
tmpfile = tempfile.mktemp(prefix="scriptform_")
f = file(tmpfile, 'w')
tmp_fname = tempfile.mktemp(prefix="scriptform_")
tmp_file = file(tmp_fname, 'w')
while True:
buf = field.file.read(1024 * 16)
if not buf:
break
f.write(buf)
f.close()
tmp_file.write(buf)
tmp_file.close()
field.file.close()
tmp_files.append(tmpfile) # For later cleanup
values[field_name] = tmpfile
tmp_files.append(tmp_fname) # For later cleanup
values[field_name] = tmp_fname
values['{0}__name'.format(field_name)] = field.filename
else:
# Field is a normal form field. Store its value.
@ -455,28 +500,35 @@ class ScriptFormWebApp(WebAppHandler):
# Log the callback and its parameters for auditing purposes.
log = logging.getLogger('CALLBACK_AUDIT')
cwd = os.path.realpath(os.curdir)
username = getattr(self.request, 'username', 'None')
log.info("Calling script: {0}".format(form_def.script))
log.info("Current working dir: {0}".format(os.path.realpath(os.curdir)))
log.info("User: {0}".format(getattr(self.request, 'username', 'None')))
log.info("Current working dir: {0}".format(cwd))
log.info("User: {0}".format(username))
log.info("Variables: {0}".format(dict(form_values.items())))
result = form_config.callback(form_name, form_values, self.wfile, self.wfile)
result = form_config.callback(form_name, form_values, self.wfile,
self.wfile)
if form_def.output != 'raw':
# Ignore everything if we're doing raw output, since it's the
# scripts responsibility.
if result['exitcode'] != 0:
msg = u'<span class="error">{0}</span>'.format(cgi.escape(result['stderr'].decode('utf8')))
stderr = cgi.escape(result['stderr'].decode('utf8'))
msg = u'<span class="error">{0}</span>'.format(stderr)
else:
if form_def.output == 'escaped':
msg = u'<pre>{0}</pre>'.format(cgi.escape(result['stdout'].decode('utf8')))
stdout = cgi.escape(result['stdout'].decode('utf8'))
msg = u'<pre>{0}</pre>'.format(stdout)
else:
# Non-escaped output (html, usually)
msg = result['stdout'].decode('utf8')
output = html_submit_response.format(
header=html_header.format(title=form_config.title,
custom_css=form_config.custom_css),
footer=html_footer,
output = HTML_SUBMIT_RESPONSE.format(
header=HTML_HEADER.format(
title=form_config.title,
custom_css=form_config.custom_css
),
footer=HTML_FOOTER,
title=form_def.title,
form_name=form_def.name,
msg=msg,
@ -512,7 +564,7 @@ class ScriptFormWebApp(WebAppHandler):
if not os.path.exists(path):
raise HTTPError(404, "Not found")
f = file(path, 'r')
static_file = file(path, 'r')
self.send_response(200)
self.end_headers()
self.wfile.write(f.read())
self.wfile.write(static_file.read())

Loading…
Cancel
Save