22import functools
33import logging
44import os
5+ from dataclasses import dataclass
56from datetime import datetime
67from enum import Enum
8+ from typing import Any
79
810import httpx
911from adaptive_cards import card_types as ct
1012from adaptive_cards .actions import ActionOpenUrl
1113from adaptive_cards .card import AdaptiveCard
12- from adaptive_cards .containers import Column , ColumnSet
14+ from adaptive_cards .card_types import MSTeams , MSTeamsCardWidth
15+ from adaptive_cards .containers import Column , ColumnSet , Container
1316from adaptive_cards .elements import TextBlock
1417from django .conf import settings
1518from jinja2 import Environment , FileSystemLoader , select_autoescape
1619from markdown_it import MarkdownIt
1720from mpt_extension_sdk .mpt_http .base import MPTClient
18- from mpt_extension_sdk .mpt_http .mpt import get_rendered_template , notify
21+ from mpt_extension_sdk .mpt_http .mpt import (
22+ get_rendered_template ,
23+ notify ,
24+ )
1925
2026from ffc .flows .order import OrderContext
2127from ffc .parameters import PARAM_CONTACT , get_ordering_parameter
2228
2329logger = logging .getLogger (__name__ )
24- NotifyCategories = Enum ("NotifyCategories" , settings .MPT_NOTIFY_CATEGORIES )
30+ NotifyCategories = Enum ("NotifyCategories" , settings .MPT_NOTIFY_CATEGORIES ) # type: ignore[misc]
2531
2632
2733def dateformat (date_string ):
@@ -40,6 +46,7 @@ def dateformat(date_string):
4046
4147env .filters ["dateformat" ] = dateformat
4248
49+
4350def mpt_notify (
4451 mpt_client ,
4552 account_id : str ,
@@ -117,9 +124,9 @@ def send_mpt_notification(client: MPTClient, order_context: type[OrderContext])
117124 "portal_base_url" : settings .MPT_PORTAL_BASE_URL ,
118125 }
119126 buyer_name = order_context .order ["agreement" ]["buyer" ]["name" ]
120- subject = f"Order status update { order_context .order_id } " f" for { buyer_name } "
127+ subject = f"Order status update { order_context .order_id } for { buyer_name } "
121128 if order_context .order ["status" ] == "Querying" :
122- subject = f"This order need your attention { order_context .order_id } " f" for { buyer_name } "
129+ subject = f"This order need your attention { order_context .order_id } for { buyer_name } "
123130 mpt_notify (
124131 client ,
125132 order_context .order ["agreement" ]["client" ]["id" ],
@@ -142,41 +149,86 @@ def notify_unhandled_exception_in_teams(process, order_id, traceback): # pragma
142149 )
143150
144151
152+ @dataclass
153+ class ColumnHeader :
154+ text : str
155+ width : str = "auto"
156+ horizontal_alignment : ct .HorizontalAlignment | None = None
157+
145158
146159class NotificationDetails :
147- def __init__ (self , header : tuple [str , ...], rows : list [tuple [str , ...]]):
160+ def __init__ (self , header : tuple [str | ColumnHeader , ...], rows : list [tuple [str , ...]]):
148161 if not all (len (t ) == len (header ) for t in rows ):
149162 raise ValueError ("All rows must have the same number of columns as the header." )
150163 self .header = header
151164 self .rows = rows
152165
153- def to_column_set (self ) -> ColumnSet :
154- columns = []
155- for title in self .header :
156- column = Column (
157- width = "auto" ,
158- items = [
159- TextBlock (
160- text = title ,
161- weight = ct .FontWeight .BOLDER ,
162- wrap = True ,
163- )
164- ],
166+ @staticmethod
167+ def _get_header_text_and_width (col : str | ColumnHeader ) -> tuple [str , str ]:
168+ if isinstance (col , ColumnHeader ):
169+ return col .text , col .width
170+ return str (col ), "auto"
171+
172+ def to_container (self ) -> Container :
173+ items = []
174+
175+ # Header row
176+ header_columns = []
177+ for col in self .header :
178+ text , width = self ._get_header_text_and_width (col )
179+ alignment = (
180+ col .horizontal_alignment .value
181+ if isinstance (col , ColumnHeader ) and col .horizontal_alignment
182+ else None
165183 )
166- columns .append (column )
167-
168- column_set = ColumnSet (columns = columns )
169- for row_idx , row in enumerate (self .rows ):
170- for col_idx , item in enumerate (row ):
171- columns [col_idx ].items .append (
172- TextBlock (
173- text = item ,
174- wrap = True ,
175- color = ct .Colors .DEFAULT if row_idx % 2 == 0 else ct .Colors .ACCENT ,
184+ header_columns .append (
185+ Column (
186+ width = width ,
187+ items = [
188+ TextBlock (
189+ text = text ,
190+ horizontal_alignment = alignment ,
191+ weight = ct .FontWeight .BOLDER ,
192+ wrap = True ,
193+ color = ct .Colors .ACCENT ,
194+ )
195+ ],
196+ )
197+ )
198+ items .append (ColumnSet (columns = header_columns ))
199+
200+ # Data rows
201+ for _idx , row in enumerate (self .rows ):
202+ row_columns = []
203+ for col_idx , value in enumerate (row ):
204+ col = self .header [col_idx ]
205+ _ , width = self ._get_header_text_and_width (col )
206+ alignment = (
207+ col .horizontal_alignment .value
208+ if isinstance (col , ColumnHeader ) and col .horizontal_alignment
209+ else None
210+ )
211+ row_columns .append (
212+ Column (
213+ width = width ,
214+ items = [
215+ TextBlock (
216+ text = value ,
217+ horizontal_alignment = alignment ,
218+ wrap = True ,
219+ color = ct .Colors .DEFAULT ,
220+ )
221+ ],
176222 )
177223 )
224+ items .append (
225+ ColumnSet (
226+ columns = row_columns ,
227+ spacing = ct .Spacing .SMALL ,
228+ )
229+ )
178230
179- return column_set
231+ return Container ( items = items )
180232
181233
182234async def send_notification (
@@ -190,7 +242,7 @@ async def send_notification(
190242 logger .warning ("MSTeams notifications are disabled." )
191243 return
192244
193- card_items = [
245+ card_items : list [ Any ] = [
194246 TextBlock (
195247 text = title ,
196248 size = ct .FontSize .LARGE ,
@@ -205,19 +257,17 @@ async def send_notification(
205257 ),
206258 ]
207259 if details :
208- card_items .append (details .to_column_set ())
260+ card_items .append (details .to_container ())
209261
262+ card_actions = []
210263 if open_url :
211- card_items .append (
212- ActionOpenUrl (
213- title = "Open" ,
214- url = open_url ,
215- )
216- )
264+ card_actions .append (ActionOpenUrl (title = "Open" , url = open_url ))
217265
218- version : str = "1.4"
219- card : AdaptiveCard = AdaptiveCard .new ().version (version ).add_items (card_items ).create ()
220- card .msteams = {"width" : "Full" }
266+ card = (
267+ AdaptiveCard .new ().version ("1.4" ).add_items (card_items ).add_actions (card_actions ).create ()
268+ )
269+
270+ card .msteams = MSTeams (width = MSTeamsCardWidth .FULL )
221271 message = {
222272 "type" : "message" ,
223273 "attachments" : [
@@ -227,6 +277,7 @@ async def send_notification(
227277 }
228278 ],
229279 }
280+
230281 async with httpx .AsyncClient () as client :
231282 response = await client .post (
232283 settings .EXTENSION_CONFIG ["MSTEAMS_NOTIFICATIONS_WEBHOOKS_URL" ],
0 commit comments