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
19 changes: 18 additions & 1 deletion store/backend/neurostore/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Base Classes/functions for constructing views
"""

import json
import re

from connexion.context import context
Expand All @@ -11,6 +12,7 @@
abort_permission,
abort_validation,
abort_not_found,
abort_unprocessable,
)

from psycopg2 import errors
Expand Down Expand Up @@ -45,6 +47,12 @@
from . import data as viewdata


@parser.error_handler
def handle_parser_error(err, req, schema, *, error_status_code, error_headers):
detail = json.dumps(err.messages)
abort_unprocessable(f"input does not conform to specification: {detail}")


def create_user():
from auth0.v3.authentication.users import Users

Expand Down Expand Up @@ -128,10 +136,13 @@ def update_annotations(self, annotations):
create_annotation_analyses = []
for result in results:
if result.analysis_id and not result.annotation_analysis_id:
note_payload = result.note or self._build_default_note(result.note_keys)
if note_payload is None:
note_payload = {}
params = {
"analysis_id": result.analysis_id,
"annotation_id": result.id,
"note": result.note or {},
"note": note_payload,
"user_id": result.user_id,
"study_id": result.study_id,
"studyset_id": result.studyset_id,
Expand Down Expand Up @@ -215,6 +226,12 @@ def update_base_studies(self, base_studies):
def eager_load(self, q, args):
return q

@staticmethod
def _build_default_note(note_keys):
if not note_keys:
return None
return {key: None for key in note_keys.keys()}

def db_validation(self, record, data):
"""
Custom validation for database constraints.
Expand Down
14 changes: 14 additions & 0 deletions store/backend/neurostore/schemas/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
pre_dump,
pre_load,
post_load,
validates_schema,
EXCLUDE,
ValidationError,
)
Expand Down Expand Up @@ -662,6 +663,19 @@ def add_id(self, data, **kwargs):
data["studyset"] = {"id": data.pop("studyset_id")}
return data

@validates_schema
def validate_notes(self, data, **kwargs):
notes = data.get("annotation_analyses") or []
invalid = {}

for idx, note in enumerate(notes):
note_payload = note.get("note") if isinstance(note, dict) else None
if not isinstance(note_payload, dict) or len(note_payload) == 0:
invalid[idx] = ["note must include at least one field"]

if invalid:
raise ValidationError({"notes": invalid})


class BaseSnapshot(object):
def __init__(self):
Expand Down
42 changes: 42 additions & 0 deletions store/backend/neurostore/tests/api/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,48 @@ def test_post_blank_annotation(auth_client, ingest_neurosynth, session):
assert annot.annotation_analyses[0].user_id == annot.user_id


def test_blank_annotation_populates_note_fields(auth_client, ingest_neurosynth, session):
dset = Studyset.query.first()
note_keys = {"included": "boolean", "quality": "string"}
payload = {
"studyset": dset.id,
"note_keys": note_keys,
"name": "with defaults",
}

resp = auth_client.post("/api/annotations/", data=payload)
assert resp.status_code == 200

for note in resp.json()["notes"]:
assert set(note["note"].keys()) == set(note_keys.keys())
assert all(value is None for value in note["note"].values())


def test_annotation_rejects_empty_note(auth_client, ingest_neurosynth, session):
dset = Studyset.query.first()
study = dset.studies[0]
analysis = study.analyses[0]

payload = {
"studyset": dset.id,
"notes": [
{
"study": study.id,
"analysis": analysis.id,
"note": {},
}
],
"note_keys": {"included": "boolean"},
"name": "invalid annotation",
}

resp = auth_client.post("/api/annotations/", data=payload)

assert resp.status_code == 422
error = resp.json()
assert "note must include at least one field" in error["detail"]


def test_post_annotation(auth_client, ingest_neurosynth, session):
dset = Studyset.query.first()
# y for x in non_flat for y in x
Expand Down
Loading