firefighter-incident 0.0.1rc1__py3-none-any.whl → 0.0.2__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 (204) hide show
  1. firefighter/_version.py +9 -4
  2. firefighter/api/admin.py +31 -12
  3. firefighter/api/migrations/0003_alter_apitokenproxy_options.py +30 -0
  4. firefighter/api/models.py +2 -2
  5. firefighter/api/renderer.py +1 -1
  6. firefighter/api/serializers.py +6 -4
  7. firefighter/api/urls.py +13 -19
  8. firefighter/components/avatar/avatar.py +24 -7
  9. firefighter/components/card/card.py +9 -4
  10. firefighter/components/export_button/export_button.html +2 -2
  11. firefighter/components/export_button/export_button.py +26 -7
  12. firefighter/components/form/form.html +1 -1
  13. firefighter/components/form/form.py +15 -12
  14. firefighter/components/form_field/form_field.html +4 -4
  15. firefighter/components/form_field/form_field.py +20 -12
  16. firefighter/components/messages/messages.html +39 -49
  17. firefighter/components/messages/messages.py +10 -5
  18. firefighter/components/modal/modal.html +20 -25
  19. firefighter/components/modal/modal.py +24 -9
  20. firefighter/confluence/models.py +2 -0
  21. firefighter/confluence/service.py +3 -3
  22. firefighter/confluence/tasks/archive_postmortems.py +7 -2
  23. firefighter/confluence/tasks/sync_postmortems.py +1 -1
  24. firefighter/confluence/tasks/sync_runbooks.py +3 -3
  25. firefighter/confluence/templates/oncall_team.xml +3 -3
  26. firefighter/confluence/templates/pages/runbook_list.html +4 -5
  27. firefighter/confluence/utils.py +4 -1
  28. firefighter/firefighter/filters.py +3 -2
  29. firefighter/firefighter/http_client.py +7 -9
  30. firefighter/firefighter/management/commands/task.py +1 -1
  31. firefighter/firefighter/settings/__init__.py +1 -0
  32. firefighter/firefighter/settings/components/common.py +2 -0
  33. firefighter/firefighter/settings/components/logging.py +1 -1
  34. firefighter/firefighter/settings/components/raid.py +3 -3
  35. firefighter/firefighter/settings/environments/dev.py +1 -3
  36. firefighter/firefighter/settings/environments/prod.py +1 -0
  37. firefighter/firefighter/sso.py +1 -1
  38. firefighter/firefighter/templates/admin/base.html +3 -2
  39. firefighter/firefighter/templates/admin/login.html +2 -2
  40. firefighter/firefighter/templates/admin/send_message_conversation.html +4 -4
  41. firefighter/firefighter/urls.py +7 -8
  42. firefighter/firefighter/utils.py +1 -3
  43. firefighter/firefighter/wsgi.py +2 -1
  44. firefighter/incidents/admin.py +11 -7
  45. firefighter/incidents/factories.py +33 -32
  46. firefighter/incidents/forms/edit.py +33 -0
  47. firefighter/incidents/forms/update_key_events.py +1 -1
  48. firefighter/incidents/forms/update_roles.py +1 -1
  49. firefighter/incidents/forms/utils.py +3 -3
  50. firefighter/incidents/menus.py +5 -4
  51. firefighter/incidents/migrations/0002_alter_severity_name_alter_user_password_featureteam.py +35 -0
  52. firefighter/incidents/migrations/0003_delete_featureteam.py +16 -0
  53. firefighter/incidents/migrations/0004_incidentupdate_environment.py +27 -0
  54. firefighter/incidents/models/component.py +8 -11
  55. firefighter/incidents/models/group.py +1 -0
  56. firefighter/incidents/models/impact.py +8 -8
  57. firefighter/incidents/models/incident.py +14 -11
  58. firefighter/incidents/models/incident_cost.py +2 -2
  59. firefighter/incidents/models/incident_membership.py +5 -5
  60. firefighter/incidents/models/incident_update.py +14 -5
  61. firefighter/incidents/models/metric_type.py +2 -2
  62. firefighter/incidents/signals.py +0 -5
  63. firefighter/incidents/static/css/main.min.css +1 -1
  64. firefighter/incidents/static/css/tailwind.css +18 -34
  65. firefighter/incidents/static/js/main.min.js +12 -12
  66. firefighter/incidents/tables.py +1 -5
  67. firefighter/incidents/tasks/updateoncall.py +1 -3
  68. firefighter/incidents/templates/incidents/errors/base.html +2 -1
  69. firefighter/incidents/templates/incidents/filter.html +33 -33
  70. firefighter/incidents/templates/incidents/table/priority_column.html +1 -1
  71. firefighter/incidents/templates/incidents/table.html +2 -3
  72. firefighter/incidents/templates/incidents/widgets/form_container.html +60 -61
  73. firefighter/incidents/templates/incidents/widgets/grouped_checkbox_nested.html +52 -52
  74. firefighter/incidents/templates/layouts/index.html +3 -3
  75. firefighter/incidents/templates/layouts/partials/created_at_help.html +2 -3
  76. firefighter/incidents/templates/layouts/partials/environment_pill.html +6 -6
  77. firefighter/incidents/templates/layouts/partials/footer.html +4 -7
  78. firefighter/incidents/templates/layouts/partials/header.html +41 -31
  79. firefighter/incidents/templates/layouts/partials/incident_card.html +6 -6
  80. firefighter/incidents/templates/layouts/partials/incident_metrics.html +1 -2
  81. firefighter/incidents/templates/layouts/partials/incident_timeline.html +35 -6
  82. firefighter/incidents/templates/layouts/partials/incident_update_key_events_view.html +3 -3
  83. firefighter/incidents/templates/layouts/partials/incident_update_key_events_view_modal.html +5 -7
  84. firefighter/incidents/templates/layouts/partials/partial_table_list_paginated.html +8 -9
  85. firefighter/incidents/templates/layouts/partials/priority_pill.html +1 -1
  86. firefighter/incidents/templates/layouts/partials/status_pill.html +15 -15
  87. firefighter/incidents/templates/layouts/partials/table.html +3 -3
  88. firefighter/incidents/templates/layouts/partials/user_card.html +3 -4
  89. firefighter/incidents/templates/layouts/partials/user_tooltip.html +1 -1
  90. firefighter/incidents/templates/layouts/view_filters.html +9 -9
  91. firefighter/incidents/templates/pages/component_detail.html +9 -9
  92. firefighter/incidents/templates/pages/component_list.html +5 -7
  93. firefighter/incidents/templates/pages/dashboard.html +4 -4
  94. firefighter/incidents/templates/pages/docs_metrics.html +43 -41
  95. firefighter/incidents/templates/pages/incident_create.html +8 -9
  96. firefighter/incidents/templates/pages/incident_detail.html +52 -46
  97. firefighter/incidents/templates/pages/incident_list.html +7 -7
  98. firefighter/incidents/templates/pages/incident_role_types_detail.html +3 -5
  99. firefighter/incidents/templates/pages/incident_role_types_list.html +3 -3
  100. firefighter/incidents/templates/pages/incident_statistics.html +10 -10
  101. firefighter/incidents/templates/pages/incident_statistics_partial.html +7 -11
  102. firefighter/incidents/templates/pages/incident_update_key_events_form.html +1 -1
  103. firefighter/incidents/templates/pages/user_detail.html +15 -15
  104. firefighter/incidents/views/docs/role_types.py +2 -1
  105. firefighter/incidents/views/errors.py +15 -16
  106. firefighter/incidents/views/reports.py +8 -8
  107. firefighter/incidents/views/users/details.py +1 -4
  108. firefighter/jira_app/client.py +3 -3
  109. firefighter/jira_app/models.py +2 -2
  110. firefighter/logging/custom_json_formatter.py +2 -2
  111. firefighter/pagerduty/models.py +9 -1
  112. firefighter/pagerduty/tasks/__init__.py +1 -0
  113. firefighter/pagerduty/tasks/trigger_oncall.py +7 -9
  114. firefighter/pagerduty/templates/pages/oncall_list.html +7 -7
  115. firefighter/pagerduty/templates/pages/oncall_trigger.html +1 -1
  116. firefighter/pagerduty/templates/partials/trigger_oncall_form_view.html +3 -3
  117. firefighter/pagerduty/templates/partials/trigger_oncall_form_view_modal.html +5 -7
  118. firefighter/raid/admin.py +13 -87
  119. firefighter/raid/apps.py +0 -1
  120. firefighter/raid/client.py +12 -7
  121. firefighter/raid/forms.py +5 -17
  122. firefighter/raid/messages.py +6 -90
  123. firefighter/raid/migrations/0002_featureteam_remove_qualifierrotation_jira_user_and_more.py +36 -0
  124. firefighter/raid/models.py +28 -46
  125. firefighter/raid/resources.py +15 -0
  126. firefighter/raid/service.py +5 -28
  127. firefighter/raid/signals/incident_created.py +2 -13
  128. firefighter/raid/tasks/__init__.py +0 -3
  129. firefighter/raid/views/open_normal.py +1 -1
  130. firefighter/slack/admin.py +39 -3
  131. firefighter/slack/factories.py +32 -32
  132. firefighter/slack/messages/slack_messages.py +47 -33
  133. firefighter/slack/migrations/0002_usergroup_tag.py +22 -0
  134. firefighter/slack/models/conversation.py +3 -3
  135. firefighter/slack/models/incident_channel.py +2 -2
  136. firefighter/slack/models/message.py +4 -4
  137. firefighter/slack/models/sos.py +2 -2
  138. firefighter/slack/models/user_group.py +6 -0
  139. firefighter/slack/rules.py +3 -4
  140. firefighter/slack/signals/create_incident_conversation.py +2 -1
  141. firefighter/slack/signals/get_users.py +29 -3
  142. firefighter/slack/signals/incident_updated.py +2 -2
  143. firefighter/slack/slack_app.py +18 -2
  144. firefighter/slack/slack_incident_context.py +8 -8
  145. firefighter/slack/tasks/fetch_conversations_members.py +1 -1
  146. firefighter/slack/tasks/send_reminders.py +1 -1
  147. firefighter/slack/tasks/update_usergroups_members.py +1 -1
  148. firefighter/slack/views/events/commands.py +3 -0
  149. firefighter/slack/views/events/home.py +22 -20
  150. firefighter/slack/views/modals/__init__.py +2 -0
  151. firefighter/slack/views/modals/base_modal/base.py +2 -2
  152. firefighter/slack/views/modals/base_modal/form_utils.py +6 -10
  153. firefighter/slack/views/modals/base_modal/mixins.py +1 -2
  154. firefighter/slack/views/modals/close.py +15 -13
  155. firefighter/slack/views/modals/downgrade_workflow.py +13 -17
  156. firefighter/slack/views/modals/edit.py +117 -0
  157. firefighter/slack/views/modals/key_event_message.py +9 -9
  158. firefighter/slack/views/modals/open.py +15 -15
  159. firefighter/slack/views/modals/opening/check_current_incidents.py +3 -3
  160. firefighter/slack/views/modals/opening/details/critical.py +7 -5
  161. firefighter/slack/views/modals/opening/select_impact.py +11 -14
  162. firefighter/slack/views/modals/opening/set_details.py +4 -2
  163. firefighter/slack/views/modals/opening/types.py +1 -1
  164. firefighter/slack/views/modals/postmortem.py +16 -20
  165. firefighter/slack/views/modals/select.py +1 -1
  166. firefighter/slack/views/modals/status.py +17 -21
  167. firefighter/slack/views/modals/trigger_oncall.py +19 -21
  168. firefighter/slack/views/modals/update.py +5 -0
  169. firefighter/slack/views/modals/update_status.py +7 -5
  170. {firefighter_incident-0.0.1rc1.dist-info → firefighter_incident-0.0.2.dist-info}/METADATA +13 -8
  171. {firefighter_incident-0.0.1rc1.dist-info → firefighter_incident-0.0.2.dist-info}/RECORD +201 -191
  172. {firefighter_incident-0.0.1rc1.dist-info → firefighter_incident-0.0.2.dist-info}/WHEEL +1 -1
  173. firefighter_tests/conftest.py +8 -9
  174. firefighter_tests/test_api/test_api_urls.py +4 -4
  175. firefighter_tests/test_firefighter/test_urls.py +12 -12
  176. firefighter_tests/test_incidents/test_forms/test_form_select_impact.py +3 -3
  177. firefighter_tests/test_incidents/test_forms/test_form_utils.py +4 -5
  178. firefighter_tests/test_incidents/test_forms/test_update_key_events.py +3 -3
  179. firefighter_tests/test_incidents/test_incident_urls.py +10 -10
  180. firefighter_tests/test_incidents/test_models/test_incident_model.py +1 -1
  181. firefighter_tests/test_incidents/test_utils/test_date_utils.py +3 -2
  182. firefighter_tests/test_incidents/test_views/test_incident_detail_view.py +3 -3
  183. firefighter_tests/test_incidents/test_views/test_index_view.py +6 -6
  184. firefighter_tests/test_raid/test_raid_client_users.py +12 -12
  185. firefighter_tests/test_slack/conftest.py +6 -6
  186. firefighter_tests/test_slack/test_models/test_conversations.py +2 -2
  187. firefighter_tests/test_slack/test_models/test_incident_channel.py +55 -46
  188. firefighter_tests/test_slack/test_models/test_slack_user.py +9 -9
  189. firefighter_tests/test_slack/test_slack_utils.py +6 -6
  190. firefighter_tests/test_slack/views/modals/test_close.py +7 -7
  191. firefighter_tests/test_slack/views/modals/test_open.py +6 -6
  192. firefighter_tests/test_slack/views/modals/test_send_sos.py +2 -2
  193. firefighter_tests/test_slack/views/modals/test_status.py +2 -2
  194. firefighter_tests/test_slack/views/modals/test_update_status.py +4 -4
  195. manage.py +1 -0
  196. package-lock.json +6442 -0
  197. package.json +49 -0
  198. scripts/gen_credits.py +165 -0
  199. scripts/hatch_build.py +15 -0
  200. firefighter/raid/signals/update_qualifiers_rotation.py +0 -97
  201. firefighter/raid/tasks/daily_qualifier.py +0 -67
  202. firefighter/raid/tasks/weekly_qualifier.py +0 -63
  203. {firefighter_incident-0.0.1rc1.dist-info → firefighter_incident-0.0.2.dist-info}/entry_points.txt +0 -0
  204. {firefighter_incident-0.0.1rc1.dist-info → firefighter_incident-0.0.2.dist-info}/licenses/LICENSE +0 -0
firefighter/_version.py CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.0.1rc1'
16
- __version_tuple__ = version_tuple = (0, 0, 1)
20
+ __version__ = version = '0.0.2'
21
+ __version_tuple__ = version_tuple = (0, 0, 2)
firefighter/api/admin.py CHANGED
@@ -29,7 +29,12 @@ class APITokenAdmin(TokenAdmin):
29
29
  Add supports for custom permissions.
30
30
  """
31
31
 
32
- def formfield_for_foreignkey(self, db_field: ForeignKey[Any, Any], request: HttpRequest, **kwargs: Any) -> ModelChoiceField: # type: ignore[override]
32
+ def formfield_for_foreignkey(
33
+ self,
34
+ db_field: ForeignKey[Any, Any],
35
+ request: HttpRequest, # type: ignore[override]
36
+ **kwargs: Any,
37
+ ) -> ModelChoiceField: # type: ignore[type-arg]
33
38
  """Show all or only current user depending on permissions."""
34
39
  if db_field.name == "user":
35
40
  if request.user.has_perm("api.can_add_any") or request.user.has_perm(
@@ -38,9 +43,15 @@ class APITokenAdmin(TokenAdmin):
38
43
  kwargs["queryset"] = User.objects.all()
39
44
  elif request.user.has_perm("api.can_add_own"):
40
45
  kwargs["queryset"] = User.objects.filter(id=request.user.id)
41
- return super().formfield_for_foreignkey(db_field, request, **kwargs)
42
-
43
- def get_form(self, request: HttpRequest, obj: APITokenProxy | None = None, change: bool = False, **kwargs: Any) -> type[ModelForm[APITokenProxy]]: # type: ignore[override] # noqa: FBT001, FBT002
46
+ return super().formfield_for_foreignkey(db_field, request, **kwargs) # type: ignore[return-value]
47
+
48
+ def get_form(
49
+ self,
50
+ request: HttpRequest, # type: ignore[override]
51
+ obj: APITokenProxy | None = None,
52
+ change: bool = False, # noqa: FBT001, FBT002
53
+ **kwargs: Any,
54
+ ) -> type[ModelForm[APITokenProxy]]:
44
55
  """Prefill the form with the current user."""
45
56
  form: type[ModelForm[APITokenProxy]] = super().get_form(
46
57
  request, obj, change, **kwargs
@@ -65,7 +76,11 @@ class APITokenAdmin(TokenAdmin):
65
76
  )
66
77
  return super().get_sortable_by(request)
67
78
 
68
- def has_view_permission(self, request: HttpRequest, obj: APITokenProxy | None = None) -> bool: # type: ignore[override]
79
+ def has_view_permission(
80
+ self,
81
+ request: HttpRequest, # type: ignore[override]
82
+ obj: APITokenProxy | None = None,
83
+ ) -> bool:
69
84
  if obj is None:
70
85
  return request.user.has_perm("api.can_view_any") or request.user.has_perm(
71
86
  "api.can_view_own"
@@ -81,7 +96,11 @@ class APITokenAdmin(TokenAdmin):
81
96
  "api.can_add_own"
82
97
  )
83
98
 
84
- def has_delete_permission(self, request: HttpRequest, obj: APITokenProxy | None = None) -> bool: # type: ignore[override]
99
+ def has_delete_permission(
100
+ self,
101
+ request: HttpRequest, # type: ignore[override]
102
+ obj: APITokenProxy | None = None,
103
+ ) -> bool:
85
104
  if obj is None:
86
105
  return request.user.has_perm("api.can_delete_any") or request.user.has_perm(
87
106
  "api.can_delete_own"
@@ -92,12 +111,12 @@ class APITokenAdmin(TokenAdmin):
92
111
  return bool(obj.user == request.user)
93
112
  return False
94
113
 
95
- def has_change_permission(self, request: HttpRequest, obj: APITokenProxy | None = None) -> bool: # type: ignore[override]
96
- if obj is None:
97
- return request.user.has_perm("api.can_edit_any")
98
- if request.user.has_perm("api.can_edit_any"):
99
- return True
100
- return False
114
+ def has_change_permission(
115
+ self,
116
+ request: HttpRequest, # type: ignore[override]
117
+ _obj: APITokenProxy | None = None,
118
+ ) -> bool:
119
+ return request.user.has_perm("api.can_edit_any")
101
120
 
102
121
 
103
122
  # Remove the default TokenAdmin created by DRF and replace it with our custom one.
@@ -0,0 +1,30 @@
1
+ # Generated by Django 4.2.11 on 2024-04-30 15:39
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("api", "0002_alter_apitokenproxy_options"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name="apitokenproxy",
15
+ options={
16
+ "default_permissions": [],
17
+ "permissions": [
18
+ ("can_edit_any", "Can reassign token to any user"),
19
+ ("can_add_any", "Can add token to any user"),
20
+ ("can_view_any", "Can view token of all users"),
21
+ ("can_delete_any", "Can delete token of any user"),
22
+ ("can_add_own", "Can add own tokens"),
23
+ ("can_view_own", "Can view own tokens"),
24
+ ("can_delete_own", "Can delete own tokens"),
25
+ ],
26
+ "verbose_name": "API Token",
27
+ "verbose_name_plural": "API Tokens",
28
+ },
29
+ ),
30
+ ]
firefighter/api/models.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import uuid
4
3
  from typing import TYPE_CHECKING, ClassVar, Self, cast
5
4
 
6
5
  from django.conf import settings
@@ -9,6 +8,7 @@ from django_stubs_ext.db.models import TypedModelMeta
9
8
  from rest_framework.authtoken.models import Token
10
9
 
11
10
  if TYPE_CHECKING:
11
+ import uuid
12
12
  from collections.abc import Sequence
13
13
 
14
14
 
@@ -26,7 +26,7 @@ class APITokenProxy(APIToken):
26
26
 
27
27
  @property
28
28
  def pk(self: Self) -> uuid.UUID:
29
- return cast(uuid.UUID, self.user_id) # pyright: ignore[reportGeneralTypeIssues]
29
+ return cast("uuid.UUID", self.user_id) # pyright: ignore[reportGeneralTypeIssues]
30
30
 
31
31
  class Meta(TypedModelMeta):
32
32
  permissions = [
@@ -71,7 +71,7 @@ class CSVRenderer(BaseCSVRenderer):
71
71
  pass
72
72
 
73
73
  def _get_headers(
74
- self, data: Iterable[Any], header: None | list[str]
74
+ self, data: Iterable[Any], header: list[str] | None
75
75
  ) -> tuple[Iterable[Any], list[str]]:
76
76
  # If we already have a header, and it does not contain any wildcards,
77
77
  # we can use it as-is.
@@ -72,11 +72,13 @@ class GroupedModelSerializerOpenAPI(OpenApiSerializerExtension): # type: ignore
72
72
  child_schema = auto_schema.resolve_serializer(
73
73
  self.target.child_serializer, direction
74
74
  )
75
- child_schema_ref = child_schema.ref if child_schema.ref else child_schema
75
+ child_schema_ref = child_schema.ref or child_schema
76
76
  return {"type": "object", "additionalProperties": child_schema_ref}
77
77
 
78
78
  def get_name(
79
- self, auto_schema: AutoSchema, direction: Direction # noqa: ARG002
79
+ self,
80
+ auto_schema: AutoSchema, # noqa: ARG002
81
+ direction: Direction, # noqa: ARG002
80
82
  ) -> str:
81
83
  return f"GroupedModelSerializer_{self.target.child_serializer.__name__}"
82
84
 
@@ -217,12 +219,12 @@ class IncidentSerializer(TaggitSerializer, serializers.ModelSerializer[Incident]
217
219
  created_by = UserSerializer(read_only=True)
218
220
  slack_channel_name = serializers.SerializerMethodField()
219
221
 
220
- created_by_email = CreatableSlugRelatedField[User]( # type: ignore[misc]
222
+ created_by_email = CreatableSlugRelatedField[User](
221
223
  source="created_by",
222
224
  write_only=True,
223
225
  slug_field="email",
224
226
  queryset=User.objects.all(),
225
- validators=[EmailValidator()],
227
+ validators=[EmailValidator()], # type: ignore[list-item]
226
228
  )
227
229
 
228
230
  tags = TagListSerializerField(read_only=True)
firefighter/api/urls.py CHANGED
@@ -54,26 +54,20 @@ urlpatterns: list[URLPattern | URLResolver] = [
54
54
  path("", include(router.urls)),
55
55
  path(
56
56
  "incidents",
57
- views.incidents.CreateIncidentViewSet.as_view(
58
- {"post": "create"}
59
- ), # pyright: ignore[reportGeneralTypeIssues]
57
+ views.incidents.CreateIncidentViewSet.as_view({"post": "create"}), # pyright: ignore[reportGeneralTypeIssues]
60
58
  name="incidents",
61
59
  ),
62
60
  ]
63
61
  if settings.FF_EXPOSE_API_DOCS:
64
- urlpatterns.extend(
65
- (
66
- path(
67
- "schema",
68
- SpectacularAPIView.as_view(), # pyright: ignore[reportGeneralTypeIssues]
69
- name="schema",
70
- ),
71
- path(
72
- "schema/swagger-ui",
73
- SpectacularSwaggerView.as_view(
74
- url_name="api:schema"
75
- ), # pyright: ignore[reportGeneralTypeIssues]
76
- name="swagger-ui",
77
- ),
78
- )
79
- )
62
+ urlpatterns.extend((
63
+ path(
64
+ "schema",
65
+ SpectacularAPIView.as_view(), # pyright: ignore[reportGeneralTypeIssues]
66
+ name="schema",
67
+ ),
68
+ path(
69
+ "schema/swagger-ui",
70
+ SpectacularSwaggerView.as_view(url_name="api:schema"), # pyright: ignore[reportGeneralTypeIssues]
71
+ name="swagger-ui",
72
+ ),
73
+ ))
@@ -1,20 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import TYPE_CHECKING, Any
4
+ from typing import Any, NotRequired, Required, TypedDict
5
5
 
6
- from django_components import component
6
+ from django_components import EmptyTuple, component
7
7
 
8
- if TYPE_CHECKING:
9
- from firefighter.incidents.models.user import User
8
+ from firefighter.incidents.models.user import User
10
9
 
11
10
  logger = logging.getLogger(__name__)
12
11
 
13
12
 
13
+ class Data(TypedDict):
14
+ user: User
15
+ size_tailwind: int
16
+ size_px: int
17
+
18
+
19
+ Args = tuple[User]
20
+
21
+
22
+ class Kwargs(TypedDict, total=False):
23
+ user: Required[User]
24
+ size: NotRequired[str]
25
+
26
+
14
27
  @component.register("avatar")
15
- class Avatar(component.Component):
28
+ class Avatar(component.Component[EmptyTuple, Kwargs, Data, Any]): # type: ignore[type-var]
16
29
  template_name = "avatar/avatar.html"
17
30
 
18
- def get_context_data(self, user: User, *args: Any, **kwargs: Any) -> dict[str, Any]:
31
+ def get_context_data(self, user: User, **kwargs: Any) -> Data:
19
32
  size_px, size_tailwind = (40, 10) if kwargs.get("size") == "md" else (80, 20)
20
- return {"user": user, "size_tailwind": size_tailwind, "size_px": size_px}
33
+ return {
34
+ "user": user,
35
+ "size_tailwind": size_tailwind,
36
+ "size_px": size_px,
37
+ }
@@ -1,16 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Any
4
+ from typing import Any, NotRequired, Required, TypedDict, Unpack
5
5
 
6
- from django_components import component
6
+ from django_components import EmptyTuple, component
7
7
 
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
+ class Data(TypedDict, total=False):
12
+ id: Required[str]
13
+ card_title: NotRequired[str]
14
+
15
+
11
16
  @component.register("card")
12
- class Card(component.Component):
17
+ class Card(component.Component[EmptyTuple, Data, Data, Any]): # type: ignore[type-var]
13
18
  template_name = "card/card.html"
14
19
 
15
- def get_context_data(self, *args: Any, **kwargs: dict[str, Any]) -> dict[str, Any]:
20
+ def get_context_data(self, *args: Any, **kwargs: Unpack[Data]) -> Data:
16
21
  return kwargs
@@ -24,8 +24,8 @@
24
24
  {% for format in formats %}
25
25
  <li>
26
26
  <button method="get" formtarget="_blank" type="submit"
27
- name="fields" value="{{format.fields}}"
28
- form="main-filter" formaction="{{format.url}}" class="" role="menuitem" tabindex="-1">{{format.label}}</button>
27
+ name="fields" value="{{format.fields}}"
28
+ form="main-filter" formaction="{{format.url}}" class="" role="menuitem" tabindex="-1">{{format.label}}</button>
29
29
  </li>
30
30
  {% endfor %}
31
31
  </ul>
@@ -1,22 +1,41 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any, NotRequired, Required, TypedDict
4
5
 
5
6
  from django.urls import reverse
6
- from django_components import component
7
+ from django_components import EmptyTuple, component
8
+
9
+
10
+ class _Format(TypedDict):
11
+ label: str
12
+ url: str
13
+ fields: str
14
+
15
+
16
+ class Data(TypedDict):
17
+ default_format: Mapping[str, Any] # _Format
18
+ formats: Sequence[Mapping[str, Any]] # list[_Format]
19
+
20
+
21
+ Args = tuple[str]
22
+
23
+
24
+ class Kwargs(TypedDict, total=False):
25
+ base_url: Required[str]
26
+ default_fmt: NotRequired[tuple[str, str | None, str | None]]
7
27
 
8
28
 
9
29
  @component.register("export_button")
10
- class ExportButton(component.Component):
30
+ class ExportButton(component.Component[EmptyTuple, Kwargs, Data, Any]): # type: ignore[type-var]
11
31
  template_name = "export_button/export_button.html"
12
32
 
13
33
  def get_context_data(
14
34
  self,
15
35
  base_url: str,
16
- *args: Any,
17
36
  default_fmt: tuple[str, str | None, str | None] | None = None,
18
37
  **kwargs: Any,
19
- ) -> dict[str, Any]:
38
+ ) -> Data:
20
39
  default_fmt = default_fmt or ("csv", None, None)
21
40
  fmts: list[tuple[str, str | None, str | None]] = [
22
41
  ("json", None, None),
@@ -28,12 +47,12 @@ class ExportButton(component.Component):
28
47
  default_format = self.make_fmt(default_fmt, base_url)
29
48
  formats = [self.make_fmt(fmt, base_url) for fmt in fmts]
30
49
 
31
- return {"default_format": default_format, "formats": formats}
50
+ return Data(default_format=default_format, formats=formats)
32
51
 
33
52
  @staticmethod
34
53
  def make_fmt(
35
54
  format_: tuple[str, str | None, str | None], base_reverse: str
36
- ) -> dict[str, str]:
55
+ ) -> _Format:
37
56
  return {
38
57
  "label": f"Export {format_[0].upper()}{' ' + format_[2] if format_[2] else ''}",
39
58
  "url": f"{reverse(base_reverse, args=[format_[0]])}",
@@ -5,7 +5,7 @@
5
5
 
6
6
  <div class="grid grid-cols-6 gap-1">
7
7
  {% for field in form %}
8
- {% component "form_field" field=field%}
8
+ {% component "form_field" field=field%}{% endcomponent %}
9
9
  {% endfor %}
10
10
  </div>
11
11
 
@@ -1,23 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import TYPE_CHECKING, Any
4
+ from typing import Any, NotRequired, TypedDict
5
5
 
6
- from django_components import component
7
-
8
- if TYPE_CHECKING:
9
- from django import forms
6
+ from django import forms
7
+ from django_components import EmptyTuple, component
8
+ from django_components.slots import SlotContent
10
9
 
11
10
  logger = logging.getLogger(__name__)
12
11
 
13
12
 
13
+ class Slots(TypedDict, total=False):
14
+ form_footer: NotRequired[SlotContent[Any]]
15
+
16
+
17
+ class Data(TypedDict):
18
+ form: forms.Form
19
+
20
+
14
21
  @component.register("form")
15
- class Form(component.Component):
22
+ class Form(component.Component[EmptyTuple, Data, Data, Slots]): # type: ignore[type-var] # type: ignore[override]
16
23
  template_name = "form/form.html"
17
24
 
18
- def get_context_data(
19
- self, form: forms.Form, *args: Any, **kwargs: Any
20
- ) -> dict[str, Any]:
21
- return {
22
- "form": form,
23
- }
25
+ def get_context_data(self, form: forms.Form, **kwargs: Any) -> Data: # type: ignore[override]
26
+ return Data(form=form)
@@ -10,9 +10,9 @@
10
10
 
11
11
  {% render_field field class=field_input_class class+="input input-bordered w-full" %}
12
12
  {% if field.help_text %}
13
- <label class="label">
14
- <span class="label-text-alt">{{field.help_text}}</span>
15
- </label>
16
- {% endif %}
13
+ <label class="label">
14
+ <span class="label-text-alt">{{field.help_text}}</span>
15
+ </label>
16
+ {% endif %}
17
17
  </div>
18
18
  </div>
@@ -1,26 +1,34 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import TYPE_CHECKING, Any
4
+ from typing import (
5
+ Any,
6
+ Never,
7
+ TypedDict,
8
+ )
5
9
 
6
- from django_components import component
7
-
8
- if TYPE_CHECKING:
9
- from django import forms
10
+ from django import forms
11
+ from django_components import EmptyTuple, component
10
12
 
11
13
  logger = logging.getLogger(__name__)
12
14
 
13
15
 
16
+ class Data(TypedDict):
17
+ field: forms.Field | forms.BoundField
18
+ field_input_class: str
19
+
20
+
21
+ class Kwargs(TypedDict, total=True):
22
+ field: forms.Field | forms.BoundField
23
+
24
+
14
25
  @component.register("form_field")
15
- class FormField(component.Component):
26
+ class FormField(component.Component[EmptyTuple, Kwargs, Data, Any]): # type: ignore[type-var]
16
27
  template_name = "form_field/form_field.html"
17
28
 
18
29
  def get_context_data(
19
- self, field: forms.BoundField | forms.Field, *args: Any, **kwargs: Any
20
- ) -> dict[str, Any]:
21
- if field is None:
22
- raise ValueError("Field not set!")
23
-
24
- input_class = "input-ff"
30
+ self, field: forms.BoundField | forms.Field, **kwargs: Never
31
+ ) -> Data:
32
+ input_class = "input input-bordered input-md"
25
33
 
26
34
  return {"field": field, "field_input_class": input_class}
@@ -1,52 +1,42 @@
1
1
  {% if messages %}
2
- <ul class="messages">
3
- {% for message in messages %}
4
- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
5
- <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
6
- <div class="alert alert-error shadow-lg">
7
- <div>
8
- <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
9
- <span> {{ message }}</span>
2
+ <ul class="messages">
3
+ {% for message in messages %}
4
+ {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
5
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
6
+ <div class="alert alert-error shadow-lg">
7
+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
8
+ <span> {{ message }}</span>
10
9
  </div>
11
- </div>
12
- </li>
13
- {% elif message.level == DEFAULT_MESSAGE_LEVELS.INFO %}
14
- <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
15
- <div class="alert alert-info shadow-lg">
16
- <div>
17
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current flex-shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
18
- <span> {{ message }}</span>
19
- </div>
20
- </div>
21
- </li>
22
- {% elif message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
23
- <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
24
- <div class="alert alert-success shadow-lg">
25
- <div>
26
- <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
27
- <span> {{ message }}</span>
28
- </div>
29
- </div>
30
- </li>
31
- {% elif message.level == DEFAULT_MESSAGE_LEVELS.WARNING %}
32
- <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
33
- <div class="alert alert-warning shadow-lg">
34
- <div>
35
- <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
36
- <span> {{ message }}</span>
37
- </div>
38
- </div>
39
- </li>
40
- {% else %}
41
- <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
42
- <div class="alert shadow-lg">
43
- <div>
44
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info flex-shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
45
- <span> {{ message }}</span>
46
- </div>
47
- </div>
48
- </li>
49
- {% endif %}
50
- {% endfor %}
51
- </ul>
10
+ </li>
11
+ {% elif message.level == DEFAULT_MESSAGE_LEVELS.INFO %}
12
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
13
+ <div class="alert alert-info shadow-lg">
14
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current flex-shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
15
+ <span> {{ message }}</span>
16
+ </div>
17
+ </li>
18
+ {% elif message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
19
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
20
+ <div class="alert alert-success shadow-lg">
21
+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
22
+ <span> {{ message }}</span>
23
+ </div>
24
+ </li>
25
+ {% elif message.level == DEFAULT_MESSAGE_LEVELS.WARNING %}
26
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
27
+ <div class="alert alert-warning shadow-lg">
28
+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
29
+ <span> {{ message }}</span>
30
+ </div>
31
+ </li>
32
+ {% else %}
33
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
34
+ <div class="alert shadow-lg">
35
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info flex-shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
36
+ <span> {{ message }}</span>
37
+ </div>
38
+ </li>
39
+ {% endif %}
40
+ {% endfor %}
41
+ </ul>
52
42
  {% endif %}
@@ -1,16 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Any
4
+ from typing import Any, TypedDict
5
5
 
6
- from django_components import component
6
+ from django.contrib.messages.storage.base import BaseStorage
7
+ from django_components import EmptyTuple, component
7
8
 
8
9
  logger = logging.getLogger(__name__)
9
10
 
10
11
 
12
+ class Data(TypedDict):
13
+ messages: BaseStorage
14
+
15
+
11
16
  @component.register("messages")
12
- class Messages(component.Component):
17
+ class Messages(component.Component[EmptyTuple, Data, Data, Any]): # type: ignore[type-var]
13
18
  template_name = "messages/messages.html"
14
19
 
15
- def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
16
- return kwargs
20
+ def get_context_data(self, messages: BaseStorage, **kwargs: Any) -> Data:
21
+ return Data(messages=messages)