Source code for fastpyxl.worksheet.datavalidation

# Copyright (c) 2010-2024 fastpyxl

from collections import defaultdict
from itertools import chain
from operator import itemgetter

from fastpyxl.typed_serialisable.base import Serialisable
from fastpyxl.typed_serialisable.errors import FieldValidationError
from fastpyxl.typed_serialisable.fields import AliasField, Field

from fastpyxl.utils import (
    rows_from_range,
    coordinate_to_tuple,
    get_column_letter,
)
from fastpyxl.worksheet.cell_range import MultiCellRange


[docs] def collapse_cell_addresses(cells, input_ranges=()): """ Collapse a collection of cell co-ordinates down into an optimal range or collection of ranges. E.g. Cells A1, A2, A3, B1, B2 and B3 should have the data-validation object applied, attempt to collapse down to a single range, A1:B3. Currently only collapsing contiguous vertical ranges (i.e. above example results in A1:A3 B1:B3). """ ranges = list(input_ranges) # convert cell into row, col tuple raw_coords = (coordinate_to_tuple(cell) for cell in cells) # group by column in order grouped_coords = defaultdict(list) for row, col in sorted(raw_coords, key=itemgetter(1)): grouped_coords[col].append(row) # create range string from first and last row in column for col, cells in grouped_coords.items(): col = get_column_letter(col) fmt = "{0}{1}:{2}{3}" if len(cells) == 1: fmt = "{0}{1}" r = fmt.format(col, min(cells), col, max(cells)) ranges.append(r) return " ".join(ranges)
[docs] def expand_cell_ranges(range_string): """ Expand cell ranges to a sequence of addresses. Reverse of collapse_cell_addresses Eg. converts "A1:A2 B1:B2" to (A1, A2, B1, B2) """ # expand ranges to rows and then flatten rows = (rows_from_range(rs) for rs in range_string.split()) # list of rows cells = (chain(*row) for row in rows) # flatten rows return set(chain(*cells))
[docs] class DataValidation(Serialisable): tagname = "dataValidation" sqref: MultiCellRange | None = Field.attribute(expected_type=MultiCellRange, default=None) cells = AliasField("sqref", default=None) ranges = AliasField("sqref", default=None) showDropDown: bool | None = Field.attribute(expected_type=bool, allow_none=True, default=None) hide_drop_down = AliasField('showDropDown', default=None) showInputMessage: bool | None = Field.attribute(expected_type=bool, allow_none=True, default=None) showErrorMessage: bool | None = Field.attribute(expected_type=bool, allow_none=True, default=None) allowBlank: bool | None = Field.attribute(expected_type=bool, allow_none=True, default=None) allow_blank = AliasField('allowBlank', default=None) errorTitle: str | None = Field.attribute(expected_type=str, allow_none=True, default=None) error: str | None = Field.attribute(expected_type=str, allow_none=True, default=None) promptTitle: str | None = Field.attribute(expected_type=str, allow_none=True, default=None) prompt: str | None = Field.attribute(expected_type=str, allow_none=True, default=None) formula1: str | None = Field.nested_text(allow_none=True, expected_type=str, default=None) formula2: str | None = Field.nested_text(allow_none=True, expected_type=str, default=None) type: str | None = Field.attribute( expected_type=str, allow_none=True, converter=lambda v: _enum_converter(v, ("whole", "decimal", "list", "date", "time", "textLength", "custom"), "type"), default=None, ) errorStyle: str | None = Field.attribute( expected_type=str, allow_none=True, converter=lambda v: _enum_converter(v, ("stop", "warning", "information"), "errorStyle"), default=None, ) imeMode: str | None = Field.attribute( expected_type=str, allow_none=True, converter=lambda v: _enum_converter(v, ("noControl", "off", "on", "disabled", "hiragana", "fullKatakana", "halfKatakana", "fullAlpha", "halfAlpha", "fullHangul", "halfHangul"), "imeMode"), default=None, ) operator: str | None = Field.attribute( expected_type=str, allow_none=True, converter=lambda v: _enum_converter(v, ("between", "notBetween", "equal", "notEqual", "lessThan", "lessThanOrEqual", "greaterThan", "greaterThanOrEqual"), "operator"), default=None, ) validation_type = AliasField('type', default=None) xml_order = ("formula1", "formula2") def __init__(self, type=None, formula1=None, formula2=None, showErrorMessage=False, showInputMessage=False, showDropDown=False, allowBlank=False, sqref=(), promptTitle=None, errorStyle=None, error=None, prompt=None, errorTitle=None, imeMode=None, operator=None, allow_blank=None, ): self.sqref = MultiCellRange(sqref) self.showDropDown = showDropDown self.imeMode = imeMode self.operator = operator self.formula1 = formula1 self.formula2 = formula2 if allow_blank is not None: allowBlank = allow_blank self.allowBlank = allowBlank self.showErrorMessage = showErrorMessage self.showInputMessage = showInputMessage self.type = type self.promptTitle = promptTitle self.errorStyle = errorStyle self.error = error self.prompt = prompt self.errorTitle = errorTitle
[docs] def add(self, cell): """Adds a cell or cell coordinate to this validator""" if hasattr(cell, "coordinate"): cell = cell.coordinate self.sqref += cell
def __contains__(self, cell): if hasattr(cell, "coordinate"): cell = cell.coordinate sq = self.sqref return sq is not None and cell in sq
[docs] class DataValidationList(Serialisable): tagname = "dataValidations" disablePrompts: bool | None = Field.attribute(expected_type=bool, allow_none=True, default=None) xWindow: int | None = Field.attribute(expected_type=int, allow_none=True, default=None) yWindow: int | None = Field.attribute(expected_type=int, allow_none=True, default=None) dataValidation: list[DataValidation] = Field.sequence(expected_type=DataValidation, default=list) def __init__(self, disablePrompts=None, xWindow=None, yWindow=None, count=None, dataValidation=(), ): del count self.disablePrompts = disablePrompts self.xWindow = xWindow self.yWindow = yWindow self.dataValidation = list(dataValidation) @property def count(self): return len(self) def __len__(self): return len(self.dataValidation) def __iter__(self): for k, v in super().__iter__(): yield k, v yield 'count', str(self.count)
[docs] def append(self, dv): self.dataValidation.append(dv)
[docs] def to_tree(self, tagname=None, idx=None, namespace=None): """ Need to skip validations that have no cell ranges """ ranges = self.dataValidation # copy self.dataValidation = [r for r in self.dataValidation if bool(r.sqref)] xml = super().to_tree(tagname, idx, namespace) self.dataValidation = ranges return xml
def _enum_converter(value, allowed_values, field_name: str): if value is None: return None if value not in allowed_values: raise FieldValidationError(f"{field_name} rejected value {value!r}") return value