Skip to content

Commit 82a0b1d

Browse files
committed
feat: add/clone question at position
Signed-off-by: Christian Hartmann <chris-hartmann@gmx.de>
1 parent 794c3f4 commit 82a0b1d

4 files changed

Lines changed: 271 additions & 75 deletions

File tree

docs/API_v3.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ Returns the questions and options of the given form (without submissions).
370370
|-----------|---------|----------|-------------|
371371
| _type_ | [QuestionType](DataStructure.md#question-types) | | The question-type of the new question |
372372
| _text_ | String | yes | _Optional_ The text of the new question. |
373+
| _position_ | Integer | yes | _(optional)_ 1-based position to insert the new question. When omitted the question is appended at the end. |
373374
- Response: The new question object.
374375

375376
```
@@ -506,6 +507,10 @@ Creates a clone of a question with all its options.
506507
|-----------|---------|-------------|
507508
| _formId_ | Integer | ID of the form containing the question |
508509
| _questionId_ | Integer | ID of the question to clone |
510+
- Parameters:
511+
| Parameter | Type | Optional | Description |
512+
|-----------|---------|----------|-------------|
513+
| _position_ | Integer | yes | _(optional)_ 1-based position to insert the cloned question. When omitted the clone is appended at the end. |
509514
- Response: Returns cloned question object with the new ID set.
510515

511516
```

lib/Controller/ApiController.php

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
485485
* @param FormsQuestionGridCellType $subtype the new question subtype
486486
* @param string $text the new question title
487487
* @param ?int $fromId (optional) id of the question that should be cloned
488+
* @param ?int $position (optional) the position of the new question
488489
* @return DataResponse<Http::STATUS_CREATED, FormsQuestion, array{}>
489490
* @throws OCSBadRequestException Invalid type
490491
* @throws OCSBadRequestException Datetime question type no longer supported
@@ -499,7 +500,7 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
499500
#[NoAdminRequired()]
500501
#[BruteForceProtection(action: 'form')]
501502
#[ApiRoute(verb: 'POST', url: '/api/v3/forms/{formId}/questions')]
502-
public function newQuestion(int $formId, ?string $type = null, ?string $subtype = null, string $text = '', ?int $fromId = null): DataResponse {
503+
public function newQuestion(int $formId, ?string $type = null, ?string $subtype = null, string $text = '', ?int $fromId = null, ?int $position = null): DataResponse {
503504
$form = $this->formsService->getFormIfAllowed($formId, Constants::PERMISSION_EDIT);
504505
$this->formsService->obtainFormLock($form);
505506

@@ -526,13 +527,20 @@ public function newQuestion(int $formId, ?string $type = null, ?string $subtype
526527
throw new OCSBadRequestException('Datetime question type no longer supported');
527528
}
528529

529-
// Retrieve all active questions sorted by Order. Takes the order of the last array-element and adds one.
530+
// Retrieve all active questions sorted by Order.
530531
$questions = $this->questionMapper->findByForm($formId);
531-
$lastQuestion = array_pop($questions);
532-
if ($lastQuestion) {
533-
$questionOrder = $lastQuestion->getOrder() + 1;
532+
533+
if ($position !== null) {
534+
$position = $this->shiftQuestionsForInsert($questions, $position);
535+
$questionOrder = $position;
534536
} else {
535-
$questionOrder = 1;
537+
// Append at the end
538+
$lastQuestion = array_pop($questions);
539+
if ($lastQuestion) {
540+
$questionOrder = $lastQuestion->getOrder() + 1;
541+
} else {
542+
$questionOrder = 1;
543+
}
536544
}
537545

538546
$question = new Question();
@@ -574,7 +582,13 @@ public function newQuestion(int $formId, ?string $type = null, ?string $subtype
574582

575583
$questionData = $sourceQuestion->read();
576584
unset($questionData['id']);
577-
$questionData['order'] = end($allQuestions)->getOrder() + 1;
585+
586+
if ($position !== null) {
587+
$position = $this->shiftQuestionsForInsert($allQuestions, $position);
588+
$questionData['order'] = $position;
589+
} else {
590+
$questionData['order'] = end($allQuestions)->getOrder() + 1;
591+
}
578592

579593
$newQuestion = Question::fromParams($questionData);
580594
$this->questionMapper->insert($newQuestion);
@@ -1992,4 +2006,36 @@ private function handleOwnerTransfer(Form $form, int $formId, string $currentUse
19922006
$this->formMapper->update($form);
19932007
return new DataResponse($form->getOwnerId());
19942008
}
2009+
2010+
/**
2011+
* Shift existing question orders to make room for an insertion at $position.
2012+
* Normalizes the given $position to the valid range and updates all questions
2013+
* with order >= $position by incrementing their order by one.
2014+
*
2015+
* @param Question[] $questions
2016+
* @param int $position 1-based desired position
2017+
* @return int normalized position
2018+
*/
2019+
private function shiftQuestionsForInsert(array $questions, int $position): int {
2020+
$maxOrder = 0;
2021+
if (count($questions) > 0) {
2022+
$maxOrder = end($questions)->getOrder();
2023+
}
2024+
if ($position < 1) {
2025+
$position = 1;
2026+
}
2027+
if ($position > $maxOrder + 1) {
2028+
$position = $maxOrder + 1;
2029+
}
2030+
2031+
for ($i = count($questions) - 1; $i >= 0; $i--) {
2032+
$q = $questions[$i];
2033+
if ($q->getOrder() >= $position) {
2034+
$q->setOrder($q->getOrder() + 1);
2035+
$this->questionMapper->update($q);
2036+
}
2037+
}
2038+
2039+
return $position;
2040+
}
19952041
}

openapi.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,13 @@
16861686
"nullable": true,
16871687
"default": null,
16881688
"description": "(optional) id of the question that should be cloned"
1689+
},
1690+
"position": {
1691+
"type": "integer",
1692+
"format": "int64",
1693+
"nullable": true,
1694+
"default": null,
1695+
"description": "(optional) the position of the new question"
16891696
}
16901697
}
16911698
}

0 commit comments

Comments
 (0)