Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions src/django/api/extended_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,25 @@ def get_facility_and_processing_type_extendfield_value(


def get_isic_4_extendedfield_value(field_value):
if field_value is None:
return {'raw_value': []}

if isinstance(field_value, list):
normalized_value = (
field_value[0] if len(field_value) == 1 else field_value
)
entries = field_value
else:
normalized_value = field_value
return {
'raw_value': normalized_value,
}
entries = [field_value]

normalized_entries = []
for entry in entries:
if entry in (None, '', {}):
continue
if not isinstance(entry, dict):
continue
if all_values_empty(entry):
continue
normalized_entries.append(entry)

return {'raw_value': normalized_entries}


def get_parent_company_extendedfield_value(field_value):
Expand Down Expand Up @@ -140,7 +150,10 @@ def create_extendedfield(field, field_value, item, contributor):
'raw_value': field_value,
}
elif field == ExtendedField.ISIC_4:
field_value = get_isic_4_extendedfield_value(field_value)
normalized_isic = get_isic_4_extendedfield_value(field_value)
if not normalized_isic.get('raw_value'):
return
field_value = normalized_isic

ExtendedField.objects.create(
contributor=contributor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.core.management.base import BaseCommand
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction, IntegrityError, DatabaseError
from django.utils import timezone

Expand All @@ -17,20 +17,9 @@ class Command(BaseCommand):

@staticmethod
def _normalize_isic_entries(raw_value):
if isinstance(raw_value, list):
entries = raw_value
else:
entries = [raw_value]

normalized_entries = []
for entry in entries:
if entry in (None, '', {}):
continue
normalized_entry = (
get_isic_4_extendedfield_value(entry)['raw_value']
)
normalized_entries.append(normalized_entry)
return normalized_entries
normalized = get_isic_4_extendedfield_value(raw_value)
entries = normalized.get('raw_value', [])
return entries

@staticmethod
def _build_extended_field(item, normalized_entry):
Expand Down Expand Up @@ -75,6 +64,15 @@ def add_arguments(self, parser):
'print the related facility OS ID'
)
)
parser.add_argument(
'--os-id',
type=str,
default=None,
help=(
'Available only with --singleisic. If provided, backfill only '
'the specified OS ID.'
)
)

def handle(self, *args, **options):
self.stdout.write('Backfilling isic_4 extended fields (ORM)...')
Expand All @@ -84,6 +82,12 @@ def handle(self, *args, **options):
continue_on_error = options['continue_on_error']
contributor_filter = options['contributor_id']
single_only = options['singleisic']
os_id_filter = options['os_id']

if os_id_filter and not single_only:
raise CommandError(
'--os-id can only be used together with --singleisic.'
)

if dry_run:
self.stdout.write(self.style.WARNING(
Expand Down Expand Up @@ -131,7 +135,11 @@ def handle(self, *args, **options):

# If only one record should be backfilled, handle here and exit.
if single_only:
item = items_qs.first()
single_qs = items_qs
if os_id_filter:
single_qs = single_qs.filter(facility__id=os_id_filter)

item = single_qs.first()
if item is None:
self.stdout.write(
'No eligible items found for single backfill.'
Expand All @@ -154,8 +162,7 @@ def handle(self, *args, **options):
return

extended_fields = [
self._build_extended_field(item, normalized_entry)
for normalized_entry in normalized_entries
self._build_extended_field(item, normalized_entries)
]

if dry_run:
Expand Down Expand Up @@ -251,14 +258,13 @@ def flush_batch():
stats['skipped_empty_value'] += 1
continue

for normalized_entry in normalized_entries:
extended_field = self._build_extended_field(
item,
normalized_entry,
)
to_create.append(extended_field)
extended_field = self._build_extended_field(
item,
normalized_entries
)
to_create.append(extended_field)

stats['queued'] += len(normalized_entries)
stats['queued'] += 1

if len(to_create) >= batch_size:
flush_batch()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ class ProductionLocationSchemaSerializer(serializers.Serializer):
required=False,
allow_empty=False,
min_length=1,
max_length=1,
max_length=15,
error_messages={
'min_length': 'Provide exactly one isic_4 object.',
'max_length': 'Provide exactly one isic_4 object.',
'min_length': 'Provide at least one isic_4 object.',
'max_length': 'Provide at most 15 isic_4 objects.',
'invalid': 'Field isic_4 must be a list of objects.',
'empty': 'Field isic_4 cannot be empty.',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ def assert_extended_fields_creation(self, response, status_code):
expected_value = {
'raw_value': self.moderation_event.cleaned_data[
'fields'
]['isic_4'][0]
]['isic_4']
}
self.assertEqual(isic_field.value, expected_value)

Expand Down
9 changes: 2 additions & 7 deletions src/django/api/tests/test_isic4_entry_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def setUp(self):
'country': 'US',
}

def test_multiple_isic_entries_rejected(self):
def test_multiple_isic_entries_allowed(self):
payload = {
**self.base_payload,
'isic_4': [
Expand All @@ -70,9 +70,4 @@ def test_multiple_isic_entries_rejected(self):
}
serializer = ProductionLocationPostSchemaSerializer(data=payload)

self.assertFalse(serializer.is_valid())
self.assertIn('isic_4', serializer.errors)
self.assertIn(
'Provide exactly one isic_4 object.',
serializer.errors['isic_4'],
)
self.assertTrue(serializer.is_valid(), serializer.errors)
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,57 @@ def test_creation_of_extended_fields(self):

self.assert_extended_fields_creation(response, 201)

def test_isic4_multiple_entries_create_multiple_extended_fields(self):
isic_entries = [
{
"class": (
"282 - Manufacture of other special-purpose "
"machinery"
),
"group": (
"282 - Manufacture of other special-purpose "
"machinery"
),
"section": "C - Manufacturing",
"division": (
"28 - Manufacture of machinery and equipment n.e.c."
),
},
{
"class": (
"561 - Restaurants and mobile food service "
"activities"
),
"group": "56 - Food and beverage service activities",
"section": (
"I - Accommodation and food service activities"
),
"division": "56 - Food and beverage service activities",
},
]
self.moderation_event.cleaned_data['fields']['isic_4'] = isic_entries
self.moderation_event.save()

self.login_as_superuser()
response = self.client.post(
self.get_url(),
data=json.dumps({}),
content_type="application/json",
)

self.assertEqual(201, response.status_code)

item = FacilityListItem.objects.get(facility_id=response.data["os_id"])
isic_fields = ExtendedField.objects.filter(
facility_list_item=item.id,
field_name=ExtendedField.ISIC_4,
)

self.assertEqual(isic_fields.count(), 1)

stored_values = isic_fields[0].value.get('raw_value')
self.assertEqual(stored_values, isic_entries)

def test_creation_of_facilitymatch(self):
self.login_as_superuser()
response = self.client.post(
Expand Down
88 changes: 88 additions & 0 deletions src/react/src/components/FacilityDetailsGeneralFields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,94 @@

if (!values.length || !values[0]) return null;

if (fieldName === 'isic_4') {
const groupedContributions = [];
values.forEach(item => {
const targetCount = item?.value_count || 1;
const contributorKey =
item?.contributor_id ??
item?.contributor_name ??
item?.source_by ??
'unknown';
const lastGroup =
groupedContributions[groupedContributions.length - 1];

Check warning on line 121 in src/react/src/components/FacilityDetailsGeneralFields.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.at(…)` over `[….length - index]`.

See more on https://sonarcloud.io/project/issues?id=opensupplyhub_open-supply-hub&issues=AZrAhAVADvggpPC8wjPI&open=AZrAhAVADvggpPC8wjPI&pullRequest=825
if (
lastGroup &&
lastGroup.contributorKey === contributorKey &&
lastGroup.remaining > 0
) {
lastGroup.items.push(item);
lastGroup.remaining -= 1;
} else {
groupedContributions.push({
contributorKey,
remaining: targetCount - 1,
items: [item],
});
}
});

const formattedGroups = groupedContributions
.map(group => {
const formattedEntries = group.items
.map(formatField)
.filter(Boolean);

if (!formattedEntries.length) {
return null;
}

const primary = formattedEntries.reduce(
(acc, value, index) => {
if (index > 0) {
acc.push('');
}
if (Array.isArray(value.primary)) {
return acc.concat(value.primary);
}
acc.push(value.primary);
return acc;
},
[],
);

const groupKeyParts = [
fieldName,
group.contributorKey,
formattedEntries[0]?.secondary,
formattedEntries.length,
]
.filter(Boolean)
.join('-');

return {
...formattedEntries[0],
primary,
key: groupKeyParts,
};
})
.filter(Boolean);

if (!formattedGroups.length) {
return null;
}

const [topGroup, ...restGroups] = formattedGroups;

return (
<Grid item xs={12} md={6} key={fieldName}>
<FacilityDetailsItem
{...topGroup}
label={label}
additionalContent={restGroups}
additionalContentText="entry"
additionalContentTextPlural="entries"
embed={embed}
/>
</Grid>
);
}

const topValue = formatField(values[0]);

return (
Expand Down
31 changes: 23 additions & 8 deletions src/react/src/util/constants.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1131,14 +1131,29 @@ export const EXTENDED_FIELD_TYPES = [
label: 'ISIC 4',
fieldName: 'isic_4',
formatValue: value => {
const { section, division, group, class: isicClass } =
value?.raw_value || value || {};
return [
section && `Section: ${section}`,
division && `Division: ${division}`,
group && `Group: ${group}`,
isicClass && `Class: ${isicClass}`,
].filter(Boolean);
const rawValue = value?.raw_value ?? value ?? {};
const entries = Array.isArray(rawValue) ? rawValue : [rawValue];

return entries.reduce((acc, entry, index) => {
const { section, division, group, class: isicClass } =
entry || {};
const lines = [
section && `Section: ${section}`,
division && `Division: ${division}`,
group && `Group: ${group}`,
isicClass && `Class: ${isicClass}`,
].filter(Boolean);

if (!lines.length) {
return acc;
}

if (acc.length && index > 0) {
return acc.concat(['', ...lines]);
}

return acc.concat(lines);
}, []);
},
},
];
Expand Down
Loading