Crude section/teams implementation

do not merge to main, very buggy rough implementation of sorting between teams and sections. I have rewritten many areas.
This commit is contained in:
Corban-Lee 2023-05-10 00:58:38 +01:00
parent 75a97d762b
commit e2ad5676c7
11 changed files with 1186 additions and 257 deletions

View File

@ -2,17 +2,17 @@
from django.contrib import admin
from .models import Peg, Member, Team
from .models import Member, Team, Section
@admin.register(Peg)
class PegAdmin(admin.ModelAdmin):
"""Admin model for the Peg model."""
# @admin.register(Peg)
# class PegAdmin(admin.ModelAdmin):
# """Admin model for the Peg model."""
change_list_template = "entities/bulk_pegging.html"
readonly_fields = ("peg_number",)
list_display = ("peg_number",)
search_fields = ("peg_number",)
# change_list_template = "entities/bulk_pegging.html"
# readonly_fields = ("peg_number",)
# list_display = ("peg_number",)
# search_fields = ("peg_number",)
@admin.register(Member)
@ -28,14 +28,14 @@ class MemberAdmin(admin.ModelAdmin):
class TeamAdmin(admin.ModelAdmin):
"""Admin model for the Team model."""
readonly_fields = ("team_number",)
list_display = ("team_number",)
search_fields = ("team_number",)
readonly_fields = ("name",)
list_display = ("name",)
search_fields = ("name",)
# @admin.register(Section)
# class SectionAdmin(admin.ModelAdmin):
# """Admin model for the Section model."""
@admin.register(Section)
class SectionAdmin(admin.ModelAdmin):
"""Admin model for the Section model."""
# list_display = ("character",)
# search_fields = ("character",)
list_display = ("name",)
search_fields = ("name",)

File diff suppressed because it is too large Load Diff

View File

@ -2,71 +2,71 @@
{
"model": "mainapp.team",
"pk": 1,
"fields": {
"section_letter": "D"
}
},
{
"model": "mainapp.team",
"pk": 2,
"fields": {
"section_letter": "P"
}
},
{
"model": "mainapp.team",
"pk": 3,
"fields": {
"section_letter": "L"
}
},
{
"model": "mainapp.team",
"pk": 4,
"fields": {
"section_letter": "M"
}
},
{
"model": "mainapp.team",
"pk": 2,
"fields": {
"section_letter": "K"
}
},
{
"model": "mainapp.team",
"pk": 3,
"fields": {
"section_letter": "Q"
}
},
{
"model": "mainapp.team",
"pk": 4,
"fields": {
"section_letter": "V"
}
},
{
"model": "mainapp.team",
"pk": 5,
"fields": {
"section_letter": "Z"
"section_letter": "U"
}
},
{
"model": "mainapp.team",
"pk": 6,
"fields": {
"section_letter": "Y"
"section_letter": "H"
}
},
{
"model": "mainapp.team",
"pk": 7,
"fields": {
"section_letter": "J"
"section_letter": "A"
}
},
{
"model": "mainapp.team",
"pk": 8,
"fields": {
"section_letter": "W"
"section_letter": "D"
}
},
{
"model": "mainapp.team",
"pk": 9,
"fields": {
"section_letter": "T"
"section_letter": "P"
}
},
{
"model": "mainapp.team",
"pk": 10,
"fields": {
"section_letter": "F"
"section_letter": "G"
}
}
]

View File

