Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6fbf229
added RichTextField (Editor) field source_by to partner_field model
roman-stolar Nov 4, 2025
62e4c3f
configuration improvments
roman-stolar Nov 4, 2025
397bbdc
serialize source_by data to response
roman-stolar Nov 4, 2025
e23f923
displayed source_by information in Production Location Profile partne…
roman-stolar Nov 4, 2025
71d3ded
Merge commit '07e3039a9690f18bc531a9dd28e86accc0d597d4' into OSDEV-21…
roman-stolar Nov 4, 2025
268eb71
fix
roman-stolar Nov 4, 2025
3a47558
small issues fixes + fixed linter
roman-stolar Nov 4, 2025
c1d7043
Merge branch 'main' into OSDEV-2185-display-source-by
roman-stolar Nov 4, 2025
185344f
fixed sonarqube comment
roman-stolar Nov 4, 2025
c4fc76e
added serializer unit tests
roman-stolar Nov 5, 2025
d926f47
added tests for FacilityDetailsDetail component
roman-stolar Nov 5, 2025
4b7da95
updated release notes
roman-stolar Nov 5, 2025
6a0c198
fix linter
roman-stolar Nov 5, 2025
794ff67
Merge branch 'main' into OSDEV-2185-display-source-by
roman-stolar Nov 5, 2025
802e04f
fix
roman-stolar Nov 5, 2025
87c0400
Merge commit '794ff678619bd1a748aaed92a96bd52a7f35f896' into OSDEV-21…
roman-stolar Nov 5, 2025
616e2d5
Merge branch 'main' into OSDEV-2185-display-source-by
roman-stolar Nov 6, 2025
7ce7a7b
Merge branch 'main' into OSDEV-2185-display-source-by
roman-stolar Nov 10, 2025
0fef524
Merge branch 'main' into OSDEV-2185-display-source-by
roman-stolar Nov 11, 2025
508af65
fix conflict
roman-stolar Nov 11, 2025
26abcc5
updated release notes
roman-stolar Nov 11, 2025
04fff34
address Vlad comments
roman-stolar Nov 12, 2025
0d483a8
fix migration number in release notes
roman-stolar Nov 12, 2025
d1ce0a8
[OSDEV-2199] [Climate TRACE] Display partner field "label" and "unit"…
roman-stolar Nov 12, 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
23 changes: 22 additions & 1 deletion doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html

## Introduction
* Product name: Open Supply Hub
* Release date: November 15, 2025
* Release date: November 29, 2025

