Skip to content

Commit

Permalink
Merge pull request #70 from TheJacksonLaboratory/G3-490-update-genewe…
Browse files Browse the repository at this point in the history
…aver-db-for-threshold-fixes

G3-490: Improvements for set threshold fixes.
  • Loading branch information
bergsalex authored Oct 23, 2024
2 parents a40a2ff + a8cc0d4 commit 7f84314
Show file tree
Hide file tree
Showing 10 changed files with 603 additions and 269 deletions.
460 changes: 235 additions & 225 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "geneweaver-db"
version = "0.5.0"
version = "0.6.0a0"
description = "Database Interaction Services for GeneWeaver"
authors = ["Jax Computational Sciences <[email protected]>"]
readme = "README.md"
Expand Down
19 changes: 19 additions & 0 deletions src/geneweaver/db/aio/threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,37 @@
from psycopg import AsyncCursor


async def user_can_set_threshold(
cursor: AsyncCursor, user_id: int, geneset_id: int
) -> bool:
"""Check if a user can set the threshold of a geneset.
:param cursor: An async database cursor.
:param user_id: The ID of the user to check.
:param geneset_id: The ID of the geneset to check.
:return: True if the user can set the threshold, False otherwise.
"""
await cursor.execute(*threshold_query.user_can_set_threshold(user_id, geneset_id))
return bool(await cursor.fetchone())


async def set_geneset_threshold(
cursor: AsyncCursor,
user_id: int,
geneset_id: int,
geneset_score_type: GenesetScoreType,
) -> None:
"""Update the threshold of a geneset and its values.
:param cursor: An async database cursor.
:param user_id: The ID of the user.
:param geneset_id: The ID of the geneset to update.
:param geneset_score_type: The score threshold to set.
:return: Nothing.
"""
if not (await user_can_set_threshold(cursor, user_id, geneset_id)):
raise ValueError("User cannot set threshold for geneset")

