Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to item creation #17

Merged
merged 7 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions src/cds_portal/components/input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from solara.alias import rv as v
import solara
from typing import Callable, Dict, List, Optional, TypeVar, Union

from solara.components.input import _use_input_type, use_change


T = TypeVar("T")

@solara.component
def NumericInput(
str_to_numeric: Callable[[Optional[str]], T],
label: str,
value: Union[None, T, solara.Reactive[Optional[T]], solara.Reactive[T]],
on_value: Union[None, Callable[[Optional[T]], None], Callable[[T], None]] = None,
on_error_change: Union[None, Callable[[Optional[bool]], None], Callable[[bool], None]] = None,
disabled: bool = False,
continuous_update: bool = False,
clearable: bool = False,
hide_details: Optional[Union[bool, str]] = None,
outlined: bool = False,
classes: List[str] = [],
style: Optional[Union[str, Dict[str, str]]] = None,
):
"""Integer input.

## Arguments

* `label`: Label to display next to the slider.
* `value`: The currently entered value.
* `on_value`: Callback to call when the value changes.
* `disabled`: Whether the input is disabled.
* `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed.
* `classes`: List of CSS classes to apply to the input.
* `style`: CSS style to apply to the input.
"""


style_flat = solara.util._flatten_style(style)
classes_flat = solara.util._combine_classes(classes)

internal_value, error, set_value_cast = _use_input_type(
value,
str_to_numeric,
str,
on_value,
)

def on_v_model(value):
if continuous_update:
set_value_cast(value)

if on_error_change:
on_error_change(bool(error))

if error:
label += f" ({error})"
text_field = v.TextField(
v_model=internal_value,
on_v_model=on_v_model,
label=label,
disabled=disabled,
outlined=outlined,
# we are not using the number type, since we cannot validate invalid input
# see https://stackoverflow.blog/2022/12/26/why-the-number-input-is-the-worst-input/
# type="number",
hide_details=hide_details,
clearable=clearable,
error=bool(error),
class_=classes_flat,
style_=style_flat,
)
# use_change(text_field, set_value_cast, enabled=not continuous_update)
return text_field


@solara.component
def IntegerInput(
label: str,
value: Union[None, int, solara.Reactive[Optional[int]], solara.Reactive[int]],
on_value: Union[None, Callable[[Optional[int]], None], Callable[[int], None]] = None,
on_error_change: Union[None, Callable[[Optional[bool]], None], Callable[[bool], None]] = None,
disabled: bool = False,
continuous_update: bool = False,
clearable: bool = False,
hide_details: Optional[Union[bool, str]] = None,
optional: bool = False,
outlined: bool = False,
classes: List[str] = [],
style: Optional[Union[str, Dict[str, str]]] = None,
):

def str_to_int(value: Optional[str]) -> Optional[int]:
if value:
try:
return int(value)
except ValueError:
if optional:
return None
raise ValueError("Value must be an integer")
else:
if optional:
return None
else:
raise ValueError("Value cannot be empty")

return NumericInput(
str_to_int,
label=label,
value=value,
on_value=on_value,
on_error_change=on_error_change,
disabled=disabled,
continuous_update=continuous_update,
clearable=clearable,
hide_details=hide_details,
classes=classes,
outlined=outlined,
style=style,
)
38 changes: 36 additions & 2 deletions src/cds_portal/pages/manage_classes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import solara
from solara.alias import rv

from cds_portal.components.input import IntegerInput

from ...remote import BASE_API


Expand All @@ -11,6 +13,9 @@ def CreateClassDialog(on_create_clicked: callable = None):
active, set_active = solara.use_state(False) #
text, set_text = solara.use_state("")
stories, set_stories = solara.use_state([])
expected_size, set_expected_size = solara.use_state(20)
asynchronous, set_asynchronous = solara.use_state(False)
expected_size_error = solara.use_reactive(False)

with rv.Dialog(
v_model=active,
Expand Down Expand Up @@ -54,15 +59,39 @@ def CreateClassDialog(on_create_clicked: callable = None):
multiple=False,
)

IntegerInput(
label="Expected size",
value=expected_size,
on_value=set_expected_size,
on_error_change=expected_size_error.set,
continuous_update=True,
outlined=True,
hide_details="auto",
classes=["pt-2"]
)

solara.Checkbox(
label="Asynchronous class",
value=asynchronous,
on_value=set_asynchronous,
)

rv.Divider()

with rv.CardActions():

@solara.lab.computed
def create_button_disabled():
print(expected_size_error)
return expected_size_error.value or (not (text and stories))

def _add_button_clicked(*args):
on_create_clicked(
{
"name": f"{text}",
"stories": f"{', '.join(stories)}",
"expected_size": expected_size,
"asynchronous": asynchronous,
}
)
set_active(False)
Expand All @@ -71,7 +100,8 @@ def _add_button_clicked(*args):

solara.Button("Cancel", on_click=lambda: set_active(False), elevation=0)
solara.Button(
"Create", color="info", on_click=_add_button_clicked, elevation=0
"Create", color="info", on_click=_add_button_clicked, elevation=0,
disabled=create_button_disabled.value
)

return dialog
Expand Down Expand Up @@ -147,6 +177,8 @@ def _retrieve_classes():
"story": "Hubble's Law",
"code": cls["code"],
"id": cls["id"],
"expected_size": cls["expected_size"],
"asynchronous": cls["asynchronous"],
}

new_classes.append(new_class)
Expand All @@ -156,7 +188,7 @@ def _retrieve_classes():
solara.use_effect(_retrieve_classes, [])

def _create_class_callback(class_info):
BASE_API.create_new_class(class_info["name"])
BASE_API.create_new_class(class_info)
_retrieve_classes()

def _delete_class_callback():
Expand Down Expand Up @@ -195,6 +227,8 @@ def _delete_class_callback():
{"text": "Story", "value": "story"},
{"text": "Code", "value": "code"},
{"text": "ID", "value": "id", "align": " d-none"},
{"text": "Expected size", "value": "expected_size"},
{"text": "Asynchronous", "value": "asynchronous"},
# {"text": "Actions", "value": "actions", "align": "end"},
],
)
19 changes: 12 additions & 7 deletions src/cds_portal/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ def create_new_student(self, class_code: str) -> Response:
}

r = self.request_session.post(
f"{self.API_URL}/student-sign-up",
f"{self.API_URL}/students/create",
json=payload,
)

if r.status_code != 200:
if r.status_code != 201:
logger.error("Failed to create new user.")
else:
logger.info(
Expand Down Expand Up @@ -156,11 +156,11 @@ def create_new_educator(self, form_data: dict) -> Response:
form_data.update({"username": self.hashed_user, "password": str(uuid.uuid4())})

r = self.request_session.post(
f"{self.API_URL}/educator-sign-up",
f"{self.API_URL}/educators/create",
json=form_data,
)

if r.status_code != 200:
if r.status_code != 201:
logger.error("Failed to create new user.")

r = Response()
Expand All @@ -174,13 +174,18 @@ def create_new_educator(self, form_data: dict) -> Response:

return r

def create_new_class(self, name: str) -> dict:
def create_new_class(self, info: dict) -> dict:
r = self.request_session.get(f"{self.API_URL}/educators/{self.hashed_user}")
educator = r.json()["educator"]

r = self.request_session.post(
f"{self.API_URL}/create-class",
json={"educator_id": educator["id"], "name": name},
f"{self.API_URL}/classes/create",
json={
"educator_id": educator["id"],
"name": info["name"],
"expected_size": info["expected_size"],
"asynchronous": info["asynchronous"],
},
)

return r.json()
Expand Down