diff --git a/.travis.yml b/.travis.yml index 926fd2f..640a63d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,17 @@ language: php php: - - 5.3 + - 5.4 env: - - DB=MYSQL CORE_RELEASE=3.1 - DB=MYSQL CORE_RELEASE=3 + - DB=MYSQL CORE_RELEASE=3.1 - DB=PGSQL CORE_RELEASE=3.1 matrix: include: - - php: 5.4 - env: DB=MYSQL CORE_RELEASE=master + - php: 5.3 + env: DB=MYSQL CORE_RELEASE=3.1 before_script: - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support diff --git a/code/model/UserDefinedForm.php b/code/model/UserDefinedForm.php index 212c688..d4cc360 100755 --- a/code/model/UserDefinedForm.php +++ b/code/model/UserDefinedForm.php @@ -25,6 +25,11 @@ class UserDefinedForm extends Page { private static $translate_excluded_fields = array( 'Fields' ); + + /** + * @var string + */ + private static $email_template_directory = 'userforms/templates/email/'; /** * @var array Fields on the user defined form page. @@ -37,6 +42,7 @@ class UserDefinedForm extends Page { 'DisableSaveSubmissions' => 'Boolean', 'EnableLiveValidation' => 'Boolean', 'HideFieldLabels' => 'Boolean', + 'DisplayErrorMessagesAtTop' => 'Boolean', 'DisableAuthenicatedFinishAction' => 'Boolean', 'DisableCsrfSecurityToken' => 'Boolean' ); @@ -59,6 +65,22 @@ class UserDefinedForm extends Page { "EmailRecipients" => "UserDefinedForm_EmailRecipient" ); + /** + * @var array + * @config + */ + private static $casting = array( + 'ErrorContainerID' => 'Text' + ); + + /** + * Error container selector which matches the element for grouped messages + * + * @var string + * @config + */ + private static $error_container_id = 'error-container'; + /** * Temporary storage of field ids when the form is duplicated. * Example layout: array('EditableCheckbox3' => 'EditableCheckbox14') @@ -78,6 +100,7 @@ class UserDefinedForm extends Page { // define tabs $fields->findOrMakeTab('Root.FormContent', _t('UserDefinedForm.FORM', 'Form')); $fields->findOrMakeTab('Root.FormOptions', _t('UserDefinedForm.CONFIGURATION', 'Configuration')); + $fields->findOrMakeTab('Root.Recipients', _t('UserDefinedForm.RECIPIENTS', 'Recipients')); $fields->findOrMakeTab('Root.Submissions', _t('UserDefinedForm.SUBMISSIONS', 'Submissions')); // field editor @@ -94,25 +117,27 @@ class UserDefinedForm extends Page { $editor->setRows(3); $label->addExtraClass('left'); - // Set the summary fields of UserDefinedForm_EmailRecipient dynamically via config system - Config::inst()->update( - 'UserDefinedForm_EmailRecipient', - 'summary_fields', - array( - 'EmailAddress' => _t('UserDefinedForm.EMAILADDRESS', 'Email'), - 'EmailSubject' => _t('UserDefinedForm.EMAILSUBJECT', 'Subject'), - 'EmailFrom' => _t('UserDefinedForm.EMAILFROM', 'From'), - ) - ); + // Define config for email recipients + $emailRecipientsConfig = GridFieldConfig_RecordEditor::create(10); + $emailRecipientsConfig->getComponentByType('GridFieldAddNewButton') + ->setButtonName( + _t('UserDefinedForm.ADDEMAILRECIPIENT', 'Add Email Recipient') + ); // who do we email on submission - $emailRecipients = new GridField('EmailRecipients', _t('UserDefinedForm.EMAILRECIPIENTS', 'Email Recipients'), $self->EmailRecipients(), GridFieldConfig_RecordEditor::create(10)); - $emailRecipients->getConfig()->getComponentByType('GridFieldAddNewButton')->setButtonName( - _t('UserDefinedForm.ADDEMAILRECIPIENT', 'Add Email Recipient') + $emailRecipients = new GridField( + 'EmailRecipients', + _t('UserDefinedForm.EMAILRECIPIENTS', 'Email Recipients'), + $self->EmailRecipients(), + $emailRecipientsConfig ); + $emailRecipients + ->getConfig() + ->getComponentByType('GridFieldDetailForm') + ->setItemRequestClass('UserDefinedForm_EmailRecipient_ItemRequest'); $fields->addFieldsToTab('Root.FormOptions', $onCompleteFieldSet); - $fields->addFieldToTab('Root.FormOptions', $emailRecipients); + $fields->addFieldToTab('Root.Recipients', $emailRecipients); $fields->addFieldsToTab('Root.FormOptions', $self->getFormOptions()); @@ -313,7 +338,12 @@ SQL; * @return ArrayList */ public function FilteredEmailRecipients($data = null, $form = null) { - $recipients = new ArrayList($this->getComponents('EmailRecipients')->toArray()); + $recipients = new ArrayList($this->EmailRecipients()->toArray()); + + // Filter by rules + $recipients = $recipients->filterByCallback(function($recipient) use ($data, $form) { + return $recipient->canSend($data, $form); + }); $this->extend('updateFilteredEmailRecipients', $recipients, $data, $form); @@ -397,6 +427,7 @@ SQL; new CheckboxField("ShowClearButton", _t('UserDefinedForm.SHOWCLEARFORM', 'Show Clear Form Button'), $this->ShowClearButton), new CheckboxField("EnableLiveValidation", _t('UserDefinedForm.ENABLELIVEVALIDATION', 'Enable live validation')), new CheckboxField("HideFieldLabels", _t('UserDefinedForm.HIDEFIELDLABELS', 'Hide field labels')), + new CheckboxField("DisplayErrorMessagesAtTop", _t('UserDefinedForm.DISPLAYERRORMESSAGESATTOP', 'Display error messages above the form?')), new CheckboxField('DisableCsrfSecurityToken', _t('UserDefinedForm.DISABLECSRFSECURITYTOKEN', 'Disable CSRF Token')), new CheckboxField('DisableAuthenicatedFinishAction', _t('UserDefinedForm.DISABLEAUTHENICATEDFINISHACTION', 'Disable Authenication on finish action')) ); @@ -435,6 +466,15 @@ SQL; } return $isModified; } + + /** + * Get the HTML id of the error container displayed above the form. + * + * @return string + */ + public function getErrorContainerID() { + return $this->config()->error_container_id; + } } /** @@ -538,6 +578,8 @@ class UserDefinedForm_Controller extends Page_Controller { if($this->DisableCsrfSecurityToken) { $form->disableSecurityToken(); } + + $this->generateValidationJavascript($form); return $form; } @@ -622,16 +664,11 @@ class UserDefinedForm_Controller extends Page_Controller { } /** - * Get the required form fields for this form. Includes building the jQuery - * validate structure + * Get the required form fields for this form. * * @return RequiredFields */ public function getRequiredFields() { - - // set the custom script for this form - Requirements::customScript($this->renderWith('ValidationScript'), 'UserFormsValidation'); - // Generate required field validator $requiredNames = $this ->Fields() @@ -643,6 +680,21 @@ class UserDefinedForm_Controller extends Page_Controller { return $required; } + + /** + * Build jQuery validation script and require as a custom script + * + * @param Form $form + */ + public function generateValidationJavascript($form) { + // set the custom script for this form + Requirements::customScript( + $this + ->customise(array('Form' => $form)) + ->renderWith('ValidationScript'), + 'UserFormsValidation' + ); + } /** * Generate the javascript for the conditional field show / hiding logic. @@ -989,7 +1041,8 @@ JS // email users on submit. if($recipients = $this->FilteredEmailRecipients($data, $form)) { $email = new UserDefinedForm_SubmittedFormEmail($submittedFields); - + $mergeFields = $this->getMergeFieldsMap($emailData['Fields']); + if($attachments) { foreach($attachments as $file) { if($file->ID != 0) { @@ -1003,10 +1056,16 @@ JS } foreach($recipients as $recipient) { + $parsedBody = SSViewer::execute_string($recipient->getEmailBodyContent(), $mergeFields); + + if (!$recipient->SendPlain && $recipient->emailTemplateExists()) { + $email->setTemplate($recipient->EmailTemplate); + } + $email->populateTemplate($recipient); $email->populateTemplate($emailData); $email->setFrom($recipient->EmailFrom); - $email->setBody($recipient->EmailBody); + $email->setBody($parsedBody); $email->setTo($recipient->EmailAddress); $email->setSubject($recipient->EmailSubject); @@ -1043,7 +1102,7 @@ JS $this->extend('updateEmail', $email, $recipient, $emailData); if($recipient->SendPlain) { - $body = strip_tags($recipient->EmailBody) . "\n"; + $body = strip_tags($recipient->getEmailBodyContent()) . "\n"; if(isset($emailData['Fields']) && !$recipient->HideFormData) { foreach($emailData['Fields'] as $Field) { $body .= $Field->Title .': '. $Field->Value ." \n"; @@ -1091,6 +1150,22 @@ JS return $this->redirect($this->Link('finished') . $referrer . $this->config()->finished_anchor); } + /** + * Allows the use of field values in email body. + * + * @param ArrayList fields + * @return ViewableData + */ + private function getMergeFieldsMap($fields = array()) { + $data = new ViewableData(); + + foreach ($fields as $field) { + $data->setField($field->Name, DBField::create_field('Text', $field->Value)); + } + + return $data; + } + /** * This action handles rendering the "finished" message, which is * customizable by editing the ReceivedFormSubmission template. @@ -1137,91 +1212,252 @@ JS } /** - * A Form can have multiply members / emails to email the submission + * A Form can have multiply members / emails to email the submission * to and custom subjects - * + * * @package userforms */ class UserDefinedForm_EmailRecipient extends DataObject { - + private static $db = array( 'EmailAddress' => 'Varchar(200)', 'EmailSubject' => 'Varchar(200)', 'EmailFrom' => 'Varchar(200)', 'EmailReplyTo' => 'Varchar(200)', 'EmailBody' => 'Text', + 'EmailBodyHtml' => 'HTMLText', + 'EmailTemplate' => 'Varchar', 'SendPlain' => 'Boolean', - 'HideFormData' => 'Boolean' + 'HideFormData' => 'Boolean', + 'CustomRulesCondition' => 'Enum("And,Or")' ); - + private static $has_one = array( 'Form' => 'UserDefinedForm', 'SendEmailFromField' => 'EditableFormField', 'SendEmailToField' => 'EditableFormField', 'SendEmailSubjectField' => 'EditableFormField' ); - - private static $summary_fields = array(); + + private static $has_many = array( + 'CustomRules' => 'UserDefinedForm_EmailRecipientCondition' + ); + + private static $summary_fields = array( + 'EmailAddress', + 'EmailSubject', + 'EmailFrom' + ); + + public function summaryFields() { + $fields = parent::summaryFields(); + if(isset($fields['EmailAddress'])) { + $fields['EmailAddress'] = _t('UserDefinedForm.EMAILADDRESS', 'Email'); + } + if(isset($fields['EmailSubject'])) { + $fields['EmailSubject'] = _t('UserDefinedForm.EMAILSUBJECT', 'Subject'); + } + if(isset($fields['EmailFrom'])) { + $fields['EmailFrom'] = _t('UserDefinedForm.EMAILFROM', 'From'); + } + return $fields; + } + + /** + * Get instance of UserDefinedForm when editing in getCMSFields + * + * @return UserDefinedFrom + */ + protected function getFormParent() { + $formID = $this->FormID + ? $this->FormID + : Session::get('CMSMain.currentPage'); + return UserDefinedForm::get()->byID($formID); + } + + public function getTitle() { + if($this->EmailAddress) { + return $this->EmailAddress; + } + if($this->EmailSubject) { + return $this->EmailSubject; + } + return parent::getTitle(); + } + + /** + * Generate a gridfield config for editing filter rules + * + * @return GridFieldConfig + */ + protected function getRulesConfig() { + $formFields = $this->getFormParent()->Fields(); + + $config = GridFieldConfig::create() + ->addComponents( + new GridFieldButtonRow('before'), + new GridFieldToolbarHeader(), + new GridFieldAddNewInlineButton(), + new GridState_Component(), + new GridFieldDeleteAction(), + $columns = new GridFieldEditableColumns() + ); + + $columns->setDisplayFields(array( + 'ConditionFieldID' => function($record, $column, $grid) use ($formFields) { + return DropdownField::create($column, false, $formFields->map('ID', 'Title')); + }, + 'ConditionOption' => function($record, $column, $grid) { + $options = UserDefinedForm_EmailRecipientCondition::config()->condition_options; + return DropdownField::create($column, false, $options); + }, + 'ConditionValue' => function($record, $column, $grid) { + return TextField::create($column); + } + )); + + return $config; + } /** * @return FieldList */ public function getCMSFields() { - - $fields = new FieldList( - new TextField('EmailSubject', _t('UserDefinedForm.EMAILSUBJECT', 'Email subject')), - new LiteralField('EmailFromContent', '

'._t( - 'UserDefinedForm.EmailFromContent', - "The from address allows you to set who the email comes from. On most servers this ". - "will need to be set to an email address on the same domain name as your site. ". - "For example on yoursite.com the from address may need to be something@yoursite.com. ". - "You can however, set any email address you wish as the reply to address." - ) . "

"), - new TextField('EmailFrom', _t('UserDefinedForm.FROMADDRESS','Send email from')), - new TextField('EmailReplyTo', _t('UserDefinedForm.REPLYADDRESS', 'Email for reply to')), - new TextField('EmailAddress', _t('UserDefinedForm.SENDEMAILTO','Send email to')), - new CheckboxField('HideFormData', _t('UserDefinedForm.HIDEFORMDATA', 'Hide form data from email?')), - new CheckboxField('SendPlain', _t('UserDefinedForm.SENDPLAIN', 'Send email as plain text? (HTML will be stripped)')), - new TextareaField('EmailBody', _t('UserDefinedForm.EMAILBODY','Body')) - ); - - $formID = ($this->FormID != 0) ? $this->FormID : Session::get('CMSMain.currentPage'); - $dropdowns = array(); - // if they have email fields then we could send from it - $validEmailFields = EditableEmailField::get()->filter('ParentID', (int)$formID); - // for the subject, only one-line entry boxes make sense - $validSubjectFields = EditableTextField::get()->filter('ParentID', (int)$formID)->filterByCallback(function($item, $list) { return (int)$item->getSetting('Rows') === 1; }); + // Determine optional field values + $form = $this->getFormParent(); + // predefined choices are also candidates - $multiOptionFields = EditableMultipleOptionField::get()->filter('ParentID', (int)$formID); + $multiOptionFields = EditableMultipleOptionField::get()->filter('ParentID', $form->ID); - $fields->insertAfter($dropdowns[] = new DropdownField( - 'SendEmailFromFieldID', - _t('UserDefinedForm.ORSELECTAFIELDTOUSEASFROM', '.. or select a field to use as reply to address'), - $validEmailFields->map('ID', 'Title') - ), 'EmailReplyTo'); + // if they have email fields then we could send from it + $validEmailFromFields = EditableEmailField::get()->filter('ParentID', $form->ID); - $validEmailFields = new ArrayList($validEmailFields->toArray()); - $validEmailFields->merge($multiOptionFields); + // For the subject, only one-line entry boxes make sense + $validSubjectFields = EditableTextField::get() + ->filter('ParentID', $form->ID) + ->filterByCallback(function($item, $list) { + return (int)$item->getSetting('Rows') === 1; + }); $validSubjectFields->merge($multiOptionFields); - $fields->insertAfter($dropdowns[] = new DropdownField( - 'SendEmailToFieldID', - _t('UserDefinedForm.ORSELECTAFIELDTOUSEASTO', '.. or select a field to use as the to address'), - $validEmailFields->map('ID', 'Title') - ), 'EmailAddress'); - $fields->insertAfter($dropdowns[] = new DropdownField( - 'SendEmailSubjectFieldID', - _t('UserDefinedForm.SELECTAFIELDTOSETSUBJECT', '.. or select a field to use as the subject'), - $validSubjectFields->map('ID', 'Title') - ), 'EmailSubject'); + // To address can only be email fields or multi option fields + $validEmailToFields = new ArrayList($validEmailFromFields->toArray()); + $validEmailToFields->merge($multiOptionFields); - foreach($dropdowns as $dropdown) { - $dropdown->setHasEmptyDefault(true); - $dropdown->setEmptyString(" "); + // Build fieldlist + $fields = FieldList::create(Tabset::create('Root')->addExtraClass('EmailRecipientForm')); + + // Configuration fields + $fields->addFieldsToTab('Root.EmailDetails', array( + // Subject + FieldGroup::create( + TextField::create('EmailSubject', _t('UserDefinedForm.TYPESUBJECT', 'Type subject')) + ->setAttribute('style', 'min-width: 400px;'), + DropdownField::create( + 'SendEmailSubjectFieldID', + _t('UserDefinedForm.SELECTAFIELDTOSETSUBJECT', '.. or select a field to use as the subject'), + $validSubjectFields->map('ID', 'Title') + )->setEmptyString('') + ) + ->setTitle(_t('UserDefinedForm.EMAILSUBJECT', 'Email subject')), + + // To + FieldGroup::create( + TextField::create('EmailAddress', _t('UserDefinedForm.TYPETO', 'Type to address')) + ->setAttribute('style', 'min-width: 400px;'), + DropdownField::create( + 'SendEmailToFieldID', + _t('UserDefinedForm.ORSELECTAFIELDTOUSEASTO', '.. or select a field to use as the to address'), + $validEmailToFields->map('ID', 'Title') + )->setEmptyString(' ') + ) + ->setTitle(_t('UserDefinedForm.SENDEMAILTO','Send email to')) + ->setDescription(_t( + 'UserDefinedForm.SENDEMAILTO_DESCRIPTION', + 'You may enter multiple email addresses as a comma separated list.' + )), + + + // From + TextField::create('EmailFrom', _t('UserDefinedForm.FROMADDRESS','Send email from')) + ->setDescription(_t( + 'UserDefinedForm.EmailFromContent', + "The from address allows you to set who the email comes from. On most servers this ". + "will need to be set to an email address on the same domain name as your site. ". + "For example on yoursite.com the from address may need to be something@yoursite.com. ". + "You can however, set any email address you wish as the reply to address." + )), + + + // Reply-To + FieldGroup::create( + TextField::create('EmailReplyTo', _t('UserDefinedForm.TYPEREPLY', 'Type reply address')) + ->setAttribute('style', 'min-width: 400px;'), + DropdownField::create( + 'SendEmailFromFieldID', + _t('UserDefinedForm.ORSELECTAFIELDTOUSEASFROM', '.. or select a field to use as reply to address'), + $validEmailFromFields->map('ID', 'Title') + )->setEmptyString(' ') + ) + ->setTitle(_t('UserDefinedForm.REPLYADDRESS', 'Email for reply to')) + ->setDescription(_t( + 'UserDefinedForm.REPLYADDRESS_DESCRIPTION', + 'The email address which the recipient is able to \'reply\' to.' + )) + )); + + // Only show the preview link if the recipient has been saved. + if (!empty($this->EmailTemplate)) { + $preview = sprintf( + '

%s

%s', + "admin/pages/edit/EditForm/field/EmailRecipients/item/{$this->ID}/preview", + _t('UserDefinedForm.PREVIEW_EMAIL', 'Preview email'), + _t('UserDefinedForm.PREVIEW_EMAIL_DESCRIPTION', 'Note: Unsaved changes will not appear in the preview.') + ); + } else { + $preview = sprintf( + '%s', + _t( + 'UserDefinedForm.PREVIEW_EMAIL_UNAVAILABLE', + 'You can preview this email once you have saved the Recipient.' + ) + ); } - $this->extend('updateCMSFields', $fields); + // Email templates + $fields->addFieldsToTab('Root.EmailContent', array( + new CheckboxField('HideFormData', _t('UserDefinedForm.HIDEFORMDATA', 'Hide form data from email?')), + new CheckboxField('SendPlain', _t('UserDefinedForm.SENDPLAIN', 'Send email as plain text? (HTML will be stripped)')), + new DropdownField('EmailTemplate', _t('UserDefinedForm.EMAILTEMPLATE', 'Email template'), $this->getEmailTemplateDropdownValues()), + new HTMLEditorField('EmailBodyHtml', _t('UserDefinedForm.EMAILBODYHTML','Body')), + new TextareaField('EmailBody', _t('UserDefinedForm.EMAILBODY','Body')), + new LiteralField('EmailPreview', '
' . $preview . '
') + )); + // Custom rules for sending this field + $grid = new GridField( + "CustomRules", + _t('EditableFormField.CUSTOMRULES', 'Custom Rules'), + $this->CustomRules(), + $this->getRulesConfig() + ); + $grid->setDescription(_t( + 'UserDefinedForm.RulesDescription', + 'Emails will only be sent to the recipient if the custom rules are met. If no rules are defined, this receipient will receive notifications for every submission.' + )); + $fields->addFieldsToTab('Root.CustomRules', array( + new DropdownField( + 'CustomRulesCondition', + _t('UserDefinedForm.SENDIF', 'Send condition'), + array( + 'Or' => 'Any conditions are true', + 'And' => 'All conditions are true' + ) + ), + $grid + )); + + $this->extend('updateCMSFields', $fields); return $fields; } @@ -1242,7 +1478,7 @@ class UserDefinedForm_EmailRecipient extends DataObject { public function canView($member = null) { return $this->Form()->canView(); } - + /** * @param Member * @@ -1251,7 +1487,7 @@ class UserDefinedForm_EmailRecipient extends DataObject { public function canEdit($member = null) { return $this->Form()->canEdit(); } - + /** * @param Member * @@ -1260,6 +1496,187 @@ class UserDefinedForm_EmailRecipient extends DataObject { public function canDelete($member = null) { return $this->Form()->canDelete(); } + + /* + * Determine if this recipient may receive notifications for this submission + * + * @param array $data + * @param Form $form + * @return bool + */ + public function canSend($data, $form) { + // Skip if no rules configured + $customRules = $this->CustomRules(); + if(!$customRules->count()) { + return true; + } + + // Check all rules + $isAnd = $this->CustomRulesCondition === 'And'; + foreach($customRules as $customRule) { + $matches = $customRule->matches($data, $form); + if($isAnd && !$matches) { + return false; + } + if(!$isAnd && $matches) { + return true; + } + } + + // Once all rules are checked + return $isAnd; + } + + /** + * Make sure the email template saved against the recipient exists on the file system. + * + * @param string + * + * @return boolean + */ + public function emailTemplateExists($template = '') { + $t = ($template ? $template : $this->EmailTemplate); + + return in_array($t, $this->getEmailTemplateDropdownValues()); + } + + /** + * Get the email body for the current email format + * + * @return string + */ + public function getEmailBodyContent() { + return $this->SendPlain ? $this->EmailBody : $this->EmailBodyHtml; + } + + /** + * Gets a list of email templates suitable for populating the email template dropdown. + * + * @return array + */ + public function getEmailTemplateDropdownValues() { + $templates = array(); + + $finder = new SS_FileFinder(); + $finder->setOption('name_regex', '/^.*\.ss$/'); + + $found = $finder->find(BASE_PATH . '/' . UserDefinedForm::config()->email_template_directory); + + foreach ($found as $key => $value) { + $template = pathinfo($value); + + $templates[$template['filename']] = $template['filename']; + } + + return $templates; + } +} + +/** + * Controller that handles requests to EmailRecipient's + * + * @package userforms + */ +class UserDefinedForm_EmailRecipient_ItemRequest extends GridFieldDetailForm_ItemRequest { + + private static $allowed_actions = array( + 'edit', + 'view', + 'ItemEditForm', + 'preview' + ); + + public function edit($request) { + Requirements::javascript(USERFORMS_DIR . '/javascript/Recipient.js'); + return parent::edit($request); + } + + /** + * Renders a preview of the recipient email. + */ + public function preview() { + return $this->customise(new ArrayData(array( + 'Body' => $this->record->getEmailBodyContent(), + 'HideFormData' => $this->record->HideFormData, + 'Fields' => $this->getPreviewFieldData() + )))->renderWith($this->record->EmailTemplate); + } + + /** + * Get some placeholder field values to display in the preview + * @return ArrayList + */ + private function getPreviewFieldData() { + $data = new ArrayList(); + + $fields = $this->record->Form()->Fields()->filter(array( + 'ClassName:not' => 'EditableLiteralField', + 'ClassName:not' => 'EditableFormHeading' + )); + + foreach ($fields as $field) { + $data->push(new ArrayData(array( + 'Name' => $field->Name, + 'Title' => $field->Title, + 'Value' => '$' . $field->Name, + 'FormattedValue' => '$' . $field->Name + ))); + } + + return $data; + } +} + +/** + * Declares a condition that determines whether an email can be sent to a given recipient + */ +class UserDefinedForm_EmailRecipientCondition extends DataObject { + + /** + * List of options + * + * @config + * @var array + */ + private static $condition_options = array( + "IsBlank" => "Is blank", + "IsNotBlank" => "Is not blank", + "Equals" => "Equals", + "NotEquals" => "Doesn't equal" + ); + + private static $db = array( + 'ConditionOption' => 'Enum("IsBlank,IsNotBlank,Equals,NotEquals")', + 'ConditionValue' => 'Varchar' + ); + + private static $has_one = array( + 'Parent' => 'UserDefinedForm_EmailRecipient', + 'ConditionField' => 'EditableFormField' + ); + + /** + * Determine if this rule matches the given condition + * + * @param array $data + * @param Form $form + * @return bool + */ + public function matches($data, $form) { + $fieldName = $this->ConditionField()->Name; + $fieldValue = isset($data[$fieldName]) ? $data[$fieldName] : null; + switch($this->ConditionOption) { + case 'IsBlank': + return empty($fieldValue); + case 'IsNotBlank': + return !empty($fieldValue); + default: + $matches = is_array($fieldValue) + ? in_array($this->ConditionValue, $fieldValue) + : $this->ConditionValue === (string)$fieldValue; + return ($this->ConditionOption === 'Equals') === (bool)$matches; + } + } } /** @@ -1287,5 +1704,5 @@ class UserDefinedForm_SubmittedFormEmail extends Email { */ public function setReplyTo($email) { $this->customHeaders['Reply-To'] = $email; - } + } } diff --git a/code/model/formfields/EditableFileField.php b/code/model/formfields/EditableFileField.php index 4d7ee9a..15ff253 100755 --- a/code/model/formfields/EditableFileField.php +++ b/code/model/formfields/EditableFileField.php @@ -32,6 +32,11 @@ class EditableFileField extends EditableFormField { public function getFormField() { $field = FileField::create($this->Name, $this->Title); + // filter out '' since this would be a regex problem on JS end + $field->getValidator()->setAllowedExtensions( + array_filter(Config::inst()->get('File', 'allowed_extensions')) + ); + if($this->getSetting('Folder')) { $folder = Folder::get()->byId($this->getSetting('Folder')); diff --git a/code/model/formfields/EditableFormField.php b/code/model/formfields/EditableFormField.php index 79f3ea3..7e9f559 100755 --- a/code/model/formfields/EditableFormField.php +++ b/code/model/formfields/EditableFormField.php @@ -401,6 +401,16 @@ class EditableFormField extends DataObject { public function getFieldConfiguration() { $extraClass = ($this->getSetting('ExtraClass')) ? $this->getSetting('ExtraClass') : ''; + $mergeFieldName = new LiteralField('MergeFieldName', _t('EditableFormField.MERGEFIELDNAME', + '
' . + '' . + '
' . + '

$' . $this->Name . '

' . + 'Use this to display the field\'s value in email content.' . + '
' . + '
' + )); + if (is_array(self::$allowed_css) && !empty(self::$allowed_css)) { foreach(self::$allowed_css as $k => $v) { if (!is_array($v)) $cssList[$k]=$v; @@ -429,6 +439,7 @@ class EditableFormField extends DataObject { ); $fields = FieldList::create( + $mergeFieldName, $ec, $right ); diff --git a/composer.json b/composer.json index a71b7a6..8e7ee18 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ }], "require": { "silverstripe/framework": ">=3.1.0", - "silverstripe/cms": ">=3.1.0" + "silverstripe/cms": ">=3.1.0", + "silverstripe-australia/gridfieldextensions": "1.1.0" }, "suggest": { "colymba/gridfield-bulk-editing-tools": "Allows for bulk management of form submissions" diff --git a/css/FieldEditor.css b/css/FieldEditor.css index 5d39220..7b4cced 100755 --- a/css/FieldEditor.css +++ b/css/FieldEditor.css @@ -51,6 +51,19 @@ li.class-UserDefinedForm > a .jstree-pageicon { background-position: 0 -64px; } } */ +/* Email Recipient Form +---------------------------------------- */ +.EmailRecipientForm .fieldgroup .fieldgroup-field { + padding-top: 0; +} + +.EmailRecipientForm .fieldgroup .fieldgroup-field.last { + padding-bottom: 0; +} + +.EmailRecipientForm .ss-ui-button { + margin-bottom: 4px; +} /* Field Listing diff --git a/docs/en/_images/add-email-recipient.png b/docs/en/_images/add-email-recipient.png new file mode 100644 index 0000000..9518ccb Binary files /dev/null and b/docs/en/_images/add-email-recipient.png differ diff --git a/docs/en/_images/mergefield.png b/docs/en/_images/mergefield.png new file mode 100644 index 0000000..60a7aae Binary files /dev/null and b/docs/en/_images/mergefield.png differ diff --git a/docs/en/_images/mergefieldcontent.png b/docs/en/_images/mergefieldcontent.png new file mode 100644 index 0000000..ea6b256 Binary files /dev/null and b/docs/en/_images/mergefieldcontent.png differ diff --git a/docs/en/_images/userforms-config.png b/docs/en/_images/userforms-config.png new file mode 100644 index 0000000..57a6886 Binary files /dev/null and b/docs/en/_images/userforms-config.png differ diff --git a/docs/en/_images/viewing-submissions.png b/docs/en/_images/viewing-submissions.png new file mode 100644 index 0000000..9ecf395 Binary files /dev/null and b/docs/en/_images/viewing-submissions.png differ diff --git a/docs/en/index.md b/docs/en/index.md index 49ccb27..0a7aab6 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -16,6 +16,7 @@ and without getting involved in any PHP code. * Construct a form using all major form fields (text, email, dropdown, radio, checkbox..) * Ability to extend userforms from other modules to provide extra fields. * Ability to email multiple people the form submission +* Custom email templates * View submitted submissions and export them to CSV * Define custom error messages and validation settings * Optionally display and hide fields using javascript based on users input diff --git a/docs/en/installation.md b/docs/en/installation.md index 7c93fc5..d810c40 100644 --- a/docs/en/installation.md +++ b/docs/en/installation.md @@ -18,3 +18,14 @@ Installation can be done either by composer or by manually downloading a release After installation, make sure you rebuild your database through `dev/build`. You should see a new PageType in the CMS 'User Defined Form'. This has a new 'Form' tab which has your form builder. + +### Custom email templates + +If you want to use custom email templates set the following config option. + +```` +UserDefinedForm: + email_template_directory: your/template/path/ +```` + +Any SilverStripe templates placed in your `email_template_directory` directory will be available for use with submission emails. diff --git a/docs/en/user-documentation.md b/docs/en/user-documentation.md index 4344bd2..8df0275 100644 --- a/docs/en/user-documentation.md +++ b/docs/en/user-documentation.md @@ -4,7 +4,8 @@ In this section: * Learn how to create and edit forms * Learn how to add fields to a form -* Learn how to view submissions and reply to them automatically +* Learn how to view submissions +* Learn how to set up automated emails upon form completion ## Before we begin: @@ -29,8 +30,6 @@ You will notice that a new page has been created, with the name of "New UserDefi Simply click on the new page in the content page to bring it up in the editing pane.
-### Notes: - Don't worry if you create your page in the "wrong" place. Pages can be moved and re-ordered easily, and we will cover that under "Managing Your Site."
@@ -173,4 +172,106 @@ to determine the size and the number of rows in a text field. **Or perhaps you'd like to add informational content to your form?** -* Use [HTML Block](#html-block), with the appropriate level [Heading](#heading). \ No newline at end of file +* Use [HTML Block](#html-block), with the appropriate level [Heading](#heading). + + +## Viewing form submissions + +To view form submissions navigate to the 'Submissions' tab. You can click any of the listed submissions to view the content of each submission. + +![Viewing submissions](_images/viewing-submissions.png) + +## Setting up automated emails + +It is possible to set up automated emails upon each form submission, to do this navigate to the "Recipients" tab and click "Add Email Recipient". + +![Add email recipient](_images/add-email-recipient.png) + +You will be prompted with a form where you can fill in the details of the email. + +### Using form fields in submission emails + +Each form field has a unique merge field located under the field's options. + +![Merge field option](_images/mergefield.png) + +Simply insert the merge field into the email content, and the field's value will be displayed, when the email is sent. + +![Merge field in content](_images/mergefieldcontent.png) + +### Email details + +#### Email Subject + +The subject of the email, you can either type a custom subject here or select a field from the form to use as the email subject. + +#### Send email to + +This is the recipient's address where the email will be sent. + +#### Send email from + +This shows where the email was sent from, and will most likely need to be an email address on the same domain as your site. For example If your website is yoursite.com, the email address for this field should be something@yoursite.com. + +#### Email for reply to + +This will be the address which the email recipient will be able to 'reply' to. + +#### Email content + +In this field you can add a custom message to add to the email + +#### Hide form data from email? + +You can check this if you do not wish for the email recipient to see the form submission's data in the email. + +#### Send email as plain text? + +You can check this if you want to remove all of the HTML from the email, this means the email +will have no custom styling and the recipient will only see the plain text. + +If `Send email as plain text?` is unselected, several additional options for HTML editing are displayed. + +If sending as HTML, there is the option to preview the HTML that is sent in the editor. Additionally, a HTML +template can be selected to provide a standard formatted email to contain the editable HTML content. + +The list of available templates can be controlled by specifying the folder for these template files in yaml config. + + + :::yaml + UserDefinedForm: + email_template_directory: mysite/templates/useremails/ + + +### Custom Rules + +In this section you can determine whether to send the email to the recipient based on the data in the form submission. + +#### Send conditions + +This decides whether to send the email based on two options + +1. *All* conditions are true (Every single custom rule must be met in order to send the email) +2. *Any* conditions are true (At least one of the custom rules must be met in order to send the email) + +#### Adding a custom rule + +* Click 'Add' to add a custom sending rule. +* Select the field which you want the custom rule to apply to +* Select the condition the field must follow +* enter for the condition (the 'is blank' and 'is not blank' conditions do not require any text) + + +## Configuration + +The 'Configuration' tab has a number of options used for customising your form's behaviour and appearance. + +![Configuration](_images/userforms-config.png) + +### Validation messages + +Validation messages are displayed below invalid fields by default. By checking the 'Display error messages above the form' +option, an additional set of validation messages are displayed, at the top of the form. + +When a user submits an invalid form, they are directed to the top of the form, where they can review the messages. +Each message links to it's corresponding field so users can easily make the required changes. diff --git a/javascript/Recipient.js b/javascript/Recipient.js new file mode 100644 index 0000000..1cc88d0 --- /dev/null +++ b/javascript/Recipient.js @@ -0,0 +1,44 @@ +/** + * Email recipient behaviour. + */ + +(function ($) { + $(document).ready(function () { + + var recipient = { + // Some fields are only visible when HTML email are being sent. + updateFormatSpecificFields: function () { + var sendPlainChecked = $('#SendPlain').find('input[type="checkbox"]').is(':checked'); + + // Hide the preview link when 'SendPlain' is selected. + $('#EmailPreview')[sendPlainChecked ? 'hide' : 'show'](); + + // Hide the template selector when 'SendPlain' is selected. + $('#EmailTemplate')[sendPlainChecked ? 'hide' : 'show'](); + + // Hide the HTML editor when 'SendPlain' is selected. + $('#EmailBodyHtml')[sendPlainChecked ? 'hide' : 'show'](); + + // Show the body teaxtarea when 'SendPlain' is selected. + $('#EmailBody')[sendPlainChecked ? 'show' : 'hide'](); + } + }; + + $.entwine('udf.recipient', function ($) { + $('#Form_ItemEditForm').entwine({ + onmatch: function () { + recipient.updateFormatSpecificFields(); + }, + onunmatch: function () { + this._super(); + } + }); + + $('#SendPlain').entwine({ + onchange: function () { + recipient.updateFormatSpecificFields(); + } + }); + }); + }); +}(jQuery)); diff --git a/javascript/lang/en.js b/javascript/lang/en.js index f623d98..c2ebfa1 100644 --- a/javascript/lang/en.js +++ b/javascript/lang/en.js @@ -15,6 +15,7 @@ if(typeof(ss) == 'undefined' || typeof(ss.i18n) == 'undefined') { "UserForms.ERROR_CREATING_OPTION": "Error creating option", "UserForms.REMOVED_OPTION": "Removed option", "UserForms.ADDING_RULE": "Adding rule", + "UserForms.ERROR_CONTAINER_HEADER": "Please correct the following errors and try again:", "GRIDFIELD.ERRORINTRANSACTION": "An error occured while fetching data from the server\n Please try again later." }); } \ No newline at end of file diff --git a/javascript/lang/src/en.js b/javascript/lang/src/en.js index ee5ad4b..4e2b6bb 100644 --- a/javascript/lang/src/en.js +++ b/javascript/lang/src/en.js @@ -10,5 +10,6 @@ "UserForms.ERROR_CREATING_OPTION": "Error creating option", "UserForms.REMOVED_OPTION": "Removed option", "UserForms.ADDING_RULE": "Adding rule", + "UserForms.ERROR_CONTAINER_HEADER": "Please correct the following errors and try again:", "GRIDFIELD.ERRORINTRANSACTION": "An error occured while fetching data from the server\n Please try again later." } \ No newline at end of file diff --git a/templates/ValidationScript.ss b/templates/ValidationScript.ss index f9473fe..2e02cb8 100644 --- a/templates/ValidationScript.ss +++ b/templates/ValidationScript.ss @@ -1,6 +1,10 @@ (function($) { $(document).ready(function() { - var messages = {<% loop $Fields %><% if $ErrorMessage && not $SetsOwnError %><% if ClassName == EditableCheckboxGroupField %> + var formId = "{$Form.FormName.JS}", + errorContainerId = "{$ErrorContainerID.JS}", + errorContainer = $('

'); + + var messages = {<% loop $Fields %><% if $ErrorMessage && not $SetsOwnError %><% if $ClassName == 'EditableCheckboxGroupField' %> '{$Name.JS}[]': '{$ErrorMessage.JS}'<% if not Last %>,<% end_if %><% else %> '{$Name.JS}': '{$ErrorMessage.JS}'<% if not Last %>,<% end_if %><% end_if %><% end_if %><% end_loop %> }; @@ -13,7 +17,7 @@ } }); - $("#Form_Form").validate({ + $("#" + formId).validate({ ignore: ':hidden', errorClass: "required", errorElement: "span", @@ -25,6 +29,13 @@ } else { error.insertAfter(element); } + + <% if $DisplayErrorMessagesAtTop %> + applyTopErrorMessage(element, error.html()); + <% end_if %> + }, + success: function (error) { + error.remove(); }, messages: messages, rules: { @@ -35,20 +46,112 @@ '{$Name.JS}': {$ValidationJSON.RAW}, <% end_if %><% end_if %> <% end_loop %> - }, + } + + /* + * Conditional options. + * Using leading commas so we don't get a trailing comma on + * the last option. Trailing commas can break IE. + */ <% if $EnableLiveValidation %> // Enable live validation - onfocusout : function(element) { this.element(element); } + ,onfocusout: function (element) { this.element(element); } + <% end_if %> + + <% if $DisplayErrorMessagesAtTop %> + ,invalidHandler: function (event, validator) { + var errorList = $('#' + errorContainerId + ' ul'); + + // Update the error list with errors from the validator. + // We do this because top messages are not part of the regular + // error message life cycle, which jquery.validate handles for us. + errorList.empty(); + + $.each(validator.errorList, function () { + applyTopErrorMessage($(this.element), this.message); + }); + } + ,onfocusout: false <% end_if %> }); + <% if $HideFieldLabels %> // Hide field labels (use HTML5 placeholder instead) - $("#Form_Form label.left").each(function() { + $("#" + formId + "label.left").each(function() { $("#"+$(this).attr("for")) .attr("placeholder", $(this).text()); $(this).remove(); }); Placeholders.init(); <% end_if %> + + <% if $DisplayErrorMessagesAtTop %> + /** + * @applyTopErrorMessage + * @param {jQuery} input - The jQuery input object which contains the field to validate + * @param {string} message - The error message to display (html escaped) + * @desc Update an error message (displayed at the top of the form). + */ + function applyTopErrorMessage(input, message) { + var inputID = input.attr('id'), + anchor = '#' + inputID, + elementID = inputID + '-top-error', + errorContainer = $('#' + errorContainerId), + messageElement = $('#' + elementID), + describedBy = input.attr('aria-describedby'); + + // The 'message' param will be an empty string if the field is valid. + if (!message) { + // Style issues as fixed if they already exist + messageElement.addClass('fixed'); + return; + } + + messageElement.removeClass('fixed'); + errorContainer.show(); + + if (messageElement.length === 1) { + // Update the existing error message. + messageElement.show().find('a').html(message); + } else { + // Generate better link to field + input.closest('.field[id]').each(function(){ + anchor = '#' + $(this).attr('id'); + }); + + // Add a new error message + messageElement = $('
  • '); + messageElement + .attr('id', elementID) + .find('a') + .attr('href', location.pathname + location.search + anchor) + .html(message); + errorContainer + .find('ul') + .append(messageElement); + + // link back to original input via aria + // Respect existing non-error aria-describedby + if ( !describedBy ) { + describedBy = elementID; + } else if ( !describedBy.match( new RegExp( "\\b" + elementID + "\\b" ) ) ) { + // Add to end of list if not already present + describedBy += " " + elementID; + } + input.attr( "aria-describedby", describedBy ); + } + } + + // Build container + errorContainer + .hide() + .attr('id', errorContainerId) + .find('h4') + .text(ss.i18n._t( + "UserForms.ERROR_CONTAINER_HEADER", + "Please correct the following errors and try again:" + )); + $('#' + formId).prepend(errorContainer); + <% end_if %> }); })(jQuery); diff --git a/tests/EditableFormFieldTest.php b/tests/EditableFormFieldTest.php index 6ff7004..6fdfe1c 100644 --- a/tests/EditableFormFieldTest.php +++ b/tests/EditableFormFieldTest.php @@ -327,6 +327,14 @@ class EditableFormFieldTest extends FunctionalTest { $this->assertNotNull($validationField); } + public function testFileField() { + $fileField = $this->objFromFixture('EditableFileField', 'file-field'); + $formField = $fileField->getFormField(); + + $this->assertContains('jpg', $formField->getValidator()->getAllowedExtensions()); + $this->assertNotContains('notallowedextension', $formField->getValidator()->getAllowedExtensions()); + } + } /** diff --git a/tests/EditableFormFieldTest.yml b/tests/EditableFormFieldTest.yml index 5b82e25..19a75c5 100644 --- a/tests/EditableFormFieldTest.yml +++ b/tests/EditableFormFieldTest.yml @@ -1,135 +1,141 @@ EditableOption: - option-1: - Name: Option1 - Title: Option 1 + option-1: + Name: Option1 + Title: Option 1 + + option-2: + Name: Option2 + Title: Option 2 + + department-1: + Name: dept1 + Title: sales@example.com + + department-2: + Name: dept2 + Title: accounts@example.com + + option-3: + Name: Option3 + Title: Option 3 + + option-4: + Name: Option4 + Title: Option 4 + + option-5: + Name: Option5 + Title: Option 5 + + option-6: + Name: Option6 + Title: Option 6 - option-2: - Name: Option2 - Title: Option 2 - - department-1: - Name: dept1 - Title: sales@example.com - - department-2: - Name: dept2 - Title: accounts@example.com - - option-3: - Name: Option3 - Title: Option 3 - - option-4: - Name: Option4 - Title: Option 4 - - option-5: - Name: Option5 - Title: Option 5 - - option-6: - Name: Option6 - Title: Option 6 - UserDefinedForm_EmailRecipient: - recipient-1: - EmailAddress: test@example.com - EmailSubject: Email Subject - EmailFrom: no-reply@example.com - - no-html: - EmailAddress: nohtml@example.com - EmailSubject: Email Subject - EmailFrom: no-reply@example.com - SendPlain: true - - no-data: - EmailAddress: nodata@example.com - EmailSubject: Email Subject - EmailFrom: no-reply@example.com - HideFormData: true + recipient-1: + EmailAddress: test@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + no-html: + EmailAddress: nohtml@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + SendPlain: true + + no-data: + EmailAddress: nodata@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + HideFormData: true + EditableTextField: - basic-text: - Name: basic-text-name - Title: Basic Text Field + basic-text: + Name: basic-text-name + Title: Basic Text Field - basic-text-2: - Name: basic-text-name - Title: Basic Text Field - - required-text: - Name: required-text-field - Title: Required Text Field - CustomErrorMessage: Custom Error Message - Required: true - + basic-text-2: + Name: basic-text-name + Title: Basic Text Field + + required-text: + Name: required-text-field + Title: Required Text Field + CustomErrorMessage: Custom Error Message + Required: true + EditableDropdown: - basic-dropdown: - Name: basic-dropdown - Title: Basic Dropdown Field - Options: =>EditableOption.option-1, =>EditableOption.option-2 - - department-dropdown: - Name: department - Title: Department - Options: =>EditableOption.department-1, =>EditableOption.department-2 - + basic-dropdown: + Name: basic-dropdown + Title: Basic Dropdown Field + Options: =>EditableOption.option-1, =>EditableOption.option-2 + + department-dropdown: + Name: department + Title: Department + Options: =>EditableOption.department-1, =>EditableOption.department-2 + EditableCheckbox: - checkbox-1: - Name: checkbox-1 - Title: Checkbox 1 - - checkbox-2: - Name: checkbox-1 - Title: Checkbox 1 - + checkbox-1: + Name: checkbox-1 + Title: Checkbox 1 + + checkbox-2: + Name: checkbox-1 + Title: Checkbox 1 + EditableCheckboxGroupField: - checkbox-group: - Name: check-box-group - Title: Check box group - Options: =>EditableOption.option-3, =>EditableOption.option-4 - -EditableEmailField: - email-field: - Name: email-field - Title: Email + checkbox-group: + Name: check-box-group + Title: Check box group + Options: =>EditableOption.option-3, =>EditableOption.option-4 +EditableEmailField: + email-field: + Name: email-field + Title: Email + EditableRadioField: - radio-field: - Name: radio-option - Title: Radio Option - Options: =>EditableOption.option-5, =>EditableOption.option-6 + radio-field: + Name: radio-option + Title: Radio Option + Options: =>EditableOption.option-5, =>EditableOption.option-6 + + +EditableFileField: + file-field: + Name: file-uploader + Title: Set file ExtendedEditableFormField: - extended-field: - Name: extended-field - Title: Extended Field - TestExtraField: Extra Field - TestValidationField: Extra Validation Field + extended-field: + Name: extended-field + Title: Extended Field + TestExtraField: Extra Field + TestValidationField: Extra Validation Field UserDefinedForm: - basic-form-page: - Title: User Defined Form - Fields: =>EditableTextField.basic-text - EmailRecipients: =>UserDefinedForm_EmailRecipient.recipient-1, =>UserDefinedForm_EmailRecipient.no-html, =>UserDefinedForm_EmailRecipient.no-data - - form-with-reset-and-custom-action: - Title: Form with Reset Action - SubmitButtonText: Custom Button - ShowClearButton: true - - validation-form: - Title: Validation Form - Fields: =>EditableTextField.required-text - - custom-rules-form: - Title: Custom Rules Form - Fields: =>EditableCheckbox.checkbox-2, =>EditableTextField.basic-text-2 - empty-form: - Title: Empty Form - - + basic-form-page: + Title: User Defined Form + Fields: =>EditableTextField.basic-text + EmailRecipients: =>UserDefinedForm_EmailRecipient.recipient-1, =>UserDefinedForm_EmailRecipient.no-html, =>UserDefinedForm_EmailRecipient.no-data + + form-with-reset-and-custom-action: + Title: Form with Reset Action + SubmitButtonText: Custom Button + ShowClearButton: true + + validation-form: + Title: Validation Form + Fields: =>EditableTextField.required-text + + custom-rules-form: + Title: Custom Rules Form + Fields: =>EditableCheckbox.checkbox-2, =>EditableTextField.basic-text-2 + empty-form: + Title: Empty Form + + diff --git a/tests/UserDefinedFormTest.php b/tests/UserDefinedFormTest.php index 3287a3d..082f681 100644 --- a/tests/UserDefinedFormTest.php +++ b/tests/UserDefinedFormTest.php @@ -13,8 +13,8 @@ class UserDefinedFormTest extends FunctionalTest { $this->markTestSkipped( 'UserDefinedForm::rollback() has not been implemented completely' ); - - // @todo + + // @todo $this->logInWithPermission('ADMIN'); $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); @@ -22,7 +22,7 @@ class UserDefinedFormTest extends FunctionalTest { $form->write(); $form->doPublish(); $origVersion = $form->Version; - + $form->SubmitButtonText = 'Updated Button Text'; $form->write(); $form->doPublish(); @@ -32,15 +32,15 @@ class UserDefinedFormTest extends FunctionalTest { $this->assertEquals($updated->SubmitButtonText, 'Updated Button Text'); $form->doRollbackTo($origVersion); - + $orignal = Versioned::get_one_by_stage("UserDefinedForm", "Stage", "\"UserDefinedForm\".\"ID\" = $form->ID"); $this->assertEquals($orignal->SubmitButtonText, 'Button Text'); } - + function testGetCMSFields() { $this->logInWithPermission('ADMIN'); $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); - + $fields = $form->getCMSFields(); $this->assertTrue($fields->dataFieldByName('Fields') !== null); @@ -51,40 +51,85 @@ class UserDefinedFormTest extends FunctionalTest { function testEmailRecipientPopup() { $this->logInWithPermission('ADMIN'); - + $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); - + $popup = new UserDefinedForm_EmailRecipient(); - + $popup->FormID = $form->ID; + $fields = $popup->getCMSFields(); - + $this->assertTrue($fields->dataFieldByName('EmailSubject') !== null); $this->assertTrue($fields->dataFieldByName('EmailFrom') !== null); $this->assertTrue($fields->dataFieldByName('EmailAddress') !== null); $this->assertTrue($fields->dataFieldByName('HideFormData') !== null); $this->assertTrue($fields->dataFieldByName('SendPlain') !== null); $this->assertTrue($fields->dataFieldByName('EmailBody') !== null); - + // add an email field, it should now add a or from X address picker $email = $this->objFromFixture('EditableEmailField','email-field'); $form->Fields()->add($email); - $popup->FormID = $form->ID; $popup->write(); $fields = $popup->getCMSFields(); - $this->assertThat($fields->fieldByName('SendEmailToFieldID'), $this->isInstanceOf('DropdownField')); - + $this->assertThat($fields->dataFieldByName('SendEmailToFieldID'), $this->isInstanceOf('DropdownField')); + // if the front end has checkboxs or dropdown they can select from that can also be used to send things $dropdown = $this->objFromFixture('EditableDropdown', 'department-dropdown'); $form->Fields()->add($dropdown); - + $fields = $popup->getCMSFields(); $this->assertTrue($fields->dataFieldByName('SendEmailToFieldID') !== null); - + $popup->delete(); } - + + function testGetEmailBodyContent() { + $recipient = new UserDefinedForm_EmailRecipient(); + + $emailBody = 'not html'; + $emailBodyHtml = '

    html

    '; + + $recipient->EmailBody = $emailBody; + $recipient->EmailBodyHtml = $emailBodyHtml; + $recipient->write(); + + $this->assertEquals($recipient->SendPlain, 0); + $this->assertEquals($recipient->getEmailBodyContent(), $emailBodyHtml); + + $recipient->SendPlain = 1; + $recipient->write(); + + $this->assertEquals($recipient->getEmailBodyContent(), $emailBody); + + $recipient->delete(); + } + + function testGetEmailTemplateDropdownValues() { + $recipient = new UserDefinedForm_EmailRecipient(); + + $defaultValues = array('SubmittedFormEmail' => 'SubmittedFormEmail'); + + $this->assertEquals($recipient->getEmailTemplateDropdownValues(), $defaultValues); + } + + function testEmailTemplateExists() { + $recipient = new UserDefinedForm_EmailRecipient(); + + // Set the default template + $recipient->EmailTemplate = current(array_keys($recipient->getEmailTemplateDropdownValues())); + $recipient->write(); + + // The default template exists + $this->assertTrue($recipient->emailTemplateExists()); + + // A made up template doesn't exists + $this->assertFalse($recipient->emailTemplateExists('MyTemplateThatsNotThere')); + + $recipient->delete(); + } + function testCanEditAndDeleteRecipient() { $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); @@ -93,95 +138,95 @@ class UserDefinedFormTest extends FunctionalTest { $this->assertTrue($recipient->canEdit()); $this->assertTrue($recipient->canDelete()); } - + $member = Member::currentUser(); $member->logOut(); - + $this->logInWithPermission('SITETREE_VIEW_ALL'); foreach($form->EmailRecipients() as $recipient) { $this->assertFalse($recipient->canEdit()); $this->assertFalse($recipient->canDelete()); } } - + function testPublishing() { $this->logInWithPermission('ADMIN'); - + $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); $form->write(); - + $form->doPublish(); - + $live = Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID"); - + $this->assertNotNull($live); $this->assertEquals($live->Fields()->Count(), 1); - + $dropdown = $this->objFromFixture('EditableDropdown', 'basic-dropdown'); $form->Fields()->add($dropdown); - + $stage = Versioned::get_one_by_stage("UserDefinedForm", "Stage", "\"UserDefinedForm\".\"ID\" = $form->ID"); $this->assertEquals($stage->Fields()->Count(), 2); - + // should not have published the dropdown $liveDropdown = Versioned::get_one_by_stage("EditableFormField", "Live", "\"EditableFormField_Live\".\"ID\" = $dropdown->ID"); $this->assertNull($liveDropdown); - + // when publishing it should have added it $form->doPublish(); - + $live = Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID"); $this->assertEquals($live->Fields()->Count(), 2); - - // edit the title + + // edit the title $text = $form->Fields()->First(); - + $text->Title = 'Edited title'; $text->write(); - + $liveText = Versioned::get_one_by_stage("EditableFormField", "Live", "\"EditableFormField_Live\".\"ID\" = $text->ID"); $this->assertFalse($liveText->Title == $text->Title); - + $form->doPublish(); - + $liveText = Versioned::get_one_by_stage("EditableFormField", "Live", "\"EditableFormField_Live\".\"ID\" = $text->ID"); $this->assertTrue($liveText->Title == $text->Title); } - + function testUnpublishing() { $this->logInWithPermission('ADMIN'); $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); $form->write(); - + $form->doPublish(); // assert that it exists and has a field $live = Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID"); - + $this->assertTrue(isset($live)); $this->assertEquals(DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value(), 1); - + // unpublish $form->doUnpublish(); - + $this->assertNull(Versioned::get_one_by_stage("UserDefinedForm", "Live", "\"UserDefinedForm_Live\".\"ID\" = $form->ID")); - $this->assertEquals(DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value(), 0); - + $this->assertEquals(DB::query("SELECT COUNT(*) FROM \"EditableFormField_Live\"")->value(), 0); + } - + function testDoRevertToLive() { $this->logInWithPermission('ADMIN'); $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); $field = $form->Fields()->First(); - + $field->Title = 'Title'; $field->write(); - + $form->doPublish(); - + $field->Title = 'Edited title'; $field->write(); - + // check that the published version is not updated $live = Versioned::get_one_by_stage("EditableFormField", "Live", "\"EditableFormField_Live\".\"ID\" = $field->ID"); $this->assertEquals('Title', $live->Title); @@ -189,21 +234,21 @@ class UserDefinedFormTest extends FunctionalTest { // revert back to the live data $form->doRevertToLive(); $form->flushCache(); - + $check = Versioned::get_one_by_stage("EditableFormField", "Stage", "\"EditableFormField\".\"ID\" = $field->ID"); - + $this->assertEquals('Title', $check->Title); } - + function testDuplicatingForm() { $this->logInWithPermission('ADMIN'); $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); - + $duplicate = $form->duplicate(); - + $this->assertEquals($form->Fields()->Count(), $duplicate->Fields()->Count()); $this->assertEquals($form->EmailRecipients()->Count(), $form->EmailRecipients()->Count()); - + // can't compare object since the dates/ids change $this->assertEquals($form->Fields()->First()->Title, $duplicate->Fields()->First()->Title); } @@ -211,7 +256,7 @@ class UserDefinedFormTest extends FunctionalTest { function testFormOptions() { $this->logInWithPermission('ADMIN'); $form = $this->objFromFixture('UserDefinedForm', 'basic-form-page'); - + $fields = $form->getFormOptions(); $submit = $fields->fieldByName('SubmitButtonText'); $reset = $fields->fieldByName('ShowClearButton'); @@ -219,4 +264,97 @@ class UserDefinedFormTest extends FunctionalTest { $this->assertEquals($submit->Title(), 'Text on submit button:'); $this->assertEquals($reset->Title(), 'Show Clear Form Button'); } + + public function testEmailRecipientFilters() { + $form = $this->objFromFixture('UserDefinedForm', 'filtered-form-page'); + + // Check unfiltered recipients + $result0 = $form + ->EmailRecipients() + ->sort('EmailAddress') + ->column('EmailAddress'); + $this->assertEquals( + array( + 'filtered1@example.com', + 'filtered2@example.com', + 'unfiltered@example.com' + ), + $result0 + ); + + // check filters based on given data + $result1 = $form->FilteredEmailRecipients( + array( + 'your-name' => 'Value', + 'address' => '', + 'street' => 'Anything', + 'city' => 'Matches Not Equals', + 'colours' => array('Red') // matches 2 + ), null + ) + ->sort('EmailAddress') + ->column('EmailAddress'); + $this->assertEquals( + array( + 'filtered2@example.com', + 'unfiltered@example.com' + ), + $result1 + ); + + // Check all positive matches + $result2 = $form->FilteredEmailRecipients( + array( + 'your-name' => '', + 'address' => 'Anything', + 'street' => 'Matches Equals', + 'city' => 'Anything', + 'colours' => array('Red', 'Blue') // matches 2 + ), null + ) + ->sort('EmailAddress') + ->column('EmailAddress'); + $this->assertEquals( + array( + 'filtered1@example.com', + 'filtered2@example.com', + 'unfiltered@example.com' + ), + $result2 + ); + + + $result3 = $form->FilteredEmailRecipients( + array( + 'your-name' => 'Should be blank but is not', + 'address' => 'Anything', + 'street' => 'Matches Equals', + 'city' => 'Anything', + 'colours' => array('Blue') + ), null + )->column('EmailAddress'); + $this->assertEquals( + array( + 'unfiltered@example.com' + ), + $result3 + ); + + + $result4 = $form->FilteredEmailRecipients( + array( + 'your-name' => '', + 'address' => 'Anything', + 'street' => 'Wrong value for this field', + 'city' => '', + 'colours' => array('Blue', 'Green') + ), null + )->column('EmailAddress'); + $this->assertEquals( + array( + 'unfiltered@example.com' + ), + $result4 + ); + } } \ No newline at end of file diff --git a/tests/UserDefinedFormTest.yml b/tests/UserDefinedFormTest.yml index 7b95324..ef657e0 100644 --- a/tests/UserDefinedFormTest.yml +++ b/tests/UserDefinedFormTest.yml @@ -1,127 +1,212 @@ EditableOption: - option-1: - Name: Option1 - Title: Option 1 - - option-2: - Name: Option2 - Title: Option 2 + option-1: + Name: Option1 + Title: Option 1 + + option-2: + Name: Option2 + Title: Option 2 - department-1: - Name: dept1 - Title: sales@example.com + department-1: + Name: dept1 + Title: sales@example.com - department-2: - Name: dept2 - Title: accounts@example.com + department-2: + Name: dept2 + Title: accounts@example.com - option-3: - Name: Option3 - Title: Option 3 + option-3: + Name: Option3 + Title: Option 3 - option-4: - Name: Option4 - Title: Option 4 + option-4: + Name: Option4 + Title: Option 4 - option-5: - Name: Option5 - Title: Option 5 + option-5: + Name: Option5 + Title: Option 5 - option-6: - Name: Option6 - Title: Option 6 - -UserDefinedForm_EmailRecipient: - recipient-1: - EmailAddress: test@example.com - EmailSubject: Email Subject - EmailFrom: no-reply@example.com - - no-html: - EmailAddress: nohtml@example.com - EmailSubject: Email Subject - EmailFrom: no-reply@example.com - SendPlain: true - - no-data: - EmailAddress: nodata@example.com - EmailSubject: Email Subject - EmailFrom: no-reply@example.com - HideFormData: true - + option-6: + Name: Option6 + Title: Option 6 + + option-7: + Name: Option7 + Title: Red + + option-8: + Name: Option8 + Title: Blue + + option-9: + Name: Option9 + Title: Green + EditableTextField: - basic-text: - Name: basic-text-name - Title: Basic Text Field + basic-text: + Name: basic-text-name + Title: Basic Text Field - basic-text-2: - Name: basic-text-name - Title: Basic Text Field - - required-text: - Name: required-text-field - Title: Required Text Field - CustomErrorMessage: Custom Error Message - Required: true - + basic-text-2: + Name: basic-text-name + Title: Basic Text Field + + your-name-field: + Name: your-name + Title: Name + + address-field: + Name: address + Title: Address + + street-field: + Name: street + Title: Street + + city-field: + Name: city + Title: City + + required-text: + Name: required-text-field + Title: Required Text Field + CustomErrorMessage: Custom Error Message + Required: true + EditableDropdown: - basic-dropdown: - Name: basic-dropdown - Title: Basic Dropdown Field - Options: =>EditableOption.option-1, =>EditableOption.option-2 - - department-dropdown: - Name: department - Title: Department - Options: =>EditableOption.department-1, =>EditableOption.department-2 - + basic-dropdown: + Name: basic-dropdown + Title: Basic Dropdown Field + Options: =>EditableOption.option-1, =>EditableOption.option-2 + + department-dropdown: + Name: department + Title: Department + Options: =>EditableOption.department-1, =>EditableOption.department-2 + EditableCheckbox: - checkbox-1: - Name: checkbox-1 - Title: Checkbox 1 - - checkbox-2: - Name: checkbox-1 - Title: Checkbox 1 - + checkbox-1: + Name: checkbox-1 + Title: Checkbox 1 + + checkbox-2: + Name: checkbox-1 + Title: Checkbox 1 + EditableCheckboxGroupField: - checkbox-group: - Name: check-box-group - Title: Check box group - Options: =>EditableOption.option-3, =>EditableOption.option-4 - + checkbox-group: + Name: check-box-group + Title: Check box group + Options: =>EditableOption.option-3, =>EditableOption.option-4 + + colour-checkbox-group: + Name: colours + Title: 'Select Colours' + Options: =>EditableOption.option-7, =>EditableOption.option-8, =>EditableOption.option-9 + EditableEmailField: - email-field: - Name: email-field - Title: Email - + email-field: + Name: email-field + Title: Email EditableRadioField: - radio-field: - Name: radio-option - Title: Radio Option - Options: =>EditableOption.option-5, =>EditableOption.option-6 + radio-field: + Name: radio-option + Title: Radio Option + Options: =>EditableOption.option-5, =>EditableOption.option-6 +UserDefinedForm_EmailRecipientCondition: +# filtered recipient 1 + blank-rule: + ConditionOption: IsBlank + ConditionField: =>EditableTextField.your-name-field + + not-blank-rule: + ConditionOption: IsNotBlank + ConditionField: =>EditableTextField.address-field + + equals-rule: + ConditionOption: Equals + ConditionField: =>EditableTextField.street-field + ConditionValue: 'Matches Equals' + + not-equals-rule: + ConditionOption: NotEquals + ConditionField: =>EditableTextField.city-field + ConditionValue: 'Matches Not Equals' + +# filtered recipient 2 + group-equals-rule: + ConditionOption: Equals + ConditionField: =>EditableCheckboxGroupField.colour-checkbox-group + ConditionValue: Red + + group-not-equals-rule: + ConditionOption: NotEquals + ConditionField: =>EditableCheckboxGroupField.colour-checkbox-group + ConditionValue: Blue + + +UserDefinedForm_EmailRecipient: + recipient-1: + EmailAddress: test@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + + no-html: + EmailAddress: nohtml@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + SendPlain: true + + no-data: + EmailAddress: nodata@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + HideFormData: true + + unfiltered-recipient-1: + EmailAddress: unfiltered@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + + filtered-recipient-1: + EmailAddress: filtered1@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + CustomRules: =>UserDefinedForm_EmailRecipientCondition.blank-rule, =>UserDefinedForm_EmailRecipientCondition.not-blank-rule, =>UserDefinedForm_EmailRecipientCondition.equals-rule, =>UserDefinedForm_EmailRecipientCondition.not-equals-rule + CustomRulesCondition: 'And' + + filtered-recipient-2: + EmailAddress: filtered2@example.com + EmailSubject: Email Subject + EmailFrom: no-reply@example.com + CustomRules: =>UserDefinedForm_EmailRecipientCondition.group-equals-rule, =>UserDefinedForm_EmailRecipientCondition.group-not-equals-rule + CustomRulesCondition: 'Or' UserDefinedForm: - basic-form-page: - Title: User Defined Form - Fields: =>EditableTextField.basic-text - EmailRecipients: =>UserDefinedForm_EmailRecipient.recipient-1, =>UserDefinedForm_EmailRecipient.no-html, =>UserDefinedForm_EmailRecipient.no-data - - form-with-reset-and-custom-action: - Title: Form with Reset Action - SubmitButtonText: Custom Button - ShowClearButton: true - - validation-form: - Title: Validation Form - Fields: =>EditableTextField.required-text - - custom-rules-form: - Title: Custom Rules Form - Fields: =>EditableCheckbox.checkbox-2, =>EditableTextField.basic-text-2 - empty-form: - Title: Empty Form - - + basic-form-page: + Title: User Defined Form + Fields: =>EditableTextField.basic-text + EmailRecipients: =>UserDefinedForm_EmailRecipient.recipient-1, =>UserDefinedForm_EmailRecipient.no-html, =>UserDefinedForm_EmailRecipient.no-data + + form-with-reset-and-custom-action: + Title: Form with Reset Action + SubmitButtonText: Custom Button + ShowClearButton: true + + validation-form: + Title: Validation Form + Fields: =>EditableTextField.required-text + + custom-rules-form: + Title: Custom Rules Form + Fields: =>EditableCheckbox.checkbox-2, =>EditableTextField.basic-text-2 + empty-form: + Title: Empty Form + + filtered-form-page: + Title: 'Page with filtered recipients' + Fields: =>EditableCheckboxGroupField.checkbox-group, =>EditableTextField.your-name-field, =>EditableTextField.street-field, =>EditableTextField.city-field + EmailRecipients: =>UserDefinedForm_EmailRecipient.unfiltered-recipient-1, =>UserDefinedForm_EmailRecipient.filtered-recipient-1, =>UserDefinedForm_EmailRecipient.filtered-recipient-2 diff --git a/thirdparty/jquery-validate/additional-methods.js b/thirdparty/jquery-validate/additional-methods.js old mode 100644 new mode 100755 index eea71ee..dd328bd --- a/thirdparty/jquery-validate/additional-methods.js +++ b/thirdparty/jquery-validate/additional-methods.js @@ -1,5 +1,5 @@ /*! - * jQuery Validation Plugin v1.12.1pre + * jQuery Validation Plugin v1.13.1 * * http://jqueryvalidation.org/ * @@ -300,7 +300,7 @@ $.validator.addMethod("currency", function(value, element, param) { }, "Please specify a valid currency"); -jQuery.validator.addMethod("dateFA", function(value, element) { +$.validator.addMethod("dateFA", function(value, element) { return this.optional(element) || /^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(value); }, "Please enter a correct date"); @@ -329,11 +329,11 @@ $.validator.addMethod("dateITA", function(value, element) { adata, gg, mm, aaaa, xdata; if ( re.test(value)) { adata = value.split("/"); - gg = parseInt(adata[0],10); - mm = parseInt(adata[1],10); - aaaa = parseInt(adata[2],10); + gg = parseInt(adata[0], 10); + mm = parseInt(adata[1], 10); + aaaa = parseInt(adata[2], 10); xdata = new Date(aaaa, mm - 1, gg, 12, 0, 0, 0); - if ( ( xdata.getFullYear() === aaaa ) && ( xdata.getMonth() === mm - 1 ) && ( xdata.getDate() === gg ) ){ + if ( ( xdata.getUTCFullYear() === aaaa ) && ( xdata.getUTCMonth () === mm - 1 ) && ( xdata.getUTCDate() === gg ) ) { check = true; } else { check = false; @@ -372,7 +372,7 @@ $.validator.addMethod("iban", function(value, element) { } // remove spaces and to upper case - var iban = value.replace(/ /g,"").toUpperCase(), + var iban = value.replace(/ /g, "").toUpperCase(), ibancheckdigits = "", leadingZeroes = true, cRest = "", @@ -384,7 +384,7 @@ $.validator.addMethod("iban", function(value, element) { } // check the country code and find the country specific format - countrycode = iban.substring(0,2); + countrycode = iban.substring(0, 2); bbancountrypatterns = { "AL": "\\d{8}[\\dA-Z]{16}", "AD": "\\d{8}[\\dA-Z]{12}", @@ -468,7 +468,7 @@ $.validator.addMethod("iban", function(value, element) { } // now check the checksum, first convert to digits - ibancheck = iban.substring(4,iban.length) + iban.substring(0,4); + ibancheck = iban.substring(4, iban.length) + iban.substring(0, 4); for (i = 0; i < ibancheck.length; i++) { charAt = ibancheck.charAt(i); if (charAt !== "0") { @@ -609,7 +609,7 @@ $.validator.addMethod("pattern", function(value, element, param) { return true; } if (typeof param === "string") { - param = new RegExp(param); + param = new RegExp("^(?:" + param + ")$"); } return param.test(value); }, "Invalid format."); @@ -685,10 +685,22 @@ $.validator.addMethod("phonesUK", function(phone_number, element) { * @type Boolean * @cat Plugins/Validate/Methods */ -jQuery.validator.addMethod( "postalCodeCA", function( value, element ) { +$.validator.addMethod( "postalCodeCA", function( value, element ) { return this.optional( element ) || /^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$/.test( value ); }, "Please specify a valid postal code" ); +/* +* Valida CEPs do brasileiros: +* +* Formatos aceitos: +* 99999-999 +* 99.999-999 +* 99999999 +*/ +$.validator.addMethod("postalcodeBR", function(cep_value, element) { + return this.optional(element) || /^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test( cep_value ); +}, "Informe um CEP válido."); + /* Matches Italian postcode (CAP) */ $.validator.addMethod("postalcodeIT", function(value, element) { return this.optional(element) || /^\d{5}$/.test(value); @@ -785,6 +797,65 @@ $.validator.addMethod("skip_or_fill_minimum", function(value, element, options) return isValid; }, $.validator.format("Please either skip these fields or fill at least {0} of them.")); +/* Validates US States and/or Territories by @jdforsythe + * Can be case insensitive or require capitalization - default is case insensitive + * Can include US Territories or not - default does not + * Can include US Military postal abbreviations (AA, AE, AP) - default does not + * + * Note: "States" always includes DC (District of Colombia) + * + * Usage examples: + * + * This is the default - case insensitive, no territories, no military zones + * stateInput: { + * caseSensitive: false, + * includeTerritories: false, + * includeMilitary: false + * } + * + * Only allow capital letters, no territories, no military zones + * stateInput: { + * caseSensitive: false + * } + * + * Case insensitive, include territories but not military zones + * stateInput: { + * includeTerritories: true + * } + * + * Only allow capital letters, include territories and military zones + * stateInput: { + * caseSensitive: true, + * includeTerritories: true, + * includeMilitary: true + * } + * + * + * + */ + +jQuery.validator.addMethod("stateUS", function(value, element, options) { + var isDefault = typeof options === "undefined", + caseSensitive = ( isDefault || typeof options.caseSensitive === "undefined" ) ? false : options.caseSensitive, + includeTerritories = ( isDefault || typeof options.includeTerritories === "undefined" ) ? false : options.includeTerritories, + includeMilitary = ( isDefault || typeof options.includeMilitary === "undefined" ) ? false : options.includeMilitary, + regex; + + if (!includeTerritories && !includeMilitary) { + regex = "^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; + } else if (includeTerritories && includeMilitary) { + regex = "^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; + } else if (includeTerritories) { + regex = "^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$"; + } else { + regex = "^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$"; + } + + regex = caseSensitive ? new RegExp(regex) : new RegExp(regex, "i"); + return this.optional(element) || regex.test(value); +}, +"Please specify a valid state"); + // TODO check if value starts with <, otherwise don't try stripping anything $.validator.addMethod("strippedminlength", function(value, element, param) { return $(value).text().length >= param; @@ -826,9 +897,9 @@ $.validator.addMethod("vinUS", function(v) { rs = 0, i, n, d, f, cd, cdv; - for (i = 0; i < 17; i++){ + for (i = 0; i < 17; i++) { f = FL[i]; - d = v.slice(i,i + 1); + d = v.slice(i, i + 1); if (i === 8) { cdv = d; } diff --git a/thirdparty/jquery-validate/additional-methods.min.js b/thirdparty/jquery-validate/additional-methods.min.js old mode 100644 new mode 100755 index 8720a1b..6e6f0e9 --- a/thirdparty/jquery-validate/additional-methods.min.js +++ b/thirdparty/jquery-validate/additional-methods.min.js @@ -1,4 +1,4 @@ -/*! jQuery Validation Plugin - v1.12.1pre - 5/22/2014 +/*! jQuery Validation Plugin - v1.13.1 - 10/14/2014 * http://jqueryvalidation.org/ * Copyright (c) 2014 Jörn Zaefferer; Licensed MIT */ -!function(a){"function"==typeof define&&define.amd?define(["jquery","./jquery.validate.min"],a):a(jQuery)}(function(a){!function(){function b(a){return a.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g,"")}a.validator.addMethod("maxWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length<=d},a.validator.format("Please enter {0} words or less.")),a.validator.addMethod("minWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length>=d},a.validator.format("Please enter at least {0} words.")),a.validator.addMethod("rangeWords",function(a,c,d){var e=b(a),f=/\b\w+\b/g;return this.optional(c)||e.match(f).length>=d[0]&&e.match(f).length<=d[1]},a.validator.format("Please enter between {0} and {1} words."))}(),a.validator.addMethod("accept",function(b,c,d){var e,f,g="string"==typeof d?d.replace(/\s/g,"").replace(/,/g,"|"):"image/*",h=this.optional(c);if(h)return h;if("file"===a(c).attr("type")&&(g=g.replace(/\*/g,".*"),c.files&&c.files.length))for(e=0;ec;c++)d=h-c,e=f.substring(c,c+1),g+=d*e;return g%11===0},"Please specify a valid bank account number"),a.validator.addMethod("bankorgiroaccountNL",function(b,c){return this.optional(c)||a.validator.methods.bankaccountNL.call(this,b,c)||a.validator.methods.giroaccountNL.call(this,b,c)},"Please specify a valid bank or giro account number"),a.validator.addMethod("bic",function(a,b){return this.optional(b)||/^([A-Z]{6}[A-Z2-9][A-NP-Z1-2])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test(a)},"Please specify a valid BIC code"),a.validator.addMethod("cifES",function(a){"use strict";var b,c,d,e,f,g,h=[];if(a=a.toUpperCase(),!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)"))return!1;for(d=0;9>d;d++)h[d]=parseInt(a.charAt(d),10);for(c=h[2]+h[4]+h[6],e=1;8>e;e+=2)f=(2*h[e]).toString(),g=f.charAt(1),c+=parseInt(f.charAt(0),10)+(""===g?0:parseInt(g,10));return/^[ABCDEFGHJNPQRSUVW]{1}/.test(a)?(c+="",b=10-parseInt(c.charAt(c.length-1),10),a+=b,h[8].toString()===String.fromCharCode(64+b)||h[8].toString()===a.charAt(a.length-1)):!1},"Please specify a valid CIF number."),a.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9\-]+/.test(a))return!1;a=a.replace(/\D/g,"");var d=0;return c.mastercard&&(d|=1),c.visa&&(d|=2),c.amex&&(d|=4),c.dinersclub&&(d|=8),c.enroute&&(d|=16),c.discover&&(d|=32),c.jcb&&(d|=64),c.unknown&&(d|=128),c.all&&(d=255),1&d&&/^(5[12345])/.test(a)?16===a.length:2&d&&/^(4)/.test(a)?16===a.length:4&d&&/^(3[47])/.test(a)?15===a.length:8&d&&/^(3(0[012345]|[68]))/.test(a)?14===a.length:16&d&&/^(2(014|149))/.test(a)?15===a.length:32&d&&/^(6011)/.test(a)?16===a.length:64&d&&/^(3)/.test(a)?16===a.length:64&d&&/^(2131|1800)/.test(a)?15===a.length:128&d?!0:!1},"Please enter a valid credit card number."),a.validator.addMethod("currency",function(a,b,c){var d,e="string"==typeof c,f=e?c:c[0],g=e?!0:c[1];return f=f.replace(/,/g,""),f=g?f+"]":f+"]?",d="^["+f+"([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$",d=new RegExp(d),this.optional(b)||d.test(a)},"Please specify a valid currency"),jQuery.validator.addMethod("dateFA",function(a,b){return this.optional(b)||/^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(a)},"Please enter a correct date"),a.validator.addMethod("dateITA",function(a,b){var c,d,e,f,g,h=!1,i=/^\d{1,2}\/\d{1,2}\/\d{4}$/;return i.test(a)?(c=a.split("/"),d=parseInt(c[0],10),e=parseInt(c[1],10),f=parseInt(c[2],10),g=new Date(f,e-1,d,12,0,0,0),h=g.getFullYear()===f&&g.getMonth()===e-1&&g.getDate()===d?!0:!1):h=!1,this.optional(b)||h},"Please enter a correct date"),a.validator.addMethod("dateNL",function(a,b){return this.optional(b)||/^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(a)},"Please enter a correct date"),a.validator.addMethod("extension",function(a,b,c){return c="string"==typeof c?c.replace(/,/g,"|"):"png|jpe?g|gif",this.optional(b)||a.match(new RegExp(".("+c+")$","i"))},a.validator.format("Please enter a value with a valid extension.")),a.validator.addMethod("giroaccountNL",function(a,b){return this.optional(b)||/^[0-9]{1,7}$/.test(a)},"Please specify a valid giro account number"),a.validator.addMethod("iban",function(a,b){if(this.optional(b))return!0;var c,d,e,f,g,h,i,j,k,l=a.replace(/ /g,"").toUpperCase(),m="",n=!0,o="",p="";if(!/^([a-zA-Z0-9]{4} ){2,8}[a-zA-Z0-9]{1,4}|[a-zA-Z0-9]{12,34}$/.test(l))return!1;if(c=l.substring(0,2),h={AL:"\\d{8}[\\dA-Z]{16}",AD:"\\d{8}[\\dA-Z]{12}",AT:"\\d{16}",AZ:"[\\dA-Z]{4}\\d{20}",BE:"\\d{12}",BH:"[A-Z]{4}[\\dA-Z]{14}",BA:"\\d{16}",BR:"\\d{23}[A-Z][\\dA-Z]",BG:"[A-Z]{4}\\d{6}[\\dA-Z]{8}",CR:"\\d{17}",HR:"\\d{17}",CY:"\\d{8}[\\dA-Z]{16}",CZ:"\\d{20}",DK:"\\d{14}",DO:"[A-Z]{4}\\d{20}",EE:"\\d{16}",FO:"\\d{14}",FI:"\\d{14}",FR:"\\d{10}[\\dA-Z]{11}\\d{2}",GE:"[\\dA-Z]{2}\\d{16}",DE:"\\d{18}",GI:"[A-Z]{4}[\\dA-Z]{15}",GR:"\\d{7}[\\dA-Z]{16}",GL:"\\d{14}",GT:"[\\dA-Z]{4}[\\dA-Z]{20}",HU:"\\d{24}",IS:"\\d{22}",IE:"[\\dA-Z]{4}\\d{14}",IL:"\\d{19}",IT:"[A-Z]\\d{10}[\\dA-Z]{12}",KZ:"\\d{3}[\\dA-Z]{13}",KW:"[A-Z]{4}[\\dA-Z]{22}",LV:"[A-Z]{4}[\\dA-Z]{13}",LB:"\\d{4}[\\dA-Z]{20}",LI:"\\d{5}[\\dA-Z]{12}",LT:"\\d{16}",LU:"\\d{3}[\\dA-Z]{13}",MK:"\\d{3}[\\dA-Z]{10}\\d{2}",MT:"[A-Z]{4}\\d{5}[\\dA-Z]{18}",MR:"\\d{23}",MU:"[A-Z]{4}\\d{19}[A-Z]{3}",MC:"\\d{10}[\\dA-Z]{11}\\d{2}",MD:"[\\dA-Z]{2}\\d{18}",ME:"\\d{18}",NL:"[A-Z]{4}\\d{10}",NO:"\\d{11}",PK:"[\\dA-Z]{4}\\d{16}",PS:"[\\dA-Z]{4}\\d{21}",PL:"\\d{24}",PT:"\\d{21}",RO:"[A-Z]{4}[\\dA-Z]{16}",SM:"[A-Z]\\d{10}[\\dA-Z]{12}",SA:"\\d{2}[\\dA-Z]{18}",RS:"\\d{18}",SK:"\\d{20}",SI:"\\d{15}",ES:"\\d{20}",SE:"\\d{20}",CH:"\\d{5}[\\dA-Z]{12}",TN:"\\d{20}",TR:"\\d{5}[\\dA-Z]{17}",AE:"\\d{3}\\d{16}",GB:"[A-Z]{4}\\d{14}",VG:"[\\dA-Z]{4}\\d{16}"},g=h[c],"undefined"!=typeof g&&(i=new RegExp("^[A-Z]{2}\\d{2}"+g+"$",""),!i.test(l)))return!1;for(d=l.substring(4,l.length)+l.substring(0,4),j=0;j9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("nieES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[T]{1}/.test(a)?a[8]===/^[T]{1}[A-Z0-9]{8}$/.test(a):/^[XYZ]{1}/.test(a)?a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.replace("X","0").replace("Y","1").replace("Z","2").substring(0,8)%23):!1:!1},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):/^[KLM]{1}/.test(a)?a[8]===String.fromCharCode(64):!1:!1},"Please specify a valid NIF number."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return this.optional(b)?!0:("string"==typeof c&&(c=new RegExp(c)),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),jQuery.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;17>b;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g,"")}a.validator.addMethod("maxWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length<=d},a.validator.format("Please enter {0} words or less.")),a.validator.addMethod("minWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length>=d},a.validator.format("Please enter at least {0} words.")),a.validator.addMethod("rangeWords",function(a,c,d){var e=b(a),f=/\b\w+\b/g;return this.optional(c)||e.match(f).length>=d[0]&&e.match(f).length<=d[1]},a.validator.format("Please enter between {0} and {1} words."))}(),a.validator.addMethod("accept",function(b,c,d){var e,f,g="string"==typeof d?d.replace(/\s/g,"").replace(/,/g,"|"):"image/*",h=this.optional(c);if(h)return h;if("file"===a(c).attr("type")&&(g=g.replace(/\*/g,".*"),c.files&&c.files.length))for(e=0;ec;c++)d=h-c,e=f.substring(c,c+1),g+=d*e;return g%11===0},"Please specify a valid bank account number"),a.validator.addMethod("bankorgiroaccountNL",function(b,c){return this.optional(c)||a.validator.methods.bankaccountNL.call(this,b,c)||a.validator.methods.giroaccountNL.call(this,b,c)},"Please specify a valid bank or giro account number"),a.validator.addMethod("bic",function(a,b){return this.optional(b)||/^([A-Z]{6}[A-Z2-9][A-NP-Z1-2])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test(a)},"Please specify a valid BIC code"),a.validator.addMethod("cifES",function(a){"use strict";var b,c,d,e,f,g,h=[];if(a=a.toUpperCase(),!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)"))return!1;for(d=0;9>d;d++)h[d]=parseInt(a.charAt(d),10);for(c=h[2]+h[4]+h[6],e=1;8>e;e+=2)f=(2*h[e]).toString(),g=f.charAt(1),c+=parseInt(f.charAt(0),10)+(""===g?0:parseInt(g,10));return/^[ABCDEFGHJNPQRSUVW]{1}/.test(a)?(c+="",b=10-parseInt(c.charAt(c.length-1),10),a+=b,h[8].toString()===String.fromCharCode(64+b)||h[8].toString()===a.charAt(a.length-1)):!1},"Please specify a valid CIF number."),a.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9\-]+/.test(a))return!1;a=a.replace(/\D/g,"");var d=0;return c.mastercard&&(d|=1),c.visa&&(d|=2),c.amex&&(d|=4),c.dinersclub&&(d|=8),c.enroute&&(d|=16),c.discover&&(d|=32),c.jcb&&(d|=64),c.unknown&&(d|=128),c.all&&(d=255),1&d&&/^(5[12345])/.test(a)?16===a.length:2&d&&/^(4)/.test(a)?16===a.length:4&d&&/^(3[47])/.test(a)?15===a.length:8&d&&/^(3(0[012345]|[68]))/.test(a)?14===a.length:16&d&&/^(2(014|149))/.test(a)?15===a.length:32&d&&/^(6011)/.test(a)?16===a.length:64&d&&/^(3)/.test(a)?16===a.length:64&d&&/^(2131|1800)/.test(a)?15===a.length:128&d?!0:!1},"Please enter a valid credit card number."),a.validator.addMethod("currency",function(a,b,c){var d,e="string"==typeof c,f=e?c:c[0],g=e?!0:c[1];return f=f.replace(/,/g,""),f=g?f+"]":f+"]?",d="^["+f+"([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$",d=new RegExp(d),this.optional(b)||d.test(a)},"Please specify a valid currency"),a.validator.addMethod("dateFA",function(a,b){return this.optional(b)||/^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(a)},"Please enter a correct date"),a.validator.addMethod("dateITA",function(a,b){var c,d,e,f,g,h=!1,i=/^\d{1,2}\/\d{1,2}\/\d{4}$/;return i.test(a)?(c=a.split("/"),d=parseInt(c[0],10),e=parseInt(c[1],10),f=parseInt(c[2],10),g=new Date(f,e-1,d,12,0,0,0),h=g.getUTCFullYear()===f&&g.getUTCMonth()===e-1&&g.getUTCDate()===d?!0:!1):h=!1,this.optional(b)||h},"Please enter a correct date"),a.validator.addMethod("dateNL",function(a,b){return this.optional(b)||/^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(a)},"Please enter a correct date"),a.validator.addMethod("extension",function(a,b,c){return c="string"==typeof c?c.replace(/,/g,"|"):"png|jpe?g|gif",this.optional(b)||a.match(new RegExp(".("+c+")$","i"))},a.validator.format("Please enter a value with a valid extension.")),a.validator.addMethod("giroaccountNL",function(a,b){return this.optional(b)||/^[0-9]{1,7}$/.test(a)},"Please specify a valid giro account number"),a.validator.addMethod("iban",function(a,b){if(this.optional(b))return!0;var c,d,e,f,g,h,i,j,k,l=a.replace(/ /g,"").toUpperCase(),m="",n=!0,o="",p="";if(!/^([a-zA-Z0-9]{4} ){2,8}[a-zA-Z0-9]{1,4}|[a-zA-Z0-9]{12,34}$/.test(l))return!1;if(c=l.substring(0,2),h={AL:"\\d{8}[\\dA-Z]{16}",AD:"\\d{8}[\\dA-Z]{12}",AT:"\\d{16}",AZ:"[\\dA-Z]{4}\\d{20}",BE:"\\d{12}",BH:"[A-Z]{4}[\\dA-Z]{14}",BA:"\\d{16}",BR:"\\d{23}[A-Z][\\dA-Z]",BG:"[A-Z]{4}\\d{6}[\\dA-Z]{8}",CR:"\\d{17}",HR:"\\d{17}",CY:"\\d{8}[\\dA-Z]{16}",CZ:"\\d{20}",DK:"\\d{14}",DO:"[A-Z]{4}\\d{20}",EE:"\\d{16}",FO:"\\d{14}",FI:"\\d{14}",FR:"\\d{10}[\\dA-Z]{11}\\d{2}",GE:"[\\dA-Z]{2}\\d{16}",DE:"\\d{18}",GI:"[A-Z]{4}[\\dA-Z]{15}",GR:"\\d{7}[\\dA-Z]{16}",GL:"\\d{14}",GT:"[\\dA-Z]{4}[\\dA-Z]{20}",HU:"\\d{24}",IS:"\\d{22}",IE:"[\\dA-Z]{4}\\d{14}",IL:"\\d{19}",IT:"[A-Z]\\d{10}[\\dA-Z]{12}",KZ:"\\d{3}[\\dA-Z]{13}",KW:"[A-Z]{4}[\\dA-Z]{22}",LV:"[A-Z]{4}[\\dA-Z]{13}",LB:"\\d{4}[\\dA-Z]{20}",LI:"\\d{5}[\\dA-Z]{12}",LT:"\\d{16}",LU:"\\d{3}[\\dA-Z]{13}",MK:"\\d{3}[\\dA-Z]{10}\\d{2}",MT:"[A-Z]{4}\\d{5}[\\dA-Z]{18}",MR:"\\d{23}",MU:"[A-Z]{4}\\d{19}[A-Z]{3}",MC:"\\d{10}[\\dA-Z]{11}\\d{2}",MD:"[\\dA-Z]{2}\\d{18}",ME:"\\d{18}",NL:"[A-Z]{4}\\d{10}",NO:"\\d{11}",PK:"[\\dA-Z]{4}\\d{16}",PS:"[\\dA-Z]{4}\\d{21}",PL:"\\d{24}",PT:"\\d{21}",RO:"[A-Z]{4}[\\dA-Z]{16}",SM:"[A-Z]\\d{10}[\\dA-Z]{12}",SA:"\\d{2}[\\dA-Z]{18}",RS:"\\d{18}",SK:"\\d{20}",SI:"\\d{15}",ES:"\\d{20}",SE:"\\d{20}",CH:"\\d{5}[\\dA-Z]{12}",TN:"\\d{20}",TR:"\\d{5}[\\dA-Z]{17}",AE:"\\d{3}\\d{16}",GB:"[A-Z]{4}\\d{14}",VG:"[\\dA-Z]{4}\\d{16}"},g=h[c],"undefined"!=typeof g&&(i=new RegExp("^[A-Z]{2}\\d{2}"+g+"$",""),!i.test(l)))return!1;for(d=l.substring(4,l.length)+l.substring(0,4),j=0;j9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("nieES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[T]{1}/.test(a)?a[8]===/^[T]{1}[A-Z0-9]{8}$/.test(a):/^[XYZ]{1}/.test(a)?a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.replace("X","0").replace("Y","1").replace("Z","2").substring(0,8)%23):!1:!1},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):/^[KLM]{1}/.test(a)?a[8]===String.fromCharCode(64):!1:!1},"Please specify a valid NIF number."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return this.optional(b)?!0:("string"==typeof c&&(c=new RegExp("^(?:"+c+")$")),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),a.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeBR",function(a,b){return this.optional(b)||/^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test(a)},"Informe um CEP válido."),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),jQuery.validator.addMethod("stateUS",function(a,b,c){var d,e="undefined"==typeof c,f=e||"undefined"==typeof c.caseSensitive?!1:c.caseSensitive,g=e||"undefined"==typeof c.includeTerritories?!1:c.includeTerritories,h=e||"undefined"==typeof c.includeMilitary?!1:c.includeMilitary;return d=g||h?g&&h?"^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":g?"^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":"^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$":"^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",d=f?new RegExp(d):new RegExp(d,"i"),this.optional(b)||d.test(a)},"Please specify a valid state"),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;17>b;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c