Repository: hed-standard/hed-python (language: python)

Issue:
Web work merging with master



Discussion / hints:

Fix patch:
diff --git a/hedtools/requirements.txt b/hedtools/requirements.txt
index a4cc38bcc..3ed7e91f6 100644
--- a/hedtools/requirements.txt
+++ b/hedtools/requirements.txt
@@ -7,7 +7,7 @@ openpyxl==3.0.5
 pandas==1.1.4
 portalocker==2.0.0
 python-dateutil==2.8.1
-pytz==2020.4
+pytz==2021.1
 semantic_version==2.8.5
 six==1.15.0
 xlrd==1.2.0
diff --git a/hedweb/hed/web/constants/common_constants.py b/hedweb/hed/web/constants/common_constants.py
index 9587dfd24..894437100 100644
--- a/hedweb/hed/web/constants/common_constants.py
+++ b/hedweb/hed/web/constants/common_constants.py
@@ -54,6 +54,9 @@
 SCHEMA_URL_FLASH = 'schema-url-flash'
 SCHEMA_URL_DISPLAY_NAME = 'schema-url-display-name'
 SCHEMA_URL_OPTION = 'schema-url-option'
+
+SERVICE_SUBMIT = 'service-submit'
+
 # Type constants
 BOOLEAN = 'boolean'
 STRING = 'string'
@@ -61,7 +64,7 @@
 
 # Validation-specific constants
 VALIDATION_FORM = 'validation-form'
-VALIDATION_OUTPUT_FILE_PREFIX = 'validated'
+VALIDATION_OUTPUT_FILE_PREFIX = 'validation_errors'
 VALIDATION_SUBMIT = 'validation-submit'
 VALIDATION_SUBMIT_FLASH = 'validation-submit-flash'
-EEG_SUBMIT = 'eeg-submit'
+
diff --git a/hedweb/hed/web/constants/page_constants.py b/hedweb/hed/web/constants/page_constants.py
index 0a3b21575..77129f215 100644
--- a/hedweb/hed/web/constants/page_constants.py
+++ b/hedweb/hed/web/constants/page_constants.py
@@ -1,9 +1,9 @@
 ADDITIONAL_EXAMPLES_PAGE = 'additional-examples.html'
 COMMON_ERRORS_PAGE = 'common-errors.html'
 DICTIONARY_VALIDATION_PAGE = 'dictionary.html'
-EEG_VALIDATION_PAGE = 'eeg-spreadsheet.html'
+EVENTS_PAGE = 'events.html'
+HED_SERVICES_PAGE = 'hed-services.html'
 HED_TOOLS_HELP_PAGE = 'hed-tools-help.html'
 HED_TOOLS_HOME_PAGE = 'hed-tools-home.html'
 SCHEMA_PAGE = 'schema.html'
-SPREADSHEET_VALIDATION_PAGE = 'spreadsheet.html'
-EVENTS_VALIDATION_PAGE = 'events.html'
+SPREADSHEET_PAGE = 'spreadsheet.html'
diff --git a/hedweb/hed/web/constants/route_constants.py b/hedweb/hed/web/constants/route_constants.py
index 1bdf83a1d..5883c945f 100644
--- a/hedweb/hed/web/constants/route_constants.py
+++ b/hedweb/hed/web/constants/route_constants.py
@@ -1,21 +1,22 @@
 ADDITIONAL_EXAMPLES_ROUTE = '/additional-examples'
 COMMON_ERRORS_ROUTE = '/common-errors'
 DELETE_FILE_ROUTE = '/delete/<filename>'
-DICTIONARY_VALIDATION_ROUTE = '/dictionary-validation'
+DICTIONARY_ROUTE = '/dictionary'
 DICTIONARY_VALIDATION_SUBMIT_ROUTE = '/dictionary-validation-submit'
 DOWNLOAD_FILE_ROUTE = '/download-file/<filename>'
-EEG_VALIDATION_SUBMIT_ROUTE = '/eeg-validation-submit'
-EEG_VALIDATION_ROUTE = '/eeg-validation'
-EVENTS_VALIDATION_ROUTE = '/events-validation'
+
+EVENTS_ROUTE = '/events'
 EVENTS_VALIDATION_SUBMIT_ROUTE = '/events-validation-submit'
 HED_MAJOR_VERSION_ROUTE = '/get-hed-major-versions'
+HED_SERVICES_SUBMIT_ROUTE = '/hed-services-submit'
+HED_SERVICES_ROUTE = '/hed-services'
 HED_TOOLS_HELP_ROUTE = '/hed-tools-help'
 HED_TOOLS_HOME_ROUTE = '/'
 HED_VERSION_ROUTE = '/get-hed-version'
 SCHEMA_ROUTE = '/schema'
-SCHEMA_CONVERSION_SUBMIT_ROUTE = '/schema-conversion-submit'
 SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE = '/schema-compliance-check-submit'
+SCHEMA_CONVERSION_SUBMIT_ROUTE = '/schema-conversion-submit'
 SPREADSHEET_COLUMN_INFO_ROUTE = '/get-spreadsheet-columns-info'
-SPREADSHEET_VALIDATION_ROUTE = '/spreadsheet-validation'
+SPREADSHEET_ROUTE = '/spreadsheet'
 SPREADSHEET_VALIDATION_SUBMIT_ROUTE = '/spreadsheet-validation-submit'
 WORKSHEET_COLUMN_INFO_ROUTE = '/get-worksheets-info'
diff --git a/hedweb/hed/web/dictionary.py b/hedweb/hed/web/dictionary.py
index 0bf4d083c..57caece50 100644
--- a/hedweb/hed/web/dictionary.py
+++ b/hedweb/hed/web/dictionary.py
@@ -1,29 +1,27 @@
 import json
-import os
 import traceback
 from urllib.error import URLError, HTTPError
 from flask import current_app
 
-from hed.util.file_util import get_file_extension, delete_file_if_it_exists
+from hed.util.file_util import get_file_extension, delete_file_if_it_exists, write_strings_to_file
 from hed.validator.hed_validator import HedValidator
 from hed.util.column_def_group import ColumnDefGroup
 from hed.util.hed_schema import HedSchema
 
 from hed.web.constants import common_constants, error_constants, file_constants
-from hed.web.web_utils import generate_issues_filename, generate_download_file_response, \
-    handle_http_error, get_hed_path_from_pull_down, \
-    get_uploaded_file_path_from_form, save_file_to_upload_folder
-from hed.web.utils import get_optional_form_field
+from hed.web.web_utils import generate_filename, generate_download_file_response, \
+    handle_http_error, get_hed_path_from_pull_down, get_printable_issue_string, \
+    get_uploaded_file_path_from_form, save_file_to_upload_folder, save_text_to_upload_folder, get_optional_form_field
 
 app_config = current_app.config
 
 
-def generate_arguments_from_dictionary_form(form_request_object):
+def generate_arguments_from_dictionary_form(request):
     """Gets the validation function input arguments from a request object associated with the validation form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -31,17 +29,19 @@ def generate_arguments_from_dictionary_form(form_request_object):
     dictionary
         A dictionary containing input arguments for calling the underlying validation function.
     """
-    hed_file_path, hed_display_name = get_hed_path_from_pull_down(form_request_object)
-    uploaded_file_name, original_file_name = \
-        get_uploaded_file_path_from_form(form_request_object, common_constants.JSON_FILE,
-                                         file_constants.DICTIONARY_FILE_EXTENSIONS)
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
+    uploaded_file_name, original_file_name = get_uploaded_file_path_from_form(request, common_constants.JSON_FILE,
+                                                                              file_constants.DICTIONARY_FILE_EXTENSIONS)
 
     input_arguments = {common_constants.HED_XML_FILE: hed_file_path,
                        common_constants.HED_DISPLAY_NAME: hed_display_name,
                        common_constants.JSON_PATH: uploaded_file_name,
-                       common_constants.JSON_FILE: original_file_name}
-    input_arguments[common_constants.CHECK_FOR_WARNINGS] = get_optional_form_field(
-        form_request_object, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
+                       common_constants.JSON_FILE: original_file_name,
+                       common_constants.CHECK_FOR_WARNINGS: get_optional_form_field(request,
+                                                                                    common_constants.CHECK_FOR_WARNINGS,
+                                                                                    common_constants.BOOLEAN)
+                       }
+
     return input_arguments
 
 
@@ -91,12 +91,12 @@ def report_eeg_events_validation_status(request):
     return validation_status
 
 
-def report_dictionary_validation_status(form_request_object):
+def report_dictionary_validation_status(request):
     """Reports the spreadsheet validation status.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -107,15 +107,8 @@ def report_dictionary_validation_status(form_request_object):
     """
     input_arguments = []
     try:
-        input_arguments = generate_arguments_from_dictionary_form(form_request_object)
-        validation_issues = validate_dictionary(input_arguments)
-        if validation_issues:
-            issue_file = save_issues_to_upload_folder(input_arguments.get(common_constants.JSON_FILE, ''),
-                                                      validation_issues)
-            download_response = generate_download_file_response(issue_file)
-            if isinstance(download_response, str):
-                return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-            return download_response
+        input_arguments = generate_arguments_from_dictionary_form(request)
+        return validate_dictionary(input_arguments, hed_schema=None)
     except HTTPError:
         return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
@@ -127,49 +120,38 @@ def report_dictionary_validation_status(form_request_object):
     return ""
 
 
-def save_issues_to_upload_folder(dictionary_filename, validation_issues, file_mode='w'):
-    """Saves the validation issues found to a other in the upload folder.
-
-    Parameters
-    ----------
-    dictionary_filename: str
-        The name to the dictionary.
-    validation_issues: dictionary
-        A string containing the validation issues.
-    file_mode: str
-        Either 'w' to create a new file and write or 'a' to create a new file if necessary and append to end.
-    Returns
-    -------
-    string
-        The name of the validation output other.
-
-    """
-    validation_issues_filename = \
-        generate_issues_filename(dictionary_filename, common_constants.VALIDATION_OUTPUT_FILE_PREFIX, '')
-    validation_issues_file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], validation_issues_filename)
-    with open(validation_issues_file_path, file_mode, encoding='utf-8') as validation_issues_file:
-        for val_issue in validation_issues:
-            validation_issues_file.write(val_issue['message'] + "\n")
-    validation_issues_file.close()
-    return validation_issues_file_path
-
-
-def validate_dictionary(validation_arguments):
-    """Validates the dictionary.
+def validate_dictionary(input_arguments, hed_schema=None, return_response=True):
+    """Validates the dictionary and returns a
 
     Parameters
     ----------
-    validation_arguments: dictionary
-        A dictionary containing the arguments for the validation function.
+    input_arguments: dict
+        Dictionary containing standard input form arguments
+    hed_schema: str or HedSchema
+        Version number or path or HedSchema object to be used
+    return_response: bool
+        If true, return a Response. If false return a printable issue string
 
     Returns
     -------
