Skip to content

Commit 570aabb

Browse files
vlad-shapikvladsha-devprotsack-stephan
authored
[OSDEV-2305] Add LiwingWage indicator support to the platform (#842)
[OSDEV-2305](https://opensupplyhub.atlassian.net/browse/OSDEV-2305) Introduced automatic wage indicator reference links on production location profiles for 171 countries. - Each production location now displays country-specific links to authoritative living wage and minimum wage information and its regional partner sites. - The wage indicator data is presented in both the national language and English, providing users with easy access to benchmarking information for fair wage assessments. - The links appear automatically based on the production location's country and are managed as a system-generated partner field that cannot be manually edited or deleted, ensuring data consistency and reliability across the platform. <img width="1071" height="719" alt="Screenshot 2025-12-18 at 18 08 44" src="https://github.com/user-attachments/assets/a8a1b91b-3b2d-4cdc-b1b9-956cf5878766" /> [OSDEV-2305]: https://opensupplyhub.atlassian.net/browse/OSDEV-2305?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: vladsha-dev <newshapovalov2002@gmail.com> Co-authored-by: Stephan Protsack <stephan@speedandfunction.com>
1 parent 28b22c3 commit 570aabb

22 files changed

Lines changed: 3893 additions & 691 deletions

doc/release/RELEASE-NOTES.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,20 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
99
* Product name: Open Supply Hub
1010
* Release date: January 10, 2026
1111

12+
### Database changes
13+
14+
#### Migrations
15+
* 0190_add_active_system_field_to_partnerfield.py - This migration adds two boolean fields to the `PartnerField` model: `active` (default True) to control whether a partner field is available for contributions and appears in listings, and `system_field` (default False) to mark system-managed fields that cannot be deleted and have restricted editing permissions. Along with the migration, a custom `PartnerFieldManager` class is introduced, which automatically filters queries to return only active partner fields by default, with an `all_including_inactive()` method to access all fields when needed.
16+
* 0191_create_wage_indicator_partner_field.py - This data migration creates the `wage_indicator` system partner field with type `object` and a comprehensive JSON schema defining six properties for wage indicator reference links. The field is marked as `system_field=True` and `active=True`, and includes source attribution.
17+
* 0192_create_wage_indicator_models.py - This migration creates two new models: `WageIndicatorCountryData` to store wage indicator URLs (living wage and minimum wage links in national and English languages) for each country indexed by ISO 3166-1 alpha-2 country code, and `WageIndicatorLinkTextConfig` to store customizable display text for each link type. The models support system-generated partner field data that will be automatically displayed on production location profiles based on the location's country.
18+
* 0193_populate_wage_indicator_data.py - This data migration populates the `WageIndicatorCountryData` table with wage indicator reference links for 171 countries. Each country entry includes URLs for living wage benchmarks and minimum wage information in both national languages and English where available. The migration also populates the `WageIndicatorLinkTextConfig` table with default display text for all three link types.
19+
20+
#### Schema changes
21+
* [OSDEV-2305](https://opensupplyhub.atlassian.net/browse/OSDEV-2305) - Added wage indicator system partner field infrastructure: The `PartnerField` model has been updated with two new boolean fields (`active` and `system_field`) to support system-managed partner fields. Two new models were introduced: `WageIndicatorCountryData` (stores wage indicator URLs for 171 countries) and `WageIndicatorLinkTextConfig` (stores customizable display text for wage indicator links). A new `wage_indicator` system partner field was created to display country-specific living wage and minimum wage reference links on production location profiles. The implementation includes a custom manager for filtering active partner fields, a provider registry pattern for system-generated fields, and admin panel protections to prevent deletion or unauthorized modification of system fields.
22+
1223
### Code/API changes
1324
* [Follow-up][OSDEV-2114](https://opensupplyhub.atlassian.net/browse/OSDEV-2114) - Removed the `reindex_locations_with_environmental_data` Django management command from the parent `post_deployment` command so it no longer runs, as it was only needed for the `2.17.0` release.
25+
* [OSDEV-2305](https://opensupplyhub.atlassian.net/browse/OSDEV-2305) - Enhanced the `GET /api/facilities/{os_id}` endpoint to support system-generated partner fields. The `partner_fields` object in the response now includes the `wage_indicator` field when a contributor is assigned to it, automatically providing country-specific wage indicator reference links (living wage and minimum wage URLs in both national language and English) based on the production location's country code. System partner fields are populated dynamically through a provider registry pattern and follow the same structure as user-contributed partner fields, appearing alongside them in the API response.
1426

1527
### Architecture/Environment changes
1628
* [OSDEV-2047](https://opensupplyhub.atlassian.net/browse/OSDEV-2047) - Removed all Terraform configurations and ECS service definitions related to the deprecated standalone ContriCleaner service. Cleaned up the repository by deleting unused code and references, as ContriCleaner now operates exclusively as an internal Django library.
@@ -20,6 +32,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
2032
* [OSDEV-2047](https://opensupplyhub.atlassian.net/browse/OSDEV-2047) - Previously, there were two security groups with the same tags: one for the Django app and another for ContriCleaner. After removing the ContriCleaner service infrastructure, a bug was eliminated in which the Django CLI task in the Development environment selected the wrong security group - the one without database access, belonging to ContriCleaner - which prevented Django management commands from running against the database in the Development environment.
2133

2234
### What's new
35+
* [OSDEV-2305](https://opensupplyhub.atlassian.net/browse/OSDEV-2305) - Introduced automatic wage indicator reference links on production location profiles for 171 countries. Each production location now displays country-specific links to authoritative living wage and minimum wage information and its regional partner sites. The wage indicator data is presented in both the national language and English, providing users with easy access to benchmarking information for fair wage assessments. The links appear automatically based on the production location's country and are managed as a system-generated partner field that cannot be manually edited or deleted, ensuring data consistency and reliability across the platform.
2336
* [OSDEV-2221](https://opensupplyhub.atlassian.net/browse/OSDEV-2221) - Updated post claim page `claimed/{claim-id}/`. This update includes:
2437
* Added emission data input fields with checkboxes. UI is consitent with other input fields in the `ClaimedFacilitiesDetails` component.
2538
* Applied Yup validation schema for `facility_website`, `point_of_contact_email`, `facility_workers_count`.
@@ -59,7 +72,7 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
5972

6073
#### Migrations
6174
* 0187_remove_null_from_contributor_partner_fields.py - This migration removes the unnecessary `null=True` parameter from the `partner_fields` ManyToManyField on the Contributor model.
62-
* 0188_introduce_indexing_for_new_operational_and_environmental_data.py - Updated the `index_claim_info` function to collect new environmental data (opening_date, closing_date, estimated_annual_throughput, actual_annual_energy_consumption) for the `api_facilityindex.claim_info` column.
75+
* 0188_introduce_indexing_for_new_operational_and_environmental_data.py - Updated the `index_claim_info` function to collect new environmental data (opening_date, closing_date, estimated_annual_throughput, actual_annual_energy_consumption) for the `api_facilityindex.claim_info` column.
6376

6477
### Code/API changes
6578
* [OSDEV-2280](https://opensupplyhub.atlassian.net/browse/OSDEV-2280) - Introduced a new reusable `ImportantNote` component in the new claim flow to replace custom implementations of important notes across multiple components (`BusinessStep`, `ContactInfoStep`, and `ClaimInfoSection`), improving code maintainability and consistency.

src/django/api/admin.py

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23

34
from django import forms
45
from django.urls import path
@@ -12,6 +13,10 @@
1213
from django.utils.translation import gettext as _
1314
from api.models.sector_group import SectorGroup
1415
from api.models.partner_field import PartnerField
16+
from api.models.wage_indicator_country_data import WageIndicatorCountryData
17+
from api.models.wage_indicator_link_text_config import (
18+
WageIndicatorLinkTextConfig
19+
)
1520
from allauth.account.models import EmailAddress
1621
from simple_history.admin import SimpleHistoryAdmin
1722
from waffle.models import Flag, Sample, Switch
@@ -23,6 +28,8 @@
2328

2429
from api.reports import get_report_names, run_report
2530

31+
logger = logging.getLogger(__name__)
32+
2633

2734
class ApiAdminSite(AdminSite):
2835
site_header = 'Open Supply Hub Admin'
@@ -270,18 +277,90 @@ class Meta:
270277
'source_by',
271278
'base_url',
272279
'display_text',
273-
'json_schema'
280+
'json_schema',
281+
'active',
282+
'system_field'
274283
]
275284

276285
def __init__(self, *args, **kwargs):
277286
super().__init__(*args, **kwargs)
278287

288+
def clean(self):
289+
'''
290+
Validate that protected fields of system fields are not modified.
291+
'''
292+
cleaned_data = super().clean()
293+
294+
if self.instance and self.instance.pk and self.instance.system_field:
295+
try:
296+
original = PartnerField.objects \
297+
.get_all_including_inactive() \
298+
.get(pk=self.instance.pk)
299+
300+
protected_fields = {
301+
'name': 'Name',
302+
'type': 'Type',
303+
'json_schema': 'JSON Schema',
304+
'system_field': 'System Field'
305+
}
306+
307+
for field_name, field_label in protected_fields.items():
308+
original_value = getattr(original, field_name)
309+
current_value = cleaned_data.get(field_name)
310+
311+
if original_value != current_value:
312+
self.add_error(
313+
field_name,
314+
f'{field_label} cannot be modified for '
315+
'system-defined fields. Editing this field may '
316+
'break the application or data display for users. '
317+
'Only label, unit, source by, base url, display '
318+
'text, and active can be edited.'
319+
)
320+
321+
except PartnerField.DoesNotExist:
322+
logger.warning(
323+
f'Partner field `{self.instance.pk}` not found. '
324+
'System field must exist in database.'
325+
)
326+
327+
return cleaned_data
328+
279329

280330
class PartnerFieldAdmin(admin.ModelAdmin):
281331
form = PartnerFieldAdminForm
282-
list_display = ('name', 'type', 'label', 'unit', 'source_by', 'created_at')
332+
list_display = ('name', 'type', 'label', 'unit', 'active', 'system_field',
333+
'created_at')
283334
search_fields = ('name', 'type', 'label', 'unit', 'source_by')
335+
list_filter = ('active', 'system_field', 'type')
284336
readonly_fields = ('uuid', 'created_at', 'updated_at')
337+
fields = ('name', 'type', 'unit', 'label', 'source_by', 'base_url',
338+
'display_text', 'json_schema', 'active', 'system_field',
339+
'created_at', 'updated_at')
340+
341+
def get_queryset(self, request):
342+
'''
343+
Override to show all partner fields including inactive ones in admin.
344+
'''
345+
qs = self.model.objects.get_all_including_inactive()
346+
ordering = self.get_ordering(request)
347+
if ordering:
348+
qs = qs.order_by(*ordering)
349+
return qs
350+
351+
def has_delete_permission(self, request, obj=None):
352+
'''
353+
Prevent deletion of system fields.
354+
'''
355+
if obj and obj.system_field:
356+
messages.warning(
357+
request,
358+
f'Partner field \'{obj.name}\' cannot be deleted because it '
359+
'is a system-defined field.'
360+
)
361+
return False
362+
363+
return super().has_delete_permission(request, obj)
285364

286365
class Media:
287366
js = (
@@ -296,6 +375,29 @@ class EmailAddressAdmin(admin.ModelAdmin):
296375
list_filter = ('verified', 'primary')
297376

298377

378+
class WageIndicatorCountryDataAdmin(admin.ModelAdmin):
379+
list_display = ('country_code', 'living_wage_link_national',
380+
'minimum_wage_link_english', 'minimum_wage_link_national')
381+
search_fields = ('country_code',)
382+
fields = ('country_code', 'living_wage_link_national',
383+
'minimum_wage_link_english', 'minimum_wage_link_national',
384+
'created_at', 'updated_at')
385+
386+
def get_readonly_fields(self, request, obj=None):
387+
'''
388+
Make country_code readonly when editing, but editable when creating.
389+
'''
390+
if obj:
391+
return ('country_code', 'created_at', 'updated_at')
392+
393+
return ('created_at', 'updated_at')
394+
395+
396+
class WageIndicatorLinkTextConfigAdmin(admin.ModelAdmin):
397+
list_display = ('link_type', 'display_text')
398+
search_fields = ('link_type', 'display_text')
399+
400+
299401
admin_site.register(models.Version)
300402
admin_site.register(models.User, OarUserAdmin)
301403
admin_site.register(models.Contributor, ContributorAdmin)
@@ -320,3 +422,7 @@ class EmailAddressAdmin(admin.ModelAdmin):
320422
admin_site.register(models.FacilityDownloadLimit, FacilityDownloadLimitAdmin)
321423
admin_site.register(PartnerField, PartnerFieldAdmin)
322424
admin_site.register(EmailAddress, EmailAddressAdmin)
425+
admin_site.register(WageIndicatorCountryData, WageIndicatorCountryDataAdmin)
426+
admin_site.register(
427+
WageIndicatorLinkTextConfig, WageIndicatorLinkTextConfigAdmin
428+
)

src/django/api/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,3 +300,4 @@ class APIV1MatchTypes:
300300
JS_MAX_SAFE_INTEGER = 9007199254740991
301301

302302
PARTNER_FIELD_LIST_KEY = 'partner_field_list'
303+
PARTNER_FIELD_NAMES_KEY = 'partner_field_names'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Generated by Django 3.2.17 on 2025-12-17 10:51
2+
3+
from django.db.migrations import Migration, AddField
4+
from django.db.models import BooleanField
5+
6+
7+
class Migration(Migration):
8+
9+
dependencies = [
10+
('api', '0189_add_base_url_and_text_field_to_partner_field'),
11+
]
12+
13+
operations = [
14+
AddField(
15+
model_name='partnerfield',
16+
name='active',
17+
field=BooleanField(
18+
default=True,
19+
help_text=(
20+
'Indicates if this partner field is active. '
21+
'Inactive fields are not available for contributions and '
22+
'will not appear in listings.'
23+
),
24+
),
25+
),
26+
AddField(
27+
model_name='partnerfield',
28+
name='system_field',
29+
field=BooleanField(
30+
default=False,
31+
help_text=(
32+
'Indicates if this is a system field. '
33+
'System fields cannot be deleted and have restricted '
34+
'editing.'
35+
),
36+
),
37+
),
38+
]
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Generated by Django 3.2.17 on 2025-12-17 11:19
2+
3+
from django.db.migrations import Migration, RunPython
4+
5+
6+
def create_wage_indicator_partner_field(apps, schema_editor):
7+
'''Create wage_indicator partner field.'''
8+
PartnerField = apps.get_model('api', 'PartnerField')
9+
10+
json_schema = {
11+
'$schema': 'https://json-schema.org/draft/2020-12/schema',
12+
'type': 'object',
13+
'title': 'Wage indicator reference links',
14+
'properties': {
15+
'living_wage_link_national': {
16+
'type': 'string',
17+
'format': 'uri',
18+
'title': 'Living wage reference link (national language)'
19+
},
20+
'minimum_wage_link_english': {
21+
'type': 'string',
22+
'format': 'uri',
23+
'title': 'Minimum wage reference link (English)'
24+
},
25+
'minimum_wage_link_national': {
26+
'type': 'string',
27+
'format': 'uri',
28+
'title': 'Minimum wage reference link (national language)'
29+
},
30+
'living_wage_link_national_text': {
31+
'type': 'string',
32+
'title': 'Living wage link text (national language)'
33+
},
34+
'minimum_wage_link_english_text': {
35+
'type': 'string',
36+
'title': 'Minimum wage link text (English)'
37+
},
38+
'minimum_wage_link_national_text': {
39+
'type': 'string',
40+
'title': 'Minimum wage link text (national language)'
41+
}
42+
}
43+
}
44+
45+
source_by = (
46+
'<p>2024 Minimum Wage Benchmark sourced by '
47+
'<a href="https://wageindicator.org/" target="_blank" '
48+
'title="WageIndicator">WageIndicator</a></p>'
49+
)
50+
51+
PartnerField.objects.create(
52+
name='wage_indicator',
53+
type='object',
54+
source_by=source_by,
55+
json_schema=json_schema,
56+
system_field=True
57+
)
58+
59+
60+
def reverse_wage_indicator_partner_field(apps, schema_editor):
61+
'''Remove wage_indicator partner field.'''
62+
PartnerField = apps.get_model('api', 'PartnerField')
63+
PartnerField.objects.filter(name='wage_indicator').delete()
64+
65+
66+
class Migration(Migration):
67+
68+
dependencies = [
69+
('api', '0190_add_active_system_field_to_partnerfield'),
70+
]
71+
72+
operations = [
73+
RunPython(
74+
create_wage_indicator_partner_field,
75+
reverse_wage_indicator_partner_field
76+
),
77+
]

0 commit comments

Comments
 (0)