diff --git a/apcd_cms/src/apps/admin_extension/views.py b/apcd_cms/src/apps/admin_extension/views.py index 06a59457..8e92a9c4 100644 --- a/apcd_cms/src/apps/admin_extension/views.py +++ b/apcd_cms/src/apps/admin_extension/views.py @@ -1,14 +1,11 @@ -from django.http import HttpResponseRedirect, HttpResponse, JsonResponse -from django.core.paginator import Paginator, EmptyPage +from django.http import HttpResponseRedirect, JsonResponse from django.views.generic.base import TemplateView from django.views import View -from django.template import loader -from apps.utils.apcd_database import get_all_extensions, update_extension +from apps.utils.apcd_database import get_all_extensions, update_extension from apps.utils.apcd_groups import is_apcd_admin from apps.utils.utils import table_filter from apps.utils.utils import title_case from apps.components.paginator.paginator import paginator -from dateutil import parser from datetime import date as datetimeDate from datetime import datetime import logging @@ -16,21 +13,18 @@ logger = logging.getLogger(__name__) + class AdminExtensionsTable(TemplateView): template_name = 'list_admin_extension.html' def get(self, request, *args, **kwargs): extension_content = get_all_extensions() - - #context = self.get_context_data(extension_content, *args,**kwargs) - #template = loader.get_template(self.template_name) - #return HttpResponse(template.render(context, request)) context = self.get_extensions_list_json(extension_content, *args, **kwargs) return JsonResponse({'response': context}) def dispatch(self, request, *args, **kwargs): - if not request.user.is_authenticated or not is_apcd_admin(request.user): + if not request.user.is_authenticated or not is_apcd_admin(request.user): return HttpResponseRedirect('/') return super(AdminExtensionsTable, self).dispatch(request, *args, **kwargs) @@ -131,6 +125,7 @@ def _set_extension(self, ext): 'notes': ext[17] if ext[17] else "None", } + # function converts int value in the format YYYYMM to a string with abbreviated month and year def _get_applicable_data_period(value): try: @@ -138,6 +133,7 @@ def _get_applicable_data_period(value): except: return None + class UpdateExtensionsView(View): def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated or not is_apcd_admin(request.user): diff --git a/apcd_cms/src/apps/common_api/urls.py b/apcd_cms/src/apps/common_api/urls.py index 5786e9f4..1ca64d93 100644 --- a/apcd_cms/src/apps/common_api/urls.py +++ b/apcd_cms/src/apps/common_api/urls.py @@ -1,10 +1,10 @@ from django.urls import path -from apps.common_api.views import EntitiesView, cdlsView -from . import views +from apps.common_api.views import EntitiesView, cdlsView, DataPeriodsView app_name = 'common_api' urlpatterns = [ path('entities/', EntitiesView.as_view(), name='entities_api'), path('cdls/', cdlsView.as_view(), name='cdls_api'), - path('cdls/', cdlsView.as_view(), name='cdls_api') + path('cdls/', cdlsView.as_view(), name='cdls_api'), + path('data_periods/', DataPeriodsView.as_view(), name='dataperiods_api') ] diff --git a/apcd_cms/src/apps/common_api/views.py b/apcd_cms/src/apps/common_api/views.py index 943ea6bb..c72bb4d8 100644 --- a/apcd_cms/src/apps/common_api/views.py +++ b/apcd_cms/src/apps/common_api/views.py @@ -1,15 +1,14 @@ -from django.http import HttpResponse, HttpResponseRedirect, JsonResponse -from django.template import loader +from django.http import HttpResponseRedirect, JsonResponse, Http404 from django.views.generic import TemplateView from apps.utils import apcd_database -from apps.utils.apcd_groups import has_apcd_group +from apps.utils.apcd_groups import has_apcd_group, is_apcd_admin from apps.utils.utils import title_case from datetime import datetime import logging -import json logger = logging.getLogger(__name__) + class EntitiesView(TemplateView): def get(self, request, *args, **kwargs): submitters = apcd_database.get_submitter_info(request.user.username) @@ -27,12 +26,6 @@ def dispatch(self, request, *args, **kwargs): def get_submitter_info_json(self, submitters): context = {} - def _get_applicable_data_period(value): - try: - return datetime.strptime(str(value), '%Y%m').strftime('%Y-%m') - except Exception: - return None - def _set_submitter(sub, data_periods): return { "submitter_id": sub[0], @@ -46,23 +39,18 @@ def _set_submitter(sub, data_periods): context["submitters"] = [] for submitter in submitters: - data_periods = [] - submitter_id = submitter[0] - applicable_data_periods = apcd_database.get_applicable_data_periods(submitter[0]) - for data_period_tuple in applicable_data_periods: - for data_period in data_period_tuple: - data_period = _get_applicable_data_period(data_period) - expected_dates = apcd_database.get_current_exp_date(submitter_id=submitter_id, applicable_data_period=data_period.replace('-', '')) - data_periods.append({ - 'data_period': data_period, - 'expected_date': expected_dates[0][0] if expected_dates else '' - }) - data_periods = sorted(data_periods, key=lambda x: x['data_period'], reverse=True) + data_periods = _getApplicableDataPeriods(submitter[0]) context["submitters"].append(_set_submitter(submitter, data_periods)) return context + class cdlsView(TemplateView): + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated or not has_apcd_group(request.user): + return HttpResponseRedirect('/') + return super(cdlsView, self).dispatch(request, *args, **kwargs) + def get(self, request, *args, **kwargs): file_type = kwargs.get('file_type') @@ -81,3 +69,39 @@ def _set_cdls(cdl): cdls_response.append(_set_cdls(cdl)) return JsonResponse({"cdls": cdls_response}) + + +class DataPeriodsView(TemplateView): + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated or not is_apcd_admin(request.user): + return HttpResponseRedirect('/') + return super(DataPeriodsView, self).dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + submitter_id = request.GET.get('submitter_id', None) + if submitter_id is None: + raise Http404("Submitter Id not provided") + applicable_data_periods = _getApplicableDataPeriods(submitter_id) + + return JsonResponse({'response': {"data_periods": applicable_data_periods}}) + + +def _getApplicableDataPeriods(submitter_id): + def _get_applicable_data_period(value): + try: + return datetime.strptime(str(value), '%Y%m').strftime('%Y-%m') + except Exception: + return None + + data_periods = [] + applicable_data_periods = apcd_database.get_applicable_data_periods(submitter_id) + for data_period_tuple in applicable_data_periods: + for data_period in data_period_tuple: + data_period = _get_applicable_data_period(data_period) + expected_dates = apcd_database.get_current_exp_date(submitter_id=submitter_id, applicable_data_period=data_period.replace('-', '')) + data_periods.append({ + 'data_period': data_period, + 'expected_date': expected_dates[0][0] if expected_dates else '' + }) + data_periods = sorted(data_periods, key=lambda x: x['data_period'], reverse=True) + return data_periods diff --git a/apcd_cms/src/apps/utils/apcd_database.py b/apcd_cms/src/apps/utils/apcd_database.py index 66ed5e61..2beab80e 100644 --- a/apcd_cms/src/apps/utils/apcd_database.py +++ b/apcd_cms/src/apps/utils/apcd_database.py @@ -1070,63 +1070,47 @@ def create_extension(form, extension, sub_data): if conn is not None: conn.close() + def update_extension(form): cur = None conn = None try: - conn = psycopg.connect( - host=APCD_DB['host'], - dbname=APCD_DB['database'], - user=APCD_DB['user'], - password=APCD_DB['password'], - port=APCD_DB['port'], - sslmode='require' - ) - cur = conn.cursor() - operation = """UPDATE extensions - SET - updated_at= %s,""" - - set_values = [] - # to set column names for query to the correct DB name - columns = { - 'applicable_data_period': 'applicable_data_period', - 'status': 'status', - 'outcome': 'outcome', - 'approved_expiration_date': 'approved_expiration_date' - } - # To make sure fields are not blank. - # If they aren't, add column to update set operation - for field, column_name in columns.items(): - value = form.get(field) - if value not in (None, ""): - set_values.append(f"{column_name} = %s") - - # to allow notes to be cleared, need to move notes out of the loop that ignores none - operation += ", ".join(set_values) + ", notes = %s WHERE extension_id = %s" - ## add last update time to all extension updates - values = [ - datetime.now(), - ] - - for field, column_name in columns.items(): - value = form.get(field) - print(value) - if value not in (None, ""): - # to make sure applicable data period field is an int to insert to DB - if column_name == 'applicable_data_period': - values.append(int(value.replace('-', ''))) - # else server side clean values - else: - values.append(_clean_value(value)) - - # to allow notes to be cleared, need to move notes out of the loop that ignores none - values.append(_clean_value(form['notes'])) - ## to make sure extension id is last in query to match with WHERE statement - values.append(_clean_value(form['extension_id'])) - - cur.execute(operation, values) - conn.commit() + with db_connect() as conn: + cur = conn.cursor() + query = "UPDATE extensions SET updated_at = %s" + values = [datetime.now()] # Timestamp for updated_at + + # Map form fields to DB columns + columns = { + 'applicable_data_period': 'applicable_data_period', + 'status': 'status', + 'outcome': 'outcome', + 'approved_expiration_date': 'approved_expiration_date' + } + + # Build the SET clause dynamically + set_clauses = [] + for field, column_name in columns.items(): + value = form.get(field) + if value not in (None, "", "None"): + set_clauses.append(f"{column_name} = %s") + if column_name == 'applicable_data_period': + values.append(int(value.replace('-', ''))) + elif column_name == 'approved_expiration_date': + # Convert to None if the value is 'None' (string) + values.append(None if value == 'None' else _clean_value(value)) + else: + values.append(_clean_value(value)) + + # Include 'notes' field, allowing it to be cleared + set_clauses.append("notes = %s") + values.append(_clean_value(form.get('notes', ""))) + + query += ", " + ", ".join(set_clauses) + " WHERE extension_id = %s" + values.append(_clean_value(form['extension_id'])) + + cur.execute(query, values) + conn.commit() except Exception as error: logger.error(error) return error @@ -1134,6 +1118,7 @@ def update_extension(form): if cur is not None: cur.close() + def get_submitter_info(user): cur = None conn = None diff --git a/apcd_cms/src/client/src/components/Extensions/EditExtensionModal/EditExtensionModal.tsx b/apcd_cms/src/client/src/components/Extensions/EditExtensionModal/EditExtensionModal.tsx index bbbd1fa4..82bb37f8 100644 --- a/apcd_cms/src/client/src/components/Extensions/EditExtensionModal/EditExtensionModal.tsx +++ b/apcd_cms/src/client/src/components/Extensions/EditExtensionModal/EditExtensionModal.tsx @@ -21,7 +21,7 @@ import { import { fetchUtil } from 'utils/fetchUtil'; import * as Yup from 'yup'; import { ExtensionRow } from 'hooks/admin'; -import { useEntities } from 'hooks/entities'; +import { useSubmitterDataPeriods } from 'hooks/entities'; import QueryWrapper from 'core-wrappers/QueryWrapper'; import { convertPeriodLabelToApiValue, @@ -60,7 +60,7 @@ const EditExtensionModal: React.FC = ({ const [showSuccessMessage, setShowSuccessMessage] = useState(false); const [showErrorMessage, setShowErrorMessage] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - const [userFields, setUserFields] = useState([ + const [extensionFields, setExtensionFields] = useState([ { label: 'Applicable Data Period', value: extension?.applicable_data_period || 'None', @@ -69,15 +69,15 @@ const EditExtensionModal: React.FC = ({ label: 'Approved Expiration Date', value: extension?.approved_expiration_date || 'None', }, - { label: 'Exception Status', value: extension?.ext_status }, - { label: 'Exception Outcome', value: extension?.ext_outcome }, - { label: 'Exception Notes', value: extension?.notes || 'None' }, + { label: 'Extension Status', value: extension?.ext_status }, + { label: 'Extension Outcome', value: extension?.ext_outcome }, + { label: 'Extension Notes', value: extension?.notes || 'None' }, ]); const { data: submitterData, - isLoading: entitiesLoading, - error: entitiesError, - } = useEntities(); + isLoading: submitterDataLoading, + error: submitterDataError, + } = useSubmitterDataPeriods(extension?.submitter_id); if (!extension) return null; @@ -114,6 +114,7 @@ const EditExtensionModal: React.FC = ({ try { setShowSuccessMessage(false); + setShowErrorMessage(false); const response = await fetchUtil({ url, method: 'PUT', @@ -122,7 +123,7 @@ const EditExtensionModal: React.FC = ({ if (onEditSuccess && response) { onEditSuccess(response); - setUserFields([ + setExtensionFields([ { label: 'Applicable Data Period', value: @@ -133,9 +134,9 @@ const EditExtensionModal: React.FC = ({ label: 'Approved Expiration Date', value: values.approved_expiration_date || 'None', }, - { label: 'Exception Status', value: values.ext_status }, - { label: 'Exception Outcome', value: values.ext_outcome }, - { label: 'Exception Notes', value: values.notes || 'None' }, + { label: 'Extension Status', value: values.ext_status }, + { label: 'Extension Outcome', value: values.ext_outcome }, + { label: 'Extension Notes', value: values.notes || 'None' }, ]); } @@ -188,10 +189,10 @@ const EditExtensionModal: React.FC = ({ Edit Extension ID {extension.ext_id} for {extension.org_name} -

