Chapter 7: Back to backend modules

Yes! We are back to the backend. In this chapter we will create the Quotes module. From the schema we can derive the specifications. The module must list quotes and also the companies that have viewed quotes. It is quite evident by now that the main table, in this case Quote, becomes the "main" view while its relationships are the actions that form the nested views.

Create a new backend module named Quotes. The following code generates the "main" view:

public function showMain()
{
    $mf = new Curry_Form_ModelForm('Quote', array(
        'withRelations' => array('RecyclingType', 'ClientCity'),
        'columnElements' => array(
            'created_at' => false,
            'updated_at' => false,
        ),
    ));
    $list = new Curry_ModelView_List('Quote', array(
        'modelForm' => $mf,
        // hide group of list columns
        'hide' => array('client_email', 'client_telephone', 'created_at', 'updated_at'),
        'columns' => array(
            'heading' => array(
                // always make this the first column
                'order' => -1,
                // attach the "edit" action to this column
                'action' => 'edit',
            ),
        ),
        'actions' => array(
            'action_company_agreements' => array(
                'label' => 'Company agreements',
                'action' => $this->getCompanyList(),
                'single' => true,
                'class' => 'inline',
            ),
        ),
    ));
    $list->show($this);
}

The nested view is the list of companies that have viewed the quote. Here is the code for the nested view.

protected function getCompanyList()
{
    $q = QuoteCompanyQuery::create()
        ->joinWithCompany();
    return new Curry_ModelView_List($q, array(
        'columns' => array(
            'company_id' => array(
                'label' => 'Company Id',
                'callback' => function($o)
                {
                    return $o->getCompanyId();
                },
                'order' => 0,
            ),
            'company_name' => array(
                'label' => 'CompanyName',
                'callback' => function($o)
                {
                    return $o->getCompany()->getName();
                },
                'order' => 1,
            ),
        ),
        'actions' => array(
            // we don't want to create a record through the backend.
            // This should happen from the front end.
            // Hide the "Create new" button.
            'new' => false,
            // cannot delete
            'delete' => false,
            // cannot edit
            'edit' => false,
        ),
    ));
}

It's becoming a tedious affair for me to paste whole blocks of code. Therefore, please refer to the project on Github. I will only paste whatever is necessary.

Perfect! Now we need to add some functionality to email these quotes to appropriate companies. We will do this in the next section.

Create HTML template for email.

In this section, I will show you how to create and send HTML formatted email with dynamic content. The technique is simple. You create a regular page and turn off all visibility and accessibility settings. You can have custom Twig variables in the attached page template or you can attach an Article module with the email template. The Article module will contain Twig variables. Then, you "handover" this page to Curry's mail routine which will parse this page. All Twig variables will be replaced with their proper data during the parsing stage before the mail is sent.

In the Templates page tree, I have created a new page named EmailRoot. I have set the Base page to Do not inherit so that it does not inherit any module or template settings from the parent. The EmailRoot page will be the root page for all email. Next, create a subpage named QuoteEmail.

Email template pages

Attach an Article module to the newly created page. email article

Now, edit the Article module and type the following content into the HTML editor:

<p>Hello {{ company.ContactPerson }},</p>
<p>We have a new quote request for you. Below is some teaser information:</p>
<pre>
Heading: {{ quote.Heading }}
Service period required: {{ quote.ServicePeriod }}
Recycling type: {{ quote.RecyclingType.Name }}
</pre>

<p>If you find this interesting please click the link below to make an agreement and see the client's contact information.</p>

<a href="{{ buy_quote_link }}">Buy Quote</a>

Also, ensure that you check the Allow template syntax option in the Advanced section of the module. email article content

Notice that we have added Twig variables to the template content. Next, create a page where the company contact can come to inorder to buy the quote. buy quote page

Now, we will write the routine that will parse this page. You can write a simple function in the backend module itself. But since this feature corresponds to a mail going out to a company, I prefer to write this code to the Company propel class file. Write the following code to the Company class file in path cms/propel/build/classes/project/Company.php.

class Company extends BaseCompany
{
    // check the address bar in the Page module when this page is active
    const QUOTE_MAIL_TPL_ID = 8;
    // see Properties tab of this page
    const BUY_QUOTE_PAGE_LINK = 'demo/buy-quote/';

    public function sendQuoteMail(Quote $quote, $subject = 'New quote request')
    {
        $mailTpl = PageQuery::create()->findPk(self::QUOTE_MAIL_TPL_ID);
        if (!$mailTpl) {
            throw new Exception('Cannot find mail template page.');
        }

        // NOTE: These are the Twig variables that will become available
        // in the Article module.
        $globals = array(
            'company' => $this,
            'quote' => $quote,
            'buy_quote_link' => url(self::BUY_QUOTE_PAGE_LINK, array(
                'qid' => $this->getPrimaryKey()
            ))->getAbsolute('&', true),
        );

        $mail = Curry_Mail::createFromPage($mailTpl, null, $globals);
        $mail->addTo($this->getClientEmail(), $this->getClientName());
        $mail->setFrom(Curry_Core::$config->curry->adminEmail, Curry_Core::$config->curry->projectName);
        $mail->setSubject($subject);
        try {
            $mail->send();
        } catch (Exception $e) {
            throw $e;
        }
    }
}

