diff --git a/back/.env b/back/.env index 3b79ce5..fbac74c 100644 --- a/back/.env +++ b/back/.env @@ -1,4 +1,4 @@ -EMAIL_HOST_USER=accountemail@youremail.com -EMAIL_HOST_PASSWORD=yourpassword -SUBMIT_REPORT_DESTINATION_EMAIL=administratoremail@yourmail.com -SUBMIT_REPORT_FROM_EMAIL=from-address@yourmail.com +EMAIL_HOST_USER=accountemail@yourmail.com +EMAIL_HOST_PASSWORD=accountpasswordhere +SUBMIT_REPORT_DESTINATION_EMAIL=to-address@yourmail.com +SUBMIT_REPORT_FROM_EMAIL=from-address@yourmail.com \ No newline at end of file diff --git a/back/backend/policy.py b/back/backend/policy.py index 41f8b73..9d37806 100644 --- a/back/backend/policy.py +++ b/back/backend/policy.py @@ -1,8 +1,12 @@ -from datetime import date +import datetime +import datetime as mydate #### Classes for policy, sections. Do not edit these. ##################################################### +def to_date(iso8601): + return mydate.datetime.strptime(iso8601, "%Y-%m-%d") + class Policy(): """ Represents the policy for the company/organization. @@ -97,6 +101,7 @@ planning_section = Section( "preferred_flight_fare": {"number": 6, "label": "Fare of your preferred flight", "field_type": "decimal"}, "preferred_flight_duration": {"number": 7, "label": "Flight duration of your preferred flight (hours)", "field_type": "decimal"}, "international_flight": {"number": 8, "label": "Is this an international flight?", "field_type": "boolean"}, + "economy_class": {"number": 9, "label": "Is your ticket in economy/coach?", "field_type": "boolean"}, } ) @@ -127,21 +132,27 @@ def lowest_fare_rule(report, fields): maximum = lowest_fare + 350 else: maximum = lowest_fare + 600 - if fields['preferred_fare'] > maximum: + if fields['preferred_flight_fare'] > maximum: return "For the lowest fare you have provided, your maximum in-policy fare amount is {} USD.".format(maximum) return None planning_section.add_rule(title="Lowest fare check", rule=lowest_fare_rule) def departure_date_limit_rule(report, fields): - days_to_departure = date(fields['departure_date']) - date(fields['screenshot_date']) - if days_to_departure < 14: + days_to_departure = to_date(fields['departure_date']) - to_date(fields['screenshot_date']) + if days_to_departure < datetime.timedelta(days=14): return "Flights must be booked at least 14 days in advance." - if days_to_departure > 365: + if days_to_departure > datetime.timedelta(days=365): return "Flights must be booked no more than 365 days in advance." return None planning_section.add_rule(title="Departure date limit", rule=departure_date_limit_rule) + +def economy_class_rule(report, fields): + if not fields['economy_class']: + return "Only economy or coach class tickets are within policy." + +planning_section.add_rule(title="Economy class check", rule=economy_class_rule) pol.add_section(planning_section) #### Flight Info @@ -173,9 +184,9 @@ def actual_fare_limit_rule(report, fields): flight_section.add_rule(title="Fare limits", rule=actual_fare_limit_rule) def request_date_rule(report, fields): - now = date.today() - last_travel = date(fields['return_date']) - if now - last_travel > 90: + now = datetime.datetime.now() + last_travel = to_date(fields['return_date']) + if now - last_travel > datetime.timedelta(days=90): return "Reimbursement requests must be made within 90 days of the last day of travel." return None @@ -198,9 +209,9 @@ lodging_section = Section( ) def nightly_rate_check(report, fields): - checkin_date = date(fields['check_in_date']) - checkout_date = date(fields['check_out_date']) - duration = checkout_date - checkin_date + check_in_date = to_date(fields['check_in_date']) + check_out_date = to_date(fields['check_out_date']) + duration = (check_out_date - check_in_date).days if fields['cost'] > duration * fields['per_diem_rate']: return "The average nightly rate cannot exceed the U.S. GSA rate." return None diff --git a/back/backend/test.py b/back/backend/test.py deleted file mode 100644 index 6ecdccb..0000000 --- a/back/backend/test.py +++ /dev/null @@ -1,3 +0,0 @@ -from policy import pol - -print(pol) diff --git a/back/backend/test_policy.py b/back/backend/test_policy.py new file mode 100644 index 0000000..c909fb4 --- /dev/null +++ b/back/backend/test_policy.py @@ -0,0 +1,84 @@ +from django.test import TestCase +from .policy import pol +from unittest import mock +import datetime + +class PolicyTests(TestCase): + report = {"key":"value"} + + def test_general_section_pre_post_trip_check(self): + fields = {'after_trip':True} + result = pol.sections[0].rules[0]['rule'](self.report, fields) + self.assertEqual(result, "If you have already take the trip your request will require special approval by the administrator. You may skip the following 'Pre-trip Planning' section.") + + def test_pre_flight_section_fare_limit_domestic(self): + fields = {'preferred_flight_fare':751,'international_flight':False} + result = pol.sections[1].rules[0]['rule'](self.report, fields) + self.assertEqual(result, "Fares for domestic flights over 750 USD require Conservancy pre-approval, even if other policy conditions have been met.") + + def test_pre_flight_section_fare_limit_international(self): + fields = {'preferred_flight_fare':1651,'international_flight':True} + result = pol.sections[1].rules[0]['rule'](self.report, fields) + self.assertEqual(result, "Fares for international flights over 1,650 USD require Conservancy pre-approval, even if other policy conditions have been met.") + + def test_pre_flight_section_lowest_fare_check_less_than_zero(self): + fields = {'lowest_fare_duration':10,'preferred_flight_duration':11,'lowest_fare':100,'preferred_flight_fare':1000} + result = pol.sections[1].rules[1]['rule'](self.report, fields) + self.assertEqual(result, "For the lowest fare you have provided, your maximum in-policy fare amount is 200 USD.") + + def test_pre_flight_section_lowest_fare_check_less_than_three(self): + fields = {'lowest_fare_duration':10,'preferred_flight_duration':8,'lowest_fare':100,'preferred_flight_fare':1000} + result = pol.sections[1].rules[1]['rule'](self.report, fields) + self.assertEqual(result, "For the lowest fare you have provided, your maximum in-policy fare amount is 200 USD.") + + def test_pre_flight_section_lowest_fare_check_less_than_six(self): + fields = {'lowest_fare_duration':10,'preferred_flight_duration':5,'lowest_fare':100,'preferred_flight_fare':1000} + result = pol.sections[1].rules[1]['rule'](self.report, fields) + self.assertEqual(result, "For the lowest fare you have provided, your maximum in-policy fare amount is 300 USD.") + + def test_pre_flight_section_lowest_fare_check_less_than_ten(self): + fields = {'lowest_fare_duration':10,'preferred_flight_duration':2,'lowest_fare':100,'preferred_flight_fare':1000} + result = pol.sections[1].rules[1]['rule'](self.report, fields) + self.assertEqual(result, "For the lowest fare you have provided, your maximum in-policy fare amount is 450 USD.") + + def test_pre_flight_section_lowest_fare_check_other(self): + fields = {'lowest_fare_duration':12,'preferred_flight_duration':1,'lowest_fare':100,'preferred_flight_fare':1000} + result = pol.sections[1].rules[1]['rule'](self.report, fields) + self.assertEqual(result, "For the lowest fare you have provided, your maximum in-policy fare amount is 700 USD.") + + def test_pre_flight_section_departure_date_too_late(self): + fields = {'departure_date':'2019-03-13','screenshot_date':'2019-03-01'} + result = pol.sections[1].rules[2]['rule'](self.report, fields) + self.assertEqual(result, "Flights must be booked at least 14 days in advance.") + + @mock.patch('datetime.date') + def test_pre_flight_section_departure_date_too_early(self, mocked_date): + mocked_date.today = mock.Mock(return_value=datetime.date(2019,1,1)) + fields = {'departure_date':'2020-03-10','screenshot_date':'2019-03-01'} + result = pol.sections[1].rules[2]['rule'](self.report, fields) + self.assertEqual(result, "Flights must be booked no more than 365 days in advance.") + + def test_flight_info_section_fare_limit_domestic(self): + fields = {'fare':751,'international_flight':False} + result = pol.sections[2].rules[0]['rule'](self.report, fields) + self.assertEqual(result, "Fares for domestic flights over 750 USD require Conservancy pre-approval, even if other policy conditions have been met.") + + def test_flight_info_section_fare_limit_international(self): + fields = {'fare':1651,'international_flight':True} + result = pol.sections[2].rules[0]['rule'](self.report, fields) + self.assertEqual(result, "Fares for international flights over 1,650 USD require Conservancy pre-approval, even if other policy conditions have been met.") + + def test_flight_info_section_request_date(self): + fields = {'return_date':'2018-01-01'} + result = pol.sections[2].rules[1]['rule'](self.report, fields) + self.assertEqual(result, "Reimbursement requests must be made within 90 days of the last day of travel.") + + def test_hotels_lodging_section_average_nightly_rate(self): + fields = {'check_in_date':'2019-03-01','check_out_date':'2019-03-11','cost':1100,'per_diem_rate':100} + result = pol.sections[3].rules[0]['rule'](self.report, fields) + self.assertEqual(result, "The average nightly rate cannot exceed the U.S. GSA rate.") + + def test_other_expenses_section_per_diem_check(self): + fields = {'rate':100,'full_days':3,'partial_days':2,'cost':451} + result = pol.sections[5].rules[0]['rule'](self.report, fields) + self.assertEqual(result, "You may only request a maximum of 450.0 USD for the rate and trip duration provided.") diff --git a/back/backend/test_report.py b/back/backend/test_report.py new file mode 100644 index 0000000..9ef0845 --- /dev/null +++ b/back/backend/test_report.py @@ -0,0 +1,59 @@ +from django.test import TestCase +from rest_framework.test import APIRequestFactory, force_authenticate +from backend.models import Report +from users.models import CustomUser +from unittest.mock import Mock, patch +from datetime import date +from backend.views import * + +class ReportTests(TestCase): + + def create_test_user(self, email, first, last, password): + user = CustomUser.objects.create_user(username=email, email=email, first_name=first, last_name=last, password=password) + return user + + def mock_report(): + r = Mock() + r.report_pk = 1 + r.title = 'Report Title' + r.date_created = '2019-03-01' + r.date_submitted = '2019-03-01' + r.submitted = False + r.reference_number = '12345' + return r + + def setUp(self): + self.test_user_1 = self.create_test_user('one@one.com', 'One', 'Mr. One', '1password') + self.test_user_1.save() + + def test_create_report_logged_in(self): + factory = APIRequestFactory() + request = factory.post('/api/v1/report', {'title':'Test Report', 'reference':'12345'}) + user = CustomUser.objects.get(email='one@one.com') + force_authenticate(request, user=user) + response = create_report(request) + self.assertEqual(response.status_code, 200) + report = Report.objects.get(user_id=user) + self.assertEqual(report.title, 'Test Report') + + def test_create_report_logged_out(self): + factory = APIRequestFactory() + request = factory.post('/api/v1/report', {'title':'Test Report', 'reference':'12345'}) + response = create_report(request) + self.assertEqual(response.status_code, 401) + + @patch('backend.models.Report.objects.filter', Mock(return_value=[mock_report()])) + @patch('backend.views.get_sections', Mock(return_value={})) + def test_get_report(self): + result = get_report(1) + self.assertEqual( + result, + { + 'date_created':'2019-03-01', + 'reference_number':'12345', + 'report_pk':1, + 'title':'Report Title', + 'date_submitted':'2019-03-01', + 'submitted':False + } + ) diff --git a/back/backend/tests.py b/back/backend/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/back/backend/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/back/backend/urls.py b/back/backend/urls.py index 83f7279..33eb2f6 100644 --- a/back/backend/urls.py +++ b/back/backend/urls.py @@ -5,9 +5,10 @@ from rest_framework.urlpatterns import format_suffix_patterns from . import views urlpatterns = [ - path('report', views.report), + path('report', views.create_report), path('reports', views.reports), path('report/', views.report_detail), + path('report//final', views.finalize_report), path('report//section/', views.section), ] diff --git a/back/backend/views.py b/back/backend/views.py index d31728f..d9997a1 100644 --- a/back/backend/views.py +++ b/back/backend/views.py @@ -3,8 +3,6 @@ from django.http import JsonResponse from .models import * from .policy import pol import os -from rest_framework.exceptions import ParseError -from rest_framework.parsers import FileUploadParser, MultiPartParser from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from decouple import config @@ -99,7 +97,6 @@ def get_fields(s_id): return field_set - def generate_named_fields_for_section(fields): """ Prepares a dictionary of key-value pairs based on the raw fields @@ -115,7 +112,7 @@ def generate_named_fields_for_section(fields): return result @api_view(['POST']) -def report(request): +def create_report(request): """ Generates a new empty report for the current user and returns it in json format. The title of the report should be provided as @@ -205,17 +202,11 @@ def report_detail(request, report_pk): return JsonResponse(data) # PUT: Submits a report to the administrator for review, - # and marks it as "submitted", after which changes may - # not be made. + # but is still allowed to make further changes elif request.method == 'PUT': - rep = Report.objects.get(id=report_pk) - if rep.submitted == True: - return JsonResponse({"message": "Cannot submit a report that has already been submitted."}, status=409) - rep.submitted = True; - rep.save() # Send email - send_report_to_admin(request, report_pk) - return JsonResponse({"message": "Report submitted."}) + send_report_to_admin(request, report_pk, status="REVIEW") + return JsonResponse({"message": "Request for review is submitted."}) # DELETE: Deletes a report from the user's account. elif request.method == 'DELETE': @@ -237,6 +228,26 @@ def report_detail(request, report_pk): r.delete() return JsonResponse({"message": "Deleted report: {0}.".format(title)}) +@api_view(['PUT']) +def finalize_report(request, report_pk): + """ + This function serves as an API endpoint for submitting + the final report. + + :param request: incoming request packet + :param report_pk: report ID + :return: JSON response containing user message + """ + r = Report.objects.get(id=report_pk) + if r.submitted: + return JsonResponse({"message": "Cannot submit a report that has already been submitted."}, status=409) + r.submitted = True + r.save() + # Send email + send_report_to_admin(request, report_pk, status="FINAL") + return JsonResponse({"message": "Final report submitted."}) + + def user_owns_section(user, section): """ Returns true if the specified user is owner of the section. @@ -383,7 +394,7 @@ def section_complete(section_pk): return True return False -def send_report_to_admin(request, report_pk): +def send_report_to_admin(request, report_pk, status): """ Sends an email message to admin with html/txt version of report, along with file attachments. Cc sent to user. @@ -400,7 +411,7 @@ def send_report_to_admin(request, report_pk): message = None if params['reference_number'] == '': message = EmailMultiAlternatives( - "{}".format(params['title']), + "{} ({})".format(params['title'], status), msg_plain, from_email, [to_email], @@ -408,7 +419,7 @@ def send_report_to_admin(request, report_pk): ) else: message = EmailMultiAlternatives( - "[RT - Request Tracker #{}] {}".format(params['reference_number'], params['title']), + "[Reimbursinator #{}] {} ({})".format(params['reference_number'], params['title'], status), msg_plain, from_email, [to_email], diff --git a/back/db.sqlite3 b/back/db.sqlite3 index 06e1e06..8b0f51b 100644 Binary files a/back/db.sqlite3 and b/back/db.sqlite3 differ diff --git a/front/static/js/viewHistory.js b/front/static/js/viewHistory.js index 8b2619a..6752fb4 100644 --- a/front/static/js/viewHistory.js +++ b/front/static/js/viewHistory.js @@ -232,7 +232,7 @@ function createCollapsibleCard(sectionIdStr, sectionTitle, sectionCompleted, rul return card; } -function createCollapsibleCardBody(form, type, sectionIdStr, sectionDescription, sectionCompleted, ruleViolations) { +function createCollapsibleCardBody(form, sectionIdStr, sectionDescription, sectionCompleted, ruleViolations) { // Create wrapper div const collapseDiv = document.createElement("div"); collapseDiv.id = sectionIdStr + "collapse"; @@ -342,13 +342,6 @@ function createReportForm(parsedData, type) { // Traverse the fields of this section let fields = sections[i].fields; for (let j = 0; j < fields.length; j++) { - - /* - console.log("Field label: " + fields[j].label); - console.log("Field type: " + fields[j].field_type); - console.log("Field value: " + fields[j].value); - */ - // Create a form group for each field and add it to the form form.appendChild(createFormGroup(sectionIdStr, fields[j])); } @@ -361,7 +354,7 @@ function createReportForm(parsedData, type) { form.appendChild(saveButton); // Create collapsible card body, append form to it, append card to accordion - let cardBody = createCollapsibleCardBody(form, type, sectionIdStr, + let cardBody = createCollapsibleCardBody(form, sectionIdStr, sections[i].html_description, sections[i].completed, sections[i].rule_violations); let cardFooter = createCardFooter(sections[i].rule_violations); if (cardFooter) { diff --git a/front/static/tests/qunit_tests.html b/front/static/tests/qunit_tests.html deleted file mode 100644 index b751c4a..0000000 --- a/front/static/tests/qunit_tests.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - QUnit Tests - - - - - -
-
- - - - diff --git a/front/tests/qunit_tests.html b/front/tests/qunit_tests.html new file mode 100644 index 0000000..6636874 --- /dev/null +++ b/front/tests/qunit_tests.html @@ -0,0 +1,451 @@ + + + + QUnit Tests + + + + + + +
+
+
+
+ +
+ + + + + + + + + + + + +
+ + +
+ + + + diff --git a/front/tests/testObjects.js b/front/tests/testObjects.js new file mode 100644 index 0000000..d9284d0 --- /dev/null +++ b/front/tests/testObjects.js @@ -0,0 +1,44 @@ +const testReport = {"title": "New Report 1", "report_pk": 2, "date_submitted": "2019-03-04T08:00:00Z", "sections": [{"title": "General Info", "rule_violations": [], "fields": [{"field_type": "boolean", "label": "Have you taken this trip already?", "field_name": "after_trip", "value": false}], "completed": false, "id": 10, "html_description": "

Each section of this report is designed to guide you through the reimbursement process. Please read through each and answer as many questions as you can that apply to you.

Be sure to click 'Save' after completing each section. Your entered data will be saved as you progress. You may also receive feedback from sections regarding policy restrictions and special requirements.

"}, {"title": "Pre-trip Planning", "rule_violations": [], "fields": [{"field_type": "date", "label": "Departure date", "field_name": "departure_date", "value": "None"}, {"field_type": "date", "label": "Return date", "field_name": "return_date", "value": "None"}, {"field_type": "file", "label": "Screenshot of least expensive ticket fare", "field_name": "screenshot", "value": ""}, {"field_type": "date", "label": "Date of screenshot", "field_name": "screenshot_date", "value": "None"}, {"field_type": "decimal", "label": "Lowest fare", "field_name": "lowest_fare", "value": "0.00"}, {"field_type": "decimal", "label": "Flight duration of lowest fare (hours)", "field_name": "lowest_fare_duration", "value": "0.00"}, {"field_type": "decimal", "label": "Fare of your preferred flight", "field_name": "preferred_flight_fare", "value": "0.00"}, {"field_type": "decimal", "label": "Flight duration of your preferred flight (hours)", "field_name": "preferred_flight_duration", "value": "0.00"}, {"field_type": "boolean", "label": "Is this an international flight?", "field_name": "international_flight", "value": false}], "completed": false, "id": 11, "html_description": "

At least 14 days before buying tickets for your trip, take a screenshot of a flight search showing the least expensive fare available for the dates you need to travel. Include fares from multiple airlines if possible. This information will be used to calculate reimbursable fare amounts.

"}, {"title": "Flight Info", "rule_violations": [], "fields": [{"field_type": "date", "label": "Actual departure date", "field_name": "departure_date", "value": "None"}, {"field_type": "date", "label": "Actual return date", "field_name": "return_date", "value": "None"}, {"field_type": "decimal", "label": "Ticket fare", "field_name": "fare", "value": "0.00"}, {"field_type": "file", "label": "Screenshot of confirmation of purchase", "field_name": "confirmation_screenshot", "value": ""}, {"field_type": "boolean", "label": "Was this an international flight?", "field_name": "international_flight", "value": false}], "completed": false, "id": 12, "html_description": "

Enter the details of your flight once you have made your purchase.

"}, {"title": "Hotel / Lodging", "rule_violations": [], "fields": [{"field_type": "decimal", "label": "USGSA Per diem rate", "field_name": "per_diem_rate", "value": "0.00"}, {"field_type": "decimal", "label": "Total cost for lodging", "field_name": "cost", "value": "0.00"}, {"field_type": "date", "label": "Check-in date", "field_name": "check_in_date", "value": "None"}, {"field_type": "date", "label": "Check-out date", "field_name": "check_out_date", "value": "None"}, {"field_type": "file", "label": "Screenshot of invoice", "field_name": "invoice_screenshot", "value": ""}], "completed": false, "id": 13, "html_description": "

Please submit a receipt from your hotel including both the total amount and the dates of your stay. Per diem rates can be found on the U.S. GSA website.

"}, {"title": "Local Transportation", "rule_violations": [], "fields": [{"field_type": "decimal", "label": "Total cost of local transportation", "field_name": "cost", "value": "0.00"}], "completed": false, "id": 14, "html_description": "

This amount includes taxis, uber, and public transportation.

"}, {"title": "Per Diem and Other Expenses", "rule_violations": [], "fields": [{"field_type": "decimal", "label": "Per diem rate", "field_name": "rate", "value": "0.00"}, {"field_type": "integer", "label": "Number of full days of travel", "field_name": "full_days", "value": 0}, {"field_type": "integer", "label": "Number of partial days of travel", "field_name": "partial_days", "value": 0}, {"field_type": "decimal", "label": "Total Cost for meals and incidentals", "field_name": "cost", "value": "0.00"}], "completed": false, "id": 15, "html_description": "

Your per diem allowance is used to cover meals and incidental expenses. The rate for your travel destination can be found on the following websites:

You may request up to 100% of the listed rate for a full day of travel, or 75% for a partial day of travel."}, {"title": "Payment Option - Paypal", "rule_violations": [], "fields": [{"field_type": "string", "label": "Email address used with Paypal", "field_name": "paypal_email", "value": ""}, {"field_type": "string", "label": "Preferred currency", "field_name": "preferred_currency", "value": ""}], "completed": false, "id": 16, "html_description": "

Complete this section if you wish to be reimbursed via Paypal. This is the preferred reimbursement method of Software Freedom Conservancy.

"}, {"title": "Payment Option - Check", "rule_violations": [], "fields": [{"field_type": "string", "label": "Street address", "field_name": "address_1", "value": ""}, {"field_type": "string", "label": "Street address 2", "field_name": "address_2", "value": ""}, {"field_type": "string", "label": "City", "field_name": "city", "value": ""}, {"field_type": "string", "label": "State", "field_name": "state", "value": ""}, {"field_type": "string", "label": "Zip code", "field_name": "zip", "value": ""}], "completed": false, "id": 17, "html_description": "

Complete this section if you wish to be reimbursed in USD via check sent by mail.

"}, {"title": "Payment Option - Bank Wire", "rule_violations": [], "fields": [{"field_type": "string", "label": "Full name of account holder", "field_name": "name", "value": ""}, {"field_type": "string", "label": "Street address", "field_name": "address_1", "value": ""}, {"field_type": "string", "label": "Street address 2", "field_name": "address_2", "value": ""}, {"field_type": "string", "label": "City", "field_name": "city", "value": ""}, {"field_type": "string", "label": "State", "field_name": "state", "value": ""}, {"field_type": "string", "label": "Zip code", "field_name": "zip", "value": ""}, {"field_type": "string", "label": "Account number", "field_name": "account", "value": ""}, {"field_type": "string", "label": "Preferred currency", "field_name": "currency", "value": ""}, {"field_type": "string", "label": "Bank name", "field_name": "bank_name", "value": ""}, {"field_type": "string", "label": "Bank address", "field_name": "bank_address", "value": ""}, {"field_type": "string", "label": "Bank ACH/ABA routing number (US) or SWIFT/BIC code (non-US)", "field_name": "routing_number", "value": ""}, {"field_type": "string", "label": "Additional information (see SFC policy)", "field_name": "additional_info", "value": ""}], "completed": false, "id": 18, "html_description": "

Complete this section if you wish to be wired the amount to your bank in your local currency. Please fill in as much of the following information as is possible. Please refer to the SFC travel policy for additional bank information required for certain countries.

"}], "submitted": false, "reference_number": "1234", "date_created": "2019-03-04T08:00:00Z"}; + +const typeNewExpectedHTML = ``; + +const typeEditExpectedHTML = ``; + +const displayReportsOneReportExpected = `
TitleDate CreatedDate SubmittedAction
TEST13/5/2019TBD
`; + +const displayReportsTwoReportsExpected = `
TitleDate CreatedDate SubmittedAction
TEST13/5/2019TBD
TEST23/5/2019TBD
`; + +const displayReportsOneViewableExpected = `
TitleDate CreatedDate SubmittedAction
TEST23/5/20193/5/2019
`;