Ctrl K
ComunidadGuías…

controllers/MotionActionsTrait.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\api\imotion\SupportRequest;
use app\models\forms\ProposedChangeForm;
use app\models\settings\{AntragsgruenApp, PrivilegeQueryContext, Privileges};
use app\models\sectionTypes\ISectionType;
use app\models\sectionTypes\TextEditorial;
use app\models\http\{HtmlErrorResponse,
    HtmlResponse,
    JsonResponse,
    RedirectResponse,
    ResponseInterface,
    RestApiExceptionResponse};
use app\models\notifications\MotionProposedProcedure;
use app\components\{Tools, UrlHelper};
use app\models\db\{Amendment,
    ConsultationLog,
    ConsultationSettingsTag,
    IComment,
    IMotion,
    Motion,
    MotionAdminComment,
    MotionComment,
    MotionSupporter,
    User,
    Consultation};
use app\models\exceptions\{Access, DB, FormError, Internal, MailNotSent, ResponseException};
use app\models\forms\CommentForm;
use app\models\events\MotionEvent;
use app\models\supportTypes\SupportBase;
use yii\web\{Request, Response, Session};

/**
 * @property Consultation $consultation
 * @method Session getHttpSession()
 * @method Request getHttpRequest()
 * @method Response getHttpResponse()
 */
trait MotionActionsTrait
{
    /**
     * @throws Internal
     */
    private function getComment(Motion $motion, int $commentId, bool $needsScreeningRights): MotionComment
    {
        $comment = MotionComment::findOne($commentId);
        if (!$comment || $comment->motionId != $motion->id || $comment->status != IComment::STATUS_VISIBLE) {
            throw new Internal(\Yii::t('comment', 'err_not_found'));
        }
        if ($needsScreeningRights) {
            if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null)) {
                throw new Internal(\Yii::t('comment', 'err_no_screening'));
            }
        }
        return $comment;
    }

    private function writeComment(Motion $motion, array &$viewParameters): void
    {
        $postComment = $this->getHttpRequest()->post('comment');

        $replyTo = null;
        if (isset($postComment['parentCommentId']) && $postComment['parentCommentId']) {
            $replyTo = MotionComment::findOne([
                'id'              => $postComment['parentCommentId'],
                'motionId'        => $motion->id,
                'parentCommentId' => null,
            ]);
            if ($replyTo && $replyTo->status === IComment::STATUS_DELETED) {
                $replyTo = null;
            }
        }

        $commentForm = new CommentForm($motion, $replyTo);
        $commentForm->setAttributes($postComment, $motion->getActiveSections());
        $redirectUrl = null;

        try {
            $commentForm->saveNotificationSettings();
            $comment = $commentForm->saveMotionCommentWithChecks($motion);

            if ($comment->status === MotionComment::STATUS_SCREENING) {
                $this->getHttpSession()->setFlash('screening', \Yii::t('comment', 'created_needs_screening'));
            } else {
                $this->getHttpSession()->setFlash('screening', \Yii::t('comment', 'created'));
            }

            $redirectUrl = UrlHelper::createMotionCommentUrl($comment);
        } catch (\Exception $e) {
            $viewParameters['commentForm'] = $commentForm;
            if (!isset($viewParameters['openedComments'][$commentForm->sectionId])) {
                $viewParameters['openedComments'][$commentForm->sectionId] = [];
            }
            $viewParameters['openedComments'][$commentForm->sectionId][] = $commentForm->paragraphNo;
            $this->getHttpSession()->setFlash('error', $e->getMessage());
        }

        if ($redirectUrl) {
            throw new ResponseException(new RedirectResponse($redirectUrl));
        }
    }

    /**
     * @throws DB
     * @throws Internal
     */
    private function deleteComment(Motion $motion, int $commentId): void
    {
        $comment = $this->getComment($motion, $commentId, false);
        if (!$comment->canDelete(User::getCurrentUser())) {
            throw new Internal(\Yii::t('comment', 'err_no_del'));
        }

        $comment->status = IComment::STATUS_DELETED;
        if (!$comment->save(false)) {
            throw new DB($comment->getErrors());
        }
        ConsultationLog::logCurrUser(
            $motion->getMyConsultation(),
            ConsultationLog::MOTION_COMMENT_DELETE,
            $comment->id
        );

        $this->getHttpSession()->setFlash('success', \Yii::t('comment', 'del_done'));
    }

    /**
     * @throws Internal
     */
    private function screenCommentAccept(Motion $motion, int $commentId): void
    {
        $comment = MotionComment::findOne($commentId);
        if (!$comment || $comment->motionId !== $motion->id) {
            throw new Internal(\Yii::t('comment', 'err_not_found'));
        }
        if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null)) {
            throw new Internal(\Yii::t('comment', 'err_no_screening'));
        }

        $comment->status = IComment::STATUS_VISIBLE;
        $comment->save();

        $motion->refresh();

        ConsultationLog::logCurrUser(
            $motion->getMyConsultation(),
            ConsultationLog::MOTION_COMMENT_SCREEN,
            $comment->id
        );

        $comment->trigger(IComment::EVENT_PUBLISHED);
    }

    /**
     * @throws Internal
     */
    private function screenCommentReject(Motion $motion, int $commentId): void
    {
        $comment = MotionComment::findOne($commentId);
        if (!$comment || $comment->motionId !== $motion->id) {
            throw new Internal(\Yii::t('comment', 'err_not_found'));
        }
        if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_SCREENING, null)) {
            throw new Internal(\Yii::t('comment', 'err_no_screening'));
        }

        $comment->status = IComment::STATUS_DELETED;
        $comment->save();

        $motion->refresh();

        ConsultationLog::logCurrUser(
            $motion->getMyConsultation(),
            ConsultationLog::MOTION_COMMENT_DELETE,
            $comment->id
        );
    }

    /**
     * @throws FormError
     * @throws Internal
     */
    private function motionLikeDislike(Motion $motion, string $role, string $string, string $name, string $orga = '', string $gender = '', bool $nonPublic = false): void
    {
        $currentUser = User::getCurrentUser();
        if (!$motion->motionType->getMotionSupportPolicy()->checkCurrUser(allowAdmins: false)) {
            throw new FormError('Supporting this motion is not possible');
        }

        MotionSupporter::createSupport($motion, $currentUser, $name, $orga, $role, $gender, $nonPublic);

        $this->getHttpSession()->setFlash('success', $string);
    }

    /**
     * @throws FormError
     * @throws Internal
     */
    private function motionLike(Motion $motion): void
    {
        if (!($motion->getLikeDislikeSettings() & SupportBase::LIKEDISLIKE_LIKE)) {
            throw new FormError('Not supported');
        }

        $name = (User::getCurrentUser() ? '' : $this->getHttpRequest()->post('likeName', ''));

        $this->motionLikeDislike($motion, MotionSupporter::ROLE_LIKE, \Yii::t('motion', 'like_done'), $name);
        ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_LIKE, $motion->id);
    }

    /**
     * @throws Access
     * @throws FormError
     */
    private function motionSupport(Motion $motion): void
    {
        $user = User::getCurrentUser();
        $supportType = $motion->getMyMotionType()->getMotionSupportTypeClass();
        $dto = SupportRequest::fromWebRequest($this->getPostValues(), $user, $supportType);

        if (!MotionSupporter::createSupportFromRequest($motion, $user, $dto)) {
            $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'support_already'));
            return;
        }

        $this->getHttpSession()->set('user_gender', $dto->gender ?? '');
        $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'support_done'));
    }

    /**
     * @throws FormError
     * @throws Internal
     */
    private function motionDislike(Motion $motion): void
    {
        if (!($motion->getLikeDislikeSettings() & SupportBase::LIKEDISLIKE_DISLIKE)) {
            throw new FormError('Not supported');
        }

        $name = (User::getCurrentUser() ? '' : $this->getHttpRequest()->post('likeName', ''));

        $this->motionLikeDislike($motion, MotionSupporter::ROLE_DISLIKE, \Yii::t('motion', 'dislike_done'), $name);
        ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_DISLIKE, $motion->id);
    }

    /**
     * @throws Access
     */
    private function motionSupportRevoke(Motion $motion): void
    {
        $currentUser = User::getCurrentUser();
        MotionSupporter::revokeSupportFromRequest($motion, $currentUser, MotionSupporter::getMyLoginlessSupportIds());

        $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'neutral_done'));
    }

    private function motionSupportFinish(Motion $motion): void
    {
        if (!$motion->canFinishSupportCollection()) {
            $this->getHttpSession()->setFlash('error', \Yii::t('motion', 'support_finish_err'));
            return;
        }

        $motion->trigger(Motion::EVENT_CREATED, new MotionEvent($motion));

        if ($motion->status == Motion::STATUS_SUBMITTED_SCREENED) {
            $motion->trigger(Motion::EVENT_PUBLISHED, new MotionEvent($motion));
        }

        ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_SUPPORT_FINISH, $motion->id);
        $this->getHttpSession()->setFlash('success', \Yii::t('motion', 'support_finish_done'));
    }

    /**
     * @throws Internal
     */
    private function motionAddTag(Motion $motion): void
    {
        if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_MOTION_STATUS_EDIT, PrivilegeQueryContext::motion($motion))) {
            throw new Internal(\Yii::t('comment', 'err_no_screening'));
        }
        foreach ($motion->getMyConsultation()->getSortedTags(ConsultationSettingsTag::TYPE_PUBLIC_TOPIC) as $tag) {
            if ($tag->id === intval($this->getHttpRequest()->post('tagId'))) {
                $motion->link('tags', $tag);
            }
        }
    }

    /**
     * @throws Internal
     */
    private function motionDelTag(Motion $motion): void
    {
        if (!$this->consultation->havePrivilege(Privileges::PRIVILEGE_MOTION_STATUS_EDIT, PrivilegeQueryContext::motion($motion))) {
            throw new Internal(\Yii::t('comment', 'err_no_screening'));
        }
        foreach ($motion->getMyConsultation()->getSortedTags(ConsultationSettingsTag::TYPE_PUBLIC_TOPIC) as $tag) {
            if ($tag->id === intval($this->getHttpRequest()->post('tagId'))) {
                $motion->unlink('tags', $tag, true);
            }
        }
    }

    private function setProposalAgreement(Motion $motion, int $status, ?string $comment): void
    {
        $procedureToken = $this->getHttpRequest()->post('procedureToken');
        $proposal = $motion->getProposalByToken($procedureToken);
        if (!$proposal->canAgreeToProposedProcedure($procedureToken) || !$proposal->proposalFeedbackHasBeenRequested()) {
            $this->getHttpSession()->setFlash('error', 'Not allowed to perform this action');
            return;
        }

        if ($proposal->userStatus === Amendment::STATUS_ACCEPTED) {
            // No changing once accepted
            return;
        }

        $proposal->userStatus = $status;
        $proposal->save();

        $data = ProposedProcedureAgreement::create(true, $proposal->version, $proposal->id, $comment)->jsonSerialize();
        if ($status === Motion::STATUS_ACCEPTED) {
            ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_ACCEPT_PROPOSAL, $motion->id, $data);
        }
        if ($status === Motion::STATUS_REJECTED) {
            ConsultationLog::logCurrUser($motion->getMyConsultation(), ConsultationLog::MOTION_REJECT_PROPOSAL, $motion->id, $data);
        }

        $this->getHttpSession()->setFlash('success', \Yii::t('amend', 'proposal_user_saved'));
    }

    /**
     * @throws \Throwable
     */
    private function savePrivateNote(Motion $motion): void
    {
        $user = User::getCurrentUser();
        if (!$user || !$user->getSettingsObj()->allowPrivateComments) {
            return;
        }

        $noteText  = trim($this->getHttpRequest()->post('noteText', ''));
        $paragraph = IntVal($this->getHttpRequest()->post('paragraphNo', -1));

        if ($this->getHttpRequest()->post('sectionId', 0) > 0) {
            $section = IntVal($this->getHttpRequest()->post('sectionId', 0));
        } else {
            $section = null;
        }

        $comment = $motion->getPrivateComment($section, $paragraph);
        if ($comment && $noteText === '') {
            $comment->delete();
            $motion->refresh();
            return;
        }

        if (!$comment) {
            $comment = new MotionComment();
        }
        $comment->motionId     = $motion->id;
        $comment->userId       = $user->id;
        $comment->text         = $noteText;
        $comment->status       = IComment::STATUS_PRIVATE;
        $comment->paragraph    = $paragraph;
        $comment->sectionId    = $section;
        $comment->dateCreation = date('Y-m-d H:i:s');
        $comment->name         = ($user->name ? $user->name : '-');
        $comment->save();

        $motion->refresh();

        throw new ResponseException(new RedirectResponse(UrlHelper::createMotionCommentUrl($comment)));
    }

    /**
     * @throws \Throwable
     */
    private function performShowActions(Motion $motion, int $commentId, array &$viewParameters): void
    {
        $post = $this->getPostValues();
        if ($commentId === 0 && isset($post['commentId'])) {
            $commentId = intval($post['commentId']);
        }
        if (isset($post['deleteComment'])) {
            $this->deleteComment($motion, $commentId);
        } elseif (isset($post['commentScreeningAccept'])) {
            $this->screenCommentAccept($motion, $commentId);
        } elseif (isset($post['commentScreeningReject'])) {
            $this->screenCommentReject($motion, $commentId);
        } elseif (isset($post['motionLike'])) {
            $this->motionLike($motion);
        } elseif (isset($post['motionDislike'])) {
            $this->motionDislike($motion);
        } elseif (isset($post['motionSupport'])) {
            $this->motionSupport($motion);
        } elseif (isset($post['motionSupportRevoke'])) {
            $this->motionSupportRevoke($motion);
        } elseif (isset($post['motionSupportFinish'])) {
            $this->motionSupportFinish($motion);
        } elseif (isset($post['addTag'])) {
            $this->motionAddTag($motion);
        } elseif (isset($post['delTag'])) {
            $this->motionDelTag($motion);
        } elseif (isset($post['writeComment'])) {
            $this->writeComment($motion, $viewParameters);
        } elseif (isset($post['setProposalAgree'])) {
            $this->setProposalAgreement($motion, Motion::STATUS_ACCEPTED, $post['comment'] ?? null);
        } elseif (isset($post['setProposalDisagree'])) {
            $this->setProposalAgreement($motion, Motion::STATUS_REJECTED, $post['comment'] ?? null);
        } elseif (isset($post['savePrivateNote'])) {
            $this->savePrivateNote($motion);
        }
    }

    public function actionDelProposalComment(string $motionSlug): ResponseInterface
    {
        $motion = $this->getMotionWithCheck($motionSlug);

        $commentId = $this->getHttpRequest()->post('id');
        $comment   = MotionAdminComment::findOne(['id' => $commentId, 'motionId' => $motion->id]);
        if ($comment && User::isCurrentUser($comment->getMyUser())) {
            $comment->delete();
            return new JsonResponse(['success' => true]);
        } else {
            return new RestApiExceptionResponse(403, 'No permission to delete this comment');
        }
    }

    public function actionSaveProposalStatus(string $motionSlug): ResponseInterface
    {
        $motion = $this->consultation->getMotion($motionSlug);
        if (!$motion) {
            return new RestApiExceptionResponse(404, 'Motion not found');
        }

        $post = $this->getPostValues();
        if (!in_array('version', array_keys($post))) {
            return new RestApiExceptionResponse(400, 'No version provided');
        }
        $proposal = $motion->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) {
            $originalMotionId = $motion->id;
            $originalProposalStatus = $proposal->proposalStatus;
            foreach (AntragsgruenApp::getActivePlugins() as $plugin) {
                /** @var Motion $motion */
                $motion = $plugin::onBeforeProposedProcedureStatusSave($motion);
            }

            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', '');
            }

            $motion->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::createMotionUrl($motion, 'view');
                    }
                    $proposal->setPublished();
                } else {
                    $proposal->visibleFrom = null;
                }
            }

            try {
                $motion->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
                );
            } catch (FormError $e) {
                return new RestApiExceptionResponse(400, $e->getMessage());
            }

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

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

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

            if ($proposal->proposalStatus === IMotion::STATUS_MODIFIED_ACCEPTED && $originalProposalStatus !== $proposal->proposalStatus) {
                $response['redirectToUrl'] = UrlHelper::createMotionUrl($motion, 'edit-proposed-change', ['proposalVersion' => $proposal->version]);
            } elseif ($motion->id !== $originalMotionId) {
                // This can happen if a plugin enforces the creation of a new motion when saving
                $response['redirectToUrl'] = UrlHelper::createMotionUrl($motion, 'view');
            }
        }

        if ($this->getHttpRequest()->post('notifyProposer', false)) {
            try {
                new MotionProposedProcedure(
                    $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($motion->getMyConsultation(), ConsultationLog::MOTION_NOTIFY_PROPOSAL, $motion->id, $data);

                $response['success'] = true;
                $response['html']    = $this->renderPartial('_set_proposed_procedure', [
                    'motion'   => $motion,
                    '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 = Motion::STATUS_ACCEPTED;
            $proposal->save();

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

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

        if ($this->getHttpRequest()->post('writeComment')) {
            $adminComment               = new MotionAdminComment();
            $adminComment->userId       = User::getCurrentUser()->id;
            $adminComment->text         = $this->getHttpRequest()->post('writeComment');
            $adminComment->status       = MotionAdminComment::TYPE_PROPOSED_PROCEDURE;
            $adminComment->dateCreation = date('Y-m-d H:i:s');
            $adminComment->motionId     = $motion->id;
            if (!$adminComment->save()) {
                return new RestApiExceptionResponse(500, 'could not save the comment');
            }

            $response['success'] = true;
            $response['proposalId'] = $proposal->id;
            $response['comment'] = [
                'username'      => $adminComment->getMyUser()->name,
                'id'            => $adminComment->id,
                'text'          => $adminComment->text,
                'delLink'       => UrlHelper::createMotionUrl($motion, '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();

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

        return new JsonResponse($response);
    }

    public function actionEditProposedChange(string $motionSlug, int $proposalVersion): ResponseInterface
    {
        $motion = $this->consultation->getMotion($motionSlug);
        if (!$motion) {
            return new HtmlErrorResponse(404, 'Motion not found');
        }
        if ($this->getHttpRequest()->post('newVersion', null) === 'new') {
            $oldProposal = $motion->getProposalByVersion($proposalVersion);
            $proposal = $motion->getProposalById(null); // Create a new one
            $proposal->proposalStatus = $oldProposal->proposalStatus;
        } else {
            $proposal = $motion->getProposalByVersion($proposalVersion);
        }

        if (!$proposal->canEditProposedProcedure()) {
            return new HtmlErrorResponse(403, 'Not permitted to edit 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_MOTION) {
                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($motion->getMyConsultation(), ConsultationLog::MOTION_SET_PROPOSAL, $motion->id, $ppChanges->jsonSerialize());
            }
            $motion->flushCacheItems(['procedure']);
        }

        $form = new ProposedChangeForm($motion, $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($motion->getMyConsultation(), ConsultationLog::MOTION_SET_PROPOSAL, $motion->id, $ppChanges->jsonSerialize());

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

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

    public function actionSaveEditorial(string $motionSlug, int $sectionId): ResponseInterface
    {
        $motion = $this->consultation->getMotion($motionSlug);
        if (!$motion) {
            return new RestApiExceptionResponse(404, 'Motion not found');
        }
        if (!User::havePrivilege($this->consultation, Privileges::PRIVILEGE_CHANGE_EDITORIAL, PrivilegeQueryContext::motion($motion))) {
            return new RestApiExceptionResponse(403, 'Not permitted to change the editorial');
        }

        $section = null;
        foreach ($motion->getActiveSections(ISectionType::TYPE_TEXT_EDITORIAL) as $searchSection) {
            if ($searchSection->sectionId === $sectionId) {
                $section = $searchSection;
            }
        }
        if (!$section) {
            return new RestApiExceptionResponse(404, 'Section not found');
        }
        /** @var TextEditorial $sectionType */
        $sectionType = $section->getSectionType();
        $sectionType->setEditorialData(
            $this->getPostValue('data'),
            $this->getPostValue('author'),
            $this->getPostValue('updateDate') ? new \DateTime() : $sectionType->getSectionMetadata()['lastUpdate']
        );
        $section->save();

        return new JsonResponse([
            'success' => true,
            'html' => $section->getData(),
            'metadataFormatted' => $sectionType->getFormattedSectionMetadata(true),
        ]);
    }
}