Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cb18082
basic implementation to display partner field value by json schema fo…
roman-stolar Nov 25, 2025
baf8202
support displaying schema data with and without uri format
roman-stolar Nov 26, 2025
b74273b
refactored
roman-stolar Nov 26, 2025
f6e4034
simplify and refactored logic + added FE tests
roman-stolar Nov 26, 2025
1d6ad6b
fix test
roman-stolar Nov 26, 2025
830885a
remove useless
roman-stolar Nov 26, 2025
fa9e685
updated release notes
roman-stolar Nov 27, 2025
a9152a6
added simple unit tests
roman-stolar Nov 27, 2025
8effb97
small improvments
roman-stolar Nov 28, 2025
937d850
Merge branch 'main' into OSDEV-2269-add-uri-format-support-for-partne…
roman-stolar Dec 1, 2025
5331897
Merge commit 'f5bc61cdd2ad903ca73072a1f1977623f3edd066' into OSDEV-22…
roman-stolar Dec 1, 2025
e5be75b
fix
roman-stolar Dec 1, 2025
574b2f1
Merge commit '58b2d1081934bb20abcf9cde49ffd2d1787eda75' into OSDEV-22…
roman-stolar Dec 1, 2025
90b188a
Big refactoring
roman-stolar Dec 2, 2025
95f86b5
sonar qube fix
roman-stolar Dec 2, 2025
3abbc7d
Merge commit '74469ffaa4629335b2737c1b2df97c9a8cc7e505' into OSDEV-22…
roman-stolar Dec 2, 2025
ad6bdd2
small fixes
roman-stolar Dec 2, 2025
5032a77
improvements
roman-stolar Dec 2, 2025
f41e18e
linter
roman-stolar Dec 2, 2025
fe1b620
code rabbit note
roman-stolar Dec 2, 2025
4c1f357
addressed Vadim and Vlad comments
roman-stolar Dec 2, 2025
6c5d722
safe return
roman-stolar Dec 2, 2025
c170a40
Merge branch 'main' into OSDEV-2269-add-uri-format-support-for-partne…
roman-stolar Dec 3, 2025
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
2 changes: 2 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
### What's new
* [OSDEV-2280](https://opensupplyhub.atlassian.net/browse/OSDEV-2280) - Added prominent PII (Personally Identifiable Information) warning notes at file upload stages throughout the new claim flow to inform users that they should NOT submit documents containing personal information, home addresses, personal utility bills, or personal phone numbers, enhancing data security and user privacy protection.
* [OSDEV-2114](https://opensupplyhub.atlassian.net/browse/OSDEV-2114) - Extended the `GET api/facilities/{os_id}/` endpoint with additional claim environmental data including opening date, closing date, estimated annual throughput, and actual annual energy consumption. The production location profile page now displays these new environmental data points as part of the claim information.
* [OSDEV-2269](https://opensupplyhub.atlassian.net/browse/OSDEV-2269) - Added URI format support for partner fields with JSON schema. Properties defined with `"format": "uri"` are rendered as clickable links on production location profile pages.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
Expand Down Expand Up @@ -70,6 +71,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
7. Finally, run real `isic_4` backfilling using this command for all fields: `backfill_isic_4_extended_fields`.
8. Make sure no errors appear.


## Release 2.15.2

## Introduction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self,
'updated_at', 'contributor_name',
'contributor_id', 'value_count', 'is_from_claim',
'field_name', 'verified_count', 'source_by',
'unit', 'label']
'unit', 'label', 'json_schema']
self.data: list = []

if exclude_fields:
Expand All @@ -37,7 +37,7 @@ def _serialize_extended_field_list(self) -> None:
'is_from_claim': self._get_is_from_claim,
'verified_count': self._get_verified_count,
}
context_overrides = {'source_by', 'unit', 'label'}
context_overrides = {'source_by', 'unit', 'label', 'json_schema'}

