Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
db16210
Introduce __check_all_contributor_facilities method
VadimKovalenkoSNF Aug 5, 2025
3427b1f
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
VadimKovalenkoSNF Aug 5, 2025
ee2b807
Check same contributor on BE, add tests
VadimKovalenkoSNF Aug 5, 2025
2fd38f7
Update release notes
VadimKovalenkoSNF Aug 5, 2025
ae3f51a
Update Release instructions section
VadimKovalenkoSNF Aug 5, 2025
31bfd72
Minor optimization for is_same_contributor, test fixes
VadimKovalenkoSNF Aug 6, 2025
a9c7525
Fix release notes
VadimKovalenkoSNF Aug 6, 2025
ca844d8
Minor refactoring of release notes
VadimKovalenkoSNF Aug 6, 2025
8615b4a
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
VadimKovalenkoSNF Aug 6, 2025
57a15de
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
VadimKovalenkoSNF Aug 13, 2025
cff28f6
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
VadimKovalenkoSNF Aug 15, 2025
a5763ab
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
VadimKovalenkoSNF Aug 18, 2025
fe87bed
Make variables name more explicit
VadimKovalenkoSNF Aug 18, 2025
83ee7d6
Users can download their list items even if they reach download limit
VadimKovalenkoSNF Aug 18, 2025
a729d01
Update FE to handle download limit
VadimKovalenkoSNF Aug 18, 2025
1c2eac1
Fix Download button conditional rendering (partial impl)
VadimKovalenkoSNF Aug 19, 2025
401f697
Add is_same_contributor to the GET api/facility
VadimKovalenkoSNF Aug 19, 2025
51ca50e
Update tooltip for free countributor downloads
VadimKovalenkoSNF Aug 19, 2025
3ce912a
Add FE tests, update serialization in api/facilities
VadimKovalenkoSNF Aug 19, 2025
9de8402
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
VadimKovalenkoSNF Aug 19, 2025
b20697a
Refactor LoginRequiredDialog component
VadimKovalenkoSNF Aug 19, 2025
62be8c1
Remove redundant hook
VadimKovalenkoSNF Aug 19, 2025
9100059
Add props validation
VadimKovalenkoSNF Aug 19, 2025
dcd7366
Update unit test, fix lint issues
VadimKovalenkoSNF Aug 19, 2025
b41c20a
feat(api, fe): add is_same_contributor support, enforce download caps…
VadimKovalenkoSNF Aug 20, 2025
729f8fb
Refactor GET api/facilities
VadimKovalenkoSNF Aug 20, 2025
e8adcfb
Update DownloadButtonWithFlags
VadimKovalenkoSNF Aug 20, 2025
55a6a6f
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
VadimKovalenkoSNF Sep 2, 2025
b725c55
Fix list query
VadimKovalenkoSNF Sep 2, 2025
94bd8d4
Fix register_download_if_needed
VadimKovalenkoSNF Sep 2, 2025
90c88e4
Minor code prettify
VadimKovalenkoSNF Sep 2, 2025
3828351
Merge branch 'main' into OSDEV-2068-users-can-download-their-lists-fo…
roman-stolar Sep 2, 2025
fbe14e3
Pass actual returned row count
VadimKovalenkoSNF Sep 3, 2025
7d8a6aa
Fix multipage downloads quota
VadimKovalenkoSNF Sep 3, 2025
e9522a9
Fix unit tests
VadimKovalenkoSNF Sep 3, 2025
2ac4fab
Fix flake8 issues
VadimKovalenkoSNF Sep 3, 2025
f97dbdd
Update unit tests for exhausted quota
VadimKovalenkoSNF Sep 3, 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
2 changes: 2 additions & 0 deletions doc/release/RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html

