django-lambda-tasks 0.4.1__tar.gz → 0.4.2__tar.gz

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 (101) hide show
  1. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/PKG-INFO +1 -1
  2. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/admin.py +22 -1
  3. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/pyproject.toml +1 -1
  4. django_lambda_tasks-0.4.2/tests/test_admin.py +282 -0
  5. django_lambda_tasks-0.4.1/tests/test_admin.py +0 -21
  6. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.github/workflows/ci.yml +0 -0
  7. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.github/workflows/release.yml +0 -0
  8. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.gitignore +0 -0
  9. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/.config.kiro +0 -0
  10. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/design.md +0 -0
  11. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/requirements.md +0 -0
  12. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/async-local-execution/tasks.md +0 -0
  13. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
  14. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
  15. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
  16. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
  17. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
  18. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
  19. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
  20. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
  21. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
  22. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
  23. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
  24. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
  25. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
  26. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
  27. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
  28. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
  29. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
  30. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/design.md +0 -0
  31. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/requirements.md +0 -0
  32. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/retry-delay/tasks.md +0 -0
  33. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
  34. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
  35. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
  36. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
  37. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
  38. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
  39. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
  40. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
  41. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
  42. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/design.md +0 -0
  43. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/requirements.md +0 -0
  44. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/singleton-task/tasks.md +0 -0
  45. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
  46. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
  47. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
  48. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
  49. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/.config.kiro +0 -0
  50. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/design.md +0 -0
  51. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/requirements.md +0 -0
  52. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/specs/task-retry/tasks.md +0 -0
  53. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/steering/product.md +0 -0
  54. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/steering/structure.md +0 -0
  55. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.kiro/steering/tech.md +0 -0
  56. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.pre-commit-config.yaml +0 -0
  57. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/.vscode/settings.json +0 -0
  58. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/README.md +0 -0
  59. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/README.md +0 -0
  60. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_app/__init__.py +0 -0
  61. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_app/apps.py +0 -0
  62. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_app/tasks.py +0 -0
  63. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_app/urls.py +0 -0
  64. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_app/views.py +0 -0
  65. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_project/__init__.py +0 -0
  66. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_project/settings.py +0 -0
  67. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_project/urls.py +0 -0
  68. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/example_project/wsgi.py +0 -0
  69. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/example/manage.py +0 -0
  70. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/__init__.py +0 -0
  71. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/apps.py +0 -0
  72. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/decorators.py +0 -0
  73. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/environment_loader.py +0 -0
  74. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/handler.py +0 -0
  75. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/local_executor.py +0 -0
  76. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/logging.py +0 -0
  77. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/migrations/0001_initial.py +0 -0
  78. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/migrations/0002_alter_taskrecord_status.py +0 -0
  79. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/migrations/__init__.py +0 -0
  80. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/models.py +0 -0
  81. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/secret_loader.py +0 -0
  82. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/settings.py +0 -0
  83. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/tasks.py +0 -0
  84. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/lambda_tasks/timeouts.py +0 -0
  85. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/conftest.py +0 -0
  86. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/settings.py +0 -0
  87. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_decorator.py +0 -0
  88. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_decorators.py +0 -0
  89. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_deferred_enqueue.py +0 -0
  90. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_environment_loader.py +0 -0
  91. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_handler.py +0 -0
  92. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_kwargs_only.py +0 -0
  93. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_local_executor.py +0 -0
  94. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_logging.py +0 -0
  95. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_models.py +0 -0
  96. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_secret_loader.py +0 -0
  97. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_serializer.py +0 -0
  98. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_settings.py +0 -0
  99. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_tasks.py +0 -0
  100. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_timeout_validation.py +0 -0
  101. {django_lambda_tasks-0.4.1 → django_lambda_tasks-0.4.2}/tests/test_timeouts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-lambda-tasks
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Run async tasks in a lambda function
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awslambdaric
@@ -3,8 +3,13 @@
3
3
  from django.contrib import admin
4
4
  from django.db.models import DurationField, ExpressionWrapper, F, QuerySet
5
5
  from django.http import HttpRequest
6
+ from django.utils.module_loading import import_string
6
7
 
7
- from lambda_tasks.models import TaskRecord
8
+ from lambda_tasks.models import (
9
+ SQSLambdaTask,
10
+ SQSLambdaTaskMessage,
11
+ TaskRecord,
12
+ )
8
13
 
9
14
 
10
15
  @admin.register(TaskRecord)
@@ -33,6 +38,7 @@ class TaskRecordAdmin(admin.ModelAdmin):
33
38
  "result",
34
39
  "traceback",
35
40
  )
41
+ actions = ["replay_tasks"]
36
42
 
37
43
  def get_queryset(self, request: HttpRequest) -> QuerySet:
38
44
  return (
@@ -52,3 +58,18 @@ class TaskRecordAdmin(admin.ModelAdmin):
52
58
  return None
53
59
  total_seconds = d.total_seconds()
54
60
  return f"{total_seconds:.3f}s"
61
+
62
+ @admin.action(description="Replay selected tasks", permissions=("change",))
63
+ def replay_tasks(self, request: HttpRequest, queryset: QuerySet) -> None:
64
+ for record in queryset:
65
+ queue = import_string(record.task_name).queue
66
+ task = SQSLambdaTask(
67
+ message=SQSLambdaTaskMessage(
68
+ task_name=record.task_name,
69
+ kwargs=record.kwargs,
70
+ n_retries=0,
71
+ ),
72
+ delay=0,
73
+ queue=queue,
74
+ )
75
+ task.execute_on_commit()
@@ -7,7 +7,7 @@ packages = ["lambda_tasks"]
7
7
 
8
8
  [project]
9
9
  name = "django-lambda-tasks"
10
- version = "0.4.1"
10
+ version = "0.4.2"
11
11
  description = "Run async tasks in a lambda function"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -0,0 +1,282 @@
1
+ from unittest.mock import patch
2
+
3
+ import pytest
4
+ from django.contrib import admin
5
+ from django.contrib.auth.models import Permission, User
6
+ from django.contrib.contenttypes.models import ContentType
7
+ from django.test import RequestFactory
8
+ from django.utils.timezone import now
9
+
10
+ import lambda_tasks.admin # noqa: F401 — triggers @admin.register side-effect
11
+ from lambda_tasks.models import SQSLambdaTask, TaskRecord, TaskStatus
12
+
13
+
14
+ def test_task_record_registered_in_admin():
15
+ assert TaskRecord in admin.site._registry
16
+
17
+
18
+ def test_task_record_admin_list_display():
19
+ assert admin.site._registry[TaskRecord].list_display == (
20
+ "pk",
21
+ "task_name",
22
+ "status",
23
+ "start_time",
24
+ "end_time",
25
+ "n_retries",
26
+ "duration",
27
+ "result",
28
+ )
29
+
30
+
31
+ @pytest.fixture()
32
+ def admin_user(db: None) -> User:
33
+ return User.objects.create_superuser(
34
+ username="admin", password="password", email="admin@example.com"
35
+ )
36
+
37
+
38
+ @pytest.fixture()
39
+ def task_record(db: None) -> TaskRecord:
40
+ return TaskRecord.objects.create(
41
+ id="00000000-0000-0000-0000-000000000001",
42
+ task_name="myapp.tasks.my_task",
43
+ kwargs={"user_id": 42},
44
+ n_retries=3,
45
+ status=TaskStatus.FAILED,
46
+ start_time=now(),
47
+ end_time=now(),
48
+ )
49
+
50
+
51
+ @pytest.fixture()
52
+ def _mock_import_string():
53
+ class _FakeWrapper:
54
+ queue = "default"
55
+
56
+ with patch("lambda_tasks.admin.import_string", return_value=_FakeWrapper()):
57
+ yield
58
+
59
+
60
+ @pytest.mark.django_db()
61
+ class TestReplayAction:
62
+ def test_replay_action_is_registered(self) -> None:
63
+ model_admin = admin.site._registry[TaskRecord]
64
+ request = RequestFactory().post("/")
65
+ request.user = User(is_staff=True, is_superuser=True)
66
+ action_names = list(model_admin.get_actions(request).keys())
67
+ assert "replay_tasks" in action_names
68
+
69
+ def test_replay_enqueues_task_with_original_kwargs(
70
+ self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
71
+ ) -> None:
72
+ model_admin = admin.site._registry[TaskRecord]
73
+ request = RequestFactory().post("/")
74
+ request.user = admin_user
75
+
76
+ queryset = TaskRecord.objects.filter(pk=task_record.pk)
77
+
78
+ with patch(
79
+ "lambda_tasks.admin.SQSLambdaTask.execute_on_commit"
80
+ ) as mock_execute:
81
+ model_admin.replay_tasks(request, queryset)
82
+
83
+ mock_execute.assert_called_once()
84
+
85
+ def test_replay_resets_n_retries_to_zero(
86
+ self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
87
+ ) -> None:
88
+ model_admin = admin.site._registry[TaskRecord]
89
+ request = RequestFactory().post("/")
90
+ request.user = admin_user
91
+
92
+ queryset = TaskRecord.objects.filter(pk=task_record.pk)
93
+
94
+ built_tasks: list[SQSLambdaTask] = []
95
+
96
+ def capture_execute(self: SQSLambdaTask) -> None:
97
+ built_tasks.append(self)
98
+
99
+ with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
100
+ model_admin.replay_tasks(request, queryset)
101
+
102
+ assert len(built_tasks) == 1
103
+ assert built_tasks[0].message.n_retries == 0
104
+
105
+ def test_replay_preserves_task_name(
106
+ self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
107
+ ) -> None:
108
+ model_admin = admin.site._registry[TaskRecord]
109
+ request = RequestFactory().post("/")
110
+ request.user = admin_user
111
+
112
+ queryset = TaskRecord.objects.filter(pk=task_record.pk)
113
+
114
+ built_tasks: list[SQSLambdaTask] = []
115
+
116
+ def capture_execute(self: SQSLambdaTask) -> None:
117
+ built_tasks.append(self)
118
+
119
+ with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
120
+ model_admin.replay_tasks(request, queryset)
121
+
122
+ assert built_tasks[0].message.task_name == "myapp.tasks.my_task"
123
+ assert built_tasks[0].message.kwargs == {"user_id": 42}
124
+
125
+ def test_replay_uses_delay_zero(
126
+ self, admin_user: User, task_record: TaskRecord, _mock_import_string: None
127
+ ) -> None:
128
+ model_admin = admin.site._registry[TaskRecord]
129
+ request = RequestFactory().post("/")
130
+ request.user = admin_user
131
+
132
+ queryset = TaskRecord.objects.filter(pk=task_record.pk)
133
+
134
+ built_tasks: list[SQSLambdaTask] = []
135
+
136
+ def capture_execute(self: SQSLambdaTask) -> None:
137
+ built_tasks.append(self)
138
+
139
+ with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
140
+ model_admin.replay_tasks(request, queryset)
141
+
142
+ assert built_tasks[0].delay == 0
143
+
144
+ def test_replay_resolves_queue_from_wrapper(
145
+ self, admin_user: User, db: None
146
+ ) -> None:
147
+ record = TaskRecord.objects.create(
148
+ id="00000000-0000-0000-0000-000000000002",
149
+ task_name="myapp.tasks.custom_queue_task",
150
+ kwargs={"x": 1},
151
+ n_retries=0,
152
+ status=TaskStatus.FAILED,
153
+ start_time=now(),
154
+ end_time=now(),
155
+ )
156
+
157
+ model_admin = admin.site._registry[TaskRecord]
158
+ request = RequestFactory().post("/")
159
+ request.user = admin_user
160
+
161
+ queryset = TaskRecord.objects.filter(pk=record.pk)
162
+
163
+ built_tasks: list[SQSLambdaTask] = []
164
+
165
+ def capture_execute(self: SQSLambdaTask) -> None:
166
+ built_tasks.append(self)
167
+
168
+ class _FakeWrapper:
169
+ queue = "high-priority"
170
+
171
+ with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
172
+ with patch(
173
+ "lambda_tasks.admin.import_string",
174
+ return_value=_FakeWrapper(),
175
+ ):
176
+ model_admin.replay_tasks(request, queryset)
177
+
178
+ assert built_tasks[0].queue == "high-priority"
179
+
180
+ def test_replay_raises_on_import_error(
181
+ self, admin_user: User, task_record: TaskRecord
182
+ ) -> None:
183
+ model_admin = admin.site._registry[TaskRecord]
184
+ request = RequestFactory().post("/")
185
+ request.user = admin_user
186
+
187
+ queryset = TaskRecord.objects.filter(pk=task_record.pk)
188
+
189
+ with patch(
190
+ "lambda_tasks.admin.import_string",
191
+ side_effect=ImportError("no such module"),
192
+ ):
193
+ with pytest.raises(ImportError, match="no such module"):
194
+ model_admin.replay_tasks(request, queryset)
195
+
196
+ def test_replay_raises_when_not_wrapper(
197
+ self, admin_user: User, task_record: TaskRecord
198
+ ) -> None:
199
+ model_admin = admin.site._registry[TaskRecord]
200
+ request = RequestFactory().post("/")
201
+ request.user = admin_user
202
+
203
+ queryset = TaskRecord.objects.filter(pk=task_record.pk)
204
+
205
+ with patch(
206
+ "lambda_tasks.admin.import_string",
207
+ return_value="not a wrapper",
208
+ ):
209
+ with pytest.raises(AttributeError):
210
+ model_admin.replay_tasks(request, queryset)
211
+
212
+ def test_replay_multiple_records(
213
+ self, admin_user: User, db: None, _mock_import_string: None
214
+ ) -> None:
215
+ records = [
216
+ TaskRecord.objects.create(
217
+ id=f"00000000-0000-0000-0000-00000000000{i}",
218
+ task_name=f"myapp.tasks.task_{i}",
219
+ kwargs={"key": i},
220
+ n_retries=0,
221
+ status=TaskStatus.FAILED,
222
+ start_time=now(),
223
+ end_time=now(),
224
+ )
225
+ for i in range(3)
226
+ ]
227
+
228
+ model_admin = admin.site._registry[TaskRecord]
229
+ request = RequestFactory().post("/")
230
+ request.user = admin_user
231
+
232
+ queryset = TaskRecord.objects.filter(pk__in=[r.pk for r in records])
233
+
234
+ built_tasks: list[SQSLambdaTask] = []
235
+
236
+ def capture_execute(self: SQSLambdaTask) -> None:
237
+ built_tasks.append(self)
238
+
239
+ with patch.object(SQSLambdaTask, "execute_on_commit", capture_execute):
240
+ model_admin.replay_tasks(request, queryset)
241
+
242
+ assert len(built_tasks) == 3
243
+ task_names = {t.message.task_name for t in built_tasks}
244
+ assert task_names == {
245
+ "myapp.tasks.task_0",
246
+ "myapp.tasks.task_1",
247
+ "myapp.tasks.task_2",
248
+ }
249
+
250
+ def test_replay_action_hidden_without_change_permission(self, db: None) -> None:
251
+ user = User.objects.create_user(
252
+ username="viewer", password="password", is_staff=True
253
+ )
254
+ content_type = ContentType.objects.get_for_model(TaskRecord)
255
+ view_perm = Permission.objects.get(
256
+ codename="view_taskrecord", content_type=content_type
257
+ )
258
+ user.user_permissions.add(view_perm)
259
+
260
+ model_admin = admin.site._registry[TaskRecord]
261
+ request = RequestFactory().get("/")
262
+ request.user = user
263
+
264
+ action_names = list(model_admin.get_actions(request).keys())
265
+ assert "replay_tasks" not in action_names
266
+
267
+ def test_replay_action_visible_with_change_permission(self, db: None) -> None:
268
+ user = User.objects.create_user(
269
+ username="editor", password="password", is_staff=True
270
+ )
271
+ content_type = ContentType.objects.get_for_model(TaskRecord)
272
+ change_perm = Permission.objects.get(
273
+ codename="change_taskrecord", content_type=content_type
274
+ )
275
+ user.user_permissions.add(change_perm)
276
+
277
+ model_admin = admin.site._registry[TaskRecord]
278
+ request = RequestFactory().get("/")
279
+ request.user = user
280
+
281
+ action_names = list(model_admin.get_actions(request).keys())
282
+ assert "replay_tasks" in action_names
@@ -1,21 +0,0 @@
1
- from django.contrib import admin
2
-
3
- import lambda_tasks.admin # noqa: F401 — triggers @admin.register side-effect
4
- from lambda_tasks.models import TaskRecord
5
-
6
-
7
- def test_task_record_registered_in_admin():
8
- assert TaskRecord in admin.site._registry
9
-
10
-
11
- def test_task_record_admin_list_display():
12
- assert admin.site._registry[TaskRecord].list_display == (
13
- "pk",
14
- "task_name",
15
- "status",
16
- "start_time",
17
- "end_time",
18
- "n_retries",
19
- "duration",
20
- "result",
21
- )