Edit Selected Exception

+

Edit Selected Extension

@@ -212,19 +213,14 @@ const EditExtensionModal: React.FC = ({ onChange={formik.handleChange} onBlur={formik.handleBlur} > - {submitterData?.submitters - .find( - (s) => - s.submitter_id === Number(extension.submitter_id) - ) - ?.data_periods?.map((item) => ( - - ))} + {submitterData?.data_periods?.map((item) => ( + + ))}
Current: {extension.applicable_data_period} @@ -262,7 +258,7 @@ const EditExtensionModal: React.FC = ({ = ({ @@ -347,9 +343,9 @@ const EditExtensionModal: React.FC = ({
-

Current Exception Information

+

Current Extension Information

- {userFields.map((field, index) => ( + {extensionFields.map((field, index) => (

{field.label}:

diff --git a/apcd_cms/src/client/src/hooks/entities/index.ts b/apcd_cms/src/client/src/hooks/entities/index.ts index 6f1c95a9..ece03725 100644 --- a/apcd_cms/src/client/src/hooks/entities/index.ts +++ b/apcd_cms/src/client/src/hooks/entities/index.ts @@ -16,4 +16,8 @@ export type ApplicableDataPeriod = { expected_date: string; }; -export { useEntities } from './useEntities'; +export type SubmitterDataPeriods = { + data_periods: ApplicableDataPeriod[]; +}; + +export { useEntities, useSubmitterDataPeriods } from './useEntities'; diff --git a/apcd_cms/src/client/src/hooks/entities/useEntities.ts b/apcd_cms/src/client/src/hooks/entities/useEntities.ts index 796e56ed..e726c886 100644 --- a/apcd_cms/src/client/src/hooks/entities/useEntities.ts +++ b/apcd_cms/src/client/src/hooks/entities/useEntities.ts @@ -1,6 +1,6 @@ import { useQuery, UseQueryResult } from 'react-query'; import { fetchUtil } from 'utils/fetchUtil'; -import { SubmitterEntityData } from '.'; +import { SubmitterEntityData, SubmitterDataPeriods } from '.'; const getEntities = async () => { const url = `common_api/entities/`; @@ -17,3 +17,24 @@ export const useEntities = (): UseQueryResult => { return { ...query }; }; + +const getSubmitterDataPeriods = async (params: any) => { + const url = `common_api/data_periods/`; + const response = await fetchUtil({ url, params }); + return response.response; +}; + +export const useSubmitterDataPeriods = ( + submitter_id: string | undefined +): UseQueryResult => { + const params: { submitter_id?: string } = { submitter_id }; + const query = useQuery( + ['submitterDataPeriods'], + () => getSubmitterDataPeriods(params), + { + enabled: submitter_id !== undefined && submitter_id !== null, // Allow `0` as valid + } + ) as UseQueryResult; + + return { ...query }; +};