Refine survey editor flow and auth styling
This commit is contained in:
@@ -11,10 +11,8 @@ use Contao\CoreBundle\Twig\FragmentTemplate;
|
||||
use Contao\FrontendUser;
|
||||
use Contao\ModuleModel;
|
||||
use Contao\PageModel;
|
||||
use Mummert\SurveyBundle\Form\Model\SurveyConditionData;
|
||||
use Mummert\SurveyBundle\Form\Model\SurveyEditorData;
|
||||
use Mummert\SurveyBundle\Form\Model\SurveyQuestionData;
|
||||
use Mummert\SurveyBundle\Form\SurveyConditionEditorType;
|
||||
use Mummert\SurveyBundle\Form\SurveyEditorType;
|
||||
use Mummert\SurveyBundle\Form\SurveyQuestionEditorType;
|
||||
use Mummert\SurveyBundle\Model\SurveyModel;
|
||||
@@ -22,9 +20,10 @@ use Mummert\SurveyBundle\Repository\SurveyConditionRepository;
|
||||
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
|
||||
use Mummert\SurveyBundle\Repository\SurveyQuestionRepository;
|
||||
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
||||
use Mummert\SurveyBundle\Security\SurveyEditorVoter;
|
||||
use Mummert\SurveyBundle\Service\SurveyEditorService;
|
||||
use Mummert\SurveyBundle\Service\SurveySubmissionService;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -39,43 +38,57 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
private readonly SurveyEditorRepository $surveyEditorRepository,
|
||||
private readonly SurveyEditorService $surveyEditorService,
|
||||
private readonly SurveySubmissionService $surveySubmissionService,
|
||||
private readonly FormFactoryInterface $formFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
||||
{
|
||||
$user = $this->getUser();
|
||||
$memberId = $this->resolveEditorMemberId($request);
|
||||
|
||||
if (!$user instanceof FrontendUser) {
|
||||
if ($memberId <= 0) {
|
||||
$template->set('loginRequired', true);
|
||||
$template->set('entryNotAllowed', false);
|
||||
$template->set('backUrl', $this->resolveListUrl($model));
|
||||
$template->set('createQuestionMode', false);
|
||||
|
||||
return $template->getResponse();
|
||||
}
|
||||
|
||||
$survey = $this->resolveSurvey($request, $user);
|
||||
if (!$this->isEditorEntryAllowed($request)) {
|
||||
$template->set('loginRequired', false);
|
||||
$template->set('entryNotAllowed', true);
|
||||
$template->set('createMode', false);
|
||||
$template->set('survey', null);
|
||||
$template->set('backUrl', $this->resolveListUrl($model));
|
||||
$template->set('createQuestionMode', false);
|
||||
|
||||
if ($survey instanceof SurveyModel && $request->isMethod('POST') && $request->request->has('_survey_action')) {
|
||||
return $this->handleAction($survey, $request, $model);
|
||||
return $template->getResponse();
|
||||
}
|
||||
|
||||
$questionChoices = $survey instanceof SurveyModel ? $this->buildQuestionChoices((int) $survey->id) : [];
|
||||
$surveyForm = $this->createNamed('survey_meta', SurveyEditorType::class, $survey instanceof SurveyModel ? SurveyEditorData::fromModel($survey) : new SurveyEditorData());
|
||||
$survey = $this->resolveSurvey($request, $memberId);
|
||||
$createQuestionMode = $this->isCreateQuestionRequested($request);
|
||||
|
||||
if ($survey instanceof SurveyModel && $request->isMethod('POST') && $request->request->has('_survey_action')) {
|
||||
return $this->handleAction($survey, $request, $model, $memberId);
|
||||
}
|
||||
|
||||
$surveyForm = $this->formFactory->createNamed('survey_meta', SurveyEditorType::class, $survey instanceof SurveyModel ? SurveyEditorData::fromModel($survey) : new SurveyEditorData());
|
||||
$surveyForm->handleRequest($request);
|
||||
|
||||
if ($surveyForm->isSubmitted() && $surveyForm->isValid()) {
|
||||
try {
|
||||
if ($survey instanceof SurveyModel) {
|
||||
$this->surveyEditorService->updateSurvey($survey, (int) $user->id, $surveyForm->getData());
|
||||
$this->surveyEditorService->updateSurvey($survey, $memberId, $surveyForm->getData());
|
||||
$this->addFlash('success', 'Die Umfrage wurde gespeichert.');
|
||||
|
||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
||||
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||
}
|
||||
|
||||
$createdSurvey = $this->surveyEditorService->createSurvey((int) $user->id, $surveyForm->getData());
|
||||
$createdSurvey = $this->surveyEditorService->createSurvey($memberId, $surveyForm->getData());
|
||||
$this->addFlash('success', 'Die Umfrage wurde angelegt.');
|
||||
|
||||
return new RedirectResponse($this->buildEditUrl($model, (int) $createdSurvey->id));
|
||||
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $createdSurvey->id));
|
||||
} catch (\Throwable $exception) {
|
||||
$this->addFlash('error', $exception->getMessage());
|
||||
}
|
||||
@@ -83,76 +96,101 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
|
||||
if (!$survey instanceof SurveyModel) {
|
||||
$template->set('loginRequired', false);
|
||||
$template->set('entryNotAllowed', false);
|
||||
$template->set('createMode', true);
|
||||
$template->set('surveyForm', $surveyForm->createView());
|
||||
$template->set('backUrl', $this->resolveListUrl($model));
|
||||
$template->set('publicUrl', null);
|
||||
$template->set('survey', null);
|
||||
$template->set('questionForm', null);
|
||||
$template->set('conditionForm', null);
|
||||
$template->set('questions', []);
|
||||
$template->set('conditions', []);
|
||||
$template->set('editors', []);
|
||||
$template->set('submissions', []);
|
||||
$template->set('activeQuestionId', 0);
|
||||
$template->set('activeConditionId', 0);
|
||||
$template->set('createQuestionMode', false);
|
||||
|
||||
return $template->getResponse();
|
||||
}
|
||||
|
||||
$questions = $this->surveyQuestionRepository->findAllBySurvey((int) $survey->id);
|
||||
$activeQuestion = $this->resolveActiveQuestion($request, (int) $survey->id);
|
||||
$activeCondition = $this->resolveActiveCondition($request, (int) $survey->id);
|
||||
$questionForm = $this->createNamed('survey_question', SurveyQuestionEditorType::class, $activeQuestion ? SurveyQuestionData::fromModel($activeQuestion) : new SurveyQuestionData(), [
|
||||
'question_choices' => $this->buildQuestionChoices((int) $survey->id),
|
||||
]);
|
||||
$conditionForm = $this->createNamed('survey_condition', SurveyConditionEditorType::class, $activeCondition ? SurveyConditionData::fromModel($activeCondition) : new SurveyConditionData(), [
|
||||
'question_choices' => $questionChoices,
|
||||
]);
|
||||
$activeQuestionId = $activeQuestion ? (int) $activeQuestion->id : 0;
|
||||
$questionFormViews = [];
|
||||
|
||||
$questionForm->handleRequest($request);
|
||||
$conditionForm->handleRequest($request);
|
||||
foreach ($questions as $question) {
|
||||
$questionId = (int) $question->id;
|
||||
$questionForm = $this->formFactory->createNamed('survey_question_'.$questionId, SurveyQuestionEditorType::class, SurveyQuestionData::fromModel($question), [
|
||||
'question_choices' => $this->buildQuestionChoices((int) $survey->id, $questionId),
|
||||
]);
|
||||
$questionForm->handleRequest($request);
|
||||
|
||||
if ($questionForm->isSubmitted() && $questionForm->isValid()) {
|
||||
try {
|
||||
$this->surveyEditorService->saveQuestion($survey, $questionForm->getData(), $activeQuestion ? (int) $activeQuestion->id : 0);
|
||||
$this->addFlash('success', $activeQuestion ? 'Die Frage wurde aktualisiert.' : 'Die Frage wurde angelegt.');
|
||||
if ($questionForm->isSubmitted()) {
|
||||
$activeQuestionId = $questionId;
|
||||
$createQuestionMode = false;
|
||||
|
||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
||||
} catch (\Throwable $exception) {
|
||||
$this->addFlash('error', $exception->getMessage());
|
||||
if ($questionForm->isValid()) {
|
||||
try {
|
||||
$savedQuestionId = $this->surveyEditorService->saveQuestion($survey, $questionForm->getData(), $questionId);
|
||||
$this->addFlash('success', 'Die Frage wurde aktualisiert.');
|
||||
|
||||
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||
} catch (\Throwable $exception) {
|
||||
$this->addFlash('error', $exception->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->addFlash('error', 'Frage konnte nicht gespeichert werden: '.$this->describeFormErrors($questionForm));
|
||||
}
|
||||
}
|
||||
|
||||
$questionFormViews[$questionId] = $questionForm->createView();
|
||||
}
|
||||
|
||||
if ($conditionForm->isSubmitted() && $conditionForm->isValid()) {
|
||||
try {
|
||||
$this->surveyEditorService->saveCondition($survey, $conditionForm->getData(), $activeCondition ? (int) $activeCondition->id : 0);
|
||||
$this->addFlash('success', $activeCondition ? 'Die Bedingung wurde aktualisiert.' : 'Die Bedingung wurde angelegt.');
|
||||
$newQuestionForm = $this->formFactory->createNamed('survey_question_new', SurveyQuestionEditorType::class, new SurveyQuestionData(), [
|
||||
'question_choices' => $this->buildQuestionChoices((int) $survey->id),
|
||||
]);
|
||||
$newQuestionForm->handleRequest($request);
|
||||
|
||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
||||
} catch (\Throwable $exception) {
|
||||
$this->addFlash('error', $exception->getMessage());
|
||||
if ($newQuestionForm->isSubmitted()) {
|
||||
$activeQuestionId = 0;
|
||||
$createQuestionMode = true;
|
||||
|
||||
if ($newQuestionForm->isValid()) {
|
||||
try {
|
||||
$savedQuestionId = $this->surveyEditorService->saveQuestion($survey, $newQuestionForm->getData(), 0);
|
||||
$this->addFlash('success', 'Die Frage wurde angelegt.');
|
||||
|
||||
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||
} catch (\Throwable $exception) {
|
||||
$this->addFlash('error', $exception->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->addFlash('error', 'Neue Frage konnte nicht gespeichert werden: '.$this->describeFormErrors($newQuestionForm));
|
||||
}
|
||||
}
|
||||
|
||||
$template->set('loginRequired', false);
|
||||
$template->set('entryNotAllowed', false);
|
||||
$template->set('createMode', false);
|
||||
$template->set('createQuestionMode', $createQuestionMode);
|
||||
$template->set('survey', $survey);
|
||||
$template->set('surveyForm', $surveyForm->createView());
|
||||
$template->set('questionForm', $questionForm->createView());
|
||||
$template->set('conditionForm', $conditionForm->createView());
|
||||
$template->set('questions', $this->surveyQuestionRepository->findAllBySurvey((int) $survey->id));
|
||||
$template->set('conditions', $this->surveyConditionRepository->findOverviewBySurvey((int) $survey->id));
|
||||
$template->set('questionForms', $questionFormViews);
|
||||
$template->set('newQuestionForm', $newQuestionForm->createView());
|
||||
$template->set('questions', $questions);
|
||||
$conditionOverview = $this->surveyConditionRepository->findOverviewBySurvey((int) $survey->id);
|
||||
|
||||
$template->set('conditions', $this->buildJumpRuleOverview($questions, $conditionOverview));
|
||||
$template->set('editors', $this->surveyEditorRepository->findEditorsBySurvey((int) $survey->id));
|
||||
$template->set('submissions', $this->surveySubmissionService->getSubmissionOverview($survey));
|
||||
$template->set('activeQuestionId', $activeQuestion ? (int) $activeQuestion->id : 0);
|
||||
$template->set('activeConditionId', $activeCondition ? (int) $activeCondition->id : 0);
|
||||
$template->set('activeQuestionId', $activeQuestionId);
|
||||
$template->set('backUrl', $this->resolveListUrl($model));
|
||||
$template->set('publicUrl', $this->resolveReaderUrl($model, (string) $survey->alias));
|
||||
$template->set('metadataFormActionUrl', $this->buildEditUrl($model, $request, (int) $survey->id));
|
||||
|
||||
return $template->getResponse();
|
||||
}
|
||||
|
||||
private function resolveSurvey(Request $request, FrontendUser $user): ?SurveyModel
|
||||
private function resolveSurvey(Request $request, int $memberId): ?SurveyModel
|
||||
{
|
||||
$surveyId = (int) $request->query->get('survey', 0);
|
||||
|
||||
@@ -166,12 +204,55 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
throw new \RuntimeException('Die Umfrage wurde nicht gefunden.');
|
||||
}
|
||||
|
||||
$this->denyAccessUnlessGranted(SurveyEditorVoter::EDIT, $survey);
|
||||
if (!$this->surveyEditorRepository->isEditor((int) $survey->id, $memberId)) {
|
||||
throw new \RuntimeException('Sie duerfen diese Umfrage nicht bearbeiten.');
|
||||
}
|
||||
|
||||
return $survey;
|
||||
}
|
||||
|
||||
private function handleAction(SurveyModel $survey, Request $request, ModuleModel $model): Response
|
||||
private function resolveEditorMemberId(Request $request): int
|
||||
{
|
||||
$user = $this->getUser();
|
||||
|
||||
if ($user instanceof FrontendUser) {
|
||||
return (int) $user->id;
|
||||
}
|
||||
|
||||
return $this->isDebugAccessAllowed($request) ? $this->resolveDebugMemberId($request) : 0;
|
||||
}
|
||||
|
||||
private function resolveDebugMemberId(Request $request): int
|
||||
{
|
||||
$memberId = (int) $request->query->get('debugMember', 0);
|
||||
|
||||
if ($memberId > 0) {
|
||||
return $memberId;
|
||||
}
|
||||
|
||||
$surveyId = (int) $request->query->get('survey', 0);
|
||||
|
||||
if ($surveyId <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$memberIds = $this->surveyEditorRepository->findMemberIdsBySurvey($surveyId);
|
||||
|
||||
return $memberIds[0] ?? 0;
|
||||
}
|
||||
|
||||
private function isDebugAccessAllowed(Request $request): bool
|
||||
{
|
||||
$host = strtolower((string) ($request->server->get('HTTP_HOST') ?: $request->getHost()));
|
||||
|
||||
return (
|
||||
'localhost' === $host
|
||||
|| '127.0.0.1' === $host
|
||||
|| str_ends_with($host, '.ddev.site')
|
||||
) && $request->query->getBoolean('debugAccess');
|
||||
}
|
||||
|
||||
private function handleAction(SurveyModel $survey, Request $request, ModuleModel $model, int $memberId): Response
|
||||
{
|
||||
$action = (string) $request->request->get('_survey_action');
|
||||
$itemId = (int) $request->request->get('item_id', 0);
|
||||
@@ -182,6 +263,11 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
}
|
||||
|
||||
try {
|
||||
if ('toggle-published' === $action) {
|
||||
$isPublished = $this->surveyEditorService->toggleSurveyPublished($survey, $memberId);
|
||||
$this->addFlash('success', $isPublished ? 'Die Umfrage ist jetzt veroeffentlicht.' : 'Die Umfrage ist jetzt nicht mehr veroeffentlicht.');
|
||||
}
|
||||
|
||||
if ('delete-question' === $action) {
|
||||
$this->surveyEditorService->deleteQuestion($survey, $itemId);
|
||||
$this->addFlash('success', 'Die Frage wurde geloescht.');
|
||||
@@ -195,7 +281,7 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
$this->addFlash('error', $exception->getMessage());
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->buildEditUrl($model, (int) $survey->id));
|
||||
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
||||
}
|
||||
|
||||
private function resolveActiveQuestion(Request $request, int $surveyId): mixed
|
||||
@@ -205,16 +291,18 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
return $questionId > 0 ? $this->surveyQuestionRepository->findByIdForSurvey($surveyId, $questionId) : null;
|
||||
}
|
||||
|
||||
private function resolveActiveCondition(Request $request, int $surveyId): mixed
|
||||
private function isCreateQuestionRequested(Request $request): bool
|
||||
{
|
||||
$conditionId = (int) $request->query->get('condition', 0);
|
||||
$condition = $conditionId > 0 ? $this->surveyConditionRepository->findById($conditionId) : null;
|
||||
return 'new' === trim((string) $request->query->get('question', ''));
|
||||
}
|
||||
|
||||
if (null === $condition || (int) $condition->pid !== $surveyId) {
|
||||
return null;
|
||||
private function isEditorEntryAllowed(Request $request): bool
|
||||
{
|
||||
if ((int) $request->query->get('survey', 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $condition;
|
||||
return $request->query->getBoolean('create');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,6 +323,84 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
return $choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $questions
|
||||
* @param list<array<string, mixed>> $conditionOverview
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function buildJumpRuleOverview(array $questions, array $conditionOverview): array
|
||||
{
|
||||
$questionLabels = [];
|
||||
$questionSortings = [];
|
||||
$rules = [];
|
||||
|
||||
foreach ($questions as $question) {
|
||||
$questionId = (int) ($question->id ?? 0);
|
||||
|
||||
if ($questionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$questionLabels[$questionId] = (string) ($question->question ?? '');
|
||||
$questionSortings[$questionId] = (int) ($question->sorting ?? 0);
|
||||
}
|
||||
|
||||
foreach ($questions as $question) {
|
||||
$questionId = (int) ($question->id ?? 0);
|
||||
|
||||
if ($questionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('yes_no_maybe' !== (string) ($question->type ?? '')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (['Ja' => (int) ($question->jumpOnYes ?? 0), 'Nein' => (int) ($question->jumpOnNo ?? 0)] as $answerValue => $targetQuestionId) {
|
||||
if ($targetQuestionId <= 0 || !isset($questionLabels[$targetQuestionId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rules[] = [
|
||||
'id' => 'question-'.$questionId.'-'.$targetQuestionId.'-'.$answerValue,
|
||||
'answerValue' => $answerValue,
|
||||
'sourceQuestion' => $questionId,
|
||||
'targetQuestion' => $targetQuestionId,
|
||||
'sourceQuestionLabel' => (string) ($question->question ?? ''),
|
||||
'targetQuestionLabel' => $questionLabels[$targetQuestionId] ?? '',
|
||||
'sourceSorting' => $questionSortings[$questionId] ?? 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($conditionOverview as $condition) {
|
||||
$sourceQuestionId = (int) ($condition['sourceQuestion'] ?? 0);
|
||||
|
||||
$rules[] = [
|
||||
'id' => $condition['id'] ?? 0,
|
||||
'answerValue' => (string) ($condition['answerValue'] ?? ''),
|
||||
'sourceQuestion' => $sourceQuestionId,
|
||||
'targetQuestion' => (int) ($condition['targetQuestion'] ?? 0),
|
||||
'sourceQuestionLabel' => (string) ($condition['sourceQuestionLabel'] ?? ''),
|
||||
'targetQuestionLabel' => (string) ($condition['targetQuestionLabel'] ?? ''),
|
||||
'sourceSorting' => $questionSortings[$sourceQuestionId] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
usort($rules, static function (array $left, array $right): int {
|
||||
$sortingComparison = ((int) ($left['sourceSorting'] ?? 0)) <=> ((int) ($right['sourceSorting'] ?? 0));
|
||||
|
||||
if (0 !== $sortingComparison) {
|
||||
return $sortingComparison;
|
||||
}
|
||||
|
||||
return ((int) ($left['targetQuestion'] ?? 0)) <=> ((int) ($right['targetQuestion'] ?? 0));
|
||||
});
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
private function resolveListUrl(ModuleModel $model): string
|
||||
{
|
||||
$page = $this->resolvePage((int) ($model->surveyListPage ?? 0)) ?? $this->getPageModel();
|
||||
@@ -253,11 +419,44 @@ final class MemberSurveyEditController extends AbstractFrontendModuleController
|
||||
return $this->generateContentUrl($page, ['auto_item' => $publicAlias]);
|
||||
}
|
||||
|
||||
private function buildEditUrl(ModuleModel $model, int $surveyId): string
|
||||
private function buildEditUrl(ModuleModel $model, Request $request, int $surveyId): string
|
||||
{
|
||||
$page = $this->getPageModel();
|
||||
$parameters = ['survey' => $surveyId];
|
||||
|
||||
return $page instanceof PageModel ? $this->generateContentUrl($page, ['survey' => $surveyId]) : $this->resolveListUrl($model);
|
||||
if ($request->query->getBoolean('debugAccess') && $this->isDebugAccessAllowed($request)) {
|
||||
$parameters['debugAccess'] = '1';
|
||||
}
|
||||
|
||||
$debugMemberId = (int) $request->query->get('debugMember', 0);
|
||||
|
||||
if ($debugMemberId > 0) {
|
||||
$parameters['debugMember'] = (string) $debugMemberId;
|
||||
}
|
||||
|
||||
$basePath = $request->getBaseUrl().$request->getPathInfo();
|
||||
|
||||
if ('' !== $basePath) {
|
||||
$queryString = http_build_query($parameters);
|
||||
$url = $basePath.('' !== $queryString ? '?'.$queryString : '');
|
||||
} else {
|
||||
$page = $this->getPageModel();
|
||||
$url = $page instanceof PageModel ? $this->generateContentUrl($page, $parameters) : $this->resolveListUrl($model);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
private function describeFormErrors(FormInterface $form): string
|
||||
{
|
||||
$messages = [];
|
||||
|
||||
foreach ($form->getErrors(true, true) as $error) {
|
||||
$origin = $error->getOrigin();
|
||||
$name = $origin instanceof FormInterface ? $origin->getName() : 'form';
|
||||
$messages[] = sprintf('%s: %s', $name, $error->getMessage());
|
||||
}
|
||||
|
||||
return [] !== $messages ? implode(' | ', array_unique($messages)) : 'Unbekannter Formularfehler.';
|
||||
}
|
||||
|
||||
private function resolvePage(int $pageId): ?PageModel
|
||||
|
||||
@@ -11,8 +11,11 @@ use Contao\CoreBundle\Twig\FragmentTemplate;
|
||||
use Contao\FrontendUser;
|
||||
use Contao\ModuleModel;
|
||||
use Contao\PageModel;
|
||||
use Mummert\SurveyBundle\Model\SurveyModel;
|
||||
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
|
||||
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
||||
use Mummert\SurveyBundle\Service\SurveySubmissionService;
|
||||
use Mummert\SurveyBundle\Service\SurveyEditorService;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
@@ -21,7 +24,8 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SurveyRepository $surveyRepository,
|
||||
private readonly SurveySubmissionService $surveySubmissionService,
|
||||
private readonly SurveyEditorRepository $surveyEditorRepository,
|
||||
private readonly SurveyEditorService $surveyEditorService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -37,6 +41,10 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
||||
return $template->getResponse();
|
||||
}
|
||||
|
||||
if ($request->isMethod('POST') && $request->request->has('_survey_action')) {
|
||||
return $this->handleAction($request, (int) $user->id);
|
||||
}
|
||||
|
||||
$editPage = $this->resolvePage((int) ($model->surveyEditPage ?? 0));
|
||||
$readerPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyReaderPage'));
|
||||
$resultsPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyResultsPage'));
|
||||
@@ -44,7 +52,6 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
||||
|
||||
foreach ($this->surveyRepository->findEditableByMember((int) $user->id) as $survey) {
|
||||
$surveyId = (int) $survey['id'];
|
||||
$isCompletedInCurrentSession = $this->surveySubmissionService->hasFinishedSubmissionForSurvey($surveyId, $request->getSession());
|
||||
|
||||
$items[] = [
|
||||
'id' => $surveyId,
|
||||
@@ -53,20 +60,60 @@ final class MemberSurveyListController extends AbstractFrontendModuleController
|
||||
'isLocked' => !empty($survey['isLocked']),
|
||||
'published' => !empty($survey['published']),
|
||||
'questionCount' => (int) ($survey['questionCount'] ?? 0),
|
||||
'answerCount' => (int) ($survey['answerCount'] ?? 0),
|
||||
'participationCount' => (int) ($survey['participationCount'] ?? 0),
|
||||
'editUrl' => $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['survey' => $surveyId]) : null,
|
||||
'publicUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['auto_item' => (string) $survey['alias']]) : null,
|
||||
'resultsUrl' => !$isCompletedInCurrentSession && $resultsPage instanceof PageModel ? $this->generateContentUrl($resultsPage, ['auto_item' => (string) $survey['alias']]) : null,
|
||||
'resultsUrl' => $resultsPage instanceof PageModel ? $this->generateContentUrl($resultsPage, ['auto_item' => (string) $survey['alias']]) : null,
|
||||
];
|
||||
}
|
||||
|
||||
$template->set('loginRequired', false);
|
||||
$template->set('surveys', $items);
|
||||
$template->set('createUrl', $editPage instanceof PageModel ? $this->generateContentUrl($editPage) : null);
|
||||
$template->set('createUrl', $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['create' => '1']) : null);
|
||||
|
||||
return $template->getResponse();
|
||||
}
|
||||
|
||||
private function handleAction(Request $request, int $memberId): Response
|
||||
{
|
||||
$surveyId = (int) $request->request->get('item_id', 0);
|
||||
$action = (string) $request->request->get('_survey_action');
|
||||
$token = (string) $request->request->get('_token');
|
||||
|
||||
if ($surveyId <= 0 || !$this->isCsrfTokenValid($action.'-'.$surveyId, $token)) {
|
||||
throw new \RuntimeException('Ungueltiger CSRF-Token.');
|
||||
}
|
||||
|
||||
$survey = $this->resolveEditableSurvey($surveyId, $memberId);
|
||||
|
||||
try {
|
||||
if ('toggle-published' === $action) {
|
||||
$isPublished = $this->surveyEditorService->toggleSurveyPublished($survey, $memberId);
|
||||
$this->addFlash('success', $isPublished ? 'Die Umfrage ist jetzt veroeffentlicht.' : 'Die Umfrage ist jetzt nicht mehr veroeffentlicht.');
|
||||
}
|
||||
|
||||
if ('delete-survey' === $action) {
|
||||
$this->surveyEditorService->deleteSurvey($survey);
|
||||
$this->addFlash('success', 'Die Umfrage mit allen Fragen, Antworten und Ergebnissen wurde geloescht.');
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
$this->addFlash('error', $exception->getMessage());
|
||||
}
|
||||
|
||||
return new RedirectResponse($request->getBaseUrl().$request->getPathInfo());
|
||||
}
|
||||
|
||||
private function resolveEditableSurvey(int $surveyId, int $memberId): SurveyModel
|
||||
{
|
||||
$survey = $this->surveyRepository->findById($surveyId);
|
||||
|
||||
if (!$survey instanceof SurveyModel || !$this->surveyEditorRepository->isEditor($surveyId, $memberId)) {
|
||||
throw new \RuntimeException('Sie duerfen diese Umfrage nicht bearbeiten.');
|
||||
}
|
||||
|
||||
return $survey;
|
||||
}
|
||||
|
||||
private function resolvePage(int $pageId): ?PageModel
|
||||
{
|
||||
if ($pageId <= 0) {
|
||||
|
||||
@@ -10,23 +10,23 @@ final class SurveyQuestionData
|
||||
{
|
||||
public string $type = 'yes_no_maybe';
|
||||
public string $question = '';
|
||||
public string $description = '';
|
||||
public ?string $description = '';
|
||||
public bool $mandatory = true;
|
||||
public bool $published = true;
|
||||
public bool $allowMaybe = false;
|
||||
public bool $allowMultiple = false;
|
||||
public int $jumpOnYes = 0;
|
||||
public int $jumpOnNo = 0;
|
||||
public string $answerOption1 = '';
|
||||
public string $answerOption2 = '';
|
||||
public string $answerOption3 = '';
|
||||
public string $answerOption4 = '';
|
||||
public string $answerOption5 = '';
|
||||
public string $answerOption6 = '';
|
||||
public string $answerOption7 = '';
|
||||
public string $answerOption8 = '';
|
||||
public string $answerOption9 = '';
|
||||
public string $answerOption10 = '';
|
||||
public ?int $jumpOnYes = 0;
|
||||
public ?int $jumpOnNo = 0;
|
||||
public ?string $answerOption1 = '';
|
||||
public ?string $answerOption2 = '';
|
||||
public ?string $answerOption3 = '';
|
||||
public ?string $answerOption4 = '';
|
||||
public ?string $answerOption5 = '';
|
||||
public ?string $answerOption6 = '';
|
||||
public ?string $answerOption7 = '';
|
||||
public ?string $answerOption8 = '';
|
||||
public ?string $answerOption9 = '';
|
||||
public ?string $answerOption10 = '';
|
||||
public int $rangeMin = 0;
|
||||
public int $rangeMax = 10;
|
||||
public int $rangeStep = 1;
|
||||
@@ -64,13 +64,13 @@ final class SurveyQuestionData
|
||||
$payload = [
|
||||
'type' => $this->type,
|
||||
'question' => $this->question,
|
||||
'description' => $this->description,
|
||||
'description' => (string) ($this->description ?? ''),
|
||||
'mandatory' => $this->mandatory,
|
||||
'published' => $this->published,
|
||||
'allowMaybe' => $this->allowMaybe,
|
||||
'allowMultiple' => $this->allowMultiple,
|
||||
'jumpOnYes' => $this->jumpOnYes,
|
||||
'jumpOnNo' => $this->jumpOnNo,
|
||||
'jumpOnYes' => max(0, (int) ($this->jumpOnYes ?? 0)),
|
||||
'jumpOnNo' => max(0, (int) ($this->jumpOnNo ?? 0)),
|
||||
'rangeMin' => $this->rangeMin,
|
||||
'rangeMax' => $this->rangeMax,
|
||||
'rangeStep' => $this->rangeStep,
|
||||
@@ -78,7 +78,7 @@ final class SurveyQuestionData
|
||||
|
||||
for ($index = 1; $index <= 10; ++$index) {
|
||||
$property = 'answerOption'.$index;
|
||||
$payload[$property] = $this->{$property};
|
||||
$payload[$property] = (string) ($this->{$property} ?? '');
|
||||
}
|
||||
|
||||
return $payload;
|
||||
|
||||
@@ -26,6 +26,7 @@ final class SurveyEditorType extends AbstractType
|
||||
->add('description', TextareaType::class, [
|
||||
'label' => 'Beschreibung',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['rows' => 5],
|
||||
])
|
||||
->add('published', CheckboxType::class, [
|
||||
|
||||
@@ -12,7 +12,10 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\GreaterThanOrEqual;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
@@ -26,6 +29,9 @@ final class SurveyQuestionEditorType extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT, $this->normalizeOptionalFields(...));
|
||||
$builder->addEventListener(FormEvents::POST_SUBMIT, $this->validateChoiceOptions(...));
|
||||
|
||||
$builder
|
||||
->add('type', ChoiceType::class, [
|
||||
'label' => 'Fragetyp',
|
||||
@@ -43,6 +49,7 @@ final class SurveyQuestionEditorType extends AbstractType
|
||||
->add('description', TextareaType::class, [
|
||||
'label' => 'Beschreibung',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['rows' => 4],
|
||||
])
|
||||
->add('mandatory', CheckboxType::class, [
|
||||
@@ -61,15 +68,17 @@ final class SurveyQuestionEditorType extends AbstractType
|
||||
->add('jumpOnYes', ChoiceType::class, [
|
||||
'label' => 'Zur Frage springen bei Auswahl ja',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'choices' => array_flip($options['question_choices']),
|
||||
'placeholder' => 'keine',
|
||||
'placeholder' => 'normal weitermachen',
|
||||
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
|
||||
])
|
||||
->add('jumpOnNo', ChoiceType::class, [
|
||||
'label' => 'Zur Frage springen bei Auswahl nein',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'choices' => array_flip($options['question_choices']),
|
||||
'placeholder' => 'keine',
|
||||
'placeholder' => 'normal weitermachen',
|
||||
'attr' => ['data-question-editor-target' => 'yesNoMaybeField'],
|
||||
])
|
||||
->add('allowMultiple', CheckboxType::class, [
|
||||
@@ -80,71 +89,130 @@ final class SurveyQuestionEditorType extends AbstractType
|
||||
->add('answerOption1', TextType::class, [
|
||||
'label' => 'Antwort 1',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption2', TextType::class, [
|
||||
'label' => 'Antwort 2',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption3', TextType::class, [
|
||||
'label' => 'Antwort 3',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption4', TextType::class, [
|
||||
'label' => 'Antwort 4',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption5', TextType::class, [
|
||||
'label' => 'Antwort 5',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption6', TextType::class, [
|
||||
'label' => 'Antwort 6',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption7', TextType::class, [
|
||||
'label' => 'Antwort 7',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption8', TextType::class, [
|
||||
'label' => 'Antwort 8',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption9', TextType::class, [
|
||||
'label' => 'Antwort 9',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('answerOption10', TextType::class, [
|
||||
'label' => 'Antwort 10',
|
||||
'required' => false,
|
||||
'empty_data' => '',
|
||||
'attr' => ['data-question-editor-target' => 'choiceField'],
|
||||
])
|
||||
->add('rangeMin', IntegerType::class, [
|
||||
'label' => 'Kleinster Wert',
|
||||
'empty_data' => '0',
|
||||
'constraints' => [new GreaterThanOrEqual(['value' => -100000])],
|
||||
'attr' => ['data-question-editor-target' => 'rangeField'],
|
||||
])
|
||||
->add('rangeMax', IntegerType::class, [
|
||||
'label' => 'Groesster Wert',
|
||||
'empty_data' => '10',
|
||||
'constraints' => [new GreaterThanOrEqual(['value' => -100000])],
|
||||
'attr' => ['data-question-editor-target' => 'rangeField'],
|
||||
])
|
||||
->add('rangeStep', IntegerType::class, [
|
||||
'label' => 'Schrittweite',
|
||||
'empty_data' => '1',
|
||||
'constraints' => [new GreaterThanOrEqual(['value' => 1])],
|
||||
'attr' => ['data-question-editor-target' => 'rangeField'],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
private function normalizeOptionalFields(FormEvent $event): void
|
||||
{
|
||||
$data = $event->getData();
|
||||
|
||||
if (!\is_array($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stringFields = ['description'];
|
||||
|
||||
for ($index = 1; $index <= 10; ++$index) {
|
||||
$stringFields[] = 'answerOption'.$index;
|
||||
}
|
||||
|
||||
foreach ($stringFields as $field) {
|
||||
if (null === ($data[$field] ?? null)) {
|
||||
$data[$field] = '';
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['jumpOnYes' => '', 'jumpOnNo' => '', 'rangeMin' => '0', 'rangeMax' => '10', 'rangeStep' => '1'] as $field => $defaultValue) {
|
||||
if (null === ($data[$field] ?? null) || '' === $data[$field]) {
|
||||
$data[$field] = $defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
$event->setData($data);
|
||||
}
|
||||
|
||||
private function validateChoiceOptions(FormEvent $event): void
|
||||
{
|
||||
$data = $event->getData();
|
||||
|
||||
if (!$data instanceof SurveyQuestionData || 'choice' !== $data->type) {
|
||||
return;
|
||||
}
|
||||
|
||||
for ($index = 1; $index <= 10; ++$index) {
|
||||
if ('' !== trim((string) ($data->{'answerOption'.$index} ?? ''))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$event->getForm()->get('answerOption1')->addError(new FormError('Mindestens eine Antwortoption ist erforderlich.'));
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
|
||||
@@ -59,32 +59,34 @@ final class SurveyQuestionRepository
|
||||
public function create(int $surveyId, array $data): SurveyContentModel
|
||||
{
|
||||
$sorting = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(sorting), 0) FROM tl_survey_content WHERE pid = ?', [$surveyId]) + 128;
|
||||
$type = (string) ($data['type'] ?? 'yes_no_maybe');
|
||||
$isRangeType = 'range' === $type;
|
||||
|
||||
$this->connection->insert('tl_survey_content', [
|
||||
'pid' => $surveyId,
|
||||
'sorting' => $sorting,
|
||||
'type' => (string) ($data['type'] ?? 'yes_no_maybe'),
|
||||
'type' => $type,
|
||||
'question' => (string) ($data['question'] ?? ''),
|
||||
'description' => (string) ($data['description'] ?? ''),
|
||||
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
||||
'published' => !empty($data['published']) ? '1' : '',
|
||||
'allowMaybe' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') && !empty($data['allowMaybe']) ? '1' : '',
|
||||
'jumpOnYes' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') ? max(0, (int) ($data['jumpOnYes'] ?? 0)) : 0,
|
||||
'jumpOnNo' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') ? max(0, (int) ($data['jumpOnNo'] ?? 0)) : 0,
|
||||
'allowMultiple' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') && !empty($data['allowMultiple']) ? '1' : '',
|
||||
'answerOption1' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption1'] ?? '')) : '',
|
||||
'answerOption2' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption2'] ?? '')) : '',
|
||||
'answerOption3' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption3'] ?? '')) : '',
|
||||
'answerOption4' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption4'] ?? '')) : '',
|
||||
'answerOption5' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption5'] ?? '')) : '',
|
||||
'answerOption6' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption6'] ?? '')) : '',
|
||||
'answerOption7' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption7'] ?? '')) : '',
|
||||
'answerOption8' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption8'] ?? '')) : '',
|
||||
'answerOption9' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption9'] ?? '')) : '',
|
||||
'answerOption10' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption10'] ?? '')) : '',
|
||||
'rangeMin' => (int) ($data['rangeMin'] ?? 0),
|
||||
'rangeMax' => (int) ($data['rangeMax'] ?? 10),
|
||||
'rangeStep' => max(1, (int) ($data['rangeStep'] ?? 1)),
|
||||
'allowMaybe' => 'yes_no_maybe' === $type && !empty($data['allowMaybe']) ? '1' : '',
|
||||
'jumpOnYes' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnYes'] ?? 0)) : 0,
|
||||
'jumpOnNo' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnNo'] ?? 0)) : 0,
|
||||
'allowMultiple' => 'choice' === $type && !empty($data['allowMultiple']) ? '1' : '',
|
||||
'answerOption1' => 'choice' === $type ? trim((string) ($data['answerOption1'] ?? '')) : '',
|
||||
'answerOption2' => 'choice' === $type ? trim((string) ($data['answerOption2'] ?? '')) : '',
|
||||
'answerOption3' => 'choice' === $type ? trim((string) ($data['answerOption3'] ?? '')) : '',
|
||||
'answerOption4' => 'choice' === $type ? trim((string) ($data['answerOption4'] ?? '')) : '',
|
||||
'answerOption5' => 'choice' === $type ? trim((string) ($data['answerOption5'] ?? '')) : '',
|
||||
'answerOption6' => 'choice' === $type ? trim((string) ($data['answerOption6'] ?? '')) : '',
|
||||
'answerOption7' => 'choice' === $type ? trim((string) ($data['answerOption7'] ?? '')) : '',
|
||||
'answerOption8' => 'choice' === $type ? trim((string) ($data['answerOption8'] ?? '')) : '',
|
||||
'answerOption9' => 'choice' === $type ? trim((string) ($data['answerOption9'] ?? '')) : '',
|
||||
'answerOption10' => 'choice' === $type ? trim((string) ($data['answerOption10'] ?? '')) : '',
|
||||
'rangeMin' => $isRangeType ? (int) ($data['rangeMin'] ?? 0) : 0,
|
||||
'rangeMax' => $isRangeType ? (int) ($data['rangeMax'] ?? 10) : 10,
|
||||
'rangeStep' => $isRangeType ? max(1, (int) ($data['rangeStep'] ?? 1)) : 1,
|
||||
]);
|
||||
|
||||
$id = (int) $this->connection->lastInsertId();
|
||||
@@ -94,29 +96,32 @@ final class SurveyQuestionRepository
|
||||
|
||||
public function update(SurveyContentModel $question, array $data): void
|
||||
{
|
||||
$type = (string) ($data['type'] ?? $question->type);
|
||||
$isRangeType = 'range' === $type;
|
||||
|
||||
$this->connection->update('tl_survey_content', [
|
||||
'type' => (string) ($data['type'] ?? $question->type),
|
||||
'type' => $type,
|
||||
'question' => (string) ($data['question'] ?? $question->question),
|
||||
'description' => (string) ($data['description'] ?? $question->description),
|
||||
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
||||
'published' => !empty($data['published']) ? '1' : '',
|
||||
'allowMaybe' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) && !empty($data['allowMaybe']) ? '1' : '',
|
||||
'jumpOnYes' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) ? max(0, (int) ($data['jumpOnYes'] ?? $question->jumpOnYes)) : 0,
|
||||
'jumpOnNo' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) ? max(0, (int) ($data['jumpOnNo'] ?? $question->jumpOnNo)) : 0,
|
||||
'allowMultiple' => 'choice' === (string) ($data['type'] ?? $question->type) && !empty($data['allowMultiple']) ? '1' : '',
|
||||
'answerOption1' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption1'] ?? $question->answerOption1)) : '',
|
||||
'answerOption2' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption2'] ?? $question->answerOption2)) : '',
|
||||
'answerOption3' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption3'] ?? $question->answerOption3)) : '',
|
||||
'answerOption4' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption4'] ?? $question->answerOption4)) : '',
|
||||
'answerOption5' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption5'] ?? $question->answerOption5)) : '',
|
||||
'answerOption6' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption6'] ?? $question->answerOption6)) : '',
|
||||
'answerOption7' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption7'] ?? $question->answerOption7)) : '',
|
||||
'answerOption8' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption8'] ?? $question->answerOption8)) : '',
|
||||
'answerOption9' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption9'] ?? $question->answerOption9)) : '',
|
||||
'answerOption10' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption10'] ?? $question->answerOption10)) : '',
|
||||
'rangeMin' => (int) ($data['rangeMin'] ?? $question->rangeMin),
|
||||
'rangeMax' => (int) ($data['rangeMax'] ?? $question->rangeMax),
|
||||
'rangeStep' => max(1, (int) ($data['rangeStep'] ?? $question->rangeStep)),
|
||||
'allowMaybe' => 'yes_no_maybe' === $type && !empty($data['allowMaybe']) ? '1' : '',
|
||||
'jumpOnYes' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnYes'] ?? $question->jumpOnYes)) : 0,
|
||||
'jumpOnNo' => 'yes_no_maybe' === $type ? max(0, (int) ($data['jumpOnNo'] ?? $question->jumpOnNo)) : 0,
|
||||
'allowMultiple' => 'choice' === $type && !empty($data['allowMultiple']) ? '1' : '',
|
||||
'answerOption1' => 'choice' === $type ? trim((string) ($data['answerOption1'] ?? $question->answerOption1)) : '',
|
||||
'answerOption2' => 'choice' === $type ? trim((string) ($data['answerOption2'] ?? $question->answerOption2)) : '',
|
||||
'answerOption3' => 'choice' === $type ? trim((string) ($data['answerOption3'] ?? $question->answerOption3)) : '',
|
||||
'answerOption4' => 'choice' === $type ? trim((string) ($data['answerOption4'] ?? $question->answerOption4)) : '',
|
||||
'answerOption5' => 'choice' === $type ? trim((string) ($data['answerOption5'] ?? $question->answerOption5)) : '',
|
||||
'answerOption6' => 'choice' === $type ? trim((string) ($data['answerOption6'] ?? $question->answerOption6)) : '',
|
||||
'answerOption7' => 'choice' === $type ? trim((string) ($data['answerOption7'] ?? $question->answerOption7)) : '',
|
||||
'answerOption8' => 'choice' === $type ? trim((string) ($data['answerOption8'] ?? $question->answerOption8)) : '',
|
||||
'answerOption9' => 'choice' === $type ? trim((string) ($data['answerOption9'] ?? $question->answerOption9)) : '',
|
||||
'answerOption10' => 'choice' === $type ? trim((string) ($data['answerOption10'] ?? $question->answerOption10)) : '',
|
||||
'rangeMin' => $isRangeType ? (int) ($data['rangeMin'] ?? $question->rangeMin) : 0,
|
||||
'rangeMax' => $isRangeType ? (int) ($data['rangeMax'] ?? $question->rangeMax) : 10,
|
||||
'rangeStep' => $isRangeType ? max(1, (int) ($data['rangeStep'] ?? $question->rangeStep)) : 1,
|
||||
], [
|
||||
'id' => (int) $question->id,
|
||||
]);
|
||||
@@ -129,6 +134,20 @@ final class SurveyQuestionRepository
|
||||
'pid' => $surveyId,
|
||||
]);
|
||||
|
||||
$this->connection->update('tl_survey_content', [
|
||||
'jumpOnYes' => 0,
|
||||
], [
|
||||
'pid' => $surveyId,
|
||||
'jumpOnYes' => $questionId,
|
||||
]);
|
||||
|
||||
$this->connection->update('tl_survey_content', [
|
||||
'jumpOnNo' => 0,
|
||||
], [
|
||||
'pid' => $surveyId,
|
||||
'jumpOnNo' => $questionId,
|
||||
]);
|
||||
|
||||
$this->connection->delete('tl_survey_condition', [
|
||||
'sourceQuestion' => $questionId,
|
||||
]);
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Mummert\SurveyBundle\Repository;
|
||||
|
||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||
use Contao\StringUtil;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mummert\SurveyBundle\Model\SurveyModel;
|
||||
|
||||
@@ -67,12 +68,11 @@ final class SurveyRepository
|
||||
s.isLocked,
|
||||
s.updatedAt,
|
||||
COUNT(DISTINCT q.id) AS questionCount,
|
||||
COUNT(DISTINCT a.id) AS answerCount
|
||||
COUNT(DISTINCT CASE WHEN sub.isFinished = 1 THEN sub.id END) AS participationCount
|
||||
FROM tl_survey s
|
||||
INNER JOIN tl_survey_editor e ON COALESCE(NULLIF(e.survey, 0), e.pid) = s.id AND e.member = :member
|
||||
LEFT JOIN tl_survey_content q ON q.pid = s.id
|
||||
LEFT JOIN tl_survey_submission sub ON sub.survey = s.id
|
||||
LEFT JOIN tl_survey_answer a ON a.submission = sub.id
|
||||
GROUP BY s.id
|
||||
ORDER BY s.updatedAt DESC, s.title ASC
|
||||
SQL,
|
||||
@@ -91,7 +91,7 @@ final class SurveyRepository
|
||||
'alias' => $this->ensurePublicAlias(null),
|
||||
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])),
|
||||
'description' => (string) ($data['description'] ?? ''),
|
||||
'published' => !empty($data['published']) ? '1' : '',
|
||||
'published' => '',
|
||||
'isLocked' => '',
|
||||
'createdBy' => $memberId,
|
||||
'updatedBy' => $memberId,
|
||||
@@ -212,6 +212,31 @@ final class SurveyRepository
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteCascade(int $surveyId): void
|
||||
{
|
||||
$this->connection->transactional(function () use ($surveyId): void {
|
||||
$submissionIds = array_values(array_map('intval', $this->connection->fetchFirstColumn(
|
||||
'SELECT id FROM tl_survey_submission WHERE survey = ?',
|
||||
[$surveyId],
|
||||
)));
|
||||
|
||||
if ([] !== $submissionIds) {
|
||||
$this->connection->executeStatement(
|
||||
'DELETE FROM tl_survey_answer WHERE submission IN (?)',
|
||||
[$submissionIds],
|
||||
[ArrayParameterType::INTEGER],
|
||||
);
|
||||
}
|
||||
|
||||
$this->connection->delete('tl_survey_submission', ['survey' => $surveyId]);
|
||||
$this->connection->delete('tl_survey_condition', ['pid' => $surveyId]);
|
||||
$this->connection->delete('tl_survey_content', ['pid' => $surveyId]);
|
||||
$this->connection->delete('tl_survey_editor', ['survey' => $surveyId]);
|
||||
$this->connection->delete('tl_survey_editor', ['pid' => $surveyId]);
|
||||
$this->connection->delete('tl_survey', ['id' => $surveyId]);
|
||||
});
|
||||
}
|
||||
|
||||
private function generateRandomPublicAlias(): string
|
||||
{
|
||||
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
@@ -208,7 +208,7 @@
|
||||
<p>{{ (survey.description|striptags|trim) ?: 'survey.pdf.no_description'|trans }}</p>
|
||||
|
||||
<div class="meta-row">
|
||||
<span class="meta-pill">{{ 'survey.pdf.completed_submissions'|trans({'%count%': completedSubmissionCount}) }}</span>
|
||||
<span class="meta-pill">{{ 'survey.pdf.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
|
||||
<span class="meta-pill">{{ 'survey.pdf.questions'|trans({'%count%': questionResults|length}) }}</span>
|
||||
<span class="meta-pill">{{ 'survey.pdf.export_prefix'|trans({'%date%': exportedAt}) }}</span>
|
||||
</div>
|
||||
@@ -223,7 +223,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="meta-row">
|
||||
<span class="meta-pill">{{ 'survey.pdf.answers'|trans({'%count%': question.totalAnswers}) }}</span>
|
||||
<span class="meta-pill">{{ 'survey.pdf.participations'|trans({'%count%': completedSubmissionCount}) }}</span>
|
||||
<span class="meta-pill">{{ 'survey.pdf.response_rate'|trans({'%rate%': question.responseRate}) }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -41,7 +41,17 @@ final class SurveyEditorService
|
||||
$this->surveyRepository->update($survey, $memberId, $data->toArray());
|
||||
}
|
||||
|
||||
public function saveQuestion(SurveyModel $survey, SurveyQuestionData $data, int $questionId = 0): void
|
||||
public function toggleSurveyPublished(SurveyModel $survey, int $memberId): bool
|
||||
{
|
||||
$data = SurveyEditorData::fromModel($survey);
|
||||
$data->published = !$data->published;
|
||||
|
||||
$this->surveyRepository->update($survey, $memberId, $data->toArray());
|
||||
|
||||
return $data->published;
|
||||
}
|
||||
|
||||
public function saveQuestion(SurveyModel $survey, SurveyQuestionData $data, int $questionId = 0): int
|
||||
{
|
||||
$this->assertUnlocked($survey);
|
||||
|
||||
@@ -55,11 +65,13 @@ final class SurveyEditorService
|
||||
$this->surveyQuestionRepository->update($question, $data->toArray());
|
||||
$this->surveyRepository->touch((int) $survey->id);
|
||||
|
||||
return;
|
||||
return (int) $question->id;
|
||||
}
|
||||
|
||||
$this->surveyQuestionRepository->create((int) $survey->id, $data->toArray());
|
||||
$question = $this->surveyQuestionRepository->create((int) $survey->id, $data->toArray());
|
||||
$this->surveyRepository->touch((int) $survey->id);
|
||||
|
||||
return (int) $question->id;
|
||||
}
|
||||
|
||||
public function deleteQuestion(SurveyModel $survey, int $questionId): void
|
||||
@@ -70,6 +82,11 @@ final class SurveyEditorService
|
||||
$this->surveyRepository->touch((int) $survey->id);
|
||||
}
|
||||
|
||||
public function deleteSurvey(SurveyModel $survey): void
|
||||
{
|
||||
$this->surveyRepository->deleteCascade((int) $survey->id);
|
||||
}
|
||||
|
||||
public function saveCondition(SurveyModel $survey, SurveyConditionData $data, int $conditionId = 0): void
|
||||
{
|
||||
$this->assertUnlocked($survey);
|
||||
|
||||
@@ -12,6 +12,7 @@ use Mummert\SurveyBundle\Repository\SurveyAnswerRepository;
|
||||
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
||||
use Mummert\SurveyBundle\Repository\SurveySubmissionRepository;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
final class SurveySubmissionService
|
||||
@@ -21,6 +22,7 @@ final class SurveySubmissionService
|
||||
private readonly SurveyAnswerRepository $surveyAnswerRepository,
|
||||
private readonly SurveyRepository $surveyRepository,
|
||||
private readonly QuestionTypeRegistry $questionTypeRegistry,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -80,7 +82,9 @@ final class SurveySubmissionService
|
||||
*/
|
||||
public function getAnswersForSubmission(SurveySubmissionModel $submission): array
|
||||
{
|
||||
return $this->surveyAnswerRepository->findAnswersBySubmission((int) $submission->id);
|
||||
$answers = $this->surveyAnswerRepository->findAnswersBySubmission((int) $submission->id);
|
||||
|
||||
return array_map(fn (array $answer): array => $this->formatSubmissionAnswer($answer), $answers);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,23 +95,48 @@ final class SurveySubmissionService
|
||||
return $this->surveySubmissionRepository->findOverviewBySurvey((int) $survey->id);
|
||||
}
|
||||
|
||||
public function hasFinishedSubmissionForSurvey(int $surveyId, SessionInterface $session): bool
|
||||
{
|
||||
if ($surveyId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = (string) $session->get($this->buildSessionKey($surveyId), '');
|
||||
|
||||
if ('' === $token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->surveySubmissionRepository->hasFinishedBySurveyAndToken($surveyId, $token);
|
||||
}
|
||||
|
||||
private function buildSessionKey(int $surveyId): string
|
||||
{
|
||||
return 'mummert_survey_submission_'.$surveyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $answer
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatSubmissionAnswer(array $answer): array
|
||||
{
|
||||
$questionType = (string) ($answer['questionType'] ?? 'text');
|
||||
$value = trim((string) ($answer['value'] ?? ''));
|
||||
|
||||
$answer['questionType'] = $this->translateQuestionType($questionType);
|
||||
$answer['value'] = $this->translateAnswerValue($questionType, $value);
|
||||
|
||||
return $answer;
|
||||
}
|
||||
|
||||
private function translateQuestionType(string $questionType): string
|
||||
{
|
||||
return $this->translator->trans('survey.survey.question_type_'.$questionType, [], 'messages');
|
||||
}
|
||||
|
||||
private function translateAnswerValue(string $questionType, string $value): string
|
||||
{
|
||||
if ('' === $value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ('yes_no_maybe' === $questionType) {
|
||||
return $this->translator->trans('survey.survey.answer_'.$value, [], 'messages');
|
||||
}
|
||||
|
||||
if ('choice' === $questionType) {
|
||||
$parts = array_values(array_filter(array_map('trim', explode(' | ', $value)), static fn (string $entry): bool => '' !== $entry));
|
||||
|
||||
return implode(' | ', $parts);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user