Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Team attendance register #84

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python: [3.9, "3.10", "3.11"]
python: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v4

Expand Down
4 changes: 2 additions & 2 deletions helpdesk/helpdesk/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
BASE_DIR = Path(__file__).resolve().parent.parent

# Validate Python version
if parse_version(platform.python_version()) < parse_version("3.9.0"): # pragma: nocover
if parse_version(platform.python_version()) < parse_version("3.10.0"): # pragma: nocover
raise RuntimeError(
f"Helpdesk requires Python 3.9 or higher (current: Python {platform.python_version()})",
f"Helpdesk requires Python 3.10 or higher (current: Python {platform.python_version()})",
)

#
Expand Down
13 changes: 13 additions & 0 deletions helpdesk/teams/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django import forms

from teams.models import Team, TeamAttendanceEvent, TeamAttendanceEventType


class TeamAttendanceLogForm(forms.ModelForm):
type = forms.Select(choices=TeamAttendanceEventType.choices)
comment = forms.CharField(required=False)
Comment on lines +7 to +8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: These should be the default types for these fields. You should be able to remove these lines entirely.

team = forms.ModelChoiceField(queryset=Team.objects.all(), widget=forms.HiddenInput())

class Meta:
model = TeamAttendanceEvent
fields = ("type", "comment", "team")
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 4.2.11 on 2024-12-24 18:40

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("teams", "0004_add_team_comment"),
]

operations = [
migrations.AlterModelOptions(
name="team",
options={"ordering": ["tla"]},
),
migrations.CreateModel(
name="TeamAttendanceEvent",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"type",
models.TextField(
choices=[("AR", "Arrived"), ("LE", "Left"), ("DE", "Delayed"), ("DO", "Dropped Out")],
max_length=2,
),
),
("comment", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"team",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="team_attendance_events",
related_query_name="team_attendance_events",
to="teams.team",
),
),
("user", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
]
29 changes: 29 additions & 0 deletions helpdesk/teams/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,32 @@ class Meta:

def __str__(self) -> str:
return f"Comment on {self.team.name} at {self.created_at} by {self.author}"


class TeamAttendanceEventType(models.TextChoices):
ARRIVED = "AR", "Arrived"
LEFT = "LE", "Left"
DELAYED = "DE", "Delayed"
DROPPED_OUT = "DO", "Dropped Out"

Comment on lines +64 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Since these are already small, might as well let the values be human readable

Suggested change
ARRIVED = "AR", "Arrived"
LEFT = "LE", "Left"
DELAYED = "DE", "Delayed"
DROPPED_OUT = "DO", "Dropped Out"
ARRIVED = "ARRIVED", "Arrived"
LEFT = "LEFT", "Left"
DELAYED = "DELAYED", "Delayed"
DROPPED_OUT = "DROPPED_OUT", "Dropped Out"


class TeamAttendanceEvent(models.Model):
team = models.ForeignKey(
Team,
on_delete=models.CASCADE,
related_name="team_attendance_events",
related_query_name="team_attendance_events",
)
user = models.ForeignKey(
"accounts.User",
on_delete=models.PROTECT,
)
type = models.TextField(
max_length=2,
choices=TeamAttendanceEventType.choices,
)
comment = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self) -> str:
return f"Attendance Event: {self.team.name} {self.type} at {self.created_at}"
22 changes: 21 additions & 1 deletion helpdesk/teams/tables.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import django_tables2 as tables

from .models import Team
from .models import Team, TeamAttendanceEventType


class TeamTable(tables.Table):
Expand All @@ -13,3 +13,23 @@ class Meta:
model = Team
exclude = ("id", "pit_location")
order_by = "tla"


class TeamAttendanceTable(tables.Table):
name = tables.LinkColumn("teams:team_detail", args=[tables.A("tla")])
latest_event__0__type = tables.Column("Latest Event")
latest_event__0__comment = tables.Column("Comment")
latest_event__0__created_at = tables.DateTimeColumn(verbose_name="Time")
user = tables.TemplateColumn(
verbose_name="Logged by",
template_code='{{record.latest_event.0.user|default:"—"}}',
)
actions = tables.LinkColumn("teams:team_log_attendance_form", args=[tables.A("tla")], text="Log")

def render_latest_event__0__type(self, value: str) -> str | None:
lookups = dict(TeamAttendanceEventType.choices)
return lookups.get(value)
Comment on lines +30 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Because this is an enum, I think you can do:

Suggested change
lookups = dict(TeamAttendanceEventType.choices)
return lookups.get(value)
return TeamAttendanceEventType[value].value


class Meta:
model = Team
exclude = ["id", "tla", "is_rookie", "pit_location"]
4 changes: 4 additions & 0 deletions helpdesk/teams/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.urls import path

from .views import (
TeamAttendanceFormView,
TeamAttendanceView,
TeamDetailAboutView,
TeamDetailCommentsView,
TeamDetailTicketsView,
Expand All @@ -14,6 +16,8 @@

urlpatterns = [
path("", TeamListView.as_view(), name="team_list"),
path("attendance", TeamAttendanceView.as_view(), name="team_list_attendance"),
path("attendance/<slug:tla>", TeamAttendanceFormView.as_view(), name="team_log_attendance_form"),
path("<slug:slug>/", TicketDetailRedirectView.as_view(), name="team_detail"),
path("<slug:slug>/about", TeamDetailAboutView.as_view(), name="team_detail_about"),
path("<slug:slug>/comments", TeamDetailCommentsView.as_view(), name="team_detail_comments"),
Expand Down
57 changes: 52 additions & 5 deletions helpdesk/teams/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@
from typing import Any

from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import CharField, F, QuerySet, Value
from django.db.models import CharField, F, Prefetch, QuerySet, Value
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse_lazy
from django.views.generic import DetailView, RedirectView
from django.views.generic import CreateView, DetailView, ListView, RedirectView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormMixin, ProcessFormView
from django_filters.views import FilterView
from django_tables2 import SingleTableMixin

from helpdesk.forms import CommentSubmitForm
from helpdesk.utils import is_filterset_filtered
from helpdesk.utils import get_object_or_none, is_filterset_filtered
from tickets.filters import TicketFilter
from tickets.models import Ticket, TicketEvent
from tickets.tables import TicketTable

from .filters import TeamFilterset
from .models import Team, TeamComment
from .forms import TeamAttendanceLogForm
from .models import Team, TeamAttendanceEvent, TeamComment
from .srcomp import srcomp
from .tables import TeamTable
from .tables import TeamAttendanceTable, TeamTable


class TicketDetailRedirectView(RedirectView):
Expand Down Expand Up @@ -172,3 +173,49 @@ def get_entries(self) -> QuerySet[Any]:

def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
return super().get_context_data(entries=self.get_entries(), **kwargs)


class TeamAttendanceView(LoginRequiredMixin, SingleTableMixin, ListView):
model = Team
table_class = TeamAttendanceTable

def get_queryset(self) -> QuerySet[Any]:
return Team.objects.all().prefetch_related(
Prefetch(
"team_attendance_events",
TeamAttendanceEvent.objects.order_by("-created_at")[:1],
to_attr="latest_event",
)
)


class TeamAttendanceFormView(LoginRequiredMixin, CreateView):
http_method_names = ["get", "post"]
model = TeamAttendanceEvent
form_class = TeamAttendanceLogForm

def get_initial(self) -> dict[str, Any]:
return {
"team": get_object_or_none(Team, tla=self.kwargs["tla"]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Whilst I like the idea, it's probably better to keep the team only in the URL, rather than in the form.

}

def get_success_url(self) -> str:
return reverse_lazy("teams:team_list_attendance")

def form_valid(self, form: TeamAttendanceLogForm) -> HttpResponse:
assert self.request.user.is_authenticated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: This assertion shouldn't be necessary

Suggested change
assert self.request.user.is_authenticated

team = form.cleaned_data["team"]
team.team_attendance_events.create(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: You should be able to use form.save() to do this instead.

https://forum.djangoproject.com/t/modify-form-field-before-saving-it/4090/3 is a good example.

type=form.cleaned_data["type"],
comment=form.cleaned_data["comment"],
user=self.request.user,
)
return HttpResponseRedirect(redirect_to=self.get_success_url())

def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["team"] = self.get_initial()["team"]
return context

def form_invalid(self, form: TeamAttendanceLogForm) -> HttpResponse:
return HttpResponse("Please fill out the form correctly.")
Comment on lines +220 to +221
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: This is a bad idea. If you leave this out, the form will show its own errors

16 changes: 13 additions & 3 deletions helpdesk/templates/inc/nav/nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@
<a class="navbar-item" href="{% url 'tickets:queue_default' %}">
Queues
</a>
<a class="navbar-item" href="{% url 'teams:team_list' %}">
Teams
</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Teams
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'teams:team_list' %}">
All Teams
</a>
<a class="navbar-item" href="{% url 'teams:team_list_attendance' %}">
Attendance
</a>
</div>
</div>
<a class="navbar-item" href="{% url 'tickets:ticket_all' %}">
All Tickets
</a>
Expand Down
28 changes: 14 additions & 14 deletions helpdesk/templates/inc/nav/team-tabs.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@
<span>Tickets</span>
</a>
</li>
<li {% if active == "comments" %}class="is-active"{% endif %}></li>
<a href="{% url 'teams:team_detail_comments' team.tla %}">
<span class="icon is-small"><i class="fas fa-comment" aria-hidden="true"></i></span>
<span>Comments</span>
</a>
</li>
<li {% if active == "timeline" %}class="is-active"{% endif %}></li>
<a href="{% url 'teams:team_detail_timeline' team.tla %}">
<span class="icon is-small"><i class="fas fa-timeline" aria-hidden="true"></i></span>
<span>Timeline</span>
</a>
</li>
</ul>
</div>
<li {% if active == "comments" %}class="is-active"{% endif %}>
<a href="{% url 'teams:team_detail_comments' team.tla %}">
<span class="icon is-small"><i class="fas fa-comment" aria-hidden="true"></i></span>
<span>Comments</span>
</a>
</li>
<li {% if active == "timeline" %}class="is-active"{% endif %}>
<a href="{% url 'teams:team_detail_timeline' team.tla %}">
<span class="icon is-small"><i class="fas fa-timeline" aria-hidden="true"></i></span>
<span>Timeline</span>
</a>
</li>
</ul>
</div>
20 changes: 20 additions & 0 deletions helpdesk/templates/teams/team_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% extends "layouts/base_app.html" %}
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}

