feat: allow authorized task deletion
This commit is contained in:
@@ -37,6 +37,35 @@ class TaskAccessTests(TestCase):
|
|||||||
response = self.client.get(reverse("task-edit", args=(self.task.pk,)))
|
response = self.client.get(reverse("task-edit", args=(self.task.pk,)))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_worker_can_delete_own_task(self):
|
||||||
|
own_task = Task.objects.create(
|
||||||
|
project=self.project, assignee=self.worker, title="Своя задача", description="Описание",
|
||||||
|
work_date=date(2026, 7, 14), duration_minutes=60, hourly_rate=Decimal("1000.00"),
|
||||||
|
amount=Decimal("1000.00"), created_by=self.worker,
|
||||||
|
)
|
||||||
|
self.client.force_login(self.worker)
|
||||||
|
response = self.client.post(reverse("task-delete", args=(own_task.pk,)))
|
||||||
|
self.assertRedirects(response, reverse("task-list"))
|
||||||
|
self.assertFalse(Task.objects.filter(pk=own_task.pk).exists())
|
||||||
|
|
||||||
|
def test_worker_cannot_delete_another_users_task(self):
|
||||||
|
self.client.force_login(self.worker)
|
||||||
|
response = self.client.post(reverse("task-delete", args=(self.task.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertTrue(Task.objects.filter(pk=self.task.pk).exists())
|
||||||
|
|
||||||
|
def test_admin_can_delete_any_task(self):
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.post(reverse("task-delete", args=(self.task.pk,)))
|
||||||
|
self.assertRedirects(response, reverse("task-list"))
|
||||||
|
self.assertFalse(Task.objects.filter(pk=self.task.pk).exists())
|
||||||
|
|
||||||
|
def test_delete_confirmation_does_not_delete_task(self):
|
||||||
|
self.client.force_login(self.other)
|
||||||
|
response = self.client.get(reverse("task-delete", args=(self.task.pk,)))
|
||||||
|
self.assertContains(response, "Удаление задачи")
|
||||||
|
self.assertTrue(Task.objects.filter(pk=self.task.pk).exists())
|
||||||
|
|
||||||
def test_create_copies_rate_and_calculates_rounded_amount(self):
|
def test_create_copies_rate_and_calculates_rounded_amount(self):
|
||||||
self.client.force_login(self.worker)
|
self.client.force_login(self.worker)
|
||||||
response = self.client.post(reverse("task-create"), {
|
response = self.client.post(reverse("task-create"), {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ urlpatterns = [
|
|||||||
path("", views.task_list, name="task-list"),
|
path("", views.task_list, name="task-list"),
|
||||||
path("create/", views.task_create, name="task-create"),
|
path("create/", views.task_create, name="task-create"),
|
||||||
path("<int:pk>/edit/", views.task_edit, name="task-edit"),
|
path("<int:pk>/edit/", views.task_edit, name="task-edit"),
|
||||||
|
path("<int:pk>/delete/", views.task_delete, name="task-delete"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,3 +59,15 @@ def task_edit(request, pk):
|
|||||||
task.save()
|
task.save()
|
||||||
return redirect("task-list")
|
return redirect("task-list")
|
||||||
return render(request, "tasks/task_form.html", {"form": form, "heading": "Редактирование задачи", "task": task})
|
return render(request, "tasks/task_form.html", {"form": form, "heading": "Редактирование задачи", "task": task})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@transaction.atomic
|
||||||
|
def task_delete(request, pk):
|
||||||
|
task = get_object_or_404(Task.objects.select_related("project__company", "assignee"), pk=pk)
|
||||||
|
if not request.user.is_app_admin and task.assignee_id != request.user.id:
|
||||||
|
raise PermissionDenied
|
||||||
|
if request.method == "POST":
|
||||||
|
task.delete()
|
||||||
|
return redirect("task-list")
|
||||||
|
return render(request, "tasks/task_confirm_delete.html", {"task": task})
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
:root{font-family:Inter,Segoe UI,Arial,sans-serif;color:#192033;background:#f4f6fa}*{box-sizing:border-box}body{margin:0}header{height:64px;background:#172033;color:white;display:flex;align-items:center;padding:0 max(24px,calc((100% - 1200px)/2));gap:36px}header a{color:white;text-decoration:none}.brand{font-size:22px;font-weight:800}nav{display:flex;align-items:center;gap:20px;margin-left:auto}nav form{margin:0}.link{background:none;border:0;color:white;padding:0}main{max-width:1200px;margin:32px auto;padding:0 24px}h1{margin-top:0}.card{background:white;border-radius:10px;padding:28px;box-shadow:0 3px 16px #18243b12}.narrow{max-width:480px;margin:60px auto}.toolbar,.filters{display:flex;align-items:center;gap:16px;justify-content:space-between}.filters{justify-content:flex-start;background:white;padding:16px;border-radius:8px;margin-bottom:22px;flex-wrap:wrap}label{font-weight:600}input,select,textarea{display:block;width:100%;padding:9px;border:1px solid #bac2d0;border-radius:6px;margin-top:5px}button,.button{border:0;border-radius:6px;background:#2f67dc;color:white;padding:10px 16px;text-decoration:none;cursor:pointer;font-weight:600}.secondary{background:#5a6477}table{width:100%;border-collapse:collapse;background:white}th,td{padding:11px;border-bottom:1px solid #dde2ea;text-align:left;vertical-align:top}th{background:#e9edf4}.table-wrap{overflow:auto}small{color:#647086}.total,.company-total,.grand-total{font-weight:700}.grand-total{font-size:20px;text-align:right}.report-company{margin:24px 0 36px}.errorlist{color:#b42318}.duration-fields{display:flex;gap:16px;border:0;padding:0;margin:20px 0}.duration-fields legend{font-weight:700;margin-bottom:8px;width:100%}.duration-fields label{width:150px}.duration-fields input{min-width:0}
|
:root{font-family:Inter,Segoe UI,Arial,sans-serif;color:#192033;background:#f4f6fa}*{box-sizing:border-box}body{margin:0}header{height:64px;background:#172033;color:white;display:flex;align-items:center;padding:0 max(24px,calc((100% - 1200px)/2));gap:36px}header a{color:white;text-decoration:none}.brand{font-size:22px;font-weight:800}nav{display:flex;align-items:center;gap:20px;margin-left:auto}nav form{margin:0}.link{background:none;border:0;color:white;padding:0}main{max-width:1200px;margin:32px auto;padding:0 24px}h1{margin-top:0}.card{background:white;border-radius:10px;padding:28px;box-shadow:0 3px 16px #18243b12}.narrow{max-width:480px;margin:60px auto}.toolbar,.filters,.form-actions{display:flex;align-items:center;gap:16px;justify-content:space-between}.filters{justify-content:flex-start;background:white;padding:16px;border-radius:8px;margin-bottom:22px;flex-wrap:wrap}label{font-weight:600}input,select,textarea{display:block;width:100%;padding:9px;border:1px solid #bac2d0;border-radius:6px;margin-top:5px}button,.button{border:0;border-radius:6px;background:#2f67dc;color:white;padding:10px 16px;text-decoration:none;cursor:pointer;font-weight:600}.secondary{background:#5a6477}.danger{background:#b42318}.warning{color:#8f1d14}table{width:100%;border-collapse:collapse;background:white}th,td{padding:11px;border-bottom:1px solid #dde2ea;text-align:left;vertical-align:top}th{background:#e9edf4}.table-wrap{overflow:auto}small{color:#647086}.total,.company-total,.grand-total{font-weight:700}.grand-total{font-size:20px;text-align:right}.report-company{margin:24px 0 36px}.errorlist{color:#b42318}.duration-fields{display:flex;gap:16px;border:0;padding:0;margin:20px 0}.duration-fields legend{font-weight:700;margin-bottom:8px;width:100%}.duration-fields label{width:150px}.duration-fields input{min-width:0}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends 'base.html' %}{% block content %}<section class="card narrow"><h1>Удаление задачи</h1>
|
||||||
|
<p>Вы действительно хотите удалить задачу <strong>«{{ task.title }}»</strong> от {{ task.work_date|date:'d.m.Y' }}?</p>
|
||||||
|
<p class="warning">Задача будет удалена безвозвратно и перестанет отображаться в отчётах.</p>
|
||||||
|
<form method="post">{% csrf_token %}<div class="form-actions"><a href="{% url 'task-edit' task.pk %}">Отмена</a><button class="danger">Удалить</button></div></form>
|
||||||
|
</section>{% endblock %}
|
||||||
@@ -10,4 +10,4 @@
|
|||||||
<label>Минуты{{ form.duration_minutes_input }}{{ form.duration_minutes_input.errors }}</label>
|
<label>Минуты{{ form.duration_minutes_input }}{{ form.duration_minutes_input.errors }}</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<p>{{ form.status.label_tag }}{{ form.status }}{{ form.status.errors }}</p>
|
<p>{{ form.status.label_tag }}{{ form.status }}{{ form.status.errors }}</p>
|
||||||
<button>Сохранить</button> <a href="{% url 'task-list' %}">Отмена</a></form></section>{% endblock %}
|
<div class="form-actions"><div><button>Сохранить</button> <a href="{% url 'task-list' %}">Отмена</a></div>{% if task %}<a class="button danger" href="{% url 'task-delete' task.pk %}">Удалить задачу</a>{% endif %}</div></form></section>{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user