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 logging
import os import os
import sys import sys
@ -8,10 +12,13 @@ import atexit
class DaemonError(Exception): class DaemonError(Exception):
"""
Default error for Daemon class.
"""
pass pass
class Daemon: # pragma: no cover class Daemon(object): # pragma: no cover
""" """
Daemonize the current process (detach it from the console). Daemonize the current process (detach it from the console).
""" """
@ -27,17 +34,24 @@ class Daemon: # pragma: no cover
self.log_file = log_file self.log_file = log_file
self.foreground = foreground self.foreground = foreground
log_fmt = '%(asctime)s:%(name)s:%(levelname)s:%(message)s'
logging.basicConfig(level=log_level, logging.basicConfig(level=log_level,
format='%(asctime)s:%(name)s:%(levelname)s:%(message)s', format=log_fmt,
filename=self.log_file, filename=self.log_file,
filemode='a') filemode='a')
self.log = logging.getLogger('DAEMON') self.log = logging.getLogger('DAEMON')
self.shutdown_cb = None self.shutdown_callback = None
def register_shutdown_cb(self, cb): def register_shutdown_callback(self, callback):
self.shutdown_cb = cb """
Register a callback to be executed when the daemon is stopped.
"""
self.shutdown_callback = callback
def start(self): def start(self):
"""
Start the daemon. Raises a DaemonError if it's already running.
"""
self.log.info("Starting") self.log.info("Starting")
if self.is_running(): if self.is_running():
self.log.error('Already running') self.log.error('Already running')
@ -46,6 +60,10 @@ class Daemon: # pragma: no cover
self._fork() self._fork()
def stop(self): 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(): if not self.is_running():
raise DaemonError("Not running") raise DaemonError("Not running")
@ -101,6 +119,11 @@ class Daemon: # pragma: no cover
return True return True
def _fork(self): 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) # Fork a child and end the parent (detach from parent)
pid = os.fork() pid = os.fork()
if pid > 0: if pid > 0:
@ -114,9 +137,9 @@ class Daemon: # pragma: no cover
pid = os.fork() pid = os.fork()
if pid > 0: if pid > 0:
self.log.info("PID = {0}".format(pid)) self.log.info("PID = {0}".format(pid))
f = file(self.pid_file, 'w') pidfile = file(self.pid_file, 'w')
f.write(str(pid)) pidfile.write(str(pid))
f.close() pidfile.close()
sys.exit(0) # End parent sys.exit(0) # End parent
atexit.register(self._cleanup) 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 # Close STDIN, STDOUT and STDERR so we don't tie up the controlling
# terminal # terminal
for fd in (0, 1, 2): for fdescriptor in (0, 1, 2):
try: try:
os.close(fd) os.close(fdescriptor)
except OSError: except OSError:
pass pass
@ -139,7 +162,10 @@ class Daemon: # pragma: no cover
return pid return pid
def _cleanup(self, sig=None): def _cleanup(self, sig=None):
"""
Remvoe pid files and call registered shutodnw callbacks.
"""
self.log.info("Received signal {0}".format(sig)) self.log.info("Received signal {0}".format(sig))
if os.path.exists(self.pid_file): if os.path.exists(self.pid_file):
os.unlink(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 logging
import stat import stat
import os import os
@ -5,16 +11,20 @@ import subprocess
class FormConfigError(Exception): class FormConfigError(Exception):
"""
Default error for FormConfig errors
"""
pass pass
class FormConfig: class FormConfig(object):
""" """
FormConfig is the in-memory representation of a form configuration JSON FormConfig is the in-memory representation of a form configuration JSON
file. It holds information (title, users, the form definitions) on the file. It holds information (title, users, the form definitions) on the
form configuration being served by this instance of ScriptForm. 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.title = title
self.users = {} self.users = {}
if users is not None: if users is not None:
@ -27,7 +37,8 @@ class FormConfig:
# Validate scripts # Validate scripts
for form_def in self.forms: for form_def in self.forms:
if not stat.S_IXUSR & os.stat(form_def.script)[stat.ST_MODE]: 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): def get_form_def(self, form_name):
""" """
@ -70,29 +81,33 @@ class FormConfig:
# Validate params # Validate params
if form.output == 'raw' and (stdout is None or stderr is None): 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. # Pass form values to the script through the environment as strings.
env = os.environ.copy() env = os.environ.copy()
for k, v in form_values.items(): for key, value in form_values.items():
env[k] = str(v) env[key] = str(value)
# If the form output type is 'raw', we directly stream the output to # If the form output type is 'raw', we directly stream the output to
# the browser. Otherwise we store it for later displaying. # the browser. Otherwise we store it for later displaying.
if form.output == 'raw': if form.output == 'raw':
p = subprocess.Popen(form.script, shell=True, proc = subprocess.Popen(form.script, shell=True,
stdout=stdout, stdout=stdout,
stderr=stderr, stderr=stderr,
env=env) env=env)
stdout, stderr = p.communicate(input) stdout, stderr = proc.communicate(input)
return p.returncode return proc.returncode
else: else:
p = subprocess.Popen(form.script, shell=True, stdin=subprocess.PIPE, proc = subprocess.Popen(form.script, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env) env=env)
stdout, stderr = p.communicate() stdout, stderr = proc.communicate()
return { return {
'stdout': stdout, 'stdout': stdout,
'stderr': stderr, '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 os
import datetime import datetime
class ValidationError(Exception): class ValidationError(Exception):
"""Default exception for Validation errors"""
pass pass
class FormDefinition: class FormDefinition(object):
""" """
FormDefinition holds information about a single form and provides methods FormDefinition holds information about a single form and provides methods
for validation of the form values. for validation of the form values.
@ -25,6 +31,9 @@ class FormDefinition:
self.allowed_users = allowed_users self.allowed_users = allowed_users
def get_field_def(self, field_name): def get_field_def(self, field_name):
"""
Return the field definition for `field_name`.
"""
for field in self.fields: for field in self.fields:
if field['name'] == field_name: if field['name'] == field_name:
return field return field
@ -40,9 +49,11 @@ class FormDefinition:
# First make sure all required fields are there # First make sure all required fields are there
for field in self.fields: for field in self.fields:
if 'required' in field and \ field_required = ('required' in field and
field['required'] is True and \ field['required'] is True)
(field['name'] not in form_values or form_values[field['name']] == ''): field_missing = (field['name'] not in form_values or
form_values[field['name']] == '')
if field_required and field_missing:
errors.setdefault(field['name'], []).append( errors.setdefault(field['name'], []).append(
"This field is required" "This field is required"
) )
@ -51,14 +62,15 @@ class FormDefinition:
for field in self.fields: for field in self.fields:
field_name = field['name'] field_name = field['name']
if field_name in errors: 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 continue
try: try:
v = self._field_validate(field_name, form_values) value = self._field_validate(field_name, form_values)
if v is not None: if value is not None:
values[field_name] = v values[field_name] = value
except ValidationError, e: except ValidationError, err:
errors.setdefault(field_name, []).append(str(e)) errors.setdefault(field_name, []).append(str(err))
return (errors, values) return (errors, values)
@ -75,6 +87,9 @@ class FormDefinition:
return validate_cb(field_def, form_values) return validate_cb(field_def, form_values)
def validate_string(self, 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']] value = form_values[field_def['name']]
maxlen = field_def.get('maxlen', None) maxlen = field_def.get('maxlen', None)
minlen = field_def.get('minlen', None) minlen = field_def.get('minlen', None)
@ -87,6 +102,9 @@ class FormDefinition:
return value return value
def validate_integer(self, field_def, form_values): def validate_integer(self, field_def, form_values):
"""
Validate a form field of type 'integer'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
maxval = field_def.get('max', None) maxval = field_def.get('max', None)
minval = field_def.get('min', None) minval = field_def.get('min', None)
@ -104,6 +122,9 @@ class FormDefinition:
return int(value) return int(value)
def validate_float(self, field_def, form_values): def validate_float(self, field_def, form_values):
"""
Validate a form field of type 'float'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
maxval = field_def.get('max', None) maxval = field_def.get('max', None)
minval = field_def.get('min', None) minval = field_def.get('min', None)
@ -121,6 +142,9 @@ class FormDefinition:
return float(value) return float(value)
def validate_date(self, field_def, form_values): def validate_date(self, field_def, form_values):
"""
Validate a form field of type 'date'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
maxval = field_def.get('max', None) maxval = field_def.get('max', None)
minval = field_def.get('min', None) minval = field_def.get('min', None)
@ -140,6 +164,9 @@ class FormDefinition:
return value return value
def validate_radio(self, field_def, form_values): def validate_radio(self, field_def, form_values):
"""
Validate a form field of type 'radio'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
if not value in [o[0] for o in field_def['options']]: if not value in [o[0] for o in field_def['options']]:
raise ValidationError( raise ValidationError(
@ -147,6 +174,9 @@ class FormDefinition:
return value return value
def validate_select(self, field_def, form_values): def validate_select(self, field_def, form_values):
"""
Validate a form field of type 'select'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
if not value in [o[0] for o in field_def['options']]: if not value in [o[0] for o in field_def['options']]:
raise ValidationError( raise ValidationError(
@ -154,6 +184,9 @@ class FormDefinition:
return value return value
def validate_checkbox(self, field_def, form_values): def validate_checkbox(self, field_def, form_values):
"""
Validate a form field of type 'checkbox'.
"""
value = form_values.get(field_def['name'], 'off') value = form_values.get(field_def['name'], 'off')
if not value in ['on', 'off']: if not value in ['on', 'off']:
raise ValidationError( raise ValidationError(
@ -161,6 +194,9 @@ class FormDefinition:
return value return value
def validate_text(self, field_def, form_values): def validate_text(self, field_def, form_values):
"""
Validate a form field of type 'text'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
minlen = field_def.get('minlen', None) minlen = field_def.get('minlen', None)
maxlen = field_def.get('maxlen', None) maxlen = field_def.get('maxlen', None)
@ -174,6 +210,9 @@ class FormDefinition:
return value return value
def validate_password(self, field_def, form_values): def validate_password(self, field_def, form_values):
"""
Validate a form field of type 'password'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
minlen = field_def.get('minlen', None) minlen = field_def.get('minlen', None)
@ -183,6 +222,9 @@ class FormDefinition:
return value return value
def validate_file(self, field_def, form_values): def validate_file(self, field_def, form_values):
"""
Validate a form field of type 'file'.
"""
value = form_values[field_def['name']] value = form_values[field_def['name']]
field_name = field_def['name'] field_name = field_def['name']
upload_fname = form_values[u'{0}__name'.format(field_name)] upload_fname = form_values[u'{0}__name'.format(field_name)]
@ -190,6 +232,7 @@ class FormDefinition:
extensions = field_def.get('extensions', None) extensions = field_def.get('extensions', None)
if extensions is not None and upload_fname_ext not in extensions: 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 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}"> <li class="{classes}">
<p class="form-field-title">{title}</p> <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> </li>
''' '''
html_field_checkbox = u''' HTML_FIELD_CHECKBOX = u'''
<li class="checkbox {classes}"> <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> </li>
''' '''
class FormRender(): class FormRender(object):
"""
FormRender takes care of the rendering of forms to HTML.
"""
field_tpl = { field_tpl = {
"string": u'<input {required} type="text" name="{name}" value="{value}" size="{size}" class="{classes}" style="{style}" />', "string": u'<input {required} type="text" name="{name}" '
"number": u'<input {required} type="number" min="{minval}" max="{maxval}" name="{name}" value="{value}" class="{classes}" style="{style}" />', u'value="{value}" size="{size}" '
"integer": u'<input {required} type="number" min="{minval}" max="{maxval}" name="{name}" value="{value}" class="{classes}" style="{style}" />', u'class="{classes}" style="{style}" />',
"float": u'<input {required} type="number" min="{minval}" max="{maxval}" step="any" name="{name}" value="{value}" class="{classes}" style="{style}" />', "number": u'<input {required} type="number" min="{minval}" '
"date": u'<input {required} type="date" name="{name}" value="{value}" class="{classes}" style="{style}" />', u'max="{maxval}" name="{name}" value="{value}" '
"file": u'<input {required} type="file" name="{name}" class="{classes}" style="{style}" />', u'class="{classes}" style="{style}" />',
"password": u'<input {required} type="password" min="{minval}" name="{name}" value="{value}" class="{classes}" style="{style}" />', "integer": u'<input {required} type="number" min="{minval}" '
"text": u'<textarea {required} name="{name}" rows="{rows}" cols="{cols}" style="{style}" class="{classes}">{value}</textarea>', u'max="{maxval}" name="{name}" value="{value}" '
"radio_option": u'<input {checked} type="radio" name="{name}" value="{value}" class="{classes} style="{style}"">{label}<br/>', u'class="{classes}" style="{style}" />',
"select_option": u'<option value="{value}" style="{style}" {selected}>{label}</option>', "float": u'<input {required} type="number" min="{minval}" '
"select": u'<select name="{name}" class="{classes}" style="{style}">{select_elems}</select>', u'max="{maxval}" step="any" name="{name}" '
"checkbox": u'<input {checked} type="checkbox" name="{name}" value="on" class="{classes} style="{style}"" />', 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): def __init__(self, form_def):
self.form_def = form_def self.form_def = form_def
def cast_params(self, params): 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() new_params = params.copy()
if 'required' in new_params: if 'required' in new_params:
@ -52,60 +89,105 @@ class FormRender():
return new_params return new_params
def r_field(self, field_type, **kwargs): def r_field(self, field_type, **kwargs):
"""
Render a generic field to HTML.
"""
params = self.cast_params(kwargs) params = self.cast_params(kwargs)
method_name = 'r_field_{0}'.format(field_type) method_name = 'r_field_{0}'.format(field_type)
method = getattr(self, method_name, None) method = getattr(self, method_name, None)
return method(**params) 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: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['string'] tpl = self.field_tpl['string']
return tpl.format(name=name, value=value, size=size, required=required, classes=classes, style=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=""):
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: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['number'] tpl = self.field_tpl['number']
return tpl.format(name=name, value=value, minval=minval, maxval=maxval, required=required, classes=classes, style=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=""):
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: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['integer'] tpl = self.field_tpl['integer']
return tpl.format(name=name, value=value, minval=minval, maxval=maxval, required=required, classes=classes, style=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=""):
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: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['integer'] tpl = self.field_tpl['integer']
return tpl.format(name=name, value=value, minval=minval, maxval=maxval, required=required, classes=classes, style=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=""):
def r_field_date(self, name, value, required=False, classes=None,
style=""):
"""
Render a date field to HTML.
"""
if classes is None: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['date'] 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=""): def r_field_file(self, name, required=False, classes=None, style=""):
"""
Render a file field to HTML.
"""
if classes is None: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['file'] tpl = self.field_tpl['file']
return tpl.format(name=name, required=required, classes=classes, style=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=""):
def r_field_password(self, name, value, minval=None, required=False,
classes=None, style=""):
"""
Render a password field to HTML.
"""
if classes is None: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['password'] tpl = self.field_tpl['password']
return tpl.format(name=name, value=value, minval=minval, required=required, classes=classes, style=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=""):
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: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['text'] 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=""): def r_field_radio(self, name, value, options, classes=None, style=""):
"""
Render a radio field to HTML.
"""
if classes is None: if classes is None:
classes = [] classes = []
tpl_option = self.field_tpl['radio_option'] tpl_option = self.field_tpl['radio_option']
@ -114,16 +196,25 @@ class FormRender():
checked = '' checked = ''
if o_value == value: if o_value == value:
checked = 'checked' 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) return u''.join(radio_elems)
def r_field_checkbox(self, name, checked, classes='', style=""): def r_field_checkbox(self, name, checked, classes='', style=""):
"""
Render a checkbox field to HTML.
"""
if classes is None: if classes is None:
classes = [] classes = []
tpl = self.field_tpl['checkbox'] 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=""): def r_field_select(self, name, value, options, classes=None, style=""):
"""
Render a select field to HTML.
"""
if classes is None: if classes is None:
classes = [] classes = []
tpl_option = self.field_tpl['select_option'] tpl_option = self.field_tpl['select_option']
@ -132,16 +223,22 @@ class FormRender():
selected = '' selected = ''
if o_value == value: if o_value == value:
selected = 'selected' 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'] 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): def r_form_line(self, field_type, title, h_input, classes, errors):
"""
Render a line (label + input) to HTML.
"""
if field_type == 'checkbox': if field_type == 'checkbox':
html = html_field_checkbox html = HTML_FIELD_CHECKBOX
else: else:
html = html_field html = HTML_FIELD
return (html.format(classes=' '.join(classes), return (html.format(classes=' '.join(classes),
title=title, title=title,

@ -1,6 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#
# Scriptform roughly works like this: # Scriptform roughly works like this:
# #
# 1. Instantiate a ScriptForm class. This takes care of loading the form config # 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 # 5. Depending on the request, a method is called on ScriptFormWebApp. These
# methods render HTML to as a response. # methods render HTML to as a response.
# 6. If a form is submitted, its fields are validated and the script callback # 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 # is called. Depending on the output type, the output of the script is
# captured and displayed as HTML to the user or directly streamed to the # either captured and displayed as HTML to the user or directly streamed to
# browser. # the browser.
# 7. GOTO 4. # 7. GOTO 4.
# 8. Upon receiving an OS signal (kill, etc) the daemon calls the shutdown # 8. Upon receiving an OS signal (kill, etc) the daemon calls the shutdown
# callback. # callback.
@ -28,6 +28,10 @@
# until the next request) to stop the server. # until the next request) to stop the server.
# 10. The program exits. # 10. The program exits.
"""
Main ScriptForm program
"""
import sys import sys
import optparse import optparse
import os import os
@ -43,10 +47,13 @@ from webapp import ThreadedHTTPServer, ScriptFormWebApp
class ScriptFormError(Exception): class ScriptFormError(Exception):
"""
Default exception thrown by ScriptForm errors.
"""
pass pass
class ScriptForm: class ScriptForm(object):
""" """
'Main' class that orchestrates parsing the Form configurations and running 'Main' class that orchestrates parsing the Form configurations and running
the webserver. the webserver.
@ -60,7 +67,8 @@ class ScriptForm:
self.running = False self.running = False
self.httpd = None 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): def get_form_config(self):
""" """
@ -122,7 +130,8 @@ class ScriptForm:
is called or something like SystemExit is raised in a handler. is called or something like SystemExit is raised in a handler.
""" """
ScriptFormWebApp.scriptform = self 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.httpd.daemon_threads = True
self.log.info("Listening on {0}:{1}".format(listen_addr, listen_port)) self.log.info("Listening on {0}:{1}".format(listen_addr, listen_port))
self.running = True self.running = True
@ -136,10 +145,14 @@ class ScriptForm:
""" """
self.log.info("Attempting server shutdown") self.log.info("Attempting server shutdown")
def t_shutdown(sf): def t_shutdown(scriptform_instance):
sf.log.info(self.websrv) """
sf.httpd.socket.close() # Undocumented requirement to shut the server Callback for when the server is shutdown.
sf.httpd.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, # 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 # because doing it from the main thread blocks, since the server is
@ -148,6 +161,9 @@ class ScriptForm:
def main(): # pragma: no cover def main(): # pragma: no cover
"""
main method
"""
usage = [ usage = [
sys.argv[0] + " [option] (--start|--stop) <form_definition.json>", sys.argv[0] + " [option] (--start|--stop) <form_definition.json>",
" " + sys.argv[0] + " --generate-pw", " " + sys.argv[0] + " --generate-pw",
@ -205,15 +221,16 @@ def main(): # pragma: no cover
log = logging.getLogger('MAIN') log = logging.getLogger('MAIN')
try: try:
if options.action_start: if options.action_start:
sf = ScriptForm(args[0], cache=not options.reload) cache = not options.reload
daemon.register_shutdown_cb(sf.shutdown) scriptform_instance = ScriptForm(args[0], cache=cache)
daemon.register_shutdown_callback(scriptform_instance.shutdown)
daemon.start() daemon.start()
sf.run(listen_port=options.port) scriptform_instance.run(listen_port=options.port)
elif options.action_stop: elif options.action_stop:
daemon.stop() daemon.stop()
sys.exit(0) sys.exit(0)
except Exception, e: except Exception, err:
log.exception(e) log.exception(err)
raise raise
if __name__ == "__main__": # pragma: no cover 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 from SocketServer import ThreadingMixIn
import BaseHTTPServer import BaseHTTPServer
from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import BaseHTTPRequestHandler
@ -12,7 +17,7 @@ import hashlib
from formrender import FormRender from formrender import FormRender
html_header = u'''<html> HTML_HEADER = u'''<html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
@ -49,7 +54,8 @@ html_header = u'''<html>
div.form li.hidden {{ display: none; }} div.form li.hidden {{ display: none; }}
div.form p.form-field-title {{ margin-bottom: 0px; }} div.form p.form-field-title {{ margin-bottom: 0px; }}
div.form p.form-field-input {{ margin-top: 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, select,
textarea, textarea,
input[type=text], input[type=text],
@ -82,14 +88,18 @@ html_header = u'''<html>
<div class="page"> <div class="page">
''' '''
html_footer = u''' HTML_FOOTER = u'''
<div class="about">Powered by <a href="https://github.com/fboender/scriptform">Scriptform</a> v%%VERSION%%</div> <div class="about">
Powered by
<a href="https://github.com/fboender/scriptform">Scriptform</a>
v%%VERSION%%
</div>
</div> </div>
</body> </body>
</html> </html>
''' '''
html_list = u'''' HTML_LIST = u''''
{header} {header}
<div class="list"> <div class="list">
{form_list} {form_list}
@ -97,18 +107,23 @@ html_list = u''''
{footer} {footer}
''' '''
html_form = u''' HTML_FORM = u'''
{header} {header}
<div class="form"> <div class="form">
<h2 class="form-title">{title}</h2> <h2 class="form-title">{title}</h2>
<p class="form-description">{description}</p> <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}" /> <input type="hidden" name="form_name" value="{name}" />
<ul> <ul>
{fields} {fields}
<li class="submit"> <li class="submit">
<input type="submit" class="btn btn-act" value="{submit_title}" /> <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> </li>
</ul> </ul>
</form> </form>
@ -116,7 +131,17 @@ html_form = u'''
{footer} {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} {header}
<div class="result"> <div class="result">
<h2 class="result-title">{title}</h2> <h2 class="result-title">{title}</h2>
@ -136,6 +161,10 @@ html_submit_response = u'''
class HTTPError(Exception): 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): def __init__(self, status_code, msg, headers=None):
if headers is None: if headers is None:
headers = {} headers = {}
@ -146,6 +175,9 @@ class HTTPError(Exception):
class ThreadedHTTPServer(ThreadingMixIn, BaseHTTPServer.HTTPServer): class ThreadedHTTPServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
"""
Base class for multithreaded HTTP servers.
"""
pass pass
@ -163,9 +195,15 @@ class WebAppHandler(BaseHTTPRequestHandler):
self.scriptform.log.info(fmt.format(self.address_string(), args)) self.scriptform.log.info(fmt.format(self.address_string(), args))
def do_GET(self): def do_GET(self):
"""
Handle a GET request.
"""
self._call(*self._parse(self.path)) self._call(*self._parse(self.path))
def do_POST(self): def do_POST(self):
"""
Handle a POST request.
"""
form_values = cgi.FieldStorage( form_values = cgi.FieldStorage(
fp=self.rfile, fp=self.rfile,
headers=self.headers, headers=self.headers,
@ -173,12 +211,15 @@ class WebAppHandler(BaseHTTPRequestHandler):
self._call(self.path.strip('/'), params={'form_values': form_values}) self._call(self.path.strip('/'), params={'form_values': form_values})
def _parse(self, reqinfo): def _parse(self, reqinfo):
"""
Parse information from a request.
"""
url_comp = urlparse.urlsplit(reqinfo) url_comp = urlparse.urlsplit(reqinfo)
path = url_comp.path 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 # Only return the first value of each query var. E.g. for
# "?foo=1&foo=2" return '1'. # "?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) return (path.strip('/'), var_values)
def _call(self, path, params): def _call(self, path, params):
@ -206,21 +247,21 @@ class WebAppHandler(BaseHTTPRequestHandler):
else: else:
raise HTTPError(404, "Not found") raise HTTPError(404, "Not found")
method_cb(**params) method_cb(**params)
except HTTPError, e: except HTTPError, err:
# HTTP erors are generally thrown by the webapp on purpose. Send # HTTP erors are generally thrown by the webapp on purpose. Send
# error to the browser. # error to the browser.
if e.status_code not in (401, ): if err.status_code not in (401, ):
self.scriptform.log.exception(e) self.scriptform.log.exception(err)
self.send_response(e.status_code) self.send_response(err.status_code)
for header_k, header_v in e.headers.items(): for header_k, header_v in err.headers.items():
self.send_header(header_k, header_v) self.send_header(header_k, header_v)
self.end_headers() self.end_headers()
self.wfile.write("Error {}: {}".format(e.status_code, self.wfile.write("Error {}: {}".format(err.status_code,
e.msg)) err.msg))
self.wfile.flush() self.wfile.flush()
return False return False
except Exception, e: except Exception, err:
self.scriptform.log.exception(e) self.scriptform.log.exception(err)
self.send_error(500, "Internal server error") self.send_error(500, "Internal server error")
raise raise
@ -236,7 +277,8 @@ class ScriptFormWebApp(WebAppHandler):
""" """
form_config = self.scriptform.get_form_config() 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: if len(visible_forms) == 1:
first_form = visible_forms[0] first_form = visible_forms[0]
return self.h_form(first_form.name) return self.h_form(first_form.name)
@ -282,24 +324,20 @@ class ScriptFormWebApp(WebAppHandler):
form_config = self.scriptform.get_form_config() form_config = self.scriptform.get_form_config()
h_form_list = [] h_form_list = []
for form_def in form_config.get_visible_forms(getattr(self, 'username', None)): username = getattr(self, 'username', None)
h_form_list.append(u''' for form_def in form_config.get_visible_forms(username):
<li> h_form_list.append(
<h2 class="form-title">{title}</h2> HTML_FORM_LIST.format(
<p class="form-description">{description}</p> title=form_def.title,
<a class="form-link btn btn-act" href="./form?form_name={name}">
{title}
</a>
</li>
'''.format(title=form_def.title,
description=form_def.description, description=form_def.description,
name=form_def.name) name=form_def.name
)
) )
output = html_list.format( output = HTML_LIST.format(
header=html_header.format(title=form_config.title, header=HTML_HEADER.format(title=form_config.title,
custom_css=form_config.custom_css), custom_css=form_config.custom_css),
footer=html_footer, footer=HTML_FOOTER,
form_list=u''.join(h_form_list) form_list=u''.join(h_form_list)
) )
self.send_response(200) self.send_response(200)
@ -317,9 +355,12 @@ class ScriptFormWebApp(WebAppHandler):
return return
form_config = self.scriptform.get_form_config() form_config = self.scriptform.get_form_config()
fr = FormRender(None) fr_inst = FormRender(None)
def render_field(field, errors): def render_field(field, errors):
"""
Render a HTML field.
"""
params = { params = {
'name': field['name'], 'name': field['name'],
'classes': [], 'classes': [],
@ -336,7 +377,7 @@ class ScriptFormWebApp(WebAppHandler):
if field['type'] not in ('radio', 'checkbox', 'select'): if field['type'] not in ('radio', 'checkbox', 'select'):
params['required'] = field.get('required', False) params['required'] = field.get('required', False)
if field['type'] in ('string'): if field['type'] == 'string':
params['size'] = field.get('size', '') params['size'] = field.get('size', '')
if field['type'] in ('number', 'integer', 'float', 'password'): if field['type'] in ('number', 'integer', 'float', 'password'):
@ -345,7 +386,7 @@ class ScriptFormWebApp(WebAppHandler):
if field['type'] in ('number', 'integer', 'float'): if field['type'] in ('number', 'integer', 'float'):
params['maxval'] = field.get("max", '') params['maxval'] = field.get("max", '')
if field['type'] in ('text'): if field['type'] == 'text':
params['rows'] = field.get("rows", '') params['rows'] = field.get("rows", '')
params['cols'] = field.get("cols", '') params['cols'] = field.get("cols", '')
@ -359,12 +400,13 @@ class ScriptFormWebApp(WebAppHandler):
if field['type'] == 'checkbox': if field['type'] == 'checkbox':
params['checked'] = False 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 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) h_input, params['classes'], errors)
# Make sure the user is allowed to access this form. # 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'<li class="error">{0}</li>'.format(error)
html_errors += u'</ul>' html_errors += u'</ul>'
output = html_form.format( output = HTML_FORM.format(
header=html_header.format(title=form_config.title, header=HTML_HEADER.format(title=form_config.title,
custom_css=form_config.custom_css), custom_css=form_config.custom_css),
footer=html_footer, footer=HTML_FOOTER,
title=form_def.title, title=form_def.title,
description=form_def.description, description=form_def.description,
errors=html_errors, errors=html_errors,
name=form_def.name, 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 submit_title=form_def.submit_title
) )
self.send_response(200) self.send_response(200)
@ -427,18 +472,18 @@ class ScriptFormWebApp(WebAppHandler):
# something was actually uploaded # something was actually uploaded
if field.filename == '': if field.filename == '':
continue continue
tmpfile = tempfile.mktemp(prefix="scriptform_") tmp_fname = tempfile.mktemp(prefix="scriptform_")
f = file(tmpfile, 'w') tmp_file = file(tmp_fname, 'w')
while True: while True:
buf = field.file.read(1024 * 16) buf = field.file.read(1024 * 16)
if not buf: if not buf:
break break
f.write(buf) tmp_file.write(buf)
f.close() tmp_file.close()
field.file.close() field.file.close()
tmp_files.append(tmpfile) # For later cleanup tmp_files.append(tmp_fname) # For later cleanup
values[field_name] = tmpfile values[field_name] = tmp_fname
values['{0}__name'.format(field_name)] = field.filename values['{0}__name'.format(field_name)] = field.filename
else: else:
# Field is a normal form field. Store its value. # 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 the callback and its parameters for auditing purposes.
log = logging.getLogger('CALLBACK_AUDIT') 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("Calling script: {0}".format(form_def.script))
log.info("Current working dir: {0}".format(os.path.realpath(os.curdir))) log.info("Current working dir: {0}".format(cwd))
log.info("User: {0}".format(getattr(self.request, 'username', 'None'))) log.info("User: {0}".format(username))
log.info("Variables: {0}".format(dict(form_values.items()))) 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': if form_def.output != 'raw':
# Ignore everything if we're doing raw output, since it's the # Ignore everything if we're doing raw output, since it's the
# scripts responsibility. # scripts responsibility.
if result['exitcode'] != 0: 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: else:
if form_def.output == 'escaped': 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: else:
# Non-escaped output (html, usually) # Non-escaped output (html, usually)
msg = result['stdout'].decode('utf8') msg = result['stdout'].decode('utf8')
output = html_submit_response.format( output = HTML_SUBMIT_RESPONSE.format(
header=html_header.format(title=form_config.title, header=HTML_HEADER.format(
custom_css=form_config.custom_css), title=form_config.title,
footer=html_footer, custom_css=form_config.custom_css
),
footer=HTML_FOOTER,
title=form_def.title, title=form_def.title,
form_name=form_def.name, form_name=form_def.name,
msg=msg, msg=msg,
@ -512,7 +564,7 @@ class ScriptFormWebApp(WebAppHandler):
if not os.path.exists(path): if not os.path.exists(path):
raise HTTPError(404, "Not found") raise HTTPError(404, "Not found")
f = file(path, 'r') static_file = file(path, 'r')
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
self.wfile.write(f.read()) self.wfile.write(static_file.read())

Loading…
Cancel
Save