Insert Submission, new API

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2020-04-28 15:36:12 +02:00 committed by Jonas Rittershofer
parent 7462be7bfe
commit e0d41aef0c
6 changed files with 147 additions and 86 deletions

View file

@ -32,7 +32,6 @@ return [
['name' => 'page#getResult', 'url' => '/{hash}/results', 'verb' => 'GET'],
['name' => 'page#goto_form', 'url' => '/{hash}', 'verb' => 'GET'],
['name' => 'page#insert_submission', 'url' => '/insert/submission', 'verb' => 'POST'],
// Forms
['name' => 'api#getForms', 'url' => '/api/v1/forms', 'verb' => 'GET'],
@ -53,6 +52,7 @@ return [
['name' => 'api#deleteOption', 'url' => '/api/v1/option/{id}', 'verb' => 'DELETE'],
['name' => 'api#getSubmissions', 'url' => '/api/v1/submissions/{hash}', 'verb' => 'GET'],
['name' => 'api#insertSubmission', 'url' => '/api/v1/submissions/insert', 'verb' => 'POST'],
['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'],
]

View file

@ -38,6 +38,7 @@ use OCP\AppFramework\Db\IMapperException;
use OCP\ILogger;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use OCA\Forms\Db\Form;
@ -62,12 +63,13 @@ class ApiController extends Controller {
/** @var ILogger */
private $logger;
/** @var string */
private $userId;
/** @var IUserSession */
private $userSession;
public function __construct(
IRequest $request,
$userId,
$userId, // TODO remove & replace with userSession below.
IUserSession $userSession,
FormMapper $formMapper,
SubmissionMapper $submissionMapper,
AnswerMapper $answerMapper,
@ -77,6 +79,7 @@ class ApiController extends Controller {
) {
parent::__construct(Application::APP_ID, $request);
$this->userId = $userId;
$this->userSession = $userSession;
$this->formMapper = $formMapper;
$this->submissionMapper = $submissionMapper;
$this->answerMapper = $answerMapper;
@ -603,4 +606,84 @@ class ApiController extends Controller {
return new Http\JSONResponse($result);
}
/**
* @NoAdminRequired
* @PublicPage
*
* Process a new submission
* @param int $formId
* @param array $answers [question_id => arrayOfString]
*/
public function insertSubmission(int $formId, array $answers) {
$this->logger->debug('Inserting submission: formId: {formId}, answers: {answers}', [
'formId' => $formId,
'answers' => $answers,
]);
try {
$form = $this->formMapper->findById($formId);
$questions = $this->getQuestions($formId);
} catch (IMapperException $e) {
$this->logger->debug('Could not find form');
return new Http\JSONResponse(['message' => 'Could not find form'], Http::STATUS_BAD_REQUEST);
}
$user = $this->userSession->getUser();
// TODO check again hasUserAccess?!
// Create Submission
$submission = new Submission();
$submission->setFormId($formId);
$submission->setTimestamp(time());
// If not logged in or anonymous use anonID
if (!$user || $form->getIsAnonymous()){
$anonID = "anon-user-". hash('md5', (time() + rand()));
$submission->setUserId($anonID);
} else {
$submission->setUserId($user->getUID());
}
// Insert new submission
$this->submissionMapper->insert($submission);
$submissionId = $submission->getId();
// Process Answers
foreach($answers as $questionId => $answerArray) {
// Search corresponding Question, skip processing if not found
$questionIndex = array_search($questionId, array_column($questions, 'id'));
if ($questionIndex === false) {
continue;
} else {
$question = $questions[$questionIndex];
}
foreach($answerArray as $answer) {
if($question['type'] === 'multiple' || $question['type'] === 'multiple_unique') {
// Search corresponding option, skip processing if not found
$optionIndex = array_search($answer, array_column($question['options'], 'id'));
if($optionIndex === false) {
continue;
} else {
$option = $question['options'][$optionIndex];
}
// Load option-text
$answerText = $option['text'];
} else {
$answerText = $answer; // Not a multiple-question, answerText is given answer
}
$answerEntity = new Answer();
$answerEntity->setSubmissionId($submissionId);
$answerEntity->setQuestionId($question['id']);
$answerEntity->setText($answerText);
$this->answerMapper->insert($answerEntity);
}
}
return new Http\JSONResponse([]);
}
}

View file

@ -29,19 +29,16 @@
namespace OCA\Forms\Controller;
use Exception;
use OCA\Forms\Db\Form;
use OCA\Forms\Db\FormMapper;
use OCA\Forms\Db\Submission;
use OCA\Forms\Db\SubmissionMapper;
use OCA\Forms\Db\Answer;
use OCA\Forms\Db\AnswerMapper;
use OCA\Forms\Db\OptionMapper;
use OCA\Forms\Db\Question;
use OCA\Forms\Db\QuestionMapper;
use OCA\Forms\Db\Option;
use OCA\Forms\Db\OptionMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IGroupManager;
use OCP\IRequest;
@ -57,12 +54,6 @@ class PageController extends Controller {
/** @var FormMapper */
private $formMapper;
/** @var SubmissionMapper */
private $submissionMapper;
/** @var AnswerMapper */
private $answerMapper;
/** @var IURLGenerator */
private $urlGenerator;
@ -82,8 +73,6 @@ class PageController extends Controller {
FormMapper $formMapper,
QuestionMapper $questionMapper,
OptionMapper $optionMapper,
SubmissionMapper $SubmissionMapper,
AnswerMapper $AnswerMapper,
IUserSession $userSession,
IInitialStateService $initialStateService) {
parent::__construct($appName, $request);
@ -94,8 +83,6 @@ class PageController extends Controller {
$this->formMapper = $formMapper;
$this->questionMapper = $questionMapper;
$this->optionMapper = $optionMapper;
$this->submissionMapper = $SubmissionMapper;
$this->answerMapper = $AnswerMapper;
$this->userSession = $userSession;
$this->initialStateService = $initialStateService;
}
@ -237,62 +224,6 @@ class PageController extends Controller {
return new TemplateResponse($this->appName, 'main', [], $renderAs);
}
/**
* @NoAdminRequired
* @PublicPage
*
* Process a new submission
* @param int $formId
* @param string $userId
* @param array $answers
* @param array $questions
* @return RedirectResponse
*/
public function insertSubmission(int $id, string $userId, array $answers, array $questions) {
$form = $this->formMapper->findById($id);
$anonID = "anon-user-". hash('md5', (time() + rand()));
// Insert Submission
$submission = new Submission();
$submission->setFormId($id);
if ($form->getIsAnonymous()){
$submission->setUserId($anonID);
} else {
$submission->setUserId($userId);
}
$submission->setTimestamp(time());
$this->submissionMapper->insert($submission);
$submissionId = $submission->getId();
//Insert Answers
foreach($questions as $question) {
// If question is answered, the questionText exists as key in $answers. Does not exist, when a (non-mandatory) question was not answered.
if (array_key_exists($question['text'], $answers)) {
if($question['type'] === "checkbox"){
foreach(($answers[$question['text']]) as $ansText) {
$answer = new Answer();
$answer->setSubmissionId($submissionId);
$answer->setQuestionId($question['id']);
$answer->setText($ansText);
$this->answerMapper->insert($answer);
}
} else {
$answer = new Answer();
$answer->setSubmissionId($submissionId);
$answer->setQuestionId($question['id']);
$answer->setText($answers[$question['text']]);
$this->answerMapper->insert($answer);
}
}
}
$hash = $form->getHash();
$url = $this->urlGenerator->linkToRoute('forms.page.goto_form', ['hash' => $hash]);
return new RedirectResponse($url);
}
/**
* @NoAdminRequired
* Check if user has access to this form

View file

@ -43,7 +43,8 @@
}"
:name="`${id}-answer`"
:required="true /* TODO: implement required option */"
:type="isUnique ? 'radio' : 'checkbox'">
:type="isUnique ? 'radio' : 'checkbox'"
@change="onChange($event, answer.id)">
<label v-if="!edit"
ref="label"
:for="`${id}-answer-${answer.id}`"
@ -126,6 +127,26 @@ export default {
},
methods: {
onChange(event, answerId) {
const isChecked = event.target.checked === true
let values = this.values.slice()
// Radio
if (this.isUnique) {
this.$emit('update:values', [answerId])
return
}
// Checkbox
if (isChecked) {
values.push(answerId)
} else {
values = values.filter(id => id !== answerId)
}
// Emit values and remove duplicates
this.$emit('update:values', [...new Set(values)])
},
/**
* Is the provided index checked

View file

@ -392,7 +392,7 @@ export default {
font-size: 2em;
font-weight: bold;
padding-left: 14px; // align with description (compensate font size diff)
overflow: hidden;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View file

@ -44,9 +44,14 @@
:read-only="true"
:model="answerTypes[question.type]"
:index="index + 1"
v-bind="question" />
v-bind="question"
:values.sync="answers[question.id]" />
</ul>
<input class="primary" type="submit" :value="t('forms', 'Submit')" :aria-label="t('forms', 'Submit form')">
<input class="primary"
type="submit"
:value="t('forms', 'Submit')"
:disabled="loading"
:aria-label="t('forms', 'Submit form')">
</form>
</AppContent>
</Content>
@ -54,12 +59,14 @@
<script>
import { loadState } from '@nextcloud/initial-state'
import answerTypes from '../models/AnswerTypes'
import { generateUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import Content from '@nextcloud/vue/dist/Components/Content'
import answerTypes from '../models/AnswerTypes'
import Question from '../components/Questions/Question'
import QuestionLong from '../components/Questions/QuestionLong'
import QuestionShort from '../components/Questions/QuestionShort'
@ -81,6 +88,9 @@ export default {
return {
form: loadState('forms', 'form'),
answerTypes,
answers: {},
loading: false,
success: false,
}
},
@ -102,8 +112,24 @@ export default {
},
methods: {
onSubmit(e) {
console.info(e)
/**
* Submit the form after the browser validated it 🚀
*/
async onSubmit() {
this.loading = true
try {
await axios.post(generateUrl('/apps/forms/api/v1/submissions/insert'), {
formId: this.form.id,
answers: this.answers,
})
this.success = true
} catch (error) {
console.error(error)
showError(t('forms', 'There was an error submitting the form'))
} finally {
this.loading = false
}
},
},