-    list
-        Contains a list of dictionaries of errors.
+    Response or str
+        Either a Response or a printable issue string
     """
 
-    json_dictionary = ColumnDefGroup(validation_arguments.get(common_constants.JSON_PATH, ''))
-    hed_schema = HedSchema(validation_arguments.get(common_constants.HED_XML_FILE, ''))
-    display_name = validation_arguments.get(common_constants.JSON_FILE, '')
-    issues = json_dictionary.validate_entries(hed_schema=hed_schema, display_filename=display_name)
-    return issues
+    json_dictionary = ColumnDefGroup(input_arguments.get(common_constants.JSON_PATH, ''))
+    if not hed_schema:
+        hed_schema = HedSchema(input_arguments.get(common_constants.HED_XML_FILE, ''))
+    issues = json_dictionary.validate_entries(hed_schema)
+    if issues:
+        display_name = input_arguments.get(common_constants.JSON_FILE, '')
+        issue_str = get_printable_issue_string(issues, display_name, 'HED validation errors')
+        if not return_response:
+            return issue_str
+        file_name = generate_filename(display_name, suffix='validation_errors', extension='.txt')
+        issue_file = save_text_to_upload_folder(issue_str, file_name)
+        download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                            msg='JSON dictionary had validation errors')
+        if isinstance(download_response, str):
+            return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
+        return download_response
+    return ""
diff --git a/hedweb/hed/web/events.py b/hedweb/hed/web/events.py
index 12c7b1c75..a4c07d57f 100644
--- a/hedweb/hed/web/events.py
+++ b/hedweb/hed/web/events.py
@@ -4,25 +4,22 @@
 
 from hed.util.file_util import delete_file_if_it_exists
 from hed.validator.hed_validator import HedValidator
-from hed.util.event_file_input import EventFileInput
-from hed.util.column_def_group import ColumnDefGroup
 from hed.util.hed_schema import HedSchema
 
 from hed.web.constants import common_constants, error_constants, file_constants
-from hed.web.web_utils import generate_issues_filename, generate_download_file_response, \
-    get_hed_path_from_pull_down, get_uploaded_file_path_from_form, handle_http_error, \
-    save_issues_to_upload_folder
-from hed.web.utils import get_optional_form_field
+from hed.web.dictionary import validate_dictionary
+from hed.web.spreadsheet import validate_spreadsheet
+from hed.web.web_utils import get_hed_path_from_pull_down, get_uploaded_file_path_from_form, get_optional_form_field
 
 app_config = current_app.config
 
 
-def generate_arguments_from_events_form(form_request_object):
+def generate_input_from_events_form(request):
     """Gets the validation function input arguments from a request object associated with the validation form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -30,36 +27,40 @@ def generate_arguments_from_events_form(form_request_object):
     dictionary
         A dictionary containing input arguments for calling the underlying validation function.
     """
-    hed_file_path, hed_display_name = get_hed_path_from_pull_down(form_request_object)
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
     uploaded_events_name, original_events_name = \
-        get_uploaded_file_path_from_form(form_request_object, common_constants.SPREADSHEET_FILE,
+        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
                                          file_constants.SPREADSHEET_FILE_EXTENSIONS)
 
     uploaded_json_name, original_json_name = \
-        get_uploaded_file_path_from_form(form_request_object, common_constants.JSON_FILE,
+        get_uploaded_file_path_from_form(request, common_constants.JSON_FILE,
                                          file_constants.DICTIONARY_FILE_EXTENSIONS)
 
-    input_arguments = {common_constants.HED_XML_FILE: hed_file_path,
-                       common_constants.HED_DISPLAY_NAME: hed_display_name,
-                       common_constants.SPREADSHEET_PATH: uploaded_events_name,
-                       common_constants.SPREADSHEET_FILE: original_events_name,
-                       common_constants.JSON_PATH: uploaded_json_name,
-                       common_constants.JSON_FILE: original_json_name}
-    input_arguments[common_constants.WORKSHEET_NAME] = \
-        get_optional_form_field(form_request_object, common_constants.WORKSHEET_NAME, common_constants.STRING)
-    input_arguments[common_constants.HAS_COLUMN_NAMES] = \
-        get_optional_form_field(form_request_object, common_constants.HAS_COLUMN_NAMES, common_constants.BOOLEAN)
-    input_arguments[common_constants.CHECK_FOR_WARNINGS] = \
-        get_optional_form_field(form_request_object, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
+    input_arguments = {
+        common_constants.HED_XML_FILE: hed_file_path,
+        common_constants.HED_DISPLAY_NAME: hed_display_name,
+        common_constants.SPREADSHEET_PATH: uploaded_events_name,
+        common_constants.SPREADSHEET_FILE: original_events_name,
+        common_constants.JSON_PATH: uploaded_json_name,
+        common_constants.JSON_FILE: original_json_name,
+        common_constants.WORKSHEET_NAME: get_optional_form_field(request, common_constants.WORKSHEET_NAME,
+                                                                 common_constants.STRING),
+        common_constants.HAS_COLUMN_NAMES: get_optional_form_field(request,
+                                                                   common_constants.HAS_COLUMN_NAMES,
+                                                                   common_constants.BOOLEAN),
+        common_constants.CHECK_FOR_WARNINGS: get_optional_form_field(request,
+                                                                     common_constants.CHECK_FOR_WARNINGS,
+                                                                     common_constants.BOOLEAN)
+    }
     return input_arguments
 
 
-def report_events_validation_status(form_request_object):
+def report_events_validation_status(request):
     """Reports the spreadsheet validation status.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -70,18 +71,15 @@ def report_events_validation_status(form_request_object):
     """
     input_arguments = []
     try:
-        input_arguments = generate_arguments_from_events_form(form_request_object)
-        validation_issues = validate_events(input_arguments)
-        if validation_issues:
-            issues_filename = \
-                generate_issues_filename(input_arguments.get(common_constants.SPREADSHEET_FILE, ''),
-                                         common_constants.VALIDATION_OUTPUT_FILE_PREFIX,
-                                         input_arguments.get(common_constants.WORKSHEET_NAME, ''))
-
-            issue_file = save_issues_to_upload_folder(validation_issues, issues_filename)
-            download_response = generate_download_file_response(issue_file)
-            if isinstance(download_response, str):
-                return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
+        input_arguments = generate_input_from_events_form(request)
+        hed_schema = HedSchema(input_arguments.get(common_constants.HED_XML_FILE, ''))
+        download_response = validate_dictionary(input_arguments, hed_schema=hed_schema)
+        if download_response:
+            return download_response
+        hed_validator = HedValidator(hed_schema=hed_schema,
+                                     check_for_warnings=input_arguments.get(common_constants.CHECK_FOR_WARNINGS, False))
+        download_response = validate_spreadsheet(input_arguments, hed_validator)
+        if download_response:
             return download_response
     except HTTPError:
         return error_constants.NO_URL_CONNECTION_ERROR
@@ -91,41 +89,5 @@ def report_events_validation_status(form_request_object):
         return "Unexpected processing error: " + str(e)
     finally:
         delete_file_if_it_exists(input_arguments.get(common_constants.SPREADSHEET_PATH, ''))
+        delete_file_if_it_exists(input_arguments.get(common_constants.JSON_PATH, ''))
     return ""
-
-
-def validate_events(validation_arguments):
-    """Validates the spreadsheet.
-
-    Parameters
-    ----------
-    validation_arguments: dictionary
-        A dictionary containing the arguments for the validation function.
-
-    Returns
-    -------
-    HedValidator object
-        A HedValidator object containing the validation results.
-    """
-
-    schema = HedSchema(validation_arguments.get(common_constants.HED_XML_FILE, ''))
-    worksheet = validation_arguments.get(common_constants.WORKSHEET_NAME, '')
-    spreadsheet = validation_arguments.get(common_constants.SPREADSHEET_PATH, '')
-    json = validation_arguments.get(common_constants.JSON_PATH, '')
-    issue_list = []
-    if json:
-        json_def = ColumnDefGroup(json)
-        display_name = validation_arguments.get(common_constants.JSON_FILE, '')
-        json_issues = json_def.validate_entries(hed_schema=schema, display_filename=display_name)
-        if json_issues:
-            issue_list = issue_list + json_issues
-    else:
-        json_def = None
-    if spreadsheet:
-        input_object = EventFileInput(spreadsheet, worksheet_name=worksheet, json_def_files=json_def, hed_schema=schema)
-        validator = HedValidator(input_object, hed_schema=schema,
-                                 check_for_warnings=validation_arguments.get(common_constants.CHECK_FOR_WARNINGS, False))
-        spreadsheet_issues = validator.get_validation_issues()
-        if spreadsheet_issues:
-            issue_list = issue_list + spreadsheet_issues
-    return issue_list
diff --git a/hedweb/hed/web/routes.py b/hedweb/hed/web/routes.py
index dc2482e19..9ca98cc6c 100644
--- a/hedweb/hed/web/routes.py
+++ b/hedweb/hed/web/routes.py
@@ -5,7 +5,7 @@
 
 from hed.util import hed_cache
 
-from hed.web import events, spreadsheet, schema, utils
+from hed.web import events, spreadsheet, schema, services, utils
 from hed.web.constants import blueprint_constants, common_constants, error_constants, page_constants, route_constants
 from hed.web.web_utils import delete_file_if_it_exists, find_hed_version_in_uploaded_file, save_file_to_upload_folder, \
     generate_download_file_response, handle_http_error
@@ -70,7 +70,6 @@ def get_dictionary_validation_results():
         A serialized JSON string containing information related to the worksheet columns. If the validation fails then a
         500 error message is returned.
     """
-    x = request
     validation_response = report_dictionary_validation_status(request)
     return validation_response
     # validation_response = dictionary.report_dictionary_validation_status(request)
@@ -108,35 +107,8 @@ def get_events_validation_results():
             return ""
 
 
-@route_blueprint.route(route_constants.SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
-def get_schema_compliance_check_results():
-    """Check the HED specification in the form after submission and return an attachment other containing the output.
-
-    Parameters
-    ----------
-
-    Returns
-    -------
-        string
-        A serialized JSON string containing the hed specification to check. If the conversion fails then a
-        500 error message is returned.
-    """
-    comparison_response = schema.run_schema_compliance_check(request)
-    # Success
-    if isinstance(comparison_response, Response):
-        return comparison_response
-    if isinstance(comparison_response, str):
-        if comparison_response:
-            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, comparison_response, as_text=True)
-        else:
-            return ""
-
-    return handle_http_error(error_constants.INTERNAL_SERVER_ERROR,
-                             "Invalid duplicate tag check. This should not happen.", as_text=True)
-
-
-@route_blueprint.route(route_constants.EEG_VALIDATION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
-def get_eeg_events_validation_results():
+@route_blueprint.route(route_constants.HED_SERVICES_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
+def get_hed_services_results():
     """Validate the hed strings associated with EEG events after submission from HEDTools EEGLAB plugin and
     return json string containing the output.
 
@@ -149,15 +121,15 @@ def get_eeg_events_validation_results():
         A serialized JSON string containing information related to the EEG events' hed-strings.
         If the validation fails then a 500 error message is returned.
     """
-    validation_status = spreadsheet.report_eeg_events_validation_status(request)
+    status = services.report_services_status(request)
 
-    if error_constants.ERROR_KEY in validation_status:
-        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, validation_status[error_constants.ERROR_KEY])
-    return json.dumps(validation_status)
+    if error_constants.ERROR_KEY in status:
+        return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, status[error_constants.ERROR_KEY])
+    return json.dumps(status)
 
 
 @route_blueprint.route(route_constants.HED_VERSION_ROUTE, methods=['POST'])
-def get_hed_version_in_file():
+def get_hed_version():
     """Finds the information about the HED version of a file and returns as JSON.
 
     Parameters
@@ -199,6 +171,33 @@ def get_major_hed_versions():
     return json.dumps(hed_info)
 
 
+@route_blueprint.route(route_constants.SCHEMA_COMPLIANCE_CHECK_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
+def get_schema_compliance_check_results():
+    """Check the HED specification in the form after submission and return an attachment other containing the output.
+
+    Parameters
+    ----------
+
+    Returns
+    -------
+        string
+        A serialized JSON string containing the hed specification to check. If the conversion fails then a
+        500 error message is returned.
+    """
+    comparison_response = schema.run_schema_compliance_check(request)
+    # Success
+    if isinstance(comparison_response, Response):
+        return comparison_response
+    if isinstance(comparison_response, str):
+        if comparison_response:
+            return handle_http_error(error_constants.INTERNAL_SERVER_ERROR, comparison_response, as_text=True)
+        else:
+            return ""
+
+    return handle_http_error(error_constants.INTERNAL_SERVER_ERROR,
+                             "Invalid duplicate tag check. This should not happen.", as_text=True)
+
+
 @route_blueprint.route(route_constants.SCHEMA_CONVERSION_SUBMIT_ROUTE, strict_slashes=False, methods=['POST'])
 def get_schema_conversion_results():
     """Convert the given HED specification and return an attachment with the result.
@@ -334,8 +333,8 @@ def render_common_error_page():
     return render_template(page_constants.COMMON_ERRORS_PAGE)
 
 
-@route_blueprint.route(route_constants.DICTIONARY_VALIDATION_ROUTE, strict_slashes=False, methods=['GET'])
-def render_dictionary_validation_form():
+@route_blueprint.route(route_constants.DICTIONARY_ROUTE, strict_slashes=False, methods=['GET'])
+def render_dictionary_form():
     """Handles the site root and Validation tab functionality.
 
     Parameters
@@ -352,9 +351,9 @@ def render_dictionary_validation_form():
     # return render_template(page_constants.DICTIONARY_VALIDATION_PAGE)
 
 
-@route_blueprint.route(route_constants.EEG_VALIDATION_ROUTE, strict_slashes=False, methods=['GET'])
-def render_eeg_validation_form():
-    """Handles the site root and EEG Validation tab functionality.
+@route_blueprint.route(route_constants.EVENTS_ROUTE, strict_slashes=False, methods=['GET'])
+def render_events_form():
+    """Handles the site root and Validation tab functionality.
 
     Parameters
     ----------
@@ -366,13 +365,12 @@ def render_eeg_validation_form():
         displayed. If the HTTP method is a POST then the validation form is submitted.
 
     """
-    return render_template(page_constants.EEG_VALIDATION_PAGE)
-
+    return render_template(page_constants.EVENTS_PAGE)
 
 
-@route_blueprint.route(route_constants.EVENTS_VALIDATION_ROUTE, strict_slashes=False, methods=['GET'])
-def render_events_validation_form():
-    """Handles the site root and Validation tab functionality.
+@route_blueprint.route(route_constants.HED_SERVICES_ROUTE, strict_slashes=False, methods=['GET'])
+def render_hed_services_form():
+    """Handles the site root and EEG Validation tab functionality.
 
     Parameters
     ----------
@@ -384,12 +382,12 @@ def render_events_validation_form():
         displayed. If the HTTP method is a POST then the validation form is submitted.
 
     """
-    return render_template(page_constants.EVENTS_VALIDATION_PAGE)
+    return render_template(page_constants.HED_SERVICES_PAGE)
 
 
-@route_blueprint.route(route_constants.HED_TOOLS_HOME_ROUTE, strict_slashes=False, methods=['GET'])
-def render_home_page():
-    """Handles the home page.
+@route_blueprint.route(route_constants.HED_TOOLS_HELP_ROUTE, strict_slashes=False, methods=['GET'])
+def render_help_page():
+    """Handles the site help page.
 
     Parameters
     ----------
@@ -397,15 +395,15 @@ def render_home_page():
     Returns
     -------
     Rendered template
-        A rendered template for the home page.
+        A rendered template for the help page.
 
     """
