Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b91f2a0
Implemented basic wage indicator mapping logic
vladsha-dev Dec 15, 2025
0344ae7
Refactored code
vladsha-dev Dec 16, 2025
0df08d2
Merge branch 'main' into OSDEV-2305-upload-the-wageindicator-data-int…
vlad-shapik Dec 16, 2025
e688f10
Updated FE to render multiple links one under another. Added link tex…
vladsha-dev Dec 16, 2025
c980a9d
Added active and system_field flag for partner fields; added dynamic …
vladsha-dev Dec 17, 2025
88638ae
Reworked validation for the PartnerField form; moved getting contribu…
vladsha-dev Dec 18, 2025
ad6a125
Created tests
vladsha-dev Dec 18, 2025
c479427
Handled coderabbit comments
vladsha-dev Dec 18, 2025
ebe486c
Updated release notes and naming
vladsha-dev Dec 18, 2025
99b2838
Fixed tests
vladsha-dev Dec 18, 2025
d989054
Fixed tests
vladsha-dev Dec 18, 2025
7083459
Fixed tests
vladsha-dev Dec 18, 2025
5d5110d
Added prints to debug
vladsha-dev Dec 18, 2025
6bddcd9
Added more prints
vladsha-dev Dec 18, 2025
fce929c
Fixed tests
vladsha-dev Dec 18, 2025
1430956
Fixed tests
vladsha-dev Dec 18, 2025
72d7691
Updated release notes
vladsha-dev Dec 18, 2025
f6ea4aa
Merge main
protsack-stephan Dec 26, 2025
133a4b5
Change labels in the Django admin
protsack-stephan Dec 26, 2025
c9d716b
Cleanup admin.py file from unneeded comments
protsack-stephan Dec 26, 2025
a7d3419
Refacot the partner_field.py to be more readable
protsack-stephan Dec 26, 2025
7947b5f
Fix review comments
protsack-stephan Dec 26, 2025
57f221a
Fix linting issues
protsack-stephan Dec 26, 2025
8e87a43
Fix the issue with base provider
protsack-stephan Dec 26, 2025
024651d
Fix linting issues and change errors to warnings in logs
protsack-stephan Dec 26, 2025
0134c9d
Merge branch 'main' of github.com:opensupplyhub/open-supply-hub into …
protsack-stephan Dec 26, 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
15 changes: 15 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,30 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
* Product name: Open Supply Hub
* Release date: January 10, 2026

### Database changes

#### Migrations
* 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.
* 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.
* 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.
* 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.

#### Schema changes
* [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.

### Code/API changes
* [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.
* [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.

### Architecture/Environment changes
* [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.

### Bugfix
* [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.

### What's new
* [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.

### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
* `migrate`
Expand Down
108 changes: 106 additions & 2 deletions src/django/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
from django.utils.translation import gettext as _
from api.models.sector_group import SectorGroup
from api.models.partner_field import PartnerField
from api.models.wage_indicator_country_data import WageIndicatorCountryData
from api.models.wage_indicator_link_text_config import (
WageIndicatorLinkTextConfig
)
from allauth.account.models import EmailAddress
from simple_history.admin import SimpleHistoryAdmin
from waffle.models import Flag, Sample, Switch
Expand Down Expand Up @@ -270,18 +274,91 @@ class Meta:
'source_by',
'base_url',
'display_text',
'json_schema'
'json_schema',
'active',
'system_field'
]

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

def clean(self):
'''
Validate that protected fields of system fields are not modified.
'''
cleaned_data = super().clean()

# Only validate for existing system fields.
if self.instance and self.instance.pk and self.instance.system_field:
from api.models.partner_field import PartnerField

try:
# Get the original instance from database.
original = PartnerField.objects \
.get_all_including_inactive() \
.get(pk=self.instance.pk)

protected_fields = {
'name': 'Name',
'type': 'Type',
'json_schema': 'JSON Schema',
'system_field': 'System Field'
}

for field_name, field_label in protected_fields.items():
original_value = getattr(original, field_name)
current_value = cleaned_data.get(field_name)

if original_value != current_value:
# Add field-specific error.
self.add_error(
field_name,
f'{field_label} cannot be modified for '
'system-defined fields. Editing this field may '
'break the application or data display for users. '
'Only label, unit, source by, base url, display '
'text, and active can be edited.'
)

except PartnerField.DoesNotExist:
pass

return cleaned_data


class PartnerFieldAdmin(admin.ModelAdmin):
form = PartnerFieldAdminForm
list_display = ('name', 'type', 'label', 'unit', 'source_by', 'created_at')
list_display = ('name', 'type', 'label', 'unit', 'active', 'system_field',
'created_at')
search_fields = ('name', 'type', 'label', 'unit', 'source_by')
list_filter = ('active', 'system_field', 'type')
readonly_fields = ('uuid', 'created_at', 'updated_at')
fields = ('name', 'type', 'unit', 'label', 'source_by', 'base_url',
'display_text', 'json_schema', 'active', 'system_field',
'created_at', 'updated_at')

def get_queryset(self, request):
'''
Override to show all partner fields including inactive ones in admin.
'''
qs = self.model.objects.get_all_including_inactive()
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs

def has_delete_permission(self, request, obj=None):
'''
Prevent deletion of system fields.
'''
if obj and obj.system_field:
messages.warning(
request,
f'Partner field \'{obj.name}\' cannot be deleted because it '
'is a system-defined field.'
)
return False
return super().has_delete_permission(request, obj)

class Media:
js = (
Expand All @@ -296,6 +373,29 @@ class EmailAddressAdmin(admin.ModelAdmin):
list_filter = ('verified', 'primary')


class WageIndicatorCountryDataAdmin(admin.ModelAdmin):
list_display = ('country_code', 'living_wage_link_national',
'minimum_wage_link_english', 'minimum_wage_link_national')
search_fields = ('country_code',)
fields = ('country_code', 'living_wage_link_national',
'minimum_wage_link_english', 'minimum_wage_link_national',
'created_at', 'updated_at')

def get_readonly_fields(self, request, obj=None):
'''
Make country_code readonly when editing, but editable when creating.
'''
if obj: # Editing existing object.
return ('country_code', 'created_at', 'updated_at')
# Creating new object.
return ('created_at', 'updated_at')


class WageIndicatorLinkTextConfigAdmin(admin.ModelAdmin):
list_display = ('link_type', 'display_text')
search_fields = ('link_type', 'display_text')


admin_site.register(models.Version)
admin_site.register(models.User, OarUserAdmin)
admin_site.register(models.Contributor, ContributorAdmin)
Expand All @@ -320,3 +420,7 @@ class EmailAddressAdmin(admin.ModelAdmin):
admin_site.register(models.FacilityDownloadLimit, FacilityDownloadLimitAdmin)
admin_site.register(PartnerField, PartnerFieldAdmin)
admin_site.register(EmailAddress, EmailAddressAdmin)
admin_site.register(WageIndicatorCountryData, WageIndicatorCountryDataAdmin)
admin_site.register(
WageIndicatorLinkTextConfig, WageIndicatorLinkTextConfigAdmin
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 3.2.17 on 2025-12-17 10:51

from django.db.migrations import Migration, AddField
from django.db.models import BooleanField


class Migration(Migration):

dependencies = [
('api', '0189_add_base_url_and_text_field_to_partner_field'),
]

operations = [
AddField(
model_name='partnerfield',
name='active',
field=BooleanField(
default=True,
help_text=(
'Indicates if this partner field is active. '
'Inactive fields are not available for contributions and '
'will not appear in listings.'
),
),
),
AddField(
model_name='partnerfield',
name='system_field',
field=BooleanField(
default=False,
help_text=(
'Indicates if this is a system field. '
'System fields cannot be deleted and have restricted '
'editing.'
),
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Generated by Django 3.2.17 on 2025-12-17 11:19

from django.db.migrations import Migration, RunPython


def create_wage_indicator_partner_field(apps, schema_editor):
'''Create wage_indicator partner field.'''
PartnerField = apps.get_model('api', 'PartnerField')

json_schema = {
'$schema': 'https://json-schema.org/draft/2020-12/schema',
'type': 'object',
'title': 'Wage indicator reference links',
'properties': {
'living_wage_link_national': {
'type': 'string',
'format': 'uri',
'title': 'Living wage reference link (national language)'
},
'minimum_wage_link_english': {
'type': 'string',
'format': 'uri',
'title': 'Minimum wage reference link (English)'
},
'minimum_wage_link_national': {
'type': 'string',
'format': 'uri',
'title': 'Minimum wage reference link (national language)'
},
'living_wage_link_national_text': {
'type': 'string',
'title': 'Living wage link text (national language)'
},
'minimum_wage_link_english_text': {
'type': 'string',
'title': 'Minimum wage link text (English)'
},
'minimum_wage_link_national_text': {
'type': 'string',
'title': 'Minimum wage link text (national language)'
}
}
}

source_by = (
'<p>2024 Minimum Wage Benchmark sourced by '
'<a href="https://wageindicator.org/" target="_blank" '
'title="WageIndicator">WageIndicator</a></p>'
)

PartnerField.objects.create(
name='wage_indicator',
type='object',
source_by=source_by,
json_schema=json_schema,
system_field=True
)


def reverse_wage_indicator_partner_field(apps, schema_editor):
'''Remove wage_indicator partner field.'''
PartnerField = apps.get_model('api', 'PartnerField')
PartnerField.objects.filter(name='wage_indicator').delete()


class Migration(Migration):

dependencies = [
('api', '0190_add_active_system_field_to_partnerfield'),
]

operations = [
RunPython(
create_wage_indicator_partner_field,
reverse_wage_indicator_partner_field
),
]
Loading