Attendance matrix.

This commit is contained in:
Casper V. Kristensen 2020-01-07 00:04:58 +01:00
parent c01bb56b5d
commit 84f3554d98
Signed by: caspervk
GPG key ID: 289CA03790535054
8 changed files with 108 additions and 51 deletions

View file

@ -165,3 +165,9 @@ DEFAULT_ATTENDANCE = {
"Stand By": 1.0,
"Confirmed": 1.0,
}
ATTENDANCE_COLORS = [
(0.8, "#28a745"),
(0.5, "#ffc107"),
(-10, "#dc3545")
]

View file

@ -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 %}

View 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 %}

View file

@ -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 %}

View file

View 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)

View file

@ -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"),
]

View file

@ -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: