Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hive/clive
1 result
Show changes
Commits on Source (14)
Showing
with 241 additions and 103 deletions
......@@ -5,7 +5,7 @@ from clive.__private.cli.commands.abc.profile_based_command import ProfileBasedC
from clive.__private.cli.exceptions import CLIPrettyError
from clive.__private.core.formatters.humanize import humanize_validation_result
from clive.__private.storage.accounts import Account
from clive.__private.validators.account_name_validator import AccountNameValidator
from clive.__private.validators.set_tracked_account_validator import SetTrackedAccountValidator
@dataclass(kw_only=True)
......@@ -13,7 +13,7 @@ class AddWatchedAccount(ProfileBasedCommand):
account_name: str
async def validate(self) -> None:
result = AccountNameValidator().validate(self.account_name)
result = SetTrackedAccountValidator(self.profile_data).validate(self.account_name)
if not result.is_valid:
raise CLIPrettyError(f"Can't use this account name: {humanize_validation_result(result)}", errno.EINVAL)
......
......@@ -8,7 +8,7 @@ from clive.__private.cli.exceptions import (
CLIWorkingAccountIsNotSetError,
)
from clive.__private.core.formatters.humanize import humanize_validation_result
from clive.__private.validators.account_name_validator import AccountNameValidator
from clive.__private.validators.set_tracked_account_validator import SetTrackedAccountValidator
@dataclass(kw_only=True)
......@@ -19,7 +19,7 @@ class AddWorkingAccount(ProfileBasedCommand):
if self.profile_data.is_working_account_set():
raise CLIWorkingAccountIsAlreadySetError(self.profile_data)
result = AccountNameValidator().validate(self.account_name)
result = SetTrackedAccountValidator(self.profile_data).validate(self.account_name)
if not result.is_valid:
raise CLIPrettyError(f"Can't use this account name: {humanize_validation_result(result)}", errno.EINVAL)
......
import typing
from enum import Enum
from typing import cast
import typer
......@@ -92,13 +92,13 @@ else:
# unfortunately typer doesn't support Literal types yet, so we have to convert it to an enum
OrdersEnum = Enum( # type: ignore[misc, no-redef]
"OrdersEnum", {option: option for option in typing.get_args(ProposalsDataRetrieval.Orders)}
"OrdersEnum", {option: option for option in ProposalsDataRetrieval.ORDERS}
)
OrderDirectionsEnum = Enum( # type: ignore[misc, no-redef]
"OrderDirectionsEnum", {option: option for option in typing.get_args(ProposalsDataRetrieval.OrderDirections)}
"OrderDirectionsEnum", {option: option for option in ProposalsDataRetrieval.ORDER_DIRECTIONS}
)
StatusesEnum = Enum( # type: ignore[misc, no-redef]
"StatusesEnum", {option: option for option in typing.get_args(ProposalsDataRetrieval.Statuses)}
"StatusesEnum", {option: option for option in ProposalsDataRetrieval.STATUSES}
)
DEFAULT_ORDER = ProposalsDataRetrieval.DEFAULT_ORDER
......@@ -186,20 +186,21 @@ async def show_proposals( # noqa: PLR0913
"""List proposals filtered by status."""
from clive.__private.cli.commands.show.show_proposals import ShowProposals
if isinstance(order_by, Enum):
order_by = order_by.value
if isinstance(order_direction, Enum):
order_direction = order_direction.value
if isinstance(status, Enum):
status = status.value
assert isinstance(order_by, Enum), f"Expected Enum type, but got: {type(order_by)}"
assert isinstance(order_direction, Enum), f"Expected Enum type, but got: {type(order_by)}"
assert isinstance(status, Enum), f"Expected Enum type, but got: {type(order_by)}"
order_by_ = cast(ProposalsDataRetrieval.Orders, order_by.value)
order_direction_ = cast(ProposalsDataRetrieval.OrderDirections, order_direction.value)
status_ = cast(ProposalsDataRetrieval.Statuses, status.value)
common = WorldWithoutBeekeeperCommonOptions.get_instance()
await ShowProposals(
**common.as_dict(),
account_name=account_name,
order_by=order_by,
order_direction=order_direction,
status=status,
order_by=order_by_,
order_direction=order_direction_,
status=status_,
page_size=page_size,
page_no=page_no,
).run()
......
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, ClassVar, Literal, TypeAlias
from typing import TYPE_CHECKING, ClassVar, Literal, get_args
from typing_extensions import TypeAliasType
from clive.__private.core.commands.abc.command_data_retrieval import CommandDataRetrieval
from clive.__private.core.formatters.humanize import humanize_datetime, humanize_votes_with_suffix
......@@ -56,13 +58,20 @@ class ProposalsData:
proposals: list[Proposal]
_Orders = Literal["by_total_votes_with_voted_first", "by_total_votes", "by_start_date", "by_end_date", "by_creator"]
_OrderDirections = Literal["ascending", "descending"]
_Statuses = Literal["all", "active", "inactive", "votable", "expired", "inactive"]
@dataclass(kw_only=True)
class ProposalsDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedData, ProposalsData]):
Orders: ClassVar[TypeAlias] = Literal[
"by_total_votes_with_voted_first", "by_total_votes", "by_start_date", "by_end_date", "by_creator"
]
OrderDirections: ClassVar[TypeAlias] = Literal["ascending", "descending"]
Statuses: ClassVar[TypeAlias] = Literal["all", "active", "inactive", "votable", "expired", "inactive"]
Orders = TypeAliasType("Orders", _Orders)
OrderDirections = TypeAliasType("OrderDirections", _OrderDirections)
Statuses = TypeAliasType("Statuses", _Statuses)
ORDERS: ClassVar[tuple[Orders, ...]] = get_args(_Orders)
ORDER_DIRECTIONS: ClassVar[tuple[OrderDirections, ...]] = get_args(_OrderDirections)
STATUSES: ClassVar[tuple[Statuses, ...]] = get_args(_Statuses)
MAX_POSSIBLE_NUMBER_OF_VOTES: ClassVar[int] = 2**63 - 1
MAX_SEARCHED_PROPOSALS_HARD_LIMIT: ClassVar[int] = 100
......@@ -90,7 +99,7 @@ class ProposalsDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedDat
order: DatabaseApi.SORT_TYPES
if self.order == "by_total_votes_with_voted_first":
order = "by_total_votes"
elif self.order in self.Orders.__args__:
elif self.order in self.ORDERS:
order = self.order
else:
raise ValueError(f"Unknown order: {self.order}")
......
......@@ -2,7 +2,9 @@ from __future__ import annotations
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, ClassVar, Literal, TypeAlias
from typing import TYPE_CHECKING, ClassVar, Literal
from typing_extensions import TypeAliasType
from clive.__private.core.commands.abc.command_data_retrieval import (
CommandDataRetrieval,
......@@ -68,7 +70,7 @@ class WitnessesData:
@dataclass(kw_only=True)
class WitnessesDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedData, WitnessesData]):
Modes: ClassVar[TypeAlias] = Literal["search_by_pattern", "search_top_with_unvoted_first"]
Modes = TypeAliasType("Modes", Literal["search_by_pattern", "search_top_with_voted_first"])
"""
Available modes for retrieving witnesses data.
......@@ -78,10 +80,10 @@ class WitnessesDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedDat
call.
Amount of witnesses is limited to the search_by_name_limit.
search_top_with_unvoted_first:
search_top_with_voted_first:
------------------------------
Retrieves top witnesses with the ones voted for by the account.
The list is sorted by the unvoted status and then by rank.
The list is sorted by the voted status and then by rank.
Amount of witnesses is limited to the TOP_WITNESSES_HARD_LIMIT.
"""
......@@ -91,7 +93,7 @@ class WitnessesDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedDat
TOP_WITNESSES_HARD_LIMIT: ClassVar[int] = 150
DEFAULT_SEARCH_BY_NAME_LIMIT: ClassVar[int] = 50
DEFAULT_MODE: ClassVar[Modes] = "search_top"
DEFAULT_MODE: ClassVar[Modes] = "search_top_with_voted_first"
node: Node
account_name: str
......@@ -140,8 +142,8 @@ class WitnessesDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedDat
)
async def _process_data(self, data: SanitizedData) -> WitnessesData:
if self.mode == "search_top":
witnesses = self.__get_top_witnesses_with_unvoted_first(data)
if self.mode == "search_top_with_voted_first":
witnesses = self.__get_top_witnesses_with_voted_first(data)
elif self.mode == "search_by_pattern":
witnesses = self.__get_witnesses_by_pattern(data)
else:
......@@ -157,7 +159,7 @@ class WitnessesDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedDat
}
)
def __get_top_witnesses_with_unvoted_first(self, data: SanitizedData) -> OrderedDict[str, WitnessData]:
def __get_top_witnesses_with_voted_first(self, data: SanitizedData) -> OrderedDict[str, WitnessData]:
top_witnesses = self.__get_top_witnesses(data)
# Include witnesses that account voted for but are not included in the top
......@@ -172,7 +174,7 @@ class WitnessesDataRetrieval(CommandDataRetrieval[HarvestedDataRaw, SanitizedDat
top_witnesses_with_all_voted_for = voted_but_not_in_top_witnesses | top_witnesses
# Sort the witnesses based on unvoted status and rank
# Sort the witnesses based on voted status and rank
return OrderedDict(
sorted(
top_witnesses_with_all_voted_for.items(), key=lambda witness: (not witness[1].voted, witness[1].rank)
......
......@@ -149,7 +149,7 @@ class ProfileData(Context):
raise ProfileInvalidNameError(name, reason=humanize_validation_result(result))
def get_account_by_name(self, value: str | Account) -> Account:
searched_account_name = value if isinstance(value, str) else value.name
searched_account_name = self._get_account_name(value)
for account in self.get_tracked_accounts():
if account.name == searched_account_name:
return account
......@@ -178,6 +178,18 @@ class ProfileData(Context):
def known_accounts_sorted(self) -> list[Account]:
return sorted(self.known_accounts, key=lambda account: account.name)
@property
def tracked_accounts_sorted(self) -> list[Account]:
"""Working account is always first then watched accounts sorted alphabetically."""
return sorted(
self.get_tracked_accounts(),
key=lambda account: (not isinstance(account, WorkingAccount), account.name),
)
@staticmethod
def _get_account_name(account: str | Account) -> str:
return account if isinstance(account, str) else account.name
def set_working_account(self, value: str | WorkingAccount) -> None:
if isinstance(value, str):
value = WorkingAccount(value)
......@@ -221,12 +233,35 @@ class ProfileData(Context):
def is_working_account_set(self) -> bool:
return self.__working_account is not None
def is_account_tracked(self, account: str | Account) -> bool:
account_name = self._get_account_name(account)
return account_name not in [tracked_account.name for tracked_account in self.get_tracked_accounts()]
def get_tracked_accounts(self) -> set[Account]:
accounts = self.watched_accounts.copy()
if self.is_working_account_set():
accounts.add(self.working_account)
return accounts
def has_known_accounts(self) -> bool:
return bool(self.known_accounts)
def has_tracked_accounts(self) -> bool:
return bool(self.get_tracked_accounts())
def remove_tracked_account(self, to_remove: str | Account) -> None:
account_name = self._get_account_name(to_remove)
if self.is_working_account_set() and account_name == self.working_account.name:
self.unset_working_account()
else:
self.remove_watched_account(to_remove)
def remove_watched_account(self, to_remove: str | Account) -> None:
account_name = self._get_account_name(to_remove)
account = next((account for account in self.watched_accounts if account.name == account_name), None)
if account is not None:
self.watched_accounts.discard(account)
@property
def is_accounts_alarms_data_available(self) -> bool:
tracked_accounts = self.get_tracked_accounts()
......
from __future__ import annotations
from typing import TYPE_CHECKING, Final
from typing import TYPE_CHECKING
from textual.binding import Binding
from clive.__private.ui.account_list_management.known_accounts import KnownAccounts
from clive.__private.ui.account_list_management.watched_accounts import WatchedAccounts
from clive.__private.ui.account_list_management.tracked_accounts import TrackedAccounts
from clive.__private.ui.account_list_management.working_account import WorkingAccount
from clive.__private.ui.get_css import get_relative_css_path
from clive.__private.ui.shared.base_screen import BaseScreen
......@@ -14,10 +14,6 @@ from clive.__private.ui.widgets.clive_tabbed_content import CliveTabbedContent
if TYPE_CHECKING:
from textual.app import ComposeResult
WATCHED_ACCOUNTS_TAB_LABEL: Final[str] = "Watched accounts"
KNOWN_ACCOUNTS_TAB_LABEL: Final[str] = "Known accounts"
WORKING_ACCOUNT_TAB_LABEL: Final[str] = "Working account"
class AccountListManagement(BaseScreen):
CSS_PATH = [get_relative_css_path(__file__)]
......@@ -28,9 +24,9 @@ class AccountListManagement(BaseScreen):
def create_main_panel(self) -> ComposeResult:
with CliveTabbedContent():
yield WatchedAccounts(WATCHED_ACCOUNTS_TAB_LABEL)
yield KnownAccounts(KNOWN_ACCOUNTS_TAB_LABEL)
yield WorkingAccount(WORKING_ACCOUNT_TAB_LABEL)
yield TrackedAccounts()
yield KnownAccounts()
yield WorkingAccount()
def on_mount(self) -> None:
self.app.trigger_profile_data_watchers()
......@@ -23,10 +23,13 @@ class AccountsTableHeader(Horizontal):
}
"""
def __init__(self, account_column_name: str = "Name") -> None:
def __init__(self, account_column_name: str = "Name", *, show_type_column: bool = False) -> None:
super().__init__()
self._account_column_name = account_column_name
self.show_type_column = show_type_column
def compose(self) -> ComposeResult:
yield Label(self._account_column_name, classes=ODD_CLASS_NAME)
yield Label("Action", classes=EVEN_CLASS_NAME)
if self.show_type_column:
yield Label("Account type", classes=EVEN_CLASS_NAME)
yield Label("Action", classes=ODD_CLASS_NAME if self.show_type_column else EVEN_CLASS_NAME)
......@@ -14,7 +14,7 @@ from clive.__private.ui.widgets.inputs.account_name_input import AccountNameInpu
from clive.__private.ui.widgets.scrolling import ScrollablePart
from clive.__private.ui.widgets.section import Section
from clive.__private.validators.set_known_account_validator import SetKnownAccountValidator
from clive.__private.validators.set_watched_account_validator import SetWatchedAccountValidator
from clive.__private.validators.set_tracked_account_validator import SetTrackedAccountValidator
if TYPE_CHECKING:
from textual.app import ComposeResult
......@@ -30,8 +30,8 @@ class ManageAccountsTabPane(TabPane, CliveWidget):
self._accounts_input = AccountNameInput(
required=False,
validators=(
SetWatchedAccountValidator(self.app.world.profile_data)
if accounts_type == "watched_accounts"
SetTrackedAccountValidator(self.app.world.profile_data)
if accounts_type == "tracked_accounts"
else SetKnownAccountValidator(self.app.world.profile_data)
),
ask_known_account=accounts_type != "known_accounts",
......@@ -40,10 +40,10 @@ class ManageAccountsTabPane(TabPane, CliveWidget):
def compose(self) -> ComposeResult:
with ScrollablePart():
with Section(f"{'Watch' if self._accounts_type == 'watched_accounts' else 'Known'} account"):
with Section(f"{'Track' if self._accounts_type == 'tracked_accounts' else 'Add known'} account"):
yield self._accounts_input
yield CliveButton(
f"{'Watch' if self._accounts_type == 'watched_accounts' else 'Mark as known'}",
"Add",
variant="success",
id_="save-account-button",
)
......@@ -67,7 +67,10 @@ class ManageAccountsTabPane(TabPane, CliveWidget):
account = Account(name=self._accounts_input.value_or_error)
getattr(self.app.world.profile_data, self._accounts_type).add(account)
if self._accounts_type == "tracked_accounts":
self.app.world.profile_data.watched_accounts.add(account)
else:
self.app.world.profile_data.known_accounts.add(account)
self.app.trigger_profile_data_watchers()
self._accounts_input.input.clear()
self.app.update_alarms_data_asap()
......@@ -19,28 +19,41 @@ if TYPE_CHECKING:
from clive.__private.core.profile_data import ProfileData
from clive.__private.core.world import TextualWorld
from clive.__private.storage.accounts import Account
from clive.__private.storage.accounts import WorkingAccount
AccountsType = Literal["known_accounts", "watched_accounts"]
AccountsType = Literal["known_accounts", "tracked_accounts"]
class AccountRow(CliveCheckerboardTableRow):
def __init__(self, account: Account, account_type: AccountsType) -> None:
super().__init__(
CliveCheckerBoardTableCell(account.name),
self._is_working_account = isinstance(account, WorkingAccount)
self._account = account
self._account_type = account_type
super().__init__(*self._create_cells())
def _create_cells(self) -> list[CliveCheckerBoardTableCell]:
cells_unfiltered = [
CliveCheckerBoardTableCell(self._account.name),
self._create_account_type_column() if self._account_type == "tracked_accounts" else None,
CliveCheckerBoardTableCell(
OneLineButton(
"Mark as unknown" if account_type == "known_accounts" else "Unwatch",
"Remove",
variant="error",
id_="discard-account-button",
)
),
)
self._account = account
self._account_type = account_type
]
return list(filter(None, cells_unfiltered))
def _create_account_type_column(self) -> CliveCheckerBoardTableCell:
return CliveCheckerBoardTableCell("working" if self._is_working_account else "watched")
@on(CliveButton.Pressed, "#discard-account-button")
def discard_account(self) -> None:
getattr(self.app.world.profile_data, self._account_type).discard(self._account)
if self._account_type == "known_accounts":
self.app.world.profile_data.known_accounts.discard(self._account)
else:
self.app.world.profile_data.remove_tracked_account(self._account)
self.app.trigger_profile_data_watchers()
......@@ -66,7 +79,7 @@ class ManageAccountsTable(CliveCheckerboardTable):
def __init__(self, accounts_type: AccountsType) -> None:
super().__init__(
header=AccountsTableHeader(),
header=AccountsTableHeader(show_type_column=accounts_type == "tracked_accounts"),
title=f"Your {self.remove_underscore_from_text(accounts_type)}",
)
self._previous_accounts: set[Account] | NotUpdatedYet = NotUpdatedYet()
......@@ -83,17 +96,27 @@ class ManageAccountsTable(CliveCheckerboardTable):
)
def check_if_should_be_updated(self, content: ProfileData) -> bool:
return getattr(content, self._accounts_type) != self._previous_accounts # type: ignore[no-any-return]
actual_accounts = self._get_accounts_from_new_content(content)
return actual_accounts != self._previous_accounts
def is_anything_to_display(self, content: ProfileData) -> bool:
return len(getattr(content, self._accounts_type)) != 0
return (
content.has_known_accounts() if self._accounts_type == "known_accounts" else content.has_tracked_accounts()
)
@property
def object_to_watch(self) -> TextualWorld:
return self.app.world
def update_previous_state(self, content: ProfileData) -> None:
self._previous_accounts = getattr(content, self._accounts_type).copy()
self._previous_accounts = self._get_accounts_from_new_content(content)
def _get_accounts_from_new_content(self, content: ProfileData) -> set[Account]:
return (
content.known_accounts.copy()
if self._accounts_type == "known_accounts"
else content.get_tracked_accounts().copy()
)
def remove_underscore_from_text(self, text: str) -> str:
return text.replace("_", " ")
from __future__ import annotations
from typing import Final
from clive.__private.ui.account_list_management.common.manage_accounts_tab_pane import ManageAccountsTabPane
class KnownAccounts(ManageAccountsTabPane):
"""TabPane used to add and delete known accounts."""
def __init__(self, title: str) -> None:
super().__init__(title=title, accounts_type="known_accounts")
TITLE: Final[str] = "Known accounts"
def __init__(self) -> None:
super().__init__(title=self.TITLE, accounts_type="known_accounts")
from __future__ import annotations
from typing import Final
from clive.__private.ui.account_list_management.common.manage_accounts_tab_pane import ManageAccountsTabPane
class WatchedAccounts(ManageAccountsTabPane):
"""TabPane used to add and delete watched accounts."""
class TrackedAccounts(ManageAccountsTabPane):
"""TabPane used to add and delete tracked accounts."""
DEFAULT_CSS = """
WatchedAccounts {
TrackedAccounts {
#input-with-button {
CliveButton {
width: 10;
......@@ -16,6 +18,7 @@ class WatchedAccounts(ManageAccountsTabPane):
}
}
"""
TITLE: Final[str] = "Tracked accounts"
def __init__(self, title: str) -> None:
super().__init__(title=title, accounts_type="watched_accounts")
def __init__(self) -> None:
super().__init__(title=self.TITLE, accounts_type="tracked_accounts")
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Final
from textual import on
from textual.containers import Vertical
......@@ -152,6 +152,10 @@ class WorkingAccount(TabPane, CliveWidget):
"""TabPane used to add and delete working account."""
DEFAULT_CSS = get_css_from_relative_path(__file__)
TITLE: Final[str] = "Working account"
def __init__(self) -> None:
super().__init__(self.TITLE)
def compose(self) -> ComposeResult:
with ScrollablePart():
......
......@@ -35,12 +35,12 @@ DashboardBase {
}
#tables {
width: 1fr;
width: 8fr;
}
}
AccountInfo {
width: auto;
width: 3fr;
#account-alarms-and-details {
width: auto;
......
......@@ -17,9 +17,9 @@ class ProposalsDataProvider(DataProvider[ProposalsData]):
def __init__(self, *, paused: bool = False, init_update: bool = True) -> None:
super().__init__(paused=paused, init_update=init_update)
self.__order = ProposalsDataRetrieval.DEFAULT_ORDER
self.__order_direction = ProposalsDataRetrieval.DEFAULT_ORDER_DIRECTION
self.__status = ProposalsDataRetrieval.DEFAULT_STATUS
self.__order: ProposalsDataRetrieval.Orders = ProposalsDataRetrieval.DEFAULT_ORDER
self.__order_direction: ProposalsDataRetrieval.OrderDirections = ProposalsDataRetrieval.DEFAULT_ORDER_DIRECTION
self.__status: ProposalsDataRetrieval.Statuses = ProposalsDataRetrieval.DEFAULT_STATUS
@work(name="proposals data update worker")
async def update(self) -> None:
......@@ -43,7 +43,12 @@ class ProposalsDataProvider(DataProvider[ProposalsData]):
if result.proposals != self.content.proposals:
self._content = result
def change_order(self, order: str, order_direction: str, status: str) -> Worker[None]:
def change_order(
self,
order: ProposalsDataRetrieval.Orders,
order_direction: ProposalsDataRetrieval.OrderDirections,
status: ProposalsDataRetrieval.Statuses,
) -> Worker[None]:
self.__order = order
self.__order_direction = order_direction
self.__status = status
......
......@@ -132,9 +132,6 @@ class NewKeyAliasBase(KeyAliasForm, ABC):
def _default_key(self) -> str:
return typing.cast(str, settings.get("secrets.default_key", ""))
def _default_key_alias_name(self) -> str:
return f"{self.context.working_account.name}@active"
class NewKeyAlias(NewKeyAliasBase):
BIG_TITLE = "Configuration"
......
......@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, ClassVar
from textual import on
from textual.containers import Horizontal, Vertical
from textual.message import Message
from textual.widgets import Label, Select
from textual.widgets import Label
from clive.__private.core.commands.data_retrieval.proposals_data import Proposal as ProposalData
from clive.__private.core.commands.data_retrieval.proposals_data import ProposalsDataRetrieval
......@@ -27,6 +27,7 @@ from clive.__private.ui.operations.governance_operations.common_governance.gover
GovernanceTable,
GovernanceTableRow,
)
from clive.__private.ui.widgets.clive_select import CliveSelect
from clive.__private.ui.widgets.ellipsed_static import EllipsedStatic
from clive.__private.ui.widgets.scrolling import ScrollablePart
from clive.__private.ui.widgets.section_title import SectionTitle
......@@ -41,7 +42,7 @@ if TYPE_CHECKING:
from clive.models import Operation
class ProposalsOrderSelect(Select[ProposalsDataRetrieval.Orders]):
class ProposalsOrderSelect(CliveSelect[ProposalsDataRetrieval.Orders]):
SELECTABLES: Final[list[tuple[str, ProposalsDataRetrieval.Orders]]] = [
("Total votes, mine first", "by_total_votes_with_voted_first"),
("Total votes", "by_total_votes"),
......@@ -58,9 +59,9 @@ class ProposalsOrderSelect(Select[ProposalsDataRetrieval.Orders]):
)
class ProposalsOrderDirectionSelect(Select[ProposalsDataRetrieval.OrderDirections]):
class ProposalsOrderDirectionSelect(CliveSelect[ProposalsDataRetrieval.OrderDirections]):
SELECTABLES: Final[list[tuple[str, ProposalsDataRetrieval.OrderDirections]]] = [
(value.capitalize(), value) for value in ProposalsDataRetrieval.OrderDirections.__args__
(value.capitalize(), value) for value in ProposalsDataRetrieval.ORDER_DIRECTIONS
]
def __init__(self) -> None:
......@@ -71,9 +72,9 @@ class ProposalsOrderDirectionSelect(Select[ProposalsDataRetrieval.OrderDirection
)
class ProposalsStatusSelect(Select[ProposalsDataRetrieval.Statuses]):
class ProposalsStatusSelect(CliveSelect[ProposalsDataRetrieval.Statuses]):
SELECTABLES: Final[list[tuple[str, ProposalsDataRetrieval.Statuses]]] = [
(value.capitalize(), value) for value in ProposalsDataRetrieval.Statuses.__args__
(value.capitalize(), value) for value in ProposalsDataRetrieval.STATUSES
]
def __init__(self) -> None:
......@@ -183,7 +184,12 @@ class ProposalsListHeader(GovernanceListHeader):
class ProposalsTable(GovernanceTable[ProposalData, ProposalsDataProvider]):
async def change_order(self, order: str, order_direction: str, status: str) -> None:
async def change_order(
self,
order: ProposalsDataRetrieval.Orders,
order_direction: ProposalsDataRetrieval.OrderDirections,
status: ProposalsDataRetrieval.Statuses,
) -> None:
await self.provider.change_order(order=order, order_direction=order_direction, status=status).wait()
await self.reset_page()
......@@ -214,7 +220,7 @@ class ProposalsOrderChange(Vertical):
def __init__(self) -> None:
super().__init__()
with self.prevent(Select.Changed):
with self.prevent(CliveSelect.Changed):
self.__order_by_select = ProposalsOrderSelect()
self.__order_direction_select = ProposalsOrderDirectionSelect()
self.__proposal_status_select = ProposalsStatusSelect()
......@@ -229,11 +235,11 @@ class ProposalsOrderChange(Vertical):
yield self.__order_direction_select
yield self.__proposal_status_select
@on(Select.Changed)
@on(CliveSelect.Changed)
def search_witnesses(self) -> None:
order_by = self.__order_by_select.value
order_direction = self.__order_direction_select.value
status = self.__proposal_status_select.value
order_by = self.__order_by_select.value_ensure
order_direction = self.__order_direction_select.value_ensure
status = self.__proposal_status_select.value_ensure
self.post_message(self.Search(order_by, order_direction, status))
......
from __future__ import annotations
from textual.widgets import Select
from textual.widgets._select import NoSelection, SelectType
class CliveSelect(Select[SelectType]):
@property
def value_ensure(self) -> SelectType:
"""
Easier access to ensure selected value (SelectType) is returned and not SelectType | NoSelection.
Textual's Select widget has allow_blank=False parameter, but does not provide easy access to
tighten the type to SelectType. This property allows for that.
"""
assert not isinstance(self.value, NoSelection), "Value is not selected"
return self.value
......@@ -3,10 +3,8 @@ from __future__ import annotations
from abc import abstractmethod
from typing import Generic
from textual.widgets import Select
from textual.widgets._select import NoSelection
from clive.__private.abstract_class import AbstractClassMessagePump
from clive.__private.ui.widgets.clive_select import CliveSelect
from clive.models.asset import (
AssetAmount,
AssetFactory,
......@@ -15,7 +13,7 @@ from clive.models.asset import (
)
class CurrencySelectorBase(Select[AssetFactoryHolder[AssetT]], Generic[AssetT], AbstractClassMessagePump):
class CurrencySelectorBase(CliveSelect[AssetFactoryHolder[AssetT]], Generic[AssetT], AbstractClassMessagePump):
"""Base Currency Selector for operations, which require to choose type of Assets."""
def __init__(self) -> None:
......@@ -44,13 +42,6 @@ class CurrencySelectorBase(Select[AssetFactoryHolder[AssetT]], Generic[AssetT],
"""Return selectable item for given asset."""
return self._selectable[asset]
@property
def value_ensure(self) -> AssetFactoryHolder[AssetT]:
"""Returns selected asset factory."""
value = self.value
assert not isinstance(value, NoSelection), "Value should be set."
return value
@property
def asset_cls(self) -> type[AssetT]:
"""Returns selected asset type."""
......
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, Final
from textual.validation import Function, ValidationResult
from clive.__private.config import settings
from clive.__private.validators.account_name_validator import AccountNameValidator
if TYPE_CHECKING:
from clive.__private.core.profile_data import ProfileData
class SetTrackedAccountValidator(AccountNameValidator):
MAX_NUMBER_OF_TRACKED_ACCOUNTS: Final[int] = settings.get("MAX_NUMBER_OF_TRACKED_ACCOUNTS", 6)
MAX_NUMBER_OF_TRACKED_ACCOUNTS_FAILURE: Final[str] = (
f"You can only track {MAX_NUMBER_OF_TRACKED_ACCOUNTS} accounts."
)
ACCOUNT_ALREADY_TRACKED_FAILURE: ClassVar[str] = "You cannot track account while its already tracked."
def __init__(self, profile_data: ProfileData) -> None:
super().__init__()
self._profile_data = profile_data
def validate(self, value: str) -> ValidationResult:
super_result = super().validate(value)
validators = [
Function(self._validate_account_already_tracked, self.ACCOUNT_ALREADY_TRACKED_FAILURE),
Function(self._validate_max_tracked_accounts_reached, self.MAX_NUMBER_OF_TRACKED_ACCOUNTS_FAILURE),
]
return ValidationResult.merge([super_result] + [validator.validate(value) for validator in validators])
def _validate_account_already_tracked(self, value: str) -> bool:
return self._profile_data.is_account_tracked(value)
def _validate_max_tracked_accounts_reached(self, _: str) -> bool:
return len(self._profile_data.get_tracked_accounts()) < self.MAX_NUMBER_OF_TRACKED_ACCOUNTS