faa06b9f82
- core/views.py: fix open redirect in login — validate next_url with url_has_allowed_host_and_scheme before redirecting - visit_manager/api/views.py: add explicit permission_classes/ authentication_classes on all 3 API views; add MIME magic-byte validation + Pillow verify for photo uploads; validate employee_id/ location_id/date query params; restrict photo access to is_staff, own record, or direct manager - employees/serializers.py: split into EmployeeSerializer (public fields only) and EmployeeDetailSerializer (PII fields, for HR/admin) - mattermost_service.py: sanitize sensitive fields (password, token) before logging; log only error message not full response body - settings/base.py: add SESSION/CSRF cookie HttpOnly + SameSite flags; set file upload size limits (15MB); remove BasicAuthentication from DRF (sends credentials in plaintext)
201 lines
7.3 KiB
Python
201 lines
7.3 KiB
Python
from django.shortcuts import render, redirect
|
|
from django.contrib.auth import authenticate, login, logout
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
from django.conf import settings
|
|
from apps.employees.models.base import Employee
|
|
from apps.locations.models.pharmacies import Pharmacy
|
|
from core.models.directories import Directory
|
|
import json
|
|
|
|
# Create your views here.
|
|
|
|
def is_htmx_request(request):
|
|
"""Check if request is from HTMX"""
|
|
return request.headers.get('HX-Request') == 'true'
|
|
|
|
@login_required
|
|
def index_view(request):
|
|
return render(request, 'index.html')
|
|
|
|
@login_required
|
|
def dashboard_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/dashboard.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/dashboard.html'})
|
|
|
|
@login_required
|
|
def employees_view(request):
|
|
location_type = request.GET.get('location_type', '')
|
|
organization_id = request.GET.get('organization_id', '')
|
|
pharmacy_id = request.GET.get('pharmacy_id', '')
|
|
|
|
employees = Employee.objects.select_related(
|
|
'department', 'post', 'user_pharmacy', 'user_organization',
|
|
'user_pharmacy__organization',
|
|
).order_by('last_name', 'first_name')
|
|
|
|
if location_type == 'office':
|
|
employees = employees.filter(user_location_type='office')
|
|
elif location_type == 'pharmacy':
|
|
employees = employees.filter(user_location_type='pharmacy')
|
|
if organization_id:
|
|
employees = employees.filter(user_pharmacy__organization_id=organization_id)
|
|
if pharmacy_id:
|
|
employees = employees.filter(user_pharmacy_id=pharmacy_id)
|
|
|
|
active_count = employees.filter(is_active=True).count()
|
|
|
|
organizations = Directory.objects.filter(
|
|
directory_type__code='organization', is_active=True
|
|
).order_by('name')
|
|
|
|
pharmacies_qs = (
|
|
Pharmacy.objects
|
|
.select_related('organization')
|
|
.filter(is_active=True)
|
|
.order_by('organization__name', 'pharmacy_number')
|
|
)
|
|
|
|
pharmacies_by_org = {}
|
|
for ph in pharmacies_qs:
|
|
key = ph.organization_id
|
|
if key not in pharmacies_by_org:
|
|
pharmacies_by_org[key] = []
|
|
pharmacies_by_org[key].append({
|
|
'id': ph.id,
|
|
'label': f"Аптека №{ph.pharmacy_number} — {ph.address}",
|
|
})
|
|
|
|
current_pharmacies = (
|
|
pharmacies_qs.filter(organization_id=organization_id)
|
|
if organization_id else pharmacies_qs
|
|
)
|
|
|
|
context = {
|
|
'employees': employees,
|
|
'active_count': active_count,
|
|
'organizations': organizations,
|
|
'current_pharmacies': current_pharmacies,
|
|
'pharmacies_by_org': pharmacies_by_org,
|
|
'filter_location_type': location_type,
|
|
'filter_organization_id': organization_id,
|
|
'filter_pharmacy_id': pharmacy_id,
|
|
}
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/employees.html', context)
|
|
return render(request, 'index.html', {'page_template': 'pages/employees.html', **context})
|
|
|
|
def build_region_tree(region_nodes):
|
|
allowed_types = {'region', 'district', 'city', 'city_district'}
|
|
tree = []
|
|
|
|
def lookup_node(list_nodes, node_id):
|
|
for item in list_nodes:
|
|
if item['id'] == node_id:
|
|
return item
|
|
return None
|
|
|
|
for region in region_nodes:
|
|
ancestors = [node for node in list(region.get_ancestors()) if node.directory_type.code in allowed_types]
|
|
parent_list = tree
|
|
for ancestor in ancestors:
|
|
existing = lookup_node(parent_list, ancestor.id)
|
|
if existing is None:
|
|
existing = {
|
|
'id': ancestor.id,
|
|
'name': ancestor.name,
|
|
'children': []
|
|
}
|
|
parent_list.append(existing)
|
|
parent_list = existing['children']
|
|
return tree
|
|
|
|
|
|
@login_required
|
|
def pharmacies_view(request):
|
|
pharmacies = Pharmacy.objects.select_related('organization', 'location_region', 'head_pharmacist').order_by('organization__name', 'pharmacy_number')
|
|
active_count = pharmacies.filter(is_active=True).count()
|
|
|
|
# Get unique regions for filter
|
|
regions = pharmacies.values_list('location_region__name', flat=True).distinct().order_by('location_region__name')
|
|
region_ids = sorted({rid for rid in pharmacies.values_list('location_region_id', flat=True).distinct() if rid})
|
|
region_nodes = Directory.objects.filter(id__in=region_ids).select_related('parent', 'directory_type')
|
|
region_tree = build_region_tree(region_nodes)
|
|
|
|
context = {
|
|
'pharmacies': pharmacies,
|
|
'active_count': active_count,
|
|
'regions': regions,
|
|
'region_tree_json': json.dumps(region_tree, ensure_ascii=False),
|
|
}
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/pharmacies.html', context)
|
|
return render(request, 'index.html', {'page_template': 'pages/pharmacies.html', **context})
|
|
|
|
@login_required
|
|
def calendar_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/calendar.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/calendar.html'})
|
|
|
|
@login_required
|
|
def timetrack_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/timetrack.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/timetrack.html'})
|
|
|
|
@login_required
|
|
def reports_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/reports.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/reports.html'})
|
|
|
|
@login_required
|
|
def documents_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/documents.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/documents.html'})
|
|
|
|
@login_required
|
|
def recruiting_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/recruiting.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/recruiting.html'})
|
|
|
|
@login_required
|
|
def structure_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/structure.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/structure.html'})
|
|
|
|
@login_required
|
|
def settings_view(request):
|
|
if is_htmx_request(request):
|
|
return render(request, 'pages/settings.html')
|
|
return render(request, 'index.html', {'page_template': 'pages/settings.html'})
|
|
|
|
|
|
def login_view(request):
|
|
if request.method == 'POST':
|
|
username = request.POST.get('username')
|
|
password = request.POST.get('password')
|
|
user = authenticate(request, username=username, password=password)
|
|
if user is not None:
|
|
login(request, user)
|
|
next_url = request.GET.get('next') or request.POST.get('next') or '/'
|
|
if not url_has_allowed_host_and_scheme(next_url, allowed_hosts=settings.ALLOWED_HOSTS):
|
|
next_url = '/'
|
|
return redirect(next_url)
|
|
return render(request, 'login.html', {'login_error': 'Неверный логин или пароль'})
|
|
return render(request, 'login.html')
|
|
|
|
|
|
def logout_view(request):
|
|
logout(request)
|
|
return redirect('login')
|
|
|
|
|
|
def modal_add_employee_view(request):
|
|
return render(request, 'partials/modal-add-employee.html')
|