Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion partner_programs/serializers/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class ManagementParticipantQuery(ManagementListQuery):
choices=("all", "period", "active", "inactive", "never"), default="all"
)
order = serializers.ChoiceField(
choices=("registration", "activity_asc", "activity_desc"), default="registration"
choices=("registration", "activity_asc", "activity_desc"), default="activity_desc"
)
activity_from = serializers.DateField(required=False)
activity_to = serializers.DateField(required=False)
Expand Down
4 changes: 2 additions & 2 deletions partner_programs/services/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ def participant_rows(program, search="", query=None):
if query.get("activity_to"):
end = datetime.combine(query["activity_to"], time.max, zone)
rows = rows.filter(user__last_activity__lte=end)
order = query.get("order", "registration")
order = query.get("order", "activity_desc")
if order in ("activity_asc", "activity_desc"):
activity = F("user__last_activity")
rows = rows.order_by(
activity.asc(nulls_last=True)
activity.asc(nulls_first=True)
if order == "activity_asc"
else activity.desc(nulls_last=True),
"pk",
Expand Down
36 changes: 30 additions & 6 deletions partner_programs/tests/test_management_participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def test_invalid_filters_fail_for_list_and_export(self):
def test_activity_presence_filters_match_selection_and_export(self):
selection = self.root + "selection/"
for mode, expected in [
("active", self.rows[:4]),
("active", [self.rows[i] for i in (2, 1, 3, 0)]),
("inactive", self.rows[4:]),
("never", self.rows[4:]),
]:
Expand All @@ -123,19 +123,21 @@ def test_activity_presence_filters_match_selection_and_export(self):
isinstance(sheet.cell(row, 6).value, datetime), mode == "active"
)

def test_activity_sort_is_global_stable_and_nulls_always_last(self):
# Одинаковые даты сортируются по id, отсутствие активности идет в конце.
def test_activity_sort_is_global_stable_and_missing_activity_is_least_active(self):
# Одинаковые даты и отсутствие активности сортируются стабильно по id.
user = self.rows[3].user
user.last_activity = datetime.fromisoformat("2026-10-19T20:59:59+00:00")
user.save(update_fields=["last_activity"])
for order, indices in [
("activity_asc", [0, 1, 3, 2, 4, 5]),
("activity_asc", [4, 5, 0, 1, 3, 2]),
("activity_desc", [2, 1, 3, 0, 4, 5]),
]:
with self.subTest(order=order):
expected = [self.rows[i].pk for i in indices]
full = self.client.get(self.root, {"order": order})
self.assertEqual([p["id"] for p in full.data["results"]], expected)
selected = self.client.get(self.root + "selection/", {"order": order})
self.assertEqual(selected.data["ids"], expected)
page = self.client.get(
self.root, {"order": order, "limit": 2, "offset": 2}
)
Expand All @@ -158,6 +160,26 @@ def test_activity_sort_is_global_stable_and_nulls_always_last(self):
[self.rows[i].user.email for i in indices],
)

def test_default_is_recent_activity_first_with_legacy_registration_order_supported(
self,
):
expected = [self.rows[i].pk for i in (2, 1, 3, 0, 4, 5)]
response = self.client.get(self.root)
self.assertEqual([p["id"] for p in response.data["results"]], expected)
selected = self.client.get(self.root + "selection/")
self.assertEqual(selected.data["ids"], expected)
sheet = load_workbook(
BytesIO(self.client.get(self.root + "export/").content)
).active
self.assertEqual(
[sheet.cell(i, 3).value for i in range(2, 8)],
[self.rows[i].user.email for i in (2, 1, 3, 0, 4, 5)],
)
legacy = self.client.get(self.root, {"order": "registration"})
self.assertEqual(
[p["id"] for p in legacy.data["results"]], [p.pk for p in self.rows]
)

def test_export_uses_all_filtered_rows_ignoring_pagination_and_keeps_typed_dates(
self,
):
Expand All @@ -170,7 +192,7 @@ def test_export_uses_all_filtered_rows_ignoring_pagination_and_keeps_typed_dates
sheet = load_workbook(BytesIO(response.content)).active
self.assertEqual(sheet.max_row, 4)
self.assertEqual([sheet.cell(row, 1).value for row in range(2, 5)], [1, 2, 3])
self.assertEqual(sheet.cell(2, 6).value, datetime(2026, 10, 19))
self.assertEqual(sheet.cell(2, 6).value, datetime(2026, 10, 20))
self.assertEqual(sheet.cell(2, 6).data_type, "d")

def test_region_matches_profile_picker_city_with_legacy_region_fallback(self):
Expand All @@ -190,7 +212,9 @@ def test_export_neutralizes_user_formulas(self):
user.first_name = '=HYPERLINK("bad")'
user.save(update_fields=["first_name"])
sheet = load_workbook(
BytesIO(self.client.get(self.root + "export/").content)
BytesIO(
self.client.get(self.root + "export/", {"search": user.email}).content
)
).active
self.assertTrue(sheet.cell(2, 2).value.startswith("="))
self.assertEqual(sheet.cell(2, 2).data_type, "s")
Expand Down
Loading