Repository: spacetelescope/jwql

Issue:
Smart search for program numbers (without zeros)
As @Johannes-Sahlmann mentioned in #253, it would be preferable if the JWQL search feature didn't enforce trailing zeros. That is, to search for data from program 778, you shouldn't have to type 00778 into the search.

Should be an easy fix, but will require parsing what are currently strings into ints.


Discussion:
Perhaps a separate issue, but we should also be sure to enable a user to enter a non-zero-padded URL (e.g. `https://dljwql.stsci.edu/FGS/archive/315/` instead of ``https://dljwql.stsci.edu/FGS/archive/00315/``) and have everything work.

Resolving patch:
diff --git a/jwql/website/apps/jwql/data_containers.py b/jwql/website/apps/jwql/data_containers.py
--- a/jwql/website/apps/jwql/data_containers.py
+++ b/jwql/website/apps/jwql/data_containers.py
@@ -414,7 +414,7 @@ def get_filenames_by_proposal(proposal):
     Parameters
     ----------
     proposal : str
-        The five-digit proposal number (e.g. ``88600``).
+        The one- to five-digit proposal number (e.g. ``88600``).
 
     Returns
     -------
@@ -422,8 +422,9 @@ def get_filenames_by_proposal(proposal):
         A list of filenames associated with the given ``proposal``.
     """
 
+    proposal_string = '{:05d}'.format(int(proposal))
     filenames = sorted(glob.glob(os.path.join(
-        FILESYSTEM_DIR, 'jw{}'.format(proposal), '*')))
+        FILESYSTEM_DIR, 'jw{}'.format(proposal_string), '*')))
     filenames = [os.path.basename(filename) for filename in filenames]
 
     return filenames
@@ -612,7 +613,7 @@ def get_preview_images_by_proposal(proposal):
     Parameters
     ----------
     proposal : str
-        The five-digit proposal number (e.g. ``88600``).
+        The one- to five-digit proposal number (e.g. ``88600``).
 
     Returns
     -------
@@ -621,7 +622,8 @@ def get_preview_images_by_proposal(proposal):
         given ``proposal``.
     """
 
-    preview_images = glob.glob(os.path.join(PREVIEW_IMAGE_FILESYSTEM, 'jw{}'.format(proposal), '*'))
+    proposal_string = '{:05d}'.format(int(proposal))
+    preview_images = glob.glob(os.path.join(PREVIEW_IMAGE_FILESYSTEM, 'jw{}'.format(proposal_string), '*'))
     preview_images = [os.path.basename(preview_image) for preview_image in preview_images]
 
     return preview_images
@@ -743,7 +745,7 @@ def get_thumbnails_by_proposal(proposal):
     Parameters
     ----------
     proposal : str
-        The five-digit proposal number (e.g. ``88600``).
+        The one- to five-digit proposal number (e.g. ``88600``).
 
     Returns
     -------
@@ -752,7 +754,8 @@ def get_thumbnails_by_proposal(proposal):
         ``proposal``.
     """
 
-    thumbnails = glob.glob(os.path.join(THUMBNAIL_FILESYSTEM, 'jw{}'.format(proposal), '*'))
+    proposal_string = '{:05d}'.format(int(proposal))
+    thumbnails = glob.glob(os.path.join(THUMBNAIL_FILESYSTEM, 'jw{}'.format(proposal_string), '*'))
     thumbnails = [os.path.basename(thumbnail) for thumbnail in thumbnails]
 
     return thumbnails
@@ -851,7 +854,8 @@ def thumbnails(inst, proposal=None):
     # If the proposal is specified (i.e. if the page being loaded is
     # an archive page), only collect data for given proposal
     if proposal is not None:
-        full_ids = [f for f in full_ids if f[2:7] == proposal]
+        proposal_string = '{:05d}'.format(int(proposal))
+        full_ids = [f for f in full_ids if f[2:7] == proposal_string]
 
     detectors = []
     proposals = []
@@ -917,7 +921,8 @@ def thumbnails_ajax(inst, proposal=None):
     # If the proposal is specified (i.e. if the page being loaded is
     # an archive page), only collect data for given proposal
     if proposal is not None:
-        rootnames = [rootname for rootname in rootnames if rootname[2:7] == proposal]
+        proposal_string = '{:05d}'.format(int(proposal))
+        rootnames = [rootname for rootname in rootnames if rootname[2:7] == proposal_string]
 
     # Initialize dictionary that will contain all needed data
     data_dict = {}
@@ -932,14 +937,14 @@ def thumbnails_ajax(inst, proposal=None):
             filename_dict = filename_parser(rootname)
         except ValueError:
             # Temporary workaround for noncompliant files in filesystem
-            filename_dict = {'activity': file_id[17:19],
-                             'detector': file_id[26:],
-                             'exposure_id': file_id[20:25],
-                             'observation': file_id[7:10],
-                             'parallel_seq_id': file_id[16],
-                             'program_id': file_id[2:7],
-                             'visit': file_id[10:13],
-                             'visit_group': file_id[14:16]}
+            filename_dict = {'activity': rootname[17:19],
+                             'detector': rootname[26:],
+                             'exposure_id': rootname[20:25],
+                             'observation': rootname[7:10],
+                             'parallel_seq_id': rootname[16],
+                             'program_id': rootname[2:7],
+                             'visit': rootname[10:13],
+                             'visit_group': rootname[14:16]}
 
         # Get list of available filenames
         available_files = get_filenames_by_rootname(rootname)
diff --git a/jwql/website/apps/jwql/forms.py b/jwql/website/apps/jwql/forms.py
--- a/jwql/website/apps/jwql/forms.py
+++ b/jwql/website/apps/jwql/forms.py
@@ -82,7 +82,7 @@ def clean_search(self):
         search = self.cleaned_data['search']
 
         # Make sure the search is either a proposal or fileroot
-        if len(search) == 5 and search.isnumeric():
+        if search.isnumeric() and 1 < int(search) < 99999:
             self.search_type = 'proposal'
         elif self._search_is_fileroot(search):
             self.search_type = 'fileroot'
@@ -94,8 +94,9 @@ def clean_search(self):
         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))
+            proposal_string = '{:05d}'.format(int(search))
+            search_string = os.path.join(FILESYSTEM_DIR, 'jw{}'.format(proposal_string),
+                                         '*{}*.fits'.format(proposal_string))
             all_files = glob.glob(search_string)
             if len(all_files) > 0:
                 all_instruments = []
@@ -104,7 +105,7 @@ def clean_search(self):
                     all_instruments.append(instrument)
                 if len(set(all_instruments)) > 1:
                     raise forms.ValidationError('Cannot return result for proposal with multiple '
-                                                'instruments.')
+                                                'instruments ({}).'.format(', '.join(set(all_instruments))))
 
                 self.instrument = all_instruments[0]
             else:
@@ -156,10 +157,11 @@ def redirect_to_files(self):
         """
         # Process the data in form.cleaned_data as required
         search = self.cleaned_data['search']
+        proposal_string = '{:05d}'.format(int(search))
 
         # If they searched for a proposal
         if self.search_type == 'proposal':
-            return redirect('/{}/archive/{}'.format(self.instrument, search))
+            return redirect('/{}/archive/{}'.format(self.instrument, proposal_string))
 
         # If they searched for a file root
         elif self.search_type == 'fileroot':
diff --git a/jwql/website/apps/jwql/urls.py b/jwql/website/apps/jwql/urls.py
--- a/jwql/website/apps/jwql/urls.py
+++ b/jwql/website/apps/jwql/urls.py
@@ -75,20 +75,20 @@
     re_path(r'^(?P<inst>({}))/unlooked/$'.format(instruments), views.unlooked_images, name='unlooked'),
     re_path(r'^(?P<inst>({}))/(?P<file_root>[\w]+)/$'.format(instruments), views.view_image, name='view_image'),
     re_path(r'^(?P<inst>({}))/(?P<file>.+)/hdr/$'.format(instruments), views.view_header, name='view_header'),
-    re_path(r'^(?P<inst>({}))/archive/(?P<proposal>[\d]{{5}})/$'.format(instruments), views.archive_thumbnails, name='archive_thumb'),
+    re_path(r'^(?P<inst>({}))/archive/(?P<proposal>[\d]{{1,5}})/$'.format(instruments), views.archive_thumbnails, name='archive_thumb'),
 
     # AJAX views
     re_path(r'^ajax/(?P<inst>({}))/archive/$'.format(instruments), views.archived_proposals_ajax, name='archive_ajax'),
-    re_path(r'^ajax/(?P<inst>({}))/archive/(?P<proposal>[\d]{{5}})/$'.format(instruments), views.archive_thumbnails_ajax, name='archive_thumb_ajax'),
+    re_path(r'^ajax/(?P<inst>({}))/archive/(?P<proposal>[\d]{{1,5}})/$'.format(instruments), views.archive_thumbnails_ajax, name='archive_thumb_ajax'),
 
     # REST API views
     path('api/proposals/', api_views.all_proposals, name='all_proposals'),
     re_path(r'^api/(?P<inst>({}))/proposals/$'.format(instruments), api_views.instrument_proposals, name='instrument_proposals'),
     re_path(r'^api/(?P<inst>({}))/preview_images/$'.format(instruments), api_views.preview_images_by_instrument, name='preview_images_by_instrument'),
     re_path(r'^api/(?P<inst>({}))/thumbnails/$'.format(instruments), api_views.thumbnails_by_instrument, name='thumbnails_by_instrument'),
-    re_path(r'^api/(?P<proposal>[\d]{5})/filenames/$', api_views.filenames_by_proposal, name='filenames_by_proposal'),
-    re_path(r'^api/(?P<proposal>[\d]{5})/preview_images/$', api_views.preview_images_by_proposal, name='preview_images_by_proposal'),
-    re_path(r'^api/(?P<proposal>[\d]{5})/thumbnails/$', api_views.thumbnails_by_proposal, name='preview_images_by_proposal'),
+    re_path(r'^api/(?P<proposal>[\d]{1,5})/filenames/$', api_views.filenames_by_proposal, name='filenames_by_proposal'),
+    re_path(r'^api/(?P<proposal>[\d]{1,5})/preview_images/$', api_views.preview_images_by_proposal, name='preview_images_by_proposal'),
+    re_path(r'^api/(?P<proposal>[\d]{1,5})/thumbnails/$', api_views.thumbnails_by_proposal, name='preview_images_by_proposal'),
     re_path(r'^api/(?P<rootname>[\w]+)/filenames/$', api_views.filenames_by_rootname, name='filenames_by_rootname'),
     re_path(r'^api/(?P<rootname>[\w]+)/preview_images/$', api_views.preview_images_by_rootname, name='preview_images_by_rootname'),
     re_path(r'^api/(?P<rootname>[\w]+)/thumbnails/$', api_views.thumbnails_by_rootname, name='thumbnails_by_rootname'),


Test patch:
diff --git a/jwql/tests/test_api_views.py b/jwql/tests/test_api_views.py
--- a/jwql/tests/test_api_views.py
+++ b/jwql/tests/test_api_views.py
@@ -52,6 +52,7 @@
              '98012',  # MIRI
              '93025',  # NIRCam
              '00308',  # NIRISS
+             '308',  # NIRISS
              '96213']  # NIRSpec
 for proposal in proposals:
     urls.append('api/{}/filenames/'.format(proposal))  # filenames_by_proposal