-    return render_template(page_constants.HED_TOOLS_HOME_PAGE)
+    return render_template(page_constants.HED_TOOLS_HELP_PAGE)
 
 
-@route_blueprint.route(route_constants.HED_TOOLS_HELP_ROUTE, strict_slashes=False, methods=['GET'])
-def render_help_page():
-    """Handles the site help page.
+@route_blueprint.route(route_constants.HED_TOOLS_HOME_ROUTE, strict_slashes=False, methods=['GET'])
+def render_home_page():
+    """Handles the home page.
 
     Parameters
     ----------
@@ -413,10 +411,10 @@ def render_help_page():
     Returns
     -------
     Rendered template
-        A rendered template for the help page.
+        A rendered template for the home page.
 
     """
-    return render_template(page_constants.HED_TOOLS_HELP_PAGE)
+    return render_template(page_constants.HED_TOOLS_HOME_PAGE)
 
 
 @route_blueprint.route(route_constants.SCHEMA_ROUTE, strict_slashes=False, methods=['GET'])
@@ -436,8 +434,8 @@ def render_schema_form():
     return render_template(page_constants.SCHEMA_PAGE)
 
 
-@route_blueprint.route(route_constants.SPREADSHEET_VALIDATION_ROUTE, strict_slashes=False, methods=['GET'])
-def render_spreadsheet_validation_form():
+@route_blueprint.route(route_constants.SPREADSHEET_ROUTE, strict_slashes=False, methods=['GET'])
+def render_spreadsheet_form():
     """Handles the site root and Validation tab functionality.
 
     Parameters
@@ -450,4 +448,4 @@ def render_spreadsheet_validation_form():
         displayed. If the HTTP method is a POST then the validation form is submitted.
 
     """
-    return render_template(page_constants.SPREADSHEET_VALIDATION_PAGE)
+    return render_template(page_constants.SPREADSHEET_PAGE)
diff --git a/hedweb/hed/web/schema.py b/hedweb/hed/web/schema.py
index fb6b488df..4930361e6 100644
--- a/hedweb/hed/web/schema.py
+++ b/hedweb/hed/web/schema.py
@@ -1,27 +1,28 @@
-from os.path import basename
+from os.path import basename, splitext
 import traceback
 from urllib.error import URLError, HTTPError
 from urllib.parse import urlparse
 from flask import current_app, Response
 
 from hed.schema import xml2wiki, wiki2xml, schema_validator
-from hed.util.file_util import delete_file_if_it_exists, url_to_file, get_file_extension, write_errors_to_file
-from hed.util.exceptions import SchemaFileError
+from hed.util.file_util import delete_file_if_it_exists, url_to_file, get_file_extension
+from hed.util.exceptions import HedFileError
 
 from hed.web.web_utils import file_extension_is_valid, form_has_file, form_has_option, form_has_url, \
-    handle_http_error, save_file_to_upload_folder, generate_download_file_response
+    generate_download_file_response, generate_filename, get_printable_issue_string, \
+    handle_http_error, save_file_to_upload_folder, save_text_to_upload_folder
 
 from hed.web.constants import common_constants, error_constants, file_constants
 
 app_config = current_app.config
 
 
-def generate_input_from_schema_form(form_request_object):
+def generate_input_from_schema_form(request):
     """Gets the conversion function input arguments from a request object associated with the conversion form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the conversion form.
 
     Returns
@@ -31,16 +32,16 @@ def generate_input_from_schema_form(form_request_object):
     """
     arguments = {}
    
-    if form_has_option(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
+    if form_has_option(request, common_constants.SCHEMA_UPLOAD_OPTIONS,
                        common_constants.SCHEMA_FILE_OPTION) and \
-            form_has_file(form_request_object, common_constants.SCHEMA_FILE, file_constants.SCHEMA_EXTENSIONS):
-        schema_file = form_request_object.files[common_constants.SCHEMA_FILE]
+            form_has_file(request, common_constants.SCHEMA_FILE, file_constants.SCHEMA_EXTENSIONS):
+        schema_file = request.files[common_constants.SCHEMA_FILE]
         arguments[common_constants.SCHEMA_PATH] = save_file_to_upload_folder(schema_file)
         arguments[common_constants.SCHEMA_DISPLAY_NAME] = schema_file.filename
-    elif form_has_option(form_request_object, common_constants.SCHEMA_UPLOAD_OPTIONS,
+    elif form_has_option(request, common_constants.SCHEMA_UPLOAD_OPTIONS,
                          common_constants.SCHEMA_URL_OPTION) and \
-            form_has_url(form_request_object, common_constants.SCHEMA_URL, file_constants.SCHEMA_EXTENSIONS):
-        schema_url = form_request_object.values[common_constants.SCHEMA_URL]
+            form_has_url(request, common_constants.SCHEMA_URL, file_constants.SCHEMA_EXTENSIONS):
+        schema_url = request.values[common_constants.SCHEMA_URL]
         arguments[common_constants.SCHEMA_PATH] = url_to_file(schema_url)
         url_parsed = urlparse(schema_url)
         arguments[common_constants.SCHEMA_DISPLAY_NAME] = basename(url_parsed.path)
@@ -72,7 +73,7 @@ def get_schema_conversion(schema_local_path):
     return converted_schema_path, errors
 
 
-def run_schema_compliance_check(form_request_object):
+def run_schema_compliance_check(request):
     """Run tag comparison(map_schema from converter)
 
     returns: Response or string.
@@ -82,8 +83,8 @@ def run_schema_compliance_check(form_request_object):
     """
     hed_file_path = ''
     try:
-        conversion_input_arguments = generate_input_from_schema_form(form_request_object)
-        hed_file_path = conversion_input_arguments.get(common_constants.SCHEMA_PATH, '')
+        input_arguments = generate_input_from_schema_form(request)
+        hed_file_path = input_arguments.get(common_constants.SCHEMA_PATH, '')
         if hed_file_path and hed_file_path.endswith(".mediawiki"):
             new_file_path, errors = get_schema_conversion(hed_file_path)
             if new_file_path:
@@ -92,12 +93,15 @@ def run_schema_compliance_check(form_request_object):
 
         if not hed_file_path or not file_extension_is_valid(hed_file_path, [file_constants.SCHEMA_XML_EXTENSION]):
             return f"Invalid file name {hed_file_path}"
-        issues = schema_validator.validate_schema(hed_file_path)
 
+        issues = schema_validator.validate_schema(hed_file_path)
         if issues:
-            #issue_file = write_text_iter_to_file(issues)
-            issue_file = write_errors_to_file(issues, extension=".txt")
-            download_response = generate_download_file_response(issue_file, f"HED-3G format issues for: {hed_file_path}")
+            display_name = input_arguments.get(common_constants.SCHEMA_DISPLAY_NAME)
+            issue_str = get_printable_issue_string(issues, display_name, 'Schema HED 3G compliance errors for ')
+            file_name = generate_filename(display_name, suffix='schema_errors', extension='.txt')
+            issue_file = save_text_to_upload_folder(issue_str, file_name)
+            download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                                msg='Schema is not HED 3G compliant')
             if isinstance(download_response, str):
                 return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
             return download_response
@@ -105,7 +109,7 @@ def run_schema_compliance_check(form_request_object):
         return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
         return error_constants.INVALID_URL_ERROR
-    except SchemaFileError as e:
+    except HedFileError as e:
         return e.message
     except Exception as e:
         return "Unexpected processing error: " + str(e)
@@ -114,7 +118,7 @@ def run_schema_compliance_check(form_request_object):
     return ""
 
 
-def run_schema_conversion(form_request_object):
+def run_schema_conversion(request):
     """Run conversion(wiki2xml or xml2wiki from converter)
 
     returns: Response or string.
