package LIMS::Validate::Roles::ConstraintMethods; # contains constraint_method helper methods for LIMS::Validate use Moose::Role; use namespace::clean -except => 'meta'; use Data::Dumper; use LIMS::Local::Utils; use FormValidator::Simple qw(+LIMS::Validate::DatePlugin); use Data::FormValidator::Constraints qw(:regexp_common FV_max_length FV_length_between valid_email FV_eq_with); use Data::FormValidator::Constraints::DateTime qw(:all); # using: ymd_before_today() use Data::FormValidator::Constraints::MethodsFactory qw(:bool); # FV_and #------------------------------------------------------------------------------- sub is_integer { FV_num_int } # must be integer; Regexp::Common, s/RE/FV/ # sub is_decimal { FV_num_decimal } # doesn't work sub is_decimal { sub { $_[0]->get_current_constraint_value =~ /^\d+\.\d+$/ } } #------------------------------------------------------------------------------- sub check_nhsno_is_valid { my $self = shift; # warn 'here'; warn Dumper $self; return sub { my $dfv = shift; # warn 'here'; warn Dumper $dfv; my $nhs_number = shift || return 0; # warn 'NHSNo:'.$nhs_number; $dfv->name_this('is_valid'); # check_nhsno() return true if valid: my $is_valid = LIMS::Local::Utils::check_nhsno( $nhs_number ); # warn $is_valid; return $is_valid; } } #------------------------------------------------------------------------------- sub check_nhsno_is_unique { my $self = shift; return sub { my $dfv = shift; # warn 'here'; warn Dumper $dfv; my $nhs_number = shift || return 0; # warn 'NHSNo:'.$nhs_number; $dfv->name_this('is_unique'); my $validation_model = $self->models->{validate_nhs_number_unique}; my $not_unique = $validation_model->($nhs_number); return $not_unique ? 0 : 1; } } #------------------------------------------------------------------------------- sub check_email_is_valid { my $self = shift; my $args = shift; # warn Dumper $args; # optional - to force nhs.net address return sub { my ($dfv, $email) = @_; return 0 if $args->{nhs_net} && $email !~ /\@nhs.net$/; return valid_email($email); # Data::FormValidator::Constraints method } } #------------------------------------------------------------------------------- sub check_emails_match { return sub { my $dfv = shift; # DEBUG($dfv); $dfv->name_this('email_mismatch'); my $data = $dfv->get_filtered_data; # DEBUG($data); return ($data->{email} eq $data->{email2}); # returns truth }; } #------------------------------------------------------------------------------- sub check_post_code_is_valid { my $self = shift; return sub { my ($dfv, $zip) = @_; # warn $zip; return LIMS::Local::Utils::format_postcode($zip); } } #------------------------------------------------------------------------------- =begin # moved to C::Request::_check_secure_recipient() sub check_secure_email_domain { my $self = shift; my $settings = $self->settings; my @valid_domains = split /\,\s?|\s+/, $settings->{valid_recipient_domains}; return sub { my ($dfv, $email) = @_; # warn $email; return grep $email =~ /$_\Z/, @valid_domains; } } =cut #------------------------------------------------------------------------------- sub check_no_spaces { my $self = shift; return sub { my $dfv = shift; # warn 'here'; $dfv->name_this('no_spaces'); my $val = $dfv->get_current_constraint_value(); # warn $val; my ($is_valid) = $val =~ /^[a-zA-Z0-9_]+$/; # warn $is_valid; return $is_valid; } } #------------------------------------------------------------------------------- sub check_username { # allows alphanumeric with optional hyphen (Foo-Bar) my $self = shift; return sub { my $dfv = shift; # warn 'here'; $dfv->name_this('username'); my $val = $dfv->get_current_constraint_value(); # warn $val; # requires first letter, then apostrophe, hyphen or more letters: my $re = qr{[a-z][-'\.a-z]+}; my ($is_valid) = $val =~ /\A($re)\Z/; # warn Dumper $is_valid; return $is_valid; } } #------------------------------------------------------------------------------- # used by new_request() & Ajax::_validate_params() sub get_request_number_constraints { my $self = shift; my $is_unique = sub { my $dfv = shift; # warn 'here'; $dfv->name_this('is_unique'); my $val = $dfv->get_current_constraint_value(); # warn $val; my $uniqueness = $self->check_request_number($val); return $uniqueness; }; my $max_request_number_length = $self->settings->{max_request_number_length}; return [ # TODO: join_these() doesn't work here $is_unique, # check it's unique FV_num_int, # must be integer; Regexp::Common, s/RE/FV/ $self->check_param_length([ 1, $max_request_number_length ]), # don't expect >99_999/yr # FV_clamp(1, 1, 5), # MethodsFactory method - no configurable msgs output ? ]; } #------------------------------------------------------------------------------- # used by new_request() & Ajax::_validate_params() sub get_request_number_messages { my $self = shift; return { # messages need to be set to constraint_names, not field_names: constraints => { is_unique => $self->messages->{not_unique}, FV_num_int => $self->messages->{need_integer}, # FV_length_between in check_param_length() doesn't work with D::F 4.71 ?? length_between => $self->messages->{too_long}, #now using 'name_this' in check_param_length() instead: max_length => $self->messages->{too_long}, }, }; } #------------------------------------------------------------------------------- # uses model to get param type (int or decimal) from defaults_and_ranges table: sub get_hmrn_param_constraints { my ($self, $fields) = @_; # warn Dumper $fields; # get all hmrn param defaults_and_ranges data: my $data = $self->models->{hmrn_param_constraints}->(); # warn Dumper $data; my %constraint_method = (); # hash for constraint methods PARAM: for my $param(@$fields) { # get data for this param, or skip to next: if ( my $d = $data->{$param} ) { # warn Dumper $d; my $field_type = $d->{field_type}; # int or decimal if ($field_type eq 'int') { $constraint_method{$param} = FV_num_real(); } elsif ($field_type eq 'decimal') { $constraint_method{$param} = FV_num_decimal(); } } } # warn Dumper \%constraint_method; return \%constraint_method; } #------------------------------------------------------------------------------- # can have non-unique test_names, but not with same lab_section_id: =begin ? not in use sub check_lab_test_is_valid { my $self = shift; return sub { my $dfv = shift; $dfv->name_this('is_unique'); my $data = $dfv->get_filtered_data; # warn Dumper $data; my $validation_model = $self->models->{validate_lab_test}; my $is_valid = $validation_model->($data); return $is_valid; } } =cut #------------------------------------------------------------------------------- sub get_new_request_specimen_constraints { # needs to check block_ref as well: my $self = shift; my $constraint = FV_and( $self->check_external_reference(), $self->get_specimen_constraints(), ); return $constraint; } #------------------------------------------------------------------------------- # used by new_request() & Ajax::_validate_params() sub get_specimen_constraints { my $self = shift; return sub { my $dfv = shift; # warn 'here'; $dfv->name_this('check_specimen'); my $val = $dfv->get_current_constraint_value(); # warn $val; my $is_valid = $self->check_specimen($val); # warn $is_valid; return $is_valid; } } #------------------------------------------------------------------------------- # checks entry(s) in specimen field exists in specimens table: # (not a DFV method so doesn't receive dfv object) sub check_specimen { # warn Dumper @_; my $self = shift; my $str = shift || return 0; # warn 'specimens:'.$str; my $models = $self->models; # extract specimens from $str: my $specimens = LIMS::Local::Utils::get_specimens($str); # warn Dumper $specimens; my $max_length # get max length of sample_code col from specimens table meta data: = $models->{get_meta}->('Specimen')->column('sample_code')->length; my $validator = sub { $models->{validate_specimen}->(@_) }; foreach my $specimen (@$specimens) { # warn Dumper $specimen; return 0 if length $specimen > $max_length; # as next line is fatal if > max length my $valid = &$validator($specimen); # warn Dumper [$specimen, $valid]; return 0 if ! $valid; } # OK, specimen(s) valid: return 1; } #------------------------------------------------------------------------------- sub check_referrer_code { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('referrer_code'); my $val = $dfv->get_current_constraint_value(); # warn $val; # C, D, E or CS followed by 5-7 numbers: return ($val =~ /\A([CDE]|CS)\d{5,7}\Z/) ? 1 : 0; } } #------------------------------------------------------------------------------- sub check_snomed_code { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('snomed_code'); my $val = $dfv->get_current_constraint_value(); # warn $val; # C, 2 numbers, dot, number: return ( $val =~ /\A(C\d{2}\.\d)\Z/ ) ? 1 : 0; } } #------------------------------------------------------------------------------- sub check_request_number { my $self = shift; my $request_number = shift || return 0; # warn 'request_number:'.$request_number; my $validation_model = $self->models->{validate_request_number}; my $not_unique = $validation_model->($request_number); return $not_unique ? 0 : 1; } #------------------------------------------------------------------------------- sub get_date_constraints { my $self = shift; my $prefix = shift; # optional - to support forms with multiple dates eg dob, request date, etc my $constraint = FV_and( $self->check_four_digit_year($prefix), $self->check_date_is_valid($prefix), $self->check_date_not_future($prefix), );# warn Dumper $constraint; return $constraint; } #------------------------------------------------------------------------------- sub get_change_password_profile { my $self = shift; return [ # FormValidator::Simple method new_password => [ 'NOT_BLANK', ['LENGTH', 5, 255 ] ], old_password => [ 'NOT_BLANK', ['LENGTH', 4, 255 ] ], # HILIS3 min. new_password_confirm => [ 'NOT_BLANK', ['LENGTH', 5, 255 ] ], { pwds => ['new_password', 'new_password_confirm'] } => [ 'DUPLICATION' ], ]; } #------------------------------------------------------------------------------- # uses FV_and() to return constraint_method, which executes list in order & # exits on 1st failure, instead of executing all and potentially generating errs sub join_these { my $self = shift; return FV_and(@_); } #------------------------------------------------------------------------------- sub check_four_digit_year { my $self = shift; my $prefix = shift; # optional, used if form has >1 date type (eg request date, dob) my $year_field_name = $prefix ? $prefix.'_year' # eg request_year, dob_year : 'year'; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('yyyy_format'); my $data = $dfv->get_filtered_data; # warn Dumper $data; return ( length $data->{$year_field_name} == 4 ); } } #------------------------------------------------------------------------------- sub check_numeric { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('numeric_format'); my $val = $dfv->get_current_constraint_value(); # warn $val; # digits only: my ($is_valid) = $val =~ /^[0-9]+$/; # warn $is_valid; return $is_valid; } } #------------------------------------------------------------------------------- sub check_vial_id { my ($self, $messageId) = @_; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this($messageId); my $data = $dfv->get_filtered_data; # warn Dumper $data; return ( $data->{vialId} eq $data->{vial_id} ); }; } #------------------------------------------------------------------------------- sub check_secondary_diagnosis { # checks secondary_diagnosis_id NOT equal to diagnosis_id my ($self, $messageId) = @_; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this($messageId); my $data = $dfv->get_filtered_data; # warn Dumper $data; return ( $data->{secondary_diagnosis_id} ne $data->{diagnosis_id} ); }; } #------------------------------------------------------------------------------- # override MethodsFactory method until can find out how to target msgs at it sub _FV_clamp { my $self = shift; my ($result, $low, $high) = @_; return sub { my $dfv = shift; $dfv->name_this('FV_clamp'); my $val = $dfv->get_current_constraint_value(); return (($val < $low) or ($val > $high)) ? !$result : $result; } } #------------------------------------------------------------------------------- # or could use 'day => ymd_before_today( qw/year month day/ )' sub check_date_not_future { my $self = shift; my $prefix = shift; # optional, used if form has >1 date type (eg request date, dob) my $param_names = $self->_get_date_param_names($prefix); #arrayref return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('future_date'); my $data = $dfv->get_filtered_data; # warn Dumper $data; # returns error if date not less than DateTime->now: my @cmp = ( 'DATE_LESS_THAN', DateTime->now ); my $result = FormValidator::Simple->check( $data => [ { grouped => $param_names } => [ \@cmp ], ] ); # warn Dumper $result->has_error; return $result->has_error ? 0 : 1; } } #------------------------------------------------------------------------------- # if hidden flag '_diagnosis_id' passed, also need either 'option_id' or # 'final_diagnosis' param: sub check_report_params { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('diagnosis_resubmission'); my $data = $dfv->get_filtered_data; # warn Dumper $data; my $original_diagnosis_id = $dfv->get_current_constraint_value(); return ( $data->{option_id} # need either no change OR reason for change: || $original_diagnosis_id == $data->{diagnosis_id} ); =begin # changed this to ALWAYS require reason for change diagnosis # eg authorisation, without change of diagnosis_id: return 1 if $original_diagnosis_id == $data->{diagnosis_id}; # require either 'final_diagnosis' or 'option_id' params: return grep $data->{$_}, qw(final_diagnosis option_id); =cut } } #------------------------------------------------------------------------------- # external_reference required if sample-type is histological sub check_external_reference { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('block_reference'); my $data = $dfv->get_filtered_data; # warn Dumper $data; my $specimens = LIMS::Local::Utils::get_specimens($data->{specimen}); # warn Dumper $specimens; return 1 unless grep $_ =~ /[BS]L\Z/i, @$specimens; return ($data->{external_reference}); # returns true if exists }; } #------------------------------------------------------------------------------- # # check end_date NOT before start_date - returns true (1) UNLESS $second_date # BEFORE $first_date: sub check_date_order { my $self = shift; my $cfg = $self->settings; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('date_order'); my $data = $dfv->get_filtered_data; # warn Dumper $data; # LIMS::Local::Utils::check_chronological_order() requires DateTime objects: my $start_date = LIMS::Local::Utils::to_datetime_using_datecalc($data->{start_date}); my $end_date = LIMS::Local::Utils::to_datetime_using_datecalc($data->{end_date}); # exempt default unknown date from date order check: return 1 if $start_date && $end_date # don't try ymd() method on undef val!! && grep $_->ymd eq $cfg->{default_unknown_date}, ($start_date, $end_date); my $val # returns '-1' if dates in chronological order (0 if equal, otherwise 1): = LIMS::Local::Utils::check_chronological_order($end_date, $start_date); # warn Dumper $val; return $val == -1 ? 0 : 1; # return false if $expr eq -1 ie in date order } } #------------------------------------------------------------------------------- sub check_date_is_valid { my $self = shift; my $prefix = shift; # optional, used if form has >1 date type (eg request date, dob) my $param_names = $self->_get_date_param_names($prefix); # arrayref eg [dob_day, etc] # warn Dumper $param_names; return sub { my $dfv = shift; # warn Dumper $dfv; my $data = $dfv->get_filtered_data; # warn Dumper $data; my $name = ( $data->{hour} || $data->{minute} ) ? 'valid_datetime' : 'valid_date'; $dfv->name_this($name); =begin # fails to detect invalid yr = 19.7 !! my $result = FormValidator::Simple->check( $data => [ { datetime => $param_names } => [ 'DATETIME' ] ] ); # warn Dumper $result->has_error; return $result->has_error ? 0 : 1; =cut my ($d) = map $data->{$_}, grep $_ =~ /day\Z/, @$param_names; # warn $d; my ($m) = map $data->{$_}, grep $_ =~ /month\Z/, @$param_names; # warn $m; my ($y) = map $data->{$_}, grep $_ =~ /year\Z/, @$param_names; # warn $y; eval { DateTime->new(year => $y, month => $m, day => $d) }; # warn $@ if $@; return $@ ? 0 : 1; }; } #------------------------------------------------------------------------------- sub check_EU_date_is_valid { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('valid_date'); my $val = $dfv->get_current_constraint_value(); # warn $val; my $result = LIMS::Local::Utils::check_date($val); return $result; }; } #------------------------------------------------------------------------------- sub check_yyyymmdd_date_is_valid { # works for dates & datetimes my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; my $val = $dfv->get_current_constraint_value(); # warn $val; my $dt = LIMS::Local::Utils::to_datetime_using_dateparse($val); # warn Dumper $dt; return $dt ? 1 : 0; } } #------------------------------------------------------------------------------- sub check_vial_Id_length { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('invalid_length'); my $val = $dfv->get_current_constraint_value(); # warn $val; warn length($val); # multiples of 10 chars: # return ( length($val) % 10 ) ? 0 : 1; # fails if modulus result evals 'true' return ( length($val) == 10 ); # only allowing singlicates now using partnums } } #------------------------------------------------------------------------------- sub check_vial_Id_format { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('invalid_format'); my $val = $dfv->get_current_constraint_value(); # warn $val; # numbers, letters & hyphens only (not '/' due to url constraints): return ( $val =~ /^[\w-]+$/ ); } } #------------------------------------------------------------------------------- sub check_vial_Id_unique { my $self = shift; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('not_unique'); my $val = $dfv->get_current_constraint_value(); # warn $val; my $vials = LIMS::Local::Utils::extract_storage_vial_ids($val); # arrayref my %seen; # in case same vial re-scanned for my $id(@$vials) { # warn $id; return 0 if $seen{$id}++; # ie if vial re-scanned # check vialID unique (function returns false if ID already exists): $self->models->{has_unique_storage_vialId}->($id) || return 0; } return 1; } } #------------------------------------------------------------------------------- sub check_param_length { my $self = shift; my $arg = shift; # warn Dumper $arg; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('max_length'); my $val = $dfv->get_current_constraint_value(); # warn $val; my $result = ref $arg eq 'ARRAY' ? ( length($val) >= $arg->[0] && length($val) <= $arg->[-1] ) : length($val) <= $arg; # warn Dumper $result; return $result; }; =begin # doesn't work after Data::FormValidator updated from 4.63 to 4.71 # if $arg is arrayref, assume length_between, otherwise max_length: # return # ref $arg eq 'ARRAY' ? # FV_length_between(@$arg) : # FV_max_length($arg); =cut } #------------------------------------------------------------------------------- # checks param is either unique, or belongs to submitted record id: sub check_param_ownership { my $self = shift; my $tbl_info = shift; # arryref of 'col_name' & 'class' return sub { my $dfv = shift; # warn Dumper $dfv; my $data = $dfv->get_filtered_data; # warn Dumper $data; $dfv->name_this('ownership'); my %args = ( col => $tbl_info->[0], # can be arrayref of col_names class => $tbl_info->[1], data => $data, ); # warn Dumper \%args; # returns 'true' if param unique, or already belongs to record id: my $unique_or_belongs_to_record = $self->models->{validate_param_ownership}->(\%args); # so just return value of expression: return $unique_or_belongs_to_record; }; } #------------------------------------------------------------------------------- # check service entry in application email matches logged in user centre: sub check_lims_service { my ($self, $service) = @_; # warn $service; return sub { my $dfv = shift; # warn Dumper $dfv; $dfv->name_this('lims_service'); my $val = $dfv->get_current_constraint_value(); # warn $val; return ( $val eq $service ); } } #------------------------------------------------------------------------------- # allows for form to submit several date params, eg request_year, dob_year, etc sub _get_date_param_names { my $self = shift; my $prefix = shift; # optional, used if form has >1 date type (eg request date, dob) my @param_names = qw(year month day hour minute); if ($prefix) { # eg validating request_year, request_day, etc map { $_ = $prefix . '_' . $_ } @param_names; # eg year => dob_year } # warn Dumper \@param_names; return \@param_names; } 1;