Skip to content

Commit

Permalink
WIP project/permission
Browse files Browse the repository at this point in the history
  • Loading branch information
eboileau committed May 15, 2024
1 parent 20e2f59 commit f2625ed
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 30 deletions.
27 changes: 25 additions & 2 deletions server/src/scimodom/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
add_annotation,
add_assembly,
add_project,
add_user_to_project,
add_dataset,
add_all,
validate_dataset_title,
Expand Down Expand Up @@ -113,12 +114,34 @@ def annotation(id):
"project", epilog="Check docs at https://dieterich-lab.github.io/scimodom/."
)
@click.argument("template", type=click.Path(exists=True))
def project(template):
@click.option(
"--skip-add-user",
is_flag=True,
show_default=True,
default=False,
help="Do not add user to project.",
)
def project(template, skip_add_user):
"""Add a new project to the database.
TEMPLATE is the path to a project template (json).
"""
add_project(template)
add_user = not skip_add_user
add_project(template, add_user=add_user)

@app.cli.command(
"permission", epilog="Check docs at https://dieterich-lab.github.io/scimodom/."
)
@click.argument("username", type=click.STRING)
@click.argument("smid", type=click.STRING)
def permission(username, smid):
"""Force add a user to a project.
\b
USERNAME is the user email.
SMID is the project ID to which this user is to be associated.
"""
add_user_to_project(username, smid)

@app.cli.command(
"dataset", epilog="Check docs at https://dieterich-lab.github.io/scimodom/."
Expand Down
81 changes: 72 additions & 9 deletions server/src/scimodom/plugins/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import traceback

import click
from sqlalchemy import select

from scimodom.config import Config
from scimodom.database.database import get_session
from scimodom.database.models import (
Project,
Dataset,
Modification,
DetectionTechnology,
Expand All @@ -18,9 +20,10 @@
import scimodom.database.queries as queries
from scimodom.services.annotation import AnnotationService
from scimodom.services.assembly import AssemblyService
from scimodom.services.project import ProjectService
from scimodom.services.project import get_project_service
from scimodom.services.dataset import DataService
from scimodom.services.setup import get_setup_service
from scimodom.services.user import get_user_service, NoSuchUser
import scimodom.utils.utils as utils


Expand Down Expand Up @@ -97,14 +100,15 @@ def add_annotation(annotation_id: int) -> None:
session.close()


def add_project(project_template: str | Path) -> None:
def add_project(project_template: str | Path, add_user: bool = True) -> None:
"""Provides a CLI function to add a new project.
:param project_template: Path to a json file with
require fields.
:type project_template: str or Path
:param add_user: Associate user and newly created project
:type add_user: bool
"""
session = get_session()
# load project metadata
project = json.load(open(project_template))
# add project
Expand All @@ -115,13 +119,72 @@ def add_project(project_template: str | Path) -> None:
c = click.getchar()
if c not in ["y", "Y"]:
return
service = ProjectService(session, project)
service.create_project()
project_service = get_project_service()
project_service.create_project(project)
click.secho(
f"Successfully created. The SMID for this project is {service.get_smid()}.",
f"Successfully created. The SMID for this project is {project_service.get_smid()}.",
fg="green",
)
session.close()
if add_user:
username = project["contact_email"]
click.secho(f"Adding user {username} to newly created project...", fg="green")
click.secho("Continue [y/n]?", fg="green")
c = click.getchar()
if c not in ["y", "Y"]:
return
user_service = get_user_service()
try:
user = user_service.get_user_by_email(username)
except NoSuchUser:
click.secho(
f"Unknown user {username}... Aborting!",
fg="red",
)
else:
project_service.associate_project_to_user(user)
click.secho(
"Successfully added user to project.",
fg="green",
)


def add_user_to_project(username: str, smid: str) -> None:
"""Provides a CLI function to force add a user to a project.
:param username: Username (email)
:type username: str
:param smid: SMID
:type smid: str
"""
session = get_session()
click.secho(f"Adding user {username} to {smid}...", fg="green")
click.secho("Continue [y/n]?", fg="green")
c = click.getchar()
if c not in ["y", "Y"]:
return
project_service = get_project_service()
user_service = get_user_service()
try:
user = user_service.get_user_by_email(username)
except NoSuchUser:
click.secho(
f"Unknown user {username}... Aborting!",
fg="red",
)
else:
query = select(Project.id)
smids = session.execute(query).scalars().all()
if smid not in smids:
click.secho(
f"Unrecognised SMID {smid}... Aborting!",
fg="red",
)
return
project_service.associate_project_to_user(user, smid=smid)
click.secho(
"Successfully added user to project.",
fg="green",
)


def add_dataset(
Expand Down Expand Up @@ -210,8 +273,8 @@ def add_all(directory: Path, templates: list[str]) -> None:
project_title = project["title"]
click.secho(f"Adding {project_title}...", fg="green")
try:
project_service = ProjectService(session, project)
project_service.create_project()
project_service = get_project_service()
project_service.create_project(project)
smid = project_service.get_smid()
metadata = _get_dataset(project, extra_cols["file"])
for filen, meta in metadata.items():
Expand Down
18 changes: 16 additions & 2 deletions server/src/scimodom/services/permission.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class PermissionService:
def __init__(self, session: Session):
self._db_session = session
self._session = session

def may_change_dataset(self, user: User, dataset: Dataset) -> bool:
query = select(UserProjectAssociation).where(
Expand All @@ -18,9 +18,23 @@ def may_change_dataset(self, user: User, dataset: Dataset) -> bool:
UserProjectAssociation.project_id == dataset.project_id,
)
)
results = self._db_session.execute(query).fetchall()
results = self._session.execute(query).fetchall()
return len(results) > 0

def insert_into_user_project_association(self, user: User, project_id: str) -> None:
"""Insert values into table.
:param user: User
:type user: User
:param project_id: SMID. There is no check
on the validity of this value, this must be done
before calling this function.
:type project_id: str
"""
permission = UserProjectAssociation(user_id=user.id, project_id=project_id)
self._session.add(permission)
self._session.commit()


_cached_permission_service: Optional[PermissionService] = None

Expand Down
73 changes: 62 additions & 11 deletions server/src/scimodom/services/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import json
import logging
from pathlib import Path
from typing import ClassVar
from typing import ClassVar, Optional

from sqlalchemy.orm import Session
from sqlalchemy import select, func

from scimodom.config import Config
from scimodom.database.database import get_session
from scimodom.database.models import (
Project,
ProjectSource,
Expand All @@ -18,10 +19,12 @@
DetectionTechnology,
Organism,
Selection,
User,
)
import scimodom.database.queries as queries
from scimodom.services.annotation import AnnotationService
from scimodom.services.assembly import AssemblyService
from scimodom.services.permission import get_permission_service
import scimodom.utils.specifications as specs
import scimodom.utils.utils as utils

Expand All @@ -35,12 +38,10 @@ class DuplicateProjectError(Exception):


class ProjectService:
"""Utility class to create a project.
"""Utility class to create/manage a project.
:param session: SQLAlchemy ORM session
:type session: Session
:param project: Project description (json template)
:type project: dict
:param SMID_LENGTH: Length of Sci-ModoM ID (SMID)
:type SMID_LENGTH: int
:param DATA_PATH: Data path
Expand All @@ -54,14 +55,15 @@ class ProjectService:
DATA_SUB_PATH: ClassVar[str] = "metadata"
DATA_SUB_PATH_SUB: ClassVar[str] = "project_requests"

def __init__(self, session: Session, project: dict) -> None:
def __init__(self, session: Session) -> None:
"""Initializer method."""
self._session = session
self._project = project

self._project: dict
self._smid: str
self._assemblies: set[tuple[int, str]] = set()

def __new__(cls, session: Session, project: dict):
def __new__(cls, session: Session):
"""Constructor method."""
if cls.DATA_PATH is None:
msg = "Missing environment variable: DATA_PATH. Terminating!"
Expand Down Expand Up @@ -121,8 +123,16 @@ def create_project_request(project: dict):
json.dump(project, f, indent="\t")
return uuid

def create_project(self, wo_assembly: bool = False) -> None:
"""Project constructor."""
def create_project(self, project: dict, wo_assembly: bool = False) -> None:
"""Project constructor.
:param project: Project description (json template)
:type project: dict
:param wo_assembly: Skip assembly set up
:type wo_assembly: bool
"""
self._project = project

self._validate_keys()
self._validate_entry()
self._add_selection()
Expand All @@ -141,13 +151,39 @@ def create_project(self, wo_assembly: bool = False) -> None:
session=self._session, taxa_id=taxid
).create_annotation()

def associate_project_to_user(self, user: User, smid: str | None = None):
"""Associate a project to a user.
When called after project creation, the SMID is
available (default), else nothing is done, unless
it is passed as argument.
:param smid: SMID. There is no check
on the validity of this value, this must be done
before calling this function.
:type smid: str
"""
if not smid:
try:
smid = self._smid
except AttributeError:
msg = "Undefined SMID. Nothing will be done."
logger.warning(msg)
return
permission_service = get_permission_service()
permission_service.insert_into_user_project_association(user, smid)

def get_smid(self) -> str:
"""Return newly created SMID.
"""Return newly created SMID, else
raises a ValueError.
:returns: SMID
:rtype: str
"""
return self._smid
try:
return self._smid
except AttributeError:
msg = "Undefined SMID. This is only defined when creating a project."
raise AttributeError(msg)

def _validate_keys(self) -> None:
"""Validate keys from project description (dictionary)."""
Expand Down Expand Up @@ -353,3 +389,18 @@ def _write_metadata(self) -> None:
parent = Path(self.DATA_PATH, self.DATA_SUB_PATH)
with open(Path(parent, f"{self._smid}.json"), "w") as f:
json.dump(self._project, f, indent="\t")


_cached_project_service: Optional[ProjectService] = None


def get_project_service():
"""Helper function to set up a ProjectService object by injecting its dependencies.
:returns: Project service instance
:rtype: ProjectService
"""
global _cached_project_service
if _cached_project_service is None:
_cached_project_service = ProjectService(session=get_session())
return _cached_project_service
45 changes: 45 additions & 0 deletions server/tests/unit/services/test_permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import datetime, timezone

import pytest
from sqlalchemy import select

from scimodom.services.permission import PermissionService
from scimodom.database.models import (
User,
UserState,
Project,
ProjectContact,
UserProjectAssociation,
)


def test_insert_user_project_association(Session):
stamp = datetime.now(timezone.utc).replace(microsecond=0)
with Session() as session, session.begin():
contact = ProjectContact(
contact_name="contact_name",
contact_institution="contact_institution",
contact_email="contact@email",
)
user = User(email="contact@email", state=UserState.active, password_hash="xxx")
session.add_all([contact, user])
session.flush()
contact_id = contact.id
project = Project(
id="12345678",
title="title",
summary="summary",
contact_id=contact_id,
date_published=datetime.fromisoformat("2024-01-01"),
date_added=stamp,
)
session.add(project)
session.flush()
smid = project.id

service = PermissionService(Session())
service.insert_into_user_project_association(user, smid)

records = session.execute(select(UserProjectAssociation)).scalar()
assert records.user_id == user.id
assert records.project_id == smid
Loading

0 comments on commit f2625ed

Please sign in to comment.