Fix dashboards
This commit is contained in:
parent
d539a72e6a
commit
5434c7d6ef
6 changed files with 330 additions and 74 deletions
|
@ -1 +1 @@
|
|||
__version__: str = '2023.4.dev51+g15fe7fa.d20240318'
|
||||
__version__: str = '2023.4.dev53+gd539a72.d20240320'
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from json import dumps
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, APIRouter, HTTPException, status, responses
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
@ -14,8 +15,8 @@ from gisaf.database import fastapi_db_session as db_session
|
|||
from gisaf.models.authentication import User
|
||||
from gisaf.models.dashboard import (
|
||||
DashboardPage, DashboardPageSection,
|
||||
DashboadPageSectionType, DashboardPage_,
|
||||
DashboardGroup, DashboardHome,
|
||||
DashboardPageMetaData,
|
||||
DashboardGroup, DashboardHome, Dashboard, DashboardSection
|
||||
)
|
||||
from gisaf.models.misc import NotADataframeError
|
||||
from gisaf.security import get_current_active_user
|
||||
|
@ -46,10 +47,12 @@ async def get_groups(
|
|||
) -> list[DashboardGroup]:
|
||||
query = select(DashboardPage)
|
||||
data = await db_session.exec(query)
|
||||
groups: dict[str, DashboardPage_] = {}
|
||||
groups: dict[str, DashboardPageMetaData] = {}
|
||||
for page in data.all():
|
||||
page_field = DashboardPage_(name=page.name, group=page.group,
|
||||
description=page.description)
|
||||
page_field = DashboardPageMetaData(name=page.name, group=page.group,
|
||||
description=page.description,
|
||||
viewable_role=page.viewable_role
|
||||
)
|
||||
group = groups.get(page.group)
|
||||
if group is None:
|
||||
group = DashboardGroup(name=page.group, pages=[page_field])
|
||||
|
@ -80,9 +83,10 @@ async def get_home() -> DashboardHome:
|
|||
async def get_dashboard_page(group: str, name: str,
|
||||
db_session: db_session,
|
||||
user: User = Depends(get_current_active_user),
|
||||
) -> DashboardPage_:
|
||||
query1 = select(DashboardPage).where((DashboardPage.name==name)
|
||||
& (DashboardPage.group==group))
|
||||
) -> Dashboard:
|
||||
query1 = select(DashboardPage).\
|
||||
options(selectinload(DashboardPage.sections)).\
|
||||
where((DashboardPage.name==name) & (DashboardPage.group==group))
|
||||
data1 = await db_session.exec(query1)
|
||||
page = data1.one_or_none()
|
||||
if not page:
|
||||
|
@ -98,7 +102,7 @@ async def get_dashboard_page(group: str, name: str,
|
|||
username = user.username if user is not None else "Anonymous"
|
||||
logger.info(f'{username} tried to access dashboard page {name}')
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
dp = DashboardPage_(
|
||||
dp = Dashboard(
|
||||
name=page.name,
|
||||
group=page.group,
|
||||
description=page.description,
|
||||
|
@ -111,7 +115,7 @@ async def get_dashboard_page(group: str, name: str,
|
|||
for p in page.expanded_panes.split(',')
|
||||
] if page.expanded_panes else [],
|
||||
sections=[
|
||||
DashboadPageSectionType(
|
||||
DashboardSection(
|
||||
name=dps.name,
|
||||
plot=dps.get_plot_url()
|
||||
)
|
||||
|
@ -126,7 +130,7 @@ async def get_dashboard_page(group: str, name: str,
|
|||
if isinstance(df, gpd.GeoDataFrame):
|
||||
gdf = pd.DataFrame(df.drop(columns=['geometry']))
|
||||
df = gdf
|
||||
dp.dfData = df.to_json(orient='table', double_precision=2)
|
||||
dp.dfData = df.reset_index().to_dict(orient='records')
|
||||
except NotADataframeError:
|
||||
logger.warning(f'Dashboard: cannot read dataframe for page {page.name}')
|
||||
except Exception as err:
|
||||
|
|
|
@ -21,7 +21,7 @@ from gisaf.models.info import (LegendItem, ModelAction, ModelInfo,
|
|||
TagActions)
|
||||
from gisaf.models.measures import MeasuresItem
|
||||
from gisaf.models.survey import Equipment, SurveyMeta, Surveyor
|
||||
from gisaf.config import Survey, conf
|
||||
from gisaf.config import conf
|
||||
from gisaf.models.bootstrap import BootstrapData
|
||||
from gisaf.models.store import Store, StoreNameOnly
|
||||
from gisaf.models.project import Project
|
||||
|
@ -29,14 +29,12 @@ from gisaf.models.authentication import UserRoleLink #, ACL
|
|||
from gisaf.database import pandas_query, fastapi_db_session as db_session
|
||||
from gisaf.security import (
|
||||
Token,
|
||||
authenticate_user, get_current_user, create_access_token,
|
||||
authenticate_user, get_current_active_user, create_access_token,
|
||||
)
|
||||
from gisaf.registry import registry, NotInRegistry
|
||||
from gisaf.custom_store_base import BaseStore
|
||||
from gisaf.redis_tools import store as redis_store
|
||||
from gisaf.models.info import (
|
||||
FeatureInfo, InfoItem, Attachment, InfoCategory
|
||||
)
|
||||
from gisaf.models.info import FeatureInfo
|
||||
from gisaf.live_utils import get_live_feature_info
|
||||
from gisaf.plugins import manager as plugin_manager, NoSuchAction
|
||||
from gisaf.utils import gisTypeSymbolMap
|
||||
|
@ -53,7 +51,7 @@ api = APIRouter(
|
|||
|
||||
@api.get('/bootstrap')
|
||||
async def bootstrap(
|
||||
user: Annotated[UserRead, Depends(get_current_user)]) -> BootstrapData:
|
||||
user: Annotated[UserRead, Depends(get_current_active_user)]) -> BootstrapData:
|
||||
return BootstrapData(user=user)
|
||||
|
||||
|
||||
|
@ -78,7 +76,7 @@ async def login_for_access_token(
|
|||
|
||||
@api.get('/logout')
|
||||
async def logout(
|
||||
user: Annotated[UserRead, Depends(get_current_user)]):
|
||||
user: Annotated[UserRead, Depends(get_current_active_user)]):
|
||||
logger.info(f'{user.username} ({user.id}) logged out')
|
||||
|
||||
|
||||
|
@ -100,7 +98,7 @@ async def get_roles(
|
|||
|
||||
@api.get('/acls')
|
||||
async def get_acls(db_session: db_session,
|
||||
user: Annotated[User, Depends(get_current_user)]) -> list[UserRoleLink]:
|
||||
user: Annotated[User, Depends(get_current_active_user)]) -> list[UserRoleLink]:
|
||||
"""New: ACLs returned as UserRoleLink"""
|
||||
if not user or not user.has_role('manager'):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
|
|
@ -3,8 +3,10 @@ from pickle import loads
|
|||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import logging
|
||||
# from typing import Any
|
||||
|
||||
from sqlmodel import Field, Relationship, String
|
||||
from matplotlib.figure import Figure
|
||||
from sqlmodel import Field, Relationship, String, JSON
|
||||
from pydantic import BaseModel
|
||||
import pandas as pd
|
||||
|
||||
|
@ -13,12 +15,10 @@ from gisaf.models.metadata import gisaf
|
|||
from gisaf.models.models_base import Model
|
||||
from gisaf.models.misc import NotADataframeError
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
except ImportError:
|
||||
plt = None # type: ignore
|
||||
|
||||
|
||||
class DashboardPageSource(Model, table=True):
|
||||
|
@ -34,17 +34,26 @@ class DashboardPageCommon:
|
|||
Base class for DashboardPage and DashboardPageSection, where some methods
|
||||
are common, eg. attachments
|
||||
"""
|
||||
name: str
|
||||
df: bytes
|
||||
plot: bytes
|
||||
#plot: dict[str, Any] | None = Field(sa_type=JSON(none_as_null=True)) # type: ignore
|
||||
attachment: str | None
|
||||
html: str | None = None
|
||||
|
||||
def get_attachment_url(self):
|
||||
## Serve through web front-end (nginx static file)
|
||||
if not self.attachment:
|
||||
return
|
||||
base_storage_url = conf.dashboard.base_storage_url
|
||||
if not base_storage_url:
|
||||
base_storage_url = '/dashboard-attachment/'
|
||||
return f'{base_storage_url}{self.group}/{self.attachment}'
|
||||
def ensure_dir_exists(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_plot(self, plot):
|
||||
def get_plot_file_path(self) -> Path:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_attachment_file_name(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_plot_file_name(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_plot(self, plot: plt.Axes | plt.Figure): # type: ignore
|
||||
"""
|
||||
Render the matplotlib plot (or figure) and save it in the filesystem
|
||||
:param plot: matplotlib plot or figure...
|
||||
|
@ -53,15 +62,14 @@ class DashboardPageCommon:
|
|||
self.ensure_dir_exists()
|
||||
|
||||
## Different types of figures supported
|
||||
fig = None
|
||||
if plt:
|
||||
if isinstance(plot, plt.Axes):
|
||||
fig = plot.figure
|
||||
elif isinstance(plot, plt.Figure):
|
||||
fig = plot
|
||||
if fig:
|
||||
fig.savefig(self.get_plot_file_path(), bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
fig: Figure | None = None
|
||||
if isinstance(plot, plt.Axes): # type: ignore
|
||||
fig = plot.figure # type: ignore
|
||||
elif isinstance(plot, plt.Figure): # type: ignore
|
||||
fig = plot
|
||||
if fig:
|
||||
fig.savefig(self.get_plot_file_path(), bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
if plot and not fig:
|
||||
logger.warning('Cannot save dashboard attachment (unknown attachment type)')
|
||||
|
@ -71,7 +79,7 @@ class DashboardPageCommon:
|
|||
#f'in {self.get_attachment_file_name()}')
|
||||
return self.get_plot_file_name()
|
||||
|
||||
def save_attachment(self, attached, name=None):
|
||||
def save_attachment(self, attached, name=None) -> str | None:
|
||||
"""
|
||||
Save the attachment in the filesystem
|
||||
:param attached: matplotlib plot or figure...
|
||||
|
@ -84,11 +92,11 @@ class DashboardPageCommon:
|
|||
self.ensure_dir_exists()
|
||||
|
||||
## Different types of figures supported
|
||||
fig = None
|
||||
fig: Figure | None = None
|
||||
if plt:
|
||||
if isinstance(attached, plt.Axes):
|
||||
fig = attached.figure
|
||||
elif isinstance(attached, plt.Figure):
|
||||
if isinstance(attached, plt.Axes): # type: ignore
|
||||
fig = attached.figure # type: ignore
|
||||
elif isinstance(attached, plt.Figure): # type: ignore
|
||||
fig = attached
|
||||
if fig:
|
||||
fig.savefig(self.get_attachment_file_name(), bbox_inches='tight')
|
||||
|
@ -96,7 +104,7 @@ class DashboardPageCommon:
|
|||
|
||||
if attached and not fig:
|
||||
logger.warning('Cannot save dashboard attachment (unknown attachment type)')
|
||||
return
|
||||
return None
|
||||
|
||||
#logger.info(f'Saved attachment of dashboard page {self.group}/{self.name} '
|
||||
#f'in {self.get_attachment_file_name()}')
|
||||
|
@ -114,10 +122,18 @@ class DashboardPageCommon:
|
|||
raise NotADataframeError()
|
||||
|
||||
def get_plot(self):
|
||||
return loads(self.plot)
|
||||
if self.plot is not None:
|
||||
return loads(self.plot)
|
||||
|
||||
|
||||
class DashboardPage(Model, DashboardPageCommon, table=True):
|
||||
class DashboardPageMetaData(BaseModel):
|
||||
name: str
|
||||
group: str
|
||||
description: str
|
||||
viewable_role: str | None = None
|
||||
|
||||
|
||||
class DashboardPage(Model, DashboardPageCommon, DashboardPageMetaData, table=True):
|
||||
__tablename__ = 'dashboard_page' # type: ignore
|
||||
__table_args__ = gisaf.table_args
|
||||
|
||||
|
@ -125,19 +141,12 @@ class DashboardPage(Model, DashboardPageCommon, table=True):
|
|||
menu = 'Dashboard'
|
||||
|
||||
id: int = Field(primary_key=True)
|
||||
name: str
|
||||
notebook: str
|
||||
group: str
|
||||
description: str
|
||||
attachment: str
|
||||
html: str
|
||||
viewable_role: str
|
||||
df: bytes
|
||||
plot: bytes
|
||||
source_id: int = Field(foreign_key=gisaf.table('dashboard_page_source.id'))
|
||||
time: datetime
|
||||
expanded_panes: str
|
||||
time: datetime | None = Field(default_factory=datetime.now)
|
||||
notebook: str | None = None
|
||||
source_id: int | None = Field(foreign_key=gisaf.table('dashboard_page_source.id'))
|
||||
expanded_panes: str | None
|
||||
source: DashboardPageSource = Relationship()
|
||||
sections: list['DashboardPageSection'] = Relationship()
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.group:s}/{self.name:s}'
|
||||
|
@ -169,6 +178,15 @@ class DashboardPage(Model, DashboardPageCommon, table=True):
|
|||
else:
|
||||
raise UserWarning('Cannot save attachment: no notebook/base_storage_dir in gisaf config')
|
||||
|
||||
def get_attachment_url(self):
|
||||
## Serve through web front-end (nginx static file)
|
||||
if not self.attachment:
|
||||
return
|
||||
base_storage_url = conf.dashboard.base_storage_url
|
||||
if not base_storage_url:
|
||||
base_storage_url = '/dashboard-attachment/'
|
||||
return f'{base_storage_url}{self.group}/{self.attachment}'
|
||||
|
||||
def get_notebook_url(self):
|
||||
if self.notebook:
|
||||
base_url = conf.dashboard.base_source_url
|
||||
|
@ -188,13 +206,9 @@ class DashboardPageSection(Model, DashboardPageCommon, table=True):
|
|||
id: str = Field(primary_key=True)
|
||||
name: str
|
||||
dashboard_page_id: int = Field(foreign_key=gisaf.table('dashboard_page.id'))
|
||||
dashboard_page: DashboardPage = Relationship()
|
||||
dashboard_page: DashboardPage = Relationship(back_populates='sections')
|
||||
|
||||
description: str
|
||||
attachment: str
|
||||
html: str
|
||||
df: bytes
|
||||
plot: str
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name} for dashboard page #{self.dashboard_page_id}'
|
||||
|
@ -266,28 +280,28 @@ class Widget(Model, table=True):
|
|||
menu = 'Dashboard'
|
||||
|
||||
|
||||
class DashboadPageSectionType(BaseModel):
|
||||
class DashboardSection(BaseModel):
|
||||
name: str
|
||||
plot: str
|
||||
|
||||
|
||||
class DashboardPage_(BaseModel):
|
||||
class Dashboard(BaseModel):
|
||||
name: str
|
||||
group: str
|
||||
description: str
|
||||
time: datetime | None = Field(default_factory=datetime.now)
|
||||
html: str | None = None
|
||||
attachment: str | None = None
|
||||
dfData: str | None = None
|
||||
dfData: list = []
|
||||
plotData: str | None = None
|
||||
notebook: str | None = None
|
||||
expandedPanes: list[str] | None = None
|
||||
sections: list[DashboadPageSectionType] | None = None
|
||||
sections: list[DashboardSection] | None = None
|
||||
|
||||
|
||||
class DashboardGroup(BaseModel):
|
||||
name: str
|
||||
pages: list[DashboardPage_]
|
||||
pages: list[DashboardPageMetaData]
|
||||
|
||||
|
||||
class DashboardHome(BaseModel):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue