django-slack-tools 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. django_slack_tools/__init__.py +0 -1
  2. django_slack_tools/app_settings.py +54 -90
  3. django_slack_tools/locale/ko_KR/LC_MESSAGES/django.po +150 -186
  4. django_slack_tools/py.typed +0 -0
  5. django_slack_tools/slack_messages/admin/message.py +1 -52
  6. django_slack_tools/slack_messages/admin/message_recipient.py +2 -2
  7. django_slack_tools/slack_messages/admin/messaging_policy.py +5 -5
  8. django_slack_tools/slack_messages/backends/__init__.py +3 -3
  9. django_slack_tools/slack_messages/backends/base.py +39 -203
  10. django_slack_tools/slack_messages/backends/dummy.py +0 -12
  11. django_slack_tools/slack_messages/backends/{logging.py → logging_.py} +0 -6
  12. django_slack_tools/slack_messages/backends/slack.py +21 -75
  13. django_slack_tools/slack_messages/message_templates/__init__.py +5 -0
  14. django_slack_tools/slack_messages/message_templates/base.py +17 -0
  15. django_slack_tools/{utils/template → slack_messages/message_templates}/django.py +30 -25
  16. django_slack_tools/slack_messages/message_templates/python.py +38 -0
  17. django_slack_tools/slack_messages/messenger.py +158 -0
  18. django_slack_tools/slack_messages/middlewares/__init__.py +4 -0
  19. django_slack_tools/slack_messages/middlewares/base.py +34 -0
  20. django_slack_tools/slack_messages/middlewares/django.py +218 -0
  21. django_slack_tools/slack_messages/migrations/0001_initial.py +4 -4
  22. django_slack_tools/slack_messages/migrations/0005_alter_slackmessagingpolicy_template_type.py +46 -0
  23. django_slack_tools/slack_messages/migrations/0006_alter_slackmessage_id.py +26 -0
  24. django_slack_tools/slack_messages/models/mention.py +1 -1
  25. django_slack_tools/slack_messages/models/message.py +5 -2
  26. django_slack_tools/slack_messages/models/message_recipient.py +1 -1
  27. django_slack_tools/slack_messages/models/messaging_policy.py +4 -4
  28. django_slack_tools/slack_messages/request.py +91 -0
  29. django_slack_tools/slack_messages/response.py +21 -0
  30. django_slack_tools/slack_messages/shortcuts.py +78 -0
  31. django_slack_tools/slack_messages/tasks.py +16 -49
  32. django_slack_tools/slack_messages/template_loaders/__init__.py +11 -0
  33. django_slack_tools/slack_messages/template_loaders/base.py +16 -0
  34. django_slack_tools/slack_messages/template_loaders/django.py +74 -0
  35. django_slack_tools/slack_messages/template_loaders/errors.py +9 -0
  36. django_slack_tools/{utils/slack/django.py → slack_messages/validators.py} +1 -1
  37. django_slack_tools/utils/django/__init__.py +0 -0
  38. django_slack_tools/utils/import_helper.py +37 -0
  39. django_slack_tools/utils/repr.py +10 -0
  40. django_slack_tools/utils/slack/__init__.py +1 -9
  41. {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/METADATA +12 -60
  42. django_slack_tools-0.3.0.dist-info/RECORD +58 -0
  43. {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/WHEEL +1 -1
  44. django_slack_tools/slack_messages/message.py +0 -110
  45. django_slack_tools/utils/slack/message.py +0 -93
  46. django_slack_tools/utils/template/__init__.py +0 -5
  47. django_slack_tools/utils/template/base.py +0 -17
  48. django_slack_tools/utils/template/dict.py +0 -52
  49. django_slack_tools-0.2.2.dist-info/RECORD +0 -43
  50. /django_slack_tools/utils/{model_mixins.py → django/model_mixins.py} +0 -0
  51. /django_slack_tools/utils/{widgets.py → django/widgets.py} +0 -0
  52. {django_slack_tools-0.2.2.dist-info → django_slack_tools-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,38 @@
1
+ # noqa: D100
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any, TypeVar
6
+
7
+ from .base import BaseTemplate
8
+
9
+ _PyObj = TypeVar("_PyObj", dict, list, str)
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class PythonTemplate(BaseTemplate[_PyObj]):
15
+ """Template that renders a dictionary."""
16
+
17
+ def __init__(self, template: _PyObj) -> None:
18
+ """Initialize the template."""
19
+ self.template = template
20
+
21
+ def render(self, context: dict[str, Any]) -> _PyObj: # noqa: D102
22
+ logger.debug("Rendering template %r with context %r", self.template, context)
23
+ result = _format_obj(self.template, context=context)
24
+ logger.debug("Rendered template %r to %r", self.template, result)
25
+ return result
26
+
27
+
28
+ def _format_obj(obj: _PyObj, *, context: dict[str, Any]) -> _PyObj:
29
+ if isinstance(obj, dict):
30
+ return {key: _format_obj(value, context=context) for key, value in obj.items()}
31
+
32
+ if isinstance(obj, list):
33
+ return [_format_obj(item, context=context) for item in obj]
34
+
35
+ if isinstance(obj, str):
36
+ return obj.format_map(context)
37
+
38
+ return obj
@@ -0,0 +1,158 @@
1
+ # noqa: D100
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from django_slack_tools.slack_messages.backends import BaseBackend
8
+ from django_slack_tools.slack_messages.middlewares import BaseMiddleware
9
+ from django_slack_tools.slack_messages.request import MessageBody, MessageHeader, MessageRequest
10
+ from django_slack_tools.slack_messages.template_loaders import BaseTemplateLoader, TemplateNotFoundError
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Sequence
14
+
15
+ from django_slack_tools.slack_messages.message_templates import BaseTemplate
16
+ from django_slack_tools.slack_messages.response import MessageResponse
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class Messenger:
22
+ """Messenger class that sends message using templates and middlewares.
23
+
24
+ Components evaluated in order:
25
+
26
+ 1. Request processing middlewares
27
+ 2. Load template by key and render message in-place
28
+ 4. Send message
29
+ 5. Response processing middlewares (in reverse order)
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ *,
35
+ template_loaders: Sequence[BaseTemplateLoader],
36
+ middlewares: Sequence[BaseMiddleware],
37
+ messaging_backend: BaseBackend,
38
+ ) -> None:
39
+ """Initialize the Messenger.
40
+
41
+ Args:
42
+ template_loaders: A sequence of template loaders.
43
+ It is tried in order to load the template and the first one that returns a template is used.
44
+ middlewares: A sequence of middlewares.
45
+ Middlewares are applied in the order they are provided for request, and in reverse order for response.
46
+ messaging_backend: The messaging backend to be used.
47
+ """
48
+ # Validate the template loaders
49
+ for tl in template_loaders:
50
+ if not isinstance(tl, BaseTemplateLoader):
51
+ msg = f"Expected inherited from {BaseTemplateLoader!s}, got {type(tl)}"
52
+ raise TypeError(msg)
53
+
54
+ self.template_loaders = template_loaders
55
+
56
+ # Validate the middlewares
57
+ for mw in middlewares:
58
+ if not isinstance(mw, BaseMiddleware):
59
+ msg = f"Expected inherited from {BaseMiddleware!s}, got {type(mw)}"
60
+ raise TypeError(msg)
61
+
62
+ self.middlewares = middlewares
63
+
64
+ # Validate the messaging backend
65
+ if not isinstance(messaging_backend, BaseBackend):
66
+ msg = f"Expected inherited from {BaseBackend!s}, got {type(messaging_backend)}"
67
+ raise TypeError(msg)
68
+
69
+ self.messaging_backend = messaging_backend
70
+
71
+ def send(
72
+ self,
73
+ to: str,
74
+ *,
75
+ template: str | None = None,
76
+ context: dict[str, str],
77
+ header: MessageHeader | dict[str, Any] | None = None,
78
+ ) -> MessageResponse | None:
79
+ """Simplified shortcut for `.send_request()`."""
80
+ header = MessageHeader.model_validate(header or {})
81
+ request = MessageRequest(template_key=template, channel=to, context=context, header=header)
82
+ return self.send_request(request=request)
83
+
84
+ def send_request(self, request: MessageRequest) -> MessageResponse | None:
85
+ """Sends a message request and processes the response."""
86
+ logger.info("Sending request: %s", request)
87
+ _request = self._process_request(request)
88
+ if _request is None:
89
+ return None
90
+
91
+ self._render_message(_request)
92
+ response = self._deliver_message(_request)
93
+ _response = self._process_response(response)
94
+ if _response is None:
95
+ return None
96
+
97
+ logger.info("Response: %s", _response)
98
+ return response
99
+
100
+ def _process_request(self, request: MessageRequest) -> MessageRequest | None:
101
+ """Processes the request with middlewares in forward order."""
102
+ for middleware in self.middlewares:
103
+ logger.debug("Processing request (%s) with middleware %s", request, middleware)
104
+ new_request = middleware.process_request(request)
105
+ if new_request is None:
106
+ logger.warning("Middleware %s returned `None`, skipping remaining middlewares", middleware)
107
+ return None
108
+
109
+ request = new_request
110
+
111
+ logger.debug("Request after processing: %s", request)
112
+ return request
113
+
114
+ def _render_message(self, request: MessageRequest) -> None:
115
+ """Updates the request with rendered message, in-place."""
116
+ if request.body is not None:
117
+ logger.debug("Request already has a body, skipping rendering")
118
+ return
119
+
120
+ if request.template_key is None:
121
+ msg = "Template key is required to render the message"
122
+ raise ValueError(msg)
123
+
124
+ template = self._get_template(request.template_key)
125
+ logger.debug("Rendering request %s with template: %s", request, template)
126
+ rendered = template.render(request.context)
127
+ request.body = MessageBody.model_validate(rendered)
128
+
129
+ def _get_template(self, key: str) -> BaseTemplate:
130
+ """Loads the template by key."""
131
+ for loader in self.template_loaders:
132
+ template = loader.load(key)
133
+ if template is not None:
134
+ return template
135
+
136
+ msg = f"Template with key '{key}' not found"
137
+ raise TemplateNotFoundError(msg)
138
+
139
+ def _deliver_message(self, request: MessageRequest) -> MessageResponse:
140
+ """Invoke the messaging backend to deliver the message."""
141
+ logger.debug("Delivering message request: %s", request)
142
+ response = self.messaging_backend.deliver(request)
143
+ logger.debug("Response after delivery: %s", response)
144
+ return response
145
+
146
+ def _process_response(self, response: MessageResponse) -> MessageResponse | None:
147
+ """Processes the response with middlewares in reverse order."""
148
+ for middleware in reversed(self.middlewares):
149
+ logger.debug("Processing response (%s) with middleware: %s", response, middleware)
150
+ new_response = middleware.process_response(response)
151
+ if new_response is None:
152
+ logger.warning("Middleware %s returned `None`, skipping remaining middlewares", middleware)
153
+ return None
154
+
155
+ response = new_response
156
+
157
+ logger.debug("Response after processing: %s", response)
158
+ return response
@@ -0,0 +1,4 @@
1
+ from .base import BaseMiddleware
2
+ from .django import DjangoDatabasePersister, DjangoDatabasePolicyHandler
3
+
4
+ __all__ = ("BaseMiddleware", "DjangoDatabasePersister", "DjangoDatabasePolicyHandler")
@@ -0,0 +1,34 @@
1
+ # noqa: D100
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from django_slack_tools.slack_messages.request import MessageRequest
8
+ from django_slack_tools.slack_messages.response import MessageResponse
9
+
10
+
11
+ class BaseMiddleware:
12
+ """Base class for middleware components."""
13
+
14
+ def process_request(self, request: MessageRequest) -> MessageRequest | None: # pragma: no cover
15
+ """Process the incoming requests.
16
+
17
+ Args:
18
+ request: Message request.
19
+
20
+ Returns:
21
+ MessageRequest objects or `None`.
22
+ """
23
+ return request
24
+
25
+ def process_response(self, response: MessageResponse) -> MessageResponse | None: # pragma: no cover
26
+ """Processes a sequence of MessageResponse objects and returns the processed sequence.
27
+
28
+ Args:
29
+ response: Message response.
30
+
31
+ Returns:
32
+ MessageResponse objects or `None`.
33
+ """
34
+ return response
@@ -0,0 +1,218 @@
1
+ # noqa: D100
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from slack_bolt import App
8
+ from slack_sdk.errors import SlackApiError
9
+
10
+ from django_slack_tools.slack_messages.models import SlackMessage, SlackMessageRecipient, SlackMessagingPolicy
11
+ from django_slack_tools.slack_messages.request import MessageHeader, MessageRequest
12
+
13
+ from .base import BaseMiddleware
14
+
15
+ if TYPE_CHECKING:
16
+ from django_slack_tools.slack_messages.messenger import Messenger
17
+ from django_slack_tools.slack_messages.response import MessageResponse
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class DjangoDatabasePersister(BaseMiddleware):
23
+ """Persist message history to database. If request is `None`, will do nothing."""
24
+
25
+ def __init__(self, *, slack_app: App | None = None, get_permalink: bool = False) -> None:
26
+ """Initialize the middleware.
27
+
28
+ Args:
29
+ slack_app: Slack app instance to use for certain tasks, such as getting permalinks.
30
+ get_permalink: If `True`, will try to get the permalink of the message.
31
+ """
32
+ if get_permalink and not isinstance(slack_app, App):
33
+ msg = "`slack_app` must be an instance of `App` if `get_permalink` is set `True`."
34
+ raise ValueError(msg)
35
+
36
+ self.slack_app = slack_app
37
+ self.get_permalink = get_permalink
38
+
39
+ def process_response(self, response: MessageResponse) -> MessageResponse | None: # noqa: D102
40
+ request = response.request
41
+ if request is None:
42
+ logger.warning("No request found in response, skipping persister.")
43
+ return response
44
+
45
+ logger.debug("Getting permalink for message: %s", response)
46
+ if self.get_permalink: # noqa: SIM108
47
+ permalink = self._get_permalink(channel=request.channel, ts=response.ts)
48
+ else:
49
+ permalink = ""
50
+
51
+ logger.debug("Persisting message history to database: %s", response)
52
+ try:
53
+ history = SlackMessage(
54
+ id=request.id_,
55
+ channel=request.channel,
56
+ header=request.header.model_dump(),
57
+ body=request.body.model_dump() if request.body else {},
58
+ ok=response.ok,
59
+ permalink=permalink,
60
+ ts=response.ts,
61
+ parent_ts=response.parent_ts or "",
62
+ request=request.model_dump(),
63
+ response=response.model_dump(exclude={"request"}),
64
+ exception=response.error or "",
65
+ )
66
+ history.save()
67
+ except Exception:
68
+ logger.exception("Error while saving message history: %s", response)
69
+
70
+ return response
71
+
72
+ def _get_permalink(self, *, channel: str, ts: str | None) -> str:
73
+ """Get permalink of the message. It returns empty string on error."""
74
+ if not self.slack_app:
75
+ logger.warning("Slack app not provided, cannot get permalink.")
76
+ return ""
77
+
78
+ if not ts:
79
+ logger.warning("No message ts provided, cannot get permalink.")
80
+ return ""
81
+
82
+ try:
83
+ response = self.slack_app.client.chat_getPermalink(channel=channel, message_ts=ts)
84
+ return response.get("permalink", default="")
85
+ except SlackApiError as err:
86
+ logger.debug("Error while getting permalink: %s", exc_info=err)
87
+ return ""
88
+
89
+
90
+ OnPolicyNotExists = Literal["create", "default", "error"]
91
+
92
+
93
+ class DjangoDatabasePolicyHandler(BaseMiddleware):
94
+ """Middleware to handle Slack messaging policies stored in the database.
95
+
96
+ Be cautious when using this middleware because it includes functionality to distribute messages to multiple recipients,
97
+ which could lead to unwanted infinite loop or recursion if used improperly.
98
+
99
+ This middleware contains a secondary protection against infinite loops by injecting a context key to the message context.
100
+ If the key is found in the context, the middleware will stop the message from being sent. So be careful when modifying the context.
101
+ """ # noqa: E501
102
+
103
+ _RECURSION_DETECTION_CONTEXT_KEY = "__final__"
104
+ """Recursion detection key injected to message context for fanned-out messages to provide secondary protection against infinite loops.""" # noqa: E501
105
+
106
+ def __init__(
107
+ self,
108
+ *,
109
+ messenger: Messenger | str,
110
+ on_policy_not_exists: OnPolicyNotExists = "error",
111
+ ) -> None:
112
+ """Initialize the middleware.
113
+
114
+ This middleware will load the policy from the database and send the message to all recipients.
115
+
116
+ Args:
117
+ messenger: Messenger instance or name to use for sending messages.
118
+ The messenger instance should be different from the one used in the policy handler,
119
+ because this middleware cannot properly handle fanned-out messages modified by this middleware.
120
+ Also, there are chances of infinite loops if the same messenger is used.
121
+ on_policy_not_exists: Action to take when policy is not found.
122
+ """
123
+ if on_policy_not_exists not in ("create", "default", "error"):
124
+ msg = f'Unknown value for `on_policy_not_exists`: "{on_policy_not_exists}"'
125
+ raise ValueError(msg)
126
+
127
+ self._messenger = messenger
128
+ self.on_policy_not_exists = on_policy_not_exists
129
+
130
+ # * It's not desirable to put import in the method,
131
+ # * but it's the only way to avoid circular imports for now (what's the fix?)
132
+ @property
133
+ def messenger(self) -> Messenger:
134
+ """Get the messenger instance. If it's a string, will get the messenger from the app settings."""
135
+ if isinstance(self._messenger, str):
136
+ from django_slack_tools.app_settings import get_messenger
137
+
138
+ self._messenger = get_messenger(self._messenger)
139
+
140
+ return self._messenger
141
+
142
+ def process_request(self, request: MessageRequest) -> MessageRequest | None: # noqa: D102
143
+ # TODO(lasuillard): Hacky way to stop the request, need to find a better way
144
+ # Some extra field (request.meta) could be added to share control context
145
+ if request.context.get(self._RECURSION_DETECTION_CONTEXT_KEY, False):
146
+ return request
147
+
148
+ code = request.channel
149
+ policy = self._get_policy(code=code)
150
+ if not policy.enabled:
151
+ logger.debug("Policy %s is disabled, skipping further messaging", policy)
152
+ return None
153
+
154
+ requests: list[MessageRequest] = []
155
+ for recipient in policy.recipients.all():
156
+ default_context = self._get_default_context(recipient)
157
+ context = {
158
+ **default_context,
159
+ **request.context,
160
+ self._RECURSION_DETECTION_CONTEXT_KEY: True,
161
+ }
162
+ header = MessageHeader.model_validate(
163
+ {
164
+ **policy.header_defaults,
165
+ **request.header.model_dump(),
166
+ },
167
+ )
168
+ req = MessageRequest(channel=recipient.channel, template_key=policy.code, context=context, header=header)
169
+ requests.append(req)
170
+
171
+ # TODO(lasuillard): How to provide users the access the newly created messages?
172
+ # currently, it's possible with persisters but it would require some additional work
173
+ # TODO(lasuillard): Can `sys.setrecursionlimit` be used to prevent spamming if recursion occurs?
174
+ for req in requests:
175
+ self.messenger.send_request(req)
176
+
177
+ # Stop current request
178
+ return None
179
+
180
+ def _get_policy(self, *, code: str) -> SlackMessagingPolicy:
181
+ """Get the policy for the given code."""
182
+ try:
183
+ policy = SlackMessagingPolicy.objects.get(code=code)
184
+ except SlackMessagingPolicy.DoesNotExist:
185
+ if self.on_policy_not_exists == "create":
186
+ logger.warning("No policy found for template key, creating one: %s", code)
187
+ policy = self._create_policy(code=code)
188
+ elif self.on_policy_not_exists == "default":
189
+ policy = SlackMessagingPolicy.objects.get(code="DEFAULT")
190
+ elif self.on_policy_not_exists == "error":
191
+ raise
192
+ else:
193
+ msg = f'Unknown value for `on_policy_not_exists`: "{self.on_policy_not_exists}"'
194
+ raise ValueError(msg) from None
195
+
196
+ return policy
197
+
198
+ def _create_policy(self, *, code: str) -> SlackMessagingPolicy:
199
+ """Create a policy with the given code.
200
+
201
+ Policy created is disabled by default, thus no message will be sent.
202
+ To modify the default policy creation behavior, simply override this method.
203
+ """
204
+ policy = SlackMessagingPolicy.objects.create(
205
+ code=code,
206
+ enabled=False,
207
+ template_type=SlackMessagingPolicy.TemplateType.UNKNOWN,
208
+ )
209
+ default_recipients = SlackMessageRecipient.objects.filter(alias="DEFAULT")
210
+ policy.recipients.set(default_recipients)
211
+ return policy
212
+
213
+ def _get_default_context(self, recipient: SlackMessageRecipient) -> dict:
214
+ """Create default context for the recipient."""
215
+ mentions = [mention.mention for mention in recipient.mentions.all()]
216
+ return {
217
+ "mentions": mentions,
218
+ }
@@ -3,7 +3,7 @@
3
3
  import django.db.models.deletion
4
4
  from django.db import migrations, models
5
5
 
6
- import django_slack_tools.utils.slack.django
6
+ import django_slack_tools.slack_messages.validators
7
7
 
8
8
 
9
9
  class Migration(migrations.Migration):
@@ -152,7 +152,7 @@ class Migration(migrations.Migration):
152
152
  blank=True,
153
153
  default=dict,
154
154
  help_text="Default header values applied to messages on creation.",
155
- validators=[django_slack_tools.utils.slack.django.header_validator],
155
+ validators=[django_slack_tools.slack_messages.validators.header_validator],
156
156
  verbose_name="Default header",
157
157
  ),
158
158
  ),
@@ -207,7 +207,7 @@ class Migration(migrations.Migration):
207
207
  "header",
208
208
  models.JSONField(
209
209
  help_text="Slack control arguments. Allowed fields are `mrkdwn`, `parse`, `reply_broadcast`, `thread_ts`, `unfurl_links`, `unfurl_media`.", # noqa: E501
210
- validators=[django_slack_tools.utils.slack.django.header_validator],
210
+ validators=[django_slack_tools.slack_messages.validators.header_validator],
211
211
  verbose_name="Header",
212
212
  ),
213
213
  ),
@@ -215,7 +215,7 @@ class Migration(migrations.Migration):
215
215
  "body",
216
216
  models.JSONField(
217
217
  help_text="Message body. Allowed fields are `attachments`, `body`, `text`, `icon_emoji`, `icon_url`, `metadata`, `username`.", # noqa: E501
218
- validators=[django_slack_tools.utils.slack.django.body_validator],
218
+ validators=[django_slack_tools.slack_messages.validators.body_validator],
219
219
  verbose_name="Body",
220
220
  ),
221
221
  ),
@@ -0,0 +1,46 @@
1
+ # Generated by Django 4.2.17 on 2024-12-31 13:40
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ from django.db import migrations, models
7
+
8
+ if TYPE_CHECKING:
9
+ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
10
+ from django.db.migrations.state import StateApps
11
+
12
+
13
+ def _change_dict_to_python(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
14
+ db_alias = schema_editor.connection.alias
15
+ model_type = apps.get_model("slack_messages", "SlackMessagingPolicy")
16
+ model_type.objects.using(db_alias).filter(template_type="D").update(template_type="P")
17
+
18
+
19
+ def _revert_python_to_dict(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
20
+ db_alias = schema_editor.connection.alias
21
+ model_type = apps.get_model("slack_messages", "SlackMessagingPolicy")
22
+ model_type.objects.using(db_alias).filter(template_type="P").update(template_type="D")
23
+
24
+
25
+ class Migration(migrations.Migration):
26
+ dependencies = [
27
+ ("slack_messages", "0004_slackmessagingpolicy_template_type"),
28
+ ]
29
+
30
+ operations = [
31
+ migrations.AlterField(
32
+ model_name="slackmessagingpolicy",
33
+ name="template_type",
34
+ field=models.CharField(
35
+ choices=[("P", "Python"), ("DJ", "Django"), ("DI", "Django Inline"), ("?", "Unknown")],
36
+ default="P",
37
+ help_text="Type of message template.",
38
+ max_length=2,
39
+ verbose_name="Template type",
40
+ ),
41
+ ),
42
+ migrations.RunPython(
43
+ code=_change_dict_to_python,
44
+ reverse_code=_revert_python_to_dict,
45
+ ),
46
+ ]
@@ -0,0 +1,26 @@
1
+ # Generated by Django 4.2.18 on 2025-03-13 14:03
2
+
3
+ import uuid
4
+
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+ dependencies = [
10
+ ("slack_messages", "0005_alter_slackmessagingpolicy_template_type"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name="slackmessage",
16
+ name="id",
17
+ field=models.CharField(
18
+ default=uuid.uuid4,
19
+ editable=False,
20
+ max_length=255,
21
+ primary_key=True,
22
+ serialize=False,
23
+ unique=True,
24
+ ),
25
+ ),
26
+ ]
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from django.db import models
6
6
  from django.utils.translation import gettext_lazy as _
7
7
 
8
- from django_slack_tools.utils.model_mixins import TimestampMixin
8
+ from django_slack_tools.utils.django.model_mixins import TimestampMixin
9
9
 
10
10
 
11
11
  class SlackMentionManager(models.Manager["SlackMention"]):
@@ -2,11 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import uuid
6
+
5
7
  from django.db import models
6
8
  from django.utils.translation import gettext_lazy as _
7
9
 
8
- from django_slack_tools.utils.model_mixins import TimestampMixin
9
- from django_slack_tools.utils.slack import body_validator, header_validator
10
+ from django_slack_tools.slack_messages.validators import body_validator, header_validator
11
+ from django_slack_tools.utils.django.model_mixins import TimestampMixin
10
12
 
11
13
  from .messaging_policy import SlackMessagingPolicy
12
14
 
@@ -18,6 +20,7 @@ class SlackMessageManager(models.Manager["SlackMessage"]):
18
20
  class SlackMessage(TimestampMixin, models.Model):
19
21
  """An Slack message."""
20
22
 
23
+ id = models.CharField(primary_key=True, unique=True, max_length=255, default=uuid.uuid4, editable=False)
21
24
  policy = models.ForeignKey(
22
25
  SlackMessagingPolicy,
23
26
  verbose_name=_("Messaging Policy"),
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from django.db import models
6
6
  from django.utils.translation import gettext_lazy as _
7
7
 
8
- from django_slack_tools.utils.model_mixins import TimestampMixin
8
+ from django_slack_tools.utils.django.model_mixins import TimestampMixin
9
9
 
10
10
  from .mention import SlackMention
11
11
 
@@ -7,8 +7,8 @@ from typing import TYPE_CHECKING
7
7
  from django.db import models
8
8
  from django.utils.translation import gettext_lazy as _
9
9
 
10
- from django_slack_tools.utils.model_mixins import TimestampMixin
11
- from django_slack_tools.utils.slack import header_validator
10
+ from django_slack_tools.slack_messages.validators import header_validator
11
+ from django_slack_tools.utils.django.model_mixins import TimestampMixin
12
12
 
13
13
  from .message_recipient import SlackMessageRecipient
14
14
 
@@ -26,7 +26,7 @@ class SlackMessagingPolicy(TimestampMixin, models.Model):
26
26
  class TemplateType(models.TextChoices):
27
27
  """Possible template types."""
28
28
 
29
- DICT = "D", _("Dictionary")
29
+ PYTHON = "P", _("Python")
30
30
  "Dictionary-based template."
31
31
 
32
32
  DJANGO = "DJ", _("Django")
@@ -66,7 +66,7 @@ class SlackMessagingPolicy(TimestampMixin, models.Model):
66
66
  help_text=_("Type of message template."),
67
67
  max_length=2,
68
68
  choices=TemplateType.choices,
69
- default=TemplateType.DICT,
69
+ default=TemplateType.PYTHON,
70
70
  )
71
71
  template: models.JSONField[Any] = models.JSONField(
72
72
  verbose_name=_("Message template object"),