From a302e251d68d8f9aec9e6f085a6bea5824baa2c3 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Fri, 8 Mar 2024 12:28:42 +0800 Subject: [PATCH 1/2] [#525] Delete answers from db where not listed in answer payload (data PUT endpoint) --- backend/db/crud_answer.py | 4 ++++ backend/routes/data.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/backend/db/crud_answer.py b/backend/db/crud_answer.py index 66d70438..bceaaa42 100644 --- a/backend/db/crud_answer.py +++ b/backend/db/crud_answer.py @@ -84,6 +84,10 @@ def get_answer_by_data_and_question( ) +def get_answer_by_data(session: Session, data: int) -> List[AnswerBase]: + return session.query(Answer).filter(Answer.data == data).all() + + def delete_answer_by_id(session: Session, id: int) -> None: session.query(Answer).filter(Answer.id == id).delete() session.commit() diff --git a/backend/routes/data.py b/backend/routes/data.py index d631f026..f1f29d12 100644 --- a/backend/routes/data.py +++ b/backend/routes/data.py @@ -637,8 +637,11 @@ def update_by_id( for qid in qg["question"]: repeat_qids.append(qid) # questions = form.list_of_questions + checked = {} checked_payload = {} + answer_payload_ids_with_repeat_index = [] # for DELETE answer + # if data_cleaning, delete old answer and save payload answer_ids = [] if data_cleaning: @@ -665,6 +668,7 @@ def update_by_id( for a in answers: key = f"{a['question']}_{a['repeat_index']}" + answer_payload_ids_with_repeat_index.append(key) # for DELETE answer checked_payload.update({key: a}) execute = "update" if a["question"] not in list(questions): @@ -731,6 +735,22 @@ def update_by_id( synchronize_session="fetch" ) session.commit() + + # HANDLE DELETE + # need to check if current answers in DB not available + # in answers payload (that mean DELETE) + if not data_cleaning: + all_answers = crud_answer.get_answer_by_data(session=session, data=id) + all_answers = [a.format_with_answer_id for a in all_answers] + for a in all_answers: + key = f"{a['question']}_{a['repeat_index']}" + if key in answer_payload_ids_with_repeat_index: + # ignore + continue + # delete answer + crud_answer.delete_answer_by_id(session=session, id=a["id"]) + # EOL handle DELETE + # if submitted send and not # data_cleaning notification email to secretariat admin if submitted and not data_cleaning: From c54e34bc0b378388db04e9776acccdd7f4783c96 Mon Sep 17 00:00:00 2001 From: wayangalihpratama Date: Fri, 8 Mar 2024 15:41:16 +0800 Subject: [PATCH 2/2] [#525] Update data:update test case, support delete answer --- backend/tests/test_050_submission.py | 770 +++++++++++++++------------ 1 file changed, 415 insertions(+), 355 deletions(-) diff --git a/backend/tests/test_050_submission.py b/backend/tests/test_050_submission.py index 0b5ea46c..285d26eb 100644 --- a/backend/tests/test_050_submission.py +++ b/backend/tests/test_050_submission.py @@ -12,47 +12,53 @@ today = datetime.today().strftime("%B %d, %Y") -class TestSubmissionRoutes(): +class TestSubmissionRoutes: @pytest.mark.asyncio async def test_save_data( self, app: FastAPI, session: Session, client: AsyncClient ) -> None: - payload = [{ - "question": 1, - "repeat_index": 0, - "comment": None, - "value": "Option 1" - }, { - "question": 2, - "repeat_index": 0, - "comment": "This is comment", - "value": "Depend to Q1 Option 1" - }, { - "question": 3, - "repeat_index": 0, - "comment": None, - "value": "Male" - }] + payload = [ + { + "question": 1, + "repeat_index": 0, + "comment": None, + "value": "Option 1", + }, + { + "question": 2, + "repeat_index": 0, + "comment": "This is comment", + "value": "Depend to Q1 Option 1", + }, + { + "question": 3, + "repeat_index": 0, + "comment": None, + "value": "Male", + }, + ] # direct submit without core mandatory answered res = await client.post( app.url_path_for("data:create", form_id=1, submitted=1), params={"locked_by": 1}, json=payload, - headers={"Authorization": f"Bearer {account.token}"}) + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 400 # save data will not validate computed value or core mandatory res = await client.post( app.url_path_for("data:create", form_id=1, submitted=0), params={"locked_by": 1}, json=payload, - headers={"Authorization": f"Bearer {account.token}"}) + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert res == { "id": 1, "form": 1, "form_name": "Form Test", - 'name': "Option 1", + "name": "Option 1", "geo": None, "locked_by": 1, "created": today, @@ -61,72 +67,75 @@ async def test_save_data( "submitted_by": None, "updated": None, "submitted": None, - "answer": [{ - "comment": None, - "question": 1, - "repeat_index": 0, - "value": "Option 1" - }, { - "comment": "This is comment", - "question": 2, - "repeat_index": 0, - "value": "Depend to Q1 Option 1" - }, { - "comment": None, - "question": 3, - "repeat_index": 0, - "value": "Male" - }] + "answer": [ + { + "comment": None, + "question": 1, + "repeat_index": 0, + "value": "Option 1", + }, + { + "comment": "This is comment", + "question": 2, + "repeat_index": 0, + "value": "Depend to Q1 Option 1", + }, + { + "comment": None, + "question": 3, + "repeat_index": 0, + "value": "Male", + }, + ], } @pytest.mark.asyncio async def test_get_webform_from_bucket_with_initial_values( - self, - app: FastAPI, - session: Session, - client: AsyncClient + self, app: FastAPI, session: Session, client: AsyncClient ) -> None: # get form res = await client.get( - app.url_path_for( - "form:get_webform_from_bucket", - form_id=1 - ), + app.url_path_for("form:get_webform_from_bucket", form_id=1), params={"data_id": 1}, - headers={"Authorization": f"Bearer {account.token}"}) + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert "form" in res assert "initial_values" in res assert res["initial_values"] == { - 'created': today, - 'created_by': 'John Doe', - 'form': 1, + "created": today, + "created_by": "John Doe", + "form": 1, "form_name": "Form Test", - 'geo': None, - 'id': 1, - 'locked_by': 1, - 'name': 'Option 1', - 'organisation': 'staff Akvo', - 'submitted': None, - 'submitted_by': None, - 'updated': None, - 'answer': [{ - 'comment': None, - 'question': 1, - 'repeat_index': 0, - 'value': 'Option 1' - }, { - 'comment': 'This is comment', - 'question': 2, - 'repeat_index': 0, - 'value': 'Depend to Q1 Option 1' - }, { - 'comment': None, - 'question': 3, - 'repeat_index': 0, - 'value': 'Male' - }], + "geo": None, + "id": 1, + "locked_by": 1, + "name": "Option 1", + "organisation": "staff Akvo", + "submitted": None, + "submitted_by": None, + "updated": None, + "answer": [ + { + "comment": None, + "question": 1, + "repeat_index": 0, + "value": "Option 1", + }, + { + "comment": "This is comment", + "question": 2, + "repeat_index": 0, + "value": "Depend to Q1 Option 1", + }, + { + "comment": None, + "question": 3, + "repeat_index": 0, + "value": "Male", + }, + ], } @pytest.mark.asyncio @@ -136,61 +145,68 @@ async def test_update_data( # get data by id res = await client.get( app.url_path_for("data:get_by_id", id=1), - headers={"Authorization": f"Bearer {account.token}"}) + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert res["id"] == 1 # update and submit data without core mandatory answered res = await client.put( app.url_path_for("data:update", id=1, submitted=1), - json=[{ - "question": 1, - "repeat_index": 0, - "comment": None, - "value": "Option 1" - }], - headers={"Authorization": f"Bearer {account.token}"}) + json=[ + { + "question": 1, + "repeat_index": 0, + "comment": None, + "value": "Option 1", + } + ], + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 400 # update data res = await client.put( app.url_path_for("data:update", id=1, submitted=0), - json=[{ - "question": 1, - "repeat_index": 0, - "comment": None, - "value": "Option 1" - }, { - "question": 2, - "repeat_index": 0, - "comment": "This is comment", - "value": "Depend to Q1 Option 1" - }, { - "question": 3, - "repeat_index": 0, - "comment": "Add comment on update", - "value": "Female" - }, { - "question": 1, - "repeat_index": 1, - "comment": None, - "value": "Option 1" - }, { - "question": 2, - "repeat_index": 1, - "comment": None, - "value": "Test repeat" - }, { - "question": 3, - "repeat_index": 1, - "comment": None, - "value": "Male" - }, { - "question": 4, - "comment": "Q4 comment", - "repeat_index": 0, - "value": 20 - }], - headers={"Authorization": f"Bearer {account.token}"}) + json=[ + { + "question": 1, + "repeat_index": 0, + "comment": None, + "value": "Option 1", + }, + { + "question": 2, + "repeat_index": 0, + "comment": "This is comment", + "value": "Depend to Q1 Option 1", + }, + { + "question": 3, + "repeat_index": 0, + "comment": "Add comment on update", + "value": "Female", + }, + { + "question": 1, + "repeat_index": 1, + "comment": None, + "value": "Option 1", + }, + { + "question": 2, + "repeat_index": 1, + "comment": None, + "value": "Test repeat", + }, + { + "question": 3, + "repeat_index": 1, + "comment": None, + "value": "Male", + }, + ], + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert res == { @@ -206,83 +222,89 @@ async def test_update_data( "submitted_by": None, "updated": today, "submitted": None, - "answer": [{ - "comment": None, - "question": 1, - "repeat_index": 0, - "value": "Option 1" - }, { - "comment": "This is comment", - "question": 2, - "repeat_index": 0, - "value": "Depend to Q1 Option 1" - }, { - "comment": "Add comment on update", - "question": 3, - "repeat_index": 0, - "value": "Female" - }, { - "comment": None, - "question": 1, - "repeat_index": 1, - "value": "Option 1" - }, { - "comment": None, - "question": 2, - "repeat_index": 1, - "value": "Test repeat" - }, { - "comment": None, - "question": 3, - "repeat_index": 1, - "value": "Male" - }, { - "comment": "Q4 comment", - "question": 4, - "repeat_index": 0, - "value": 20.0 - }] + "answer": [ + { + "comment": None, + "question": 1, + "repeat_index": 0, + "value": "Option 1", + }, + { + "comment": "This is comment", + "question": 2, + "repeat_index": 0, + "value": "Depend to Q1 Option 1", + }, + { + "comment": "Add comment on update", + "question": 3, + "repeat_index": 0, + "value": "Female", + }, + { + "comment": None, + "question": 1, + "repeat_index": 1, + "value": "Option 1", + }, + { + "comment": None, + "question": 2, + "repeat_index": 1, + "value": "Test repeat", + }, + { + "comment": None, + "question": 3, + "repeat_index": 1, + "value": "Male", + }, + ], } @pytest.mark.asyncio async def test_update_data_with_deleted_repeat( - self, - app: FastAPI, - session: Session, - client: AsyncClient + self, app: FastAPI, session: Session, client: AsyncClient ) -> None: # get data by id res = await client.get( app.url_path_for("data:get_by_id", id=1), - headers={"Authorization": f"Bearer {account.token}"}) + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert res["id"] == 1 # update data res = await client.put( app.url_path_for("data:update", id=1, submitted=0), - json=[{ - "comment": None, - "question": 1, - "repeat_index": 0, - "value": "Option 1" - }, { - "comment": "This is comment", - "question": 2, - "repeat_index": 0, - "value": "Depend to Q1 Option 1" - }, { - "comment": "Add comment on update", - "question": 3, - "repeat_index": 0, - "value": "Female" - }, { - "question": 4, - "comment": "Q4 comment", - "repeat_index": 0, - "value": 20 - }], - headers={"Authorization": f"Bearer {account.token}"}) + json=[ + { + "comment": None, + "question": 1, + "repeat_index": 0, + "value": "Option 1", + }, + { + "comment": "This is comment", + "question": 2, + "repeat_index": 0, + "value": "Depend to Q1 Option 1", + }, + { + "comment": "Add comment on update", + "question": 3, + "repeat_index": 0, + "value": "Female", + }, + { + "question": 4, + "comment": "Q4 comment", + "repeat_index": 0, + "value": 20, + }, + ], + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert res == { @@ -298,39 +320,43 @@ async def test_update_data_with_deleted_repeat( "submitted_by": None, "updated": today, "submitted": None, - "answer": [{ - "comment": None, - "question": 1, - "repeat_index": 0, - "value": "Option 1" - }, { - "comment": "This is comment", - "question": 2, - "repeat_index": 0, - "value": "Depend to Q1 Option 1" - }, { - "comment": "Add comment on update", - "question": 3, - "repeat_index": 0, - "value": "Female" - }, { - "comment": "Q4 comment", - "question": 4, - "repeat_index": 0, - "value": 20 - }] + "answer": [ + { + "comment": None, + "question": 1, + "repeat_index": 0, + "value": "Option 1", + }, + { + "comment": "This is comment", + "question": 2, + "repeat_index": 0, + "value": "Depend to Q1 Option 1", + }, + { + "comment": "Add comment on update", + "question": 3, + "repeat_index": 0, + "value": "Female", + }, + { + "comment": "Q4 comment", + "question": 4, + "repeat_index": 0, + "value": 20, + }, + ], } @pytest.mark.asyncio async def test_update_then_submit_data( - self, app: FastAPI, - session: Session, - client: AsyncClient + self, app: FastAPI, session: Session, client: AsyncClient ) -> None: # get data by id res = await client.get( app.url_path_for("data:get_by_id", id=1), - headers={"Authorization": f"Bearer {account.token}"}) + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert res["id"] == 1 @@ -338,68 +364,82 @@ async def test_update_then_submit_data( res = await client.put( app.url_path_for("data:update", id=1, submitted=1), params={"locked_by": 1}, - json=[{ - "comment": None, - "question": 1, - "repeat_index": 0, - "value": "Option 1" - }, { - "comment": "This is comment", - "question": 2, - "repeat_index": 0, - "value": "Depend to Q1 Option 1" - }, { - "question": 3, - "repeat_index": 0, - "comment": "Q3 comment", - "value": "Male" - }, { - "question": 1, - "repeat_index": 1, - "comment": None, - "value": "Option 1" - }, { - "question": 2, - "repeat_index": 1, - "comment": None, - "value": "Test repeat" - }, { - "question": 3, - "repeat_index": 1, - "comment": "Q3 comment 1", - "value": "Female" - }, { - "question": 4, - "repeat_index": 0, - "comment": "Q4 comment", - "value": 25 - }, { - "question": 5, - "repeat_index": 0, - "comment": "Test with zero value", - "value": 0 - }, { - "question": 6, - "repeat_index": 0, - "comment": None, - "value": [2, 12] - }, { - "question": 7, - "repeat_index": 0, - "comment": None, - "value": ["Technology|Programming", "Sports|Football"] - }, { - "question": 8, - "repeat_index": 0, - "comment": None, - "value": "2022-01-01" - }, { - "question": 9, - "repeat_index": 0, - "comment": None, - "value": ["MO-1", "MO-2"] - }], - headers={"Authorization": f"Bearer {account.token}"}) + json=[ + { + "comment": None, + "question": 1, + "repeat_index": 0, + "value": "Option 1", + }, + { + "comment": "This is comment", + "question": 2, + "repeat_index": 0, + "value": "Depend to Q1 Option 1", + }, + { + "question": 3, + "repeat_index": 0, + "comment": "Q3 comment", + "value": "Male", + }, + { + "question": 1, + "repeat_index": 1, + "comment": None, + "value": "Option 1", + }, + { + "question": 2, + "repeat_index": 1, + "comment": None, + "value": "Test repeat", + }, + { + "question": 3, + "repeat_index": 1, + "comment": "Q3 comment 1", + "value": "Female", + }, + { + "question": 4, + "repeat_index": 0, + "comment": "Q4 comment", + "value": 25, + }, + { + "question": 5, + "repeat_index": 0, + "comment": "Test with zero value", + "value": 0, + }, + { + "question": 6, + "repeat_index": 0, + "comment": None, + "value": [2, 12], + }, + { + "question": 7, + "repeat_index": 0, + "comment": None, + "value": ["Technology|Programming", "Sports|Football"], + }, + { + "question": 8, + "repeat_index": 0, + "comment": None, + "value": "2022-01-01", + }, + { + "question": 9, + "repeat_index": 0, + "comment": None, + "value": ["MO-1", "MO-2"], + }, + ], + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 200 res = res.json() assert res == { @@ -415,67 +455,80 @@ async def test_update_then_submit_data( "submitted_by": "John Doe", "updated": today, "submitted": today, - "answer": [{ - "comment": None, - "question": 1, - "repeat_index": 0, - "value": "Option 1" - }, { - "comment": "This is comment", - "question": 2, - "repeat_index": 0, - "value": "Depend to Q1 Option 1" - }, { - "comment": "Q3 comment", - "question": 3, - "repeat_index": 0, - "value": "Male" - }, { - "comment": "Q4 comment", - "question": 4, - "repeat_index": 0, - "value": 25.0 - }, { - "comment": None, - "question": 1, - "repeat_index": 1, - "value": "Option 1" - }, { - "comment": None, - "question": 2, - "repeat_index": 1, - "value": "Test repeat" - }, { - "comment": "Q3 comment 1", - "question": 3, - "repeat_index": 1, - "value": "Female" - }, { - "comment": "Test with zero value", - "question": 5, - "repeat_index": 0, - "value": 0.0 - }, { - 'comment': None, - 'question': 6, - 'repeat_index': 0, - 'value': [2.0, 12.0] - }, { - 'comment': None, - 'question': 7, - 'repeat_index': 0, - 'value': ['Technology|Programming', 'Sports|Football'] - }, { - 'comment': None, - 'question': 8, - 'repeat_index': 0, - 'value': '2022-01-01' - }, { - 'comment': None, - 'question': 9, - 'repeat_index': 0, - 'value': ['MO-1', 'MO-2'] - }] + "answer": [ + { + "comment": None, + "question": 1, + "repeat_index": 0, + "value": "Option 1", + }, + { + "comment": "This is comment", + "question": 2, + "repeat_index": 0, + "value": "Depend to Q1 Option 1", + }, + { + "comment": "Q3 comment", + "question": 3, + "repeat_index": 0, + "value": "Male", + }, + { + "comment": "Q4 comment", + "question": 4, + "repeat_index": 0, + "value": 25.0, + }, + { + "comment": None, + "question": 1, + "repeat_index": 1, + "value": "Option 1", + }, + { + "comment": None, + "question": 2, + "repeat_index": 1, + "value": "Test repeat", + }, + { + "comment": "Q3 comment 1", + "question": 3, + "repeat_index": 1, + "value": "Female", + }, + { + "comment": "Test with zero value", + "question": 5, + "repeat_index": 0, + "value": 0.0, + }, + { + "comment": None, + "question": 6, + "repeat_index": 0, + "value": [2.0, 12.0], + }, + { + "comment": None, + "question": 7, + "repeat_index": 0, + "value": ["Technology|Programming", "Sports|Football"], + }, + { + "comment": None, + "question": 8, + "repeat_index": 0, + "value": "2022-01-01", + }, + { + "comment": None, + "question": 9, + "repeat_index": 0, + "value": ["MO-1", "MO-2"], + }, + ], } @pytest.mark.asyncio @@ -485,31 +538,38 @@ async def test_submit_data( res = await client.post( app.url_path_for("data:create", form_id=1, submitted=1), params={"locked_by": 1}, - json=[{ - "question": 1, - "repeat_index": 0, - "comment": None, - "value": "Option 1" - }, { - "question": 2, - "repeat_index": 0, - "comment": None, - "value": "Direct submit" - }, { - "question": 3, - "repeat_index": 0, - "comment": None, - "value": "Female" - }, { - "question": 4, - "repeat_index": 0, - "comment": None, - "value": 35 - }, { - "question": 5, - "repeat_index": 0, - "comment": "Q5 comment", - "value": 55 - }], - headers={"Authorization": f"Bearer {account.token}"}) + json=[ + { + "question": 1, + "repeat_index": 0, + "comment": None, + "value": "Option 1", + }, + { + "question": 2, + "repeat_index": 0, + "comment": None, + "value": "Direct submit", + }, + { + "question": 3, + "repeat_index": 0, + "comment": None, + "value": "Female", + }, + { + "question": 4, + "repeat_index": 0, + "comment": None, + "value": 35, + }, + { + "question": 5, + "repeat_index": 0, + "comment": "Q5 comment", + "value": 55, + }, + ], + headers={"Authorization": f"Bearer {account.token}"}, + ) assert res.status_code == 208