The function which does the magic is Curry_Mail::createFromPage. It parses the page and calls the Twig parser to parse Twig variables passed in the globals array. Notice the buy_quote_link value. We use the url function to return the absolute url instead of relative ones. If the last parameter of the getAbsolute function is true then the url function appends a hash code to the url to ensure that the url query parameters are not tampered with.

Now, it's time to create an email action for each quote and write code to email it to appropriate companies.

Email action for the Quotes module

Create a new action named action_quote_mail.

'action_quote_mail' => array(
    'label' => 'Send quote mail',
    'href' => (string) url('', array('module', 'view' => 'QuoteMail')),
    'single' => true,
    'class' => 'inline',
),

Notice that this action uses an href parameter. You use the action parameter when you want this list to chain to another list. I did not use the term another view because the nested list is in the same view. We simply "chain" it or "link" to it. When you want to "chain" to a view showing some HTML content, you use the href parameter. The value is the URL to the view within the same module or another. Can I chain to a view in another module? Sure! Just set the module query parameter to the module's class name. In our case we want to chain to a view within the same module. Hence, we just declare the module parameter without setting any value. The url() function will add the module parameter to the new URL giving it the same value found in the URL passed as the first argument. An empty string passed to the first argument of the url() function resolves to the current URL.

As a rule of thumb, you will use action when you want to "chain" to a ModelView list and href when you want to "chain" to a view.

In the showQuoteMail view we will embed some JavaScript to draw a progress bar. Once again notice that we chain to another view named showMailBatchJson. This view will be called by our Ajax JavaScript function. It is in this method that we run the query to select companies, send email and update the progress bar.

QuoteMail code

MailBatchJson code

If you wrote the code correctly, you should be able to see a view as shown in the picture below when you click the Send quote mail action.

quote mail view

For email testing, you can setup Postfix or maybe type your Google account credentials in System -> System -> Mail. SMTP settings

Click the Test email button to send an email to yourself to test if mail is working. Also ensure that Divert outgoing email to adminEmail is checked so that the admin can receive all test email. Please type your email into the Admin email text box.

Alright! There is still one more part remaining to complete the project in this tutorial. We need to update a quote's "view" status when an company views it by following the email link. Let's do that in the next section.

When you clicked the Send quote mail, you should receive email in your inbox (provided you followed email setup instruction as specified above).

Quote email

Click the Buy Quote link. This will take you to the website provided you have set up a proper domain. If not, copy the link and paste in your VM. Notice the URL query parameters. We have a parameter named hash that we didn't specify. It was put there by the url() function. Remember the following code?

'buy_quote_link' => url(self::BUY_QUOTE_PAGE_LINK, array(
    'qid' => $this->getPrimaryKey()
))->getAbsolute('&', true),

The getAbsolute() method puts the hash parameter there. It is a hash of all the query parameters so that they cannot be manipulated externally.

URL hash

We have to verify such a url in our module. Let's write the code. Create a new page module named BuyQuote and attach it to the Buy Quote page. That's the page where the Buy Quote link in the email leads to.

New BuyQuote module

<?php

class Project_Module_BuyQuote extends Curry_Module
{

    public function toTwig()
    {
        if (!Curry_URL::validate()) {
            throw new Exception('URL validation failed. Parameters are manipulated externally.');
        }

        $r = $this->getRequest();
        $quote = QuoteQuery::create()->findPk($r->getParam('qid'));
        $company = CompanyQuery::create()->findPk($r->getParam('cid'));

        $form = new Curry_Form(array(
            'method' => 'post',
            'elements' => array(
                'chk_agree' => array('checkbox', array(
                    'label' => 'I agree to terms and conditions',
                )),
                'btn_buy' => array('submit', array('label' => 'Buy quote')),
            ),
        ));

        if ($r->isPost() && $form->isValid($r->post)) {
            if (!$form->chk_agree->isChecked()) {
                $form->chk_agree->addError('Please agree to Terms and Conditions.');
                $form->chk_agree->markAsError();
            } else {
                // TODO: update views
                $quote_company = new QuoteCompany();
                $quote_company->setQuoteId($quote->getPrimaryKey())
                    ->setCompanyId($company->getPrimaryKey())
                    ->save();

                return array(
                    'quote' => $quote,
                    'company' => $company,
                );
            }
        }

        return array(
            'form' => $form,
            'quote' => $quote,
            'company' => $company,
        );
    }
}

Notice the URL validator function Curry_URL::validate(). That's the method that validates whether the URL hash is valid or not. The rest of the code is simple business logic to purchase the quote.

That's it! we are done with the basic demo.

results matching ""

    No results matching ""