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, "Stand By": 1.0,
"Confirmed": 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="collapse navbar-collapse" id="navbar-collapse">
<div class="navbar-nav mr-auto"> <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 '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>--> <!--<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">DKP</a>-->
</div> </div>
{% if user.is_authenticated %} {% 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 from . import views
urlpatterns = [ 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.db.models import Avg, Q
from django.utils import timezone from django.utils import timezone
from django.views.generic import ListView from django.views.generic import TemplateView
from .models import User from .models import User
from ..raids.models import Raid, RaidResponse
class UserListView(ListView): class AttendanceView(TemplateView):
def get_queryset(self): template_name = "users/attendance.html"
return User.objects.select_related("main").annotate(
avg_attendance_total=Avg( def get_context_data(self, **kwargs):
"characters__raid_responses__attendance", context = super().get_context_data(**kwargs)
filter=Q(characters__raid_responses__raid__date__lte=timezone.now()) # only count past raids users = User.objects \
), .filter(is_active=True) \
avg_attendance_month=Avg( .select_related("main") \
.annotate(
avg_attendance=Avg(
"characters__raid_responses__attendance", "characters__raid_responses__attendance",
filter=Q( filter=Q(
characters__raid_responses__raid__date__lte=timezone.now(), characters__raid_responses__raid__date__lte=timezone.now(), # only count past raids
characters__raid_responses__raid__date__gte=timezone.now() - timezone.timedelta(days=31) 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: # CharacterDetailView: