Skip to content

Commit f18ead6

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

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
@@ -487,6 +487,7 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
487487
* @param FormsQuestionGridCellType $subtype the new question subtype
488488
* @param string $text the new question title
489489
* @param ?int $fromId (optional) id of the question that should be cloned
490+
* @param ?int $position (optional) the position of the new question
490491
* @return DataResponse<Http::STATUS_CREATED, FormsQuestion, array{}>
491492
* @throws OCSBadRequestException Invalid type
492493
* @throws OCSBadRequestException Datetime question type no longer supported
@@ -501,7 +502,7 @@ public function getQuestion(int $formId, int $questionId): DataResponse {
501502
#[NoAdminRequired()]
502503
#[BruteForceProtection(action: 'form')]
503504
#[ApiRoute(verb: 'POST', url: '/api/v3/forms/{formId}/questions')]
504-
public function newQuestion(int $formId, ?string $type = null, ?string $subtype = null, string $text = '', ?int $fromId = null): DataResponse {
505+
public function newQuestion(int $formId, ?string $type = null, ?string $subtype = null, string $text = '', ?int $fromId = null, ?int $position = null): DataResponse {
505506
$form = $this->formsService->getFormIfAllowed($formId, Constants::PERMISSION_EDIT);
506507
$this->formsService->obtainFormLock($form);
507508

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

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

540548
$question = new Question();
@@ -572,7 +580,13 @@ public function newQuestion(int $formId, ?string $type = null, ?string $subtype
572580

573581
$questionData = $sourceQuestion->read();
574582
unset($questionData['id']);
575-
$questionData['order'] = end($allQuestions)->getOrder() + 1;
583+
584+
if ($position !== null) {
585+
$position = $this->shiftQuestionsForInsert($allQuestions, $position);
586+
$questionData['order'] = $position;
587+
} else {
588+
$questionData['order'] = end($allQuestions)->getOrder() + 1;
589+
}
576590

577591
$newQuestion = Question::fromParams($questionData);
578592
$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)