Skip to content

Duplicate a question through the API - #262

Merged
PhilReinking merged 6 commits into
mainfrom
feat/bits-275-duplicate-question
Oct 13, 2026
Merged

PhilReinking merged 6 commits into
mainfrom
feat/bits-275-duplicate-question

Conversation

@PhilReinking

@PhilReinking PhilReinking commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Adds POST /api/blocks/{block}/duplicate for the editor's question menu. It copies a question's text, type, settings, answers and Required, not its rules or Identifier, and puts the copy right after the original.

Fixes BITS-275

  • One transaction: if any part fails, the form stays as it was. It locks the form first, so duplicates at the same time wait.
  • The copy has no Identifier (title): prefill links and webhook names use it, so a shared one clashes.
  • Renumbers the form's blocks from 0, since older forms can have gaps or two blocks on one position (adding after a delete).
  • A question in a group stays in that group. A group itself gets 422: copying a whole group isn't in the ticket.
  • Returns the copy (201) in the shape of the other block endpoints, with interactions and empty logics.

Tested: full PHP suite on MariaDB and SQLite. New tests fail on main (no route), the rollback test fails without the transaction. 16 duplicates at once on MariaDB: 21 questions on 15 positions before the lock, 21 on 21 after. Scribe lists the endpoint.

Not tested: no UI uses it yet. The frontend adds the menu entry.

🤖 Generated with Claude Code

PhilReinking and others added 2 commits October 13, 2026 00:45
POST /api/blocks/{block}/duplicate copies a question with its text, type,
settings, answers and Required, but not its rules. The copy goes right
after the original and the form's blocks are renumbered in one
transaction. A group is refused with 422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK at 5a02fb5, CI green.

The new tests fail on main (no route), and the rollback test fails without the transaction. Beyond that: the response equals the block in the block list, nested options, turned-off answers and file limits copy over, and existing submissions stay on the original. A guest gets 401, a block of a trashed form 403. The generated API docs list the endpoint.

PhilReinking and others added 3 commits October 13, 2026 03:31
The Identifier names the answer in prefill links and webhooks, so a copy
that keeps it clashes with the original.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Duplicates of one form at the same time renumbered over each other
(16 at once on MariaDB left 21 questions on 15 positions) or deadlocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…test

Both blocks had position 0, so MariaDB could list the child first,
depending on what ran before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK at a26b17b, CI green. Both new tests fail on 5a02fb5. MariaDB, 4 workers, 16 duplicates at once: with the lock 21 questions on 21 positions in 3 of 3 rounds; without it, two on one position in round 1. The child's own position only fixes the test's tie, the import works in any order. A copy without Identifier breaks nothing: new questions start that way, and webhooks fall back to the uuid.

Comment thread app/Models/FormBlock.php Outdated
Comment on lines +169 to +170
// duplicates at the same time wait here, else they renumber over each other or deadlock
$this->form()->lockForUpdate()->first();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should: SQLite, the Docker image's default, ignores lockForUpdate, and on PHP 8.3 the transaction starts deferred, so a read that turns into a write fails at once. 16 duplicates at once gave 5 to 9 errors 500 "database is locked" per round (positions stayed right). A write first waits on both databases: 16 of 16, 21 on 21, on SQLite and MariaDB. It also marks the form as edited. Tested together with the test change below.

Suggested change
// duplicates at the same time wait here, else they renumber over each other or deadlock
$this->form()->lockForUpdate()->first();
// duplicates at the same time wait here, else they renumber over each other or deadlock.
// A write, not lockForUpdate: SQLite ignores that and fails a read that turns into a write
$this->form()->touch();

Comment thread tests/Feature/DuplicateBlockTest.php Outdated
Comment on lines +143 to +144
expect(DB::getQueryLog()[0]['query'])->toStartWith('select * from `forms` where `forms`.`id` = ?')->toEndWith('for update');
})->skip(fn () => DB::getDriverName() === 'sqlite', 'SQLite locks the whole database on write');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the write first, this check runs on SQLite too. Passes on MariaDB and SQLite.

Suggested change
expect(DB::getQueryLog()[0]['query'])->toStartWith('select * from `forms` where `forms`.`id` = ?')->toEndWith('for update');
})->skip(fn () => DB::getDriverName() === 'sqlite', 'SQLite locks the whole database on write');
expect(DB::getQueryLog()[0]['query'])->toMatch('/^update [`"]forms[`"] set /');
});

SQLite ignores lockForUpdate, and PHP 8.3 starts the transaction
deferred, so parallel duplicates failed with "database is locked".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK at b7105c3, CI green. The change since a26b17b is the write-first lock and the test without the SQLite skip. 16 duplicates at once, 4 workers: 21 questions on 21 positions and no errors, on MariaDB in 3 rounds and on SQLite in 2.

@PhilReinking
PhilReinking merged commit d311ecf into main Oct 13, 2026
6 checks passed
@PhilReinking
PhilReinking deleted the feat/bits-275-duplicate-question branch October 13, 2026 12:47
ferbinu pushed a commit to ferbinu/input that referenced this pull request Oct 15, 2026
Brings in the duplicate endpoint (deck9#262) and the editor loading states (deck9#263).
The page keeps deck9#263's loading; the document shows its skeleton and error box
instead of loading on its own. The old list (Storyboard.vue) stays removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant