"""Validation functions for trans-row validations. The rows are tuples of openpyxl cell objects. The header is a list of strings representing the header row of the spreadsheet. The functions in this module are used to validate data across rows of a spreadsheet.""" from constants import field_constants as fc def uniform_field_values(rows, header, key_field, description, fields): """Ensure that all values in a list of fields are the same. For each field in the list, ensure that all rows have the same value at that field. Args: rows (list): A list of dictionaries. Each dictionary represents a row of data. The keys are the field names and the values are openpyxl cell objects. header (list): The header row associated with the rows. key_field (str): The field the rows are grouped by. description (str): The description of the validation. fields (list): A list of fields to check. Returns: None or tuple: None if the validation passes, otherwise a tuple containing the error code and error message. """ if not description: description = 'Uniform Field Values' # Check the conditions and return the appropriate result for field in fields: if len(set([row[field].value for row in rows])) > 1: return (description, f'{rows[0][key_field].value} -' f'All values in {field} must be the same.') return def distinct_field_values(rows, header, key_field, description, fields): """Ensure that all values in a field are distinct. For each field in the list, ensure that all rows have a unique value at that field. Args: rows (list): A list of dictionaries. Each dictionary represents a row of data. The keys are the field names and the values are openpyxl cell objects. header (list): The header row associated with the rows. key_field (str): The field the rows are grouped by. description (str): The description of the validation. fields (list): A list of fields to check. Returns: None or tuple: None if the validation passes, otherwise a tuple containing the error code and error message. """ if not description: description = 'Distinct Field Values' # Check the conditions and return the appropriate result for field in fields: if len(set([row[field].value for row in rows])) != len(rows): return (description, f'{rows[0][key_field].value} -' f'All values in {field} must be distinct.') def consecutive_values(rows, header, key_field, description, field): """Ensure that the values in a field are consecutive. The values should start from 1 and proceed sequentially. Args: rows (list): A list of dictionaries. Each dictionary represents a row of data. The keys are the field names and the values are openpyxl cell objects. header (list): The header row associated with the rows. key_field (str): The field the rows are grouped by. description (str): The description of the validation. field (str): The field to check. Returns: None or tuple: None if the validation passes, otherwise a tuple containing the error code and error message. """ if not description: description = 'Consecutive Values' # Check the conditions and return the appropriate result values = sorted([row[field].value for row in rows]) if values != list(range(1, len(values) + 1)): return (description, f'{rows[0][key_field].value} -' f'The values in {field} must be consecutive.') def validate_track_and_volume_numbers(rows, header, key_field, description): """Ensure that track and volume numbers are sequential. Volume should start from 1, and proceed sequentially. Track numbers should also start from 1 and proceed sequentially. Each Volume should have a track number starting from 1. There should be no gaps in the track numbers on a volume, and no gaps in the volume numbers. Args: rows (list): A list of dictionaries. Each dictionary represents a row of data. The keys are the field names and the values are openpyxl cell objects. header (list): The header row associated with the rows. key_field (str): The field the rows are grouped by. NOT USED. description (str): The description of the validation. NOT USED. Returns: None or tuple: None if the validation passes, otherwise a tuple containing the error code and error message. """ if not description: description = 'Validate Track and Volume Numbers' # Check that every track and volume is either an int, or a string that is # an int and that it's not None for row in rows: if not (isinstance(row[fc.TRACK_NO].value, int) or (isinstance(row[fc.TRACK_NO].value, str) and row[fc.TRACK_NO].value.isdigit())): return if not (isinstance(row[fc.VOLUME].value, int) or (isinstance(row[fc.VOLUME].value, str) and row[fc.VOLUME].value.isdigit())): return # Check Tracks alone - sort without de-dupe track_numbers = sorted([row[fc.TRACK_NO].value for row in rows]) if track_numbers != list(range(1, len(track_numbers)+1)): return (description, f'{rows[0][key_field].value} -' f'The values in {fc.TRACK_NO} must be consecutive.') # Check Volumes alone - sort and de-dupe volume_numbers = sorted(set([row[fc.VOLUME].value for row in rows])) if volume_numbers != list(range(1, len(volume_numbers)+1)): return (description, f'{rows[0][key_field].value} -' f'The values in {fc.VOLUME} must be consecutive.') # Check tracks per sorted, de-duped volumes for volume in volume_numbers: # Get the track numbers for the volume volume_tracks = set([row[fc.TRACK_NO].value for row in rows if row[fc.VOLUME].value == volume]) # Check if the track numbers are consecutive if list(volume_tracks) != list(range(1, len(volume_tracks)+1)): return (description, f'{rows[0][key_field].value} -' f'Volume {volume} has non-consecutive track numbers.')