### What's new
* [OSDEV-2112](https://opensupplyhub.atlassian.net/browse/OSDEV-2112) - Moved "Recruitment Agency" (previously classified as a location type) under the "Office / HQ" location type as a processing type. Also introduced a new processing type, "Union Headquarters/Office", under the "Office / HQ" location type. This update affects both search and newly contributed data: from now on, "Union Headquarters/Office" and "Recruitment Agency" will appear under the "Office / HQ" location type when displayed in search dropdowns or shown on location profiles for **newly** added locations.
Expand All @@ -18,6 +18,27 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* `reindex_database`


## Release 2.15.1

## Introduction
* Product name: Open Supply Hub
* Release date: November 14, 2025

### Database changes

#### Migrations
* 0185_add_source_by_to_partner_field.py - This migration adds a `source_by` RichTextField to the `PartnerField` model, allowing administrators to document the data source for each partner field using rich text formatting (bold, italic, links, lists). The field is optional and uses CKEditor for content editing.

### What's new
* [OSDEV-2185](https://opensupplyhub.atlassian.net/browse/OSDEV-2185) - Enhanced partner field display on production location profiles by adding a `source_by` field to the `PartnerField` model. This allows administrators to provide rich text descriptions of data sources. The source information is displayed on the facility details page below each partner field value, supporting HTML formatting for links, emphasis, and lists. Updated the facility index serializer to include `source_by` in the partner fields response only when the field contains content.
* [OSDEV-2199](https://opensupplyhub.atlassian.net/browse/OSDEV-2199) - Added `unit` and `label` metadata from `PartnerField` to the serialized partner fields payload. Production Location detail pages now render the `unit` inline with field values and display custom partner field `label`.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
* `migrate`
* `reindex_database`


## Release 2.15.0

## Introduction
Expand Down
18 changes: 16 additions & 2 deletions src/django/api/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

from django import forms
from django.urls import path
from django.contrib import admin, messages
from django.contrib.admin import AdminSite
Expand All @@ -15,6 +16,7 @@
from simple_history.admin import SimpleHistoryAdmin
from waffle.models import Flag, Sample, Switch
from waffle.admin import FlagAdmin, SampleAdmin, SwitchAdmin
from ckeditor.widgets import CKEditorWidget

from api import models

Expand Down Expand Up @@ -242,9 +244,21 @@ def get_ordering(self, request):
return ['name']


class PartnerFieldAdminForm(forms.ModelForm):
source_by = forms.CharField(
required=False,
widget=CKEditorWidget()
)

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


class PartnerFieldAdmin(admin.ModelAdmin):
list_display = ('name', 'type', 'label', 'unit', 'created_at')
search_fields = ('name', 'type', 'label', 'unit')
form = PartnerFieldAdminForm
list_display = ('name', 'type', 'label', 'unit', 'source_by', 'created_at')
search_fields = ('name', 'type', 'label', 'unit', 'source_by')
readonly_fields = ('uuid', 'created_at', 'updated_at')


Expand Down
2 changes: 1 addition & 1 deletion src/django/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,4 @@ class APIV1MatchTypes:
# Use this for frontend compatibility.
JS_MAX_SAFE_INTEGER = 9007199254740991

PARTNER_FIELD_NAMES_LIST_KEY = 'partner_field_names_list'
PARTNER_FIELD_LIST_KEY = 'partner_field_list'
20 changes: 20 additions & 0 deletions src/django/api/migrations/0185_add_source_by_to_partner_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.17 on 2025-11-04 11:47

from django.db import migrations
import ckeditor.fields


class Migration(migrations.Migration):

dependencies = [
('api', '0184_remove_facilityclaim_facility_type_choices'),
]

operations = [
migrations.AddField(
model_name='partnerfield',
name='source_by',
field=ckeditor.fields.RichTextField(blank=True, null=True, config_name='default'),
),
]

17 changes: 14 additions & 3 deletions src/django/api/models/partner_field.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import uuid
from django.db import models
from django.core.cache import cache
from api.constants import PARTNER_FIELD_NAMES_LIST_KEY
from ckeditor.fields import RichTextField
from api.constants import PARTNER_FIELD_LIST_KEY


class PartnerField(models.Model):
Expand Down Expand Up @@ -49,6 +50,16 @@ class Meta:
blank=True,
help_text=('The partner field label.'))

source_by = RichTextField(
blank=True,
null=True,
config_name='default',
help_text=(
'Rich text field describing '
'the source of this partner field.'
)
)

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

Expand All @@ -57,9 +68,9 @@ def __str__(self):

def save(self, *args, **kwargs):
super().save(*args, **kwargs)
cache.delete(PARTNER_FIELD_NAMES_LIST_KEY)
cache.delete(PARTNER_FIELD_LIST_KEY)

def delete(self, *args, **kwargs):
result = super().delete(*args, **kwargs)
cache.delete(PARTNER_FIELD_NAMES_LIST_KEY)
cache.delete(PARTNER_FIELD_LIST_KEY)
return result
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def __init__(self,
self.fields: list = ['id', 'is_verified', 'value', 'created_at',
'updated_at', 'contributor_name',
'contributor_id', 'value_count', 'is_from_claim',
'field_name', 'verified_count']
'field_name', 'verified_count', 'source_by',
'unit', 'label']
self.data: list = []

if exclude_fields:
Expand All @@ -28,31 +29,34 @@ def __init__(self,
self._serialize_extended_field_list()

def _serialize_extended_field_list(self) -> None:
field_serializers = {
'created_at': self._get_created_at,
'updated_at': self._get_updated_at,
'contributor_name': self._get_contributor_name,
'contributor_id': self._get_contributor_id,
'is_from_claim': self._get_is_from_claim,
'verified_count': self._get_verified_count,
}
context_overrides = {'source_by', 'unit', 'label'}

for extended_field in self.extended_field_list:
serialized_extended_field = {}

for field in self.fields:
if field == 'created_at':
serialized_extended_field[field] = \
self._get_created_at(extended_field)
elif field == 'updated_at':
serialized_extended_field[field] = \
self._get_updated_at(extended_field)
elif field == 'contributor_name':
serialized_extended_field[field] = \
self._get_contributor_name(extended_field)
elif field == 'contributor_id':
serialized_extended_field[field] = \
self._get_contributor_id(extended_field)
elif field == 'is_from_claim':
serialized_extended_field[field] = \
self._get_is_from_claim(extended_field)
elif field == 'verified_count':
if field in field_serializers:
serialized_extended_field[field] = \
self._get_verified_count(extended_field)
else:
serialized_extended_field[field] = extended_field.get(
field)
field_serializers[field](
extended_field
)
continue

if field in context_overrides:
context_value = self.context.get(field)
if context_value is not None:
serialized_extended_field[field] = context_value
continue

serialized_extended_field[field] = extended_field.get(field)

self.data.append(serialized_extended_field)

Expand Down
29 changes: 18 additions & 11 deletions src/django/api/serializers/facility/facility_index_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)

from countries.lib.countries import COUNTRY_NAMES
from api.constants import PARTNER_FIELD_NAMES_LIST_KEY
from api.constants import PARTNER_FIELD_LIST_KEY
from ...models import Contributor
from ...models.facility.facility_index import FacilityIndex
from ...models.embed_config import EmbedConfig
Expand Down Expand Up @@ -95,14 +95,18 @@ def __get_request(self):
def __serialize_and_sort_partner_fields(
self,
grouped_fields: Dict[str, List[Dict[str, Any]]],
partner_field_names: List[str],
partner_fields: List[PartnerField],
user_can_see_detail: bool,
embed_mode_active: bool,
use_main_created_at: bool,
date_field_to_sort: str
) -> Dict[str, List[Dict[str, Any]]]:
grouped_data = {}
for field_name in partner_field_names:
for field in partner_fields:
field_name = field.name
source_by = field.source_by
unit = field.unit
label = field.label
fields = grouped_fields.get(field_name, [])
if not fields:
continue
Expand All @@ -113,6 +117,9 @@ def __serialize_and_sort_partner_fields(
context={
'user_can_see_detail': user_can_see_detail,
'embed_mode_active': embed_mode_active,
'source_by': source_by,
'unit': unit,
'label': label
},
exclude_fields=(
['created_at'] if not use_main_created_at else []
Expand Down Expand Up @@ -190,19 +197,19 @@ def __group_fields_by_name(
return grouped

@staticmethod
def __get_partner_field_names():
cached_names = cache.get(PARTNER_FIELD_NAMES_LIST_KEY)
def __get_cached_partner_fields():
cached_names = cache.get(PARTNER_FIELD_LIST_KEY)

if cached_names is not None:
return cached_names

names = list(
PartnerField.objects.values_list("name", flat=True)
partner_fields = list(
PartnerField.objects.all()
)

cache.set(PARTNER_FIELD_NAMES_LIST_KEY, names, 600)
cache.set(PARTNER_FIELD_LIST_KEY, partner_fields, 60)

return names
return partner_fields

def get_location(self, facility):
return facility.location
Expand Down Expand Up @@ -429,11 +436,11 @@ def get_partner_fields(self, facility):

user_can_see_detail = can_user_see_detail(self)
embed_mode_active = is_embed_mode_active(self)
field_names = self.__get_partner_field_names()
partner_fields = self.__get_cached_partner_fields()

return self.__serialize_and_sort_partner_fields(
grouped_fields,
field_names,
partner_fields,
user_can_see_detail,
embed_mode_active,
use_main_created_at,
Expand Down
Loading