Skip to content

Commit 6530fa9

Browse files
Merge branch 'main' into OSDEV-2353-production-location-skeleton-page
2 parents 5262030 + 2dcd34e commit 6530fa9

9 files changed

Lines changed: 309 additions & 108 deletions

doc/release/RELEASE-NOTES.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
1313

1414
#### Migrations
1515
* 0199_add_production_location_page_switch.py - Adds `enable_production_location_page` feature flag to redirect FE route of `facilities/:osID` to the `production-locations/:osID`.
16+
* 0200_introduce_indexing_of_the_creation_date_of_the_claim_request.py - Updated the `index_claim_info` function to include the claim request creation date in the `api_facilityindex.claim_info` column.
17+
18+
### Code/API changes
19+
* [OSDEV-2355](https://opensupplyhub.atlassian.net/browse/OSDEV-2355) - The following changes have been made:
20+
* Updated the GET `api/facilities/` and `api/facilities/{os_id}/` endpoints to include `contributor_type` (the raw type from the database) for both public and anonymous sources. Each contributor entry now also includes a `count` field (1 for public contributors and an aggregated count for anonymous entries of the same type), allowing the front end to display and sum counts by type (e.g., “18 Brands”, “9 Suppliers”).
21+
* Additionally, updated GET `api/facilities/{os_id}/` to return the claim request creation date. All of this information is required for the redesigned Production Location page - specifically for the claim banner - as well as for the supply chain network.
22+
23+
### Architecture/Environment changes
24+
* Increased the CPU and memory allocation for the DedupeHub container to `8 CPU` and `40 GB` in the Terraform deployment configuration to address memory overload issues during production location reindexing for the `Test` environment.
1625

1726
### What's new
1827
* [OSDEV-2352](https://opensupplyhub.atlassian.net/browse/OSDEV-2352) - Added feature flag named `enable_production_location_page` to enable production location pages with the new design. When the feature flag is enabled in the Django admin panel:
@@ -21,13 +30,11 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
2130
* Previously opened facility pages at `/facilities/:osID` will redirect to `/production-locations/:osID` after page refresh.
2231
* When the feature flag is disabled, accessing `/production-locations/:osID` routes will result in a "Not found" page with no automatic redirection to the legacy `/facilities/:osID` route.
2332

24-
### Architecture/Environment changes
25-
* Increased the CPU and memory allocation for the DedupeHub container to `8 CPU` and `40 GB` in the Terraform deployment configuration to address memory overload issues during production location reindexing for the `Test` environment.
26-
2733
### Release instructions
2834
* Ensure that the following commands are included in the `post_deployment` command:
2935
* `migrate`
3036
* `reindex_database`
37+
* `reindex_locations_with_approved_claim`
3138

3239

3340
## Release 2.19.0

src/django/api/management/commands/post_deployment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ class Command(BaseCommand):
88
'post-deployment tasks.')
99

1010
def handle(self, *args, **options):
11-
call_command('delete_emailaddress_for_deleted_users')
1211
call_command('migrate')
1312
call_command('reindex_database')
13+
call_command('reindex_locations_with_approved_claim')

src/django/api/management/commands/reindex_locations_with_environmental_data.py renamed to src/django/api/management/commands/reindex_locations_with_approved_claim.py

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from django.core.management.base import BaseCommand
2-
from django.db.models import Q
32

43
from api.constants import FacilityClaimStatuses
54
from api.models.facility.facility_claim import FacilityClaim
@@ -8,9 +7,7 @@
87

98
class Command(BaseCommand):
109
help = (
11-
'Reindexes only those locations that have approved claims '
12-
'containing environmental data (energy consumption, throughput, '
13-
'opening/closing dates).'
10+
'Reindexes only those locations that have approved claims.'
1411
)
1512

1613
def add_arguments(self, parser):
@@ -27,54 +24,28 @@ def handle(self, *args, **options):
2724
dry_run = options.get('dry_run', False)
2825

2926
self.stdout.write(
30-
'Searching for approved claims with environmental data...'
27+
'Searching for approved claims...'
3128
)
3229

33-
# Query for approved location claims that have at least one
34-
# environmental field populated.
35-
environmental_fields_filter = (
36-
Q(opening_date__isnull=False) |
37-
Q(closing_date__isnull=False) |
38-
Q(estimated_annual_throughput__isnull=False) |
39-
Q(energy_coal__isnull=False) |
40-
Q(energy_natural_gas__isnull=False) |
41-
Q(energy_diesel__isnull=False) |
42-
Q(energy_kerosene__isnull=False) |
43-
Q(energy_biomass__isnull=False) |
44-
Q(energy_charcoal__isnull=False) |
45-
Q(energy_animal_waste__isnull=False) |
46-
Q(energy_electricity__isnull=False) |
47-
Q(energy_other__isnull=False)
48-
)
49-
50-
approved_claims_with_env_data = (
30+
location_ids = list(
5131
FacilityClaim.objects
5232
.filter(status=FacilityClaimStatuses.APPROVED)
53-
.filter(environmental_fields_filter)
54-
.select_related('facility')
55-
)
56-
57-
# Extract location IDs (facility_id field).
58-
location_ids = list(
59-
approved_claims_with_env_data
60-
.order_by('facility_id')
6133
.values_list('facility_id', flat=True)
6234
.distinct()
6335
)
6436

6537
if not location_ids:
6638
self.stdout.write(
6739
self.style.WARNING(
68-
'No locations found with approved claims containing '
69-
'environmental data.'
40+
'No locations found with approved claims.'
7041
)
7142
)
7243
return
7344

7445
self.stdout.write(
7546
self.style.SUCCESS(
7647
f'Found {len(location_ids)} locations with '
77-
'environmental data:'
48+
'approved claims.'
7849
)
7950
)
8051

@@ -98,6 +69,6 @@ def handle(self, *args, **options):
9869
self.stdout.write(
9970
self.style.SUCCESS(
10071
f'Successfully reindexed {len(location_ids)} '
101-
'locations with environmental data.'
72+
'locations with approved claims.'
10273
)
10374
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Generated by Django 5.2.10 on 2026-02-18 10:54
2+
3+
from django.db.migrations import Migration, RunPython
4+
from django.db import connection
5+
6+
7+
from api.migrations._migration_helper import MigrationHelper
8+
9+
helper = MigrationHelper(connection)
10+
11+
12+
def update_indexing_function(apps, schema_editor):
13+
'''
14+
This function replaces the old index_claim_info function with a similar
15+
one that additionally indexes the creation date of the claim request.
16+
'''
17+
18+
helper.run_sql_files([
19+
'0200_index_claim_info.sql'
20+
])
21+
22+
23+
def revert_updating_indexing_function(apps, schema_editor):
24+
helper.run_sql_files([
25+
'0188_index_claim_info.sql'
26+
])
27+
28+
29+
class Migration(Migration):
30+
31+
dependencies = [
32+
('api', '0199_add_production_location_page_switch'),
33+
]
34+
35+
operations = [
36+
RunPython(update_indexing_function,
37+
revert_updating_indexing_function)
38+
]

src/django/api/serializers/facility/facility_index_serializer.py

Lines changed: 66 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -334,81 +334,80 @@ def get_contributors(self, facility):
334334
if is_embed_mode_active(self):
335335
return []
336336

337-
request = self._get_request()
338-
user = request.user if request is not None else None
339-
if user is not None and not user.is_anonymous:
340-
user_can_see_detail = user.can_view_full_contrib_details
341-
else:
342-
user_can_see_detail = True
337+
user_can_see_detail = can_user_see_detail(self)
343338

344-
def format_source(contributor):
345-
if type(contributor) is not str:
339+
def format_source(source):
340+
if source.get('admin_id') is None:
346341
return {
347-
'id': contributor['admin_id']
348-
if contributor['admin_id'] else None,
349-
'name': contributor['name'],
350-
'is_verified': contributor['is_verified']
351-
if contributor['is_verified'] else False,
352-
'contributor_name': contributor['contributor_name']
353-
if contributor['contributor_name']
354-
else '[Unknown Contributor]',
355-
'list_name': contributor['list_name']
356-
if contributor['list_name'] else None,
342+
'name': source.get('name'),
343+
'contributor_type': source.get('contributor_type') or None,
344+
'count': source.get('count', 1),
357345
}
358346
return {
359-
'name': contributor,
347+
'id': source.get('admin_id') or None,
348+
'name': source.get('name'),
349+
'is_verified': bool(source.get('is_verified')),
350+
'contributor_name': source.get('contributor_name')
351+
or '[Unknown Contributor]',
352+
'contributor_type': source.get('contrib_type') or None,
353+
'list_name': source.get('list_name') or None,
354+
'count': 1,
360355
}
361356

362-
res = [contributor
363-
for contributor in facility.contributors
364-
if contributor['id'] is not None]
365-
res.sort(key=lambda x: (x['id'], x['fl_id'] or 0))
366-
367-
public_contributors = []
368-
anonymous_contributors = []
369-
for contributor in res:
370-
if ((contributor['should_display_associations'] is True)
371-
and user_can_see_detail):
372-
public_contributors.append(contributor)
373-
else:
374-
anonymous_contributors.append(contributor)
375-
376-
p_contributors_names = [contributor['name']
377-
for contributor in public_contributors]
378-
p_contributors_id = [contributor['id']
379-
for contributor in public_contributors]
380-
anonymous_contributors_type = []
381-
for anon_contributor in anonymous_contributors:
382-
if (anon_contributor['name'] not in p_contributors_names
383-
and anon_contributor['id'] not in p_contributors_id):
384-
p_contributors_names.append(anon_contributor['name'])
385-
p_contributors_id.append(anon_contributor['id'])
386-
anonymous_contributors_type.append(
387-
anon_contributor['contrib_type'])
388-
389-
from api.models.contributor.contributor import Contributor
390-
anonymous_contributors_type = [
391-
Contributor.prefix_with_count(name, len(list(x)))
392-
for name, x in groupby(sorted(anonymous_contributors_type))
357+
valid_contributors = [
358+
contributor for contributor in facility.contributors
359+
if contributor.get('id') is not None
393360
]
361+
valid_contributors.sort(
362+
key=lambda contributor: (
363+
contributor['id'],
364+
contributor.get('fl_id') or 0,
365+
)
366+
)
367+
368+
seen_public_names = set()
369+
distinct_public = []
370+
public_names = set()
371+
public_ids = set()
372+
anonymous_types = []
394373

395-
distinct_p_contributors_name = []
396-
distinct_p_contributors = []
397-
for pc in public_contributors:
398-
if pc['name'] not in distinct_p_contributors_name:
399-
distinct_p_contributors_name.append(pc['name'])
400-
distinct_p_contributors.append(pc)
401-
402-
sources = distinct_p_contributors + anonymous_contributors_type
403-
distinct_names = []
404-
distinct_sources = []
405-
formatted_sources = [
406-
format_source(source) for source in sources]
407-
for formatted_source in formatted_sources:
408-
if formatted_source['name'] not in distinct_names:
409-
distinct_names.append(formatted_source['name'])
410-
distinct_sources.append(formatted_source)
411-
return formatted_sources
374+
for contributor in valid_contributors:
375+
is_public = (
376+
contributor.get('should_display_associations') is True
377+
and user_can_see_detail
378+
)
379+
if is_public:
380+
if contributor['name'] not in seen_public_names:
381+
seen_public_names.add(contributor['name'])
382+
distinct_public.append(contributor)
383+
public_names.add(contributor['name'])
384+
public_ids.add(contributor['id'])
385+
else:
386+
if (contributor['name'] not in public_names
387+
and contributor['id'] not in public_ids):
388+
public_names.add(contributor['name'])
389+
public_ids.add(contributor['id'])
390+
anonymous_types.append(contributor.get('contrib_type'))
391+
392+
anonymous_entries = []
393+
for contrib_type, group in groupby(sorted(anonymous_types)):
394+
group_list = list(group)
395+
count = len(group_list)
396+
anonymous_entries.append({
397+
'name': Contributor.prefix_with_count(contrib_type, count),
398+
'contributor_type': contrib_type,
399+
'count': count,
400+
})
401+
402+
sources = distinct_public + anonymous_entries
403+
seen_names = set()
404+
result = []
405+
for source in sources:
406+
formatted = format_source(source)
407+
if formatted['name'] not in seen_names:
408+
seen_names.add(formatted['name'])
409+
result.append(formatted)
410+
return result
412411

413412
def _get_request(self):
414413
if self.context is None:

src/django/api/tests/test_approved_facility_claim.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,14 @@ def test_clears_location(self):
295295
json.loads(updated_facility_location.geojson),
296296
json.loads(original_facility_location.geojson),
297297
)
298+
299+
@override_switch("claim_a_facility", active=True)
300+
def test_claim_request_creation_date_is_returned(self):
301+
self.facility_claim.status = FacilityClaimStatuses.APPROVED
302+
self.facility_claim.save()
303+
304+
response = self.client.get(
305+
"/api/facilities/{}/".format(self.facility_claim.facility.id)
306+
).json()["properties"]["claim_info"]["created_at"]
307+
308+
self.assertIsNotNone(response)

src/django/api/tests/test_facility_search_contributor.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ def test_multiple(self):
165165
contributors = self.fetch_facility_contributors(self.facility)
166166
self.assertEqual(1, len(contributors))
167167
self.assertEqual("2 Others", contributors[0].get("name"))
168+
self.assertEqual(contributors[0]["contributor_type"], "Other")
169+
self.assertEqual(contributors[0]["count"], 2)
168170

169171
def test_private_user(self):
170172
self.client.login(
@@ -208,3 +210,19 @@ def get_facility_count():
208210
self.source.is_active = False
209211
self.source.save()
210212
self.assertEqual(0, get_facility_count())
213+
214+
def test_public_contributor_has_count_and_contributor_type(self):
215+
self.contributor.contrib_type = "Brand / Retailer"
216+
self.contributor.name = "Public Contributor"
217+
self.contributor.save()
218+
contributors = self.fetch_facility_contributors(self.facility)
219+
self.assertEqual(1, len(contributors))
220+
self.assertEqual(
221+
contributors[0]["contributor_type"],
222+
"Brand / Retailer",
223+
)
224+
self.assertEqual(
225+
contributors[0]["contributor_name"],
226+
"Public Contributor",
227+
)
228+
self.assertEqual(contributors[0]["count"], 1)

src/django/api/views/facility/facilities_view_set.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ def list(self, request):
188188
"name": "contributor_list_name",
189189
"is_verified": false,
190190
"contributor_name": "contributor_name",
191-
"list_name": "list_name"
191+
"list_name": "list_name",
192+
"contributor_type": "contributor_type",
193+
"count": 1
192194
}
193195
],
194196
"has_approved_claim": false,
@@ -326,7 +328,9 @@ def retrieve(self, request, pk=None):
326328
{
327329
"id": 1,
328330
"name": "Brand A (2019 Q1 List)",
329-
"is_verified": true
331+
"is_verified": true,
332+
"contributor_type": "Brand/Retailer",
333+
"count": 1
330334
}
331335
]
332336
}

0 commit comments

Comments
 (0)