Skip to content

Commit 756b722

Browse files
committed
restrictions to edits via API based on status. github checks for that and for github comment creation
1 parent 6d6054c commit 756b722

File tree

2 files changed

+116
-113
lines changed

2 files changed

+116
-113
lines changed

nmdc_server/api.py

Lines changed: 72 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,7 +1151,7 @@ async def get_submission(
11511151
raise HTTPException(status_code=403, detail="Must have access.")
11521152

11531153

1154-
def can_save_submission(role: models.SubmissionRole, data: dict):
1154+
def can_save_submission(role: models.SubmissionRole, data: dict, status: str):
11551155
"""Compare a patch payload with what the user can actually save."""
11561156
metadata_contributor_fields = set(["sampleData", "metadata_submission"])
11571157
editor_fields = set(
@@ -1174,13 +1174,16 @@ def can_save_submission(role: models.SubmissionRole, data: dict):
11741174
models.SubmissionEditorRole.metadata_contributor: metadata_contributor_fields,
11751175
}
11761176
permission_level = models.SubmissionEditorRole(role.role)
1177-
if permission_level == models.SubmissionEditorRole.owner:
1178-
return True
1179-
elif permission_level == models.SubmissionEditorRole.viewer:
1180-
return False
1177+
if status in [SubmissionStatusEnum.InProgress.text, SubmissionStatusEnum.UpdatesRequired.text]:
1178+
if permission_level == models.SubmissionEditorRole.owner:
1179+
return True
1180+
elif permission_level == models.SubmissionEditorRole.viewer:
1181+
return False
1182+
else:
1183+
allowed_fields = fields_for_permission[permission_level]
1184+
return all([field in allowed_fields for field in attempted_patch_fields])
11811185
else:
1182-
allowed_fields = fields_for_permission[permission_level]
1183-
return all([field in allowed_fields for field in attempted_patch_fields])
1186+
return False
11841187

11851188

11861189
@router.patch(
@@ -1202,7 +1205,11 @@ async def update_submission(
12021205

12031206
current_user_role = crud.get_submission_role(db, id, user.orcid)
12041207
if not (
1205-
user.is_admin or (current_user_role and can_save_submission(current_user_role, body_dict))
1208+
user.is_admin
1209+
or (
1210+
current_user_role
1211+
and can_save_submission(current_user_role, body_dict, submission.status)
1212+
)
12061213
):
12071214
raise HTTPException(403, detail="Must have access.")
12081215

@@ -1270,15 +1277,9 @@ def create_github_issue(submission: schemas_submission.SubmissionMetadataSchema,
12701277
headers = {"Authorization": f"Bearer {token}", "Content-Type": "text/plain; charset=utf-8"}
12711278

12721279
# Check for existing issues first
1273-
existing_issue = check_existing_github_issue(submission.id, headers, gh_url, user)
1274-
if existing_issue:
1275-
logging.info(
1276-
f"GitHub issue already exists for submission {submission.id}: \
1277-
{existing_issue['html_url']}"
1278-
)
1279-
return existing_issue
1280+
existing_issue = check_existing_github_issues(submission.id, headers, gh_url, user)
12801281

1281-
else:
1282+
if existing_issue is None:
12821283

12831284
# Gathering the fields we want to display in the issue
12841285
study_form = submission.metadata_submission.studyForm
@@ -1337,100 +1338,82 @@ def create_github_issue(submission: schemas_submission.SubmissionMetadataSchema,
13371338
return res
13381339

13391340

1340-
def update_github_issue_for_resubmission(existing_issue, user, headers):
1341+
def check_existing_github_issues(submission_id: UUID, headers: dict, gh_url: str, user):
13411342
"""
1342-
Update an existing GitHub issue to note that the submission was resubmitted.
1343-
Adds a comment and optionally reopens the issue if it was closed.
1343+
Check if a GitHub issue already exists for the given submission ID using GitHub's search API.
13441344
"""
1345-
try:
1346-
issue_number = existing_issue.get("number")
1347-
issue_url = existing_issue.get("url") # API URL for the issue
1345+
expected_title = f"NMDC Submission: {submission_id}"
1346+
params = {
1347+
"state": "all",
1348+
"per_page": 100,
1349+
}
1350+
response = requests.get(gh_url, headers=headers, params=cast(Any, params))
13481351

1349-
if not issue_number or not issue_url:
1350-
logging.error("Could not find issue number or URL for existing GitHub issue")
1351-
return existing_issue
1352+
if response.status_code == 200:
1353+
issues = response.json()
13521354

1353-
# Create a comment noting the resubmission
1354-
from datetime import datetime
1355+
# Look for an issue with matching title
1356+
for issue in issues:
1357+
if issue.get("title") == expected_title:
1358+
updated_issue = update_github_issue_for_resubmission(issue, user, headers)
1359+
return updated_issue
1360+
else:
1361+
return None # No matching gihub issues
1362+
else:
1363+
raise HTTPException(
1364+
status_code=response.status_code,
1365+
detail=f"Search for existing Github issues failed: {response.reason}",
1366+
)
1367+
1368+
1369+
def update_github_issue_for_resubmission(existing_issue, user, headers):
1370+
"""
1371+
Update an existing GitHub issue to note that the submission was resubmitted.
1372+
Adds a comment and reopens the issue if it was closed.
1373+
"""
1374+
issue_url = existing_issue.get("url") # API URL for the issue
13551375

1356-
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
1376+
# Create a comment noting the resubmission
1377+
from datetime import datetime
13571378

1358-
comment_body = f"""
1379+
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
1380+
comment_body = f"""
13591381
## 🔄 Submission Resubmitted
13601382
13611383
**Resubmitted by:** {user.name} ({user.orcid})
13621384
**Timestamp:** {timestamp}
13631385
**Status:** {SubmissionStatusEnum.SubmittedPendingReview.text}
13641386
13651387
The submission has been updated and resubmitted for review.
1366-
""".strip()
1388+
""".strip()
1389+
1390+
# Add comment to the issue
1391+
comment_url = f"{issue_url}/comments"
1392+
comment_payload = {"body": comment_body}
13671393

1368-
# Add comment to the issue
1369-
comment_url = f"{issue_url}/comments"
1370-
comment_payload = {"body": comment_body}
1394+
comment_response = requests.post(comment_url, headers=headers, data=json.dumps(comment_payload))
13711395

1372-
comment_response = requests.post(
1373-
comment_url, headers=headers, data=json.dumps(comment_payload)
1396+
if comment_response.status_code != 201:
1397+
raise HTTPException(
1398+
status_code=comment_response.status_code,
1399+
detail=f"Failed to add comment to GitHub issue: {comment_response.reason}",
13741400
)
13751401

1376-
if comment_response.status_code == 201:
1377-
logging.info(f"Successfully added resubmission comment to GitHub issue #{issue_number}")
1378-
else:
1379-
logging.error(f"Failed to add comment to GitHub issue: {comment_response.status_code}")
1402+
# If the issue is closed, reopen it
1403+
if existing_issue.get("state") == "closed":
1404+
reopen_payload = {"state": "open", "state_reason": "reopened"}
13801405

1381-
# If the issue is closed, reopen it
1382-
if existing_issue.get("state") == "closed":
1383-
reopen_payload = {"state": "open", "state_reason": "reopened"}
1406+
reopen_response = requests.patch(
1407+
issue_url, headers=headers, data=json.dumps(reopen_payload)
1408+
)
13841409

1385-
reopen_response = requests.patch(
1386-
issue_url, headers=headers, data=json.dumps(reopen_payload)
1410+
if reopen_response.status_code != 200:
1411+
raise HTTPException(
1412+
status_code=reopen_response.status_code,
1413+
detail=f"Failed to reopen GitHub issue: {reopen_response.reason}",
13871414
)
13881415

1389-
if reopen_response.status_code == 200:
1390-
logging.info(f"Successfully reopened GitHub issue #{issue_number}")
1391-
else:
1392-
logging.error(f"Failed to reopen GitHub issue: {reopen_response.status_code}")
1393-
1394-
return existing_issue
1395-
1396-
except Exception as e:
1397-
logging.error(f"Error updating GitHub issue for resubmission: {str(e)}")
1398-
return existing_issue
1399-
1400-
1401-
def check_existing_github_issue(submission_id: UUID, headers: dict, gh_base_url: str, user):
1402-
"""
1403-
Check if a GitHub issue already exists for the given submission ID using GitHub's search API.
1404-
"""
1405-
try:
1406-
repo_url = gh_base_url.replace("/issues", "")
1407-
search_url = f"{repo_url}/issues"
1408-
expected_title = f"NMDC Submission: {submission_id}"
1409-
params = {
1410-
"state": "all",
1411-
"per_page": 100,
1412-
}
1413-
response = requests.get(search_url, headers=headers, params=cast(Any, params))
1414-
1415-
if response.status_code == 200:
1416-
issues = response.json()
1417-
1418-
# Look for an issue with matching title
1419-
for issue in issues:
1420-
if issue.get("title") == expected_title:
1421-
logging.info(f"Found existing GitHub issue for submission {submission_id}")
1422-
updated_issue = update_github_issue_for_resubmission(issue, user, headers)
1423-
return updated_issue
1424-
else:
1425-
logging.info(f"No existing GitHub issue found for submission {submission_id}")
1426-
return None
1427-
else:
1428-
logging.warning(f"Failed to search GitHub issues: {response.status_code}")
1429-
return None
1430-
1431-
except Exception as e:
1432-
logging.error(f"Error searching GitHub issues: {str(e)}")
1433-
return None
1416+
return existing_issue
14341417

14351418

14361419
@router.delete(

tests/test_submission.py

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,11 +1430,42 @@ def test_delete_submission_study_images_success(
14301430
assert other_blob.exists() is True
14311431

14321432

1433+
@pytest.mark.parametrize(
1434+
"test_status,code",
1435+
[
1436+
(SubmissionStatusEnum.InProgress.text, 200),
1437+
(SubmissionStatusEnum.SubmittedPendingReview.text, 403),
1438+
],
1439+
)
1440+
def test_edit_submission_status_uneditable(
1441+
db: Session, client: TestClient, logged_in_user, test_status, code
1442+
):
1443+
"""Test that a submission with an uneditable status (anything other than InProgress or UpdatesRequired)"""
1444+
submission = fakes.MetadataSubmissionFactory(status=test_status)
1445+
fakes.SubmissionRoleFactory(
1446+
submission=submission,
1447+
submission_id=submission.id,
1448+
user_orcid=logged_in_user.orcid,
1449+
role=SubmissionEditorRole.owner,
1450+
)
1451+
payload = SubmissionMetadataSchema.model_validate(submission)
1452+
db.commit()
1453+
response = client.request(
1454+
method="patch",
1455+
url=f"/api/metadata_submission/{submission.id}",
1456+
json=jsonable_encoder(payload),
1457+
)
1458+
assert response.status_code == code
1459+
1460+
14331461
def test_github_issue_resubmission_creates_comment_only(
14341462
db: Session, client: TestClient, logged_in_user
14351463
):
1436-
"""Test that when a GitHub issue already exists for a submission,
1437-
only a comment is created (no new issue)."""
1464+
"""
1465+
Confirm that when a submission status becomes 'SubmittedPendingReview',
1466+
the Github API searches for an existing issue with the same ID and either
1467+
creates one if it doesn't exist or adds a comment if it does
1468+
"""
14381469

14391470
# Create a submission
14401471
submission = fakes.MetadataSubmissionFactory(
@@ -1460,33 +1491,30 @@ def test_github_issue_resubmission_creates_comment_only(
14601491
"state": "open",
14611492
}
14621493

1463-
# Mock responses for the GitHub API calls
1464-
mock_responses = []
1465-
1466-
# Mock the search for existing issues (returns the existing issue)
1494+
# Fake response from github API call searching for the existing issue
14671495
search_response = Mock()
14681496
search_response.status_code = 200
1469-
search_response.json.return_value = [existing_issue] # List of issues from repo issues endpoint
1470-
mock_responses.append(search_response)
1497+
search_response.json.return_value = [existing_issue]
14711498

1472-
# Mock the comment creation response
1499+
# Fake response from github API call making a comment on existing issue
14731500
comment_response = Mock()
14741501
comment_response.status_code = 201
14751502
comment_response.json.return_value = {"id": 456, "body": "comment content"}
1476-
mock_responses.append(comment_response)
14771503

1478-
# Patch the requests.get and requests.post calls
1504+
# Patch the normal API settings as well as
1505+
# search(requests.get) and comment (requests.post) API calls
1506+
# with their mock versions defined below
14791507
with patch("nmdc_server.api.requests.get") as mock_get, patch(
14801508
"nmdc_server.api.requests.post"
14811509
) as mock_post, patch("nmdc_server.api.settings") as mock_settings:
14821510

1483-
# Configure settings
1511+
# Configure fake github settings
14841512
mock_settings.github_issue_url = "https://api.github.com/repos/owner/repo/issues"
14851513
mock_settings.github_authentication_token = "fake_token"
14861514
mock_settings.github_issue_assignee = "assignee"
14871515
mock_settings.host = "test-host"
14881516

1489-
# Set up the mock responses
1517+
# Set fake responses for search and comment API calls
14901518
mock_get.return_value = search_response
14911519
mock_post.return_value = comment_response
14921520

@@ -1495,34 +1523,26 @@ def test_github_issue_resubmission_creates_comment_only(
14951523
"status": SubmissionStatusEnum.SubmittedPendingReview.text,
14961524
"metadata_submission": {},
14971525
}
1498-
14991526
response = client.request(
15001527
method="PATCH",
15011528
url=f"/api/metadata_submission/{submission.id}",
15021529
json=payload,
15031530
)
1504-
15051531
assert response.status_code == 200
15061532

1507-
# Verify that requests.get was called to search for existing issues
1533+
# Verify that the mock version of requests.get was used to search existing issues once
15081534
assert mock_get.call_count == 1
15091535
get_call = mock_get.call_args
15101536
assert "https://api.github.com/repos/owner/repo/issues" in get_call[0][0]
15111537

1512-
# Verify that requests.post was called to create a comment (not a new issue)
1538+
# Verify that the mock version of request.post was used to create a comment once (not a new issue)
15131539
assert mock_post.call_count == 1
15141540
post_call = mock_post.call_args
1515-
1516-
# Verify the comment endpoint was called
15171541
assert post_call[0][0] == "https://api.github.com/repos/owner/repo/issues/123/comments"
15181542

1519-
# Verify the comment content includes resubmission information
1543+
# Verify the mocked comment includes resubmission information
15201544
comment_data = json.loads(post_call[1]["data"])
15211545
assert "Submission Resubmitted" in comment_data["body"]
15221546
assert logged_in_user.name in comment_data["body"]
15231547
assert logged_in_user.orcid in comment_data["body"]
15241548
assert SubmissionStatusEnum.SubmittedPendingReview.text in comment_data["body"]
1525-
1526-
# Verify headers include authorization
1527-
headers = post_call[1]["headers"]
1528-
assert headers["Authorization"] == "Bearer fake_token"

0 commit comments

Comments
 (0)