Repository: nismod/smif

Issue:
smif app throws TemplateNotFound when run from fresh source checkout
The fix is [documented](https://smif.readthedocs.io/en/latest/developers.html#install) - it's missing the template because we need to `npm i; npm run build` - but this exception should be caught and a clearer message shown.

Details:

```
ERROR    Exception on / [GET]
Traceback (most recent call last):
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\_compat.py", line 35, in reraise
    raise value
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "\projects\mistral\smif\src\smif\http_api\register.py", line 19, in home
    return render_template('index.html')
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\templating.py", line 134, in render_template
    return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
  File "\miniconda3\envs\testenv\lib\site-packages\jinja2-2.10-py3.5.egg\jinja2\environment.py", line 869, in get_or_select_template
    return self.get_template(template_name_or_list, parent, globals)
  File "\miniconda3\envs\testenv\lib\site-packages\jinja2-2.10-py3.5.egg\jinja2\environment.py", line 830, in get_template
    return self._load_template(name, self.make_globals(globals))
  File "\miniconda3\envs\testenv\lib\site-packages\jinja2-2.10-py3.5.egg\jinja2\environment.py", line 804, in _load_template
    template = self.loader.load(self, name, globals)
  File "\miniconda3\envs\testenv\lib\site-packages\jinja2-2.10-py3.5.egg\jinja2\loaders.py", line 113, in load
    source, filename, uptodate = self.get_source(environment, name)
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\templating.py", line 58, in get_source
    return self._get_source_fast(environment, template)
  File "\miniconda3\envs\testenv\lib\site-packages\flask-1.0.2-py3.5.egg\flask\templating.py", line 86, in _get_source_fast
    raise TemplateNotFound(template)
jinja2.exceptions.TemplateNotFound: index.html

```


Discussion:

Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index 99bf76e5..7cd7b73c 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -8,16 +8,19 @@ environment:
     PGPORT: 5432
     PGPASSWORD: Password12!
   matrix:
-    - PYTHON_VERSION: 3.6
+    - PYTHON_VERSION: 3.7
       MINICONDA: C:\Miniconda3-x64
 init:
   - "ECHO %PYTHON_VERSION% %MINICONDA%"
 install:
   - "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%"
-  - conda config --set always_yes yes --set changeps1 no
+  - conda config --set always_yes true
+  - conda update conda
+  - conda config --set changeps1 false
+  - conda config --set channel_priority strict
   - conda config --add channels conda-forge
   - conda info -a
-  - "conda create -n testenv --yes python=%PYTHON_VERSION% \
+  - "conda create -n testenv python=%PYTHON_VERSION% \
         fiona \
         flask \
         gdal \
@@ -31,8 +34,8 @@ install:
         pyarrow \
         pytest \
         python-dateutil \
-        ruamel.yaml \
         rtree \
+        ruamel.yaml \
         shapely \
         xarray"
   - activate testenv
diff --git a/ci/install.sh b/ci/install.sh
index 2a4d8d29..1543cbcf 100755
--- a/ci/install.sh
+++ b/ci/install.sh
@@ -24,7 +24,6 @@ if [[ "$DISTRIB" == "conda" ]]; then
     conda config --set always_yes true
     # Update conda
     conda update conda
-
     # Follow channel priority strictly
     conda config --set channel_priority strict
     # Don't change prompt
diff --git a/src/smif/app/package.json b/src/smif/app/package.json
index 1637e534..653793eb 100644
--- a/src/smif/app/package.json
+++ b/src/smif/app/package.json
@@ -13,7 +13,7 @@
     "report": "nyc report --reporter=lcov > coverage.lcov",
     "build": "webpack --mode production",
     "watch": "webpack --progress --watch --mode development",
-    "start": "webpack-dev-server --open"
+    "start": "webpack-dev-server --open --mode development"
   },
   "author": "Tom Russell",
   "license": "MIT",
diff --git a/src/smif/http_api/register.py b/src/smif/http_api/register.py
index 962535fe..c947c293 100644
--- a/src/smif/http_api/register.py
+++ b/src/smif/http_api/register.py
@@ -1,3 +1,5 @@
+import logging
+import jinja2.exceptions
 from flask import jsonify, render_template
 from smif.exception import (SmifDataExistsError, SmifDataMismatchError,
                             SmifDataNotFoundError)
@@ -16,7 +18,24 @@ def register_routes(app):
     def home(path=None):
         """Render single page
         """
-        return render_template('index.html')
+        try:
+            return render_template('index.html')
+        except jinja2.exceptions.TemplateNotFound as ex:
+            logging.error(ex, exc_info=True)
+            return """<!doctype html>
+            <style>html, body { font-family: sans-serif; }</style>
+            <h1>Error: smif app template not found</h1>
+            <p>If you are running from a development build of smif, you may need to build the
+            app:</p>
+            <code><pre>
+cd ./src/smif/app
+npm install
+npm run build
+            </pre>
+            </code>
+            <p>Otherwise, please report the issue, including as much detail as possible, on
+            <a href="https://github.com/nismod/smif/issues">GitHub</a>.</p>
+            """
 
 
 def register_api_endpoints(app):


Test patch:
diff --git a/tests/http_api/test_crud.py b/tests/http_api/test_crud.py
index ec5b7ff7..35d5b285 100644
--- a/tests/http_api/test_crud.py
+++ b/tests/http_api/test_crud.py
@@ -85,7 +85,6 @@ def mock_data_interface(model_run, get_sos_model, get_sector_model,
 
 @pytest.fixture
 def app(request, mock_scheduler, mock_data_interface):
-
     """Return an app
     """
     test_app = create_app(
@@ -98,6 +97,20 @@ def app(request, mock_scheduler, mock_data_interface):
     with test_app.app_context():
         yield test_app
 
+@pytest.fixture
+def app_fail(request, mock_scheduler, mock_data_interface):
+    """Return an app which will fail to find templates
+    """
+    test_app = create_app(
+        static_folder=os.path.join(os.path.dirname(__file__), '..', 'fixtures', '404'),
+        template_folder=os.path.join(os.path.dirname(__file__), '..', 'fixtures', '404'),
+        data_interface=mock_data_interface,
+        scheduler=mock_scheduler
+    )
+
+    with test_app.app_context():
+        yield test_app
+
 
 @pytest.fixture
 def client(request, app):
@@ -112,6 +125,19 @@ def client(request, app):
     return test_client
 
 
+@pytest.fixture
+def client_fail(request, app_fail):
+    """Return an API client which will fail on request for home page
+    """
+    test_client = app_fail.test_client()
+
+    def teardown():
+        pass
+
+    request.addfinalizer(teardown)
+    return test_client
+
+
 def parse_json(response):
     """Parse response data
     """
@@ -136,6 +162,13 @@ def test_hello(client):
     assert "Welcome to smif" in str(response.data)
 
 
+def test_template_not_found(client_fail):
+    """Clear error if template not found
+    """
+    response = client_fail.get('/')
+    assert "Error: smif app template not found" in str(response.data)
+
+
 def test_get_smif(client):
     """GET smif details
     """