├── djangui ├── backend │ ├── ast │ │ ├── __init__.py │ │ ├── source_parser.py │ │ └── codegen.py │ ├── __init__.py │ ├── command_line.py │ ├── argparse_specs.py │ └── utils.py ├── migrations │ ├── __init__.py │ ├── 0002_remove_scriptparameter_output_path.py │ ├── 0003_populate_from_slug.py │ └── 0001_initial.py ├── conf │ ├── __init__.py │ └── project_template │ │ ├── urls │ │ ├── __init__.py │ │ ├── djangui_urls.py │ │ └── user_urls.py │ │ ├── settings │ │ ├── __init__.py │ │ ├── djangui_settings.py │ │ └── user_settings.py │ │ ├── requirements.txt │ │ ├── __init__.py │ │ ├── middleware.py │ │ └── djangui_celery_app.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── addscript.py ├── templatetags │ ├── __init__.py │ └── djangui_tags.py ├── tests │ ├── scripts │ │ ├── __init__.py │ │ ├── command_order.py │ │ ├── gaussian.py │ │ ├── crop.py │ │ ├── heatmap.py │ │ ├── mandlebrot.py │ │ ├── fetch_cats.py │ │ ├── translate.py │ │ └── nested_heatmap.py │ ├── data │ │ ├── fasta.fasta │ │ └── delimited.tsv │ ├── __init__.py │ ├── test_commands.py │ ├── config.py │ ├── mixins.py │ ├── test_forms.py │ ├── factories.py │ ├── test_scripts.py │ ├── test_utils.py │ ├── test_models.py │ └── test_views.py ├── models │ ├── __init__.py │ ├── fields.py │ └── mixins.py ├── tests.py ├── static │ └── djangui │ │ └── djangui_logo.png ├── forms │ ├── __init__.py │ ├── scripts.py │ ├── fields.py │ └── factory.py ├── views │ ├── __init__.py │ ├── mixins.py │ ├── authentication.py │ ├── views.py │ └── djangui_celery.py ├── version.py ├── templates │ └── djangui │ │ ├── profile │ │ └── profile_base.html │ │ ├── modals │ │ ├── resubmit_modal.html │ │ └── base_modal.html │ │ ├── registration │ │ ├── login_header.html │ │ ├── login.html │ │ └── register.html │ │ ├── base.html │ │ ├── tasks │ │ └── task_view.html │ │ └── djangui_home.html ├── test_urls.py ├── apps.py ├── __init__.py ├── settings.py ├── admin.py ├── django_compat.py ├── djanguistorage.py ├── signals.py ├── test_settings.py ├── urls.py └── tasks.py ├── requirements.txt ├── .coveragerc ├── scripts └── djanguify ├── MANIFEST.in ├── Makefile ├── .travis.yml ├── tox.ini ├── .gitignore ├── setup.py ├── tests └── test_project.py └── README.md /djangui/backend/ast/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djangui/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /djangui/conf/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | -------------------------------------------------------------------------------- /djangui/management/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | -------------------------------------------------------------------------------- /djangui/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | -------------------------------------------------------------------------------- /djangui/tests/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | -------------------------------------------------------------------------------- /djangui/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | -------------------------------------------------------------------------------- /djangui/tests/data/fasta.fasta: -------------------------------------------------------------------------------- 1 | >abc 2 | ABC 3 | >abc123 4 | ABCCCC -------------------------------------------------------------------------------- /djangui/backend/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /djangui/conf/project_template/urls/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_urls import * -------------------------------------------------------------------------------- /djangui/conf/project_template/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_settings import * -------------------------------------------------------------------------------- /djangui/models/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .core import * -------------------------------------------------------------------------------- /djangui/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /djangui/tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | import os 3 | os.environ['TESTING'] = 'True' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django-autoslug 2 | django-celery 3 | nose 4 | factory-boy 5 | coveralls 6 | six 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = djangui/conf*,djangui/migrations*,djangui/tests*,djangui/backend/ast* 4 | -------------------------------------------------------------------------------- /djangui/tests/data/delimited.tsv: -------------------------------------------------------------------------------- 1 | Header1 Header2 Header3 2 | data1 data2 data3 3 | data4 data5 data6 4 | data7 data8 data9 -------------------------------------------------------------------------------- /djangui/static/djangui/djangui_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chris7/django-djangui/HEAD/djangui/static/djangui/djangui_logo.png -------------------------------------------------------------------------------- /djangui/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | __author__ = 'chris' 3 | from .factory import * 4 | from .scripts import * -------------------------------------------------------------------------------- /scripts/djanguify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | __author__ = 'chris' 3 | from djangui.backend import command_line 4 | 5 | if __name__ == "__main__": 6 | command_line.bootstrap() -------------------------------------------------------------------------------- /djangui/views/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .mixins import * 3 | from .views import * 4 | from .djangui_celery import * 5 | from .authentication import * -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include djangui/templates * 4 | recursive-include djangui/static * 5 | recursive-include djangui/conf * 6 | recursive-exclude * *.pyc 7 | -------------------------------------------------------------------------------- /djangui/conf/project_template/urls/djangui_urls.py: -------------------------------------------------------------------------------- 1 | from .django_urls import * 2 | 3 | urlpatterns += [ 4 | #url(r'^admin/', include(admin.site.urls)), 5 | url(r'^', include('djangui.urls')), 6 | ] -------------------------------------------------------------------------------- /djangui/conf/project_template/requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-djangui 3 | django-sslify 4 | django-storages 5 | django-celery 6 | django-autoslug 7 | boto 8 | waitress 9 | psycopg2 10 | collectfast 11 | honcho -------------------------------------------------------------------------------- /djangui/version.py: -------------------------------------------------------------------------------- 1 | from django import get_version 2 | from distutils.version import StrictVersion 3 | DJANGO_VERSION = StrictVersion(get_version()) 4 | DJ18 = StrictVersion('1.8') 5 | DJ17 = StrictVersion('1.7') 6 | DJ16 = StrictVersion('1.6') -------------------------------------------------------------------------------- /djangui/conf/project_template/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .djangui_celery_app import app as celery_app -------------------------------------------------------------------------------- /djangui/templates/djangui/profile/profile_base.html: -------------------------------------------------------------------------------- 1 | {% extends "djangui/djangui_home.html" %} 2 | {% load i18n %} 3 | {% load djangui_tags %} 4 | {% block left_sidebar %}{% endblock left_sidebar %} 5 | {% block center_content_class %}col-md-9 col-xs-12{% endblock center_content_class %} 6 | {% block center_content %} 7 | {% endblock center_content %} -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | testenv: 2 | pip install -r requirements.txt 3 | pip install Django 4 | pip install -e . 5 | 6 | test: 7 | nosetests --with-coverage --cover-erase --cover-package=djangui tests 8 | coverage run --append --branch --source=djangui `which django-admin.py` test --settings=djangui.test_settings djangui.tests 9 | coverage report 10 | -------------------------------------------------------------------------------- /djangui/test_urls.py: -------------------------------------------------------------------------------- 1 | from . import test_settings 2 | from django.conf import settings 3 | from django.conf.urls.static import static 4 | 5 | # the DEBUG setting in test_settings is not respected 6 | settings.DEBUG = True 7 | urlpatterns = static(test_settings.MEDIA_URL, document_root=test_settings.MEDIA_ROOT) 8 | urlpatterns += static(test_settings.STATIC_URL, document_root=test_settings.STATIC_ROOT) -------------------------------------------------------------------------------- /djangui/tests/scripts/command_order.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | parser = argparse.ArgumentParser(description="Something") 5 | parser.add_argument('link', help='the url containing the metadata') 6 | parser.add_argument('name', help='the name of the file') 7 | 8 | if __name__ == '__main__': 9 | args = parser.parse_args() 10 | sys.stderr.write('{} {}'.format(args.link,args.name)) -------------------------------------------------------------------------------- /djangui/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | 5 | from . import config 6 | from ..backend import utils 7 | from . import mixins 8 | 9 | class FormTestCase(mixins.ScriptFactoryMixin, TestCase): 10 | 11 | def test_addscript(self): 12 | from django.core.management import call_command 13 | call_command('addscript', os.path.join(config.DJANGUI_TEST_SCRIPTS, 'command_order.py')) -------------------------------------------------------------------------------- /djangui/conf/project_template/urls/user_urls.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from django.conf.urls import include, url 4 | from django.conf import settings 5 | from django.conf.urls.static import static 6 | 7 | from .djangui_urls import * 8 | 9 | if settings.DEBUG: 10 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 11 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -------------------------------------------------------------------------------- /djangui/conf/project_template/middleware.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | import traceback 3 | import sys 4 | 5 | class ProcessExceptionMiddleware(object): 6 | def process_response(self, request, response): 7 | if response.status_code != 200: 8 | try: 9 | sys.stderr.write('{}'.format(''.join(traceback.format_exc()))) 10 | except AttributeError: 11 | pass 12 | return response 13 | -------------------------------------------------------------------------------- /djangui/forms/scripts.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django import forms 3 | 4 | 5 | class DjanguiForm(forms.Form): 6 | 7 | def add_djangui_fields(self): 8 | # This adds fields such as job name, description that we like to validate on but don't want to include in 9 | # form rendering 10 | self.fields['job_name'] = forms.CharField() 11 | self.fields['job_description'] = forms.CharField(required=False) 12 | 13 | -------------------------------------------------------------------------------- /djangui/templates/djangui/modals/resubmit_modal.html: -------------------------------------------------------------------------------- 1 | {% extends "djangui/modals/base_modal.html" %} 2 | {% load i18n %} 3 | {% block modal_title %}{% trans "Resubmission Complete" %}{% endblock modal_title %} 4 | {% block modal_body %}{% trans "Your task has been successfully resubmitted." %}{% endblock modal_body %} 5 | {% block modal_buttons %} 6 | 7 | {% endblock modal_buttons %} 8 | -------------------------------------------------------------------------------- /djangui/migrations/0002_remove_scriptparameter_output_path.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('djangui', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='scriptparameter', 16 | name='output_path', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /djangui/tests/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.split(__file__)[0] 4 | DJANGUI_TEST_SCRIPTS = os.path.join(BASE_DIR, 'scripts') 5 | DJANGUI_TEST_DATA = os.path.join(BASE_DIR, 'data') 6 | 7 | SCRIPT_DATA = { 8 | 'translate': 9 | { 10 | 'data': { 11 | 'djangui_type': '1', 12 | 'job_name': 'abc', 13 | 'sequence': 'ATATATATATA', 14 | 'frame': '+3', 15 | 'out': 'abc' 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /djangui/apps.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | import sys 3 | 4 | try: 5 | from django.apps import AppConfig 6 | except ImportError: 7 | AppConfig = object 8 | 9 | class DjanguiConfig(AppConfig): 10 | name = 'djangui' 11 | verbose_name = 'Djangui' 12 | 13 | def ready(self): 14 | from .backend import utils 15 | try: 16 | utils.load_scripts() 17 | except: 18 | sys.stderr.write('Unable to load scripts:\n{}\n'.format(traceback.format_exc())) 19 | from . import signals -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | env: 3 | - DJANGO_VERSION="Django>=1.6,<1.7" DJANGO_SETTINGS_MODULE= 4 | - DJANGO_VERSION="Django>=1.7,<1.8" DJANGO_SETTINGS_MODULE= 5 | - DJANGO_VERSION="Django>=1.8,<1.9" DJANGO_SETTINGS_MODULE= 6 | python: 7 | - "2.7" 8 | - "3.3" 9 | - "3.4" 10 | # command to install dependencies 11 | install: 12 | - pip install -q $DJANGO_VERSION 13 | - pip install -q -r requirements.txt 14 | - pip install -e . 15 | # command to run tests 16 | script: make test 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /djangui/__init__.py: -------------------------------------------------------------------------------- 1 | from . import version 2 | import os 3 | if version.DJANGO_VERSION >= version.DJ17: 4 | default_app_config = 'djangui.apps.DjanguiConfig' 5 | else: 6 | if os.environ.get('TESTING') != 'True': 7 | from . import settings as djangui_settings 8 | # we need to call from within djangui_settings so the celery/etc vars are setup 9 | if not djangui_settings.settings.configured: 10 | djangui_settings.settings.configure() 11 | from .apps import DjanguiConfig 12 | DjanguiConfig().ready() -------------------------------------------------------------------------------- /djangui/conf/project_template/djangui_celery_app.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | from django.conf import settings 6 | 7 | from celery import app as celery_app 8 | 9 | app = celery_app.app_or_default() 10 | # app = Celery('{{ project_name }}') 11 | 12 | # Using a string here means the worker will not have to 13 | # pickle the object when using Windows. 14 | app.config_from_object('django.conf:settings') 15 | 16 | 17 | @app.task(bind=True) 18 | def debug_task(self): 19 | print('Request: {0!r}'.format(self.request)) -------------------------------------------------------------------------------- /djangui/forms/fields.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | __author__ = 'chris' 3 | from django.forms import CharField, FileField, FilePathField 4 | from django.forms import widgets 5 | 6 | class DjanguiOutputFileField(FileField): 7 | widget = widgets.TextInput 8 | 9 | def __init__(self, *args, **kwargs): 10 | kwargs['allow_empty_file'] = True 11 | super(DjanguiOutputFileField, self).__init__(*args, **kwargs) 12 | 13 | # TODO: Make a complex widget of filepathfield/filefield 14 | class DjanguiUploadFileField(FileField): 15 | pass 16 | -------------------------------------------------------------------------------- /djangui/conf/project_template/settings/djangui_settings.py: -------------------------------------------------------------------------------- 1 | from .django_settings import * 2 | 3 | INSTALLED_APPS += ( 4 | # 'corsheaders', 5 | 'djangui', 6 | ) 7 | 8 | # MIDDLEWARE_CLASSES = [[i] if i == 'django.middleware.common.CommonMiddleware' else ['corsheaders.middleware.CorsMiddleware',i] for i in MIDDLEWARE_CLASSES] 9 | MIDDLEWARE_CLASSES = list(MIDDLEWARE_CLASSES) 10 | MIDDLEWARE_CLASSES.append('{{ project_name }}.middleware.ProcessExceptionMiddleware') 11 | 12 | PROJECT_NAME = "{{ project_name }}" 13 | DJANGUI_CELERY_APP_NAME = 'djangui.celery' 14 | DJANGUI_CELERY_TASKS = 'djangui.tasks' 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [deps] 2 | two = 3 | flake8 4 | coverage 5 | three = 6 | flake8 7 | coverage 8 | 9 | [tox] 10 | envlist = 11 | {py27,py34}-{1.6.X}, 12 | {py27,py34}-{1.7.X}, 13 | {py27,py34}-{1.8.X} 14 | [testenv] 15 | basepython = 16 | py27: python2.7 17 | py33: python3.3 18 | py34: python3.4 19 | usedevelop = true 20 | setenv = 21 | CPPFLAGS=-O0 22 | whitelist_externals = /usr/bin/make 23 | downloadcache = {toxworkdir}/_download/ 24 | commands = 25 | django-admin.py --version 26 | make testenv 27 | make test 28 | deps = 29 | 1.6.X: Django>=1.6,<1.7 30 | 1.7.X: Django>=1.7,<1.8 31 | 1.8.X: Django>=1.8,<1.9 32 | py27: {[deps]two} 33 | py34: {[deps]three} 34 | django-discover-runner 35 | -------------------------------------------------------------------------------- /djangui/settings.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | from django.conf import settings 3 | 4 | def get(key, default): 5 | return getattr(settings, key, default) 6 | 7 | DJANGUI_FILE_DIR = get('DJANGUI_FILE_DIR', 'djangui_files') 8 | DJANGUI_SCRIPT_DIR = get('DJANGUI_SCRIPT_DIR', 'djangui_scripts') 9 | DJANGUI_CELERY = get('DJANGUI_CELERY', True) 10 | DJANGUI_CELERY_TASKS = get('DJANGUI_CELERY_TASKS', 'djangui.tasks') 11 | DJANGUI_ALLOW_ANONYMOUS = get('DJANGUI_ALLOW_ANONYMOUS', True) 12 | DJANGUI_AUTH = get('DJANGUI_AUTH', True) 13 | DJANGUI_LOGIN_URL = get('DJANGUI_LOGIN_URL', settings.LOGIN_URL) 14 | DJANGUI_REGISTER_URL = get('DJANGUI_REGISTER_URL', '/accounts/register/') 15 | DJANGUI_SHOW_LOCKED_SCRIPTS = get('DJANGUI_SHOW_LOCKED_SCRIPTS', True) 16 | DJANGUI_EPHEMERAL_FILES = get('DJANGUI_EPHEMERAL_FILES', False) -------------------------------------------------------------------------------- /djangui/templatetags/djangui_tags.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, absolute_import 2 | from django import template 3 | from .. import settings as djangui_settings 4 | 5 | register = template.Library() 6 | @register.filter 7 | def divide(value, arg): 8 | try: 9 | return float(value)/float(arg) 10 | except ZeroDivisionError: 11 | return None 12 | 13 | @register.filter 14 | def endswith(value, arg): 15 | return str(value).endswith(arg) 16 | 17 | @register.filter 18 | def valid_user(obj, user): 19 | from ..backend import utils 20 | valid = utils.valid_user(obj, user) 21 | return True if valid.get('valid') else valid.get('display') 22 | 23 | @register.filter 24 | def complete_job(status): 25 | from ..models import DjanguiJob 26 | from celery import states 27 | return status in (DjanguiJob.COMPLETED, states.REVOKED) -------------------------------------------------------------------------------- /djangui/models/fields.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | __author__ = 'chris' 3 | from django.db import models 4 | from ..forms import fields as djangui_form_fields 5 | 6 | class DjanguiOutputFileField(models.FileField): 7 | def formfield(self, **kwargs): 8 | # TODO: Make this from an app that is plugged in 9 | defaults = {'form_class': djangui_form_fields.DjanguiOutputFileField} 10 | defaults.update(kwargs) 11 | return super(DjanguiOutputFileField, self).formfield(**defaults) 12 | 13 | class DjanguiUploadFileField(models.FileField): 14 | def formfield(self, **kwargs): 15 | # TODO: Make this from an app that is plugged in 16 | defaults = {'form_class': djangui_form_fields.DjanguiUploadFileField} 17 | defaults.update(kwargs) 18 | return super(DjanguiUploadFileField, self).formfield(**defaults) -------------------------------------------------------------------------------- /djangui/templates/djangui/registration/login_header.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | -------------------------------------------------------------------------------- /djangui/tests/mixins.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | 3 | from ..models import Script, DjanguiFile, DjanguiJob 4 | from ..backend import utils 5 | 6 | class FileCleanupMixin(object): 7 | def tearDown(self): 8 | for i in DjanguiFile.objects.all(): 9 | utils.get_storage().delete(i.filepath.path) 10 | # delete job dirs 11 | local_storage = utils.get_storage(local=True) 12 | for i in DjanguiJob.objects.all(): 13 | shutil.rmtree(local_storage.path(i.get_output_path())) 14 | super(FileCleanupMixin, self).tearDown() 15 | 16 | class ScriptFactoryMixin(object): 17 | def tearDown(self): 18 | for i in Script.objects.all(): 19 | path = i.script_path.path 20 | utils.get_storage().delete(path) 21 | path += 'c' # handle pyc junk 22 | utils.get_storage().delete(path) 23 | super(ScriptFactoryMixin, self).tearDown() -------------------------------------------------------------------------------- /djangui/templates/djangui/modals/base_modal.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | -------------------------------------------------------------------------------- /djangui/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..backend import utils 4 | from ..forms import DjanguiForm 5 | 6 | from . import factories 7 | from . import config 8 | from . import mixins 9 | 10 | 11 | class FormTestCase(mixins.ScriptFactoryMixin, mixins.FileCleanupMixin, TestCase): 12 | 13 | def test_master_form(self): 14 | script = factories.TranslateScriptFactory() 15 | form = utils.get_master_form(model=script) 16 | assert(isinstance(form, DjanguiForm) is True) 17 | utils.validate_form(form=form, data=config.SCRIPT_DATA['translate'].get('data'), 18 | files=config.SCRIPT_DATA['translate'].get('files')) 19 | assert(form.is_valid() is True) 20 | 21 | def test_group_form(self): 22 | script = factories.TranslateScriptFactory() 23 | form = utils.get_form_groups(model=script) 24 | self.assertEqual(len(form['groups']), 1) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /djangui/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.contrib.admin import ModelAdmin, site 3 | 4 | from .models import Script, ScriptGroup, ScriptParameter, DjanguiJob, ScriptParameterGroup, DjanguiFile 5 | 6 | class JobAdmin(ModelAdmin): 7 | list_display = ('user', 'job_name', 'script', 'status', 'created_date') 8 | 9 | class ScriptAdmin(ModelAdmin): 10 | list_display = ('script_name', 'script_group', 'is_active', 'script_version') 11 | 12 | class ParameterAdmin(ModelAdmin): 13 | list_display = ('script', 'parameter_group', 'short_param') 14 | 15 | class GroupAdmin(ModelAdmin): 16 | list_display = ('group_name', 'is_active') 17 | 18 | class ParameterGroupAdmin(ModelAdmin): 19 | list_display = ('script', 'group_name') 20 | 21 | class FileAdmin(ModelAdmin): 22 | pass 23 | 24 | site.register(DjanguiJob, JobAdmin) 25 | site.register(DjanguiFile, FileAdmin) 26 | site.register(Script, ScriptAdmin) 27 | site.register(ScriptParameter, ParameterAdmin) 28 | site.register(ScriptGroup, GroupAdmin) 29 | site.register(ScriptParameterGroup, ParameterGroupAdmin) -------------------------------------------------------------------------------- /djangui/migrations/0003_populate_from_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import autoslug.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('djangui', '0002_remove_scriptparameter_output_path'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='script', 17 | name='slug', 18 | field=autoslug.fields.AutoSlugField(populate_from='script_name', unique=True, editable=False), 19 | ), 20 | migrations.AlterField( 21 | model_name='scriptgroup', 22 | name='slug', 23 | field=autoslug.fields.AutoSlugField(populate_from='group_name', unique=True, editable=False), 24 | ), 25 | migrations.AlterField( 26 | model_name='scriptparameter', 27 | name='slug', 28 | field=autoslug.fields.AutoSlugField(populate_from='script_param', unique=True, editable=False), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /djangui/tests/scripts/gaussian.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | __author__ = 'chris' 3 | import argparse 4 | import sys 5 | import math 6 | from matplotlib import pyplot as plt 7 | import numpy as np 8 | 9 | parser = argparse.ArgumentParser(description="This will plot a gaussian distribution with the given parameters.") 10 | parser.add_argument('--mean', help='The mean of the gaussian.', type=float, required=True) 11 | parser.add_argument('--std', help='The standard deviation (width) of the gaussian.', type=float, required=True) 12 | 13 | def main(): 14 | args = parser.parse_args() 15 | u = args.mean 16 | s = abs(args.std) 17 | variance = s**2 18 | amplitude = 1/(s*math.sqrt(2*math.pi)) 19 | fit = lambda x: [amplitude*math.exp((-1*(xi-u)**2)/(2*variance)) for xi in x] 20 | # plot +- 4 standard deviations 21 | X = np.linspace(u-4*s, u+4*s, 100) 22 | Y = fit(X) 23 | plt.plot(X, Y) 24 | plt.title('Gaussian distribution with mu={0:.2f}, sigma={1:.2f}'.format(u, s)) 25 | plt.savefig('gaussian.png') 26 | 27 | if __name__ == "__main__": 28 | sys.exit(main()) 29 | -------------------------------------------------------------------------------- /djangui/django_compat.py: -------------------------------------------------------------------------------- 1 | from . version import DJANGO_VERSION, DJ18, DJ17, DJ16 2 | 3 | if DJANGO_VERSION < DJ17: 4 | import json 5 | from django.http import HttpResponse 6 | 7 | class JsonResponse(HttpResponse): 8 | """ 9 | JSON response 10 | # from https://gist.github.com/philippeowagner/3179eb475fe1795d6515 11 | """ 12 | def __init__(self, content, mimetype='application/json', status=None, content_type=None, **kwargs): 13 | super(JsonResponse, self).__init__( 14 | content=json.dumps(content), 15 | mimetype=mimetype, 16 | status=status, 17 | content_type=content_type, 18 | ) 19 | else: 20 | from django.http import JsonResponse 21 | 22 | if DJANGO_VERSION >= DJ18: 23 | from django.template import Engine 24 | else: 25 | from django.template import Template 26 | from django.conf import settings 27 | try: 28 | settings.configure() 29 | except RuntimeError: 30 | pass 31 | 32 | class Engine(object): 33 | 34 | @staticmethod 35 | def from_string(code): 36 | return Template(code) -------------------------------------------------------------------------------- /djangui/tests/scripts/crop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = 'chris' 4 | import argparse 5 | import sys 6 | from PIL import Image 7 | 8 | parser = argparse.ArgumentParser(description="Crop images") 9 | parser.add_argument('--image', help='The image to crop', type=argparse.FileType('r'), required=True) 10 | parser.add_argument('--left', help='The number of pixels to crop from the left', type=int, default=0) 11 | parser.add_argument('--right', help='The number of pixels to crop from the right', type=int, default=0) 12 | parser.add_argument('--top', help='The number of pixels to crop from the top', type=int, default=0) 13 | parser.add_argument('--bottom', help='The number of pixels to crop from the bottom', type=int, default=0) 14 | parser.add_argument('--save', help='Where to save the new image', type=argparse.FileType('w'), required=True) 15 | 16 | def main(): 17 | args = parser.parse_args() 18 | im = Image.open(args.image) 19 | width, height = im.size 20 | right = width-args.right 21 | bottom = height-args.bottom 22 | new = im.crop((args.left, args.top, right, bottom)) 23 | new.save(args.save) 24 | 25 | if __name__ == "__main__": 26 | sys.exit(main()) 27 | -------------------------------------------------------------------------------- /djangui/djanguistorage.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from boto.utils import parse_ts 4 | from django.core.files.storage import get_storage_class 5 | from storages.backends.s3boto import S3BotoStorage 6 | 7 | from . import settings as djangui_settings 8 | 9 | # From https://github.com/jezdez/django_compressor/issues/100 10 | 11 | class CachedS3BotoStorage(S3BotoStorage): 12 | def __init__(self, *args, **kwargs): 13 | super(CachedS3BotoStorage, self).__init__(*args, **kwargs) 14 | self.local_storage = get_storage_class('django.core.files.storage.FileSystemStorage')() 15 | 16 | def _open(self, name, mode='rb'): 17 | original_file = super(CachedS3BotoStorage, self)._open(name, mode=mode) 18 | if name.endswith('.gz'): 19 | return original_file 20 | return original_file 21 | 22 | def modified_time(self, name): 23 | name = self._normalize_name(self._clean_name(name)) 24 | entry = self.entries.get(name) 25 | if entry is None: 26 | entry = self.bucket.get_key(self._encode_name(name)) 27 | # Parse the last_modified string to a local datetime object. 28 | return parse_ts(entry.last_modified) 29 | 30 | def path(self, name): 31 | return self.local_storage.path(name) -------------------------------------------------------------------------------- /djangui/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import os 3 | import six 4 | 5 | from django.contrib.auth import get_user_model 6 | 7 | from ..models import DjanguiJob, ScriptGroup, Script, ScriptParameter, ScriptParameterGroup, ScriptParameters 8 | 9 | from . import config 10 | 11 | class ScriptGroupFactory(factory.DjangoModelFactory): 12 | class Meta: 13 | model = ScriptGroup 14 | 15 | group_name = 'test group' 16 | group_description = 'test desc' 17 | 18 | class ScriptFactory(factory.DjangoModelFactory): 19 | class Meta: 20 | model = Script 21 | 22 | script_name = 'test script' 23 | script_group = factory.SubFactory(ScriptGroupFactory) 24 | script_description = 'test script desc' 25 | 26 | class TranslateScriptFactory(ScriptFactory): 27 | 28 | script_path = factory.django.FileField(from_path=os.path.join(config.DJANGUI_TEST_SCRIPTS, 'translate.py')) 29 | 30 | class UserFactory(factory.DjangoModelFactory): 31 | class Meta: 32 | model = get_user_model() 33 | 34 | username = 'user' 35 | email = 'a@a.com' 36 | password = 'testuser' 37 | 38 | class JobFactory(factory.DjangoModelFactory): 39 | class Meta: 40 | model = DjanguiJob 41 | 42 | script = factory.SubFactory(TranslateScriptFactory) 43 | job_name = six.u('\xd0\xb9\xd1\x86\xd1\x83') 44 | job_description = six.u('\xd0\xb9\xd1\x86\xd1\x83\xd0\xb5\xd0\xba\xd0\xb5') -------------------------------------------------------------------------------- /djangui/signals.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django.db.models.signals import post_delete 4 | from django.db.utils import InterfaceError, DatabaseError 5 | from django import db 6 | 7 | from celery.signals import task_postrun, task_prerun, task_revoked 8 | 9 | 10 | @task_postrun.connect 11 | @task_prerun.connect 12 | def task_completed(sender=None, **kwargs): 13 | task_kwargs = kwargs.get('kwargs') 14 | job_id = task_kwargs.get('djangui_job') 15 | from .models import DjanguiJob 16 | from celery import states 17 | try: 18 | job = DjanguiJob.objects.get(pk=job_id) 19 | except (InterfaceError, DatabaseError) as e: 20 | db.connection.close() 21 | job = DjanguiJob.objects.get(pk=job_id) 22 | state = kwargs.get('state') 23 | if state: 24 | job.status = DjanguiJob.COMPLETED if state == states.SUCCESS else state 25 | job.celery_id = kwargs.get('task_id') 26 | job.save() 27 | 28 | def reload_scripts(**kwargs): 29 | from .backend import utils 30 | utils.load_scripts() 31 | 32 | # TODO: Figure out why relative imports fail here 33 | from .models import Script, ScriptGroup, ScriptParameter, ScriptParameterGroup 34 | post_delete.connect(reload_scripts, sender=Script) 35 | post_delete.connect(reload_scripts, sender=ScriptGroup) 36 | post_delete.connect(reload_scripts, sender=ScriptParameter) 37 | post_delete.connect(reload_scripts, sender=ScriptParameterGroup) -------------------------------------------------------------------------------- /djangui/tests/test_scripts.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | 5 | from . import config, mixins 6 | from ..backend import utils 7 | from .. import settings as djangui_settings 8 | 9 | class ScriptAdditionTests(mixins.ScriptFactoryMixin, TestCase): 10 | 11 | def test_command_order(self): 12 | script = os.path.join(config.DJANGUI_TEST_SCRIPTS, 'command_order.py') 13 | new_file = utils.get_storage(local=True).save(os.path.join(djangui_settings.DJANGUI_SCRIPT_DIR, 'command_order.py'), open(script)) 14 | new_file = utils.get_storage(local=True).path(new_file) 15 | added, errors = utils.add_djangui_script(script=new_file, group=None) 16 | self.assertEqual(added, True, errors) 17 | job = utils.create_djangui_job(script_pk=1, data={'job_name': 'abc', 'link': 'alink', 'name': 'aname'}) 18 | # These are positional arguments -- we DO NOT want them returning anything 19 | self.assertEqual(['', ''], [i.parameter.short_param for i in job.get_parameters()]) 20 | # These are the params shown to the user, we want them returning their destination 21 | # This also checks that we maintain the expected order 22 | self.assertEqual(['link', 'name'], [i.parameter.script_param for i in job.get_parameters()]) 23 | # Check the job command 24 | commands = utils.get_job_commands(job=job)[2:] 25 | self.assertEqual(['alink', 'aname'], commands) 26 | -------------------------------------------------------------------------------- /djangui/test_settings.py: -------------------------------------------------------------------------------- 1 | # copied from django-compressor since I like their style 2 | import os 3 | import django 4 | DEBUG = True 5 | 6 | TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests') 7 | 8 | 9 | CACHES = { 10 | 'default': { 11 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 12 | 'LOCATION': 'unique-snowflake' 13 | } 14 | } 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': ':memory:', 20 | } 21 | } 22 | 23 | INSTALLED_APPS = [ 24 | 'django.contrib.admin', 25 | 'django.contrib.auth', 26 | 'django.contrib.contenttypes', 27 | 'django.contrib.sessions', 28 | 'django.contrib.messages', 29 | 'django.contrib.staticfiles', 30 | 'djangui', 31 | ] 32 | 33 | 34 | STATICFILES_FINDERS = [ 35 | 'django.contrib.staticfiles.finders.FileSystemFinder', 36 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 37 | ] 38 | 39 | STATIC_URL = '/static/' 40 | 41 | 42 | STATIC_ROOT = os.path.join(TEST_DIR, 'static') 43 | 44 | MEDIA_URL = '/files/' 45 | MEDIA_ROOT = os.path.join(TEST_DIR, 'media') 46 | 47 | if django.VERSION[:2] < (1, 6): 48 | TEST_RUNNER = 'discover_runner.DiscoverRunner' 49 | 50 | SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!" 51 | 52 | PASSWORD_HASHERS = ( 53 | 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', 54 | ) 55 | 56 | MIDDLEWARE_CLASSES = [] 57 | 58 | ROOT_URLCONF = 'djangui.urls' -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 5 | README = readme.read() 6 | 7 | # allow setup.py to be run from any path 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | name='django-djangui', 12 | version='0.2.8', 13 | packages=find_packages(), 14 | scripts=['scripts/djanguify'], 15 | entry_points={'console_scripts': ['djanguify = djangui.backend.command_line:bootstrap',]}, 16 | install_requires = ['Django>=1.6', 'django-autoslug', 'django-celery', 'six'], 17 | include_package_data=True, 18 | license='GPLv3', 19 | description='A Django app which creates a web GUI and task interface for argparse scripts', 20 | url='http://www.github.com/chris7/django-djangui', 21 | author='Chris Mitchell', 22 | author_email='chris.mit7@gmail.com', 23 | classifiers=[ 24 | 'Environment :: Web Environment', 25 | 'Framework :: Django', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | 'Topic :: Internet :: WWW/HTTP', 35 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 36 | ], 37 | ) 38 | 39 | -------------------------------------------------------------------------------- /tests/test_project.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | from unittest import TestCase 3 | import os 4 | import subprocess 5 | import shutil 6 | import sys 7 | 8 | BASE_DIR = os.path.split(__file__)[0] 9 | DJANGUI_SCRIPT_PATH = os.path.join(BASE_DIR, '..', 'scripts', 'djanguify') 10 | DJANGUI_TEST_PROJECT_NAME = 'djangui_project' 11 | DJANGUI_TEST_PROJECT_PATH = os.path.join(BASE_DIR, DJANGUI_TEST_PROJECT_NAME) 12 | DJANGUI_TEST_PROJECT_MANAGE = os.path.join(DJANGUI_TEST_PROJECT_PATH, 'manage.py') 13 | PYTHON_INTERPRETTER = sys.executable 14 | 15 | env = os.environ 16 | env['DJANGO_SETTINGS_MODULE'] = '{}.settings'.format(DJANGUI_TEST_PROJECT_NAME) 17 | env['TESTING'] = 'True' 18 | 19 | class TestProject(TestCase): 20 | def setUp(self): 21 | os.chdir(BASE_DIR) 22 | # if old stuff exists, remove it 23 | if os.path.exists(DJANGUI_TEST_PROJECT_PATH): 24 | shutil.rmtree(DJANGUI_TEST_PROJECT_PATH) 25 | 26 | def tearDown(self): 27 | os.chdir(BASE_DIR) 28 | if os.path.exists(DJANGUI_TEST_PROJECT_PATH): 29 | shutil.rmtree(DJANGUI_TEST_PROJECT_PATH) 30 | 31 | def test_bootstrap(self): 32 | from djangui.backend import command_line 33 | sys.argv = [DJANGUI_SCRIPT_PATH, '-p', DJANGUI_TEST_PROJECT_NAME] 34 | ret = command_line.bootstrap(env=env, cwd=BASE_DIR) 35 | self.assertIsNone(ret) 36 | # test our script is executable from the command line, it will fail with return code of 1 since 37 | # the project already exists 38 | proc = subprocess.Popen([PYTHON_INTERPRETTER, DJANGUI_SCRIPT_PATH, '-p', DJANGUI_TEST_PROJECT_NAME]) 39 | stdout, stderr = proc.communicate() 40 | self.assertEqual(proc.returncode, 1, stderr) -------------------------------------------------------------------------------- /djangui/templates/djangui/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "djangui/base.html" %} 2 | {% load i18n %} 3 | {% block center_content %} 4 | {% if form.non_field_errors %} 5 | 6 | {% endif %} 7 |
8 | {% csrf_token %} 9 |
10 | 11 |
12 | 13 | {% if 'username' in form.errors %} 14 | 15 | {% endif %} 16 |
17 |
18 |
19 | 20 |
21 | 22 | {% if 'password' in form.errors %} 23 | 24 | {% endif %} 25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | {% endblock center_content %} -------------------------------------------------------------------------------- /djangui/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from django.conf.urls import include, url 4 | from django.conf import settings 5 | from django.conf.urls.static import static 6 | 7 | # from djangui.admin import 8 | from .views import (celery_status, CeleryTaskView, celery_task_command, DjanguiScriptJSON, 9 | DjanguiHomeView, DjanguiRegister, djangui_login, DjanguiProfileView) 10 | 11 | from . import settings as djangui_settings 12 | 13 | djangui_patterns = [ 14 | url(r'^celery/command$', celery_task_command, name='celery_task_command'), 15 | url(r'^celery/status$', celery_status, name='celery_results'), 16 | url(r'^celery/(?P[a-zA-Z0-9\-]+)/$', CeleryTaskView.as_view(), name='celery_results_info'), 17 | # url(r'^admin/', include(djangui_admin.urls)), 18 | url(r'^djscript/(?P[a-zA-Z0-9\-\_]+)/(?P[a-zA-Z0-9\-\_]+)/(?P[a-zA-Z0-9\-]+)$', 19 | DjanguiScriptJSON.as_view(), name='djangui_script_clone'), 20 | url(r'^djscript/(?P[a-zA-Z0-9\-\_]+)/(?P[a-zA-Z0-9\-\_]+)/$', DjanguiScriptJSON.as_view(), name='djangui_script'), 21 | url(r'^profile/$', DjanguiProfileView.as_view(), name='profile_home'), 22 | url(r'^$', DjanguiHomeView.as_view(), name='djangui_home'), 23 | url(r'^$', DjanguiHomeView.as_view(), name='djangui_task_launcher'), 24 | url('^{}'.format(djangui_settings.DJANGUI_LOGIN_URL.lstrip('/')), djangui_login, name='djangui_login'), 25 | url('^{}'.format(djangui_settings.DJANGUI_REGISTER_URL.lstrip('/')), DjanguiRegister.as_view(), name='djangui_register'), 26 | ] 27 | 28 | urlpatterns = [ 29 | url('^', include(djangui_patterns, namespace='djangui')), 30 | url('^', include('django.contrib.auth.urls')), 31 | ] -------------------------------------------------------------------------------- /djangui/views/mixins.py: -------------------------------------------------------------------------------- 1 | # __author__ = 'chris' 2 | # from django.views.generic import CreateView, UpdateView 3 | # from django.forms import modelform_factory 4 | # from django.core.urlresolvers import reverse_lazy 5 | # from django.conf import settings 6 | # 7 | # 8 | # class DjanguiScriptMixin(object): 9 | # def dispatch(self, request, *args, **kwargs): 10 | # import pdb; pdb.set_trace(); 11 | # self.script_name = kwargs.pop('script_name', None) 12 | # if not self.script_name: 13 | # return super(DjanguiScriptMixin, self).dispatch(request, *args, **kwargs) 14 | # klass = getattr(self.djangui_models, self.script_name) 15 | # self.model = klass 16 | # return super(DjanguiScriptMixin, self).dispatch(request, *args, **kwargs) 17 | # 18 | # def get_queryset(self): 19 | # klass = getattr(self.djangui_models, self.script_name) 20 | # return klass.objects.all() 21 | # 22 | # def get_context_data(self, **kwargs): 23 | # ctx = super(DjanguiScriptMixin, self).get_context_data(**kwargs) 24 | # ctx['script_name'] = self.script_name 25 | # return ctx 26 | # 27 | # def get_form_class(self): 28 | # return modelform_factory(self.model, fields=self.fields, exclude=('djangui_script_name', 'djangui_celery_id', 'djangui_celery_state')) 29 | # 30 | # 31 | # class DjanguiScriptEdit(DjanguiScriptMixin, UpdateView): 32 | # template_name = 'generic_script_view.html' 33 | # 34 | # 35 | # class DjanguiScriptCreate(DjanguiScriptMixin, CreateView): 36 | # template_name = 'generic_script_create.html' 37 | # 38 | # def get_success_url(self): 39 | # return reverse_lazy(getattr(settings, 'POST_SCRIPT_URL', '{}_home'.format(self.app_name))) 40 | # 41 | # def get_form_class(self): 42 | # return modelform_factory(self.model, fields=self.fields, exclude=('djangui_user', 'djangui_script_name', 'djangui_celery_id', 'djangui_celery_state')) 43 | # 44 | -------------------------------------------------------------------------------- /djangui/models/mixins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | __author__ = 'chris' 3 | from django.forms.models import model_to_dict 4 | import six 5 | 6 | class UpdateScriptsMixin(object): 7 | def save(self, **kwargs): 8 | super(UpdateScriptsMixin, self).save(**kwargs) 9 | from ..backend.utils import load_scripts 10 | load_scripts() 11 | 12 | 13 | class DjanguiPy2Mixin(object): 14 | def __unicode__(self): 15 | return unicode(self.__str__()) 16 | 17 | # from 18 | # http://stackoverflow.com/questions/1355150/django-when-saving-how-can-you-check-if-a-field-has-changed 19 | class ModelDiffMixin(object): 20 | """ 21 | A model mixin that tracks model fields' values and provide some useful api 22 | to know what fields have been changed. 23 | """ 24 | 25 | def __init__(self, *args, **kwargs): 26 | super(ModelDiffMixin, self).__init__(*args, **kwargs) 27 | self.__initial = self._dict 28 | 29 | @property 30 | def diff(self): 31 | d1 = self.__initial 32 | d2 = self._dict 33 | diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]] 34 | return dict(diffs) 35 | 36 | @property 37 | def has_changed(self): 38 | return bool(self.diff) 39 | 40 | @property 41 | def changed_fields(self): 42 | return self.diff.keys() 43 | 44 | def get_field_diff(self, field_name): 45 | """ 46 | Returns a diff for field if it's changed and None otherwise. 47 | """ 48 | return self.diff.get(field_name, None) 49 | 50 | def save(self, *args, **kwargs): 51 | """ 52 | Saves model and set initial state. 53 | """ 54 | super(ModelDiffMixin, self).save(*args, **kwargs) 55 | self.__initial = self._dict 56 | 57 | @property 58 | def _dict(self): 59 | return model_to_dict(self, fields=[field.name for field in 60 | self._meta.fields]) -------------------------------------------------------------------------------- /djangui/tests/scripts/heatmap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = 'chris' 4 | import argparse 5 | import os 6 | import sys 7 | import pandas as pd 8 | import seaborn as sns 9 | import numpy as np 10 | 11 | parser = argparse.ArgumentParser(description="Create a heatmap from a delimited file.") 12 | parser.add_argument('--tsv', help='The delimited file to plot.', type=argparse.FileType('r'), required=True) 13 | parser.add_argument('--delimiter', help='The delimiter for fields. Default: tab', type=str, default='\t') 14 | parser.add_argument('--row', help='The column containing row to create a heatmap from. Default to first row.', type=str) 15 | parser.add_argument('--cols', help='The columns to choose values from (separate by a comma for multiple). Default: All non-rows', type=str) 16 | parser.add_argument('--log-normalize', help='Whether to log normalize data.', action='store_true') 17 | 18 | def main(): 19 | args = parser.parse_args() 20 | data = pd.read_table(args.tsv, index_col=args.row if args.row else 0, sep=args.delimiter, encoding='utf-8') 21 | if args.cols: 22 | try: 23 | data = data.loc[:,args.cols.split(',')] 24 | except KeyError: 25 | data = data.iloc[:,[int(i)-1 for i in args.cols.split(',')]] 26 | if len(data.columns) > 50: 27 | raise BaseException('Too many columns') 28 | data = np.log2(data) if args.log_normalize else data 29 | data[data==-1*np.inf] = data[data!=-1*np.inf].min().min() 30 | width = 5+0 if len(data.columns)<50 else (len(data.columns)-50)/100 31 | row_cutoff = 1000 32 | height = 15+0 if len(data)2.0 32 | img[ix[rem], iy[rem]] = i+1 33 | rem = -rem 34 | z = z[rem] 35 | ix, iy = ix[rem], iy[rem] 36 | c = c[rem] 37 | 38 | plt.imshow(img) 39 | plt.savefig('fractal.png') 40 | 41 | def limit(value, min, max, default): 42 | if value < min or value > max: 43 | return default 44 | return value 45 | 46 | if __name__ == '__main__': 47 | args = parser.parse_args() 48 | height = limit(args.height, 1, 2000, 400) 49 | width = limit(args.width, 1, 2000, 400) 50 | mandel(args.height, args.width, 100, args.xmin, args.xmax, args.ymin, args.ymax) -------------------------------------------------------------------------------- /djangui/management/commands/addscript.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chris' 2 | import os 3 | import sys 4 | from django.core.management.base import BaseCommand, CommandError 5 | from django.core.files import File 6 | from django.core.files.storage import default_storage 7 | from django.conf import settings 8 | 9 | from ...backend.utils import add_djangui_script 10 | from ... import settings as djangui_settings 11 | 12 | 13 | class Command(BaseCommand): 14 | help = 'Adds a script to Djangui' 15 | 16 | def add_arguments(self, parser): 17 | parser.add_argument('script', type=str, help='A script or folder of scripts to add to Djangui.') 18 | parser.add_argument('--group', 19 | dest='group', 20 | default='Djangui Scripts', 21 | help='The name of the group to create scripts under. Default: Djangui Scripts') 22 | 23 | def handle(self, *args, **options): 24 | script = options.get('script') 25 | if not script: 26 | if len(args): 27 | script = args[0] 28 | else: 29 | raise CommandError('You must provide a script path or directory containing scripts.') 30 | if not os.path.exists(script): 31 | raise CommandError('{0} does not exist.'.format(script)) 32 | group = options.get('group', 'Djangui Scripts') 33 | scripts = [os.path.join(script, i) for i in os.listdir(script)] if os.path.isdir(script) else [script] 34 | converted = 0 35 | for script in scripts: 36 | if script.endswith('.pyc') or '__init__' in script: 37 | continue 38 | if script.endswith('.py'): 39 | sys.stdout.write('Converting {}\n'.format(script)) 40 | # copy the script to our storage 41 | with open(script, 'r') as f: 42 | script = default_storage.save(os.path.join(djangui_settings.DJANGUI_SCRIPT_DIR, os.path.split(script)[1]), File(f)) 43 | added, error = add_djangui_script(script=os.path.abspath(os.path.join(settings.MEDIA_ROOT, script)), group=group) 44 | if added: 45 | converted += 1 46 | sys.stdout.write('Converted {} scripts\n'.format(converted)) -------------------------------------------------------------------------------- /djangui/tests/scripts/fetch_cats.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = 'Chris Mitchell' 4 | 5 | # Code modified from http://stackoverflow.com/questions/20718819/downloading-images-from-google-search-using-python-gives-error 6 | 7 | import argparse 8 | import sys 9 | import os 10 | import imghdr 11 | from urllib import FancyURLopener 12 | import urllib2 13 | import json 14 | 15 | description = """ 16 | This will find you cats, and optionally, kitties. 17 | """ 18 | 19 | parser = argparse.ArgumentParser(description = description) 20 | parser.add_argument('--count', help='The number of cats to find (max: 10)', type=int, default=1) 21 | parser.add_argument('--kittens', help='Search for kittens.', action='store_true') 22 | parser.add_argument('--breed', help='The breed of cat to find', type=str, choices=('lol', 'tabby', 'bengal', 'scottish', 'grumpy')) 23 | 24 | 25 | # Start FancyURLopener with defined version 26 | class MyOpener(FancyURLopener): 27 | version = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11)Gecko/20071127 Firefox/2.0.0.11' 28 | 29 | myopener = MyOpener() 30 | 31 | 32 | def main(): 33 | args = parser.parse_args() 34 | searchTerm = 'kittens' if args.kittens else 'cats' 35 | cat_count = args.count if args.count < 10 else 10 36 | if args.breed: 37 | searchTerm += '%20{0}'.format(args.breed) 38 | 39 | # Notice that the start changes for each iteration in order to request a new set of images for each loop 40 | found = 0 41 | i = 0 42 | downloaded = set([]) 43 | while found <= cat_count: 44 | i += 1 45 | template = 'https://ajax.googleapis.com/ajax/services/search/images?v=1.0&q={}&start={}&userip=MyIP' 46 | url = template.format(searchTerm, i+1) 47 | request = urllib2.Request(url, None, {'Referer': 'testing'}) 48 | response = urllib2.urlopen(request) 49 | 50 | # Get results using JSON 51 | results = json.load(response) 52 | data = results['responseData'] 53 | dataInfo = data['results'] 54 | 55 | # Iterate for each result and get unescaped url 56 | for count, myUrl in enumerate(dataInfo, 1): 57 | if found > cat_count: 58 | break 59 | uurl = myUrl['unescapedUrl'] 60 | filename = uurl.split('/')[-1] 61 | if filename in downloaded: 62 | continue 63 | myopener.retrieve(uurl, filename) 64 | if imghdr.what(filename): 65 | found += 1 66 | downloaded.add(filename) 67 | else: 68 | os.remove(filename) 69 | 70 | 71 | if __name__ == "__main__": 72 | sys.exit(main()) 73 | -------------------------------------------------------------------------------- /djangui/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | 5 | from ..backend import utils 6 | 7 | from . import factories 8 | from . import config 9 | from . import mixins 10 | 11 | class TestUtils(mixins.ScriptFactoryMixin, TestCase): 12 | def test_sanitize_name(self): 13 | assert(utils.sanitize_name('abc')) == 'abc' 14 | assert(utils.sanitize_name('ab c')) == 'ab_c' 15 | assert(utils.sanitize_name('ab-c')) == 'ab_c' 16 | 17 | def test_sanitize_string(self): 18 | assert(utils.sanitize_string('ab"c')) == 'ab\\"c' 19 | 20 | def test_add_script(self): 21 | pass 22 | # TODO: fix me 23 | # utils.add_djangui_script(script=os.path.join(config.DJANGUI_TEST_SCRIPTS, 'translate.py')) 24 | 25 | def test_anonymous_users(self): 26 | from .. import settings as djangui_settings 27 | from django.contrib.auth.models import AnonymousUser 28 | user = AnonymousUser() 29 | script = factories.TranslateScriptFactory() 30 | d = utils.valid_user(script, user) 31 | self.assertTrue(d['valid']) 32 | djangui_settings.DJANGUI_ALLOW_ANONYMOUS = False 33 | d = utils.valid_user(script, user) 34 | self.assertFalse(d['valid']) 35 | 36 | def test_valid_user(self): 37 | user = factories.UserFactory() 38 | script = factories.TranslateScriptFactory() 39 | d = utils.valid_user(script, user) 40 | self.assertTrue(d['valid']) 41 | from .. import settings as djangui_settings 42 | self.assertEqual('disabled', d['display']) 43 | djangui_settings.DJANGUI_SHOW_LOCKED_SCRIPTS = False 44 | d = utils.valid_user(script, user) 45 | self.assertEqual('hide', d['display']) 46 | from django.contrib.auth.models import Group 47 | test_group = Group(name='test') 48 | test_group.save() 49 | script.user_groups.add(test_group) 50 | d = utils.valid_user(script, user) 51 | self.assertFalse(d['valid']) 52 | user.groups.add(test_group) 53 | d = utils.valid_user(script, user) 54 | self.assertTrue(d['valid']) 55 | 56 | 57 | class TestFileDetectors(TestCase): 58 | def test_detector(self): 59 | self.file = os.path.join(config.DJANGUI_TEST_DATA, 'fasta.fasta') 60 | res, preview = utils.test_fastx(self.file) 61 | self.assertEqual(res, True, 'Fastx parser fail') 62 | self.assertEqual(preview, open(self.file).readlines(), 'Fastx Preview Fail') 63 | 64 | def test_delimited(self): 65 | self.file = os.path.join(config.DJANGUI_TEST_DATA, 'delimited.tsv') 66 | res, preview = utils.test_delimited(self.file) 67 | self.assertEqual(res, True, 'Delimited parser fail') 68 | self.assertEqual(preview, [i.strip().split('\t') for i in open(self.file).readlines()], 'Delimited Preview Fail') 69 | 70 | -------------------------------------------------------------------------------- /djangui/templates/djangui/registration/register.html: -------------------------------------------------------------------------------- 1 | {% extends "djangui/base.html" %} 2 | {% load i18n %} 3 | {% block center_content %} 4 | {% if form.non_field_errors %} 5 | 6 | {% endif %} 7 |
8 | {% csrf_token %} 9 |
10 | 11 |
12 | 13 | {% if 'username' in form.errors %} 14 | 15 | {% endif %} 16 |
17 |
18 |
19 | 20 |
21 | 22 | {% if 'email' in form.errors %} 23 | 24 | {% endif %} 25 |
26 |
27 |
28 | 29 |
30 | 31 | {% if 'password' in form.errors %} 32 | 33 | {% endif %} 34 |
35 |
36 |
37 | 38 |
39 | 40 | {% if 'password2' in form.errors %} 41 | 42 | {% endif %} 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 | {% endblock center_content %} -------------------------------------------------------------------------------- /djangui/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase, Client 4 | 5 | from . import factories, config, mixins 6 | from .. import version 7 | 8 | class ScriptTestCase(mixins.ScriptFactoryMixin, TestCase): 9 | 10 | def test_script_creation(self): 11 | script = factories.TranslateScriptFactory() 12 | 13 | 14 | class ScriptGroupTestCase(TestCase): 15 | 16 | def test_script_group_creation(self): 17 | group = factories.ScriptGroupFactory() 18 | 19 | 20 | class TestJob(mixins.ScriptFactoryMixin, mixins.FileCleanupMixin, TestCase): 21 | urls = 'djangui.test_urls' 22 | 23 | def test_jobs(self): 24 | script = factories.TranslateScriptFactory() 25 | from ..backend import utils 26 | from .. import settings 27 | # the test server doesn't have celery running 28 | settings.DJANGUI_CELERY = False 29 | job = utils.create_djangui_job(script_pk=script.pk, data={'job_name': 'abc', 'sequence': 'aaa', 'out': 'abc'}) 30 | job = job.submit_to_celery() 31 | old_pk = job.pk 32 | new_job = job.submit_to_celery(resubmit=True) 33 | self.assertNotEqual(old_pk, new_job.pk) 34 | # test rerunning, our output should be removed 35 | from ..models import DjanguiFile 36 | old_output = sorted([i.pk for i in DjanguiFile.objects.filter(job=new_job)]) 37 | job.submit_to_celery(rerun=True) 38 | # check that we overwrite our output 39 | new_output = sorted([i.pk for i in DjanguiFile.objects.filter(job=new_job)]) 40 | # Django 1.6 has a bug where they are reusing pk numbers 41 | if version.DJANGO_VERSION >= version.DJ17: 42 | self.assertNotEqual(old_output, new_output) 43 | self.assertEqual(len(old_output), len(new_output)) 44 | # check the old entries are gone 45 | if version.DJANGO_VERSION >= version.DJ17: 46 | # Django 1.6 has a bug where they are reusing pk numbers, so once again we cannot use this check 47 | self.assertEqual([], list(DjanguiFile.objects.filter(pk__in=old_output))) 48 | 49 | # check our download links are ok 50 | file_previews = utils.get_file_previews(job) 51 | for group, files in file_previews.items(): 52 | for fileinfo in files: 53 | response = Client().get(fileinfo.get('url')) 54 | self.assertEqual(response.status_code, 200) 55 | 56 | job = utils.create_djangui_job(script_pk=script.pk, 57 | data={'fasta': open(os.path.join(config.DJANGUI_TEST_DATA, 'fasta.fasta')), 58 | 'out': 'abc', 'job_name': 'abc'}) 59 | 60 | # check our upload link is ok 61 | file_previews = utils.get_file_previews(job) 62 | for group, files in file_previews.items(): 63 | for fileinfo in files: 64 | response = Client().get(fileinfo.get('url')) 65 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /djangui/views/authentication.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.views.generic import CreateView 3 | from django.http import HttpResponseRedirect 4 | from django.core.urlresolvers import reverse 5 | from django.forms.models import modelform_factory 6 | from django.contrib.auth import login, authenticate, get_user_model 7 | from django.utils.translation import gettext_lazy as _ 8 | from django.utils.encoding import force_text 9 | 10 | from .. import settings as djangui_settings 11 | from ..django_compat import JsonResponse 12 | 13 | class DjanguiRegister(CreateView): 14 | template_name = 'djangui/registration/register.html' 15 | model = get_user_model() 16 | fields = ('username', 'email', 'password') 17 | 18 | def dispatch(self, request, *args, **kwargs): 19 | if djangui_settings.DJANGUI_AUTH is False: 20 | return HttpResponseRedirect(djangui_settings.DJANGUI_REGISTER_URL) 21 | return super(DjanguiRegister, self).dispatch(request, *args, **kwargs) 22 | 23 | def post(self, request, *args, **kwargs): 24 | self.object = None 25 | form = self.get_form_class() 26 | post = request.POST.copy() 27 | post['username'] = post['username'].lower() 28 | form = form(post) 29 | if request.POST['password'] != request.POST['password2']: 30 | form.add_error('password', _('Passwords do not match.')) 31 | if request.POST['username'].lower() == 'admin': 32 | form.add_error('username', _('Reserved username.')) 33 | if not request.POST['email']: 34 | form.add_error('email', _('Please enter your email address.')) 35 | if form.is_valid(): 36 | return self.form_valid(form) 37 | else: 38 | return self.form_invalid(form) 39 | 40 | def get_success_url(self): 41 | next_url = self.request.POST.get('next') 42 | # for some bizarre reason the password isn't setting by the modelform 43 | self.object.set_password(self.request.POST['password']) 44 | self.object.save() 45 | auser = authenticate(username=self.object.username, password=self.request.POST['password']) 46 | login(self.request, auser) 47 | return reverse(next_url) if next_url else reverse('djangui:djangui_home') 48 | 49 | def djangui_login(request): 50 | if djangui_settings.DJANGUI_AUTH is False: 51 | return HttpResponseRedirect(djangui_settings.DJANGUI_LOGIN_URL) 52 | User = get_user_model() 53 | form = modelform_factory(User, fields=('username', 'password')) 54 | user = User.objects.filter(username=request.POST.get('username')) 55 | if user: 56 | user = user[0] 57 | else: 58 | user = None 59 | form = form(request.POST, instance=user) 60 | if form.is_valid(): 61 | data = form.cleaned_data 62 | user = authenticate(username=data['username'], password=data['password']) 63 | if user is None: 64 | return JsonResponse({'valid': False, 'errors': {'__all__': [force_text(_('You have entered an invalid username or password.'))]}}) 65 | login(request, user) 66 | return JsonResponse({'valid': True, 'redirect': request.POST['next']}) 67 | else: 68 | return JsonResponse({'valid': False, 'errors': form.errors}) -------------------------------------------------------------------------------- /djangui/tests/scripts/translate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | __author__ = 'chris' 3 | import argparse 4 | import sys 5 | 6 | BASE_PAIR_COMPLEMENTS = {'a': 't', 't': 'a', 'c': 'g', 'g': 'c', 'A': 'T', 'T': 'A', 'C': 'G', 'G': 'C', 'n': 'n', 'N': 'N'} 7 | 8 | CODON_TABLE = {'AAA': 'K', 'AAC': 'N', 'AAG': 'K', 'AAT': 'N', 'ACA': 'T', 'ACC': 'T', 'ACG': 'T', 'ACT': 'T', 9 | 'AGA': 'R', 'AGC': 'S', 'AGG': 'R', 'AGT': 'S', 'ATA': 'I', 'ATC': 'I', 'ATG': 'M', 'ATT': 'I', 10 | 'CAA': 'Q', 'CAC': 'H', 'CAG': 'Q', 'CAT': 'H', 'CCA': 'P', 'CCC': 'P', 'CCG': 'P', 'CCT': 'P', 11 | 'CGA': 'R', 'CGC': 'R', 'CGG': 'R', 'CGT': 'R', 'CTA': 'L', 'CTC': 'L', 'CTG': 'L', 'CTT': 'L', 12 | 'GAA': 'E', 'GAC': 'D', 'GAG': 'E', 'GAT': 'D', 'GCA': 'A', 'GCC': 'A', 'GCG': 'A', 'GCT': 'A', 13 | 'GGA': 'G', 'GGC': 'G', 'GGG': 'G', 'GGT': 'G', 'GTA': 'V', 'GTC': 'V', 'GTG': 'V', 'GTT': 'V', 14 | 'TAA': '*', 'TAC': 'Y', 'TAG': '*', 'TAT': 'Y', 'TCA': 'S', 'TCC': 'S', 'TCG': 'S', 'TCT': 'S', 15 | 'TGA': '*', 'TGC': 'C', 'TGG': 'W', 'TGT': 'C', 'TTA': 'L', 'TTC': 'F', 'TTG': 'L', 'TTT': 'F', 16 | 'NNN': 'X'} 17 | 18 | for i in 'ACTG': 19 | for j in 'ACTG': 20 | CODON_TABLE['%s%sN' % (i,j)] = 'X' 21 | CODON_TABLE['%sN%s' % (i,j)] = 'X' 22 | CODON_TABLE['N%s%s' % (i,j)] = 'X' 23 | CODON_TABLE['%sNN' % i] = 'X' 24 | CODON_TABLE['N%sN' % i] = 'X' 25 | CODON_TABLE['NN%s' % i] = 'X' 26 | 27 | parser = argparse.ArgumentParser(description="This will translate a given DNA sequence to protein.") 28 | group = parser.add_mutually_exclusive_group(required=True) 29 | group.add_argument('--sequence', help='The sequence to translate.', type=str) 30 | group.add_argument('--fasta', help='The fasta file to translate.', type=argparse.FileType('rb')) 31 | parser.add_argument('--frame', help='The frame to translate in.', type=str, choices=['+1', '+2', '+3', '-1', '-2', '-3'], default='+1') 32 | parser.add_argument('--out', help='The file to save translations to.', type=argparse.FileType('wb')) 33 | 34 | def main(): 35 | args = parser.parse_args() 36 | seq = args.sequence 37 | fasta = args.fasta 38 | 39 | def translate(seq=None, frame=None): 40 | if frame.startswith('-'): 41 | seq = ''.join([BASE_PAIR_COMPLEMENTS.get(i, 'N') for i in seq]) 42 | frame = int(frame[1])-1 43 | return ''.join([CODON_TABLE.get(seq[i:i+3], 'X') for i in xrange(frame, len(seq), 3) if i+3<=len(seq)]) 44 | 45 | frame = args.frame 46 | with args.out as fasta_out: 47 | if fasta: 48 | with args.fasta as fasta_in: 49 | header = '' 50 | seq = '' 51 | for row in fasta_in: 52 | if row[0] == '>': 53 | if seq: 54 | fasta_out.write('{}\n{}\n'.format(header, translate(seq, frame))) 55 | header = row 56 | seq = '' 57 | else: 58 | seq += row.strip() 59 | if seq: 60 | fasta_out.write('{}\n{}\n'.format(header, translate(seq, frame))) 61 | else: 62 | fasta_out.write('{}\n{}\n'.format('>1', translate(seq.upper(), frame))) 63 | 64 | if __name__ == "__main__": 65 | sys.exit(main()) 66 | -------------------------------------------------------------------------------- /djangui/tests/scripts/nested_heatmap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __author__ = 'chris' 4 | import argparse 5 | import os 6 | import sys 7 | 8 | 9 | parser = argparse.ArgumentParser(description="Create a nested heatmap from a delimited file.") 10 | parser.add_argument('--tsv', help='The delimited file to plot.', type=argparse.FileType('r'), required=True) 11 | parser.add_argument('--delimiter', help='The delimiter for fields. Default: tab', type=str, default='\t') 12 | parser.add_argument('--major-index', help='The first column to group by.', type=str, required=True) 13 | parser.add_argument('--minor-index', help='The second column to group by.', type=str, required=True) 14 | parser.add_argument('--minor-cutoff', help='The minimum number of minor entries grouped to be considered.', type=int, default=2) 15 | parser.add_argument('--log-normalize', help='Whether to log normalize data.', action='store_true') 16 | parser.add_argument('--translate', help='Whether to translate data so the minimum value is zero.', action='store_true') 17 | 18 | def main(): 19 | args = parser.parse_args() 20 | import numpy as np 21 | import pandas as pd 22 | import seaborn as sns 23 | major_index = args.major_index 24 | minor_index = args.minor_index 25 | df = pd.read_table(args.tsv, index_col=[major_index, minor_index], sep=args.delimiter) 26 | df = np.log2(df) if args.log_normalize else df 27 | # set our undected samples to our lowest detection 28 | df[df==-1*np.inf] = df[df!=-1*np.inf].min().min() 29 | # translate our data so we have no negatives (which would screw up our addition and makes no biological sense) 30 | if args.translate: 31 | df+=abs(df.min().min()) 32 | major_counts = df.groupby(level=[major_index]).count() 33 | # we only want to plot samples with multiple values in the minor index 34 | cutoff = args.minor_cutoff 35 | multi = df[df.index.get_level_values(major_index).isin(major_counts[major_counts>=cutoff].dropna().index)] 36 | 37 | # Let's select the most variable minor axis elements 38 | most_variable = multi.groupby(level=major_index).var().mean(axis=1).order(ascending=False) 39 | # and group by 20s 40 | for i in xrange(11): 41 | dat = multi[multi.index.get_level_values(major_index).isin(most_variable.index[10*i:10*(i+1)])] 42 | # we want to cluster by our major index, and then under these plot the values of our minor index 43 | major_dat = dat.groupby(level=major_index).sum() 44 | seaborn_map = sns.clustermap(major_dat, row_cluster=True, col_cluster=True) 45 | # now we keep this clustering, but recreate our data to fit the above clustering, with our minor 46 | # index below the major index (you can think of transcript levels under gene levels if you are 47 | # a biologist) 48 | merged_dat = pd.DataFrame(columns=[seaborn_map.data2d.columns]) 49 | for major_val in seaborn_map.data2d.index: 50 | minor_rows = multi[multi.index.get_level_values(major_index)==major_val][seaborn_map.data2d.columns] 51 | major_row = major_dat.loc[major_val,][seaborn_map.data2d.columns] 52 | merged_dat.append(major_row) 53 | merged_dat = merged_dat.append(major_row).append(minor_rows) 54 | merged_map = sns.clustermap(merged_dat, row_cluster=False, col_cluster=False) 55 | 56 | # recreate our dendrogram, this is undocumented and probably a hack but it works 57 | seaborn_map.dendrogram_col.plot(merged_map.ax_col_dendrogram) 58 | 59 | # for rows, I imagine at some point it will fail to fall within the major axis but fortunately 60 | # for this dataset it is not true 61 | seaborn_map.dendrogram_row.plot(merged_map.ax_row_dendrogram) 62 | merged_map.savefig('{}_heatmap_{}.png'.format(os.path.split(args.tsv.name)[1], i)) 63 | 64 | if __name__ == "__main__": 65 | sys.exit(main()) -------------------------------------------------------------------------------- /djangui/tests/test_views.py: -------------------------------------------------------------------------------- 1 | # TODO: Test for viewing a user's job as an anonymous user (fail case) 2 | 3 | import json 4 | 5 | from django.test import TestCase, RequestFactory, Client 6 | from django.core.urlresolvers import reverse 7 | from django.contrib.auth.models import AnonymousUser 8 | 9 | from . import factories, mixins 10 | from ..views import djangui_celery 11 | 12 | class CeleryViews(mixins.ScriptFactoryMixin, mixins.FileCleanupMixin, TestCase): 13 | def setUp(self): 14 | self.factory = RequestFactory() 15 | 16 | def test_celery_results(self): 17 | request = self.factory.get(reverse('djangui:celery_results')) 18 | user = factories.UserFactory() 19 | request.user = user 20 | response = djangui_celery.celery_status(request) 21 | d = response.content.decode("utf-8") 22 | self.assertEqual({'anon': [], 'user': []}, json.loads(d)) 23 | job = factories.JobFactory() 24 | job.save() 25 | response = djangui_celery.celery_status(request) 26 | d = json.loads(response.content.decode("utf-8")) 27 | self.assertEqual(1, len(d['anon'])) 28 | job.user = user 29 | job.save() 30 | response = djangui_celery.celery_status(request) 31 | d = json.loads(response.content.decode("utf-8")) 32 | # we now are logged in, make sure the job appears under the user jobs 33 | self.assertEqual(1, len(d['user'])) 34 | user = AnonymousUser() 35 | request.user = user 36 | response = djangui_celery.celery_status(request) 37 | d = json.loads(response.content.decode("utf-8")) 38 | # test empty response since anonymous users should not see users jobs 39 | self.assertEqual({'anon': [], 'user': []}, d) 40 | 41 | def test_celery_commands(self): 42 | user = factories.UserFactory() 43 | job = factories.JobFactory() 44 | job.user = user 45 | job.save() 46 | celery_command = {'celery-command': ['delete'], 'job-id': [job.pk]} 47 | # test that we cannot modify a users script 48 | request = self.factory.post(reverse('djangui:celery_task_command'), 49 | celery_command) 50 | anon = AnonymousUser() 51 | request.user = anon 52 | response = djangui_celery.celery_task_command(request) 53 | d = response.content.decode("utf-8") 54 | self.assertFalse(json.loads(d).get('valid')) 55 | 56 | # test a nonsense command 57 | celery_command.update({'celery-command': ['thisshouldfail']}) 58 | response = djangui_celery.celery_task_command(request) 59 | d = response.content.decode("utf-8") 60 | self.assertFalse(json.loads(d).get('valid')) 61 | 62 | # test that the user can interact with it 63 | request.user = user 64 | for i in ['resubmit', 'rerun', 'clone', 'stop', 'delete']: 65 | celery_command.update({'celery-command': [i]}) 66 | response = djangui_celery.celery_task_command(request) 67 | d = response.content.decode("utf-8") 68 | self.assertTrue(json.loads(d).get('valid')) 69 | 70 | def test_celery_task_view(self): 71 | user = factories.UserFactory() 72 | job = factories.JobFactory() 73 | job.user = user 74 | job.save() 75 | # test that an anonymous user cannot view a user's job 76 | view = djangui_celery.CeleryTaskView.as_view() 77 | request = self.factory.get(reverse('djangui:celery_results_info', kwargs={'job_id': job.pk})) 78 | request.user = AnonymousUser() 79 | response = view(request, job_id=job.pk) 80 | self.assertIn('task_error', response.context_data) 81 | self.assertNotIn('task_info', response.context_data) 82 | 83 | # test the user can view the job 84 | request.user = user 85 | response = view(request, job_id=job.pk) 86 | self.assertNotIn('task_error', response.context_data) 87 | self.assertIn('task_info', response.context_data) 88 | 89 | # test that jobs that don't exist don't fail horridly 90 | request = self.factory.get(reverse('djangui:celery_results_info', kwargs={'job_id': '-1'})) 91 | response = view(request, job_id=-1) 92 | self.assertIn('task_error', response.context_data) 93 | self.assertNotIn('task_info', response.context_data) -------------------------------------------------------------------------------- /djangui/backend/command_line.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import subprocess 4 | import shutil 5 | from argparse import ArgumentParser 6 | from django.template import Context 7 | import djangui 8 | from .. import django_compat 9 | 10 | def which(pgm): 11 | # from http://stackoverflow.com/questions/9877462/is-there-a-python-equivalent-to-the-which-command 12 | path=os.getenv('PATH') 13 | for p in path.split(os.path.pathsep): 14 | p=os.path.join(p,pgm) 15 | if os.path.exists(p) and os.access(p, os.X_OK): 16 | return p 17 | 18 | def walk_dir(templates, dest, filter=None): 19 | l = [] 20 | for root, folders, files in os.walk(templates): 21 | for filename in files: 22 | if filename.endswith('.pyc') or (filter and filename not in filter): 23 | continue 24 | relative_dir = '.{0}'.format(os.path.split(os.path.join(root, filename).replace(templates, ''))[0]) 25 | l.append((os.path.join(root, filename), os.path.join(dest, relative_dir))) 26 | return l 27 | 28 | def bootstrap(env=None, cwd=None): 29 | if env is None: 30 | env = os.environ 31 | parser = ArgumentParser(description="Create a Django app with Djangui setup.") 32 | parser.add_argument('-p', '--project', help='The name of the django project to create.', type=str, required=True) 33 | args = parser.parse_args() 34 | 35 | project_name = args.project 36 | new_project = not os.path.exists(project_name) 37 | if not new_project: 38 | sys.stderr.write('Project {0} already exists.\n'.format(project_name)) 39 | sys.exit(1) 40 | env['DJANGO_SETTINGS_MODULE'] = '' 41 | admin_command = [sys.executable] if sys.executable else [] 42 | admin_path = which('django-admin.py') 43 | admin_command.extend([admin_path, 'startproject', project_name]) 44 | admin_kwargs = {'env': env} 45 | if cwd is not None: 46 | admin_kwargs.update({'cwd': cwd}) 47 | subprocess.call(admin_command, **admin_kwargs) 48 | project_root = project_name 49 | project_base_dir = os.path.join(os.path.realpath(os.path.curdir), project_root, project_name) 50 | 51 | djanguify_folder = os.path.split(os.path.realpath(djangui.__file__))[0] 52 | project_template_dir = os.path.join(djanguify_folder, 'conf', 'project_template') 53 | 54 | context = Context( 55 | dict({ 56 | 'project_name': project_name, 57 | }, 58 | autoescape=False 59 | )) 60 | 61 | template_files = [] 62 | 63 | template_files += walk_dir(project_template_dir, project_base_dir) 64 | 65 | for template_file, dest_dir in template_files: 66 | template_file = open(template_file) 67 | content = template_file.read() 68 | template = django_compat.Engine().from_string(content) 69 | content = template.render(context) 70 | content = content.encode('utf-8') 71 | to_name = os.path.join(dest_dir, os.path.split(template_file.name)[1]) 72 | try: 73 | os.mkdir(dest_dir) 74 | except: 75 | pass 76 | with open(to_name, 'wb') as new_file: 77 | new_file.write(content) 78 | 79 | # move the django settings to the settings path so we don't have to chase Django changes. 80 | shutil.move(os.path.join(project_base_dir, 'settings.py'), os.path.join(project_base_dir, 'settings', 'django_settings.py')) 81 | # do the same with urls 82 | shutil.move(os.path.join(project_base_dir, 'urls.py'), os.path.join(project_base_dir, 'urls', 'django_urls.py')) 83 | env['DJANGO_SETTINGS_MODULE'] = '.'.join([project_name, 'settings', 'user_settings']) 84 | if django_compat.DJANGO_VERSION >= django_compat.DJ17: 85 | subprocess.call(['python', os.path.join(project_root, 'manage.py'), 'makemigrations'], env=env) 86 | subprocess.call(['python', os.path.join(project_root, 'manage.py'), 'migrate'], env=env) 87 | else: 88 | subprocess.call(['python', os.path.join(project_root, 'manage.py'), 'syncdb', '--noinput'], env=env) 89 | subprocess.call(['python', os.path.join(project_root, 'manage.py'), 'collectstatic', '--noinput'], env=env) 90 | sys.stdout.write("Please enter the project directory {0}, and run python manage.py createsuperuser and" 91 | " python manage.py runserver to start. The admin can be found at localhost:8000/admin. You may also want to set your " 92 | "DJANGO_SETTINGS_MODULE environment variable to {0}.settings \n".format(project_name)) 93 | -------------------------------------------------------------------------------- /djangui/views/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.views.generic import DetailView, TemplateView 3 | from django.conf import settings 4 | from django.forms import FileField 5 | from django.utils.translation import gettext_lazy as _ 6 | from django.utils.encoding import force_text 7 | 8 | from ..backend import utils 9 | from ..models import DjanguiJob, Script 10 | from .. import settings as djangui_settings 11 | from ..django_compat import JsonResponse 12 | 13 | 14 | class DjanguiScriptJSON(DetailView): 15 | model = Script 16 | slug_field = 'slug' 17 | slug_url_kwarg = 'script_name' 18 | 19 | def render_to_response(self, context, **response_kwargs): 20 | # returns the models required and optional fields as html 21 | job_id = self.kwargs.get('job_id') 22 | initial = None 23 | if job_id: 24 | job = DjanguiJob.objects.get(pk=job_id) 25 | if job.user is None or (self.request.user.is_authenticated() and job.user == self.request.user): 26 | initial = {} 27 | for i in job.get_parameters(): 28 | value = i.value 29 | if value is not None: 30 | initial[i.parameter.slug] = value 31 | d = utils.get_form_groups(model=self.object, initial=initial) 32 | return JsonResponse(d) 33 | 34 | def post(self, request, *args, **kwargs): 35 | post = request.POST.copy() 36 | user = request.user if request.user.is_authenticated() else None 37 | if not djangui_settings.DJANGUI_ALLOW_ANONYMOUS and user is None: 38 | return JsonResponse({'valid': False, 'errors': {'__all__': [force_text(_('You are not permitted to access this script.'))]}}) 39 | form = utils.get_master_form(pk=post['djangui_type']) 40 | # TODO: Check with people who know more if there's a smarter way to do this 41 | utils.validate_form(form=form, data=post, files=request.FILES) 42 | # for cloned jobs, we don't have the files in input fields, they'll be in a list like ['', filename] 43 | # This will cause issues. 44 | to_delete = [] 45 | for i in post: 46 | if isinstance(form.fields.get(i), FileField): 47 | # if we have a value set, reassert this 48 | new_value = post.get(i) 49 | if i not in request.FILES and (i not in form.cleaned_data or (not form.cleaned_data[i] and new_value)): 50 | # this is a previously set field, so a cloned job 51 | if new_value is not None: 52 | form.cleaned_data[i] = utils.get_storage(local=False).open(new_value) 53 | to_delete.append(i) 54 | for i in to_delete: 55 | if i in form.errors: 56 | del form.errors[i] 57 | 58 | if not form.errors: 59 | # data = form.cleaned_data 60 | script_pk = form.cleaned_data.get('djangui_type') 61 | script = Script.objects.get(pk=script_pk) 62 | valid = utils.valid_user(script, request.user).get('valid') 63 | if valid is True: 64 | group_valid = utils.valid_user(script.script_group, request.user).get('valid') 65 | if valid is True and group_valid is True: 66 | job = utils.create_djangui_job(script_pk=script_pk, user=user, data=form.cleaned_data) 67 | job.submit_to_celery() 68 | return JsonResponse({'valid': True}) 69 | return JsonResponse({'valid': False, 'errors': {'__all__': [force_text(_('You are not permitted to access this script.'))]}}) 70 | return JsonResponse({'valid': False, 'errors': form.errors}) 71 | 72 | 73 | class DjanguiHomeView(TemplateView): 74 | template_name = 'djangui/djangui_home.html' 75 | 76 | def get_context_data(self, **kwargs): 77 | job_id = self.request.GET.get('job_id') 78 | ctx = super(DjanguiHomeView, self).get_context_data(**kwargs) 79 | ctx['djangui_scripts'] = getattr(settings, 'DJANGUI_SCRIPTS', {}) 80 | if job_id: 81 | job = DjanguiJob.objects.get(pk=job_id) 82 | if job.user is None or (self.request.user.is_authenticated() and job.user == self.request.user): 83 | ctx['clone_job'] = {'job_id': job_id, 'url': job.get_resubmit_url(), 'data_url': job.script.get_url()} 84 | return ctx 85 | 86 | class DjanguiProfileView(TemplateView): 87 | template_name = 'djangui/profile/profile_base.html' -------------------------------------------------------------------------------- /djangui/conf/project_template/settings/user_settings.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from .djangui_settings import * 3 | 4 | # This file is where the user can override and customize their installation of djangui 5 | 6 | # Djangui Apps - add additional apps here after the initial install (remember to follow everything by a comma) 7 | 8 | INSTALLED_APPS += ( 9 | ) 10 | 11 | # Whether to allow anonymous job submissions, set False to disallow 'guest' job submissions 12 | DJANGUI_ALLOW_ANONYMOUS = True 13 | 14 | ## Celery related options 15 | INSTALLED_APPS += ( 16 | 'djcelery', 17 | 'kombu.transport.django', 18 | ) 19 | 20 | CELERY_RESULT_BACKEND='djcelery.backends.database:DatabaseBackend' 21 | BROKER_URL = 'django://' 22 | CELERY_TRACK_STARTED = True 23 | DJANGUI_CELERY = True 24 | CELERY_SEND_EVENTS = True 25 | 26 | # Things you most likely do not need to change 27 | 28 | # the directory for uploads (physical directory) 29 | MEDIA_ROOT = os.path.join(BASE_DIR, 'user_uploads') 30 | # the url mapping 31 | MEDIA_URL = '/uploads/' 32 | 33 | # the directory to store our webpage assets (images, javascript, etc.) 34 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 35 | # the url mapping 36 | STATIC_URL = '/static/' 37 | 38 | ## Here is a setup example for production servers 39 | 40 | ## A postgres database -- for multiple users a sqlite based database is asking for trouble 41 | 42 | # DATABASES = { 43 | # 'default': { 44 | # 'ENGINE': 'django.db.backends.postgresql_psycopg2', 45 | # # for production environments, these should be stored as environment variables 46 | # # I also recommend the django-heroku-postgresify package for a super simple setup 47 | # 'NAME': os.environ.get('DATABASE_NAME', 'djangui'), 48 | # 'USER': os.environ.get('DATABASE_USER', 'djangui'), 49 | # 'PASSWORD': os.environ.get('DATABASE_PASSWORD', 'djangui'), 50 | # 'HOST': os.environ.get('DATABASE_URL', 'localhost'), 51 | # 'PORT': os.environ.get('DATABASE_PORT', '5432') 52 | # } 53 | # } 54 | 55 | ## A better celery backend -- using RabbitMQ (these defaults are from two free rabbitmq Heroku providers) 56 | # CELERY_RESULT_BACKEND = 'amqp' 57 | # BROKER_URL = os.environ.get('AMQP_URL') or \ 58 | # os.environ.get('RABBITMQ_BIGWIG_TX_URL') or \ 59 | # os.environ.get('CLOUDAMQP_URL', 'amqp://guest:guest@localhost:5672/') 60 | # BROKER_POOL_LIMIT = 1 61 | # CELERYD_CONCURRENCY = 1 62 | # CELERY_TASK_SERIALIZER = 'json' 63 | # ACKS_LATE = True 64 | # 65 | 66 | ## for production environments, django-storages abstracts away much of the difficulty of various storage engines. 67 | ## Here is an example for hosting static and user generated content with S3 68 | 69 | # from boto.s3.connection import VHostCallingFormat 70 | # 71 | # INSTALLED_APPS += ( 72 | # 'storages', 73 | # 'collectfast', 74 | # ) 75 | 76 | ## We have user authentication -- we need to use https (django-sslify) 77 | # if not DEBUG: 78 | # MIDDLEWARE_CLASSES = ['sslify.middleware.SSLifyMiddleware']+list(MIDDLEWARE_CLASSES) 79 | # SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 80 | # 81 | # ALLOWED_HOSTS = ( 82 | # 'localhost', 83 | # '127.0.0.1', 84 | # "djangui.herokuapp.com",# put your site here 85 | # ) 86 | # 87 | # AWS_CALLING_FORMAT = VHostCallingFormat 88 | # 89 | # AWS_ACCESS_KEY_ID = environ.get('AWS_ACCESS_KEY_ID', '') 90 | # AWS_SECRET_ACCESS_KEY = environ.get('AWS_SECRET_ACCESS_KEY', '') 91 | # AWS_STORAGE_BUCKET_NAME = environ.get('AWS_STORAGE_BUCKET_NAME', '') 92 | # AWS_AUTO_CREATE_BUCKET = True 93 | # AWS_QUERYSTRING_AUTH = False 94 | # AWS_S3_SECURE_URLS = True 95 | # AWS_FILE_OVERWRITE = False 96 | # AWS_PRELOAD_METADATA = True 97 | # AWS_S3_CUSTOM_DOMAIN = environ.get('AWS_S3_CUSTOM_DOMAIN', '') 98 | # 99 | # GZIP_CONTENT_TYPES = ( 100 | # 'text/css', 101 | # 'application/javascript', 102 | # 'application/x-javascript', 103 | # 'text/javascript', 104 | # ) 105 | # 106 | # AWS_EXPIREY = 60 * 60 * 7 107 | # AWS_HEADERS = { 108 | # 'Cache-Control': 'max-age=%d, s-maxage=%d, must-revalidate' % (AWS_EXPIREY, 109 | # AWS_EXPIREY) 110 | # } 111 | # 112 | # STATIC_URL = 'http://%s.s3.amazonaws.com/' % AWS_STORAGE_BUCKET_NAME 113 | # MEDIA_URL = '/user-uploads/' 114 | # 115 | # STATICFILES_STORAGE = DEFAULT_FILE_STORAGE = 'djangui.djanguistorage.CachedS3BotoStorage' 116 | # DJANGUI_EPHEMERAL_FILES = True 117 | 118 | 119 | AUTHENTICATION_BACKEND = 'django.contrib.auth.backends.ModelBackend' -------------------------------------------------------------------------------- /djangui/forms/factory.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | __author__ = 'chris' 3 | import copy 4 | import json 5 | import six 6 | from collections import OrderedDict 7 | 8 | from django import forms 9 | 10 | from .scripts import DjanguiForm 11 | from ..backend import utils 12 | from ..models import ScriptParameter 13 | 14 | 15 | class DjanguiFormFactory(object): 16 | djangui_forms = {} 17 | 18 | @staticmethod 19 | def get_field(param, initial=None): 20 | field = param.form_field 21 | choices = json.loads(param.choices) 22 | field_kwargs = {'label': param.script_param.title(), 23 | 'required': param.required, 24 | 'help_text': param.param_help, 25 | } 26 | if choices: 27 | field = 'ChoiceField' 28 | base_choices = [(None, '----')] if not param.required else [] 29 | field_kwargs['choices'] = base_choices+[(str(i), str(i).title()) for i in choices] 30 | if field == 'FileField': 31 | if param.is_output: 32 | field = 'CharField' 33 | initial = None 34 | else: 35 | if initial is not None: 36 | initial = utils.get_storage_object(initial) if not hasattr(initial, 'path') else initial 37 | field_kwargs['widget'] = forms.ClearableFileInput() 38 | if initial is not None: 39 | field_kwargs['initial'] = initial 40 | field = getattr(forms, field) 41 | return field(**field_kwargs) 42 | 43 | def get_group_forms(self, model=None, pk=None, initial=None): 44 | pk = int(pk) if pk is not None else pk 45 | if pk is not None and pk in self.djangui_forms: 46 | if 'groups' in self.djangui_forms[pk]: 47 | return copy.deepcopy(self.djangui_forms[pk]['groups']) 48 | params = ScriptParameter.objects.filter(script=model).order_by('pk') 49 | # set a reference to the object type for POST methods to use 50 | script_id_field = forms.CharField(widget=forms.HiddenInput) 51 | group_map = {} 52 | for param in params: 53 | field = self.get_field(param, initial=initial.get(param.slug) if initial else None) 54 | group_id = -1 if param.required else param.parameter_group.pk 55 | group_name = 'Required' if param.required else param.parameter_group.group_name 56 | group = group_map.get(group_id, { 57 | 'group': group_name, 58 | 'fields': OrderedDict() 59 | }) 60 | group['fields'][param.slug] = field 61 | group_map[group_id] = group 62 | # create individual forms for each group 63 | group_map = OrderedDict([(i, group_map[i]) for i in sorted(group_map.keys())]) 64 | d = {'action': model.get_url()} 65 | d['groups'] = [] 66 | pk = model.pk 67 | for group_index, group in enumerate(six.iteritems(group_map)): 68 | group_pk, group_info = group 69 | form = DjanguiForm() 70 | if group_index == 0: 71 | form.fields['djangui_type'] = script_id_field 72 | form.fields['djangui_type'].initial = pk 73 | for field_pk, field in six.iteritems(group_info['fields']): 74 | form.fields[field_pk] = field 75 | d['groups'].append({'group_name': group_info['group'], 'form': str(form)}) 76 | try: 77 | self.djangui_forms[pk]['groups'] = d 78 | except KeyError: 79 | self.djangui_forms[pk] = {'groups': d} 80 | # if the master form doesn't exist, create it while we have the model 81 | if 'master' not in self.djangui_forms[pk]: 82 | self.get_master_form(model=model, pk=pk) 83 | return d 84 | 85 | def get_master_form(self, model=None, pk=None): 86 | pk = int(pk) if pk is not None else pk 87 | if pk is not None and pk in self.djangui_forms: 88 | if 'master' in self.djangui_forms[pk]: 89 | return copy.deepcopy(self.djangui_forms[pk]['master']) 90 | master_form = DjanguiForm() 91 | params = ScriptParameter.objects.filter(script=model).order_by('pk') 92 | # set a reference to the object type for POST methods to use 93 | pk = model.pk 94 | script_id_field = forms.CharField(widget=forms.HiddenInput) 95 | master_form.fields['djangui_type'] = script_id_field 96 | master_form.fields['djangui_type'].initial = pk 97 | 98 | for param in params: 99 | field = self.get_field(param) 100 | master_form.fields[param.slug] = field 101 | try: 102 | self.djangui_forms[pk]['master'] = master_form 103 | except KeyError: 104 | self.djangui_forms[pk] = {'master': master_form} 105 | # create the group forms while we have the model 106 | if 'groups' not in self.djangui_forms[pk]: 107 | self.get_group_forms(model=model, pk=pk) 108 | return master_form 109 | 110 | DJ_FORM_FACTORY = DjanguiFormFactory() -------------------------------------------------------------------------------- /djangui/backend/ast/source_parser.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Dec 11, 2013 3 | 4 | @author: Chris 5 | 6 | Collection of functions for extracting argparse related statements from the 7 | client code. 8 | ''' 9 | 10 | import ast 11 | import _ast 12 | from itertools import * 13 | 14 | from . import codegen 15 | 16 | 17 | def parse_source_file(file_name): 18 | """ 19 | Parses the AST of Python file for lines containing 20 | references to the argparse module. 21 | 22 | returns the collection of ast objects found. 23 | 24 | Example client code: 25 | 26 | 1. parser = ArgumentParser(desc="My help Message") 27 | 2. parser.add_argument('filename', help="Name of the file to load") 28 | 3. parser.add_argument('-f', '--format', help='Format of output \nOptions: ['md', 'html'] 29 | 4. args = parser.parse_args() 30 | 31 | Variables: 32 | * nodes Primary syntax tree object 33 | * argparse_assignments The assignment of the ArgumentParser (line 1 in example code) 34 | * add_arg_assignments Calls to add_argument() (lines 2-3 in example code) 35 | * parser_var_name The instance variable of the ArgumentParser (line 1 in example code) 36 | * ast_source The curated collection of all parser related nodes in the client code 37 | """ 38 | with open(file_name, 'r') as f: 39 | s = f.read() 40 | 41 | nodes = ast.parse(s) 42 | 43 | module_imports = get_nodes_by_instance_type(nodes, _ast.Import) 44 | specific_imports = get_nodes_by_instance_type(nodes, _ast.ImportFrom) 45 | 46 | assignment_objs = get_nodes_by_instance_type(nodes, _ast.Assign) 47 | call_objects = get_nodes_by_instance_type(nodes, _ast.Call) 48 | 49 | argparse_assignments = get_nodes_by_containing_attr(assignment_objs, 'ArgumentParser') 50 | group_arg_assignments = get_nodes_by_containing_attr(assignment_objs, 'add_argument_group') 51 | add_arg_assignments = get_nodes_by_containing_attr(call_objects, 'add_argument') 52 | parse_args_assignment = get_nodes_by_containing_attr(call_objects, 'parse_args') 53 | # there are cases where we have custom argparsers, such as subclassing ArgumentParser. The above 54 | # will fail on this. However, we can use the methods known to ArgumentParser to do a duck-type like 55 | # approach to finding what is the arg parser 56 | if not argparse_assignments: 57 | aa_references = set([i.func.value.id for i in chain(add_arg_assignments, parse_args_assignment)]) 58 | argparse_like_objects = [getattr(i.value.func, 'id', None) for p_ref in aa_references for i in get_nodes_by_containing_attr(assignment_objs, p_ref)] 59 | argparse_like_objects = filter(None, argparse_like_objects) 60 | argparse_assignments = [get_nodes_by_containing_attr(assignment_objs, i) for i in argparse_like_objects] 61 | # for now, we just choose one 62 | try: 63 | argparse_assignments = argparse_assignments[0] 64 | except IndexError: 65 | pass 66 | 67 | 68 | # get things that are assigned inside ArgumentParser or its methods 69 | argparse_assigned_variables = get_node_args_and_keywords(assignment_objs, argparse_assignments, 'ArgumentParser') 70 | add_arg_assigned_variables = get_node_args_and_keywords(assignment_objs, add_arg_assignments, 'add_argument') 71 | parse_args_assigned_variables = get_node_args_and_keywords(assignment_objs, parse_args_assignment, 'parse_args') 72 | 73 | ast_argparse_source = chain( 74 | module_imports, 75 | specific_imports, 76 | argparse_assigned_variables, 77 | add_arg_assigned_variables, 78 | parse_args_assigned_variables, 79 | argparse_assignments, 80 | group_arg_assignments, 81 | add_arg_assignments, 82 | ) 83 | return ast_argparse_source 84 | 85 | 86 | def read_client_module(filename): 87 | with open(filename, 'r') as f: 88 | return f.readlines() 89 | 90 | 91 | def get_node_args_and_keywords(assigned_objs, assignments, selector=None): 92 | referenced_nodes = set([]) 93 | selector_line = -1 94 | assignment_nodes = [] 95 | for node in assignments: 96 | for i in walk_tree(node): 97 | if i and isinstance(i, (_ast.keyword, _ast.Name)) and 'id' in i.__dict__: 98 | if i.id == selector: 99 | selector_line = i.lineno 100 | elif i.lineno == selector_line: 101 | referenced_nodes.add(i.id) 102 | for node in assigned_objs: 103 | for target in node.targets: 104 | if getattr(target, 'id', None) in referenced_nodes: 105 | assignment_nodes.append(node) 106 | return assignment_nodes 107 | 108 | 109 | def get_nodes_by_instance_type(nodes, object_type): 110 | return [node for node in walk_tree(nodes) if isinstance(node, object_type)] 111 | 112 | 113 | def get_nodes_by_containing_attr(nodes, attr): 114 | return [node for node in nodes if attr in walk_tree(node)] 115 | 116 | 117 | def walk_tree(node): 118 | try: 119 | d = node.__dict__ 120 | except AttributeError: 121 | d = {} 122 | yield node 123 | for key, value in d.items(): 124 | if isinstance(value, list): 125 | for val in value: 126 | for _ in ast.walk(val): 127 | yield _ 128 | elif issubclass(type(value), ast.AST): 129 | for _ in walk_tree(value): 130 | yield _ 131 | else: 132 | yield value 133 | 134 | 135 | def convert_to_python(ast_source): 136 | """ 137 | Converts the ast objects back into human readable Python code 138 | """ 139 | return map(codegen.to_source, ast_source) -------------------------------------------------------------------------------- /djangui/views/djangui_celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | import six 4 | 5 | from django.core.urlresolvers import reverse 6 | from django.views.generic import TemplateView 7 | from django.conf import settings 8 | from django.utils.translation import gettext_lazy as _ 9 | from django.utils.encoding import force_text 10 | from django.db.models import Q 11 | from django.template.defaultfilters import escape 12 | 13 | from djcelery.models import TaskMeta 14 | from celery import app, states 15 | 16 | celery_app = app.app_or_default() 17 | 18 | from ..models import DjanguiJob 19 | from .. import settings as djangui_settings 20 | from ..backend.utils import valid_user, get_file_previews 21 | from ..django_compat import JsonResponse 22 | 23 | def celery_status(request): 24 | # TODO: This function can use some sprucing up, design a better data structure for returning jobs 25 | spanbase = "" 26 | STATE_MAPPER = { 27 | DjanguiJob.COMPLETED: spanbase.format('glyphicon-ok', _('Success')), 28 | DjanguiJob.RUNNING: spanbase.format('glyphicon-refresh spinning', _('Executing')), 29 | states.PENDING: spanbase.format('glyphicon-time', _('In queue')), 30 | states.REVOKED: spanbase.format('glyphicon-stop', _('Halted')), 31 | DjanguiJob.SUBMITTED: spanbase.format('glyphicon-hourglass', _('Waiting to be queued')) 32 | } 33 | user = request.user 34 | if user.is_superuser: 35 | jobs = DjanguiJob.objects.all() 36 | else: 37 | jobs = DjanguiJob.objects.filter(Q(user=None) | Q(user=user) if request.user.is_authenticated() else Q(user=None)) 38 | jobs = jobs.exclude(status=DjanguiJob.DELETED) 39 | # divide into user and anon jobs 40 | def get_job_list(job_query): 41 | return [{'job_name': escape(job.job_name), 'job_status': STATE_MAPPER.get(job.status, job.status), 42 | 'job_submitted': job.created_date.strftime('%b %d %Y, %H:%M:%S'), 43 | 'job_id': job.pk, 44 | 'job_description': escape(six.u('Script: {}\n{}').format(job.script.script_name, job.job_description)), 45 | 'job_url': reverse('djangui:celery_results_info', kwargs={'job_id': job.pk})} for job in job_query] 46 | d = {'user': get_job_list([i for i in jobs if i.user == user]), 47 | 'anon': get_job_list([i for i in jobs if i.user == None or (user.is_superuser and i.user != user)])} 48 | return JsonResponse(d, safe=False) 49 | 50 | 51 | def celery_task_command(request): 52 | 53 | command = request.POST.get('celery-command') 54 | job_id = request.POST.get('job-id') 55 | job = DjanguiJob.objects.get(pk=job_id) 56 | response = {'valid': False,} 57 | valid = valid_user(job.script, request.user) 58 | if valid.get('valid') is True: 59 | user = request.user if request.user.is_authenticated() else None 60 | if user == job.user or job.user == None: 61 | if command == 'resubmit': 62 | new_job = job.submit_to_celery(resubmit=True, user=request.user) 63 | response.update({'valid': True, 'extra': {'task_url': reverse('djangui:celery_results_info', kwargs={'job_id': new_job.pk})}}) 64 | elif command == 'rerun': 65 | job.submit_to_celery(user=request.user, rerun=True) 66 | response.update({'valid': True, 'redirect': reverse('djangui:celery_results_info', kwargs={'job_id': job_id})}) 67 | elif command == 'clone': 68 | response.update({'valid': True, 'redirect': '{0}?job_id={1}'.format(reverse('djangui:djangui_task_launcher'), job_id)}) 69 | elif command == 'delete': 70 | job.status = DjanguiJob.DELETED 71 | job.save() 72 | response.update({'valid': True, 'redirect': reverse('djangui:djangui_home')}) 73 | elif command == 'stop': 74 | celery_app.control.revoke(job.celery_id, signal='SIGKILL', terminate=True) 75 | job.status = states.REVOKED 76 | job.save() 77 | response.update({'valid': True, 'redirect': reverse('djangui:celery_results_info', kwargs={'job_id': job_id})}) 78 | else: 79 | response.update({'errors': {'__all__': [force_text(_("Unknown Command"))]}}) 80 | else: 81 | response.update({'errors': {'__all__': [force_text(valid.get('error'))]}}) 82 | return JsonResponse(response) 83 | 84 | 85 | class CeleryTaskView(TemplateView): 86 | template_name = 'djangui/tasks/task_view.html' 87 | 88 | def get_context_data(self, **kwargs): 89 | ctx = super(CeleryTaskView, self).get_context_data(**kwargs) 90 | job_id = ctx.get('job_id') 91 | try: 92 | djangui_job = DjanguiJob.objects.get(pk=job_id) 93 | except DjanguiJob.DoesNotExist: 94 | ctx['task_error'] = _('This task does not exist.') 95 | else: 96 | user = self.request.user 97 | user = None if not user.is_authenticated() and djangui_settings.DJANGUI_ALLOW_ANONYMOUS else user 98 | job_user = djangui_job.user 99 | if job_user == None or job_user == user or (user != None and user.is_superuser): 100 | out_files = get_file_previews(djangui_job) 101 | all = out_files.pop('all', []) 102 | archives = out_files.pop('archives', []) 103 | ctx['task_info'] = { 104 | 'all_files': all, 105 | 'archives': archives, 106 | 'file_groups': out_files, 107 | 'status': djangui_job.status, 108 | 'last_modified': djangui_job.modified_date, 109 | 'job': djangui_job 110 | } 111 | else: 112 | ctx['task_error'] = _('You are not authenticated to view this job.') 113 | return ctx 114 | 115 | -------------------------------------------------------------------------------- /djangui/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import subprocess 3 | import tarfile 4 | import os 5 | import zipfile 6 | import six 7 | 8 | from django.utils.text import get_valid_filename 9 | from django.core.files.storage import default_storage 10 | from django.core.files import File 11 | from django.conf import settings 12 | from django.db.transaction import atomic 13 | 14 | 15 | from celery import Task 16 | from celery import states 17 | from celery import app 18 | from celery.contrib import rdb 19 | 20 | from . import settings as djangui_settings 21 | 22 | celery_app = app.app_or_default() 23 | 24 | class DjanguiTask(Task): 25 | pass 26 | # def after_return(self, status, retval, task_id, args, kwargs, einfo): 27 | # job, created = DjanguiJob.objects.get_or_create(djangui_celery_id=task_id) 28 | # job.content_type.djangui_celery_state = status 29 | # job.save() 30 | 31 | @celery_app.task(base=DjanguiTask) 32 | def submit_script(**kwargs): 33 | job_id = kwargs.pop('djangui_job') 34 | resubmit = kwargs.pop('djangui_resubmit', False) 35 | rerun = kwargs.pop('rerun', False) 36 | from .backend import utils 37 | from .models import DjanguiJob, DjanguiFile 38 | job = DjanguiJob.objects.get(pk=job_id) 39 | 40 | command = utils.get_job_commands(job=job) 41 | if resubmit: 42 | # clone ourselves, setting pk=None seems hackish but it works 43 | job.pk = None 44 | 45 | # This is where the script works from -- it is what is after the media_root since that may change between 46 | # setups/where our user uploads are stored. 47 | cwd = job.get_output_path() 48 | 49 | abscwd = os.path.abspath(os.path.join(settings.MEDIA_ROOT, cwd)) 50 | job.command = ' '.join(command) 51 | job.save_path = cwd 52 | 53 | if rerun: 54 | # cleanup the old files, we need to be somewhat aggressive here. 55 | local_storage = utils.get_storage(local=True) 56 | remote_storage = utils.get_storage(local=False) 57 | to_delete = [] 58 | with atomic(): 59 | for dj_file in DjanguiFile.objects.filter(job=job): 60 | if dj_file.parameter is None or dj_file.parameter.parameter.is_output: 61 | to_delete.append(dj_file) 62 | path = local_storage.path(dj_file.filepath.name) 63 | dj_file.filepath.delete(False) 64 | if local_storage.exists(path): 65 | local_storage.delete(path) 66 | # TODO: This needs to be tested to make sure it's being nuked 67 | if remote_storage.exists(path): 68 | remote_storage.delete(path) 69 | [i.delete() for i in to_delete] 70 | 71 | utils.mkdirs(abscwd) 72 | # make sure we have the script, otherwise download it. This can happen if we have an ephemeral file system or are 73 | # executing jobs on a worker node. 74 | script_path = job.script.script_path 75 | if not utils.get_storage(local=True).exists(script_path.path): 76 | utils.get_storage(local=True).save(script_path.path, script_path.file) 77 | 78 | job.status = DjanguiJob.RUNNING 79 | job.save() 80 | 81 | proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=abscwd) 82 | 83 | stdout, stderr = proc.communicate() 84 | # tar/zip up the generated content for bulk downloads 85 | def get_valid_file(cwd, name, ext): 86 | out = os.path.join(cwd, name) 87 | index = 0 88 | while os.path.exists(six.u('{}.{}').format(out, ext)): 89 | index += 1 90 | out = os.path.join(cwd, six.u('{}_{}').format(name, index)) 91 | return six.u('{}.{}').format(out, ext) 92 | 93 | # fetch the job again in case the database connection was lost during the job or something else changed. 94 | job = DjanguiJob.objects.get(pk=job_id) 95 | 96 | # if there are files generated, make zip/tar files for download 97 | if len(os.listdir(abscwd)): 98 | tar_out = get_valid_file(abscwd, get_valid_filename(job.job_name), 'tar.gz') 99 | tar = tarfile.open(tar_out, "w:gz") 100 | tar_name = os.path.splitext(os.path.splitext(os.path.split(tar_out)[1])[0])[0] 101 | tar.add(abscwd, arcname=tar_name) 102 | tar.close() 103 | 104 | zip_out = get_valid_file(abscwd, get_valid_filename(job.job_name), 'zip') 105 | zip = zipfile.ZipFile(zip_out, "w") 106 | arcname = os.path.splitext(os.path.split(zip_out)[1])[0] 107 | zip.write(abscwd, arcname=arcname) 108 | for root, folders, filenames in os.walk(os.path.split(zip_out)[0]): 109 | for filename in filenames: 110 | path = os.path.join(root, filename) 111 | if path == tar_out: 112 | continue 113 | if path == zip_out: 114 | continue 115 | zip.write(path, arcname=os.path.join(arcname, filename)) 116 | zip.close() 117 | 118 | # save all the files generated as well to our default storage for ephemeral storage setups 119 | if djangui_settings.DJANGUI_EPHEMERAL_FILES: 120 | for root, folders, files in os.walk(abscwd): 121 | for filename in files: 122 | filepath = os.path.join(root, filename) 123 | s3path = os.path.join(root[root.find(cwd):], filename) 124 | remote = utils.get_storage(local=False) 125 | exists = remote.exists(s3path) 126 | filesize = remote.size(s3path) 127 | if not exists or (exists and filesize == 0): 128 | if exists: 129 | remote.delete(s3path) 130 | remote.save(s3path, File(open(filepath, 'rb'))) 131 | 132 | utils.create_job_fileinfo(job) 133 | 134 | 135 | job.stdout = stdout 136 | job.stderr = stderr 137 | job.status = DjanguiJob.COMPLETED 138 | job.save() 139 | 140 | return (stdout, stderr) -------------------------------------------------------------------------------- /djangui/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import autoslug.fields 6 | from django.conf import settings 7 | import djangui.models.mixins 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='DjanguiFile', 19 | fields=[ 20 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 21 | ('filepath', models.FileField(max_length=500, upload_to=b'')), 22 | ('filepreview', models.TextField(null=True, blank=True)), 23 | ('filetype', models.CharField(max_length=255, null=True, blank=True)), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='DjanguiJob', 28 | fields=[ 29 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 30 | ('celery_id', models.CharField(max_length=255, null=True)), 31 | ('job_name', models.CharField(max_length=255)), 32 | ('job_description', models.TextField(null=True, blank=True)), 33 | ('stdout', models.TextField(null=True, blank=True)), 34 | ('stderr', models.TextField(null=True, blank=True)), 35 | ('status', models.CharField(default=b'submitted', max_length=255, choices=[(b'submitted', 'Submitted'), (b'running', 'Running'), (b'completed', 'Completed'), (b'deleted', 'Deleted')])), 36 | ('save_path', models.CharField(max_length=255, null=True, blank=True)), 37 | ('command', models.TextField()), 38 | ('created_date', models.DateTimeField(auto_now_add=True)), 39 | ('modified_date', models.DateTimeField(auto_now=True)), 40 | ], 41 | ), 42 | migrations.CreateModel( 43 | name='Script', 44 | fields=[ 45 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 46 | ('script_name', models.CharField(max_length=255)), 47 | ('slug', autoslug.fields.AutoSlugField(unique=True, editable=False)), 48 | ('script_description', models.TextField(null=True, blank=True)), 49 | ('script_order', models.PositiveSmallIntegerField(default=1)), 50 | ('is_active', models.BooleanField(default=True)), 51 | ('script_path', models.FileField(upload_to=b'')), 52 | ('execute_full_path', models.BooleanField(default=True)), 53 | ('save_path', models.CharField(help_text=b'By default save to the script name, this will change the output folder.', max_length=255, null=True, blank=True)), 54 | ('script_version', models.PositiveSmallIntegerField(default=0)), 55 | ('created_date', models.DateTimeField(auto_now_add=True)), 56 | ('modified_date', models.DateTimeField(auto_now=True)), 57 | ], 58 | bases=(djangui.models.mixins.ModelDiffMixin, models.Model), 59 | ), 60 | migrations.CreateModel( 61 | name='ScriptGroup', 62 | fields=[ 63 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 64 | ('group_name', models.TextField()), 65 | ('slug', autoslug.fields.AutoSlugField(unique=True, editable=False)), 66 | ('group_description', models.TextField(null=True, blank=True)), 67 | ('group_order', models.SmallIntegerField(default=1)), 68 | ('is_active', models.BooleanField(default=True)), 69 | ('user_groups', models.ManyToManyField(to='auth.Group', blank=True)), 70 | ], 71 | bases=(djangui.models.mixins.UpdateScriptsMixin, models.Model), 72 | ), 73 | migrations.CreateModel( 74 | name='ScriptParameter', 75 | fields=[ 76 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 77 | ('short_param', models.CharField(max_length=255)), 78 | ('script_param', models.CharField(max_length=255)), 79 | ('slug', autoslug.fields.AutoSlugField(unique=True, editable=False)), 80 | ('is_output', models.BooleanField(default=None)), 81 | ('required', models.BooleanField(default=False)), 82 | ('output_path', models.FilePathField(path=b'', max_length=255, allow_files=False, recursive=True, allow_folders=True)), 83 | ('choices', models.CharField(max_length=255, null=True, blank=True)), 84 | ('choice_limit', models.PositiveSmallIntegerField(null=True, blank=True)), 85 | ('form_field', models.CharField(max_length=255)), 86 | ('default', models.CharField(max_length=255, null=True, blank=True)), 87 | ('input_type', models.CharField(max_length=255)), 88 | ('param_help', models.TextField(null=True, verbose_name=b'help', blank=True)), 89 | ('is_checked', models.BooleanField(default=False)), 90 | ], 91 | bases=(djangui.models.mixins.UpdateScriptsMixin, models.Model), 92 | ), 93 | migrations.CreateModel( 94 | name='ScriptParameterGroup', 95 | fields=[ 96 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 97 | ('group_name', models.TextField()), 98 | ('script', models.ForeignKey(to='djangui.Script')), 99 | ], 100 | bases=(djangui.models.mixins.UpdateScriptsMixin, models.Model), 101 | ), 102 | migrations.CreateModel( 103 | name='ScriptParameters', 104 | fields=[ 105 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 106 | ('_value', models.TextField(db_column='value')), 107 | ('job', models.ForeignKey(to='djangui.DjanguiJob')), 108 | ('parameter', models.ForeignKey(to='djangui.ScriptParameter')), 109 | ], 110 | ), 111 | migrations.AddField( 112 | model_name='scriptparameter', 113 | name='parameter_group', 114 | field=models.ForeignKey(to='djangui.ScriptParameterGroup'), 115 | ), 116 | migrations.AddField( 117 | model_name='scriptparameter', 118 | name='script', 119 | field=models.ForeignKey(to='djangui.Script'), 120 | ), 121 | migrations.AddField( 122 | model_name='script', 123 | name='script_group', 124 | field=models.ForeignKey(to='djangui.ScriptGroup'), 125 | ), 126 | migrations.AddField( 127 | model_name='script', 128 | name='user_groups', 129 | field=models.ManyToManyField(to='auth.Group', blank=True), 130 | ), 131 | migrations.AddField( 132 | model_name='djanguijob', 133 | name='script', 134 | field=models.ForeignKey(to='djangui.Script'), 135 | ), 136 | migrations.AddField( 137 | model_name='djanguijob', 138 | name='user', 139 | field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True), 140 | ), 141 | migrations.AddField( 142 | model_name='djanguifile', 143 | name='job', 144 | field=models.ForeignKey(to='djangui.DjanguiJob'), 145 | ), 146 | migrations.AddField( 147 | model_name='djanguifile', 148 | name='parameter', 149 | field=models.ForeignKey(blank=True, to='djangui.ScriptParameters', null=True), 150 | ), 151 | ] 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Djangui has been merged with Wooey! You can get the latest and greatest at the organization [Wooey](http://www.github.com/wooey) 2 | 3 | # Djangui 4 | 5 | [![Build Status](https://travis-ci.org/Chris7/django-djangui.svg?branch=master)](https://travis-ci.org/Chris7/django-djangui) [![Coverage Status](https://coveralls.io/repos/Chris7/django-djangui/badge.svg?branch=master)](https://coveralls.io/r/Chris7/django-djangui?branch=master) 6 | 7 | 1. [Installation](#install) 8 | 1. [A Djangui Only Project](#djonly) 9 | 2. [Adding Djangui to Existing Projects](#existing) 10 | 2. [Running Djangui](#running) 11 | 1. [A Procfile](#procfile) 12 | 2. [Two processes](#two-procs) 13 | 3. [Adding Scripts](#adding) 14 | 4. [Script Organization](#organization) 15 | 5. [Script Permissions](#permissions) 16 | 6. [Configuration](#config) 17 | 7. [Usage with S3/remote file systems](#s3) 18 | 8. [Script Guidelines](#script-guide) 19 | 20 | Djangui is designed to take scripts implemented with a python command line argument parser (such as argparse), and convert them into a web interface. 21 | 22 | This project was inspired by how simply and powerfully [sandman](https://github.com/jeffknupp/sandman) could expose users to a database. 23 | It was also based on my own needs as a data scientist to have a system that could: 24 | 25 | 1. Autodocument my workflows for data analysis 26 | (simple model saving). 27 | 2. Enable fellow lab members with no command line 28 | experience to utilize python scripts. 29 | 3. Enable the easy wrapping of any program in simple 30 | python instead of having to use language specific 31 | to existing tools such as Galaxy. 32 | 33 | # Installation 34 | 35 | pip install django-djangui 36 | 37 | ## A Djangui only project 38 | 39 | There is a bootstrapper included with djangui, which will create a Django project and setup most of the needed settings automagically for you. 40 | 41 | 1. djanguify -p ProjectName 42 | 2. Follow the instructions at the end of the bootstrapper 43 | to create the admin user and access the admin page 44 | 3. Login to the admin page wherever the project is 45 | being hosted (locally this would be localhost:8000/admin) 46 | 47 | ## Installation with existing Django Projects 48 | 49 | 1. Add 'djangui' to INSTALLED_APPS in settings.py (and optionally, djcelery unless you wish to tie into an existing celery instance) 50 | 2. Add the following to your urls.py: 51 | `url(r'^', include('djangui.urls')),` 52 | (Note: it does not need to be rooted at your site base, 53 | you can have r'^djangui/'... as your router): 54 | 55 | 3. Migrate your database: 56 | # Django 1.6 and below: 57 | `./manage.py syncdb` 58 | 59 | # Django 1.7 and above 60 | `./manage.py makemigrations` 61 | `./manage.py migrate` 62 | 63 | 4. Ensure the following are in your TEMPLATE_CONTEXT_PROCSSORS variable: 64 | TEMPLATE_CONTEXT_PROCESSORS = [ 65 | ... 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.core.context_processors.request' 68 | ...] 69 | 70 | # Running Djangui 71 | 72 | Djangui depends on a distributed worker to handle tasks, you can disable this by setting **DJANGUI_CELERY** to False in your settings, which will allow you to run Djangui through the simple command: 73 | 74 | ``` 75 | python manage.py runserver 76 | ``` 77 | 78 | However, this will cause the server to execute tasks, which will block the site. 79 | 80 | The recommended ways to run Djangui are: 81 | 82 | ## Through a Procfile 83 | 84 | The simplest way to run Djangui is to use a Procfile with [honcho](https://github.com/nickstenning/honcho), which can be installed via pip. Make a file, called Procfile in the root of your project (the same place as manage.py) with the following contents: 85 | 86 | ``` 87 | web: python manage.py runserver 88 | worker: python manage.py celery worker -c 1 --beat -l info 89 | EOM 90 | ``` 91 | 92 | Your server can then be run by the simple command: 93 | ``` 94 | honcho start 95 | ``` 96 | 97 | ## Through two separate processes 98 | 99 | You can also run djangui by invoking two commands (you will need a separate process for each): 100 | 101 | ``` 102 | python manage.py celery worker -c 1 --beat -l info 103 | python manage.py runserver 104 | ``` 105 | 106 | 107 | # Adding & Managing Scripts 108 | 109 | Scripts may be added in two ways, through the Django admin interface as well as through the *addscript* command in manage.py. 110 | 111 | ### The admin Interface 112 | 113 | Within the django admin interface, scripts may be added to through the 'scripts' model. Here, the user permissions may be set, as 114 | well as cosmetic features such as the script's display name, description (if provided, otherwise the script name and description 115 | will be automatically populated by the description from argparse if available). 116 | 117 | ### The command line 118 | 119 | `./manage.py addscript` 120 | 121 | This will add a script or a folder of scripts to Djangui (if a folder is passed instead of a file). 122 | By default, scripts will be created in the 'Djangui Scripts' group. 123 | 124 | # Script Organization 125 | 126 | Scripts can be viewed at the root url of Djangui. The ordering of scripts, and groupings of scripts can be altered by 127 | changing the 'Script order' or 'Group order' options within the admin. 128 | 129 | # Script Permissions 130 | 131 | Scripts and script groups can be relegated to certain groups of users. The 'user groups' option, if set, will restrict script usage 132 | to users within selected groups. 133 | 134 | Scripts and groups may also be shutoff to all users by unchecked the 'script/group active' option. 135 | 136 | # Configuration 137 | 138 | **DJANGUI_FILE_DIR**: String, where the files uploaded by the user will be saved (Default: djangui_files) 139 | 140 | **DJANGUI_CELERY**: Boolean, whether or not celery is enabled. If disabled, tasks will run locally and block execution. (Default: True) 141 | 142 | **DJANGUI_CELERY_TASKS**: String, the name of the celery tasks for Djangui. (Default: 'djangui.tasks') 143 | 144 | **DJANGUI_ALLOW_ANONYMOUS**: Boolean, whether to allow submission of jobs by anonymous users. (Default: True) 145 | 146 | By default, Djangui has a basic user account system. It is very basic, and doesn't confirm registrations via email. 147 | 148 | **DJANGUI_AUTH**: Boolean, whether to use the authorization system of Djangui for simple login/registration. (Default: True) 149 | 150 | **DJANGUI_LOGIN_URL**: String, if you have an existing authorization system, the login url: (Default: settings.LOGIN_URL 151 | 152 | **DJANGUI_REGISTER_URL**: String, if you have an existing authorization system, the registration url: (Default: /accounts/register/) 153 | 154 | **DJANGUI_EPHEMERAL_FILES**: Boolean, if your file system changes with each restart. (Default: False) 155 | 156 | **DJANGUI_SHOW_LOCKED_SCRIPTS**: Boolean, whether to show locked scripts as disabled or hide them entirely. (Defalt: True -- show as disabled) 157 | 158 | # Remote File Systems 159 | 160 | Djangui has been tested on heroku with S3 as a file storage system. Settings for this can be seen in the user_settings.py, which give you a starting point for a non-local server. In short, you need to change your storage settings like such: 161 | 162 | 163 | 164 | STATICFILES_STORAGE = DEFAULT_FILE_STORAGE = 'djangui.djanguistorage.CachedS3BotoStorage' 165 | DJANGUI_EPHEMERAL_FILES = True 166 | 167 | 168 | 169 | # Script Guidelines 170 | 171 | The easiest way to make your scripts compatible with Djangui is to define your ArgParse class in the global scope. For instance: 172 | 173 | ``` 174 | 175 | import argparse 176 | import sys 177 | 178 | parser = argparse.ArgumentParser(description="https://projecteuler.net/problem=1 -- Find the sum of all the multiples of 3 or 5 below a number.") 179 | parser.add_argument('--below', help='The number to find the sum of multiples below.', type=int, default=1000) 180 | 181 | def main(): 182 | args = parser.parse_args() 183 | ... 184 | 185 | if __name__ == "__main__": 186 | sys.exit(main()) 187 | 188 | ``` 189 | 190 | If you have failing scripts, please open an issue with their contents so I can handle cases as they appear and try to make this as all-encompasing as possible. One known area which fails currently is defining your argparse instance inside the `if __name__ == "__main__" block` 191 | -------------------------------------------------------------------------------- /djangui/templates/djangui/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% load staticfiles %} 3 | 4 | 5 | 6 | 7 | {% block title %}Djangui!{% endblock title %} 8 | 9 | 10 | 11 | 12 | {% block extra_css %} 13 | {% endblock extra_css %} 14 | 15 | 16 | 84 | 85 | 86 | 112 | 113 | {% block left_sidebar %} 114 |
115 | {% block left_sidebar_content %} 116 | {% endblock left_sidebar_content %} 117 |
118 | {% endblock left_sidebar %} 119 | 120 | {% block center %} 121 | 122 |
123 | {# #} 124 | {% block center_content %} 125 | {% endblock center_content %} 126 | {#
#} 127 | 128 | {% endblock center %} 129 | 130 | {% block right_sidebar %} 131 |
132 | {% block right_sidebar_content %} 133 | {% endblock right_sidebar_content %} 134 |
135 | {% endblock right_sidebar %} 136 | 137 | 138 | {% block js %} 139 | 140 | 141 | 142 | 143 | 144 | {% endblock js %} 145 | {% block extra_js %} 146 | {% endblock extra_js %} 147 | {% block inline_js %} 148 | 149 | 225 | {% endblock inline_js %} 226 | -------------------------------------------------------------------------------- /djangui/backend/argparse_specs.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import argparse 3 | import sys 4 | import json 5 | import imp 6 | import traceback 7 | import tempfile 8 | import six 9 | import copy 10 | from collections import OrderedDict 11 | from .ast import source_parser 12 | from itertools import chain 13 | 14 | def is_upload(action): 15 | """Checks if this should be a user upload 16 | 17 | :param action: 18 | :return: True if this is a file we intend to upload from the user 19 | """ 20 | return 'r' in action.type._mode and (action.default is None or 21 | getattr(action.default, 'name') not in (sys.stderr.name, sys.stdout.name)) 22 | 23 | # input attributes we try to set: 24 | # checked, name, type, value 25 | # extra information we want to append: 26 | # help, 27 | # required, 28 | # param (for executing the script and knowing if we need - or --), 29 | # upload (boolean providing info on whether it's a file are we uploading or saving) 30 | # choices (for selections) 31 | # choice_limit (for multi selection) 32 | 33 | CHOICE_LIMIT_MAP = {'?': '1', '+': '>=1', '*': '>=0'} 34 | 35 | # We want to map to model fields as well as html input types we encounter in argparse 36 | # keys are known variable types, as defined in __builtins__ 37 | # the model is a Django based model, which can be fitted in the future for other frameworks. 38 | # The type is the HTML input type 39 | # nullcheck is a function to determine if the default value should be checked (for cases like default='' for strings) 40 | # the attr_kwargs is a mapping of the action attributes to its related html input type. It is a dict 41 | # of the form: {'name_for_html_input': { 42 | # and either one or both of: 43 | # 'action_name': 'attribute_name_on_action', 'callback': 'function_to_evaluate_action_and_return_value'} } 44 | 45 | GLOBAL_ATTRS = ['model', 'type'] 46 | 47 | GLOBAL_ATTR_KWARGS = { 48 | 'name': {'action_name': 'dest'}, 49 | 'value': {'action_name': 'default'}, 50 | 'required': {'action_name': 'required'}, 51 | 'help': {'action_name': 'help'}, 52 | 'param': {'callback': lambda x: x.option_strings[0] if x.option_strings else ''}, 53 | 'choices': {'callback': lambda x: x.choices}, 54 | 'choice_limit': {'callback': lambda x: CHOICE_LIMIT_MAP.get(x.nargs, x.nargs)} 55 | } 56 | 57 | TYPE_FIELDS = { 58 | # Python Builtins 59 | bool: {'model': 'BooleanField', 'type': 'checkbox', 'nullcheck': lambda x: x.default is None, 60 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 61 | float: {'model': 'FloatField', 'type': 'text', 'html5-type': 'number', 'nullcheck': lambda x: x.default is None, 62 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 63 | int: {'model': 'IntegerField', 'type': 'text', 'nullcheck': lambda x: x.default is None, 64 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 65 | None: {'model': 'CharField', 'type': 'text', 'nullcheck': lambda x: x.default is None, 66 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 67 | str: {'model': 'CharField', 'type': 'text', 'nullcheck': lambda x: x.default == '' or x.default is None, 68 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 69 | 70 | # argparse Types 71 | argparse.FileType: {'model': 'FileField', 'type': 'file', 'nullcheck': lambda x: False, 72 | 'attr_kwargs': dict(GLOBAL_ATTR_KWARGS, **{ 73 | 'value': None, 74 | 'required': {'callback': lambda x: x.required or x.default in (sys.stdout, sys.stdin)}, 75 | 'upload': {'callback': is_upload} 76 | })}, 77 | } 78 | if six.PY2: 79 | TYPE_FIELDS.update({ 80 | file: {'model': 'FileField', 'type': 'file', 'nullcheck': lambda x: False, 81 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 82 | unicode: {'model': 'CharField', 'type': 'text', 'nullcheck': lambda x: x.default == '' or x.default is None, 83 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 84 | }) 85 | elif six.PY3: 86 | import io 87 | TYPE_FIELDS.update({ 88 | io.IOBase: {'model': 'FileField', 'type': 'file', 'nullcheck': lambda x: False, 89 | 'attr_kwargs': GLOBAL_ATTR_KWARGS}, 90 | }) 91 | 92 | def update_dict_copy(a, b): 93 | temp = copy.deepcopy(a) 94 | temp.update(b) 95 | return temp 96 | 97 | # There are cases where we can glean additional information about the form structure, e.g. 98 | # a StoreAction with default=True can be different than a StoreTrueAction with default=False 99 | ACTION_CLASS_TO_TYPE_FIELD = { 100 | argparse._StoreAction: update_dict_copy(TYPE_FIELDS, {}), 101 | argparse._StoreConstAction: update_dict_copy(TYPE_FIELDS, {}), 102 | argparse._StoreTrueAction: update_dict_copy(TYPE_FIELDS, { 103 | None: {'model': 'BooleanField', 'type': 'checkbox', 'nullcheck': lambda x: x.default is None, 104 | 'attr_kwargs': update_dict_copy(GLOBAL_ATTR_KWARGS, { 105 | 'checked': {'callback': lambda x: x.default}, 106 | 'value': None, 107 | }) 108 | }, 109 | }), 110 | argparse._StoreFalseAction: update_dict_copy(TYPE_FIELDS, { 111 | None: {'model': 'BooleanField', 'type': 'checkbox', 'nullcheck': lambda x: x.default is None, 112 | 'attr_kwargs': update_dict_copy(GLOBAL_ATTR_KWARGS, { 113 | 'checked': {'callback': lambda x: x.default}, 114 | 'value': None, 115 | }) 116 | }, 117 | }) 118 | } 119 | 120 | class ArgParseNode(object): 121 | """ 122 | This class takes an argument parser entry and assigns it to a Build spec 123 | """ 124 | def __init__(self, action=None): 125 | fields = ACTION_CLASS_TO_TYPE_FIELD.get(type(action), TYPE_FIELDS) 126 | field_type = fields.get(action.type) 127 | if field_type is None: 128 | field_types = [i for i in fields.keys() if i is not None and issubclass(type(action.type), i)] 129 | if len(field_types) > 1: 130 | field_types = [i for i in fields.keys() if i is not None and isinstance(action.type, i)] 131 | if len(field_types) == 1: 132 | field_type = fields[field_types[0]] 133 | self.node_attrs = dict([(i, field_type[i]) for i in GLOBAL_ATTRS]) 134 | null_check = field_type['nullcheck'](action) 135 | for attr, attr_dict in six.iteritems(field_type['attr_kwargs']): 136 | if attr_dict is None: 137 | continue 138 | if attr == 'value' and null_check: 139 | continue 140 | if 'action_name' in attr_dict: 141 | self.node_attrs[attr] = getattr(action, attr_dict['action_name']) 142 | elif 'callback' in attr_dict: 143 | self.node_attrs[attr] = attr_dict['callback'](action) 144 | 145 | @property 146 | def name(self): 147 | return self.node_attrs.get('name') 148 | 149 | def __str__(self): 150 | return json.dumps(self.node_attrs) 151 | 152 | def to_django(self): 153 | """ 154 | This is a debug function to see what equivalent django models are being generated 155 | """ 156 | exclude = {'name', 'model'} 157 | field_module = 'models' 158 | django_kwargs = {} 159 | if self.node_attrs['model'] == 'CharField': 160 | django_kwargs['max_length'] = 255 161 | django_kwargs['blank'] = not self.node_attrs['required'] 162 | try: 163 | django_kwargs['default'] = self.node_attrs['value'] 164 | except KeyError: 165 | pass 166 | return u'{0} = {1}.{2}({3})'.format(self.node_attrs['name'], field_module, self.node_attrs['model'], 167 | ', '.join(['{0}={1}'.format(i,v) for i,v in six.iteritems(django_kwargs)]),) 168 | 169 | 170 | class ArgParseNodeBuilder(object): 171 | def __init__(self, script_path=None, script_name=None): 172 | self.valid = False 173 | self.error = '' 174 | parsers = [] 175 | try: 176 | module = imp.load_source(script_name, script_path) 177 | except: 178 | sys.stderr.write('Error while loading {0}:\n'.format(script_path)) 179 | self.error = '{0}\n'.format(traceback.format_exc()) 180 | sys.stderr.write(self.error) 181 | else: 182 | main_module = module.main.__globals__ if hasattr(module, 'main') else globals() 183 | parsers = [v for i, v in chain(six.iteritems(main_module), six.iteritems(vars(module))) 184 | if issubclass(type(v), argparse.ArgumentParser)] 185 | if not parsers: 186 | f = tempfile.NamedTemporaryFile() 187 | ast_source = source_parser.parse_source_file(script_path) 188 | python_code = source_parser.convert_to_python(list(ast_source)) 189 | f.write(six.b('\n'.join(python_code))) 190 | f.seek(0) 191 | module = imp.load_source(script_name, f.name) 192 | main_module = module.main.__globals__ if hasattr(module, 'main') else globals() 193 | parsers = [v for i, v in chain(six.iteritems(main_module), six.iteritems(vars(module))) 194 | if issubclass(type(v), argparse.ArgumentParser)] 195 | if not parsers: 196 | sys.stderr.write('Unable to identify ArgParser for {0}:\n'.format(script_path)) 197 | return 198 | self.valid = True 199 | parser = parsers[0] 200 | self.class_name = script_name 201 | self.script_path = script_path 202 | self.script_description = getattr(parser, 'description', None) 203 | self.script_groups = [] 204 | self.nodes = OrderedDict() 205 | self.script_groups = [] 206 | non_req = set([i.dest for i in parser._get_optional_actions()]) 207 | self.optional_nodes = set([]) 208 | self.containers = OrderedDict() 209 | for action in parser._actions: 210 | # This is the help message of argparse 211 | if action.default == argparse.SUPPRESS: 212 | continue 213 | node = ArgParseNode(action=action) 214 | container = action.container.title 215 | container_node = self.containers.get(container, None) 216 | if container_node is None: 217 | container_node = [] 218 | self.containers[container] = container_node 219 | self.nodes[node.name] = node 220 | container_node.append(node.name) 221 | if action.dest in non_req: 222 | self.optional_nodes.add(node.name) 223 | 224 | def get_script_description(self): 225 | return {'name': self.class_name, 'path': self.script_path, 226 | 'description': self.script_description, 227 | 'inputs': [{'group': container_name, 'nodes': [self.nodes[node].node_attrs for node in nodes]} 228 | for container_name, nodes in six.iteritems(self.containers)]} 229 | 230 | @property 231 | def json(self): 232 | return json.dumps(self.get_script_description()) -------------------------------------------------------------------------------- /djangui/templates/djangui/tasks/task_view.html: -------------------------------------------------------------------------------- 1 | {% extends "djangui/djangui_home.html" %} 2 | {% load i18n %} 3 | {% load djangui_tags %} 4 | {% block extra_js %} 5 | {{ js.super }} 6 | 7 | {% endblock extra_js %} 8 | {% block extra_css %} 9 | {{ extra_css.super }} 10 | 11 | {% endblock extra_css %} 12 | {% block extra_style %} 13 | {{ extra_style.super }} 14 | #djangui-task-submit { 15 | float: left; 16 | margin-left: 3px; 17 | } 18 | {% endblock extra_style %} 19 | {% block left_sidebar %}{% endblock left_sidebar %} 20 | {% block center_content_class %}col-md-9 col-xs-12{% endblock center_content_class %} 21 | {% block center_content %} 22 | {% if task_error %} 23 |

{{ task_error }}

24 | {% else %} 25 | {% include "djangui/modals/resubmit_modal.html" %} 26 |

{{ task_info.job.job_name }}

27 |
28 | 29 | 30 | 76 | 77 | 78 | 79 |
80 |
81 |
82 | 83 |
84 |
85 |

{% trans "Job Information" %}

86 |
87 |
88 |
89 |
{% trans "Job Status" %}
90 |
{{ task_info.job.status }}
91 |
{% trans "Job Description" %}
92 |
{{ task_info.job.job_description }}
93 |
94 |
95 |
96 |
97 |
98 |

{% trans "Command Sent" %}

99 |
100 |
101 | {{ task_info.job.command }} 102 |
103 |
104 |
105 |
106 |

{% trans "Job Output" %}

107 |
108 |
109 | {{ task_info.job.stdout|linebreaks }} 110 |
111 |
112 |
113 |
114 |

{% trans "Job Errors" %}

115 |
116 |
117 | {{ task_info.job.stderr|linebreaks }} 118 |
119 |
120 |
121 |
122 |
123 | 124 | 130 | 131 |
132 | 133 |
134 |
135 | {% for file in task_info.all_files %} 136 |
{% if file.slug %}{{ file.slug }}{% endif %}
137 |
{{ file.name }}
138 | {% endfor %} 139 |
140 |
141 | {% for output_group, output_files in task_info.file_groups.items %} 142 |
143 | {% if output_group == 'tabular' %} 144 | {% for output_file_content in output_files %} 145 |
146 | 147 | 148 | 149 | 150 |
151 | 152 | {% for entry in output_file_content.preview %} 153 | {% if forloop.first %} 154 | 155 | {% for item in entry %} 156 | 157 | {% endfor %} 158 | 159 | 160 | {% else %} 161 | 162 | {% for item in entry %} 163 | 164 | {% endfor %} 165 | 166 | {% endif %} 167 | {% if forloop.last %} 168 | 169 | {% endif %} 170 | {% endfor %} 171 |
{{ item }}
{{ item }}
172 |
173 |
174 | {% endfor %} 175 | 176 | {% elif output_group == 'images' %} 177 | {% if output_files %} 178 | 194 | {% endif %} 195 |
196 |
197 | {% endif %} 198 |
{{ output_file_content.name }}
199 | {% if not forloop.empty and forloop.last %} 200 |
201 | {% endif %} 202 | {% endfor %} 203 |
204 | 205 | 206 | 207 | 208 | Previous 209 | 210 | 211 | 212 | Next 213 | 214 |
215 | {% endif %} 216 | {% elif output_group == 'fasta' %} 217 | {% for output_file_content in output_files %} 218 |
219 | 220 |
221 | {% for entry in output_file_content.preview %} 222 | {{ entry }}
223 | {% endfor %} 224 |
225 | {% endfor %} 226 | {% endif %} 227 |
228 | {% endfor %} 229 |
230 |
231 |
232 | 233 | 234 | {% endif %} 235 | {% endblock center_content %} 236 | 237 | {% block inline_js %} 238 | {{ block.super }} 239 | 277 | {% endblock %} -------------------------------------------------------------------------------- /djangui/templates/djangui/djangui_home.html: -------------------------------------------------------------------------------- 1 | {% extends "djangui/base.html" %} 2 | {% load i18n %} 3 | {% load djangui_tags %} 4 | 5 | {% block extra_style %} 6 | 7 | {% endblock extra_style %} 8 | 9 | {% block left_sidebar_content %} 10 |
11 | {% for group_id, group in djangui_scripts.items %} 12 | {# check the user has access to the group#} 13 | {% with group_show=group.group|valid_user:request.user %} 14 | {% if group_show != 'hide' %} 15 |
16 | 23 |
24 |
25 |

{{ group.group.group_description }}

26 |
27 | {% for script in group.scripts %} 28 | {% with script_show=script|valid_user:request.user %} 29 | {% if script_show != 'hide' %} 30 | 31 |

{{ script.script_name }}

32 |

{{ script.script_description }}

33 |
34 | {% endif %} 35 | {% endwith %} 36 | {% endfor %} 37 |
38 |
39 |
40 |
41 | {% endif %} 42 | {% endwith %} 43 | {% endfor %} 44 |
45 | {% endblock %} 46 | 47 | {% block center_content %} 48 | 52 |
53 | {% csrf_token %} 54 |
55 | 56 | 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 67 | 68 | 69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 | {% endblock center_content %} 80 | 81 | {% block right_sidebar_content %} 82 |
83 | 95 |
96 | {% if request.user.is_authenticated %} 97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
{% trans "Id" %}{% trans "Name" %}{% trans "Status" %}{% trans "Submitted" %}
110 |
111 | {% endif %} 112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
{% trans "Id" %}{% trans "Name" %}{% trans "Status" %}{% trans "Submitted" %}
126 |
127 |
128 |
129 | {% endblock right_sidebar_content %} 130 | 131 | {% block inline_js %} 132 | {{ block.super }} 133 | 351 | {% endblock inline_js %} -------------------------------------------------------------------------------- /djangui/backend/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | __author__ = 'chris' 3 | import json 4 | import errno 5 | import os 6 | import sys 7 | import six 8 | import traceback 9 | from operator import itemgetter 10 | from collections import OrderedDict 11 | 12 | from django.conf import settings 13 | from django.db import transaction 14 | from django.db.utils import OperationalError 15 | from django.core.files.storage import default_storage 16 | from django.core.files import File 17 | from django.utils.translation import gettext_lazy as _ 18 | from celery.contrib import rdb 19 | 20 | from .argparse_specs import ArgParseNodeBuilder 21 | 22 | from .. import settings as djangui_settings 23 | 24 | def sanitize_name(name): 25 | return name.replace(' ', '_').replace('-', '_') 26 | 27 | 28 | def sanitize_string(value): 29 | return value.replace('"', '\\"') 30 | 31 | def get_storage(local=True): 32 | if djangui_settings.DJANGUI_EPHEMERAL_FILES: 33 | storage = default_storage.local_storage if local else default_storage 34 | else: 35 | storage = default_storage 36 | return storage 37 | 38 | def get_job_commands(job=None): 39 | script = job.script 40 | com = ['python', script.get_script_path()] 41 | parameters = job.get_parameters() 42 | for param in parameters: 43 | com.extend(param.get_subprocess_value()) 44 | return com 45 | 46 | @transaction.atomic 47 | def create_djangui_job(user=None, script_pk=None, data=None): 48 | from ..models import Script, DjanguiJob, ScriptParameter, ScriptParameters 49 | script = Script.objects.get(pk=script_pk) 50 | if data is None: 51 | data = {} 52 | job = DjanguiJob(user=user, job_name=data.pop('job_name', None), job_description=data.pop('job_description', None), 53 | script=script) 54 | job.save() 55 | parameters = OrderedDict([(i.slug, i) for i in ScriptParameter.objects.filter(slug__in=data.keys()).order_by('pk')]) 56 | for slug, param in six.iteritems(parameters): 57 | new_param = ScriptParameters(job=job, parameter=param) 58 | new_param.value = data.get(slug) 59 | new_param.save() 60 | return job 61 | 62 | 63 | def get_master_form(model=None, pk=None): 64 | from ..forms.factory import DJ_FORM_FACTORY 65 | return DJ_FORM_FACTORY.get_master_form(model=model, pk=pk) 66 | 67 | 68 | def get_form_groups(model=None, pk=None, initial=None): 69 | from ..forms.factory import DJ_FORM_FACTORY 70 | return DJ_FORM_FACTORY.get_group_forms(model=model, pk=pk, initial=initial) 71 | 72 | def validate_form(form=None, data=None, files=None): 73 | form.add_djangui_fields() 74 | form.data = data if data is not None else {} 75 | form.files = files if files is not None else {} 76 | form.is_bound = True 77 | form.full_clean() 78 | 79 | def load_scripts(): 80 | from ..models import Script 81 | # select all the scripts we have, then divide them into groups 82 | dj_scripts = {} 83 | try: 84 | scripts = Script.objects.count() 85 | except OperationalError: 86 | # database not initialized yet 87 | return 88 | if scripts: 89 | scripts = Script.objects.all() 90 | for script in scripts: 91 | key = (script.script_group.group_order, script.script_group.pk) 92 | group = dj_scripts.get(key, { 93 | # 'url': reverse_lazy('script_group', kwargs={'script_group', script.script_group.slug}), 94 | 'group': script.script_group, 'scripts': [] 95 | }) 96 | dj_scripts[key] = group 97 | group['scripts'].append((script.script_order, script)) 98 | 99 | for group_pk, group_info in six.iteritems(dj_scripts): 100 | # order scripts 101 | group_info['scripts'].sort(key=itemgetter(0)) 102 | latest_scripts = OrderedDict() 103 | for i in group_info['scripts']: 104 | script = i[1] 105 | script_name = script.script_name 106 | if script_name not in latest_scripts or latest_scripts[script_name].script_version < script.script_version: 107 | latest_scripts[script_name] = script 108 | valid_scripts = [] 109 | for i in latest_scripts.values(): 110 | try: 111 | # make sure we can load the form 112 | get_master_form(script) 113 | except: 114 | sys.stdout.write('Traceback while loading {0}:\n {1}\n'.format(script, traceback.format_exc())) 115 | else: 116 | valid_scripts.append(i) 117 | group_info['scripts'] = valid_scripts 118 | # keep only the latest version of scripts 119 | # order groups 120 | ordered_scripts = OrderedDict() 121 | for key in sorted(dj_scripts.keys(), key=itemgetter(0)): 122 | ordered_scripts[key[1]] = dj_scripts[key] 123 | settings.DJANGUI_SCRIPTS = ordered_scripts 124 | 125 | 126 | def get_storage_object(path, local=False): 127 | storage = get_storage(local=local) 128 | obj = storage.open(path) 129 | obj.url = storage.url(path) 130 | obj.path = storage.path(path) 131 | return obj 132 | 133 | def add_djangui_script(script=None, group=None): 134 | from ..models import Script, ScriptGroup, ScriptParameter, ScriptParameterGroup 135 | # if we have a script, it will at this point be saved in the model pointing to our file system, which may be 136 | # ephemeral. So the path attribute may not be implemented 137 | if not isinstance(script, six.string_types): 138 | try: 139 | script_path = script.script_path.path 140 | except NotImplementedError: 141 | script_path = script.script_path.name 142 | 143 | script_obj, script = (script, get_storage_object(script_path, local=True).path) if isinstance(script, Script) else (False, script) 144 | if isinstance(group, ScriptGroup): 145 | group = group.group_name 146 | if group is None: 147 | group = 'Djangui Scripts' 148 | basename, extension = os.path.splitext(script) 149 | filename = os.path.split(basename)[1] 150 | 151 | parser = ArgParseNodeBuilder(script_name=filename, script_path=script) 152 | if not parser.valid: 153 | return (False, parser.error) 154 | # make our script 155 | d = parser.get_script_description() 156 | script_group, created = ScriptGroup.objects.get_or_create(group_name=group) 157 | if script_obj is False: 158 | djangui_script, created = Script.objects.get_or_create(script_group=script_group, script_description=d['description'], 159 | script_path=script, script_name=d['name']) 160 | else: 161 | created = False 162 | if not script_obj.script_description: 163 | script_obj.script_description = d['description'] 164 | if not script_obj.script_name: 165 | script_obj.script_name = d['name'] 166 | # probably a much better way to avoid this recursion 167 | script_obj._add_script = False 168 | script_obj.save() 169 | if not created: 170 | if script_obj is False: 171 | djangui_script.script_version += 1 172 | djangui_script.save() 173 | if script_obj: 174 | djangui_script = script_obj 175 | # make our parameters 176 | CHOICE_MAPPING = { 177 | 178 | } 179 | for param_group_info in d['inputs']: 180 | param_group, created = ScriptParameterGroup.objects.get_or_create(group_name=param_group_info.get('group'), script=djangui_script) 181 | for param in param_group_info.get('nodes'): 182 | # TODO: fix choice limits 183 | #choice_limit = CHOICE_MAPPING[param.get('choice_limit')] 184 | # TODO: fix 'file' to be global in argparse 185 | is_out = True if param.get('upload', None) is False and param.get('type') == 'file' else not param.get('upload', False) 186 | script_param, created = ScriptParameter.objects.get_or_create(script=djangui_script, short_param=param['param'], script_param=param['name'], 187 | is_output=is_out, required=param.get('required', False), 188 | form_field=param['model'], default=param.get('default'), input_type=param.get('type'), 189 | choices=json.dumps(param.get('choices')), choice_limit=None, 190 | param_help=param.get('help'), is_checked=param.get('checked', False), 191 | parameter_group=param_group) 192 | # update our loaded scripts 193 | load_scripts() 194 | return (True, '') 195 | 196 | def valid_user(obj, user): 197 | groups = obj.user_groups.all() 198 | from ..models import Script 199 | ret = {'valid': False, 'error': '', 'display': ''} 200 | if djangui_settings.DJANGUI_ALLOW_ANONYMOUS or user.is_authenticated(): 201 | if isinstance(obj, Script): 202 | from itertools import chain 203 | groups = list(chain(groups, obj.script_group.user_groups.all())) 204 | if not user.is_authenticated() and djangui_settings.DJANGUI_ALLOW_ANONYMOUS and len(groups) == 0: 205 | ret['valid'] = True 206 | elif groups: 207 | ret['error'] = _('You are not permitted to use this script') 208 | if not groups and obj.is_active: 209 | ret['valid'] = True 210 | if obj.is_active is True: 211 | if set(list(user.groups.all())) & set(list(groups)): 212 | ret['valid'] = True 213 | ret['display'] = 'disabled' if djangui_settings.DJANGUI_SHOW_LOCKED_SCRIPTS else 'hide' 214 | return ret 215 | 216 | def mkdirs(path): 217 | try: 218 | os.makedirs(path) 219 | except OSError as exc: 220 | if exc.errno == errno.EEXIST and os.path.isdir(path): 221 | pass 222 | else: 223 | raise 224 | 225 | 226 | def get_file_info(filepath): 227 | # returns info about the file 228 | filetype, preview = False, None 229 | tests = [('tabular', test_delimited), ('fasta', test_fastx)] 230 | while filetype is False and tests: 231 | ptype, pmethod = tests.pop() 232 | filetype, preview = pmethod(filepath) 233 | filetype = ptype if filetype else filetype 234 | preview = None if filetype is False else preview 235 | filetype = None if filetype is False else filetype 236 | try: 237 | json_preview = json.dumps(preview) 238 | except: 239 | sys.stderr.write('Error encountered in file preview:\n {}\n'.format(traceback.format_exc())) 240 | json_preview = json.dumps(None) 241 | return {'type': filetype, 'preview': json_preview} 242 | 243 | 244 | def test_delimited(filepath): 245 | import csv 246 | if six.PY3: 247 | handle = open(filepath, 'r', newline='') 248 | else: 249 | handle = open(filepath, 'rb') 250 | with handle as csv_file: 251 | try: 252 | dialect = csv.Sniffer().sniff(csv_file.read(1024*16), delimiters=',\t') 253 | except Exception as e: 254 | return False, None 255 | csv_file.seek(0) 256 | reader = csv.reader(csv_file, dialect) 257 | rows = [] 258 | try: 259 | for index, entry in enumerate(reader): 260 | if index == 5: 261 | break 262 | rows.append(entry) 263 | except Exception as e: 264 | return False, None 265 | return True, rows 266 | 267 | def test_fastx(filepath): 268 | # if we can be delimited by + or > we're maybe a fasta/q 269 | with open(filepath) as fastx_file: 270 | sequences = OrderedDict() 271 | seq = [] 272 | header = '' 273 | for row_index, row in enumerate(fastx_file, 1): 274 | if row_index > 30: 275 | break 276 | if row and row[0] == '>': 277 | if seq: 278 | sequences[header] = ''.join(seq) 279 | seq = [] 280 | header = row 281 | elif row: 282 | # we bundle the fastq stuff in here since it's just a visual 283 | seq.append(row) 284 | if seq and header: 285 | sequences[header] = ''.join(seq) 286 | if sequences: 287 | rows = [] 288 | [rows.extend([i, v]) for i,v in six.iteritems(sequences)] 289 | return True, rows 290 | return False, None 291 | 292 | @transaction.atomic 293 | def create_job_fileinfo(job): 294 | parameters = job.get_parameters() 295 | from ..models import DjanguiFile 296 | # first, create a reference to things the script explicitly created that is a parameter 297 | files = [] 298 | for field in parameters: 299 | try: 300 | if field.parameter.form_field == 'FileField': 301 | value = field.value 302 | if value is None: 303 | continue 304 | if isinstance(value, six.string_types): 305 | # check if this was ever created and make a fileobject if so 306 | if get_storage(local=True).exists(value): 307 | if not get_storage(local=False).exists(value): 308 | get_storage(local=False).save(value, File(get_storage(local=True).open(value))) 309 | value = field.value 310 | else: 311 | field.force_value(None) 312 | field.save() 313 | continue 314 | d = {'parameter': field, 'file': value} 315 | files.append(d) 316 | except ValueError: 317 | continue 318 | 319 | known_files = {i['file'].name for i in files} 320 | # add the user_output files, these are things which may be missed by the model fields because the script 321 | # generated them without an explicit arguments reference in the script 322 | file_groups = {'archives': []} 323 | absbase = os.path.join(settings.MEDIA_ROOT, job.save_path) 324 | for filename in os.listdir(absbase): 325 | new_name = os.path.join(job.save_path, filename) 326 | if any([i.endswith(new_name) for i in known_files]): 327 | continue 328 | try: 329 | filepath = os.path.join(absbase, filename) 330 | if os.path.isdir(filepath): 331 | continue 332 | d = {'name': filename, 'file': get_storage_object(os.path.join(job.save_path, filename))} 333 | if filename.endswith('.tar.gz') or filename.endswith('.zip'): 334 | file_groups['archives'].append(d) 335 | else: 336 | files.append(d) 337 | except IOError: 338 | sys.stderr.format('{}'.format(traceback.format_exc())) 339 | continue 340 | 341 | # establish grouping by inferring common things 342 | file_groups['all'] = files 343 | import imghdr 344 | file_groups['images'] = [] 345 | for filemodel in files: 346 | if imghdr.what(filemodel['file'].path): 347 | file_groups['images'].append(filemodel) 348 | file_groups['tabular'] = [] 349 | file_groups['fasta'] = [] 350 | 351 | for filemodel in files: 352 | fileinfo = get_file_info(filemodel['file'].path) 353 | filetype = fileinfo.get('type') 354 | if filetype is not None: 355 | file_groups[filetype].append(dict(filemodel, **{'preview': fileinfo.get('preview')})) 356 | else: 357 | filemodel['preview'] = json.dumps(None) 358 | 359 | # Create our DjanguiFile models 360 | 361 | # mark things that are in groups so we don't add this to the 'all' category too to reduce redundancy 362 | grouped = set([i['file'].path for file_type, groups in six.iteritems(file_groups) for i in groups if file_type != 'all']) 363 | for file_type, group_files in six.iteritems(file_groups): 364 | for group_file in group_files: 365 | if file_type == 'all' and group_file['file'].path in grouped: 366 | continue 367 | try: 368 | preview = group_file.get('preview') 369 | dj_file = DjanguiFile(job=job, filetype=file_type, filepreview=preview, 370 | parameter=group_file.get('parameter')) 371 | filepath = group_file['file'].path 372 | save_path = job.get_relative_path(filepath) 373 | dj_file.filepath.name = save_path 374 | dj_file.save() 375 | except: 376 | sys.stderr.write('Error in saving DJFile: {}\n'.format(traceback.format_exc())) 377 | continue 378 | 379 | 380 | def get_file_previews(job): 381 | from ..models import DjanguiFile 382 | files = DjanguiFile.objects.filter(job=job) 383 | groups = {'all': []} 384 | for file_info in files: 385 | filedict = {'name': file_info.filepath.name, 'preview': json.loads(file_info.filepreview) if file_info.filepreview else None, 386 | 'url': get_storage(local=False).url(file_info.filepath.name), 387 | 'slug': file_info.parameter.parameter.script_param if file_info.parameter else None} 388 | try: 389 | groups[file_info.filetype].append(filedict) 390 | except KeyError: 391 | groups[file_info.filetype] = [filedict] 392 | if file_info.filetype != 'all': 393 | groups['all'].append(filedict) 394 | return groups -------------------------------------------------------------------------------- /djangui/backend/ast/codegen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | codegen 4 | ~~~~~~~ 5 | 6 | Extension to ast that allow ast -> python code generation. 7 | 8 | :copyright: Copyright 2008 by Armin Ronacher. 9 | :license: BSD. 10 | """ 11 | from ast import * 12 | 13 | BOOLOP_SYMBOLS = { 14 | And: 'and', 15 | Or: 'or' 16 | } 17 | 18 | BINOP_SYMBOLS = { 19 | Add: '+', 20 | Sub: '-', 21 | Mult: '*', 22 | Div: '/', 23 | FloorDiv: '//', 24 | Mod: '%', 25 | LShift: '<<', 26 | RShift: '>>', 27 | BitOr: '|', 28 | BitAnd: '&', 29 | BitXor: '^' 30 | } 31 | 32 | CMPOP_SYMBOLS = { 33 | Eq: '==', 34 | Gt: '>', 35 | GtE: '>=', 36 | In: 'in', 37 | Is: 'is', 38 | IsNot: 'is not', 39 | Lt: '<', 40 | LtE: '<=', 41 | NotEq: '!=', 42 | NotIn: 'not in' 43 | } 44 | 45 | UNARYOP_SYMBOLS = { 46 | Invert: '~', 47 | Not: 'not', 48 | UAdd: '+', 49 | USub: '-' 50 | } 51 | 52 | ALL_SYMBOLS = {} 53 | ALL_SYMBOLS.update(BOOLOP_SYMBOLS) 54 | ALL_SYMBOLS.update(BINOP_SYMBOLS) 55 | ALL_SYMBOLS.update(CMPOP_SYMBOLS) 56 | ALL_SYMBOLS.update(UNARYOP_SYMBOLS) 57 | 58 | 59 | def to_source(node, indent_with=' ' * 4, add_line_information=False): 60 | """This function can convert a node tree back into python sourcecode. 61 | This is useful for debugging purposes, especially if you're dealing with 62 | custom asts not generated by python itself. 63 | 64 | It could be that the sourcecode is evaluable when the AST itself is not 65 | compilable / evaluable. The reason for this is that the AST contains some 66 | more data than regular sourcecode does, which is dropped during 67 | conversion. 68 | 69 | Each level of indentation is replaced with `indent_with`. Per default this 70 | parameter is equal to four spaces as suggested by PEP 8, but it might be 71 | adjusted to match the application's styleguide. 72 | 73 | If `add_line_information` is set to `True` comments for the line numbers 74 | of the nodes are added to the output. This can be used to spot wrong line 75 | number information of statement nodes. 76 | """ 77 | generator = SourceGenerator(indent_with, add_line_information) 78 | generator.visit(node) 79 | return ''.join(str(s) for s in generator.result) 80 | 81 | 82 | class SourceGenerator(NodeVisitor): 83 | """This visitor is able to transform a well formed syntax tree into python 84 | sourcecode. For more details have a look at the docstring of the 85 | `node_to_source` function. 86 | """ 87 | 88 | def __init__(self, indent_with, add_line_information=False): 89 | self.result = [] 90 | self.indent_with = indent_with 91 | self.add_line_information = add_line_information 92 | self.indentation = 0 93 | self.new_lines = 0 94 | 95 | def write(self, x): 96 | if self.new_lines: 97 | if self.result: 98 | self.result.append('\n' * self.new_lines) 99 | self.result.append(self.indent_with * self.indentation) 100 | self.new_lines = 0 101 | self.result.append(x) 102 | 103 | def newline(self, node=None, extra=0): 104 | self.new_lines = max(self.new_lines, 1 + extra) 105 | if node is not None and self.add_line_information: 106 | self.write('# line: %s' % node.lineno) 107 | self.new_lines = 1 108 | 109 | def body(self, statements): 110 | self.new_line = True 111 | self.indentation += 1 112 | for stmt in statements: 113 | self.visit(stmt) 114 | self.indentation -= 1 115 | 116 | def body_or_else(self, node): 117 | self.body(node.body) 118 | if node.orelse: 119 | self.newline() 120 | self.write('else:') 121 | self.body(node.orelse) 122 | 123 | def signature(self, node): 124 | want_comma = [] 125 | 126 | def write_comma(): 127 | if want_comma: 128 | self.write(', ') 129 | else: 130 | want_comma.append(True) 131 | 132 | padding = [None] * (len(node.args) - len(node.defaults)) 133 | for arg, default in zip(node.args, padding + node.defaults): 134 | write_comma() 135 | self.visit(arg) 136 | if default is not None: 137 | self.write('=') 138 | self.visit(default) 139 | if node.vararg is not None: 140 | write_comma() 141 | self.write('*' + node.vararg) 142 | if node.kwarg is not None: 143 | write_comma() 144 | self.write('**' + node.kwarg) 145 | 146 | def decorators(self, node): 147 | for decorator in node.decorator_list: 148 | self.newline(decorator) 149 | self.write('@') 150 | self.visit(decorator) 151 | # Statements 152 | 153 | def visit_Assign(self, node): 154 | self.newline(node) 155 | for idx, target in enumerate(node.targets): 156 | if idx: 157 | self.write(', ') 158 | self.visit(target) 159 | self.write(' = ') 160 | self.visit(node.value) 161 | 162 | def visit_AugAssign(self, node): 163 | self.newline(node) 164 | self.visit(node.target) 165 | self.write(BINOP_SYMBOLS[type(node.op)] + '=') 166 | self.visit(node.value) 167 | 168 | def visit_ImportFrom(self, node): 169 | self.newline(node) 170 | self.write('from %s%s import ' % ('.' * node.level, node.module)) 171 | for idx, item in enumerate(node.names): 172 | if idx: 173 | self.write(', ') 174 | self.write(item.name) 175 | 176 | def visit_Import(self, node): 177 | self.newline(node) 178 | if node.names: 179 | self.write('import ') 180 | for item_index, item in enumerate(node.names, 1): 181 | self.visit(item) 182 | if item_index != len(node.names): 183 | self.write(', ') 184 | 185 | def visit_Expr(self, node): 186 | self.newline(node) 187 | self.generic_visit(node) 188 | 189 | def visit_FunctionDef(self, node): 190 | self.newline(extra=1) 191 | self.decorators(node) 192 | self.newline(node) 193 | self.write('def %s(' % node.name) 194 | self.signature(node.args) 195 | self.write('):') 196 | self.body(node.body) 197 | 198 | def visit_ClassDef(self, node): 199 | have_args = [] 200 | 201 | def paren_or_comma(): 202 | if have_args: 203 | self.write(', ') 204 | else: 205 | have_args.append(True) 206 | self.write('(') 207 | 208 | self.newline(extra=2) 209 | self.decorators(node) 210 | self.newline(node) 211 | self.write('class %s' % node.name) 212 | for base in node.bases: 213 | paren_or_comma() 214 | self.visit(base) 215 | # XXX: the if here is used to keep this module compatible 216 | # with python 2.6. 217 | if hasattr(node, 'keywords'): 218 | for keyword in node.keywords: 219 | paren_or_comma() 220 | self.write(keyword.arg + '=') 221 | self.visit(keyword.value) 222 | if node.starargs is not None: 223 | paren_or_comma() 224 | self.write('*') 225 | self.visit(node.starargs) 226 | if node.kwargs is not None: 227 | paren_or_comma() 228 | self.write('**') 229 | self.visit(node.kwargs) 230 | self.write(have_args and '):' or ':') 231 | self.body(node.body) 232 | 233 | def visit_If(self, node): 234 | self.newline(node) 235 | self.write('if ') 236 | self.visit(node.test) 237 | self.write(':') 238 | self.body(node.body) 239 | while True: 240 | else_ = node.orelse 241 | if len(else_) == 1 and isinstance(else_[0], If): 242 | node = else_[0] 243 | self.newline() 244 | self.write('elif ') 245 | self.visit(node.test) 246 | self.write(':') 247 | self.body(node.body) 248 | else: 249 | self.newline() 250 | self.write('else:') 251 | self.body(else_) 252 | break 253 | 254 | def visit_For(self, node): 255 | self.newline(node) 256 | self.write('for ') 257 | self.visit(node.target) 258 | self.write(' in ') 259 | self.visit(node.iter) 260 | self.write(':') 261 | self.body_or_else(node) 262 | 263 | def visit_While(self, node): 264 | self.newline(node) 265 | self.write('while ') 266 | self.visit(node.test) 267 | self.write(':') 268 | self.body_or_else(node) 269 | 270 | def visit_With(self, node): 271 | self.newline(node) 272 | self.write('with ') 273 | self.visit(node.context_expr) 274 | if node.optional_vars is not None: 275 | self.write(' as ') 276 | self.visit(node.optional_vars) 277 | self.write(':') 278 | self.body(node.body) 279 | 280 | def visit_Pass(self, node): 281 | self.newline(node) 282 | self.write('pass') 283 | 284 | def visit_Print(self, node): 285 | # XXX: python 2.6 only 286 | self.newline(node) 287 | self.write('print ') 288 | want_comma = False 289 | if node.dest is not None: 290 | self.write(' >> ') 291 | self.visit(node.dest) 292 | want_comma = True 293 | for value in node.values: 294 | if want_comma: 295 | self.write(', ') 296 | self.visit(value) 297 | want_comma = True 298 | if not node.nl: 299 | self.write(',') 300 | 301 | def visit_Delete(self, node): 302 | self.newline(node) 303 | self.write('del ') 304 | for idx, target in enumerate(node): 305 | if idx: 306 | self.write(', ') 307 | self.visit(target) 308 | 309 | def visit_TryExcept(self, node): 310 | self.newline(node) 311 | self.write('try:') 312 | self.body(node.body) 313 | for handler in node.handlers: 314 | self.visit(handler) 315 | 316 | def visit_TryFinally(self, node): 317 | self.newline(node) 318 | self.write('try:') 319 | self.body(node.body) 320 | self.newline(node) 321 | self.write('finally:') 322 | self.body(node.finalbody) 323 | 324 | def visit_Global(self, node): 325 | self.newline(node) 326 | self.write('global ' + ', '.join(node.names)) 327 | 328 | def visit_Nonlocal(self, node): 329 | self.newline(node) 330 | self.write('nonlocal ' + ', '.join(node.names)) 331 | 332 | def visit_Return(self, node): 333 | self.newline(node) 334 | if node.value: 335 | self.write('return ') 336 | self.visit(node.value) 337 | else: 338 | self.write('return') 339 | 340 | def visit_Break(self, node): 341 | self.newline(node) 342 | self.write('break') 343 | 344 | def visit_Continue(self, node): 345 | self.newline(node) 346 | self.write('continue') 347 | 348 | def visit_Raise(self, node): 349 | # XXX: Python 2.6 / 3.0 compatibility 350 | self.newline(node) 351 | self.write('raise') 352 | if hasattr(node, 'exc') and node.exc is not None: 353 | self.write(' ') 354 | self.visit(node.exc) 355 | if node.cause is not None: 356 | self.write(' from ') 357 | self.visit(node.cause) 358 | elif hasattr(node, 'type') and node.type is not None: 359 | self.visit(node.type) 360 | if node.inst is not None: 361 | self.write(', ') 362 | self.visit(node.inst) 363 | if node.tback is not None: 364 | self.write(', ') 365 | self.visit(node.tback) 366 | # Expressions 367 | 368 | def visit_Attribute(self, node): 369 | self.visit(node.value) 370 | self.write('.' + node.attr) 371 | 372 | def visit_Call(self, node): 373 | want_comma = [] 374 | 375 | def write_comma(): 376 | if want_comma: 377 | self.write(', ') 378 | else: 379 | want_comma.append(True) 380 | 381 | self.visit(node.func) 382 | self.write('(') 383 | for arg in node.args: 384 | write_comma() 385 | self.visit(arg) 386 | for keyword in node.keywords: 387 | write_comma() 388 | self.write(keyword.arg + '=') 389 | self.visit(keyword.value) 390 | if node.starargs is not None: 391 | write_comma() 392 | self.write('*') 393 | self.visit(node.starargs) 394 | if node.kwargs is not None: 395 | write_comma() 396 | self.write('**') 397 | self.visit(node.kwargs) 398 | self.write(')') 399 | 400 | def visit_Name(self, node): 401 | self.write(node.id) 402 | 403 | def visit_Str(self, node): 404 | self.write(repr(node.s)) 405 | 406 | def visit_Bytes(self, node): 407 | self.write(repr(node.s)) 408 | 409 | def visit_Num(self, node): 410 | self.write(repr(node.n)) 411 | 412 | def visit_Tuple(self, node): 413 | self.write('(') 414 | idx = -1 415 | for idx, item in enumerate(node.elts): 416 | if idx: 417 | self.write(', ') 418 | self.visit(item) 419 | self.write(idx and ')' or ',)') 420 | 421 | def sequence_visit(left, right): 422 | def visit(self, node): 423 | self.write(left) 424 | for idx, item in enumerate(node.elts): 425 | if idx: 426 | self.write(', ') 427 | self.visit(item) 428 | self.write(right) 429 | return visit 430 | 431 | visit_List = sequence_visit('[', ']') 432 | visit_Set = sequence_visit('{', '}') 433 | del sequence_visit 434 | 435 | def visit_Dict(self, node): 436 | self.write('{') 437 | for idx, (key, value) in enumerate(zip(node.keys, node.values)): 438 | if idx: 439 | self.write(', ') 440 | self.visit(key) 441 | self.write(': ') 442 | self.visit(value) 443 | self.write('}') 444 | 445 | def visit_BinOp(self, node): 446 | self.visit(node.left) 447 | self.write(' %s ' % BINOP_SYMBOLS[type(node.op)]) 448 | self.visit(node.right) 449 | 450 | def visit_BoolOp(self, node): 451 | self.write('(') 452 | for idx, value in enumerate(node.values): 453 | if idx: 454 | self.write(' %s ' % BOOLOP_SYMBOLS[type(node.op)]) 455 | self.visit(value) 456 | self.write(')') 457 | 458 | def visit_Compare(self, node): 459 | self.write('(') 460 | self.write(node.left) 461 | for op, right in zip(node.ops, node.comparators): 462 | self.write(' %s %%' % CMPOP_SYMBOLS[type(op)]) 463 | self.visit(right) 464 | self.write(')') 465 | 466 | def visit_UnaryOp(self, node): 467 | self.write('(') 468 | op = UNARYOP_SYMBOLS[type(node.op)] 469 | self.write(op) 470 | if op == 'not': 471 | self.write(' ') 472 | self.visit(node.operand) 473 | self.write(')') 474 | 475 | def visit_Subscript(self, node): 476 | self.visit(node.value) 477 | self.write('[') 478 | self.visit(node.slice) 479 | self.write(']') 480 | 481 | def visit_Slice(self, node): 482 | if node.lower is not None: 483 | self.visit(node.lower) 484 | self.write(':') 485 | if node.upper is not None: 486 | self.visit(node.upper) 487 | if node.step is not None: 488 | self.write(':') 489 | if not (isinstance(node.step, Name) and node.step.id == 'None'): 490 | self.visit(node.step) 491 | 492 | def visit_ExtSlice(self, node): 493 | for idx, item in node.dims: 494 | if idx: 495 | self.write(', ') 496 | self.visit(item) 497 | 498 | def visit_Yield(self, node): 499 | self.write('yield ') 500 | self.visit(node.value) 501 | 502 | def visit_Lambda(self, node): 503 | self.write('lambda ') 504 | self.signature(node.args) 505 | self.write(': ') 506 | self.visit(node.body) 507 | 508 | def visit_Ellipsis(self, node): 509 | self.write('Ellipsis') 510 | 511 | def generator_visit(left, right): 512 | def visit(self, node): 513 | self.write(left) 514 | self.visit(node.elt) 515 | for comprehension in node.generators: 516 | self.visit(comprehension) 517 | self.write(right) 518 | return visit 519 | 520 | visit_ListComp = generator_visit('[', ']') 521 | visit_GeneratorExp = generator_visit('(', ')') 522 | visit_SetComp = generator_visit('{', '}') 523 | del generator_visit 524 | 525 | def visit_DictComp(self, node): 526 | self.write('{') 527 | self.visit(node.key) 528 | self.write(': ') 529 | self.visit(node.value) 530 | for comprehension in node.generators: 531 | self.visit(comprehension) 532 | self.write('}') 533 | 534 | def visit_IfExp(self, node): 535 | self.visit(node.body) 536 | self.write(' if ') 537 | self.visit(node.test) 538 | self.write(' else ') 539 | self.visit(node.orelse) 540 | 541 | def visit_Starred(self, node): 542 | self.write('*') 543 | self.visit(node.value) 544 | 545 | def visit_Repr(self, node): 546 | # XXX: python 2.6 only 547 | self.write('`') 548 | self.visit(node.value) 549 | self.write('`') 550 | # Helper Nodes 551 | 552 | def visit_alias(self, node): 553 | self.write(node.name) 554 | if node.asname is not None: 555 | self.write(' as ' + node.asname) 556 | 557 | def visit_comprehension(self, node): 558 | self.write(' for ') 559 | self.visit(node.target) 560 | self.write(' in ') 561 | self.visit(node.iter) 562 | if node.ifs: 563 | for if_ in node.ifs: 564 | self.write(' if ') 565 | self.visit(if_) 566 | 567 | def visit_excepthandler(self, node): 568 | self.newline(node) 569 | self.write('except') 570 | if node.type is not None: 571 | self.write(' ') 572 | self.visit(node.type) 573 | if node.name is not None: 574 | self.write(' as ') 575 | self.visit(node.name) 576 | self.write(':') 577 | self.body(node.body) --------------------------------------------------------------------------------