@@ -123,20 +127,29 @@ def run_schema_conversion(form_request_object):
     """
     hed_file_path = ''
     try:
-        arguments = generate_input_from_schema_form(form_request_object)
-        hed_file_path = arguments.get(common_constants.SCHEMA_PATH)
-        schema_file, errors = get_schema_conversion(hed_file_path)
-        if errors:
-            download_response = errors
+        input_arguments = generate_input_from_schema_form(request)
+        hed_file_path = input_arguments.get(common_constants.SCHEMA_PATH)
+        display_name = input_arguments.get(common_constants.SCHEMA_DISPLAY_NAME)
+        schema_file, issues = get_schema_conversion(hed_file_path)
+        if issues:
+            issue_str = get_printable_issue_string(issues, display_name, 'Schema conversion errors for ')
+            file_name = generate_filename(display_name, suffix='conversion_errors', extension='.txt')
+            issue_file = save_text_to_upload_folder(issue_str, file_name)
+            download_response = \
+                generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                msg='Schema had validation errors and could not be converted')
         else:
-            download_response = generate_download_file_response(schema_file)
+            schema_name, schema_ext = splitext(schema_file)
+            file_name = generate_filename(display_name,  extension=schema_ext)
+            download_response = generate_download_file_response(schema_file, display_name=file_name, category='success',
+                                                                msg='Schema was successfully converted')
         if isinstance(download_response, Response):
             return download_response
         elif isinstance(download_response, str):
             return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
         else:
             raise Exception("Unable to download schema file for unknown reasons")
-    except SchemaFileError as e:
+    except HedFileError as e:
         return e.message
     except HTTPError:
         return error_constants.NO_URL_CONNECTION_ERROR
diff --git a/hedweb/hed/web/services.py b/hedweb/hed/web/services.py
new file mode 100644
index 000000000..4641918bf
--- /dev/null
+++ b/hedweb/hed/web/services.py
@@ -0,0 +1,92 @@
+import json
+import traceback
+from urllib.error import URLError, HTTPError
+from flask import current_app
+
+from hed.util.file_util import get_file_extension, delete_file_if_it_exists
+from hed.validator.hed_validator import HedValidator
+from hed.util.hed_file_input import HedFileInput
+
+from hed.web.constants import common_constants, error_constants, file_constants
+from hed.web.web_utils import convert_number_str_to_list, generate_filename, get_printable_issue_string, \
+    generate_download_file_response, get_hed_path_from_pull_down, get_uploaded_file_path_from_form, \
+    handle_http_error, save_file_to_upload_folder, save_text_to_upload_folder
+from hed.web import utils
+
+app_config = current_app.config
+
+
+def generate_arguments_from_form(request):
+    """Gets the input arguments from a request object associated with a service.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+
+    Returns
+    -------
+    dictionary
+        A dictionary containing input arguments for calling the underlying validation function.
+    """
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
+    uploaded_file_name, original_file_name = \
+        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
+                                         file_constants.SPREADSHEET_FILE_EXTENSIONS)
+
+    input_arguments = { }
+    #     common_constants.HED_VERSION:  form_data = request.form,
+    #     common_constants.HED_DISPLAY_NAME: hed_display_name,
+    #
+    #
+    #     common_constants.CHECK_FOR_WARNINGS:
+    #         utils.get_optional_form_field(request, common_constants.CHECK_FOR_WARNINGS,
+    #                                       common_constants.BOOLEAN)
+    # }
+    return input_arguments
+
+
+def report_services_status(request):
+    """
+    Reports validation status of hed strings associated with EEG events received from EEGLAB plugin HEDTools
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data submitted by HEDTools.
+        Keys include "hed_strings", "check_for_warnings", and "hed_xml_file"
+
+    Returns
+    -------
+    string
+        A serialized JSON string containing information related to the hed strings validation result.
+        If the validation fails then a 500 error message is returned.
+    """
+    validation_status = {}
+
+    # Parse uploaded data
+    form_data = request.form
+    check_for_warnings = form_data["check_for_warnings"] == '1' if "check_for_warnings" in form_data else False
+    # if hed_xml_file was submitted, it's accessed by request.files, otherwise empty
+    if "hed-xml-file" in request.files and get_file_extension(request.files["hed-xml-file"].filename) == "xml":
+        hed_xml_file = save_file_to_upload_folder(request.files["hed-xml-file"])
+    else:
+        hed_xml_file = ''
+
+    try:
+        # parse hed_strings from json
+        hed_strings = json.loads(form_data["hed-strings"])
+        # hed_strings is a list of HED strings associated with events in EEG.event (order preserved)
+        hed_input_reader = HedValidator(hed_strings, check_for_warnings=check_for_warnings, hed_xml_file=hed_xml_file)
+        # issues is a list of lists. Element list is empty if no error,
+        # else is a list of dictionaries, each dictionary contains an error-message key-value pair
+        issues = hed_input_reader.get_validation_issues()
+
+        # Prepare response
+        validation_status["issues"] = issues
+    except:
+        validation_status[error_constants.ERROR_KEY] = traceback.format_exc()
+    finally:
+        delete_file_if_it_exists(hed_xml_file)
+
+    return validation_status
diff --git a/hedweb/hed/web/spreadsheet.py b/hedweb/hed/web/spreadsheet.py
index 9a8c4db76..2f50f7173 100644
--- a/hedweb/hed/web/spreadsheet.py
+++ b/hedweb/hed/web/spreadsheet.py
@@ -1,26 +1,26 @@
-import json
-import traceback
 from urllib.error import URLError, HTTPError
 from flask import current_app
-from werkzeug.utils import secure_filename
 
-from hed.util.file_util import get_file_extension, delete_file_if_it_exists
+from hed.util.file_util import delete_file_if_it_exists
 from hed.validator.hed_validator import HedValidator
 from hed.util.hed_file_input import HedFileInput
 
+import hed.web.web_utils
 from hed.web.constants import common_constants, error_constants, file_constants
-from hed.web import web_utils
+from hed.web.web_utils import convert_number_str_to_list, generate_filename, get_printable_issue_string, \
+    generate_download_file_response, get_hed_path_from_pull_down, get_uploaded_file_path_from_form, \
+    handle_http_error, save_text_to_upload_folder
 from hed.web import utils
 
 app_config = current_app.config
 
 
-def generate_arguments_from_validation_form(form_request_object):
+def generate_input_from_validation_form(request):
     """Gets the validation function input arguments from a request object associated with the validation form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -28,81 +28,34 @@ def generate_arguments_from_validation_form(form_request_object):
     dictionary
         A dictionary containing input arguments for calling the underlying validation function.
     """
-    hed_file_path, hed_display_name = web_utils.get_hed_path_from_pull_down(form_request_object)
+    hed_file_path, hed_display_name = get_hed_path_from_pull_down(request)
     uploaded_file_name, original_file_name = \
-        web_utils.get_uploaded_file_path_from_form(form_request_object, common_constants.SPREADSHEET_FILE,
-                                                   file_constants.SPREADSHEET_FILE_EXTENSIONS)
-
-    validation_input_arguments = {common_constants.HED_XML_FILE: hed_file_path,
-                                  common_constants.HED_DISPLAY_NAME: hed_display_name,
-                                  common_constants.SPREADSHEET_PATH: uploaded_file_name,
-                                  common_constants.SPREADSHEET_FILE: original_file_name}
-    validation_input_arguments[common_constants.TAG_COLUMNS] = \
-        web_utils.convert_number_str_to_list(form_request_object.form[common_constants.TAG_COLUMNS])
-    validation_input_arguments[common_constants.COLUMN_PREFIX_DICTIONARY] = \
-        utils.get_specific_tag_columns_from_form(form_request_object)
-    validation_input_arguments[common_constants.WORKSHEET_NAME] = \
-        utils.get_optional_form_field(form_request_object, common_constants.WORKSHEET_NAME,
-                                      common_constants.STRING)
-    validation_input_arguments[common_constants.HAS_COLUMN_NAMES] = utils.get_optional_form_field(
-        form_request_object, common_constants.HAS_COLUMN_NAMES, common_constants.BOOLEAN)
-    validation_input_arguments[common_constants.CHECK_FOR_WARNINGS] = utils.get_optional_form_field(
-        form_request_object, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
+        get_uploaded_file_path_from_form(request, common_constants.SPREADSHEET_FILE,
+                                         file_constants.SPREADSHEET_FILE_EXTENSIONS)
+
+    validation_input_arguments = {
+        common_constants.HED_XML_FILE: hed_file_path,
+        common_constants.HED_DISPLAY_NAME: hed_display_name,
+        common_constants.SPREADSHEET_PATH: uploaded_file_name,
+        common_constants.SPREADSHEET_FILE: original_file_name,
+        common_constants.TAG_COLUMNS: convert_number_str_to_list(request.form[common_constants.TAG_COLUMNS]),
+        common_constants.COLUMN_PREFIX_DICTIONARY: utils.get_specific_tag_columns_from_form(request),
+        common_constants.WORKSHEET_NAME: hed.web.web_utils.get_optional_form_field(request, common_constants.WORKSHEET_NAME,
+                                                                                   common_constants.STRING),
+        common_constants.HAS_COLUMN_NAMES:
+            hed.web.web_utils.get_optional_form_field(request, common_constants.HAS_COLUMN_NAMES, common_constants.BOOLEAN),
+        common_constants.CHECK_FOR_WARNINGS:
+            hed.web.web_utils.get_optional_form_field(request, common_constants.CHECK_FOR_WARNINGS, common_constants.BOOLEAN)
+    }
     return validation_input_arguments
 
 
-def report_eeg_events_validation_status(request):
-    """
-    Reports validation status of hed strings associated with EEG events received from EEGLAB plugin HEDTools
-
-    Parameters
-    ----------
-    request: Request object
-        A Request object containing user data submitted by HEDTools.
-        Keys include "hed_strings", "check_for_warnings", and "hed_xml_file"
-
-    Returns
-    -------
-    string
-        A serialized JSON string containing information related to the hed strings validation result.
-        If the validation fails then a 500 error message is returned.
-    """
-    validation_status = {}
-
-    # Parse uploaded data
-    form_data = request.form
-    check_for_warnings = form_data["check_for_warnings"] == '1' if "check_for_warnings" in form_data else False
-    # if hed_xml_file was submitted, it's accessed by request.files, otherwise empty
-    if "hed-xml-file" in request.files and get_file_extension(request.files["hed-xml-file"].filename) == "xml":
-        hed_xml_file = web_utils.save_file_to_upload_folder(request.files["hed-xml-file"])
-    else:
-        hed_xml_file = ''
-
-    try:
-        # parse hed_strings from json
-        hed_strings = json.loads(form_data["hed-strings"])
-        # hed_strings is a list of HED strings associated with events in EEG.event (order preserved)
-        hed_input_reader = HedValidator(hed_strings, check_for_warnings=check_for_warnings, hed_xml_file=hed_xml_file)
-        # issues is a list of lists. Element list is empty if no error,
-        # else is a list of dictionaries, each dictionary contains an error-message key-value pair
-        issues = hed_input_reader.get_validation_issues()
-
-        # Prepare response
-        validation_status["issues"] = issues
-    except:
-        validation_status[error_constants.ERROR_KEY] = traceback.format_exc()
-    finally:
-        delete_file_if_it_exists(hed_xml_file)
-
-    return validation_status
-
-
-def report_spreadsheet_validation_status(form_request_object):
+def report_spreadsheet_validation_status(request):
     """Reports the spreadsheet validation status.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -113,18 +66,8 @@ def report_spreadsheet_validation_status(form_request_object):
     """
     input_arguments = []
     try:
-        input_arguments = generate_arguments_from_validation_form(form_request_object)
-        hed_validator = validate_spreadsheet(input_arguments)
-        validation_issues = hed_validator.get_validation_issues()
-        if validation_issues:
-            issues_filename = web_utils.generate_issues_filename(common_constants.VALIDATION_OUTPUT_FILE_PREFIX,
-                                                                 input_arguments[common_constants.SPREADSHEET_FILE],
-                                                                 input_arguments[common_constants.WORKSHEET_NAME])
-            issue_file = web_utils.save_issues_to_upload_folder(validation_issues, issues_filename)
-            download_response = web_utils.generate_download_file_response(issue_file)
-            if isinstance(download_response, str):
-                return web_utils.handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
-            return download_response
+        input_arguments = generate_input_from_validation_form(request)
+        return validate_spreadsheet(input_arguments)
     except HTTPError:
         return error_constants.NO_URL_CONNECTION_ERROR
     except URLError:
@@ -133,30 +76,53 @@ def report_spreadsheet_validation_status(form_request_object):
         return "Unexpected processing error: " + str(e)
     finally:
         delete_file_if_it_exists(input_arguments.get(common_constants.SPREADSHEET_PATH, ''))
-    return ""
 
 
-def validate_spreadsheet(validation_arguments):
+def validate_spreadsheet(input_arguments, hed_validator=None, return_response=True):
     """Validates the spreadsheet.
 
     Parameters
     ----------
-    validation_arguments: dictionary
+    input_arguments: dictionary
         A dictionary containing the arguments for the validation function.
