Skip to content

Commit 7ecdced

Browse files
authored
Fix/get templates (#870) (#871)
- Fixes issue introduced with #866: template studies did not open. GET project/{project_uuid} was only returning template projects - sets all as default in GET projects/ - Fixed fake_db fixture and doc Cherry-picked squashed PR #870 to master
1 parent 345802a commit 7ecdced

File tree

9 files changed

+115
-55
lines changed

9 files changed

+115
-55
lines changed

api/specs/webserver/v0/openapi-projects.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ paths:
1616
in: query
1717
schema:
1818
type: string
19+
default: 'all'
1920
enum: [template, user, all]
2021
description: if true only templates otherwise only users
2122
- name: start

services/web/server/src/simcore_service_webserver/projects/projects_api.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,27 @@ def validate_project(app: web.Application, project: Dict):
4040
validate_instance(project, project_schema) # TODO: handl
4141

4242

43-
async def get_project_for_user(request: web.Request, project_uuid, user_id) -> Dict:
43+
async def get_project_for_user(request: web.Request, project_uuid, user_id, *, include_templates=False) -> Dict:
44+
""" Returns a project accessible to user
45+
46+
:raises web.HTTPNotFound: if no match found
47+
:return: schema-compliant project data
48+
:rtype: Dict
49+
"""
4450
await check_permission(request, "project.read")
4551

4652
try:
4753
db = request.config_dict[APP_PROJECT_DBAPI]
48-
project = await db.get_user_project(user_id, project_uuid)
54+
55+
project = None
56+
if include_templates:
57+
project = await db.get_template_project(project_uuid)
58+
59+
if not project:
60+
project = await db.get_user_project(user_id, project_uuid)
4961

5062
# TODO: how to handle when database has an invalid project schema???
63+
# Notice that db model does not include a check on project schema.
5164
validate_project(request.app, project)
5265
return project
5366

services/web/server/src/simcore_service_webserver/projects/projects_db.py

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22
33
- Adds a layer to the postgres API with a focus on the projects data
44
- Shall be used as entry point for all the queries to the database regarding projects
5-
"""
65
7-
#TODO: move here class ProjectDB:
8-
# TODO: should implement similar model as services/web/server/src/simcore_service_webserver/login/storage.py
6+
"""
97

108
import logging
119
import uuid as uuidlib
@@ -26,8 +24,8 @@
2624
from ..utils import format_datetime, now_str
2725
from .projects_exceptions import (ProjectInvalidRightsError,
2826
ProjectNotFoundError)
29-
from .projects_models import ProjectType, projects, user_to_projects
3027
from .projects_fakes import Fake
28+
from .projects_models import ProjectType, projects, user_to_projects
3129

3230
log = logging.getLogger(__name__)
3331

@@ -41,7 +39,6 @@ def _convert_to_db_names(project_data: Dict) -> Dict:
4139
converted_args[ChangeCase.camel_to_snake(key)] = value
4240
return converted_args
4341

44-
4542
def _convert_to_schema_names(project_db_data: Mapping) -> Dict:
4643
converted_args = {}
4744
for key, value in project_db_data.items():
@@ -54,7 +51,6 @@ def _convert_to_schema_names(project_db_data: Mapping) -> Dict:
5451
return converted_args
5552

5653

57-
5854
# TODO: test all function return schema-compatible data
5955
# TODO: is user_id str or int?
6056
# TODO: systemaic user_id, project
@@ -190,6 +186,7 @@ async def load_user_projects(self, user_id: str) -> List[Dict]:
190186

191187
async def load_template_projects(self) -> List[Dict]:
192188
log.info("Loading template projects")
189+
# TODO: eliminate this and use mock to replace get_user_project instead
193190
projects_list = [prj.data for prj in Fake.projects.values() if prj.template]
194191

195192
async with self.engine.acquire() as conn:
@@ -202,46 +199,54 @@ async def load_template_projects(self) -> List[Dict]:
202199
projects_list.append(_convert_to_schema_names(result_dict))
203200
return projects_list
204201

205-
async def get_template_project(self, project_uuid: str) -> Dict:
206-
prj = Fake.projects.get(project_uuid)
207-
if prj and prj.template:
208-
return prj.data
209-
210-
template_prj = None
211-
async with self.engine.acquire() as conn:
212-
query = select([projects]).where(
213-
and_(projects.c.type == ProjectType.TEMPLATE, projects.c.uuid == project_uuid)
214-
)
215-
result = await conn.execute(query)
216-
row = await result.first()
217-
template_prj = _convert_to_schema_names(row) if row else None
218-
219-
return template_prj
220-
221202
async def get_user_project(self, user_id: str, project_uuid: str) -> Dict:
222-
"""
203+
""" Returns all projects *owned* by the user
204+
205+
- A project is owned with it is mapped in user_to_projects list
206+
- prj_owner field is not
207+
- Notice that a user can have access to a template but he might not onw it
223208
224209
:raises ProjectNotFoundError: project is not assigned to user
225210
:return: schema-compliant project
226211
:rtype: Dict
227212
"""
213+
# TODO: eliminate this and use mock to replace get_user_project instead
228214
prj = Fake.projects.get(project_uuid)
229215
if prj and not prj.template:
230216
return Fake.projects[project_uuid].data
231217

232-
log.info("Getting project %s for user %s", project_uuid, user_id)
233218
async with self.engine.acquire() as conn:
234219
joint_table = user_to_projects.join(projects)
235-
query = select([projects]).\
236-
select_from(joint_table).\
237-
where(and_(projects.c.uuid == project_uuid, user_to_projects.c.user_id == user_id))
220+
query = select([projects]).select_from(joint_table).where(
221+
and_(projects.c.uuid == project_uuid,
222+
user_to_projects.c.user_id == user_id)
223+
)
238224
result = await conn.execute(query)
239225
row = await result.first()
226+
227+
# FIXME: prefer None to raise an exception. Read https://stackoverflow.com/questions/1313812/raise-exception-vs-return-none-in-functions?answertab=votes#tab-top
240228
if not row:
241229
raise ProjectNotFoundError(project_uuid)
242-
result_dict = {key:value for key,value in row.items()}
243-
log.debug("found project: %s", result_dict)
244-
return _convert_to_schema_names(result_dict)
230+
return _convert_to_schema_names(row)
231+
232+
async def get_template_project(self, project_uuid: str) -> Dict:
233+
# TODO: eliminate this and use mock to replace get_user_project instead
234+
prj = Fake.projects.get(project_uuid)
235+
if prj and prj.template:
236+
return prj.data
237+
238+
template_prj = None
239+
async with self.engine.acquire() as conn:
240+
query = select([projects]).where(
241+
and_(projects.c.type == ProjectType.TEMPLATE,
242+
projects.c.uuid == project_uuid)
243+
)
244+
result = await conn.execute(query)
245+
row = await result.first()
246+
if row:
247+
template_prj = _convert_to_schema_names(row)
248+
249+
return template_prj
245250

246251
async def update_user_project(self, project_data: Dict, user_id: str, project_uuid: str):
247252
""" updates a project from a user
@@ -317,7 +322,6 @@ async def delete_user_project(self, user_id: int, project_uuid: str):
317322
where(projects.c.id == row[projects.c.id])
318323
await conn.execute(query)
319324

320-
321325
async def make_unique_project_uuid(self) -> str:
322326
""" Generates a project identifier still not used in database
323327
@@ -340,7 +344,6 @@ async def make_unique_project_uuid(self) -> str:
340344
break
341345
return project_uuid
342346

343-
344347
async def _get_user_email(self, user_id):
345348
async with self.engine.acquire() as conn:
346349
result = await conn.execute(

services/web/server/src/simcore_service_webserver/projects/projects_handlers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def list_projects(request: web.Request):
7171
# TODO: implement all query parameters as
7272
# in https://www.ibm.com/support/knowledgecenter/en/SSCRJU_3.2.0/com.ibm.swg.im.infosphere.streams.rest.api.doc/doc/restapis-queryparms-list.html
7373
user_id = request[RQT_USERID_KEY]
74-
ptype = request.query.get('type', 'user')
74+
ptype = request.query.get('type', 'all') # TODO: get default for oaspecs
7575
db = request.config_dict[APP_PROJECT_DBAPI]
7676

7777
projects_list = []
@@ -103,12 +103,18 @@ async def list_projects(request: web.Request):
103103

104104
@login_required
105105
async def get_project(request: web.Request):
106+
""" Returns all projects accessible to a user (not necesarly owned)
107+
108+
"""
106109
# TODO: temporary hidden until get_handlers_from_namespace refactor to seek marked functions instead!
107110
from .projects_api import get_project_for_user
108111

112+
project_uuid = request.match_info.get("project_id")
113+
109114
project = await get_project_for_user(request,
110-
project_uuid=request.match_info.get("project_id"),
111-
user_id=request[RQT_USERID_KEY]
115+
project_uuid=project_uuid,
116+
user_id=request[RQT_USERID_KEY],
117+
include_templates=True
112118
)
113119

114120
return {

services/web/server/src/simcore_service_webserver/studies_access.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,12 @@ async def access_study(request: web.Request) -> web.Response:
156156

157157
# FIXME: if identified user, then he can access not only to template but also his own projects!
158158
if study_id not in SHARABLE_TEMPLATE_STUDY_IDS:
159-
raise web.HTTPNotFound(reason="Requested study is not shared ['%s']" % study_id)
159+
raise web.HTTPNotFound(reason="This study was not shared [{}]".format(study_id))
160160

161161
# TODO: should copy **any** type of project is sharable -> get_sharable_project
162162
template_project = await get_template_project(request.app, study_id)
163163
if not template_project:
164-
raise RuntimeError("Unable to load study %s" % study_id)
164+
raise web.HTTPNotFound(reason="Invalid study [{}]".format(study_id))
165165

166166
user = None
167167
is_anonymous_user = await is_anonymous(request)
@@ -174,12 +174,12 @@ async def access_study(request: web.Request) -> web.Response:
174174
if not user:
175175
raise RuntimeError("Unable to start user session")
176176

177+
msg_tail = "study {} to {} account ...".format(template_project.get('name'), user.get("email"))
178+
log.debug("Copying %s ...", msg_tail)
177179

178-
log.debug("Copying study %s to %s account ...", template_project['name'], user["email"])
179180
copied_project_id = await copy_study_to_account(request, template_project, user)
180181

181-
log.debug("Coped study %s to %s account as %s",
182-
template_project['name'], user["email"], copied_project_id)
182+
log.debug("Copied %s as %s", msg_tail, copied_project_id)
183183

184184

185185
try:

services/web/server/tests/data/fake-project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"uuid": "template-uuid-6257-a462-d7bf73b76c0c",
3-
"name": "ex",
3+
"name": "fake-project-name",
44
"description": "anim sint pariatur do dolore",
55
"prjOwner": "dolore ad do consectetur",
66
"creationDate": "1865-11-30T04:00:14.000Z",

services/web/server/tests/unit/test_template_projects.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ async def project_specs(loop, project_schema_file: Path) -> Dict:
3333
def fake_db():
3434
Fake.reset()
3535
Fake.load_template_projects()
36+
yield Fake
37+
Fake.reset()
3638

3739
async def test_validate_templates(loop, project_specs: Dict, fake_db):
38-
for pid, project in Fake.projects.items():
40+
for pid, project in fake_db.projects.items():
3941
try:
4042
validate_instance(project.data, project_specs)
4143
except ValidationError:
4244
pytest.fail("validation of project {} failed".format(pid))
43-

services/web/server/tests/unit/with_postgres/test_access_to_studies.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ async def logged_user(client): #, role: UserRole):
111111

112112
async def _get_user_projects(client):
113113
url = client.app.router["list_projects"].url_for()
114-
resp = await client.get(url.with_query(start=0, count=3))
114+
resp = await client.get(url.with_query(start=0, count=3, type="user"))
115115
payload = await resp.json()
116116
assert resp.status == 200, payload
117117

@@ -157,7 +157,6 @@ async def test_access_study_anonymously(client, qx_client_outdir):
157157
}
158158

159159
async with NewProject(params, client.app, force_uuid=True) as template_project:
160-
161160
url_path = "/study/%s" % SHARED_STUDY_UUID
162161
resp = await client.get(url_path)
163162
content = await resp.text()

services/web/server/tests/unit/with_postgres/test_projects.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def client(loop, aiohttp_client, aiohttp_unused_port, app_cfg, postgres_service)
6363

6464
# teardown here ...
6565

66-
@pytest.fixture
66+
@pytest.fixture()
6767
async def logged_user(client, user_role: UserRole):
6868
""" adds a user in db and logs in with client
6969
@@ -74,7 +74,9 @@ async def logged_user(client, user_role: UserRole):
7474
{"role": user_role.name},
7575
check_if_succeeds = user_role!=UserRole.ANONYMOUS
7676
) as user:
77+
print("-----> logged in user", user_role)
7778
yield user
79+
print("<----- logged out user", user_role)
7880

7981
@pytest.fixture
8082
async def user_project(client, fake_project, logged_user):
@@ -83,17 +85,26 @@ async def user_project(client, fake_project, logged_user):
8385
client.app,
8486
user_id=logged_user["id"]
8587
) as project:
88+
print("-----> added project", project["name"])
8689
yield project
90+
print("<----- removed project", project["name"])
8791

8892

8993
@pytest.fixture
9094
async def template_project(client, fake_project):
95+
project_data = deepcopy(fake_project)
96+
project_data["name"] = "Fake template"
97+
project_data["uuid"] = "d4d0eca3-d210-4db6-84f9-63670b07176b"
98+
9199
async with NewProject(
92-
fake_project,
100+
project_data,
93101
client.app,
94-
user_id=None
95-
) as template_prj:
96-
yield template_prj
102+
user_id=None,
103+
clear_all=True
104+
) as template_project:
105+
print("-----> added template project", template_project["name"])
106+
yield template_project
107+
print("<----- removed template project", template_project["name"])
97108

98109
def assert_replaced(current_project, update_data):
99110
def _extract(dikt, keys):
@@ -115,20 +126,35 @@ def _extract(dikt, keys):
115126
(UserRole.USER, web.HTTPOk),
116127
(UserRole.TESTER, web.HTTPOk),
117128
])
118-
async def test_list_projects(client, logged_user, user_project, expected):
129+
async def test_list_projects(client, logged_user, user_project, template_project, expected):
119130
# GET /v0/projects
120131
url = client.app.router["list_projects"].url_for()
121132
assert str(url) == API_PREFIX + "/projects"
122133

