Skip to content

Commit

Permalink
feat: Allow for duplicating questions
Browse files Browse the repository at this point in the history
* ApiController can now receive a duplication request, copies the question and options to new ones and then returns that new question object.
* All questions can now handle duplication.
* Create can now handle the duplication of questions.
* Added the new api route.
* Some styling and variables renamed to fit nextcloud guidelines
* Written an integration test.
* Added some comments to new methods added.
* Added start for translation
* Refactored variable names and some cleanup.
* Create is now more concise.
* Updated routes

Signed-off-by: Mitchel van Hamburg [email protected]
  • Loading branch information
Mitchel authored and susnux committed Dec 18, 2023
1 parent 0ba2e9a commit 3cb1150
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 0 deletions.
8 changes: 8 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@
'apiVersion' => 'v2(\.[1-2])?'
]
],
[
'name' => 'api#duplicateQuestion',
'url' => '/api/{apiVersion}/question/clone/{id}',
'verb' => 'POST',
'requirements' => [
'apiVersion' => 'v2.1'
]
],

// Options
[
Expand Down
54 changes: 54 additions & 0 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,60 @@ public function deleteQuestion(int $id): DataResponse {

/**
* @CORS
* @NoAdminRequired
*
* Duplicate a question
*
* @param int $id the question id
* @return DataResponse
* @throws OCSBadRequestException|OCSForbiddenException
*/
public function duplicateQuestion(int $id): DataResponse {
$this->logger->debug('Question to be duplicated: {id}', [
'id' => $id
]);

Check warning on line 729 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L726-L729

Added lines #L726 - L729 were not covered by tests

try {
$sourceQuestion = $this->questionMapper->findById($id);
$sourceOptions = $this->optionMapper->findByQuestion($id);
$form = $this->formMapper->findById($sourceQuestion->getFormId());
} catch (IMapperException $e) {
$this->logger->debug('Could not find form or question');
throw new OCSBadRequestException('Could not find form or question');

Check warning on line 737 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L732-L737

Added lines #L732 - L737 were not covered by tests
}

if ($form->getOwnerId() !== $this->currentUser->getUID()) {
$this->logger->debug('This form is not owned by the current user');
throw new OCSForbiddenException();

Check warning on line 742 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L740-L742

Added lines #L740 - L742 were not covered by tests
}

$allQuestions = $this->questionMapper->findByForm($form->getId());

Check warning on line 745 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L745

Added line #L745 was not covered by tests

$questionData = $sourceQuestion->read();
unset($questionData['id']);
$questionData["order"] = end($allQuestions)->getOrder() + 1;

Check warning on line 749 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L747-L749

Added lines #L747 - L749 were not covered by tests

$newQuestion = Question::fromParams($questionData);
$this->questionMapper->insert($newQuestion);

Check warning on line 752 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L751-L752

Added lines #L751 - L752 were not covered by tests

$response = $newQuestion->read();
$response['options'] = [];

Check warning on line 755 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L754-L755

Added lines #L754 - L755 were not covered by tests

foreach ($sourceOptions as $sourceOption) {
$optionData = $sourceOption->read();

Check warning on line 758 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L757-L758

Added lines #L757 - L758 were not covered by tests

unset($optionData['id']);
$optionData['questionId'] = $newQuestion->getId();
$newOption = Option::fromParams($optionData);
$insertedOption = $this->optionMapper->insert($newOption);

Check warning on line 763 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L760-L763

Added lines #L760 - L763 were not covered by tests

$response['options'][] = $insertedOption->read();

Check warning on line 765 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L765

Added line #L765 was not covered by tests
}

return new DataResponse($response);

Check warning on line 768 in lib/Controller/ApiController.php

View check run for this annotation

Codecov / codecov/patch

lib/Controller/ApiController.php#L768

Added line #L768 was not covered by tests
}

/**
* @NoAdminRequired
*
* Add a new option to a question
Expand Down
15 changes: 15 additions & 0 deletions src/components/Questions/Question.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
</template>
{{ t('forms', 'Technical name') }}
</NcActionInput>
<NcActionButton @click="onDuplicate">
<template #icon>
<IconContentDuplicate :size="20" />
</template>
{{ t('forms', 'Copy question') }}
</NcActionButton>
<NcActionButton @click="onDelete">
<template #icon>
<IconDelete :size="20" />
Expand Down Expand Up @@ -140,6 +146,7 @@ import IconArrowUp from 'vue-material-design-icons/ArrowUp.vue'
import IconDelete from 'vue-material-design-icons/Delete.vue'
import IconDragHorizontalVariant from 'vue-material-design-icons/DragHorizontalVariant.vue'
import IconIdentifier from 'vue-material-design-icons/Identifier.vue'
import IconContentDuplicate from 'vue-material-design-icons/ContentDuplicate.vue'
export default {
name: 'Question',
Expand All @@ -148,6 +155,7 @@ export default {
IconAlertCircleOutline,
IconArrowDown,
IconArrowUp,
IconContentDuplicate,
IconDelete,
IconDragHorizontalVariant,
IconIdentifier,
Expand Down Expand Up @@ -305,6 +313,13 @@ export default {
onDelete() {
this.$emit('delete')
},
/**
* Duplicate this question
*/
onDuplicate() {
this.$emit('duplicate')
},
},
}
</script>
Expand Down
8 changes: 8 additions & 0 deletions src/mixins/QuestionMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export default {
commonListeners() {
return {
delete: this.onDelete,
duplicate: this.onDuplicate,
'update:text': this.onTitleChange,
'update:description': this.onDescriptionChange,
'update:isRequired': this.onRequiredChange,
Expand Down Expand Up @@ -290,6 +291,13 @@ export default {
this.$emit('delete')
},

/**
* Duplicate this question.
*/
onDuplicate() {
this.$emit('duplicate')
},

/**
* Don't automatically submit form on Enter, parent will handle that
* To be called with prevent: @keydown.enter.prevent="onKeydownEnter"
Expand Down
30 changes: 30 additions & 0 deletions src/views/Create.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
:max-string-lengths="maxStringLengths"
v-bind.sync="form.questions[index]"
@delete="deleteQuestion(question)"
@duplicate="duplicateQuestion(question)"
@move-down="onMoveDown(index)"
@move-up="onMoveUp(index)" />
</transition-group>
Expand Down Expand Up @@ -393,6 +394,35 @@ export default {
}
},
/**
* Duplicate a question
*
* @param {number} id the question id to duplicate in the current form
*/
async duplicateQuestion({ id }) {
this.isLoadingQuestions = true
try {
const response = await axios.post(generateOcsUrl('apps/forms/api/v2.1/question/clone/{id}', { id }))
const question = OcsResponse2Data(response)
this.form.questions.push(Object.assign({
answers: [],
}, question))
this.$nextTick(() => {
const lastQuestion = this.$refs.questions[this.$refs.questions.length - 1]
lastQuestion.focus()
})
} catch (error) {
logger.error(`Error while duplicating question ${id}`, { error })
showError('There was an error while duplicating the question')
} finally {
this.isLoadingQuestions = false
}
},
/**
* Reorder questions on dragEnd
*/
Expand Down
25 changes: 25 additions & 0 deletions tests/Integration/Api/ApiV2Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,31 @@ public function testDeleteQuestion(array $fullFormExpected) {
$this->testGetFullForm($fullFormExpected);
}

public function dataDuplicateQuestion() {
$fullFormExpected = $this->dataGetFullForm()['getFullForm']['expected'];
array_splice($fullFormExpected['questions'][1]['options'], 0, 1);

return [
'duplicateQuestion' => [
'fullFormExpected' => $fullFormExpected
]
];
}

/**
* @dataProvider dataDuplicateQuestion
* @param array $fullFormExpected
*/
public function testDuplicateQuestion(array $fullFormExpected) {
$resp = $this->http->request('POST', "api/v2/question/{$this->testForms[0]['questions'][0]['id']}");
$data = $this->OcsResponse2Data($resp);

$this->assertEquals(200, $resp->getStatusCode());
$this->assertEquals($this->testForms[0]['questions'][count($this->testForms[0]['questions'])]['id'], $data);

$this->testGetFullForm($fullFormExpected);
}

public function dataCreateNewOption() {
return [
'newOption' => [
Expand Down

0 comments on commit 3cb1150

Please sign in to comment.