diff --git a/backend/audit/cross_validation/test_check_award_ref_declaration.py b/backend/audit/cross_validation/test_check_award_ref_declaration.py index 3ed266948..1bdfb0c61 100644 --- a/backend/audit/cross_validation/test_check_award_ref_declaration.py +++ b/backend/audit/cross_validation/test_check_award_ref_declaration.py @@ -14,13 +14,13 @@ def setUp(self): self.AWARD_MAX = 1500 self.AUDITEE_UEI = "AAA123456BBB" self.award1 = { - "award_reference": f"AWARD-{generate_random_integer(self.AWARD_MIN,self.AWARD_MAX)}" + "award_reference": f"AWARD-{generate_random_integer(self.AWARD_MIN, self.AWARD_MAX)}" } self.award2 = { - "award_reference": f"AWARD-{generate_random_integer(self.AWARD_MIN *2,self.AWARD_MAX *2)}" + "award_reference": f"AWARD-{generate_random_integer(self.AWARD_MIN * 2, self.AWARD_MAX * 2)}" } self.award3 = { - "award_reference": f"AWARD-{generate_random_integer(self.AWARD_MIN *3,self.AWARD_MAX *3)}" + "award_reference": f"AWARD-{generate_random_integer(self.AWARD_MIN * 3, self.AWARD_MAX * 3)}" } self.award_with_longer_ref = {"award_reference": "AWARD-00123"} self.award_with_shorter_ref = {"award_reference": "AWARD-0123"} diff --git a/backend/audit/management/commands/maintenance_mode.py b/backend/audit/management/commands/maintenance_mode.py new file mode 100644 index 000000000..613c46590 --- /dev/null +++ b/backend/audit/management/commands/maintenance_mode.py @@ -0,0 +1,38 @@ +from config import middleware +from django.core.management.base import BaseCommand +import logging + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Django management command for switching "Maintenance Mode". + When switched on, the entire site and requests will feed through + /config/middleware.py. + """ + + def add_arguments(self, parser): + parser.add_argument( + "--on", + action="store_true", + help="Activates maintenance mode, which disables user access.", + ) + parser.add_argument( + "--off", + action="store_true", + help="Deactivates maintenance mode, which re-enables user access.", + ) + + def handle(self, *args, **options): + print( + f"Starting switch... Maintenance mode is currently set to {middleware.is_maintenance_on()}." + ) + if options.get("off"): + middleware.change_maintenance(False) + logger.info("MAINTENANCE_MODE OFF") + elif options.get("on"): + middleware.change_maintenance(True) + logger.info("MAINTENANCE_MODE ON") + else: + print("Invalid syntax. Please enter this command with --on or --off.") diff --git a/backend/audit/views/__init__.py b/backend/audit/views/__init__.py index a5fc42619..30e4814aa 100644 --- a/backend/audit/views/__init__.py +++ b/backend/audit/views/__init__.py @@ -1,5 +1,6 @@ from .audit_info_form_view import AuditInfoFormView from .home import Home +from .home import Maintenance from .manage_submission import ManageSubmissionView from .manage_submission_access import ( ChangeOrAddRoleView, @@ -50,6 +51,7 @@ EditSubmission, ExcelFileHandlerView, Home, + Maintenance, ManageSubmissionView, MySubmissions, no_robots, diff --git a/backend/audit/views/home.py b/backend/audit/views/home.py index cf86c565b..94b029a6f 100644 --- a/backend/audit/views/home.py +++ b/backend/audit/views/home.py @@ -20,3 +20,16 @@ def get(self, request, *args, **kwargs): template_name = "home.html" extra_context = {"DISABLE_AUTH": settings.DISABLE_AUTH} return render(request, template_name, extra_context) + + +class Maintenance(generic.View): + """ + This is the redirected path for Maintenance mode. + + It will return the home template with an error status for every single request + so long as maintenance is enabled. + """ + + def get(self, request, *args, **kwargs): + template_name = "503.html" + return render(request, template_name) diff --git a/backend/census_historical_migration/workbooklib/findings.py b/backend/census_historical_migration/workbooklib/findings.py index eb231cd8d..397f500fd 100644 --- a/backend/census_historical_migration/workbooklib/findings.py +++ b/backend/census_historical_migration/workbooklib/findings.py @@ -153,7 +153,7 @@ def xform_construct_award_references(audits, findings): # Transformation recorded. e2a = {} for index, audit in enumerate(audits): - e2a[audit.ELECAUDITSID] = f"AWARD-{index+1:04d}" + e2a[audit.ELECAUDITSID] = f"AWARD-{index + 1:04d}" award_references = [] change_records = [] for find in findings: diff --git a/backend/config/MAINTENANCE_MODE b/backend/config/MAINTENANCE_MODE new file mode 100644 index 000000000..e69de29bb diff --git a/backend/config/middleware.py b/backend/config/middleware.py new file mode 100644 index 000000000..eb60a1e42 --- /dev/null +++ b/backend/config/middleware.py @@ -0,0 +1,74 @@ +from dissemination.file_downloads import file_exists +from django.conf import settings +from django.shortcuts import redirect +import boto3 + +LOCAL_FILENAME = "./runtime/MAINTENANCE_MODE" +S3_FILENAME = "runtime/MAINTENANCE_MODE" + + +def is_maintenance_on(): + """ + Get current status of maintenance mode. + """ + + return file_exists(S3_FILENAME) + + +def change_maintenance(enabled): + """ + Update status of maintenance mode. + """ + + s3_client = boto3.client( + "s3", + aws_access_key_id=settings.AWS_PRIVATE_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_PRIVATE_SECRET_ACCESS_KEY, + endpoint_url=settings.AWS_S3_ENDPOINT_URL, + ) + + # turn on. + if enabled: + s3_client.put_object( + Bucket=settings.AWS_PRIVATE_STORAGE_BUCKET_NAME, + Body=LOCAL_FILENAME, + Key=S3_FILENAME, + ) + + # turn off. + else: + if file_exists(S3_FILENAME): + s3_client.delete_object( + Bucket=settings.AWS_PRIVATE_STORAGE_BUCKET_NAME, Key=S3_FILENAME + ) + + +class MaintenanceCheck: + """ + Middleware that prevents clients from accessing the + FAC application so long as "MAINTENANCE_MODE" (a file in + the S3 bucket) exists. + """ + + def __init__(self, get_response): + """Initializes the middleware.""" + + self.get_response = get_response + + def __call__(self, request): + """ + Check that maintenance mode is disabled before running request. + """ + + # redirect to maintenance page. + if is_maintenance_on(): + if request.path != "/maintenance": + return redirect("/maintenance") + + else: + # redirect to home page if on maintenance. + if request.path == "/maintenance": + return redirect("/") + + response = self.get_response(request) + return response diff --git a/backend/config/runtime/MAINTENANCE_MODE b/backend/config/runtime/MAINTENANCE_MODE new file mode 100644 index 000000000..e69de29bb diff --git a/backend/config/settings.py b/backend/config/settings.py index 482f98c77..3b9202fa3 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -141,6 +141,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "config.middleware.MaintenanceCheck", ] ROOT_URLCONF = "config.urls" @@ -223,9 +224,9 @@ STATIC_URL = "/static/" - # Environment specific configurations DEBUG = False + if ENVIRONMENT not in ["DEVELOPMENT", "PREVIEW", "STAGING", "PRODUCTION"]: DATABASES = { "default": env.dj_db_url( diff --git a/backend/config/urls.py b/backend/config/urls.py index 3e065f25e..8ff33ef1e 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -82,6 +82,7 @@ # home page & robots.txt path("", auditviews.Home.as_view(), name="Home"), path("robots.txt", auditviews.no_robots, name="no_robots"), + path("maintenance", auditviews.Maintenance.as_view(), name="Maintenance"), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) if settings.ENABLE_DEBUG_TOOLBAR: diff --git a/backend/dissemination/summary_reports.py b/backend/dissemination/summary_reports.py index d41ca16e3..a2d2fb97c 100644 --- a/backend/dissemination/summary_reports.py +++ b/backend/dissemination/summary_reports.py @@ -631,7 +631,7 @@ def generate_summary_report(report_ids, include_private=False): (filename, workbook_bytes, tpw) = prepare_workbook_for_download(workbook) t1 = time.time() logger.info( - f"SUMMARY_REPORTS generate_summary_report\n\ttotal: {t1-t0} ttri: {ttri} tgrdd: {tgrdd} tcw: {tcw} tpw: {tpw}" + f"SUMMARY_REPORTS generate_summary_report\n\ttotal: {t1 - t0} ttri: {ttri} tgrdd: {tgrdd} tcw: {tcw} tpw: {tpw}" ) return (filename, workbook_bytes) diff --git a/backend/schemas/scripts/render.py b/backend/schemas/scripts/render.py index 1f4f7aca6..eed66136b 100644 --- a/backend/schemas/scripts/render.py +++ b/backend/schemas/scripts/render.py @@ -90,7 +90,7 @@ def process_spec(WBNT): password = generate_password() for ndx, sheet in enumerate(WBNT.sheets): print("########################") - print(f"## Processing sheet {ndx+1}") + print(f"## Processing sheet {ndx + 1}") print("########################") ws = create_protected_sheet(wb, sheet, password, ndx) if sheet.hide_col_from is not None: diff --git a/backend/support/management/commands/fac_s3.py b/backend/support/management/commands/fac_s3.py index 9884d5b5f..4cad6b302 100644 --- a/backend/support/management/commands/fac_s3.py +++ b/backend/support/management/commands/fac_s3.py @@ -65,7 +65,7 @@ def handle(self, *args, **options): for file in files: full_path = os.path.join(subdir, file) s3_client.upload_file(full_path, bucket_name, object_name + file) - print(f"Copied {full_path} to {bucket_name} {object_name+file}.") + print(f"Copied {full_path} to {bucket_name} {object_name + file}.") return if options["download"]: diff --git a/backend/templates/503.html b/backend/templates/503.html new file mode 100644 index 000000000..200f8656e --- /dev/null +++ b/backend/templates/503.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block content %} +
+
+

503 Error: Service Unavailable

+
+

The server is temporarily busy. Please try again later.

+
+ {% if exception %}

Error code: {{ exception }}

{% endif %} +
+
+{% endblock content %}