await cursor.execute(
*threshold_query.set_geneset_threshold(geneset_id, geneset_score_type)
)
Expand Down
134 changes: 134 additions & 0 deletions src/geneweaver/db/aio/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Functions for querying the user table.
The functions that return one or more entire user records are:
- by_api_key
- by_sso_id
- by_user_id
- by_email
"""

from typing import List

from geneweaver.db.query import user
from geneweaver.db.utils import temp_override_row_factory
from psycopg import AsyncCursor, rows


async def by_api_key(cursor: AsyncCursor, api_key: str) -> List:
"""Get user info by api key.
:param cursor: The database cursor.
:param api_key: The api key to search for.
:return: list of results using `.fetchall()`
"""
await cursor.execute(*user.by_api_key(api_key))
return await cursor.fetchall()


async def by_sso_id(cursor: AsyncCursor, sso_id: str) -> List:
"""Get user info by sso id.
:param cursor: The database cursor.
:param sso_id: The sso id to search for.
:return: list of results using `.fetchall()`
"""
await cursor.execute(*user.by_sso_id(sso_id))
return await cursor.fetchall()


async def by_sso_id_and_email(cursor: AsyncCursor, sso_id: str, email: str) -> List:
"""Get user info by sso id and email.
:param cursor: The database cursor.
:param sso_id: The sso id to search for.
:param email: The email to search for.
:return: list of results using `.fetchall()`
"""
await cursor.execute(*user.by_sso_id_and_email(sso_id, email))
return await cursor.fetchall()


async def by_user_id(cursor: AsyncCursor, user_id: int) -> List:
"""Get user info by user id.
:param cursor: The database cursor.
:param user_id: The user id (internal) to search for.
:return: list of results using `.fetchall()`
"""
await cursor.execute(*user.by_id(user_id))
return await cursor.fetchall()


async def by_email(cursor: AsyncCursor, email: str) -> List:
"""Get user info by email.
:param cursor: The database cursor.
:param email: The email to search for.
:return: list of results using `.fetchall()`
"""
await cursor.execute(*user.by_email(email))
return await cursor.fetchall()


@temp_override_row_factory(rows.tuple_row)
async def email_exists(cursor: AsyncCursor, email: str) -> bool:
"""Check if email exists.
:param cursor: The database cursor.
:param email: The email to check.
:return: True if the email exists, otherwise False.
"""
await cursor.execute(*user.email_exists(email))
exists = bool((await cursor.fetchone())[0])
return exists


@temp_override_row_factory(rows.tuple_row)
async def sso_id_exists(cursor: AsyncCursor, sso_id: str) -> bool:
"""Check if sso id exists.
:param cursor: The database cursor.
:param sso_id: The sso id to check.
:return: True if the sso id exists, otherwise False.
"""
await cursor.execute(*user.sso_id_exists(sso_id))
exists = bool((await cursor.fetchone())[0])
return exists


@temp_override_row_factory(rows.tuple_row)
async def is_curator_or_higher(cursor: AsyncCursor, user_id: int) -> bool:
"""Check if a user is a curator or higher.
:param cursor: The database cursor.
:param user_id: The user id to check.
:return: True if the user is a curator or higher, otherwise False.
"""
await cursor.execute(*user.is_curator_or_higher(user_id))
exists = bool((await cursor.fetchone())[0])
return exists


@temp_override_row_factory(rows.tuple_row)
async def is_assigned_curation(
cursor: AsyncCursor, user_id: int, geneset_id: int
) -> bool:
"""Check if a user is assigned curation.
:param cursor: The database cursor.
:param user_id: The user id to check.
:param geneset_id: The geneset id to check.
:return: True if the user is assigned curation, otherwise False.
"""
await cursor.execute(*user.is_assigned_curation(user_id, geneset_id))
exists = bool((await cursor.fetchone())[0])
return exists
2 changes: 1 addition & 1 deletion src/geneweaver/db/query/search/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def genesets(
"JOIN geneset_search ON geneset_search.gs_id = geneset.gs_id"
)

filtering, params = is_readable(filtering, params, is_readable_by, "geneset_search")
filtering, params = is_readable(filtering, params, is_readable_by)
filtering, params = search(
filtering, params, const.SEARCH_COMBINED_COL, search_text
)
Expand Down
37 changes: 37 additions & 0 deletions src/geneweaver/db/query/threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Tuple

from geneweaver.core.schema.score import GenesetScoreType
from geneweaver.db.query import user
from psycopg.sql import SQL, Composed


Expand Down Expand Up @@ -100,3 +101,39 @@ def set_geneset_value_threshold(
query = query.join(" ")

return query, params


def user_can_set_threshold(user_id: int, geneset_id: int) -> Tuple[Composed, dict]:
"""Check if a user can set the threshold of a geneset.
A user can set the threshold of a geneset if:
- They own the Geneset, or
- They are an admin, or
- They are a curator, or
- They are assigned curation on the geneset.
:param user_id: The ID of the user to check.
:param geneset_id: The ID of the geneset to check.
:return: A query (and params) that can be executed on a cursor.
"""
params = {"user_id": user_id, "geneset_id": geneset_id}
query = (
SQL("SELECT EXISTS(")
+ SQL("SELECT 1")
+ SQL("FROM geneset g")
+ SQL("WHERE g.gs_id = %(geneset_id)s")
+
# The user must either
# 1) own the geneset, or
SQL("AND g.usr_id = %(user_id)s")
+
# 2) be a curator or an admin, or
SQL("OR")
+ user.is_curator_or_higher__query()
+
# 3) be assigned curation on the geneset.
SQL("OR")
+ user.is_assigned_curation__query()
+ SQL(");")
).join(" ")
return query, params
116 changes: 116 additions & 0 deletions src/geneweaver/db/query/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Query Builder for Users DB functions."""

from typing import Tuple

from geneweaver.db.utils import format_sql_fields
from psycopg.sql import SQL, Composed

USER_FIELD_MAP = {
"usr_id": "id",
"usr_email": "email",
"user_prefs": "prefs",
"is_guest": "is_guest",
"usr_first_name": "first_name",
"usr_last_name": "last_name",
"usr_admin": "admin",
"usr_last_seen": "last_seen",
"usr_created": "created",
"ip_addr": "ip_address",
"apikey": "api_key",
"usr_sso_id": "sso_id",
}

USER_FIELDS = format_sql_fields(USER_FIELD_MAP, query_table="usr")


USER_QUERY = SQL("SELECT") + SQL(",").join(USER_FIELDS) + SQL("FROM usr")


def by_id(user_id: int) -> Tuple[Composed, dict]:
"""Get user by id."""
query = USER_QUERY + SQL("WHERE usr_id = %(user_id)s;")
params = {"user_id": user_id}
return query.join(" "), params


