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.


Discussion:
Bonus: Enable the form to accept a list of rootnames instead of just one!

Resolving patch:
diff --git a/jwql/utils/utils.py b/jwql/utils/utils.py
--- a/jwql/utils/utils.py
+++ b/jwql/utils/utils.py
@@ -29,6 +29,7 @@
 
 __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 @@
 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
--- /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/views.py b/jwql/website/apps/jwql/views.py
--- a/jwql/website/apps/jwql/views.py
+++ b/jwql/website/apps/jwql/views.py
@@ -44,6 +44,7 @@
 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
--- 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