Adding Teams & Sections is functional-ish

This commit is contained in:
Corban-Lee 2023-05-11 15:43:33 +01:00
parent 8dfe4eb9f2
commit 628764a338
7 changed files with 343 additions and 94 deletions

View File

@ -1,5 +1,7 @@
"""Models for the mainapp."""
from string import ascii_uppercase
from django.db import models
from django.core.exceptions import ValidationError
@ -96,12 +98,65 @@ class SectionValidator:
return value
class SectionManager(models.Manager):
@staticmethod
def get_max_section():
max_section = None
max_number = -1
# Iterate through all sections in the database
for section in Section.objects.all():
section_name = section.name
section_number = 0
# Calculate the section number based on the section name
for i, char in enumerate(section_name):
section_number += (ord(char) - ord('A') + 1) * (26 ** (len(section_name) - i - 1))
# Check if this section has a higher number than the current maximum
if section_number > max_number:
max_number = section_number
max_section = section_name
return max_section
@staticmethod
def find_next_section(current_section):
if not current_section:
return 'A'
# Split current section name into a list of characters
chars = list(current_section)
# Increment the last character
chars[-1] = chr(ord(chars[-1]) + 1)
# Check if the last character is "Z", and carry over to the next character if necessary
for i in range(len(chars) - 1, -1, -1):
if chars[i] > 'Z':
chars[i] = 'A'
if i == 0:
# If the first character needs to be incremented, add a new character "A"
chars.insert(0, 'A')
else:
# Increment the previous character
chars[i - 1] = chr(ord(chars[i - 1]) + 1)
else:
break
# Join the characters back into a string and return the result
return ''.join(chars)
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)
objects = SectionManager()
def clean(self):
super().clean()
@ -118,52 +173,6 @@ class Section(models.Model):
def get_members(self):
return Member.objects.filter(section=self)
@property
def section_after(self):
"""Returns the next section after this one, or none if not found."""
sections = [sec for sec in Section.objects.all()]
sections.sort(key=lambda sec: sec.name)
try:
return sections[sections.index(self) + 1]
except IndexError:
return None
@property
def section_before(self):
"""Returns the last section before this one, or none if not found."""
sections = [sec for sec in Section.objects.all()]
sections.sort(key=lambda sec: sec.name)
try:
return sections[sections.index(self) - 1]
except IndexError:
return None
def is_joinable(self, member, /) -> bool:
"""Returns boolean if a member is able to join this section."""
if not member.team:
return True
teammates = [teammate for teammate in Member.objects.filter(team=member.team)]
section_after = self.section_after.get_members if self.section_after else []
section_before = self.section_before.get_members if self.section_before else []
for teammate in teammates:
if teammate in section_after:
return False
if teammate in section_before:
return False
return True
return any(
teammate in section_after or teammate in section_before
for teammate in teammates
)
class Member(models.Model):
"""Represents a member of a team"""
@ -192,11 +201,18 @@ class Member(models.Model):
def fullname(self) -> str:
return f"{self.first_name} {self.last_name}"
class TeamManager(models.Manager):
pass
class Team(models.Model):
"""Represents a team"""
name = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
objects = TeamManager()
def __str__(self):
return f"Team {self.name}"

View File

@ -24,7 +24,7 @@
</div> -->
<div class="fluid-hover-zoom d-flex flex-wrap justify-content-center shadow-sm md-shadow on-hover bg-body-tertiary rounded text-center p-5 h-100">
<h3 class="h2 w-100">
Member Management
Angler Management
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
</h3>
<p class="mt-3">

View File

