Skip to content

object actions template ok? #183

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
calvera opened this issue Jul 26, 2015 · 6 comments
Closed

object actions template ok? #183

calvera opened this issue Jul 26, 2015 · 6 comments

Comments

@calvera
Copy link
Contributor

calvera commented Jul 26, 2015

hi,

i'm wondering if object actions template is 100% correct...

see https://github.com/symfony2admingenerator/GeneratorBundle/blob/master/Resources/templates/CommonAdmin/ActionsAction/object_action.php.twig#L40

the return is never called...

also if I return from my objectActionXXX response (for example render page with form), it is not propagated and the page is not rendered.

I would propose an enhacement:

{% block attemptObjectAction %}

    /**
     * This function handles common object actions behaviour like
     * checking CSRF protection token or credentials.
     *
     * To customize your action look into:
     * executeObject{{ action.name|php_name|capitalize }}() - holds action logic
     * successObject{{ action.name|php_name|capitalize }}() - called if action was successfull
     * errorObject{{ action.name|php_name|capitalize }}()   - called if action errored
     */
    protected function attemptObject{{ action.name|php_name|capitalize }}($pk)
    {
        ${{ builder.ModelClass }} = $this->getObject($pk);

        try {
            {{ block('security_action_with_object') -}}

            {%- if action.csrfProtected %}
            if ('POST' == $this->get('request')->getMethod()) {
                {{ block('csrf_action_check_token') }}

                $response = $this->executeObject{{ action.name|php_name|capitalize }}(${{ builder.ModelClass }});

                if ($response) {
                    return $response;
                }

                return $this->successObject{{ action.name|php_name|capitalize }}(${{ builder.ModelClass }});
            }
            {% else %}
            $response = $this->executeObject{{ action.name|php_name|capitalize }}(${{ builder.ModelClass }});

            if ($response) {
                return $response;
            }

            return $this->successObject{{ action.name|php_name|capitalize }}(${{ builder.ModelClass }});
            {%- endif %}

        } catch (\Exception $e) {
            return $this->errorObject{{ action.name|php_name|capitalize }}($e, ${{ builder.ModelClass }});
        }
    }

{% endblock %}

what do you think?

-ks-

@sescandell
Copy link
Member

Hi @calvera

The line you mentioned on your link is working only in the situation your objectAction is CSRF protected (meaning, require POST request) but the request made is a GET one. We so in that situation display a simple page with the object form action so the user can submit it with a POST request... (this is only if javacript fails, or user call the action in a new window or something like that).

Everything else should be enough to handle all your situations.
If you want to return a given view on success, override the method successObjectMY_ACTION. If you want to return a given view on error, errorObjectMY_ACTION is what your are looking for.

Your proposition failed on csrf protected action with a GET request.

Let us know what you were trying to do, and we'll advice you on how to proceed.

@calvera
Copy link
Contributor Author

calvera commented Jul 27, 2015

i want to implement sending invitation to user as object action, administrator will choose email template and email is sent... I implemented it by the following code (overriding attemptObjectMail), but I'm looking for better/prefered solution (just curios):

    protected function attemptObjectMail($pk)
    {
        $user = $this->getObject($pk);

        try {
            $this->checkCredentials('hasRole("ROLE_ADMIN")', $user);

            $form = $this->createFormBuilder(array())
                ->add('template', 's2a_select2_entity', array(
                    'class' => 'CHCLanguageTestingBundle:MailTemplate',
                    'property' => 'name',
                    )
                )
                ->getForm();

            $form->handleRequest($this->get('request'));

            if ($form->isValid()) {
                $data = $form->getData();
                $mailTemplate = $data['template'];
                $twig = $this->get('twig');
                $template = twig_template_from_string($twig, $mailTemplate->getBody());

                $message = \Swift_Message::newInstance()
                    ->setSubject($mailTemplate->getSubject())
                    ->setFrom('[email protected]', "Channel Crossings Language Testing")
                    ->setTo($user->getEmail(), $user->getName() . ' ' . $user->getSurname())
                    ->setBody(
                        $template->render(
                            array('login' => $user->getUsername(),
                                  'password' => $user->getOpenpassword(),
                                )
                            ),
                        'text/html'
                        );
                $this->get('mailer')->send($message);
                $this->get('session')->getFlashBag()->add(
                    'success',
                    $this->get('translator')->trans(
                        "action.custom.success",
                        array('%name%' => 'Send invitation'),
                        'Admingenerator'
                    )
                );

                return new RedirectResponse($this->generateUrl("CHC_LanguageTestingBundle_User_list"));
            }
            return $this->render(
                'CHCLanguageTestingBundle:UserActions:index.html.twig',
                $this->getAdditionalRenderParameters($user, 'mail') + array(
                    "User" => $user,
                    "title" => $this->get('translator')->trans(
                        "action.custom.title",
                        array('%name%' => 'mail'),
                        'Admingenerator'
                    ),
                    "actionRoute" => "CHC_LanguageTestingBundle_User_object",
                    "actionParams" => array("pk" => $pk, "action" => "mail"),
                    "form" => $form->createView(),
                )
            );


        } catch (ValidationException $e) {
            return $this->errorObjectMail($e, $user);
        }

    }

