Initial commit.

This commit is contained in:
Martin Fitzpatrick 2015-03-08 21:06:56 +01:00
parent e6eb2f013a
commit 26f8a9668a
71 changed files with 3687 additions and 52 deletions

3
.bowerrc Normal file
View File

@ -0,0 +1,3 @@
{
"directory": "wooey/static/libs"
}

67
.gitignore vendored
View File

@ -1,54 +1,51 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
# Packages
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
*.egg-info
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
.tox
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Sphinx documentation
docs/_build/
# Complexity
output/*.html
output/*/index.html
# PyBuilder
target/
# Sphinx
docs/_build
.webassets-cache
# Virtualenvs
env
env*
.idea
dev.db

11
.travis.yml Normal file
View File

@ -0,0 +1,11 @@
# Config file for automatic testing at travis-ci.org
language: python
python:
- "3.3"
- "2.7"
install: pip install -r requirements/dev.txt
script: py.test tests

24
LICENSE
View File

@ -1,22 +1,12 @@
The MIT License (MIT)
Copyright (c) 2014, Martin Fitzpatrick
All rights reserved.
Copyright (c) 2015 Martin Fitzpatrick
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
* Neither the name of Wooey nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: gunicorn wooey.app:create_app\(\) -b 0.0.0.0:$PORT -w 3

106
README.md Normal file
View File

@ -0,0 +1,106 @@
Wooey
=====
Automated web UIs for Python scripts
## About
Wooey is a simple web interface (built on Flask) to run command line Python scripts. Think of it as an easy solution
to routine data analysis, file processing, or anything else. Get your scripts up on the web.
![Welcome](welcome_to_wooey
![mock_argparse example script](mock_argparse_example
![plot_some_numbers script with docs](plot_some_numbers_with_documentation
![User job listing](user_job_list
![Job with success 1](job_success_1.png)
![Job with success 2](job_success_2
![Job with error console](job_with_error
## Quickstart
First, set your app's secret key as an environment variable. For example, example add the following to ``.bashrc`` or ``.bash_profile``.
export WOOEY_SECRET='something-really-secret'
Then run the following commands to bootstrap your environment.
git clone https://github.com/mfitzp/wooey
cd wooey
pip install -r requirements/dev.txt
python manage.py server
You will see a pretty welcome screen.
Once you have installed your DBMS, run the following to create your app's database tables and perform the initial migration:
python manage.py db init
python manage.py db migrate
python manage.py db upgrade
python manage.py server
The server will start up. But you will see it is empty! To add the example scripts to the database and allow you to test
also run:
python manage.py build_scripts
python manage.py find_scripts
This will build (create JSON for Python scripts using argparse) and then add them to the database.
In another shell, you can start the temporary dev 'daemon' (which is nothing of the sort, yet) using:
python manage.py start_daemon
Deployment
----------
In your production environment, make sure the ``WOOEY_ENV`` environment variable is set to ``"prod"``.
Shell
-----
To open the interactive shell, run ::
python manage.py shell
By default, you will have access to ``app``, ``db``, and the ``User`` model. This can be used to quickly recreate database tables
during development, i.e. delete `dev.db` (SQLite) and then from the shell enter:
db.create_all()
Running Tests
-------------
To run all tests, run ::
python manage.py test
Migrations
----------
Whenever a database migration needs to be made. Run the following commmands:
::
python manage.py db migrate
This will generate a new migration script. Then run:
::
python manage.py db upgrade
To apply the migration.
For a full migration command reference, run ``python manage.py db --help``.

5
bower.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "wooey",
"dependencies": {
}
}

File diff suppressed because one or more lines are too long

209
manage.py Normal file
View File

@ -0,0 +1,209 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import subprocess
import json
import time
from flask.ext.script import Manager, Shell, Server
from flask.ext.migrate import MigrateCommand
from wooey.app import create_app
from wooey.user.models import User
from wooey.settings import DevConfig, ProdConfig
from wooey.database import db
from wooey.lib.utils import find_files
from wooey.lib.python import collect_argparses
from wooey.public.models import Script, Job, STATUS_WAITING, STATUS_COMPLETE, STATUS_ERROR, STATUS_RUNNING
import select
if os.environ.get("WOOEY_ENV") == 'prod':
app = create_app(ProdConfig)
else:
app = create_app(DevConfig)
HERE = os.path.abspath(os.path.dirname(__file__))
TEST_PATH = os.path.join(HERE, 'tests')
manager = Manager(app)
def _make_context():
"""Return context dict for a shell session so you can access
app, db, and the User model by default.
"""
return {'app': app, 'db': db, 'User': User}
@manager.command
def test():
"""Run the tests."""
import pytest
exit_code = pytest.main([TEST_PATH, '--verbose'])
return exit_code
manager.add_command('server', Server())
manager.add_command('shell', Shell(make_context=_make_context))
manager.add_command('db', MigrateCommand)
@manager.command
def build_scripts():
'''
Build script JSON data for UIs (default), don't overwrite
:return:
'''
scripts = find_files(os.path.join('.', 'scripts'), '.py')
collect_argparses(scripts)
@manager.command
def find_scripts():
'''
Loader that iterates over script config folder, reading JSON files and updating the
admin config to store. The config itself is not loaded (parsed and managed on output).
:return:
'''
jsons = find_files(os.path.join('.', 'scripts'), '.json')
for json_filename in jsons:
# Extract to dict structure, then interrogate the database to see if we already have this and update
with open(json_filename, 'r') as f:
jo = json.load(f)
full_path = os.path.realpath(json_filename)
# Try query the db for object with the same (?hash)
script = Script.query.filter_by(config_path=full_path).first() # Will be only one
if not script:
script = Script(config_path=full_path) # Create it
# Amend the object
script.exec_path = jo['program']['path']
script.description = jo['program']['description']
script.name = jo['program']['name']
if 'documentation' in jo['program']:
script.doc_path = jo['program']['documentation']
elif os.path.exists(os.path.splitext(script.exec_path)[0] + '.md'):
script.doc_path = os.path.splitext(script.exec_path)[0] + '.md'
elif os.path.exists(os.path.splitext(script.config_path)[0] + '.md'):
script.doc_path = os.path.splitext(script.config_path)[0] + '.md'
db.session.add(script)
db.session.commit()
def read_all_so_far(proc, out=''):
while (select.select([proc.stdout], [], [], 0)[0] != []):
out += proc.stdout.read(1)
return out
@manager.command
def start_daemon():
'''
Infinitely loop, checking the database for waiting Jobs and executing them.
This is nasty.
Keep handles to subprocesses so we can kill them if receiving signal (via database) from user/admin.
:return:
'''
# Track the Popen objects for each, dict to map via Job PK
pids = {}
# Initialise by setting all running jobs to error (must have died on previous execution)
Job.query.filter(Job.status == STATUS_RUNNING).update({Job.status: STATUS_ERROR})
db.session.commit()
while True:
# Query the database for running jobs
jobs_running = Job.query.filter(Job.status == STATUS_RUNNING)
no_of_running_jobs = jobs_running.count()
for job in jobs_running:
print(job)
# Check each vs. running subprocesses to see if still active (if not, update database with new status)
for k, v in pids.items():
# Check process status and flush to file
v['pid'].poll()
if v['pid'].returncode is None: # Still running
pass
else:
try:
v['out'].close()
except IOError:
pass
if v['pid'].returncode == 0: # Complete
job = Job.query.get(k)
job.status = STATUS_COMPLETE
else: # Error
job = Job.query.get(k)
job.status = STATUS_ERROR
# Delete the process object
del pids[k]
db.session.commit()
# If number of running jobs < MAX_RUNNING_JOBS start some more
if no_of_running_jobs < 5:
jobs_to_run = Job.query.filter(Job.status == STATUS_WAITING).order_by(Job.created_at.desc())
for job in jobs_to_run:
# Get the config settings from the database (dict of values via JSON)
if job.config:
config = json.loads(job.config)
else:
config = {}
# Get args from the config dict (stored in job)
args = config['args']
# Add the executable to the beginning of the sequence
args.insert(0, job.script.exec_path)
print(' '.join(args))
out = open(os.path.join(job.path, 'STDOUT'), 'w')
# Run the command and store the object for future use
pids[job.id] = {
'pid': subprocess.Popen(args, cwd=job.path, stdout=out, stderr=subprocess.STDOUT),
'out': out,
}
# Update the job status
job.status = STATUS_RUNNING
db.session.commit()
print(pids)
time.sleep(5)
if __name__ == '__main__':
manager.run()

1
migrations/README Executable file
View File

@ -0,0 +1 @@
Generic single-database configuration.

45
migrations/alembic.ini Normal file
View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

22
migrations/script.py.mako Executable file
View File

@ -0,0 +1,22 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,80 @@
"""empty message
Revision ID: 280825fe3a4f
Revises: None
Create Date: 2015-03-08 20:53:26.958201
"""
# revision identifiers, used by Alembic.
revision = '280825fe3a4f'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('scripts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=False),
sa.Column('display_name', sa.String(length=80), nullable=True),
sa.Column('exec_path', sa.String(length=255), nullable=False),
sa.Column('config_path', sa.String(length=255), nullable=False),
sa.Column('doc_path', sa.String(length=255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('config_path'),
sa.UniqueConstraint('doc_path'),
sa.UniqueConstraint('exec_path')
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=80), nullable=False),
sa.Column('email', sa.String(length=80), nullable=False),
sa.Column('password', sa.String(length=128), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('first_name', sa.String(length=30), nullable=True),
sa.Column('last_name', sa.String(length=30), nullable=True),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('is_admin', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('username')
)
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('jobs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('script_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('path', sa.String(length=255), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('status', sa.Enum('W', 'R', 'C', 'X'), nullable=False),
sa.Column('config', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['script_id'], ['scripts.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('path')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('jobs')
op.drop_table('roles')
op.drop_table('users')
op.drop_table('scripts')
### end Alembic commands ###

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
# Included because many Paas's require a requirements.txt file in the project root
# Just installs the production requirements.
-r requirements/prod.txt

10
requirements/dev.txt Normal file
View File

@ -0,0 +1,10 @@
# Everything the developer needs in addition to the production requirements
-r prod.txt
# Testing
pytest>=2.6.3
webtest
factory-boy==2.4.1
# Management script
Flask-Script

40
requirements/prod.txt Normal file
View File

@ -0,0 +1,40 @@
# Everything needed in production
# Flask
Flask==0.10.1
MarkupSafe==0.23
Werkzeug==0.9.6
Jinja2==2.7.3
itsdangerous==0.24
# Database
Flask-SQLAlchemy==2.0
SQLAlchemy==0.9.8
# Migrations
Flask-Migrate==1.3.0
# Forms
Flask-WTF==0.10.3
WTForms==2.0.1
# Deployment
gunicorn>=19.1.1
# Assets
Flask-Assets==0.10
cssmin>=0.2.0
jsmin>=2.0.11
# Auth
Flask-Login==0.2.11
Flask-Bcrypt==0.6.0
# Caching
Flask-Cache>=0.13.1
# Debug toolbar
Flask-DebugToolbar==0.9.1
# Admin
Flask-Admin=1.1.0

263
scripts/bar.json Normal file
View File

@ -0,0 +1,263 @@
{
"program": {
"path": "/Users/mxf793/repos/wooey/scripts/bar.py",
"epilog": null,
"name": "bar",
"description": null
},
"parser": {
"prefix_chars": "-",
"argument_default": null
},
"required": [
{
"data": {
"commands": [
"-f",
"--file"
],
"display_name": "file",
"help": "csv file containing dumped data from plsmulti",
"default": null,
"nargs": "",
"choices": []
},
"widget": "FileChooser",
"type": "FileType"
}
],
"optional": [
{
"data": {
"commands": [
"--statistic"
],
"display_name": "statistic",
"help": "Statistical test: tind (t-independent), trel (t-related)",
"default": null,
"nargs": "",
"choices": [
"tind",
"trel"
]
},
"widget": "Dropdown",
"type": "NoneType"
},
{
"data": {
"commands": [
"-m",
"--metabolite"
],
"display_name": "metabolite",
"help": "name of metabolite to plot graph for",
"default": "",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-g",
"--group"
],
"display_name": "axisgroup",
"help": "Regular Expression pattern for axis cluster",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-s",
"--search"
],
"display_name": "search",
"help": "Show classes matching this regex",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-t"
],
"display_name": "tests",
"help": "CLASS,CLASS combinations to statisically test (space-separated)",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--xlabel"
],
"display_name": "xlabel",
"help": "X axis label",
"default": "",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--ylabel"
],
"display_name": "ylabel",
"help": "Y axis label",
"default": "",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--ylim"
],
"display_name": "ylim",
"help": "min,max for y axis",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "float"
},
{
"data": {
"commands": [
"--title"
],
"display_name": "title",
"help": "Graph title",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--dpi"
],
"display_name": "dpi",
"help": "DPI of output (TIF format only)",
"default": 72,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "int"
},
{
"data": {
"commands": [
"--format"
],
"display_name": "format",
"help": "File format for output",
"default": "png",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-b",
"--batch"
],
"display_name": "batch_mode",
"help": "Batch mode (process all metabolite matches with same parameters)",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"--multiplot"
],
"display_name": "multiplot",
"help": "Plot more than one graph per figure",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"--shareyaxis"
],
"display_name": "shareyaxis",
"help": "Share y axis scale",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"-d",
"--display"
],
"display_name": "display",
"help": "Display graphs to screen",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"--annotate"
],
"display_name": "annotate",
"help": "show command annotation for generation",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
}
]
}

271
scripts/bar.py Executable file
View File

@ -0,0 +1,271 @@
#!/usr/bin/env python
from numpy.random import uniform, seed
from collections import OrderedDict
from matplotlib.mlab import griddata
from matplotlib import ticker
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import re
import os
import random
import csv
import argparse
from collections import defaultdict
import utils
parser = argparse.ArgumentParser()
parser.add_argument("-m", "--metabolite", dest="metabolite", default='',
help="name of metabolite to plot graph for")
parser.add_argument("-f", "--file", dest="file", type=argparse.FileType('rU'), required=True,
help="csv file containing dumped data from plsmulti", metavar="FILE")
parser.add_argument("-b", "--batch", action="store_true", dest="batch_mode", default=False,
help="Batch mode (process all metabolite matches with same parameters)")
parser.add_argument("-g", "--group", dest="axisgroup", default=None,
help="Regular Expression pattern for axis cluster")
parser.add_argument("--multiplot", action="store_true", dest="multiplot", default=False,
help="Plot more than one graph per figure")
parser.add_argument("--shareyaxis", action="store_true", dest="shareyaxis", default=False,
help="Share y axis scale")
parser.add_argument("-s", "--search", dest="search", default=None,
help="Show classes matching this regex")
parser.add_argument("-t", dest="tests", default=None,
help="CLASS,CLASS combinations to statisically test (space-separated)")
parser.add_argument("-d", "--display", action="store_true", dest="display", default=False,
help="Display graphs to screen")
parser.add_argument("--statistic", dest="statistic", default=None, choices=['tind', 'trel'],
help="Statistical test: tind (t-independent), trel (t-related)")
parser.add_argument("--xlabel", dest="xlabel", default='',
help="X axis label")
parser.add_argument("--ylabel", dest="ylabel", default='',
help="Y axis label")
parser.add_argument("--ylim", dest="ylim", default=None, type=float,
help="min,max for y axis")
parser.add_argument("--title", dest="title", default=None,
help="Graph title")
parser.add_argument("--dpi", dest="dpi", default=72, type=int,
help="DPI of output (TIF format only)")
parser.add_argument("--format", dest="format", default='png',
help="File format for output")
parser.add_argument("--annotate", action="store_true", dest="annotate", default=False,
help="show command annotation for generation")
args = parser.parse_args()
colors = ['#348ABD', '#7A68A6', '#A60628', '#467821', '#CF4457', '#188487', '#E24A33', 'r', 'b', 'g', 'c', 'm', 'y', 'k', 'w']
# Extract file root from the edge file name
filebase = os.path.splitext(args.file.name)[0]
[null, sep, string] = filebase.partition('-')
filesuff = sep + string
nodes = OrderedDict()
(metabolites, allquants, globalylim) = utils.read_metabolite_datafile(args.file, args)
# Turn off interactive plotting, speed up
plt.ioff()
figures = list()
multiax = None
ymaxf = 0
for metabolite in metabolites[:]:
print "Processing %s" % metabolite
quants = allquants[metabolite]
if args.search:
okeys = quants.keys()
for label in quants.keys():
match = re.search(args.search, label)
if not match:
del quants[label]
print "Filter matching classes '%s' with '%s' gives '%s'" % (', '.join(okeys), args.search, ', '.join(quants.keys()))
if len(quants.keys()) == 0:
print "Nothing left!; deleting metabolite"
metabolites.remove(metabolite)
continue
# Apply regex to split axis groups if specified
if args.axisgroup:
axisgroups = defaultdict(list)
for label in quants.keys():
match = re.search(args.axisgroup, label)
if match:
axisgroups[match.group(1)].append(label)
#else:
# axisgroups['non-matched'].append(label)
if len(axisgroups) == 0:
print "No matching classes found for axisgroup regex, try again"
exit()
print "Axis groupings completed: " + ", ".join(axisgroups)
else:
# No groups, create dummy for 'all'
axisgroups = {'not-grouped': quants.keys()}
ind = list()
graph = {
'ticks': list(),
'means': list(),
'stddev': list(),
'colors': list(),
'ylim': (0, 0),
}
# Sort the axis groups so follow some sort of logical order
axisgroupsk = axisgroups.keys()
axisgroupsk.sort()
ymin = 0
ymax = 0
for counter, axisgroup in enumerate(axisgroupsk):
l = axisgroups[axisgroup]
l.sort()
graph['ticks'] = graph['ticks'] + l
for key in l:
graph['means'].append(np.mean(quants[key]))
graph['stddev'].append(np.std(quants[key]))
ymax = max(ymax, max(quants[key]))
ymin = min(ymin, min(quants[key]))
ind = ind + list(np.arange(1 + counter + len(ind), 1 + counter + len(ind) + len(l)))
graph['colors'] = graph['colors'] + colors[0: len(l)]
num = len(ind)
width = 0.8 # the width of the bars: can also be len(x) sequence
if args.shareyaxis:
graph['ylim'] = globalylim
else:
graph['ylim'] = (ymin, ymax)
# Split error pos+neg to give single up/down errorbar in correct direction
yperr = [(1, 0)[x > 0] for x in graph['means']]
ynerr = [(1, 0)[x < 0] for x in graph['means']]
yperr = np.array(list(yperr)) * np.array(list(graph['stddev']))
ynerr = np.array(list(ynerr)) * np.array(list(graph['stddev']))
if args.multiplot:
# Keep using same figure, append subplots
if not multiax:
#adjustprops = dict(left=0.1, bottom=0.1, right=0.97, top=0.93, wspace=0.2, hspace=0.2)
fig = plt.figure()
multiax = fig.add_subplot(1, len(metabolites), 1)
figures.append(multiax)
else:
if not args.shareyaxis:
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1, sharey=multiax)
plt.setp(fp.get_yticklabels(), visible=False)
plt.setp(fp.get_yaxis(), visible=False)
else:
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1)
figures.append(fp)
else:
# New figure
figures.append(plt.figure())
plt.bar(ind, graph['means'], width, label=graph['ticks'], color=graph['colors'], ecolor='black', align='center', yerr=[yperr, ynerr])
plt.xticks(ind, graph['ticks']) #, rotation=45)
if args.title:
plt.title(args.title)
else:
plt.title(metabolite)
plt.gca().xaxis.set_label_text(args.xlabel)
plt.gca().yaxis.set_label_text(args.ylabel)
# Add some padding either side of graphs
plt.xlim(ind[0] - 1, ind[-1] + 1)
if args.annotate:
utils.annotate_plot(plt, options)
fig = plt.gcf()
# Horizontal axis through zero
plt.axhline(0, color='k')
if args.multiplot: # Scale the multiplots a bit more reasonably
fig.set_size_inches(5 + len(metabolites) * 3, 6)
else:
fig.set_size_inches(8, 6)
# Get ylimits for significance bars
ymin, ymax = graph['ylim']
sigs = list()
if args.tests:
from scipy import stats
tests = args.tests.split()
for test in tests:
classt = test.split(',')
if args.statistic == 'trel':
t, p = stats.ttest_rel(quants[classt[0]], quants[classt[1]])
else:
t, p = stats.ttest_ind(quants[classt[0]], quants[classt[1]])
# sigs.append( { a:classt[0], b:classt[1], p:p } )
# We now lave a list of significant comparisons in significant
bxstart = ind[graph['ticks'].index(classt[0])]
bxend = ind[graph['ticks'].index(classt[1])]
# Nudge up to prevent overlapping
ymax = ymax + ((ymax - ymin) * 0.1)
# Plot the bar on the graph
c = patches.FancyArrowPatch(
(bxstart, ymax),
(bxend, ymax),
arrowstyle="|-|", lw=2)
ax = plt.gca()
plt.text(bxstart + (bxend - float(bxstart)) / 2, ymax + (ymax * 0.01), utils.sigstars(p), size=16, ha='center', va='bottom')
ax.add_patch(c)
print "Stats (%s): %s vs %s; p=%s" % (args.statistic, classt[0], classt[1], p)
# Final nudge up over last bar
ymax = ymax + ((ymax - ymin) * 0.1)
# Store final limit
ymaxf = max(ymaxf, ymax)
if args.ylim:
ylim = args.ylim.split(',')
plt.ylim(int(ylim[0]), int(ylim[1]))
if not args.multiplot:
# Adjust plot on multiplot
plt.ylim(ymin, ymaxf)
ymaxf = 0
print "Save as 'bar%s-%s.%s'" % (filesuff, metabolite, args.format)
plt.savefig('bar%s-%s.%s' % (filesuff, metabolite, args.format), dpi=args.dpi, transparent=False)
if args.multiplot:
# Adjust plot on multiplot
plt.ylim(ymin, ymaxf)
print "Save as 'bar%s-%s.%s'" % (filesuff, '-'.join(metabolites), args.format)
plt.savefig('bar%s-%s.%s' % (filesuff, '-'.join(metabolites), args.format), dpi=args.dpi, transparent=False)
if args.display:
plt.show()
plt.close()

304
scripts/longitudinal.json Normal file
View File

@ -0,0 +1,304 @@
{
"program": {
"path": "/Users/mxf793/repos/wooey/scripts/longitudinal.py",
"epilog": null,
"name": "longitudinal",
"description": null
},
"parser": {
"prefix_chars": "-",
"argument_default": null
},
"required": [
{
"data": {
"commands": [
"-f",
"--file"
],
"display_name": "file",
"help": "csv file containing dumped data from plsmulti",
"default": null,
"nargs": "",
"choices": []
},
"widget": "FileChooser",
"type": "FileType"
}
],
"optional": [
{
"data": {
"commands": [
"-m",
"--metabolite"
],
"display_name": "metabolite",
"help": "name of metabolite to plot graph for",
"default": "",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-l",
"--longitude"
],
"display_name": "longitudinal",
"help": "regex pattern for longitudinal data: grouped area is x axis",
"default": "(\\d+)",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-c",
"--control"
],
"display_name": "control",
"help": "regex pattern for control data",
"default": false,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-s",
"--search"
],
"display_name": "search",
"help": "only show classes matching this regex",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--styles"
],
"display_name": "styles",
"help": "style pattern to use to assign colors/markers/lines to lines; (3),(regex),(groups)",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--xlabel"
],
"display_name": "xlabel",
"help": "x axis label for graph",
"default": "",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--ylabel"
],
"display_name": "ylabel",
"help": "y axis label for graph",
"default": "",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--xax"
],
"display_name": "xax",
"help": "log/reg/(lin) x axis",
"default": false,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--yax"
],
"display_name": "yax",
"help": "log/reg/(lin) y axis",
"default": false,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--ylim"
],
"display_name": "ylim",
"help": "min,max for y axis",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-t",
"--title"
],
"display_name": "title",
"help": "title for graph",
"default": null,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--dpi"
],
"display_name": "dpi",
"help": "dpi for output",
"default": 72,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"--format"
],
"display_name": "format",
"help": "fileformat for output",
"default": "png",
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "NoneType"
},
{
"data": {
"commands": [
"-b",
"--batch"
],
"display_name": "batch_mode",
"help": "batch mode, process all metabolite matches with same parameters",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"--multiplot"
],
"display_name": "multiplot",
"help": "plot more than one graph on a figure",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"--shareyaxis"
],
"display_name": "shareyaxis",
"help": "whether to share y axis scale (and show on multiplots)",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"-d",
"--display"
],
"display_name": "display",
"help": "display resulting graphs to screen",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"--smooth"
],
"display_name": "smooth",
"help": "smooth graph through cubic interpolation",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
},
{
"data": {
"commands": [
"--annotate"
],
"display_name": "annotate",
"help": "show command annotation for generation",
"default": false,
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
}
]
}

385
scripts/longitudinal.py Executable file
View File

@ -0,0 +1,385 @@
#!/usr/bin/env python
from numpy.random import uniform, seed
from collections import OrderedDict
from matplotlib.mlab import griddata
from matplotlib import ticker
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import re
import os
import random
import csv
import argparse
from collections import defaultdict
import utils
parser = argparse.ArgumentParser()
parser.add_argument("-m", "--metabolite", dest="metabolite", default='',
help="name of metabolite to plot graph for")
parser.add_argument("-f", "--file", dest="file", type=argparse.FileType('rU'), required=True,
help="csv file containing dumped data from plsmulti", metavar="FILE")
parser.add_argument("-b", "--batch", action="store_true", dest="batch_mode", default=False,
help="batch mode, process all metabolite matches with same parameters")
parser.add_argument("-l", "--longitude", dest="longitudinal", default='(\d+)',
help="regex pattern for longitudinal data: grouped area is x axis")
parser.add_argument("-c", "--control", dest="control", default=False,
help="regex pattern for control data")
parser.add_argument("--multiplot", action="store_true", dest="multiplot", default=False,
help="plot more than one graph on a figure")
parser.add_argument("--shareyaxis", action="store_true", dest="shareyaxis", default=False,
help="whether to share y axis scale (and show on multiplots)")
parser.add_argument("-s", "--search", dest="search", default=None,
help="only show classes matching this regex")
parser.add_argument("--styles", dest="styles", default=None,
help="style pattern to use to assign colors/markers/lines to lines; (3),(regex),(groups)")
parser.add_argument("-d", "--display", action="store_true", dest="display", default=False,
help="display resulting graphs to screen")
parser.add_argument("--xlabel", dest="xlabel", default='',
help="x axis label for graph")
parser.add_argument("--ylabel", dest="ylabel", default='',
help="y axis label for graph")
parser.add_argument("--xax", dest="xax", default=False,
help="log/reg/(lin) x axis")
parser.add_argument("--yax", dest="yax", default=False,
help="log/reg/(lin) y axis")
parser.add_argument("--smooth", dest="smooth", action="store_true", default=False,
help="smooth graph through cubic interpolation")
parser.add_argument("--ylim", dest="ylim", default=None,
help="min,max for y axis")
parser.add_argument("-t", "--title", dest="title", default=None,
help="title for graph")
parser.add_argument("--dpi", dest="dpi", default=72,
help="dpi for output")
parser.add_argument("--format", dest="format", default='png',
help="fileformat for output")
parser.add_argument("--annotate", action="store_true", dest="annotate", default=False,
help="show command annotation for generation")
args = parser.parse_args()
colors = ['r', 'b', 'g', 'c', 'm', 'y', 'k']
markers = ['o', 's', 'v', '^', 'D', '+', 'x']
linestyles = ['solid', 'dashed', 'dashdot'] # Do not include dotted, used for controls
if args.styles is None:
# Build a full table we'll just iterate over
styles = [(z, y, x) for x in linestyles for y in markers for z in colors]
# Extract file root from the edge file name
filebase = os.path.splitext(args.file.name)[0]
[null, sep, string] = filebase.partition('-')
filesuff = sep + string
nodes = OrderedDict()
(metabolites, allquants, globalylim) = utils.read_metabolite_datafile(args.file, args)
# Turn off interactive plotting, speed up
plt.ioff()
figures = list()
multiax = None
ymaxf = 0
for metabolite in metabolites[:]:
print "Processing %s" % metabolite
quants = allquants[metabolite]
if args.search:
okeys = quants.keys()
for label in quants.keys():
match = re.search(args.search, label)
if not match:
del quants[label]
print "Filter matching classes '%s' with '%s' gives '%s'" % (', '.join(okeys), args.search, ', '.join(quants.keys()))
if len(quants.keys()) == 0:
print "Nothing left!; deleting metabolite"
metabolites.remove(metabolite)
continue
# Apply regex to class string for each variable;
# the *.(\d+)*. longitudinal bit extracts that
# use remainder for class assignment *at that timepoint*
# Possible to use the same structure?
timepoints = set()
classes = set()
classtrans = defaultdict(list)
non_decimal = re.compile(r'[^\d.]+')
for label in quants.keys():
match = re.search(args.longitudinal, label)
if match:
# Rewrite the class label to not include the match content
classlabel = label.replace(match.group(1), '', 1)
timepoint = float(non_decimal.sub('', match.group(1)))
timepoints.add(timepoint) # Build timepoint list
classes.add(classlabel) # Build class list
classtrans[(classlabel, timepoint)] = label # Store translation for lookup
if len(timepoints) == 0:
print "No matching classes found for longitude regex, try again"
exit()
ind = list()
graphs = dict()
classes = list(classes)
# Remove axis timepoint duplicates and sort
timepoints = list(timepoints)
timepoints.sort(key=float)
print "Axis longitude completed: " + ", ".join(str(x) for x in timepoints)
print "Re-classification: " + ", ".join(classes)
# If set run styles setting against the class list and build a lookup table of variants
# with assigned colours and styles
if args.styles:
classstyles = dict()
classmatch = defaultdict(list)
stylesets = list()
for n, styleret in enumerate(args.styles.split(',')):
stylere = re.compile('(%s)' % styleret)
stylesets.append(set())
# Build table, then assign
for classl in classes:
match = stylere.search(classl)
if match:
stylesets[n].add(match.group(1))
classmatch[classl].append(match.group(1))
else:
classmatch[classl].append(None)
# Sort the stylesets
for n, ss in enumerate(stylesets):
stylesets[n] = sorted(ss, reverse=True)
# Now have 3 sets of styles, assign
for (classl, classm) in classmatch.items():
classstyles[classl] = (
colors[list(stylesets[0]).index(classm[0])],
markers[list(stylesets[1]).index(classm[1])],
linestyles[list(stylesets[2]).index(classm[2])],
)
# Get common substring from classes, for improved naming of saved graph file
common_classname = ''
# Exclude classes matching control regexp
# ...
classnc = [classl for classl in classes if not re.search(".*%s.*" % args.control, classl)]
if len(classnc) > 1 and len(classnc[0]) > 0:
for i in range(len(classnc[0])):
for j in range(len(classnc[0]) - i + 1):
if j > len(common_classname) and all(classnc[0][i:i + j] in x for x in classnc):
common_classname = classnc[0][i:i + j]
common_classname = common_classname.strip("-")
# Output is a dict of lists, containing values matching each point on the X axis (or None if not existing)
ymin = 0
ymax = 0
controls = 0
datasets = defaultdict(list)
for n, classl in enumerate(classes):
graph = {
'timepoints': list(),
'means': list(),
'stddev': list(),
'samples': list(),
'color': 'k',
'control': False,
}
if args.control:
match = re.search(".*%s.*" % args.control, classl)
if match: # This is a control sample, mark as such
graph['control'] = True
controls += 1
for t in timepoints:
if (classl, t) in classtrans:
key = classtrans[(classl, t)] # Get original classname for this timepoint/class combo
graph['timepoints'].append(float(t))
graph['means'].append(np.mean(quants[key]))
graph['stddev'].append(np.std(quants[key]))
graph['samples'].append(len(quants[key]))
ymax = max(ymax, max(quants[key]))
ymin = min(ymin, min(quants[key]))
if args.styles:
graph['style'] = classstyles[classl]
else:
graph['style'] = styles[n]
# Store completed graph
graphs[classl] = graph
if args.shareyaxis:
ylim = globalylim
else:
ylim = (ymin, ymax)
if args.multiplot:
# Keep using same figure, append subplots
if not multiax:
#adjustprops = dict(left=0.1, bottom=0.1, right=0.97, top=0.93, wspace=0.2, hspace=0.2)
fig = plt.figure()
multiax = fig.add_subplot(1, len(metabolites), 1)
figures.append(multiax)
else:
if args.shareyaxis:
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1, sharey=multiax)
plt.setp(fp.get_yticklabels(), visible=False)
plt.setp(fp.get_yaxis(), visible=False)
else:
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1)
figures.append(fp)
else:
# New figure
figures.append(plt.figure())
if args.xax:
xticks = range(0, len(graph['timepoints']))
else:
xticks = graph['timepoints']
for classl in classes:
graph = graphs[classl]
if args.smooth and len(xticks) >= 3:
l = plt.errorbar(xticks, graph['means'], yerr=graph['stddev'], label=classl, fmt=graph['style'][0], marker=graph['style'][1], linestyle=graph['style'][2])
# Calculate spline and plot that over
# Duplicate data to additional timepoints
# Convert data to linear timecourse first (doh!)
ynew = list()
for n in range(len(graph['means'][:-1])):
t1 = xticks[n]
t2 = xticks[n + 1]
m1 = graph['means'][n]
m2 = graph['means'][n + 1]
dm = int(t2 - t1)
for tp in range(dm):
av = ((m1 * (dm - tp) / dm) + (m2 * tp) / dm)
ynew.append(av)
window_len = 11
xnew = np.linspace(min(xticks), max(xticks), len(ynew))
ynew = np.array(ynew)
# Iteratively smooth with a small window to minimise distortion
# smooth the xaxis alongside to prevent x-distortion
for x in range(20):
ynew = utils.smooth(ynew, window_len=window_len, window='blackman')
xnew = utils.smooth(xnew, window_len=window_len, window='blackman')
plt.plot(xnew, ynew, color=graph['color'])
else:
if graph['control']:
plt.fill_between(xticks, [a + b for a, b in zip(graph['means'], graph['stddev'])], y2=[a - b for a, b in zip(graph['means'], graph['stddev'])], alpha=0.05, color=graph['style'][0])
plt.plot(xticks, graph['means'], color=graph['style'][0], linestyle=graph['style'][2], label=classl, alpha=0.2)
else:
plt.errorbar(xticks, graph['means'], yerr=graph['stddev'], label=classl, fmt=graph['style'][0], marker=graph['style'][1], linestyle=graph['style'][2])
if args.control:
pass
#for classl in baselines:
# p = mpatches.Rectangle(xy, width, height, facecolor="orange", edgecolor="red")
# plt.gca().add_patch(p)
# plt.draw()
ax = plt.gca()
handles, labels = ax.get_legend_handles_labels()
hl = sorted(zip(labels, handles))
labels = [hi[0] for hi in hl]
handles = [hi[1] for hi in hl]
ax.legend(handles, labels)
# Optional log x and y axes
if args.xax == 'log':
ax.set_xscale('log')
if args.yax == 'log':
ax.set_yscale('log')
#plt.xticks(timepoints, timepoints, rotation=45)
if args.title:
plt.title("%s (%s)" % (args.title, metabolite))
else:
plt.title(metabolite)
plt.gca().set_xticklabels(graph['timepoints'], minor=False, rotation=45)
plt.gca().xaxis.set_label_text(args.xlabel)
plt.gca().yaxis.set_label_text(args.ylabel)
# Add some padding either side of graphs
#plt.xlim( ind[0]-1, ind[-1]+1)
if args.annotate:
utils.annotate_plot(plt, args)
fig = plt.gcf()
# Horizontal axis through zero
#plt.axhline(0, color='k')
if args.multiplot: # Scale the multiplots a bit more reasonably
fig.set_size_inches(5 + len(metabolites) * 3, 6)
else:
fig.set_size_inches(8, 6)
# Get ylimits for significance bars
if args.ylim:
ylim = args.ylim.split(',')
plt.ylim(int(ylim[0]), int(ylim[1]))
if not args.multiplot:
# Adjust plot on multiplot
#plt.ylim( ymin, ymaxf)
ymaxf = 0
print "Save as 'long%s-%s-%s.%s'" % (filesuff, metabolite, common_classname, args.format)
plt.savefig('long%s-%s-%s.%s' % (filesuff, metabolite, common_classname, args.format), dpi=args.dpi, transparent=False)
if args.multiplot:
# Adjust plot on multiplot
plt.ylim(ymin, ymaxf)
print "Save as 'long%s-%s-%s.%s'" % (filesuff, '-'.join(metabolites), common_classname, args.format)
plt.savefig('long%s-%s-%s.%s' % (filesuff, '-'.join(metabolites), common_classname, args.format), dpi=args.dpi, transparent=False)
if args.display:
plt.show()
plt.close()

View File

@ -0,0 +1,56 @@
{
"program": {
"path": "/Users/mxf793/repos/wooey/scripts/mock_argparse_example.py",
"epilog": null,
"name": "mock_argparse_example",
"description": "Process some integers."
},
"parser": {
"prefix_chars": "-",
"argument_default": null
},
"required": [
{
"data": {
"commands": [],
"display_name": "integers",
"help": "an integer for the accumulator",
"default": null,
"nargs": "+",
"choices": []
},
"widget": "TextField",
"type": "int"
}
],
"optional": [
{
"data": {
"commands": [
"--womp"
],
"display_name": "womp",
"help": "subtract value",
"default": 0,
"nargs": "",
"choices": []
},
"widget": "TextField",
"type": "int"
},
{
"data": {
"commands": [
"--sum"
],
"display_name": "accumulate",
"help": "sum the integers (default: find the max)",
"default": "max",
"nargs": "",
"choices": []
},
"widget": "CheckBox",
"type": "NoneType"
}
]
}

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
import argparse
def main():
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')
parser.add_argument('--womp', dest='womp', action='store',
type=int, default=0,
help='subtract value')
args = parser.parse_args()
print(args.accumulate(args.integers) - args.womp)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,27 @@
{
"program": {
"path": "/Users/mxf793/repos/wooey/scripts/plot_some_numbers.py",
"epilog": null,
"name": "plot_some_numbers",
"description": "Plot some numbers."
},
"parser": {
"prefix_chars": "-",
"argument_default": null
},
"required": [
{
"data": {
"commands": [],
"display_name": "integers",
"help": "a space separated list of numbers to plot",
"default": null,
"nargs": "+",
"choices": []
},
"widget": "TextField",
"type": "int"
}
],
"optional": []
}

View File

@ -0,0 +1,30 @@
# Plot some numbers
This script allows you to plot some numbers.
This is some markdown-based documentation for the script. Source files
can be specified in the JSON markup or will be automatically loaded
from the script source or config folders.
A First Level Header
====================
A Second Level Header
---------------------
Now is the time for all good men to come to
the aid of their country. This is just a
regular paragraph.
The quick brown fox jumped over the lazy
dog's back.
### Header 3
> This is a blockquote.
>
> This is the second paragraph in the blockquote.
>
> ## This is an H2 in a blockquote

