From ec64eea713bdd199b81a1a8a994df214e5f476c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20B=C3=A4uerle?= Date: Fri, 29 Sep 2023 03:05:49 +0200 Subject: [PATCH] feat: add a description to reports and projects (#239) Co-authored-by: Alex Cabrera --- backend/init.sql | 6 +- backend/zeno_backend/classes/project.py | 2 + backend/zeno_backend/classes/report.py | 14 + backend/zeno_backend/database/insert.py | 5 +- backend/zeno_backend/database/select.py | 86 ++++- backend/zeno_backend/database/update.py | 15 +- backend/zeno_backend/server.py | 17 +- .../lib/components/popups/ProjectPopup.svelte | 355 +++++++++--------- .../lib/components/popups/ReportPopup.svelte | 40 +- .../src/lib/components/project/Project.svelte | 39 +- .../src/lib/components/report/Report.svelte | 117 +++--- frontend/src/lib/zenoapi/index.ts | 1 + frontend/src/lib/zenoapi/models/Project.ts | 2 + frontend/src/lib/zenoapi/models/Report.ts | 2 + .../src/lib/zenoapi/models/ReportStats.ts | 16 + .../src/lib/zenoapi/services/ZenoService.ts | 27 +- .../(app)/(home)/[user]/reports/+page.svelte | 2 +- 17 files changed, 473 insertions(+), 273 deletions(-) create mode 100644 frontend/src/lib/zenoapi/models/ReportStats.ts diff --git a/backend/init.sql b/backend/init.sql index 9c9a7baf..2460eff1 100644 --- a/backend/init.sql +++ b/backend/init.sql @@ -12,7 +12,8 @@ CREATE TABLE projects ( data_url text, calculate_histogram_metrics boolean NOT NULL DEFAULT false, samples_per_page integer NOT NULL DEFAULT 10, - public boolean NOT NULL DEFAULT false + public boolean NOT NULL DEFAULT false, + description text NOT NULL DEFAULT '' ); CREATE TABLE charts ( @@ -27,7 +28,8 @@ CREATE TABLE reports ( id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, owner_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, - public boolean NOT NULL DEFAULT false + public boolean NOT NULL DEFAULT false, + description text NOT NULL DEFAULT '' ); CREATE TABLE report_elements ( diff --git a/backend/zeno_backend/classes/project.py b/backend/zeno_backend/classes/project.py index 32a157ef..45a15efd 100644 --- a/backend/zeno_backend/classes/project.py +++ b/backend/zeno_backend/classes/project.py @@ -22,6 +22,7 @@ class Project(CamelModel): Default True. samples_per_page (int): number of datapoints to show per page. Default 10. public (bool): whether the task is public. Default False. + description (str): description of the project. Default "". """ uuid: str @@ -34,6 +35,7 @@ class Project(CamelModel): calculate_histogram_metrics: bool = True samples_per_page: int = 10 public: bool = False + description: str = "" class ProjectStats(CamelModel): diff --git a/backend/zeno_backend/classes/report.py b/backend/zeno_backend/classes/report.py index d2ef6946..5cda913d 100644 --- a/backend/zeno_backend/classes/report.py +++ b/backend/zeno_backend/classes/report.py @@ -26,6 +26,7 @@ class Report(CamelModel): linked_projects (list[str]): all projects that can be used with the report. editor (bool): whether the current user can edit the report. public (bool): whether the report is publically visible. + description (str): description of the report. Default "". """ id: int @@ -34,6 +35,7 @@ class Report(CamelModel): linked_projects: list[str] editor: bool public: bool = False + description: str = "" class ReportElement(CamelModel): @@ -62,3 +64,15 @@ class ReportResponse(CamelModel): report: Report report_elements: list[ReportElement] + + +class ReportStats(CamelModel): + """Statistical numbers of a Zeno report. + + Attributes: + num_projects (int): number of projects that are linked to the report. + num_elements (int): number of elements in the report. + """ + + num_projects: int + num_elements: int diff --git a/backend/zeno_backend/database/insert.py b/backend/zeno_backend/database/insert.py index bab38942..25965920 100644 --- a/backend/zeno_backend/database/insert.py +++ b/backend/zeno_backend/database/insert.py @@ -62,8 +62,8 @@ def project(project_config: Project, owner_id: int): with Database() as db: db.execute( "INSERT INTO projects (uuid, name, owner_id, view, data_url, " - + "calculate_histogram_metrics, samples_per_page, public) " - + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s);", + + "calculate_histogram_metrics, samples_per_page, public, description) " + + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s);", [ project_config.uuid, project_config.name, @@ -73,6 +73,7 @@ def project(project_config: Project, owner_id: int): project_config.calculate_histogram_metrics, project_config.samples_per_page, project_config.public, + project_config.description, ], ) db.execute( diff --git a/backend/zeno_backend/database/select.py b/backend/zeno_backend/database/select.py index 6ea1c8c3..26772a21 100644 --- a/backend/zeno_backend/database/select.py +++ b/backend/zeno_backend/database/select.py @@ -10,7 +10,12 @@ from zeno_backend.classes.metadata import StringFilterRequest from zeno_backend.classes.metric import Metric from zeno_backend.classes.project import Project, ProjectState, ProjectStats -from zeno_backend.classes.report import Report, ReportElement, ReportResponse +from zeno_backend.classes.report import ( + Report, + ReportElement, + ReportResponse, + ReportStats, +) from zeno_backend.classes.slice import Slice from zeno_backend.classes.slice_finder import SQLTable from zeno_backend.classes.table import TableRequest @@ -50,7 +55,7 @@ def projects(user: User) -> list[Project]: with Database() as db: own_projects_result = db.execute_return( "SELECT uuid, name, view, data_url, calculate_histogram_metrics, " - "samples_per_page, public FROM projects WHERE owner_id = %s;", + "samples_per_page, public, description FROM projects WHERE owner_id = %s;", [user.id], ) own_projects = list( @@ -65,6 +70,7 @@ def projects(user: User) -> list[Project]: samples_per_page=project[5], editor=True, public=project[6], + description=project[7], ), own_projects_result, ) @@ -72,9 +78,9 @@ def projects(user: User) -> list[Project]: user_projects_result = db.execute_return( "SELECT p.uuid, p.name, p.owner_id, p.view, p.data_url, " - "p.calculate_histogram_metrics, p.samples_per_page, up.editor, p.public " - "FROM projects AS p JOIN user_project AS up ON p.uuid = up.project_uuid " - "WHERE up.user_id = %s;", + "p.calculate_histogram_metrics, p.samples_per_page, up.editor, p.public, " + "p.description FROM projects AS p JOIN user_project AS up " + "ON p.uuid = up.project_uuid WHERE up.user_id = %s;", [user.id], ) user_projects = [] @@ -95,14 +101,15 @@ def projects(user: User) -> list[Project]: samples_per_page=res[6], editor=res[7], public=res[8], + description=res[9], ) ) project_org_result = db.execute_return( "SELECT p.uuid, p.name, p.owner_id, p.view, p.calculate_histogram_metrics," - " p.data_url, p.samples_per_page, op.editor, p.public FROM projects AS p " - "JOIN (SELECT op.project_uuid, uo.organization_id, editor " - "FROM user_organization as uo JOIN organization_project as op" + " p.data_url, p.samples_per_page, op.editor, p.public, p.description " + "FROM projects AS p JOIN (SELECT op.project_uuid, uo.organization_id, " + "editor FROM user_organization as uo JOIN organization_project as op" " ON uo.organization_id = op.organization_id " "WHERE user_id = %s) AS op ON p.uuid = op.project_uuid;", [user.id], @@ -125,6 +132,7 @@ def projects(user: User) -> list[Project]: samples_per_page=res[6], editor=res[7], public=res[8], + description=res[9], ) ) org_projects = list( @@ -146,7 +154,7 @@ def public_projects() -> list[Project]: with Database() as db: project_result = db.execute_return( "SELECT uuid, name, owner_id, view, data_url, calculate_histogram_metrics, " - "samples_per_page FROM projects WHERE public IS TRUE;", + "samples_per_page, description FROM projects WHERE public IS TRUE;", ) projects = [] for res in project_result: @@ -166,6 +174,7 @@ def public_projects() -> list[Project]: samples_per_page=res[6], editor=False, public=True, + description=res[7], ) ) return projects @@ -182,7 +191,7 @@ def reports(user: User) -> list[Report]: """ with Database() as db: own_reports_result = db.execute_return( - "SELECT id, name, public FROM reports WHERE owner_id = %s;", + "SELECT id, name, public, description FROM reports WHERE owner_id = %s;", [user.id], ) own_reports = [] @@ -202,11 +211,12 @@ def reports(user: User) -> list[Report]: else list(map(lambda linked: str(linked[0]), linked_projects)), editor=True, public=report[2], + description=report[3], ) ) user_reports_result = db.execute_return( - "SELECT r.id, r.name, r.owner_id, ur.editor, r.public " + "SELECT r.id, r.name, r.owner_id, ur.editor, r.public, r.description " "FROM reports AS r JOIN user_report AS ur ON r.id = ur.report_id " "WHERE ur.user_id = %s;", [user.id], @@ -235,13 +245,14 @@ def reports(user: User) -> list[Report]: ), editor=res[4], public=res[5], + description=res[6], ) ) report_org_result = db.execute_return( - "SELECT r.id, r.name, r.owner_id, op.editor, r.public FROM reports AS r " - "JOIN (SELECT op.report_id, uo.organization_id, op.editor " - "FROM user_organization as uo JOIN organization_report as op" + "SELECT r.id, r.name, r.owner_id, op.editor, r.public, r.description " + "FROM reports AS r JOIN (SELECT op.report_id, uo.organization_id, " + "op.editor FROM user_organization as uo JOIN organization_report as op" " ON uo.organization_id = op.organization_id " "WHERE user_id = %s) AS op ON r.id = op.report_id;", [user.id], @@ -270,6 +281,7 @@ def reports(user: User) -> list[Report]: ), editor=res[4], public=res[5], + description=res[6], ) ) org_reports = list( @@ -290,7 +302,7 @@ def public_reports() -> list[Report]: """ with Database() as db: report_result = db.execute_return( - "SELECT id, name, owner_id FROM reports WHERE public IS TRUE;", + "SELECT id, name, owner_id, description FROM reports WHERE public IS TRUE;", ) reports = [] if report_result is not None: @@ -316,6 +328,7 @@ def public_reports() -> list[Report]: ), editor=False, public=True, + description=res[3], ) ) return reports @@ -480,7 +493,7 @@ def report( owner_id = owner_id[0][0] report_result = db.execute_return( - "SELECT id, name, owner_id, public FROM reports " + "SELECT id, name, owner_id, public, description FROM reports " "WHERE name = %s AND owner_id = %s;", [report_name, owner_id], ) @@ -537,6 +550,7 @@ def report( else list(map(lambda linked: str(linked[0]), linked_projects)), editor=editor, public=bool(report_result[0][3]), + description=str(report_result[0][4]), ), report_elements=list( map( @@ -599,8 +613,8 @@ def project_from_uuid(project_uuid: str) -> Project | None: """ with Database() as db: project_result = db.execute_return( - "SELECT uuid, name, owner_id, view, " - "data_url, calculate_histogram_metrics, samples_per_page, public " + "SELECT uuid, name, owner_id, view, data_url, calculate_histogram_metrics, " + "samples_per_page, public, description " "FROM projects WHERE uuid = %s;", [project_uuid], ) @@ -623,6 +637,7 @@ def project_from_uuid(project_uuid: str) -> Project | None: if isinstance(project_result[6], int) else 10, public=bool(project_result[7]), + description=str(project_result[8]), ) @@ -637,7 +652,8 @@ def report_from_id(report_id: int) -> Report | None: """ with Database() as db: report_result = db.execute_return( - "SELECT id, name, owner_id, public FROM reports WHERE id = %s;", + "SELECT id, name, owner_id, public, description " + "FROM reports WHERE id = %s;", [report_id], ) if len(report_result) == 0: @@ -663,6 +679,7 @@ def report_from_id(report_id: int) -> Report | None: else list(map(lambda linked: str(linked[0]), linked_projects)), editor=False, public=bool(report_result[3]), + description=str(report_result[4]), ) @@ -855,6 +872,37 @@ def project_stats(project: str) -> ProjectStats | None: ) +def report_stats(report_id: int) -> ReportStats | None: + """Get statistics for a specified report. + + Args: + report_id (int): id of the report to get statistics for. + + Returns: + ReportStats | None: statistics of the specified report. + """ + with Database() as db: + num_projects = db.execute_return( + "SELECT COUNT(*) FROM report_project WHERE report_id = %s;", + [report_id], + ) + num_elements = db.execute_return( + "SELECT COUNT(*) FROM report_elements " "WHERE report_id = %s;", [report_id] + ) + return ( + ReportStats( + num_projects=num_projects[0][0] + if isinstance(num_projects[0][0], int) + else 0, + num_elements=num_elements[0][0] + if isinstance(num_elements[0][0], int) + else 0, + ) + if len(num_projects) > 0 and len(num_elements) > 0 + else None + ) + + def metrics(project_uuid: str) -> list[Metric]: """Get all metrics for a specified project. diff --git a/backend/zeno_backend/database/update.py b/backend/zeno_backend/database/update.py index 3a36f78d..542c4695 100644 --- a/backend/zeno_backend/database/update.py +++ b/backend/zeno_backend/database/update.py @@ -205,8 +205,8 @@ def project(project_config: Project): with Database() as db: db.execute( "UPDATE projects SET name = %s, calculate_histogram_metrics = %s, " - "view = %s, data_url = %s, samples_per_page = %s, public = %s " - "WHERE uuid = %s;", + "view = %s, data_url = %s, samples_per_page = %s, public = %s, " + "description = %s WHERE uuid = %s;", [ project_config.name, project_config.calculate_histogram_metrics, @@ -214,6 +214,7 @@ def project(project_config: Project): project_config.data_url, project_config.samples_per_page, project_config.public, + project_config.description, project_config.uuid, ], ) @@ -251,13 +252,9 @@ def report(report: Report): return db = Database() db.connect_execute( - "UPDATE reports SET name = %s, owner_id = %s, public = %s WHERE id = %s;", - [ - report.name, - owner_id.id, - report.public, - report.id, - ], + "UPDATE reports SET name = %s, owner_id = %s, public = %s, description = %s " + "WHERE id = %s;", + [report.name, owner_id.id, report.public, report.description, report.id], ) diff --git a/backend/zeno_backend/server.py b/backend/zeno_backend/server.py index 19e71c79..f4d46224 100644 --- a/backend/zeno_backend/server.py +++ b/backend/zeno_backend/server.py @@ -26,7 +26,12 @@ from zeno_backend.classes.metadata import HistogramBucket, StringFilterRequest from zeno_backend.classes.metric import Metric, MetricRequest from zeno_backend.classes.project import Project, ProjectState, ProjectStats -from zeno_backend.classes.report import Report, ReportElement, ReportResponse +from zeno_backend.classes.report import ( + Report, + ReportElement, + ReportResponse, + ReportStats, +) from zeno_backend.classes.slice import Slice from zeno_backend.classes.slice_finder import SliceFinderRequest, SliceFinderReturn from zeno_backend.classes.table import TableRequest @@ -150,6 +155,16 @@ def get_project_stats(project: str, request: Request): return Response(status_code=401) return select.project_stats(project) + @api_app.get( + "/report-stats/{report_id}", + response_model=ReportStats, + tags=["zeno"], + ) + def get_report_stats(report_id: int, request: Request): + if not util.report_access_valid(report_id, request): + return Response(status_code=401) + return select.report_stats(report_id) + @api_app.get( "/models/{project}", response_model=list[str], diff --git a/frontend/src/lib/components/popups/ProjectPopup.svelte b/frontend/src/lib/components/popups/ProjectPopup.svelte index cea63fb8..0977f72b 100644 --- a/frontend/src/lib/components/popups/ProjectPopup.svelte +++ b/frontend/src/lib/components/popups/ProjectPopup.svelte @@ -68,189 +68,202 @@

Project Administration

Settings

-
-
-
- +
+
+
+
+ +
+
+ +
-
+
+
+ + (config.calculateHistogramMetrics = !config.calculateHistogramMetrics)} + /> + Calculate histogram metrics +
+
+ (config.public = !config.public)} /> + Public visibility +
-
-
- (config.calculateHistogramMetrics = !config.calculateHistogramMetrics)} - /> - Calculate histogram metrics -
-
- (config.public = !config.public)} /> - Public visibility -
-
-
- {#if !config.public && userRequest} - {#await userRequest then currentUsers} -
-

Viewers

- {#if currentUsers.length > 0} - - - - - - - {#each currentUsers.sort((a, b) => { - if (a.id === user.id) return -1; - else if (b.id === user.id) return 1; - else if (a.admin && !b.admin) return -1; - else if (!a.admin && b.admin) return 1; - else return 0; - }) as member} - - - - + + {/each} + +
EmailAdmin -
- {member.name} - - - ZenoService.updateProjectUser($project.uuid, { - ...member, - admin: !member.admin - }).then(() => (userRequest = ZenoService.getProjectUsers($project.uuid)))} - disabled={member.id === user.id} - /> - - {#if member.id !== user.id} - +

Viewers

+ {#if currentUsers.length > 0} + + + + + + + {#each currentUsers.sort((a, b) => { + if (a.id === user.id) return -1; + else if (b.id === user.id) return 1; + else if (a.admin && !b.admin) return -1; + else if (!a.admin && b.admin) return 1; + else return 0; + }) as member} + + + + + + {/each} + +
EmailAdmin +
+ {member.name} + + - ZenoService.deleteProjectUser($project.uuid, member).then( + ZenoService.updateProjectUser($project.uuid, { + ...member, + admin: !member.admin + }).then( () => (userRequest = ZenoService.getProjectUsers($project.uuid)) )} + disabled={member.id === user.id} + /> + + {#if member.id !== user.id} + + ZenoService.deleteProjectUser($project.uuid, member).then( + () => (userRequest = ZenoService.getProjectUsers($project.uuid)) + )} + > + + + + + {/if} +
+ {/if} + {#await ZenoService.getUsers() then users} + {@const availableUsers = users.filter( + (currentUser) => + !( + currentUser.id === user.id || + currentUsers.some((member) => member.id === currentUser.id) + ) + )} + {#if availableUsers.length > 0} + + {/if} + {/await} + + {/await} + {/if} + {#if !config.public && organizationRequest} + {#await organizationRequest then currentOrgs} +
+

Organizations

+ {#if currentOrgs.length > 0} + + + + + + + {#each currentOrgs.sort((a, b) => { + if (a.admin && !b.admin) return -1; + else if (!a.admin && b.admin) return 1; + return a.name && b.name ? a.name.localeCompare(b.name) : 0; + }) as org} + + + + - - {/each} - -
NameAdmin +
+ {org.name} + + + ZenoService.updateProjectOrg($project.uuid, { + ...org, + admin: !org.admin + }).then( + () => + (organizationRequest = ZenoService.getProjectOrgs($project.uuid)) + )} + /> + + + ZenoService.deleteProjectOrg($project.uuid, org).then( + () => + (organizationRequest = ZenoService.getProjectOrgs($project.uuid)) + )} > - {/if} -
- {/if} - {#await ZenoService.getUsers() then users} - {@const availableUsers = users.filter( - (currentUser) => - !( - currentUser.id === user.id || - currentUsers.some((member) => member.id === currentUser.id) - ) - )} - {#if availableUsers.length > 0} - +
{/if} - {/await} -
- {/await} - {/if} - {#if !config.public && organizationRequest} - {#await organizationRequest then currentOrgs} -
-

Organizations

- {#if currentOrgs.length > 0} - - - - - - - {#each currentOrgs.sort((a, b) => { - if (a.admin && !b.admin) return -1; - else if (!a.admin && b.admin) return 1; - return a.name && b.name ? a.name.localeCompare(b.name) : 0; - }) as org} - - - - - - {/each} - -
NameAdmin -
- {org.name} - - - ZenoService.updateProjectOrg($project.uuid, { - ...org, - admin: !org.admin - }).then( - () => (organizationRequest = ZenoService.getProjectOrgs($project.uuid)) - )} - /> - - - ZenoService.deleteProjectOrg($project.uuid, org).then( - () => (organizationRequest = ZenoService.getProjectOrgs($project.uuid)) - )} - > - - - - -
- {/if} - {#await ZenoService.getOrganizationNames() then oragnizationNames} - {@const availableOrgs = oragnizationNames.filter( - (currentOrg) => !currentOrgs.some((org) => org.id === currentOrg.id) - )} - {#if availableOrgs.length > 0} - - {/if} - {/await} -
- {/await} - {/if} -
- - -
- + {#await ZenoService.getOrganizationNames() then oragnizationNames} + {@const availableOrgs = oragnizationNames.filter( + (currentOrg) => !currentOrgs.some((org) => org.id === currentOrg.id) + )} + {#if availableOrgs.length > 0} + + {/if} + {/await} +
+ {/await} + {/if} +
+ + +
+
diff --git a/frontend/src/lib/components/popups/ReportPopup.svelte b/frontend/src/lib/components/popups/ReportPopup.svelte index 2264784d..42488694 100644 --- a/frontend/src/lib/components/popups/ReportPopup.svelte +++ b/frontend/src/lib/components/popups/ReportPopup.svelte @@ -30,7 +30,7 @@ input.getElement().focus(); } - function updateProject() { + function updateReport() { ZenoService.updateReport(reportConfig).then(() => { report.set(reportConfig); dispatch('close'); @@ -42,7 +42,7 @@ dispatch('close'); } if (e.key === 'Enter') { - updateProject(); + updateReport(); } } @@ -66,23 +66,31 @@ -

Project Administration

+

Report Administration

Settings

-
-
-
- +
+
+
+
+ +
-
-
-
- (reportConfig.public = !reportConfig.public)} - /> - Public visibility +
+
+ (reportConfig.public = !reportConfig.public)} + /> + Public visibility +
+
{#if !reportConfig.public && userRequest} {#await userRequest then currentUsers} @@ -237,7 +245,7 @@ style="margin-left: 5px;" variant="outlined" disabled={invalidName} - on:click={() => updateProject()}>{'Update'} updateReport()}>{'Update'}
diff --git a/frontend/src/lib/components/project/Project.svelte b/frontend/src/lib/components/project/Project.svelte index 4e2279c1..5f25a950 100644 --- a/frontend/src/lib/components/project/Project.svelte +++ b/frontend/src/lib/components/project/Project.svelte @@ -1,6 +1,7 @@