for extended_field in self.extended_field_list:
serialized_extended_field = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def __serialize_and_sort_partner_fields(
source_by = field.source_by
unit = field.unit
label = field.label
json_schema = field.json_schema
fields = grouped_fields.get(field_name, [])
if not fields:
continue
Expand All @@ -119,7 +120,8 @@ def __serialize_and_sort_partner_fields(
'embed_mode_active': embed_mode_active,
'source_by': source_by,
'unit': unit,
'label': label
'label': label,
'json_schema': json_schema
},
exclude_fields=(
['created_at'] if not use_main_created_at else []
Expand Down
97 changes: 97 additions & 0 deletions src/django/api/tests/test_facility_index_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,100 @@ def test_partner_fields_label_is_empty_when_not_set(self):
self.assertEqual(len(test_data_field), 1)
self.assertIn('label', test_data_field[0])
self.assertEqual(test_data_field[0]['label'], '')

def test_partner_fields_includes_json_schema(self):
self.partner_field_1.json_schema = {
'type': 'object',
'properties': {
'url': {
'type': 'string',
'format': 'uri',
},
},
}
self.partner_field_1.save()

extended_field = ExtendedField.objects.create(
facility=self.facility,
field_name='test_data_field',
value={'raw_value': 'Test Value'},
contributor=self.contrib_one
)

facility_index = FacilityIndex.objects.get(id=self.facility.id)
facility_index.extended_fields.append({
'id': extended_field.id,
'field_name': 'test_data_field',
'value': {'raw_value': 'Test Value'},
'contributor': {
'id': self.contrib_one.id,
'name': self.contrib_one.name,
'is_verified': self.contrib_one.is_verified,
},
'created_at': extended_field.created_at.isoformat(),
'updated_at': extended_field.updated_at.isoformat(),
'is_verified': False,
'facility_list_item_id': None,
'should_display_association': True,
'value_count': 1,
})
facility_index.save()
facility_index.refresh_from_db()

data = FacilityIndexSerializer(facility_index).data
partner_fields = data["properties"]["partner_fields"]
test_data_field = partner_fields['test_data_field']

self.assertEqual(len(test_data_field), 1)
self.assertIn('json_schema', test_data_field[0])
self.assertEqual(
test_data_field[0]['json_schema'],
{
'type': 'object',
'properties': {
'url': {
'type': 'string',
'format': 'uri',
},
},
}
)

def test_partner_fields_json_schema_none(self):
self.partner_field_1.json_schema = None
self.partner_field_1.save()

extended_field = ExtendedField.objects.create(
facility=self.facility,
field_name='test_data_field',
value={'raw_value': 'Test Value'},
contributor=self.contrib_one
)

facility_index = FacilityIndex.objects.get(id=self.facility.id)
facility_index.extended_fields.append({
'id': extended_field.id,
'field_name': 'test_data_field',
'value': {'raw_value': 'Test Value'},
'contributor': {
'id': self.contrib_one.id,
'name': self.contrib_one.name,
'is_verified': self.contrib_one.is_verified,
},
'created_at': extended_field.created_at.isoformat(),
'updated_at': extended_field.updated_at.isoformat(),
'is_verified': False,
'facility_list_item_id': None,
'should_display_association': True,
'value_count': 1,
})
facility_index.save()
facility_index.refresh_from_db()

data = FacilityIndexSerializer(facility_index).data
partner_fields = data["properties"]["partner_fields"]
test_data_field = partner_fields['test_data_field']

self.assertEqual(len(test_data_field), 1)
self.assertIn('json_schema', test_data_field[0])
self.assertIsNone(test_data_field[0]['json_schema'])
120 changes: 120 additions & 0 deletions src/react/src/__tests__/components/PartnerFieldSchemaValue.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import PartnerFieldSchemaValue from '../../components/PartnerFields/PartnerFieldSchemaValue';

describe('PartnerFieldSchemaValue', () => {
it('renders URI field with _text property as clickable link and skips _text', () => {
const value = {
url: 'https://example.com/audit-123',
url_text: 'View Audit Report',
};
const jsonSchema = {
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
},
url_text: {
type: 'string',
},
},
};

render(<PartnerFieldSchemaValue value={value} jsonSchema={jsonSchema} />);

const link = screen.getByRole('link', { name: 'View Audit Report' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://example.com/audit-123');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');

const urlTextElements = screen.queryAllByText('View Audit Report');
expect(urlTextElements.length).toBe(1); // Only in the link
});

it('renders URI field without _text property, using URI as link text', () => {
const value = {
mit_data_url: 'https://livingwage.mit.edu/locations/123',
};
const jsonSchema = {
type: 'object',
properties: {
mit_data_url: {
type: 'string',
format: 'uri',
},
},
};

render(<PartnerFieldSchemaValue value={value} jsonSchema={jsonSchema} />);

const link = screen.getByRole('link', {
name: 'https://livingwage.mit.edu/locations/123',
});
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://livingwage.mit.edu/locations/123');
});

it('renders non-URI field with title as "Title: value" format', () => {
const value = {
internal_id: 'abc-123-xyz',
};
const jsonSchema = {
type: 'object',
properties: {
internal_id: {
type: 'string',
title: 'Internal ID',
},
},
};

render(<PartnerFieldSchemaValue value={value} jsonSchema={jsonSchema} />);

const text = screen.getByText('Internal ID: abc-123-xyz');
expect(text).toBeInTheDocument();
expect(text.tagName).toBe('SPAN');
});

it('renders mixed URI and non-URI fields correctly, skipping _text property', () => {
const value = {
url: 'https://example.com/report',
url_text: 'View Report',
internal_id: 'ABC-123',
status: 'active',
};
const jsonSchema = {
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
},
url_text: {
type: 'string',
},
internal_id: {
type: 'string',
title: 'Internal ID',
},
status: {
type: 'string',
title: 'Status',
},
},
};

render(<PartnerFieldSchemaValue value={value} jsonSchema={jsonSchema} />);

const link = screen.getByRole('link', { name: 'View Report' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://example.com/report');

expect(screen.getByText('Internal ID: ABC-123')).toBeInTheDocument();
expect(screen.getByText('Status: active')).toBeInTheDocument();

const urlTextElements = screen.queryAllByText('View Report');
expect(urlTextElements.length).toBe(1);
});
});
27 changes: 27 additions & 0 deletions src/react/src/__tests__/utils.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2831,4 +2831,31 @@ describe('formatPartnerFieldValue', () => {
const result = formatPartnerFieldValue(value);
expect(result).toBe('active: true, deleted: false, notes: null');
});

it('formats object with schema', () => {
const value = {
raw_value: {
url: 'https://example.com/report',
url_text: 'View Report',
},
};
const jsonSchema = {
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
},
url_text: {
type: 'string',
},
},
};
const result = formatPartnerFieldValue(value, jsonSchema);

expect(result).toEqual({
url: 'https://example.com/report',
url_text: 'View Report',
});
});
});
11 changes: 10 additions & 1 deletion src/react/src/components/FacilityDetailsDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Tooltip from '@material-ui/core/Tooltip';
import ShowOnly from './ShowOnly';
import BadgeVerified from './BadgeVerified';
import FeatureFlag from './FeatureFlag';
import PartnerFieldSchemaValue from './PartnerFields/PartnerFieldSchemaValue';

