datavalidation.py 6.0 KB

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