@ -1,12 +1,46 @@
import random
import names
import json
from typing import Optional, List, Set
from string import ascii_uppercase
from django.core.management.base import BaseCommand
from mainapp.models import Member, Team
from mainapp.models import Member, Team, SectionValidator
# TODO: refactor this file like create_teams_fixtures.py
# SECTION_LETTERS = ascii_uppercase
# def get_next_available_section(member: Member, section_validator: SectionValidator) -> Optional[str]:
# """Returns the next available section for a member."""
# member_sections = member.sections.all().values_list('name', flat=True)
# used_sections = set(member_sections)
# if not member_sections:
# return "A"
# used_sections = sorted(used_sections)
# last_section = member_sections[-1]
# last_section_index = SECTION_LETTERS.index(last_section)
# for i in range(last_section_index + 1, len(SECTION_LETTERS)):
# section = SECTION_LETTERS[i]
# if section_validator.is_valid_section(section) and section not in used_sections:
# return section
# for i in range(last_section_index - 1, -1, -1):
# section = SECTION_LETTERS[i]
# if section_validator.is_valid_section(section) and section not in used_sections:
# return section
# return None
class Command(BaseCommand):
help = "Creates a fixture with randomly generated Member objects"
@ -25,11 +59,9 @@ class Command(BaseCommand):
for _ in range(num_members):
first_name = names.get_first_name()
last_name = names.get_last_name()
member = Member(first_name=first_name, last_name=last_name, team=team)
member = Member.objects.create(first_name=first_name, last_name=last_name, team=team)
members.append(member)
Member.objects.bulk_create(members)
self.stdout.write(self.style.SUCCESS(f"Created {num_members} members."))
# create a members fixture file
@ -42,7 +74,7 @@ class Command(BaseCommand):
"first_name": member.first_name,
"last_name": member.last_name,
"team": member.team_id,
"peg_number": member.peg_number
"peg_number": member.peg_number,
}
}
members_fixture.append(member_fixture)

View File

@ -1,4 +1,4 @@
# Generated by Django 4.1.5 on 2023-05-07 21:57
# Generated by Django 4.1.5 on 2023-05-09 22:52
from django.db import migrations, models
import django.db.models.deletion
@ -14,16 +14,16 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='Peg',
name='Section',
fields=[
('peg_number', mainapp.models.ReusableAutoField(default=mainapp.models.ReusableAutoField.get_default, editable=False, primary_key=True, serialize=False)),
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(editable=False, max_length=3, unique=True)),
],
),
migrations.CreateModel(
name='Team',
fields=[
('team_number', mainapp.models.ReusableAutoField(default=mainapp.models.ReusableAutoField.get_default, editable=False, primary_key=True, serialize=False)),
('section_letter', models.CharField(choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G'), ('H', 'H'), ('I', 'I'), ('J', 'J'), ('K', 'K'), ('L', 'L'), ('M', 'M'), ('N', 'N'), ('O', 'O'), ('P', 'P'), ('Q', 'Q'), ('R', 'R'), ('S', 'S'), ('T', 'T'), ('U', 'U'), ('V', 'V'), ('W', 'W'), ('X', 'X'), ('Y', 'Y'), ('Z', 'Z')], max_length=1, unique=True)),
],
),
migrations.CreateModel(
@ -33,6 +33,7 @@ class Migration(migrations.Migration):
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('peg_number', models.PositiveIntegerField(null=True, unique=True)),
('section', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='mainapp.section')),
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='mainapp.team', validators=[mainapp.models.validate_team_size])),
],
),

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1.5 on 2023-05-09 23:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='team',
old_name='team_number',
new_name='name',
),
migrations.AlterField(
model_name='section',
name='name',
field=models.CharField(max_length=3, unique=True),
),
]

View File