@calvera
Copy link
Contributor Author

calvera commented Jul 27, 2015

my alternative solution is:

    object_actions:
        mail:
            label: Send invitation to user
            credentials: 'hasRole("ROLE_ADMIN")'
            icon: fa-envelope
            route:    CHC_LanguageTestingBundle_User_mail
            params:
                pk:       "{{ User.id }}"

then i have dedicated controller:

/**
 * @Route("/admin/user")
 */
class MailController extends ShowController
{
    /**
     * @Route("/{pk}/mail", name="CHC_LanguageTestingBundle_User_mail")
     */
    public function assignAction(Request $request, $pk)
    {
        $User = $this->getObject($pk);

etc.

so I'm wondering which is better, i do not want to 'repeat myself' ;-)

@sescandell
Copy link
Member

Actually, you should not override the attemptObjectXXX methods but executeObjectXXX ones (and successObjectXXX and errorObjectXXX). I sent you a fully working sample about what you are trying to do:

/**
     * (non-PHPdoc)
     * @see \Admingenerated\AppBundle\BaseModuleController\ActionsController::executeObjectCopyTo()
     */
    protected function executeObjectCopyTo(\Sescandell\AppBundle\Entity\Module $module)
    {
        $contextManager = $this->get('context_restrictor.manager');
        $disabled = $contextManager->disable();

        $this->form = $this->createForm(
            new ModuleCopyType($this->get('security.context'), $this->get('translator')),
            $module->copy()/* clone $module */,
            array('action' => $this->getRequest()->getRequestUri())
        );

        $this->form->handleRequest($this->getRequest());

        if ($this->form->isValid()) {
            $copy = $this->form->getData();
            $copy->setFromAdmin(true);
            $copy->setAuthor($this->getUser());
            if ($copy->getCustodyAccount()) {
                $copy->setCompany(null);
            }

            $this->getDoctrine()->getManager()->persist($copy);
            $this->getDoctrine()->getManager()->flush();
        } elseif ($this->form->isSubmitted()) {
            if ($disabled) {
                $contextManager->enable();
            }
            throw new \InvalidArgumentException('Form is not valid');
        }

        if ($disabled) {
            $contextManager->enable();
        }
    }

    /**
     * (non-PHPdoc)
     * @see \Admingenerated\AppBundle\BaseModuleController\ActionsController::successObjectCopyTo()
     */
    protected function successObjectCopyTo(\Sescandell\AppBundle\Entity\Module $module)
    {
        if ($this->form->isSubmitted() && $this->form->isValid()) {
            $this->get('session')->getFlashBag()->set('from_copy', true);
            return new JsonResponse(array(
                'success' => true,
                'type' => 'redirect',
                'content' => $this->generateUrl("Sescandell_AppBundle_Module_edit", array('pk' => $this->form->getData()->getId())),
            ));
        }

        // Get
        return $this->render(
            'SescandellAppBundle:ModuleActions:copy.html.twig',
            array('form' => $this->form->createView())
        );
    }

    /**
     * (non-PHPdoc)
     * @see \Admingenerated\AppBundle\BaseModuleController\ActionsController::errorObjectCopyTo()
     */
    protected function errorObjectCopyTo(\Exception $e, \Sescandell\AppBundle\Entity\Module $module = null)
    {
        if (is_null($module)) {
            return parent::errorObjectCopyTo($e, $module);
        }

        if (!is_null($this->form) && $this->form->isSubmitted()) {
            $this->get('session')->getFlashBag()->add(
                    'error',
                    $this->get('translator')->trans("admin.module.object_actions.copy.error", array('%name%' => $module->getName()), 'app')
            );

            return new JsonResponse(array(
                    'success' => false,
                    'type'    => 'html',
                    'content' => $this->renderView(
                            'SescandellAppBundle:ModuleActions:copy.html.twig',
                            array('form' => $this->form->createView())
                    )
            ));
        }
    }

Just one thing I would do differently is throwing dedicated exception in case of error injecting the form so error handler method would be simpler.

What this sample do is:

  • when the object action is called the first time with a get request (inf act through an AJAX request in my application), I display a popin with the form so the user can select a category to copy my object (No exception thrown in executeObjectCopyTo and //Get part called in the method successObjectCopyTo)
  • So when the user submit the form after it has been displayed to the user, if everything went well ($this->form->isValid() from method executeObjectCopyTo) user is redirected to another page (JsonResponse from method successObjectCopyTo)
  • If the form submitted is invalid (so $this->form->isValid() is false and $this->form->isSubmitted() is true, an exception is thrown: throw new \InvalidArgumentException('Form is not valid'); so the errorObjectCopyTo method handler is called and re-display the form with errors:
return new JsonResponse(array(
                    'success' => false,
                    'type'    => 'html',
                    'content' => $this->renderView(
                            'SescandellAppBundle:ModuleActions:copy.html.twig',
                            array('form' => $this->form->createView())
                    )
            ));

Please note that JsonResponse here are because I made an Ajax library able to mix some Admingenerator views&action and AJAX. It might be different in your situation.

@sescandell
Copy link
Member

So taking your cas, it should be something like this:

    protected function executeObjectMail(User $user)
    {
            $form = $this->createFormBuilder(array())
                ->add('template', 's2a_select2_entity', array(
                    'class' => 'CHCLanguageTestingBundle:MailTemplate',
                    'property' => 'name',
                    )
                )
                ->getForm();

            $form->handleRequest($this->get('request'));

            if ($form->isValid()) {
                $data = $form->getData();
                $mailTemplate = $data['template'];
                $twig = $this->get('twig');
                $template = twig_template_from_string($twig, $mailTemplate->getBody());

                $message = \Swift_Message::newInstance()
                    ->setSubject($mailTemplate->getSubject())
                    ->setFrom('[email protected]', "Channel Crossings Language Testing")
                    ->setTo($user->getEmail(), $user->getName() . ' ' . $user->getSurname())
                    ->setBody(
                        $template->render(
                            array('login' => $user->getUsername(),
                                  'password' => $user->getOpenpassword(),
                                )
                            ),
                        'text/html'
                        );
                $this->get('mailer')->send($message);
            } elseif ($form->isSubmitted()) {
                 throw new InvalidMailTemplateForm($form);
            }

           throw new UnsubmittedMailTemplateForm($form);
}


    }

protected function successObjectMail(User $user)
{
$this->get('session')->getFlashBag()->add(
                    'success',
                    $this->get('translator')->trans(
                        "action.custom.success",
                        array('%name%' => 'Send invitation'),
                        'Admingenerator'
                    )
                );

                return new RedirectResponse($this->generateUrl("CHC_LanguageTestingBundle_User_list"));
}

prtected function errorObjectMail(\Exception $e, User $user)
{
if ($e instanceof InvalidMailTemplateForm) {
         return $this->render(
                'CHCLanguageTestingBundle:UserActions:index.html.twig',
                $this->getAdditionalRenderParameters($user, 'mail') + array(
                    "User" => $user,
                    "title" => $this->get('translator')->trans(
                        "action.custom.title",
                        array('%name%' => 'mail'),
                        'Admingenerator'
                    ),
                    "actionRoute" => "CHC_LanguageTestingBundle_User_object",
                    "actionParams" => array("pk" => $pk, "action" => "mail"),
                    "form" => $e->getForm()->createView(),
                )
            );
} elseif ($e instanceof UnsubmittedMailTemplateForm) {
   // Display the form for the first time...
}

return parent::errorObjectMail($e, $user);
}

And there is no duplication of code.

Everything is then depending on how you display your form to correctly manage error / success and throwing exceptions

Hoping that helps.

@calvera
Copy link
Contributor Author

calvera commented Jul 27, 2015

@sescandell Thanks! I was confused by function names: success and error. I supposed their purpose is different...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants