diff --git a/pyworkflow/project.py b/pyworkflow/project.py index 4637a75899..8cedb0dc61 100644 --- a/pyworkflow/project.py +++ b/pyworkflow/project.py @@ -360,7 +360,9 @@ def getCurrentProtocolView(self): return self._protocolViews[viewKey] - def create(self, runsView=1, readOnly=False, hostsConf=None, protocolsConf=None, chdir=True, workflow=None): + def create(self, runsView=1, readOnly=False, hostsConf=None, + protocolsConf=None, chdir=True, workflow=None): + """Prepare all required paths and files to create a new project. Params: hosts: a list of configuration hosts associated to this projects diff --git a/pyworkflow/web/app/views_project.py b/pyworkflow/web/app/views_project.py index 958aedba4e..1c10d4cc53 100644 --- a/pyworkflow/web/app/views_project.py +++ b/pyworkflow/web/app/views_project.py @@ -39,7 +39,7 @@ from views_util import loadProject, getResourceCss, getResourceIcon, getResourceJs, \ getServiceManager, PROJECT_NAME, SERVICE_NAME, \ getVarFromRequest, CTX_PROJECT_PATH, CTX_PROJECT_NAME, WORKFLOW, MODE - +from workflowarchive.models import Workflow def projects(request): from pyworkflow.utils.utils import prettyDate @@ -66,9 +66,7 @@ def projects(request): def workflows(request): - workflows = getWorkflowsList() - # Annotate existence of projects (hashed) manager = getServiceManager(None) diff --git a/pyworkflow/web/pages/resources/css/projects_style.css b/pyworkflow/web/pages/resources/css/projects_style.css index be1ff3916e..0211efc4fd 100644 --- a/pyworkflow/web/pages/resources/css/projects_style.css +++ b/pyworkflow/web/pages/resources/css/projects_style.css @@ -25,6 +25,10 @@ letter-spacing: 0.3em; text-align: center; } +.workFlowForm { + list-style: none; + margin: 10px; +} .projList { list-style: none; margin: 10px; @@ -52,6 +56,7 @@ overflow-x: hidden; width: 100%; } + li:nth-child(odd) { background-color: #FFF; padding: 0px 10px; diff --git a/pyworkflow/web/pages/settings.py b/pyworkflow/web/pages/settings.py index 0f437b9a82..0cc9203c83 100644 --- a/pyworkflow/web/pages/settings.py +++ b/pyworkflow/web/pages/settings.py @@ -12,8 +12,8 @@ WEB_CONF = pwconfig.loadWebConf() -# DEBUG = True -DEBUG = WEB_CONF['DEBUG'] +DEBUG = True +#DEBUG = WEB_CONF['DEBUG'] TEMPLATE_DEBUG = DEBUG ADMINS = ( @@ -105,8 +105,10 @@ # Additional locations of static files WS_ROOT = os.path.join(pw.HOME, 'web', 'webtools') -serviceFolders = [os.path.join(WS_ROOT, f) for f in os.listdir(WS_ROOT) if os.path.isdir(os.path.join(WS_ROOT, f))] -staticDirs = [os.path.join(pw.HOME, 'web', 'pages')] + serviceFolders +serviceFolders = [os.path.join(WS_ROOT, f) for f in os.listdir(WS_ROOT) + if os.path.isdir(os.path.join(WS_ROOT, f))] +staticDirs = [os.path.join(pw.HOME, 'web', 'pages')] \ + + serviceFolders # Put strings here, like "/home/html/static" or "C:/www/django/static". # Always use forward slashes, even on Windows. @@ -123,7 +125,9 @@ # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. templateDirs = [os.path.join(DIRECTORY_PROJECT)] + serviceFolders -TEMPLATE_DIRS = tuple([os.path.join(d, 'templates') for d in templateDirs]) +templateDirs += [os.path.join(pw.HOME, 'web', 'workflowarchive')] + +TEMPLATE_DIRS = tuple([os.path.join(d, 'templates') for d in templateDirs]) # TEMPLATE_DIRS = ( # os.path.join(DIRECTORY_PROJECT, 'templates'), @@ -179,7 +183,9 @@ # 'django.contrib.admindocs', # 'gunicorn', 'app', - 'resumable' + 'resumable', + 'workflowarchive',# workflow archive project + 'tastypie',# web services support ] try: @@ -235,4 +241,4 @@ EMAIL_HOST = EMAIL_CONF['EMAIL_HOST'] EMAIL_PORT = EMAIL_CONF['EMAIL_PORT'] EMAIL_HOST_USER = EMAIL_CONF['EMAIL_HOST_USER'] -EMAIL_HOST_PASSWORD = EMAIL_CONF['EMAIL_HOST_PASSWORD'] \ No newline at end of file +EMAIL_HOST_PASSWORD = EMAIL_CONF['EMAIL_HOST_PASSWORD'] diff --git a/pyworkflow/web/pages/urls.py b/pyworkflow/web/pages/urls.py index c9422a7cb5..010ffbdcfb 100644 --- a/pyworkflow/web/pages/urls.py +++ b/pyworkflow/web/pages/urls.py @@ -8,12 +8,12 @@ from pyworkflow.web import app from pyworkflow.web.app import views_util, views_home, views_showj from pyworkflow.web.app.views_management import ScipionResumableUploadView - +#from pyworkflow.web.workflowarchive import views as views_workflowarchive admin.autodiscover() from django.conf import settings from pyworkflow.web.pages.settings import WS_ROOT, serviceFolders from django.views.generic import TemplateView - +from django.conf.urls import include # =============================================================================== # URL ASSOCIATION # =============================================================================== @@ -27,7 +27,11 @@ #PROJECT (CONTENT, RUNTABLE AND GRAPH) url(r'^projects/', 'app.views_project.projects'), url(r'^create_project/$', 'app.views_project.create_project'), + #INFO: ROB next should finish in $? url(r'^workflows/', 'app.views_project.workflows'), + #TODO: ROB: I do not like to put all urls together + # I think they should be at their respective applications + url(r'^workflowarchive/', include('workflowarchive.urls')), url(r'^delete_project/$', 'app.views_project.delete_project'), url(r'^project_content/$', 'app.views_project.project_content'), url(r'^get_protocols/$', 'app.views_project.get_protocols'), @@ -89,8 +93,9 @@ #SHOWJ url(r'^showj/$', app.views_showj.showj), #Load showj web url(r'^update_session_table/$', 'app.views_showj.updateSessionTable'), - url(r'^jsmol/$', 'app.views_showj.jsmol'), - url(r'^get_chimera_html/$', 'app.views_showj.get_chimera_html'), + #TODO: ROB the following two modules do not exists!? + ####url(r'^jsmol/$', 'app.views_showj.jsmol'), + ####url(r'^get_chimera_html/$', 'app.views_showj.get_chimera_html'), #BROWSER & UPLOAD FILES url(r'^upload/', 'app.views_management.upload'), @@ -131,3 +136,7 @@ handler500 = "app.views_util.handle500error" urlpatterns = patterns(*mainUrls) + +#TODO ROB need css files +from django.contrib.staticfiles.urls import staticfiles_urlpatterns +urlpatterns += staticfiles_urlpatterns() diff --git a/pyworkflow/web/workflowarchive/HtmlTest/workflow.json b/pyworkflow/web/workflowarchive/HtmlTest/workflow.json new file mode 100644 index 0000000000..5e8c9babba --- /dev/null +++ b/pyworkflow/web/workflowarchive/HtmlTest/workflow.json @@ -0,0 +1,72 @@ +[ + { + "object.className": "ProtStress", + "object.id": "103", + "object.label": "scipion - stress 3", + "object.comment": "", + "runName": null, + "runMode": 0, + "noCpu": 0, + "noMem": 2, + "amountMem": 256, + "noIO": 0, + "timeout": 60, + "delay": 0, + "extraParams": " " + }, + { + "object.className": "ProtMonitorSystem", + "object.id": "200", + "object.label": "scipion - system_monitor", + "object.comment": "", + "runName": null, + "runMode": 0, + "inputProtocols": [ + "103" + ], + "samplingInterval": 1, + "cpuAlert": 90.0, + "memAlert": 90.0, + "swapAlert": 100.0, + "monitorTime": 300.0, + "doMail": false, + "emailFrom": "from@from.fakeadress.com", + "emailTo": "to@to.fakeadress.com", + "smtp": "smtp.fakeadress.com" + }, + { + "object.className": "ProtStress", + "object.id": "467", + "object.label": "scipion - stress 3 (copy)", + "object.comment": "", + "runName": null, + "runMode": 0, + "noCpu": 0, + "noMem": 2, + "amountMem": 256, + "noIO": 0, + "timeout": 60, + "delay": 0, + "extraParams": " " + }, + { + "object.className": "ProtMonitorSystem", + "object.id": "499", + "object.label": "scipion - system_monitor (copy)", + "object.comment": "", + "runName": null, + "runMode": 0, + "inputProtocols": [ + "467" + ], + "samplingInterval": 1, + "cpuAlert": 90.0, + "memAlert": 90.0, + "swapAlert": 100.0, + "monitorTime": 300.0, + "doMail": false, + "emailFrom": "from@from.fakeadress.com", + "emailTo": "to@to.fakeadress.com", + "smtp": "smtp.fakeadress.com" + } +] \ No newline at end of file diff --git a/pyworkflow/web/workflowarchive/HtmlTest/workflow.py b/pyworkflow/web/workflowarchive/HtmlTest/workflow.py new file mode 100644 index 0000000000..c5810dbc3f --- /dev/null +++ b/pyworkflow/web/workflowarchive/HtmlTest/workflow.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import Select +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoAlertPresentException +import unittest, time, re +import string, random +import tempfile + +example_content=""" + + + + + +workflow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
workflow
open/workflowarchive/
typeid=id_workflowNameworkflow_random
typeid=id_descriptionHere goes a description of the tests
typeid=id_file/home/roberto/Scipion/scipion-web/pyworkflow/web/workflowarchive/HtmlTest/workflow.json
clickAndWaitcss=button[type="submit"]
assertTextcss=div.workFlowFormworkflow uploaded
+ +""" + +class Workflow(unittest.TestCase): + def setUp(self): + self.driver = webdriver.Chrome() + self.driver.implicitly_wait(30) + self.base_url = "http://0.0.0.0:8000" + self.verificationErrors = [] + self.accept_next_alert = True + self.workflowName= "wf_" + ''.join(random.choice(string.lowercase) for x in range(16)) + self.createJsonFile() + + def createJsonFile(self): + self.f = tempfile.NamedTemporaryFile(delete=False) + self.f.write(example_content) + self.f.close() + + def test_workflow(self): + #connect and paint upload workflow form + driver = self.driver + driver.get(self.base_url + "/workflowarchive/") + + #fill in form using a random workflow name + driver.find_element_by_id("id_workflowName").clear() + driver.find_element_by_id("id_workflowName").send_keys(self.workflowName) + driver.find_element_by_id("id_description").clear() + driver.find_element_by_id("id_description").send_keys("Here goes a description of the tests") + driver.find_element_by_id("id_file").clear() + driver.find_element_by_id("id_file").send_keys(self.f.name) + + #wait two second before sending workflow + time.sleep(2) + driver.find_element_by_css_selector("button[type=\"submit\"]").click() + + # chek that resul contain magic string + import re + returnedText = driver.find_element_by_css_selector("div.workFlowForm").text + text_found = re.search('workflow uploaded', returnedText) + self.assertNotEqual(text_found, None) + text_found = re.search('hi you', returnedText) + self.assertEqual(text_found, None) + + # wait a bit before clossing window + time.sleep(3) + os.unlink(self.f.name) + + + def is_element_present(self, how, what): + try: self.driver.find_element(by=how, value=what) + except NoSuchElementException as e: return False + return True + + def is_alert_present(self): + try: self.driver.switch_to_alert() + except NoAlertPresentException as e: return False + return True + + def close_alert_and_get_its_text(self): + try: + alert = self.driver.switch_to_alert() + alert_text = alert.text + if self.accept_next_alert: + alert.accept() + else: + alert.dismiss() + return alert_text + finally: self.accept_next_alert = True + + def tearDown(self): + self.driver.quit() + self.assertEqual([], self.verificationErrors) + +if __name__ == "__main__": + unittest.main() diff --git a/pyworkflow/web/workflowarchive/HtmlTest/workflow.selenium b/pyworkflow/web/workflowarchive/HtmlTest/workflow.selenium new file mode 100644 index 0000000000..250575dca3 --- /dev/null +++ b/pyworkflow/web/workflowarchive/HtmlTest/workflow.selenium @@ -0,0 +1,47 @@ + + + + + + +workflow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
workflow
open/workflowarchive/
typeid=id_workflowNameworkflow_random
typeid=id_descriptionHere goes a description of the tests
typeid=id_file/home/roberto/Scipion/scipion-web/pyworkflow/web/workflowarchive/HtmlTest/workflow.json
clickAndWaitcss=button[type="submit"]
assertTextcss=div.workFlowFormworkflow uploaded
+ + diff --git a/pyworkflow/web/workflowarchive/HtmlTest/workflow_betagal1.json b/pyworkflow/web/workflowarchive/HtmlTest/workflow_betagal1.json new file mode 100644 index 0000000000..e5572a69f2 --- /dev/null +++ b/pyworkflow/web/workflowarchive/HtmlTest/workflow_betagal1.json @@ -0,0 +1,146 @@ +[ + { + "object.className": "ProtImportMovies", + "object.id": "2", + "object.label": "import movies", + "object.comment": "\n", + "runName": null, + "runMode": 0, + "importFrom": 0, + "filesPath": "", + "filesPattern": "Falcon*.mrcs", + "copyFiles": false, + "acquisitionWizard": null, + "voltage": 300.0, + "sphericalAberration": 2.0, + "amplitudeContrast": 0.1, + "magnification": 50000, + "samplingRateMode": 0, + "samplingRate": 3.54, + "scannedPixelSize": 7.0, + "gainFile": null + }, + { + "object.className": "ProtMovieAlignment", + "object.id": "40", + "object.label": "movie alignment", + "object.comment": "\n", + "runName": null, + "runMode": 0, + "cleanMovieData": true, + "alignMethod": 0, + "alignFrame0": 0, + "alignFrameN": 0, + "doGPU": false, + "GPUCore": 0, + "winSize": 150, + "sumFrame0": 0, + "sumFrameN": 0, + "cropOffsetX": 0, + "cropOffsetY": 0, + "cropDimX": 0, + "cropDimY": 0, + "binFactor": 1, + "extraParams": "", + "hostName": "localhost", + "numberOfThreads": 4, + "numberOfMpi": 1, + "inputMovies": "2.__attribute__outputMovies" + }, + { + "object.className": "ProtCTFFind", + "object.id": "82", + "object.label": "ctffind4", + "object.comment": "\n", + "runName": null, + "runMode": 0, + "recalculate": false, + "sqliteFile": null, + "ctfDownFactor": 1.0, + "useCftfind4": true, + "astigmatism": 100.0, + "findPhaseShift": false, + "lowRes": 0.05, + "highRes": 0.35, + "minDefocus": 0.5, + "maxDefocus": 4.0, + "windowSize": 256, + "hostName": "localhost", + "numberOfThreads": 4, + "numberOfMpi": 1, + "inputMicrographs": "40.__attribute__outputMicrographs" + }, + { + "object.className": "EmanProtBoxing", + "object.id": "369", + "object.label": "eman2 - boxer", + "object.comment": "", + "runName": null, + "runMode": 0, + "inputMicrographs": "40.__attribute__outputMicrographs" + }, + { + "object.className": "ProtUserSubSet", + "object.id": "380", + "object.label": "3mics", + "object.comment": "", + "runName": null, + "runMode": 0, + "other": null, + "sqliteFile": "Runs/000082_ProtCTFFind/ctfs_selection.sqlite,", + "outputClassName": "SetOfMicrographs", + "inputObject": "82.__attribute__outputCTF" + }, + { + "object.className": "XmippProtParticlePicking", + "object.id": "420", + "object.label": "xmipp3 - manual picking", + "object.comment": "", + "runName": null, + "runMode": 0, + "memory": 2.0, + "inputMicrographs": "40.__attribute__outputMicrographs" + }, + { + "object.className": "XmippProtExtractParticles", + "object.id": "449", + "object.label": "extract 3 mics", + "object.comment": "\n", + "runName": null, + "runMode": 0, + "downsampleType": 0, + "downFactor": 2.0, + "boxSize": 64, + "doSort": false, + "rejectionMethod": 0, + "maxZscore": 3, + "percentage": 5, + "doRemoveDust": true, + "thresholdDust": 3.5, + "doInvert": true, + "doFlip": false, + "doNormalize": true, + "normType": 2, + "backRadius": -1, + "hostName": "localhost", + "numberOfThreads": 1, + "numberOfMpi": 1, + "ctfRelations": "82.__attribute__outputCTF", + "inputCoordinates": "123.__attribute__outputCoordinates", + "inputMicrographs": "369.outputMicrographs" + }, + { + "object.className": "XmippParticlePickingAutomatic", + "object.id": "517", + "object.label": "xmipp3 - auto-picking", + "object.comment": "", + "runName": null, + "runMode": 0, + "micsToPick": 0, + "memory": 2.0, + "hostName": "localhost", + "numberOfThreads": 1, + "numberOfMpi": 1, + "xmippParticlePicking": "420" + } +] diff --git a/pyworkflow/web/workflowarchive/HtmlTest/workflow_betagal2.json b/pyworkflow/web/workflowarchive/HtmlTest/workflow_betagal2.json new file mode 100644 index 0000000000..597411754a --- /dev/null +++ b/pyworkflow/web/workflowarchive/HtmlTest/workflow_betagal2.json @@ -0,0 +1,101 @@ +[ + { + "object.className": "SpiderProtFilter", + "object.id": "2070", + "object.label": "spider - filter particles", + "object.comment": "", + "runName": null, + "runMode": 0, + "filterType": 3, + "filterMode": 0, + "usePadding": true, + "filterRadius": 0.12, + "lowFreq": 0.1, + "highFreq": 0.5, + "temperature": 0.3, + "hostName": "localhost", + "numberOfThreads": 2, + "numberOfMpi": 1 + }, + { + "object.className": "XmippProtCL2DAlign", + "object.id": "2120", + "object.label": "xmipp3 - align with cl2d", + "object.comment": "", + "runName": null, + "runMode": 0, + "useReferenceImage": false, + "maximumShift": 10, + "numberOfIterations": 10, + "hostName": "localhost", + "numberOfThreads": 1, + "numberOfMpi": 4, + "inputParticles": "2070.__attribute__outputParticles" + }, + { + "object.className": "SpiderProtAlignPairwise", + "object.id": "2152", + "object.label": "spider - align pairwise", + "object.comment": "", + "runName": null, + "runMode": 0, + "innerRadius": 5, + "outerRadius": 29, + "cgOption": 1, + "searchRange": 8, + "stepSize": 2, + "hostName": "localhost", + "numberOfThreads": 2, + "inputParticles": "2070.__attribute__outputParticles" + }, + { + "object.className": "SpiderProtCustomMask", + "object.id": "2233", + "object.label": "spider - custom mask", + "object.comment": "", + "runName": null, + "runMode": 0, + "filterRadius1": 0.1, + "sdFactor": 0.9, + "filterRadius2": 0.2, + "maskThreshold": 0.01, + "inputImage": "2152.__attribute__outputAverage" + }, + { + "object.className": "SpiderProtCAPCA", + "object.id": "2267", + "object.label": "spider - capca", + "object.comment": "", + "runName": null, + "runMode": 0, + "analysisType": 0, + "addConstant": 0.0, + "numberOfFactors": 25, + "maskType": 1, + "radius": -1, + "inputParticles": "2152.__attribute__outputParticles", + "maskImage": "2233.__attribute__outputMask" + }, + { + "object.className": "SpiderProtClassifyDiday", + "object.id": "2319", + "object.label": "spider - classify diday", + "object.comment": "", + "runName": null, + "runMode": 0, + "numberOfFactors": 10, + "inputParticles": "2152.__attribute__outputParticles", + "pcaFile": "2267.__attribute__imcFile" + }, + { + "object.className": "SpiderProtClassifyWard", + "object.id": "2346", + "object.label": "spider - classify ward", + "object.comment": "", + "runName": null, + "runMode": 0, + "numberOfFactors": 10, + "inputParticles": "2152.__attribute__outputParticles", + "pcaFile": "2267.__attribute__imcFile" + } +] diff --git a/pyworkflow/web/workflowarchive/__init__.py b/pyworkflow/web/workflowarchive/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pyworkflow/web/workflowarchive/admin.py b/pyworkflow/web/workflowarchive/admin.py new file mode 100644 index 0000000000..31657be115 --- /dev/null +++ b/pyworkflow/web/workflowarchive/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from workflowarchive.models import Workflow + +admin.site.register(Workflow) diff --git a/pyworkflow/web/workflowarchive/api.py b/pyworkflow/web/workflowarchive/api.py new file mode 100644 index 0000000000..923f1fe5fd --- /dev/null +++ b/pyworkflow/web/workflowarchive/api.py @@ -0,0 +1,32 @@ +from tastypie.resources import ModelResource +from tastypie.constants import ALL +from tastypie.utils import trailing_slash +from django.conf.urls import url +import json +from collections import Counter + +from models import Workflow + + +class WorkflowResource(ModelResource): + """allow search in workflow table""" + """http://0.0.0.0:8000/workflowarchive/api/workflow/?format=json + You should get back a list of Workflow-like objects. + http://0.0.0.0:8000/workflowarchive/api/workflow/?format=json&id=1 + http://127.0.0.1:8000/workflowarchive/api/workflow/?format=json + http://127.0.0.1:8000/workflowarchive/api/workflow/1/?format=json + http://127.0.0.1:8000/workflowarchive/api/workflow/schema/?format=json + http://127.0.0.1:8000/workflowarchive/api/workflow/set/1;3/?format=json + http://127.0.0.1:8000/workflowarchive/api/workflow/?uploaded_at__gte=2017-01-01&format=json + """ + + class Meta: + queryset = Workflow.objects.all() + resource_name = 'workflow' + filtering = {'id': ALL, + 'name': ALL, + 'slug': ALL, + 'uploaded_at': ['exact', 'lt', 'lte', 'gte', 'gt'], + } + excludes = ['id','name','uploaded_at','description','resource_uri'] + allowed_methods = ['get'] diff --git a/pyworkflow/web/workflowarchive/batch b/pyworkflow/web/workflowarchive/batch new file mode 100644 index 0000000000..bdcf13ba26 --- /dev/null +++ b/pyworkflow/web/workflowarchive/batch @@ -0,0 +1,14 @@ +webservices + install tastypie django-tastypie==0.12.1 (use 1.13 if django >= 1.7) +selenium tests: + install selenium selenium==3.0.2 + exec files: chromedriver geckodriver + + +Oyther + pytz==2016.7 needs to be installed by hand + +------ + +made urls and templates local + diff --git a/pyworkflow/web/workflowarchive/forms.py b/pyworkflow/web/workflowarchive/forms.py new file mode 100644 index 0000000000..3ed46fbaa4 --- /dev/null +++ b/pyworkflow/web/workflowarchive/forms.py @@ -0,0 +1,70 @@ +from django import forms +from .models import Workflow, VERSIONS + +#The basic idea is to create two forms; one with the FileField and the other +# form with the document, description, ... fields. +#The user views the form with FileField first. Once the user uploads the file +# and submits the request, I call the other form with initial values read +# from the file (I can also delete the file at this step). + +class WorkflowForm(forms.ModelForm): + + class Meta: + model = Workflow + +class FileUploadForm(forms.Form): + + + version = forms.ChoiceField(label="Scipion version", choices=VERSIONS, required=True ) + workflowName = forms.CharField(max_length=128, required=True) + description = forms.CharField(widget=forms.Textarea(), max_length= 512) + email = forms.EmailField(label="Contact person (optinal)", max_length=128, required=False ) + file = forms.FileField(label='Attach workflow file') + + data_dict={} + + def clean_email(self): + self.data_dict['email'] = self.cleaned_data["email"] + + def clean_version(self): + self.data_dict['version'] = self.cleaned_data["version"] + + def clean_workflowName(self): + self.data_dict['name'] = self.cleaned_data["workflowName"] + + def clean_description(self): + self.data_dict['description'] = self.cleaned_data["description"] + + def clean_file(self): + data = self.cleaned_data["file"] + # read and parse the file, create a Python dictionary `data_dict` from it + + self.data_dict['content'] = data.read() + + form = WorkflowForm(self.data_dict) + if form.is_valid(): + # we don't want to put the object to the database on this step + #do I need to save this? + self.instance = form.save(commit=False) + else: + # You can use more specific error message here + raise forms.ValidationError(form.errors) + return data + + def save(self): + # We are not overriding the `save` method here because `form.Form` does not have it. + # We just add it for convenience. + instance = getattr(self, "instance", None) + if instance: + instance.save() + return instance + #fields = ('description', 'document', ) + + def as_p(self): + "Returns this form rendered as HTML

