Add survey duplication actions

This commit is contained in:
Jürgen Mummert
2026-06-08 14:06:32 +02:00
parent 97a302a985
commit d44f3be571
11 changed files with 266 additions and 11 deletions
@@ -90,6 +90,11 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
$survey = $this->resolveEditableSurvey($surveyId, $memberId);
try {
if ('duplicate-survey' === $action) {
$duplicate = $this->surveyEditorService->duplicateSurvey($survey, $memberId);
$this->addFlash('success', sprintf('Die Umfrage "%s" wurde als Kopie angelegt.', (string) $duplicate->title));
}
if ('publish-survey' === $action) {
$this->surveyEditorService->publishSurvey($survey, $memberId);
$this->addFlash('success', 'Die Umfrage ist jetzt veröffentlicht.');
+107
View File
@@ -6,22 +6,30 @@ namespace Mummert\SurveyBundle\EventListener;
use Contao\Config;
use Contao\CoreBundle\DataContainer\DataContainerOperation;
use Contao\CoreBundle\Exception\RedirectResponseException;
use Contao\CoreBundle\Routing\ContentUrlGenerator;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\DataContainer;
use Contao\Image;
use Contao\Input;
use Contao\Message;
use Contao\PageModel;
use Contao\StringUtil;
use Contao\System;
use Doctrine\DBAL\Connection;
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
use Mummert\SurveyBundle\Repository\SurveyQuestionRepository;
use Mummert\SurveyBundle\Repository\SurveyRepository;
use Mummert\SurveyBundle\Service\SurveyEditorService;
use Mummert\SurveyBundle\Service\SurveyCategoryAssignmentService;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
final class SurveyDcaListener
{
private ?SurveyEditorService $surveyEditorService;
private ?RequestStack $requestStack;
public function __construct(
private readonly SurveyRepository $surveyRepository,
private readonly SurveyQuestionRepository $surveyQuestionRepository,
@@ -30,7 +38,11 @@ final class SurveyDcaListener
private readonly ContaoFramework $framework,
private readonly Connection $connection,
private readonly ContentUrlGenerator $contentUrlGenerator,
?SurveyEditorService $surveyEditorService = null,
?RequestStack $requestStack = null,
) {
$this->surveyEditorService = $surveyEditorService;
$this->requestStack = $requestStack;
}
public function generateSurveyAlias(mixed $value, DataContainer $dataContainer): string
@@ -114,6 +126,51 @@ final class SurveyDcaListener
));
}
public function generateDuplicateSurveyButton(DataContainerOperation $operation): void
{
$row = $operation->getRecord() ?? [];
$title = trim(strip_tags((string) ($row['title'] ?? '')));
$confirmText = sprintf(
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyConfirm'] ?? 'Sind Sie sicher, dass Sie die Umfrage "%s" mit allen Fragen duplizieren wollen?',
'' !== $title ? $title : '#'.(string) ($row['id'] ?? 0),
);
$operation['label'] = $GLOBALS['TL_LANG']['tl_survey']['duplicateSurvey'][0] ?? 'Duplizieren';
$operation['title'] = $GLOBALS['TL_LANG']['tl_survey']['duplicateSurvey'][1] ?? 'Diese Umfrage mit allen Fragen duplizieren';
$operation['icon'] = 'copy.svg';
$operation['attributes']
->set('onclick', sprintf('if(!confirm(%s))return false;Backend.getScrollOffset()', json_encode($confirmText, JSON_THROW_ON_ERROR)))
->set('data-contao--tooltips-target', 'tooltip')
;
}
public function handleDuplicateSurveyAction(): void
{
if ('duplicateSurvey' !== (string) Input::get('key')) {
return;
}
$surveyId = (int) Input::get('id');
try {
$survey = $this->surveyRepository->findById($surveyId);
if (null === $survey) {
throw new \RuntimeException('Die Umfrage wurde nicht gefunden.');
}
$duplicate = $this->getSurveyEditorService()->duplicateSurvey($survey);
Message::addConfirmation(sprintf(
$GLOBALS['TL_LANG']['tl_survey']['duplicateSurveyDone'] ?? 'Die Umfrage "%s" wurde dupliziert.',
(string) $duplicate->title,
));
} catch (\Throwable $exception) {
Message::addError($exception->getMessage());
}
throw new RedirectResponseException($this->buildBackendListUrl());
}
/**
* @return array<int, string>
*/
@@ -496,4 +553,54 @@ final class SurveyDcaListener
{
return (int) $this->framework->getAdapter(Config::class)->get($settingKey);
}
private function buildBackendListUrl(): string
{
$request = $this->getRequestStack()->getCurrentRequest();
if (null === $request) {
return '/contao';
}
$query = $request->query->all();
unset($query['key'], $query['id']);
$url = $request->getBaseUrl().$request->getPathInfo();
if ([] !== $query) {
$url .= '?'.http_build_query($query);
}
return $url;
}
private function getSurveyEditorService(): SurveyEditorService
{
if ($this->surveyEditorService instanceof SurveyEditorService) {
return $this->surveyEditorService;
}
$service = System::getContainer()->get(SurveyEditorService::class);
if (!$service instanceof SurveyEditorService) {
throw new \RuntimeException('SurveyEditorService konnte nicht geladen werden.');
}
return $this->surveyEditorService = $service;
}
private function getRequestStack(): RequestStack
{
if ($this->requestStack instanceof RequestStack) {
return $this->requestStack;
}
$service = System::getContainer()->get('request_stack');
if (!$service instanceof RequestStack) {
throw new \RuntimeException('RequestStack konnte nicht geladen werden.');
}
return $this->requestStack = $service;
}
}
+2 -1
View File
@@ -94,8 +94,9 @@ final class SurveyRepository
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])),
'description' => (string) ($data['description'] ?? ''),
'published' => '',
'isActive' => '',
'isActive' => '1',
'isLocked' => '',
'isTemplate' => !empty($data['isTemplate']) ? '1' : '',
'createdBy' => $memberId,
'updatedBy' => $memberId,
'createdAt' => $now,
+121
View File
@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Mummert\SurveyBundle\Service;
use Doctrine\DBAL\Connection;
use Mummert\SurveyBundle\Form\Model\SurveyConditionData;
use Mummert\SurveyBundle\Form\Model\SurveyEditorData;
use Mummert\SurveyBundle\Form\Model\SurveyQuestionData;
use Mummert\SurveyBundle\Model\SurveyContentModel;
use Mummert\SurveyBundle\Model\SurveyModel;
use Mummert\SurveyBundle\Repository\SurveyConditionRepository;
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
@@ -21,6 +23,7 @@ final class SurveyEditorService
private readonly SurveyConditionRepository $surveyConditionRepository,
private readonly SurveyEditorRepository $surveyEditorRepository,
private readonly SurveyCategoryAssignmentService $surveyCategoryAssignmentService,
private readonly Connection $connection,
) {
}
@@ -99,6 +102,113 @@ final class SurveyEditorService
$this->surveyRepository->deleteCascade((int) $survey->id);
}
public function duplicateSurvey(SurveyModel $survey, ?int $actorMemberId = null): SurveyModel
{
return $this->connection->transactional(function () use ($survey, $actorMemberId): SurveyModel {
$sourceSurveyId = (int) $survey->id;
$memberIds = $this->surveyEditorRepository->findMemberIdsBySurvey($sourceSurveyId);
$duplicate = $this->surveyRepository->create($actorMemberId ?? (int) ($survey->createdBy ?? 0), [
'title' => $this->buildDuplicateTitle((string) $survey->title),
'description' => (string) ($survey->description ?? ''),
'category' => $survey->category,
'isTemplate' => '1' === (string) ($survey->isTemplate ?? ''),
]);
$duplicateSurveyId = (int) $duplicate->id;
if ([] !== $memberIds) {
$this->surveyEditorRepository->syncMembersForSurvey($duplicateSurveyId, $memberIds);
}
$duplicateQuestions = [];
$questionIdMap = [];
$sourceQuestions = $this->surveyQuestionRepository->findAllBySurvey($sourceSurveyId);
foreach ($sourceQuestions as $sourceQuestion) {
$newQuestion = $this->surveyQuestionRepository->create($duplicateSurveyId, [
'type' => (string) $sourceQuestion->type,
'question' => (string) $sourceQuestion->question,
'description' => (string) ($sourceQuestion->description ?? ''),
'mandatory' => '1' === (string) $sourceQuestion->mandatory,
'published' => '1' === (string) $sourceQuestion->published,
'allowMaybe' => '1' === (string) ($sourceQuestion->allowMaybe ?? ''),
'allowMultiple' => '1' === (string) ($sourceQuestion->allowMultiple ?? ''),
'answerOption1' => (string) ($sourceQuestion->answerOption1 ?? ''),
'answerOption2' => (string) ($sourceQuestion->answerOption2 ?? ''),
'answerOption3' => (string) ($sourceQuestion->answerOption3 ?? ''),
'answerOption4' => (string) ($sourceQuestion->answerOption4 ?? ''),
'answerOption5' => (string) ($sourceQuestion->answerOption5 ?? ''),
'answerOption6' => (string) ($sourceQuestion->answerOption6 ?? ''),
'answerOption7' => (string) ($sourceQuestion->answerOption7 ?? ''),
'answerOption8' => (string) ($sourceQuestion->answerOption8 ?? ''),
'answerOption9' => (string) ($sourceQuestion->answerOption9 ?? ''),
'answerOption10' => (string) ($sourceQuestion->answerOption10 ?? ''),
'rangeMin' => (int) ($sourceQuestion->rangeMin ?? 0),
'rangeMax' => (int) ($sourceQuestion->rangeMax ?? 10),
'rangeStep' => (int) ($sourceQuestion->rangeStep ?? 1),
'jumpOnYes' => 0,
'jumpOnNo' => 0,
]);
$sourceQuestionId = (int) $sourceQuestion->id;
$questionIdMap[$sourceQuestionId] = (int) $newQuestion->id;
$duplicateQuestions[$sourceQuestionId] = $newQuestion;
}
foreach ($sourceQuestions as $sourceQuestion) {
$sourceQuestionId = (int) $sourceQuestion->id;
$duplicateQuestion = $duplicateQuestions[$sourceQuestionId] ?? null;
if (!$duplicateQuestion instanceof SurveyContentModel) {
throw new \RuntimeException('Fragenkopie konnte nicht vervollständigt werden.');
}
$this->surveyQuestionRepository->update($duplicateQuestion, [
'type' => (string) $sourceQuestion->type,
'question' => (string) $sourceQuestion->question,
'description' => (string) ($sourceQuestion->description ?? ''),
'mandatory' => '1' === (string) $sourceQuestion->mandatory,
'published' => '1' === (string) $sourceQuestion->published,
'allowMaybe' => '1' === (string) ($sourceQuestion->allowMaybe ?? ''),
'allowMultiple' => '1' === (string) ($sourceQuestion->allowMultiple ?? ''),
'answerOption1' => (string) ($sourceQuestion->answerOption1 ?? ''),
'answerOption2' => (string) ($sourceQuestion->answerOption2 ?? ''),
'answerOption3' => (string) ($sourceQuestion->answerOption3 ?? ''),
'answerOption4' => (string) ($sourceQuestion->answerOption4 ?? ''),
'answerOption5' => (string) ($sourceQuestion->answerOption5 ?? ''),
'answerOption6' => (string) ($sourceQuestion->answerOption6 ?? ''),
'answerOption7' => (string) ($sourceQuestion->answerOption7 ?? ''),
'answerOption8' => (string) ($sourceQuestion->answerOption8 ?? ''),
'answerOption9' => (string) ($sourceQuestion->answerOption9 ?? ''),
'answerOption10' => (string) ($sourceQuestion->answerOption10 ?? ''),
'rangeMin' => (int) ($sourceQuestion->rangeMin ?? 0),
'rangeMax' => (int) ($sourceQuestion->rangeMax ?? 10),
'rangeStep' => (int) ($sourceQuestion->rangeStep ?? 1),
'jumpOnYes' => $questionIdMap[(int) ($sourceQuestion->jumpOnYes ?? 0)] ?? 0,
'jumpOnNo' => $questionIdMap[(int) ($sourceQuestion->jumpOnNo ?? 0)] ?? 0,
]);
}
foreach ($this->surveyConditionRepository->findOverviewBySurvey($sourceSurveyId) as $condition) {
$sourceQuestionId = $questionIdMap[(int) ($condition['sourceQuestion'] ?? 0)] ?? 0;
if ($sourceQuestionId <= 0) {
continue;
}
$this->surveyConditionRepository->create($duplicateSurveyId, [
'sourceQuestion' => $sourceQuestionId,
'answerValue' => (string) ($condition['answerValue'] ?? ''),
'targetQuestion' => $questionIdMap[(int) ($condition['targetQuestion'] ?? 0)] ?? 0,
]);
}
$this->surveyRepository->refreshListMetadata($duplicateSurveyId);
return $this->surveyRepository->findById($duplicateSurveyId) ?? throw new \RuntimeException('Umfragekopie konnte nicht angelegt werden.');
});
}
public function saveCondition(SurveyModel $survey, SurveyConditionData $data, int $conditionId = 0): void
{
$this->assertUnlocked($survey);
@@ -133,4 +243,15 @@ final class SurveyEditorService
throw new \RuntimeException('Die Fragen dieser Umfrage können nach der Veröffentlichung oder nach ersten Teilnahmen nicht mehr geändert werden.');
}
}
private function buildDuplicateTitle(string $title): string
{
$title = trim($title);
if ('' === $title) {
$title = 'Neue Umfrage';
}
return mb_substr($title.' - Kopie', 0, 255);
}
}