"""Портал: список / карточка проектов, создание, активация.""" from __future__ import annotations from datetime import datetime from django.contrib.auth.decorators import login_required from django.db.models import Prefetch from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect from django.views.decorators.http import require_POST from apps.projects.models import ( Project, ProjectStep, ProjectStepItem, ProjectTemplate, ) from apps.projects.services import ( activate_project, activate_step, create_manual_project, create_project_from_template, ) from core.models.directories import Directory from core.portal_access import section_required from core.utils.htmx import render_page def _parse_date(raw: str): raw = (raw or '').strip() if not raw: return None try: return datetime.strptime(raw, '%Y-%m-%d').date() except ValueError: return None def _can_manage_project(user, project: Project) -> bool: if not user or not user.is_authenticated: return False if user.is_staff or user.is_superuser: return True return project.owner_id == user.pk or project.created_by_id == user.pk def _departments(): return list( Directory.objects.filter( directory_type__code='department', is_active=True, ).order_by('name') ) def _project_qs(): return ( Project.objects.select_related('owner', 'pharmacy', 'template') .prefetch_related( Prefetch( 'steps', queryset=ProjectStep.objects.select_related('department').prefetch_related( Prefetch( 'items', queryset=ProjectStepItem.objects.select_related( 'task', 'task__creator', ).prefetch_related('task__responsible'), ), ), ), ) ) @login_required @section_required('projects') def projects_list_view(request): status = (request.GET.get('status') or '').strip() qs = Project.objects.for_user(request.user).select_related( 'owner', 'pharmacy', 'template', ) if status in { Project.STATUS_DRAFT, Project.STATUS_ACTIVE, Project.STATUS_DONE, Project.STATUS_CANCELLED, }: qs = qs.filter(status=status) projects = list(qs[:200]) for p in projects: p.done_count, p.total_count = p.progress_counts() p.pct = p.progress_percent return render_page(request, 'projects.html', { 'projects': projects, 'active_status': status, 'templates': list( ProjectTemplate.objects.filter(is_active=True).order_by('name') ), 'departments': _departments(), 'ug_message': request.session.pop('projects_message', ''), 'ug_error': request.session.pop('projects_error', ''), }) @login_required @section_required('projects') def project_detail_view(request, pk: int): project = get_object_or_404(_project_qs(), pk=pk) if not Project.objects.for_user(request.user).filter(pk=project.pk).exists(): return HttpResponseForbidden('Нет доступа к проекту.') done, total = project.progress_counts() return render_page(request, 'project-detail.html', { 'project': project, 'done_count': done, 'total_count': total, 'pct': project.progress_percent, 'can_manage': _can_manage_project(request.user, project), 'departments': _departments(), 'ug_message': request.session.pop('projects_message', ''), 'ug_error': request.session.pop('projects_error', ''), }) @login_required @section_required('projects') @require_POST def project_create_view(request): mode = (request.POST.get('mode') or 'manual').strip() title = (request.POST.get('title') or '').strip() description = (request.POST.get('description') or '').strip() start_date = _parse_date(request.POST.get('start_date')) due_date = _parse_date(request.POST.get('due_date')) pharmacy_id = (request.POST.get('pharmacy_id') or '').strip() pharmacy = None if pharmacy_id.isdigit(): from apps.locations.models import Pharmacy pharmacy = Pharmacy.objects.filter(pk=int(pharmacy_id)).first() try: if mode == 'template': tid = (request.POST.get('template_id') or '').strip() if not tid.isdigit(): raise ValueError('Выберите шаблон.') template = get_object_or_404( ProjectTemplate, pk=int(tid), is_active=True, ) project = create_project_from_template( template=template, title=title or template.name, owner=request.user, created_by=request.user, start_date=start_date, due_date=due_date, pharmacy=pharmacy, description=description, ) else: if not title: raise ValueError('Укажите название проекта.') steps = [] step_titles = request.POST.getlist('step_title') step_depts = request.POST.getlist('step_department_id') step_items = request.POST.getlist('step_items') for i, st in enumerate(step_titles): st = (st or '').strip() if not st: continue dept_raw = step_depts[i] if i < len(step_depts) else '' items_raw = step_items[i] if i < len(step_items) else '' items = [] for line in (items_raw or '').splitlines(): line = line.strip() if line: items.append({'title': line}) if not items: items = [{'title': st}] steps.append({ 'title': st, 'department_id': int(dept_raw) if str(dept_raw).isdigit() else None, 'order': i, 'items': items, }) project = create_manual_project( title=title, owner=request.user, created_by=request.user, description=description, start_date=start_date, due_date=due_date, pharmacy=pharmacy, steps=steps, ) except ValueError as exc: request.session['projects_error'] = str(exc) return redirect('projects') request.session['projects_message'] = f'Проект «{project.title}» создан (черновик).' return redirect('project_detail', pk=project.pk) @login_required @section_required('projects') @require_POST def project_activate_view(request, pk: int): project = get_object_or_404(Project, pk=pk) if not _can_manage_project(request.user, project): return HttpResponseForbidden('Нет права запускать проект.') try: activate_project(project) except ValueError as exc: request.session['projects_error'] = str(exc) return redirect('project_detail', pk=pk) request.session['projects_message'] = 'Проект активирован, задачи созданы.' return redirect('project_detail', pk=pk) @login_required @section_required('projects') @require_POST def project_step_activate_view(request, pk: int, step_id: int): project = get_object_or_404(Project, pk=pk) if not _can_manage_project(request.user, project): return HttpResponseForbidden('Нет права запускать шаг.') step = get_object_or_404(ProjectStep, pk=step_id, project=project) activate_step(step) request.session['projects_message'] = f'Шаг «{step.title}» запущен.' return redirect('project_detail', pk=pk) @login_required @section_required('projects') @require_POST def project_add_step_view(request, pk: int): project = get_object_or_404(Project, pk=pk) if not _can_manage_project(request.user, project): return HttpResponseForbidden('Нет права менять шаги.') title = (request.POST.get('title') or '').strip() if not title: request.session['projects_error'] = 'Укажите название шага.' return redirect('project_detail', pk=pk) dept_raw = (request.POST.get('department_id') or '').strip() order = project.steps.count() step = ProjectStep.objects.create( project=project, title=title, order=order, department_id=int(dept_raw) if dept_raw.isdigit() else None, planned_start=project.start_date, planned_due=project.due_date, ) items_raw = (request.POST.get('items') or '').strip() lines = [ln.strip() for ln in items_raw.splitlines() if ln.strip()] or [title] for j, line in enumerate(lines): ProjectStepItem.objects.create(step=step, title=line, order=j) request.session['projects_message'] = f'Шаг «{title}» добавлен.' return redirect('project_detail', pk=pk)