From 88f6dd99fde260b81308d22c783221bb92316658 Mon Sep 17 00:00:00 2001 From: hamza221 Date: Sun, 13 Nov 2022 20:23:07 +0100 Subject: [PATCH] Save a Single Question as csv Signed-off-by: hamza221 --- appinfo/routes.php | 16 ++ lib/Controller/ApiController.php | 65 ++++++++ lib/Db/AnswerMapper.php | 23 +++ lib/Service/SubmissionService.php | 181 +++++++++++++++++++++- src/components/Results/ResultsSummary.vue | 33 +++- src/components/Results/Submission.vue | 15 ++ 6 files changed, 322 insertions(+), 11 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index 10cb35c8e..afd549f3c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -245,6 +245,22 @@ 'apiVersion' => 'v2(\.1)?' ] ], + [ + 'name' => 'api#exportQuestion', + 'url' => '/api/{apiVersion}/submissions/exportQuestion/{questionId}', + 'verb' => 'GET', + 'requirements' => [ + 'apiVersion' => 'v2' + ] + ], + [ + 'name' => 'api#exportSubmission', + 'url' => '/api/{apiVersion}/submissions/exportSubmission/{submissionId}', + 'verb' => 'GET', + 'requirements' => [ + 'apiVersion' => 'v2' + ] + ], [ 'name' => 'api#exportSubmissionsToCloud', 'url' => '/api/{apiVersion}/submissions/export', diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 673b2a0a5..1bf5f4707 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -1143,7 +1143,72 @@ public function exportSubmissions(string $hash): DataDownloadResponse { $csv = $this->submissionService->getSubmissionsCsv($hash); return new DataDownloadResponse($csv['data'], $csv['fileName'], 'text/csv'); } + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * Export submissions of a specified Question + * + * @param int $questionId of the question + * @return DataDownloadResponse + * @throws OCSBadRequestException + * @throws OCSForbiddenException + */ + public function exportQuestion(int $questionId): DataDownloadResponse { + $this->logger->debug('Export submissions for Question: {questionId}', [ + 'questionId' => $questionId, + ]); + + try { + $question = $this->questionMapper->findById($questionId); + $formId = $question->getFormId(); + $form = $this->formMapper->findById($formId); + } catch (IMapperException $e) { + $this->logger->debug('Could not find question'); + throw new OCSBadRequestException(); + } + + if ($form->getOwnerId() !== $this->currentUser->getUID()) { + $this->logger->debug('This form is not owned by the current user'); + throw new OCSForbiddenException(); + } + $csv = $this->submissionService->getQuestionCsv($formId, $questionId); + return new DataDownloadResponse($csv['data'], $csv['fileName'], 'text/csv'); + } + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * Export a single submission + * + * @param int $submissionId of the submission + * @return DataDownloadResponse + * @throws OCSBadRequestException + * @throws OCSForbiddenException + */ + public function exportSubmission(int $submissionId): DataDownloadResponse { + $this->logger->debug('Export submission: {submissionId}', [ + 'submissionId' => $submissionId, + ]); + + try { + $submission = $this->submissionMapper->findById($submissionId); + $formId = $submission->getFormId(); + $form = $this->formMapper->findById($formId); + } catch (IMapperException $e) { + $this->logger->debug('Could not find submission'); + throw new OCSBadRequestException(); + } + + if (!$this->formsService->canSeeResults($form->id)) { + $this->logger->debug('The current user has no permission to get the results for this form'); + throw new OCSForbiddenException(); + } + + $csv = $this->submissionService->getSubmissionCsv($formId, $submissionId); + return new DataDownloadResponse($csv['data'], $csv['fileName'], 'text/csv'); + } /** * @CORS * @NoAdminRequired diff --git a/lib/Db/AnswerMapper.php b/lib/Db/AnswerMapper.php index 710f95818..506996e7e 100644 --- a/lib/Db/AnswerMapper.php +++ b/lib/Db/AnswerMapper.php @@ -60,6 +60,29 @@ public function findBySubmission(int $submissionId): array { return $this->findEntities($qb); } + /** + * @param int $submissionId + * @param int $questionId + * @throws \OCP\AppFramework\Db\DoesNotExistException if not found + * @return Answer[] + */ + + public function findBySubmissionAndQuestionId(int $submissionId, int $questionId): array { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('submission_id', $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $qb->expr()->eq('question_id', $qb->createNamedParameter($questionId, IQueryBuilder::PARAM_INT)) + + ); + + return $this->findEntities($qb); + } + /** * @param int $submissionId */ diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php index 5a4980b55..d337a5742 100644 --- a/lib/Service/SubmissionService.php +++ b/lib/Service/SubmissionService.php @@ -38,11 +38,11 @@ use OCA\Forms\Db\FormMapper; use OCA\Forms\Db\QuestionMapper; use OCA\Forms\Db\SubmissionMapper; -use OCP\AppFramework\Db\DoesNotExistException; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; use OCP\IConfig; +use OCP\IDateTimeFormatter; use OCP\IL10N; use OCP\IUser; @@ -89,6 +89,7 @@ public function __construct(FormMapper $formMapper, AnswerMapper $answerMapper, IRootFolder $storage, IConfig $config, + IDateTimeFormatter $dateTimeFormatter, IL10N $l10n, LoggerInterface $logger, IUserManager $userManager, @@ -99,6 +100,7 @@ public function __construct(FormMapper $formMapper, $this->answerMapper = $answerMapper; $this->storage = $storage; $this->config = $config; + $this->dateTimeFormatter = $dateTimeFormatter; $this->l10n = $l10n; $this->logger = $logger; $this->userManager = $userManager; @@ -156,7 +158,6 @@ public function getSubmissions(int $formId): array { * @throws NotPermittedException */ public function writeCsvToCloud(string $hash, string $path): string { - /** @var \OCP\Files\Folder|\OCP\Files\File $node */ $node = $this->storage->getUserFolder($this->currentUser->getUID())->get($path); // Get Data @@ -167,17 +168,14 @@ public function writeCsvToCloud(string $hash, string $path): string { if ($node->getExtension() === 'csv') { $csvData['fileName'] = $node->getName(); } - /** @var \OCP\Files\Folder $node */ $node = $node->getParent(); } // check if file exists, create otherwise. try { - /** @var \OCP\Files\File $file */ $file = $node->get($csvData['fileName']); } catch (\OCP\Files\NotFoundException $e) { $node->newFile($csvData['fileName']); - /** @var \OCP\Files\File $file */ $file = $node->get($csvData['fileName']); } @@ -232,9 +230,9 @@ public function getSubmissionsCsv(string $hash): array { $row[] = $user->getUID(); $row[] = $user->getDisplayName(); } - + // Date - $row[] = date_format(date_timestamp_set(new DateTime(), $submission->getTimestamp())->setTimezone(new DateTimeZone($userTimezone)), 'c'); + $row[] = $this->dateTimeFormatter->formatDateTime($submission->getTimestamp(), 'full', 'full', new DateTimeZone($userTimezone), $this->l10n); // Answers, make sure we keep the question order $answers = array_reduce($this->answerMapper->findBySubmission($submission->getId()), function (array $carry, Answer $answer) { @@ -269,7 +267,174 @@ public function getSubmissionsCsv(string $hash): array { 'data' => $this->array2csv($header, $data), ]; } - + /** + * Create Question specific CSV from Submissions to form + * @param string $formId id of the form + * @param string $questionId id of the question + * @return array{fileName:string,data:string} Array with 'fileName' and 'data' + */ + + public function getQuestionCsv(int $formId, int $questionId): array { + $form = $this->formMapper->findByid($formId); + + try { + $submissionEntities = $this->submissionMapper->findByForm($form->getId()); + } catch (DoesNotExistException $e) { + // Just ignore, if no Data. Returns empty Submissions-Array + } + + $question = $this->questionMapper->findById($questionId); + $defaultTimeZone = date_default_timezone_get(); + $userTimezone = $this->config->getUserValue($this->currentUser->getUID(), 'core', 'timezone', $defaultTimeZone); + + // Process initial header + $header = []; + $header[] = $this->l10n->t('User ID'); + $header[] = $this->l10n->t('User display name'); + $header[] = $this->l10n->t('Timestamp'); + $header[] = $question->getText(); + + + // Init dataset + $data = []; + + + // Process each answers + foreach ($submissionEntities as $submission) { + $currentSubmissionAnswers = $this->answerMapper->findBySubmissionAndQuestionId($submission->getId(), $questionId); + $row = []; + + // User + $user = $this->userManager->get($submission->getUserId()); + if ($user === null) { + // Give empty userId + $row[] = ''; + // TRANSLATORS Shown on export if no Display-Name is available. + $row[] = $this->l10n->t('Anonymous user'); + } else { + $row[] = $user->getUID(); + $row[] = $user->getDisplayName(); + } + + // Date + $row[] = $this->dateTimeFormatter->formatDateTime($submission->getTimestamp(), 'full', 'full', new DateTimeZone($userTimezone), $this->l10n); + + // Answers, make sure we keep the question order + $answers = array_reduce($currentSubmissionAnswers, function (array $carry, Answer $answer) { + $questionId = $answer->getQuestionId(); + + // If key exists, insert separator + if (key_exists($questionId, $carry)) { + $carry[$questionId] .= '; ' . $answer->getText(); + } else { + $carry[$questionId] = $answer->getText(); + } + + return $carry; + }, []); + + + $row[] = key_exists($question->getId(), $answers) + ? $answers[$question->getId()] + : null; + + + $data[] = $row; + } + + // TRANSLATORS Appendix for CSV-Export: 'Form Title (responses).csv' + $fileName = $form->getTitle() . ' (' . $this->l10n->t('responses') . ').csv'; + + return [ + 'fileName' => $fileName, + 'data' => $this->array2csv($header, $data), + ]; + } + + /** + * Create a submission Csv + * @param string $formId id of the form + * @param string $questionId id of the question + * @return array{fileName:string,data:string} Array with 'fileName' and 'data' + */ + + public function getSubmissionCsv(int $formId, int $submissionId): array { + + try { + $submission = $this->submissionMapper->findById($submissionId); + $form = $this->formMapper->findById($formId); + + } catch (DoesNotExistException $e) { + // Just ignore, if no Data. Returns empty Submissions-Array + } + + $questions = $this->questionMapper->findByForm($form->getId()); + $defaultTimeZone = date_default_timezone_get(); + $userTimezone = $this->config->getUserValue($this->currentUser->getUID(), 'core', 'timezone', $defaultTimeZone); + + // Process initial header + $header = []; + $header[] = $this->l10n->t('User ID'); + $header[] = $this->l10n->t('User display name'); + $header[] = $this->l10n->t('Timestamp'); + foreach ($questions as $question) { + $header[] = $question->getText(); + } + + // Init dataset + $data = []; + + // Process each answers + + $row = []; + + // User + $user = $this->userManager->get($submission->getUserId()); + if ($user === null) { + // Give empty userId + $row[] = ''; + // TRANSLATORS Shown on export if no Display-Name is available. + $row[] = $this->l10n->t('Anonymous user'); + } else { + $row[] = $user->getUID(); + $row[] = $user->getDisplayName(); + } + + // Date + $row[] = $this->dateTimeFormatter->formatDateTime($submission->getTimestamp(), 'full', 'full', new DateTimeZone($userTimezone), $this->l10n); + + // Answers, make sure we keep the question order + $answers = array_reduce($this->answerMapper->findBySubmission($submission->getId()), function (array $carry, Answer $answer) { + $questionId = $answer->getQuestionId(); + + // If key exists, insert separator + if (key_exists($questionId, $carry)) { + $carry[$questionId] .= '; ' . $answer->getText(); + } else { + $carry[$questionId] = $answer->getText(); + } + + return $carry; + }, []); + + foreach ($questions as $question) { + $row[] = key_exists($question->getId(), $answers) + ? $answers[$question->getId()] + : null; + } + + $data[] = $row; + + + // TRANSLATORS Appendix for CSV-Export: 'Form Title (responses).csv' + $fileName = $form->getTitle() . ' (' . $this->l10n->t('responses') . ').csv'; + + return [ + 'fileName' => $fileName, + 'data' => $this->array2csv($header, $data), + ]; + } + /** * Convert an array to a csv string * @param array $array diff --git a/src/components/Results/ResultsSummary.vue b/src/components/Results/ResultsSummary.vue index fc1279c04..ec7e3521c 100644 --- a/src/components/Results/ResultsSummary.vue +++ b/src/components/Results/ResultsSummary.vue @@ -22,9 +22,20 @@