From 87ac81f217bea03e5948d0c5df04d314aeb764d8 Mon Sep 17 00:00:00 2001 From: Rello Date: Tue, 2 Sep 2025 16:08:03 +0700 Subject: [PATCH 1/3] Add OpenAPI specification for file import endpoint --- CHANGELOG.md | 2 + appinfo/openapi.json | 59 +++++++++++++++++++++++++++ appinfo/routes.php | 4 +- lib/AppInfo/Application.php | 9 ++-- lib/Capabilities.php | 32 +++++++++++++++ lib/Controller/ApiController.php | 70 ++++++++++++++++++++++++++++++++ lib/Service/ReportService.php | 49 ++++++++++++++-------- 7 files changed, 204 insertions(+), 21 deletions(-) create mode 100644 appinfo/openapi.json create mode 100644 lib/Capabilities.php create mode 100644 lib/Controller/ApiController.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d00b2a1b..e70a14bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added - Cache report data in the browser (using ETag & If-None-Match) #535 - Warn about unsaved changes before leaving a report +- OCS endpoint to create reports from CSV files via context menu +- OpenAPI documentation for report creation OCS endpoint ### Fixed - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) diff --git a/appinfo/openapi.json b/appinfo/openapi.json new file mode 100644 index 00000000..479f6251 --- /dev/null +++ b/appinfo/openapi.json @@ -0,0 +1,59 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Analytics OCS API", + "version": "1.0.0" + }, + "paths": { + "/ocs/v2.php/apps/analytics/createFromDataFile": { + "post": { + "summary": "Create analytics report from a data file", + "operationId": "createFromDataFile", + "tags": ["analytics"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "fileId": { + "type": "integer", + "description": "ID of the file to import" + } + }, + "required": ["fileId"] + } + } + } + }, + "responses": { + "200": { + "description": "Report created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "version": {"type": "number"}, + "root": { + "type": "object", + "properties": { + "orientation": {"type": "string"}, + "rows": { + "type": "array", + "items": {"type": "object"} + } + } + } + } + } + } + } + }, + "404": {"description": "File not found"} + } + } + } + } +} diff --git a/appinfo/routes.php b/appinfo/routes.php index 369aba94..03b7efdb 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -145,6 +145,6 @@ // whatsnew ['name' => 'whatsNew#get', 'url' => '/whatsnew', 'verb' => 'GET'], - ['name' => 'whatsNew#dismiss', 'url' => '/whatsnew', 'verb' => 'POST'], - ] + ['name' => 'whatsNew#dismiss', 'url' => '/whatsnew', 'verb' => 'POST'], + ] ]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index bb696157..b82a6e41 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -17,6 +17,7 @@ use OCA\Analytics\Search\SearchProvider; use OCA\Analytics\Listener\ReferenceListener; use OCA\Analytics\Reference\ReferenceProvider; +use OCA\Analytics\Capabilities; use OCA\ShareReview\Sources\SourceEvent; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -36,10 +37,12 @@ public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); } - public function register(IRegistrationContext $context): void { - $context->registerDashboardWidget(Widget::class); + public function register(IRegistrationContext $context): void { + $context->registerDashboardWidget(Widget::class); - $context->registerSearchProvider(SearchProvider::class); + $context->registerSearchProvider(SearchProvider::class); + + $context->registerCapability(Capabilities::class); // file actions are not working at the moment // $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScripts::class); diff --git a/lib/Capabilities.php b/lib/Capabilities.php new file mode 100644 index 00000000..17ff1f42 --- /dev/null +++ b/lib/Capabilities.php @@ -0,0 +1,32 @@ + [ + 'hooks' => [ + [ + 'type' => 'context-menu', + 'endpoints' => [ + [ + 'name' => 'Show data in Analytics', + 'url' => '/ocs/v2.php/apps/analytics/createFromDataFile', + 'filter' => 'text/csv', + ], + ], + ], + ], + ], + ]; + } +} diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php new file mode 100644 index 00000000..4acd3048 --- /dev/null +++ b/lib/Controller/ApiController.php @@ -0,0 +1,70 @@ +reportService = $reportService; + } + + #[ApiRoute(verb: 'POST', url: '/createFromDataFile')] + /** + * Create an analytics report from an existing data file. + * + * @param int $fileId ID of the file to import + * + * @return JSONResponse HTTP 200 with a link to the created report + * + * @OA\Post( + * path="/createFromDataFile", + * summary="Create report from data file", + * requestBody=@OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"fileId"}, + * @OA\Property(property="fileId", type="integer", description="File identifier") + * ) + * ), + * @OA\Response(response="200", description="Report created"), + * @OA\Response(response="404", description="File not found") + * ) + * + * @NoAdminRequired + * @NoCSRFRequired + */ + public function createFromDataFile(int $fileId): JSONResponse { + $reportId = $this->reportService->createFromDataFile($fileId); + $url = '/apps/analytics/r/' . $reportId; + return new JSONResponse([ + 'version' => 0.1, + 'root' => [ + 'orientation' => 'vertical', + 'rows' => [ + [ + 'children' => [ + [ + 'element' => 'Analytics report created', + 'text' => $url, + ], + ], + ], + ], + ], + ]); + } +} diff --git a/lib/Service/ReportService.php b/lib/Service/ReportService.php index 9ddb8019..e4f31749 100644 --- a/lib/Service/ReportService.php +++ b/lib/Service/ReportService.php @@ -234,22 +234,39 @@ public function createCopy(int $reportId, $chartoptions, $dataoptions, $filterop * @param string $file * @return int */ - public function createFromDataFile($file = '') { - $this->ActivityManager->triggerEvent(0, ActivityManager::OBJECT_REPORT, ActivityManager::SUBJECT_REPORT_ADD); - - if ($file !== '') { - $name = explode('.', end(explode('/', $file)))[0]; - $subheader = $file; - $parent = 0; - $dataset = 0; - $type = DatasourceController::DATASET_TYPE_LOCAL_CSV; - $link = $file; - $visualization = 'table'; - $chart = 'line'; - $reportId = $this->ReportMapper->create($name, $subheader, $parent, $type, $dataset, $link, $visualization, $chart, '', '', ''); - } - return $reportId; - } + /** + * Create a report based on a data file. + * + * @param int|string $file Path to the file or the numeric file ID + * + * @return int ID of the newly created report + */ + public function createFromDataFile($file = '') { + $this->ActivityManager->triggerEvent(0, ActivityManager::OBJECT_REPORT, ActivityManager::SUBJECT_REPORT_ADD); + + $reportId = 0; + + if ($file !== '') { + if (is_numeric($file)) { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + $nodes = $userFolder->getById((int)$file); + if (isset($nodes[0])) { + $file = $userFolder->getRelativePath($nodes[0]->getPath()); + } + } + + $name = explode('.', end(explode('/', $file)))[0]; + $subheader = $file; + $parent = 0; + $dataset = 0; + $type = DatasourceController::DATASET_TYPE_LOCAL_CSV; + $link = $file; + $visualization = 'table'; + $chart = 'line'; + $reportId = $this->ReportMapper->create($name, $subheader, $parent, $type, $dataset, $link, $visualization, $chart, '', '', ''); + } + return $reportId; + } /** * update report details From 68db7be1bf2f03569aa60d45b043125a9c298d1d Mon Sep 17 00:00:00 2001 From: Rello Date: Sat, 6 Sep 2025 10:28:57 +0700 Subject: [PATCH 2/3] finetuning --- CHANGELOG.md | 3 +- appinfo/routes.php | 8 +-- js/sidebar.js | 1 + lib/Capabilities.php | 45 ++++++++------ lib/Controller/ApiController.php | 93 ++++++++++++---------------- appinfo/openapi.json => openapi.json | 0 6 files changed, 73 insertions(+), 77 deletions(-) rename appinfo/openapi.json => openapi.json (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70a14bd..4e2f5a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,7 @@ ### Added - Cache report data in the browser (using ETag & If-None-Match) #535 - Warn about unsaved changes before leaving a report -- OCS endpoint to create reports from CSV files via context menu -- OpenAPI documentation for report creation OCS endpoint +- OCS endpoint to create reports from CSV files via files clients ### Fixed - Fix PHP 8.4 deprecation warnings #534 @[robertoschwald](https://github.com/robertoschwald) diff --git a/appinfo/routes.php b/appinfo/routes.php index 03b7efdb..82de7506 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -25,7 +25,7 @@ ['name' => 'report#update', 'url' => '/report/{reportId}', 'verb' => 'PUT'], ['name' => 'report#rename', 'url' => '/report/{reportId}/rename', 'verb' => 'PUT'], ['name' => 'report#createCopy', 'url' => '/report/copy', 'verb' => 'POST'], - ['name' => 'report#createFromDataFile', 'url' => '/report/file', 'verb' => 'POST'], + //['name' => 'report#createFromDataFile', 'url' => '/report/file/{fileId}', 'verb' => 'POST'], ['name' => 'report#updateOptions', 'url' => '/report/{reportId}/options', 'verb' => 'POST'], ['name' => 'report#updateRefresh', 'url' => '/report/{reportId}/refresh', 'verb' => 'POST'], ['name' => 'report#updateGroup', 'url' => '/report/{reportId}/group', 'verb' => 'POST'], @@ -145,6 +145,6 @@ // whatsnew ['name' => 'whatsNew#get', 'url' => '/whatsnew', 'verb' => 'GET'], - ['name' => 'whatsNew#dismiss', 'url' => '/whatsnew', 'verb' => 'POST'], - ] -]; + ['name' => 'whatsNew#dismiss', 'url' => '/whatsnew', 'verb' => 'POST'], + ] +]; \ No newline at end of file diff --git a/js/sidebar.js b/js/sidebar.js index 5112697b..a190e37e 100644 --- a/js/sidebar.js +++ b/js/sidebar.js @@ -608,6 +608,7 @@ OCA.Analytics.Sidebar.Report = { }); }, + // not used; rework to be done as the fileId is expected now createFromDataFile: function (file = '') { let requestUrl = OC.generateUrl('apps/analytics/report/file'); fetch(requestUrl, { diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 17ff1f42..aeb4def2 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -9,24 +9,31 @@ namespace OCA\Analytics; use OCP\Capabilities\ICapability; +use OCA\Analytics\AppInfo\Application; +use OCP\IL10N; class Capabilities implements ICapability { - public function getCapabilities() { - return [ - 'declarativeui' => [ - 'hooks' => [ - [ - 'type' => 'context-menu', - 'endpoints' => [ - [ - 'name' => 'Show data in Analytics', - 'url' => '/ocs/v2.php/apps/analytics/createFromDataFile', - 'filter' => 'text/csv', - ], - ], - ], - ], - ], - ]; - } -} + + public function __construct(IL10N $l10n) { + $this->l10n = $l10n; + } + + public function getCapabilities() { + return [ + 'declarativeui' => [ + Application::APP_ID => [ + 'context-menu' => [ + [ + 'name' => $this->l10n->t('Show data in Analytics'), + 'url' => '/ocs/v2.php/apps/analytics/createFromDataFile?fileId={fileId}', + 'method' => 'POST', + 'mimetype_filters' => 'text/csv', + 'bodyParams' => [], + 'icon' => '/apps/analytics/img/app.svg' + ], + ], + ], + ], + ]; + } +} \ No newline at end of file diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 4acd3048..b7dffbcf 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -9,62 +9,51 @@ namespace OCA\Analytics\Controller; use OCA\Analytics\Service\ReportService; -use OCP\AppFramework\OCSController; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\JSONResponse; -use OCP\AppFramework\OCS\Attribute\ApiRoute; +use OCP\AppFramework\OCSController; use OCP\IRequest; class ApiController extends OCSController { - private ReportService $reportService; + private ReportService $reportService; - public function __construct(string $appName, IRequest $request, ReportService $reportService) { - parent::__construct($appName, $request, 'POST'); - $this->reportService = $reportService; - } + public function __construct(string $appName, IRequest $request, ReportService $reportService) { + parent::__construct($appName, $request); + $this->reportService = $reportService; + } - #[ApiRoute(verb: 'POST', url: '/createFromDataFile')] - /** - * Create an analytics report from an existing data file. - * - * @param int $fileId ID of the file to import - * - * @return JSONResponse HTTP 200 with a link to the created report - * - * @OA\Post( - * path="/createFromDataFile", - * summary="Create report from data file", - * requestBody=@OA\RequestBody( - * required=true, - * @OA\JsonContent( - * required={"fileId"}, - * @OA\Property(property="fileId", type="integer", description="File identifier") - * ) - * ), - * @OA\Response(response="200", description="Report created"), - * @OA\Response(response="404", description="File not found") - * ) - * - * @NoAdminRequired - * @NoCSRFRequired - */ - public function createFromDataFile(int $fileId): JSONResponse { - $reportId = $this->reportService->createFromDataFile($fileId); - $url = '/apps/analytics/r/' . $reportId; - return new JSONResponse([ - 'version' => 0.1, - 'root' => [ - 'orientation' => 'vertical', - 'rows' => [ - [ - 'children' => [ - [ - 'element' => 'Analytics report created', - 'text' => $url, - ], - ], - ], - ], - ], - ]); - } + /** + * Create an analytics report from an existing data file. + * + * @param int $fileId ID of the file to import + * + * @return JSONResponse HTTP 200 with a link to the created report + * + */ + #[NoAdminRequired] + #[NoCSRFRequired] + #[ApiRoute(verb: 'POST', url: '/createFromDataFile')] + public function createFromDataFile(int $fileId): JSONResponse { + $reportId = $this->reportService->createFromDataFile($fileId); + $url = '/apps/analytics/r/' . $reportId; + return new JSONResponse([ + 'version' => 0.1, + 'root' => [ + 'orientation' => 'vertical', + 'rows' => [ + [ + 'children' => [ + [ + 'element' => 'URL', + 'text' => 'Analytics report created', + 'url' => $url, + ], + ], + ], + ], + ], + ]); + } } diff --git a/appinfo/openapi.json b/openapi.json similarity index 100% rename from appinfo/openapi.json rename to openapi.json From a31661773bbe42ed7b6ed0b69ab83d66895962cd Mon Sep 17 00:00:00 2001 From: Rello Date: Mon, 15 Sep 2025 22:30:39 +0700 Subject: [PATCH 3/3] finetuning --- lib/AppInfo/Application.php | 8 ++++---- lib/Capabilities.php | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b82a6e41..34097070 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -37,12 +37,12 @@ public function __construct(array $urlParams = []) { parent::__construct(self::APP_ID, $urlParams); } - public function register(IRegistrationContext $context): void { - $context->registerDashboardWidget(Widget::class); + public function register(IRegistrationContext $context): void { + $context->registerDashboardWidget(Widget::class); - $context->registerSearchProvider(SearchProvider::class); + $context->registerSearchProvider(SearchProvider::class); - $context->registerCapability(Capabilities::class); + $context->registerCapability(Capabilities::class); // file actions are not working at the moment // $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScripts::class); diff --git a/lib/Capabilities.php b/lib/Capabilities.php index aeb4def2..79d4cbb2 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -18,13 +18,16 @@ public function __construct(IL10N $l10n) { $this->l10n = $l10n; } + /** + * Expose the endpoint to create a report from a csv file + */ public function getCapabilities() { return [ 'declarativeui' => [ Application::APP_ID => [ 'context-menu' => [ [ - 'name' => $this->l10n->t('Show data in Analytics'), + 'name' => $this->l10n->t('Visualize data in Analytics'), 'url' => '/ocs/v2.php/apps/analytics/createFromDataFile?fileId={fileId}', 'method' => 'POST', 'mimetype_filters' => 'text/csv',