Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5ea3e22
Show mit fields in v1 (partial imple)
VadimKovalenkoSNF Jan 9, 2026
7b70958
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 12, 2026
58c4f63
Minor refactoring
VadimKovalenkoSNF Jan 12, 2026
d4d4f88
Refactor __get_partner_fields method
VadimKovalenkoSNF Jan 12, 2026
34eccae
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 13, 2026
4fc446f
Fix unit tests
VadimKovalenkoSNF Jan 13, 2026
e3173b7
Minor fix
VadimKovalenkoSNF Jan 13, 2026
026614a
Apply rich living wage output
VadimKovalenkoSNF Jan 13, 2026
c57aea1
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 13, 2026
d78d659
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 15, 2026
7f19bf6
Add release notes
VadimKovalenkoSNF Jan 15, 2026
40819ff
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 16, 2026
3b58dd5
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 16, 2026
305e36e
Upd living wage response format
VadimKovalenkoSNF Jan 19, 2026
f629b79
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 19, 2026
e28ebfa
Minor restructuring of partner field methods
VadimKovalenkoSNF Jan 22, 2026
c615181
Merge branch 'main' into OSDEV-2329-mit-living-wage-production-locations
VadimKovalenkoSNF Jan 23, 2026
6c4e5be
Update response payload
VadimKovalenkoSNF Jan 27, 2026
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
136 changes: 136 additions & 0 deletions src/django/api/tests/test_production_locations_get.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import unittest.mock
from django.contrib.gis.geos import Point, MultiPolygon
from django.core.cache import cache
from rest_framework.test import APITestCase
from rest_framework import status
from api.views.v1.response_mappings.production_locations_response \
import ProductionLocationsResponseMapping
from api.models.partner_field import PartnerField
from api.models.contributor.contributor import Contributor
from api.models.facility.facility import Facility
from api.models.facility.facility_list import FacilityList
from api.models.facility.facility_list_item import FacilityListItem
from api.models.source import Source
from api.models.user import User
from api.models.us_county_tigerline import USCountyTigerline
from api.models.wage_indicator_country_data import WageIndicatorCountryData


OPEN_SEARCH_SERVICE = "api.views.v1.production_locations.OpenSearchService"
Expand Down Expand Up @@ -132,3 +143,128 @@ def test_single_production_location_response_mapping(self):
query_body["_source"],
ProductionLocationsResponseMapping.PRODUCTION_LOCATION_BY_OS_ID,
)

def _create_facility_with_partner_data(self):
cache.clear()
user = User.objects.create(email="[email protected]")
contributor = Contributor.objects.create(
admin=user,
name="Test Contributor",
description="",
website="",
contrib_type=Contributor.OTHER_CONTRIB_TYPE,
)

wage_indicator_field, _ = PartnerField.objects.get_or_create(
name="wage_indicator",
defaults={
"type": PartnerField.OBJECT,
"system_field": True,
"base_url": "https://wageindicator.example/",
}
)
mit_living_wage_field, _ = PartnerField.objects.get_or_create(
name="mit_living_wage",
defaults={
"type": PartnerField.OBJECT,
"system_field": True,
"base_url": "https://livingwage.mit.edu/counties/",
}
)

contributor.partner_fields.add(
wage_indicator_field,
mit_living_wage_field,
)

facility_list = FacilityList.objects.create(
name="Test List",
file_name="test.csv",
header="name,address,country",
)
source = Source.objects.create(
source_type=Source.LIST,
facility_list=facility_list,
contributor=contributor,
)
facility_list_item = FacilityListItem.objects.create(
name="Test Facility",
address="123 Example St",
country_code="US",
sector=["Apparel"],
row_index=0,
status=FacilityListItem.CONFIRMED_MATCH,
source=source,
raw_data="Test Facility,123 Example St,US",
raw_header="name,address,country",
raw_json={},
processing_results=[],
)

location = Point(-73.935242, 40.73061, srid=4326)
facility = Facility.objects.create(
id=self.os_id,
name="Test Facility",
address="123 Example St",
country_code="US",
location=location,
created_from=facility_list_item,
)

WageIndicatorCountryData.objects.update_or_create(
country_code=facility.country_code,
defaults={
"living_wage_link_national": (
"https://paywizard.org/salary/living-wages"
),
"minimum_wage_link_english": (
"https://wageindicator.org/salary/minimum-wage/"
"united-states-of-america"
),
"minimum_wage_link_national": (
"https://paywizard.org/salary/minimum-wage"
),
},
)