-
+    hed_validator: HedValidator
+        Validator passed if previously created in another phase
+    return_response: bool
+        If true returns a response object, otherwise directly returns a printable issue string
     Returns
     -------
     HedValidator object
         A HedValidator object containing the validation results.
     """
 
-    file_input_object = HedFileInput(validation_arguments[common_constants.SPREADSHEET_PATH],
-                                     worksheet_name=validation_arguments[common_constants.WORKSHEET_NAME],
-                                     tag_columns=validation_arguments[common_constants.TAG_COLUMNS],
-                                     has_column_names=validation_arguments[common_constants.HAS_COLUMN_NAMES],
-                                     column_prefix_dictionary=validation_arguments[
-                                         common_constants.COLUMN_PREFIX_DICTIONARY])
-
-    return HedValidator(file_input_object,
-                        check_for_warnings=validation_arguments[common_constants.CHECK_FOR_WARNINGS],
-                        hed_xml_file=validation_arguments[common_constants.HED_XML_FILE])
+    file_input = HedFileInput(input_arguments.get(common_constants.SPREADSHEET_PATH, None),
+                              worksheet_name=input_arguments.get(common_constants.WORKSHEET_NAME, None),
+                              tag_columns=input_arguments.get(common_constants.TAG_COLUMNS, None),
+                              has_column_names=input_arguments.get(common_constants.HAS_COLUMN_NAMES, None),
+                              column_prefix_dictionary=input_arguments.get(common_constants.COLUMN_PREFIX_DICTIONARY,
+                                                                           None))
+    if not hed_validator:
+        hed_validator = HedValidator(hed_xml_file=input_arguments.get(common_constants.HED_XML_FILE, ''),
+                                     check_for_warnings=input_arguments.get(common_constants.CHECK_FOR_WARNINGS, False))
+
+    issues = hed_validator.validate_input(file_input)
+
+    if issues:
+        display_name = input_arguments.get(common_constants.SPREADSHEET_FILE, None)
+        worksheet_name = input_arguments.get(common_constants.WORKSHEET_NAME, None)
+        title_string = display_name
+        suffix = 'validation_errors'
+        if worksheet_name:
+            title_string = display_name + ' [worksheet ' + worksheet_name + ']'
+            suffix = '_worksheet_' + worksheet_name + '_' + suffix
+        issue_str = get_printable_issue_string(issues, title_string, 'HED validation errors')
+        if not return_response:
+            return issue_str
+        file_name = generate_filename(display_name, suffix=suffix, extension='.txt')
+        issue_file = save_text_to_upload_folder(issue_str, file_name)
+        download_response = generate_download_file_response(issue_file, display_name=file_name, category='warning',
+                                                            msg='Spreadsheet had validation errors')
+        if isinstance(download_response, str):
+            return handle_http_error(error_constants.NOT_FOUND_ERROR, download_response)
+        return download_response
+    return ""
diff --git a/hedweb/hed/web/templates/eeg-validation.html b/hedweb/hed/web/templates/hed-services.html
similarity index 56%
rename from hedweb/hed/web/templates/eeg-validation.html
rename to hedweb/hed/web/templates/hed-services.html
index b579c4f8b..f85acafb0 100644
--- a/hedweb/hed/web/templates/eeg-validation.html
+++ b/hedweb/hed/web/templates/hed-services.html
@@ -1,11 +1,11 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
     <meta charset="UTF-8">
-    <title>EEG Validation</title>
+    <title>HED services</title>
 </head>
 <body>
-<form id="eegvalidation-form" method="POST" enctype="multipart/form-data">
+<form id="hed-services-form" method="POST" enctype="multipart/form-data">
     <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 </form>
 </body>
diff --git a/hedweb/hed/web/templates/hed-tools-home.html b/hedweb/hed/web/templates/hed-tools-home.html
index 6fbc52217..1d5a26cdc 100644
--- a/hedweb/hed/web/templates/hed-tools-home.html
+++ b/hedweb/hed/web/templates/hed-tools-home.html
@@ -28,9 +28,9 @@ <h2>HED online tools</h2>
     <p>
         Several online HED tools are currently available:
         <ul>
-            <li><a href="{{ url_for('route_blueprint.render_spreadsheet_validation_form') }}"><b>Validate spreadsheet</b></a>
+            <li><a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}"><b>Validate spreadsheet</b></a>
             validates HED tags in a tab-separated value or Excel spreadsheet and produces a download file with any errors.</li>
-        <li><a href="{{ url_for('route_blueprint.render_dictionary_validation_form') }}"><b>Validate dictionary</b></a>
+        <li><a href="{{ url_for('route_blueprint.render_dictionary_form') }}"><b>Validate dictionary</b></a>
             validates a JSON dictionary containing HED tags.</li>
         <li><a href="{{ url_for('route_blueprint.render_schema_form') }}"><b>Process schema</b></a> converts HED vocabulary
             schema between .mediawiki and .xml format. This tool also has options for checking that individual HED
diff --git a/hedweb/hed/web/templates/js/dictionary-form.js b/hedweb/hed/web/templates/js/dictionary-form.js
index 6dca4402a..98b006943 100644
--- a/hedweb/hed/web/templates/js/dictionary-form.js
+++ b/hedweb/hed/web/templates/js/dictionary-form.js
@@ -59,21 +59,11 @@ function submitForm() {
             contentType: false,
             processData: false,
             dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success', 'dictionary-validation-submit-flash');
-                      triggerDownloadBlob(downloaded_file, display_name);
-                  } else {
-                      flashMessageOnScreen('No errors found.', 'success', 'dictionary-validation-submit-flash');
-                  }
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'dictionary-validation-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error', 'dictionary-validation-submit-flash');
-                } else {
-                    flashMessageOnScreen('Dictionary not processed', 'error','dictionary-validation-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'dictionary-validation-submit-flash')
             }
         }
     )
diff --git a/hedweb/hed/web/templates/js/events-form.js b/hedweb/hed/web/templates/js/events-form.js
index 523f98b1f..d48415e93 100644
--- a/hedweb/hed/web/templates/js/events-form.js
+++ b/hedweb/hed/web/templates/js/events-form.js
@@ -168,27 +168,12 @@ function submitForm() {
             contentType: false,
             processData: false,
             dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success',
-                          'events-validation-submit-flash');
-                      triggerDownloadBlob(downloaded_file, display_name);
-                  } else {
-                      flashMessageOnScreen('No validation errors found.', 'success',
-                          'events-validation-submit-flash');
-                  }
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'events-validation-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'events-validation-submit-flash');
-                } else {
-                    flashMessageOnScreen('Event annotations could not be processed',
-                        'error','events-validation-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'events-validation-submit-flash')
             }
         }
     )
-    ;
 }
\ No newline at end of file
diff --git a/hedweb/hed/web/templates/js/form-helpers.js b/hedweb/hed/web/templates/js/form-helpers.js
index 9b22b7cf3..fb2a119ca 100644
--- a/hedweb/hed/web/templates/js/form-helpers.js
+++ b/hedweb/hed/web/templates/js/form-helpers.js
@@ -34,6 +34,7 @@ function convertToResultsName(filename, prefix) {
     return prefix + "_" + parts[0] + ".txt"
 }
 
+
 /**
  * Compares the file extension of the file at the specified path to an Array of accepted file extensions.
  * @param {String} filePath - A path to a file.
@@ -49,18 +50,6 @@ function fileHasValidExtension(filePath, acceptedFileExtensions) {
 }
 
 
-/**
- * Checks to see if a string is empty. Empty meaning null or a length of zero.
- * @param {String} str - A string.
- * @returns {boolean} - True if the string is null or its length is 0.
- */
-function isEmptyStr(str) {
-    if (str === null || str.length === 0) {
-        return true;
-    }
-    return false;
-}
-
 
 /**
  * Flash a message on the screen.
@@ -88,6 +77,82 @@ function getFilenameAndExtension(pathname){
 }
 
 
+/**
+ * Gets the file download name from a Response header
+ * @param {Object} xhr - Dictionary containing Response header information
+ * @param {String} default_name - The default name to use
+ * @returns {String} - Name of the save file
+ */
+function getFilenameFromResponseHeader(xhr, default_name) {
+    let disposition = xhr.getResponseHeader('Content-Disposition')
+
+    let filename = "";
+    if (disposition && disposition.indexOf('attachment') !== -1) {
+        let filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+        let matches = filenameRegex.exec(disposition);
+        if (matches != null && matches[1]) {
+            filename = matches[1].replace(/['"]/g, '');
+        }
+    }
+    if (!filename) {
+        filename = default_name;
+    }
+    return filename
+}
+
+/**
+ * Gets standard failure response for download
+ * @param {String} download - Downloaded string response blob
+ * @param {Object} xhr - Dictionary containing Response header information
+ * @param {String} display_name - Name used for the downloaded blob file
+ * @param {String} flash_location - ID of the flash location element for displaying response Message
+ */
+function getResponseFailure(download, xhr, display_name, flash_location) {
+    console.log(download.responseText);
+    if (download.responseText.length < 100) {
+        flashMessageOnScreen(download_response.responseText, 'error', flash_location);
+    } else {
+        flashMessageOnScreen('Not processed', 'error', flash_location);
+    }
+}
+
+/**
+ * Gets standard response with download
+ * @param {String} download - Downloaded string response blob
+ * @param {Object} xhr - Dictionary containing Response header information
+ * @param {String} display_name - Name used for the downloaded blob file
+ * @param {String} flash_location - ID of the flash location element for displaying response Message
+ */
+function getResponseSuccess(download, xhr, display_name, flash_location) {
+    if (download) {
+        let filename = getFilenameFromResponseHeader(xhr, display_name)
+        triggerDownloadBlob(download, filename);
+        let info = xhr.getResponseHeader('Message')
+        let category =  xhr.getResponseHeader('Category')
+        if (info) {
+            flashMessageOnScreen(info, category, flash_location);
+        } else {
+            flashMessageOnScreen('', 'success', flash_location);
+        }
+    } else {
+        flashMessageOnScreen('No errors found.', 'success', flash_location);
+    }
+}
+
+
+/**
+ * Checks to see if a string is empty. Empty meaning null or a length of zero.
+ * @param {String} str - A string.
+ * @returns {boolean} - True if the string is null or its length is 0.
+ */
+function isEmptyStr(str) {
+    if (str === null || str.length === 0) {
+        return true;
+    }
+    return false;
+}
+
+
 /**
  * Flash a message on the screen.
  * @param {Object} flashMessage - The li element containing the flash message.
@@ -99,7 +164,7 @@ function setFlashMessageCategory(flashMessage, category) {
     } else if ("success" === category) {
         flashMessage.style.backgroundColor = 'palegreen';
     } else if ("warning" === category) {
-        flashMessage.style.backgroundColor = 'darkorange';
+        flashMessage.style.backgroundColor = 'orange';
     } else {
         flashMessage.style.backgroundColor = '#f0f0f5';
     }
diff --git a/hedweb/hed/web/templates/js/hed-pulldown.js b/hedweb/hed/web/templates/js/hed-pulldown.js
index d2300cc95..3076af023 100644
--- a/hedweb/hed/web/templates/js/hed-pulldown.js
+++ b/hedweb/hed/web/templates/js/hed-pulldown.js
@@ -69,7 +69,7 @@ function getVersionFromHEDFile(hedXMLFile) {
     formData.append('hed-xml-file', hedXMLFile);
     $.ajax({
         type: 'POST',
-        url: "{{ url_for('route_blueprint.get_hed_version_in_file')}}",
+        url: "{{ url_for('route_blueprint.get_hed_version')}}",
         data: formData,
         contentType: false,
         processData: false,
diff --git a/hedweb/hed/web/templates/js/schema-form.js b/hedweb/hed/web/templates/js/schema-form.js
index b3dcafcaa..9eb884839 100644
--- a/hedweb/hed/web/templates/js/schema-form.js
+++ b/hedweb/hed/web/templates/js/schema-form.js
@@ -26,7 +26,7 @@ $('#schema-url').on('change', function () {
 /**
  * Submits the form for conversion if we have a valid file.
  */
-$('#schema-conversion-submit').click(function () {
+$('#schema-conversion-submit').on('click', function () {
     if (getSchemaFilename() === "") {
         flashMessageOnScreen('No valid source input file.  See above.', 'error',
             'schema-submit-flash')
@@ -64,9 +64,9 @@ function convertToOutputName(original_filename) {
     let basename = file_parts[0]
     let extension = file_parts[1]
     let new_extension = 'bad'
-    if (extension == SCHEMA_XML_EXTENSION) {
+    if (extension === SCHEMA_XML_EXTENSION) {
         new_extension = SCHEMA_MEDIAWIKI_EXTENSION
-    } else if (extension == SCHEMA_MEDIAWIKI_EXTENSION) {
+    } else if (extension === SCHEMA_MEDIAWIKI_EXTENSION) {
         new_extension = SCHEMA_XML_EXTENSION
     }
 
@@ -83,7 +83,7 @@ function getSchemaFilename() {
     let checkRadioVal = checkRadio.id
     let schemaFile = $('#schema-file');
     let schemaFileIsEmpty = schemaFile[0].files.length === 0;
-    if (checkRadioVal == "schema-file-option") {
+    if (checkRadioVal === "schema-file-option") {
         if (schemaFileIsEmpty) {
             flashMessageOnScreen('Schema file not specified.', 'error',
                 'schema-file-flash');
@@ -95,7 +95,7 @@ function getSchemaFilename() {
 
     let schemaUrl = $('#schema-url').val();
     let schemaUrlIsEmpty = schemaUrl === "";
-    if (checkRadioVal == "schema-url-option") {
+    if (checkRadioVal === "schema-url-option") {
         if (schemaUrlIsEmpty) {
             flashMessageOnScreen('URL not specified.', 'error', 'schema-url-flash');
             return '';
@@ -146,7 +146,7 @@ function splitExt(filename) {
 function submitSchemaConversionForm() {
     let schemaForm = document.getElementById("schema-form");
     let formData = new FormData(schemaForm);
-    let download_display_name = convertToOutputName(getSchemaFilename())
+    let display_name = convertToOutputName(getSchemaFilename())
     resetFlashMessages(false);
     flashMessageOnScreen('Specification is being converted...', 'success',
         'schema-submit-flash')
@@ -157,19 +157,11 @@ function submitSchemaConversionForm() {
             contentType: false,
             processData: false,
             dataType: "text",
-            success: function (downloaded_file) {
-                  flashMessageOnScreen('', 'success', 'schema-submit-flash');
-                  triggerDownloadBlob(downloaded_file, download_display_name);
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'schema-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'schema-submit-flash');
-                } else {
-                    flashMessageOnScreen('XML could not be processed', 'error',
-                        'schema-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'schema-submit-flash')
             }
         }
     )
@@ -179,7 +171,7 @@ function submitSchemaConversionForm() {
 function submitSchemaComplianceCheckForm() {
     let schemaForm = document.getElementById("schema-form");
     let formData = new FormData(schemaForm);
-    let download_display_name = convertToResultsName(getSchemaFilename(), 'HED3G_format_issues')
+    let display_name = convertToResultsName(getSchemaFilename(), 'HED3G_format_issues')
     resetFlashMessages(false);
     flashMessageOnScreen('Checking schema for HED-3G compliance...', 'success',
         'schema-submit-flash')
@@ -190,28 +182,14 @@ function submitSchemaComplianceCheckForm() {
             contentType: false,
             processData: false,
             dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success', 'schema-submit-flash');
-                      triggerDownloadBlob(downloaded_file, download_display_name);
-                  } else {
-                      flashMessageOnScreen('No HED-3G compliance issues found.', 'success',
-                          'schema-submit-flash');
-                  }
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'schema-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'schema-submit-flash');
-                } else {
-                    flashMessageOnScreen('Schema could not be processed',
-                        'error','schema-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'schema-submit-flash')
             }
         }
     )
-    ;
 }
 
 function updateFormGui() {
@@ -256,24 +234,6 @@ function updateFormGui() {
      flashMessageOnScreen('', 'success', 'schema-submit-flash')
 }
 
-/**
- * Updates the HED file label.
- * @param {String} hedPath - The path to the HED XML file.
- */
-function updateSchemaFileLabel(hedPath) {
-    let hedFilename = hedPath.split('\\').pop();
-    $('#schema-file-display-name').text(hedFilename);
-}
-
-/**
- * Updates the HED file label.
- * @param {String} hedPath - The path to the HED XML file.
- */
-function updateSchemaUrlLabel(hedPath) {
-    let hedFilename = hedPath.split('\\').pop();
-    $('#schema-url-display-name').text(hedFilename);
-}
-
 
 function urlFileBasename(url) {
     let urlObj = null
diff --git a/hedweb/hed/web/templates/js/spreadsheet-form.js b/hedweb/hed/web/templates/js/spreadsheet-form.js
index 138752288..394fbd7fb 100644
--- a/hedweb/hed/web/templates/js/spreadsheet-form.js
+++ b/hedweb/hed/web/templates/js/spreadsheet-form.js
@@ -255,29 +255,14 @@ function submitForm() {
             contentType: false,
             processData: false,
             dataType: 'text',
-            success: function (downloaded_file) {
-                  if (downloaded_file) {
-                      flashMessageOnScreen('', 'success',
-                          'spreadsheet-validation-submit-flash');
-                      triggerDownloadBlob(downloaded_file, display_name);
-                  } else {
-                      flashMessageOnScreen('No validation errors found.', 'success',
-                          'spreadsheet-validation-submit-flash');
-                  }
+            success: function (download, status, xhr) {
+                getResponseSuccess(download, xhr, display_name, 'spreadsheet-validation-submit-flash')
             },
-            error: function (download_response) {
-                console.log(download_response.responseText);
-                if (download_response.responseText.length < 100) {
-                    flashMessageOnScreen(download_response.responseText, 'error',
-                        'spreadsheet-validation-submit-flash');
-                } else {
-                    flashMessageOnScreen('Spreadsheet could not be processed',
-                        'error','spreadsheet-validation-submit-flash');
-                }
+            error: function (download, status, xhr) {
+                getResponseFailure(download, xhr, display_name, 'spreadsheet-validation-submit-flash')
             }
         }
     )
-    ;
 }
 
 /**
diff --git a/hedweb/hed/web/templates/layout.html b/hedweb/hed/web/templates/layout.html
index b77e60075..d0fee1655 100644
--- a/hedweb/hed/web/templates/layout.html
+++ b/hedweb/hed/web/templates/layout.html
@@ -1,6 +1,8 @@
 <!doctype html>
-<html>
+<html lang="en">
 <head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <!-- Latest compiled and minified CSS -->
     <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
           integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
@@ -11,7 +13,7 @@
           crossorigin="anonymous">
     <link rel="shortcut icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/layout.css') }}">
-    <script src="//ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
     {% block head %}
     {% endblock %}
     <title>
@@ -25,9 +27,9 @@
     <h1 class="project-name">Hierarchical Event Descriptor (HED) online tools</h1>
     <h2 class="project-tagline">Online tools for HED schema, HED tags, and event files</h2>
     <a href="{{ url_for('route_blueprint.render_home_page') }}" class="btn">HED tools online</a>
-     <a href="{{ url_for('route_blueprint.render_events_validation_form') }}" class="btn">Validate events</a>
-    <a href="{{ url_for('route_blueprint.render_spreadsheet_validation_form') }}" class="btn">Validate spreadsheet</a>
-    <a href="{{ url_for('route_blueprint.render_dictionary_validation_form') }}" class="btn">Validate dictionary</a>
+     <a href="{{ url_for('route_blueprint.render_events_form') }}" class="btn">Validate events</a>
+    <a href="{{ url_for('route_blueprint.render_spreadsheet_form') }}" class="btn">Validate spreadsheet</a>
+    <a href="{{ url_for('route_blueprint.render_dictionary_form') }}" class="btn">Validate dictionary</a>
     <a href="{{ url_for('route_blueprint.render_schema_form') }}" class="btn">Process schema</a>
    <!-- <a href="{{ url_for('route_blueprint.render_help_page') }}" class="btn">HED tools help</a> -->
     <a href="https://github.com/hed-standard" class="btn">HED standard</a>
@@ -48,7 +50,7 @@ <h2 class="project-tagline">Online tools for HED schema, HED tags, and event fil
         integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
         crossorigin="anonymous"></script>
 <script type="text/javascript">
-    var csrf_token = "{{ csrf_token() }}";
+    let csrf_token = "{{ csrf_token() }}";
     $.ajaxSetup({
         beforeSend: function (xhr, settings) {
             if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
diff --git a/hedweb/hed/web/templates/schema.html b/hedweb/hed/web/templates/schema.html
index 42d83d5d7..560b4c9b7 100644
--- a/hedweb/hed/web/templates/schema.html
+++ b/hedweb/hed/web/templates/schema.html
@@ -4,29 +4,31 @@ <h2>Process HED Schema</h2>
     <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/schema-form.css') }}">
     <form id="schema-form" method="POST" enctype="multipart/form-data">
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
-        <h3>Source file (.xml/.mediawiki): </h3>
+        <h3>Select a schema source file (.xml/.mediawiki): </h3>
         <div class="form-group" id="schema-url-group">
-            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-url-option" value="schema-url-option">
-            <label class="secondary-label" for="schema-url-version">Enter HED schema URL:</label>
+            <input class="radio-btn" type="radio" name="schema-upload-options"
+                   id="schema-url-option" value="schema-url-option" aria-label="Select this button for HED schema URL">
+            <label class="secondary-label">Enter HED schema URL:</label>
             <label class="btn-group url-browse-group">
                 <label id="schema-url-display-name"></label>
-                <input type="url" name="schema-url" id="schema-url">
+                <input type="url" name="schema-url" id="schema-url" aria-label="Enter schema URL here">
             </label>
             <p class="flash" id="schema-url-flash"></p>
         </div>
 
         <div class="form-group" id="schema-file-group">
-            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-file-option" value="schema-file-option">
+            <input class="radio-btn" type="radio" name="schema-upload-options" id="schema-file-option"
+                   value="schema-file-option" aria-label="Select this button for HED schema file">
             <label class="secondary-label" for="schema-file">Upload HED schema file:</label>
             <label class="btn-group file-browse-group">
-                <button for="schema-file">Browse ...</button>
+                <button aria-label="Button for uploading HED file">Browse ...</button>
                 <label id="schema-file-display-name"></label>
-                <input type="file" name="schema-file" id="schema-file"/>
+                <input type="file" name="schema-file" id="schema-file" aria-label="Enter schema file to upload here"/>
             </label>
             <p class="flash" id="schema-file-flash"></p>
         </div>
 
-        <h3>Actions (pick one): </h3>
+        <h3>Pick an action: </h3>
         <div class="form-group">
             <button id="schema-conversion-submit" type="button">Convert schema format</button>
         </div>
diff --git a/hedweb/hed/web/utils.py b/hedweb/hed/web/utils.py
index 7990df99b..7470cef53 100644
--- a/hedweb/hed/web/utils.py
+++ b/hedweb/hed/web/utils.py
@@ -11,12 +11,12 @@
 app_config = current_app.config
 
 
-def find_spreadsheet_columns_info(form_request_object):
+def find_spreadsheet_columns_info(request):
     """Finds the info associated with the spreadsheet columns.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -28,11 +28,11 @@ def find_spreadsheet_columns_info(form_request_object):
     spreadsheet_columns_info = []
     try:
         spreadsheet_columns_info = {common_constants.COLUMN_NAMES: [], common_constants.TAG_COLUMN_INDICES: []}
