From 4aeb4de0e204ad37656d105c79bcece1aae655eb Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Fri, 31 Jul 2015 16:30:46 +0300 Subject: [PATCH] Bug 14620 - Contact information validations I'm thinking we should centralize all patron's contact information validators into one module. Currently we only have validations for e-mail addresses, using the Email::Valid module. In our local library we also want to validate phone numbers with a regex that only recognizes phone numbers from my country. So I suggest we create a phone number validation as well, with an option to easily create custom regex for your local use. If a library does not wish to use validations, we should give them an option to turn them off. If more local validators are implemented, we should also give the library an option to select which validator they wish to use. With this method we could implement phone number validators from around the world and add them into our module. Then we would make the validations simply by calling our validator. The validator would know by system preference which one of the implemented phone number validators it should use. Validations should be both client and server side. Test plan: 1. Apply the patches (and run updatedatabase.pl). 2. Set system preferences ValidateEmailAddress to "Enable" and ValidatePhoneNumber to "International Phone Numbers". 3. Make sure JavaScript is enabled. 4. Navigate to edit user contact informations in Staff client and OPAC. 5. Clear email and phone fields. 6. Insert whitespaces to email and phone fields and submit the form. 7. Edit patron. 8. Observe that there are no longer whitespaces in email and phone fields. 9. Insert invalid email (e.g. "invalid") and invalid phone number ("+123invalid") and send the form. 10. Confirm that form will not be submitted and errors will be given. 11. Disable JavaScript. 12. Insert invalid email and invalid phone number 13. Try sending the form again. 14. Confirm that form will be submitted but errors will be given on top of the form. 15. Observe that none of the changes are accepted. 16. Set system preferences ValidateEmailAddress to "Disable" and ValidatePhoneNumber to "none". 17. Repeat step 3-9 but observe that errors will not be given and changes will be submitted and saved. --- Koha/Validation.pm | 138 ++++++++++++++++ .../bug_14620_email_and_phone_validations.sql | 2 + installer/data/mysql/sysprefs.sql | 2 + .../prog/en/modules/admin/preferences/patrons.pref | 16 ++ .../prog/en/modules/members/memberentrygen.tt | 67 ++++++++ .../bootstrap/en/modules/opac-memberentry.tt | 49 ++++++ .../bootstrap/en/modules/opac-messaging.tt | 56 +++++++ .../bootstrap/lib/bootstrap/css/bootstrap.css | 24 +++ .../bootstrap/lib/bootstrap/css/bootstrap.min.css | 2 +- members/memberentry.pl | 38 +++-- opac/opac-memberentry.pl | 25 ++- opac/opac-messaging.pl | 13 +- t/db_dependent/Validations.t | 184 +++++++++++++++++++++ 13 files changed, 594 insertions(+), 22 deletions(-) create mode 100644 Koha/Validation.pm create mode 100644 installer/data/mysql/atomicupdate/bug_14620_email_and_phone_validations.sql create mode 100644 t/db_dependent/Validations.t diff --git a/Koha/Validation.pm b/Koha/Validation.pm new file mode 100644 index 0000000..ee84b7d --- /dev/null +++ b/Koha/Validation.pm @@ -0,0 +1,138 @@ +package Koha::Validation; + +# Copyright 2015 Vaara-kirjastot +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use strict; +use warnings; + +use C4::Context; +use Email::Valid; + +use vars qw($VERSION); + +=head1 NAME + +Koha::Validation - validates inputs + +=head1 SYNOPSIS + + use Koha::Validation + +=head1 DESCRIPTION + +This module lets you validate given inputs. + +=head2 validate_email + +Validates given email. + + Koha::Validation::validate_email("email@address.com"); + +returns: 1 if the given email is valid, 0 otherwise. + +=cut + +sub validate_email { + + my $address = shift; + + # make sure we are allowed to validate emails + return 1 if not C4::Context->preference("ValidateEmailAddress"); + return 1 if not $address; + return 0 if $address =~ /(^(\s))|((\s)$)/; + return (not defined Email::Valid->address($address)) ? 0:1; +} + +=head2 validate_phonenumber + +Validates given phone number. + + Koha::Validation::validate_phonenumber(123456789); + +returns: 1 if the given phone number is valid, 0 otherwise. + +=cut + +sub validate_phonenumber { + my $phonenumber = shift; + + # make sure we are allowed to validate phone numbers + return 1 if C4::Context->preference("ValidatePhoneNumber") eq "OFF"; + return 1 if not $phonenumber; + return 0 if $phonenumber =~ /(^(\s))|((\s)$)/; + + my $regex = get_phonenumber_regex(C4::Context->preference("ValidatePhoneNumber")); + return ($phonenumber !~ /$regex/) ? 0:1; +} + +=head2 get_phonenumber_regex + +Returns the used regex (according to ValidatePhoneNumber system preference) for phone numbers. +This is used to share the same regex between Perl scripts and JavaScript in templates. + + Koha::Validation::get_phonenumber_regex(); + +International phone numbers (ipn): http://regexlib.com/REDetails.aspx?regexp_id=3009 + +returns: the regex + +=cut + +sub get_phonenumber_regex { + if (C4::Context->preference("ValidatePhoneNumber") eq "ipn") { + return qr/^((\+)?[1-9]{1,2})?([-\s\.])?((\(\d{1,4}\))|\d{1,4})(([-\s\.])?[0-9]{1,12}){1,2}$/; + } + elsif (C4::Context->preference("ValidatePhoneNumber") eq "fin") { + return qr/^((90[0-9]{3})?0|\+358\s?)(?!(100|20(0|2(0|[2-3])|9[8-9])|300|600|700|708|75(00[0-3]|(1|2)\d{2}|30[0-2]|32[0-2]|75[0-2]|98[0-2])))(4|50|10[1-9]|20(1|2(1|[4-9])|[3-9])|29|30[1-9]|71|73|75(00[3-9]|30[3-9]|32[3-9]|53[3-9]|83[3-9])|2|3|5|6|8|9|1[3-9])\s?(\d\s?){4,19}\d$/; + } + + return ""; + } + + +=head2 use_validator + +Validates given input with given validator. + + Koha::Validation::use_validator("phone", 123456789); + Koha::Validation::use_validator("email", "email@address.com"); + +Currently supported validators are + email, e-mail + phone, phonenumber + +returns: 1 if the given input is valid with the given validator, 0 otherwise. + +=cut + +sub use_validator { + my ($validator, $input) = @_; + + if (not defined $validator or not defined $input) { + warn "Subroutine must be called with validator and input"; + return 0; + } + + return validate_email($input) if $validator eq "email" or $validator eq "e-mail"; + return validate_phonenumber($input) if $validator eq "phone" or $validator eq "phonenumber"; + + warn "Validator not found"; + return 0; +} + +1; diff --git a/installer/data/mysql/atomicupdate/bug_14620_email_and_phone_validations.sql b/installer/data/mysql/atomicupdate/bug_14620_email_and_phone_validations.sql new file mode 100644 index 0000000..cee5e35 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_14620_email_and_phone_validations.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES ('ValidatePhoneNumber','OFF','ipn|fin|new|OFF','Validation of phone number on patrons.','Choice'); +INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES ('ValidateEmailAddress','0','','Validation of email address on patrons.','YesNo'); \ No newline at end of file diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 0fa2b81..8e23a33 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -475,6 +475,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('UseQueryParser','0',NULL,'If enabled, try to use QueryParser for queries.','YesNo'), ('UseTransportCostMatrix','0','','Use Transport Cost Matrix when filling holds','YesNo'), ('UseWYSIWYGinSystemPreferences','0','','Show WYSIWYG editor when editing certain HTML system preferences.','YesNo'), +('ValidateEmailAddress','0','','Validation of email address on patrons.','YesNo'), +('ValidatePhoneNumber','OFF','ipn|fin|new|OFF','Validation of phone number on patrons.','Choice'), ('viewISBD','1','','Allow display of ISBD view of bibiographic records','YesNo'), ('viewLabeledMARC','0','','Allow display of labeled MARC view of bibiographic records','YesNo'), ('viewMARC','1','','Allow display of MARC view of bibiographic records','YesNo'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref index c5a1744..9445bcb 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref @@ -138,6 +138,22 @@ Patrons: yes: Allow no: "Don't allow" - librarians to discharge borrowers and borrowers to request a discharge. + - + - Use validator + - pref: ValidatePhoneNumber + choices: + ipn: International Phone Numbers + fin: Finnish Phone Numbers + new: ...Not yet implemented... + "OFF": none + - to validate phone numbers on patrons. + - + - pref: ValidateEmailAddress + choices: + yes: Enable + no: Disable + - validation of email addresses on patrons. + "Norwegian patron database": - - pref: NorwegianPatronDBEnable diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt index 56b58e8..2a34c2e 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt @@ -34,8 +34,39 @@ } [% END %] $("#dateofbirth").datepicker({ maxDate: "-1D" }); + + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + $.validator.addMethod('phone', function(value) { + value = value.trim(); + if (!value.trim()) { + return 1; + } + else { + return (value.match(/[% phone_regex %]/)); + } + }, + MSG_INCORRECT_PHONE); + $("#entryform").validate({ rules: { + [% IF ( ValidatePhoneNumber ) %] + phone: { + phone: true + }, + phonepro: { + phone: true + }, + mobile: { + phone: true + }, + SMSnumber: { + phone: true + }, + B_phone: { + phone: true + }[% IF ( ValidateEmailAddress ) %],[% END %] + [% END %] + [% IF ( ValidateEmailAddress ) %] email: { email: true }, @@ -45,6 +76,7 @@ B_email: { email: true } + [% END %] }, submitHandler: function(form) { $("body, form input[type='submit'], form button[type='submit'], form a").addClass('waiting'); @@ -52,10 +84,33 @@ return false; else form.beenSubmitted = true; + [% IF ValidateEmailAddress %] + $("#email, #emailpro, #B_email").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + [% IF ValidatePhoneNumber %] + $("#phone, #phonepro, #B_phone, #SMSnumber").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] form.submit(); } }); + [% IF ValidateEmailAddress %] + $("#email, #emailpro, #B_email").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + [% IF ValidatePhoneNumber %] + $("#SMSnumber").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + $("#phone, #phonepro, #B_phone").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] var mrform = $("#manual_restriction_form"); var mrlink = $("#add_manual_restriction"); mrform.hide(); @@ -254,6 +309,18 @@ [% IF ERROR_bad_email_alternative %]
  • The alternative email is invalid.
  • [% END %] + [% IF ERROR_bad_phone %] +
  • The primary phone is invalid.
  • + [% END %] + [% IF ERROR_bad_phone_secondary %] +
  • The secondary phone is invalid.
  • + [% END %] + [% IF ERROR_bad_phone_alternative %] +
  • The alternative phone is invalid.
  • + [% END %] + [% IF ERROR_bad_smsnumber %] +
  • The SMS number is invalid.
  • + [% END %] [% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt index ca35988..516d67d 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt @@ -60,6 +60,10 @@ [% IF field == "email" %]
  • Contact information: primary email address
  • [% END %] [% IF field == "emailpro" %]
  • Contact information: secondary email address
  • [% END %] [% IF field == "B_email" %]
  • Alternate address information: email address
  • [% END %] + [% IF field == "phone" %]
  • Contact information: primary phone
  • [% END %] + [% IF field == "phonepro" %]
  • Contact information: secondary phone
  • [% END %] + [% IF field == "mobile" %]
  • Contact information: other phone
  • [% END %] + [% IF field == "B_phone" %]
  • Alternate address information: phone
  • [% END %] [% END %] Please correct the errors and resubmit. @@ -822,8 +826,19 @@ [% ELSE %] $( "#borrower_dateofbirth" ).datepicker({ yearRange: "c-120:c" }); [% END %] + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + $.validator.addMethod('phone', function(value) { + if (!value.trim()) { + return 1; + } + else { + return (value.match(/[% phone_regex %]/)); + } + }, + MSG_INCORRECT_PHONE); $("#memberentry-form").validate({ rules: { + [% IF ( ValidateEmailAddress ) %] borrower_email: { email: true }, @@ -832,7 +847,25 @@ }, borrower_B_email: { email: true + }[% IF ( ValidatePhoneNumber ) %],[% END %] + [% END %] + [% IF ( ValidatePhoneNumber ) %] + borrower_phone: { + phone: true + }, + borrower_phonepro: { + phone: true + }, + borrower_mobile: { + phone: true + }, + SMSnumber: { + phone: true + }, + borrower_B_phone: { + phone: true } + [% END %] }, submitHandler: function(form) { if (form.beenSubmitted) { @@ -840,6 +873,16 @@ } else { form.beenSubmitted = true; + [% IF ValidateEmailAddress %] + $("#borrower_email, #borrower_emailpro, #borrower_B_email").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + [% IF ValidatePhoneNumber %] + $("#borrower_phone, #borrower_phonepro, #borrower_B_phone, #SMSnumber").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] form.submit(); } }, @@ -853,6 +896,12 @@ error.css('width', 'auto'); } }); + $("#borrower_email, #borrower_emailpro, #borrower_B_email").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + $("#borrower_phone, #borrower_phonepro, #borrower_B_phone").on("change", function(){ + $(this).val($.trim($(this).val())); + }); }); //]]> diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-messaging.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-messaging.tt index b27756f..b700659 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-messaging.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-messaging.tt @@ -149,7 +149,63 @@ } }); $("#info_digests").tooltip(); + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + + $("fieldset.rows input, fieldset.rows select").addClass("noEnterSubmit"); + + var MSG_INCORRECT_PHONE = _("Please enter a valid phone number."); + $.validator.addMethod('phone', function(value) { + if (!value.trim()) { + return 1; + } + else { + return (value.match(/[% phone_regex %]/)); + } + }, + MSG_INCORRECT_PHONE); + + $("form[name='opacmessaging']").validate({ + rules: { + [% IF ( ValidatePhoneNumber ) %] + SMSnumber: { + phone: true + } + [% END %] + }, + submitHandler: function(form) { + $("body, form input[type='submit'], form button[type='submit'], form a").addClass('waiting'); + if (form.beenSubmitted) { + return false; + } + else { + form.beenSubmitted = true; + [% IF ValidatePhoneNumber %] + $("#SMSnumber").each(function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + form.submit(); + } + }, + errorPlacement: function(error, element) { + offset = element.offset(); + error.insertAfter(element) + error.addClass('error'); // add a class to the wrapper + error.css('position', 'absolute'); + error.css('left', offset.left + element.outerWidth() + 10); + error.css('top', offset.top); + error.css('width', 'auto'); + } + }); + + [% IF ( ValidatePhoneNumber ) %] + $("#SMSnumber").on("change", function(){ + $(this).val($.trim($(this).val())); + }); + [% END %] + }); //]]> + [% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.css b/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.css index 6b2ba30..2a38ae8 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.css +++ b/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.css @@ -6305,3 +6305,27 @@ button.close { overflow: visible !important; } } +.lang_icon { + padding-right: 5px; + vertical-align: baseline; + height: 11px; +} +#memberentry-form input.error, +form[name=opacmessaging] input.error { + border-color: #c00; + box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; + color: red; outline: 0 none; +} +#memberentry-form input.error:focus, +form[name=opacmessaging] input.error:focus { + border-color: #c00; + box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; + color: red; + outline: 0 none; +} +#memberentry-form label.error, +form[name=opacmessaging] label.error { + color: #c00; + float: none; + font-size: 90%; +} diff --git a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.min.css b/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.min.css index 0f48a96..76c6064 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.min.css +++ b/koha-tmpl/opac-tmpl/bootstrap/lib/bootstrap/css/bootstrap.min.css @@ -870,4 +870,4 @@ button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-ap .hidden-desktop{display:none !important;} .visible-desktop{display:inherit !important;} @media (min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important ;} .visible-tablet{display:inherit !important;} .hidden-tablet{display:none !important;}}@media (max-width:767px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important;} .visible-phone{display:inherit !important;} .hidden-phone{display:none !important;}}.visible-print{display:none !important;} -@media print{.visible-print{display:inherit !important;} .hidden-print{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .row-fluid [class*="offset"]:first-child{margin-left:0;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade{top:-100px;} .modal.fade.in{top:20px;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .media .pull-left,.media .pull-right{float:none;display:block;margin-bottom:10px;} .media-object{margin-right:0;margin-left:0;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12,textarea.span12,.uneditable-input.span12{width:710px;} input.span11,textarea.span11,.uneditable-input.span11{width:648px;} input.span10,textarea.span10,.uneditable-input.span10{width:586px;} input.span9,textarea.span9,.uneditable-input.span9{width:524px;} input.span8,textarea.span8,.uneditable-input.span8{width:462px;} input.span7,textarea.span7,.uneditable-input.span7{width:400px;} input.span6,textarea.span6,.uneditable-input.span6{width:338px;} input.span5,textarea.span5,.uneditable-input.span5{width:276px;} input.span4,textarea.span4,.uneditable-input.span4{width:214px;} input.span3,textarea.span3,.uneditable-input.span3{width:152px;} input.span2,textarea.span2,.uneditable-input.span2{width:90px;} input.span1,textarea.span1,.uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12,textarea.span12,.uneditable-input.span12{width:1156px;} input.span11,textarea.span11,.uneditable-input.span11{width:1056px;} input.span10,textarea.span10,.uneditable-input.span10{width:956px;} input.span9,textarea.span9,.uneditable-input.span9{width:856px;} input.span8,textarea.span8,.uneditable-input.span8{width:756px;} input.span7,textarea.span7,.uneditable-input.span7{width:656px;} input.span6,textarea.span6,.uneditable-input.span6{width:556px;} input.span5,textarea.span5,.uneditable-input.span5{width:456px;} input.span4,textarea.span4,.uneditable-input.span4{width:356px;} input.span3,textarea.span3,.uneditable-input.span3{width:256px;} input.span2,textarea.span2,.uneditable-input.span2{width:156px;} input.span1,textarea.span1,.uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999999;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:none;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .open>.dropdown-menu{display:block;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}} +@media print{.visible-print{display:inherit !important;} .hidden-print{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .row-fluid [class*="offset"]:first-child{margin-left:0;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade{top:-100px;} .modal.fade.in{top:20px;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .media .pull-left,.media .pull-right{float:none;display:block;margin-bottom:10px;} .media-object{margin-right:0;margin-left:0;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12,textarea.span12,.uneditable-input.span12{width:710px;} input.span11,textarea.span11,.uneditable-input.span11{width:648px;} input.span10,textarea.span10,.uneditable-input.span10{width:586px;} input.span9,textarea.span9,.uneditable-input.span9{width:524px;} input.span8,textarea.span8,.uneditable-input.span8{width:462px;} input.span7,textarea.span7,.uneditable-input.span7{width:400px;} input.span6,textarea.span6,.uneditable-input.span6{width:338px;} input.span5,textarea.span5,.uneditable-input.span5{width:276px;} input.span4,textarea.span4,.uneditable-input.span4{width:214px;} input.span3,textarea.span3,.uneditable-input.span3{width:152px;} input.span2,textarea.span2,.uneditable-input.span2{width:90px;} input.span1,textarea.span1,.uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12,textarea.span12,.uneditable-input.span12{width:1156px;} input.span11,textarea.span11,.uneditable-input.span11{width:1056px;} input.span10,textarea.span10,.uneditable-input.span10{width:956px;} input.span9,textarea.span9,.uneditable-input.span9{width:856px;} input.span8,textarea.span8,.uneditable-input.span8{width:756px;} input.span7,textarea.span7,.uneditable-input.span7{width:656px;} input.span6,textarea.span6,.uneditable-input.span6{width:556px;} input.span5,textarea.span5,.uneditable-input.span5{width:456px;} input.span4,textarea.span4,.uneditable-input.span4{width:356px;} input.span3,textarea.span3,.uneditable-input.span3{width:256px;} input.span2,textarea.span2,.uneditable-input.span2{width:156px;} input.span1,textarea.span1,.uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999999;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:none;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .open>.dropdown-menu{display:block;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}} #memberentry-form input.error, form[name=opacmessaging] input.error {border-color: #c00; box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; color: red; outline: 0 none;} #memberentry-form input.error:focus, form[name=opacmessaging] input.error:focus {border-color: #c00; box-shadow: 0 1px 1px #c00 inset, 0 0 8px #c00; color: red; outline: 0 none;} #memberentry-form label.error, form[name=opacmessaging] label.error {color: #c00; float: none; font-size: 90%;} diff --git a/members/memberentry.pl b/members/memberentry.pl index 7904e55..9382078 100755 --- a/members/memberentry.pl +++ b/members/memberentry.pl @@ -42,12 +42,13 @@ use C4::Branch; # GetBranches use C4::Form::MessagingPreferences; use Koha::Borrower::Debarments; use Koha::DateUtils; -use Email::Valid; +use Koha::Validation; use Module::Load; if ( C4::Context->preference('NorwegianPatronDBEnable') && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { load Koha::NorwegianPatronDB, qw( NLGetSyncDataFromBorrowernumber ); } +use re 'regexp_pattern'; use vars qw($debug); BEGIN { @@ -327,20 +328,14 @@ if ($op eq 'save' || $op eq 'insert'){ push @errors, "ERROR_password_mismatch" if ( $password ne $password2 ); push @errors, "ERROR_short_password" if( $password && $minpw && $password ne '****' && (length($password) < $minpw) ); - # Validate emails - my $emailprimary = $input->param('email'); - my $emailsecondary = $input->param('emailpro'); - my $emailalt = $input->param('B_email'); - - if ($emailprimary) { - push (@errors, "ERROR_bad_email") if (!Email::Valid->address($emailprimary)); - } - if ($emailsecondary) { - push (@errors, "ERROR_bad_email_secondary") if (!Email::Valid->address($emailsecondary)); - } - if ($emailalt) { - push (@errors, "ERROR_bad_email_alternative") if (!Email::Valid->address($emailalt)); - } + # validate emails + push (@errors, "ERROR_bad_email") if ($input->param('email') && !Koha::Validation::validate_email($input->param('email'))); + push (@errors, "ERROR_bad_email_secondary") if ($input->param('emailpro') && !Koha::Validation::validate_email($input->param('emailpro'))); + push (@errors, "ERROR_bad_email_alternative") if ($input->param('B_email') && !Koha::Validation::validate_email($input->param('B_email'))); + # validate phone numbers + push (@errors, "ERROR_bad_phone") if ($input->param('phone') && !Koha::Validation::validate_phonenumber($input->param('phone'))); + push (@errors, "ERROR_bad_phone_secondary") if ($input->param('phonepro') && !Koha::Validation::validate_phonenumber($input->param('phonepro'))); + push (@errors, "ERROR_bad_phone_alternative") if ($input->param('B_phone') && !Koha::Validation::validate_phonenumber($input->param('B_phone'))); if (C4::Context->preference('ExtendedPatronAttributes')) { $extended_patron_attributes = parse_extended_patron_attributes($input); @@ -372,7 +367,12 @@ if ( or $input->param('SMSnumber') ne $newdata{'mobile'} ) ) { - $newdata{smsalertnumber} = $input->param('SMSnumber'); + if (Koha::Validation::validate_phonenumber($input->param('SMSnumber'))){ + $newdata{smsalertnumber} = $input->param('SMSnumber'); + } else { + push (@errors, "ERROR_bad_smsnumber"); + } + } ### Error checks should happen before this line. @@ -681,6 +681,12 @@ if (C4::Context->preference('ExtendedPatronAttributes')) { patron_attributes_form($template, $borrowernumber); } +$template->param( + ValidateEmailAddress => C4::Context->preference('ValidateEmailAddress'), + ValidatePhoneNumber => C4::Context->preference('ValidatePhoneNumber') ne "OFF", + phone_regex => regexp_pattern Koha::Validation::get_phonenumber_regex() +); + if (C4::Context->preference('EnhancedMessagingPreferences')) { if ($op eq 'add') { C4::Form::MessagingPreferences::set_form_values({ categorycode => $categorycode }, $template); diff --git a/opac/opac-memberentry.pl b/opac/opac-memberentry.pl index f696208..46eeb2f 100755 --- a/opac/opac-memberentry.pl +++ b/opac/opac-memberentry.pl @@ -28,7 +28,9 @@ use C4::Form::MessagingPreferences; use Koha::Borrower::Modifications; use C4::Branch qw(GetBranchesLoop); use C4::Scrubber; -use Email::Valid; +use Koha::Validation; + +use re 'regexp_pattern'; my $cgi = new CGI; my $dbh = C4::Context->dbh; @@ -68,6 +70,9 @@ $template->param( member_titles => GetTitles() || undef, branches => GetBranchesLoop(), OPACPatronDetails => C4::Context->preference('OPACPatronDetails'), + ValidateEmailAddress => C4::Context->preference('ValidateEmailAddress'), + ValidatePhoneNumber => (C4::Context->preference('ValidatePhoneNumber') ne "OFF"), + phone_regex => regexp_pattern Koha::Validation::get_phonenumber_regex() ); if ( $action eq 'create' ) { @@ -307,13 +312,25 @@ sub CheckForInvalidFields { my $borrower = shift; my @invalidFields; if ($borrower->{'email'}) { - push(@invalidFields, "email") if (!Email::Valid->address($borrower->{'email'})); + push(@invalidFields, "email") if (!Koha::Validation::validate_email($borrower->{'email'})); } if ($borrower->{'emailpro'}) { - push(@invalidFields, "emailpro") if (!Email::Valid->address($borrower->{'emailpro'})); + push(@invalidFields, "emailpro") if (!Koha::Validation::validate_email($borrower->{'emailpro'})); } if ($borrower->{'B_email'}) { - push(@invalidFields, "B_email") if (!Email::Valid->address($borrower->{'B_email'})); + push(@invalidFields, "B_email") if (!Koha::Validation::validate_email($borrower->{'B_email'})); + } + if ($borrower->{'mobile'}) { + push(@invalidFields, "mobile") if (!Koha::Validation::validate_phonenumber($borrower->{'mobile'})); + } + if ($borrower->{'phone'}) { + push(@invalidFields, "phone") if (!Koha::Validation::validate_phonenumber($borrower->{'phone'})); + } + if ($borrower->{'phonepro'}) { + push(@invalidFields, "phonepro") if (!Koha::Validation::validate_phonenumber($borrower->{'phonepro'})); + } + if ($borrower->{'B_phone'}) { + push(@invalidFields, "B_phone") if (!Koha::Validation::validate_phonenumber($borrower->{'B_phone'})); } return \@invalidFields; } diff --git a/opac/opac-messaging.pl b/opac/opac-messaging.pl index 69ae517..daed17d 100755 --- a/opac/opac-messaging.pl +++ b/opac/opac-messaging.pl @@ -32,6 +32,9 @@ use C4::Members; use C4::Members::Messaging; use C4::Branch; use C4::Form::MessagingPreferences; +use Koha::Validation; + +use re 'regexp_pattern'; my $query = CGI->new(); @@ -48,10 +51,18 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user( my $borrower = GetMemberDetails( $borrowernumber ); my $messaging_options = C4::Members::Messaging::GetMessagingOptions(); +$template->param( + ValidateEmailAddress => C4::Context->preference('ValidateEmailAddress'), + ValidatePhoneNumber => (C4::Context->preference('ValidatePhoneNumber') ne "OFF"), + phone_regex => regexp_pattern Koha::Validation::get_phonenumber_regex() +); if ( defined $query->param('modify') && $query->param('modify') eq 'yes' ) { + my $valid_smsnumber = Koha::Validation::validate_phonenumber($query->param('SMSnumber')); + $template->param( invalid_smsnumber => $valid_smsnumber ); + # If they've modified the SMS number, record it. - if ( ( defined $query->param('SMSnumber') ) && ( $query->param('SMSnumber') ne $borrower->{'mobile'} ) ) { + if ( ( defined $query->param('SMSnumber') ) && ( $query->param('SMSnumber') ne $borrower->{'mobile'} ) && $valid_smsnumber ) { ModMember( borrowernumber => $borrowernumber, smsalertnumber => $query->param('SMSnumber') ); $borrower = GetMemberDetails( $borrowernumber ); diff --git a/t/db_dependent/Validations.t b/t/db_dependent/Validations.t new file mode 100644 index 0000000..1a3b3db --- /dev/null +++ b/t/db_dependent/Validations.t @@ -0,0 +1,184 @@ +#!/usr/bin/env perl + +# Copyright 2015 Open Source Freedom Fighters +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . +$ENV{KOHA_PAGEOBJECT_DEBUG} = 1; +use Modern::Perl; + +use Test::More; +use Try::Tiny; #Even Selenium::Remote::Driver uses Try::Tiny :) + +use Koha::Auth::PermissionManager; + +use t::lib::Page::Mainpage; +use t::lib::Page::Opac::OpacMain; +use t::lib::Page::Opac::OpacMemberentry; +use t::lib::Page::Members::Memberentry; +use t::lib::Page::Members::Moremember; + +use t::lib::TestObjects::BorrowerFactory; +use t::lib::TestObjects::SystemPreferenceFactory; + +##Setting up the test context +my $testContext = {}; + +my $password = '1234'; +my $borrowerFactory = t::lib::TestObjects::BorrowerFactory->new(); +my $borrowers = $borrowerFactory->createTestGroup([ + {firstname => 'Testone', + surname => 'Testtwo', + cardnumber => '1A01', + branchcode => 'CPL', + userid => 'normal_user', + password => $password, + }, + {firstname => 'Testthree', + surname => 'Testfour', + cardnumber => 'superuberadmin', + branchcode => 'CPL', + userid => 'god', + password => $password, + }, + ], undef, $testContext); + +my $systempreferences = t::lib::TestObjects::SystemPreferenceFactory->createTestGroup([ + {preference => 'ValidateEmailAddress', + value => 1 + }, + {preference => 'ValidatePhoneNumber', + value => 'ipn', + }, + ], undef, $testContext); + +my $permissionManager = Koha::Auth::PermissionManager->new(); +$permissionManager->grantPermissions($borrowers->{'superuberadmin'}, {superlibrarian => 'superlibrarian'}); + +eval { + OpacValidations(); + StaffValidations(); +}; + +if ($@) { #Catch all leaking errors and gracefully terminate. + warn $@; + tearDown(); + exit 1; +} + +##All tests done, tear down test context +tearDown(); +done_testing; + +sub tearDown { + t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); +} + + + + + + + + +###################################################### + ### STARTING TEST IMPLEMENTATIONS ### +###################################################### +sub setValidationsOff { + C4::Context->set_preference('ValidateEmailAddress', 0); + C4::Context->set_preference('ValidatePhoneNumber', 'OFF'); +} +sub setValidationsOn { + C4::Context->set_preference('ValidateEmailAddress', 1); + C4::Context->set_preference('ValidatePhoneNumber', 'ipn'); +} + +sub OpacValidations { + my $main = t::lib::Page::Opac::OpacMain->new({borrowernumber => $borrowers->{'1A01'}->borrowernumber}); + + ok (0, "Email validation is OFF!") if (C4::Context->preference('ValidateEmailAddress') == 0); + ok (0, "Phone validation is OFF") if C4::Context->preference('ValidatePhoneNumber') eq "OFF"; + $main + ->doPasswordLogin($borrowers->{'1A01'}->userid(), $password) + ->navigateYourPersonalDetails() + ->setEmail("valid\@email.com") # test valid email + ->submitForm(1) # expecting success + ->navigateYourPersonalDetails() + ->setEmail("invalidemail") # test invalid email + ->submitForm(0) # expecting error + ->navigateYourPersonalDetails() + ->setPhone("+3585012345667") # test valid phone number + ->submitForm(1) # expecting success + ->navigateYourPersonalDetails() + ->setPhone("1234phone56789") # test invalid phone number + ->submitForm(0); # expecting error + + print "--Setting validations off--\n"; + setValidationsOff(); # set validations off from system prefs + #then test validations again + ok (0, "Email validation is ON!") if (C4::Context->preference('ValidateEmailAddress') == 1); + ok (0, "Phone validation is ON!") if C4::Context->preference('ValidatePhoneNumber') ne "OFF"; + + $main + ->navigateYourPersonalDetails() + ->setEmail("invalidemail_validations_off") # test invalid email + ->submitForm(1) # expecting success + ->navigateYourPersonalDetails() + ->setPhone("1234phone56789_validations_off") # test invalid phone number + ->submitForm(1); # expecting success + + setValidationsOn(); +} + +sub StaffValidations { + my $memberentry = t::lib::Page::Members::Memberentry->new({borrowernumber => $borrowers->{'superuberadmin'}->borrowernumber, op => 'modify', destination => 'circ', categorycode => 'PT'}); + + ok (0, "Email validation is OFF!") if (C4::Context->preference('ValidateEmailAddress') == 0); + ok (0, "Phone validation is OFF") if C4::Context->preference('ValidatePhoneNumber') eq "OFF"; + + $memberentry + ->doPasswordLogin($borrowers->{'superuberadmin'}->userid(), $password) + ->setEmail("valid\@email.com") # test valid email + ->submitForm(1) # expecting success + ->navigateEditPatron() + ->setEmail("invalidemail") # test invalid email + ->submitForm(0) # expecting error + ->setEmail("") + ->setPhone("+3585012345667") # test valid phone number + ->submitForm(1) # expecting success + ->navigateEditPatron() + ->setPhone("1234phone56789") # test invalid phone number + ->submitForm(0); # expecting error + + print "--Setting validations off--\n"; + setValidationsOff(); # set validations off from system prefs + #then test validations again + + ok (0, "Email validation is ON!") if (C4::Context->preference('ValidateEmailAddress') == 1); + ok (0, "Phone validation is ON!") if C4::Context->preference('ValidatePhoneNumber') ne "OFF"; + + $memberentry + ->setPhone("") # refreshing + ->setEmail("") # the + ->submitForm(1) # page + ->navigateEditPatron() + ->setEmail("invalidemail_validations_off") # test invalid email + ->submitForm(1) # expecting success + ->navigateEditPatron() + ->setPhone("1234phone56789_validations_off") # test invalid phone number + ->submitForm(1); # expecting success + + setValidationsOn(); +} \ No newline at end of file -- 1.9.1