s." + return self._html_output( + normal_row = u'%(label)s

  %(field)s%(help_text)s

 

', + error_row = u'%s', + row_ender = '

', + help_text_html = u' %s', + errors_on_separate_row = True) \ No newline at end of file diff --git a/pyworkflow/web/workflowarchive/models.py b/pyworkflow/web/workflowarchive/models.py new file mode 100644 index 0000000000..7da27d1061 --- /dev/null +++ b/pyworkflow/web/workflowarchive/models.py @@ -0,0 +1,31 @@ +from django.db import models +from django.utils.text import slugify +from tastypie.utils.timezone import now + +VERSIONS = ( + ('0.0','Unknown'), + ('1.0', 'V-1.0'), + ('1.1', 'V-1.1'), +) + +class Workflow(models.Model): + uploaded_at = models.DateTimeField(auto_now_add=True) + name = models.CharField(max_length=128, unique=True) + description = models.CharField(max_length=512, blank=True) + content = models.CharField(max_length=16384)#For secury reasons this can not be unlimited + slug = models.SlugField(null=True, blank=True) + version = models.CharField(max_length=3, choices=VERSIONS, default='1') + email = models.EmailField(max_length=128,blank=True) + + #name is going to work as PK and as part of URL + #so we need to sanitize it (remove spaces, "/", etc + def save(self, *args, **kwargs): + # For automatic slug generation. + if not self.slug: + self.slug = slugify(self.name)[:50] + + return super(Workflow, self).save(*args, **kwargs) + + def __unicode__(self): + return "%s (%d)"%(self.name, self.id) + diff --git a/pyworkflow/web/workflowarchive/templates/wa_download.html b/pyworkflow/web/workflowarchive/templates/wa_download.html new file mode 100644 index 0000000000..a962666b5c --- /dev/null +++ b/pyworkflow/web/workflowarchive/templates/wa_download.html @@ -0,0 +1,6 @@ +{% block title %} {{Workflows}}{% endblock %} + + {{content}} + + + diff --git a/pyworkflow/web/workflowarchive/templates/wa_model_form_upload.html b/pyworkflow/web/workflowarchive/templates/wa_model_form_upload.html new file mode 100644 index 0000000000..0c97be7210 --- /dev/null +++ b/pyworkflow/web/workflowarchive/templates/wa_model_form_upload.html @@ -0,0 +1,40 @@ +{% extends 'base_grid.html'%} +{% block content_head %} + + + + +{% endblock %} + + +{% block content %} +
+ + {% for error in form.non_field_errors %} + {{ error|escape }} + {% endfor %} + {% if done %} + workflow uploaded + {% else %} +
+ {% csrf_token %} + + {{ form.version.errors }} + {{ form.as_p }} + + + + + + +
+ {% endif %} +