123134
resp = await client.get(url)
124135
data, errors = await assert_status(resp, expected)
125136

126-
#TODO: GET /v0/projects?type=user
137+
if not errors:
138+
assert len(data) == 2
139+
assert data[0] == template_project
140+
assert data[1] == user_project
127141

142+
#GET /v0/projects?type=user
143+
resp = await client.get(url.with_query(type='user'))
144+
data, errors = await assert_status(resp, expected)
128145
if not errors:
129146
assert len(data) == 1
130147
assert data[0] == user_project
131148

149+
#GET /v0/projects?type=template
150+
resp = await client.get(url.with_query(type='template'))
151+
data, errors = await assert_status(resp, expected)
152+
if not errors:
153+
assert len(data) == 1
154+
assert data[0] == template_project
155+
156+
157+
132158
@pytest.mark.skip("TODO")
133159
async def test_list_templates_only(client, logged_user, user_project, expected):
134160
#TODO: GET /v0/projects?type=template
@@ -141,8 +167,10 @@ async def test_list_templates_only(client, logged_user, user_project, expected):
141167
(UserRole.USER, web.HTTPOk),
142168
(UserRole.TESTER, web.HTTPOk),
143169
])
144-
async def test_get_project(client, logged_user, user_project, expected):
170+
async def test_get_project(client, logged_user, user_project, template_project, expected):
145171
# GET /v0/projects/{project_id}
172+
173+
# with a project owned by user
146174
url = client.app.router["get_project"].url_for(project_id=user_project["uuid"])
147175

148176
resp = await client.get(url)
@@ -151,6 +179,15 @@ async def test_get_project(client, logged_user, user_project, expected):
151179
if not error:
152180
assert data == user_project
153181

182+
# with a template
183+
url = client.app.router["get_project"].url_for(project_id=template_project["uuid"])
184+
185+
resp = await client.get(url)
186+
data, error = await assert_status(resp, expected)
187+
188+
if not error:
189+
assert data == template_project
190+
154191

155192
# POST --------
156193
@pytest.mark.parametrize("user_role,expected", [

0 commit comments

Comments
 (0)