Compare commits
2
Commits
4dd08624f7
...
55f8e929c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55f8e929c4 | ||
|
|
961f52ea82 |
@@ -1,3 +1,7 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your tests here.
|
||||
class HomeRedirectTests(TestCase):
|
||||
def test_root_redirects_to_task_list(self):
|
||||
response = self.client.get("/")
|
||||
self.assertRedirects(response, reverse("task-list"), fetch_redirect_response=False)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.urls import path
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", RedirectView.as_view(pattern_name="task-list", permanent=False), name="home"),
|
||||
path("login/", auth_views.LoginView.as_view(template_name="accounts/login.html"), name="login"),
|
||||
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
|
||||
path("password/change/", views.password_change, name="password-change"),
|
||||
|
||||
@@ -37,6 +37,35 @@ class TaskAccessTests(TestCase):
|
||||
response = self.client.get(reverse("task-edit", args=(self.task.pk,)))
|
||||
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):
|
||||
self.client.force_login(self.worker)
|
||||
response = self.client.post(reverse("task-create"), {
|
||||
|
||||
@@ -6,4 +6,5 @@ urlpatterns = [
|
||||
path("", views.task_list, name="task-list"),
|
||||
path("create/", views.task_create, name="task-create"),
|
||||
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()
|
||||
return redirect("task-list")
|
||||
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>
|
||||
</fieldset>
|
||||
<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