projects: модуль проектов с шагами по отделам и Task.
Шаблоны/ручное создание, access_projects, сид «Открытие аптеки». Плюс SILO/S3 для Django media. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
"""Портал: список / карточка проектов, создание, активация."""
|
||||
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)
|
||||
Reference in New Issue
Block a user