def by_sso_id(sso_id: str) -> Tuple[Composed, dict]:
"""Get user by sso id."""
query = USER_QUERY + SQL("WHERE usr_sso_id = %(user_id)s;")
params = {"sso_id": sso_id}
return query.join(" "), params


def by_email(email: str) -> Tuple[Composed, dict]:
"""Get user by email."""
query = USER_QUERY + SQL("WHERE usr_email = %(email)s;")
params = {"email": email}
return query.join(" "), params


def by_sso_id_and_email(sso_id: str, email: str) -> Tuple[Composed, dict]:
"""Get user by sso id and email."""
query = USER_QUERY + SQL("WHERE usr_sso_id = %(sso_id)s AND usr_email = %(email)s;")
params = {"sso_id": sso_id, "email": email}
return query.join(" "), params


def by_api_key(api_key: str) -> Tuple[Composed, dict]:
"""Get user by api key."""
query = USER_QUERY + SQL("WHERE apikey = %(api_key)s;")
params = {"api_key": api_key}
return query.join(" "), params


def email_exists(email: str) -> Tuple[Composed, dict]:
"""Check if email exists."""
query = SQL("SELECT") + SQL(
"EXISTS(SELECT 1 FROM usr WHERE usr_email = %(email)s);"
)
params = {"email": email}
return query.join(" "), params


def sso_id_exists(sso_id: str) -> Tuple[Composed, dict]:
"""Check if sso id exists."""
query = SQL("SELECT") + SQL(
"EXISTS(SELECT 1 FROM usr WHERE usr_sso_id = %(sso_id)s);"
)
params = {"sso_id": sso_id}
return query.join(" "), params


def is_curator_or_higher__query() -> Composed:
"""Build SQL query to check if user is a curator or higher."""
return (
SQL("EXISTS(")
+ SQL("SELECT 1")
+ SQL("FROM usr")
+ SQL("WHERE usr_id = %(user_id)s")
+ SQL("AND usr_admin > 0)")
)


def is_assigned_curation__query() -> Composed:
"""Build SQL query to check if user is assigned curation."""
return (
SQL("EXISTS(")
+ SQL("SELECT 1")
+ SQL("FROM curation_assignments")
+ SQL("WHERE curator = %(user_id)s")
+ SQL("AND gs_id = %(geneset_id)s")
+ SQL("AND curation_state = 2)")
)


def is_curator_or_higher(user_id: int) -> Tuple[Composed, dict]:
"""Check if user is a curator or higher."""
query = SQL("SELECT") + is_curator_or_higher__query() + SQL(";")
params = {"user_id": user_id}
return query.join(" "), params


def is_assigned_curation(user_id: int, geneset_id: int) -> Tuple[Composed, dict]:
"""Check if user is assigned curation."""
query = SQL("SELECT") + is_assigned_curation__query() + SQL(";")
params = {"user_id": user_id, "geneset_id": geneset_id}
return query.join(" "), params
19 changes: 18 additions & 1 deletion src/geneweaver/db/threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,35 @@
from psycopg import Cursor


def user_can_set_threshold(cursor: Cursor, user_id: int, geneset_id: int) -> bool:
"""Check if a user can set the threshold of a geneset.
:param cursor: An async database cursor.
:param user_id: The ID of the user to check.
:param geneset_id: The ID of the geneset to check.
:return: True if the user can set the threshold, False otherwise.
"""
cursor.execute(*threshold_query.user_can_set_threshold(user_id, geneset_id))
return bool(cursor.fetchone())


def set_geneset_threshold(
cursor: Cursor,
user_id: int,
geneset_id: int,
geneset_score_type: GenesetScoreType,
) -> None:
"""Update the threshold of a geneset and its values.
:param cursor: An database cursor.
:param cursor: A database cursor.
:param user_id: The ID of the user.
:param geneset_id: The ID of the geneset to update.
:param geneset_score_type: The score threshold to set.
:return: Nothing.
"""
if not user_can_set_threshold(cursor, user_id, geneset_id):
raise ValueError("User cannot set threshold for geneset")

cursor.execute(
*threshold_query.set_geneset_threshold(geneset_id, geneset_score_type)
)
Expand Down
Loading

0 comments on commit 7f84314

Please sign in to comment.