Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: jsonschema validation for backend #15

Merged
merged 84 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
9c05aa1
feat: validation schema for sign up endpoint
slugb0t Oct 11, 2023
69fbd84
style: 🎨 fix code style issues with Black
lint-action Oct 11, 2023
24f49a6
feat: validate email with email_validator
slugb0t Oct 11, 2023
21478ab
feat: schema validation for login
slugb0t Oct 11, 2023
588992b
feat: schema validation for user details PUT
slugb0t Oct 11, 2023
8ba4bb3
merge: merge conflicts resolved
slugb0t Oct 11, 2023
fe6a6d8
feat: POST, PUT schemas added for /study endpoint
slugb0t Oct 11, 2023
286cd1a
feat: schema validation added for arm metadata
slugb0t Oct 11, 2023
b1c1ba7
style: 🎨 fix code style issues with Black
lint-action Oct 12, 2023
44a11ed
feat: schema validation for available ipd
slugb0t Oct 12, 2023
f3c68aa
style: 🎨 fix code style issues with Black
lint-action Oct 12, 2023
e5206c3
feat: study contact schema validation added
slugb0t Oct 12, 2023
388f87f
Merge branch 'json-schema' of https://github.com/AI-READI/api.fairhub…
slugb0t Oct 12, 2023
76172fd
feat: schema validation for collaborators
slugb0t Oct 12, 2023
b3ab32d
style: 🎨 fix code style issues with Black
lint-action Oct 12, 2023
9ce4f11
feat: conditions schema validation added"
slugb0t Oct 12, 2023
b42d4e2
Merge branch 'json-schema' of https://github.com/AI-READI/api.fairhub…
slugb0t Oct 12, 2023
7380fa7
style: 🎨 fix code style issues with Black
lint-action Oct 12, 2023
8b3bb90
feat: send better error message for invalid email
slugb0t Oct 12, 2023
0f96342
feat: schema validation for study description
slugb0t Oct 12, 2023
9375fcb
feat: schema validation for study design
slugb0t Oct 12, 2023
90c86f2
feat: schema validation for study intervention
slugb0t Oct 12, 2023
0db9020
schema validation for ipd sharing
slugb0t Oct 12, 2023
0b378d9
feat: schema validation for study link
slugb0t Oct 12, 2023
5de56ba
fix: fixed import typo
slugb0t Oct 12, 2023
34df8f6
feat: schema validation for other metadata
slugb0t Oct 12, 2023
321998a
feat: schema validation for study location
slugb0t Oct 12, 2023
49c65bc
feat: schema validation for oversight endpoint
slugb0t Oct 12, 2023
5356787
feat: schema validation for study reference
slugb0t Oct 12, 2023
4dc9be5
feat: schema validation for sponsors collaborators
slugb0t Oct 12, 2023
ec7f51e
feat: schema validate password meets all criterias on signup
slugb0t Oct 12, 2023
9922795
wip: additional requirements/checks for schema validations
slugb0t Oct 13, 2023
8556516
style: 🎨 fix code style issues with Black
lint-action Oct 13, 2023
232dadb
feat: study design carries additional validations
slugb0t Oct 13, 2023
0517bd4
feat: study contact carries additional validations
slugb0t Oct 13, 2023
fdeb9c5
feat: study eligibility carries additional validations
slugb0t Oct 13, 2023
0a2d84d
feat: study intervention carries additional validations
slugb0t Oct 13, 2023
98ed136
feat: overal official has enums and other validations
slugb0t Oct 13, 2023
338bc67
feat: enums and other validations for location endpoint
slugb0t Oct 13, 2023
7a6f9d4
feat: enums and additional validations for ipd sharing
slugb0t Oct 13, 2023
5605dd3
feat: additional validations for study_status
slugb0t Oct 13, 2023
c354829
feat: additional validations for study reference
slugb0t Oct 13, 2023
38f96d4
feat: additional validations for available ipds
slugb0t Oct 13, 2023
32df276
feat: enums added and additional validations for sponsor collaborators
slugb0t Oct 13, 2023
d521557
additional enums and validations for study links
slugb0t Oct 13, 2023
940d3f7
refactor: removed marshal_with for validation errors
slugb0t Oct 13, 2023
e849203
feat: additional validations added for study contact
slugb0t Oct 13, 2023
6cb444e
feat: enums added for enrollment_type in design endpoint
slugb0t Oct 13, 2023
d7e3377
refactor: other metadata validation formatted
slugb0t Oct 13, 2023
ac4baeb
feat: minimum length added for validating sponsor collaborators
slugb0t Oct 13, 2023
058518a
merge: merge staging to current branch
slugb0t Oct 14, 2023
98233ce
fix: remove double imports
slugb0t Oct 14, 2023
31775c0
refactor: isort imports
slugb0t Oct 14, 2023
b3b3e2a
style: 🎨 fix code style issues with Black
lint-action Oct 14, 2023
a0d7d4c
feat: intervention schema validation added
slugb0t Oct 16, 2023
875b5a4
feat: conditional requirements added to status metadata
slugb0t Oct 17, 2023
fdc1ab5
feat: conditional validations added for sponsor collaborators
slugb0t Oct 17, 2023
b6af1e9
refactor: endpoint comment added
slugb0t Oct 17, 2023
04f97f1
feat: additional validations for study identification
slugb0t Oct 17, 2023
595bc39
style: 🎨 fix code style issues with Black
lint-action Oct 17, 2023
2fdc860
feat: conditional validations and enums added for final endpoints
slugb0t Oct 17, 2023
f33efc5
style: 🎨 fix code style issues with Black
lint-action Oct 17, 2023
f1dbafb
fix: re-add study type for conditions endpoint
slugb0t Oct 17, 2023
67dcc70
fix: remove last marshal_with on put, fix type def for data (overall-…
slugb0t Oct 17, 2023
67d8d4c
Merge branch 'json-schema' of https://github.com/AI-READI/api.fairhub…
slugb0t Oct 17, 2023
42b1c0d
fix: update pytest fixtures for validation schemas
slugb0t Oct 18, 2023
1fbb121
style: 🎨 fix code style issues with Black
lint-action Oct 18, 2023
e22d098
fix: updated pytests for schema validations
slugb0t Oct 18, 2023
bd947da
style: 🎨 fix code style issues with Black
lint-action Oct 18, 2023
1685d0c
fix: fix: invalid characters for regex
slugb0t Oct 18, 2023
fec916c
Merge branch 'json-schema' of https://github.com/AI-READI/api.fairhub…
slugb0t Oct 18, 2023
af0fa10
fix: update flake8 errors
slugb0t Oct 18, 2023
a081030
style: 🎨 fix code style issues with Black
lint-action Oct 18, 2023
747d19b
fix: update for flake8 errors
slugb0t Oct 18, 2023
6ec13d6
Merge branch 'json-schema' of https://github.com/AI-READI/api.fairhub…
slugb0t Oct 18, 2023
c3f5efc
style: 🎨 fix code style issues with Black
lint-action Oct 18, 2023
83919b3
fix: updating for flake8 issues
slugb0t Oct 18, 2023
b32960e
style: 🎨 fix code style issues with Black
lint-action Oct 18, 2023
4a9de70
fix: correct issues for pyflake8
slugb0t Oct 18, 2023
e71ca58
style: 🎨 fix code style issues with Black
lint-action Oct 18, 2023
b955cb1
fix: type def for request.json
slugb0t Oct 18, 2023
fa9b075
Merge branch 'json-schema' of https://github.com/AI-READI/api.fairhub…
slugb0t Oct 18, 2023
115f469
fix: update for pylint issue
slugb0t Oct 18, 2023
77a4e37
🐛 fix: update schemas
megasanjay Oct 19, 2023
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
105 changes: 100 additions & 5 deletions apis/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
from typing import Any, Union

import jwt
from email_validator import EmailNotValidError, validate_email
from flask import g, make_response, request
from flask_restx import Namespace, Resource, fields
from jsonschema import FormatChecker, ValidationError, validate

import model

Expand Down Expand Up @@ -46,10 +48,71 @@ class SignUpUser(Resource):
def post(self):
"""signs up the new users and saves data in DB"""
data: Union[Any, dict] = request.json
# TODO data[email doesnt exist then raise error; json validation library
pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
if not data["email_address"] or not re.match(pattern, data["email_address"]):
return "Email address is invalid", 422

def validate_is_valid_email(instance):
# Turn on check_deliverability
# for first-time validations like on account creation pages (but not
# login pages).
email_address = instance
try:
validate_email(email_address, check_deliverability=False)
return True
except EmailNotValidError as e:
raise ValidationError("Invalid email address format") from e

def validate_password(instance):
password = instance
# Check if password is at least 8 characters long
if len(password) < 8:
raise ValidationError("Password must be at least 8 characters long")

# Check if password contains at least one lowercase letter
if not re.search(r"[a-z]", password):
raise ValidationError(
"Password must contain at least one lowercase letter"
)

# Check if password contains at least one uppercase letter
if not re.search(r"[A-Z]", password):
raise ValidationError(
"Password must contain at least one uppercase letter"
)

# Check if password contains at least one digit
if not re.search(r"[0-9]", password):
raise ValidationError("Password must contain at least one digit")

# Check if password contains at least one special character
if not re.search(r"[~`!@#$%^&*()_+\-={[}\]|:;\"'<,>.?/]", password):
raise ValidationError(
"Password must contain at least one special character"
)

return True

# Schema validation
schema = {
"type": "object",
"required": ["email_address", "password"],
"additionalProperties": False,
"properties": {
"email_address": {"type": "string", "format": "valid_email"},
"password": {
"type": "string",
"format": "password",
},
},
}

format_checker = FormatChecker()
format_checker.checks("valid_email")(validate_is_valid_email)
format_checker.checks("password")(validate_password)

try:
validate(instance=data, schema=schema, format_checker=format_checker)
except ValidationError as e:
return e.message, 400

user = model.User.query.filter_by(
email_address=data["email_address"]
).one_or_none()
Expand Down Expand Up @@ -80,8 +143,40 @@ def post(self):

email_address = data["email_address"]

user = model.User.query.filter_by(email_address=email_address).one_or_none()
def validate_is_valid_email(instance):
print("within is_valid_email")
email_address = instance
print(email_address)
try:
validate_email(email_address)
return True
except EmailNotValidError as e:
raise ValidationError("Invalid email address format") from e

# Schema validation
schema = {
"type": "object",
"required": ["email_address", "password"],
"additionalProperties": False,
"properties": {
"email_address": {
"type": "string",
"format": "valid email",
"error_message": "Invalid email address",
},
"password": {"type": "string", "minLength": 8},
},
}

format_checker = FormatChecker()
format_checker.checks("valid email")(validate_is_valid_email)

try:
validate(instance=data, schema=schema, format_checker=format_checker)
except ValidationError as e:
return e.message, 400

user = model.User.query.filter_by(email_address=email_address).one_or_none()
if not user:
return "Invalid credentials", 401

Expand Down
2 changes: 1 addition & 1 deletion apis/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def put(self, study_id: int, dataset_id: int):
study = model.Study.query.get(study_id)
if not is_granted("update_dataset", study):
return "Access denied, you can not modify", 403
data = request.json
data: typing.Union[dict, typing.Any] = request.json
data_obj = model.Dataset.query.get(dataset_id)
data_obj.update(data)
model.db.session.commit()
Expand Down
4 changes: 3 additions & 1 deletion apis/dataset_metadata/dataset_consent.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from flask import request
from flask_restx import Resource, fields

Expand Down Expand Up @@ -31,7 +33,7 @@ def get(self, study_id: int, dataset_id: int):
return [d.to_dict() for d in dataset_consent_]

def put(self, study_id: int, dataset_id: int):
data = request.json
data: typing.Union[dict, typing.Any] = request.json
dataset_ = model.Dataset.query.get(dataset_id)
dataset_consent_ = dataset_.dataset_consent.update(data)
model.db.session.commit()
Expand Down
4 changes: 3 additions & 1 deletion apis/dataset_metadata/dataset_date.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from flask import request
from flask_restx import Resource, fields

Expand Down Expand Up @@ -27,7 +29,7 @@ def get(self, study_id: int, dataset_id: int):
return [d.to_dict() for d in dataset_date_]

def put(self, study_id: int, dataset_id: int):
data = request.json
data: typing.Union[dict, typing.Any] = request.json
dataset_ = model.Dataset.query.get(dataset_id)
dataset_date_ = dataset_.dataset_date.update(data)
model.db.session.commit()
Expand Down
4 changes: 3 additions & 1 deletion apis/dataset_metadata/dataset_de_ident_level.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from flask import request
from flask_restx import Resource, fields

Expand Down Expand Up @@ -31,7 +33,7 @@ def get(self, study_id: int, dataset_id: int):
return [d.to_dict() for d in de_ident_level_]

def put(self, study_id: int, dataset_id: int):
data = request.json
data: typing.Union[dict, typing.Any] = request.json
dataset_ = model.Dataset.query.get(dataset_id)
de_ident_level_ = dataset_.dataset_de_ident_level.update(data)
model.db.session.commit()
Expand Down
4 changes: 3 additions & 1 deletion apis/dataset_metadata/dataset_managing_organization.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from flask import request
from flask_restx import Resource, fields

Expand Down Expand Up @@ -26,7 +28,7 @@ def get(self, study_id: int, dataset_id: int):
return [d.to_dict() for d in managing_organization_]

def put(self, study_id: int, dataset_id: int):
data = request.json
data: typing.Union[dict, typing.Any] = request.json
dataset_ = model.Dataset.query.get(dataset_id)
managing_organization_ = dataset_.dataset_managing_organization.update(data)
model.db.session.commit()
Expand Down
4 changes: 3 additions & 1 deletion apis/dataset_metadata/dataset_other.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from flask import request
from flask_restx import Resource, fields

Expand Down Expand Up @@ -30,7 +32,7 @@ def get(self, study_id: int, dataset_id: int):
return [d.to_dict() for d in dataset_other_]

def put(self, study_id: int, dataset_id: int):
data = request.json
data: typing.Union[dict, typing.Any] = request.json
dataset_ = model.Dataset.query.get(dataset_id)
dataset_other_ = dataset_.dataset_other.update(data)
model.db.session.commit()
Expand Down
4 changes: 3 additions & 1 deletion apis/dataset_metadata/dataset_readme.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from flask import request
from flask_restx import Resource, fields

Expand All @@ -22,7 +24,7 @@ def get(self, study_id: int, dataset_id: int):
return [d.to_dict() for d in dataset_readme_]

def put(self, study_id: int, dataset_id: int):
data = request.json
data: typing.Union[dict, typing.Any] = request.json
dataset_ = model.Dataset.query.get(dataset_id)
dataset_readme_ = dataset_.dataset_readme.update(data)
model.db.session.commit()
Expand Down
4 changes: 3 additions & 1 deletion apis/dataset_metadata/dataset_record_keys.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing

from flask import request
from flask_restx import Resource, fields

Expand Down Expand Up @@ -26,7 +28,7 @@ def get(self, study_id: int, dataset_id: int):
return [d.to_dict() for d in dataset_record_keys_]

def put(self, study_id: int, dataset_id: int):
data = request.json
data: typing.Union[dict, typing.Any] = request.json
dataset_ = model.Dataset.query.get(dataset_id)
dataset_record_keys_ = dataset_.dataset_de_ident_level.update(data)
model.db.session.commit()
Expand Down
2 changes: 1 addition & 1 deletion apis/dataset_metadata/dataset_related_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def post(self, study_id: int, dataset_id: int):
)
class DatasetRelatedItemUpdate(Resource):
def put(self, study_id: int, dataset_id: int, related_item_id: int):
data = request.json
data: Union[Any, dict] = request.json
dataset_related_item_ = model.DatasetRelatedItem.query.get(related_item_id)
dataset_related_item_.update(data)
model.db.session.commit()
Expand Down
36 changes: 36 additions & 0 deletions apis/study.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from flask import g, request
from flask_restx import Namespace, Resource, fields, reqparse
from jsonschema import ValidationError, validate

import model

Expand Down Expand Up @@ -55,7 +56,25 @@ def get(self):
@api.response(200, "Success")
@api.response(400, "Validation Error")
def post(self):
"""Create a new study"""
# Schema validation
schema = {
"type": "object",
"required": ["title", "image"],
"additionalProperties": False,
"properties": {
"title": {"type": "string", "minLength": 1},
"image": {"type": "string", "minLength": 1},
},
}

try:
validate(request.json, schema)
except ValidationError as e:
return e.message, 400

data: Union[Any, dict] = request.json

add_study = model.Study.from_data(data)
model.db.session.add(add_study)
study_id = add_study.id
Expand All @@ -81,6 +100,23 @@ def get(self, study_id: int):
@api.response(400, "Validation Error")
@api.doc(description="Update a study's details")
def put(self, study_id: int):
"""Update a study"""
# Schema validation
schema = {
"type": "object",
"required": ["title", "image"],
"additionalProperties": False,
"properties": {
"title": {"type": "string", "minLength": 1},
"image": {"type": "string", "minLength": 1},
},
}

try:
validate(request.json, schema)
except ValidationError as e:
return e.message, 400

update_study = model.Study.query.get(study_id)
if not is_granted("update_study", update_study):
return "Access denied, you can not modify", 403
Expand Down
38 changes: 38 additions & 0 deletions apis/study_metadata/study_arm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from flask import request
from flask_restx import Resource, fields
from jsonschema import ValidationError, validate

import model
from apis.study_metadata_namespace import api
Expand Down Expand Up @@ -46,6 +47,43 @@ def get(self, study_id):

def post(self, study_id):
"""Create study arm metadata"""
# Schema validation
schema = {
"type": "array",
"items": {
"type": "object",
"additionalProperties": False,
"properties": {
"label": {"type": "string", "minLength": 1},
"type": {
"type": "string",
"enum": [
"Experimental",
"Active Comparator",
"Placebo Comparator",
"Sham Comparator",
"No Intervention",
"Other",
],
},
"description": {"type": "string", "minLength": 1},
"intervention_list": {
"type": "array",
"items": {"type": "string", "minLength": 1},
"minItems": 1,
"uniqueItems": True,
},
},
"required": ["label", "type", "description", "intervention_list"],
},
"uniqueItems": True,
}

try:
validate(request.json, schema)
except ValidationError as e:
return e.message, 400

study: model.Study = model.Study.query.get(study_id)
if not is_granted("study_metadata", study):
return "Access denied, you can not delete study", 403
Expand Down
Loading