Repository: spacetelescope/jwql

Issue:
Form on main page to quickly view a particular rootname of proposal
I think our users would find it useful to be able to get straight to viewing a single observation or a particular proposal right from the homepage, instead of having to navigate the archive.  We could have a blank form to enter a rootname or proposal number that, upon submit, redirects the user to the appropriate 'view image' or 'view proposal' page.

Comments:

Patch:
diff --git a/jwql/utils/utils.py b/jwql/utils/utils.py
index 9f3f4102..dc9eb1f1 100644
--- a/jwql/utils/utils.py
+++ b/jwql/utils/utils.py
@@ -29,6 +29,7 @@ from jwql.utils import permissions
 
 __location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
 
+FILE_SUFFIX_TYPES = ['uncal', 'cal', 'rateints', 'rate', 'trapsfilled', 'uncal']
 JWST_INSTRUMENTS = sorted(['NIRISS', 'NIRCam', 'NIRSpec', 'MIRI', 'FGS'])
 JWST_DATAPRODUCTS = ['IMAGE', 'SPECTRUM', 'SED', 'TIMESERIES', 'VISIBILITY',
                      'EVENTLIST', 'CUBE', 'CATALOG', 'ENGINEERING', 'NULL']
@@ -53,6 +54,11 @@ MONITORS = {
 NIRCAM_SHORTWAVE_DETECTORS = ['NRCA1', 'NRCA2', 'NRCA3', 'NRCA4',
                               'NRCB1', 'NRCB2', 'NRCB3', 'NRCB4']
 NIRCAM_LONGWAVE_DETECTORS = ['NRCA5', 'NRCB5']
+INSTRUMENTS_SHORTHAND = {'gui': 'FGS',
+                         'mir': 'MIRI',
+                         'nis': 'NIRISS',
+                         'nrc': 'NIRCam',
+                         'nrs': 'NIRSpec'}
 
 
 def ensure_dir_exists(fullpath):
@@ -100,17 +106,23 @@ def filename_parser(filename):
     """
     filename = os.path.basename(filename)
 
+    file_root_name = (len(filename.split('.')) < 2)
+
+    regex_string_to_compile = r"[a-z]+" \
+                               "(?P<program_id>\d{5})"\
+                               "(?P<observation>\d{3})"\
+                               "(?P<visit>\d{3})"\
+                               "_(?P<visit_group>\d{2})"\
+                               "(?P<parallel_seq_id>\d{1})"\
+                               "(?P<activity>\w{2})"\
+                               "_(?P<exposure_id>\d+)"\
+                               "_(?P<detector>\w+)"
+
+    if not file_root_name:
+        regex_string_to_compile += r"_(?P<suffix>{}).*".format('|'.join(FILE_SUFFIX_TYPES))
+
     elements = \
-        re.compile(r"[a-z]+"
-                   "(?P<program_id>\d{5})"
-                   "(?P<observation>\d{3})"
-                   "(?P<visit>\d{3})"
-                   "_(?P<visit_group>\d{2})"
-                   "(?P<parallel_seq_id>\d{1})"
-                   "(?P<activity>\w{2})"
-                   "_(?P<exposure_id>\d+)"
-                   "_(?P<detector>\w+)"
-                   "_(?P<suffix>\w+).*")
+        re.compile(regex_string_to_compile)
 
     jwst_file = elements.match(filename)
 
diff --git a/jwql/website/apps/jwql/forms.py b/jwql/website/apps/jwql/forms.py
new file mode 100644
index 00000000..dac67493
--- /dev/null
+++ b/jwql/website/apps/jwql/forms.py
@@ -0,0 +1,158 @@
+"""Defines the forms for the ``jwql`` web app.
+
+Django allows for an object-oriented model representation of forms for
+users to provide input through HTTP POST methods. This module defines
+all of the forms that are used across the various webpages used for the
+JWQL application.
+
+Authors
+-------
+
+    - Lauren Chambers
+
+Use
+---
+
+    This module is used within ``views.py`` as such:
+    ::
+        from .forms import FileSearchForm
+        def view_function(request):
+            form = FileSearchForm(request.POST or None)
+
+            if request.method == 'POST':
+                if form.is_valid():
+                    # Process form input and redirect
+                    return redirect(new_url)
+
+            template = 'some_template.html'
+            context = {'form': form, ...}
+            return render(request, template, context)
+
+References
+----------
+    For more information please see:
+        ``https://docs.djangoproject.com/en/2.1/topics/forms/``
+
+Dependencies
+------------
+    The user must have a configuration file named ``config.json``
+    placed in the ``jwql/utils/`` directory.
+"""
+
+import glob
+import os
+
+from django import forms
+from django.shortcuts import redirect
+
+from jwql.utils.utils import get_config, filename_parser, INSTRUMENTS_SHORTHAND
+
+FILESYSTEM_DIR = os.path.join(get_config()['jwql_dir'], 'filesystem')
+
+
+class FileSearchForm(forms.Form):
+    """A form that contains a single field for users to search for a proposal
+    or fileroot in the JWQL filesystem.
+    """
+    # Define search field
+    search = forms.CharField(label='', max_length=500, required=True,
+                             empty_value='Search')
+
+    # Initialize attributes
+    fileroot_dict = None
+    search_type = None
+    instrument = None
+
+    def clean_search(self):
+        """Validate the "search" field by checking to ensure the input
+        is either a proposal or fileroot, and one that matches files
+        in the filesystem.
+
+        Returns
+        -------
+        str
+            The cleaned data input into the "search" field
+        """
+        # Get the cleaned search data
+        search = self.cleaned_data['search']
+
+        # Make sure the search is either a proposal or fileroot
+        if len(search) == 5 and search.isnumeric():
+            self.search_type = 'proposal'
+        elif self._search_is_fileroot(search):
+            self.search_type = 'fileroot'
+        else:
+            raise forms.ValidationError('Invalid search term {}. Please provide proposal number or file root.'.format(search))
+
+        # If they searched for a proposal...
+        if self.search_type == 'proposal':
+            # See if there are any matching proposals and, if so, what
+            # instrument they are for
+            search_string = os.path.join(FILESYSTEM_DIR, 'jw{}'.format(search),
+                                         '*{}*.fits'.format(search))
+            all_files = glob.glob(search_string)
+            if len(all_files) > 0:
+                all_instruments = []
+                for file in all_files:
+                    instrument = filename_parser(file)['detector']
+                    all_instruments.append(instrument[:3])
+                if len(set(all_instruments)) > 1:
+                    raise forms.ValidationError('Cannot return result for proposal with multiple instruments.')
+
+                self.instrument = INSTRUMENTS_SHORTHAND[all_instruments[0]]
+            else:
+                raise forms.ValidationError('Proposal {} not in the filesystem.'.format(search))
+
+        # If they searched for a fileroot...
+        elif self.search_type == 'fileroot':
+            # See if there are any matching fileroots and, if so, what
+            # instrument they are for
+            search_string = os.path.join(FILESYSTEM_DIR, search[:7], '{}*.fits'.format(search))
+            all_files = glob.glob(search_string)
+
+            if len(all_files) == 0:
+                raise forms.ValidationError('Fileroot {} not in the filesystem.'.format(search))
+
+            instrument = search.split('_')[-1][:3]
+            self.instrument = INSTRUMENTS_SHORTHAND[instrument]
+
+        return self.cleaned_data['search']
+
+    def _search_is_fileroot(self, search):
+        """Determine if a search value is formatted like a fileroot.
+
+        Parameters
+        ----------
+        search : str
+            The search term input by the user.
+
+        Returns
+        -------
+        bool
+            Is the search term formatted like a fileroot?
+        """
+        try:
+            self.fileroot_dict = filename_parser(search)
+            return True
+        except ValueError:
+            return False
+
+    def redirect_to_files(self):
+        """Determine where to redirect the web app based on user search input.
+
+        Returns
+        -------
+        HttpResponseRedirect object
+            Outgoing redirect response sent to the webpage
+        """
+
+        # Process the data in form.cleaned_data as required
+        search = self.cleaned_data['search']
+
+        # If they searched for a proposal
+        if self.search_type == 'proposal':
+            return redirect('/jwql/{}/archive/{}'.format(self.instrument, search))
+
+        # If they searched for a file root
+        elif self.search_type == 'fileroot':
+            return redirect('/jwql/{}/{}'.format(self.instrument, search))
diff --git a/jwql/website/apps/jwql/static/css/jwql.css b/jwql/website/apps/jwql/static/css/jwql.css
index b2f109bb..1d7de0d9 100644
--- a/jwql/website/apps/jwql/static/css/jwql.css
+++ b/jwql/website/apps/jwql/static/css/jwql.css
@@ -104,6 +104,23 @@
     display: inline-block;
 }
 
+/*Stop the search box from glowing blue*/
+#homepage_filesearch #id_search {
+    width: 500px;
+    height: 100%;
+    padding: 0px;
+}
+
+#homepage_filesearch #id_search:focus {
+    box-shadow: none;
+    border-color: #cfd4da;
+}
+
+/*Make the form fields be inline*/
+.homepage_form_fieldWrapper {
+    display: inline;
+}
+
 /*Don't let the search bar be super long*/
 .input-group {
     width: 250px;
diff --git a/jwql/website/apps/jwql/templates/home.html b/jwql/website/apps/jwql/templates/home.html
index 73522109..d3e6ba4f 100644
--- a/jwql/website/apps/jwql/templates/home.html
+++ b/jwql/website/apps/jwql/templates/home.html
@@ -4,6 +4,9 @@
 
 	<title>Home - JWQL</title>
 
+    <!-- Custom styles and scripts for this template -->
+	<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous">
+
 {% endblock %}
 
 {% block content %}
@@ -32,18 +35,49 @@
 			</div>
 		</div>
 
-        <div class="row my-2" style="margin-left: 7rem; margin-right: 7rem;">
-            <div class="col-md-6 py-2"><a class="btn btn-block btn-primary" role="button" href={{ url('jwql:dashboard') }}>Dashboard</a></div>
-            <div class="col-md-6 py-2"><a class="btn btn-block btn-primary disabled" role="button" href="#">Query Database</a></div>
-        </div>
-
         <br>
         The JWST Quicklook Application (JWQL) is a database-driven web application and automation framework for use by the JWST instrument teams to monitor the health and stability of the JWST instruments.
         Visit our <a href={{ url('jwql:about') }}>about page</a> to learn more about our project, goals, and developers.<br><br>
 
         The JWQL application is currently under heavy development. The 1.0 release is expected in 2019.
 
+        <hr>
+
+        <h4>Find a JWST Proposal or File</h4>
+
+        Submit a proposal number (e.g. 86600) or file root (e.g. jw86600006001_02101_00008_guider1) to view that proposal or file:
+
+
+
+
+        <!--Load the file search form from the view-->
+        <form action="" method="post" id="homepage_filesearch">
+            <!--Show any errors from a previous form submission-->
+            {% if form.errors %}
+                {% for field in form %}
+                    {% for error in field.errors %}
+                        <div class="alert alert-danger">
+                            <strong>{{ error|escape }}</strong>
+                        </div>
+                    {% endfor %}
+                {% endfor %}
+            {% endif %}
+
+            <!--Django Cross-Site Request Forgery magic-->
+            {{ csrf_input }}
+
+            <!--Show the field forms-->
+            {% for field in form %}
+                <div class="homepage_form_fieldWrapper">
+                    {{ field }}
+                    {% if field.help_text %}
+                        <p class="help">{{ field.help_text|safe }}</p>
+                    {% endif %}
+                </div>
+            {% endfor %}
+            <button class="btn btn-primary" type="submit"><span class="fas fa-search"></span></button>
+        </form>
 
 	</main>
 
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/jwql/website/apps/jwql/views.py b/jwql/website/apps/jwql/views.py
index 52b8ec83..0f4dc375 100644
--- a/jwql/website/apps/jwql/views.py
+++ b/jwql/website/apps/jwql/views.py
@@ -44,6 +44,7 @@ from .data_containers import get_header_info
 from .data_containers import get_image_info
 from .data_containers import get_proposal_info
 from .data_containers import thumbnails
+from .forms import FileSearchForm
 from jwql.utils.utils import get_config, JWST_INSTRUMENTS, MONITORS
 
 
@@ -172,10 +173,20 @@ def home(request):
     HttpResponse object
         Outgoing response sent to the webpage
     """
+
+    # Create a form instance and populate it with data from the request
+    form = FileSearchForm(request.POST or None)
+
+    # If this is a POST request, we need to process the form data
+    if request.method == 'POST':
+        if form.is_valid():
+            return form.redirect_to_files()
+
     template = 'home.html'
     context = {'inst': '',
                'inst_list': JWST_INSTRUMENTS,
-               'tools': MONITORS}
+               'tools': MONITORS,
+               'form': form}
 
     return render(request, template, context)
 


Test patch:
diff --git a/jwql/tests/test_utils.py b/jwql/tests/test_utils.py
index 9a72a5ba..9dc73646 100644
--- a/jwql/tests/test_utils.py
+++ b/jwql/tests/test_utils.py
@@ -49,6 +49,24 @@ def test_filename_parser_filename():
 
     assert filename_dict == correct_dict
 
+def test_filename_parser_filename_root():
+    '''Generate a dictionary with parameters from a JWST filename.
+    Assert that the dictionary matches what is expected.
+    '''
+    filename = 'jw00327001001_02101_00002_nrca1'
+    filename_dict = filename_parser(filename)
+
+    correct_dict = {'activity': '01',
+                    'detector': 'nrca1',
+                    'exposure_id': '00002',
+                    'observation': '001',
+                    'parallel_seq_id': '1',
+                    'program_id': '00327',
+                    'visit': '001',
+                    'visit_group': '02'}
+
+    assert filename_dict == correct_dict
+
 
 def test_filename_parser_filepath():
     '''Generate a dictionary with parameters from a JWST filepath