Skip to content

Commit e19f6c1

Browse files
authored
✨ allowing to log back into a study when the tab was closed (#2383)
* allow logging in again when same user closed all tabs pointing to a specific project * use a project locking mechanism to prevent opening when a project is closing * create constant for redis lock keys * constant for redis client for lock db * notification when closing * steal project when opening from a disconnected same user * disallow opening the same project from 2 different tabs * disallow opening 2 tabs on same project * removed coveralls * fixed codecov badge * ensure redis database is cleaned between tests * added migration validator * GC disabled in test_resource_manager using manual calls
1 parent b452c71 commit e19f6c1

39 files changed

+1107
-360
lines changed

Makefile

+16-3
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,24 @@ endif
189189

190190
define _show_endpoints
191191
# The following endpoints are available
192-
echo "http://$(if $(IS_WSL2),$(get_my_ip),127.0.0.1):9081 - oSparc platform"
193-
echo "http://$(if $(IS_WSL2),$(get_my_ip),127.0.0.1):18080/?pgsql=postgres&username=scu&db=simcoredb&ns=public - Postgres DB"
194-
echo "http://$(if $(IS_WSL2),$(get_my_ip),127.0.0.1):9000 - Portainer"
192+
set -o allexport; \
193+
source $(CURDIR)/.env; \
194+
set +o allexport; \
195+
separator=------------------------------------------;\
196+
separator=$${separator}$${separator}$${separator};\
197+
rows="%-80s| %22s| %12s| %12s\n";\
198+
TableWidth=140;\
199+
printf "%80s| %22s| %12s| %12s\n" Endpoint Name User Password;\
200+
printf "%.$${TableWidth}s\n" "$$separator";\
201+
printf "$$rows" 'http://$(if $(IS_WSL2),$(get_my_ip),127.0.0.1):9081' 'oSparc platform';\
202+
printf "$$rows" 'http://$(if $(IS_WSL2),$(get_my_ip),127.0.0.1):18080/?pgsql=postgres&username=$${POSTGRES_USER}&db=$${POSTGRES_DB}&ns=public' 'Postgres DB' $${POSTGRES_USER} $${POSTGRES_PASSWORD};\
203+
printf "$$rows" 'http://$(if $(IS_WSL2),$(get_my_ip),127.0.0.1):9000' Portainer admin adminadmin;\
204+
printf "$$rows" 'http://$(if $(IS_WSL2),$(get_my_ip),127.0.0.1):18081' Redis
195205
endef
196206

207+
show-endpoints:
208+
@$(_show_endpoints)
209+
197210
up-devel: .stack-simcore-development.yml .init-swarm $(CLIENT_WEB_OUTPUT) ## Deploys local development stack, qx-compile+watch and ops stack (pass 'make ops_disabled=1 up-...' to disable)
198211
# Start compile+watch front-end container [front-end]
199212
@$(MAKE_C) services/web/client down compile-dev flags=--watch

README.md

+1-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
[![Requires.io]](https://requires.io/github/ITISFoundation/osparc-simcore/requirements/?branch=master "State of third party python dependencies")
1111
<!-- [![travis-ci]](https://travis-ci.org/ITISFoundation/osparc-simcore "State of CI: build, test and pushing images") Commented until #2029 is resolved-->
1212
[![Github-CI Push/PR]](https://github.com/ITISFoundation/osparc-simcore/actions?query=workflow%3A%22Github-CI+Push%2FPR%22+branch%3Amaster)
13-
[![coveralls.io]](https://coveralls.io/github/ITISFoundation/osparc-simcore?branch=master)
14-
[![codecov.io]](https://codecov.io/gh/ITISFoundation/osparc-simcore)
13+
[![codecov](https://codecov.io/gh/ITISFoundation/osparc-simcore/branch/master/graph/badge.svg?token=h1rOE8q7ic)](https://codecov.io/gh/ITISFoundation/osparc-simcore)
1514
[![github.io]](https://itisfoundation.github.io/)
1615
[![itis.dockerhub]](https://hub.docker.com/u/itisfoundation)
1716
[![license]](./LICENSE)
@@ -23,8 +22,6 @@
2322
[travis-ci]:https://travis-ci.org/ITISFoundation/osparc-simcore.svg?branch=master
2423
[github.io]:https://img.shields.io/website-up-down-green-red/https/itisfoundation.github.io.svg?label=documentation
2524
[itis.dockerhub]:https://img.shields.io/website/https/hub.docker.com/u/itisfoundation.svg?down_color=red&label=dockerhub%20repos&up_color=green
26-
[coveralls.io]:https://coveralls.io/repos/github/ITISFoundation/osparc-simcore/badge.svg?branch=master
27-
[codecov.io]:https://codecov.io/gh/ITISFoundation/osparc-simcore/branch/master/graph/badge.svg
2825
[license]:https://img.shields.io/github/license/ITISFoundation/osparc-simcore
2926
[Github-CI Push/PR]:https://github.com/ITISFoundation/osparc-simcore/workflows/Github-CI%20Push/PR/badge.svg
3027
<!------------------------------------------------------>

api/specs/common/schemas/node-meta-v0.0.1-converted.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ properties:
1616
key:
1717
type: string
1818
description: distinctive name for the node based on the docker registry path
19-
pattern: ^(simcore)/(services)/(comp|dynamic)(/[\w/-]+)+$
19+
pattern: ^(simcore)/(services)/(comp|dynamic|frontend)(/[\w/-]+)+$
2020
example: simcore/services/comp/itis/sleeper
2121
integration-version:
2222
type: string

api/specs/common/schemas/node-meta-v0.0.1.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"key": {
2020
"type": "string",
2121
"description": "distinctive name for the node based on the docker registry path",
22-
"pattern": "^(simcore)/(services)/(comp|dynamic)(/[^\\s/]+)+$",
22+
"pattern": "^(simcore)/(services)/(comp|dynamic|frontend)(/[\\w/-]+)+$",
2323
"examples": [
2424
"simcore/services/comp/itis/sleeper",
2525
"simcore/services/dynamic/3dviewer"

api/specs/common/schemas/project-v0.0.1-converted.yaml

+14-2
Original file line numberDiff line numberDiff line change
@@ -405,11 +405,11 @@ properties:
405405
properties:
406406
value:
407407
title: Value
408-
description: True if the project is locked by another user
408+
description: True if the project is locked
409409
type: boolean
410410
owner:
411411
title: Owner
412-
description: The user that owns the lock
412+
description: If locked, the user that owns the lock
413413
allOf:
414414
- title: Owner
415415
type: object
@@ -439,8 +439,20 @@ properties:
439439
- user_id
440440
- first_name
441441
- last_name
442+
status:
443+
title: Status
444+
description: The status of the project
445+
enum:
446+
- CLOSED
447+
- CLOSING
448+
- CLONING
449+
- OPENING
450+
- EXPORTING
451+
- OPENED
452+
type: string
442453
required:
443454
- value
455+
- status
444456
state:
445457
title: State
446458
description: The project running state

api/specs/common/schemas/project-v0.0.1.json

+18-4
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
"key": {
117117
"type": "string",
118118
"description": "distinctive name for the node based on the docker registry path",
119-
"pattern": "^(simcore)/(services)/(comp|dynamic|frontend)(/[^\\s/]+)+$",
119+
"pattern": "^(simcore)/(services)/(comp|dynamic|frontend)(/[\\w/-]+)+$",
120120
"examples": [
121121
"simcore/services/comp/sleeper",
122122
"simcore/services/dynamic/3dviewer",
@@ -556,12 +556,12 @@
556556
"properties": {
557557
"value": {
558558
"title": "Value",
559-
"description": "True if the project is locked by another user",
559+
"description": "True if the project is locked",
560560
"type": "boolean"
561561
},
562562
"owner": {
563563
"title": "Owner",
564-
"description": "The user that owns the lock",
564+
"description": "If locked, the user that owns the lock",
565565
"allOf": [
566566
{
567567
"title": "Owner",
@@ -600,10 +600,24 @@
600600
]
601601
}
602602
]
603+
},
604+
"status": {
605+
"title": "Status",
606+
"description": "The status of the project",
607+
"enum": [
608+
"CLOSED",
609+
"CLOSING",
610+
"CLONING",
611+
"OPENING",
612+
"EXPORTING",
613+
"OPENED"
614+
],
615+
"type": "string"
603616
}
604617
},
605618
"required": [
606-
"value"
619+
"value",
620+
"status"
607621
]
608622
}
609623
]

packages/models-library/src/models_library/projects_state.py

+50-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from enum import Enum, unique
66
from typing import Optional
77

8-
from pydantic import BaseModel, Extra, Field
8+
from pydantic import BaseModel, Extra, Field, validator
99

1010
from .projects_access import Owner
1111

@@ -29,14 +29,60 @@ class DataState(str, Enum):
2929
OUTDATED = "OUTDATED"
3030

3131

32+
@unique
33+
class ProjectStatus(str, Enum):
34+
CLOSED = "CLOSED"
35+
CLOSING = "CLOSING"
36+
CLONING = "CLONING"
37+
EXPORTING = "EXPORTING"
38+
OPENING = "OPENING"
39+
OPENED = "OPENED"
40+
41+
3242
class ProjectLocked(BaseModel):
33-
value: bool = Field(
34-
..., description="True if the project is locked by another user"
43+
value: bool = Field(..., description="True if the project is locked")
44+
owner: Optional[Owner] = Field(
45+
None, description="If locked, the user that owns the lock"
3546
)
36-
owner: Optional[Owner] = Field(None, description="The user that owns the lock")
47+
status: ProjectStatus = Field(..., description="The status of the project")
3748

3849
class Config:
3950
extra = Extra.forbid
51+
use_enum_values = True
52+
schema_extra = {
53+
"examples": [
54+
{"value": False, "status": ProjectStatus.CLOSED},
55+
{
56+
"value": True,
57+
"status": ProjectStatus.OPENED,
58+
"owner": {
59+
"user_id": 123,
60+
"first_name": "Johnny",
61+
"last_name": "Cash",
62+
},
63+
},
64+
]
65+
}
66+
67+
@validator("owner", pre=True, always=True)
68+
@classmethod
69+
def check_not_null(v, values):
70+
if values["value"] is True and v is None:
71+
raise ValueError("value cannot be None when project is locked")
72+
return v
73+
74+
@validator("status", always=True)
75+
@classmethod
76+
def check_status_compatible(v, values):
77+
if values["value"] is False and v not in ["CLOSED", "OPENED"]:
78+
raise ValueError(
79+
f"status is set to {v} and lock is set to {values['value']}!"
80+
)
81+
if values["value"] is True and v == "CLOSED":
82+
raise ValueError(
83+
f"status is set to {v} and lock is set to {values['value']}!"
84+
)
85+
return v
4086

4187

4288
class ProjectRunningState(BaseModel):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from pprint import pformat
2+
3+
import pytest
4+
from models_library.projects_state import ProjectLocked, ProjectStatus
5+
6+
7+
@pytest.mark.parametrize(
8+
"model_cls",
9+
(ProjectLocked,),
10+
)
11+
def test_projects_state_model_examples(model_cls, model_cls_examples):
12+
for name, example in model_cls_examples.items():
13+
print(name, ":", pformat(example))
14+
model_instance = model_cls(**example)
15+
assert model_instance, f"Failed with {name}"
16+
17+
18+
def test_project_locked_with_missing_owner_raises():
19+
with pytest.raises(ValueError):
20+
ProjectLocked(**{"value": True, "status": ProjectStatus.OPENED})
21+
ProjectLocked.parse_obj({"value": False, "status": ProjectStatus.OPENED})
22+
23+
24+
@pytest.mark.parametrize(
25+
"lock, status",
26+
[
27+
(False, x)
28+
for x in ProjectStatus
29+
if x not in [ProjectStatus.CLOSED, ProjectStatus.OPENED]
30+
]
31+
+ [(True, ProjectStatus.CLOSED)],
32+
)
33+
def test_project_locked_with_allowed_values(lock: bool, status: ProjectStatus):
34+
with pytest.raises(ValueError):
35+
ProjectLocked.parse_obj({"value": lock, "status": status})

packages/models-library/tests/test_services.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
import re
66
from pprint import pformat
7-
from typing import Any, Dict
7+
from typing import Any, Callable, Dict, List
88

99
import pytest
1010
import yaml
11+
from models_library.basic_regex import VERSION_RE
1112
from models_library.services import (
1213
SERVICE_KEY_RE,
1314
ServiceAccessRightsAtDB,
@@ -103,7 +104,6 @@ def test_service_models_examples(model_cls, model_cls_examples):
103104
assert model_instance, f"Failed with {name}"
104105

105106

106-
@pytest.mark.skip(reason="dev")
107107
@pytest.mark.parametrize(
108108
"service_key",
109109
[
@@ -159,7 +159,7 @@ def test_service_models_examples(model_cls, model_cls_examples):
159159
[SERVICE_KEY_RE, r"^(simcore)/(services)/(comp|dynamic|frontend)(/[^\s/]+)+$"],
160160
ids=["pattern_with_w", "pattern_with_s"],
161161
)
162-
def test_service_key_regex_patterns(service_key, regex_pattern):
162+
def test_service_key_regex_patterns(service_key: str, regex_pattern: str):
163163
match = re.match(regex_pattern, service_key)
164164
assert match
165165

@@ -178,3 +178,36 @@ def test_services_model_examples(model_cls, model_cls_examples):
178178
print(name, ":", pformat(example))
179179
model_instance = model_cls(**example)
180180
assert model_instance, f"Failed with {name}"
181+
182+
183+
@pytest.mark.parametrize(
184+
"python_regex_pattern, json_schema_file_name, json_schema_entry_paths",
185+
[
186+
(SERVICE_KEY_RE, "project-v0.0.1.json", ["key"]),
187+
(VERSION_RE, "project-v0.0.1.json", ["version"]),
188+
(VERSION_RE, "node-meta-v0.0.1.json", ["version"]),
189+
(SERVICE_KEY_RE, "node-meta-v0.0.1.json", ["key"]),
190+
],
191+
)
192+
def test_regex_pattern_same_in_jsonschema_and_python(
193+
python_regex_pattern: str,
194+
json_schema_file_name: str,
195+
json_schema_entry_paths: List[str],
196+
json_schema_dict: Callable,
197+
):
198+
# read file in
199+
json_schema_config = json_schema_dict(json_schema_file_name)
200+
# go to keys
201+
def _find_pattern_entry(obj: Dict[str, Any], key: str) -> Any:
202+
if key in obj:
203+
return obj[key]["pattern"]
204+
for v in obj.values():
205+
if isinstance(v, dict):
206+
item = _find_pattern_entry(v, key)
207+
if item is not None:
208+
return item
209+
return None
210+
211+
for x_path in json_schema_entry_paths:
212+
json_pattern = _find_pattern_entry(json_schema_config, x_path)
213+
assert json_pattern == python_regex_pattern

packages/pytest-simcore/src/pytest_simcore/redis_service.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ async def redis_service(redis_config: RedisConfig, monkeypatch) -> RedisConfig:
4949
return redis_config
5050

5151

52-
@pytest.fixture(scope="module")
52+
@pytest.fixture(scope="function")
5353
async def redis_client(loop, redis_config: RedisConfig) -> Iterator[aioredis.Redis]:
5454
client = await aioredis.create_redis_pool(redis_config.dsn, encoding="utf-8")
5555

services/api-server/tests/unit/test_utils_solver_job_models_converters.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,13 @@ def test_create_job_from_project():
173173
},
174174
"quality": {},
175175
"tags": [],
176-
"state": {"locked": {"value": False}, "state": {"value": "SUCCESS"}},
176+
"state": {
177+
"locked": {
178+
"value": False,
179+
"status": "CLOSED",
180+
},
181+
"state": {"value": "SUCCESS"},
182+
},
177183
},
178184
)
179185

services/director/src/simcore_service_director/api/v0/openapi.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ paths:
159159
key:
160160
type: string
161161
description: distinctive name for the node based on the docker registry path
162-
pattern: '^(simcore)/(services)/(comp|dynamic)(/[\w/-]+)+$'
162+
pattern: '^(simcore)/(services)/(comp|dynamic|frontend)(/[\w/-]+)+$'
163163
example: simcore/services/comp/itis/sleeper
164164
integration-version:
165165
type: string
@@ -552,7 +552,7 @@ paths:
552552
key:
553553
type: string
554554
description: distinctive name for the node based on the docker registry path
555-
pattern: '^(simcore)/(services)/(comp|dynamic)(/[\w/-]+)+$'
555+
pattern: '^(simcore)/(services)/(comp|dynamic|frontend)(/[\w/-]+)+$'
556556
example: simcore/services/comp/itis/sleeper
557557
integration-version:
558558
type: string
@@ -2199,7 +2199,7 @@ components:
21992199
key:
22002200
type: string
22012201
description: distinctive name for the node based on the docker registry path
2202-
pattern: '^(simcore)/(services)/(comp|dynamic)(/[\w/-]+)+$'
2202+
pattern: '^(simcore)/(services)/(comp|dynamic|frontend)(/[\w/-]+)+$'
22032203
example: simcore/services/comp/itis/sleeper
22042204
integration-version:
22052205
type: string

services/director/src/simcore_service_director/api/v0/schemas/node-meta-v0.0.1.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"key": {
2020
"type": "string",
2121
"description": "distinctive name for the node based on the docker registry path",
22-
"pattern": "^(simcore)/(services)/(comp|dynamic)(/[^\\s/]+)+$",
22+
"pattern": "^(simcore)/(services)/(comp|dynamic|frontend)(/[\\w/-]+)+$",
2323
"examples": [
2424
"simcore/services/comp/itis/sleeper",
2525
"simcore/services/dynamic/3dviewer"

0 commit comments

Comments
 (0)