From b6ceb89d78ae06dc88eac20439c7b186076bdd26 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. --- Koha/Validation.pm | 140 ++++++++++++++++ .../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 | 44 +++++ .../bootstrap/en/modules/opac-memberentry.tt | 33 ++++ .../bootstrap/en/modules/opac-messaging.tt | 45 +++++ .../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 | 14 +- t/db_dependent/Validations.t | 184 +++++++++++++++++++++ 13 files changed, 546 insertions(+), 23 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..90f1ef0 --- /dev/null +++ b/Koha/Validation.pm @@ -0,0 +1,140 @@ +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 0 if not defined $address; + 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; + + $phonenumber =~ s/\s+//g; + + # make sure we are allowed to validate phone numbers + return 1 if C4::Context->preference("ValidatePhoneNumber") eq "OFF"; + return 1 if $phonenumber eq ""; + return 0 if not defined $phonenumber; + + my $regex = get_phonenumber_regex(C4::Context->preference("ValidatePhoneNumber")); + #die $regex; + 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,20}$/; + } + + 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; \ No newline at end of file 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 3c7bcc0..e313591 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -470,6 +470,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('UseKohaPlugins','0','','Enable or disable the ability to use Koha Plugins.','YesNo'), ('UseQueryParser','0',NULL,'If enabled, try to use QueryParser for queries.','YesNo'), ('UseTransportCostMatrix','0','','Use Transport Cost Matrix when filling holds','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 ee908d2..1c28cd7 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 f473346..350e4ad 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'); @@ -254,6 +286,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 4eed652..d08f9d2 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-memberentry.tt @@ -57,6 +57,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. @@ -819,8 +823,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 }, @@ -829,7 +844,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) { 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..1656854 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,52 @@ } }); $("#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; + 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'); + } + }); + }); //]]> + [% 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..a593999 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 116ba7f..4f8f26b 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' ) { @@ -299,13 +304,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..99b55e0 100755 --- a/opac/opac-messaging.pl +++ b/opac/opac-messaging.pl @@ -33,6 +33,8 @@ use C4::Members::Messaging; use C4::Branch; use C4::Form::MessagingPreferences; +use re 'regexp_pattern'; + my $query = CGI->new(); my ( $template, $borrowernumber, $cookie ) = get_template_and_user( @@ -48,10 +50,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..dd67bbb --- /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