diff options
Diffstat (limited to 'bh20simplewebuploader')
-rw-r--r-- | bh20simplewebuploader/main.py | 43 | ||||
-rw-r--r-- | bh20simplewebuploader/static/main.js | 31 | ||||
-rw-r--r-- | bh20simplewebuploader/templates/form.html | 54 |
3 files changed, 74 insertions, 54 deletions
diff --git a/bh20simplewebuploader/main.py b/bh20simplewebuploader/main.py index e80713f..f4eabe6 100644 --- a/bh20simplewebuploader/main.py +++ b/bh20simplewebuploader/main.py @@ -48,10 +48,10 @@ def name_to_label(field_name): """ Turn a filed name like "host_health_status" from the metadata schema into a human-readable label. """ - + # May end in a number, which should be set off by a space set_off_number = re.sub('([0-9]+)$', r' \1', field_name) - + return string.capwords(set_off_number.replace('_', ' ')) def is_iri(string): @@ -72,13 +72,13 @@ def generate_form(schema, options): Each dict either has a 'heading' (in which case we put a heading for a form section in the template) or an 'id', 'label', 'type', and 'required' (in which case we make a form field in the template). - + Non-heading dicts with type 'select' will have an 'options' field, with a list of (name, value) tuples, and represent a form dropdown element. - + Non-heading dicts with type 'number' may have a 'step', which, if <1 or 'any', allows the number to be a float. - + Non-heading dicts may have a human-readable 'docstring' field describing them. @@ -125,7 +125,7 @@ def generate_form(schema, options): docstring = None if not isinstance(field_type, str): # If the type isn't a string - + # It may have documentation docstring = field_type.get('doc', None) @@ -159,7 +159,7 @@ def generate_form(schema, options): optional = True # Drop the ? field_type = field_type[:-1] - + # Decide if the field is a list (type ends in []) is_list = False if field_type.endswith('[]'): @@ -280,6 +280,7 @@ def receive_files(): Receive the uploaded files. """ + return 0 # We're going to work in one directory per request dest_dir = tempfile.mkdtemp() # The uploader will happily accept a FASTQ with this name @@ -310,7 +311,7 @@ def receive_files(): elif request.form.get('metadata_type', None) == 'fill': # Build a metadata dict metadata = {} - + # When we have metadata for an item, use this to set it. # If it is an item in a list, set is_list=True def set_metadata(item_id, value, is_list=False): @@ -324,7 +325,7 @@ def receive_files(): if parent not in dest_dict: dest_dict[parent] = {} dest_dict = dest_dict[parent] - + if not is_list: dest_dict[key] = value else: @@ -336,22 +337,22 @@ def receive_files(): # Pull all the field values we wanted from the form if 'heading' in item: continue - + if item['list']: # This is a list, serialized into form fields - + # We count how many values we got value_count = 0 - + for index in itertools.count(): # Get [0] through [n], until something isn't there. entry_id = '{}[{}]'.format(item['id'], index) - + if index == 1000: # Don't let them provide too much stuff. return (render_template('error.html', error_message="You provided an extremely large number of values for the metadata item {}".format(item['id'])), 403) - + if entry_id in request.form: if len(request.form[entry_id]) > 0: # Put an entry in the list @@ -371,7 +372,7 @@ def receive_files(): else: # We have run out of form fields for this list. break - + if item['required'] and value_count == 0: # They forgot a required item. Maybe all entries were empty. return (render_template('error.html', @@ -450,7 +451,7 @@ def getDetailsForSeq(): @app.route('/api/getSEQCountbytech', methods=['GET']) def getSEQCountbytech(): - query="""SELECT ?tech ?tech_label (count(?fasta) as ?fastaCount) WHERE + query="""SELECT ?tech ?tech_label (count(?fasta) as ?fastaCount) WHERE {?fasta ?x [<http://purl.obolibrary.org/obo/OBI_0600047> ?tech] . OPTIONAL {?tech <http://www.w3.org/2000/01/rdf-schema#label> ?tech_tmp_label } . BIND(IF(BOUND(?tech_tmp_label), ?tech_tmp_label,?tech) as ?tech_label)} @@ -466,8 +467,8 @@ def getSEQCountbytech(): ## Is this one really necessary or should we just use getSEQCountbytech instead? @app.route('/api/getAvailableTech', methods=['GET']) def getAvailableTech(): - query="""SELECT distinct ?tech ?tech_label WHERE - {?fasta ?x [<http://purl.obolibrary.org/obo/OBI_0600047> ?tech] + query="""SELECT distinct ?tech ?tech_label WHERE + {?fasta ?x [<http://purl.obolibrary.org/obo/OBI_0600047> ?tech] BIND (concat(?tech,"_label") as ?tech_label) } """ payload = {'query': query, 'format': 'json'} @@ -479,7 +480,7 @@ def getAvailableTech(): ## Has to be encoded again so should be --> http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2FOBI_0000759 @app.route('/api/getSEQbytech', methods=['GET']) def getSEQbytech(): - query="""SELECT ?fasta WHERE + query="""SELECT ?fasta WHERE {?fasta ?x [<http://purl.obolibrary.org/obo/OBI_0600047> <placeholder>] } """ tech = request.args.get('tech') @@ -565,7 +566,7 @@ def getSEQCountbyHostHealthStatus(): @app.route('/api/getSEQbyLocationAndTech', methods=['GET']) def getSEQbyLocationAndTech(): - query="""SELECT ?fasta WHERE { ?fasta ?x [ + query="""SELECT ?fasta WHERE { ?fasta ?x [ <http://purl.obolibrary.org/obo/GAZ_00000448> <placeholderLoc>; <http://purl.obolibrary.org/obo/OBI_0600047> <placeholderTech> ]}""" location=request.args.get('location') tech=request.args.get('tech') @@ -582,7 +583,7 @@ def getSEQbyLocationAndTech(): # Example specimen http%3A%2F%2Fpurl.obolibrary.org%2Fobo%2FNCIT_C155831 @app.route('/api/getSEQbyLocationAndSpecimenSource', methods=['GET']) def getSEQbyLocationAndSpecimenSource(): - query="""SELECT ?fasta WHERE { ?fasta ?x [ + query="""SELECT ?fasta WHERE { ?fasta ?x [ <http://purl.obolibrary.org/obo/GAZ_00000448> <placeholderLoc>; <http://purl.obolibrary.org/obo/OBI_0001479> <placeholderSpecimen> ]} """ location = request.args.get('location') diff --git a/bh20simplewebuploader/static/main.js b/bh20simplewebuploader/static/main.js index 56213fa..7084e1f 100644 --- a/bh20simplewebuploader/static/main.js +++ b/bh20simplewebuploader/static/main.js @@ -49,7 +49,7 @@ let fetchAllaccessions = () => { let uploadForm = document.getElementById('metadata_upload_form') let uploadFormSpot = document.getElementById('metadata_upload_form_spot') let fillForm = document.getElementById('metadata_fill_form') -let fillFormSpot = document.getElementById('metadata_fill_form_spot') +let fillFormSpot = document.getElementById('metadata_fill_form_spot') function setUploadMode() { // Make the upload form the one in use. @@ -91,20 +91,20 @@ setMode() function addField(e) { // Find our parent field-group div let fieldGroup = this.parentElement - + // Get its keypath let keypath = fieldGroup.dataset.keypath - + // Find its last field child let existingFields = fieldGroup.getElementsByClassName('field') let templateField = existingFields[existingFields.length - 1] - + // Get its number let fieldNumber = templateField.dataset.number - + // Duplicate it let newField = templateField.cloneNode(true) - + // Increment the number and use the keypath and number to set IDs and cross // references. // TODO: Heavily dependent on the form field HTML. Maybe we want custom @@ -117,13 +117,13 @@ function addField(e) { newControl.setAttribute('name', newID) let newLabel = newField.getElementsByTagName('label')[0] newLabel.setAttribute('for', newID) - + // Find the minus button let minusButton = fieldGroup.getElementsByClassName('remove-field')[0] - + // Put new field as a child before the minus button fieldGroup.insertBefore(newField, minusButton) - + // Enable the minus button minusButton.classList.remove('invisible') } @@ -134,16 +134,16 @@ function addField(e) { function removeField(e) { // Find our parent field-group div let fieldGroup = this.parentElement - + // Find its field children let existingFields = fieldGroup.getElementsByClassName('field') - + if (existingFields.length > 1) { // There is a last field we can safely remove. let lastField = existingFields[existingFields.length - 1] fieldGroup.removeChild(lastField) } - + if (existingFields.length <= 1) { // Collection auto-updates. Now there's only one element. Don't let the // user remove it. If they don't want it, they can leave it empty. @@ -159,3 +159,10 @@ for (let button of document.getElementsByClassName('remove-field')) { button.addEventListener('click', removeField) } +// Change the submit button after hitting + +function on_submit_button() { + var elem = document.getElementById("submit"); + elem.value = "Submitting..."; + elem.disabled = true; +} diff --git a/bh20simplewebuploader/templates/form.html b/bh20simplewebuploader/templates/form.html index 1bbf515..37a7b72 100644 --- a/bh20simplewebuploader/templates/form.html +++ b/bh20simplewebuploader/templates/form.html @@ -2,7 +2,7 @@ <html> <head> <meta charset="UTF-8"> - <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet"> <link href="/static/main.css" rel="stylesheet" type="text/css"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Web uploader for Public SARS-CoV-2 Sequence Resource</title> @@ -21,10 +21,10 @@ <p>[Demo] Display content sequences by: </p> <div> - <button class="button" onclick="fetchSEQBySpecimen()">Specimen Source</button> + <button class="button" onclick="fetchSEQBySpecimen()">Specimen source</button> <button class="button" onclick="fetchSEQByLocation()">Location</button> - <button class="button" onclick="fetchSEQByTech()">Tech</button> - <button class="button" onclick="fetchAllaccessions()">Allaccessions</button> + <button class="button" onclick="fetchSEQByTech()">Sequencer</button> + <button class="button" onclick="fetchAllaccessions()">All accessions</button> </div> </div> @@ -66,7 +66,9 @@ A free command line version of the uploader can be installed from <a href="https://github.com/arvados/bh20-seq-resource">source</a>. </p> - + <p> + Note that form fields contain web <a href="https://en.wikipedia.org/wiki/Web_Ontology_Language">onthology URI's</a> for <a href="https://en.wikipedia.org/wiki/Wikipedia:Disambiguation">disambiguation</a> and machine readable metadata. For examples of use, see the <a href="https://github.com/arvados/bh20-seq-resource/blob/master/doc/blog/using-covid-19-pubseq-part1.org">BLOG</a>. + </p> </div> <div class="fasta-file-select"> @@ -75,8 +77,8 @@ <path fill-rule="evenodd" d="M5 8.854a.5.5 0 00.707 0L8 6.56l2.293 2.293A.5.5 0 1011 8.146L8.354 5.5a.5.5 0 00-.708 0L5 8.146a.5.5 0 000 .708z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M8 6a.5.5 0 01.5.5v8a.5.5 0 01-1 0v-8A.5.5 0 018 6z" clip-rule="evenodd"/> </svg> Upload SARS-CoV-2 Sequence</h2> - - <label for="fasta">Select FASTA file of assembled genome (max 50K), or FASTQ of reads (<span class="dropt" title="For a larger fastq file you'll need to use a CLI uploader">max 150MB<span style="width:500px;"></span></span>) : </label> + + <label for="fasta">Select FASTA file of assembled genome (form upload <span class="dropt" title="For larger files or bulk uploads you can use a CLI uploader">max 50K<span style="width:500px;"></span></span>; FASTQ and BAM is coming soon): </label> <br> <input type="file" id="fasta" name="fasta" accept=".fa,.fasta,.fna,.fq" required> <br> @@ -97,7 +99,7 @@ <div id="metadata_upload_form_spot"> <div id="metadata_upload_form"> <br> - <label for="metadata">Select JSON or YAML metadata file following <a href="https://github.com/arvados/bh20-seq-resource/blob/master/bh20sequploader/bh20seq-schema.yml" target="_blank">this schema</a> and <a href="https://github.com/arvados/bh20-seq-resource/blob/master/example/metadata.yaml" target="_blank">example</a> (max 50K):</label> + <label for="metadata">Select JSON or YAML metadata file following <a href="https://github.com/arvados/bh20-seq-resource/blob/master/bh20sequploader/bh20seq-schema.yml" target="_blank">this schema</a> and <a href="https://github.com/arvados/bh20-seq-resource/blob/master/example/maximum_metadata_example.yaml" target="_blank">example</a> (max 50K):</label> <br> <input type="file" id="metadata" name="metadata" accept=".json,.yml,.yaml" required> <br> @@ -105,6 +107,7 @@ </div> </div> + <div id="metadata_fill_form_spot"> <div id="metadata_fill_form"> {% for record in fields %} @@ -113,35 +116,44 @@ {% if loop.index > 1 and 2 < 3 %} </div> {% endif %} - <div class="record"> + <div class="record"> <!-- from block, e.g. host fields --> <h4>{{ record['heading'] }}</h4> - {% else %} + {% else %} <!-- handles one field --> <div class="field-group" data-keypath="{{ record['id'] }}"> <div class="field" data-number="0"> <label for="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" title="{{ record.get('docstring', '') }}"> {{ record['label'] }} {{ "*" if record['required'] else "" }} {% if 'docstring' in record %} - <a href='javascript:alert({{ record['docstring'] | tojson }})'>❓</a> + <a href='javascript:alert({{ record['docstring'] | tojson }})'>❓</a> {% endif %} {% if 'ref_iri' in record %} - <a href="{{ record['ref_iri'] }}" target="_blank" title="Ontology Link">🔗</a> + <a href="{{ record['ref_iri'] }}" target="_blank" title="Ontology Link">🔗</a> {% endif %} </label> {% if record['type'] == 'select' %} - <select class="control" id="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" name="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" {{ "required" if record['required'] else "" }}> - <option value="" selected>Choose one...</option> + <select class="control" id="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" name="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" {{ "required" if record['required'] else "" }}> + <option value="" selected>Choose one...</option> {% for option in record['options'] %} - <option value="{{ option[1] }}">{{ option[0] }}</option> + <option value="{{ option[1] }}">{{ option[0] }}</option> {% endfor %} - </select> - {% else %} - <input class="control" type="{{ record['type'] }}" id="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" name="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" {{ "required" if record['required'] else "" }} {{ ("step=" + record['step']) if 'step' in record else ""}}> + </select> + {% else %} <!-- input field --> + <input class="control" + type="{{ record['type'] }}" + id="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" + {{ 'value=http://purl.obolibrary.org/obo/NCBITaxon_9606' if record['id']=='metadata.host.host_species'}} + {{ 'placeholder=http://www.wikidata.org/entity/Q30' if record['id']=='metadata.sample.collection_location'}} + {{ 'placeholder=MyUniqueSampleId.1' if record['id']=='metadata.sample.sample_id'}} + {{ 'value=http://purl.obolibrary.org/obo/NCBITaxon_2697049' if record['id']=='metadata.virus.virus_species'}} + {{ 'readonly=1 value=http://identifiers.org/insdc/MT334571.1#sequence' if record['id']=='metadata.id'}} + name="{{ record['id'] }}{{ '[0]' if record['list'] else ''}}" + {{ "required" if record['required'] else "" }} {{ ("step=" + record['step']) if 'step' in record else ""}}> {% endif %} </div> {% if record['list'] %} - <button type="button" title="Remove field" class="remove-field invisible">➖</button> - <button type="button" title="Add field" class="add-field">➕</button> + <button type="button" title="Remove field" class="remove-field invisible">➖</button> + <button type="button" title="Add field" class="add-field">➕</button> {% endif %} </div> {% endif %} @@ -153,7 +165,7 @@ </div> - <input class="submit" type="submit" value="Add to Pangenome"> + <input class="submit" id="submit" onclick="on_submit_button()" type="submit" value="Add to Pangenome"> </form> </section> <br> |