diff --git a/doc/release/RELEASE-NOTES.md b/doc/release/RELEASE-NOTES.md index 3cc4d72c7..dd9909039 100644 --- a/doc/release/RELEASE-NOTES.md +++ b/doc/release/RELEASE-NOTES.md @@ -11,10 +11,13 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html ### Database changes #### Migrations: -* *Describe migrations here.* +* 0146_add_facility_workers_count_new_field_to_facilityclaim - adds the facility_workers_count_new field to the FacilityClaim model. +* 0147_copy_facility_workers_count_to_facility_workers_count_new - copies the data from the facility_workers_count field to the facility_workers_count_new field. +* 0148_remove_facility_workers_count_field_from_facilityclaim - removes the facility_workers_count field from the FacilityClaim model. +* 0149_rename_facility_workers_count_new_to_facility_workers_count - renames the facility_workers_count_new field to facility_workers_count. #### Scheme changes -* *Describe scheme changes here.* +* [OSDEV-1084](https://opensupplyhub.atlassian.net/browse/OSDEV-1084) - To enable adding a range for the number of workers during the claiming process, the type of the `facility_workers_count` field in the `FacilityClaim` table was changed from `IntegerField` to `CharField`. ### Code/API changes * *Describe code/API changes here.* @@ -43,9 +46,12 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html * [OSDEV-939](https://opensupplyhub.atlassian.net/browse/OSDEV-939) - The following changes have been made: * Created new steps `Supporting Documentation` & `Additional Data` for `Facility Claim Request` page. * Added popup for successfully submitted claim. +* [OSDEV-1084](https://opensupplyhub.atlassian.net/browse/OSDEV-1084) - Enable adding a range for the number of workers during the claiming process, either after pressing the “I want to claim this production location” link or on the Claimed Facility Details page. ### Release instructions: * Update code. +* Apply DB migrations up to the latest one. +* Run the index_facilities_new management command. ## Release 1.13.0 diff --git a/src/django/api/helpers/helpers.py b/src/django/api/helpers/helpers.py index 34efcee27..5d959e5ed 100644 --- a/src/django/api/helpers/helpers.py +++ b/src/django/api/helpers/helpers.py @@ -168,3 +168,15 @@ def parse_download_date(date: str): .strptime(date, parse_dateformat) .strftime("%Y-%m-%d") ) + + +def validate_workers_count(workers_count): + single_number_pattern = r'^\d+$' + range_pattern = r'^\d+-\d+$' + + if re.match(single_number_pattern, workers_count): + return True + elif re.match(range_pattern, workers_count): + return True + else: + return False diff --git a/src/django/api/migrations/0146_add_facility_workers_count_new_field_to_facilityclaim.py b/src/django/api/migrations/0146_add_facility_workers_count_new_field_to_facilityclaim.py new file mode 100644 index 000000000..b5553da1b --- /dev/null +++ b/src/django/api/migrations/0146_add_facility_workers_count_new_field_to_facilityclaim.py @@ -0,0 +1,37 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + ''' + This migration adds the facility_workers_count_new field + to the FacilityClaim model. + ''' + + dependencies = [ + ('api', '0145_new_functions_for_clean_facilitylistitems_command'), + ] + + operations = [ + migrations.AddField( + model_name='facilityclaim', + name='facility_workers_count_new', + field=models.CharField( + blank=True, + help_text='The editable facility workers count for this claim', + max_length=200, + null=True, + verbose_name='facility workers count', + ), + ), + migrations.AddField( + model_name='historicalfacilityclaim', + name='facility_workers_count_new', + field=models.CharField( + blank=True, + help_text='The editable facility workers count for this claim', + max_length=200, + null=True, + verbose_name='facility workers count', + ), + ), + ] diff --git a/src/django/api/migrations/0147_copy_facility_workers_count_to_facility_workers_count_new.py b/src/django/api/migrations/0147_copy_facility_workers_count_to_facility_workers_count_new.py new file mode 100644 index 000000000..8f63af941 --- /dev/null +++ b/src/django/api/migrations/0147_copy_facility_workers_count_to_facility_workers_count_new.py @@ -0,0 +1,26 @@ +from django.db import migrations + + +def copy_integer_to_char(apps, schema_editor): + FacilityClaim = apps.get_model('api', 'FacilityClaim') + for instance in FacilityClaim.objects.all(): + if instance.facility_workers_count: + instance.facility_workers_count_new = str( + instance.facility_workers_count + ) + instance.save() + + +class Migration(migrations.Migration): + ''' + This migration copies the data from the facility_workers_count field + to the facility_workers_count_new field. + ''' + + dependencies = [ + ('api', '0146_add_facility_workers_count_new_field_to_facilityclaim'), + ] + + operations = [ + migrations.RunPython(copy_integer_to_char), + ] diff --git a/src/django/api/migrations/0148_remove_facility_workers_count_field_from_facilityclaim.py b/src/django/api/migrations/0148_remove_facility_workers_count_field_from_facilityclaim.py new file mode 100644 index 000000000..7e391ecc0 --- /dev/null +++ b/src/django/api/migrations/0148_remove_facility_workers_count_field_from_facilityclaim.py @@ -0,0 +1,26 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + ''' + This migration removes the facility_workers_count field + from the FacilityClaim model. + ''' + + dependencies = [ + ( + 'api', + '0147_copy_facility_workers_count_to_facility_workers_count_new', + ), + ] + + operations = [ + migrations.RemoveField( + model_name='facilityclaim', + name='facility_workers_count', + ), + migrations.RemoveField( + model_name='historicalfacilityclaim', + name='facility_workers_count', + ), + ] diff --git a/src/django/api/migrations/0149_rename_facility_workers_count_new_to_facility_workers_count.py b/src/django/api/migrations/0149_rename_facility_workers_count_new_to_facility_workers_count.py new file mode 100644 index 000000000..ed7b87ce8 --- /dev/null +++ b/src/django/api/migrations/0149_rename_facility_workers_count_new_to_facility_workers_count.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + ''' + This migration renames the facility_workers_count_new field + to facility_workers_count. + ''' + + dependencies = [ + ('api', '0148_remove_facility_workers_count_field_from_facilityclaim'), + ] + + operations = [ + migrations.RenameField( + model_name='facilityclaim', + old_name='facility_workers_count_new', + new_name='facility_workers_count', + ), + migrations.RenameField( + model_name='historicalfacilityclaim', + old_name='facility_workers_count_new', + new_name='facility_workers_count', + ), + ] diff --git a/src/django/api/models/facility/facility_claim.py b/src/django/api/models/facility/facility_claim.py index 3b4a4c935..b337997b9 100644 --- a/src/django/api/models/facility/facility_claim.py +++ b/src/django/api/models/facility/facility_claim.py @@ -258,11 +258,12 @@ class FacilityClaim(models.Model): blank=True, verbose_name='average lead time', help_text='The editable facilty avg lead time for this claim.') - facility_workers_count = models.IntegerField( + facility_workers_count = models.CharField( + max_length=200, null=True, blank=True, - help_text='The editable facility workers count for this claim.', - verbose_name='facility workers count') + verbose_name='facility workers count', + help_text='The editable facility workers count for this claim') facility_female_workers_percentage = models.IntegerField( null=True, blank=True, diff --git a/src/django/api/views/facility/facilities_view_set.py b/src/django/api/views/facility/facilities_view_set.py index 93d6bd099..ba5c729f0 100644 --- a/src/django/api/views/facility/facilities_view_set.py +++ b/src/django/api/views/facility/facilities_view_set.py @@ -11,6 +11,7 @@ from contricleaner.lib.exceptions.handler_not_set_error \ import HandlerNotSetError +from api.helpers.helpers import validate_workers_count from oar.settings import ( MAX_ATTACHMENT_SIZE_IN_BYTES, MAX_ATTACHMENT_AMOUNT, @@ -896,10 +897,14 @@ def claim(self, request, pk=None): setattr(facility_claim, 'sector', sectors) try: - workers_count = int(number_of_workers) - except ValueError: - workers_count = None - except TypeError: + workers_count = number_of_workers + + if len(workers_count) == 0: + workers_count = None + elif not validate_workers_count(workers_count): + workers_count = None + + except (ValueError, TypeError): workers_count = None facility_claim.facility_workers_count = workers_count diff --git a/src/django/api/views/facility/facility_claim_view_set.py b/src/django/api/views/facility/facility_claim_view_set.py index 881664e00..ebefa4ccc 100644 --- a/src/django/api/views/facility/facility_claim_view_set.py +++ b/src/django/api/views/facility/facility_claim_view_set.py @@ -1,6 +1,7 @@ import json from api.models.transactions.index_facilities_new import index_facilities_new +from api.helpers.helpers import validate_workers_count from rest_framework.decorators import action from rest_framework.exceptions import ( NotFound, @@ -341,10 +342,14 @@ def get_claimed_details(self, request, pk=None): claim.parent_company_name = parent_company_name try: - workers_count = int(request.data.get('facility_workers_count')) - except ValueError: - workers_count = None - except TypeError: + workers_count = request.data.get('facility_workers_count') + + if len(workers_count) == 0: + workers_count = None + elif not validate_workers_count(workers_count): + workers_count = None + + except (ValueError, TypeError): workers_count = None claim.facility_workers_count = workers_count diff --git a/src/react/src/actions/claimedFacilityDetails.js b/src/react/src/actions/claimedFacilityDetails.js index 7621e7119..1f177ce7a 100644 --- a/src/react/src/actions/claimedFacilityDetails.js +++ b/src/react/src/actions/claimedFacilityDetails.js @@ -91,11 +91,7 @@ export function submitClaimedFacilityDetailsUpdate(claimID) { 'initial_facility_address', ]), { - facility_workers_count: - isInteger(data.facility_workers_count) || - isInt(data.facility_workers_count) - ? data.facility_workers_count - : null, + facility_workers_count: data.facility_workers_count, facility_female_workers_percentage: isInteger(data.facility_female_workers_percentage) || isInt(data.facility_female_workers_percentage) @@ -103,7 +99,6 @@ export function submitClaimedFacilityDetailsUpdate(claimID) { : null, }, ); - return apiRequest .put(makeGetOrUpdateApprovedFacilityClaimURL(claimID), updateData) .then(({ data: responseData }) => diff --git a/src/react/src/components/ClaimFacilityAdditionalData.jsx b/src/react/src/components/ClaimFacilityAdditionalData.jsx index bd9df8832..3f9f30e51 100644 --- a/src/react/src/components/ClaimFacilityAdditionalData.jsx +++ b/src/react/src/components/ClaimFacilityAdditionalData.jsx @@ -13,8 +13,6 @@ import map from 'lodash/map'; import filter from 'lodash/filter'; import includes from 'lodash/includes'; import isNull from 'lodash/isNull'; -import isEmpty from 'lodash/isEmpty'; -import every from 'lodash/every'; import Select from 'react-select'; import Creatable from 'react-select/creatable'; @@ -31,14 +29,11 @@ import { fetchSectorOptions } from '../actions/filterOptions'; import { sectorOptionsPropType } from '../util/propTypes'; -import { getValueFromEvent } from '../util/util'; +import { getValueFromEvent, validateNumberOfWorkers } from '../util/util'; import { claimAFacilitySupportDocsFormStyles } from '../util/styles'; -import { - claimAFacilityAdditionalDataFormFields, - NUMERIC_DASH_REGEX, -} from '../util/constants'; +import { claimAFacilityAdditionalDataFormFields } from '../util/constants'; import COLOURS from '../util/COLOURS'; @@ -289,10 +284,7 @@ function ClaimFacilityAdditionalData({ { - if (isEmpty(data.facility_workers_count)) { - return false; - } - - return !isInt(data.facility_workers_count, { min: 0 }); - }} + hasValidationErrorFn={() => + validateNumberOfWorkers(data.facility_workers_count) + } /> Save diff --git a/src/react/src/components/ClaimedFacilitiesDetailsSidebar.jsx b/src/react/src/components/ClaimedFacilitiesDetailsSidebar.jsx index 863cd8886..40500b870 100644 --- a/src/react/src/components/ClaimedFacilitiesDetailsSidebar.jsx +++ b/src/react/src/components/ClaimedFacilitiesDetailsSidebar.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import { v4 as uuidv4 } from 'uuid'; import Typography from '@material-ui/core/Typography'; import { facilityDetailsPropType } from '../util/propTypes'; @@ -50,17 +51,20 @@ export default function ClaimedFacilitiesDetailsSidebar({ facilityDetails }) { style={claimedFacilitiesDetailsSidebarStyles.bodyTextStyles} > {facilityDetails.properties.contributors.map( - ({ id, name }) => ( -
  • - - {name} - -
  • - ), + ({ id, name }) => { + const uniqueKey = uuidv4(); + return ( +
  • + + {name} + +
  • + ); + }, )} diff --git a/src/react/src/util/constants.jsx b/src/react/src/util/constants.jsx index d84e16532..6f42f6b5e 100644 --- a/src/react/src/util/constants.jsx +++ b/src/react/src/util/constants.jsx @@ -16,8 +16,6 @@ export const REJECT_ACTION = 'reject'; export const InfoLink = 'https://info.opensupplyhub.org'; -export const NUMERIC_DASH_REGEX = /^[0-9-]+$/; - export const InfoPaths = { storiesResources: 'stories-resources', privacyPolicy: 'privacy-policy', diff --git a/src/react/src/util/util.js b/src/react/src/util/util.js index 559e2fa8a..0c09e38b4 100644 --- a/src/react/src/util/util.js +++ b/src/react/src/util/util.js @@ -28,7 +28,7 @@ import every from 'lodash/every'; import uniqWith from 'lodash/uniqWith'; import filter from 'lodash/filter'; import includes from 'lodash/includes'; -import { isURL } from 'validator'; +import { isURL, isInt } from 'validator'; import { featureCollection, bbox } from '@turf/turf'; import hash from 'object-hash'; import * as XLSX from 'xlsx'; @@ -38,7 +38,6 @@ import { OTHER, FEATURE_COLLECTION, CLAIM_A_FACILITY, - NUMERIC_DASH_REGEX, inputTypesEnum, registrationFieldsEnum, registrationFormFields, @@ -893,6 +892,33 @@ export const convertFeatureFlagsObjectToListOfActiveFlags = featureFlags => export const checkWhetherUserHasDashboardAccess = user => get(user, 'is_superuser', false); +export const validateNumberOfWorkers = value => { + if (isEmpty(value)) { + return false; + } + + const singleNumberPattern = /^\d+$/; + const rangePattern = /^\d+-\d+$/; + + if (singleNumberPattern.test(value)) { + return false; + } + + if (rangePattern.test(value)) { + const [start, end] = value.split('-'); + + if ( + isInt(start.trim(), { min: 0 }) && + isInt(end.trim(), { min: 0 }) && + parseInt(start, 10) <= parseInt(end, 10) + ) { + return false; + } + } + + return true; +}; + export const claimAFacilityFormIsValid = ({ yourName, yourTitle, @@ -904,10 +930,7 @@ export const claimAFacilityFormIsValid = ({ }) => every([!isEmpty(yourName), !isEmpty(yourTitle)], identity) && some([isEmpty(yourBusinessWebsite), isURL(yourBusinessWebsite)]) && - some([ - isEmpty(numberOfWorkers), - NUMERIC_DASH_REGEX.test(numberOfWorkers), - ]) && + !validateNumberOfWorkers(numberOfWorkers) && every([ isEmpty(businessWebsite) || (!isEmpty(businessWebsite) && isURL(businessWebsite)),