-        if common_constants.SPREADSHEET_FILE in form_request_object.files:
-            spreadsheet_file = form_request_object.files[common_constants.SPREADSHEET_FILE]
+        if common_constants.SPREADSHEET_FILE in request.files:
+            spreadsheet_file = request.files[common_constants.SPREADSHEET_FILE]
             spreadsheet_file_path = save_file_to_upload_folder(spreadsheet_file)
-            if spreadsheet_file_path and worksheet_name_present_in_form(form_request_object):
-                worksheet_name = form_request_object.form[common_constants.WORKSHEET_NAME]
+            if spreadsheet_file_path and worksheet_name_present_in_form(request):
+                worksheet_name = request.form[common_constants.WORKSHEET_NAME]
                 spreadsheet_columns_info = populate_spreadsheet_columns_info_dictionary(spreadsheet_columns_info,
                                                                                         spreadsheet_file_path,
                                                                                         worksheet_name)
@@ -86,41 +86,12 @@ def get_excel_worksheet_names(workbook_file_path):
     return worksheet_names
 
 
-def get_optional_form_field(form_request_object, form_field_name, type=''):
-    """Gets the specified optional form field if present.
-
-    Parameters
-    ----------
-    form_request_object: Request object
-        A Request object containing user data from the validation form.
-    form_field_name: string
-        The name of the optional form field.
-    type: str
-        Name of expected type: 'boolean' or 'string'
-
-    Returns
-    -------
-    boolean or string
-        A boolean or string value based on the form field type.
-
-    """
-    form_field_value = ''
-    if type == common_constants.BOOLEAN:
-        form_field_value = False
-        if form_field_name in form_request_object.form:
-            form_field_value = True
-    elif type == common_constants.STRING:
-        if form_field_name in form_request_object.form:
-            form_field_value = form_request_object.form[form_field_name]
-    return form_field_value
-
-
-def get_specific_tag_columns_from_form(form_request_object):
+def get_specific_tag_columns_from_form(request):
     """Gets the specific tag columns from the validation form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the validation form.
 
     Returns
@@ -132,8 +103,8 @@ def get_specific_tag_columns_from_form(form_request_object):
     column_prefix_dictionary = {}
     for tag_column_name in spreadsheet_constants.SPECIFIC_TAG_COLUMN_NAMES:
         form_tag_column_name = tag_column_name.lower() + common_constants.COLUMN_POSTFIX
-        if form_tag_column_name in form_request_object.form:
-            tag_column_name_index = form_request_object.form[form_tag_column_name].strip()
+        if form_tag_column_name in request.form:
+            tag_column_name_index = request.form[form_tag_column_name].strip()
             if tag_column_name_index:
                 tag_column_name_index = int(tag_column_name_index)
 
@@ -302,18 +273,18 @@ def populate_worksheets_info_dictionary(worksheets_info, spreadsheet_file_path):
     return worksheets_info
 
 
-def worksheet_name_present_in_form(validation_form_request_object):
-    """Checks to see if a worksheet name is present in a request object from the validation form.
+def worksheet_name_present_in_form(request):
+    """Checks to see if a worksheet name is present in a request object from form.
 
     Parameters
     ----------
-    validation_form_request_object: Request object
-        A Request object containing user data from the validation form.
+    request: Request object
+        A Request object containing user data from a form.
 
     Returns
     -------
     boolean
-        True if a worksheet name is present in a request object from the validation form.
+        True if a worksheet name is present in a request object from the form.
 
     """
-    return common_constants.WORKSHEET_NAME in validation_form_request_object.form
+    return common_constants.WORKSHEET_NAME in request.form
diff --git a/hedweb/hed/web/web_utils.py b/hedweb/hed/web/web_utils.py
index 47c0df1d2..0990c30d1 100644
--- a/hedweb/hed/web/web_utils.py
+++ b/hedweb/hed/web/web_utils.py
@@ -7,9 +7,10 @@
 from flask import current_app, jsonify, Response
 
 from hed.util import hed_cache
+from hed.util.error_reporter import ErrorHandler
 from hed.util.file_util import get_file_extension, delete_file_if_it_exists
 from hed.util.hed_schema import HedSchema
-from hed.web.constants import common_constants, error_constants, file_constants
+from hed.web.constants import common_constants, error_constants
 
 app_config = current_app.config
 
@@ -75,15 +76,15 @@ def find_all_str_indices_in_list(list_of_str, str_value):
             value.lower().replace(' ', '') == str_value.lower().replace(' ', '')]
 
 
-def find_hed_version_in_uploaded_file(form_request_object, key_name=common_constants.HED_XML_FILE):
+def find_hed_version_in_uploaded_file(request, key_name=common_constants.HED_XML_FILE):
     """Finds the version number in an HED XML other.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data
     key_name: str
-        Name of the key for the HED XML file in the form_request_object
+        Name of the key for the HED XML file in the request
 
     Returns
     -------
@@ -93,20 +94,20 @@ def find_hed_version_in_uploaded_file(form_request_object, key_name=common_const
     """
     hed_info = {}
     try:
-        if key_name in form_request_object.files:
-            hed_file_path = save_file_to_upload_folder(form_request_object.files[key_name])
+        if key_name in request.files:
+            hed_file_path = save_file_to_upload_folder(request.files[key_name])
             hed_info[common_constants.HED_VERSION] = HedSchema.get_hed_xml_version(hed_file_path)
     except:
         hed_info[error_constants.ERROR_KEY] = traceback.format_exc()
     return hed_info
 
 
-def form_has_file(form_request_object, file_field, valid_extensions):
+def form_has_file(request, file_field, valid_extensions):
     """Checks to see if a file name with valid extension is present in the request object.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from the schema form.
     file_field: str
         Name of the form field containing the file name
@@ -119,19 +120,18 @@ def form_has_file(form_request_object, file_field, valid_extensions):
         True if a file is present in a request object.
 
     """
-    if file_field in form_request_object.files and \
-            file_extension_is_valid(form_request_object.files[file_field].filename, valid_extensions):
+    if file_field in request.files and file_extension_is_valid(request.files[file_field].filename, valid_extensions):
         return True
     else:
         return False
 
 
-def form_has_option(form_request_object, option_name, target_value):
+def form_has_option(request, option_name, target_value):
     """Checks if the given option has a specific value. This is used for radio buttons.
 
     Parameters
     ----------
-    form_request_object: Request
+    request: Request
         A Request object produced by the post of a form
     option_name: str
         String containing the name of the radio button group in the web form
@@ -144,17 +144,17 @@ def form_has_option(form_request_object, option_name, target_value):
         True if the target radio button has been set and false otherwise
     """
 
-    if option_name in form_request_object.values and form_request_object.values[option_name] == target_value:
+    if option_name in request.values and request.values[option_name] == target_value:
         return True
     return False
 
 
-def form_has_url(form_request_object, url_field, valid_extensions):
+def form_has_url(request, url_field, valid_extensions):
     """Checks to see if the url_field has a value with a valid extension.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from a form.
     url_field: str
         The name of the value field in the form containing the URL to be parsed.
@@ -167,11 +167,11 @@ def form_has_url(form_request_object, url_field, valid_extensions):
         True if a URL is present in request object.
 
     """
-    parsed_url = urlparse(form_request_object.form.get(url_field))
+    parsed_url = urlparse(request.form.get(url_field))
     return file_extension_is_valid(parsed_url.path, valid_extensions)
 
 
-def generate_download_file_response(download_file, display_name=None, header=None):
+def generate_download_file_response(download_file, display_name=None, header=None, category='success', msg=''):
     """Generates a download other response.
 
     Parameters
@@ -181,7 +181,11 @@ def generate_download_file_response(download_file, display_name=None, header=Non
     display_name: str
         Name to be assigned to the file in the response
     header: str
-        Optional header -- usually given for error message downloads
+        Optional header -- header for download file blob
+    category: str
+        Category of the message to be displayed ('Success', 'Error', 'Warning')
+    msg: str
+        Optional message to be displayed in the submit-flash-field
 
     Returns
     -------
@@ -191,6 +195,7 @@ def generate_download_file_response(download_file, display_name=None, header=Non
     """
     if not display_name:
         display_name = download_file
