Skip to content

Commit f2625ed

Browse files
committed
WIP project/permission
1 parent 20e2f59 commit f2625ed

File tree

6 files changed

+245
-30
lines changed

6 files changed

+245
-30
lines changed

server/src/scimodom/app.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
add_annotation,
2020
add_assembly,
2121
add_project,
22+
add_user_to_project,
2223
add_dataset,
2324
add_all,
2425
validate_dataset_title,
@@ -113,12 +114,34 @@ def annotation(id):
113114
"project", epilog="Check docs at https://dieterich-lab.github.io/scimodom/."
114115
)
115116
@click.argument("template", type=click.Path(exists=True))
116-
def project(template):
117+
@click.option(
118+
"--skip-add-user",
119+
is_flag=True,
120+
show_default=True,
121+
default=False,
122+
help="Do not add user to project.",
123+
)
124+
def project(template, skip_add_user):
117125
"""Add a new project to the database.
118126
119127
TEMPLATE is the path to a project template (json).
120128
"""
121-
add_project(template)
129+
add_user = not skip_add_user
130+
add_project(template, add_user=add_user)
131+
132+
@app.cli.command(
133+
"permission", epilog="Check docs at https://dieterich-lab.github.io/scimodom/."
134+
)
135+
@click.argument("username", type=click.STRING)
136+
@click.argument("smid", type=click.STRING)
137+
def permission(username, smid):
138+
"""Force add a user to a project.
139+
140+
\b
141+
USERNAME is the user email.
142+
SMID is the project ID to which this user is to be associated.
143+
"""
144+
add_user_to_project(username, smid)
122145