Return to home

+

go to workflows

+ +
+{% endblock %} \ No newline at end of file diff --git a/pyworkflow/web/workflowarchive/templates/wa_workflows.html b/pyworkflow/web/workflowarchive/templates/wa_workflows.html new file mode 100644 index 0000000000..1539ab2a97 --- /dev/null +++ b/pyworkflow/web/workflowarchive/templates/wa_workflows.html @@ -0,0 +1,61 @@ +{% extends 'base_grid.html'%} + +{% block title %} - {{Workflows}}{% endblock %} + +{% block content_head %} + + + + + + +{% endblock %} + +{% block content_header %} {% include "header.html"%} {% endblock %} + +{% block content %} +
+ +
+{% endblock %} + + + diff --git a/pyworkflow/web/workflowarchive/tests.py b/pyworkflow/web/workflowarchive/tests.py new file mode 100644 index 0000000000..fc0d761121 --- /dev/null +++ b/pyworkflow/web/workflowarchive/tests.py @@ -0,0 +1,156 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" +from django.test import TestCase +from django.test import Client +from models import Workflow +import json, urllib2 +from tastypie.test import ResourceTestCase + +example_content=""" + + + + + +workflow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
workflow
open/workflowarchive/
typeid=id_workflowNameworkflow_random
typeid=id_descriptionHere goes a description of the tests
typeid=id_file/home/roberto/Scipion/scipion-web/pyworkflow/web/workflowarchive/HtmlTest/workflow.json
clickAndWaitcss=button[type="submit"]
assertTextcss=div.workFlowFormworkflow uploaded
+ +""" + +def deleteWorkflow( name): + try: + w = Workflow.objects.get(name=name) + w.delete() + except Workflow.DoesNotExist: + pass + +def createWorkflow( name, description, content, version='1.0'): + try: + w = Workflow.objects.get(name=name) + except Workflow.DoesNotExist: + #w = Workflow(name=name, description=description, content=content, version=version) + w = Workflow(name=name, description=description, content=content, version=version) + w.save() + return w + +def findWorkflow( workflowId): + try: + w = Workflow.objects.get(id=workflowId) + except Workflow.DoesNotExist: + return None + return w + + +class ModelsTest(TestCase): + + def setUp(self): + #self.client = Client() + self.workflowContent = example_content + self.workflowName = u"workflow_1" + self.workflowName2 = u"workflow_2" + self.workflowDescription = "description for workflow_1" + self.workflowDescription2 = "description for workflow_2" + + + def test_create_workflow(self): + + wIn = createWorkflow(self.workflowName, + self.workflowDescription, + self.workflowContent) + + wOut = findWorkflow(wIn.id) + self.assertEqual(wIn, wOut) + + wIn2 = createWorkflow(self.workflowName2, + self.workflowDescription2, + self.workflowContent) + + wOut2 = findWorkflow(wIn2.id) + self.assertEqual(wIn2, wOut2) + +class WorkflowResourceTest(ResourceTestCase, TestCase): + """Import this will not query the test data base!!!!""" + + def setUp(self): + super(WorkflowResourceTest, self).setUp() + self.client = Client() + self.workflowContent = example_content + self.workflowName = u"workflow_11" + self.workflowName2 = u"workflow_22" + self.workflowDescription = "description for workflow_11" + self.workflowDescription2 = "description for workflow_22" + #webservice url + #self.url = "http://127.0.0.1:8000" + self.api = "/workflowarchive/api/workflow/"#?format=json + + #fill the database + deleteWorkflow(self.workflowName) + deleteWorkflow(self.workflowName2) + + wIn = createWorkflow(self.workflowName, + self.workflowDescription, + self.workflowContent) + + wIn2 = createWorkflow(self.workflowName2, + self.workflowDescription2, + self.workflowContent) + + def test_query_single_object_Api(self): + _url = self.api + "1/" + resp = self.api_client.get(_url, format='json') + _queryResultDic = self.deserialize(resp) + self.assertEqual(_queryResultDic['slug'],self.workflowName) + + + + def test_query_many_objects_Api(self): + _url = self.api + "?slug=%s"%self.workflowName2 + resp = self.api_client.get(_url, format='json') + _queryResultDic = self.deserialize(resp) + q = _queryResultDic["objects"][0] + self.assertEqual(q['slug'],self.workflowName2) + + #retieve all stored workflows + #_url = self.url + self.api + "?name=%s&format=json"%self.workflowName2 + #print json.load(urllib2.urlopen(_url)) diff --git a/pyworkflow/web/workflowarchive/urls.py b/pyworkflow/web/workflowarchive/urls.py new file mode 100644 index 0000000000..ef3d61817a --- /dev/null +++ b/pyworkflow/web/workflowarchive/urls.py @@ -0,0 +1,12 @@ +from django.conf.urls import patterns, url, include +import views +from api import WorkflowResource +workflow_resource = WorkflowResource() + +urlpatterns = patterns('', + url(r'^$', views.archive_workflow, name='archive_workflow'), + url(r'^archive/$', views.archive_workflow, name='archive'), + url(r'^list/$', views.list_workflow, name='list_workflow'), + url(r'download/(?P\d+)/',views.download_workflow,name='download_workflow'), + url(r'^api/', include(workflow_resource.urls)), +) diff --git a/pyworkflow/web/workflowarchive/views.py b/pyworkflow/web/workflowarchive/views.py new file mode 100644 index 0000000000..8407e0c3b6 --- /dev/null +++ b/pyworkflow/web/workflowarchive/views.py @@ -0,0 +1,55 @@ +# Create your views here. +from django.http import HttpResponse +from forms import FileUploadForm +from django.shortcuts import render +from app.views_util import loadProject, getResourceCss, getResourceIcon, getResourceJs +from app.views_base import base_grid +from models import Workflow +import json +import urllib2 +def archive_workflow(request): + context = {'projects_css': getResourceCss('projects'), + 'project_utils_js': getResourceJs('project_utils'), + } + if request.method == 'POST': + form = FileUploadForm(request.POST, request.FILES) + if form.is_valid(): + print "Valid form, save it" + form.save() + context['done']=True + else: + print "non valid" + pass + context['form']= form + + else: + form = FileUploadForm() + context['form']= form + + context = base_grid(request, context) + return render(request, 'wa_model_form_upload.html', context) + +def list_workflow(request): + + workflows = Workflow.objects.all().order_by("-id") + + context = {'projects_css': getResourceCss('projects'), + 'project_utils_js': getResourceJs('project_utils'), + 'workflows': workflows, + 'host':request.META['HTTP_HOST'] + } + + context = base_grid(request, context) + return render(request,'wa_workflows.html', context) + +def download_workflow(request,workflow_id=1): + _url = request.build_absolute_uri("/workflowarchive/api/workflow/%s/?format=json"%workflow_id) + _workflow = json.load(urllib2.urlopen(_url)) + content = _workflow['content'] + slugName = _workflow['slug'] + #context = {'content': content} + #return render(request,'wa_download.html', context) + + response = HttpResponse(content, mimetype='application/octet-stream') + response['Content-Disposition'] = 'attachment; filename=%s' % (slugName) + return response diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..32524ad78a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +backports-abc==0.5 +backports.ssl-match-hostname==3.5.0.1 +bibtexparser==0.5 +certifi==2016.9.26 +Django==1.5.5 +django-resumable==0.1 +django-tastypie==0.12.1 +matplotlib==1.3.1 +mpi4py==1.3.1 +nose==1.3.7 +numpy==1.8.1 +Pillow==2.5.1 +psutil==2.1.1 +pyparsing==2.0.2 +python-dateutil==1.5 +python-magic==0.4.12 +python-mimeparse==1.6.0 +pytz==2016.7 +scipy==0.14.0 +selenium==3.0.2 +singledispatch==3.4.0.3 +six==1.7.3 +tornado==4.4.2