### Code/API changes
* [OSDEV-2137](https://opensupplyhub.atlassian.net/browse/OSDEV-2137) - Switched to a custom, page-compatible keyset for the `/facilities-downloads` endpoint, enabling more efficient, cursor-based pagination and improved download performance and compatibility.
* [OSDEV-2068](https://opensupplyhub.atlassian.net/browse/OSDEV-2068) - Enabled users to download their own data without impacting free & purchased data-download allowances. Introduced `is_same_contributor` field in the GET `/api/facilities-downloads` response.


### Release instructions
* Ensure that the following commands are included in the `post_deployment` command:
Expand Down
42 changes: 30 additions & 12 deletions src/django/api/facilities_download_view_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
FacilityDownloadSerializerEmbedMode
from api.serializers.utils import get_embed_contributor_id_from_query_params
from api.services.facilities_download_service import FacilitiesDownloadService
from api.serializers.facility.utils import is_same_contributor_for_queryset
from api.constants import PaginationConfig


Expand Down Expand Up @@ -44,10 +45,17 @@ def list(self, request):

base_qs = FacilitiesDownloadService.get_filtered_queryset(request)

is_same_contributor = is_same_contributor_for_queryset(
base_qs,
request
)

limit = None

if (
not switch_is_active('private_instance')
and not self.__is_embed_mode()
and not is_same_contributor
):
limit = FacilitiesDownloadService.get_download_limit(request)

Expand Down Expand Up @@ -78,9 +86,14 @@ def list(self, request):
)

list_serializer = self.get_serializer(items)
rows = [f['row'] for f in list_serializer.data]
rows = [facility_data['row'] for facility_data in list_serializer.data]
headers = list_serializer.child.get_headers()
data = {'rows': rows, 'headers': headers}

data = {
'rows': rows,
'headers': headers,
'is_same_contributor': is_same_contributor
}

payload = {
'next': next_link,
Expand All @@ -94,18 +107,23 @@ def list(self, request):
payload['count'] = base_qs.count()

if is_last_page and limit:
total_records = (page - 1) * page_size + len(items)
prev_free = getattr(limit, 'free_download_records', 0)
prev_paid = getattr(limit, 'paid_download_records', 0)
# Charge for the full result set, not just the last page size
returned_count = base_qs.count()

prev_free_amount = getattr(limit, 'free_download_records', 0)
prev_paid_amount = getattr(limit, 'paid_download_records', 0)

FacilitiesDownloadService.register_download_if_needed(
limit,
total_records
)
FacilitiesDownloadService.send_email_if_needed(
request,
limit,
prev_free,
prev_paid
returned_count,
is_same_contributor
)
if returned_count:
FacilitiesDownloadService.send_email_if_needed(
request,
limit,
prev_free_amount,
prev_paid_amount
)

return Response(payload)
27 changes: 22 additions & 5 deletions src/django/api/models/facility_download_limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,34 @@ class FacilityDownloadLimit(models.Model):
objects = FacilityDownloadLimitManager()

@transaction.atomic
def register_download(self, records_to_subtract):
def register_download(
self,
records_to_subtract: int,
):
self.refresh_from_db()

if self.free_download_records >= records_to_subtract:
self.free_download_records -= records_to_subtract
# Prevent overdrafts by capping to remaining quota
remaining_quota = (self.free_download_records or 0) + \
(self.paid_download_records or 0)
to_subtract = min(
max(records_to_subtract, 0),
remaining_quota
)

if to_subtract == 0:
return

if self.free_download_records >= to_subtract:
self.free_download_records -= to_subtract
else:
remaining_records = (
records_to_subtract - self.free_download_records
to_subtract - self.free_download_records
)
self.free_download_records = 0
self.paid_download_records -= remaining_records
self.paid_download_records = max(
self.paid_download_records - remaining_records,
0
)

self.save()

Expand Down
21 changes: 20 additions & 1 deletion src/django/api/serializers/facility/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Union
from typing import (Iterable, Union)
from itertools import groupby

from api.constants import FacilityClaimStatuses
Expand Down Expand Up @@ -346,3 +346,22 @@ def add_http_prefix_to_url(value: str) -> str:
):
value = f"https://{value}"
return value


def is_same_contributor_for_queryset(queryset: Iterable, request) -> bool:
contributor = getattr(request.user, 'contributor', None)
if not contributor:
return False
current_user_contributor_id = contributor.id

found_any_facility = False
for facility in queryset:
found_any_facility = True
facility_contributor_ids = [
contributor.get('id') for contributor in facility.contributors
if contributor.get('id') is not None
]
if current_user_contributor_id not in facility_contributor_ids:
return False

return found_any_facility
25 changes: 22 additions & 3 deletions src/django/api/services/facilities_download_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,28 @@ def enforce_limits(qs, limit, is_first_page):
)

@staticmethod
def register_download_if_needed(limit, record_count):
if limit:
limit.register_download(record_count)
def check_pagination(page_queryset):
if page_queryset is None:
raise ValidationError("Invalid pageSize parameter")
return page_queryset

@staticmethod
def register_download_if_needed(
limit: FacilityDownloadLimit,
records_returned: int,
is_same_contributor: bool = False
):
if is_same_contributor or not limit:
return
try:
count = int(records_returned)
except (TypeError, ValueError):
count = 0

if count <= 0:
return

limit.register_download(count)

@staticmethod
def send_email_if_needed(
Expand Down
Loading