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,8 +606,10 @@ class InstallableExtension(BaseModel):
source_repo, source_repo,
config.tile, config.tile,
), ),
latest_release=ExtensionRelease.from_github_release( meta=ExtensionMeta(
source_repo, latest_release latest_release=ExtensionRelease.from_github_release(
source_repo, latest_release
),
), ),
) )
except Exception as e: except Exception as 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,98 +75,84 @@ 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)
if installed_ext and installed_ext.meta: if installed_ext and installed_ext.meta:
installed_release = installed_ext.meta.installed_release installed_release = installed_ext.meta.installed_release
if installed_ext.meta.pay_to_enable and not user.admin: if installed_ext.meta.pay_to_enable and not user.admin:
# not a security leak, but better not to share the wallet id # not a security leak, but better not to share the wallet id
installed_ext.meta.pay_to_enable.wallet = None installed_ext.meta.pay_to_enable.wallet = None
pay_to_enable = installed_ext.meta.pay_to_enable pay_to_enable = installed_ext.meta.pay_to_enable
if e.meta: if e.meta:
e.meta.installed_release = installed_release e.meta.installed_release = installed_release
e.meta.pay_to_enable = pay_to_enable e.meta.pay_to_enable = pay_to_enable
else: else:
e.meta = ExtensionMeta( e.meta = ExtensionMeta(
installed_release=installed_release, installed_release=installed_release,
pay_to_enable=pay_to_enable, pay_to_enable=pay_to_enable,
) )
# use the installed extension values # use the installed extension values
e.name = installed_ext.name e.name = installed_ext.name
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: all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()]
logger.warning(ex) inactive_extensions = [e.id for e in await get_installed_extensions(active=False)]
installable_exts = [] db_version = await get_dbversions()
installed_exts_ids = [] 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: # refresh user state. Eg: enabled extensions.
all_ext_ids = [ext.code for ext in Extension.get_valid_extensions()] # TODO: refactor
inactive_extensions = [ # user = await get_user(user.id) or user
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. return template_renderer().TemplateResponse(
# TODO: refactor request,
# user = await get_user(user.id) or user "core/extensions.html",
{
return template_renderer().TemplateResponse( "user": user.json(),
request, "extensions": extensions,
"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
@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):
_dict[key] = None 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 continue
_dict[key] = type_.construct(**json.loads(value)) # 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', beginAtZero: true,
ticks: { title: {
beginAtZero: true text: 'Transaction count'
},
scaleLabel: {
display: true,
labelString: 'Tx count'
}
} }
], },
yAxes: [ y: {
{ type: 'linear',
type: 'linear', beginAtZero: true,
ticks: { title: {
beginAtZero: true 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]