diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md
index 7f38f3a31..b0450fc3c 100644
--- a/doc/release/RELEASE-NOTES.md
+++ b/doc/release/RELEASE-NOTES.md
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
#### Migrations:
* 0159_alter_status_of_moderation_events_table.py - This migration alters status of api_moderationevent table.
* 0160_allow_null_parsing_errors_in_facilitylist.py - This migration allows empty parsing_errors in api_facilitylist.
+* 0161_create_disable_list_uploading_switch.py - This migration creates disable_list_uploading switch in the Django admin panel and record in the waffle_switch table.
#### Scheme changes
* [OSDEV-1346](https://opensupplyhub.atlassian.net/browse/OSDEV-1346) - Alter status options for api_moderationevent table.
@@ -21,7 +22,8 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
### Code/API changes
* [OSDEV-1346](https://opensupplyhub.atlassian.net/browse/OSDEV-1346) - Create GET request for `v1/moderation-events` endpoint.
-* [OSDEV-1332](https://opensupplyhub.atlassian.net/browse/OSDEV-1332) - Introduced new `PATCH api/v1/moderation-events/{moderation_id}` endpoint
+* [OSDEV-1429](https://opensupplyhub.atlassian.net/browse/OSDEV-1429) - The list upload switcher has been created to disable the `Submit` button on the List Contribute page through the Switch page in the Django admin panel during the release process. Implemented a check on the list upload endpoint.
+* [OSDEV-1332](https://opensupplyhub.atlassian.net/browse/OSDEV-1332) - Introduced new `PATCH api/v1/moderation-events/{moderation_id}` endpoint
to modify moderation event `status`.
* [OSDEV-1347](https://opensupplyhub.atlassian.net/browse/OSDEV-1347) - Create GET request for `v1/moderation-events/{moderation_id}` endpoint.
diff --git a/src/django/api/exceptions.py b/src/django/api/exceptions.py
index 6ad6a89a8..55d9eefe5 100644
--- a/src/django/api/exceptions.py
+++ b/src/django/api/exceptions.py
@@ -1,7 +1,15 @@
from rest_framework.exceptions import APIException
+from rest_framework import status
class BadRequestException(APIException):
- status_code = 400
+ status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Bad request'
default_code = 'bad_request'
+
+
+class ServiceUnavailableException(APIException):
+ status_code = status.HTTP_503_SERVICE_UNAVAILABLE
+ default_detail = 'Service is temporarily unavailable due to maintenance \
+ work. Please try again later.'
+ default_code = 'service_unavailable'
diff --git a/src/django/api/management/commands/enable_switches.py b/src/django/api/management/commands/enable_switches.py
index 8764dd735..e30721e25 100644
--- a/src/django/api/management/commands/enable_switches.py
+++ b/src/django/api/management/commands/enable_switches.py
@@ -12,3 +12,4 @@ def handle(self, *args, **options):
call_command('waffle_switch', 'report_a_facility', 'on')
call_command('waffle_switch', 'embedded_map', 'on')
call_command('waffle_switch', 'extended_profile', 'on')
+ call_command('waffle_switch', 'disable_list_uploading', 'off')
diff --git a/src/django/api/migrations/0161_create_bdisable_list_uploading.py b/src/django/api/migrations/0161_create_bdisable_list_uploading.py
new file mode 100644
index 000000000..dcdabdcf1
--- /dev/null
+++ b/src/django/api/migrations/0161_create_bdisable_list_uploading.py
@@ -0,0 +1,26 @@
+# Generated by Django 3.2.17 on 2024-11-22 09:50
+
+from django.db import migrations
+
+
+def create_disable_list_uploading_switch(apps, schema_editor):
+ Switch = apps.get_model('waffle', 'Switch')
+ Switch.objects.create(name='disable_list_uploading', active=False)
+
+
+def delete_disable_list_uploading_switch(apps, schema_editor):
+ Switch = apps.get_model('waffle', 'Switch')
+ Switch.objects.get(name='disable_list_uploading').delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0160_allow_null_parsing_errors_in_facilitylist'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ create_disable_list_uploading_switch,
+ delete_disable_list_uploading_switch,)
+ ]
diff --git a/src/django/api/tests/test_facility_list_create.py b/src/django/api/tests/test_facility_list_create.py
index c62fbf50a..46084d4f2 100644
--- a/src/django/api/tests/test_facility_list_create.py
+++ b/src/django/api/tests/test_facility_list_create.py
@@ -9,6 +9,7 @@
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.test import APITestCase
+from waffle.testutils import override_switch
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
@@ -245,3 +246,33 @@ def test_list_request_by_user_with_no_contributor_returns_400(self):
Contributor.objects.all().delete()
response = self.client.get(reverse("facility-list-list"))
self.assertEqual(response.status_code, 400)
+
+ @override_switch("disable_list_uploading", active=True)
+ def test_no_upload_when_disable_list_uploading_switch_active(self):
+ previous_list_count = FacilityList.objects.all().count()
+ previous_source_count = Source.objects.all().count()
+ expected = ["Open Supply Hub is undergoing maintenance and not \
+ accepting new data at the moment. Please try again in a \
+ few minutes."]
+
+ response = self.client.post(
+ reverse("facility-list-list"),
+ {"file": self.test_file},
+ format="multipart",
+ )
+
+ self.assertEqual(response.status_code,
+ status.HTTP_503_SERVICE_UNAVAILABLE)
+
+ error_message = json.loads(response.content)['detail']
+ expected_message = " ".join(expected[0].split())
+ self.assertEqual(
+ " ".join(error_message.split()),
+ expected_message
+ )
+ self.assertEqual(
+ FacilityList.objects.all().count(), previous_list_count
+ )
+ self.assertEqual(
+ Source.objects.all().count(), previous_source_count
+ )
diff --git a/src/django/api/views/facility/facility_list_view_set.py b/src/django/api/views/facility/facility_list_view_set.py
index 9120da43d..a29c87735 100644
--- a/src/django/api/views/facility/facility_list_view_set.py
+++ b/src/django/api/views/facility/facility_list_view_set.py
@@ -2,6 +2,7 @@
import os
import logging
from functools import reduce
+from waffle import switch_is_active
from oar.settings import (
MAX_UPLOADED_FILE_SIZE_IN_BYTES,
@@ -27,6 +28,7 @@
FacilityListItemsQueryParams,
ProcessingAction,
)
+from api.exceptions import ServiceUnavailableException
from ...facility_history import create_dissociate_match_change_reason
from ...mail import send_facility_list_rejection_email
from ...models.contributor.contributor import Contributor
@@ -89,6 +91,11 @@ def create(self, request):
"is_public": true
}
"""
+ if switch_is_active('disable_list_uploading'):
+ raise ServiceUnavailableException('Open Supply Hub is undergoing \
+ maintenance and not accepting new \
+ data at the moment. Please try again \
+ in a few minutes.')
if 'file' not in request.data:
raise ValidationError('No file specified.')
uploaded_file = request.data['file']
diff --git a/src/react/src/__tests__/components/SubmitListUploadingButton.test.js b/src/react/src/__tests__/components/SubmitListUploadingButton.test.js
new file mode 100644
index 000000000..02bbcf606
--- /dev/null
+++ b/src/react/src/__tests__/components/SubmitListUploadingButton.test.js
@@ -0,0 +1,258 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { Provider } from "react-redux";
+import configureMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+
+import ContributeList from '../../components/Contribute';
+import { MAINTENANCE_MESSAGE } from '../../util/constants';
+
+jest.mock('@material-ui/core/Popper', () => ({ children }) => children);
+jest.mock('@material-ui/core/Portal', () => ({ children }) => children);
+
+afterEach(() => {
+ jest.resetAllMocks();
+});
+
+const createMockStore = (featureFlags, baseState) => {
+ const middlewares = [thunk];
+ const mockStore = configureMockStore(middlewares);
+
+ return mockStore({
+ ...baseState,
+ featureFlags: {
+ flags: featureFlags,
+ fetching: false,
+ fetchingFeatureFlags: false
+ }
+ });
+};
+
+describe('SubmitListUploadingButton component without DISABLE_LIST_UPLOADING', () => {
+ const features = {
+ disable_list_uploading: false,
+ };
+
+ const user = {
+ id: 57658,
+ email: '',
+ isModerationMode: false,
+ name: '',
+ description: '',
+ website: '',
+ contributorType: '',
+ otherContributorType: '',
+ currentPassword: '',
+ newPassword: '',
+ confirmNewPassword: '',
+ facilityLists: [],
+ };
+ const initialState = {
+ auth: {
+ user: { user },
+ session: { fetching: false },
+ },
+ upload: {
+ form: { name:'', description:'', filename:'', replaces:0 },
+ fetching: false,
+ error: null,
+ },
+ facilityLists: { facilityLists: [
+ {
+ "id": 8573,
+ "name": "Apparel",
+ "description": "Test description",
+ "file_name": "Template_Excel.xlsx",
+ "is_active": false,
+ "is_public": true,
+ "item_count": 0,
+ "items_url": "/api/facility-lists/8573/items/",
+ "statuses": [],
+ "status_counts": {
+ "UPLOADED": 1,
+ "PARSED": 1,
+ "GEOCODED": 1,
+ "GEOCODED_NO_RESULTS": 1,
+ "MATCHED": 1,
+ "POTENTIAL_MATCH": 1,
+ "CONFIRMED_MATCH": 1,
+ "ERROR": 1,
+ "ERROR_PARSING": 1,
+ "ERROR_GEOCODING": 1,
+ "ERROR_MATCHING": 1,
+ "DUPLICATE": 1,
+ "DELETED": 1,
+ "ITEM_REMOVED": 1
+ },
+ "contributor_id": 2371,
+ "created_at": "2024-01-13T10:12:05.895143Z",
+ "match_responsibility": "moderator",
+ "status": "AUTOMATIC",
+ "status_change_reason": "test",
+ "file": "/Template_Excel_KdIAiX9.xlsx",
+ "parsing_errors": []
+ },],
+ fetchingFacilityLists: false,},
+ embeddedMap: { isEmbeded:true },
+ fetching:false,
+ error: null,
+ fetchingFacilityLists:false,
+ };
+ const newPreloadedState = {
+ userHasSignedIn: true,
+ fetchingSessionSignIn: false,
+ };
+ const store = createMockStore(features, initialState);
+
+ const renderComponent = (props = {}) =>
+ render(
+