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:
7 files changed
+1494
-7
No files matched your search
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
Reference in new issue
Block a user