diff --git a/jwql/utils/constants.py b/jwql/utils/constants.py index e56d33da9..37d390a74 100644 --- a/jwql/utils/constants.py +++ b/jwql/utils/constants.py @@ -366,6 +366,7 @@ # Default Model Values DEFAULT_MODEL_CHARFIELD = "empty" +DEFAULT_MODEL_COMMENT = "" # Filename Component Lengths FILE_AC_CAR_ID_LEN = 4 diff --git a/jwql/website/apps/jwql/data_containers.py b/jwql/website/apps/jwql/data_containers.py index 49e12aaaf..a61a43804 100644 --- a/jwql/website/apps/jwql/data_containers.py +++ b/jwql/website/apps/jwql/data_containers.py @@ -27,44 +27,61 @@ """ import copy -from collections import OrderedDict import glob import json -from operator import getitem +import logging import os import re import tempfile -import logging +from collections import OrderedDict +from datetime import datetime +from operator import getitem, itemgetter +import numpy as np +import pandas as pd +import pyvo as vo +import requests from astropy.io import fits from astropy.time import Time +from astroquery.mast import Mast from bs4 import BeautifulSoup -from django import setup +from django import forms, setup from django.conf import settings from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist from django.db.models.query import QuerySet -import numpy as np -from operator import itemgetter -import pandas as pd -import pyvo as vo -import requests -from datetime import datetime from jwql.database import database_interface as di from jwql.database.database_interface import load_connection from jwql.edb.engineering_database import get_mnemonic, get_mnemonic_info, mnemonic_inventory -from jwql.utils.utils import check_config_for_key, ensure_dir_exists, filesystem_path, filename_parser, get_config -from jwql.utils.constants import MAST_QUERY_LIMIT, MONITORS, THUMBNAIL_LISTFILE, THUMBNAIL_FILTER_LOOK -from jwql.utils.constants import EXPOSURE_PAGE_SUFFIX_ORDER, IGNORED_SUFFIXES, INSTRUMENT_SERVICE_MATCH -from jwql.utils.constants import JWST_INSTRUMENT_NAMES_MIXEDCASE, JWST_INSTRUMENT_NAMES -from jwql.utils.constants import REPORT_KEYS_PER_INSTRUMENT -from jwql.utils.constants import SUFFIXES_TO_ADD_ASSOCIATION, SUFFIXES_WITH_AVERAGED_INTS, QueryConfigKeys -from jwql.utils.constants import ON_GITHUB_ACTIONS, ON_READTHEDOCS +from jwql.utils.constants import ( + DEFAULT_MODEL_COMMENT, + EXPOSURE_PAGE_SUFFIX_ORDER, + IGNORED_SUFFIXES, + INSTRUMENT_SERVICE_MATCH, + JWST_INSTRUMENT_NAMES, + JWST_INSTRUMENT_NAMES_MIXEDCASE, + MAST_QUERY_LIMIT, + MONITORS, + ON_GITHUB_ACTIONS, + ON_READTHEDOCS, + REPORT_KEYS_PER_INSTRUMENT, + SUFFIXES_TO_ADD_ASSOCIATION, + SUFFIXES_WITH_AVERAGED_INTS, + THUMBNAIL_FILTER_LOOK, + THUMBNAIL_LISTFILE, + QueryConfigKeys, +) from jwql.utils.credentials import get_mast_token from jwql.utils.permissions import set_permissions -from jwql.utils.utils import get_rootnames_for_instrument_proposal -from astroquery.mast import Mast +from jwql.utils.utils import ( + check_config_for_key, + ensure_dir_exists, + filename_parser, + filesystem_path, + get_config, + get_rootnames_for_instrument_proposal, +) # Increase the limit on the number of entries that can be returned by # a MAST query. @@ -79,8 +96,16 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jwql.website.jwql_proj.settings") setup() - from .forms import MnemonicSearchForm, MnemonicQueryForm, MnemonicExplorationForm, InstrumentAnomalySubmitForm - from jwql.website.apps.jwql.models import Observation, Proposal, RootFileInfo, Anomalies + from jwql.website.apps.jwql.models import Anomalies, Observation, Proposal, RootFileInfo + + from .forms import ( + InstrumentAnomalySubmitForm, + MnemonicExplorationForm, + MnemonicQueryForm, + MnemonicSearchForm, + RootFileInfoCommentSubmitForm, + RootFileInfoExposureCommentSubmitForm, + ) check_config_for_key('auth_mast') configs = get_config() auth_mast = configs['auth_mast'] @@ -396,11 +421,16 @@ def get_additional_exposure_info(root_file_infos, image_info): filter_value = '/'.join(set([e.filter for e in root_file_infos])) pupil_value = '/'.join(set([e.pupil for e in root_file_infos])) grating_value = '/'.join(set([e.grating for e in root_file_infos])) + exp_comment = root_file_infos.first().exp_comment elif isinstance(root_file_infos, RootFileInfo): root_file_info = root_file_infos filter_value = root_file_info.filter pupil_value = root_file_info.pupil grating_value = root_file_info.grating + exp_comment = root_file_info.exp_comment + + # Print N/A if no exposure comment is used + exp_comment = exp_comment if exp_comment != DEFAULT_MODEL_COMMENT else "N/A" # Initialize dictionary of file info to show at the top of the page, along # with another for info that will be in the collapsible text box. @@ -427,7 +457,8 @@ def get_additional_exposure_info(root_file_infos, image_info): 'TARG_DEC': 'N/A', 'CRDS context': 'N/A', 'PA_V3': 'N/A', - 'EXPSTART': root_file_info.expstart + 'EXPSTART': root_file_info.expstart, + 'EXP_COMMENT': exp_comment } elif isinstance(root_file_infos, RootFileInfo): additional_info = {'READPATT': root_file_info.read_patt, @@ -442,7 +473,8 @@ def get_additional_exposure_info(root_file_infos, image_info): 'DEC_REF': 'N/A', 'CRDS context': 'N/A', 'ROLL_REF': 'N/A', - 'EXPSTART': root_file_info.expstart + 'EXPSTART': root_file_info.expstart, + 'EXP_COMMENT': exp_comment } # Deal with instrument-specific parameters @@ -666,6 +698,74 @@ def get_anomaly_form(request, inst, file_root): return form +def get_comment_form(request, file_root): + """Generate form data for comment form + + Parameters + ---------- + request : HttpRequest object + Incoming request + file_root : str + FITS filename of selected image in filesystem. May be a + file or group root name. + + Returns + ------- + RootFileInfoCommentSubmitForm object + form object to be sent with context to template + """ + + root_file_info = RootFileInfo.objects.get(root_name=file_root) + + if request.method == 'POST': + comment_form = RootFileInfoCommentSubmitForm(request.POST, instance=root_file_info) + if comment_form.is_valid(): + comment_form.save() + else: + messages.error(request, "Failed to update comment form") + else: + comment_form = RootFileInfoCommentSubmitForm(instance=root_file_info) + + return comment_form + + +def get_exp_comment_form(request, file_root): + """Generate form data for exposure comment + This form updates all exposure level comments in each related rootfileimage model. + Each model related to this exposure will have the same exposure_comment associated with it. + When getting the default comment for this form, just use the first of the set. When updating + the comment, update for every rootfileinfo in the query set. + + Parameters + ---------- + request : HttpRequest object + Incoming request + file_root : str + Partial FITS filename substring of exposure root name. + + Returns + ------- + RootFileInfoExposureCommentSubmitForm object + form object to be sent with context to template + """ + + rootfileinfo_set = RootFileInfo.objects.filter(root_name__startswith=file_root) + + if request.method == 'POST': + exp_comment_form = RootFileInfoExposureCommentSubmitForm(request.POST, instance=rootfileinfo_set.first()) + if exp_comment_form.is_valid(): + # Update the for all images in exposure + for rootfileinfo in rootfileinfo_set: + rootfileinfo.exp_comment = exp_comment_form.cleaned_data['exp_comment'] + rootfileinfo.save() + else: + messages.error(request, "Failed to update exposure comment form") + else: + exp_comment_form = RootFileInfoExposureCommentSubmitForm(instance=rootfileinfo_set.first()) + + return exp_comment_form + + def get_group_anomalies(file_root): """Generate form data for context @@ -678,7 +778,7 @@ def get_group_anomalies(file_root): Returns ------- group_anomaly_dict dict - root file name key with string of anomalies + root file name key with string of anomalies followed by anomaly comment """ # Check for group root name rootfileinfo_set = RootFileInfo.objects.filter(root_name__startswith=file_root).order_by("root_name") @@ -687,6 +787,10 @@ def get_group_anomalies(file_root): anomalies_list = get_current_flagged_anomalies([rootfileinfo]) anomalies_string = ', '.join(anomalies_list) group_anomaly_dict[rootfileinfo.root_name] = anomalies_string + if rootfileinfo.comment != DEFAULT_MODEL_COMMENT: + anomalies_string += f" -- Comments: {rootfileinfo.comment}" + group_anomaly_dict[rootfileinfo.root_name] = anomalies_string + return group_anomaly_dict diff --git a/jwql/website/apps/jwql/forms.py b/jwql/website/apps/jwql/forms.py index 90c43cafe..c7a4bdc38 100644 --- a/jwql/website/apps/jwql/forms.py +++ b/jwql/website/apps/jwql/forms.py @@ -44,29 +44,42 @@ def view_function(request): placed in the ``jwql`` directory. """ -from collections import defaultdict import datetime import glob -import os import logging +import os +from collections import defaultdict from astropy.time import Time, TimeDelta from django import forms from django.shortcuts import redirect from django.utils.html import format_html from django.utils.safestring import mark_safe -from jwql.edb.engineering_database import is_valid_mnemonic -from jwql.website.apps.jwql.models import Anomalies +from wtforms import StringField, SubmitField - -from jwql.utils.constants import (ANOMALY_CHOICES_PER_INSTRUMENT, ANOMALIES_PER_INSTRUMENT, APERTURES_PER_INSTRUMENT, DETECTOR_PER_INSTRUMENT, - EXP_TYPE_PER_INSTRUMENT, FILTERS_PER_INSTRUMENT, GENERIC_SUFFIX_TYPES, GRATING_PER_INSTRUMENT, - GUIDER_FILENAME_TYPE, JWST_INSTRUMENT_NAMES_MIXEDCASE, JWST_INSTRUMENT_NAMES_SHORTHAND, - READPATT_PER_INSTRUMENT, IGNORED_SUFFIXES, SUBARRAYS_PER_INSTRUMENT, PUPILS_PER_INSTRUMENT, - LOOK_OPTIONS, SORT_OPTIONS, PROPOSAL_CATEGORIES) -from jwql.utils.utils import (get_config, get_rootnames_for_instrument_proposal, filename_parser, query_format) - -from wtforms import SubmitField, StringField +from jwql.edb.engineering_database import is_valid_mnemonic +from jwql.utils.constants import ( + ANOMALIES_PER_INSTRUMENT, + ANOMALY_CHOICES_PER_INSTRUMENT, + APERTURES_PER_INSTRUMENT, + DETECTOR_PER_INSTRUMENT, + EXP_TYPE_PER_INSTRUMENT, + FILTERS_PER_INSTRUMENT, + GENERIC_SUFFIX_TYPES, + GRATING_PER_INSTRUMENT, + GUIDER_FILENAME_TYPE, + IGNORED_SUFFIXES, + JWST_INSTRUMENT_NAMES_MIXEDCASE, + JWST_INSTRUMENT_NAMES_SHORTHAND, + LOOK_OPTIONS, + PROPOSAL_CATEGORIES, + PUPILS_PER_INSTRUMENT, + READPATT_PER_INSTRUMENT, + SORT_OPTIONS, + SUBARRAYS_PER_INSTRUMENT, +) +from jwql.utils.utils import filename_parser, get_config, get_rootnames_for_instrument_proposal, query_format +from jwql.website.apps.jwql.models import Anomalies, RootFileInfo class BaseForm(forms.Form): @@ -79,6 +92,26 @@ class BaseForm(forms.Form): resolve_submit = SubmitField('Resolve Target') +class RootFileInfoCommentSubmitForm(forms.ModelForm): + """Creates a ``Comment Form`` object that allows for text input in a form field. + This uses forms.ModelForm which is good for simplifying direct access to + Django Model database information + """ + class Meta: + model = RootFileInfo + fields = ['comment'] + + +class RootFileInfoExposureCommentSubmitForm(forms.ModelForm): + """Creates a ``Comment Form`` object that allows for text input in a form field. + This uses forms.ModelForm which is good for simplifying direct access to + Django Model database information + """ + class Meta: + model = RootFileInfo + fields = ['exp_comment'] + + class JwqlQueryForm(BaseForm): """Form validation for the JWQL Query viewing tool""" diff --git a/jwql/website/apps/jwql/migrations/0025_rootfileinfo_comment_rootfileinfo_exp_comment.py b/jwql/website/apps/jwql/migrations/0025_rootfileinfo_comment_rootfileinfo_exp_comment.py new file mode 100644 index 000000000..4a1192834 --- /dev/null +++ b/jwql/website/apps/jwql/migrations/0025_rootfileinfo_comment_rootfileinfo_exp_comment.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.7 on 2024-07-24 21:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('jwql', '0024_nirspecmsatastats_nirspecwatastats_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='rootfileinfo', + name='comment', + field=models.TextField(blank=True, default='', help_text='Anomaly Comment Field'), + ), + migrations.AddField( + model_name='rootfileinfo', + name='exp_comment', + field=models.TextField(blank=True, default='', help_text='Anomaly Exposure Comment Field'), + ), + ] diff --git a/jwql/website/apps/jwql/models.py b/jwql/website/apps/jwql/models.py index cd770cb18..082d9e620 100644 --- a/jwql/website/apps/jwql/models.py +++ b/jwql/website/apps/jwql/models.py @@ -32,6 +32,7 @@ from jwql.utils.constants import ( DEFAULT_MODEL_CHARFIELD, + DEFAULT_MODEL_COMMENT, MAX_LEN_APERTURE, MAX_LEN_DETECTOR, MAX_LEN_FILTER, @@ -130,6 +131,8 @@ class RootFileInfo(models.Model): pupil = models.CharField(max_length=MAX_LEN_PUPIL, help_text="Pupil", default=DEFAULT_MODEL_CHARFIELD, null=True, blank=True) exp_type = models.CharField(max_length=MAX_LEN_TYPE, help_text="Exposure Type", default=DEFAULT_MODEL_CHARFIELD, null=True, blank=True) expstart = models.FloatField(help_text='Exposure Start Time', default=0.0) + comment = models.TextField(help_text="Anomaly Comment Field", default=DEFAULT_MODEL_COMMENT, null=False, blank=True) + exp_comment = models.TextField(help_text="Anomaly Comment Field", default=DEFAULT_MODEL_COMMENT, null=False, blank=True) # Metadata class Meta: diff --git a/jwql/website/apps/jwql/static/css/jwql.css b/jwql/website/apps/jwql/static/css/jwql.css index c11e6c81e..b8e7b5c89 100644 --- a/jwql/website/apps/jwql/static/css/jwql.css +++ b/jwql/website/apps/jwql/static/css/jwql.css @@ -19,6 +19,12 @@ background-color: #f2f2f2; } +.anomaly_form button { + display: block; /* Makes the button a block element so it takes a new line */ + margin-top: 10px; /* Space between the comment field and the button */ + margin-left: 0; /* Align button to left, full width of parent div */ +} + .anomaly_choice { list-style: none; } diff --git a/jwql/website/apps/jwql/templates/explore_image.html b/jwql/website/apps/jwql/templates/explore_image.html index 62ac88c9b..84ca74bcc 100644 --- a/jwql/website/apps/jwql/templates/explore_image.html +++ b/jwql/website/apps/jwql/templates/explore_image.html @@ -102,9 +102,9 @@