Files
ACORE/apps/tasks/views.py
T
lokses bf5310e489 Hide peer logins on profiles and restrict view_all to published reports.
Login stays visible only to self/manager/HR; draft report docs stay department-head only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 15:20:49 +03:00

262 lines
10 KiB
Python

from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.db.models import Count, Prefetch, Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.http import require_POST
from django.utils.translation import gettext_lazy as _
from apps.tasks.models import Task, TaskComment
from core.models.directories import Directory
from core.utils.htmx import render_page
from core.utils.permissions import is_owner, is_owner_or_staff
Employee = get_user_model()
STATUS_FILTERS = {'pending', 'in_progress', 'done'}
# Участники задачи показываются с должностью (emp.post.name) — подгружаем
# её вместе с prefetch, чтобы не бить БД на каждого участника в шаблоне
_EMPLOYEES_WITH_POST = Employee.objects.select_related('post')
ROLE_PREFETCH = [
Prefetch('responsible', queryset=_EMPLOYEES_WITH_POST),
Prefetch('co_executors', queryset=_EMPLOYEES_WITH_POST),
Prefetch('observers', queryset=_EMPLOYEES_WITH_POST),
]
def can_edit_task(user, task) -> bool:
"""Редактировать может автор или администратор"""
return is_owner_or_staff(user, task, 'creator_id')
def can_delete_task(user, task) -> bool:
"""Удалять с портала может только автор"""
return is_owner(user, task, 'creator_id')
def _employee_dict(emp):
return {
'pk': emp.pk,
'label': emp.get_full_name() or '—',
'position': emp.post.name if emp.post_id else '',
'photo': emp.photo.url if emp.photo else '',
}
def _department_tree():
"""Дерево відділів (для навігації у пікері) + список без відділу.
Кожен вузол: {pk, name, children: [...], employees: [...]}."""
depts = list(
Directory.objects.filter(directory_type__code='department', is_active=True)
.select_related('parent')
.order_by('name')
)
employees = list(
Employee.objects.filter(is_active=True)
.select_related('post', 'department')
.order_by('last_name', 'first_name')
)
by_dept = {}
for emp in employees:
by_dept.setdefault(emp.department_id, []).append(emp)
nodes = {
d.pk: {
'pk': d.pk, 'name': d.name, 'children': [],
'employees': [_employee_dict(e) for e in by_dept.get(d.pk, [])],
}
for d in depts
}
roots = []
for d in depts:
node = nodes[d.pk]
parent_node = nodes.get(d.parent_id)
if parent_node is not None:
parent_node['children'].append(node)
else:
roots.append(node)
no_department = [_employee_dict(e) for e in by_dept.get(None, [])]
return roots, no_department, employees
def _form_choices():
"""Дані для пікерів людей: плоский список (джерело істини для <select>)
та дерево відділів (для навігації по ньому у віджеті)."""
tree, no_department, employees = _department_tree()
return {
'employee_options': [_employee_dict(e) for e in employees],
'department_tree': {'tree': tree, 'no_department': no_department},
'priorities': Task.PRIORITY_CHOICES,
'empty_ids': set(),
}
def _apply_form(task, request):
"""Заполнение задачи из формы; возвращает текст ошибки или ''"""
title = request.POST.get('title', '').strip()
if not title:
return 'Задачу не збережено: вкажіть назву.'
task.title = title
task.description = request.POST.get('description', '').strip()
priority = request.POST.get('priority', 'medium')
task.priority = priority if priority in dict(Task.PRIORITY_CHOICES) else 'medium'
task.due_date = request.POST.get('due_date') or None
task.save()
active_employees = Employee.objects.filter(is_active=True)
task.responsible.set(active_employees.filter(pk__in=request.POST.getlist('responsible')))
task.co_executors.set(active_employees.filter(pk__in=request.POST.getlist('co_executors')))
task.observers.set(active_employees.filter(pk__in=request.POST.getlist('observers')))
return ''
@login_required
def tasks_view(request):
"""Мои задачи: создал сам или назначен напрямую"""
form_error = ''
if request.method == 'POST':
task = Task(creator=request.user)
form_error = _apply_form(task, request)
# Перехід зі сторінки співробітника ("Призначити задачу") —
# відкриваємо форму одразу з обраним відповідальним
preselect_responsible_ids = set()
raw_responsible = request.GET.get('responsible', '')
if raw_responsible.isdigit() and Employee.objects.filter(pk=raw_responsible, is_active=True).exists():
preselect_responsible_ids = {int(raw_responsible)}
active_status = request.GET.get('status', '')
if active_status not in STATUS_FILTERS:
active_status = ''
base = Task.objects.for_user(request.user)
# Один aggregate-запрос вместо четырёх .count() — distinct=True считает
# уникальные pk, что корректно даже с дублями строк от JOIN по M2M
counts = base.aggregate(
all=Count('pk', distinct=True),
pending=Count('pk', distinct=True, filter=Q(status='pending')),
in_progress=Count('pk', distinct=True, filter=Q(status='in_progress')),
done=Count('pk', distinct=True, filter=Q(status='done')),
)
visible = base.select_related('creator').prefetch_related(*ROLE_PREFETCH)
tasks = visible.filter(status=active_status) if active_status else visible.exclude(status='cancelled')
tasks = list(tasks[:100])
for task in tasks:
task.can_edit = can_edit_task(request.user, task)
task.can_delete = can_delete_task(request.user, task)
# Для короткого підсумку в рядку списку: відповідальний + співвиконавці
# (обидва вже prefetch-нуті — .all() не робить нових запитів)
task.display_people = list(task.responsible.all()) + list(task.co_executors.all())
context = {
'tasks': tasks,
'counts': counts,
'active_status': active_status,
'form_error': form_error,
'form_open': bool(form_error or preselect_responsible_ids),
'preselect_responsible_ids': preselect_responsible_ids,
**_form_choices(),
}
return render_page(request, 'tasks.html', context)
def _get_visible_task(request, pk):
task = get_object_or_404(
Task.objects.select_related('creator').prefetch_related(*ROLE_PREFETCH),
pk=pk,
)
visible = (
request.user.is_staff
or Task.objects.for_user(request.user).filter(pk=task.pk).exists()
)
if not visible:
raise Http404
return task
@login_required
def task_detail_view(request, pk):
"""Задача: описание, участники, смена статуса, комментарии"""
task = _get_visible_task(request, pk)
is_participant = task.is_participant(request.user)
if request.method == 'POST':
action = request.POST.get('action')
if action == 'status' and is_participant:
status = request.POST.get('status')
if status in dict(Task.STATUS_CHOICES):
task.set_status(status)
elif action == 'comment':
text = request.POST.get('text', '').strip()
if text:
TaskComment.objects.create(task=task, author=request.user, text=text)
elif action == 'checklist_toggle' and is_participant:
item = task.checklist.filter(pk=request.POST.get('item_id')).first()
if item:
item.set_done(not item.is_done, user=request.user)
context = {
'task': task,
'comments': task.comments.select_related('author'),
'checklist': task.checklist.select_related('done_by'),
'is_participant': is_participant,
'can_edit': can_edit_task(request.user, task),
'can_delete': can_delete_task(request.user, task),
'role_groups': [
(_('Ответственный'), list(task.responsible.all())),
(_('Соисполнители'), list(task.co_executors.all())),
(_('Наблюдатели'), list(task.observers.all())),
],
}
return render_page(request, 'task-detail.html', context)
@login_required
@require_POST
def task_toggle_view(request, pk):
"""Чекбокс в списке: выполнено <-> в ожидании"""
task = _get_visible_task(request, pk)
if task.is_participant(request.user):
task.set_status('pending' if task.status == 'done' else 'done')
return redirect('tasks')
@login_required
def task_edit_view(request, pk):
"""Редактирование: автор или администратор"""
task = get_object_or_404(Task.objects.prefetch_related(*ROLE_PREFETCH), pk=pk)
if not can_edit_task(request.user, task):
raise Http404
form_error = ''
if request.method == 'POST':
form_error = _apply_form(task, request)
if not form_error:
return redirect('task_detail', pk=task.pk)
context = {
'task': task,
'form_error': form_error,
'responsible_ids': {e.pk for e in task.responsible.all()},
'co_executor_ids': {e.pk for e in task.co_executors.all()},
'observer_ids': {e.pk for e in task.observers.all()},
**_form_choices(),
}
return render_page(request, 'task-edit.html', context)
@login_required
@require_POST
def task_delete_view(request, pk):
"""Удаление: только автор"""
task = get_object_or_404(Task, pk=pk)
if not can_delete_task(request.user, task):
raise Http404
task.delete()
return redirect('tasks')