{% block page_title %}Team Attendance{% endblock %}
{% block title %}Team Attendance{% endblock %}

{% block page_buttons %}
{# <input class="input" type="date">#}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is this meant to be commented out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a WIP thing. I wanted to add the option to filter by day. Not sure if I should keep it. We'd also want to make sure that teams which have dropped out ignore the filter.

{% endblock %}

{% block content %}
<div class="container">
<div class="columns">
<div class="column">
{% render_table table %}
</div>
</div>
</div>
{% endblock %}
17 changes: 17 additions & 0 deletions helpdesk/templates/teams/teamattendanceevent_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "layouts/base_app.html" %}
{% load crispy_forms_tags %}

{% block page_title %}
Log attendance for {{ team }}
{% endblock %}
{% block title %}
Log attendance for {{ team }}
{% endblock %}

{% block content %}
<form class="block" method="post">
{% csrf_token %}
{{form|crispy}}
<input class="button is-primary" type="submit" value="Save">
</form>
{% endblock %}
2 changes: 1 addition & 1 deletion helpdesk/tickets/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TicketTable(tables.Table):
status = tables.Column()
assignee_id = tables.TemplateColumn(
verbose_name="Assignee",
template_code='{{record.assignee|default:"-"}}',
template_code='{{record.assignee|default:""}}',
)
actions = tables.LinkColumn("tickets:ticket_detail", args=[tables.A("id")], text="View")

Expand Down
Loading