@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% block title %}
Member Management |
Manage Anglers |
{% endblock title %}
{% block style %}
@ -10,21 +11,22 @@
{% endblock style %}
{% block content %}
<div class="bg-body p-4">
<div class="bg-body p-4 d-flex">
<a href="{% url 'index' %}" class="btn btn-company px-4">Back</a>
<img class="ms-auto" width="40" src="{% static 'img/logo.webp' %}">
</div>
<div class="container my-4 p-4 pb-0 bg-body rounded">
<div class="row mb-4">
<div class="col-12 col-xl-4 d-flex">
<h3 class="mb-3 mb-xl-0">
Member Management
Manage Anglers
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
</h3>
</div>
<div class="col-sm-3 col-md-6 col-xl-4 mb-3 mb-sm-0">
<div class="input-group justify-content-xl-end">
<button class="btn border-secondary-subtle btn-outline-company" id="addMember" data-bs-toggle="tooltip" data-bs-title="Add Member">
<button class="btn border-secondary-subtle btn-outline-company" id="addMember" data-bs-toggle="tooltip" data-bs-title="Add Angler">
<i class="bi bi-person"></i>
</button>
<button class="btn border-secondary-subtle btn-outline-company" id="addTeam" data-bs-toggle="tooltip" data-bs-title="Add Team">
@ -40,7 +42,7 @@
<button type="button" data-bs-toggle="dropdown" class="btn btn-outline-company border-secondary-subtle">
<i class="bi bi-sort-up"></i>
</button>
<input type="search" class="form-control border-secondary-subtle shadow-none" placeholder="Search Members" id="search">
<input type="search" class="form-control border-secondary-subtle shadow-none" placeholder="Search Anglers" id="search">
<button type="button" class="btn btn-outline-company border-secondary-subtle rounded-end" id="searchButton"><i class="bi bi-search"></i></button>
<form id="sortForm">
<ul class="dropdown-menu py-3 text-body-secondary bg-body-tertiary border border-light-subtle shadow-sm justify-self-center mt-2" onclick="event.stopPropagation()">
@ -59,7 +61,7 @@
</li>
<li class="dropdown-divider my-3 mx-4 bg-light-subtle"></li>
<li class="px-4">
<h3 class="h6 mb-3">Sort members by</h3>
<h3 class="h6 mb-3">Sort anglers by</h3>
<div>
<div class="d-inline-block form-check">
<input type="radio" class="form-check-input" value="first_name" name="sortMembers" id="sortMembersName">
@ -91,6 +93,38 @@
</div>
<!-- Edit Section Modal -->
<div class="modal fade" id="editSectionModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content border-0">
<div class="modal-header border-0">
<h3 class="modal-title fs-5" id="editTeamTitle">
Section Character Here
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="col-md-6 d-flex">
<div class="input-group">
<span class="input-group-text" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Section Character">
<i class="bi bi-layers"></i>
</span>
<input type="text" name="editSectionName" id="editSectionName" class="form-control" minlength="1" maxlength="3" value="A">
</div>
<button type="button" id="editSectionNameButton" class="btn btn-outline-company border-secondary-subtle ms-3" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Find next available character">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="col-md-6 mt-2 text-danger" id="editSectionNameError" style="display: hidden"></div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-company px-4" id="saveEditSectionModal">Save</button>
</div>
</div>
</div>
</div>
<!-- Edit Team Modal -->
<div class="modal fade" id="editTeamModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
@ -103,32 +137,21 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<div class="col-md-6">
<div class="col-md-6 d-flex">
<div class="input-group">
<span class="input-group-text">
<span class="input-group-text" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Team number">
<i class="bi bi-tag"></i>
</span>
<div class="form-floating">
<input type="number" class="form-control" name="editTeamName" id="editTeamName">
<label for="editTeamName">Number</label>
</div>
<input type="number" name="editTeamNumber" id="editTeamNumber" class="form-control" min="1" max="9999" value="1">
</div>
<button type="button" id="editTeamNumberButton" class="btn btn-outline-company border-secondary-subtle ms-3" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Find next available number">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<!-- <form id="editTeamForm">
<div class="row">
<div class="col-md-6 mb-3 mb-md-0">
<label for="editTeamNumber" class="form-label">Team Number</label>
<input type="number" name="editTeamNumber" class="form-control" id="editTeamNumber">
</div>
<div class="col-md-6">
<label for="editTeamSection" class="form-label">Section Letter</label>
<input type="text" name="editTeamSection" class="form-control" id="editTeamSection">
</div>
</div>
</form> -->
<div class="col-md-6 mt-2 text-danger" id="editTeamNumberError" style="display: hidden"></div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-company" id="saveEditTeamModal">Save Changes</button>
<button type="button" class="btn btn-company px-4" id="saveEditTeamModal">Save</button>
</div>
</div>
</div>
@ -140,7 +163,7 @@
<div class="modal-content border-0">
<div class="modal-header border-0">
<h3 class="modal-title fs-5 color" id="editMemberName">
Member Name Here
Angler Name Here
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
@ -162,7 +185,7 @@
</div>
</div>
<div class="input-group mb-3">
<span class="input-group-text">
<span class="input-group-text" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Team">
<i class="bi bi-people"></i>
</span>
<select class="form-select" name="editMemberTeam" id="editMemberTeam">
@ -173,7 +196,7 @@
</select>
</div>
<div class="input-group mb-3">
<span class="input-group-text">
<span class="input-group-text" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Section">
<i class="bi bi-layers"></i>
</span>
<select class="form-select" name="editMemberSection" id="editMemberSection">
@ -186,13 +209,10 @@
<div class="row">
<div class="col-md-6 d-flex">
<div class="input-group">
<span class="input-group-text">
<span class="input-group-text" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Peg Number">
<i class="bi bi-tag"></i>
</span>
<span class="border border-end-0 d-flex align-items-center">
<small class="text-muted mx-2" style="margin-right: 1px;">Peg</small>
</span>
<input type="number" name="editMemberPeg" id="editMemberPeg" class="form-control ps-0 border-start-0 shadow-none" min="1" max="9999" value="1">
<input type="number" name="editMemberPeg" id="editMemberPeg" class="form-control" min="1" max="9999" value="1">
</div>
<button type="button" class="btn btn-outline-company border-secondary-subtle ms-3" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title="Find next available peg">
<i class="bi bi-arrow-clockwise"></i>
@ -202,14 +222,13 @@
</form>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-company" id="saveEditModal">Save Changes</button>
<button type="button" class="btn btn-company px-4" id="saveEditModal">Save</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% load static %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.full.min.js"></script>
@ -217,6 +236,9 @@
<script type="text/javascript">
const getTeamsUrl = "{% url 'get-teams' %}";
const updateMemberUrl = "{% url 'update-member' %}";
const updateSectionUrl = "{% url 'update-section' %}"
const updateTeamUrl = "{% url 'update-team' %}"
const getNextIdentifierUrl = "{% url 'get-next-identifier' %}"
const csrfMiddlewareToken = "{{ csrf_token }}";
</script>
<script src="{% static 'js/teams.js' %}"></script>

