feat(src): replace config with Django settings and add board view with context processors for dashboard metrics

Replace decouple config calls for Google OAuth credentials with settings; add new board view and associated context processors to render project board and dashboard charts; update serializer import path; centralize configuration and improve maintainability.
This commit is contained in:
sankalp committed 2026-06-25 23:31:04 +05:45
1 parent b8070e292c
commit 70fd9711b5
7 files changed
+1494 -7

No files matched your search

+6 -6
View File
@@ -1,5 +1,5 @@
import requests
from decouple import config
from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework import generics, permissions, status
from rest_framework.decorators import api_view, permission_classes
@@ -45,9 +45,9 @@ class GoogleLoginView(APIView):
token_url = "https://oauth2.googleapis.com/token"
data = {
"code": code,
"client_id": config("GOOGLE_CLIENT_ID"),
"client_secret": config("GOOGLE_CLIENT_SECRET"),
"redirect_uri": config("GOOGLE_REDIRECT_URI"),
"client_id": settings.GOOGLE_CLIENT_ID,
"client_secret": settings.GOOGLE_CLIENT_SECRET,
"redirect_uri": settings.GOOGLE_REDIRECT_URI,
"grant_type": "authorization_code",
}
@@ -109,8 +109,8 @@ class GoogleAuthUrlView(APIView):
permission_classes = (permissions.AllowAny,)
def get(self, request):
client_id = config("GOOGLE_CLIENT_ID")
redirect_uri = config("GOOGLE_REDIRECT_URI")
client_id = settings.GOOGLE_CLIENT_ID
redirect_uri = settings.GOOGLE_REDIRECT_URI
url = (
f"https://accounts.google.com/o/oauth2/v2/auth"
f"?client_id={client_id}"
+73
View File
@@ -0,0 +1,73 @@
from django.db.models import Count
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_http_methods
from src.epics.models import Epic
from src.projects.models import Project
from src.tickets.models import Ticket, TicketPriority, TicketStatus
def project_board(request, project_id=None):
projects = Project.objects.annotate(
member_count=Count("members", distinct=True),
ticket_count=Count("tickets", distinct=True),
)
return render(
request,
"projects/board.html",
{
"projects": projects,
"selected_project_id": project_id,
},
)
@require_http_methods(["GET"])
def board_data(request, project_id):
project = get_object_or_404(Project, pk=project_id)
statuses = TicketStatus.objects.filter(project=project).order_by("sort_order")
tickets = (
Ticket.objects.filter(project=project)
.select_related("status", "priority", "epic", "created_by")
.prefetch_related("assignees")
)
sort = request.GET.get("sort", "created_at_desc")
if sort == "created_at_asc":
tickets = tickets.order_by("created_at")
elif sort == "name_asc":
tickets = tickets.order_by("name")
elif sort == "name_desc":
tickets = tickets.order_by("-name")
elif sort == "due_date_asc":
tickets = tickets.order_by("due_date")
elif sort == "priority":
tickets = tickets.order_by("priority__name")
else:
tickets = tickets.order_by("-created_at")
columns = []
for status in statuses:
col_tickets = [t for t in tickets if t.status_id == status.id]
columns.append(
{
"status": status,
"tickets": col_tickets,
"count": len(col_tickets),
}
)
priorities = TicketPriority.objects.all()
epics = Epic.objects.filter(project=project)
return render(
request,
"projects/partials/board_columns.html",
{
"project": project,
"columns": columns,
"priorities": priorities,
"epics": epics,
"sort": sort,
},
)
+20
View File
@@ -0,0 +1,20 @@
from django.contrib.auth import get_user_model
from src.accounts.models import Setting
from src.tickets.models import Ticket
def site_stats(request):
User = get_user_model()
ctx = {
"total_users": User.objects.filter(is_active=True).count(),
"total_tickets": Ticket.objects.count(),
}
user_ui_settings = {}
if request.user.is_authenticated:
for s in Setting.objects.filter(user=request.user, group="ui"):
user_ui_settings[s.key] = s.value
ctx["user_ui_settings"] = user_ui_settings
ctx["navigation_style"] = user_ui_settings.get("navigation_style", "sidebar")
ctx["color_theme"] = user_ui_settings.get("color_theme", "blue")
return ctx
+112
View File
@@ -0,0 +1,112 @@
import json
from django.contrib.auth import get_user_model
from django.db.models import Count
from django.shortcuts import render
from django.utils import timezone
from src.epics.models import Epic
from src.notifications.models import Notification
from src.projects.models import Project
from src.tickets.models import Ticket, TicketPriority, TicketStatus
User = get_user_model()
def projects_list(request):
projects = Project.objects.annotate(
member_count=Count("members", distinct=True),
ticket_count=Count("tickets", distinct=True),
)
return render(
request,
"projects/list.html",
{
"projects": projects,
},
)
def tickets_list(request):
tickets = (
Ticket.objects.select_related("status", "priority", "project", "created_by")
.prefetch_related("assignees")
.order_by("-created_at")
)
if not request.user.is_superuser:
user_project_ids = request.user.projects.values_list("id", flat=True)
tickets = tickets.filter(project_id__in=user_project_ids)
project_id = request.GET.get("project")
status_id = request.GET.get("status")
priority_id = request.GET.get("priority")
if project_id:
tickets = tickets.filter(project_id=project_id)
if status_id:
tickets = tickets.filter(status_id=status_id)
if priority_id:
tickets = tickets.filter(priority_id=priority_id)
if not request.user.is_superuser:
user_project_ids = request.user.projects.values_list("id", flat=True)
projects = Project.objects.filter(id__in=user_project_ids)
statuses = TicketStatus.objects.filter(project_id__in=user_project_ids)
epics = Epic.objects.filter(project_id__in=user_project_ids)
else:
projects = Project.objects.all()
statuses = TicketStatus.objects.all()
epics = Epic.objects.all()
priorities = TicketPriority.objects.all()
users = User.objects.filter(is_active=True).order_by("username")
today = timezone.now().date()
# JSON serialized data for Alpine.js bulk operations
statuses_json = json.dumps(
[{"id": s.id, "name": s.name, "project_id": s.project_id} for s in statuses]
)
epics_json = json.dumps(
[{"id": e.id, "name": e.name, "project_id": e.project_id} for e in epics]
)
users_json = json.dumps(
[{"id": u.id, "name": u.get_full_name() or u.username} for u in users]
)
priorities_json = json.dumps(
[{"id": p.id, "name": p.name, "color": p.color} for p in priorities]
)
ticket_ids_json = json.dumps([t.id for t in tickets])
ticket_project_map_json = json.dumps({t.id: t.project_id for t in tickets})
return render(
request,
"tickets/list.html",
{
"tickets": tickets,
"projects": projects,
"statuses": statuses,
"priorities": priorities,
"users": users,
"epics": epics,
"today": today,
"statuses_json": statuses_json,
"epics_json": epics_json,
"users_json": users_json,
"priorities_json": priorities_json,
"ticket_ids_json": ticket_ids_json,
"ticket_project_map_json": ticket_project_map_json,
},
)
def notifications_unread_count(request):
count = Notification.objects.filter(user=request.user, read_at__isnull=True).count()
return render(
request,
"dashboard/partials/notif_badge.html",
{
"count": count,
},
)
File diff suppressed because it is too large. Load diff
+214
View File
@@ -0,0 +1,214 @@
import json
from datetime import date, timedelta
from django.contrib.auth import get_user_model
from django.db.models import Count, Q
from django.http import JsonResponse
from django.shortcuts import render
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from src.projects.models import Project
from src.tickets.models import Ticket, TicketHistory
User = get_user_model()
def dashboard(request):
return render(
request,
"dashboard/index.html",
{
"project_count": Project.objects.count(),
"ticket_count": Ticket.objects.count(),
"user_count": User.objects.count(),
"my_assigned": Ticket.objects.filter(assignees=request.user).count(),
},
)
@require_http_methods(["GET"])
def stats_cards(request):
user = request.user
now = timezone.now()
today = now.date()
week_start = today - timedelta(days=today.weekday())
if user.is_superuser:
total_projects = Project.objects.count()
total_tickets = Ticket.objects.count()
team_members = User.objects.count()
else:
total_projects = Project.objects.filter(members=user).count()
total_tickets = (
Ticket.objects.filter(Q(project__members=user) | Q(assignees=user))
.distinct()
.count()
)
team_members = (
User.objects.filter(project_memberships__project__members=user)
.distinct()
.count()
)
my_assigned = Ticket.objects.filter(assignees=user).count()
my_created = Ticket.objects.filter(created_by=user).count()
my_overdue = Ticket.objects.filter(
assignees=user, due_date__lt=today, status__is_completed=False
).count()
completed_this_week = Ticket.objects.filter(
Q(assignees=user) | Q(created_by=user),
status__is_completed=True,
updated_at__gte=week_start,
).count()
new_this_week = Ticket.objects.filter(
Q(assignees=user) | Q(created_by=user),
created_at__gte=week_start,
).count()
return render(
request,
"dashboard/partials/stats_cards.html",
{
"total_projects": total_projects,
"total_tickets": total_tickets,
"team_members": team_members,
"my_assigned": my_assigned,
"my_created": my_created,
"my_overdue": my_overdue,
"completed_this_week": completed_this_week,
"new_this_week": new_this_week,
"is_superuser": user.is_superuser,
},
)
@require_http_methods(["GET"])
def tickets_per_project_chart(request):
data = list(
Project.objects.annotate(count=Count("tickets")).values(
"name", "count", "color"
)
)
max_count = max((d["count"] for d in data), default=1)
for d in data:
d["pct"] = round(d["count"] / max_count * 100, 1) if max_count else 0
return render(
request,
"dashboard/partials/tickets_per_project_chart.html",
{
"chart_data": data,
},
)
@require_http_methods(["GET"])
def user_statistics_data(request):
users = User.objects.annotate(
project_count=Count("projects", distinct=True),
assigned_count=Count("assigned_tickets", distinct=True),
).values("username", "project_count", "assigned_count")[:10]
return JsonResponse(
{
"labels": [u["username"] for u in users],
"projects": [u["project_count"] for u in users],
"assigned": [u["assigned_count"] for u in users],
}
)
@require_http_methods(["GET"])
def monthly_trend_chart(request):
now = timezone.now()
months = []
for i in range(5, -1, -1):
m = now.month - i
y = now.year
while m < 1:
m += 12
y -= 1
months.append({"year": y, "month": m, "label": date(y, m, 1).strftime("%b %Y")})
data = []
for m in months:
count = Ticket.objects.filter(
created_at__year=m["year"],
created_at__month=m["month"],
).count()
data.append({"label": m["label"], "count": count})
return render(
request,
"dashboard/partials/monthly_trend_chart.html",
{
"chart_data": json.dumps(data),
},
)
@require_http_methods(["GET"])
def recent_activity(request):
histories = TicketHistory.objects.select_related(
"ticket", "user", "status", "ticket__project"
).order_by("-created_at")[:20]
return render(
request,
"dashboard/partials/recent_activity.html",
{
"histories": histories,
},
)
@require_http_methods(["GET"])
def project_timeline_widget(request):
filter_type = request.GET.get("filter", "pinned")
qs = Project.objects.filter(start_date__isnull=False, end_date__isnull=False)
pinned_count = qs.filter(pinned_date__isnull=False).count()
all_count = qs.count()
if filter_type == "pinned":
qs = qs.filter(pinned_date__isnull=False).order_by("-pinned_date")
else:
qs = qs.order_by("start_date")
today = date.today()
for p in qs:
total = (p.end_date - p.start_date).days or 1
elapsed = (today - p.start_date).days
p.progress = max(0, min(100, int(elapsed / total * 100)))
remaining = (p.end_date - today).days
if remaining < 0:
p.status_label = "Overdue"
p.status_color = "badge-error"
p.bar_color = "bg-red-500"
elif remaining <= 7:
p.status_label = "Approaching Deadline"
p.status_color = "badge-warning"
p.bar_color = "bg-yellow-500"
elif p.progress >= 100:
p.status_label = "Completed"
p.status_color = "badge-success"
p.bar_color = "bg-emerald-500"
elif p.progress > 0:
p.status_label = "In Progress"
p.status_color = "badge-info"
p.bar_color = "bg-blue-500"
else:
p.status_label = "Not Started"
p.status_color = "badge-ghost"
p.bar_color = "bg-gray-400"
return render(
request,
"dashboard/partials/project_timeline.html",
{
"projects": qs,
"current_filter": filter_type,
"pinned_count": pinned_count,
"all_count": all_count,
},
)
+1 -1
View File
@@ -1,4 +1,4 @@
from apps.accounts.serializers import UserSerializer
from src.accounts.serializers import UserSerializer
from rest_framework import serializers
from src.notes.models import ProjectNote