Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions slack_sdk/models/blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
CarouselBlock,
ContextActionsBlock,
ContextBlock,
DataTableBlock,
DividerBlock,
FileBlock,
HeaderBlock,
Expand Down Expand Up @@ -139,6 +140,7 @@
"CarouselBlock",
"ContextActionsBlock",
"ContextBlock",
"DataTableBlock",
"DividerBlock",
"FileBlock",
"HeaderBlock",
Expand Down
62 changes: 62 additions & 0 deletions slack_sdk/models/blocks/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]:
return AlertBlock(**block)
elif type == CarouselBlock.type:
return CarouselBlock(**block)
elif type == DataTableBlock.type:
return DataTableBlock(**block)
else:
cls.logger.warning(f"Unknown block detected and skipped ({block})")
return None
Expand Down Expand Up @@ -1030,3 +1032,63 @@ def _validate_elements_present(self):
@JsonValidator(f"elements attribute cannot exceed {elements_max_length} cards")
def _validate_elements_length(self):
return self.elements is None or len(self.elements) <= self.elements_max_length


class DataTableBlock(Block):
type = "data_table"
rows_max_length = 101
columns_max_length = 20
page_size_min = 1
page_size_max = 100

@property
def attributes(self) -> Set[str]: # type: ignore[override]
return super().attributes.union({"rows", "caption", "page_size", "row_header_column_index"})

def __init__(
self,
*,
rows: Sequence[Sequence[Dict[str, Any]]],
caption: str,
page_size: Optional[int] = None,
row_header_column_index: Optional[int] = None,
block_id: Optional[str] = None,
**others: dict,
):
"""Displays structured, paginated data in a table with a required caption.
https://docs.slack.dev/reference/block-kit/blocks/data-table-block

Args:
rows (required): An array consisting of table rows. Minimum 2 rows (header plus one data row)
and maximum 101 rows (header plus 100 data rows). All rows must have an identical column
count, with a maximum of 20 columns. Each cell has a type of raw_text, raw_number, or
rich_text. The total character limit across all cells is 10,000.
caption (required): A caption for the table; used as the value for the HTML caption element.
page_size: The number of rows to show per page. Min 1, Max 100. Defaults to 5 if omitted.
row_header_column_index: The 0-based index of the column that uniquely identifies each row
(the row header). Defaults to 0 if omitted.
block_id: A unique identifier for a block. If not specified, a block_id will be generated.
You can use this block_id when you receive an interaction payload to identify the source
of the action. Maximum length for this field is 255 characters.
block_id should be unique for each message and each iteration of a message.
If a message is updated, use a new block_id.
"""
super().__init__(type=self.type, block_id=block_id)
show_unknown_key_warning(self, others)

self.rows = rows
self.caption = caption
self.page_size = page_size
self.row_header_column_index = row_header_column_index

@JsonValidator("rows attribute must be specified")
def _validate_rows(self):
return self.rows is not None and len(self.rows) > 0

@JsonValidator("caption attribute must be specified")
def _validate_caption(self):
return self.caption is not None

@JsonValidator(f"page_size must be between {page_size_min} and {page_size_max}")
def _validate_page_size(self):
return self.page_size is None or self.page_size_min <= self.page_size <= self.page_size_max
76 changes: 76 additions & 0 deletions tests/slack_sdk/models/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CarouselBlock,
ContextActionsBlock,
ContextBlock,
DataTableBlock,
DividerBlock,
FileBlock,
HeaderBlock,
Expand Down Expand Up @@ -1556,6 +1557,81 @@ def test_with_raw_text_object_helper(self):
self.assertDictEqual(expected, block.to_dict())


class DataTableBlockTests(unittest.TestCase):
def test_document(self):
"""Test basic data table block from Slack documentation example"""
input = {
"type": "data_table",
"caption": "Quarterly sales by region",
"rows": [
[{"type": "raw_text", "text": "Region"}, {"type": "raw_text", "text": "Sales"}],
[{"type": "raw_text", "text": "West"}, {"type": "raw_number", "value": 120, "text": "120"}],
[{"type": "raw_text", "text": "East"}, {"type": "raw_number", "value": 95, "text": "95"}],
],
}
self.assertDictEqual(input, DataTableBlock(**input).to_dict())
self.assertDictEqual(input, Block.parse(input).to_dict())

def test_all_fields(self):
"""Test data table block with every optional field set"""
input = {
"type": "data_table",
"block_id": "data-table-123",
"caption": "User directory",
"page_size": 25,
"row_header_column_index": 1,
"rows": [
[{"type": "raw_text", "text": "ID"}, {"type": "raw_text", "text": "Name"}],
[{"type": "raw_number", "value": 1, "text": "1"}, {"type": "raw_text", "text": "Alice"}],
],
}
self.assertDictEqual(input, DataTableBlock(**input).to_dict())
self.assertDictEqual(input, Block.parse(input).to_dict())

def test_with_rich_text(self):
"""Test data table block with rich_text cells"""
input = {
"type": "data_table",
"caption": "Links",
"rows": [
[{"type": "raw_text", "text": "Site"}],
[
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [{"text": "Slack", "type": "link", "url": "https://slack.com"}],
}
],
},
],
],
}
self.assertDictEqual(input, DataTableBlock(**input).to_dict())
self.assertDictEqual(input, Block.parse(input).to_dict())

def test_rows_validation(self):
"""Test that empty rows fail validation"""
with self.assertRaises(SlackObjectFormationError):
DataTableBlock(caption="empty", rows=[]).to_dict()

def test_caption_required(self):
"""Test that DataTableBlock requires a caption argument"""
with self.assertRaises(TypeError):
DataTableBlock(rows=[[{"type": "raw_text", "text": "A"}]])

def test_page_size_validation(self):
"""Test that page_size outside the allowed range fails validation"""
rows = [[{"type": "raw_text", "text": "A"}], [{"type": "raw_text", "text": "B"}]]
with self.assertRaises(SlackObjectFormationError):
DataTableBlock(caption="too small", rows=rows, page_size=0).to_dict()
with self.assertRaises(SlackObjectFormationError):
DataTableBlock(caption="too big", rows=rows, page_size=101).to_dict()
# A valid page_size should pass
DataTableBlock(caption="ok", rows=rows, page_size=50).to_dict()


class CardBlockTests(unittest.TestCase):
def test_document(self):
input = {
Expand Down