Skip to content

Commit 22c808c

Browse files
authored
Merge branch 'master' into mai/follow-up-error-handling
2 parents f2eeccb + 904b669 commit 22c808c

File tree

11 files changed

+210
-37
lines changed

11 files changed

+210
-37
lines changed

services/invitations/src/simcore_service_invitations/cli.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import getpass
22
import logging
3+
import os
34

45
import typer
56
from cryptography.fernet import Fernet
@@ -14,7 +15,6 @@
1415
print_as_envfile,
1516
)
1617

17-
from . import web_server
1818
from ._meta import PROJECT_NAME, __version__
1919
from .core.settings import ApplicationSettings, MinimalApplicationSettings
2020
from .services.invitations import (
@@ -50,7 +50,7 @@ def generate_key(
5050
export INVITATIONS_SECRET_KEY=$(invitations-maker generate-key)
5151
"""
5252
assert ctx # nosec
53-
print(Fernet.generate_key().decode()) # noqa: T201
53+
typer.echo(Fernet.generate_key().decode())
5454

5555

5656
@main.command()
@@ -106,6 +106,10 @@ def invite(
106106
None,
107107
help=InvitationInputs.model_fields["trial_account_days"].description,
108108
),
109+
extra_credits_in_usd: int = typer.Option(
110+
None,
111+
help=InvitationInputs.model_fields["extra_credits_in_usd"].description,
112+
),
109113
product: str = typer.Option(
110114
None,
111115
help=InvitationInputs.model_fields["product"].description,
@@ -119,7 +123,7 @@ def invite(
119123
issuer=issuer,
120124
guest=TypeAdapter(EmailStr).validate_python(email),
121125
trial_account_days=trial_account_days,
122-
extra_credits_in_usd=None,
126+
extra_credits_in_usd=extra_credits_in_usd,
123127
product=product,
124128
)
125129

@@ -129,7 +133,7 @@ def invite(
129133
base_url=settings.INVITATIONS_OSPARC_URL,
130134
default_product=settings.INVITATIONS_DEFAULT_PRODUCT,
131135
)
132-
print(invitation_link) # noqa: T201
136+
typer.echo(invitation_link)
133137

134138

135139
@main.command()
@@ -149,18 +153,8 @@ def extract(ctx: typer.Context, invitation_url: str):
149153
)
150154
assert invitation.product is not None # nosec
151155

152-
print(invitation.model_dump_json(indent=1)) # noqa: T201
153-
154-
except (InvalidInvitationCodeError, ValidationError):
155-
_err_console.print("[bold red]Invalid code[/bold red]")
156-
156+
typer.echo(invitation.model_dump_json(indent=1))
157157

158-
@main.command()
159-
def serve(
160-
ctx: typer.Context,
161-
*,
162-
reload: bool = False,
163-
):
164-
"""Starts server with http API"""
165-
assert ctx # nosec
166-
web_server.start(log_level="info", reload=reload)
158+
except (InvalidInvitationCodeError, ValidationError) as err:
159+
typer.secho("Invalid code", fg=typer.colors.RED, bold=True, err=True)
160+
raise typer.Exit(os.EX_DATAERR) from err

services/invitations/tests/unit/api/test_api_invitations.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_create_invitation(
4040
assert invitation.issuer == invitation_input.issuer
4141
assert invitation.guest == invitation_input.guest
4242
assert invitation.trial_account_days == invitation_input.trial_account_days
43+
assert invitation.extra_credits_in_usd == invitation_input.extra_credits_in_usd
4344

4445
# checks issue with `//` reported in https://github.com/ITISFoundation/osparc-simcore/issues/7055
4546
assert invitation.invitation_url
@@ -61,6 +62,7 @@ def test_check_invitation(
6162
"issuer": invitation_data.issuer,
6263
"guest": invitation_data.guest,
6364
"trial_account_days": invitation_data.trial_account_days,
65+
"extra_credits_in_usd": invitation_data.extra_credits_in_usd,
6466
},
6567
auth=basic_auth,
6668
)
@@ -85,6 +87,7 @@ def test_check_invitation(
8587
assert invitation.issuer == invitation_data.issuer
8688
assert invitation.guest == invitation_data.guest
8789
assert invitation.trial_account_days == invitation_data.trial_account_days
90+
assert invitation.extra_credits_in_usd == invitation_data.extra_credits_in_usd
8891

8992

9093
def test_check_valid_invitation(

services/invitations/tests/unit/conftest.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from cryptography.fernet import Fernet
1111
from faker import Faker
1212
from models_library.products import ProductName
13+
from pydantic import PositiveInt, TypeAdapter
1314
from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict
1415
from pytest_simcore.helpers.typing_env import EnvVarsDict
1516
from simcore_service_invitations.services.invitations import InvitationInputs
@@ -85,6 +86,11 @@ def is_trial_account(request: pytest.FixtureRequest) -> bool:
8586
return request.param
8687

8788

89+
@pytest.fixture
90+
def extra_credits_in_usd(is_trial_account: bool) -> PositiveInt | None:
91+
return TypeAdapter(PositiveInt).validate_python(123) if is_trial_account else None
92+
93+
8894
@pytest.fixture
8995
def default_product() -> ProductName:
9096
return "s4llite"
@@ -98,7 +104,10 @@ def product(request: pytest.FixtureRequest) -> ProductName | None:
98104

99105
@pytest.fixture
100106
def invitation_data(
101-
is_trial_account: bool, faker: Faker, product: ProductName | None
107+
is_trial_account: bool,
108+
faker: Faker,
109+
product: ProductName | None,
110+
extra_credits_in_usd: PositiveInt | None,
102111
) -> InvitationInputs:
103112
# first version
104113
kwargs = {
@@ -110,4 +119,7 @@ def invitation_data(
110119
if product:
111120
kwargs["product"] = product
112121

122+
if extra_credits_in_usd is not None:
123+
kwargs["extra_credits_in_usd"] = extra_credits_in_usd
124+
113125
return InvitationInputs.model_validate(kwargs)

services/invitations/tests/unit/test_cli.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,24 @@ def test_invite_user_and_check_invitation(
4545
"INVITATIONS_DEFAULT_PRODUCT": default_product,
4646
}
4747

48-
expected = {
48+
expected_invitation = {
4949
**invitation_data.model_dump(exclude={"product"}),
5050
"product": environs["INVITATIONS_DEFAULT_PRODUCT"],
5151
}
5252

5353
# invitations-maker invite [email protected] --issuer=me --trial-account-days=3
54-
trial_account = ""
54+
other_options = ""
5555
if invitation_data.trial_account_days:
56-
trial_account = f"--trial-account-days={invitation_data.trial_account_days}"
56+
other_options = f"--trial-account-days={invitation_data.trial_account_days}"
57+
58+
if invitation_data.extra_credits_in_usd:
59+
other_options += (
60+
f" --extra-credits-in-usd={invitation_data.extra_credits_in_usd}"
61+
)
5762

5863
result = cli_runner.invoke(
5964
main,
60-
f"invite {invitation_data.guest} --issuer={invitation_data.issuer} {trial_account}",
65+
f"invite {invitation_data.guest} --issuer={invitation_data.issuer} {other_options}",
6166
env=environs,
6267
)
6368
assert result.exit_code == os.EX_OK, result.output
@@ -73,7 +78,7 @@ def test_invite_user_and_check_invitation(
7378
)
7479
assert result.exit_code == os.EX_OK, result.output
7580
assert (
76-
expected
81+
expected_invitation
7782
== TypeAdapter(InvitationInputs).validate_json(result.stdout).model_dump()
7883
)
7984

@@ -99,3 +104,24 @@ def test_list_settings(cli_runner: CliRunner, app_environment: EnvVarsDict):
99104
print(result.output)
100105
settings = ApplicationSettings.model_validate_json(result.output)
101106
assert settings == ApplicationSettings.create_from_envs()
107+
108+
109+
def test_extract_invalid_invitation_code(
110+
cli_runner: CliRunner, faker: Faker, app_environment: EnvVarsDict
111+
):
112+
"""Test that extract command handles invalid invitation codes properly"""
113+
# Create an invalid invitation URL
114+
invalid_invitation_url = f"{faker.url()}#invitation=invalid_code_123"
115+
116+
# Run extract command with invalid invitation URL
117+
result = cli_runner.invoke(
118+
main,
119+
f'extract "{invalid_invitation_url}"',
120+
env=app_environment,
121+
)
122+
123+
# Verify command exits with correct error code
124+
assert result.exit_code == os.EX_DATAERR
125+
126+
# Verify error message is displayed via stderr
127+
assert "Invalid code" in result.stdout

services/invitations/tests/unit/test_core_settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ def test_valid_application_settings(app_environment: EnvVarsDict):
4242
assert settings
4343

4444
assert settings == ApplicationSettings.create_from_envs()
45+
46+
assert settings.LOG_LEVEL == "INFO"

services/static-webserver/client/source/class/osparc/conversation/AddMessage.js

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,27 @@ qx.Class.define("osparc.conversation.AddMessage", {
2323
* @param studyData {Object} serialized Study Data
2424
* @param conversationId {String} Conversation Id
2525
*/
26-
construct: function(studyData, conversationId = null) {
26+
construct: function(studyData, conversationId = null, message = null) {
2727
this.base(arguments);
2828

2929
this.__studyData = studyData;
3030
this.__conversationId = conversationId;
31+
this.__message = message;
3132

3233
this._setLayout(new qx.ui.layout.VBox(5));
3334

3435
this.__buildLayout();
3536
},
3637

3738
events: {
38-
"commentAdded": "qx.event.type.Event"
39+
"commentAdded": "qx.event.type.Data",
40+
"messageEdited": "qx.event.type.Data",
3941
},
4042

4143
members: {
4244
__studyData: null,
4345
__conversationId: null,
46+
__message: null,
4447

4548
_createChildControlImpl: function(id) {
4649
let control;
@@ -120,13 +123,22 @@ qx.Class.define("osparc.conversation.AddMessage", {
120123

121124
__buildLayout: function() {
122125
this.getChildControl("thumbnail");
123-
this.getChildControl("comment-field");
126+
const commentField = this.getChildControl("comment-field");
124127

125128
const addMessageButton = this.getChildControl("add-comment-button");
126-
addMessageButton.addListener("execute", () => this.__addComment());
129+
if (this.__message) {
130+
// edit mode
131+
addMessageButton.setLabel(this.tr("Edit message"));
132+
addMessageButton.addListener("execute", () => this.__editComment());
133+
134+
commentField.setText(this.__message["content"]);
135+
} else {
136+
// new message
137+
addMessageButton.addListener("execute", () => this.__addComment());
127138

128-
const notifyUserButton = this.getChildControl("notify-user-button");
129-
notifyUserButton.addListener("execute", () => this.__notifyUserTapped());
139+
const notifyUserButton = this.getChildControl("notify-user-button");
140+
notifyUserButton.addListener("execute", () => this.__notifyUserTapped());
141+
}
130142
},
131143

132144
__addComment: function() {
@@ -211,13 +223,24 @@ qx.Class.define("osparc.conversation.AddMessage", {
211223

212224
__postMessage: function() {
213225
const commentField = this.getChildControl("comment-field");
214-
const comment = commentField.getChildControl("text-area").getValue();
215-
if (comment) {
216-
osparc.study.Conversations.addMessage(this.__studyData["uuid"], this.__conversationId, comment)
226+
const content = commentField.getChildControl("text-area").getValue();
227+
if (content) {
228+
osparc.study.Conversations.addMessage(this.__studyData["uuid"], this.__conversationId, content)
217229
.then(data => {
218230
this.fireDataEvent("commentAdded", data);
219231
commentField.getChildControl("text-area").setValue("");
220-
osparc.FlashMessenger.logAs(this.tr("Message added"), "INFO");
232+
});
233+
}
234+
},
235+
236+
__editComment: function() {
237+
const commentField = this.getChildControl("comment-field");
238+
const content = commentField.getChildControl("text-area").getValue();
239+
if (content) {
240+
osparc.study.Conversations.editMessage(this.__studyData["uuid"], this.__conversationId, this.__message["messageId"], content)
241+
.then(data => {
242+
this.fireDataEvent("messageEdited", data);
243+
commentField.getChildControl("text-area").setValue("");
221244
});
222245
}
223246
},

services/static-webserver/client/source/class/osparc/conversation/Conversation.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ qx.Class.define("osparc.conversation.Conversation", {
195195
this.__getNextRequest()
196196
.then(resp => {
197197
const messages = resp["data"];
198+
// it's not provided by the backend
199+
messages.forEach(message => message["studyId"] = this.__studyData["uuid"]);
198200
this.__addMessages(messages);
199201
this.__nextRequestParams = resp["_links"]["next"];
200202
if (this.__nextRequestParams === null) {
@@ -236,7 +238,9 @@ qx.Class.define("osparc.conversation.Conversation", {
236238
let control = null;
237239
switch (message["type"]) {
238240
case "MESSAGE":
239-
control = new osparc.conversation.MessageUI(message);
241+
control = new osparc.conversation.MessageUI(message, this.__studyData);
242+
control.addListener("messageEdited", () => this.fetchMessages());
243+
control.addListener("messageDeleted", () => this.fetchMessages());
240244
break;
241245
case "NOTIFICATION":
242246
control = new osparc.conversation.NotificationUI(message);

0 commit comments

Comments
 (0)