Ctrl K
ComunidadGuías…

controllers/AmendmentController.php

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<?php

namespace app\controllers;

use app\models\consultationLog\{ProposedProcedureAgreement, ProposedProcedureChange, ProposedProcedureUserNotification};
use app\models\settings\{AntragsgruenApp, PrivilegeQueryContext, Privileges};
use app\views\pdfLayouts\IPDFLayout;
use app\models\http\{BinaryFileResponse,
    HtmlErrorResponse,
    HtmlResponse,
    JsonResponse,
    RedirectResponse,
    ResponseInterface,
    RestApiExceptionResponse};
use app\components\{Html, diff\AmendmentCollissionDetector, HTMLTools, Tools, UrlHelper};
use app\models\db\{Amendment, AmendmentAdminComment, AmendmentProposal, AmendmentSupporter, ConsultationLog, IMotion, IProposal, ISupporter, User};
use app\models\events\AmendmentEvent;
use app\models\exceptions\{FormError, MailNotSent, ResponseException};
use app\models\api\imotion\{AmendmentCreateRequest, AmendmentUpdateRequest};
use app\models\forms\{AdminMotionFilterForm, AmendmentEditForm, ProposedChangeForm};
use app\models\notifications\AmendmentProposedProcedure;
use app\views\amendment\LayoutHelper;
use yii\web\NotFoundHttpException;

class AmendmentController extends Base
{
    use AmendmentActionsTrait;
    use AmendmentMergingTrait;

    public function actionPdf(string $motionSlug, int $amendmentId): ResponseInterface
    {
        $amendment = $this->getAmendmentWithCheck($motionSlug, $amendmentId);
        $this->amendment = $amendment;

        $selectedPdfLayout = IPDFLayout::getPdfLayoutForMotionType($amendment->getMyMotionType());

        $hasLaTeX = ($this->getParams()->xelatexPath || $this->getParams()->lualatexPath);
        if (!($hasLaTeX && $selectedPdfLayout->latexId !== null) && $selectedPdfLayout->id === null) {
            return new HtmlErrorResponse(404, \Yii::t('motion', 'err_no_pdf'));
        }

        if (!$amendment->isReadable()) {
            return new HtmlErrorResponse(404, \Yii::t('amend', 'err_not_visible'));
        }

        if ($selectedPdfLayout->isHtmlToPdfLayout()) {
            $pdf = LayoutHelper::createPdfFromHtml($amendment);
        } elseif ($selectedPdfLayout->latexId !== null) {
            $pdf = LayoutHelper::createPdfLatex($amendment);
        } else {
            $pdf = LayoutHelper::createPdfTcpdf($amendment);
        }

        return new BinaryFileResponse(
            BinaryFileResponse::TYPE_PDF,
            $pdf,
            false,
            $amendment->getFilenameBase(false),
            $this->layoutParams->isRobotsIndex($this->action)
        );
    }

    public function actionPdfcollection(bool $inactive = false, bool $replaced = false, bool $admin = false): ResponseInterface
    {
        $search = AdminMotionFilterForm::getForConsultationFromRequest($this->consultation, $this->consultation->motions, $this->getRequestValue('Search'), false);
        $search->showReplaced = $replaced;
        $amendments = $search->getAmendmentsForExport($this->consultation, $inactive);

        $showAdminAmendments = $admin && User::havePrivilege($this->consultation, Privileges::PRIVILEGE_MOTION_SEE_UNPUBLISHED, null);

        if (count($amendments) === 0) {
            return new HtmlErrorResponse(404, \Yii::t('amend', 'none_yet'));
        }
        $texTemplate = null;
        $toShowAmendments = [];
        foreach ($amendments as $amendmentGroups) {
            if ($amendmentGroups['motion']->getMyMotionType()->amendmentsOnly) {
                continue;
            }
            // If we have multiple motion types, we just take the template from the first one.
            if ($texTemplate === null) {
                $texTemplate = $amendmentGroups['motion']->getMyMotionType()->texTemplate;
            }
            $toShowAmendments = array_merge($toShowAmendments, $amendmentGroups['amendments']);
        }
        if (count($toShowAmendments) === 0) {
            return new HtmlErrorResponse(404, \Yii::t('amend', 'none_yet'));
        }

        $selectedPdfLayout = IPDFLayout::getPdfLayoutForMotionType($amendments[0]['motion']->getMyMotionType());

        $hasLaTeX = ($this->getParams()->xelatexPath || $this->getParams()->lualatexPath);
        if (!($hasLaTeX && $selectedPdfLayout->latexId !== null) && $selectedPdfLayout->id === null) {
            return new HtmlErrorResponse(404, \Yii::t('motion', 'err_no_pdf'));
        }

        if (!$showAdminAmendments) {
            $toShowAmendments = array_values(array_filter($toShowAmendments, fn (Amendment $amendment) => $amendment->isVisible() && $amendment->getMyExistingMotion()->isVisible()));
        }

        if ($selectedPdfLayout->isHtmlToPdfLayout()) {
            $pdf = $this->renderPartial('pdf_collection_html2pdf', ['amendments' => $toShowAmendments]);
        } elseif ($selectedPdfLayout->latexId !== null) {
            $pdf = $this->renderPartial('pdf_collection_tex', [
                'amendments'  => $toShowAmendments,
                'texTemplate' => $texTemplate,
            ]);
        } else {
            $pdf = $this->renderPartial('pdf_collection_tcpdf', ['amendments' => $toShowAmendments]);
        }

        return new BinaryFileResponse(
            BinaryFileResponse::TYPE_PDF,
            $pdf,
            false,
            null,
            $this->layoutParams->isRobotsIndex($this->action)
        );
    }

    public function actionOdt(string $motionSlug, int $amendmentId): ResponseInterface
    {
        $amendment = $this->getAmendmentWithCheck($motionSlug, $amendmentId);
        $this->amendment = $amendment;

        if (!$amendment->isReadable()) {
            return new HtmlErrorResponse(404, \Yii::t('amend', 'err_not_visible'));
        }

        $doc = $amendment->getMyMotionType()->createOdtTextHandler();
        LayoutHelper::printAmendmentToOdt($amendment, $doc);

        return new BinaryFileResponse(
            BinaryFileResponse::TYPE_ODT,
            $doc->finishAndGetDocument(),
            false,
            $amendment->getFilenameBase(false),
            $this->layoutParams->isRobotsIndex($this->action)
        );
    }

    public function actionView(string $motionSlug, int $amendmentId, int $commentId = 0, ?string $procedureToken = null, ?int $proposalVersion = null): ResponseInterface
    {
        $this->layout = 'column2';

        $amendment = $this->getAmendmentWithCheck($motionSlug, $amendmentId, 'view');
        $this->amendment = $amendment;

        if ($this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null)) {
            $adminEdit = UrlHelper::createUrl(['admin/amendment/update', 'amendmentId' => $amendmentId]);
        } else {
            $adminEdit = null;
        }

        if (!$amendment->isReadable()) {
            return new HtmlErrorResponse(404, \Yii::t('amend', 'err_not_visible'));
        }

        $openedComments      = [];
        $amendmentViewParams = [
            'amendment'      => $amendment,
            'openedComments' => $openedComments,
            'adminEdit'      => $adminEdit,
            'commentForm'    => null,
            'activeProposal' => $this->getActiveProposal($amendment, $procedureToken, $proposalVersion),
            'procedureToken' => $procedureToken,
        ];

        try {
            $this->performShowActions($amendment, $commentId, $amendmentViewParams);
        } catch (ResponseException $e) {
            throw $e;
        } catch (\Throwable $e) {
            $this->getHttpSession()->setFlash('error', $e->getMessage());
        }

        $amendmentViewParams['supportStatus'] = AmendmentSupporter::getCurrUserSupportStatus($amendment);

        return new HtmlResponse($this->render('view', $amendmentViewParams));
    }

    private function getActiveProposal(Amendment $amendment, ?string $procedureToken, ?int $proposalVersion): IProposal
    {
        if ($procedureToken) {
            return $amendment->getProposalByToken($procedureToken) ?? $amendment->getLatestProposal();
        }
        if ($proposalVersion) {
            foreach ($amendment->proposals as $proposal) {
                if ($proposal->version === $proposalVersion && $proposal->canEditLimitedProposedProcedure()) {
                    return $proposal;
                }
            }
        }
        return $amendment->getLatestProposal();
    }

    public function actionAjaxDiff(string $motionSlug, int $amendmentId): ResponseInterface
    {
        $amendment = $this->getAmendmentWithCheck($motionSlug, $amendmentId);
        $this->amendment = $amendment;
        if (!$this->mayViewAjaxDiff($amendment)) {
            return new HtmlErrorResponse(404, \Yii::t('amend', 'err_not_visible'));
        }

        return new HtmlResponse($this->renderPartial('ajax_diff', ['amendment' => $amendment]));
    }

    /**
     * Besides readable amendments, the tooltips are also shown on admin pages and in the merging tool,
     * for drafts and for not yet published modifications of proposed procedures.
     */
    private function mayViewAjaxDiff(Amendment $amendment): bool
    {
        if ($amendment->isReadable()) {
            return true;
        }

        // The modified text of a published proposed procedure (shown e.g. in public merging drafts)
        $referencingProposal = $amendment->proposalReferencedByAmendment ?? $amendment->proposalReferencedByMotion;
        if ($referencingProposal && $referencingProposal->isProposalPublic()) {
            return true;
        }

        if (User::haveOneOfPrivileges($this->consultation, [
            Privileges::PRIVILEGE_SCREENING,
            Privileges::PRIVILEGE_MOTION_SEE_UNPUBLISHED,
            Privileges::PRIVILEGE_MOTION_STATUS_EDIT,
            Privileges::PRIVILEGE_CHANGE_PROPOSALS,
        ], PrivilegeQueryContext::amendment($amendment))) {
            return true;
        }

        $motion = $amendment->getMyMotion();
        return $motion !== null && $motion->canMergeAmendments();
    }

    public function actionCreatedone(string $motionSlug, int $amendmentId, string $fromMode): ResponseInterface
    {
        $motion = $this->consultation->getMotion($motionSlug);
        /** @var Amendment|null $amendment */
        $amendment = Amendment::findOne(
            [
                'id'       => $amendmentId,
                'motionId' => $motion->id
            ]
        );
        if (!$amendment) {
            $this->getHttpSession()->setFlash('error', \Yii::t('amend', 'err_not_found'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        return new HtmlResponse($this->render('create_done', ['amendment' => $amendment, 'mode' => $fromMode]));
    }

    public function actionCreateconfirm(string $motionSlug, int $amendmentId, string $fromMode): ResponseInterface
    {
        $motion = $this->consultation->getMotion($motionSlug);
        if (!$motion) {
            $this->getHttpSession()->setFlash('error', \Yii::t('motion', 'err_not_found'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }
        /** @var Amendment|null $amendment */
        $amendment = Amendment::findOne(
            [
                'id'       => $amendmentId,
                'motionId' => $motion->id,
                'status'   => Amendment::STATUS_DRAFT
            ]
        );
        if (!$amendment) {
            $this->getHttpSession()->setFlash('error', \Yii::t('amend', 'err_not_found'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }
        if (!$amendment->canEditText()) {
            $this->getHttpSession()->setFlash('error', \Yii::t('motion', 'err_edit_permission'));

            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        if (!$motion->isCurrentlyAmendable(allowAdmins: true, assumeLoggedIn: false, throwExceptions: false, amendingAmendment: $amendment->amendedAmendment)) {
            // Could be past the deadline of submission by now, or an admin changing the permissions
            return new HtmlErrorResponse(403, \Yii::t('amend', 'err_create_permission'));
        }

        if ($this->isPostSet('modify')) {
            $nextUrl = ['amendment/edit', 'amendmentId' => $amendment->id, 'motionSlug' => $motionSlug];
            return new RedirectResponse(UrlHelper::createUrl($nextUrl));
        }

        if ($this->isPostSet('confirm')) {
            $amendment->trigger(Amendment::EVENT_CREATED, new AmendmentEvent($amendment));

            if ($amendment->status === Amendment::STATUS_SUBMITTED_SCREENED) {
                $amendment->trigger(Amendment::EVENT_PUBLISHED, new AmendmentEvent($amendment));
            }

            return new RedirectResponse(UrlHelper::createAmendmentUrl($amendment, 'createdone', ['fromMode' => $fromMode]));
        } else {
            return new HtmlResponse($this->render('create_confirm', [
                'amendment'     => $amendment,
                'mode'          => $fromMode,
                'deleteDraftId' => $this->getHttpRequest()->get('draftId'),
            ]));
        }
    }

    public function actionEdit(string $motionSlug, int $amendmentId): ResponseInterface
    {
        $motion = $this->consultation->getMotion($motionSlug);
        /** @var Amendment|null $amendment */
        $amendment = Amendment::findOne(
            [
                'id'       => $amendmentId,
                'motionId' => $motion->id,
            ]
        );
        if (!$amendment) {
            $this->getHttpSession()->setFlash('error', \Yii::t('amend', 'err_not_found'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        if (!$amendment->canEditText()) {
            $this->getHttpSession()->setFlash('error', \Yii::t('amend', 'err_edit_forbidden'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        $fromMode = ($amendment->status === Amendment::STATUS_DRAFT ? 'create' : 'edit');
        $form = AmendmentEditForm::createForUserEdit($amendment, null, null);

        if ($this->isPostSet('save')) {
            $amendment->flushCacheWithChildren(null);
            try {
                $dto = AmendmentUpdateRequest::fromWebRequest($this->getPostValues(), $_FILES, $amendment);
                $form->saveAmendment($amendment, $dto);

                if ($amendment->isVisible()) {
                    ConsultationLog::logCurrUser($this->consultation, ConsultationLog::AMENDMENT_CHANGE, $amendment->id);
                }

                if ($amendment->status == Amendment::STATUS_DRAFT) {
                    $nextUrl = [
                        'amendment/createconfirm',
                        'motionSlug'  => $motionSlug,
                        'amendmentId' => $amendment->id,
                        'fromMode'    => $fromMode,
                        'draftId'     => $this->getRequestValue('draftId'),
                    ];
                    return new RedirectResponse(UrlHelper::createUrl($nextUrl));
                } else {
                    return new HtmlResponse($this->render('edit_done', ['amendment' => $amendment]));
                }
            } catch (\Throwable $e) {
                $this->getHttpSession()->setFlash('error', $e->getMessage());
            }
        }

        return new HtmlResponse($this->render(
            'edit_form',
            [
                'mode'         => $fromMode,
                'form'         => $form,
                'consultation' => $this->consultation,
            ]
        ));
    }

    /**
     * @throws \app\models\exceptions\NotAmendable
     */
    public function actionCreate(string $motionSlug, int $agendaItemId = 0, int $cloneFrom = 0, int $createFromAmendment = 0, ?int $sectionId = null, ?int $paragraphNo = null): ResponseInterface
    {
        $motion = $this->consultation->getMotion($motionSlug);
        if (!$motion) {
            $this->getHttpSession()->setFlash('error', \Yii::t('motion', 'err_not_found'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        $amendingAmendment = null;
        if ($createFromAmendment > 0 && $motion->getMyMotionType()->getSettingsObj()->allowAmendmentsToAmendments) {
            $adoptAmend = $this->consultation->getAmendment($createFromAmendment);
            if ($adoptAmend && $adoptAmend->motionId === $motion->id && $adoptAmend->isReadable()) {
                $amendingAmendment = $adoptAmend;
            }
        }
        if ($amendingAmendment?->amendingAmendmentId) {
            $this->getHttpSession()->setFlash('error', \Yii::t('amend', 'err_no_three_level_amendment'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        if (!$motion->isCurrentlyAmendable(allowAdmins: true, assumeLoggedIn: false, throwExceptions: false, amendingAmendment: $amendingAmendment)) {
            if ($motion->isCurrentlyAmendable(allowAdmins: true, assumeLoggedIn: true, throwExceptions: false, amendingAmendment: $amendingAmendment)) {
                $loginUrl = UrlHelper::createLoginUrl(['amendment/create', 'motionSlug' => $motion->getMotionSlug()]);
                return new RedirectResponse($loginUrl);
            } else {
                return new HtmlErrorResponse(403, \Yii::t('amend', 'err_create_permission'));
            }
        }

        if ($agendaItemId > 0) {
            $agendaItem = $this->consultation->getAgendaItem($agendaItemId);
        } else {
            $agendaItem = null;
        }

        $form = AmendmentEditForm::createForCreating($motion, $agendaItem, $sectionId, $paragraphNo);
        $supportType = $motion->getMyMotionType()->getAmendmentSupportTypeClass();
        $iAmAdmin = $this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null);

        if ($this->isPostSet('save')) {
            try {
                $dto = AmendmentCreateRequest::fromWebRequest($this->getPostValues(), $_FILES, $motion);
                $amendment = $form->createAmendment($dto);

                // Supporting members are not collected in the form, but need to be copied a well
                if ($supportType->collectSupportersBeforePublication() && $cloneFrom && $iAmAdmin) {
                    $adoptAmend = $this->consultation->getAmendment($cloneFrom);
                    foreach ($adoptAmend->getSupporters(true) as $supp) {
                        $suppNew = new AmendmentSupporter();
                        $suppNew->setAttributes($supp->getAttributes());
                        $suppNew->id           = null;
                        $suppNew->amendmentId  = $amendment->id;
                        $suppNew->extraData    = $supp->extraData;
                        $suppNew->dateCreation = date('Y-m-d H:i:s');
                        if ($supp->isNonPublic()) {
                            $suppNew->setExtraDataEntry(ISupporter::EXTRA_DATA_FIELD_NON_PUBLIC, true);
                        }
                        $suppNew->save();
                    }
                }

                $nextUrl = [
                    'amendment/createconfirm',
                    'motionSlug'  => $motionSlug,
                    'amendmentId' => $amendment->id,
                    'fromMode'    => 'create',
                    'draftId'     => $this->getRequestValue('draftId'),
                ];
                return new RedirectResponse(UrlHelper::createUrl($nextUrl));
            } catch (\Throwable $e) {
                $this->getHttpSession()->setFlash('error', $e->getMessage());
            }
        } elseif ($cloneFrom > 0) {
            // Using an existing amendment as template is an admin function, and also exposes unpublished amendments and contact data
            $adoptAmend = $this->consultation->getAmendment($cloneFrom);
            if ($adoptAmend && (
                $this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, PrivilegeQueryContext::amendment($adoptAmend)) ||
                $this->consultation->havePrivilege(Privileges::PRIVILEGE_MOTION_STATUS_EDIT, PrivilegeQueryContext::amendment($adoptAmend))
            )) {
                $form->cloneSupporters($adoptAmend);
                $form->cloneAmendmentText($adoptAmend, true);
            }
        } elseif ($amendingAmendment !== null) {
            $form->cloneAmendmentText($amendingAmendment, false);
            $form->setAmendedAmendment($amendingAmendment);
        }

        if (count($form->supporters) == 0) {
            $form->supporters[] = AmendmentSupporter::createInitiator($this->consultation, $supportType, $iAmAdmin);
        }

        return new HtmlResponse($this->render(
            'edit_form',
            [
                'mode'         => 'create',
                'consultation' => $this->consultation,
                'form'         => $form,
            ]
        ));
    }

    public function actionWithdraw(int $amendmentId): ResponseInterface
    {
        $amendment = $this->consultation->getAmendment($amendmentId);
        if (!$amendment) {
            $this->getHttpSession()->setFlash('error', \Yii::t('amend', 'err_not_found'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        if (!$amendment->canWithdraw()) {
            $this->getHttpSession()->setFlash('error', \Yii::t('amend', 'err_withdraw_forbidden'));
            return new RedirectResponse(UrlHelper::homeUrl() ?? '/');
        }

        if ($this->isPostSet('cancel')) {
            return new RedirectResponse(UrlHelper::createAmendmentUrl($amendment));
        }

        if ($this->isPostSet('withdraw')) {
            $amendment->withdraw();
            $this->getHttpSession()->setFlash('success', \Yii::t('amend', 'widthdraw_done'));
            return new RedirectResponse(UrlHelper::createAmendmentUrl($amendment));
        }

        return new HtmlResponse($this->render('withdraw', ['amendment' => $amendment]));
    }

    public function actionSaveProposalStatus(string $motionSlug, int $amendmentId): ResponseInterface
    {
        $amendment = $this->getAmendmentWithCheck($motionSlug, $amendmentId);
        $this->amendment = $amendment;

        $post = $this->getPostValues();
        if (!in_array('version', array_keys($post))) {
            return new RestApiExceptionResponse(400, 'No version provided');
        }
        $proposal = $amendment->getProposalById($post['version']);

        if (!$proposal->canEditLimitedProposedProcedure()) {
            return new RestApiExceptionResponse(403, 'Not permitted to change the status');
        }

        $proposalIsNew = $proposal->isNewRecord;
        $proposal->save(); // Make sure we know the ID
        $canChangeProposalUnlimitedly = $proposal->canEditProposedProcedure();

        $response = [];
        $msgAlert = null;
        $ppChanges = ProposedProcedureChange::create($proposalIsNew, $proposal->id, $proposal->version);

        if ($this->getHttpRequest()->post('setStatus', null) !== null) {
            $originalProposalStatus = $proposal->proposalStatus;

            foreach (AntragsgruenApp::getActivePlugins() as $plugin) {
                /** @var Amendment $amendment */
                $amendment = $plugin::onBeforeProposedProcedureStatusSave($amendment);
            }

            if ($canChangeProposalUnlimitedly) {
                $setStatus = intval($this->getHttpRequest()->post('setStatus'));
                if ($proposal->proposalStatus !== $setStatus) {
                    $ppChanges->setProposalStatusChanges($proposal->proposalStatus, $setStatus);
                    if ($proposal->userStatus !== null) {
                        $msgAlert = \Yii::t('amend', 'proposal_user_change_reset');
                    }
                    $proposal->userStatus = null;
                }
                $proposal->proposalStatus = $setStatus;

                $ppChanges->setProposalCommentChanges($proposal->comment, $this->getHttpRequest()->post('proposalComment', ''));
                $proposal->comment = $this->getHttpRequest()->post('proposalComment', '');
            }

            $amendment->setProposedProcedureTags($this->getHttpRequest()->post('tags', []), $ppChanges);

            if ($canChangeProposalUnlimitedly) {
                $proposalExplanationPre = $proposal->explanation;
                if ($this->getHttpRequest()->post('proposalExplanation', null) !== null) {
                    if (trim($this->getHttpRequest()->post('proposalExplanation', '')) === '') {
                        $proposal->explanation = null;
                    } else {
                        $proposal->explanation = $this->getHttpRequest()->post('proposalExplanation', '');
                    }
                } else {
                    $proposal->explanation = null;
                }
                $ppChanges->setProposalExplanationChanges($proposalExplanationPre, $proposal->explanation);

                if ($this->getHttpRequest()->post('visible', 0)) {
                    if ($proposal->visibleFrom === null) {
                        // Reload the page, to update section titles and permissions to edit the proposed procedure
                        $response['redirectToUrl'] = UrlHelper::createAmendmentUrl($amendment, 'view', ['proposalVersion' => $proposal->version]);
                    }
                    $proposal->setPublished();
                } else {
                    $proposal->visibleFrom = null;
                }
            }

            try {
                $amendment->setProposalVotingPropertiesFromRequest(
                    $this->getHttpRequest()->post('votingStatus', null),
                    $this->getHttpRequest()->post('votingBlockId', null),
                    $this->getHttpRequest()->post('votingItemBlockId', []),
                    $this->getHttpRequest()->post('votingItemBlockName', ''),
                    $this->getHttpRequest()->post('votingBlockTitle', ''),
                    true,
                    $ppChanges
                );

                if ($proposal->proposalStatus === IMotion::STATUS_MODIFIED_ACCEPTED && $originalProposalStatus !== $proposal->proposalStatus) {
                    $response['redirectToUrl'] = UrlHelper::createAmendmentUrl($amendment, 'edit-proposed-change', ['proposalVersion' => $proposal->version]);
                }
            } catch (FormError $e) {
                $response['success'] = false;
                $response['error'] = $e->getMessage();
                return new JsonResponse($response);
            }

            if ($ppChanges->hasChanges()) {
                ConsultationLog::logCurrUser($amendment->getMyConsultation(), ConsultationLog::AMENDMENT_SET_PROPOSAL, $amendment->id, $ppChanges->jsonSerialize());
            }

            $response['success'] = false;
            if ($proposal->save()) {
                $response['success'] = true;
            }
            $amendment->flushCacheItems(['procedure']);

            $this->consultation->refresh();
            $amendment->refresh();
            $response['html']        = $this->renderPartial('_set_proposed_procedure', [
                'amendment' => $amendment,
                'proposal'  => $proposal,
                'msgAlert'  => $msgAlert,
                'context'   => $this->getHttpRequest()->post('context', 'view'),
            ]);
            $response['proposalStr'] = $proposal->getFormattedProposalStatus(true);
        }

        if ($this->getHttpRequest()->post('notifyProposer', false)) {
            try {
                new AmendmentProposedProcedure(
                    $proposal,
                    $this->getHttpRequest()->post('text'),
                    $this->getHttpRequest()->post('fromName'),
                    $this->getHttpRequest()->post('replyTo')
                );
                $proposal->notifiedAt = date('Y-m-d H:i:s');
                $proposal->save();

                $data = ProposedProcedureUserNotification::create($this->getHttpRequest()->post('text'), $proposal->version, $proposal->id)->jsonSerialize();
                ConsultationLog::logCurrUser($amendment->getMyConsultation(), ConsultationLog::AMENDMENT_NOTIFY_PROPOSAL, $amendment->id, $data);

                $amendment->flushCacheItems(['procedure']);
                $response['success'] = true;
                $response['html']    = $this->renderPartial('_set_proposed_procedure', [
                    'amendment' => $amendment,
                    'proposal' => $proposal,
                    'msgAlert'  => $msgAlert,
                    'context'   => $this->getHttpRequest()->post('context', 'view'),
                ]);
            } catch (MailNotSent $e) {
                $response['success'] = false;
                $response['error']   = 'The mail could not be sent: ' . $e->getMessage();
            }
        }

        if ($this->getHttpRequest()->post('setProposerHasAccepted')) {
            $proposal->userStatus = Amendment::STATUS_ACCEPTED;
            $proposal->save();
            $amendment->flushCacheItems(['procedure']);

            $data = ProposedProcedureAgreement::create(false, $proposal->version, $proposal->id, null)->jsonSerialize();
            ConsultationLog::logCurrUser($amendment->getMyConsultation(), ConsultationLog::AMENDMENT_ACCEPT_PROPOSAL, $amendment->id, $data);

            $response['success'] = true;
            $response['html']        = $this->renderPartial('_set_proposed_procedure', [
                'amendment' => $amendment,
                'proposal'  => $proposal,
                'msgAlert'  => $msgAlert,
                'context'   => $this->getHttpRequest()->post('context', 'view'),
            ]);
        }

        if ($this->getHttpRequest()->post('writeComment')) {
            $adminComment               = new AmendmentAdminComment();
            $adminComment->userId       = User::getCurrentUser()->id;
            $adminComment->text         = $this->getHttpRequest()->post('writeComment');
            $adminComment->status       = AmendmentAdminComment::TYPE_PROPOSED_PROCEDURE;
            $adminComment->dateCreation = date('Y-m-d H:i:s');
            $adminComment->amendmentId  = $amendment->id;
            if (!$adminComment->save()) {
                $this->getHttpResponse()->statusCode = 500;
                $response['success']             = false;
                return new JsonResponse($response);
            }
            $amendment->flushCacheItems(['procedure']);

            $response['success'] = true;
            $response['proposalId'] = $proposal->id;
            $response['comment'] = [
                'username'      => $adminComment->getMyUser()->name,
                'id'            => $adminComment->id,
                'text'          => $adminComment->text,
                'delLink'       => UrlHelper::createAmendmentUrl($amendment, 'del-proposal-comment'),
                'dateFormatted' => Tools::formatMysqlDateTime($adminComment->dateCreation),
            ];
        }

        if ($this->getHttpRequest()->post('deleteProposal')) {
            if ($proposal->isNewRecord) {
                return new RestApiExceptionResponse(400, 'This proposal version has not been saved yet');
            }
            if (!$proposal->canEditProposedProcedure()) {
                return new RestApiExceptionResponse(403, 'Not permitted to delete this proposal version');
            }

            $proposal->softDelete();
            $amendment->flushCacheItems(['procedure']);

            $response['success'] = true;
            $response['redirectToUrl'] = UrlHelper::createAmendmentUrl($amendment, 'view');
        }

        return new JsonResponse($response);
    }

    public function actionEditProposedChange(string $motionSlug, int $amendmentId, int $proposalVersion): ResponseInterface
    {
        $amendment = $this->getAmendmentWithCheck($motionSlug, $amendmentId);
        $this->amendment = $amendment;

        if ($this->getHttpRequest()->post('newVersion', null) === 'new') {
            $oldProposal = $amendment->getProposalByVersion($proposalVersion);
            $proposal = $amendment->getProposalById(null); // Create a new one
            $proposal->proposalStatus = $oldProposal->proposalStatus;
        } else {
            $proposal = $amendment->getProposalByVersion($proposalVersion);
        }

        if (!$proposal->canEditProposedProcedure()) {
            return new HtmlErrorResponse(403, 'Not permitted to change the proposed procedure');
        }

        $isNewProposal = $proposal->isNewRecord;
        $proposal->save();

        if ($this->getHttpRequest()->post('reset', null) !== null) {
            $reference = $proposal->getMyProposalReference();
            if ($reference && $reference->status === Amendment::STATUS_PROPOSED_MODIFIED_AMENDMENT) {
                foreach ($reference->sections as $section) {
                    $section->delete();
                }

                $proposal->proposalReferenceId = null;
                $proposal->save();

                $reference->delete();

                $ppChanges = ProposedProcedureChange::create($isNewProposal, $proposal->id, $proposal->version);
                $ppChanges->setProposalTextChanged();
                ConsultationLog::logCurrUser($amendment->getMyConsultation(), ConsultationLog::AMENDMENT_SET_PROPOSAL, $amendmentId, $ppChanges->jsonSerialize());
            }
            $amendment->flushCacheItems(['procedure']);
        }

        $form = new ProposedChangeForm($amendment, $proposal);

        $msgSuccess = null;
        $msgAlert   = null;

        if ($this->getHttpRequest()->post('save', null) !== null) {
            $form->save($this->getPostValues(), $_FILES);
            $this->getHttpSession()->setFlash('success', \Yii::t('base', 'saved'));

            if ($proposal->userStatus !== null) {
                $this->getHttpSession()->setFlash('info', \Yii::t('amend', 'proposal_user_change_reset'));
            }
            $proposal->userStatus = null;
            $proposal->save();
            $proposal->flushViewCaches();

            $ppChanges = ProposedProcedureChange::create($isNewProposal, $proposal->id, $proposal->version);
            $ppChanges->setProposalTextChanged();
            ConsultationLog::logCurrUser($amendment->getMyConsultation(), ConsultationLog::AMENDMENT_SET_PROPOSAL, $amendmentId, $ppChanges->jsonSerialize());

            return new RedirectResponse(UrlHelper::createAmendmentUrl($amendment, 'view', ['proposalVersion' => $proposal->version]));
        }

        return new HtmlResponse($this->render('edit_proposed_change', [
            'msgSuccess' => $msgSuccess,
            'msgAlert'   => $msgAlert,
            'amendment'  => $amendment,
            'proposal'   => $proposal,
            'form'       => $form,
        ]));
    }

    public function actionEditProposedChangeCheck(string $motionSlug, int $amendmentId): ResponseInterface
    {
        $amendment = $this->getAmendmentWithCheck($motionSlug, $amendmentId);
        $this->amendment = $amendment;
        if (!User::havePrivilege($this->consultation, Privileges::PRIVILEGE_CHANGE_PROPOSALS, PrivilegeQueryContext::amendment($amendment))) {
            return new RestApiExceptionResponse(403, 'Not permitted to change the status');
        }

        $checkAgainstAmendments = AmendmentCollissionDetector::getHeuristicallyRelevantAmendments($amendment->getMyMotion(), [$amendment->id]);
        if (count($checkAgainstAmendments) > 100) {
            return new JsonResponse([
                'error' => 'Too many amendments to check for collisions (max. 100)',
                'collisions' => [],
            ]);
        }

        $newSections = $this->getHttpRequest()->post('sections', []);
        foreach ($newSections as $sectionId => $section) {
            $newSections[$sectionId] = HTMLTools::cleanSimpleHtml($section);
        }

        $collidesWith = AmendmentCollissionDetector::getAmendmentsCollidingWithSections($checkAgainstAmendments, $newSections);

        return new JsonResponse([
            'error' => null,
            'collisions' => array_map(function (Amendment $amend) {
                // Keep in sync with edit_proposed_change.php
                $urlTitle = AmendmentProposal::getAmendmentTitleUrlConsideringProposals($amend);
                if ($amend->getLatestProposal()->proposalStatus == Amendment::STATUS_VOTE) {
                    $urlTitle['title'] .= ' (' . \Yii::t('amend', 'proposal_voting') . ')';
                }

                $html = '<li>' . Html::a(Html::encode($urlTitle['title']), $urlTitle['url'], ['target' => '_blank']);
                $html .= HTMLTools::amendmentDiffTooltip($amend, 'top', 'fixedBottom');
                $html .= '</li>';

                return [
                    'id'    => $amend->id,
                    'title' => $urlTitle['title'],
                    'html'  => $html,
                ];
            }, $collidesWith),
        ]);
    }

    /**
     * URL: /[consultationPrefix]/[motionPrefix]/[amendmentPrefix]
     *
     * @throws NotFoundHttpException
     */
    public function actionGotoPrefix(string $prefix1, string $prefix2): ResponseInterface
    {
        try {
            /** @var Amendment|null $amendment */
            $amendment = Amendment::find()->joinWith('motionJoin')->where([
                'motion.consultationId' => $this->consultation->id,
                'motion.titlePrefix'    => $prefix1,
                'amendment.titlePrefix' => $prefix2,
            ])->one();

            if ($amendment && $amendment->isReadable()) {
                return new RedirectResponse($amendment->getLink());
            }
        } catch (\Exception $e) {}

        return new HtmlErrorResponse(404, 'Amendment not found');
    }
}