123146
@app.cli.command(
124147
"dataset", epilog="Check docs at https://dieterich-lab.github.io/scimodom/."

server/src/scimodom/plugins/cli.py

+72-9
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import traceback
66

77
import click
8+
from sqlalchemy import select
89

910
from scimodom.config import Config
1011
from scimodom.database.database import get_session
1112
from scimodom.database.models import (
13+
Project,
1214
Dataset,
1315
Modification,
1416
DetectionTechnology,
@@ -18,9 +20,10 @@
1820
import scimodom.database.queries as queries
1921
from scimodom.services.annotation import AnnotationService
2022
from scimodom.services.assembly import AssemblyService
21-
from scimodom.services.project import ProjectService
23+
from scimodom.services.project import get_project_service
2224
from scimodom.services.dataset import DataService
2325
from scimodom.services.setup import get_setup_service
26+
from scimodom.services.user import get_user_service, NoSuchUser
2427
import scimodom.utils.utils as utils
2528

2629

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

99102

100-
def add_project(project_template: str | Path) -> None:
103+
def add_project(project_template: str | Path, add_user: bool = True) -> None:
101104
"""Provides a CLI function to add a new project.
102105
103106
:param project_template: Path to a json file with
104107
require fields.
105108
:type project_template: str or Path
109+
:param add_user: Associate user and newly created project
110+
:type add_user: bool
106111
"""
107-
session = get_session()
108112
# load project metadata
109113
project = json.load(open(project_template))
110114
# add project
@@ -115,13 +119,72 @@ def add_project(project_template: str | Path) -> None:
115119
c = click.getchar()
116120
if c not in ["y", "Y"]:
117121
return
118-
service = ProjectService(session, project)
119-
service.create_project()
122+
project_service = get_project_service()
123+
project_service.create_project(project)
120124
click.secho(
121-
f"Successfully created. The SMID for this project is {service.get_smid()}.",
125+
f"Successfully created. The SMID for this project is {project_service.get_smid()}.",
122126
fg="green",
123127
)
124-
session.close()
128+
if add_user:
129+
username = project["contact_email"]
130+
click.secho(f"Adding user {username} to newly created project...", fg="green")
131+
click.secho("Continue [y/n]?", fg="green")
132+
c = click.getchar()
133+
if c not in ["y", "Y"]:
134+
return
135+
user_service = get_user_service()
136+
try:
137+
user = user_service.get_user_by_email(username)
138+
except NoSuchUser:
139+
click.secho(
140+
f"Unknown user {username}... Aborting!",
141+
fg="red",
142+
)
143+
else:
144+
project_service.associate_project_to_user(user)
145+
click.secho(
146+
"Successfully added user to project.",
147+
fg="green",
148+
)
149+
150+
151+
def add_user_to_project(username: str, smid: str) -> None:
152+
"""Provides a CLI function to force add a user to a project.
153+
154+
:param username: Username (email)
155+
:type username: str
156+
:param smid: SMID
157+
:type smid: str
158+
"""
159+
session = get_session()
160+
click.secho(f"Adding user {username} to {smid}...", fg="green")
161+
click.secho("Continue [y/n]?", fg="green")
162+
c = click.getchar()
163+
if c not in ["y", "Y"]:
164+
return
165+
project_service = get_project_service()
166+
user_service = get_user_service()
167+
try:
168+
user = user_service.get_user_by_email(username)
169+
except NoSuchUser:
170+
click.secho(
171+
f"Unknown user {username}... Aborting!",
172+
fg="red",
173+
)
174+
else:
175+
query = select(Project.id)
176+
smids = session.execute(query).scalars().all()
177+
if smid not in smids:
178+
click.secho(
179+
f"Unrecognised SMID {smid}... Aborting!",
180+
fg="red",
181+
)
182+
return
183+
project_service.associate_project_to_user(user, smid=smid)
184+
click.secho(
185+
"Successfully added user to project.",
186+
fg="green",
187+
)
125188

126189

127190
def add_dataset(
@@ -210,8 +273,8 @@ def add_all(directory: Path, templates: list[str]) -> None:
210273
project_title = project["title"]
211274
click.secho(f"Adding {project_title}...", fg="green")
212275
try:
213-
project_service = ProjectService(session, project)
214-
project_service.create_project()
276+
project_service = get_project_service()
277+
project_service.create_project(project)
215278
smid = project_service.get_smid()
216279
metadata = _get_dataset(project, extra_cols["file"])
217280
for filen, meta in metadata.items():

server/src/scimodom/services/permission.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
class PermissionService:
1111
def __init__(self, session: Session):
12-
self._db_session = session
12+
self._session = session
1313

1414
def may_change_dataset(self, user: User, dataset: Dataset) -> bool:
1515
query = select(UserProjectAssociation).where(
@@ -18,9 +18,23 @@ def may_change_dataset(self, user: User, dataset: Dataset) -> bool:
1818
UserProjectAssociation.project_id == dataset.project_id,
1919
)
2020
)
21-
results = self._db_session.execute(query).fetchall()
21+
results = self._session.execute(query).fetchall()
2222
return len(results) > 0
2323

24+
def insert_into_user_project_association(self, user: User, project_id: str) -> None:
25+
"""Insert values into table.
26+
27+
:param user: User
28+
:type user: User
29+
:param project_id: SMID. There is no check
30+
on the validity of this value, this must be done
31+
before calling this function.
32+
:type project_id: str
33+
"""
34+
permission = UserProjectAssociation(user_id=user.id, project_id=project_id)
35+
self._session.add(permission)
36+
self._session.commit()
37+
2438

2539
_cached_permission_service: Optional[PermissionService] = None
2640

server/src/scimodom/services/project.py

+62-11
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
import json
55
import logging
66
from pathlib import Path
7-
from typing import ClassVar
7+
from typing import ClassVar, Optional
88

99
from sqlalchemy.orm import Session
1010
from sqlalchemy import select, func
1111

1212
from scimodom.config import Config
13+
from scimodom.database.database import get_session
1314
from scimodom.database.models import (
1415
Project,
1516
ProjectSource,
@@ -18,10 +19,12 @@
1819
DetectionTechnology,
1920
Organism,
2021
Selection,
22+
User,
2123
)
2224
import scimodom.database.queries as queries
2325
from scimodom.services.annotation import AnnotationService
2426
from scimodom.services.assembly import AssemblyService
27+
from scimodom.services.permission import get_permission_service
2528
import scimodom.utils.specifications as specs
2629
import scimodom.utils.utils as utils
2730

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

3639

3740
class ProjectService:
38-
"""Utility class to create a project.
41+
"""Utility class to create/manage a project.
3942
4043
:param session: SQLAlchemy ORM session
4144
:type session: Session
42-
:param project: Project description (json template)
43-
:type project: dict
4445
:param SMID_LENGTH: Length of Sci-ModoM ID (SMID)
4546
:type SMID_LENGTH: int
4647
:param DATA_PATH: Data path
@@ -54,14 +55,15 @@ class ProjectService:
5455
DATA_SUB_PATH: ClassVar[str] = "metadata"
5556
DATA_SUB_PATH_SUB: ClassVar[str] = "project_requests"
5657

57-
def __init__(self, session: Session, project: dict) -> None:
58+
def __init__(self, session: Session) -> None:
5859
"""Initializer method."""
5960
self._session = session
60-
self._project = project
61+
62+
self._project: dict
6163
self._smid: str
6264
self._assemblies: set[tuple[int, str]] = set()
6365

64-
def __new__(cls, session: Session, project: dict):
66+
def __new__(cls, session: Session):
6567
"""Constructor method."""
6668
if cls.DATA_PATH is None:
6769
msg = "Missing environment variable: DATA_PATH. Terminating!"
@@ -121,8 +123,16 @@ def create_project_request(project: dict):
121123
json.dump(project, f, indent="\t")
122124
return uuid
123125

124-
def create_project(self, wo_assembly: bool = False) -> None:
125-
"""Project constructor."""
126+
def create_project(self, project: dict, wo_assembly: bool = False) -> None:
127+
"""Project constructor.
128+
129+
:param project: Project description (json template)
130+
:type project: dict
131+
:param wo_assembly: Skip assembly set up
132+
:type wo_assembly: bool
133+
"""
134+
self._project = project
135+
126136
self._validate_keys()
127137
self._validate_entry()
128138
self._add_selection()
@@ -141,13 +151,39 @@ def create_project(self, wo_assembly: bool = False) -> None:
141151
session=self._session, taxa_id=taxid
142152
).create_annotation()
143153

154+
def associate_project_to_user(self, user: User, smid: str | None = None):
155+
"""Associate a project to a user.
156+
When called after project creation, the SMID is
157+
available (default), else nothing is done, unless
158+
it is passed as argument.
159+
160+
:param smid: SMID. There is no check
161+
on the validity of this value, this must be done
162+
before calling this function.
163+
:type smid: str
164+
"""
165+
if not smid:
166+
try:
167+
smid = self._smid
168+
except AttributeError:
169+
msg = "Undefined SMID. Nothing will be done."
170+
logger.warning(msg)
171+
return
172+
permission_service = get_permission_service()
173+
permission_service.insert_into_user_project_association(user, smid)
174+
144175
def get_smid(self) -> str:
145-
"""Return newly created SMID.
176+
"""Return newly created SMID, else
177+
raises a ValueError.
146178
147179
:returns: SMID
148180
:rtype: str
149181
"""
150-
return self._smid
182+
try:
183+
return self._smid
184+
except AttributeError:
185+
msg = "Undefined SMID. This is only defined when creating a project."
186+
raise AttributeError(msg)
151187

152188
def _validate_keys(self) -> None:
153189
"""Validate keys from project description (dictionary)."""
@@ -353,3 +389,18 @@ def _write_metadata(self) -> None:
353389
parent = Path(self.DATA_PATH, self.DATA_SUB_PATH)
354390
with open(Path(parent, f"{self._smid}.json"), "w") as f:
355391
json.dump(self._project, f, indent="\t")
392+
393+
394+
_cached_project_service: Optional[ProjectService] = None
395+
396+
397+
def get_project_service():
398+
"""Helper function to set up a ProjectService object by injecting its dependencies.
399+
400+
:returns: Project service instance
401+
:rtype: ProjectService
402+
"""
403+
global _cached_project_service
404+
if _cached_project_service is None:
405+
_cached_project_service = ProjectService(session=get_session())
406+
return _cached_project_service
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from datetime import datetime, timezone
2+
3+
import pytest
4+
from sqlalchemy import select
5+
6+
from scimodom.services.permission import PermissionService
7+
from scimodom.database.models import (
8+
User,
9+
UserState,
10+
Project,
11+
ProjectContact,
12+
UserProjectAssociation,
13+
)
14+
15+
16+
def test_insert_user_project_association(Session):
17+
stamp = datetime.now(timezone.utc).replace(microsecond=0)
18+
with Session() as session, session.begin():
19+
contact = ProjectContact(
20+
contact_name="contact_name",
21+
contact_institution="contact_institution",
22+
contact_email="contact@email",
23+
)
24+
user = User(email="contact@email", state=UserState.active, password_hash="xxx")
25+
session.add_all([contact, user])
26+
session.flush()
27+
contact_id = contact.id
28+
project = Project(
29+
id="12345678",
30+
title="title",
31+
summary="summary",
32+
contact_id=contact_id,
33+
date_published=datetime.fromisoformat("2024-01-01"),
34+
date_added=stamp,
35+
)
36+
session.add(project)
37+
session.flush()
38+
smid = project.id
39+
40+
service = PermissionService(Session())
41+
service.insert_into_user_project_association(user, smid)
42+
43+
records = session.execute(select(UserProjectAssociation)).scalar()
44+
assert records.user_id == user.id
45+
assert records.project_id == smid

0 commit comments

Comments
 (0)