Skip to content

Commit 89c17fb

Browse files
authored
Merge pull request #84 from srobo/feature/team-attendance
Team attendance register
2 parents a3b4923 + 797f11a commit 89c17fb

File tree

12 files changed

+263
-23
lines changed

12 files changed

+263
-23
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Generated by Django 4.2.11 on 2025-03-20 08:42
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("accounts", "0005_user_onboarded_at"),
9+
]
10+
11+
operations = [
12+
migrations.AlterModelOptions(
13+
name="user",
14+
options={"ordering": ["first_name", "last_name"]},
15+
),
16+
]

helpdesk/teams/forms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django import forms
2+
3+
from teams.models import Team, TeamAttendanceEvent
4+
5+
6+
class TeamAttendanceLogForm(forms.ModelForm):
7+
team = forms.ModelChoiceField(queryset=Team.objects.all(), widget=forms.HiddenInput())
8+
9+
class Meta:
10+
model = TeamAttendanceEvent
11+
fields = ("type", "comment", "team")
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Generated by Django 4.2.11 on 2025-03-22 16:10
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
("teams", "0004_add_team_comment"),
12+
]
13+
14+
operations = [
15+
migrations.AlterModelOptions(
16+
name="team",
17+
options={"ordering": ["tla"]},
18+
),
19+
migrations.CreateModel(
20+
name="TeamAttendanceEvent",
21+
fields=[
22+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
23+
(
24+
"type",
25+
models.TextField(
26+
choices=[
27+
("ARRIVED", "Arrived"),
28+
("LEFT", "Left"),
29+
("DELAYED", "Delayed"),
30+
("DROPPED_OUT", "Dropped Out"),
31+
("UNKNOWN", "Unknown"),
32+
],
33+
max_length=11,
34+
),
35+
),
36+
("comment", models.TextField(blank=True)),
37+
("created_at", models.DateTimeField(auto_now_add=True)),
38+
(
39+
"team",
40+
models.ForeignKey(
41+
on_delete=django.db.models.deletion.CASCADE,
42+
related_name="team_attendance_events",
43+
related_query_name="team_attendance_events",
44+
to="teams.team",
45+
),
46+
),
47+
("user", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
48+
],
49+
),
50+
]

helpdesk/teams/models.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,33 @@ class Meta:
5858

5959
def __str__(self) -> str:
6060
return f"Comment on {self.team.name} at {self.created_at} by {self.author}"
61+
62+
63+
class TeamAttendanceEventType(models.TextChoices):
64+
ARRIVED = "ARRIVED", "Arrived"
65+
LEFT = "LEFT", "Left"
66+
DELAYED = "DELAYED", "Delayed"
67+
DROPPED_OUT = "DROPPED_OUT", "Dropped Out"
68+
UNKNOWN = "UNKNOWN", "Unknown"
69+
70+
71+
class TeamAttendanceEvent(models.Model):
72+
team = models.ForeignKey(
73+
Team,
74+
on_delete=models.CASCADE,
75+
related_name="team_attendance_events",
76+
related_query_name="team_attendance_events",
77+
)
78+
user = models.ForeignKey(
79+
"accounts.User",
80+
on_delete=models.PROTECT,
81+
)
82+
type = models.TextField(
83+
max_length=11,
84+
choices=TeamAttendanceEventType.choices,
85+
)
86+
comment = models.TextField(blank=True)
87+
created_at = models.DateTimeField(auto_now_add=True)
88+
89+
def __str__(self) -> str:
90+
return f"Attendance Event: {self.team.name} {self.type} at {self.created_at}"

helpdesk/teams/tables.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import django_tables2 as tables
22

3-
from .models import Team
3+
from .models import Team, TeamAttendanceEvent, TeamAttendanceEventType
44

55

66
class TeamTable(tables.Table):
@@ -13,3 +13,39 @@ class Meta:
1313
model = Team
1414
exclude = ("id", "pit_location")
1515
order_by = "tla"
16+
17+
18+
class TeamAttendanceOverviewTable(tables.Table):
19+
name = tables.LinkColumn("teams:team_detail", args=[tables.A("tla")])
20+
latest_event__0__type = tables.Column("Latest Event")
21+
latest_event__0__comment = tables.Column("Comment")
22+
latest_event__0__created_at = tables.DateTimeColumn(verbose_name="Time", format="D H:i")
23+
user = tables.TemplateColumn(
24+
verbose_name="Logged by",
25+
template_code='{{record.latest_event.0.user|default:"—"}}',
26+
)
27+
actions = tables.LinkColumn("teams:team_log_attendance_form", args=[tables.A("tla")], text="Log")
28+
29+
def render_latest_event__0__type(self, value: str) -> str | None:
30+
lookups = dict(TeamAttendanceEventType.choices)
31+
return lookups.get(value)
32+
33+
class Meta:
34+
model = Team
35+
exclude = ["id", "tla", "is_rookie", "pit_location"]
36+
37+
38+
class TeamAttendanceListTable(tables.Table):
39+
type = tables.Column()
40+
comment = tables.Column()
41+
created_at = tables.DateTimeColumn(verbose_name="Time", format="D H:i")
42+
user = tables.TemplateColumn(
43+
verbose_name="Logged by",
44+
template_code='{{record.user|default:"—"}}',
45+
)
46+
47+
class Meta:
48+
model = TeamAttendanceEvent
49+
sequence = ("created_at", "type", "comment", "user")
50+
order_by = "-created_at"
51+
exclude = ["id", "team"]

helpdesk/teams/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.urls import path
22

33
from .views import (
4+
TeamAttendanceFormView,
5+
TeamAttendanceView,
46
TeamDetailAboutView,
57
TeamDetailCommentsView,
68
TeamDetailTicketsView,
@@ -14,6 +16,8 @@
1416

1517
urlpatterns = [
1618
path("", TeamListView.as_view(), name="team_list"),
19+
path("attendance", TeamAttendanceView.as_view(), name="team_list_attendance"),
20+
path("attendance/<slug:slug>", TeamAttendanceFormView.as_view(), name="team_log_attendance_form"),
1721
path("<slug:slug>/", TicketDetailRedirectView.as_view(), name="team_detail"),
1822
path("<slug:slug>/about", TeamDetailAboutView.as_view(), name="team_detail_about"),
1923
path("<slug:slug>/comments", TeamDetailCommentsView.as_view(), name="team_detail_comments"),

helpdesk/teams/views.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from typing import Any
44

55
from django.contrib.auth.mixins import LoginRequiredMixin
6-
from django.db.models import CharField, F, QuerySet, Value
6+
from django.db.models import CharField, F, Prefetch, QuerySet, Value
77
from django.http import HttpResponse, HttpResponseRedirect
8+
from django.shortcuts import get_object_or_404
89
from django.urls import reverse_lazy
9-
from django.views.generic import DetailView, RedirectView
10+
from django.views.generic import CreateView, DetailView, ListView, RedirectView
1011
from django.views.generic.detail import SingleObjectMixin
1112
from django.views.generic.edit import FormMixin, ProcessFormView
1213
from django_filters.views import FilterView
@@ -19,9 +20,10 @@
1920
from tickets.tables import TicketTable
2021

2122
from .filters import TeamFilterset
22-
from .models import Team, TeamComment
23+
from .forms import TeamAttendanceLogForm
24+
from .models import Team, TeamAttendanceEvent, TeamComment
2325
from .srcomp import srcomp
24-
from .tables import TeamTable
26+
from .tables import TeamAttendanceListTable, TeamAttendanceOverviewTable, TeamTable
2527

2628

2729
class TicketDetailRedirectView(RedirectView):
@@ -172,3 +174,42 @@ def get_entries(self) -> QuerySet[Any]:
172174

173175
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
174176
return super().get_context_data(entries=self.get_entries(), **kwargs)
177+
178+
179+
class TeamAttendanceView(LoginRequiredMixin, SingleTableMixin, ListView):
180+
model = Team
181+
table_class = TeamAttendanceOverviewTable
182+
183+
def get_queryset(self) -> QuerySet[Any]:
184+
return Team.objects.all().prefetch_related(
185+
Prefetch(
186+
"team_attendance_events",
187+
TeamAttendanceEvent.objects.order_by("-created_at")[:1],
188+
to_attr="latest_event",
189+
)
190+
)
191+
192+
193+
class TeamAttendanceFormView(LoginRequiredMixin, CreateView):
194+
model = TeamAttendanceEvent
195+
form_class = TeamAttendanceLogForm
196+
slug_field = "tla"
197+
198+
def get_success_url(self) -> str:
199+
return reverse_lazy("teams:team_list_attendance")
200+
201+
def get_initial(self) -> dict[str, Any]:
202+
return {"team": get_object_or_404(Team, tla=self.kwargs["slug"])}
203+
204+
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
205+
context_data = super().get_context_data(**kwargs)
206+
context_data["team"] = context_data["form"].initial["team"]
207+
context_data["events"] = (
208+
TeamAttendanceEvent.objects.filter(team=context_data["team"]).order_by("-created_at").all()
209+
)
210+
context_data["table"] = TeamAttendanceListTable(context_data["events"])
211+
return context_data
212+
213+
def form_valid(self, form: TeamAttendanceLogForm) -> HttpResponse:
214+
form.instance.user = self.request.user
215+
return super().form_valid(form)

helpdesk/templates/inc/nav/team-tabs.html

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@
1212
<span>Tickets</span>
1313
</a>
1414
</li>
15-
<li {% if active == "comments" %}class="is-active"{% endif %}></li>
16-
<a href="{% url 'teams:team_detail_comments' team.tla %}">
17-
<span class="icon is-small"><i class="fas fa-comment" aria-hidden="true"></i></span>
18-
<span>Comments</span>
19-
</a>
20-
</li>
21-
<li {% if active == "timeline" %}class="is-active"{% endif %}></li>
22-
<a href="{% url 'teams:team_detail_timeline' team.tla %}">
23-
<span class="icon is-small"><i class="fas fa-timeline" aria-hidden="true"></i></span>
24-
<span>Timeline</span>
25-
</a>
26-
</li>
27-
</ul>
28-
</div>
15+
<li {% if active == "comments" %}class="is-active"{% endif %}>
16+
<a href="{% url 'teams:team_detail_comments' team.tla %}">
17+
<span class="icon is-small"><i class="fas fa-comment" aria-hidden="true"></i></span>
18+
<span>Comments</span>
19+
</a>
20+
</li>
21+
<li {% if active == "timeline" %}class="is-active"{% endif %}>
22+
<a href="{% url 'teams:team_detail_timeline' team.tla %}">
23+
<span class="icon is-small"><i class="fas fa-timeline" aria-hidden="true"></i></span>
24+
<span>Timeline</span>
25+
</a>
26+
</li>
27+
</ul>
28+
</div>

helpdesk/templates/tags/navigation.html

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,19 @@
1919
<a class="navbar-item" href="{% url 'tickets:queue_default' %}">
2020
Queues
2121
</a>
22-
<a class="navbar-item" href="{% url 'teams:team_list' %}">
23-
Teams
24-
</a>
22+
<div class="navbar-item has-dropdown is-hoverable">
23+
<a class="navbar-link">
24+
Teams
25+
</a>
26+
<div class="navbar-dropdown">
27+
<a class="navbar-item" href="{% url 'teams:team_list' %}">
28+
All Teams
29+
</a>
30+
<a class="navbar-item" href="{% url 'teams:team_list_attendance' %}">
31+
Attendance
32+
</a>
33+
</div>
34+
</div>
2535
<a class="navbar-item" href="{% url 'tickets:ticket_all' %}">
2636
All Tickets
2737
</a>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{% extends "layouts/base_app.html" %}
2+
{% load render_table from django_tables2 %}
3+
{% load crispy_forms_tags %}
4+
5+
{% block page_title %}Team Attendance{% endblock %}
6+
{% block title %}Team Attendance{% endblock %}
7+
8+
{% block content %}
9+
<div class="container">
10+
<div class="columns">
11+
<div class="column">
12+
{% render_table table %}
13+
</div>
14+
</div>
15+
</div>
16+
{% endblock %}

0 commit comments

Comments
 (0)