point_5070 = location.transform(5070, clone=True)
county_geometry = point_5070.buffer(1)
if county_geometry.geom_type != "MultiPolygon":
county_geometry = MultiPolygon(county_geometry)

USCountyTigerline.objects.create(
geoid="12345",
name="Test County",
geometry=county_geometry,
)

return facility

def test_get_single_production_location_includes_partner_fields(self):
facility = self._create_facility_with_partner_data()

self.search_index_mock.return_value = {
"count": 1,
"data": [
{
"os_id": facility.id,
"name": "location1",
}
],
}

url = f"/api/v1/production-locations/{facility.id}/"
api_res = self.client.get(url)

self.assertEqual(api_res.status_code, status.HTTP_200_OK)
self.assertIn("wage_indicator", api_res.data)
self.assertIn("mit_living_wage", api_res.data)

self.assertEqual(
api_res.data["wage_indicator"].get("living_wage_link_national"),
"https://paywizard.org/salary/living-wages",
)
self.assertEqual(
api_res.data["mit_living_wage"].get("county_id"),
"12345",
)
77 changes: 53 additions & 24 deletions src/django/api/views/v1/production_locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
from api.constants import (
APIV1CommonErrorMessages,
NON_FIELD_ERRORS_KEY,
APIV1LocationContributionErrorMessages
APIV1LocationContributionErrorMessages,
PARTNER_FIELD_NAMES_KEY,
)
from api.exceptions import ServiceUnavailableException
from api.mail import (
Expand All @@ -49,6 +50,7 @@
)
from api.views.v1.response_mappings.production_locations_response import \
ProductionLocationsResponseMapping
from api.partner_fields.registry import system_partner_field_registry


class ProductionLocations(ViewSet):
Expand All @@ -65,6 +67,22 @@ def __init_opensearch() -> Tuple[OpenSearchService,

return (opensearch_service, opensearch_query_director)

@staticmethod
def __add_partner_field_value(partner_extended_fields, field_name, value):
"""
Extract and add partner field value to the response dictionary.
"""
if not field_name or not isinstance(value, dict):
return

raw_values = value.get('raw_values')
raw_value = value.get('raw_value')

if isinstance(raw_values, (list, dict)):
partner_extended_fields[field_name] = raw_values
elif raw_value is not None:
partner_extended_fields[field_name] = raw_value

def get_permissions(self):
'''
Redefines the parent method and returns the list of permissions for
Expand Down Expand Up @@ -277,10 +295,10 @@ def partial_update(self, request, pk=None):
status=result.status_code
)

def __get_partner_fields(self, pk):
def __get_partner_fields(self, pk, facility: Facility = None):
"""
Checks and returns partner extended fields for a
facility object by its ID.
facility object by its ID or by the provided Facility instance.

Caches the list of partner field names for one hour.
Returns a dictionary of the form:
Expand All @@ -290,7 +308,7 @@ def __get_partner_fields(self, pk):
...
}
"""
cache_key = 'partner_field_names'
cache_key = PARTNER_FIELD_NAMES_KEY
partner_field_names = cache.get(cache_key)

if partner_field_names is None:
Expand All @@ -299,27 +317,38 @@ def __get_partner_fields(self, pk):
)
cache.set(cache_key, partner_field_names, 60 * 60)

if not partner_field_names:
return {}

partner_field_values = ExtendedField.objects.filter(
facility__id=pk,
field_name__in=partner_field_names
).values('field_name', 'value')

partner_extended_fields = {}

for field in partner_field_values:
field_name = field['field_name']
value = field['value']

if not isinstance(value, dict):
continue
if partner_field_names:
partner_field_values = ExtendedField.objects.filter(
facility__id=pk,
field_name__in=partner_field_names
).values('field_name', 'value')

for field in partner_field_values:
self.__add_partner_field_value(
partner_extended_fields,
field.get('field_name'),
field.get('value'),
)

facility = Facility.objects.filter(id=pk).only(
'id',
'country_code',
'location',
).first()

if facility:
for provider in system_partner_field_registry.providers:
provider_data = provider.fetch_data(facility)

if provider_data is None:
continue

self.__add_partner_field_value(
partner_extended_fields,
provider_data.get('field_name'),
provider_data.get('value'),
)

if 'raw_values' in value and isinstance(
value['raw_values'], (list, dict)
):
partner_extended_fields[field_name] = value['raw_values']
elif 'raw_value' in value:
partner_extended_fields[field_name] = value['raw_value']
return partner_extended_fields
Loading