Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a407a06
implement bulk logic to return partner_fields from /facilities/os_id …
roman-stolar Oct 3, 2025
aee65c1
small improvements
roman-stolar Oct 3, 2025
c47ae49
implement logic to display any partner fields dynamically
roman-stolar Oct 6, 2025
2c32f51
Merge branch 'main' into OSDEV-2180-display-dynamic-partner-field-on-…
roman-stolar Oct 6, 2025
cb32299
fix linter
roman-stolar Oct 6, 2025
72ac509
Merge commit '5a12ba76b2aed04a8699e247bfab0188bd581a45' into OSDEV-21…
roman-stolar Oct 7, 2025
4d5efb6
refactored get_extended_fields & get_partner_fields
roman-stolar Oct 7, 2025
779f844
fix linter
roman-stolar Oct 7, 2025
82ae01a
fix format to display for object type partner fields
roman-stolar Oct 7, 2025
a122167
updated release notes
roman-stolar Oct 7, 2025
a632347
Merge branch 'main' into OSDEV-2180-display-dynamic-partner-field-on-…
roman-stolar Oct 7, 2025
5e78707
adressed Vadim comments
roman-stolar Oct 9, 2025
712900c
added unit tests for formatPartnerFieldValue
roman-stolar Oct 9, 2025
f88c768
fix
roman-stolar Oct 9, 2025
939faa2
Merge branch 'main' into OSDEV-2180-display-dynamic-partner-field-on-…
roman-stolar Oct 9, 2025
1cde289
fix linter
roman-stolar Oct 9, 2025
8261b4f
fix linter
roman-stolar Oct 9, 2025
8e5c155
added unit tests
roman-stolar Oct 9, 2025
4f4fde0
added simple cache
roman-stolar Oct 9, 2025
e8b8e27
fix
roman-stolar Oct 9, 2025
0eb1c44
update
roman-stolar Oct 9, 2025
298a26a
fixes
roman-stolar Oct 9, 2025
6879d1b
fixes
roman-stolar Oct 9, 2025
025e746
Merge branch 'main' into OSDEV-2180-display-dynamic-partner-field-on-…
roman-stolar Oct 10, 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 @@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
### What's new
* [OSDEV-2177](https://opensupplyhub.atlassian.net/browse/OSDEV-2177) - Updated button text from `View My Claims` to `View My Approved Claims` in post-claims submission pop-up.
* [OSDEV-2067](https://opensupplyhub.atlassian.net/browse/OSDEV-2067) - Added optional emissions estimation fields to the facility claim form, allowing claimants to provide energy consumption data by source type (electricity, natural gas, diesel, coal, biomass, etc.), facility opening/closing dates, and estimated annual throughput for free emissions calculations.
* [OSDEV-2180](https://opensupplyhub.atlassian.net/browse/OSDEV-2180) - Introduced logic to return `partner_fields` for both `GET /facilities/{os_id}` and `GET /facilities/` endpoints. Also implemented dynamic rendering of any `partner_fields` returned by the API, ensuring that new fields are displayed automatically without additional actions in `Production Location Profile` page.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
Expand Down
2 changes: 2 additions & 0 deletions src/django/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,5 @@ class APIV1MatchTypes:
# JavaScript's MAX_SAFE_INTEGER (2^53 - 1).
# Use this for frontend compatibility.
JS_MAX_SAFE_INTEGER = 9007199254740991

PARTNER_FIELD_NAMES_LIST_KEY = 'partner_field_names_list'
12 changes: 1 addition & 11 deletions src/django/api/models/extended_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ class ExtendedField(models.Model):
DUNS_ID = 'duns_id'
LEI_ID = 'lei_id'
RBA_ID = 'rba_id'
ESTIMATED_EMISSIONS_ACTIVITY = 'estimated_emissions_activity'
ESTIMATED_ANNUAL_ENERGY_CONSUMPTION = 'estimated_annual_energy_consumption'

FIELD_CHOICES = (
(NAME, NAME),
Expand All @@ -37,15 +35,7 @@ class ExtendedField(models.Model):
(PARENT_COMPANY_OS_ID, PARENT_COMPANY_OS_ID),
(DUNS_ID, DUNS_ID),
(LEI_ID, LEI_ID),
(RBA_ID, RBA_ID),
(
ESTIMATED_EMISSIONS_ACTIVITY,
ESTIMATED_EMISSIONS_ACTIVITY
),
(
ESTIMATED_ANNUAL_ENERGY_CONSUMPTION,
ESTIMATED_ANNUAL_ENERGY_CONSUMPTION
)
(RBA_ID, RBA_ID)
)

uuid = models.UUIDField(
Expand Down
10 changes: 10 additions & 0 deletions src/django/api/models/partner_field.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import uuid
from django.db import models
from django.core.cache import cache
from api.constants import PARTNER_FIELD_NAMES_LIST_KEY


class PartnerField(models.Model):
Expand Down Expand Up @@ -52,3 +54,11 @@ class Meta:

def __str__(self):
return self.name

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

def delete(self, *args, **kwargs):
cache.delete(PARTNER_FIELD_NAMES_LIST_KEY)
super().delete(*args, **kwargs)
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class FacilityIndexDetailsSerializer(FacilityIndexSerializer):
created_from = SerializerMethodField()
sector = SerializerMethodField()
is_claimed = SerializerMethodField()
partner_fields = SerializerMethodField()

class Meta:
model = FacilityIndex
Expand All @@ -62,6 +63,7 @@ class Meta:
'created_from',
'sector',
'is_claimed',
'partner_fields',
)
geo_field = 'location'

Expand Down
210 changes: 174 additions & 36 deletions src/django/api/serializers/facility/facility_index_serializer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import logging

from itertools import groupby
from collections import defaultdict
from typing import Dict, List, Any
from django.core.cache import cache
from rest_framework_gis.serializers import (
GeoFeatureModelSerializer,
GeometrySerializerMethodField,
Expand All @@ -9,11 +13,13 @@
)

from countries.lib.countries import COUNTRY_NAMES
from api.constants import PARTNER_FIELD_NAMES_LIST_KEY
from ...models import Contributor
from ...models.facility.facility_index import FacilityIndex
from ...models.embed_config import EmbedConfig
from ...models.embed_field import EmbedField
from ...models.extended_field import ExtendedField
from ...models.partner_field import PartnerField
from ...models.nonstandard_field import NonstandardField
from ...helpers.helpers import parse_raw_data, get_csv_values
from ..utils import is_embed_mode_active
Expand All @@ -35,6 +41,8 @@
regroup_claims_for_sector_field
)

logger = logging.getLogger(__name__)


class FacilityIndexSerializer(GeoFeatureModelSerializer):
os_id = SerializerMethodField()
Expand All @@ -48,6 +56,7 @@ class FacilityIndexSerializer(GeoFeatureModelSerializer):
address = SerializerMethodField()
has_approved_claim = SerializerMethodField()
sector = SerializerMethodField()
partner_fields = SerializerMethodField()

class Meta:
model = FacilityIndex
Expand All @@ -66,6 +75,7 @@ class Meta:
'contributor_fields',
'extended_fields',
'sector',
'partner_fields',
)
geo_field = 'location'

Expand All @@ -77,6 +87,123 @@ def __init__(self, *args, **kwargs):
for field_name in exclude_fields:
self.fields.pop(field_name, None)

def __get_request(self):
if self.context is None:
return None
return self.context.get('request')

def __serialize_and_sort_partner_fields(
self,
grouped_fields: Dict[str, List[Dict[str, Any]]],
partner_field_names: List[str],
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:
fields = grouped_fields.get(field_name, [])
if not fields:
continue

try:
serializer = FacilityIndexExtendedFieldListSerializer(
fields,
context={
'user_can_see_detail': user_can_see_detail,
'embed_mode_active': embed_mode_active,
},
exclude_fields=(
['created_at'] if not use_main_created_at else []
)
)
grouped_data[field_name] = sorted(
serializer.data,
key=lambda k: self.__sort_order(k, date_field_to_sort),
reverse=True
)
except Exception as exc:
logger.error(
f"Failed to serialize partner field '{field_name}': "
f"{exc}"
)
grouped_data[field_name] = []

return grouped_data

@staticmethod
def __date_field_to_sort(use_main_created_at):
return (
'created_at' if use_main_created_at else 'updated_at'
)

@staticmethod
def __sort_order(item, date_field_to_sort):
return (
item.get('verified_count', 0),
item.get('is_from_claim', False),
item.get('value_count', 1),
item.get(date_field_to_sort, None)
)

@staticmethod
def __sort_order_excluding_date(item):
return (
item.get('verified_count', 0),
item.get('is_from_claim', False),
item.get('value_count', 1)
)

@staticmethod
def __filter_contributor_extended_fields(facility, request):
if request is None:
return facility.extended_fields

embed = request.query_params.get('embed')
if embed != '1':
return facility.extended_fields

contributor_id = request.query_params.get('contributor')
if contributor_id is None:
contributor_ids = request.query_params.getlist('contributors', [])
if contributor_ids:
contributor_id = contributor_ids[0]

if contributor_id:
return get_efs_associated_with_contributor(
int(contributor_id),
facility.extended_fields,
)

return facility.extended_fields

@staticmethod
def __group_fields_by_name(
fields: List[Dict[str, Any]]
) -> Dict[str, List[Dict[str, Any]]]:
grouped = defaultdict(list)
for field in fields:
name = field.get('field_name')
if name:
grouped[name].append(field)
return grouped

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

if cached_names is not None:
return cached_names

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

cache.set(PARTNER_FIELD_NAMES_LIST_KEY, names, 600)

return names

def get_location(self, facility):
return facility.location

Expand Down Expand Up @@ -207,46 +334,23 @@ def get_contributor_fields(self, facility):
return fields

def get_extended_fields(self, facility):
request = self.context.get('request') \
if self.context is not None else None
request = self.__get_request()

use_main_created_at = is_created_at_main_date(self)
date_field_to_sort = (
'created_at' if use_main_created_at else 'updated_at'
date_field_to_sort = self.__date_field_to_sort(
use_main_created_at
)

embed = request.query_params.get('embed') \
if request is not None else None
contributor_id = request.query_params.get('contributor', None) \
if request is not None and embed == '1' else None
if contributor_id is None and request is not None and embed == '1':
contributor_ids = request.query_params.getlist('contributors', [])
if len(contributor_ids):
contributor_id = contributor_ids[0]

fields = facility.extended_fields
if contributor_id is not None:
fields = get_efs_associated_with_contributor(
int(contributor_id),
facility.extended_fields)
fields = self.__filter_contributor_extended_fields(
facility,
request
)

user_can_see_detail = can_user_see_detail(self)
embed_mode_active = is_embed_mode_active(self)

grouped_data = defaultdict(list)

def sort_order(k):
return (k.get('verified_count', 0), k.get('is_from_claim', False),
k.get('value_count', 1), k.get(date_field_to_sort, None))

def sort_order_excluding_date(k):
return (k.get('verified_count', 0), k.get('is_from_claim', False),
k.get('value_count', 1))

exclude_fields = []
if not use_main_created_at:
exclude_fields.append('created_at')

for field_name, _ in ExtendedField.FIELD_CHOICES:
filtered_fields = list(filter(
lambda field: field_name == field.get('field_name'), fields
Expand All @@ -255,7 +359,9 @@ def sort_order_excluding_date(k):
filtered_fields,
context={'user_can_see_detail': user_can_see_detail,
'embed_mode_active': embed_mode_active},
exclude_fields=exclude_fields
exclude_fields=(
['created_at'] if not use_main_created_at else []
)
)

if field_name == ExtendedField.NAME and not embed_mode_active:
Expand All @@ -274,7 +380,7 @@ def sort_order_excluding_date(k):
name_obj.get('updated_at'),
user_can_see_detail))
data = sorted(unsorted_data,
key=sort_order_excluding_date,
key=self.__sort_order_excluding_date,
reverse=True)
elif field_name == ExtendedField.ADDRESS and not embed_mode_active:
unsorted_data = serializer.data
Expand All @@ -292,15 +398,48 @@ def sort_order_excluding_date(k):
address_obj.get('updated_at'),
user_can_see_detail))
data = sorted(unsorted_data,
key=sort_order_excluding_date,
key=self.__sort_order_excluding_date,
reverse=True)
else:
data = sorted(serializer.data, key=sort_order, reverse=True)
data = sorted(
serializer.data,
key=lambda k: self.__sort_order(k, date_field_to_sort),
reverse=True
)

grouped_data[field_name] = data

return grouped_data

def get_partner_fields(self, facility):
request = self.__get_request()

use_main_created_at = is_created_at_main_date(self)
date_field_to_sort = self.__date_field_to_sort(
use_main_created_at
)

fields = self.__filter_contributor_extended_fields(
facility,
request
)
grouped_fields = self.__group_fields_by_name(
fields
)

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

return self.__serialize_and_sort_partner_fields(
grouped_fields,
field_names,
user_can_see_detail,
embed_mode_active,
use_main_created_at,
date_field_to_sort
)

def get_sector(self, facility):
user_can_see_detail = can_user_see_detail(self)

Expand Down Expand Up @@ -347,8 +486,7 @@ def get_contributors(self, facility):
if is_embed_mode_active(self):
return []

request = self.context.get('request') \
if self.context is not None else None
request = self.__get_request()
user = request.user if request is not None else None
if user is not None and not user.is_anonymous:
user_can_see_detail = user.can_view_full_contrib_details
Expand Down
Loading
Loading