+    print("In generate:", display_name)
     try:
         if not download_file:
             return f"No download file given"
@@ -206,20 +211,21 @@ def generate():
                     yield line
             delete_file_if_it_exists(download_file)
         return Response(generate(), mimetype='text/plain charset=utf-8',
-                        headers={'Content-Disposition': f"attachment filename={display_name}"})
+                        headers={'Content-Disposition': f"attachment filename={display_name}",
+                                 'Category': category, 'Message': msg})
     except:
         return traceback.format_exc()
 
 
-def generate_issues_filename(basename, prefix, suffix=''):
-    """Generates a filename for the attachment of the form prefix_basename_suffix.txt.
+def generate_filename(basename, prefix=None, suffix=None, extension=None):
+    """Generates a filename for the attachment of the form prefix_basename_suffix + extension.
 
     Parameters
     ----------
    basename: str
         The name of the base, usually the name of the file that the issues were generated from
     prefix: str
-        The prefix preappended to the front of the basename
+        The prefix prepended to the front of the basename
     suffix: str
         The suffix appended to the end of the basename
     Returns
@@ -227,19 +233,30 @@ def generate_issues_filename(basename, prefix, suffix=''):
     string
         The name of the attachment other containing the issues.
     """
-    filename = secure_filename(prefix)
+
+    pieces = []
+    if prefix:
+        pieces = pieces + [secure_filename(prefix)]
     if basename:
-        filename = filename + '_' + secure_filename(basename).rsplit('.')[0]
+        pieces.append(os.path.splitext(secure_filename(basename))[0])
     if suffix:
-        return filename + '_' + secure_filename(suffix) + file_constants.TEXT_EXTENSION
-    return filename + file_constants.TEXT_EXTENSION
+        pieces = pieces + [secure_filename(suffix)]
+
+    if not pieces:
+        return ''
+    filename = pieces[0]
+    for name in pieces[1:]:
+        filename = filename + '_' + name
+    if extension:
+        filename = filename + '.' + secure_filename(extension)
+    return filename
 
 
-def get_hed_path_from_pull_down(form_request_object):
+def get_hed_path_from_pull_down(request):
     """Gets the hed path from a section of form that uses a pull-down box and hed_cache
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from a form.
 
     Returns
@@ -247,28 +264,87 @@ def get_hed_path_from_pull_down(form_request_object):
     tuple: str, str
         The HED XML local path and the HED display file name
     """
-    if common_constants.HED_VERSION not in form_request_object.form:
+    if common_constants.HED_VERSION not in request.form:
         hed_file_path = ''
         hed_display_name = ''
-    elif form_request_object.form[common_constants.HED_VERSION] != common_constants.HED_OTHER_VERSION_OPTION:
-        hed_file_path = hed_cache.get_path_from_hed_version(form_request_object.form[common_constants.HED_VERSION])
+    elif request.form[common_constants.HED_VERSION] != common_constants.HED_OTHER_VERSION_OPTION:
+        hed_file_path = hed_cache.get_path_from_hed_version(request.form[common_constants.HED_VERSION])
         hed_display_name = os.path.basename(hed_file_path)
-    elif form_request_object.form[common_constants.HED_VERSION] == common_constants.HED_OTHER_VERSION_OPTION and \
-            common_constants.HED_XML_FILE in form_request_object.files:
-        hed_file_path = save_file_to_upload_folder(form_request_object.files[common_constants.HED_XML_FILE])
-        hed_display_name = form_request_object.files[common_constants.HED_XML_FILE].filename
+    elif request.form[common_constants.HED_VERSION] == common_constants.HED_OTHER_VERSION_OPTION and \
+            common_constants.HED_XML_FILE in request.files:
+        hed_file_path = save_file_to_upload_folder(request.files[common_constants.HED_XML_FILE])
+        hed_display_name = request.files[common_constants.HED_XML_FILE].filename
     else:
         hed_file_path = ''
         hed_display_name = ''
     return hed_file_path, hed_display_name
 
 
-def get_uploaded_file_path_from_form(form_request_object, file_key, valid_extensions=None):
+def get_printable_issue_string(issues, display_name=None, title_string=''):
+    """Returns a string suitable for text blob download
+
+     Parameters
+     ----------
+     issues: list
+         List of the issues to be printed.
+     title_string: str
+         key name in the files dictionary of the Request object
+     display_name: list of str
+         List of valid extensions
+
+     Returns
+     -------
+     tuple
+         A tuple containing the other paths. The two other paths are for the spreadsheet and a optional HED XML other.
+     """
+    if not issues:
+        return ''
+    if title_string:
+        title = title_string
+    else:
+        title = ''
+    if display_name:
+        title = display_name + ' ' + title_string + '\n'
+    if not title:
+        title = None
+    return ErrorHandler.get_printable_issue_string(issues, title, skip_filename=True)
+
+
+def get_optional_form_field(request, form_field_name, type=''):
+    """Gets the specified optional form field if present.
+
+    Parameters
+    ----------
+    request: Request object
+        A Request object containing user data from the validation form.
+    form_field_name: string
+        The name of the optional form field.
+    type: str
+        Name of expected type: 'boolean' or 'string'
+
+    Returns
+    -------
+    boolean or string
+        A boolean or string value based on the form field type.
+
+    """
+    form_field_value = ''
+    if type == common_constants.BOOLEAN:
+        form_field_value = False
+        if form_field_name in request.form:
+            form_field_value = True
+    elif type == common_constants.STRING:
+        if form_field_name in request.form:
+            form_field_value = request.form[form_field_name]
+    return form_field_value
+
+
+def get_uploaded_file_path_from_form(request, file_key, valid_extensions=None):
     """Gets the other paths of the uploaded files in the form.
 
     Parameters
     ----------
-    form_request_object: Request object
+    request: Request object
         A Request object containing user data from a form.
     file_key: str
         key name in the files dictionary of the Request object
