diff --git a/appinfo/routes.php b/appinfo/routes.php index b87169c..32e9f01 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -47,6 +47,7 @@ return [ ['name' => 'api#newForm', 'url' => 'api/v1/form', 'verb' => 'POST'], ['name' => 'api#deleteForm', 'url' => 'api/v1/form/{id}', 'verb' => 'DELETE'], ['name' => 'api#updateQuestion', 'url' => 'api/v1/question/update/', 'verb' => 'POST'], + ['name' => 'api#reorderQuestions', 'url' => 'api/v1/question/reorder/', 'verb' => 'POST'], ['name' => 'api#newQuestion', 'url' => 'api/v1/question/', 'verb' => 'POST'], ['name' => 'api#deleteQuestion', 'url' => 'api/v1/question/{id}', 'verb' => 'DELETE'], ['name' => 'api#newOption', 'url' => 'api/v1/option/', 'verb' => 'POST'], diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 315a717..0dc4874 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -495,19 +495,124 @@ class ApiController extends Controller { return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); } + // Retrieve all active questions sorted by Order. Takes the order of the last array-element and adds one. + $questions = $this->questionMapper->findByForm($formId); + $lastQuestion = array_pop($questions); + if ($lastQuestion) { + $questionOrder = $lastQuestion->getOrder() + 1; + } else { + $questionOrder = 1; + } + $question = new Question(); $question->setFormId($formId); + $question->setOrder($questionOrder); $question->setType($type); $question->setText($text); $question = $this->questionMapper->insert($question); - return new Http\JSONResponse($question->getId()); + $response = [ + 'id' => $question->getId(), + 'order' => $question->getOrder() + ]; + + return new Http\JSONResponse($response); } /** * @NoAdminRequired + * Updates the Order of all Questions of a Form. + * @param int $formId Id of the form to reorder + * @param int $newOrder Array of Question-Ids in new order. + */ + public function reorderQuestions(int $formId, array $newOrder): Http\JSONResponse { + $this->logger->debug('Reordering Questions on Form {formId} as Question-Ids {newOrder}', [ + 'formId' => $formId, + 'newOrder' => $newOrder + ]); + + try { + $form = $this->formMapper->find($formId); + } catch (IMapperException $e) { + $this->logger->debug('Could not find form'); + return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + if ($form->getOwnerId() !== $this->userId) { + $this->logger->debug('This form is not owned by the current user'); + return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); + } + + // Check if array contains duplicates + if ( array_unique($newOrder) !== $newOrder ) { + $this->logger->debug('The given Array contains duplicates.'); + return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + // Check if all questions are given in Array. + $questions = $this->questionMapper->findByForm($formId); + if ( sizeof($questions) !== sizeof($newOrder) ) { + $this->logger->debug('The length of the given array does not match the number of stored questions'); + return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + $questions = []; // Clear Array of Entities + $response = []; // Array of ['questionId' => ['order' => newOrder]] + + // Store array of Question-Entities and check the Questions FormId & old Order. + foreach($newOrder as $arrayKey => $questionId) { + + try { + $questions[$arrayKey] = $this->questionMapper->findById($questionId); + } catch (IMapperException $e) { + $this->logger->debug('Could not find question. Id:{id}', [ + 'id' => $questionId + ]); + return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + // Abort if a question is not part of the Form. + if ($questions[$arrayKey]->getFormId() !== $formId) { + $this->logger->debug('This Question is not part of the given Form: questionId: {questionId}', [ + 'questionId' => $questionId + ]); + return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + // Abort if a question is already marked as deleted (order==0) + $oldOrder = $questions[$arrayKey]->getOrder(); + if ( $oldOrder === 0) { + $this->logger->debug('This Question has already been marked as deleted: Id: {id}', [ + 'id' => $questions[$arrayKey]->getId() + ]); + return new Http\JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + // Only set order, if it changed. + if ($oldOrder !== $arrayKey + 1) { + // Set Order. ArrayKey counts from zero, order counts from 1. + $questions[$arrayKey]->setOrder($arrayKey + 1); + } + } + + // Write to Database + foreach($questions as $question) { + $this->questionMapper->update($question); + + $response[$question->getId()] = [ + 'order' => $question->getOrder() + ]; + } + + return new Http\JSONResponse($response); + } + + /** + * @NoAdminRequired + * Writes the given key-value pairs into Database. + * Key 'order' should only be changed by reorderQuestions() and is not allowed here. * @param int $id QuestionId of question to update * @param array $keyvalues Array of key=>value pairs to update. */ @@ -530,6 +635,11 @@ class ApiController extends Controller { return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); } + if (array_key_exists('order', $keyvalues)) { + $this->logger->debug('Key \'order\' is not allowed on updateQuestion. Please use reorderQuestions() to change order.'); + return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); + } + $question = Question::fromParams($keyvalues); $question->setId($id); @@ -542,7 +652,7 @@ class ApiController extends Controller { * @NoAdminRequired */ public function deleteQuestion(int $id): Http\JSONResponse { - $this->logger->debug('Delete question: {id}', [ + $this->logger->debug('Mark question as deleted: {id}', [ 'id' => $id, ]); @@ -559,8 +669,22 @@ class ApiController extends Controller { return new Http\JSONResponse([], Http::STATUS_FORBIDDEN); } - $this->optionMapper->deleteByQuestion($id); - $this->questionMapper->delete($question); + // Store Order of deleted Question + $deletedOrder = $question->getOrder(); + + // Mark question as deleted + $question->setOrder(0); + $this->questionMapper->update($question); + + // Update all question-order > deleted order. + $formQuestions = $this->questionMapper->findByForm($form->getId()); + foreach ($formQuestions as $question) { + $questionOrder = $question->getOrder(); + if ( $questionOrder > $deletedOrder ) { + $question->setOrder($questionOrder - 1); + $this->questionMapper->update($question); + } + } return new Http\JSONResponse($id); } diff --git a/lib/Db/Question.php b/lib/Db/Question.php index 58034de..426db1f 100644 --- a/lib/Db/Question.php +++ b/lib/Db/Question.php @@ -29,6 +29,8 @@ use OCP\AppFramework\Db\Entity; /** * @method integer getFormId() * @method void setFormId(integer $value) + * @method integer getOrder() + * @method void setOrder(integer $value) * @method string getType() * @method void setType(string $value) * @method string getText() @@ -36,15 +38,14 @@ use OCP\AppFramework\Db\Entity; */ class Question extends Entity { protected $formId; + protected $order; protected $type; protected $mandatory; protected $text; - /** - * Question constructor. - */ public function __construct() { $this->addType('formId', 'integer'); + $this->addType('order', 'integer'); $this->addType('type', 'string'); $this->addType('mandatory', 'bool'); $this->addType('text', 'string'); @@ -54,6 +55,7 @@ class Question extends Entity { return [ 'id' => $this->getId(), 'formId' => $this->getFormId(), + 'order' => $this->getOrder(), 'type' => htmlspecialchars_decode($this->getType()), 'mandatory' => $this->getMandatory(), 'text' => htmlspecialchars_decode($this->getText()), diff --git a/lib/Db/QuestionMapper.php b/lib/Db/QuestionMapper.php index 786c6fc..a953c7e 100644 --- a/lib/Db/QuestionMapper.php +++ b/lib/Db/QuestionMapper.php @@ -46,7 +46,7 @@ class QuestionMapper extends QBMapper { * @return Question[] */ - public function findByForm(int $formId): array { + public function findByForm(int $formId, bool $loadDeleted = false): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') @@ -55,6 +55,16 @@ class QuestionMapper extends QBMapper { $qb->expr()->eq('form_id', $qb->createNamedParameter($formId, IQueryBuilder::PARAM_INT)) ); + if (!$loadDeleted) { + // Don't load questions, that are marked as deleted (marked by order==0). + $qb->andWhere( + $qb->expr()->neq('order', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)) + ); + } + + // Sort Questions by order + $qb->orderBy('order'); + return $this->findEntities($qb); } @@ -65,7 +75,7 @@ class QuestionMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); // First delete corresponding options. - $questionEntities = $this->findByForm($formId); + $questionEntities = $this->findByForm($formId, true); // findByForm - loadDeleted=true foreach ($questionEntities as $questionEntity) { $this->optionMapper->deleteByQuestion($questionEntity->id); } diff --git a/lib/Migration/Version010200Date2020323141300.php b/lib/Migration/Version010200Date2020323141300.php index 0964628..e656590 100644 --- a/lib/Migration/Version010200Date2020323141300.php +++ b/lib/Migration/Version010200Date2020323141300.php @@ -117,6 +117,10 @@ class Version010200Date2020323141300 extends SimpleMigrationStep { $table->addColumn('form_id', Type::INTEGER, [ 'notnull' => true, ]); + $table->addColumn('order', Type::INTEGER, [ + 'notnull' => true, + 'default' => 1, + ]); $table->addColumn('type', Type::STRING, [ 'notnull' => true, 'length' => 256, @@ -196,8 +200,9 @@ class Version010200Date2020323141300 extends SimpleMigrationStep { // 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 + $id_mapping['events'] = []; // Maps oldevent-id => ['newId' => newevent-id, 'nextQuestionOrder' => integer] + $id_mapping['questions'] = []; // Maps oldquestion-id => ['newId' => newquestion-id] + $id_mapping['currentSubmission'] = 0; //Fetch & Restore Events $qb_fetch = $this->connection->getQueryBuilder(); @@ -220,7 +225,10 @@ class Version010200Date2020323141300 extends SimpleMigrationStep { '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. + $id_mapping['events'][$event['id']] = [ + 'newId' => $qb_restore->getLastInsertId(), //Store new form-id to connect questions to new form. + 'nextQuestionOrder' => 1 //Prepare for sorting questions + ]; } $cursor->closeCursor(); @@ -240,12 +248,13 @@ class Version010200Date2020323141300 extends SimpleMigrationStep { $qb_restore->insert('forms_v2_questions') ->values([ - 'form_id' => $qb_restore->createNamedParameter($id_mapping['events'][$question['form_id']], IQueryBuilder::PARAM_INT), + 'form_id' => $qb_restore->createNamedParameter($id_mapping['events'][$question['form_id']]['newId'], IQueryBuilder::PARAM_INT), + 'order' => $qb_restore->createNamedParameter($id_mapping['events'][$question['form_id']]['nextQuestionOrder']++, 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. + $id_mapping['questions'][$question['id']]['newId'] = $qb_restore->getLastInsertId(); //Store new question-id to connect options to new question. } $cursor->closeCursor(); @@ -265,7 +274,7 @@ class Version010200Date2020323141300 extends SimpleMigrationStep { $qb_restore->insert('forms_v2_options') ->values([ - 'question_id' => $qb_restore->createNamedParameter($id_mapping['questions'][$answer['question_id']], IQueryBuilder::PARAM_INT), + 'question_id' => $qb_restore->createNamedParameter($id_mapping['questions'][$answer['question_id']]['newId'], IQueryBuilder::PARAM_INT), 'text' => $qb_restore->createNamedParameter($answer['text'], IQueryBuilder::PARAM_STR) ]); $qb_restore->execute(); @@ -315,7 +324,7 @@ class Version010200Date2020323141300 extends SimpleMigrationStep { 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), + 'form_id' => $qb_restore->createNamedParameter($id_mapping['events'][$vote['form_id']]['newId'], 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. ]); @@ -342,7 +351,7 @@ class Version010200Date2020323141300 extends SimpleMigrationStep { $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), + 'question_id' => $qb_restore->createNamedParameter($id_mapping['questions'][$oldQuestionId]['newId'], IQueryBuilder::PARAM_STR), 'text' => $qb_restore->createNamedParameter($vote['vote_answer'], IQueryBuilder::PARAM_STR) ]); $qb_restore->execute(); diff --git a/src/views/Create.vue b/src/views/Create.vue index f3fab16..42b2e28 100644 --- a/src/views/Create.vue +++ b/src/views/Create.vue @@ -211,10 +211,11 @@ export default { } else { 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.newQuestion }) - const questionId = response.data + const respData = response.data this.form.questions.push({ - id: questionId, + id: respData.id, + order: respData.order, text: this.newQuestion, type: this.selected, options: [],