diff --git a/src/mainapp/models.py b/src/mainapp/models.py index 14a4ffd..a74288c 100644 --- a/src/mainapp/models.py +++ b/src/mainapp/models.py @@ -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}" diff --git a/src/mainapp/templates/index.html b/src/mainapp/templates/index.html index caf0414..d586ac6 100644 --- a/src/mainapp/templates/index.html +++ b/src/mainapp/templates/index.html @@ -24,7 +24,7 @@ -->

- Member Management + Angler Management

diff --git a/src/mainapp/templates/teams.html b/src/mainapp/templates/teams.html index 0cc482e..ee4b7aa 100644 --- a/src/mainapp/templates/teams.html +++ b/src/mainapp/templates/teams.html @@ -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 %} -

+
Back +

- Member Management + Manage Anglers

- - +
{% endblock content %} -{% load static %} {% block scripts %} @@ -217,6 +236,9 @@ diff --git a/src/mainapp/urls.py b/src/mainapp/urls.py index 691cdb6..7da889d 100644 --- a/src/mainapp/urls.py +++ b/src/mainapp/urls.py @@ -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') ] \ No newline at end of file diff --git a/src/mainapp/views.py b/src/mainapp/views.py index bba9a74..cdecbb9 100644 --- a/src/mainapp/views.py +++ b/src/mainapp/views.py @@ -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}) diff --git a/src/static/img/logo.webp b/src/static/img/logo.webp new file mode 100644 index 0000000..5497afa Binary files /dev/null and b/src/static/img/logo.webp differ diff --git a/src/static/js/teams.js b/src/static/js/teams.js index e6a220c..2cc6795 100644 --- a/src/static/js/teams.js +++ b/src/static/js/teams.js @@ -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) {