@@ -282,9 +358,9 @@ def get_uploaded_file_path_from_form(form_request_object, file_key, valid_extens
     """
     uploaded_file_name = ''
     original_file_name = ''
-    if form_has_file(form_request_object, file_key, valid_extensions):
-        uploaded_file_name = save_file_to_upload_folder(form_request_object.files[file_key])
-        original_file_name = form_request_object.files[file_key].filename
+    if form_has_file(request, file_key, valid_extensions):
+        uploaded_file_name = save_file_to_upload_folder(request.files[file_key])
+        original_file_name = request.files[file_key].filename
     return uploaded_file_name, original_file_name
 
 
@@ -364,3 +440,27 @@ def save_issues_to_upload_folder(issues, filename):
             issues_file.write(val_issue['message'] + "\n")
     issues_file.close()
     return issues_file_path
+
+
+def save_text_to_upload_folder(text, filename):
+    """Saves a string in the upload folder as filename.
+
+    Parameters
+    ----------
+    text: string
+        A printable string.
+    filename: str
+        File name of the issue folder
+
+    Returns
+    -------
+    string
+        The name of the validation output other.
+
+    """
+
+    file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
+    with open(file_path, 'w', encoding='utf-8') as text_file:
+        text_file.write(text)
+    text_file.close()
+    return file_path
diff --git a/hedweb/requirements.txt b/hedweb/requirements.txt
index 0e86736c7..ea73d03d7 100644
--- a/hedweb/requirements.txt
+++ b/hedweb/requirements.txt
@@ -16,6 +16,6 @@ xlrd==1.2.0
 defusedxml==0.6.0
 portalocker==2.0.0
 python-dateutil==2.8.1
-pytz==2020.4
+pytz==2021.1
 semantic_version==2.8.5
 six==1.15.0


Test patch:
diff --git a/hedweb/tests/test_validation.py b/hedweb/tests/test_dictionary.py
similarity index 100%
rename from hedweb/tests/test_validation.py
rename to hedweb/tests/test_dictionary.py
diff --git a/hedweb/tests/test_events.py b/hedweb/tests/test_events.py
new file mode 100644
index 000000000..4b6e8b61d
--- /dev/null
+++ b/hedweb/tests/test_events.py
@@ -0,0 +1,42 @@
+import unittest
+# from flask import current_app, jsonify, Response
+# from hed.web.utils import app_config
+# from hed.web.validation import generate_dictionary_validation_filename
+# from hed.web.app_factory import AppFactory
+# from hed.web.constants import file_constants, spreadsheet_constants
+import os
+
+# app = AppFactory.create_app('config.TestConfig')
+# with app.app_context():
+#     from hed.web import web_utils
+#     from hed.web.routes import route_blueprint
+#
+#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
+#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        print("Stuff")
+        # self.create_test_app()
+        # self.app = app.app.test_client()
+        # self.major_version_key = 'major_versions'
+        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
+        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
+
+    # def create_test_app(self):
+    #     app = AppFactory.create_app('config.TestConfig')
+    #     with app.app_context():
+    #         from hed.web.routes import route_blueprint
+    #         app.register_blueprint(route_blueprint)
+    #         self.app = app.test_client()
+
+    def test_generate_input_from_events_form(self):
+        self.assertTrue(1, "Testing generate_input_from_events_form")
+
+    def test_report_events_validation_status(self):
+        self.assertTrue(1, "report_events_validation_status")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedweb/tests/test_routes.py b/hedweb/tests/test_routes.py
index f81fff8b7..f2ac8efd7 100644
--- a/hedweb/tests/test_routes.py
+++ b/hedweb/tests/test_routes.py
@@ -53,46 +53,50 @@ def test_download_file_in_upload_directory(self):
         print(response.status_code)
         # self.assertEqual(response.status_code, 204, "Dummy file should be deleted")
 
-    # def test_get_duplicate_tag_results(self):
-    #     response = self.app.get('/schema-duplicate-tag-results')
-    #     self.assertEqual(response.status_code, 404)
-    #
-    # def test_get_eeg_events_validation_results(self):
-    #     response = self.app.get('/eeg-validation-submit')
-    #     self.assertEqual(response.status_code, 404)
-    #
-    # def test_get_hed_version_in_file(self):
-    #     response = self.app.post('/get-hed-version')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_major_hed_versions(self):
-    #     response = self.app.post('/get-hed-major-versions')
-    #     self.assertEqual(response.status_code, 405)
-    # import hed.web.constants.common_constants as constants
-    # from hed.web.web_utils import find_major_hed_versions
-    # hed_info = find_major_hed_versions()
-    # self.assertTrue(constants.HED_MAJOR_VERSIONS in hed_info, "The information has key hed-major-versions")
-    # self.assertTrue('7.1.2' in hed_info[constants.HED_MAJOR_VERSIONS], "7.1.2 is a major versions")
-    #
-    # def test_get_major_hed_versions(self):
-    #     response = self.app.post('/get-hed-major-versions')
-    #     self.assertEqual(response.status_code, 405)
-    #
-    # def test_get_schema_conversion_results(self):
-    #     response = self.app.post('/schema-conversion-submit')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_spreadsheet_columns_info(self):
-    #     response = self.app.post('/get-spreadsheet-columns-info')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_validation_results(self):
-    #     response = self.app.post('/validation-submit')
-    #     self.assertEqual(response.status_code, 400)
-    #
-    # def test_get_worksheets_info(self):
-    #     response = self.app.post('/get-worksheets-info')
-    #     self.assertEqual(response.status_code, 400)
+    def test_get_dictionary_validation_results(self):
+        response = self.app.get('/dictionary-validation-submit')
+        self.assertEqual(response.status_code, 404)
+
+    def test_get_events_validation_results(self):
+        response = self.app.get('/events-validation-submit')
+        self.assertEqual(response.status_code, 404)
+
+    def test_get_hed_services_results(self):
+        response = self.app.get('/hed-services-submit')
+        self.assertEqual(response.status_code, 404)
+
+    def test_get_hed_version(self):
+        response = self.app.post('/get-hed-version')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_major_hed_versions(self):
+        response = self.app.post('/get-hed-major-versions')
+        self.assertEqual(response.status_code, 405)
+        # import hed.web.constants.common_constants as constants
+        # from hed.web.web_utils import find_major_hed_versions
+        # hed_info = find_major_hed_versions()
+        # self.assertTrue(constants.HED_MAJOR_VERSIONS in hed_info, "The information has key hed-major-versions")
+        # self.assertTrue('7.1.2' in hed_info[constants.HED_MAJOR_VERSIONS], "7.1.2 is a major versions")
+
+    def test_get_schema_compliance_check_results(self):
+        response = self.app.post('/schema-compliance-check-submit')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_schema_conversion_results(self):
+        response = self.app.post('/schema-conversion-submit')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_spreadsheet_columns_info(self):
+        response = self.app.post('/get-spreadsheet-columns-info')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_spreadsheet_validation_results(self):
+        response = self.app.post('/spreadsheet-validation-submit')
+        self.assertEqual(response.status_code, 400)
+
+    def test_get_worksheets_info(self):
+        response = self.app.post('/get-worksheets-info')
+        self.assertEqual(response.status_code, 400)
 
     def test_render_additional_examples_page(self):
         response = self.app.test.get('/additional-examples')
@@ -102,25 +106,33 @@ def test_render_common_error_page(self):
         response = self.app.test.get('/common-errors')
         self.assertEqual(response.status_code, 200, "The common-errors content page exists")
 
-    # def test_render_eeg_validation_form(self):
-    #     response = self.app.get('/eeg-validation')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_home_page(self):
-    #     response = self.app.get('/')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_help_page(self):
-    #     response = self.app.get('/hed-tools-help')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_schema_form(self):
-    #     response = self.app.get('/schema')
-    #     self.assertEqual(response.status_code, 200)
-    #
-    # def test_render_validation_form(self):
-    #     response = self.app.get('/validation')
-    #     self.assertEqual(response.status_code, 200)
+    def test_render_dictionary_form(self):
+        response = self.app.get('/dictionary')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_events_form(self):
+        response = self.app.get('/events')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_hed_services_form(self):
+        response = self.app.get('/hed-services')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_help_page(self):
+        response = self.app.get('/hed-tools-help')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_home_page(self):
+        response = self.app.get('/')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_schema_form(self):
+        response = self.app.get('/schema')
+        self.assertEqual(response.status_code, 200)
+
+    def test_render_spreadsheet_form(self):
+        response = self.app.get('/spreadsheet')
+        self.assertEqual(response.status_code, 200)
 
 
 if __name__ == '__main__':
diff --git a/hedweb/tests/test_schema.py b/hedweb/tests/test_schema.py
index 028d2c60c..6950cefc8 100644
--- a/hedweb/tests/test_schema.py
+++ b/hedweb/tests/test_schema.py
@@ -4,7 +4,6 @@
 # from hed.web.validation import generate_dictionary_validation_filename
 # from hed.web.app_factory import AppFactory
 # from hed.web.constants import file_constants, spreadsheet_constants
-import os
 
 # app = AppFactory.create_app('config.TestConfig')
 # with app.app_context():
@@ -31,25 +30,18 @@ def setUp(self):
     #         app.register_blueprint(route_blueprint)
     #         self.app = app.test_client()
 
-
-    def test_generate_input_arguments_from_schema_form(self):
+    def test_generate_input_from_schema_form(self):
         self.assertTrue(1, "Testing generate_input_from_schema_form")
 
-    def test_get_schema_conversion_function(self):
+    def test_get_schema_conversion(self):
         self.assertTrue(1, "Testing get_schema_conversion")
 
-    def test_get_uploaded_file_paths_from_schema_form(self):
-        self.assertTrue(1, "Testing get_uploaded_file_paths_from_schema_form")
+    def test_run_schema_compliance_check(self):
+        self.assertTrue(1, "Testing run_schema_compliance_check")
 
     def test_run_schema_conversion(self):
         self.assertTrue(1, "Testing run_schema_conversion")
 
-    def test_run_schema_duplicate_tag_detection(self):
-        self.assertTrue(1, "Testing run_schema_duplicate_tag_detection")
-
-    def test_url_present_in_form(self):
-        self.assertTrue(1, "Testing form_has_hed_url")
-
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/hedweb/tests/test_services.py b/hedweb/tests/test_services.py
new file mode 100644
index 000000000..1e9b1409f
--- /dev/null
+++ b/hedweb/tests/test_services.py
@@ -0,0 +1,62 @@
+import unittest
+# from flask import current_app, jsonify, Response
+# from hed.web.utils import app_config
+# from hed.web.validation import generate_dictionary_validation_filename
+# from hed.web.app_factory import AppFactory
+# from hed.web.constants import file_constants, spreadsheet_constants
+import os
+
+# app = AppFactory.create_app('config.TestConfig')
+# with app.app_context():
+#     from hed.web import web_utils
+#     from hed.web.routes import route_blueprint
+#
+#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
+#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        print("Stuff")
+        # self.create_test_app()
+        # self.app = app.app.test_client()
+        # self.major_version_key = 'major_versions'
+        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
+        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
+
+    # def create_test_app(self):
+    #     app = AppFactory.create_app('config.TestConfig')
+    #     with app.app_context():
+    #         from hed.web.routes import route_blueprint
+    #         app.register_blueprint(route_blueprint)
+    #         self.app = app.test_client()
+
+    def test_generate_input_arguments_from_validation_form(self):
+        self.assertTrue(1, "Testing generate_arguments_from_dictionary_form")
+
+    def test_generate_spreadsheet_validation_filename(self):
+        self.assertTrue(1, "Testing generate_dictionary_validation_filename")
+        # dictionary_filename = 'abc.xls'
+        # expected_spreadsheet_filename = 'validated_' + dictionary_filename.rsplit('.')[0] + '.txt'
+        # validation_file_name = generate_dictionary_validation_filename(dictionary_filename, worksheet_name='')
+        # self.assertTrue(validation_file_name)
+        # self.assertEqual(expected_spreadsheet_filename, validation_file_name)
+
+    def test_get_uploaded_file_paths_from_forms(self):
+        self.assertTrue(1, "Testing get_uploaded_file_paths_from_forms")
+
+    def test_report_eeg_events_validation_status(self):
+        self.assertTrue(1, "Testing report_eeg_events_validation_status")
+
+    def test_report_spreadsheet_validation_status(self):
+        self.assertTrue(1, "Testing report_dictionary_validation_status")
+
+    def test_save_validation_issues_to_file_in_upload_folder(self):
+        self.assertTrue(1, "Testing save_issues_to_upload_folder")
+
+    def test_validate_spreadsheet(self):
+        self.assertTrue(1, "Testing validate_dictionary")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedweb/tests/test_spreadsheet.py b/hedweb/tests/test_spreadsheet.py
new file mode 100644
index 000000000..282068243
--- /dev/null
+++ b/hedweb/tests/test_spreadsheet.py
@@ -0,0 +1,45 @@
+import unittest
+# from flask import current_app, jsonify, Response
+# from hed.web.utils import app_config
+# from hed.web.validation import generate_dictionary_validation_filename
+# from hed.web.app_factory import AppFactory
+# from hed.web.constants import file_constants, spreadsheet_constants
+import os
+
+# app = AppFactory.create_app('config.TestConfig')
+# with app.app_context():
+#     from hed.web import web_utils
+#     from hed.web.routes import route_blueprint
+#
+#     app.register_blueprint(route_blueprint, url_prefix=app.config['URL_PREFIX'])
+#     web_utils.create_upload_directory(app.config['UPLOAD_FOLDER'])
+
+
+class Test(unittest.TestCase):
+    def setUp(self):
+        print("Stuff")
+        # self.create_test_app()
+        # self.app = app.app.test_client()
+        # self.major_version_key = 'major_versions'
+        # self.hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
+        # self.tsv_file1 = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/tsv_file1.txt')
+
+    # def create_test_app(self):
+    #     app = AppFactory.create_app('config.TestConfig')
+    #     with app.app_context():
+    #         from hed.web.routes import route_blueprint
+    #         app.register_blueprint(route_blueprint)
+    #         self.app = app.test_client()
+
+    def test_generate_input_from_validation_form(self):
+        self.assertTrue(1, "Testing generate_input_from_validation_form")
+
+    def test_report_spreadsheet_validation_status(self):
+        self.assertTrue(1, "Testing report_spreadsheet_validation_status")
+
+    def test_validate_spreadsheet(self):
+        self.assertTrue(1, "Testing validate_spreadsheet")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/hedweb/tests/test_utils.py b/hedweb/tests/test_utils.py
index 8bb86a75c..a7924ef67 100644
--- a/hedweb/tests/test_utils.py
+++ b/hedweb/tests/test_utils.py
@@ -26,8 +26,8 @@ def tearDownClass(cls):
     def test_find_spreadsheet_columns_info(self):
         self.assertTrue(1, "Testing find_spreadsheet_columns_info")
 
-    # def test_get_column_delimiter_based_on_file_extension(self):
-    #     self.assertTrue(1, "Testing get_column_delimiter_based_on_file_extension")
+    def test_get_column_delimiter_based_on_file_extension(self):
+        self.assertTrue(1, "Testing get_column_delimiter_based_on_file_extension")
     #     from hed.web.utils import get_column_delimiter_based_on_file_extension
     #     delimiter = get_column_delimiter_based_on_file_extension('test.tsv')
     #     self.assertEqual('\t', delimiter, ".tsv files should have a tab delimiter")
@@ -46,9 +46,6 @@ def test_get_excel_worksheet_names(self):
         self.assertIn('PVT Events', worksheet_names, "PVT Events is one of the worksheet names")
         self.assertNotIn('Temp', worksheet_names, "Temp is not one of the worksheet names")
 
-    def test_get_optional_form_field(self):
-        self.assertTrue(1, "Testing get_optional_form_field")
-
     def test_get_original_spreadsheet_filename(self):
         self.assertTrue(1, "Testing get_original_filename")
 
@@ -77,6 +74,12 @@ def test_get_spreadsheet_specific_tag_column_indices(self):
         #     self.assertIsInstance(indices, dict)
         #
 
+    def test_get_spreadsheet_other_tag_column_indices(self):
+        self.assertTrue(1, "Testing get_spreadsheet_other_tag_column_indices")
+
+    def test_get_spreadsheet_specific_tag_column_indices(self):
+        self.assertTrue(1, "Testing get_spreadsheet_specific_tag_column_indices")
+
     def test_get_text_file_column_names(self):
         self.assertTrue(1, "Testing get_text_file_column_names")
         # def test_get_text_file_column_names(self):
diff --git a/hedweb/tests/test_web_utils.py b/hedweb/tests/test_web_utils.py
index 5d9392921..817fe53f0 100644
--- a/hedweb/tests/test_web_utils.py
+++ b/hedweb/tests/test_web_utils.py
@@ -98,9 +98,6 @@ def test_find_all_str_indices_in_list(self):
         self.assertIsInstance(indices, list)
         self.assertEqual(expected_indices, indices)
 
-    def test_find_hed_version_in_uploaded_file(self):
-        self.assertTrue(1, "Testing find_hed_version_in_uploaded_file")
-
     def test_generate_download_file_response(self):
         from hed.web.web_utils import generate_download_file_response
         hed_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/HED.xml')
@@ -122,6 +119,28 @@ def test_generate_download_file_response(self):
             os.remove(temp_file)
         self.assertFalse(os.path.isfile(temp_file), "Dummy has been deleted")
 
+    def test_generate_filename(self):
+        from hed.web.web_utils import generate_filename
+        filename = generate_filename(None, prefix=None, suffix=None, extension=None)
+        self.assertEqual('', filename, "Returns empty when all arguments are none")
+        filename = generate_filename(None, prefix=None, suffix=None, extension='.txt')
+        self.assertEqual('', filename, "Returns empty when basename, prefix, and suffix are None, but extension is not")
+        filename = generate_filename('c:/temp.json', prefix=None, suffix=None, extension='.txt')
+        self.assertEqual('c_temp.txt', filename,
+                         "Returns stripped basename + extension when prefix, and suffix are None")
+        filename = generate_filename('temp.json', prefix='prefix', suffix='suffix', extension='.txt')
+        self.assertEqual('prefix_temp_suffix.txt', filename,
+                         "Returns stripped basename + extension when prefix, and suffix are None")
+        filename = generate_filename(None, prefix='prefix', suffix='suffix', extension='.txt')
+        self.assertEqual('prefix_suffix.txt', filename,
+                         "Returns correct string when no basename")
+        filename = generate_filename('event-strategy-v3_task-matchingpennies_events.json',
+                                     suffix='blech', extension='.txt')
+        self.assertEqual('event-strategy-v3_task-matchingpennies_events_blech.txt', filename,
+                         "Returns correct string when basename with hyphens")
+        filename = generate_filename('HED7.1.2.xml', suffix='blech', extension='.txt')
+        self.assertEqual('HED7.1.2_blech.txt', filename, "Returns correct string when basename has periods")
+
     def test_get_hed_path_from_pull_down(self):
         from hed.web.constants import common_constants
         from hed.web.web_utils import get_hed_path_from_pull_down
@@ -135,6 +154,8 @@ def test_get_hed_path_from_pull_down(self):
         # self.assertTrue(hed_file_path,
         #                 'When hed-version is in form get_hed_path_from_pull_down returns a hed_file_path')
 
+    def test_get_optional_form_field(self):
+        self.assertTrue(1, "Testing get_optional_form_field")
 
     def test_get_uploaded_file_path_from_form(self):
         self.assertTrue(1, "Testingget_uploaded_file_path_from_form")
@@ -145,6 +166,9 @@ def test_handle_http_error(self):
 
         self.assertTrue(1, "Testing handle_http_error")
 
+    def test_find_hed_version_in_uploaded_file(self):
+            self.assertTrue(1, "Testing find_hed_version_in_uploaded_file")
+
     def test_save_file_to_upload_folder(self):
         from hed.web.web_utils import save_file_to_upload_folder, app_config
         temp_name = save_file_to_upload_folder('')