View File

@ -9,5 +9,8 @@ urlpatterns = [
# 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')
path('update-member/', views.update_member, name='update-member'),
path('update-section/', views.update_section, name='update-section'),
path('update-team/', views.update_team, name='update-team'),
path("get-next-identifier/", views.get_next_identifier, name='get-next-identifier')
]

View File

@ -5,8 +5,9 @@ from functools import reduce
from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.db.models import Q, Case, When, Value, IntegerField
from django.db.utils import IntegrityError
from .models import Team, Member, Section
from .models import Team, Member, Section, SectionManager, ReusableAutoField, SectionValidator
def index(request):
@ -37,7 +38,7 @@ def name_sort_key(section):
else:
return (1, section.name[:-1], section.name[-1])
def get_teams(request):
def get_teams(request, **kwargs):
"""Returns a JsonResponse containing a dictionary with a k/v pair for a list of teams.
Args:
@ -95,6 +96,9 @@ def get_teams(request):
response_data["sortGroups"] = sort_groups
response_data["sortMembers"] = sort_members
for key, value in kwargs.items():
response_data[key] = value
return JsonResponse(response_data)
def update_member(request):
@ -123,3 +127,99 @@ def update_member(request):
member.save()
return get_teams(request)
def update_section(request):
"""Update a section, returns JsonResponse with updated teams data."""
if not request.POST:
return
section_id = request.POST.get("sectionId")
section_name = request.POST.get("sectionName")
validator = SectionValidator()
if not validator.is_valid(section_name):
json_response = get_teams(request, form_errors={
"editSectionName": "This is an invalid section"
})
return json_response
if section_id == "-1":
section = Section(name=section_name)
else:
section = Section.objects.get(id=section_id)
section.name = section_name
try:
section.save()
except IntegrityError:
json_response = get_teams(request, form_errors={
"editSectionName": "A Section with this character already exists"
})
return json_response
return get_teams(request) # returns jsonresponse with new details
def update_team(request):
"""Update a team, returns a JsonResponse with updated teams data."""
if not request.POST:
return
team_id = request.POST.get("teamId")
team_number = request.POST.get("teamNumber")
try:
if team_id == "-1":
team = Team.objects.create(name=team_number)
else:
team = Team.objects.get(id=team_id)
team.name = team_number
team.save()
except IntegrityError as error:
json_response = get_teams(request, form_errors={
"editTeamNumber": "A Team with this number already exists"
})
return json_response
return get_teams(request)
def get_next_peg() -> int:
pass
def get_next_section() -> str:
section_name = SectionManager.get_max_section()
return SectionManager.find_next_section(section_name)
def get_next_team() -> int:
field = ReusableAutoField
field.model = Team
return field().get_default()
def get_next_identifier(request):
"""Get the next available identifer (peg, section character, etc.) for an object."""
if not request.POST:
return
item = request.POST.get("item")
match item:
case "member_peg":
result = get_next_peg()
case "section_name":
result = get_next_section()
case "team_number":
result = get_next_team()
case _:
raise ValueError(f"Bad identifier item: {item}")
return JsonResponse({"identifier": result})

BIN
src/static/img/logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -57,6 +57,39 @@ $(document).ready(() => {
}
});
$("#editSectionNameButton").on("click", () => {
$.ajax({
url: getNextIdentifierUrl,
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"item": "section_name"
},
error: (xhr, textStatus, errorThrown) => { alert(xhr + " " + textStatus + " " + errorThrown); },
success: (result) => {
$("#editSectionName").val(result.identifier)
}
});
});
$("#editTeamNumberButton").on("click", () => {
$.ajax({
url: getNextIdentifierUrl,
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"item": "team_number"
},
error: (xhr, textStatus, errorThrown) => { alert(xhr + " " + textStatus + " " + errorThrown); },
success: (result) => {
$("#editTeamNumber").val(result.identifier)
}
});
});
$("select").select2({theme: "bootstrap-5", minimumResultsForSearch: -1})
// load the teams with default filters
@ -180,6 +213,10 @@ function loadTeams(groups, highlightText="", groupType) {
$("#addMember").on("click", () => {
openEditMemberModal(-1);
});
$("#addSection").on("click", () => {
openEditSectionModal(-1);
});
}
function fetchAndLoadTeams(search="", sortGroups="", sortMembers="") {
@ -213,19 +250,88 @@ function teamsLoading(show=true) {
$("#teamsLoadingSpinner").hide();
}
function openEditTeamModal(teamNumber) {
function openEditSectionModal(sectionId) {
// // Team data
// const team = $(`.team[data-number='${teamNumber}']`);
// const section = team.data("section");
$("#editSectionNameError").hide();
$("#editSectionModal").modal("show");
// // Load data to form
// $("#editTeamTitle").text(`Team ${teamNumber}`);
$("#saveEditSectionModal").off("click").on("click", () => {
// $("#editTeamNumber").val(teamNumber);
// $('#editTeamSection').val(section);
if(!$("#editSectionName").val().match(/^[A-Za-z]+$/)) {
$("#editSectionName").focus();
$("#editSectionNameError").text("Invalid section character").show();
return;
}
teamsLoading(true);
const [search, sortGroups, sortMembers] = getFilters();
$.ajax({
url: updateSectionUrl,
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"sectionId": sectionId,
"sectionName": $("#editSectionName").val(),
"search": search,
"sortGroups": sortGroups,
"sortMembers": sortMembers
},
error: (xhr, textStatus, errorThrown) => { alert(xhr + " " + textStatus + " " + errorThrown); },
success: (result) => {
teamsLoading(false);
loadTeams(result.groups, search, result.sortGroups);
if (!result.form_errors) {
$("#editSectionModal").modal("hide");
}
else {
$("#editSectionName").focus();
$("#editSectionNameError").text(result.form_errors.editSectionName).show();
}
}
});
});
}
function openEditTeamModal(teamId) {
$("#editTeamModal").modal("show");
$("#saveEditTeamModal").off("click").on("click", () => {
teamsLoading(true);
const [search, sortGroups, sortMembers] = getFilters()
$.ajax({
url: updateTeamUrl,
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"teamId": teamId,
"teamNumber": $("#editTeamNumber").val(),
"search": search,
"sortGroups": sortGroups,
"sortMembers": sortMembers
},
error: (xhr, textStatus, errorThrown) => { alert(xhr + " " + textStatus + " " + errorThrown); },
success: (result) => {
teamsLoading(false);
loadTeams(result.groups, search, result.sortGroups);
if (!result.form_errors) {
$("#editTeamModal").modal("hide");
}
else {
$("#editTeamNumber").focus();
$("#editTeamNumberError").text(result.form_errors.editTeamNumber).show();
}
}
});
});
}
function openEditMemberModal(memberId) {
@ -271,7 +377,9 @@ function openEditMemberModal(memberId) {
$("#editMemberModal").modal("show");
// Update the submit button
$("#saveEditModal").off("click").on("click", () => { saveEditMemberModal(memberId); });
$("#saveEditModal").off("click").on("click", () => {
alert("you pressed save member");
});
}
function saveEditMemberModal(memberId) {