datavalidation.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. # Copyright (c) 2010-2024 openpyxl
  2. from collections import defaultdict
  3. from itertools import chain
  4. from operator import itemgetter
  5. from openpyxl.descriptors.serialisable import Serialisable
  6. from openpyxl.descriptors import (
  7. Bool,
  8. NoneSet,
  9. String,
  10. Sequence,
  11. Alias,
  12. Integer,
  13. Convertible,
  14. )
  15. from openpyxl.descriptors.nested import NestedText
  16. from openpyxl.utils import (
  17. rows_from_range,
  18. coordinate_to_tuple,
  19. get_column_letter,
  20. )
  21. def collapse_cell_addresses(cells, input_ranges=()):
  22. """ Collapse a collection of cell co-ordinates down into an optimal
  23. range or collection of ranges.
  24. E.g. Cells A1, A2, A3, B1, B2 and B3 should have the data-validation
  25. object applied, attempt to collapse down to a single range, A1:B3.
  26. Currently only collapsing contiguous vertical ranges (i.e. above
  27. example results in A1:A3 B1:B3).
  28. """
  29. ranges = list(input_ranges)
  30. # convert cell into row, col tuple
  31. raw_coords = (coordinate_to_tuple(cell) for cell in cells)
  32. # group by column in order
  33. grouped_coords = defaultdict(list)
  34. for row, col in sorted(raw_coords, key=itemgetter(1)):
  35. grouped_coords[col].append(row)
  36. # create range string from first and last row in column
  37. for col, cells in grouped_coords.items():
  38. col = get_column_letter(col)
  39. fmt = "{0}{1}:{2}{3}"
  40. if len(cells) == 1:
  41. fmt = "{0}{1}"
  42. r = fmt.format(col, min(cells), col, max(cells))
  43. ranges.append(r)
  44. return " ".join(ranges)
  45. def expand_cell_ranges(range_string):
  46. """
  47. Expand cell ranges to a sequence of addresses.
  48. Reverse of collapse_cell_addresses
  49. Eg. converts "A1:A2 B1:B2" to (A1, A2, B1, B2)
  50. """
  51. # expand ranges to rows and then flatten
  52. rows = (rows_from_range(rs) for rs in range_string.split()) # list of rows
  53. cells = (chain(*row) for row in rows) # flatten rows
  54. return set(chain(*cells))
  55. from .cell_range import MultiCellRange
  56. class DataValidation(Serialisable):
  57. tagname = "dataValidation"
  58. sqref = Convertible(expected_type=MultiCellRange)
  59. cells = Alias("sqref")
  60. ranges = Alias("sqref")
  61. showDropDown = Bool(allow_none=True)
  62. hide_drop_down = Alias('showDropDown')
  63. showInputMessage = Bool(allow_none=True)
  64. showErrorMessage = Bool(allow_none=True)
  65. allowBlank = Bool(allow_none=True)
  66. allow_blank = Alias('allowBlank')
  67. errorTitle = String(allow_none = True)
  68. error = String(allow_none = True)
  69. promptTitle = String(allow_none = True)
  70. prompt = String(allow_none = True)
  71. formula1 = NestedText(allow_none=True, expected_type=str)
  72. formula2 = NestedText(allow_none=True, expected_type=str)
  73. type = NoneSet(values=("whole", "decimal", "list", "date", "time",
  74. "textLength", "custom"))
  75. errorStyle = NoneSet(values=("stop", "warning", "information"))
  76. imeMode = NoneSet(values=("noControl", "off", "on", "disabled",
  77. "hiragana", "fullKatakana", "halfKatakana", "fullAlpha","halfAlpha",
  78. "fullHangul", "halfHangul"))
  79. operator = NoneSet(values=("between", "notBetween", "equal", "notEqual",
  80. "lessThan", "lessThanOrEqual", "greaterThan", "greaterThanOrEqual"))
  81. validation_type = Alias('type')
  82. def __init__(self,
  83. type=None,
  84. formula1=None,
  85. formula2=None,
  86. showErrorMessage=False,
  87. showInputMessage=False,
  88. showDropDown=False,
  89. allowBlank=False,
  90. sqref=(),
  91. promptTitle=None,
  92. errorStyle=None,
  93. error=None,
  94. prompt=None,
  95. errorTitle=None,
  96. imeMode=None,
  97. operator=None,
  98. allow_blank=None,
  99. ):
  100. self.sqref = sqref
  101. self.showDropDown = showDropDown
  102. self.imeMode = imeMode
  103. self.operator = operator
  104. self.formula1 = formula1
  105. self.formula2 = formula2
  106. if allow_blank is not None:
  107. allowBlank = allow_blank
  108. self.allowBlank = allowBlank
  109. self.showErrorMessage = showErrorMessage
  110. self.showInputMessage = showInputMessage
  111. self.type = type
  112. self.promptTitle = promptTitle
  113. self.errorStyle = errorStyle
  114. self.error = error
  115. self.prompt = prompt
  116. self.errorTitle = errorTitle
  117. def add(self, cell):
  118. """Adds a cell or cell coordinate to this validator"""
  119. if hasattr(cell, "coordinate"):
  120. cell = cell.coordinate
  121. self.sqref += cell
  122. def __contains__(self, cell):
  123. if hasattr(cell, "coordinate"):
  124. cell = cell.coordinate
  125. return cell in self.sqref
  126. class DataValidationList(Serialisable):
  127. tagname = "dataValidations"
  128. disablePrompts = Bool(allow_none=True)
  129. xWindow = Integer(allow_none=True)
  130. yWindow = Integer(allow_none=True)
  131. dataValidation = Sequence(expected_type=DataValidation)
  132. __elements__ = ('dataValidation',)
  133. __attrs__ = ('disablePrompts', 'xWindow', 'yWindow', 'count')
  134. def __init__(self,
  135. disablePrompts=None,
  136. xWindow=None,
  137. yWindow=None,
  138. count=None,
  139. dataValidation=(),
  140. ):
  141. self.disablePrompts = disablePrompts
  142. self.xWindow = xWindow
  143. self.yWindow = yWindow
  144. self.dataValidation = dataValidation
  145. @property
  146. def count(self):
  147. return len(self)
  148. def __len__(self):
  149. return len(self.dataValidation)
  150. def append(self, dv):
  151. self.dataValidation.append(dv)
  152. def to_tree(self, tagname=None):
  153. """
  154. Need to skip validations that have no cell ranges
  155. """
  156. ranges = self.dataValidation # copy
  157. self.dataValidation = [r for r in self.dataValidation if bool(r.sqref)]
  158. xml = super().to_tree(tagname)
  159. self.dataValidation = ranges
  160. return xml