Attendance matrix.
This commit is contained in:
parent
c01bb56b5d
commit
84f3554d98
|
@ -165,3 +165,9 @@ DEFAULT_ATTENDANCE = {
|
|||
"Stand By": 1.0,
|
||||
"Confirmed": 1.0,
|
||||
}
|
||||
|
||||
ATTENDANCE_COLORS = [
|
||||
(0.8, "#28a745"),
|
||||
(0.5, "#ffc107"),
|
||||
(-10, "#dc3545")
|
||||
]
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<div class="collapse navbar-collapse" id="navbar-collapse">
|
||||
<div class="navbar-nav mr-auto">
|
||||
<a class="nav-item nav-link" href="{% url 'raid_calendar' %}">Raids</a>
|
||||
<a class="nav-item nav-link" href="{% url 'user_list' %}">Users</a>
|
||||
<a class="nav-item nav-link" href="{% url 'attendance' %}">Attendance</a>
|
||||
<!--<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">DKP</a>-->
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
|
|
34
drakul/users/templates/users/attendance.html
Normal file
34
drakul/users/templates/users/attendance.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load users_extras %}
|
||||
|
||||
{% block title %}Attendance{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h2>Attendance</h2>
|
||||
<table class="table table-dark table-bordered text-center">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
{% for raid in raid_list %}
|
||||
<th><a class="text-reset" href="{% url 'raid_detail' raid.id %}">{{ raid.date | date:"d/m"}}</a></th>
|
||||
{% endfor %}
|
||||
<th>Avg</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-sm">
|
||||
{% for user, user_responses in attendance_matrix.items %}
|
||||
<tr>
|
||||
{% for response in user_responses.values %}
|
||||
<td class="response-status-{{ response.status | default_if_none:'no-color' }}-bg">
|
||||
{% attendance_cell response %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td class="font-weight-bold" style="color: {% avg_attendance_color user.avg_attendance %}">{{ user.avg_attendance | floatformat:2 }}</td>
|
||||
<td class="text-left color-class-{{ user.main.klass }}">{{ user.main }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -1,34 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Users{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h2>Users</h2>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead class="table-borderless">
|
||||
<tr>
|
||||
<th scope="col">Character Name</th>
|
||||
<th scope="col">Rank</th>
|
||||
<th scope="col">Class</th>
|
||||
<th scope="col">Role</th>
|
||||
<th scope="col">Avg Attendance (Month, Total)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in user_list %}
|
||||
<tr>
|
||||
<th scope="row">{{ user.main.name }}</th>
|
||||
<td>{{ user.rank }}</td>
|
||||
<td>{{ user.main.get_klass_display }}</td>
|
||||
<td>{{ user.main.get_role_display }}</td>
|
||||
<td>{{ user.avg_attendance_month | floatformat:2 }}, {{ user.avg_attendance_total | floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
0
drakul/users/templatetags/__init__.py
Normal file
0
drakul/users/templatetags/__init__.py
Normal file
33
drakul/users/templatetags/users_extras.py
Normal file
33
drakul/users/templatetags/users_extras.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from decimal import Decimal
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.utils.html import format_html
|
||||
|
||||
from drakul.raids.models import RaidResponse
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def attendance_cell(response: RaidResponse):
|
||||
if response is None:
|
||||
return ""
|
||||
|
||||
if response.attendance == RaidResponse.STATUS_DEFAULT_ATTENDANCE[response.status]:
|
||||
cell = ""
|
||||
else:
|
||||
cell = response.attendance
|
||||
|
||||
if response.note is not None:
|
||||
return format_html(
|
||||
'<abbr title="{}">{}</abbr>',
|
||||
response.note,
|
||||
cell or "?"
|
||||
)
|
||||
return cell
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def avg_attendance_color(avg: Decimal):
|
||||
return next(color for threshold, color in settings.ATTENDANCE_COLORS if avg >= threshold)
|
|
@ -3,5 +3,5 @@ from django.urls import path
|
|||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("users/", views.UserListView.as_view(), name="user_list"),
|
||||
path("attendance/", views.AttendanceView.as_view(), name="attendance"),
|
||||
]
|
||||
|
|
|
@ -1,25 +1,43 @@
|
|||
from contextlib import suppress
|
||||
|
||||
from django.db.models import Avg, Q
|
||||
from django.utils import timezone
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from .models import User
|
||||
from ..raids.models import Raid, RaidResponse
|
||||
|
||||
|
||||
class UserListView(ListView):
|
||||
def get_queryset(self):
|
||||
return User.objects.select_related("main").annotate(
|
||||
avg_attendance_total=Avg(
|
||||
"characters__raid_responses__attendance",
|
||||
filter=Q(characters__raid_responses__raid__date__lte=timezone.now()) # only count past raids
|
||||
),
|
||||
avg_attendance_month=Avg(
|
||||
"characters__raid_responses__attendance",
|
||||
filter=Q(
|
||||
characters__raid_responses__raid__date__lte=timezone.now(),
|
||||
characters__raid_responses__raid__date__gte=timezone.now() - timezone.timedelta(days=31)
|
||||
class AttendanceView(TemplateView):
|
||||
template_name = "users/attendance.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
users = User.objects \
|
||||
.filter(is_active=True) \
|
||||
.select_related("main") \
|
||||
.annotate(
|
||||
avg_attendance=Avg(
|
||||
"characters__raid_responses__attendance",
|
||||
filter=Q(
|
||||
characters__raid_responses__raid__date__lte=timezone.now(), # only count past raids
|
||||
characters__raid_responses__raid__is_optional=False
|
||||
)
|
||||
)
|
||||
)
|
||||
).order_by("rank", "main__name")
|
||||
).order_by("main__klass", "-avg_attendance", "main__name")
|
||||
|
||||
# Get the last 12 non-optional raids
|
||||
raids = Raid.objects.filter(
|
||||
date__lte=timezone.now(),
|
||||
is_optional=False
|
||||
).order_by("-date")[:12]
|
||||
context["raid_list"] = list(reversed(raids))
|
||||
|
||||
context["attendance_matrix"] = {user: {raid: None for raid in reversed(raids)} for user in users}
|
||||
for response in RaidResponse.objects.filter(raid__in=raids).select_related("raid", "character__user"):
|
||||
with suppress(KeyError): # KeyError means user was in previous raid but is now inactive
|
||||
context["attendance_matrix"][response.character.user][response.raid] = response
|
||||
return context
|
||||
|
||||
|
||||
# CharacterDetailView:
|
||||
|
|
Loading…
Reference in a new issue