Order&Reorder Questions

Signed-off-by: Jonas Rittershofer <jotoeri@users.noreply.github.com>
This commit is contained in:
Jonas Rittershofer 2020-04-09 12:40:04 +02:00
parent e87a94a5dc
commit d061e759b5
6 changed files with 166 additions and 19 deletions

View file

@ -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'],

View file

@ -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);
}

View file

@ -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()),

View file

@ -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);
}

View file

@ -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();

View file

@ -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: [],