Files
project-managment/src/dashboard/page_views/other_views.py
T
sankalp ee5338e71c style(src/dashboard/page_views): reformat render calls
Make render invocations consistently formatted across files
2026-06-29 08:05:10 +05:45

430 lines
15 KiB
Python

import hashlib
import json
from datetime import date, timedelta
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.text import slugify
from django.views.decorators.http import require_http_methods
from src.accounts.models import ADMIN_ROLES, Role, Setting
from src.projects.models import BoardRolePermission
from src.tickets.models import (
Ticket,
TicketComment,
TicketHistory,
TicketPriority,
)
User = get_user_model()
COLOR_PALETTE = [
("blue", "#3b82f6", "Blue"),
("emerald", "#10b981", "Emerald"),
("green", "#22c55e", "Green"),
("indigo", "#6366f1", "Indigo"),
("orange", "#f97316", "Orange"),
("purple", "#a855f7", "Purple"),
("red", "#ef4444", "Red"),
("rose", "#f43f5e", "Rose"),
("sky", "#0ea5e9", "Sky"),
("teal", "#14b8a6", "Teal"),
("violet", "#8b5cf6", "Violet"),
("amber", "#f59e0b", "Amber"),
("yellow", "#eab308", "Yellow"),
("pink", "#ec4899", "Pink"),
("cyan", "#06b6d4", "Cyan"),
("lime", "#84cc16", "Lime"),
("slate", "#64748b", "Slate"),
("gray", "#6b7280", "Gray"),
("zinc", "#71717a", "Zinc"),
]
# ── Leaderboard ───────────────────────────────────────────────────────────
def leaderboard(request):
days = int(request.GET.get("days", 7))
top_n = int(request.GET.get("top", 10))
since = timezone.now() - timedelta(days=days)
user_ids = list(User.objects.filter(is_active=True).values_list("id", flat=True))
tickets_created_map = dict(
Ticket.objects.filter(created_by_id__in=user_ids, created_at__gte=since)
.values("created_by_id")
.annotate(count=Count("id"))
.values_list("created_by_id", "count")
)
status_changes_map = dict(
TicketHistory.objects.filter(user_id__in=user_ids, created_at__gte=since)
.values("user_id")
.annotate(count=Count("id"))
.values_list("user_id", "count")
)
comments_map = dict(
TicketComment.objects.filter(user_id__in=user_ids, created_at__gte=since)
.values("user_id")
.annotate(count=Count("id"))
.values_list("user_id", "count")
)
active_days_map = dict(
TicketHistory.objects.filter(user_id__in=user_ids, created_at__gte=since)
.values("user_id")
.annotate(count=Count("created_at", distinct=True))
.values_list("user_id", "count")
)
users = User.objects.filter(id__in=user_ids).only(
"id", "first_name", "last_name", "username"
)
leaderboard_data = []
for user in users:
created = tickets_created_map.get(user.id, 0)
changes = status_changes_map.get(user.id, 0)
comments = comments_map.get(user.id, 0)
days_active = active_days_map.get(user.id, 0)
score = (changes * 5) + (created * 2) + (comments * 2) + (days_active * 1)
if score > 0 or created > 0 or comments > 0:
leaderboard_data.append(
{
"user": user,
"score": score,
"tickets_created": created,
"status_changes": changes,
"comments_made": comments,
"active_days": days_active,
}
)
leaderboard_data.sort(key=lambda x: x["score"], reverse=True)
leaderboard_data = leaderboard_data[:top_n]
for entry in leaderboard_data:
name = entry["user"].get_full_name() or entry["user"].username
h = hashlib.md5(name.encode()).hexdigest()[:6]
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
entry["avatar_color"] = f"rgb({r},{g},{b})"
context = {
"leaderboard": leaderboard_data,
"days": days,
"top_n": top_n,
}
# For HTMX requests, render only the partial content (avoids nesting full page inside main)
if request.META.get("HTTP_HX_REQUEST"):
return render(request, "leaderboard/partials/content.html", context)
return render(request, "leaderboard/pages/index.html", context)
# ── User Contributions (Heatmap) ──────────────────────────────────────────
def user_contributions(request):
current_year = timezone.now().year
year_str = request.GET.get("year")
if year_str and year_str.isdigit():
target_year = int(year_str)
else:
target_year = current_year
tz = timezone.get_current_timezone()
# Start and end of the target year
since = timezone.make_aware(timezone.datetime(target_year, 1, 1), timezone=tz)
if target_year == current_year:
end = timezone.now().date()
else:
end = timezone.datetime(target_year, 12, 31).date()
# Get available years for the tabs
first_history = TicketHistory.objects.order_by("created_at").first()
start_year = first_history.created_at.year if first_history else current_year
available_years = list(range(start_year, current_year + 1))
# Ensure target_year is in available_years
if target_year not in available_years and target_year <= current_year:
available_years.append(target_year)
available_years.sort(reverse=True)
user_ids = list(
User.objects.filter(is_active=True).values_list("id", flat=True)[:10]
)
tickets_created_map = dict(
Ticket.objects.filter(created_by_id__in=user_ids, created_at__year=target_year)
.values("created_by_id")
.annotate(count=Count("id"))
.values_list("created_by_id", "count")
)
status_changes_map = dict(
TicketHistory.objects.filter(user_id__in=user_ids, created_at__year=target_year)
.values("user_id")
.annotate(count=Count("id"))
.values_list("user_id", "count")
)
comments_map = dict(
TicketComment.objects.filter(user_id__in=user_ids, created_at__year=target_year)
.values("user_id")
.annotate(count=Count("id"))
.values_list("user_id", "count")
)
active_days_map = dict(
TicketHistory.objects.filter(user_id__in=user_ids, created_at__year=target_year)
.values("user_id")
.annotate(count=Count("created_at", distinct=True))
.values_list("user_id", "count")
)
all_history = TicketHistory.objects.filter(
user_id__in=user_ids, created_at__year=target_year
).values("user_id", "created_at")
history_by_user = {}
for h in all_history:
uid = h["user_id"]
if uid not in history_by_user:
history_by_user[uid] = []
history_by_user[uid].append(h["created_at"])
users = User.objects.filter(id__in=user_ids).only(
"id", "first_name", "last_name", "username"
)
contributions = []
for user in users:
uid = user.id
created = tickets_created_map.get(uid, 0)
changes = status_changes_map.get(uid, 0)
comments = comments_map.get(uid, 0)
days_active = active_days_map.get(uid, 0)
heatmap_values = {}
current = since.date()
while current <= end:
heatmap_values[current.isoformat()] = 0
current += timedelta(days=1)
for dt in history_by_user.get(uid, []):
day = dt.date().isoformat()
if day in heatmap_values:
heatmap_values[day] += 1
max_val = max(heatmap_values.values()) if heatmap_values else 1
weeks = []
week = []
sorted_days = sorted(heatmap_values.keys())
for day_str in sorted_days:
day_date = date.fromisoformat(day_str)
count = heatmap_values[day_str]
if count == 0:
level = 0
elif count <= max_val // 4:
level = 1
elif count <= max_val // 2:
level = 2
elif count <= int(max_val / 1.5):
level = 3
else:
level = 4
week.append(
{
"date": day_str,
"count": count,
"level": level,
"day": day_date.weekday(),
}
)
if day_date.weekday() == 6:
weeks.append(week)
week = []
if week:
weeks.append(week)
contributions.append(
{
"user": user,
"tickets_created": created,
"status_changes": changes,
"comments_made": comments,
"active_days": days_active,
"weeks": weeks,
"max_val": max_val,
}
)
return render(
request,
"contributions/pages/index.html",
{
"contributions": contributions,
"target_year": target_year,
"available_years": available_years,
},
)
# ── Ticket Priorities ─────────────────────────────────────────────────────
@require_http_methods(["GET"])
def ticket_priorities(request):
priorities = TicketPriority.objects.all().order_by("name")
return render(request, "tickets/pages/priorities.html", {"priorities": priorities})
@require_http_methods(["POST"])
def ticket_priority_add(request):
if not request.user.is_admin:
messages.error(request, "Permission denied.")
return redirect("ticket-priorities")
name = request.POST.get("name", "").strip()
color = request.POST.get("color", "#6B7280")
if name:
TicketPriority.objects.create(name=name, color=color)
messages.success(request, "Priority added successfully.")
else:
messages.error(request, "Priority name is required.")
return redirect("ticket-priorities")
@require_http_methods(["POST"])
def ticket_priority_edit(request, priority_id):
if not request.user.is_admin:
messages.error(request, "Permission denied.")
return redirect("ticket-priorities")
priority = get_object_or_404(TicketPriority, pk=priority_id)
name = request.POST.get("name", "").strip()
color = request.POST.get("color", priority.color)
if name:
priority.name = name
priority.color = color
priority.save(update_fields=["name", "color"])
messages.success(request, "Priority updated successfully.")
else:
messages.error(request, "Priority name is required.")
return redirect("ticket-priorities")
@require_http_methods(["POST"])
def ticket_priority_delete(request, priority_id):
if not request.user.is_admin:
messages.error(request, "Permission denied.")
return redirect("ticket-priorities")
priority = get_object_or_404(TicketPriority, pk=priority_id)
priority.delete()
messages.success(request, "Priority deleted successfully.")
return redirect("ticket-priorities")
# ── Roles CRUD ───────────────────────────────────────────────────────────────
def roles_list(request):
if not request.user.is_admin:
messages.error(request, "Permission denied.")
return redirect("dashboard")
roles = Role.objects.all().order_by("name")
return render(
request,
"accounts/pages/roles.html",
{"roles": roles, "admin_roles": ADMIN_ROLES},
)
@require_http_methods(["POST"])
def role_add(request):
if not request.user.is_admin:
messages.error(request, "Permission denied.")
return redirect("roles-list")
name = request.POST.get("name", "").strip()
if name:
slug = slugify(name)
if not slug:
messages.error(request, "Could not generate a valid slug from that name.")
elif Role.objects.filter(slug=slug).exists():
messages.error(request, f"Role with slug '{slug}' already exists.")
else:
Role.objects.create(name=name, slug=slug, is_system=False)
messages.success(request, f"Role '{name}' created.")
else:
messages.error(request, "Name is required.")
return redirect("roles-list")
@require_http_methods(["POST"])
def role_delete(request, role_id):
if not request.user.is_admin:
messages.error(request, "Permission denied.")
return redirect("roles-list")
role = get_object_or_404(Role, pk=role_id)
if role.is_system:
messages.error(request, "System roles cannot be deleted.")
return redirect("roles-list")
user_count = role.users.count()
if user_count:
messages.error(
request,
f"Cannot delete role '{role.name}' — it is assigned to {user_count} user(s).",
)
return redirect("roles-list")
BoardRolePermission.objects.filter(role=role.slug).delete()
role.delete()
messages.success(request, f"Role '{role.name}' deleted.")
return redirect("roles-list")
# ── UI Settings ────────────────────────────────────────────────────────────
def ui_settings(request):
the_user = request.user
user_settings = {}
if the_user.is_authenticated:
for s in Setting.objects.filter(user=the_user, group="ui"):
user_settings[s.key] = s.value
if request.method == "POST":
navigation_style = request.POST.get("navigation_style", "sidebar")
color_theme = request.POST.get("color_theme", "blue")
valid_nav = ("sidebar", "top")
valid_colors = {c[0] for c in COLOR_PALETTE}
if navigation_style not in valid_nav:
navigation_style = "sidebar"
if color_theme not in valid_colors:
color_theme = "blue"
Setting.objects.update_or_create(
user=the_user,
key="navigation_style",
defaults={"value": navigation_style, "group": "ui"},
)
Setting.objects.update_or_create(
user=the_user,
key="color_theme",
defaults={"value": color_theme, "group": "ui"},
)
messages.success(request, "UI settings saved successfully.")
return redirect("ui-settings")
return render(
request,
"settings/pages/ui.html",
{
"navigation_style": user_settings.get("navigation_style", "sidebar"),
"color_theme": user_settings.get("color_theme", "blue"),
"color_palette": COLOR_PALETTE,
"color_map": json.dumps({c[0]: c[1] for c in COLOR_PALETTE}),
},
)