Attendance matrix.
This commit is contained in:
parent
c01bb56b5d
commit
84f3554d98
|
@ -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")
|
||||||
|
]
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
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
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
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.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:
|
||||||
|
|
Loading…
Reference in a new issue