fix account overview

This commit is contained in:
dni ⚡ 2024-09-29 22:23:45 +02:00 committed by Vlad Stan
parent cb56509850
commit 7828e134df
6 changed files with 131 additions and 139 deletions

View File

@ -23,6 +23,7 @@ from lnbits.settings import (
from .models import ( from .models import (
Account, Account,
AccountFilters, AccountFilters,
AccountOverview,
CreatePayment, CreatePayment,
Payment, Payment,
PaymentFilters, PaymentFilters,
@ -62,7 +63,7 @@ async def delete_account(user_id: str, conn: Optional[Connection] = None) -> Non
async def get_accounts( async def get_accounts(
filters: Optional[Filters[AccountFilters]] = None, filters: Optional[Filters[AccountFilters]] = None,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> Page[Account]: ) -> Page[AccountOverview]:
return await (conn or db).fetch_page( return await (conn or db).fetch_page(
""" """
SELECT SELECT
@ -87,7 +88,7 @@ async def get_accounts(
[], [],
{}, {},
filters=filters, filters=filters,
model=Account, model=AccountOverview,
group_by=["accounts.id"], group_by=["accounts.id"],
) )
@ -906,9 +907,11 @@ async def get_payments_history(
raise ValueError(f"Invalid group value: {group}") raise ValueError(f"Invalid group value: {group}")
values = { 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( transactions: list[dict] = await db.fetchall(
f""" f"""
SELECT {date_trunc} date, SELECT {date_trunc} date,

View File

@ -109,8 +109,8 @@ class ReleasePaymentInfo(BaseModel):
class PayToEnableInfo(BaseModel): class PayToEnableInfo(BaseModel):
required: Optional[bool] = False amount: int
amount: Optional[int] = None required: bool = False
wallet: Optional[str] = None wallet: Optional[str] = None
@ -375,9 +375,12 @@ class ExtensionRelease(BaseModel):
class ExtensionMeta(BaseModel): class ExtensionMeta(BaseModel):
installed_release: Optional[ExtensionRelease] = None installed_release: Optional[ExtensionRelease] = None
latest_release: Optional[ExtensionRelease] = None
pay_to_enable: Optional[PayToEnableInfo] = None pay_to_enable: Optional[PayToEnableInfo] = None
payments: list[ReleasePaymentInfo] = [] payments: list[ReleasePaymentInfo] = []
dependencies: list[str] = [] dependencies: list[str] = []
archive: Optional[str] = None
featured: bool = False
class InstallableExtension(BaseModel): class InstallableExtension(BaseModel):
@ -388,9 +391,6 @@ class InstallableExtension(BaseModel):
short_description: Optional[str] = None short_description: Optional[str] = None
icon: Optional[str] = None icon: Optional[str] = None
stars: int = 0 stars: int = 0
featured = False
archive: Optional[str] = None
latest_release: Optional[ExtensionRelease] = None
meta: Optional[ExtensionMeta] = None meta: Optional[ExtensionMeta] = None
@property @property
@ -538,11 +538,15 @@ class InstallableExtension(BaseModel):
def check_latest_version(self, release: Optional[ExtensionRelease]): def check_latest_version(self, release: Optional[ExtensionRelease]):
if not release: if not release:
return return
if not self.latest_release: if not self.meta or not self.meta.latest_release:
self.latest_release = release meta = self.meta or ExtensionMeta()
meta.latest_release = release
self.meta = meta
return return
if version_parse(self.latest_release.version) < version_parse(release.version): if version_parse(self.meta.latest_release.version) < version_parse(
self.latest_release = release release.version
):
self.meta.latest_release = release
def find_existing_payment( def find_existing_payment(
self, pay_link: Optional[str] self, pay_link: Optional[str]
@ -602,9 +606,11 @@ class InstallableExtension(BaseModel):
source_repo, source_repo,
config.tile, config.tile,
), ),
meta=ExtensionMeta(
latest_release=ExtensionRelease.from_github_release( latest_release=ExtensionRelease.from_github_release(
source_repo, latest_release source_repo, latest_release
), ),
),
) )
except Exception as e: except Exception as e:
logger.warning(e) logger.warning(e)
@ -612,12 +618,11 @@ class InstallableExtension(BaseModel):
@classmethod @classmethod
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension: def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
meta = ExtensionMeta(dependencies=e.dependencies) meta = ExtensionMeta(archive=e.archive, dependencies=e.dependencies)
return InstallableExtension( return InstallableExtension(
id=e.id, id=e.id,
name=e.name, name=e.name,
version=e.version, version=e.version,
archive=e.archive,
short_description=e.short_description, short_description=e.short_description,
icon=e.icon, icon=e.icon,
meta=meta, meta=meta,
@ -641,11 +646,13 @@ class InstallableExtension(BaseModel):
existing_ext = next( existing_ext = next(
(ee for ee in extension_list if ee.id == r.id), None (ee for ee in extension_list if ee.id == r.id), None
) )
if existing_ext: if existing_ext and ext.meta:
existing_ext.check_latest_version(ext.latest_release) existing_ext.check_latest_version(ext.meta.latest_release)
continue 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_list += [ext]
extension_id_list += [ext.id] extension_id_list += [ext.id]
@ -659,7 +666,9 @@ class InstallableExtension(BaseModel):
continue continue
ext = InstallableExtension.from_explicit_release(e) ext = InstallableExtension.from_explicit_release(e)
ext.check_latest_version(release) 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_list += [ext]
extension_id_list += [e.id] extension_id_list += [e.id]
except Exception as e: except Exception as e:

View File

@ -113,7 +113,7 @@ class Account(BaseModel):
username: Optional[str] = None username: Optional[str] = None
password_hash: Optional[str] = None password_hash: Optional[str] = None
email: Optional[str] = None email: Optional[str] = None
extra: Optional[UserExtra] = None extra: UserExtra = UserExtra()
created_at: datetime = datetime.now() created_at: datetime = datetime.now()
updated_at: datetime = datetime.now() updated_at: datetime = datetime.now()
@ -177,7 +177,7 @@ class User(BaseModel):
admin: bool = False admin: bool = False
super_user: bool = False super_user: bool = False
has_password: bool = False has_password: bool = False
extra: Optional[UserExtra] = None extra: UserExtra = UserExtra()
@property @property
def wallet_ids(self) -> list[str]: def wallet_ids(self) -> list[str]:

View File

@ -9,7 +9,6 @@ from fastapi.exceptions import HTTPException
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.routing import APIRouter from fastapi.routing import APIRouter
from lnurl import decode as lnurl_decode from lnurl import decode as lnurl_decode
from loguru import logger
from pydantic.types import UUID4 from pydantic.types import UUID4
from lnbits.core.extensions.models import Extension, ExtensionMeta, InstallableExtension from lnbits.core.extensions.models import Extension, ExtensionMeta, InstallableExtension
@ -76,15 +75,12 @@ async def robots():
@generic_router.get("/extensions", name="extensions", response_class=HTMLResponse) @generic_router.get("/extensions", name="extensions", response_class=HTMLResponse)
async def extensions(request: Request, user: User = Depends(check_user_exists)): async def extensions(request: Request, user: User = Depends(check_user_exists)):
try:
installed_exts: List[InstallableExtension] = await get_installed_extensions() installed_exts: List[InstallableExtension] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts] installed_exts_ids = [e.id for e in installed_exts]
installable_exts = await InstallableExtension.get_installable_extensions() installable_exts = await InstallableExtension.get_installable_extensions()
installable_exts_ids = [e.id for e in installable_exts] installable_exts_ids = [e.id for e in installable_exts]
installable_exts += [ installable_exts += [e for e in installed_exts if e.id not in installable_exts_ids]
e for e in installed_exts if e.id not in installable_exts_ids
]
for e in installable_exts: for e in installable_exts:
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None) installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
@ -108,16 +104,8 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
e.short_description = installed_ext.short_description e.short_description = installed_ext.short_description
e.icon = installed_ext.icon e.icon = installed_ext.icon
except Exception as ex:
logger.warning(ex)
installable_exts = []
installed_exts_ids = []
try:
all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()] all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()]
inactive_extensions = [ inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
e.id for e in await get_installed_extensions(active=False)
]
db_version = await get_dbversions() db_version = await get_dbversions()
extensions = [ extensions = [
{ {
@ -126,7 +114,7 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"icon": ext.icon, "icon": ext.icon,
"shortDescription": ext.short_description, "shortDescription": ext.short_description,
"stars": ext.stars, "stars": ext.stars,
"isFeatured": ext.featured, "isFeatured": ext.meta.featured if ext.meta else False,
"dependencies": ext.meta.dependencies if ext.meta else "", "dependencies": ext.meta.dependencies if ext.meta else "",
"isInstalled": ext.id in installed_exts_ids, "isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": ext.id in db_version, "hasDatabaseTables": ext.id in db_version,
@ -134,7 +122,9 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"isAdminOnly": ext.id in settings.lnbits_admin_extensions, "isAdminOnly": ext.id in settings.lnbits_admin_extensions,
"isActive": ext.id not in inactive_extensions, "isActive": ext.id not in inactive_extensions,
"latestRelease": ( "latestRelease": (
dict(ext.latest_release) if ext.latest_release else None dict(ext.meta.latest_release)
if ext.meta and ext.meta.latest_release
else None
), ),
"installedRelease": ( "installedRelease": (
dict(ext.meta.installed_release) dict(ext.meta.installed_release)
@ -163,11 +153,6 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"extensions": extensions, "extensions": extensions,
}, },
) )
except Exception as exc:
logger.warning(exc)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
@generic_router.get( @generic_router.get(

View File

@ -600,7 +600,6 @@ def model_to_dict(model: BaseModel) -> dict:
private fields starting with _ are ignored private fields starting with _ are ignored
:param model: Pydantic model :param model: Pydantic model
""" """
# TODO: no recursion, maybe make them recursive?
_dict = model.dict() _dict = model.dict()
for key, value in _dict.items(): for key, value in _dict.items():
if key.startswith("_"): if key.startswith("_"):
@ -617,8 +616,6 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
:param _dict: Dictionary from database :param _dict: Dictionary from database
:param model: Pydantic model :param model: Pydantic model
""" """
# TODO: no recursion, maybe make them recursive?
# TODO: check why keys are sometimes not in the dict
_dict: dict = {} _dict: dict = {}
for key, value in _row.items(): for key, value in _row.items():
if key not in model.__fields__: if key not in model.__fields__:
@ -628,10 +625,18 @@ def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
continue continue
type_ = model.__fields__[key].type_ type_ = model.__fields__[key].type_
if issubclass(type_, BaseModel) and value is not None: if issubclass(type_, BaseModel) and value is not None:
if isinstance(value, str) and value == "null": if isinstance(value, str):
if value == "null":
_dict[key] = None _dict[key] = None
continue continue
_dict[key] = type_.construct(**json.loads(value)) _subdict = json.loads(value)
elif isinstance(value, dict):
_subdict = value
else:
logger.warning(f"Expected str or dict, got {type(value)}")
continue
# recursively convert nested models
_dict[key] = dict_to_model(_subdict, type_)
continue continue
_dict[key] = value _dict[key] = value
return model.construct(**_dict) return model.construct(**_dict)

View File

@ -165,32 +165,22 @@ window.app = Vue.createApp({
type: 'bubble', type: 'bubble',
options: { options: {
scales: { scales: {
xAxes: [ x: {
{
type: 'linear', type: 'linear',
ticks: { beginAtZero: true,
beginAtZero: true title: {
text: 'Transaction count'
}
}, },
scaleLabel: { y: {
display: true,
labelString: 'Tx count'
}
}
],
yAxes: [
{
type: 'linear', type: 'linear',
ticks: { beginAtZero: true,
beginAtZero: true title: {
}, text: 'User balance in million sats'
scaleLabel: {
display: true,
labelString: 'User balance in million sats'
} }
} }
]
}, },
tooltips: { tooltip: {
callbacks: { callbacks: {
label: function (tooltipItem, data) { label: function (tooltipItem, data) {
const dataset = data.datasets[tooltipItem.datasetIndex] const dataset = data.datasets[tooltipItem.datasetIndex]