@ -31,13 +31,101 @@ class ReusableAutoField(models.PositiveIntegerField):
return self.get_next_available_id(self.model)
class Peg(models.Model):
"""Represents a person's peg"""
# class Peg(models.Model):
# """Represents a person's peg"""
peg_number = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
# peg_number = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
def __str__(self):
return f"Peg {self.peg_number}"
# def __str__(self):
# return f"Peg {self.peg_number}"
class SectionValidator:
"""Validation class for the `section` field on the `member` model."""
def __init__(self, max_value="ZZZ"):
self.max_value = max_value.upper()
self.alphabet_size = ord("Z") - ord("A") + 1
def is_valid(self, section: str, team_sections: list[str]=None) -> bool:
"""Returns boolean if the section passed is valid."""
section = section.upper()
if not self._is_alphanumeric(section):
return False
if not self._is_in_alphabet(section[0]):
return False
if not self._is_in_range(section):
return False
if team_sections:
if not self._is_unique(section, team_sections):
return False
if not self._is_not_adjacent(section, team_sections):
return False
return True
def _is_alphanumeric(self, section: str) -> bool:
"""Returns boolean if all characters in the passed string are alphanumerical."""
return all(c.isalnum() for c in section)
def _is_in_alphabet(self, c) -> bool:
"""Returns boolean if the passed character is alphabetical."""
return "A" <= c <= "Z"
def _is_in_range(self, section) -> bool:
"""Returns boolean if the passed section less or equal to the max value."""
section_value = self._section_value(section)
max_value = self._section_value(self.max_value)
return section_value <= max_value
def _is_unique(self, section, team_sections) -> bool:
"""Returns boolean if the passed section is unique amongst `team_sections`."""
return section not in team_sections
def _is_not_adjacent(self, section, team_sections) -> bool:
"""Returns boolean if the passed section is not adjacent to any `team_sections`."""
for team_section in team_sections:
team_section_value = self._section_value(team_section)
section_value = self._section_value(section)
if abs(team_section_value - section_value) <= 1:
return False
return True
def _section_value(self, section):
"""Returns the value of the passed section."""
n = len(section)
value = sum((ord(c) - ord("A") + 1) * self.alphabet_size ** (n - i - 1) for i, c in enumerate(section))
return value
class Section(models.Model):
"""Represents a fishing area. Members can be assigned to a section,
but no 2 teammates can be in the same or adjacent section."""
name = models.CharField(max_length=3, unique=True, null=False)
def clean(self):
super().clean()
validator = SectionValidator(max_value="ZZZ")
if not validator.is_valid(self.name):
raise ValidationError("Invalid section value.")
self.name = self.name.upper()
def __str__(self) -> str:
return self.name
@property
def members(self):
return Member.objects.filter(section=self)
def validate_team_size(value):
@ -52,7 +140,7 @@ class Member(models.Model):
last_name = models.CharField(max_length=255)
team = models.ForeignKey("Team", on_delete=models.SET_NULL, null=True, blank=True, validators=(validate_team_size,), related_name='members')
peg_number = models.PositiveIntegerField(null=True, editable=True, unique=True)
# peg = models.OneToOneField("Peg", on_delete=models.SET_NULL, null=True, blank=True)
section = models.ForeignKey(to=Section, on_delete=models.SET_NULL, null=True, swappable=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -65,6 +153,13 @@ class Member(models.Model):
if peg_numbers:
self.peg_number = min (peg_numbers)
# def clean(self):
# super().clean()
# validator = SectionValidator(max_value="ZZZ")
# if not validator.is_valid(self.section):
# raise ValidationError("Invalid section value.")
def __str__(self):
return f"{self.first_name} {self.last_name} (team {self.team.team_number}))"
@ -79,53 +174,25 @@ BLOCKED_SECTION_LETTERS = ["i"] # lowercase only, sections cannot be any of the
class Team(models.Model):
"""Represents a team"""
team_number = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
section_letter = models.CharField(max_length=1, unique=True, choices=[
(char, char) for char in ascii_uppercase if char.lower() not in BLOCKED_SECTION_LETTERS
])
name = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
# section_letter = models.CharField(max_length=1, unique=True, choices=[
# (char, char) for char in ascii_uppercase if char.lower() not in BLOCKED_SECTION_LETTERS
# ])
def __str__(self):
return f"Team {self.team_number}"
return f"Team {self.name}"
@property
def members(self) -> list[Member]:
Member.objects.filter(team=self)
return Member.objects.filter(team=self)
def validate_section_character(value):
"""Validates the section character"""
# def validate_section_character(value):
# """Validates the section character"""
value = value.upper()
banned_characters = ["I"]
# value = value.upper()
# banned_characters = ["I"]
if value in banned_characters:
raise ValidationError(f"The character <{value}> is a prohibited character.")
# if value in banned_characters:
# raise ValidationError(f"The character <{value}> is a prohibited character.")
# class Section(models.Model):
# """Represents a section of the scoreboard"""
# # character field stores a single character but doesnt allow for 'I' or 'i'
# character = models.CharField(primary_key=True, max_length=1, validators=(validate_section_character, ))
# def clean(self):
# self.character = self.character.upper()
# def __str__(self):
# return f"Section {self.character}"
#class Scoreboard(models.Model):
# class Status(models.IntegerChoices):
# ACTIVE = 1, "Active"
# INACTIVE = 2, "Inactive"
# ARCHIVED = 3, "Archived"
#
# name = models.CharField(max_length=255)
# category = models.CharField(max_length=255)
# price = models.DecimalField(max_digits=10, decimal_places=2)
# cost = models.DecimalField(max_digits=10, decimal_places=2)
# status = models.PositiveSmallIntegerField(choices=Status.choices)
#
# def __str__(self):
# return self.name

View File

@ -37,11 +37,11 @@
<h3 class="h6 mb-3">Sort groups by</h3>
<div>
<div class="d-inline-block form-check">
<input type="radio" class="form-check-input" checked value="team_number" name="sortTeams" id="sortTeamsName">
<input type="radio" class="form-check-input" checked value="team" name="sortGroups" id="sortTeamsName">
<label for="sortTeamsName" class="form-check-label">Teams</label>
</div>
<div class="d-inline-block form-check ms-4">
<input type="radio" class="form-check-input" value="section_letter" name="sortTeams" id="sortTeamsSection">
<input type="radio" class="form-check-input" value="section" name="sortGroups" id="sortTeamsSection">
<label for="sortTeamsSection" class="form-check-label">Sections</label>
</div>
</div>

View File

@ -7,7 +7,7 @@ urlpatterns = [
path('scoreboard/', views.scoreboard, name='scoreboard'),
path('teams/', views.teams, name='teams'),
path('bulk-peg/', views.bulk_create_pegs, name='bulk-peg'),
# path('bulk-peg/', views.bulk_create_pegs, name='bulk-peg'),
path('get-teams/', views.get_teams, name='get-teams'),
path('update-member/', views.update_member, name='update-member')
]

View File

@ -6,7 +6,7 @@ from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.db.models import Q, Case, When, Value, IntegerField
from .models import Peg, Team, Member
from .models import Team, Member, Section
def index(request):
@ -21,15 +21,15 @@ def scoreboard(request):
def teams(request):
return render(request, 'teams.html')
def bulk_create_pegs(request):
"""Bulk create pegs"""
# def bulk_create_pegs(request):
# """Bulk create pegs"""
number_of_pegs = request.POST.get("pegAmount")
for i in range(int(number_of_pegs)):
Peg.objects.create()
# number_of_pegs = request.POST.get("pegAmount")
# for i in range(int(number_of_pegs)):
# Peg.objects.create()
# return to previous page
return redirect(request.META.get('HTTP_REFERER'))
# # return to previous page
# return redirect(request.META.get('HTTP_REFERER'))
def get_teams(request):
"""Returns a JsonResponse containing a dictionary with a k/v pair for a list of teams.
@ -44,10 +44,11 @@ def get_teams(request):
return
search = request.POST.get("search")
sort_teams = request.POST.get("sortTeams") or "team_number"
sort_groups = request.POST.get("sortGroups") or "team"
sort_members = request.POST.get("sortMembers") or "peg_number"
teams = Team.objects.order_by(sort_teams).all()
group_object = Team if sort_groups == "team" else Section
groups = group_object.objects.order_by("name").all()
# Filter out teams that don't contain members being searched for
if search:
@ -61,31 +62,32 @@ def get_teams(request):
]
)
)
teams = teams.filter(members__in=members).distinct()
groups = groups.filter(members__in=members).distinct()
# Create a dictionary for the data that is JSON safe
response_data = {"teams": []}
for team in teams:
team_data = {
"team_number": team.team_number,
"section_letter": team.section_letter if team.section_letter else None,
response_data = {"groups": []}
for group in groups:
group_data = {
"name": group.name,
"members": [
{
"first": member.first_name,
"last": member.last_name,
"id": member.id,
"team": team.team_number,
"peg": member.peg_number,
"section": team.section_letter if team.section_letter else None
"team": member.team.name if member.team else None,
"section": member.section.name if member.section else None
}
for member in team.members.order_by(sort_members).all()
for member in group.members.order_by(sort_members).all()
]
}
response_data["teams"].append(team_data)
response_data["groups"].append(group_data)
response_data["sortTeams"] = sort_teams
response_data["sortGroups"] = sort_groups
response_data["sortMembers"] = sort_members
print(sort_groups, sort_members)
return JsonResponse(response_data)
def update_member(request):

View File

@ -34,9 +34,9 @@ $(document).ready(() => {
});
// Load the last saved sort settings TODO: use local storage so it only saves on one reload
const sortTeamsValue = localStorage.getItem("sortTeams");
const sortTeamsValue = localStorage.getItem("sortGroups");
if (sortTeamsValue !== null) {
$("#sortForm input[name='sortTeams']").val([sortTeamsValue]);
$("#sortForm input[name='sortGroups']").val([sortTeamsValue]);
}
const sortMembersValue = localStorage.getItem("sortMembers");
@ -70,30 +70,31 @@ $(document).ready(() => {
function getFilters() {
return [
$("#search").val(),
$("input[name='sortTeams']:checked", "#sortForm").val(),
$("input[name='sortGroups']:checked", "#sortForm").val(),
$("input[name='sortMembers']:checked", "#sortForm").val()
]
}
function loadTeams(teams, highlightText="") {
function loadTeams(groups, highlightText="", groupType) {
$("#teamsContainer").html(""); // Clear the previous listed teams
if (teams.length < 1) {
if (groups.length < 1) {
$("#teamsNotFound").show();
}
groupType = groupType.toUpperCase();
// Iterate over and add each team
teams.forEach((team) => {
groups.forEach((group) => {
$("#teamsContainer").append(
`<div class='col-12 col-md-6 col-xl-4 mb-4'>
<div
class='team px-4 py-3 bg-body-tertiary bg-gradient rounded h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover'
data-number='${team.team_number}'
data-section='${team.section_letter}'>
data-number='${group.name}'>
<h3>
<span class='fs-4'>
<span class='fs-6'>TEAM</span>
${team.team_number}
<span class='fs-6'>${groupType}</span>
${group.name}
</span>
<button class='btn btn-sm btn-light float-end border-0 me-1'>
<i class='bi bi-gear fs-6'></i>
@ -106,8 +107,10 @@ function loadTeams(teams, highlightText="") {
);
// While we have the team, iterate over and add it's members
team.members.forEach((member) => {
group.members.forEach((member) => {
const fullname = member.first + " " + member.last;
const oppositeGroupType = groupType === "SECTION"? "Team" : "Section";
const oppositeGroupName = groupType === "SECTION"? member.team : member.section;
$("#teamsContainer").find(".team-members").last().append(
`<li class='mb-3 rounded w-100 fluid-hover-zoom'>
@ -117,13 +120,14 @@ function loadTeams(teams, highlightText="") {
data-last='${member.last}'
data-member-id='${member.id}'
data-team-number='${member.team}'
data-peg-number='${member.peg}'>
data-peg-number='${member.peg}'
data-section='${member.section}'>
<div class='px-3 py-2 w-100'>
<div class='badge bg-secondary-subtle text-body fixed-badge' data-bs-title='Peg ${member.peg}' data-bs-toggle='tooltip'>
${member.peg}
</div>
<div class='badge bg-secondary-subtle text-body fixed-badge' data-bs-title='Section ${member.section}' data-bs-toggle='tooltip'>
${member.section}
<div class='badge bg-secondary-subtle text-body fixed-badge' data-bs-title='${oppositeGroupType} ${oppositeGroupName}' data-bs-toggle='tooltip'>
${oppositeGroupName}
</div>
<div class='d-inline-block ms-2 team-member-fullname'>
${fullname}
@ -176,20 +180,20 @@ function loadTeams(teams, highlightText="") {
});
}
function fetchAndLoadTeams(search="", sortTeams="", sortMembers="") {
function fetchAndLoadTeams(search="", sortGroups="", sortMembers="") {
$.ajax({
url: getTeamsUrl,
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"search": search,
"sortTeams": sortTeams,
"sortGroups": sortGroups,
"sortMembers": sortMembers
},
error: (xhr, textStatus, errorThrown) => { alert(xhr + " " + textStatus + " " + errorThrown); },
success: (result) => {
teamsLoading(false);
loadTeams(result.teams, search);
loadTeams(result.groups, search, result.sortGroups);
}
});
}
@ -298,7 +302,7 @@ function saveEditMemberModal(memberId) {
error: (xhr, textStatus, errorThrown) => { alert(xhr + " " + textStatus + " " + errorThrown); },
success: (result) => {
teamsLoading(false);
loadTeams(result.teams, search);
loadTeams(result.teams, search, result.sortGroups);
$("#editMemberModal").modal("hide");
}
});