From 8f7b826f6f94f8d504470d00193be3b870615db1 Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Mon, 20 Apr 2020 18:40:15 +0200 Subject: [PATCH 1/9] New Result View Signed-off-by: Jonas Rittershofer --- lib/Controller/ApiController.php | 66 ++++++++++---- lib/Db/SubmissionMapper.php | 4 +- src/components/Results/Answer.vue | 62 +++++++++++++ src/components/Results/Submission.vue | 123 ++++++++++++++++++++++++++ src/views/Results.vue | 97 ++++++-------------- 5 files changed, 265 insertions(+), 87 deletions(-) create mode 100644 src/components/Results/Answer.vue create mode 100644 src/components/Results/Submission.vue diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 6e8c494..16d1fe7 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -553,6 +553,25 @@ class ApiController extends Controller { return new Http\JSONResponse($id); } + /** + * @NoAdminRequired + */ + private function getAnswers(int $submissionId): array { + try { + $answerEntities = $this->answerMapper->findBySubmission($submissionId); + } catch (DoesNotExistException $e) { + //Just ignore, if no Data. Returns empty Answers-Array + } + + // Load Answer-Data + $answers = []; + foreach ($answerEntities as $answerEntity) { + $answers[] = $answerEntity->read(); + } + + return $answers; + } + /** * @NoAdminRequired */ @@ -569,24 +588,39 @@ class ApiController extends Controller { return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); } - $result = []; - $submissionList = $this->submissionMapper->findByForm($form->getId()); - foreach ($submissionList as $submissionEntity) { - $answerList = $this->answerMapper->findBySubmission($submissionEntity->id); - foreach ($answerList as $answerEntity) { - $answer = $answerEntity->read(); - //Temporary Adapt Data to be usable by old Results-View - $answer['userId'] = $submissionEntity->getUserId(); - - $question = $this->questionMapper->findById($answer['questionId']); - $answer['questionText'] = $question->getText(); - $answer['questionType'] = $question->getType(); - - $result[] = $answer; - } + try { + $submissionEntities = $this->submissionMapper->findByForm($form->getId()); + } catch (DoesNotExistException $e) { + //Just ignore, if no Data. Returns empty Submissions-Array } - return new Http\JSONResponse($result); + $submissions = []; + foreach ($submissionEntities as $submissionEntity) { + // Load Submission-Data & corresponding Answers + $submission = $submissionEntity->read(); + $submission['answers'] = $this->getAnswers($submission['id']); + + // Add to returned List of Submissions + $submissions[] = $submission; + } + + // Load question-texts, including deleted ones. + try { + $questionEntities = $this->questionMapper->findByForm($form->getId()); + } catch (DoesNotExistException $e) { + //handle silently + } + $questions = []; + foreach ($questionEntities as $questionEntity) { + $questions[] = $questionEntity->read(); + } + + $response = [ + 'submissions' => $submissions, + 'questions' => $questions, + ]; + + return new Http\JSONResponse($response); } /** diff --git a/lib/Db/SubmissionMapper.php b/lib/Db/SubmissionMapper.php index a8131b4..729ce42 100644 --- a/lib/Db/SubmissionMapper.php +++ b/lib/Db/SubmissionMapper.php @@ -57,7 +57,9 @@ class SubmissionMapper extends QBMapper { ->from($this->getTableName()) ->where( $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) - ); + ) + //Newest submissions first + ->orderBy('timestamp', 'DESC'); return $this->findEntities($qb); } diff --git a/src/components/Results/Answer.vue b/src/components/Results/Answer.vue new file mode 100644 index 0000000..3aa5820 --- /dev/null +++ b/src/components/Results/Answer.vue @@ -0,0 +1,62 @@ + + + + + + + diff --git a/src/components/Results/Submission.vue b/src/components/Results/Submission.vue new file mode 100644 index 0000000..aa6141d --- /dev/null +++ b/src/components/Results/Submission.vue @@ -0,0 +1,123 @@ + + + + + + + diff --git a/src/views/Results.vue b/src/views/Results.vue index e40f52f..95485e0 100644 --- a/src/views/Results.vue +++ b/src/views/Results.vue @@ -39,9 +39,9 @@

{{ t('forms', 'Responses for {title}', { title: form.title }) }}

-
+
@@ -56,11 +56,13 @@
- - + +
@@ -81,10 +83,11 @@ import { generateUrl } from '@nextcloud/router' import { showError } from '@nextcloud/dialogs' import AppContent from '@nextcloud/vue/dist/Components/AppContent' import axios from '@nextcloud/axios' -import json2csvParser from 'json2csv' +// import json2csvParser from 'json2csv' import EmptyContent from '../components/EmptyContent' -import ResultItem from '../components/resultItem' +// import ResultItem from '../components/resultItem' +import Submission from '../components/Results/Submission' import TopBar from '../components/TopBar' import ViewsMixin from '../mixins/ViewsMixin' @@ -94,7 +97,7 @@ export default { components: { AppContent, EmptyContent, - ResultItem, + Submission, TopBar, }, @@ -103,45 +106,14 @@ export default { data() { return { loadingResults: true, - answers: [], + submissions: [], + questions: [], } }, computed: { - stats() { - const sums = [] - - if (this.answers != null) { - const uniqueAns = [] - const uniqueQs = [] - const ansToQ = new Map() - for (let i = 0; i < this.answers.length; i++) { - if (this.answers[i].questionType === 'radiogroup' || this.answers[i].questionType === 'dropdown') { - if (uniqueAns.includes(this.answers[i].text) === false) { - uniqueAns.push(this.answers[i].text) - ansToQ.set(this.answers[i].text, this.answers[i].questionId) - } - if (uniqueQs.includes(this.answers[i].questionId) === false) { - uniqueQs.push(this.answers[i].questionId) - } - } - } - for (let i = 0; i < uniqueAns.length; i++) { - sums[i] = 0 - } - for (let i = 0; i < this.answers.length; i++) { - sums[uniqueAns.indexOf(this.answers[i].text)]++ - } - for (let i = 0; i < sums.length; i++) { - sums[i] = 'Question ' + ansToQ.get(uniqueAns[i]) + ': ' + (sums[i] / ((this.answers.length / uniqueQs.length)) * 100).toFixed(2) + '%' + ' of respondents voted for answer choice: ' + uniqueAns[i] - } - } - - return sums.sort() - }, - noSubmissions() { - return this.answers && this.answers.length === 0 + return this.submissions && this.submissions.length === 0 }, }, @@ -150,7 +122,6 @@ export default { }, methods: { - showEdit() { this.$router.push({ name: 'edit', @@ -168,8 +139,9 @@ export default { const response = await axios.get(generateUrl('/apps/forms/api/v1/submissions/{hash}', { hash: this.form.hash, })) - this.answers = response.data - console.debug(this.answers) + this.submissions = response.data.submissions + this.questions = response.data.questions + console.debug(this.submissions) } catch (error) { console.error(error) showError(t('forms', 'There was an error while loading results')) @@ -178,9 +150,9 @@ export default { } }, - download() { +/* download() { this.loading = true - axios.get(generateUrl('apps/forms/get/form/' + this.$route.params.hash)) + axios.get(OC.generateUrl('apps/forms/get/form/' + this.$route.params.hash)) .then((response) => { this.json2csvParser = ['userId', 'questionId', 'questionText', 'Answer'] // TODO Is this one necessary?? const formattedAns = [] @@ -202,26 +174,11 @@ export default { document.body.removeChild(element) this.loading = false }, (error) => { - /* eslint-disable-next-line no-console */ + /* eslint-disable-next-line no-console * console.log(error.response) this.loading = false }) - }, + }, */ }, } - - From bb9bca3667218879bc807974310b6bddbf51f01e Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Thu, 30 Apr 2020 09:48:15 +0200 Subject: [PATCH 2/9] Fix markup and use standard styles for Submissions view Signed-off-by: Jan-Christoph Borchardt --- src/components/Results/Submission.vue | 57 ++++++++------------------- src/views/Results.vue | 2 +- 2 files changed, 17 insertions(+), 42 deletions(-) diff --git a/src/components/Results/Submission.vue b/src/components/Results/Submission.vue index aa6141d..5cc864a 100644 --- a/src/components/Results/Submission.vue +++ b/src/components/Results/Submission.vue @@ -21,15 +21,13 @@ --> From a9400b440f4afc8515e759890c58334057b98b2e Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Fri, 1 May 2020 01:26:01 +0200 Subject: [PATCH 5/9] Delete all submissions Signed-off-by: Jonas Rittershofer --- appinfo/routes.php | 3 +++ lib/Controller/ApiController.php | 26 +++++++++++++++++++++++++ src/views/Results.vue | 33 ++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index ad1e162..e051902 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -54,7 +54,10 @@ return [ ['name' => 'api#updateOption', 'url' => '/api/v1/option/update', 'verb' => 'POST'], ['name' => 'api#deleteOption', 'url' => '/api/v1/option/{id}', 'verb' => 'DELETE'], + // Submissions ['name' => 'api#getSubmissions', 'url' => '/api/v1/submissions/{hash}', 'verb' => 'GET'], + ['name' => 'api#deleteAllSubmissions', 'url' => '/api/v1/submissions/{formId}', 'verb' => 'DELETE'], + ['name' => 'api#insertSubmission', 'url' => '/api/v1/submission/insert', 'verb' => 'POST'], ['name' => 'api#deleteSubmission', 'url' => '/api/v1/submission/{id}', 'verb' => 'DELETE'], diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 85f3190..fdb3d73 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -741,4 +741,30 @@ class ApiController extends Controller { return new Http\JSONResponse($id); } + + /** + * @NoAdminRequired + */ + public function deleteAllSubmissions(int $formId): Http\JSONResponse { + $this->logger->debug('Delete all submissions to form: {formId}', [ + 'formId' => $formId, + ]); + + try { + $form = $this->formMapper->findById($formId); + } catch (IMapperException $e) { + $this->logger->debug('Could not find form'); + return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + if ($form->getOwnerId() !== $this->userId) { + $this->logger->debug('This form is not owned by the current user'); + return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); + } + + // Delete all submissions (incl. Answers) + $this->submissionMapper->deleteByForm($formId); + + return new Http\JSONResponse($id); + } } diff --git a/src/views/Results.vue b/src/views/Results.vue index 790acc5..9028c6d 100644 --- a/src/views/Results.vue +++ b/src/views/Results.vue @@ -39,10 +39,14 @@

{{ t('forms', 'Responses for {title}', { title: form.title }) }}

- + + + {{ t('forms', 'Export to CSV') }} + + + {{ t('forms', 'Delete all Submissions') }} + +
@@ -71,6 +75,8 @@ import { generateUrl } from '@nextcloud/router' import { Parser } from 'json2csv' import { showError } from '@nextcloud/dialogs' +import Actions from '@nextcloud/vue/dist/Components/Actions' +import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import AppContent from '@nextcloud/vue/dist/Components/AppContent' import axios from '@nextcloud/axios' import moment from '@nextcloud/moment' @@ -84,6 +90,8 @@ export default { name: 'Results', components: { + Actions, + ActionButton, AppContent, EmptyContent, Submission, @@ -154,6 +162,23 @@ export default { } }, + async deleteAllSubmissions() { + if (!confirm(t('forms', 'Are you sure you want to delete all submissions from this form?'))) { + return + } + + this.loadingResults = true + try { + await axios.delete(generateUrl('/apps/forms/api/v1/submissions/{formId}', { formId: this.form.id })) + this.submissions = [] + } catch (error) { + console.error(error) + showError(t('forms', 'There was an error while removing the submissions')) + } finally { + this.loadingResults = false + } + }, + download() { this.loadingResults = true From a6f77b0c2f3fc3846d78182539e9db6d04bc0011 Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Sat, 2 May 2020 18:36:52 +0200 Subject: [PATCH 6/9] Improve new result view Signed-off-by: Jonas Rittershofer Co-authored-by: Jan-Christoph Borchardt --- src/components/Results/Answer.vue | 19 ++++----- src/components/Results/Submission.vue | 56 ++++++++++++++++++--------- src/components/TopBar.vue | 5 ++- src/views/Create.vue | 1 + src/views/Results.vue | 37 ++++++++++++++---- 5 files changed, 80 insertions(+), 38 deletions(-) diff --git a/src/components/Results/Answer.vue b/src/components/Results/Answer.vue index 3aa5820..2b6f3b5 100644 --- a/src/components/Results/Answer.vue +++ b/src/components/Results/Answer.vue @@ -21,14 +21,12 @@ --> diff --git a/src/components/TopBar.vue b/src/components/TopBar.vue index 83859cf..6dc74e2 100644 --- a/src/components/TopBar.vue +++ b/src/components/TopBar.vue @@ -51,7 +51,10 @@ $top-bar-height: 60px; button { cursor: pointer; - &:not(:first-child) { + min-height: 44px; + margin: 8px; + + &.button-small { width: 44px; height: 44px; border: none; diff --git a/src/views/Create.vue b/src/views/Create.vue index a247944..ad6d277 100644 --- a/src/views/Create.vue +++ b/src/views/Create.vue @@ -38,6 +38,7 @@ diff --git a/src/views/Results.vue b/src/views/Results.vue index 9028c6d..dfc70bf 100644 --- a/src/views/Results.vue +++ b/src/views/Results.vue @@ -39,14 +39,19 @@

{{ t('forms', 'Responses for {title}', { title: form.title }) }}

- - +
+ + + + {{ t('forms', 'Delete all responses') }} + + +
@@ -217,3 +222,21 @@ export default { }, } + + From 907fcb407edea83a3b7bda26e3ed67651fc064e4 Mon Sep 17 00:00:00 2001 From: Jonas Rittershofer Date: Sat, 2 May 2020 20:30:25 +0200 Subject: [PATCH 7/9] Use users displayname Signed-off-by: Jonas Rittershofer --- lib/Controller/ApiController.php | 30 ++++++++++++++++++++++++++- src/components/Results/Submission.vue | 5 +---- src/views/Results.vue | 2 +- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index fdb3d73..13fef69 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -42,8 +42,11 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Db\IMapperException; use OCP\AppFramework\Http; use OCP\ILogger; +use OCP\IL10N; use OCP\IRequest; +use OCP\IUser; use OCP\IUserSession; +use OCP\IUserManager; use OCP\Security\ISecureRandom; class ApiController extends Controller { @@ -67,9 +70,15 @@ class ApiController extends Controller { /** @var ILogger */ private $logger; + /** @var IL10N */ + private $l10n; + /** @var IUserSession */ private $userSession; - + + /** @var IUserManager */ + private $userManager; + /** @var FormsService */ private $formsService; @@ -77,17 +86,20 @@ class ApiController extends Controller { IRequest $request, $userId, // TODO remove & replace with userSession below. IUserSession $userSession, + IUserManager $userManager, FormMapper $formMapper, SubmissionMapper $submissionMapper, AnswerMapper $answerMapper, QuestionMapper $questionMapper, OptionMapper $optionMapper, ILogger $logger, + IL10N $l10n, FormsService $formsService) { parent::__construct($appName, $request); $this->appName = $appName; $this->userId = $userId; $this->userSession = $userSession; + $this->userManager = $userManager; $this->formMapper = $formMapper; $this->questionMapper = $questionMapper; $this->optionMapper = $optionMapper; @@ -96,6 +108,7 @@ class ApiController extends Controller { $this->questionMapper = $questionMapper; $this->optionMapper = $optionMapper; $this->logger = $logger; + $this->l10n = $l10n; $this->formsService = $formsService; } @@ -600,6 +613,21 @@ class ApiController extends Controller { $submission = $submissionEntity->read(); $submission['answers'] = $this->getAnswers($submission['id']); + // Append Display Name + if (substr($submission['userId'], 0, 10) === 'anon-user-') { + // Anonymous User + $submission['userDisplayName'] = $this->l10n->t('anonymous user'); + } else { + $userEntity = $this->userManager->get($submission['userId']); + + if ($userEntity instanceof IUser) { + $submission['userDisplayName'] = $userEntity->getDisplayName(); + } else { + // Fallback, should not occur regularly. + $submission['userDisplayName'] = $submission['userId']; + } + } + // Add to returned List of Submissions $submissions[] = $submission; } diff --git a/src/components/Results/Submission.vue b/src/components/Results/Submission.vue index 3e6d102..c35feb3 100644 --- a/src/components/Results/Submission.vue +++ b/src/components/Results/Submission.vue @@ -24,7 +24,7 @@

- {{ t('forms', 'Response by {userDisplayName}', { userDisplayName }) }} + {{ t('forms', 'Response by {userDisplayName}', { userDisplayName: submission.userDisplayName }) }}

@@ -72,9 +72,6 @@ export default { }, computed: { - userDisplayName() { - return this.submission.userId - }, submissionDateTime() { return moment(this.submission.timestamp, 'X').format('LLLL') }, diff --git a/src/views/Results.vue b/src/views/Results.vue index dfc70bf..b1efe60 100644 --- a/src/views/Results.vue +++ b/src/views/Results.vue @@ -194,7 +194,7 @@ export default { const formattedSubmissions = [] this.submissions.forEach(submission => { const formattedSubmission = { - userId: submission.userId, + userDisplayName: submission.userDisplayName, timestamp: moment(submission.timestamp, 'X').format('L LT'), } From 5ee468439fdf629a0a864ed603dd1829fd0cdba0 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Tue, 5 May 2020 16:51:34 +0200 Subject: [PATCH 8/9] Fix top bar button having too little spacing left and right of text Signed-off-by: Jan-Christoph Borchardt --- src/components/TopBar.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/TopBar.vue b/src/components/TopBar.vue index 6dc74e2..711ddb8 100644 --- a/src/components/TopBar.vue +++ b/src/components/TopBar.vue @@ -52,7 +52,12 @@ $top-bar-height: 60px; button { cursor: pointer; min-height: 44px; - margin: 8px; + + // Fix button having too little spacing left and right of text + &:not(.button-small) { + padding-left: 16px; + padding-right: 16px; + } &.button-small { width: 44px; From e8abdad26af7cdac69e91bc6badfc03936cca7a7 Mon Sep 17 00:00:00 2001 From: Jan-Christoph Borchardt Date: Tue, 5 May 2020 16:51:59 +0200 Subject: [PATCH 9/9] Enhance vertical spacing of responses Signed-off-by: Jan-Christoph Borchardt --- src/components/Results/Answer.vue | 2 +- src/components/Results/Submission.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Results/Answer.vue b/src/components/Results/Answer.vue index 2b6f3b5..3352ac5 100644 --- a/src/components/Results/Answer.vue +++ b/src/components/Results/Answer.vue @@ -48,7 +48,7 @@ export default {