import { CLAIM_A_FACILITY } from '../util/constants';

Expand Down Expand Up @@ -63,6 +64,7 @@ const FacilityDetailsDetail = ({
secondary,
sourceBy,
unit,
jsonSchema,
isVerified,
isFromClaim,
classes,
Expand All @@ -84,7 +86,14 @@ const FacilityDetailsDetail = ({
</ShowOnly>
<div>
<Typography className={classes.primaryText} component="div">
{primary || locationLabeled}
{jsonSchema ? (
<PartnerFieldSchemaValue
value={primary}
jsonSchema={jsonSchema}
/>
) : (
primary || locationLabeled
)}
{unit ? <span className={classes.unitText}>{unit}</span> : null}
</Typography>
{sourceBy ? (
Expand Down
5 changes: 4 additions & 1 deletion src/react/src/components/FacilityDetailsGeneralFields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ const FacilityDetailsGeneralFields = ({
const values = get(data, `properties.partner_fields.${fieldName}`, []);

const formatField = item =>
formatExtendedField({ ...item, formatValue });
formatExtendedField({
...item,
formatValue,
});

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

Expand Down
3 changes: 3 additions & 0 deletions src/react/src/components/FacilityDetailsItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const FacilityDetailsItem = ({
secondary,
sourceBy,
unit,
jsonSchema,
classes,
embed,
isVerified,
Expand All @@ -64,6 +65,7 @@ const FacilityDetailsItem = ({
secondary={!embed ? secondary : null}
sourceBy={!embed ? sourceBy : null}
unit={!embed ? unit : null}
jsonSchema={!embed ? jsonSchema : null}
isVerified={isVerified}
isFromClaim={isFromClaim}
/>
Expand Down Expand Up @@ -99,6 +101,7 @@ const FacilityDetailsItem = ({
secondary={!embed ? secondary : null}
sourceBy={!embed ? sourceBy : null}
unit={!embed ? unit : null}
jsonSchema={!embed ? jsonSchema : null}
isVerified={isVerified}
isFromClaim={isFromClaim}
/>
Expand Down
20 changes: 20 additions & 0 deletions src/react/src/components/PartnerFields/DefaultProperty.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

/**
* Component for rendering default format properties (no format or unsupported format).
*/
const DefaultProperty = ({ propertyKey, value, schemaProperties }) => {

Check warning on line 6 in src/react/src/components/PartnerFields/DefaultProperty.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'value' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=opensupplyhub_open-supply-hub&issues=AZrfoyukVTACxFqvg7vq&open=AZrfoyukVTACxFqvg7vq&pullRequest=823

Check warning on line 6 in src/react/src/components/PartnerFields/DefaultProperty.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'propertyKey' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=opensupplyhub_open-supply-hub&issues=AZrfoyukVTACxFqvg7vp&open=AZrfoyukVTACxFqvg7vp&pullRequest=823

Check warning on line 6 in src/react/src/components/PartnerFields/DefaultProperty.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'schemaProperties' is missing in props validation

See more on https://sonarcloud.io/project/issues?id=opensupplyhub_open-supply-hub&issues=AZrfoyukVTACxFqvg7vr&open=AZrfoyukVTACxFqvg7vr&pullRequest=823
const propertyValue = value[propertyKey];
const propertySchema = schemaProperties[propertyKey] || {};
const { title } = propertySchema;
const stringValue = propertyValue != null ? String(propertyValue) : '';

Check warning on line 10 in src/react/src/components/PartnerFields/DefaultProperty.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=opensupplyhub_open-supply-hub&issues=AZrfoyukVTACxFqvg7vs&open=AZrfoyukVTACxFqvg7vs&pullRequest=823
const displayText = title ? `${title}: ${stringValue}` : stringValue;

return (
<span key={`${propertyKey}-default`} style={{ display: 'block' }}>
{displayText}
</span>
);
};

export default DefaultProperty;
Loading