Alter Database

Signed-off-by: Jonas Rittershofer <jotoeri@users.noreply.github.com>
This commit is contained in:
Jonas Rittershofer 2020-03-30 13:48:14 +02:00
parent d402107cbb
commit 03e9ff4a86
30 changed files with 1060 additions and 842 deletions

View file

@ -5,7 +5,7 @@
<name>Forms</name> <name>Forms</name>
<summary>A forms app, similar to Google Forms.</summary> <summary>A forms app, similar to Google Forms.</summary>
<description>A forms app, similar to Google Forms with the possibility to restrict access (members, certain groups/users, and public).</description> <description>A forms app, similar to Google Forms with the possibility to restrict access (members, certain groups/users, and public).</description>
<version>1.1.2</version> <version>1.2.0</version>
<licence>agpl</licence> <licence>agpl</licence>
<author>Vinzenz Rosenkranz</author> <author>Vinzenz Rosenkranz</author>
<author>René Gieling</author> <author>René Gieling</author>

View file

@ -33,23 +33,23 @@ return [
['name' => 'page#goto_form', 'url' => '/{hash}', 'verb' => 'GET'], ['name' => 'page#goto_form', 'url' => '/{hash}', 'verb' => 'GET'],
['name' => 'page#insert_vote', 'url' => '/insert/vote', 'verb' => 'POST'], ['name' => 'page#insert_submission', 'url' => '/insert/submission', 'verb' => 'POST'],
['name' => 'page#search', 'url' => '/search', 'verb' => 'POST'], ['name' => 'page#search', 'url' => '/search', 'verb' => 'POST'],
['name' => 'page#get_display_name', 'url' => '/get/displayname', 'verb' => 'POST'], ['name' => 'page#get_display_name', 'url' => '/get/displayname', 'verb' => 'POST'],
['name' => 'api#write_form', 'url' => '/write/form', 'verb' => 'POST'], ['name' => 'api#write_form', 'url' => '/write/form', 'verb' => 'POST'],
['name' => 'api#get_form', 'url' => '/get/form/{formIdOrHash}', 'verb' => 'GET'], ['name' => 'api#get_full_form', 'url' => '/get/fullform/{formIdOrHash}', 'verb' => 'GET'],
['name' => 'api#get_options', 'url' => '/get/options/{formId}', 'verb' => 'GET'], ['name' => 'api#get_options', 'url' => '/get/options/{formId}', 'verb' => 'GET'],
['name' => 'api#get_shares', 'url' => '/get/shares/{formId}', 'verb' => 'GET'], ['name' => 'api#get_shares', 'url' => '/get/shares/{formId}', 'verb' => 'GET'],
['name' => 'api#get_event', 'url' => '/get/event/{formId}', 'verb' => 'GET'], ['name' => 'api#get_form', 'url' => '/get/form/{formId}', 'verb' => 'GET'],
['name' => 'api#get_forms', 'url' => '/get/forms', 'verb' => 'GET'], ['name' => 'api#get_forms', 'url' => '/get/forms', 'verb' => 'GET'],
['name' => 'api#newForm', 'url' => 'api/v1/form', 'verb' => 'POST'], ['name' => 'api#newForm', 'url' => 'api/v1/form', 'verb' => 'POST'],
['name' => 'api#deleteForm', 'url' => 'api/v1/form/{id}', 'verb' => 'DELETE'], ['name' => 'api#deleteForm', 'url' => 'api/v1/form/{id}', 'verb' => 'DELETE'],
['name' => 'api#newQuestion', 'url' => 'api/v1/question/', 'verb' => 'POST'], ['name' => 'api#newQuestion', 'url' => 'api/v1/question/', 'verb' => 'POST'],
['name' => 'api#deleteQuestion', 'url' => 'api/v1/question/{id}', 'verb' => 'DELETE'], ['name' => 'api#deleteQuestion', 'url' => 'api/v1/question/{id}', 'verb' => 'DELETE'],
['name' => 'api#newAnswer', 'url' => 'api/v1/answer/', 'verb' => 'POST'], ['name' => 'api#newOption', 'url' => 'api/v1/option/', 'verb' => 'POST'],
['name' => 'api#deleteAnswer', 'url' => 'api/v1/answer/{id}', 'verb' => 'DELETE'], ['name' => 'api#deleteOption', 'url' => 'api/v1/option/{id}', 'verb' => 'DELETE'],
['name' => 'api#getSubmissions', 'url' => 'api/v1/submissions/{hash}', 'verb' => 'GET'], ['name' => 'api#getSubmissions', 'url' => 'api/v1/submissions/{hash}', 'verb' => 'GET'],
['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'], ['name' => 'system#get_site_users_and_groups', 'url' => '/get/siteusers', 'verb' => 'POST'],

View file

@ -8,7 +8,7 @@ function sendDataToServer(survey) {
form.userId = 'anon_' + Date.now() + '_' + Math.floor(Math.random() * 10000) form.userId = 'anon_' + Date.now() + '_' + Math.floor(Math.random() * 10000)
} }
form.questions = questions; form.questions = questions;
$.post(OC.generateUrl('apps/forms/insert/vote'), form) $.post(OC.generateUrl('apps/forms/insert/submission'), form)
.then((response) => { .then((response) => {
}, (error) => { }, (error) => {
/* eslint-disable-next-line no-console */ /* eslint-disable-next-line no-console */
@ -34,7 +34,7 @@ function cssUpdate(survey, options){
$(document).ready(function () { $(document).ready(function () {
var formJSON = $('#surveyContainer').attr('form') var formJSON = $('#surveyContainer').attr('form')
var questionJSON = $('#surveyContainer').attr('questions') var questionJSON = $('#surveyContainer').attr('questions')
form = JSON.parse(formJSON) form = JSON.parse(formJSON)
questions = JSON.parse(questionJSON) questions = JSON.parse(questionJSON)
@ -45,11 +45,11 @@ $(document).ready(function () {
}; };
questions.forEach(q => { questions.forEach(q => {
var ans = [] var qChoices = []
q.answers.forEach(a => { q.options.forEach(o => {
ans.push(a.text); qChoices.push(o.text);
}); });
surveyJSON.questions.push({type: q.type, name: q.text, choices: ans, isRequired: 'true'}); surveyJSON.questions.push({type: q.type, name: q.text, choices: qChoices, isRequired: 'true'});
}); });
$('#surveyContainer').Survey({ $('#surveyContainer').Survey({

View file

@ -41,14 +41,17 @@ use OCP\IUser;
use OCP\IUserManager; use OCP\IUserManager;
use OCP\Security\ISecureRandom; use OCP\Security\ISecureRandom;
use OCA\Forms\Db\Event; use OCA\Forms\Db\Form;
use OCA\Forms\Db\EventMapper; use OCA\Forms\Db\FormMapper;
use OCA\Forms\Db\VoteMapper; use OCA\Forms\Db\Submission;
use OCA\Forms\Db\SubmissionMapper;
use OCA\Forms\Db\Answer;
use OCA\Forms\Db\AnswerMapper;
use OCA\Forms\Db\Question; use OCA\Forms\Db\Question;
use OCA\Forms\Db\QuestionMapper; use OCA\Forms\Db\QuestionMapper;
use OCA\Forms\Db\Answer; use OCA\Forms\Db\Option;
use OCA\Forms\Db\AnswerMapper; use OCA\Forms\Db\OptionMapper;
use OCP\Util; use OCP\Util;
@ -56,10 +59,11 @@ class ApiController extends Controller {
private $groupManager; private $groupManager;
private $userManager; private $userManager;
private $eventMapper; private $formMapper;
private $voteMapper; private $submissionMapper;
private $questionMapper;
private $answerMapper; private $answerMapper;
private $questionMapper;
private $optionMapper;
/** @var ILogger */ /** @var ILogger */
private $logger; private $logger;
@ -74,10 +78,11 @@ class ApiController extends Controller {
* @param IRequest $request * @param IRequest $request
* @param IUserManager $userManager * @param IUserManager $userManager
* @param string $userId * @param string $userId
* @param EventMapper $eventMapper * @param FormMapper $formMapper
* @param VoteMapper $voteMapper * @param SubmissionMapper $submissionMapper
* @param QuestionMapper $questionMapper
* @param AnswerMapper $answerMapper * @param AnswerMapper $answerMapper
* @param QuestionMapper $questionMapper
* @param OptionMapper $optionMapper
*/ */
public function __construct( public function __construct(
$appName, $appName,
@ -85,20 +90,22 @@ class ApiController extends Controller {
IRequest $request, IRequest $request,
IUserManager $userManager, IUserManager $userManager,
$userId, $userId,
EventMapper $eventMapper, FormMapper $formMapper,
VoteMapper $voteMapper, SubmissionMapper $submissionMapper,
QuestionMapper $questionMapper,
AnswerMapper $answerMapper, AnswerMapper $answerMapper,
QuestionMapper $questionMapper,
OptionMapper $optionMapper,
ILogger $logger ILogger $logger
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->userId = $userId; $this->userId = $userId;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->userManager = $userManager; $this->userManager = $userManager;
$this->eventMapper = $eventMapper; $this->formMapper = $formMapper;
$this->voteMapper = $voteMapper; $this->submissionMapper = $submissionMapper;
$this->questionMapper = $questionMapper;
$this->answerMapper = $answerMapper; $this->answerMapper = $answerMapper;
$this->questionMapper = $questionMapper;
$this->optionMapper = $optionMapper;
$this->logger = $logger; $this->logger = $logger;
} }
@ -108,8 +115,8 @@ class ApiController extends Controller {
* @param string $item * @param string $item
* @return Array * @return Array
*/ */
private function convertAccessList($item) { private function convertAccessList($item) : array {
$split = array(); $split = [];
if (strpos($item, 'user_') === 0) { if (strpos($item, 'user_') === 0) {
$user = $this->userManager->get(substr($item, 5)); $user = $this->userManager->get(substr($item, 5));
$split = [ $split = [
@ -172,11 +179,11 @@ class ApiController extends Controller {
/** /**
* Set the access right of the current user for the form * Set the access right of the current user for the form
* @param Array $event * @param Array $form
* @param Array $shares * @param Array $shares
* @return String * @return String
*/ */
private function grantAccessAs($event, $shares) { private function grantAccessAs($form, $shares) {
if (!\OC::$server->getUserSession()->getUser() instanceof IUser) { if (!\OC::$server->getUserSession()->getUser() instanceof IUser) {
$currentUser = ''; $currentUser = '';
} else { } else {
@ -185,13 +192,13 @@ class ApiController extends Controller {
$grantAccessAs = 'none'; $grantAccessAs = 'none';
if ($event['owner'] === $currentUser) { if ($form['ownerId'] === $currentUser) {
$grantAccessAs = 'owner'; $grantAccessAs = 'owner';
} elseif ($event['access'] === 'public') { } elseif ($form['access'] === 'public') {
$grantAccessAs = 'public'; $grantAccessAs = 'public';
} elseif ($event['access'] === 'registered' && \OC::$server->getUserSession()->getUser() instanceof IUser) { } elseif ($form['access'] === 'registered' && \OC::$server->getUserSession()->getUser() instanceof IUser) {
$grantAccessAs = 'registered'; $grantAccessAs = 'registered';
} elseif ($event['access'] === 'hidden' && ($event['owner'] === \OC::$server->getUserSession()->getUser())) { } elseif ($form['access'] === 'hidden' && ($form['ownerId'] === \OC::$server->getUserSession()->getUser())) {
$grantAccessAs = 'hidden'; $grantAccessAs = 'hidden';
} elseif ($this->checkUserAccess($shares)) { } elseif ($this->checkUserAccess($shares)) {
$grantAccessAs = 'userInvitation'; $grantAccessAs = 'userInvitation';
@ -210,11 +217,11 @@ class ApiController extends Controller {
* @param Integer $formId * @param Integer $formId
* @return Array * @return Array
*/ */
public function getEvent($formId) { public function getForm($formId) {
$data = array(); $data = array();
try { try {
$data = $this->eventMapper->find($formId)->read(); $data = $this->formMapper->find($formId)->read();
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
// return silently // return silently
} finally { } finally {
@ -234,7 +241,7 @@ class ApiController extends Controller {
$accessList = array(); $accessList = array();
try { try {
$form = $this->eventMapper->find($formId); $form = $this->formMapper->find($formId);
if (!strpos('|public|hidden|registered', $form->getAccess())) { if (!strpos('|public|hidden|registered', $form->getAccess())) {
$accessList = explode(';', $form->getAccess()); $accessList = explode(';', $form->getAccess());
$accessList = array_filter($accessList); $accessList = array_filter($accessList);
@ -248,14 +255,14 @@ class ApiController extends Controller {
} }
public function getQuestions($formId) { public function getQuestions($formId) : array {
$questionList = array(); $questionList = [];
try{ try{
$questions = $this->questionMapper->findByForm($formId); $questionEntities = $this->questionMapper->findByForm($formId);
foreach ($questions as $questionElement) { foreach ($questionEntities as $questionEntity) {
$temp = $questionElement->read(); $question = $questionEntity->read();
$temp['answers'] = $this->getAnswers($formId, $temp['id']); $question['options'] = $this->getOptions($question['id']);
$questionList[] = $temp; $questionList[] = $question;
} }
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
@ -265,18 +272,18 @@ class ApiController extends Controller {
} }
} }
public function getAnswers($formId, $questionId) { public function getOptions($questionId) : array {
$answerList = array(); $optionList = [];
try{ try{
$answers = $this->answerMapper->findByForm($formId, $questionId); $optionEntities = $this->optionMapper->findByQuestion($questionId);
foreach ($answers as $answerElement) { foreach ($optionEntities as $optionEntity) {
$answerList[] = $answerElement->read(); $optionList[] = $optionEntity->read();
} }
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
//handle silently //handle silently
}finally{ }finally{
return $answerList; return $optionList;
} }
} }
@ -286,7 +293,7 @@ class ApiController extends Controller {
* @param String $formIdOrHash form id or hash * @param String $formIdOrHash form id or hash
* @return Array * @return Array
*/ */
public function getForm($formIdOrHash) { public function getFullForm($formIdOrHash) {
if (!\OC::$server->getUserSession()->getUser() instanceof IUser) { if (!\OC::$server->getUserSession()->getUser() instanceof IUser) {
$currentUser = ''; $currentUser = '';
@ -299,32 +306,30 @@ class ApiController extends Controller {
try { try {
if (is_numeric($formIdOrHash)) { if (is_numeric($formIdOrHash)) {
$formId = $this->eventMapper->find(intval($formIdOrHash))->id; $formId = $this->formMapper->find(intval($formIdOrHash))->id;
$result = 'foundById'; $result = 'foundById';
} else { } else {
$formId = $this->eventMapper->findByHash($formIdOrHash)->id; $formId = $this->formMapper->findByHash($formIdOrHash)->id;
$result = 'foundByHash'; $result = 'foundByHash';
} }
$event = $this->getEvent($formId); $form = $this->getForm($formId);
$shares = $this->getShares($event['id']); $shares = $this->getShares($form['id']);
if ($event['owner'] !== $currentUser && !$this->groupManager->isAdmin($currentUser)) { if ($form['ownerId'] !== $currentUser && !$this->groupManager->isAdmin($currentUser)) {
$mode = 'create'; $mode = 'create';
} else { } else {
$mode = 'edit'; $mode = 'edit';
} }
$data = [ $data = [
'id' => $event['id'], 'id' => $form['id'],
'result' => $result, 'result' => $result,
'grantedAs' => $this->grantAccessAs($event, $shares), 'grantedAs' => $this->grantAccessAs($form, $shares),
'mode' => $mode, 'mode' => $mode,
'event' => $event, 'form' => $form,
'shares' => $shares, 'shares' => $shares,
'options' => [ 'questions' => $this->getQuestions($form['id']),
'formQuizQuestions' => $this->getQuestions($event['id'])
]
]; ];
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
$data['form'] = ['result' => 'notFound']; $data['form'] = ['result' => 'notFound'];
@ -345,21 +350,20 @@ class ApiController extends Controller {
} }
try { try {
$events = $this->eventMapper->findAll(); $forms = $this->formMapper->findAll();
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
return new DataResponse($e, Http::STATUS_NOT_FOUND); return new DataResponse($e, Http::STATUS_NOT_FOUND);
} }
$eventsList = array(); $formsList = array();
foreach ($forms as $formElement) {
foreach ($events as $eventElement) { $form = $this->getFullForm($formElement->id);
$event = $this->getForm($eventElement->id); //if ($form['grantedAs'] !== 'none') {
//if ($event['grantedAs'] !== 'none') { $formsList[] = $form;
$eventsList[] = $event;
//} //}
} }
return new DataResponse($eventsList, Http::STATUS_OK); return new DataResponse($formsList, Http::STATUS_OK);
} }
/** /**
@ -370,17 +374,16 @@ class ApiController extends Controller {
*/ */
public function deleteForm(int $id) { public function deleteForm(int $id) {
try { try {
$formToDelete = $this->eventMapper->find($id); $formToDelete = $this->formMapper->find($id);
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
return new Http\JSONResponse([], Http::STATUS_NOT_FOUND); return new Http\JSONResponse([], Http::STATUS_NOT_FOUND);
} }
if ($this->userId !== $formToDelete->getOwner() && !$this->groupManager->isAdmin($this->userId)) { if ($this->userId !== $formToDelete->getOwnerId() && !$this->groupManager->isAdmin($this->userId)) {
return new DataResponse(null, Http::STATUS_UNAUTHORIZED); return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
} }
$this->voteMapper->deleteByForm($id); $this->submissionMapper->deleteByForm($id);
$this->questionMapper->deleteByForm($id); $this->questionMapper->deleteByForm($id);
$this->answerMapper->deleteByForm($id); $this->formMapper->delete($formToDelete);
$this->eventMapper->delete($formToDelete);
return new DataResponse(array( return new DataResponse(array(
'id' => $id, 'id' => $id,
'action' => 'deleted' 'action' => 'deleted'
@ -391,30 +394,30 @@ class ApiController extends Controller {
/** /**
* Write form (create/update) * Write form (create/update)
* @NoAdminRequired * @NoAdminRequired
* @param Array $event * @param Array $form
* @param Array $options * @param Array $options
* @param Array $shares * @param Array $shares
* @param String $mode * @param String $mode
* @return DataResponse * @return DataResponse
*/ */
public function writeForm($event, $options, $shares, $mode) { public function writeForm($form, $questions, $shares, $mode) {
if (!\OC::$server->getUserSession()->getUser() instanceof IUser) { if (!\OC::$server->getUserSession()->getUser() instanceof IUser) {
return new DataResponse(null, Http::STATUS_UNAUTHORIZED); return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
} else { } else {
$currentUser = \OC::$server->getUserSession()->getUser()->getUID(); $currentUser = \OC::$server->getUserSession()->getUser()->getUID();
$AdminAccess = $this->groupManager->isAdmin($currentUser); $adminAccess = $this->groupManager->isAdmin($currentUser);
} }
$newEvent = new Event(); $newForm = new Form();
// Set the configuration options entered by the user // Set the configuration options entered by the user
$newEvent->setTitle($event['title']); $newForm->setTitle($form['title']);
$newEvent->setDescription($event['description']); $newForm->setDescription($form['description']);
$newEvent->setIsAnonymous($event['isAnonymous']); $newForm->setIsAnonymous($form['isAnonymous']);
$newEvent->setUnique($event['unique']); $newForm->setSubmitOnce($form['submitOnce']);
if ($event['access'] === 'select') { if ($form['access'] === 'select') {
$shareAccess = ''; $shareAccess = '';
foreach ($shares as $shareElement) { foreach ($shares as $shareElement) {
if ($shareElement['type'] === 'user') { if ($shareElement['type'] === 'user') {
@ -423,50 +426,50 @@ class ApiController extends Controller {
$shareAccess = $shareAccess . 'group_' . $shareElement['id'] . ';'; $shareAccess = $shareAccess . 'group_' . $shareElement['id'] . ';';
} }
} }
$newEvent->setAccess(rtrim($shareAccess, ';')); $newForm->setAccess(rtrim($shareAccess, ';'));
} else { } else {
$newEvent->setAccess($event['access']); $newForm->setAccess($form['access']);
} }
if ($event['expiration']) { if ($form['expires']) {
$newEvent->setExpire(date('Y-m-d H:i:s', strtotime($event['expirationDate']))); $newForm->setExpirationDate(date('Y-m-d H:i:s', strtotime($form['expirationDate'])));
} else { } else {
$newEvent->setExpire(null); $newForm->setExpirationDate(null);
} }
if ($mode === 'edit') { if ($mode === 'edit') {
// Edit existing form // Edit existing form
$oldForm = $this->eventMapper->findByHash($event['hash']); $oldForm = $this->formMapper->findByHash($form['hash']);
// Check if current user is allowed to edit existing form // Check if current user is allowed to edit existing form
if ($oldForm->getOwner() !== $currentUser && !$AdminAccess) { if ($oldForm->getOwnerId() !== $currentUser && !$adminAccess) {
// If current user is not owner of existing form deny access // If current user is not owner of existing form deny access
return new DataResponse(null, Http::STATUS_UNAUTHORIZED); return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
} }
// else take owner, hash and id of existing form // else take owner, hash and id of existing form
$newEvent->setOwner($oldForm->getOwner()); $newForm->setOwnerId($oldForm->getOwnerId());
$newEvent->setHash($oldForm->getHash()); $newForm->setHash($oldForm->getHash());
$newEvent->setId($oldForm->getId()); $newForm->setId($oldForm->getId());
$this->eventMapper->update($newEvent); $this->formMapper->update($newForm);
} elseif ($mode === 'create') { } elseif ($mode === 'create') {
// Create new form // Create new form
// Define current user as owner, set new creation date and create a new hash // Define current user as owner, set new creation date and create a new hash
$newEvent->setOwner($currentUser); $newForm->setOwnerId($currentUser);
$newEvent->setCreated(date('Y-m-d H:i:s')); $newForm->setCreated(date('Y-m-d H:i:s'));
$newEvent->setHash(\OC::$server->getSecureRandom()->generate( $newForm->setHash(\OC::$server->getSecureRandom()->generate(
16, 16,
ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_DIGITS .
ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_LOWER .
ISecureRandom::CHAR_UPPER ISecureRandom::CHAR_UPPER
)); ));
$newEvent = $this->eventMapper->insert($newEvent); $newForm = $this->formMapper->insert($newForm);
} }
return new DataResponse(array( return new DataResponse(array(
'id' => $newEvent->getId(), 'id' => $newForm->getId(),
'hash' => $newEvent->getHash() 'hash' => $newForm->getHash()
), Http::STATUS_OK); ), Http::STATUS_OK);
} }
@ -475,22 +478,22 @@ class ApiController extends Controller {
* @NoAdminRequired * @NoAdminRequired
*/ */
public function newForm(): Http\JSONResponse { public function newForm(): Http\JSONResponse {
$event = new Event(); $form = new Form();
$currentUser = \OC::$server->getUserSession()->getUser()->getUID(); $currentUser = \OC::$server->getUserSession()->getUser()->getUID();
$event->setOwner($currentUser); $form->setOwnerId($currentUser);
$event->setCreated(date('Y-m-d H:i:s')); $form->setCreated(date('Y-m-d H:i:s'));
$event->setHash(\OC::$server->getSecureRandom()->generate( $form->setHash(\OC::$server->getSecureRandom()->generate(
16, 16,
ISecureRandom::CHAR_HUMAN_READABLE ISecureRandom::CHAR_HUMAN_READABLE
)); ));
$event->setTitle('New form'); $form->setTitle('New form');
$event->setDescription(''); $form->setDescription('');
$event->setAccess('public'); $form->setAccess('public');
$this->eventMapper->insert($event); $this->formMapper->insert($form);
return new Http\JSONResponse($this->getForm($event->getHash())); return new Http\JSONResponse($this->getFullForm($form->getHash()));
} }
/** /**
@ -504,13 +507,13 @@ class ApiController extends Controller {
]); ]);
try { try {
$form = $this->eventMapper->find($formId); $form = $this->formMapper->find($formId);
} catch (IMapperException $e) { } catch (IMapperException $e) {
$this->logger->debug('Could not find form'); $this->logger->debug('Could not find form');
return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST);
} }
if ($form->getOwner() !== $this->userId) { if ($form->getOwnerId() !== $this->userId) {
$this->logger->debug('This form is not owned by the current user'); $this->logger->debug('This form is not owned by the current user');
return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); return new Http\JSONResponse([], Http::STATUS_FORBIDDEN);
} }
@ -518,8 +521,8 @@ class ApiController extends Controller {
$question = new Question(); $question = new Question();
$question->setFormId($formId); $question->setFormId($formId);
$question->setFormQuestionType($type); $question->setType($type);
$question->setFormQuestionText($text); $question->setText($text);
$question = $this->questionMapper->insert($question); $question = $this->questionMapper->insert($question);
@ -536,18 +539,18 @@ class ApiController extends Controller {
try { try {
$question = $this->questionMapper->findById($id); $question = $this->questionMapper->findById($id);
$form = $this->eventMapper->find($question->getFormId()); $form = $this->formMapper->find($question->getFormId());
} catch (IMapperException $e) { } catch (IMapperException $e) {
$this->logger->debug('Could not find form or question of this answer'); $this->logger->debug('Could not find form or question');
return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST);
} }
if ($form->getOwner() !== $this->userId) { if ($form->getOwnerId() !== $this->userId) {
$this->logger->debug('This form is not owned by the current user'); $this->logger->debug('This form is not owned by the current user');
return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); return new Http\JSONResponse([], Http::STATUS_FORBIDDEN);
} }
$this->answerMapper->deleteByQuestion($id); $this->optionMapper->deleteByQuestion($id);
$this->questionMapper->delete($question); $this->questionMapper->delete($question);
return new Http\JSONResponse($id); return new Http\JSONResponse($id);
@ -556,64 +559,64 @@ class ApiController extends Controller {
/** /**
* @NoAdminRequired * @NoAdminRequired
*/ */
public function newAnswer(int $formId, int $questionId, string $text): Http\JSONResponse { public function newOption(int $formId, int $questionId, string $text): Http\JSONResponse {
$this->logger->debug('Adding new answer: formId: {formId}, questoinId: {questionId}, text: {text}', [ $this->logger->debug('Adding new option: formId: {formId}, questionId: {questionId}, text: {text}', [
'formId' => $formId, 'formId' => $formId,
'questionId' => $questionId, 'questionId' => $questionId,
'text' => $text, 'text' => $text,
]); ]);
try { try {
$form = $this->eventMapper->find($formId); $form = $this->formMapper->find($formId);
$question = $this->questionMapper->findById($questionId); $question = $this->questionMapper->findById($questionId);
} catch (IMapperException $e) { } catch (IMapperException $e) {
$this->logger->debug('Could not find form or question so answer can\'t be added'); $this->logger->debug('Could not find form or question so option can\'t be added');
return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST);
} }
if ($form->getOwner() !== $this->userId) { if ($form->getOwnerId() !== $this->userId) {
$this->logger->debug('This form is not owned by the current user'); $this->logger->debug('This form is not owned by the current user');
return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); return new Http\JSONResponse([], Http::STATUS_FORBIDDEN);
} }
if ($question->getFormId() !== $formId) { if ($question->getFormId() !== $formId) {
$this->logger->debug('This question is not owned by the current user'); $this->logger->debug('This question is not part of the current form');
return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); return new Http\JSONResponse([], Http::STATUS_FORBIDDEN);
} }
$answer = new Answer(); $option = new Option();
$answer->setFormId($formId); $option->setQuestionId($questionId);
$answer->setQuestionId($questionId); $option->setText($text);
$answer->setText($text);
$answer = $this->answerMapper->insert($answer); $option = $this->optionMapper->insert($option);
return new Http\JSONResponse($answer->getId()); return new Http\JSONResponse($option->getId());
} }
/** /**
* @NoAdminRequired * @NoAdminRequired
*/ */
public function deleteAnswer(int $id): Http\JSONResponse { public function deleteOption(int $id): Http\JSONResponse {
$this->logger->debug('Deleting answer: {id}', [ $this->logger->debug('Deleting option: {id}', [
'id' => $id 'id' => $id
]); ]);
try { try {
$answer = $this->answerMapper->findById($id); $option = $this->optionMapper->findById($id);
$form = $this->eventMapper->find($answer->getFormId()); $question = $this->questionMapper->findById($option->getQuestionId());
$form = $this->formMapper->find($question->getFormId());
} catch (IMapperException $e) { } catch (IMapperException $e) {
$this->logger->debug('Could not find form or answer'); $this->logger->debug('Could not find form or option');
return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST);
} }
if ($form->getOwner() !== $this->userId) { if ($form->getOwnerId() !== $this->userId) {
$this->logger->debug('This form is not owned by the current user'); $this->logger->debug('This form is not owned by the current user');
return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); return new Http\JSONResponse([], Http::STATUS_FORBIDDEN);
} }
$this->answerMapper->delete($answer); $this->optionMapper->delete($option);
//TODO useful response //TODO useful response
return new Http\JSONResponse($id); return new Http\JSONResponse($id);
@ -624,20 +627,30 @@ class ApiController extends Controller {
*/ */
public function getSubmissions(string $hash): Http\JSONResponse { public function getSubmissions(string $hash): Http\JSONResponse {
try { try {
$form = $this->eventMapper->findByHash($hash); $form = $this->formMapper->findByHash($hash);
} catch (IMapperException $e) { } catch (IMapperException $e) {
return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST);
} }
if ($form->getOwner() !== $this->userId) { if ($form->getOwnerId() !== $this->userId) {
return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); return new Http\JSONResponse([], Http::STATUS_FORBIDDEN);
} }
$votes = $this->voteMapper->findByForm($form->getId());
$result = []; $result = [];
foreach ($votes as $vote) { $submissionList = $this->submissionMapper->findByForm($form->getId());
$result[] = $vote->read(); 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;
}
} }
return new Http\JSONResponse($result); return new Http\JSONResponse($result);

View file

@ -29,12 +29,14 @@
namespace OCA\Forms\Controller; namespace OCA\Forms\Controller;
use OCA\Forms\AppInfo\Application; use OCA\Forms\AppInfo\Application;
use OCA\Forms\Db\Event; use OCA\Forms\Db\Form;
use OCA\Forms\Db\EventMapper; use OCA\Forms\Db\FormMapper;
use OCA\Forms\Db\Vote; use OCA\Forms\Db\Submission;
use OCA\Forms\Db\VoteMapper; use OCA\Forms\Db\SubmissionMapper;
use OCA\Forms\Db\Answer;
use OCA\Forms\Db\AnswerMapper; use OCA\Forms\Db\AnswerMapper;
use OCA\Forms\Db\OptionMapper;
use OCA\Forms\Db\QuestionMapper; use OCA\Forms\Db\QuestionMapper;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
@ -53,11 +55,12 @@ use OCP\Util;
class PageController extends Controller { class PageController extends Controller {
private $userId; private $userId;
private $eventMapper; private $formMapper;
private $voteMapper; private $submissionMapper;
private $answerMapper;
private $questionMapper; private $questionMapper;
private $answerMapper; private $optionMapper;
private $urlGenerator; private $urlGenerator;
private $userMgr; private $userMgr;
@ -69,21 +72,24 @@ class PageController extends Controller {
IGroupManager $groupManager, IGroupManager $groupManager,
IURLGenerator $urlGenerator, IURLGenerator $urlGenerator,
$userId, $userId,
EventMapper $eventMapper, FormMapper $formMapper,
QuestionMapper $questionMapper, QuestionMapper $questionMapper,
AnswerMapper $answerMapper, OptionMapper $optionMapper,
VoteMapper $VoteMapper SubmissionMapper $SubmissionMapper,
AnswerMapper $AnswerMapper
) { ) {
parent::__construct(Application::APP_ID, $request); parent::__construct(Application::APP_ID, $request);
$this->userMgr = $userMgr; $this->userMgr = $userMgr;
$this->groupManager = $groupManager; $this->groupManager = $groupManager;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->userId = $userId; $this->userId = $userId;
$this->eventMapper = $eventMapper; $this->formMapper = $formMapper;
$this->questionMapper = $questionMapper; $this->questionMapper = $questionMapper;
$this->answerMapper = $answerMapper; $this->optionMapper = $optionMapper;
$this->voteMapper = $VoteMapper; $this->submissionMapper = $SubmissionMapper;
$this->answerMapper = $AnswerMapper;
} }
/** /**
@ -155,15 +161,15 @@ class PageController extends Controller {
*/ */
public function gotoForm($hash): ?TemplateResponse { public function gotoForm($hash): ?TemplateResponse {
try { try {
$form = $this->eventMapper->findByHash($hash); $form = $this->formMapper->findByHash($hash);
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
return new TemplateResponse('forms', 'no.acc.tmpl', []); return new TemplateResponse('forms', 'no.acc.tmpl', []);
} }
if ($form->getExpire() === null) { if ($form->getExpirationDate() === null) {
$expired = false; $expired = false;
} else { } else {
$expired = time() > strtotime($form->getExpire()); $expired = time() > strtotime($form->getExpirationDate());
} }
if ($expired) { if ($expired) {
@ -172,7 +178,7 @@ class PageController extends Controller {
if ($this->hasUserAccess($form)) { if ($this->hasUserAccess($form)) {
$renderAs = $this->userId !== null ? 'user' : 'public'; $renderAs = $this->userId !== null ? 'user' : 'public';
$res = new TemplateResponse('forms', 'vote.tmpl', [ $res = new TemplateResponse('forms', 'submit.tmpl', [
'form' => $form, 'form' => $form,
'questions' => $this->getQuestions($form->getId()), 'questions' => $this->getQuestions($form->getId()),
], $renderAs); ], $renderAs);
@ -192,11 +198,11 @@ class PageController extends Controller {
public function getQuestions(int $formId): array { public function getQuestions(int $formId): array {
$questionList = []; $questionList = [];
try{ try{
$questions = $this->questionMapper->findByForm($formId); $questionEntities = $this->questionMapper->findByForm($formId);
foreach ($questions as $questionElement) { foreach ($questionEntities as $questionEntity) {
$temp = $questionElement->read(); $question = $questionEntity->read();
$temp['answers'] = $this->getAnswers($formId, $temp['id']); $question['options'] = $this->getOptions($question['id']);
$questionList[] = $temp; $questionList[] = $question;
} }
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
//handle silently //handle silently
@ -208,20 +214,20 @@ class PageController extends Controller {
/** /**
* @NoAdminRequired * @NoAdminRequired
*/ */
public function getAnswers(int $formId, int $questionId): array { public function getOptions(int $questionId): array {
$answerList = []; $optionList = [];
try{ try{
$answers = $this->answerMapper->findByForm($formId, $questionId); $optionEntities = $this->optionMapper->findByQuestion($questionId);
foreach ($answers as $answerElement) { foreach ($optionEntities as $optionEntity) {
$answerList[] = $answerElement->read(); $optionList[] = $optionEntity->read();
} }
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
//handle silently //handle silently
} }
return $answerList; return $optionList;
} }
/** /**
@ -230,14 +236,14 @@ class PageController extends Controller {
* @return TemplateResponse|RedirectResponse * @return TemplateResponse|RedirectResponse
*/ */
public function deleteForm($formId) { public function deleteForm($formId) {
$formToDelete = $this->eventMapper->find($formId); $formToDelete = $this->formMapper->find($formId);
if ($this->userId !== $formToDelete->getOwner() && !$this->groupManager->isAdmin($this->userId)) { if ($this->userId !== $formToDelete->getOwnerId() && !$this->groupManager->isAdmin($this->userId)) {
return new TemplateResponse('forms', 'no.delete.tmpl'); return new TemplateResponse('forms', 'no.delete.tmpl');
} }
$form = new Event(); $form = new Form();
$form->setId($formId); $form->setId($formId);
$this->voteMapper->deleteByForm($formId); $this->submissionMapper->deleteByForm($formId);
$this->eventMapper->delete($form); $this->formMapper->delete($form);
$url = $this->urlGenerator->linkToRoute('forms.page.index'); $url = $this->urlGenerator->linkToRoute('forms.page.index');
return new RedirectResponse($url); return new RedirectResponse($url);
} }
@ -247,51 +253,47 @@ class PageController extends Controller {
* @PublicPage * @PublicPage
* @param int $formId * @param int $formId
* @param string $userId * @param string $userId
* @param string $answers * @param array $answers
* @param string $options question id * @param array $questions
* @param bool $changed
* @return RedirectResponse * @return RedirectResponse
*/ */
public function insertVote($id, $userId, $answers, $questions) { public function insertSubmission($id, $userId, $answers, $questions) {
$form = $this->eventMapper->find($id); $form = $this->formMapper->find($id);
$count_answers = count($answers);
$count = 1;
$anonID = "anon-user-". hash('md5', (time() + rand())); $anonID = "anon-user-". hash('md5', (time() + rand()));
for ($i = 0; $i < $count_answers; $i++) { //Insert Submission
if($questions[$i]['type'] === "checkbox"){ $submission = new Submission();
foreach (($answers[$questions[$i]['text']]) as $value) { $submission->setFormId($id);
$vote = new Vote(); if($form->getIsAnonymous()){
$vote->setFormId($id); $submission->setUserId($anonID);
if($form->getIsAnonymous()){
$vote->setUserId($anonID);
}else{ }else{
$vote->setUserId($userId); $submission->setUserId($userId);
} }
$vote->setVoteOptionText(htmlspecialchars($questions[$i]['text'])); $submission->setTimestamp(date('Y-m-d H:i:s'));
$vote->setVoteAnswer($value); $this->submissionMapper->insert($submission);
$vote->setVoteOptionId($count); $submissionId = $submission->getId();
$vote->setVoteOptionType($questions[$i]['type']);
$this->voteMapper->insert($vote); //Insert Answers
foreach($questions as $question) {
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);
} }
$count++;
} else { } else {
$vote = new Vote(); $answer = new Answer();
$vote->setFormId($id); $answer->setSubmissionId($submissionId);
if($form->getIsAnonymous()){ $answer->setQuestionId($question['id']);
$vote->setUserId($anonID); $answer->setText($answers[$question['text']]);
}else{ $this->answerMapper->insert($answer);
$vote->setUserId($userId);
}
$vote->setVoteOptionText(htmlspecialchars($questions[$i]['text']));
$vote->setVoteAnswer($answers[$questions[$i]['text']]);
$vote->setVoteOptionId($count++);
$vote->setVoteOptionType($questions[$i]['type']);
$this->voteMapper->insert($vote);
} }
} }
$hash = $form->getHash(); $hash = $form->getHash();
$url = $this->urlGenerator->linkToRoute('forms.page.goto_form', ['hash' => $hash]); $url = $this->urlGenerator->linkToRoute('forms.page.goto_form', ['hash' => $hash]);
return new RedirectResponse($url); return new RedirectResponse($url);
@ -379,12 +381,12 @@ class PageController extends Controller {
/** /**
* Check if user has access to this form * Check if user has access to this form
* *
* @param Event $form * @param Form $form
* @return bool * @return bool
*/ */
private function hasUserAccess($form) { private function hasUserAccess($form) {
$access = $form->getAccess(); $access = $form->getAccess();
$owner = $form->getOwner(); $ownerId = $form->getOwnerId();
if ($access === 'public' || $access === 'hidden') { if ($access === 'public' || $access === 'hidden') {
return true; return true;
} }
@ -392,8 +394,8 @@ class PageController extends Controller {
return false; return false;
} }
if ($access === 'registered') { if ($access === 'registered') {
if ($form->getUnique()) { if ($form->getSubmitOnce()) {
$participants = $this->voteMapper->findParticipantsByForm($form->getId()); $participants = $this->submissionMapper->findParticipantsByForm($form->getId());
foreach($participants as $participant) { foreach($participants as $participant) {
// Don't allow access if user has already taken part // Don't allow access if user has already taken part
if ($participant->getUserId() === $this->userId) return false; if ($participant->getUserId() === $this->userId) return false;
@ -401,7 +403,7 @@ class PageController extends Controller {
} }
return true; return true;
} }
if ($owner === $this->userId) { if ($ownerId === $this->userId) {
return true; return true;
} }
Util::writeLog('forms', $this->userId, Util::ERROR); Util::writeLog('forms', $this->userId, Util::ERROR);

View file

@ -2,9 +2,9 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2019 Inigo Jiron <ijiron@terpmail.umd.edu> * @copyright Copyright (c) 2020 Jonas Rittershofer <jotoeri@users.noreply.github.com>
* *
* @author Inigo Jiron <ijiron@terpmail.umd.edu> * @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
* *
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
@ -28,46 +28,33 @@ namespace OCA\Forms\Db;
use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\Entity;
/** /**
* @method integer getFormId() * @method integer getSubmissionId()
* @method void setFormId(integer $value) * @method void setSubmissionId(integer $value)
* @method integer getQuestionId() * @method integer getQuestionId()
* @method void setQuestionId(integer $value) * @method void setQuestionId(integer $value)
* @method string getText() * @method string getText()
* @method void setText(string $value) * @method void setText(string $value)
* @method integer getTimestamp()
* @method void setTimestamp(integer $value)
*/ */
class Answer extends Entity { class Answer extends Entity {
protected $submissionId;
/** @var int */
protected $formId;
/** @var int */
protected $questionId; protected $questionId;
/** @var string */
protected $text; protected $text;
/** @var int */
protected $timestamp;
/** /**
* Answer constructor. * Answer constructor.
*/ */
public function __construct() { public function __construct() {
$this->addType('id', 'integer'); $this->addType('submissionId', 'integer');
$this->addType('formId', 'integer');
$this->addType('questionId', 'integer'); $this->addType('questionId', 'integer');
$this->addType('timestamp', 'integer');
} }
public function read(): array { public function read(): array {
return [ return [
'id' => $this->getId(), 'id' => $this->getId(),
'formId' => $this->getFormId(), 'submissionId' => $this->getSubmissionId(),
'questionId' => $this->getQuestionId(), 'questionId' => $this->getQuestionId(),
'text' => htmlspecialchars_decode($this->getText()), 'text' => htmlspecialchars_decode($this->getText()),
'timestamp' => $this->getTimestamp()
]; ];
} }
} }

View file

@ -1,12 +1,9 @@
<?php <?php
declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2019 Inigo Jiron <ijiron@terpmail.umd.edu> * @copyright Copyright (c) 2020 Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @author Inigo Jiron <ijiron@terpmail.umd.edu>
* @author Natalie Gilbert <ngilb634@umd.edu>
* *
* @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -33,89 +30,43 @@ use OCP\AppFramework\Db\QBMapper;
class AnswerMapper extends QBMapper { class AnswerMapper extends QBMapper {
/** /**
* TextMapper constructor. * AnswerMapper constructor.
* @param IDBConnection $db * @param IDBConnection $db
*/ */
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db) {
parent::__construct($db, 'forms_answers', Answer::class); parent::__construct($db, 'forms_v2_answers', Answer::class);
} }
// TODO: Change below functions to search by form and question id
/** /**
* @param int $formId * @param int $submissionId
* @param int $questionId
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Answer[] * @return Answer[]
*/ */
public function findByForm(int $formId, int $questionId): array { public function findBySubmission(int $submissionId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
)
->andWhere(
$qb->expr()->eq('question_id', $qb->createNamedParameter($questionId, IQueryBuilder::PARAM_INT))
);
return $this->findEntities($qb);
}
/**
* @param int $formId
* @param int $questionId
*/
public function deleteByFormAndQuestion(int $formId, int $questionId): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
)
->andWhere(
$qb->expr()->eq('question_id', $qb->createNamedParameter($questionId, IQueryBuilder::PARAM_INT))
);
$qb->execute();
}
/**
* @param int $formId
*/
public function deleteByForm(int $formId): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
);
$qb->execute();
}
public function findById(int $answerId): Answer {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())
->where( ->where(
$qb->expr()->eq('id', $qb->createNamedParameter($answerId)) $qb->expr()->eq('submission_id', $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT))
); );
return $this->findEntity($qb); return $this->findEntities($qb);
} }
public function deleteByQuestion(int $questionId): void { /**
* @param int $submissionId
*/
public function deleteBySubmission(int $submissionId): void {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName()) $qb->delete($this->getTableName())
->where( ->where(
$qb->expr()->eq('question_id', $qb->createNamedParameter($questionId)) $qb->expr()->eq('submission_id', $qb->createNamedParameter($submissionId, IQueryBuilder::PARAM_INT))
); );
$qb->execute(); $qb->execute();
} }
} }

View file

@ -29,44 +29,43 @@ namespace OCA\Forms\Db;
use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\Entity;
/** /**
* @method string getHash()
* @method void setHash(string $value)
* @method string getTitle() * @method string getTitle()
* @method void setTitle(string $value) * @method void setTitle(string $value)
* @method string getDescription() * @method string getDescription()
* @method void setDescription(string $value) * @method void setDescription(string $value)
* @method string getOwner() * @method string getOwnerId()
* @method void setOwner(string $value) * @method void setOwnerId(string $value)
* @method string getCreated()
* @method void setCreated(string $value)
* @method string getAccess() * @method string getAccess()
* @method void setAccess(string $value) * @method void setAccess(string $value)
* @method string getExpire() * @method string getCreated()
* @method void setExpire(string $value) * @method void setCreated(string $value)
* @method string getHash() * @method string getExpirationDate()
* @method void setHash(string $value) * @method void setExpirationDate(string $value)
* @method integer getIsAnonymous() * @method integer getIsAnonymous()
* @method void setIsAnonymous(integer $value) * @method void setIsAnonymous(bool $value)
* @method integer getUnique() * @method integer getSubmitOnce()
* @method void setUnique(integer $value) * @method void setSubmitOnce(bool $value)
*/ */
class Event extends Entity { class Form extends Entity {
protected $hash;
protected $title; protected $title;
protected $description; protected $description;
protected $owner; protected $ownerId;
protected $created;
protected $access; protected $access;
protected $expire; protected $created;
protected $hash; protected $expirationDate;
protected $isAnonymous; protected $isAnonymous;
protected $fullAnonymous; protected $submitOnce;
protected $allowMaybe;
protected $unique;
/** /**
* Event constructor. * Form constructor.
*/ */
public function __construct() { public function __construct() {
$this->addType('isAnonymous', 'integer'); $this->addType('isAnonymous', 'bool');
$this->addType('unique', 'integer'); $this->addType('submitOnce', 'bool');
} }
public function read() { public function read() {
@ -74,12 +73,12 @@ class Event extends Entity {
if (!strpos('|public|hidden|registered', $accessType)) { if (!strpos('|public|hidden|registered', $accessType)) {
$accessType = 'select'; $accessType = 'select';
} }
if ($this->getExpire() === null) { if ($this->getExpirationDate() === null) {
$expired = false; $expired = false;
$expiration = false; $expires = false;
} else { } else {
$expired = time() > strtotime($this->getExpire()); $expired = time() > strtotime($this->getExpirationDate());
$expiration = true; $expires = true;
} }
return [ return [
@ -87,15 +86,15 @@ class Event extends Entity {
'hash' => $this->getHash(), 'hash' => $this->getHash(),
'title' => $this->getTitle(), 'title' => $this->getTitle(),
'description' => $this->getDescription(), 'description' => $this->getDescription(),
'owner' => $this->getOwner(), 'ownerId' => $this->getOwnerId(),
'ownerDisplayName' => \OC_User::getDisplayName($this->getOwner()), 'ownerDisplayName' => \OC_User::getDisplayName($this->getOwnerId()),
'created' => $this->getCreated(), 'created' => $this->getCreated(),
'access' => $accessType, 'access' => $accessType,
'expiration' => $expiration, 'expires' => $expires,
'expired' => $expired, 'expired' => $expired,
'expirationDate' => $this->getExpire(), 'expirationDate' => $this->getExpirationDate(),
'isAnonymous' => $this->getIsAnonymous(), 'isAnonymous' => $this->getIsAnonymous(),
'unique' => $this->getUnique() 'submitOnce' => $this->getSubmitOnce()
]; ];
} }
} }

View file

@ -28,63 +28,63 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\AppFramework\Db\QBMapper; use OCP\AppFramework\Db\QBMapper;
class EventMapper extends QBMapper { class FormMapper extends QBMapper {
/** /**
* EventMapper constructor. * FormMapper constructor.
* @param IDBConnection $db * @param IDBConnection $db
*/ */
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db) {
parent::__construct($db, 'forms_events', Event::class); parent::__construct($db, 'forms_v2_forms', Form::class);
} }
/** /**
* @param Integer $id * @param Integer $id
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result
* @return Event * @return Form
*/ */
public function find(int $id): Event { public function find(int $id): Form {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->tableName)
->where( ->where(
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
); );
return $this->findEntity($qb); return $this->findEntity($qb);
} }
/** /**
* @param String $hash * @param String $hash
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result
* @return Event * @return Form
*/ */
public function findByHash(string $hash): Event { public function findByHash(string $hash): Form {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->tableName)
->where( ->where(
$qb->expr()->eq('hash', $qb->createNamedParameter($hash, IQueryBuilder::PARAM_STR)) $qb->expr()->eq('hash', $qb->createNamedParameter($hash, IQueryBuilder::PARAM_STR))
); );
return $this->findEntity($qb); return $this->findEntity($qb);
} }
/** /**
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Event[] * @return Form[]
*/ */
public function findAll(): array { public function findAll(): array {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()); ->from($this->tableName);
return $this->findEntities($qb); return $this->findEntities($qb);
} }
} }

58
lib/Db/Option.php Normal file
View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Inigo Jiron <ijiron@terpmail.umd.edu>
*
* @author Inigo Jiron <ijiron@terpmail.umd.edu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Forms\Db;
use OCP\AppFramework\Db\Entity;
/**
* @method integer getQuestionId()
* @method void setQuestionId(integer $value)
* @method string getText()
* @method void setText(string $value)
*/
class Option extends Entity {
/** @var int */
protected $questionId;
/** @var string */
protected $text;
/**
* Option constructor.
*/
public function __construct() {
$this->addType('questionId', 'integer');
$this->addType('text', 'string');
}
public function read(): array {
return [
'id' => $this->getId(),
'questionId' => $this->getQuestionId(),
'text' => htmlspecialchars_decode($this->getText()),
];
}
}

84
lib/Db/OptionMapper.php Normal file
View file

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019 Inigo Jiron <ijiron@terpmail.umd.edu>
*
* @author Inigo Jiron <ijiron@terpmail.umd.edu>
* @author Natalie Gilbert <ngilb634@umd.edu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Forms\Db;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\AppFramework\Db\QBMapper;
class OptionMapper extends QBMapper {
/**
* OptionMapper constructor.
* @param IDBConnection $db
*/
public function __construct(IDBConnection $db) {
parent::__construct($db, 'forms_v2_options', Option::class);
}
/**
* @param int $questionId
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Option[]
*/
public function findByQuestion(int $questionId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('question_id', $qb->createNamedParameter($questionId))
);
return $this->findEntities($qb);
}
public function deleteByQuestion(int $questionId): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where(
$qb->expr()->eq('question_id', $qb->createNamedParameter($questionId))
);
$qb->execute();
}
public function findById(int $optionId): Option {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where(
$qb->expr()->eq('id', $qb->createNamedParameter($optionId))
);
return $this->findEntity($qb);
}
}

View file

@ -29,39 +29,34 @@ use OCP\AppFramework\Db\Entity;
/** /**
* @method integer getFormId() * @method integer getFormId()
* @method void setFormId(integer $value) * @method void setFormId(integer $value)
* @method string getFormQuestionType() * @method string getType()
* @method void setFormQuestionType(string $value) * @method void setType(string $value)
* @method string getFormQuestionText() * @method string getText()
* @method void setFormQuestionText(string $value) * @method void setText(string $value)
* @method integer getTimestamp()
* @method void setTimestamp(integer $value)
*/ */
class Question extends Entity { class Question extends Entity {
protected $formId; protected $formId;
protected $formQuestionType; protected $type;
protected $formQuestionText; protected $mandatory;
protected $timestamp; protected $text;
/** /**
* Question constructor. * Question constructor.
*/ */
public function __construct() { public function __construct() {
$this->addType('formId', 'integer'); $this->addType('formId', 'integer');
$this->addType('timestamp', 'integer'); $this->addType('type', 'string');
$this->addType('mandatory', 'bool');
$this->addType('text', 'string');
} }
public function read(): array { public function read(): array {
return [ return [
'id' => $this->getId(), 'id' => $this->getId(),
'formId' => $this->getFormId(), 'formId' => $this->getFormId(),
'type' => htmlspecialchars_decode($this->getFormQuestionType()), 'type' => htmlspecialchars_decode($this->getType()),
'text' => htmlspecialchars_decode($this->getFormQuestionText()), 'mandatory' => $this->getMandatory(),
'timestamp' => $this->getTimestamp() 'text' => htmlspecialchars_decode($this->getText()),
]; ];
} }
} }

View file

@ -28,16 +28,22 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\AppFramework\Db\QBMapper; use OCP\AppFramework\Db\QBMapper;
use OCA\Forms\Db\OptionMapper;
class QuestionMapper extends QBMapper { class QuestionMapper extends QBMapper {
public function __construct(IDBConnection $db) { private $optionMapper;
parent::__construct($db, 'forms_questions', Question::class);
public function __construct(IDBConnection $db, OptionMapper $optionMapper) {
parent::__construct($db, 'forms_v2_questions', Question::class);
$this->optionMapper = $optionMapper;
} }
/** /**
* @param int $formId * @param int $formId
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Option[] * @return Question[]
*/ */
public function findByForm(int $formId): array { public function findByForm(int $formId): array {
@ -58,6 +64,13 @@ class QuestionMapper extends QBMapper {
public function deleteByForm(int $formId): void { public function deleteByForm(int $formId): void {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
// First delete corresponding options.
$questionEntities = $this->findByForm($formId);
foreach ($questionEntities as $questionEntity) {
$this->optionMapper->deleteByQuestion($questionEntity->id);
}
// Delete Questions
$qb->delete($this->getTableName()) $qb->delete($this->getTableName())
->where( ->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
@ -72,7 +85,7 @@ class QuestionMapper extends QBMapper {
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())
->where( ->where(
$qb->expr()->eq('id', $qb->createNamedParameter($questionId)) $qb->expr()->eq('id', $qb->createNamedParameter($questionId, IQueryBuilder::PARAM_INT))
); );
return $this->findEntity($qb); return $this->findEntity($qb);

View file

@ -2,12 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright Copyright (c) 2017 Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com> * @copyright Copyright (c) 2020 Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
* *
* @author Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com>
* @author Kai Schröer <git@schroeer.co>
* @author René Gieling <github@dartcafe.de>
*
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -34,39 +32,27 @@ use OCP\AppFramework\Db\Entity;
* @method void setFormId(integer $value) * @method void setFormId(integer $value)
* @method string getUserId() * @method string getUserId()
* @method void setUserId(string $value) * @method void setUserId(string $value)
* @method integer getVoteOptionId() * @method string getTimestamp()
* @method void setVoteOptionId(integer $value) * @method void setTimestamp(string $value)
* @method string getVoteOptionText()
* @method void setVoteOptionText(string $value)
* @method string getVoteAnswer()
* @method void setVoteAnswer(string $value)
* @method string getVoteOptionType()
* @method void setVoteOptionType(string $value)
*/ */
class Vote extends Entity { class Submission extends Entity {
protected $formId; protected $formId;
protected $userId; protected $userId;
protected $voteOptionId; protected $timestamp;
protected $voteOptionText;
protected $voteAnswer;
protected $voteOptionType;
/** /**
* Options constructor. * Submission constructor.
*/ */
public function __construct() { public function __construct() {
$this->addType('formId', 'integer'); $this->addType('formId', 'integer');
$this->addType('voteOptionId', 'integer');
} }
public function read(): array { public function read(): array {
return [ return [
'id' => $this->getId(), 'id' => $this->getId(),
'formId' => $this->getFormId(),
'userId' => $this->getUserId(), 'userId' => $this->getUserId(),
'voteOptionId' => $this->getVoteOptionId(), 'timestamp' => $this->getTimestamp(),
'voteOptionText' => htmlspecialchars_decode($this->getVoteOptionText()),
'voteAnswer' => $this->getVoteAnswer(),
'voteOptionType' => $this->getVoteOptionType()
]; ];
} }

View file

@ -1,10 +1,9 @@
<?php <?php
/** /**
* @copyright Copyright (c) 2017 Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com> * @copyright Copyright (c) 2020 Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
* *
* @author Vinzenz Rosenkranz <vinzenz.rosenkranz@gmail.com>
* @author René Gieling <github@dartcafe.de>
*
* @license GNU AGPL version 3 or any later version * @license GNU AGPL version 3 or any later version
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -28,31 +27,38 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\AppFramework\Db\QBMapper; use OCP\AppFramework\Db\QBMapper;
class VoteMapper extends QBMapper { use OCA\Forms\Db\AnswerMapper;
class SubmissionMapper extends QBMapper {
private $answerMapper;
/** /**
* VoteMapper constructor. * SubmissionMapper constructor.
* @param IDBConnection $db * @param IDBConnection $db
* @param AnswerMapper $answerMapper
*/ */
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db, AnswerMapper $answerMapper) {
parent::__construct($db, 'forms_votes', Vote::class); parent::__construct($db, 'forms_v2_submissions', Submission::class);
$this->answerMapper = $answerMapper;
} }
/** /**
* @param int $formId * @param int $formId
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found * @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @return Vote[] * @return Submission[]
*/ */
public function findByForm(int $formId): array { public function findByForm(int $formId): array {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())
->where( ->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
); );
return $this->findEntities($qb); return $this->findEntities($qb);
} }
/** /**
@ -63,31 +69,13 @@ class VoteMapper extends QBMapper {
public function findParticipantsByForm(int $formId, $limit = null, $offset = null): array { public function findParticipantsByForm(int $formId, $limit = null, $offset = null): array {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('user_id') $qb->select('user_id')
->from($this->getTableName()) ->from($this->getTableName())
->where( ->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
); );
return $this->findEntities($qb); return $this->findEntities($qb);
}
/**
* @param int $formId
* @param string $userId
*/
public function deleteByFormAndUser(int $formId, string $userId): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))
)
->andWhere(
$qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
);
$qb->execute();
} }
/** /**
@ -96,6 +84,13 @@ class VoteMapper extends QBMapper {
public function deleteByForm(int $formId): void { public function deleteByForm(int $formId): void {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
// First delete corresponding answers.
$submissionEntities = $this->findByForm($formId);
foreach ($submissionEntities as $submissionEntity) {
$this->answerMapper->deleteBySubmission($submissionEntity->id);
}
//Delete Submissions
$qb->delete($this->getTableName()) $qb->delete($this->getTableName())
->where( ->where(
$qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT))

View file

@ -1,210 +0,0 @@
<?php
/**
* @copyright Copyright (c) 2017 René Gieling <github@dartcafe.de>
*
* @author René Gieling <github@dartcafe.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Forms\Migration;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
use Doctrine\DBAL\Types\Type;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
/**
* Installation class for the forms app.
* Initial db creation
*/
class Version0009Date20190000000006 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
/** @var IConfig */
protected $config;
/**
* @param IDBConnection $connection
* @param IConfig $config
*/
public function __construct(IDBConnection $connection, IConfig $config) {
$this->connection = $connection;
$this->config = $config;
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('forms_events')) {
$table = $schema->createTable('forms_events');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('hash', Type::STRING, [
'notnull' => false,
'length' => 64,
]);
$table->addColumn('title', Type::STRING, [
'notnull' => true,
'length' => 128,
]);
$table->addColumn('description', Type::STRING, [
'notnull' => true,
'length' => 1024,
]);
$table->addColumn('owner', Type::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('created', Type::DATETIME, [
'notnull' => false,
]);
$table->addColumn('access', Type::STRING, [
'notnull' => false,
'length' => 1024,
]);
$table->addColumn('expire', Type::DATETIME, [
'notnull' => false,
]);
$table->addColumn('is_anonymous', Type::INTEGER, [
'notnull' => false,
'default' => 0,
]);
$table->addColumn('full_anonymous', Type::INTEGER, [
'notnull' => false,
'default' => 0,
]);
$table->setPrimaryKey(['id']);
} else {
}
if (!$schema->hasTable('forms_questions')) {
$table = $schema->createTable('forms_questions');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('form_id', Type::INTEGER, [
'notnull' => false,
]);
$table->addColumn('form_question_type', Type::STRING, [
'notnull' => false, // maybe true?
'length' => 256,
]);
$table->addColumn('form_question_text', Type::STRING, [
'notnull' => false, // maybe true?
'length' => 4096,
]);
$table->addColumn('timestamp', Type::INTEGER, [
'notnull' => false,
'default' => 0
]);
$table->setPrimaryKey(['id']);
}
if (!$schema->hasTable('forms_answers')) {
$table = $schema->createTable('forms_answers');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('form_id', Type::INTEGER, [
'notnull' => false,
]);
$table->addColumn('question_id', Type::INTEGER, [
'notnull' => false,
]);
$table->addColumn('text', Type::STRING, [
'notnull' => false, // maybe true?
'length' => 4096,
]);
$table->addColumn('timestamp', Type::INTEGER, [
'notnull' => false,
'default' => 0
]);
$table->setPrimaryKey(['id']);
}
if (!$schema->hasTable('forms_votes')) {
$table = $schema->createTable('forms_votes');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('form_id', Type::INTEGER, [
'notnull' => false,
]);
$table->addColumn('user_id', Type::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('vote_option_id', Type::INTEGER, [
'notnull' => true,
'default' => 0,
'length' => 64,
]);
$table->addColumn('vote_option_text', Type::STRING, [
'notnull' => false, // maybe true?
'length' => 4096,
]);
$table->addColumn('vote_answer', Type::STRING, [
'notnull' => false,
'length' => 4096,
]);
$table->addColumn('vote_option_type', Type::STRING, [
'notnull' => false,
'length' => 256,
]);
$table->setPrimaryKey(['id']);
}
if (!$schema->hasTable('forms_notif')) {
$table = $schema->createTable('forms_notif');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('form_id', Type::INTEGER, [
'notnull' => false,
]);
$table->addColumn('user_id', Type::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->setPrimaryKey(['id']);
}
return $schema;
}
}

View file

@ -23,7 +23,9 @@ class Version010102Date20200323120846 extends SimpleMigrationStep {
/** @var ISchemaWrapper $schema */ /** @var ISchemaWrapper $schema */
$schema = $schemaClosure(); $schema = $schemaClosure();
$schema->dropTable('forms_notif'); if ($schema->hasTable('forms_notif')) {
$schema->dropTable('forms_notif');
}
return $schema; return $schema;
} }

View file

@ -0,0 +1,352 @@
<?php
/**
* @copyright Copyright (c) 2020 Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Forms\Migration;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
use Doctrine\DBAL\Types\Type;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
/**
* Installation class for the forms app.
* Initial db creation
*/
class Version010200Date2020323141300 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
/** @var IConfig */
protected $config;
/**
* @param IDBConnection $connection
* @param IConfig $config
*/
public function __construct(IDBConnection $connection, IConfig $config) {
$this->connection = $connection;
$this->config = $config;
}
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('forms_v2_forms')) {
$table = $schema->createTable('forms_v2_forms');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('hash', Type::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('title', Type::STRING, [
'notnull' => true,
'length' => 256,
]);
$table->addColumn('description', Type::STRING, [
'notnull' => false,
'length' => 2048,
]);
$table->addColumn('owner_id', Type::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('access', Type::STRING, [
'notnull' => false,
'length' => 1024,
]);
$table->addColumn('created', Type::DATETIME, [
'notnull' => false,
]);
$table->addColumn('expiration_date', Type::DATETIME, [
'notnull' => false,
]);
$table->addColumn('is_anonymous', Type::BOOLEAN, [
'notnull' => true,
'default' => 0,
]);
$table->addColumn('submit_once', Type::BOOLEAN, [
'notnull' => true,
'default' => 0,
]);
$table->setPrimaryKey(['id']);
}
if (!$schema->hasTable('forms_v2_questions')) {
$table = $schema->createTable('forms_v2_questions');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('form_id', Type::INTEGER, [
'notnull' => true,
]);
$table->addColumn('type', Type::STRING, [
'notnull' => true,
'length' => 256,
]);
$table->addColumn('mandatory', Type::BOOLEAN, [
'notnull' => true,
'default' => 1,
]);
$table->addColumn('text', Type::STRING, [
'notnull' => true,
'length' => 2048,
]);
$table->setPrimaryKey(['id']);
}
if (!$schema->hasTable('forms_v2_options')) {
$table = $schema->createTable('forms_v2_options');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('question_id', Type::INTEGER, [
'notnull' => true,
]);
$table->addColumn('text', Type::STRING, [
'notnull' => true,
'length' => 1024,
]);
$table->setPrimaryKey(['id']);
}
if (!$schema->hasTable('forms_v2_submissions')) {
$table = $schema->createTable('forms_v2_submissions');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('form_id', Type::INTEGER, [
'notnull' => true,
]);
$table->addColumn('user_id', Type::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('timestamp', Type::DATETIME, [
'notnull' => false,
]);
$table->setPrimaryKey(['id']);
}
if (!$schema->hasTable('forms_v2_answers')) {
$table = $schema->createTable('forms_v2_answers');
$table->addColumn('id', Type::INTEGER, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('submission_id', Type::INTEGER, [
'notnull' => true,
]);
$table->addColumn('question_id', Type::INTEGER, [
'notnull' => true,
]);
$table->addColumn('text', Type::STRING, [
'notnull' => true,
'length' => 2048,
]);
$table->setPrimaryKey(['id']);
}
return $schema;
}
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
// if Database exists.
if( $schema->hasTable('forms_events') ){
$id_mapping = [];
$id_mapping['events'] = []; // Maps oldevent-id => newevent-id
$id_mapping['questions'] = []; // Maps oldquestion-id => newquestion-id
//Fetch & Restore Events
$qb_fetch = $this->connection->getQueryBuilder();
$qb_restore = $this->connection->getQueryBuilder();
$qb_fetch->select('id', 'hash', 'title', 'description', 'owner', 'created', 'access', 'expire', 'is_anonymous', 'unique')
->from('forms_events');
$cursor = $qb_fetch->execute();
while( $event = $cursor->fetch() ){
$qb_restore->insert('forms_v2_forms')
->values([
'hash' => $qb_restore->createNamedParameter($event['hash'], IQueryBuilder::PARAM_STR),
'title' => $qb_restore->createNamedParameter($event['title'], IQueryBuilder::PARAM_STR),
'description' => $qb_restore->createNamedParameter($event['description'], IQueryBuilder::PARAM_STR),
'owner_id' => $qb_restore->createNamedParameter($event['owner'], IQueryBuilder::PARAM_STR),
'access' => $qb_restore->createNamedParameter($event['access'], IQueryBuilder::PARAM_STR),
'created' => $qb_restore->createNamedParameter($event['created'], IQueryBuilder::PARAM_STR),
'expiration_date' => $qb_restore->createNamedParameter($event['expire'], IQueryBuilder::PARAM_STR),
'is_anonymous' => $qb_restore->createNamedParameter($event['is_anonymous'], IQueryBuilder::PARAM_BOOL),
'submit_once' => $qb_restore->createNamedParameter($event['unique'], IQueryBuilder::PARAM_BOOL)
]);
$qb_restore->execute();
$id_mapping['events'][$event['id']] = $qb_restore->getLastInsertId(); //Store new form-id to connect questions to new form.
}
$cursor->closeCursor();
//Fetch & restore Questions
$qb_fetch = $this->connection->getQueryBuilder();
$qb_restore = $this->connection->getQueryBuilder();
$qb_fetch->select('id', 'form_id', 'form_question_type', 'form_question_text')
->from('forms_questions');
$cursor = $qb_fetch->execute();
while( $question = $cursor->fetch() ){
//In case the old Question would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
if(strlen($question['form_question_text']) > 2048) {
$output->warning("Question-text is too long for new Database: '" . $question['form_question_text'] . "'");
$question['form_question_text'] = substr($question['form_question_text'], 0, 2048);
}
$qb_restore->insert('forms_v2_questions')
->values([
'form_id' => $qb_restore->createNamedParameter($id_mapping['events'][$question['form_id']], IQueryBuilder::PARAM_INT),
'type' => $qb_restore->createNamedParameter($question['form_question_type'], IQueryBuilder::PARAM_STR),
'text' => $qb_restore->createNamedParameter($question['form_question_text'], IQueryBuilder::PARAM_STR)
]);
$qb_restore->execute();
$id_mapping['questions'][$question['id']] = $qb_restore->getLastInsertId(); //Store new question-id to connect options to new question.
}
$cursor->closeCursor();
//Fetch all Answers and restore to Options
$qb_fetch = $this->connection->getQueryBuilder();
$qb_restore = $this->connection->getQueryBuilder();
$qb_fetch->select('question_id', 'text')
->from('forms_answers');
$cursor = $qb_fetch->execute();
while( $answer = $cursor->fetch() ){
//In case the old Answer would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
if(strlen($answer['text']) > 1024) {
$output->warning("Option-text is too long for new Database: '" . $answer['text'] . "'");
$answer['text'] = substr($answer['text'], 0, 1024);
}
$qb_restore->insert('forms_v2_options')
->values([
'question_id' => $qb_restore->createNamedParameter($id_mapping['questions'][$answer['question_id']], IQueryBuilder::PARAM_INT),
'text' => $qb_restore->createNamedParameter($answer['text'], IQueryBuilder::PARAM_STR)
]);
$qb_restore->execute();
}
$cursor->closeCursor();
/* Fetch old id_structure of event-ids and question-ids
* This is necessary to restore the $oldQuestionId, as the vote_option_ids do not use the true question_ids
*/
$event_structure = [];
$qb_fetch = $this->connection->getQueryBuilder();
$qb_fetch->select('id')
->from('forms_events');
$cursor = $qb_fetch->execute();
while( $tmp = $cursor->fetch() ){
$event_structure[$tmp['id']] = $tmp;
}
$cursor->closeCursor();
foreach ($event_structure as $eventkey => $event) {
$event_structure[$eventkey]['questions'] = [];
$qb_fetch = $this->connection->getQueryBuilder();
$qb_fetch->select('id', 'form_question_text')
->from('forms_questions')
->where($qb_fetch->expr()->eq('form_id', $qb_fetch->createNamedParameter($event['id'], IQueryBuilder::PARAM_INT)));
$cursor = $qb_fetch->execute();
while( $tmp = $cursor->fetch() ) {
$event_structure[$event['id']]['questions'][] = $tmp;
}
$cursor->closeCursor();
}
//Fetch Votes and restore to Submissions & Answers
$qb_fetch = $this->connection->getQueryBuilder();
$qb_restore = $this->connection->getQueryBuilder();
//initialize $last_vote
$last_vote = [];
$last_vote['form_id'] = 0;
$last_vote['user_id'] = '';
$last_vote['vote_option_id'] = 0;
$qb_fetch->select('id', 'form_id', 'user_id', 'vote_option_id', 'vote_option_text', 'vote_answer')
->from('forms_votes');
$cursor = $qb_fetch->execute();
while( $vote = $cursor->fetch() ){
//If the form changed, if the user changed or if vote_option_id became smaller than last one, then a new submission is interpreted.
if ( ($vote['form_id'] != $last_vote['form_id']) || ($vote['user_id'] != $last_vote['user_id']) || ($vote['vote_option_id'] < $last_vote['vote_option_id'])) {
$qb_restore->insert('forms_v2_submissions')
->values([
'form_id' => $qb_restore->createNamedParameter($id_mapping['events'][$vote['form_id']], IQueryBuilder::PARAM_INT),
'user_id' => $qb_restore->createNamedParameter($vote['user_id'], IQueryBuilder::PARAM_STR),
'timestamp' => $qb_restore->createNamedParameter(date('Y-m-d H:i:s'), IQueryBuilder::PARAM_STR) //Information not available. Just using Migration-Timestamp.
]);
$qb_restore->execute();
$id_mapping['currentSubmission'] = $qb_restore->getLastInsertId(); //Store submission-id to connect answers to submission.
}
$last_vote = $vote;
//In case the old Answer would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
if(strlen($vote['vote_answer']) > 2048) {
$output->warning("Answer-text is too long for new Database: '" . $vote['vote_answer'] . "'");
$vote['vote_answer'] = substr($vote['vote_answer'], 0, 2048);
}
/* Due to the unconventional storing fo vote_option_ids, the vote_option_id needs to get mapped onto old question-id and from there to new question-id.
* vote_option_ids count from 1 to x for the questions of a form. So the question at point [$vote[vote_option_id] - 1] within the id-structure is the corresponding question.
*/
$oldQuestionId = $event_structure[$vote['form_id']]['questions'][$vote['vote_option_id']-1]['id'];
//Just throw an Error, if aboves QuestionId-Mapping went wrong. Double-Checked by Question-Text.
if ($event_structure[$vote['form_id']]['questions'][$vote['vote_option_id']-1]['form_question_text'] != $vote['vote_option_text']) {
$output->warning("Some Question-Mapping went wrong within Submission-Mapping to new Database. On 'vote_id': " . $vote['id'] . " - 'vote_option_text': '" . $vote['vote_option_text'] . "'");
}
$qb_restore->insert('forms_v2_answers')
->values([
'submission_id' => $qb_restore->createNamedParameter($id_mapping['currentSubmission'], IQueryBuilder::PARAM_INT),
'question_id' => $qb_restore->createNamedParameter($id_mapping['questions'][$oldQuestionId], IQueryBuilder::PARAM_STR),
'text' => $qb_restore->createNamedParameter($vote['vote_answer'], IQueryBuilder::PARAM_STR)
]);
$qb_restore->execute();
}
}
}
}

View file

@ -107,7 +107,7 @@ export default {
selectedForm() { selectedForm() {
// TODO: replace with form.hash // TODO: replace with form.hash
return this.forms.find(form => form.event.hash === this.hash) return this.forms.find(form => form.form.hash === this.hash)
}, },
}, },
@ -141,7 +141,7 @@ export default {
const response = await axios.post(generateUrl('/apps/forms/api/v1/form')) const response = await axios.post(generateUrl('/apps/forms/api/v1/form'))
const newForm = response.data const newForm = response.data
this.forms.push(newForm) this.forms.push(newForm)
this.$router.push({ name: 'edit', params: { hash: newForm.event.hash } }) this.$router.push({ name: 'edit', params: { hash: newForm.form.hash } })
} catch (error) { } catch (error) {
showError(t('forms', 'Unable to create a new form')) showError(t('forms', 'Unable to create a new form'))
console.error(error) console.error(error)

View file

@ -26,8 +26,8 @@
<span v-if="options.expired" class="expired"> <span v-if="options.expired" class="expired">
{{ t('forms', 'Expired') }} {{ t('forms', 'Expired') }}
</span> </span>
<span v-if="options.expiration" class="open"> <span v-if="options.expires" class="open">
{{ t('forms', 'Expires %n', 1, expirationdate) }} {{ t('forms', 'Expires %n', 1, expirationDate) }}
</span> </span>
<span v-else class="open"> <span v-else class="open">
{{ t('forms', 'Expires never') }} {{ t('forms', 'Expires never') }}
@ -39,12 +39,6 @@
<span v-if="options.isAnonymous" class="information"> <span v-if="options.isAnonymous" class="information">
{{ t('forms', 'Anonymous form') }} {{ t('forms', 'Anonymous form') }}
</span> </span>
<span v-if="options.fullAnonymous" class="information">
{{ t('forms', 'Usernames hidden to Owner') }}
</span>
<span v-if="options.isAnonymous & !options.fullAnonymous" class="information">
{{ t('forms', 'Usernames visible to Owner') }}
</span>
</div> </div>
</template> </template>
@ -61,7 +55,7 @@ export default {
}, },
computed: { computed: {
expirationdate() { expirationDate() {
const date = moment(this.options.expirationDate, moment.localeData().longDateFormat('L')).fromNow() const date = moment(this.options.expirationDate, moment.localeData().longDateFormat('L')).fromNow()
return date return date
}, },

View file

@ -72,12 +72,12 @@
<img class="icontwo"> <img class="icontwo">
</div> </div>
<div class="symbol icon-voted" /> <div class="symbol icon-voted" />
<a :href="voteUrl" class="wrapper group-1-1"> <a :href="submitUrl" class="wrapper group-1-1">
<div class="name"> <div class="name">
{{ form.event.title }} {{ form.form.title }}
</div> </div>
<div class="description"> <div class="description">
{{ form.event.description }} {{ form.form.description }}
</div> </div>
</a> </a>
</div> </div>
@ -102,18 +102,18 @@
</div> </div>
<div class="wrapper group-2-1"> <div class="wrapper group-2-1">
<div v-tooltip="accessType" class="thumbnail access" :class="form.event.access"> <div v-tooltip="accessType" class="thumbnail access" :class="form.form.access">
{{ accessType }} {{ accessType }}
</div> </div>
</div> </div>
<div class="owner"> <div class="owner">
<UserDiv :user-id="form.event.owner" :display-name="form.event.ownerDisplayName" /> <UserDiv :user-id="form.form.ownerId" :display-name="form.form.ownerDisplayName" />
</div> </div>
<div class="wrapper group-2-2"> <div class="wrapper group-2-2">
<div class="created "> <div class="created ">
{{ timeSpanCreated }} {{ timeSpanCreated }}
</div> </div>
<div class="expiry" :class="{ expired : form.event.expired }"> <div class="expiry" :class="{ expired : form.form.expired }">
{{ timeSpanExpiration }} {{ timeSpanExpiration }}
</div> </div>
</div> </div>
@ -150,13 +150,13 @@ export default {
computed: { computed: {
accessType() { accessType() {
if (this.form.event.access === 'public') { if (this.form.form.access === 'public') {
return t('forms', 'Public access') return t('forms', 'Public access')
} else if (this.form.event.access === 'select') { } else if (this.form.form.access === 'select') {
return t('forms', 'Only shared') return t('forms', 'Only shared')
} else if (this.form.event.access === 'registered') { } else if (this.form.form.access === 'registered') {
return t('forms', 'Registered users only') return t('forms', 'Registered users only')
} else if (this.form.event.access === 'hidden') { } else if (this.form.form.access === 'hidden') {
return t('forms', 'Hidden form') return t('forms', 'Hidden form')
} else { } else {
return '' return ''
@ -164,12 +164,12 @@ export default {
}, },
timeSpanCreated() { timeSpanCreated() {
return moment(this.form.event.created, 'YYYY-MM-DD HH:mm') return moment(this.form.form.created, 'YYYY-MM-DD HH:mm')
}, },
timeSpanExpiration() { timeSpanExpiration() {
if (this.form.event.expiration) { if (this.form.form.expires) {
return moment(this.form.event.expirationDate) return moment(this.form.form.expirationDate)
} else { } else {
return t('forms', 'never') return t('forms', 'never')
} }
@ -179,8 +179,8 @@ export default {
return this.form.shares.length return this.form.shares.length
}, },
voteUrl() { submitUrl() {
return OC.generateUrl('apps/forms/form/') + this.form.event.hash return OC.generateUrl('apps/forms/form/') + this.form.form.hash
}, },
}, },
@ -196,7 +196,7 @@ export default {
copyLink() { copyLink() {
// this.$emit('copyLink') // this.$emit('copyLink')
this.$copyText(window.location.origin + this.voteUrl).then( this.$copyText(window.location.origin + this.submitUrl).then(
function(e) { function(e) {
OC.Notification.showTemporary(t('forms', 'Link copied to clipboard')) OC.Notification.showTemporary(t('forms', 'Link copied to clipboard'))
}, },
@ -217,6 +217,7 @@ export default {
}, },
} }
</script> </script>
<style lang="scss"> <style lang="scss">
$row-padding: 15px; $row-padding: 15px;
$table-padding: 4px; $table-padding: 4px;
@ -293,8 +294,6 @@ $mediabreak-3: $group-1-width + $owner-width + max($group-2-1-width, $group-2-2-
align-items: center; align-items: center;
position: relative; position: relative;
flex-grow: 0; flex-grow: 0;
div {
}
} }
.name { .name {

View file

@ -24,21 +24,20 @@
<div>{{ question.text }}</div> <div>{{ question.text }}</div>
<div> <div>
<input v-show="(question.type != 'text') && (question.type != 'comment')" <input v-show="(question.type != 'text') && (question.type != 'comment')"
v-model="newQuizAnswer" v-model="newOption"
style="height:30px;" style="height:30px;"
:placeholder=" t('forms', 'Add Answer')" :placeholder=" t('forms', 'Add Option')"
@keyup.enter="emitNewAnswer(question)"> @keyup.enter="emitNewOption(question)">
<transitionGroup <transitionGroup
id="form-list" id="form-list"
name="list" name="list"
tag="ul" tag="ul"
class="form-table"> class="form-table">
<TextFormItem <TextFormItem
v-for="(answer, index) in answers" v-for="(opt, index) in options"
:key="answer.id" :key="opt.id"
:option="answer" :option="opt"
@remove="emitRemoveAnswer(question, answer, index)" @remove="emitRemoveOption(question, opt, index)" />
@delete="question.answers.splice(index, 1)" />
</transitionGroup> </transitionGroup>
</div> </div>
<div> <div>
@ -61,25 +60,25 @@ export default {
}, },
data() { data() {
return { return {
nextQuizAnswerId: 1, nextOptionId: 1,
newQuizAnswer: '', newOption: '',
type: '', type: '',
} }
}, },
computed: { computed: {
answers() { options() {
return this.question.answers || [] return this.question.options || []
}, },
}, },
methods: { methods: {
emitNewAnswer(question) { emitNewOption(question) {
this.$emit('add-answer', this, question) this.$emit('addOption', this, question)
}, },
emitRemoveAnswer(question, answer, index) { emitRemoveOption(question, option, index) {
this.$emit('remove-answer', question, answer, index) this.$emit('deleteOption', question, option, index)
}, },
}, },
} }

View file

@ -66,7 +66,7 @@
</div> </div>
<div class="wrapper group-2"> <div class="wrapper group-2">
<div class="ans"> <div class="ans">
{{ answers }} {{ answerText }}
</div> </div>
</div> </div>
</div> </div>
@ -83,7 +83,7 @@ export default {
type: Object, type: Object,
default: undefined, default: undefined,
}, },
vote: { answer: {
type: Object, type: Object,
default: undefined, default: undefined,
}, },
@ -98,16 +98,16 @@ export default {
computed: { computed: {
participants() { participants() {
return this.vote.userId return this.answer.userId
}, },
questionText() { questionText() {
return this.vote.voteOptionText return this.answer.questionText
}, },
answers() { answerText() {
return this.vote.voteAnswer return this.answer.text
}, },
questionNum() { questionNum() {
return this.vote.voteOptionId return this.answer.questionId
}, },
}, },
} }

View file

@ -27,17 +27,10 @@
*/ */
const formatForm = function(form) { const formatForm = function(form) {
// clone form // clone form
const newForm = Object.assign({}, form, form.event) const newForm = Object.assign({}, form, form.form)
// migrate object architecture
Object.assign(newForm, {
questions: form.options && form.options.formQuizQuestions,
})
// cleanup // cleanup
delete newForm.options
delete newForm.event delete newForm.event
return newForm return newForm
} }

View file

@ -33,12 +33,12 @@
<label>{{ t('forms', 'Title') }}</label> <label>{{ t('forms', 'Title') }}</label>
<input id="formTitle" <input id="formTitle"
v-model="form.event.title" v-model="form.form.title"
:class="{ error: titleEmpty }" :class="{ error: titleEmpty }"
type="text"> type="text">
<label>{{ t('forms', 'Description') }}</label> <label>{{ t('forms', 'Description') }}</label>
<textarea id="formDesc" v-model="form.event.description" style="resize: vertical; width: 100%;" /> <textarea id="formDesc" v-model="form.form.description" style="resize: vertical; width: 100%;" />
</div> </div>
<div> <div>
@ -50,11 +50,15 @@
<option value="" disabled> <option value="" disabled>
Select Select
</option> </option>
<option v-for="option in options" :key="option.value" :value="option.value"> <option v-for="type in questionTypes" :key="type.value" :value="type.value">
{{ option.text }} {{ type.text }}
</option> </option>
</select> </select>
<input v-model="newQuizQuestion" :placeholder="t('forms', 'Add Question')" @keyup.enter="addQuestion()"> <input
v-model="newQuestion"
:placeholder=" t('forms', 'Add Question') "
maxlength="2048"
@keyup.enter="addQuestion()">
<button id="questButton" <button id="questButton"
@click="addQuestion()"> @click="addQuestion()">
{{ t('forms', 'Add Question') }} {{ t('forms', 'Add Question') }}
@ -67,12 +71,12 @@
tag="ul" tag="ul"
class="form-table"> class="form-table">
<QuizFormItem <QuizFormItem
v-for="(question, index) in form.options.formQuizQuestions" v-for="(question, index) in form.questions"
:key="question.id" :key="question.id"
:question="question" :question="question"
:type="question.type" :type="question.type"
@add-answer="addAnswer" @addOption="addOption"
@remove-answer="deleteAnswer" @deleteOption="deleteOption"
@deleteQuestion="deleteQuestion(question, index)" /> @deleteQuestion="deleteQuestion(question, index)" />
</transitionGroup> </transitionGroup>
</div> </div>
@ -105,18 +109,18 @@ export default {
data() { data() {
return { return {
placeholder: '', placeholder: '',
newQuizAnswer: '', newOption: '',
newQuizQuestion: '', newQuestion: '',
nextQuizAnswerId: 1, nextOptionId: 1,
nextQuizQuestionId: 1, nextQuestionId: 1,
writingForm: false, writingForm: false,
loadingForm: true, loadingForm: true,
titleEmpty: false, titleEmpty: false,
selected: '', selected: '',
uniqueName: false, uniqueQuestionText: false,
uniqueAns: false, uniqueOptionText: false,
haveAns: false, allHaveOpt: false,
options: [ questionTypes: [
{ text: 'Radio Buttons', value: 'radiogroup' }, { text: 'Radio Buttons', value: 'radiogroup' },
{ text: 'Checkboxes', value: 'checkbox' }, { text: 'Checkboxes', value: 'checkbox' },
{ text: 'Short Response', value: 'text' }, { text: 'Short Response', value: 'text' },
@ -132,10 +136,10 @@ export default {
}, },
title() { title() {
if (this.form.event.title === '') { if (this.form.form.title === '') {
return t('forms', 'Create new form') return t('forms', 'Create new form')
} else { } else {
return this.form.event.title return this.form.form.title
} }
}, },
@ -173,7 +177,7 @@ export default {
created() { created() {
if (this.$route.name === 'create') { if (this.$route.name === 'create') {
// TODO: manage this from Forms.vue, request a new form to the server // TODO: manage this from Forms.vue, request a new form to the server
this.form.event.owner = OC.getCurrentUser().uid this.form.form.owner = OC.getCurrentUser().uid
this.loadingForm = false this.loadingForm = false
} else if (this.$route.name === 'edit') { } else if (this.$route.name === 'edit') {
// TODO: fetch & update form? // TODO: fetch & update form?
@ -189,81 +193,81 @@ export default {
this.sidebar = !this.sidebar this.sidebar = !this.sidebar
}, },
checkNames() { checkQuestionText() {
this.uniqueName = true this.uniqueQuestionText = true
this.form.options.formQuizQuestions.forEach(q => { this.form.questions.forEach(q => {
if (q.text === this.newQuizQuestion) { if (q.text === this.newQuestion) {
this.uniqueName = false this.uniqueQuestionText = false
} }
}) })
}, },
async addQuestion() { async addQuestion() {
this.checkNames() this.checkQuestionText()
if (this.selected === '') { if (this.selected === '') {
showError(t('forms', 'Select a question type!'), { duration: 3000 }) showError(t('forms', 'Select a question type!'), { duration: 3000 })
} else if (!this.uniqueName) { } else if (!this.uniqueQuestionText) {
showError(t('forms', 'Cannot have the same question!')) showError(t('forms', 'Cannot have the same question!'))
} else { } else {
if (this.newQuizQuestion !== null & this.newQuizQuestion !== '' & (/\S/.test(this.newQuizQuestion))) { if (this.newQuestion !== null & this.newQuestion !== '' & (/\S/.test(this.newQuestion))) {
const response = await axios.post(generateUrl('/apps/forms/api/v1/question/'), { formId: this.form.id, type: this.selected, text: this.newQuizQuestion }) const response = await axios.post(generateUrl('/apps/forms/api/v1/question/'), { formId: this.form.id, type: this.selected, text: this.newQuestion })
const questionId = response.data const questionId = response.data
this.form.options.formQuizQuestions.push({ this.form.questions.push({
id: questionId, id: questionId,
text: this.newQuizQuestion, text: this.newQuestion,
type: this.selected, type: this.selected,
answers: [], options: [],
}) })
} }
this.newQuizQuestion = '' this.newQuestion = ''
} }
}, },
async deleteQuestion(question, index) { async deleteQuestion(question, index) {
await axios.delete(generateUrl('/apps/forms/api/v1/question/{id}', { id: question.id })) await axios.delete(generateUrl('/apps/forms/api/v1/question/{id}', { id: question.id }))
// TODO catch Error // TODO catch Error
this.form.options.formQuizQuestions.splice(index, 1) this.form.questions.splice(index, 1)
}, },
checkAnsNames(item, question) { checkOptionText(item, question) {
this.uniqueAnsName = true this.uniqueOptionText = true
question.answers.forEach(q => { question.options.forEach(o => {
if (q.text === item.newQuizAnswer) { if (o.text === item.newOption) {
this.uniqueAnsName = false this.uniqueOptionText = false
} }
}) })
}, },
async addAnswer(item, question) { async addOption(item, question) {
this.checkAnsNames(item, question) this.checkOptionText(item, question)
if (!this.uniqueAnsName) { if (!this.uniqueOptionText) {
showError(t('forms', 'Two answers cannot be the same!'), { duration: 3000 }) showError(t('forms', 'Two options cannot be the same!'), { duration: 3000 })
} else { } else {
if (item.newQuizAnswer !== null & item.newQuizAnswer !== '' & (/\S/.test(item.newQuizAnswer))) { if (item.newOption !== null & item.newOption !== '' & (/\S/.test(item.newOption))) {
const response = await axios.post(generateUrl('/apps/forms/api/v1/answer/'), { formId: this.form.id, questionId: question.id, text: item.newQuizAnswer }) const response = await axios.post(generateUrl('/apps/forms/api/v1/option/'), { formId: this.form.id, questionId: question.id, text: item.newOption })
const answerId = response.data const optionId = response.data
question.answers.push({ question.options.push({
id: answerId, id: optionId,
text: item.newQuizAnswer, text: item.newOption,
}) })
} }
item.newQuizAnswer = '' item.newOption = ''
} }
}, },
async deleteAnswer(question, answer, index) { async deleteOption(question, option, index) {
await axios.delete(generateUrl('/apps/forms/api/v1/answer/{id}', { id: answer.id })) await axios.delete(generateUrl('/apps/forms/api/v1/option/{id}', { id: option.id }))
// TODO catch errors // TODO catch errors
question.answers.splice(index, 1) question.options.splice(index, 1)
}, },
allHaveAns() { checkAllHaveOpt() {
this.haveAns = true this.allHaveOpt = true
this.form.options.formQuizQuestions.forEach(q => { this.form.questions.forEach(q => {
if (q.type !== 'text' && q.type !== 'comment' && q.answers.length === 0) { if (q.type !== 'text' && q.type !== 'comment' && q.options.length === 0) {
this.haveAns = false this.allHaveOpt = false
} }
}) })
}, },
@ -273,13 +277,13 @@ export default {
}, 200), }, 200),
writeForm() { writeForm() {
this.allHaveAns() this.checkAllHaveOpt()
if (this.form.event.title.length === 0 | !(/\S/.test(this.form.event.title))) { if (this.form.form.title.length === 0 | !(/\S/.test(this.form.form.title))) {
this.titleEmpty = true this.titleEmpty = true
showError(t('forms', 'Title must not be empty!'), { duration: 3000 }) showError(t('forms', 'Title must not be empty!'), { duration: 3000 })
} else if (!this.haveAns) { } else if (!this.allHaveOpt) {
showError(t('forms', 'All questions need answers!'), { duration: 3000 }) showError(t('forms', 'All questions need answers!'), { duration: 3000 })
} else if (this.form.event.expiration & this.form.event.expirationDate === '') { } else if (this.form.form.expires & this.form.form.expirationDate === '') {
showError(t('forms', 'Need to pick an expiration date!'), { duration: 3000 }) showError(t('forms', 'Need to pick an expiration date!'), { duration: 3000 })
} else { } else {
this.writingForm = true this.writingForm = true
@ -288,12 +292,12 @@ export default {
axios.post(OC.generateUrl('apps/forms/write/form'), this.form) axios.post(OC.generateUrl('apps/forms/write/form'), this.form)
.then((response) => { .then((response) => {
this.form.mode = 'edit' this.form.mode = 'edit'
this.form.event.hash = response.data.hash this.form.form.hash = response.data.hash
this.form.event.id = response.data.id this.form.form.id = response.data.id
this.writingForm = false this.writingForm = false
showSuccess(t('forms', '%n successfully saved', 1, this.form.event.title), { duration: 3000 }) showSuccess(t('forms', '%n successfully saved', 1, this.form.form.title), { duration: 3000 })
}, (error) => { }, (error) => {
this.form.event.hash = '' this.form.form.hash = ''
this.writingForm = false this.writingForm = false
showError(t('forms', 'Error on saving form, see console')) showError(t('forms', 'Error on saving form, see console'))
/* eslint-disable-next-line no-console */ /* eslint-disable-next-line no-console */

View file

@ -43,8 +43,8 @@
v-for="(form, index) in forms" v-for="(form, index) in forms"
:key="form.id" :key="form.id"
:form="form" :form="form"
@deleteForm="removeForm(index, form.event)" @deleteForm="removeForm(index, form.form)"
@viewResults="viewFormResults(index, form.event, 'results')" /> @viewResults="viewFormResults(index, form.form, 'results')" />
</transition-group> </transition-group>
<LoadingOverlay v-if="loading" /> <LoadingOverlay v-if="loading" />
<modal-dialog /> <modal-dialog />
@ -110,29 +110,28 @@ export default {
helpPage() { helpPage() {
window.open('https://github.com/nextcloud/forms/blob/master/Forms_Support.md') window.open('https://github.com/nextcloud/forms/blob/master/Forms_Support.md')
}, },
viewFormResults(index, form, name) {
viewFormResults(index, event, name) {
this.$router.push({ this.$router.push({
name: name, name: name,
params: { params: {
hash: event.id, hash: form.id,
}, },
}) })
}, },
removeForm(index, event) { removeForm(index, form) {
const params = { const params = {
title: t('forms', 'Delete form'), title: t('forms', 'Delete form'),
text: t('forms', 'Do you want to delete "%n"?', 1, event.title), text: t('forms', 'Do you want to delete "%n"?', 1, form.title),
buttonHideText: t('forms', 'No, keep form.'), buttonHideText: t('forms', 'No, keep form.'),
buttonConfirmText: t('forms', 'Yes, delete form.'), buttonConfirmText: t('forms', 'Yes, delete form.'),
onConfirm: () => { onConfirm: () => {
// this.deleteForm(index, event) // this.deleteForm(index, form)
axios.delete(OC.generateUrl('apps/forms/forms/{id}', { id: event.id })) axios.delete(OC.generateUrl('apps/forms/forms/{id}', { id: form.id }))
.then((response) => { .then((response) => {
this.forms.splice(index, 1) this.forms.splice(index, 1)
OC.Notification.showTemporary(t('forms', 'Form "%n" deleted', 1, event.title)) OC.Notification.showTemporary(t('forms', 'Form "%n" deleted', 1, form.title))
}, (error) => { }, (error) => {
OC.Notification.showTemporary(t('forms', 'Error while deleting Form "%n"', 1, event.title)) OC.Notification.showTemporary(t('forms', 'Error while deleting Form "%n"', 1, form.title))
/* eslint-disable-next-line no-console */ /* eslint-disable-next-line no-console */
console.log(error.response) console.log(error.response)
} }

View file

@ -43,10 +43,10 @@
:header="true" /> :header="true" />
<li <li
is="resultItem" is="resultItem"
v-for="(vote, index) in votes" v-for="(answer, index) in answers"
:key="vote.id" :key="answer.id"
:vote="vote" :answer="answer"
@viewResults="viewFormResults(index, form.event, 'results')" /> @viewResults="viewFormResults(index, form.form, 'results')" />
</transition-group> </transition-group>
<LoadingOverlay v-if="loading" /> <LoadingOverlay v-if="loading" />
<modal-dialog /> <modal-dialog />
@ -77,7 +77,7 @@ export default {
data() { data() {
return { return {
loading: true, loading: true,
votes: [], answers: [],
} }
}, },
@ -86,29 +86,29 @@ export default {
stats() { stats() {
const sums = [] const sums = []
if (this.votes != null) { if (this.answers != null) {
const uniqueAns = [] const uniqueAns = []
const uniqueQs = [] const uniqueQs = []
const ansToQ = new Map() const ansToQ = new Map()
for (let i = 0; i < this.votes.length; i++) { for (let i = 0; i < this.answers.length; i++) {
if (this.votes[i].voteOptionType === 'radiogroup' || this.votes[i].voteOptionType === 'dropdown') { if (this.answers[i].questionType === 'radiogroup' || this.answers[i].questionType === 'dropdown') {
if (uniqueAns.includes(this.votes[i].voteAnswer) === false) { if (uniqueAns.includes(this.answers[i].text) === false) {
uniqueAns.push(this.votes[i].voteAnswer) uniqueAns.push(this.answers[i].text)
ansToQ.set(this.votes[i].voteAnswer, this.votes[i].voteOptionId) ansToQ.set(this.answers[i].text, this.answers[i].questionId)
} }
if (uniqueQs.includes(this.votes[i].voteOptionId) === false) { if (uniqueQs.includes(this.answers[i].questionId) === false) {
uniqueQs.push(this.votes[i].voteOptionId) uniqueQs.push(this.answers[i].questionId)
} }
} }
} }
for (let i = 0; i < uniqueAns.length; i++) { for (let i = 0; i < uniqueAns.length; i++) {
sums[i] = 0 sums[i] = 0
} }
for (let i = 0; i < this.votes.length; i++) { for (let i = 0; i < this.answers.length; i++) {
sums[uniqueAns.indexOf(this.votes[i].voteAnswer)]++ sums[uniqueAns.indexOf(this.answers[i].text)]++
} }
for (let i = 0; i < sums.length; i++) { for (let i = 0; i < sums.length; i++) {
sums[i] = 'Question ' + ansToQ.get(uniqueAns[i]) + ': ' + (sums[i] / ((this.votes.length / uniqueQs.length)) * 100).toFixed(2) + '%' + ' of respondents voted for answer choice: ' + uniqueAns[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]
} }
} }
@ -127,10 +127,10 @@ export default {
axios.get(generateUrl('apps/forms/api/v1/submissions/{hash}', { hash: this.$route.params.hash })) axios.get(generateUrl('apps/forms/api/v1/submissions/{hash}', { hash: this.$route.params.hash }))
.then((response) => { .then((response) => {
if (response.data == null) { if (response.data == null) {
this.votes = null this.answers = null
OC.Notification.showTemporary('Access Denied') OC.Notification.showTemporary('Access Denied')
} else { } else {
this.votes = response.data this.answers = response.data
} }
this.loading = false this.loading = false
}, (error) => { }, (error) => {
@ -139,22 +139,31 @@ export default {
this.loading = false this.loading = false
}) })
}, },
viewFormResults(index, event, name) { viewFormResults(index, form, name) {
this.$router.push({ this.$router.push({
name: name, name: name,
params: { params: {
hash: event.id, hash: form.id,
}, },
}) })
}, },
download() { download() {
this.loading = true this.loading = true
axios.get(OC.generateUrl('apps/forms/get/event/' + this.$route.params.hash)) axios.get(OC.generateUrl('apps/forms/get/form/' + this.$route.params.hash))
.then((response) => { .then((response) => {
this.json2csvParser = ['userId', 'voteOptionId', 'voteOptionText', 'voteAnswer'] this.json2csvParser = ['userId', 'questionId', 'questionText', 'Answer'] // TODO Is this one necessary??
const formattedAns = []
this.answers.forEach(ans => {
formattedAns.push({
userId: ans['userId'],
questionId: ans['questionId'],
questionText: ans['questionText'],
answer: ans['text'],
})
})
const element = document.createElement('a') const element = document.createElement('a')
element.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(json2csvParser.parse(this.votes))) element.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(json2csvParser.parse(formattedAns)))
element.setAttribute('download', response.data.title + '.csv') element.setAttribute('download', response.data.title + '.csv')
element.style.display = 'none' element.style.display = 'none'

View file

@ -21,47 +21,41 @@
--> -->
<template> <template>
<AppSidebar :title="form.event.title"> <AppSidebar :title="form.form.title">
<div class="configBox "> <div class="configBox ">
<label class="title icon-settings"> <label class="title icon-settings">
{{ t('forms', 'Form configurations') }} {{ t('forms', 'Form configurations') }}
</label> </label>
<input id="anonymous" <input id="isAnonymous"
v-model="form.event.isAnonymous" v-model="form.form.isAnonymous"
type="checkbox" type="checkbox"
class="checkbox"> class="checkbox">
<label for="anonymous" class="title"> <label for="isAnonymous" class="title">
{{ t('forms', 'Anonymous form') }} {{ t('forms', 'Anonymous form') }}
</label> </label>
<input id="unique" <input id="submitOnce"
v-model="form.event.unique" v-model="form.form.submitOnce"
:disabled="form.event.access !== 'registered' || form.event.isAnonymous" :disabled="form.form.access !== 'registered' || form.form.isAnonymous"
type="checkbox" type="checkbox"
class="checkbox"> class="checkbox">
<label for="unique" class="title"> <label for="submitOnce" class="title">
<span>{{ t('forms', 'Only allow one submission per user') }}</span> <span>{{ t('forms', 'Only allow one submission per user') }}</span>
</label> </label>
<input v-show="form.event.isAnonymous" <input id="expires"
id="trueAnonymous" v-model="form.form.expires"
v-model="form.event.fullAnonymous"
type="checkbox" type="checkbox"
class="checkbox"> class="checkbox">
<input id="expiration" <label class="title" for="expires">
v-model="form.event.expiration"
type="checkbox"
class="checkbox">
<label class="title" for="expiration">
{{ t('forms', 'Expires') }} {{ t('forms', 'Expires') }}
</label> </label>
<DatetimePicker v-show="form.event.expiration" <DatetimePicker v-show="form.form.expires"
v-model="form.event.expirationDate" v-model="form.form.expirationDate"
v-bind="expirationDatePicker" v-bind="expirationDatePicker"
:time-picker-options="{ start: '00:00', step: '00:05', end: '23:55' }" :time-picker-options="{ start: '00:00', step: '00:05', end: '23:55' }"
@ -73,7 +67,7 @@
{{ t('forms', 'Access') }} {{ t('forms', 'Access') }}
</label> </label>
<input id="private" <input id="private"
v-model="form.event.access" v-model="form.form.access"
type="radio" type="radio"
value="registered" value="registered"
@ -83,7 +77,7 @@
<span>{{ t('forms', 'Registered users only') }}</span> <span>{{ t('forms', 'Registered users only') }}</span>
</label> </label>
<input id="public" <input id="public"
v-model="form.event.access" v-model="form.form.access"
type="radio" type="radio"
value="public" value="public"
@ -93,7 +87,7 @@
<span>{{ t('forms', 'Public access') }}</span> <span>{{ t('forms', 'Public access') }}</span>
</label> </label>
<input id="select" <input id="select"
v-model="form.event.access" v-model="form.form.access"
type="radio" type="radio"
value="select" value="select"
@ -104,7 +98,7 @@
</label> </label>
</div> </div>
<ShareDiv v-show="form.event.access === 'select'" <ShareDiv v-show="form.form.access === 'select'"
:active-shares="form.shares" :active-shares="form.shares"
:placeholder="t('forms', 'Name of user or group')" :placeholder="t('forms', 'Name of user or group')"
:hide-names="true" :hide-names="true"

View file

@ -23,12 +23,12 @@
use OCP\Util; use OCP\Util;
Util::addStyle('forms', 'vote'); Util::addStyle('forms', 'submit');
Util::addScript('forms', 'vote'); Util::addScript('forms', 'submit');
Util::addScript('forms', 'survey.jquery.min'); Util::addScript('forms', 'survey.jquery.min');
/** @var \OCA\Forms\Db\Event $form */ /** @var \OCA\Forms\Db\Form $form */
$form = $_['form']; $form = $_['form'];
/** @var OCA\Forms\Db\Question[] $questions */ /** @var OCA\Forms\Db\Question[] $questions */
$questions = $_['questions']; $questions = $_['questions'];