Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ef01013
Add base_url and display_text
VadimKovalenkoSNF Nov 21, 2025
a283c22
Update Release protocol
VadimKovalenkoSNF Nov 24, 2025
8c7953e
Merge branch 'main' into OSDEV-2268-add-uri-reference-support
VadimKovalenkoSNF Nov 27, 2025
b1a1528
Merge branch 'main' into OSDEV-2268-add-uri-reference-support
VadimKovalenkoSNF Dec 3, 2025
af42780
Adjust UI (partial impl)
VadimKovalenkoSNF Nov 24, 2025
ee419b4
Refactor partner fields parsing (partial impl)
VadimKovalenkoSNF Nov 28, 2025
3c4adfb
Pass base_url and display_text down to the URI reference component
VadimKovalenkoSNF Dec 4, 2025
43262a8
Refactor render logic if display text is not present
VadimKovalenkoSNF Dec 4, 2025
152dd5e
Merge branch 'main' into OSDEV-2268-add-uri-reference-support
VadimKovalenkoSNF Dec 4, 2025
0261c59
Refine partner config fields rendering
VadimKovalenkoSNF Dec 5, 2025
c185273
Add unit tests, minor fixes
VadimKovalenkoSNF Dec 5, 2025
18408d3
Show base url and display text only for object partner field type
VadimKovalenkoSNF Dec 5, 2025
58f47aa
Merge branch 'main' into OSDEV-2268-add-uri-reference-support
VadimKovalenkoSNF Dec 5, 2025
d4d5492
Add prop types, minor fixes
VadimKovalenkoSNF Dec 5, 2025
72db3da
Minor fixes for broken variable checks
VadimKovalenkoSNF Dec 5, 2025
27e73fa
Minor fix with null check for srtingValue in showFieldDefaultDisplayText
VadimKovalenkoSNF Dec 5, 2025
79cf2e2
Fix Typography component
VadimKovalenkoSNF Dec 5, 2025
4514ab4
Merge branch 'main' into OSDEV-2268-add-uri-reference-support
VadimKovalenkoSNF Dec 6, 2025
e018b72
Fix proptypes for multiple components
VadimKovalenkoSNF Dec 8, 2025
b557ed7
Abstract link text rendering
VadimKovalenkoSNF Dec 8, 2025
27b6311
Minor base_url and display_text attribute fix
VadimKovalenkoSNF Dec 8, 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
1 change: 1 addition & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* [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.
* [OSDEV-2225](https://opensupplyhub.atlassian.net/browse/OSDEV-2225) - Added a clear button to the opening and closing date inputs in the environmental data section within the claim form to allow users to remove selected opening and closing dates.
* [OSDEV-2268](https://opensupplyhub.atlassian.net/browse/OSDEV-2268) - Added URI-Reference format support for partner fields with JSON schema. Properties defined with `"format": "uri-reference"` are rendered on production location profile pages as clickable links wrapped by the text from admin `Partner field` configuration page.
* [OSDEV-2292](https://opensupplyhub.atlassian.net/browse/OSDEV-2292) - Improved ISIC-4 validation for `POST /v1/production-locations/` and `PATCH /v1/production-locations/{os_id}` endpoints to provide clearer error messages when invalid data is submitted, including rejection of empty ISIC-4 objects, non-string values, unrecognized fields, and duplicate entries. API consumers will now receive specific validation errors for previously accepted but invalid ISIC-4 taxonomy data, ensuring only properly formatted ISIC-4 classification objects (with string values for section, division, group, or class fields) are stored in the system.

### Release instructions
Expand Down
11 changes: 10 additions & 1 deletion src/django/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,16 @@ class PartnerFieldAdminForm(forms.ModelForm):

class Meta:
model = PartnerField
fields = ['name', 'type', 'unit', 'label', 'source_by', 'json_schema']
fields = [
'name',
'type',
'unit',
'label',
'source_by',
'base_url',
'display_text',
'json_schema'
]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.17 on 2025-12-03 12:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0188_introduce_indexing_for_new_operational_and_environmental_data'),
]

operations = [
migrations.AddField(
model_name='partnerfield',
name='base_url',
field=models.URLField(blank=True, max_length=2000, null=True),
),
migrations.AddField(
model_name='partnerfield',
name='display_text',
field=models.CharField(blank=True, max_length=500),
),
]
10 changes: 9 additions & 1 deletion src/django/api/models/partner_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ class Meta:
max_length=200,
blank=True,
help_text=('The partner field label.'))

base_url = models.URLField(
max_length=2000,
blank=True,
null=True
)
display_text = models.CharField(
max_length=500,
blank=True,
)
source_by = RichTextField(
blank=True,
null=True,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def __init__(self,
'updated_at', 'contributor_name',
'contributor_id', 'value_count', 'is_from_claim',
'field_name', 'verified_count', 'source_by',
'unit', 'label', 'json_schema']
'unit', 'label', 'base_url', 'display_text',
'json_schema']
self.data: list = []

if exclude_fields:
Expand All @@ -37,7 +38,14 @@ 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', 'json_schema'}
context_overrides = {
'source_by',
'unit',
'label',
'base_url',
'display_text',
'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,8 @@ def __serialize_and_sort_partner_fields(
source_by = field.source_by
unit = field.unit
label = field.label
base_url = getattr(field, 'base_url', None)
display_text = getattr(field, 'display_text', None)
json_schema = field.json_schema
fields = grouped_fields.get(field_name, [])
if not fields:
Expand All @@ -121,6 +123,8 @@ def __serialize_and_sort_partner_fields(
'source_by': source_by,
'unit': unit,
'label': label,
'base_url': base_url,
'display_text': display_text,
'json_schema': json_schema
},
exclude_fields=(
Expand Down
102 changes: 61 additions & 41 deletions src/django/api/static/admin/js/partner_field_admin.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,83 @@
(function($) {
'use strict';

function findJsonSchemaFieldRow() {
const jsonSchemaField = $('#id_json_schema');

if (jsonSchemaField.length) {
return jsonSchemaField.closest('.field-json_schema, tr, .form-row').first();
}

return $('.field-json_schema').first();
}
function findFieldRow(fieldSelector, fieldClassSelector) {
const field = $(fieldSelector);

function toggleJsonSchemaField() {
const typeField = $('#id_type');
const jsonSchemaFieldRow = findJsonSchemaFieldRow();

if (!typeField.length) {
return false;
if (field.length) {
const row = field
.closest(`${fieldClassSelector}, tr, .form-row`)
.first();
if (row.length) {
return row;
}
}

if (!jsonSchemaFieldRow.length) {
return false;
}

const currentType = typeField.val();

if (currentType === 'object') {
jsonSchemaFieldRow.show();
return true;
} else {
jsonSchemaFieldRow.hide();
return true;

if (fieldClassSelector) {
const fallbackRow = $(fieldClassSelector).first();
if (fallbackRow.length) {
return fallbackRow;
}
}

return $();
}

function setupJsonSchemaToggle() {
function setupPartnerFieldOptionsToggle() {
const typeField = $('#id_type');
const jsonSchemaField = $('#id_json_schema');

if (!typeField.length || !jsonSchemaField.length) {

if (!typeField.length) {
return false;
}

const jsonSchemaFieldRow = findJsonSchemaFieldRow();
if (!jsonSchemaFieldRow.length) {

const jsonSchemaFieldRow = findFieldRow(
'#id_json_schema',
'.field-json_schema'
);
const baseUrlFieldRow = findFieldRow(
'#id_base_url',
'.field-base_url'
);
const displayTextFieldRow = findFieldRow(
'#id_display_text',
'.field-display_text'
);

if (
!jsonSchemaFieldRow.length &&
!baseUrlFieldRow.length &&
!displayTextFieldRow.length
) {
return false;
}

toggleJsonSchemaField();


const toggleFields = function() {
const shouldShow = typeField.val() === 'object';
[jsonSchemaFieldRow, baseUrlFieldRow, displayTextFieldRow].forEach(
function(row) {
if (!row.length) {
return;
}
if (shouldShow) {
row.show();
} else {
row.hide();
}
}
);
};

toggleFields();

typeField.on('change', function() {
toggleJsonSchemaField();
toggleFields();
});

return true;
}

$(document).ready(function() {
setupJsonSchemaToggle();
setupPartnerFieldOptionsToggle();
});
})(django.jQuery || jQuery);

84 changes: 84 additions & 0 deletions src/react/src/__tests__/components/PartnerFieldSchemaValue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,88 @@ describe('PartnerFieldSchemaValue', () => {
const urlTextElements = screen.queryAllByText('View Report');
expect(urlTextElements.length).toBe(1);
});

it('renders uri-reference fields using partner configuration display text', () => {
const value = {
value: 'report-42',
status: 'active',
};
const jsonSchema = {
type: 'object',
properties: {
value: {
type: 'string',
format: 'uri-reference',
title: 'Partner Report',
description: 'Latest partner-provided report.',
},
value_text: {
type: 'string',
},
status: {
type: 'string',
title: 'Status',
},
},
};
const partnerConfigFields = {
baseUrl: 'https://portal.example.com/reports',
displayText: 'Open report',
};

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

const link = screen.getByRole('link', { name: 'Open report' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
'href',
'https://portal.example.com/reports/report-42',
);
expect(screen.getByText('Status: active')).toBeInTheDocument();
});

it('falls back to default renderer when schema loses uri-reference format', () => {
const value = {
partner_field: 'report-42',
};
const jsonSchema = {
type: 'object',
properties: {
partner_field: {
type: 'string',
title: 'Partner field',
},
},
};
const partnerConfigFields = {
baseUrl: 'https://portal.example.com/reports',
displayText: 'Open report',
};

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

const text = screen.getByText('Partner field: report-42');
expect(text).toBeInTheDocument();
expect(text.tagName).toBe('SPAN');
});

it('returns primitive values when schema or object data is missing', () => {
const { container } = render(
<PartnerFieldSchemaValue value="Raw text value" />,
);

expect(container).toHaveTextContent('Raw text value');
});
});
Loading