Skip to content

Commit 122328e

Browse files
authored
feature: Add Last Submission Timestamp to metadata endpoint (M2-7859) (#1676)
* feature: Add Last Submission Timestamp to counters endpoint (M2-7859) * Update due metadata endpoint refactoring * Returning lastSubmissionDate separated between subject and respondent * Update deps Update deps
1 parent f6990e6 commit 122328e

File tree

10 files changed

+536
-489
lines changed

10 files changed

+536
-489
lines changed

Pipfile

+25-25
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@ name = "pypi"
77
aio-pika = "==9.5.3"
88
aiofiles = "==24.1.0"
99
aiohttp = "==3.11.9"
10-
alembic = "==1.13.3"
10+
alembic = "==1.14.0"
1111
asyncpg = "==0.30.0"
12-
azure-storage-blob = "==12.23.1"
13-
bcrypt = "==4.2.0"
14-
boto3 = "==1.35.54"
15-
fastapi = "==0.115.4"
12+
azure-storage-blob = "==12.24.0"
13+
bcrypt = "==4.2.1"
14+
boto3 = "==1.35.77"
15+
fastapi = "==0.115.6"
1616
fastapi-mail = "==1.2.9"
1717
firebase-admin = "==6.5.0"
1818
httpx = "==0.28.0"
1919
jinja2 = "==3.1.4"
20+
more-itertools = "==10.5.0"
2021
nh3 = "==0.2.19"
2122
opentelemetry-api = "==1.27.0"
2223
opentelemetry-distro = "==0.48b0"
@@ -37,9 +38,10 @@ opentelemetry-sdk-extension-aws = "==2.0.2"
3738
opentelemetry-semantic-conventions = "==0.48b0"
3839
opentelemetry-test-utils = "==0.48b0"
3940
opentelemetry-util-http = "==0.48b0"
40-
pyOpenSSL = "==24.2.1"
4141
pydantic = { extras = ["email"], version = "==1.10.18" }
42-
pymongo = "*"
42+
pyjwt = "==2.10.1"
43+
pymongo = "==4.10.1"
44+
pyOpenSSL = "==24.2.1"
4345
python-multipart = "==0.0.19"
4446
redis = "==5.2.0"
4547
sentry-sdk = "~=2.13"
@@ -49,37 +51,35 @@ taskiq = { extras = ["reload"], version = "==0.11.7" }
4951
taskiq-aio-pika = "==0.4.1"
5052
taskiq-fastapi = "==0.3.2"
5153
taskiq-redis = "==1.0.2"
52-
typer = "==0.12.5"
53-
uvicorn = { extras = ["standard"], version = "==0.32.0" }
54-
pyjwt = "==2.10.1"
55-
more-itertools = "==10.5.0"
54+
typer = "==0.15.1"
55+
uvicorn = { extras = ["standard"], version = "==0.32.1" }
5656

5757
[dev-packages]
58+
allure-pytest = "==2.13.5"
59+
cachetools = "==5.3.0"
60+
gevent = "==24.2.1"
61+
greenlet = "==3.1.0"
5862
ipdb = "==0.13.13"
59-
pudb = "==2024.1.3"
63+
mypy = "==1.13.0"
64+
nest-asyncio = "==1.6.0"
6065
pre-commit = "==4.0.1"
61-
ruff = "==0.7.2"
62-
allure-pytest = "==2.13.5"
66+
pudb = "==2024.1.3"
6367
pydantic-factories = "==1.17.3"
68+
pyld = "==2.0.4"
6469
pytest = "==8.3.4"
6570
pytest-asyncio = "~=0.19"
6671
pytest-cov = "==6.0.0"
6772
pytest-env = "==1.1.5"
6873
pytest-lazy-fixtures = "==1.1.1"
6974
pytest-mock = "==3.14.0"
70-
nest-asyncio = "==1.6.0"
71-
gevent = "==24.11.1"
72-
mypy = "==1.13.0"
73-
types-python-dateutil = "==2.9.0.20241003"
74-
typing-extensions = "==4.12.2"
75-
types-requests = "==2.32.0.20241016"
76-
types-pytz = "==2024.2.0.20241003"
75+
reproschema = "==0.6.2"
76+
ruff = "==0.8.2"
7777
types-aiofiles = "==24.1.0.20240626"
7878
types-cachetools = "==5.5.0.20240820"
79-
greenlet = "==3.1.0"
80-
reproschema = "0.9.0"
81-
cachetools = "==5.3.0"
82-
pyld = "==2.0.4"
79+
types-python-dateutil = "==2.9.0.20241206 "
80+
types-pytz = "==2024.2.0.20241003"
81+
types-requests = "==2.32.0.20241016"
82+
typing-extensions = "==4.12.2"
8383

8484
[requires]
8585
python_version = "3.11"

Pipfile.lock

+427-427
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/apps/activities/api/activities.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,17 @@ async def applet_activities_metadata_for_subject(
385385
)
386386

387387
# Fetch activities submissions by the subject
388-
submitted_activities = await AnswerService(session, user.id, answer_session).get_submissions_by_subject(subject_id)
388+
submissions_metadata = await AnswerService(session, user.id, answer_session).get_submissions_metadata_by_subject(
389+
subject_id
390+
)
389391

390392
# Fetch auto-assigned activity and flow IDs by applet ID
391393
auto_activity_ids = await ActivityService(session, user.id).get_activity_and_flow_ids_by_applet_id_auto(applet_id)
392394

393395
# Combine all assigned IDs and submitted activity IDs
394396
all_activity_ids = (
395397
set(assigned_activities.activities.keys())
396-
| set(submitted_activities.activities.keys())
398+
| set(submissions_metadata.activities.keys())
397399
| set(auto_activity_ids)
398400
)
399401

@@ -411,7 +413,7 @@ async def applet_activities_metadata_for_subject(
411413
is_auto = activity_or_flow_id in auto_activity_ids
412414

413415
# Get submission and assignment data if available
414-
submission_data = submitted_activities.activities.get(activity_or_flow_id)
416+
submission_data = submissions_metadata.activities.get(activity_or_flow_id)
415417
assignments_data = assigned_activities.activities.get(activity_or_flow_id)
416418

417419
# Initialize sets for respondents and subjects
@@ -420,14 +422,18 @@ async def applet_activities_metadata_for_subject(
420422

421423
# Initialize submission counts
422424
respondent_submissions_count = 0
425+
respondent_last_submission_date = None
423426
subject_submissions_count = 0
427+
subject_last_submission_date = None
424428

425429
# Update from submission data
426430
if submission_data:
427431
respondents.update(submission_data.respondents)
428432
subjects.update(submission_data.subjects)
429433
respondent_submissions_count = submission_data.respondent_submissions_count
434+
respondent_last_submission_date = submission_data.respondent_last_submission_date
430435
subject_submissions_count = submission_data.subject_submissions_count
436+
subject_last_submission_date = submission_data.subject_last_submission_date
431437

432438
# Update from assignment data
433439
if assignments_data:
@@ -464,7 +470,9 @@ async def applet_activities_metadata_for_subject(
464470
respondents_count=respondents_count,
465471
subjects_count=subjects_count,
466472
respondent_submissions_count=respondent_submissions_count,
473+
respondent_last_submission_date=respondent_last_submission_date,
467474
subject_submissions_count=subject_submissions_count,
475+
subject_last_submission_date=subject_last_submission_date,
468476
)
469477
)
470478

src/apps/activities/domain/activity.py

+2
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,10 @@ class ActivitySubjectMetadata(PublicModel):
162162
activity_or_flow_id: uuid.UUID
163163
respondents_count: int
164164
respondent_submissions_count: int
165+
respondent_last_submission_date: datetime | None
165166
subjects_count: int
166167
subject_submissions_count: int
168+
subject_last_submission_date: datetime | None
167169

168170

169171
class ActivitiesMetadata(PublicModel):

src/apps/activities/tests/test_activities.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -1353,18 +1353,22 @@ async def test_assigned_activities_auto_assigned(
13531353
assert result["targetActivitiesCountExisting"] == 2
13541354
assert result["targetActivitiesCountDeleted"] == 0
13551355
assert len(result["activitiesOrFlows"]) == 2
1356-
flow_counters = next(item for item in result["activitiesOrFlows"] if item["activityOrFlowId"] == str(flow.id))
1357-
activity_counters = next(
1356+
flow_metadata = next(item for item in result["activitiesOrFlows"] if item["activityOrFlowId"] == str(flow.id))
1357+
activity_metadata = next(
13581358
item for item in result["activitiesOrFlows"] if item["activityOrFlowId"] == str(activity.id)
13591359
)
1360-
assert flow_counters["respondentsCount"] == 1
1361-
assert flow_counters["respondentSubmissionsCount"] == 0
1362-
assert flow_counters["subjectsCount"] == 1
1363-
assert flow_counters["subjectSubmissionsCount"] == 0
1364-
assert activity_counters["respondentsCount"] == 1
1365-
assert activity_counters["respondentSubmissionsCount"] == 0
1366-
assert activity_counters["subjectsCount"] == 1
1367-
assert activity_counters["subjectSubmissionsCount"] == 0
1360+
assert flow_metadata["respondentsCount"] == 1
1361+
assert flow_metadata["respondentSubmissionsCount"] == 0
1362+
assert flow_metadata["respondentLastSubmissionDate"] is None
1363+
assert flow_metadata["subjectsCount"] == 1
1364+
assert flow_metadata["subjectSubmissionsCount"] == 0
1365+
assert flow_metadata["subjectLastSubmissionDate"] is None
1366+
assert activity_metadata["respondentsCount"] == 1
1367+
assert activity_metadata["respondentSubmissionsCount"] == 0
1368+
assert activity_metadata["respondentLastSubmissionDate"] is None
1369+
assert activity_metadata["subjectsCount"] == 1
1370+
assert activity_metadata["subjectSubmissionsCount"] == 0
1371+
assert activity_metadata["subjectLastSubmissionDate"] is None
13681372

13691373
@pytest.mark.parametrize(
13701374
"subject_type,result_order",
@@ -1754,8 +1758,18 @@ async def test_assigned_activities_from_submission(
17541758
activityOrFlow = result["activitiesOrFlows"][0]
17551759
assert activityOrFlow["respondentsCount"] == (1 if subject_type == "target" else 0)
17561760
assert activityOrFlow["respondentSubmissionsCount"] == (0 if subject_type == "target" else 1)
1761+
assert (
1762+
activityOrFlow["respondentLastSubmissionDate"] is None
1763+
if subject_type == "target"
1764+
else activityOrFlow["respondentLastSubmissionDate"] is not None
1765+
)
17571766
assert activityOrFlow["subjectsCount"] == (0 if subject_type == "target" else 1)
17581767
assert activityOrFlow["subjectSubmissionsCount"] == (1 if subject_type == "target" else 0)
1768+
assert (
1769+
activityOrFlow["subjectLastSubmissionDate"] is not None
1770+
if subject_type == "target"
1771+
else activityOrFlow["subjectLastSubmissionDate"] is None
1772+
)
17591773

17601774
@pytest.mark.parametrize("subject_type", ["target", "respondent"])
17611775
async def test_assigned_hidden_activities(

src/apps/answers/crud/answers.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -1011,7 +1011,7 @@ async def get_activity_and_flow_ids_by_source_subject(self, source_subject_id: u
10111011
return res.mappings().all()
10121012

10131013
@staticmethod
1014-
def _query_submissions_by_subject(subject_column: InstrumentedAttribute, subject_id: uuid.UUID) -> Query:
1014+
def _query_submissions_metadata_by_subject(subject_column: InstrumentedAttribute, subject_id: uuid.UUID) -> Query:
10151015
query: Query = (
10161016
select(
10171017
func.count(func.distinct(AnswerSchema.submit_id)).label("submission_count"),
@@ -1027,6 +1027,7 @@ def _query_submissions_by_subject(subject_column: InstrumentedAttribute, subject
10271027
if subject_column == AnswerSchema.target_subject_id
10281028
else AnswerSchema.target_subject_id
10291029
).label("subject_id"),
1030+
func.max(AnswerSchema.created_at).label("last_submission_date"),
10301031
)
10311032
.where(
10321033
subject_column == subject_id,
@@ -1035,17 +1036,20 @@ def _query_submissions_by_subject(subject_column: InstrumentedAttribute, subject
10351036
)
10361037
.group_by("activity_id", "subject_id")
10371038
)
1039+
10381040
return query
10391041

1040-
async def get_submissions_by_target_subject(self, target_subject_id: uuid.UUID) -> list[dict]:
1041-
query: Query = self._query_submissions_by_subject(AnswerSchema.target_subject_id, target_subject_id)
1042+
async def get_submissions_metadata_by_target_subject(self, target_subject_id: uuid.UUID) -> list[dict]:
1043+
query: Query = self._query_submissions_metadata_by_subject(AnswerSchema.target_subject_id, target_subject_id)
10421044

10431045
res = await self._execute(query)
10441046

10451047
return res.mappings().all()
10461048

1047-
async def get_submissions_by_respondent_subject(self, respondent_subject_id: uuid.UUID) -> list[dict]:
1048-
query: Query = self._query_submissions_by_subject(AnswerSchema.source_subject_id, respondent_subject_id)
1049+
async def get_submissions_metadata_by_respondent_subject(self, respondent_subject_id: uuid.UUID) -> list[dict]:
1050+
query: Query = self._query_submissions_metadata_by_subject(
1051+
AnswerSchema.source_subject_id, respondent_subject_id
1052+
)
10491053

10501054
res = await self._execute(query)
10511055

src/apps/answers/domain/answers.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -719,9 +719,11 @@ class SubmissionsSubjectCounters(InternalModel):
719719
respondents: set[uuid.UUID] = Field(default_factory=set)
720720
subjects: set[uuid.UUID] = Field(default_factory=set)
721721
subject_submissions_count: int = 0
722+
subject_last_submission_date: datetime.datetime | None = None
722723
respondent_submissions_count: int = 0
724+
respondent_last_submission_date: datetime.datetime | None = None
723725

724726

725-
class SubmissionsActivityCountBySubject(InternalModel):
727+
class SubmissionsActivityMetadataBySubject(InternalModel):
726728
subject_id: uuid.UUID
727729
activities: dict[uuid.UUID, SubmissionsSubjectCounters] = Field(default_factory=dict)

src/apps/answers/service.py

+30-12
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
AppletSubmission,
6868
FilesCopyCheckResult,
6969
RespondentAnswerData,
70-
SubmissionsActivityCountBySubject,
70+
SubmissionsActivityMetadataBySubject,
7171
SubmissionsSubjectCounters,
7272
)
7373
from apps.answers.errors import (
@@ -1979,37 +1979,55 @@ async def _filter_out_soft_deleted_subjects(self, submissions: list[dict]) -> se
19791979

19801980
return existing_subject_ids
19811981

1982-
async def get_submissions_by_subject(self, subject_id: uuid.UUID) -> SubmissionsActivityCountBySubject:
1983-
submissions_target_coro = AnswersCRUD(self.answer_session).get_submissions_by_target_subject(subject_id)
1984-
submissions_respondent_coro = AnswersCRUD(self.answer_session).get_submissions_by_respondent_subject(subject_id)
1982+
async def get_submissions_metadata_by_subject(self, subject_id: uuid.UUID) -> SubmissionsActivityMetadataBySubject:
1983+
submissions_target_coro = AnswersCRUD(self.answer_session).get_submissions_metadata_by_target_subject(
1984+
subject_id
1985+
)
1986+
submissions_respondent_coro = AnswersCRUD(self.answer_session).get_submissions_metadata_by_respondent_subject(
1987+
subject_id
1988+
)
19851989

19861990
submissions_target, submissions_respondent = await asyncio.gather(
19871991
submissions_target_coro, submissions_respondent_coro
19881992
)
19891993

19901994
existing_subject_ids = await self._filter_out_soft_deleted_subjects(submissions_target + submissions_respondent)
19911995

1992-
submissions_activity_count = SubmissionsActivityCountBySubject(subject_id=subject_id)
1996+
submissions_activity_metadata = SubmissionsActivityMetadataBySubject(subject_id=subject_id)
19931997

19941998
for activity_submissions in submissions_target:
1995-
activity_counters = submissions_activity_count.activities.setdefault(
1999+
activity_metadata = submissions_activity_metadata.activities.setdefault(
19962000
uuid.UUID(activity_submissions["activity_id"]), SubmissionsSubjectCounters()
19972001
)
19982002
respondent_subject_id = activity_submissions["subject_id"]
19992003
if respondent_subject_id in existing_subject_ids:
2000-
activity_counters.respondents.add(respondent_subject_id)
2001-
activity_counters.subject_submissions_count += activity_submissions["submission_count"]
2004+
activity_metadata.respondents.add(respondent_subject_id)
2005+
activity_metadata.subject_submissions_count += activity_submissions["submission_count"]
2006+
activity_metadata.subject_last_submission_date = (
2007+
activity_submissions["last_submission_date"]
2008+
if not activity_metadata.subject_last_submission_date
2009+
else max(
2010+
activity_metadata.subject_last_submission_date, activity_submissions["last_submission_date"]
2011+
)
2012+
)
20022013

20032014
for activity_submissions in submissions_respondent:
2004-
activity_counters = submissions_activity_count.activities.setdefault(
2015+
activity_metadata = submissions_activity_metadata.activities.setdefault(
20052016
uuid.UUID(activity_submissions["activity_id"]), SubmissionsSubjectCounters()
20062017
)
20072018
target_subject_id = activity_submissions["subject_id"]
20082019
if target_subject_id in existing_subject_ids:
2009-
activity_counters.subjects.add(target_subject_id)
2010-
activity_counters.respondent_submissions_count += activity_submissions["submission_count"]
2020+
activity_metadata.subjects.add(target_subject_id)
2021+
activity_metadata.respondent_submissions_count += activity_submissions["submission_count"]
2022+
activity_metadata.respondent_last_submission_date = (
2023+
activity_submissions["last_submission_date"]
2024+
if not activity_metadata.respondent_last_submission_date
2025+
else max(
2026+
activity_metadata.respondent_last_submission_date, activity_submissions["last_submission_date"]
2027+
)
2028+
)
20112029

2012-
return submissions_activity_count
2030+
return submissions_activity_metadata
20132031

20142032

20152033
class ReportServerService:

src/apps/authentication/api/auth.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import uuid
2-
from datetime import datetime
2+
from datetime import datetime, timezone
33

44
import jwt
55
from fastapi import Body, Depends
@@ -86,10 +86,9 @@ async def refresh_access_token(
8686
# check transition key
8787
transition_key = settings.authentication.refresh_token.transition_key
8888
transition_expire_date = settings.authentication.refresh_token.transition_expire_date
89+
today = datetime.now(timezone.utc).date()
8990

90-
if not (
91-
transition_key and transition_expire_date and transition_expire_date > datetime.utcnow().date()
92-
):
91+
if not (transition_key and transition_expire_date and transition_expire_date > today):
9392
raise
9493
payload = jwt.decode(
9594
schema.refresh_token,

src/apps/authentication/tests/test_auth.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async def test_refresh_token_key_transition(self, client, tom: User, tom_create:
109109
token_data = TokenPayload(**payload)
110110

111111
new_token_key = "new token key"
112-
transition_expire_date = datetime.date.today() + datetime.timedelta(days=1)
112+
transition_expire_date = datetime.datetime.now(datetime.timezone.utc).date() + datetime.timedelta(days=1)
113113

114114
# refresh access token, check refresh token not changed
115115
_status_code, _token = await self._request_refresh_token(client, refresh_token)
@@ -129,7 +129,7 @@ async def test_refresh_token_key_transition(self, client, tom: User, tom_create:
129129

130130
# check transition expire date
131131
with mock.patch("apps.authentication.api.auth.datetime") as date_mock:
132-
date_mock.utcnow().date.return_value = transition_expire_date + datetime.timedelta(days=1)
132+
date_mock.now().date.return_value = transition_expire_date + datetime.timedelta(days=1)
133133
_status_code, _ = await self._request_refresh_token(client, refresh_token)
134134
assert _status_code == http.HTTPStatus.BAD_REQUEST
135135

0 commit comments

Comments
 (0)