34
scripts/plot_some_numbers.py Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python
import argparse
import time
def main():
parser = argparse.ArgumentParser(description='Plot some numbers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='a space separated list of numbers to plot')
args = parser.parse_args()
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
l1 = args.integers
ax.plot(range(0, len(l1)), l1)
print("Plotting Figure 1\n%s" % l1)
fig.savefig('Figure 1.png')
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
l2 = [x - args.integers[n + 1] for n, x in enumerate(args.integers[:-1])]
ax.plot(range(0, len(l2)), l2)
print("Plotting Figure 2\n%s" % l2)
fig.savefig('Figure 2.png')
if __name__ == '__main__':
main()

109
scripts/utils.py Normal file
View File

@ -0,0 +1,109 @@
import re,os
import csv
from collections import defaultdict
import math
def sigstars(p):
# Return appropriate number of stars or ns for significance
if (p<=0.0001):
s = '****'
elif (p<=0.001):
s = '***'
elif (p<=0.01):
s = '**'
elif (p<=0.05):
s = '*'
else:
s = 'ns'
return s
def read_metabolite_datafile( fp, options ):
# Read in data for the graphing metabolite, with associated value (generate mean)
reader = csv.reader( fp, delimiter=',', dialect='excel')
# Find matching metabolite column
hrow = reader.next()
try:
metabolite_column = hrow.index( options.metabolite )
print "'%s' found" % (options.metabolite)
metabolites = [ options.metabolite ]
except:
all_metabolites = hrow[2:]
metabolites = filter(lambda x:re.match('(.*)' + options.metabolite + '(.*)', x), all_metabolites)
if len(metabolites) ==0:
print "Metabolite not found, try again. Pick from one of:"
print ', '.join( sorted(all_metabolites) )
exit()
elif len(metabolites) > 1:
print "Searched '%s' and found multiple matches:" % (options.metabolite)
print ', '.join( sorted(metabolites) )
if not options.batch_mode:
print "To process all the above together use batch mode -b"
exit()
elif len(metabolites) ==1:
print "Searched '%s' and found match in '%s'" % (options.metabolite, metabolites[0])
# Build quants table for metabolite classes
allquants = dict()
for metabolite in metabolites:
allquants[ metabolite ] = defaultdict(list)
ymin = 0
ymax = 0
for row in reader:
if row[1] != '.': # Skip excluded classes # row[1] = Class
for metabolite in metabolites:
metabolite_column = hrow.index( metabolite )
if row[ metabolite_column ]:
allquants[metabolite][ row[1] ].append( float(row[ metabolite_column ]) )
ymin = min( ymin, float(row[ metabolite_column ]) )
ymax = max( ymax, float(row[ metabolite_column ]) )
else:
allquants[metabolite][ row[1] ].append( 0 )
return ( metabolites, allquants, (ymin,ymax) )
def annotate_plot(plt, options):
annod = vars(options)
annol = ', \n'.join(["%s=%s" % (x, annod[x]) for x in annod.keys()])
bbox = dict( facecolor='#eeeeff', alpha=1, edgecolor='#000000',boxstyle="Square,pad=1")
plt.text(1.1, 1.1, annol,
backgroundcolor='#eeeeff',
fontsize='x-small',
ha='right',
va='top',
bbox=bbox,
transform = plt.gca().transAxes)
import numpy
def smooth(x,window_len=11,window='hanning'):
if x.ndim != 1:
raise ValueError, "smooth only accepts 1 dimension arrays."
if x.size < window_len:
raise ValueError, "Input vector needs to be bigger than window size."
if window_len<3:
return x
if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']:
raise ValueError, "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'"
s=numpy.r_[x[window_len-1:0:-1],x,x[-1:-window_len:-1]]
#print(len(s))
if window == 'flat': #moving average
w=numpy.ones(window_len,'d')
else:
w=eval('numpy.'+window+'(window_len)')
y=numpy.convolve(w/w.sum(),s,mode='valid')
return y

0
tests/__init__.py Normal file
View File

47
tests/conftest.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
"""Defines fixtures available to all tests."""
import os
import pytest
from webtest import TestApp
from wooey.settings import TestConfig
from wooey.app import create_app
from wooey.database import db as _db
from .factories import UserFactory
@pytest.yield_fixture(scope='function')
def app():
_app = create_app(TestConfig)
ctx = _app.test_request_context()
ctx.push()
yield _app
ctx.pop()
@pytest.fixture(scope='session')
def testapp(app):
"""A Webtest app."""
return TestApp(app)
@pytest.yield_fixture(scope='function')
def db(app):
_db.app = app
with app.app_context():
_db.create_all()
yield _db
_db.drop_all()
@pytest.fixture
def user(db):
user = UserFactory(password='myprecious')
db.session.commit()
return user

23
tests/factories.py Normal file
View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from factory import Sequence, PostGenerationMethodCall
from factory.alchemy import SQLAlchemyModelFactory
from wooey.user.models import User
from wooey.database import db
class BaseFactory(SQLAlchemyModelFactory):
class Meta:
abstract = True
sqlalchemy_session = db.session
class UserFactory(BaseFactory):
username = Sequence(lambda n: "user{0}".format(n))
email = Sequence(lambda n: "user{0}@example.com".format(n))
password = PostGenerationMethodCall('set_password', 'example')
active = True
class Meta:
model = User

18
tests/test_config.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from wooey.app import create_app
from wooey.settings import ProdConfig, DevConfig
def test_production_config():
app = create_app(ProdConfig)
assert app.config['ENV'] == 'prod'
assert app.config['DEBUG'] is False
assert app.config['DEBUG_TB_ENABLED'] is False
assert app.config['ASSETS_DEBUG'] is False
def test_dev_config():
app = create_app(DevConfig)
assert app.config['ENV'] == 'dev'
assert app.config['DEBUG'] is True
assert app.config['ASSETS_DEBUG'] is True

62
tests/test_forms.py Normal file
View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
import pytest
from wooey.public.forms import LoginForm
from wooey.user.forms import RegisterForm
from .factories import UserFactory
class TestRegisterForm:
def test_validate_user_already_registered(self, user):
# Enters username that is already registered
form = RegisterForm(username=user.username, email='foo@bar.com',
password='example', confirm='example')
assert form.validate() is False
assert 'Username already registered' in form.username.errors
def test_validate_email_already_registered(self, user):
# enters email that is already registered
form = RegisterForm(username='unique', email=user.email,
password='example', confirm='example')
assert form.validate() is False
assert 'Email already registered' in form.email.errors
def test_validate_success(self, db):
form = RegisterForm(username='newusername', email='new@test.test',
password='example', confirm='example')
assert form.validate() is True
class TestLoginForm:
def test_validate_success(self, user):
user.set_password('example')
user.save()
form = LoginForm(username=user.username, password='example')
assert form.validate() is True
assert form.user == user
def test_validate_unknown_username(self, db):
form = LoginForm(username='unknown', password='example')
assert form.validate() is False
assert 'Unknown username' in form.username.errors
assert form.user is None
def test_validate_invalid_password(self, user):
user.set_password('example')
user.save()
form = LoginForm(username=user.username, password='wrongpassword')
assert form.validate() is False
assert 'Invalid password' in form.password.errors
def test_validate_inactive_user(self, user):
user.active = False
user.set_password('example')
user.save()
# Correct username and password, but user is not activated
form = LoginForm(username=user.username, password='example')
assert form.validate() is False
assert 'User not activated' in form.username.errors

111
tests/test_functional.py Normal file
View File

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
"""Functional tests using WebTest.
See: http://webtest.readthedocs.org/
"""
import pytest
from flask import url_for
from wooey.user.models import User
from .factories import UserFactory
class TestLoggingIn:
def test_can_log_in_returns_200(self, user, testapp):
# Goes to homepage
res = testapp.get("/")
# Fills out login form in navbar
form = res.forms['loginForm']
form['username'] = user.username
form['password'] = 'myprecious'
# Submits
res = form.submit().follow()
assert res.status_code == 200
def test_sees_alert_on_log_out(self, user, testapp):
res = testapp.get("/")
# Fills out login form in navbar
form = res.forms['loginForm']
form['username'] = user.username
form['password'] = 'myprecious'
# Submits
res = form.submit().follow()
res = testapp.get(url_for('public.logout')).follow()
# sees alert
assert 'You are logged out.' in res
def test_sees_error_message_if_password_is_incorrect(self, user, testapp):
# Goes to homepage
res = testapp.get("/")
# Fills out login form, password incorrect
form = res.forms['loginForm']
form['username'] = user.username
form['password'] = 'wrong'
# Submits
res = form.submit()
# sees error
assert "Invalid password" in res
def test_sees_error_message_if_username_doesnt_exist(self, user, testapp):
# Goes to homepage
res = testapp.get("/")
# Fills out login form, password incorrect
form = res.forms['loginForm']
form['username'] = 'unknown'
form['password'] = 'myprecious'
# Submits
res = form.submit()
# sees error
assert "Unknown user" in res
class TestRegistering:
def test_can_register(self, user, testapp):
old_count = len(User.query.all())
# Goes to homepage
res = testapp.get("/")
# Clicks Create Account button
res = res.click("Create account")
# Fills out the form
form = res.forms["registerForm"]
form['username'] = 'foobar'
form['email'] = 'foo@bar.com'
form['password'] = 'secret'
form['confirm'] = 'secret'
# Submits
res = form.submit().follow()
assert res.status_code == 200
# A new user was created
assert len(User.query.all()) == old_count + 1
def test_sees_error_message_if_passwords_dont_match(self, user, testapp):
# Goes to registration page
res = testapp.get(url_for("public.register"))
# Fills out form, but passwords don't match
form = res.forms["registerForm"]
form['username'] = 'foobar'
form['email'] = 'foo@bar.com'
form['password'] = 'secret'
form['confirm'] = 'secrets'
# Submits
res = form.submit()
# sees error message
assert "Passwords must match" in res
def test_sees_error_message_if_user_already_registered(self, user, testapp):
user = UserFactory(active=True) # A registered user
user.save()
# Goes to registration page
res = testapp.get(url_for("public.register"))
# Fills out form, but username is already registered
form = res.forms["registerForm"]
form['username'] = user.username
form['email'] = 'foo@bar.com'
form['password'] = 'secret'
form['confirm'] = 'secret'
# Submits
res = form.submit()
# sees error
assert "Username already registered" in res

58
tests/test_models.py Normal file
View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""Model unit tests."""
import datetime as dt
import pytest
from wooey.user.models import User, Role
from .factories import UserFactory
@pytest.mark.usefixtures('db')
class TestUser:
def test_get_by_id(self):
user = User('foo', 'foo@bar.com')
user.save()
retrieved = User.get_by_id(user.id)
assert retrieved == user
def test_created_at_defaults_to_datetime(self):
user = User(username='foo', email='foo@bar.com')
user.save()
assert bool(user.created_at)
assert isinstance(user.created_at, dt.datetime)
def test_password_is_nullable(self):
user = User(username='foo', email='foo@bar.com')
user.save()
assert user.password is None
def test_factory(self, db):
user = UserFactory(password="myprecious")
db.session.commit()
assert bool(user.username)
assert bool(user.email)
assert bool(user.created_at)
assert user.is_admin is False
assert user.active is True
assert user.check_password('myprecious')
def test_check_password(self):
user = User.create(username="foo", email="foo@bar.com",
password="foobarbaz123")
assert user.check_password('foobarbaz123') is True
assert user.check_password("barfoobaz") is False
def test_full_name(self):
user = UserFactory(first_name="Foo", last_name="Bar")
assert user.full_name == "Foo Bar"
def test_roles(self):
role = Role(name='admin')
role.save()
u = UserFactory()
u.roles.append(role)
u.save()
assert role in u.roles

0
wooey/__init__.py Normal file
View File

4
wooey/admin/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
'''The public module, including the homepage and user auth.'''
from . import views

26
wooey/admin/views.py Normal file
View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from flask import Blueprint, render_template
from flask.ext.login import login_required
from flask.ext import login
from flask.ext.admin import Admin, BaseView, expose
from flask.ext.admin.contrib.sqla import ModelView
class AdminView(BaseView):
def is_accessible(self):
return login.current_user.is_authenticated()
@expose('/')
def index(self):
return self.render('index.html')
from ..extensions import flask_admin, db
from ..user.models import User
flask_admin.add_view(ModelView(User, db.session, endpoint='user-admin'))
from ..public.models import Script, Job
flask_admin.add_view(ModelView(Script, db.session, endpoint='script-admin'))
flask_admin.add_view(ModelView(Job, db.session, endpoint='job-admin'))

57
wooey/app.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
'''The app module, containing the app factory function.'''
from flask import Flask, render_template
from wooey.settings import ProdConfig
from wooey.assets import assets
from wooey.extensions import (
bcrypt,
cache,
db,
login_manager,
migrate,
flask_admin,
# debug_toolbar,
)
from wooey import public, user, admin
def create_app(config_object=ProdConfig):
'''An application factory, as explained here:
http://flask.pocoo.org/docs/patterns/appfactories/
:param config_object: The configuration object to use.
'''
app = Flask(__name__)
app.config.from_object(config_object)
register_extensions(app)
register_blueprints(app)
register_errorhandlers(app)
return app
def register_extensions(app):
assets.init_app(app)
bcrypt.init_app(app)
cache.init_app(app)
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
flask_admin.init_app(app)
return None
def register_blueprints(app):
app.register_blueprint(public.views.blueprint)
app.register_blueprint(user.views.blueprint)
return None
def register_errorhandlers(app):
def render_error(error):
# If a HTTPException, pull the `code` attribute; default to 500
error_code = getattr(error, 'code', 500)
return render_template("{0}.html".format(error_code)), error_code
for errcode in [401, 404, 500]:
app.errorhandler(errcode)(render_error)
return None

19
wooey/assets.py Normal file
View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from flask.ext.assets import Bundle, Environment
css = Bundle(
"css/style.css",
filters="cssmin",
output="public/css/common.css"
)
js = Bundle(
"js/plugins.js",
filters='jsmin',
output="public/js/common.js"
)
assets = Environment()
assets.register("js_all", js)
assets.register("css_all", css)

19
wooey/compat.py Normal file
View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""Python 2/3 compatibility module."""
import sys
PY2 = int(sys.version[0]) == 2
if PY2:
text_type = unicode
binary_type = str
string_types = (str, unicode)
unicode = unicode
basestring = basestring
else:
text_type = str
binary_type = bytes
string_types = (str, )
unicode = str
basestring = (str, bytes)

80
wooey/database.py Normal file
View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""Database module, including the SQLAlchemy database object and DB-related
utilities.
"""
from sqlalchemy.orm import relationship
from .extensions import db
from .compat import basestring
# Alias common SQLAlchemy names
Column = db.Column
relationship = relationship
class CRUDMixin(object):
"""Mixin that adds convenience methods for CRUD (create, read, update, delete)
operations.
"""
@classmethod
def create(cls, **kwargs):
"""Create a new record and save it the database."""
instance = cls(**kwargs)
return instance.save()
def update(self, commit=True, **kwargs):
"""Update specific fields of a record."""
for attr, value in kwargs.iteritems():
setattr(self, attr, value)
return commit and self.save() or self
def save(self, commit=True):
"""Save the record."""
db.session.add(self)
if commit:
db.session.commit()
return self
def delete(self, commit=True):
"""Remove the record from the database."""
db.session.delete(self)
return commit and db.session.commit()
class Model(CRUDMixin, db.Model):
"""Base model class that includes CRUD convenience methods."""
__abstract__ = True
# From Mike Bayer's "Building the app" talk
# https://speakerdeck.com/zzzeek/building-the-app
class SurrogatePK(object):
"""A mixin that adds a surrogate integer 'primary key' column named
``id`` to any declarative-mapped class.
"""
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
@classmethod
def get_by_id(cls, id):
if any(
(isinstance(id, basestring) and id.isdigit(),
isinstance(id, (int, float))),
):
return cls.query.get(int(id))
return None
def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs):
"""Column that adds primary key foreign key reference.
Usage: ::
category_id = ReferenceCol('category')
category = relationship('Category', backref='categories')
"""
return db.Column(
db.ForeignKey("{0}.{1}".format(tablename, pk_name)),
nullable=nullable, **kwargs)

25
wooey/extensions.py Normal file
View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
"""Extensions module. Each extension is initialized in the app factory located
in app.py
"""
from flask.ext.bcrypt import Bcrypt
bcrypt = Bcrypt()
from flask.ext.login import LoginManager
login_manager = LoginManager()
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy()
from flask.ext.migrate import Migrate
migrate = Migrate()
from flask.ext.cache import Cache
cache = Cache()
from flask.ext.admin import Admin
flask_admin = Admin()
# from flask.ext.debugtoolbar import DebugToolbarExtension
# debug_toolbar = DebugToolbarExtension()

4
wooey/public/__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
'''The public module, including the homepage and user auth.'''
from . import views

33
wooey/public/forms.py Normal file
View File

@ -0,0 +1,33 @@
from flask_wtf import Form
from wtforms import TextField, PasswordField
from wtforms.validators import DataRequired
from wooey.user.models import User
class LoginForm(Form):
username = TextField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.user = None
def validate(self):
initial_validation = super(LoginForm, self).validate()
if not initial_validation:
return False
self.user = User.query.filter_by(username=self.username.data).first()
if not self.user:
self.username.errors.append('Unknown username')
return False
if not self.user.check_password(self.password.data):
self.password.errors.append('Invalid password')
return False
if not self.user.active:
self.username.errors.append('User not activated')
return False
return True

91
wooey/public/models.py Normal file
View File

@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
import datetime as dt
import json
from wooey.database import (
Column,
ReferenceCol,
relationship,
db,
Model,
SurrogatePK,
)
STATUS_WAITING = "W"
STATUS_RUNNING = "R"
STATUS_COMPLETE = "C"
STATUS_ERROR = "X"
class Script(SurrogatePK, Model):
__tablename__ = 'scripts'
name = Column(db.String(80), nullable=False) # The basic name of the script
display_name = Column(db.String(80), nullable=True) # A nice, user friendly name
exec_path = Column(db.String(255), unique=True, nullable=False)
config_path = Column(db.String(255), unique=True, nullable=False)
doc_path = Column(db.String(255), unique=True, nullable=True)
created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow)
updated_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow, onupdate=db.func.now())
description = Column(db.String(255), nullable=True)
is_active = Column(db.Boolean(), default=True)
def load_config(self):
'''
Load JSON config from file
:return: dict of config
'''
with open(self.config_path, 'r') as f:
return json.load(f)
def load_docs(self):
'''
Load JSON config from file
:return: dict of config
'''
if self.doc_path:
with open(self.doc_path, 'r') as f:
return f.read()
else:
return None
class Job(SurrogatePK, Model):
__tablename__ = 'jobs'
script_id = ReferenceCol('scripts')
script = relationship('Script', backref='jobs')
user_id = ReferenceCol('users')
user = relationship('User', backref='jobs')
path = Column(db.String(255), unique=True, nullable=False)
created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow)
updated_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow, onupdate=db.func.now())
status = Column(db.Enum(STATUS_WAITING, STATUS_RUNNING, STATUS_COMPLETE, STATUS_ERROR), nullable=False, default=STATUS_WAITING)
config = Column(db.String(), nullable=True)
@property
def is_waiting(self):
return self.status == STATUS_WAITING
@property
def is_running(self):
return self.status == STATUS_RUNNING
@property
def is_complete(self):
return self.status == STATUS_COMPLETE
@property
def is_error(self):
return self.status == STATUS_ERROR

201
wooey/public/views.py Normal file
View File

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
'''Public section, including homepage and signup.'''
from flask import (Blueprint, request, render_template, flash, url_for,
redirect, session)
from flask.ext.login import login_user, login_required, logout_user, current_user
from wooey.extensions import login_manager
from wooey.user.models import User
from wooey.public.forms import LoginForm
from wooey.user.forms import RegisterForm
from wooey.utils import flash_errors
from wooey.database import db
from werkzeug import secure_filename
import tempfile
import json
import os
import base64
import mistune
from .models import Script, Job
blueprint = Blueprint('public', __name__, static_folder="../static")
@login_manager.user_loader
def load_user(id):
return User.get_by_id(int(id))
@blueprint.route("/", methods=["GET", "POST"])
def home():
form = LoginForm(request.form)
# Handle logging in
if request.method == 'POST':
if form.validate_on_submit():
login_user(form.user)
flash("You are logged in.", 'success')
redirect_url = request.args.get("next") or url_for("user.members")
return redirect(redirect_url)
else:
flash_errors(form)
scripts = Script.query.all()
return render_template("public/home.html", form=form, scripts=scripts)
@blueprint.route('/logout/')
@login_required
def logout():
logout_user()
flash('You are logged out.', 'info')
return redirect(url_for('public.home'))
@blueprint.route("/register/", methods=['GET', 'POST'])
def register():
form = RegisterForm(request.form, csrf_enabled=False)
if form.validate_on_submit():
new_user = User.create(username=form.username.data,
email=form.email.data,
password=form.password.data,
active=True)
flash("Thank you for registering. You can now log in.", 'success')
return redirect(url_for('public.home'))
else:
flash_errors(form)
return render_template('public/register.html', form=form)
@blueprint.route("/about/")
def about():
form = LoginForm(request.form)
return render_template("public/about.html", form=form)
@blueprint.route("/scripts/")
def scripts():
scripts = Script.query.all()
return render_template("public/scripts.html", scripts=scripts)
@blueprint.route("/jobs/create/<int:script_id>/", methods=["GET", "POST"])
def create_job(script_id):
'''
Create a new job from the given script.
GET request results in rendering the form
POST accepts the form, creates the job and redirects to the job view
This function handles the conversion of POSTed data into a command line arg sequence for
subsequent running as a job object.
:param script_id:
:return:
'''
# Get the script object from the database
script = Script.query.get(script_id)
if request.method == 'GET':
# Find the documentation and parse it using markdown
documentation = script.load_docs()
if documentation:
documentation = mistune.markdown(documentation)
# Render the script view
return render_template("public/create-job.html", script=script, metadata=script.load_config(), documentation=documentation)
elif request.method == 'POST':
# Handle the form submission to generate the arguments for the script
metadata = script.load_config()
args = []
tempdir = tempfile.mkdtemp()
for l in ['required', 'optional']:
for a in metadata[l]:
# Positional arguments
name = a['data']['display_name']
if (name in request.form and request.form[name]) or \
(name in request.files and request.files[name]):
# Add the command switch if defined
if a['data']['commands']:
args.append(a['data']['commands'][0])
if name in request.form:
# Required arguments are positional; so plot it into place
# FIXME: Probably a better check to do here, might require additional data from the parser
# FIXME: Also need to handle FILE objects, etc.
if a['widget'] not in ["CheckBox"]:
if a['data']['nargs'] == '+' or a['data']['nargs'] == '*':
args.extend(request.form[name].split(" "))
else:
args.append(request.form[name])
elif name in request.files:
# Process file upload. We need to copy to a temporary file and
# replace the value in the dictionary (or keep in the same folder, for easier cleanup?)
# Will then need a way to identify uploaded vs. output files
file = request.files[name]
fname = os.path.join(tempdir, secure_filename(file.filename))
file.save(fname)
args.append(fname)
# Create the job
job = Job(script=script, user=current_user, path=tempdir, config=json.dumps({'args': args}))
db.session.commit()
return redirect(url_for('public.job', job_id=job.id))
@blueprint.route("/jobs/<int:job_id>/")
def job(job_id):
'''
View a single job (any status) with AJAX callback to update elements, e.g.
- STDOUT/STDERR
- File outputs (figures, etc.), intelligent render handling
- Download link for all files
:param job_id:
:return:
'''
# Get the job object from the database
job = Job.query.get(job_id)
script = job.script
try:
with open(os.path.join(job.path, 'STDOUT'), 'r') as f:
console = f.read()
except IOError:
console = ""
excluded = ['STDOUT']
# Filter files for files and not excluded (STDOUT)
files = [f for f in os.listdir(job.path) if os.path.isfile(os.path.join(job.path, f)) and f not in excluded]
display = {}
for filename in files:
fullpath = os.path.join(job.path, filename)
name, ext = os.path.splitext(filename)
src = None
if ext in ['.png', '.jpg', '.jpeg', '.tif', '.tiff']:
with open(fullpath, 'r') as f:
src = '<img src="data:image/' + ext + ';base64,' + base64.b64encode(f.read()) + '">'
if src:
display[name] = src
return render_template("public/job.html", script=script, job=job, metadata=script.load_config(), console=console, display=display)

44
wooey/settings.py Normal file
View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import os
os_env = os.environ
class Config(object):
SECRET_KEY = os_env.get('WOOEY_SECRET', 'secret-key') # TODO: Change me
APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory
PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir))
BCRYPT_LOG_ROUNDS = 13
ASSETS_DEBUG = False
DEBUG_TB_ENABLED = False # Disable Debug toolbar
DEBUG_TB_INTERCEPT_REDIRECTS = False
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
class ProdConfig(Config):
"""Production configuration."""
ENV = 'prod'
DEBUG = False
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example' # TODO: Change me
DEBUG_TB_ENABLED = False # Disable Debug toolbar
class DevConfig(Config):
"""Development configuration."""
ENV = 'dev'
DEBUG = True
DB_NAME = 'dev.db'
# Put the db file in project root
DB_PATH = os.path.join(Config.PROJECT_ROOT, DB_NAME)
SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(DB_PATH)
DEBUG_TB_ENABLED = True
ASSETS_DEBUG = True # Don't bundle/minify static assets
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
class TestConfig(Config):
TESTING = True
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'
BCRYPT_LOG_ROUNDS = 1 # For faster tests
WTF_CSRF_ENABLED = False # Allows form testing

View File

@ -0,0 +1,61 @@
/* =============================================================================
App specific CSS file.
========================================================================== */
.form-with-annotation {
margin-bottom: 0px !important;
}
.form-annotation small {
color: #bbb;
}
footer {
background: white;
padding: 20px;
height: 100px;
}
body {
height: auto !important;
}
.tab-title > a {
padding: 8px 16px !important;
}
.tab-title.active > a {
}
.settings-panel {
background-color: #f5f5f5 !important;
padding: 0.8em;
}
.settings-panel-tabs .tab-title > a {
background-color: #ddd !important;
}
.settings-panel-tabs .tab-title.active > a {
background-color: #f5f5f5 !important;
}
.console {
overflow:scroll;
background-color: #333;
color: #0f0;
font-family: "Courier New", Courier, monospace;
}
.console .console-body {
margin:1em;
white-space: pre;
font-size: 0.9rem;
}

View File

@ -0,0 +1 @@
// place any jQuery/helper plugins in here, instead of separate, slower script files.

View File

@ -0,0 +1,5 @@
(function($, window) {
}).call(this, jQuery, window);

View File

View File

View File

16
wooey/templates/401.html Normal file
View File

@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% block page_title %}Unauthorized{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>401</h1>
<p>You are not authorized to see this page. Please <a href="{{ url_for('public.home')}}">log in</a> or
<a href="{{ url_for('public.register') }}">create a new account</a>.
</p>
</div>
</div>
{% endblock %}

15
wooey/templates/404.html Normal file
View File

@ -0,0 +1,15 @@
{% extends "layout.html" %}
{% block page_title %}Page Not Found{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>404</h1>
<p>Sorry, that page doesn't exist.</p>
<p>Want to <a href="{{ url_for('public.home') }}">go home</a> instead?</p>
</div>
</div>
{% endblock %}

14
wooey/templates/500.html Normal file
View File

@ -0,0 +1,14 @@
{% extends "layout.html" %}
{% block page_title %}Server error{% endblock %}
{% block content %}
<div class="jumbotron">
<div class="text-center">
<h1>500</h1>
<p>Sorry, something went wrong on our system. Don't panic, we are fixing it! Please try again later.</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,9 @@
<footer>
<!--
<ul class="inline-list">
<li>© Martin Fitzpatrick</li>
<li><a href="{{ url_for('public.about') }}">About</a></li>
<li><a href="mailto:martin.fitzpatrick@gmail.com">Contact</a></li>
</ul>
-->
</footer>

View File

@ -0,0 +1,82 @@
<!doctype html>
<!-- paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/ -->
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<title>{% block page_title %}
Wooey
{% endblock %}
</title>
<meta name="description" content="{% block meta_description %}{% endblock %}">
<meta name="author" content="{% block meta_author %}{% endblock %}">
<!-- Mobile viewport optimized: h5bp.com/viewport -->
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/5.5.1/css/foundation.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/5.5.1/css/normalize.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/foundation/5.5.1/js/foundation.min.js"></script>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
{% assets "css_all" %}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{% endassets %}
{% block css %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
{% block body %}
{% with form=form %}
{% include "nav.html" %}
{% endwith %}
<header>{% block header %}{% endblock %}</header>
<div class="{% block content_class %}row{% endblock content_class %}">
<div role="main">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="row">
<div class="large-12 columns">
{% for category, message in messages %}
<div class="alert-box {{ category }}">
<a class="close" title="Close" href="#" data-dismiss="alert">&times;</a>
{{message}}
</div><!-- end .alert -->
{% endfor %}
</div><!-- end col-md -->
</div><!-- end row -->
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</div><!-- end container -->
{% include "footer.html" %}
<!-- JavaScript at the bottom for fast page loading -->
{% assets "js_all" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% block js %}{% endblock %}
<!-- end scripts -->
{% endblock %}
<script type="text/javascript">$(document).foundation();</script>
</body>
</html>

32
wooey/templates/nav.html Normal file
View File

@ -0,0 +1,32 @@
<nav class="top-bar" data-topbar role="navigation">
<ul class="title-area"><li class="name"><h1><a href="{{ url_for('public.home') }}">Wooey</a></h1></li></ul>
<section class="top-bar-section">
<!-- Right Nav Section -->
<ul class="right">
{% if current_user and current_user.is_authenticated() %}
<li><a href="{{ url_for('user.members') }}">Logged in as {{ current_user.username }}</a></li>
<li><a href="{{ url_for('public.logout') }}"><i class="fa fa-sign-out"></i></a></li>
{% elif form %}
<form id="loginForm" method="POST" action="/" role="login">
<li><a href="{{ url_for('public.register') }}">Create account</a></li>
<li class="has-form">{{ form.hidden_tag() }}{{ form.username(placeholder="Username", class_="form-control") }}</li>
<li class="has-form">{{ form.password(placeholder="Password", class_="form-control") }}</li>
<li class="has-form"><button type="submit" class="btn btn-default">Log in</button></li>
</form>
{% endif %}
</ul>
<!-- Left Nav Section -->
<ul class="left">
<li><a href="{{ url_for('public.home') }}">Home</a></li>
<li><a href="{{ url_for('public.about') }}">About</a></li>
<li><a href="{{ url_for('public.scripts') }}">Scripts</a></li>
</ul>
</section>
</nav>

View File

@ -0,0 +1,17 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
<div class="large-12 columns">
<h1>About</h1>
<p>Woeey is a web interface for command-line scripts. Built to target Python scripts using argparse (based on <a href="https://github.com/chriskiehl/Gooey">Gooey</a>
GUI construction from ArgumentParser instances) it will eventually run other style command lines, customisable through
a per-script JSON configuration.</p>
<p>Auto-creation of JSON descriptions for Python scripts is based on the excellent <a href="https://github.com/chriskiehl/Gooey">Gooey</a> tool.</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "layout.html" %}
{% block content %}
<h1>{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}</h1>
<p>{{ script.description }}</p>
<!-- Build the UI
{u'optional': [{u'type': u'CheckBox', u'data': {u'display_name': u'accumulate', u'commands': [u'--sum'], u'nargs': u'', u'help': u'sum the integers (default: find the max)', u'choices': []}}], u'program': {u'path': u'/Users/mxf793/repos/wooey/scripts/mock_argparse_example.py', u'epilog': None, u'name': u'mock_argparse_example', u'description': u'Process some integers.'}, u'parser': {u'prefix_chars': u'-', u'argument_default': None}, u'required': [{u'type': u'FileChooser', u'data': {u'display_name': u'integers', u'commands': [], u'nargs': u'+', u'help': u'an integer for the accumulator', u'choices': []}}]}
'FileChooser',
'MultiFileChooser',
'FileSaver',
'DirChooser',
'DateChooser',
'TextField',
'Dropdown',
'Counter',
'RadioGroup',
'CheckBox'
-->
<div class="row">
{% include "public/job-form.html" %}
<div class="large-7 columns">
{% if documentation %}
{{ documentation|safe }}
{% else %}
<p>No documentation for this script.</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "layout.html" %}
{% block content %}
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<h1>Welcome to Wooey</h1>
<p>Perform data analysis, system administration, or anything you like, via a web-based interface to command-line scripts.</p>
</div><!-- /.jumbotron -->
<div class="row">
<table width="100%">
<thead>
<tr>
<th width="200">Script</th>
<th></th>
</tr>
</thead>
<tbody>
{% for script in scripts %}
<tr>
<td><a href="{{ url_for('public.create_job', script_id=script.id) }}" class="list-group-item{% if not script.is_active %} disabled{% endif %}">
{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}</a></td>
<td>{{ script.description }}</td>
</tr>
{% endfor %}
</tbody></table>
</div>
{% endblock %}

View File

@ -0,0 +1,70 @@
<div class="large-5 columns">
<ul class="tabs settings-panel-tabs" data-tab role="tablist">
{% for section in ['required','optional'] %}
<li class="tab-title {% if loop.first %}active{% endif %} radius" role="presentational" ><a href="#settings-{{section}}" role="tab" tabindex="0" aria-selected="{% if loop.first %}true{% else %}false{% endif %}" controls="settings-{{section}}">{{section}}</a></li>
{% endfor %}
</ul>
<form method="post" enctype="multipart/form-data" action="{{ url_for('public.create_job', script_id=script.id) }}">
<div class="tabs-content settings-panel">
{% for section in ['required','optional'] %}
<section role="tabpanel" aria-hidden="false" class="content {% if loop.first %}active{% endif %}" id="settings-{{section}}">
{% for w in metadata[section] %}
<div class="row">
<div class="large-12 columns">
{% if w['widget'] == 'FileChooser' %}
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}
<input type="file" name="{{ w['data']['display_name'] }}" id="{{ w['data']['display_name'] }}">
</label>
{% elif w['widget'] == 'CheckBox' %}
<div class="row">
<div class="large-1 columns">
<input type="checkbox" placeholder="{{ w['data']['default'] }}" id="{{ w['data']['display_name'] }}" name="{{ w['data']['display_name'] }}">
</div>
<div class="large-11 columns">
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}</label>
</div>
</div>
{% elif w['widget'] == 'TextField' %}
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}
<input type="text" placeholder="{{ w['data']['default'] }}" id="{{ w['data']['display_name'] }}" name="{{ w['data']['display_name'] }}">
</label>
{% elif w['widget'] == 'Dropdown' %}
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}
<select type="text" placeholder="{{ w['data']['default'] }}" id="{{ w['data']['display_name'] }}" name="{{ w['data']['display_name'] }}">
{% for choice in w['data']['choices'] %}
<option value="{{ choice }}">{{ choice }}</option>
{% endfor %}
</select>
</label>
{% else %}
{% endif %}
</div>
</div>
{% endfor %}
</section>
{% endfor%}
</div>
<div class="row">
<div class="large-9 columns">
<input type="button" class="button alert right small" value="Cancel" onclick="window.history.back()">
</div><div class="large-3 columns">
<input type="submit" class="button success right small" value="Run">
</div>
</div>
</form>
</div>

View File

@ -0,0 +1,68 @@
{% extends "layout.html" %}
{% block content %}
<h1>{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}
<small>(Job ID {{ job.id }})</small>
{% if job.status == 'W' %}<span class="label info">Waiting</span>
{% elif job.status == 'R' %}<span class="label info">Running</span>
{% elif job.status == 'C' %}<span class="label success">Complete</span>
{% elif job.status == 'X' %}<span class="label alert">Error</span>{% endif %}
</h1>
<p>{{ script.description }}</p>
<!-- Build the UI
{u'optional': [{u'type': u'CheckBox', u'data': {u'display_name': u'accumulate', u'commands': [u'--sum'], u'nargs': u'', u'help': u'sum the integers (default: find the max)', u'choices': []}}], u'program': {u'path': u'/Users/mxf793/repos/wooey/scripts/mock_argparse_example.py', u'epilog': None, u'name': u'mock_argparse_example', u'description': u'Process some integers.'}, u'parser': {u'prefix_chars': u'-', u'argument_default': None}, u'required': [{u'type': u'FileChooser', u'data': {u'display_name': u'integers', u'commands': [], u'nargs': u'+', u'help': u'an integer for the accumulator', u'choices': []}}]}
'FileChooser',
'MultiFileChooser',
'FileSaver',
'DirChooser',
'DateChooser',
'TextField',
'Dropdown',
'Counter',
'RadioGroup',
'CheckBox'
-->
<div class="row">
{% include "public/job-form.html" %}
<div class="large-7 columns">
<ul class="tabs" data-tab role="tablist">
<li class="tab-title active" role="presentational" ><a href="#output-console" role="tab" tabindex="0" aria-selected="true" controls="output-console">Console</a></li>
{% for section in display.keys() %}
<li class="tab-title" role="presentational" ><a href="#output-{{ section.replace(' ','-') }}" role="tab" tabindex="0" aria-selected="" controls="output-{{ section.replace(' ','-') }}">{{section}}</a></li>
{% endfor %}
</ul>
<div class="tabs-content">
<section role="tabpanel" aria-hidden="false" class="content active console" id="output-console">
<div class="row">
<div class="large-12 columns console-body">{{ console }}</div>
</div>
</section>
{% for section, src in display.items() %}
<section role="tabpanel" aria-hidden="false" class="content" id="output-{{ section.replace(' ','-') }}">
<div class="row">
<div class="large-12 columns console-body">{{ src|safe }}</div>
</div>
</section>
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-narrow">
<h1>Register</h1>
<br/>
<form id="registerForm" class="form form-register" method="POST" action="" role="form">
{{ form.hidden_tag() }}
<div class="form-group">
{{form.username.label}}
{{form.username(placeholder="Username", class_="form-control")}}
</div>
<div class="form-group">
{{form.email.label}}
{{form.email(placeholder="Email", class_="form-control")}}
</div>
<div class="form-group">
{{form.password.label}}
{{form.password(placeholder="Password", class_="form-control")}}
</div>
<div class="form-group">
{{form.confirm.label}}
{{form.confirm(placeholder="Password (again)", class_="form-control")}}
</div>
<p><input class="btn btn-default btn-submit" type="submit" value="Register"></p>
</form>
<p><em>Already registered?</em> Click <a href="/">here</a> to login.</p>
</div>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "layout.html" %}
{% block content %}
<h1>Available Scripts</h1>
<table width="100%">
<thead>
<tr>
<th>Script</th>
<th></th>
<th><i class="fa fa-play" title="Enabled?"></i></th>
</tr>
</thead>
<tbody>
{% for script in scripts %}
<tr>
<td><a href="{{ url_for('public.create_job', script_id=script.id) }}" class="list-group-item{% if not script.is_active %} disabled{% endif %}">
{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}</a></td>
<td>{{ script.description }}</td>
<td>{% if script.is_active %}<i class="fa fa-check"></i>{% endif %}</td>
</tr>
{% endfor %}
</tbody></table>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "layout.html" %}
{% block content %}
<h1>{{ current_user.username }}'s Jobs</h1>
<table width="100%">
<thead>
<tr>
<th></th><th>Script name</th>
<th></th>
<th>Created</th>
<th>Updated</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for job in user.jobs %}
<tr>
<td>{{ job.id }}
</td><td>
<a href="{{ url_for('public.job', job_id=job.id) }}">
{% if job.script.nicename %}{{ job.script.display_name }}{% else %}{{ job.script.name }}{% endif %}</a></td>
<td>{{ job.script.description }}</td>
<td>{{ job.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ job.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>
{% if job.status == 'W' %}<span class="label info">Waiting</span>
{% elif job.status == 'R' %}<span class="label info">Running</span>
{% elif job.status == 'C' %}<span class="label success">Complete</span>
{% elif job.status == 'X' %}<span class="label alert">Error</span>{% endif %}
</td>
</tr>
{% endfor %}
</tbody></table>
{% endblock %}

3
wooey/user/__init__.py Normal file
View File

@ -0,0 +1,3 @@
'''The user module.'''
from . import views

34
wooey/user/forms.py Normal file
View File

@ -0,0 +1,34 @@
from flask_wtf import Form
from wtforms import TextField, PasswordField
from wtforms.validators import DataRequired, Email, EqualTo, Length
from .models import User
class RegisterForm(Form):
username = TextField('Username',
validators=[DataRequired(), Length(min=3, max=25)])
email = TextField('Email',
validators=[DataRequired(), Email(), Length(min=6, max=40)])
password = PasswordField('Password',
validators=[DataRequired(), Length(min=6, max=40)])
confirm = PasswordField('Verify password',
[DataRequired(), EqualTo('password', message='Passwords must match')])
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.user = None
def validate(self):
initial_validation = super(RegisterForm, self).validate()
if not initial_validation:
return False
user = User.query.filter_by(username=self.username.data).first()
if user:
self.username.errors.append("Username already registered")
return False
user = User.query.filter_by(email=self.email.data).first()
if user:
self.email.errors.append("Email already registered")
return False
return True

61
wooey/user/models.py Normal file
View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
import datetime as dt
from flask.ext.login import UserMixin
from wooey.extensions import bcrypt
from wooey.database import (
Column,
db,
Model,
ReferenceCol,
relationship,
SurrogatePK,
)
class Role(SurrogatePK, Model):
__tablename__ = 'roles'
name = Column(db.String(80), unique=True, nullable=False)
user_id = ReferenceCol('users', nullable=True)
user = relationship('User', backref='roles')
def __init__(self, name, **kwargs):
db.Model.__init__(self, name=name, **kwargs)
def __repr__(self):
return '<Role({name})>'.format(name=self.name)
class User(UserMixin, SurrogatePK, Model):
__tablename__ = 'users'
username = Column(db.String(80), unique=True, nullable=False)
email = Column(db.String(80), unique=True, nullable=False)
#: The hashed password
password = Column(db.String(128), nullable=True)
created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow)
first_name = Column(db.String(30), nullable=True)
last_name = Column(db.String(30), nullable=True)
active = Column(db.Boolean(), default=False)
is_admin = Column(db.Boolean(), default=False)
def __init__(self, username, email, password=None, **kwargs):
db.Model.__init__(self, username=username, email=email, **kwargs)
if password:
self.set_password(password)
else:
self.password = None
def set_password(self, password):
self.password = bcrypt.generate_password_hash(password)
def check_password(self, value):
return bcrypt.check_password_hash(self.password, value)
@property
def full_name(self):
return "{0} {1}".format(self.first_name, self.last_name)
def __repr__(self):
return '<User({username!r})>'.format(username=self.username)

13
wooey/user/views.py Normal file
View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from flask import Blueprint, render_template
from flask.ext.login import current_user, login_required
blueprint = Blueprint("user", __name__, url_prefix='/users',
static_folder="../static")
@blueprint.route("/")
@login_required
def members():
return render_template("users/members.html", user=current_user)

11
wooey/utils.py Normal file
View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
'''Helper utilities and decorators.'''
from flask import flash
def flash_errors(form, category="warning"):
'''Flash all errors for a form.'''
for field, errors in form.errors.items():
for error in errors:
flash("{0} - {1}"
.format(getattr(form, field).label.text, error), category)