From 7828e134df2adedda9a95432d32c2c82e03ce6c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Sun, 29 Sep 2024 22:23:45 +0200 Subject: [PATCH] fix account overview --- lnbits/core/crud.py | 11 ++- lnbits/core/extensions/models.py | 43 +++++---- lnbits/core/models.py | 4 +- lnbits/core/views/generic.py | 159 ++++++++++++++----------------- lnbits/db.py | 17 ++-- lnbits/static/js/users.js | 36 +++---- 6 files changed, 131 insertions(+), 139 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 396900323..b1752eaee 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -23,6 +23,7 @@ from lnbits.settings import ( from .models import ( Account, AccountFilters, + AccountOverview, CreatePayment, Payment, PaymentFilters, @@ -62,7 +63,7 @@ async def delete_account(user_id: str, conn: Optional[Connection] = None) -> Non async def get_accounts( filters: Optional[Filters[AccountFilters]] = None, conn: Optional[Connection] = None, -) -> Page[Account]: +) -> Page[AccountOverview]: return await (conn or db).fetch_page( """ SELECT @@ -87,7 +88,7 @@ async def get_accounts( [], {}, filters=filters, - model=Account, + model=AccountOverview, group_by=["accounts.id"], ) @@ -906,9 +907,11 @@ async def get_payments_history( raise ValueError(f"Invalid group value: {group}") values = { - "wallet": wallet_id, + "wallet_id": wallet_id, } - where = [f"wallet = :wallet AND (status = '{PaymentState.SUCCESS}' OR amount < 0)"] + where = [ + f"wallet_id = :wallet_id AND (status = '{PaymentState.SUCCESS}' OR amount < 0)" + ] transactions: list[dict] = await db.fetchall( f""" SELECT {date_trunc} date, diff --git a/lnbits/core/extensions/models.py b/lnbits/core/extensions/models.py index 4852e375b..81d8139d1 100644 --- a/lnbits/core/extensions/models.py +++ b/lnbits/core/extensions/models.py @@ -109,8 +109,8 @@ class ReleasePaymentInfo(BaseModel): class PayToEnableInfo(BaseModel): - required: Optional[bool] = False - amount: Optional[int] = None + amount: int + required: bool = False wallet: Optional[str] = None @@ -375,9 +375,12 @@ class ExtensionRelease(BaseModel): class ExtensionMeta(BaseModel): installed_release: Optional[ExtensionRelease] = None + latest_release: Optional[ExtensionRelease] = None pay_to_enable: Optional[PayToEnableInfo] = None payments: list[ReleasePaymentInfo] = [] dependencies: list[str] = [] + archive: Optional[str] = None + featured: bool = False class InstallableExtension(BaseModel): @@ -388,9 +391,6 @@ class InstallableExtension(BaseModel): short_description: Optional[str] = None icon: Optional[str] = None stars: int = 0 - featured = False - archive: Optional[str] = None - latest_release: Optional[ExtensionRelease] = None meta: Optional[ExtensionMeta] = None @property @@ -538,11 +538,15 @@ class InstallableExtension(BaseModel): def check_latest_version(self, release: Optional[ExtensionRelease]): if not release: return - if not self.latest_release: - self.latest_release = release + if not self.meta or not self.meta.latest_release: + meta = self.meta or ExtensionMeta() + meta.latest_release = release + self.meta = meta return - if version_parse(self.latest_release.version) < version_parse(release.version): - self.latest_release = release + if version_parse(self.meta.latest_release.version) < version_parse( + release.version + ): + self.meta.latest_release = release def find_existing_payment( self, pay_link: Optional[str] @@ -602,8 +606,10 @@ class InstallableExtension(BaseModel): source_repo, config.tile, ), - latest_release=ExtensionRelease.from_github_release( - source_repo, latest_release + meta=ExtensionMeta( + latest_release=ExtensionRelease.from_github_release( + source_repo, latest_release + ), ), ) except Exception as e: @@ -612,12 +618,11 @@ class InstallableExtension(BaseModel): @classmethod def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension: - meta = ExtensionMeta(dependencies=e.dependencies) + meta = ExtensionMeta(archive=e.archive, dependencies=e.dependencies) return InstallableExtension( id=e.id, name=e.name, version=e.version, - archive=e.archive, short_description=e.short_description, icon=e.icon, meta=meta, @@ -641,11 +646,13 @@ class InstallableExtension(BaseModel): existing_ext = next( (ee for ee in extension_list if ee.id == r.id), None ) - if existing_ext: - existing_ext.check_latest_version(ext.latest_release) + if existing_ext and ext.meta: + existing_ext.check_latest_version(ext.meta.latest_release) continue - ext.featured = ext.id in manifest.featured + meta = ext.meta or ExtensionMeta() + meta.featured = ext.id in manifest.featured + ext.meta = meta extension_list += [ext] extension_id_list += [ext.id] @@ -659,7 +666,9 @@ class InstallableExtension(BaseModel): continue ext = InstallableExtension.from_explicit_release(e) ext.check_latest_version(release) - ext.featured = ext.id in manifest.featured + meta = ext.meta or ExtensionMeta() + meta.featured = ext.id in manifest.featured + ext.meta = meta extension_list += [ext] extension_id_list += [e.id] except Exception as e: diff --git a/lnbits/core/models.py b/lnbits/core/models.py index 8c6403e8f..dbdc32748 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -113,7 +113,7 @@ class Account(BaseModel): username: Optional[str] = None password_hash: Optional[str] = None email: Optional[str] = None - extra: Optional[UserExtra] = None + extra: UserExtra = UserExtra() created_at: datetime = datetime.now() updated_at: datetime = datetime.now() @@ -177,7 +177,7 @@ class User(BaseModel): admin: bool = False super_user: bool = False has_password: bool = False - extra: Optional[UserExtra] = None + extra: UserExtra = UserExtra() @property def wallet_ids(self) -> list[str]: diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index b0733f6a7..4c4f0ada6 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -9,7 +9,6 @@ from fastapi.exceptions import HTTPException from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.routing import APIRouter from lnurl import decode as lnurl_decode -from loguru import logger from pydantic.types import UUID4 from lnbits.core.extensions.models import Extension, ExtensionMeta, InstallableExtension @@ -76,98 +75,84 @@ async def robots(): @generic_router.get("/extensions", name="extensions", response_class=HTMLResponse) async def extensions(request: Request, user: User = Depends(check_user_exists)): - try: - installed_exts: List[InstallableExtension] = await get_installed_extensions() - installed_exts_ids = [e.id for e in installed_exts] + installed_exts: List[InstallableExtension] = await get_installed_extensions() + installed_exts_ids = [e.id for e in installed_exts] - installable_exts = await InstallableExtension.get_installable_extensions() - installable_exts_ids = [e.id for e in installable_exts] - installable_exts += [ - e for e in installed_exts if e.id not in installable_exts_ids - ] + installable_exts = await InstallableExtension.get_installable_extensions() + installable_exts_ids = [e.id for e in installable_exts] + installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids] - for e in installable_exts: - installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None) - if installed_ext and installed_ext.meta: - installed_release = installed_ext.meta.installed_release - if installed_ext.meta.pay_to_enable and not user.admin: - # not a security leak, but better not to share the wallet id - installed_ext.meta.pay_to_enable.wallet = None - pay_to_enable = installed_ext.meta.pay_to_enable + for e in installable_exts: + installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None) + if installed_ext and installed_ext.meta: + installed_release = installed_ext.meta.installed_release + if installed_ext.meta.pay_to_enable and not user.admin: + # not a security leak, but better not to share the wallet id + installed_ext.meta.pay_to_enable.wallet = None + pay_to_enable = installed_ext.meta.pay_to_enable - if e.meta: - e.meta.installed_release = installed_release - e.meta.pay_to_enable = pay_to_enable - else: - e.meta = ExtensionMeta( - installed_release=installed_release, - pay_to_enable=pay_to_enable, - ) - # use the installed extension values - e.name = installed_ext.name - e.short_description = installed_ext.short_description - e.icon = installed_ext.icon + if e.meta: + e.meta.installed_release = installed_release + e.meta.pay_to_enable = pay_to_enable + else: + e.meta = ExtensionMeta( + installed_release=installed_release, + pay_to_enable=pay_to_enable, + ) + # use the installed extension values + e.name = installed_ext.name + e.short_description = installed_ext.short_description + e.icon = installed_ext.icon - except Exception as ex: - logger.warning(ex) - installable_exts = [] - installed_exts_ids = [] + all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()] + inactive_extensions = [e.id for e in await get_installed_extensions(active=False)] + db_version = await get_dbversions() + extensions = [ + { + "id": ext.id, + "name": ext.name, + "icon": ext.icon, + "shortDescription": ext.short_description, + "stars": ext.stars, + "isFeatured": ext.meta.featured if ext.meta else False, + "dependencies": ext.meta.dependencies if ext.meta else "", + "isInstalled": ext.id in installed_exts_ids, + "hasDatabaseTables": ext.id in db_version, + "isAvailable": ext.id in all_ext_ids, + "isAdminOnly": ext.id in settings.lnbits_admin_extensions, + "isActive": ext.id not in inactive_extensions, + "latestRelease": ( + dict(ext.meta.latest_release) + if ext.meta and ext.meta.latest_release + else None + ), + "installedRelease": ( + dict(ext.meta.installed_release) + if ext.meta and ext.meta.installed_release + else None + ), + "payToEnable": ( + dict(ext.meta.pay_to_enable) + if ext.meta and ext.meta.pay_to_enable + else {} + ), + "isPaymentRequired": ext.requires_payment, + } + for ext in installable_exts + ] - try: - all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()] - inactive_extensions = [ - e.id for e in await get_installed_extensions(active=False) - ] - db_version = await get_dbversions() - extensions = [ - { - "id": ext.id, - "name": ext.name, - "icon": ext.icon, - "shortDescription": ext.short_description, - "stars": ext.stars, - "isFeatured": ext.featured, - "dependencies": ext.meta.dependencies if ext.meta else "", - "isInstalled": ext.id in installed_exts_ids, - "hasDatabaseTables": ext.id in db_version, - "isAvailable": ext.id in all_ext_ids, - "isAdminOnly": ext.id in settings.lnbits_admin_extensions, - "isActive": ext.id not in inactive_extensions, - "latestRelease": ( - dict(ext.latest_release) if ext.latest_release else None - ), - "installedRelease": ( - dict(ext.meta.installed_release) - if ext.meta and ext.meta.installed_release - else None - ), - "payToEnable": ( - dict(ext.meta.pay_to_enable) - if ext.meta and ext.meta.pay_to_enable - else {} - ), - "isPaymentRequired": ext.requires_payment, - } - for ext in installable_exts - ] + # refresh user state. Eg: enabled extensions. + # TODO: refactor + # user = await get_user(user.id) or user - # refresh user state. Eg: enabled extensions. - # TODO: refactor - # user = await get_user(user.id) or user - - return template_renderer().TemplateResponse( - request, - "core/extensions.html", - { - "user": user.json(), - "extensions": extensions, - }, - ) - except Exception as exc: - logger.warning(exc) - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc) - ) from exc + return template_renderer().TemplateResponse( + request, + "core/extensions.html", + { + "user": user.json(), + "extensions": extensions, + }, + ) @generic_router.get( diff --git a/lnbits/db.py b/lnbits/db.py index 058fb36bd..75477364b 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -600,7 +600,6 @@ def model_to_dict(model: BaseModel) -> dict: private fields starting with _ are ignored :param model: Pydantic model """ - # TODO: no recursion, maybe make them recursive? _dict = model.dict() for key, value in _dict.items(): if key.startswith("_"): @@ -617,8 +616,6 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: :param _dict: Dictionary from database :param model: Pydantic model """ - # TODO: no recursion, maybe make them recursive? - # TODO: check why keys are sometimes not in the dict _dict: dict = {} for key, value in _row.items(): if key not in model.__fields__: @@ -628,10 +625,18 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel: continue type_ = model.__fields__[key].type_ if issubclass(type_, BaseModel) and value is not None: - if isinstance(value, str) and value == "null": - _dict[key] = None + if isinstance(value, str): + if value == "null": + _dict[key] = None + continue + _subdict = json.loads(value) + elif isinstance(value, dict): + _subdict = value + else: + logger.warning(f"Expected str or dict, got {type(value)}") continue - _dict[key] = type_.construct(**json.loads(value)) + # recursively convert nested models + _dict[key] = dict_to_model(_subdict, type_) continue _dict[key] = value return model.construct(**_dict) diff --git a/lnbits/static/js/users.js b/lnbits/static/js/users.js index 8dfc7bd77..c32427881 100644 --- a/lnbits/static/js/users.js +++ b/lnbits/static/js/users.js @@ -165,32 +165,22 @@ window.app = Vue.createApp({ type: 'bubble', options: { scales: { - xAxes: [ - { - type: 'linear', - ticks: { - beginAtZero: true - }, - scaleLabel: { - display: true, - labelString: 'Tx count' - } + x: { + type: 'linear', + beginAtZero: true, + title: { + text: 'Transaction count' } - ], - yAxes: [ - { - type: 'linear', - ticks: { - beginAtZero: true - }, - scaleLabel: { - display: true, - labelString: 'User balance in million sats' - } + }, + y: { + type: 'linear', + beginAtZero: true, + title: { + text: 'User balance in million sats' } - ] + } }, - tooltips: { + tooltip: { callbacks: { label: function (tooltipItem, data) { const dataset = data.datasets[tooltipItem.datasetIndex]