Anglers page refactor pt.1

This commit is contained in:
Corban-Lee 2023-05-15 00:22:17 +01:00
parent ba1b46413c
commit 426f381da2
19 changed files with 1165 additions and 1134 deletions

View File

@ -28,14 +28,14 @@ class MemberAdmin(admin.ModelAdmin):
class TeamAdmin(admin.ModelAdmin):
"""Admin model for the Team model."""
readonly_fields = ("name",)
list_display = ("name",)
search_fields = ("name",)
# readonly_fields = ("id", "number",)
list_display = ("id", "number",)
search_fields = ("number",)
@admin.register(Section)
class SectionAdmin(admin.ModelAdmin):
"""Admin model for the Section model."""
list_display = ("name",)
search_fields = ("name",)
list_display = ("id", "character",)
search_fields = ("character",)

File diff suppressed because it is too large Load Diff

View File

@ -49,10 +49,10 @@ class Command(BaseCommand):
team=team
)
if not section.is_joinable(member):
member.delete()
i += 1
continue
# if not section.is_joinable(member):
# member.delete()
# i += 1
# continue
new_members.append(member)
print(member)

View File

@ -0,0 +1,28 @@
from django.contrib.auth.management.commands import createsuperuser
from django.core.management import CommandError
class Command(createsuperuser.Command):
help = 'Crate a superuser, and allow password to be provided'
def add_arguments(self, parser):
super(Command, self).add_arguments(parser)
parser.add_argument(
'--password', dest='password', default=None,
help='Specifies the password for the superuser.',
)
def handle(self, *args, **options):
password = options.get('password')
username = options.get('username')
database = options.get('database')
if password and not username:
raise CommandError("--username is required if specifying --password")
super(Command, self).handle(*args, **options)
if password:
user = self.UserModel._default_manager.db_manager(database).get(username=username)
user.set_password(password)
user.save()

View File

@ -1,8 +1,7 @@
# Generated by Django 4.1.5 on 2023-05-10 08:30
# Generated by Django 4.1.5 on 2023-05-12 09:13
from django.db import migrations, models
import django.db.models.deletion
import mainapp.models
class Migration(migrations.Migration):
@ -17,13 +16,14 @@ class Migration(migrations.Migration):
name='Section',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=3, unique=True)),
('character', models.CharField(max_length=3, unique=True)),
],
),
migrations.CreateModel(
name='Team',
fields=[
('name', 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')),
('number', models.PositiveIntegerField()),
],
),
migrations.CreateModel(
@ -33,7 +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')),
('section', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='mainapp.section')),
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='mainapp.team')),
],
),

View File

@ -1,12 +1,8 @@
"""Models for the mainapp."""
from string import ascii_uppercase
from django.db import models
from django.core.exceptions import ValidationError
from django.db import models
class ReusableAutoField(models.PositiveIntegerField):
"""A django auto field that can reuse deleted primary keys."""
@ -98,7 +94,6 @@ class SectionValidator:
return value
class SectionManager(models.Manager):
@staticmethod
@ -153,25 +148,16 @@ 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)
character = models.CharField(max_length=3, unique=True, null=False)
objects = SectionManager()
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()
self.character = self.character.upper()
def __str__(self) -> str:
return self.name
@property
def get_members(self):
return Member.objects.filter(section=self)
return self.character
class Member(models.Model):
@ -195,28 +181,17 @@ class Member(models.Model):
self.peg_number = min (peg_numbers)
def __str__(self):
return f"{self.first_name} {self.last_name} (team {self.team.name}) [section {self.section.name}]"
return f"{self.first_name} {self.last_name} (team {self.team.number}) [section {self.section.character}]"
@property
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()
number = models.PositiveIntegerField(unique=True, null=False, blank=False)
def __str__(self):
return f"Team {self.name}"
@property
def get_members(self) -> list[Member]:
return Member.objects.filter(team=self)
return f"Team {self.number}"

View File

@ -1,11 +0,0 @@
import names
from string import ascii_lowercase
created_teams = []
for char in ascii_lowercase:
created_teams.append({
"identifier": char.upper(),
"members": [{
"name": names.get_full_name()
} for i in range(9)]
})

View File

@ -0,0 +1,147 @@
{% extends "base.html" %}
{% load static %}
{% block title %}
Manage Anglers |
{% endblock title %}
{% block style %}
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" />
{% endblock style %}
{% block content %}
<!-- Header -->
<div class="container"></div>
<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>
<!-- End Header -->
<div class="container my-4 p-4 pb-0 bg-body rounded">
<!-- Controls Header -->
<div class="row mb-4">
<div class="col-12 col-xl-4 d-flex">
<h3 class="mb-3 mb-xl-0">
<span class="me-sm-3">Manage Anglers</span>
<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="addAngler" 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">
<i class="bi bi-people"></i>
</button>
<button class="btn border-secondary-subtle btn-outline-company" id="addSection" data-bs-toggle="tooltip" data-bs-title="Add Section">
<i class="bi bi-layers"></i>
</button>
</div>
</div>
<div class="col-sm-9 col-md-6 col-xl-4">
<!-- Search Bar -->
<div class="input-group dropdown">
<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 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>
<!-- Filters Dropdown -->
<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()">
<li class="px-4">
<h3 class="h6 mb-3">Show Anglers in groups of</h3>
<div>
<div class="d-inline-block form-check">
<input type="radio" class="form-check-input" checked value="teams" name="showGroups" id="showTeams">
<label for="showTeams" class="form-check-label">Teams</label>
</div>
<div class="d-inline-block form-check ms-4">
<input type="radio" class="form-check-input" value="sections" name="showGroups" id="showSections">
<label for="showSections" class="form-check-label">Sections</label>
</div>
</div>
</li>
<li class="dropdown-divider my-3 mx-4 bg-light-subtle"></li>
<li class="px-4">
<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="names" name="sortAnglers" id="sortNames">
<label for="sortNames" class="form-check-label">Name</label>
</div>
<div class="d-inline-block form-check ms-4">
<input type="radio" class="form-check-input" checked value="pegs" name="sortAnglers" id="sortPegs">
<label for="sortPegs" class="form-check-label">Peg Number</label>
</div>
</div>
</li>
</ul>
</form>
<!-- End Filters Dropdown -->
</div>
<!-- End Search Bar -->
</div>
</div>
<!-- End Controls Header -->
<!-- Groups Container -->
<div class="row" id="groups"></div>
<!-- End Groups Container -->
<!-- Loading Spinner -->
<div class="col-12 text-center pb-4" id="loadingSpinner" style="display: none;">
<div class="spinner-border spinner-border-lg" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<!-- End Loading Spinner -->
<!-- Not Found Message -->
<div class="pb-4" id="notFound" style="display: none;">
<div class="alert alert-danger m-0" role="alert">
No groups could be found, perhaps you need to refine your search?
</div>
</div>
<!-- End Not Found Message -->
</div>
<!-- Section Modal -->
{% include "modals/section.html" %}
<!-- End Section Modal -->
<!-- Team Modal -->
{% include "modals/team.html" %}
<!-- End Team Modal -->
<!-- Angler Modal -->
{% include "modals/angler.html" %}
<!-- End Angler Modal -->
{% endblock content %}
{% 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>
<script type="text/javascript">
const pageUrl = "{% url 'anglers' %}"
// const getPageDataUrl = "{% url 'get-angler-data' %}"; DEPRICATED FOR REWRITE
// 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/mainapp/anglers.js' %}"></script>
{% endblock scripts %}

View File

@ -14,7 +14,7 @@
<!-- Teams & Members -->
<div class="col">
<a href="{% url 'members' %}" class="text-decoration-none text-body">
<a href="{% url 'anglers' %}" class="text-decoration-none text-body">
<!-- <div class="fluid-hover-zoom shadow-sm md-shadow-on-hover bg-body-tertiary rounded p-5 text-body d-flex flex-wrap justify-content-center align-items-center">
<i class="bi bi-people w-100 text-center" style="font-size: 5rem;"></i>
<h3 class=" fs-3">

View File

@ -0,0 +1,83 @@
<div class="modal fade" id="anglerModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content border-0">
<!-- Modal Header -->
<div class="modal-header border-0">
<h3 class="modal-title fs-5 color" id="anglerTitle">
Angler Modal Title
<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>
<!-- End Modal Header -->
<!-- Modal Body -->
<div class="modal-body">
<form id="anglerForm">
<!-- Angler Name -->
<div class="row g-3 mb-3">
<div class="col-md">
<div class="form-floating">
<input class="form-control" type="text" name="anglerForename" id="anglerForename" placeholder="Forename">
<label for="anglerForename">Forename</label>
</div>
</div>
<div class="col-md">
<div class="form-floating">
<input class="form-control" type="text" name="anglerSurname" id="anglerSurname" placeholder="Surname">
<label for="anglerSurname">Surname</label>
</div>
</div>
</div>
<!-- End Angler Name -->
<!-- Angler Team -->
<div class="input-group mb-3">
<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="anglerTeam" id="anglerTeam"></select>
</div>
<!-- End Angler Team -->
<!-- Angler Section -->
<div class="input-group mb-3">
<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="anglerSection" id="anglerSection"></select>
</div>
<!-- End Angler Section -->
<!-- Angler Peg Number -->
<div class="row">
<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="Peg Number">
<i class="bi bi-tag"></i>
</span>
<input type="number" name="anglerPeg" id="anglerPeg" 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>
</button>
</div>
</div>
<!-- End Angler Peg Number -->
</form>
</div>
<!-- End Modal Body -->
<!-- Modal Footer -->
<div class="modal-footer border-0">
<button type="button" class="btn btn-company px-4" id="saveAngler">Save</button>
</div>
<!-- End Modal Footer -->
</div>
</div>
</div>

View File

@ -0,0 +1,47 @@
<div class="modal fade" id="sectionModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content border-0">
<!-- Modal Header -->
<div class="modal-header border-0">
<h3 class="modal-title fs-5" id="sectionTitle">
Section Modal Title
<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>
<!-- End Modal Header -->
<!-- Modal Body -->
<div class="modal-body">
<form id="sectionForm">
<!-- Section Character -->
<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="sectionCharacter" id="sectionCharacter" class="form-control" minlength="1" maxlength="3" value="A">
</div>
<button type="button" id="sectionCharacterNext" 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="sectionCharacterError" style="display: hidden"></div>
<!-- End Section Character -->
</form>
</div>
<!-- End Modal Body -->
<!-- Modal Footer -->
<div class="modal-footer border-0">
<button type="button" class="btn btn-company px-4" id="saveSection">Save</button>
</div>
<!-- End Modal Footer -->
</div>
</div>
</div>

View File

@ -0,0 +1,47 @@
<div class="modal fade" id="teamModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content border-0">
<!-- Modal Header -->
<div class="modal-header border-0">
<h3 class="modal-title fs-5" id="teamTitle">
Team Modal Title
<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>
<!-- End Modal Header -->
<!-- Modal Body -->
<div class="modal-body">
<form id="teamForm">
<!-- Team Number -->
<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="Team number">
<i class="bi bi-tag"></i>
</span>
<input type="number" name="teamNumber" id="teamNumber" class="form-control" min="1" max="9999" value="1">
</div>
<button type="button" id="teamNumberNext" 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>
<div class="col-md-6 mt-2 text-danger" id="teamNumberError" style="display: hidden"></div>
<!-- End Team Number -->
</form>
</div>
<!-- End Modal Body -->
<!-- Modal Footer -->
<div class="modal-footer border-0">
<button type="button" class="btn btn-company px-4" id="saveTeam">Save</button>
</div>
<!-- End Modal Footer -->
</div>
</div>
</div>

View File

@ -50,11 +50,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" name="sortGroups" id="sortTeamsName">
<input type="radio" class="form-check-input" checked value="teams" 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" name="sortGroups" id="sortTeamsSection">
<input type="radio" class="form-check-input" value="sections" name="sortGroups" id="sortTeamsSection">
<label for="sortTeamsSection" class="form-check-label">Sections</label>
</div>
</div>
@ -78,8 +78,7 @@
</div>
</div>
</div>
<div class="row" id="teamsContainer"></div>
<div class="row" id="groupsContainer"></div>
<div class="col-12 text-center pb-4" id="teamsLoadingSpinner">
<div class="spinner-border spinner-border-lg" role="status">
<span class="visually-hidden">Loading...</span>
@ -234,10 +233,11 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.0.13/dist/js/select2.full.min.js"></script>
<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 pageUrl = "{% url 'anglers' %}"
// const getPageDataUrl = "{% url 'get-angler-data' %}"; DEPRICATED FOR REWRITE
// 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>

View File

@ -8,9 +8,12 @@ urlpatterns = [
path('members/', views.teams, name='members'),
# path('bulk-peg/', views.bulk_create_pegs, name='bulk-peg'),
path('get-teams/', views.get_teams, name='get-teams'),
path('get-angler-data/', views.get_angler_page_data, name='get-angler-data'),
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')
path("get-next-identifier/", views.get_next_identifier, name='get-next-identifier'),
# Rewrite
path('anglers/', views.ManageAnglersView.as_view(), name='anglers'),
]

View File

@ -6,6 +6,9 @@ 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 django.views import View
from django.views.decorators.http import require_GET, require_POST
from django.http import HttpRequest
from .models import Team, Member, Section, SectionManager, ReusableAutoField, SectionValidator
@ -38,7 +41,246 @@ def name_sort_key(section):
else:
return (1, section.name[:-1], section.name[-1])
def get_teams(request, **kwargs):
class ManageAnglersView(View):
"""View for the Manage Anglers page."""
template_name = "anglers.html"
def get(self, request: HttpRequest, *args, **kwargs) -> HttpRequest:
"""Handle GET requests to the Manage Anglers page.
Args:
request (HttpRequest): The HttpRequest object, contains GET data.
Returns:
HttpRequest: A render of the Manage Anglers page.
"""
return render(request, self.template_name)
def post(self, request: HttpRequest, *args, **kwargs) -> JsonResponse:
"""Handle POST requests to the Manage Anglers page.
Args:
request (HttpRequest): The HttpRequest object, contains POST data.
Returns:
JsonResponse: Contains the result of the action.
"""
tasks = request.POST.getlist("tasks[]")
data = {}
for task in tasks:
data.update(self.handle_task(request, task))
return JsonResponse(data)
def handle_task(self, request, task: str) -> dict[str, str]:
"""Handle a task.
Args:
request (HttpRequest): HttpRequest object, contains POST data.
task (str): The task to handle.
Raises:
ValueError: The task is invalid.
Returns:
dict[str, str]: The result of the task.
"""
# Format is {key = ACTION-TASK_NAME: value = HANDLER_FUNCTION}
task_handlers = {
"update-team": self.update_team,
"update-section": self.update_section,
"update-angler": self.update_angler,
"get-teams": self.get_teams,
"get-sections": self.get_sections,
"get-anglers": self.get_anglers,
}
handler = task_handlers.get(task)
if not handler:
raise ValueError(f"Invalid task: {task}")
return handler(request)
def update_team(self, request) -> dict[str]:
"""Update a team, returns a dictionary of the new team's data."""
result = {"form_errors": {}, "team": None}
team_id = request.POST.get("id")
team_number = request.POST.get("number")
if not (team_id and team_number):
raise ValueError("Team ID or Team Number is missing or empty")
if team_id == "-1":
team = Team(number=team_number)
else:
team = Team.objects.get(id=team_id)
team.number = team_number
try:
team.save()
result["team"] = {"id": team.id, "number": team.number}
except IntegrityError:
result["form_errors"]["#editTeamNumberError"] = "A Team with this number already exists"
return result
def update_section(self, request) -> dict[str]:
"""Update a section, returns a dictionary of the new section's data."""
result = {"form_errors": {}, "section": None}
section_id = request.POST.get("id")
section_character = request.POST.get("character")
if not (section_id and section_character):
raise ValueError("Section ID or Section Character is missing or empty")
if section_id == "-1":
section = Section(character=section_character)
else:
section = Section.objects.get(id=section_id)
section.character = section_character
try:
section.save()
result["section"] = {"id": section.id, "character": section.character}
except IntegrityError:
result["form_errors"]["#editSectionNameError"] = "A Section with this character already exists"
return result
def update_angler(self, request) -> dict[str]:
"""Update an Angler, returns a dictionary of the new angler's data."""
result = {"form_errors": {}, "angler": None}
angler_id = request.POST.get("angler_id")
forename = request.POST.get("forename")
surname = request.POST.get("surname")
peg_number = request.POST.get("peg_number")
team_id = request.POST.get("team_id")
section_id = request.POST.get("section_id")
if not angler_id:
raise ValueError("Invalid angler ID")
team = Team.objects.get(id=team_id)
section = Section.objects.get(id=section_id)
if angler_id == "-1":
angler = Member(
first_name=forename,
last_name=surname,
peg_number=peg_number,
team=team,
section=section
)
else:
angler = Member.objects.get(id=angler_id)
angler.first_name = forename
angler.last_name = surname
angler.peg_number = peg_number
angler.team = team
angler.section = section
angler.save()
result["angler"] = {
"id": id,
"forename": forename,
"surname": surname,
"peg_number": peg_number,
"team_id": team_id,
"section_id": section_id,
"team_number": angler.team.number,
"section_character": angler.section.character
}
return result
def get_teams(self, request) -> dict[str]:
"""Returns a dictionary of all teams."""
search = request.GET.get("search")
teams = Team.objects.order_by("number").all()
# Search works by exluding teams that do not contain members with the search term in their names.
if search:
search_terms = search.split()
members = Member.objects.filter(reduce(lambda x, y: x & y, [
Q(first_name__icontains=term) | Q(last_name__icontains=term)
for term in search_terms
]))
teams = teams.filter(members__in=members).distinct()
return {"teams": [{"id": team.id, "number": team.number} for team in teams]}
def get_sections(self, request) -> dict[str]:
"""Returns a dictionary of all sections."""
search = request.GET.get("search")
sections = Section.objects.order_by("character").all()
if search:
search_terms = search.split()
members = Member.objects.filter(reduce(lambda x, y: x & y, [
Q(first_name__icontains=term) | Q(last_name__icontains=term)
for term in search_terms
]))
sections = sections.filter(members__in=members).distinct()
return {"sections": [{"id": section.id, "character": section.character} for section in sections]}
def get_anglers(self, request) -> dict[str]:
"""Returns a dictionary of all anglers."""
search = request.GET.get("search")
anglers = Member.objects.order_by("first_name").all()
if search:
search_terms = search.split()
anglers = anglers.filter(reduce(lambda x, y: x & y, [
Q(first_name__icontains=term) | Q(last_name__icontains=term)
for term in search_terms
])).distinct()
return {
"anglers": [
{
"id": angler.id,
"first_name": angler.first_name,
"last_name": angler.last_name,
"peg_number": angler.peg_number,
"team_id": angler.team.id,
"section_id": angler.section.id,
"team_number": angler.team.number,
"section_character": angler.section.character
}
for angler in anglers
]
}
def get_angler_page_data(request, **kwargs):
"""Returns a JsonResponse containing a dictionary with a k/v pair for a list of teams.
Args:
@ -54,44 +296,44 @@ def get_teams(request, **kwargs):
sort_groups = request.POST.get("sortGroups") or "team"
sort_members = request.POST.get("sortMembers") or "peg_number"
group_object = Team if sort_groups == "team" else Section
groups = group_object.objects.order_by("name").all()
teams = Team.objects.order_by("number").all()
sections = Section.objects.order_by("character").all()
# Filter out teams that don't contain members being searched for
if search:
search_terms = search.split()
members = Member.objects.filter(
reduce(
lambda x, y: x | y,
lambda x, y: x & y, ## changed to AND from OR to fix bug with whitespace searches
[
Q(first_name__icontains=term) | Q(last_name__icontains=term)
for term in search_terms
]
)
)
groups = groups.filter(members__in=members).distinct()
teams = teams.filter(members__in=members).distinct()
sections = sections.filter(members__in=members).distinct()
if sort_groups == "section":
groups = sorted([group for group in groups], key=name_sort_key)
# Create a dictionary for the data that is JSON safe
response_data = {"groups": []}
for group in groups:
group_data = {
"name": group.name,
"members": [
{
"first": member.first_name,
"last": member.last_name,
"id": member.id,
"peg": member.peg_number,
"team": member.team.name if member.team else None,
"section": member.section.name if member.section else None
}
for member in group.members.order_by(sort_members).all()
]
}
response_data["groups"].append(group_data)
response_data = {
"teams": [
{"id": team.id, "number": team.number}
for team in teams
],
"sections": [
{"id": sec.id, "character": sec.character}
for sec in sections
],
"anglers": [
{
"id": member.id,
"first": member.first_name,
"last": member.last_name,
"peg": member.peg_number,
"team_id": member.team.id if member.team else None,
"section_id": member.section.id if member.section else None
}
for member in Member.objects.order_by(sort_members).all()
]
}
response_data["sortGroups"] = sort_groups
response_data["sortMembers"] = sort_members
@ -101,6 +343,10 @@ def get_teams(request, **kwargs):
return JsonResponse(response_data)
def update_member(request):
"""Update a member. Returns a JsonResponse with the updated teams."""
@ -126,7 +372,7 @@ def update_member(request):
member.save()
return get_teams(request)
return get_angler_page_data(request)
def update_section(request):
"""Update a section, returns JsonResponse with updated teams data."""
@ -139,26 +385,26 @@ def update_section(request):
validator = SectionValidator()
if not validator.is_valid(section_name):
json_response = get_teams(request, form_errors={
json_response = get_angler_page_data(request, form_errors={
"editSectionName": "This is an invalid section"
})
return json_response
if section_id == "-1":
section = Section(name=section_name)
section = Section(character=section_name)
else:
section = Section.objects.get(id=section_id)
section.name = section_name
section.character = section_name
try:
section.save()
except IntegrityError:
json_response = get_teams(request, form_errors={
json_response = get_angler_page_data(request, form_errors={
"editSectionName": "A Section with this character already exists"
})
return json_response
return get_teams(request) # returns jsonresponse with new details
return get_angler_page_data(request) # returns jsonresponse with new details
def update_team(request):
"""Update a team, returns a JsonResponse with updated teams data."""
@ -171,18 +417,18 @@ def update_team(request):
try:
if team_id == "-1":
team = Team.objects.create(name=team_number)
team = Team.objects.create(number=team_number)
else:
team = Team.objects.get(id=team_id)
team.name = team_number
team.number = team_number
team.save()
except IntegrityError as error:
json_response = get_teams(request, form_errors={
json_response = get_angler_page_data(request, form_errors={
"editTeamNumber": "A Team with this number already exists"
})
return json_response
return get_teams(request)
return get_angler_page_data(request)
def get_next_peg() -> int:
pass

View File

View File

@ -7,6 +7,8 @@
$(document).ready(() => {
activateTooltips();
pageLoadMotionEffects();
$("select").select2({theme: "bootstrap-5", minimumResultsForSearch: -1})
});
@ -46,4 +48,13 @@ function setMotionEffects(enable) {
else {
$(".fluid-hover-zoom").addClass("fluid-hover-zoom-off").removeClass("fluid-hover-zoom");
}
}
}
jQuery.expr[':'].icontains = function(a, i, m) {
return jQuery(a).text().toUpperCase()
.indexOf(m[3].toUpperCase()) >= 0;
};
const findByKey = (array, key, value) => {
return array.find(item => item[key] === value);
}

View File

@ -0,0 +1,199 @@
$(document).ready(() => {
toggleLoading(true);
// Search Bar Functionality
searchTimeout = null;
$("#search").on("input", () => {
clearTimeout(searchTimeout);
toggleLoading(true);
searchTimeout = setTimeout(() => {
fetchAndLoadGroups();
}, 500)
});
$("#searchButton").on("click", () => {
fetchAndLoadGroups();
});
$("#sortForm input").on("click", function() {
const name = $(this).attr("name");
localStorage.setItem(name, $(`input[name='${name}']:checked`, "#sortForm").val());
fetchAndLoadGroups();
});
// Modals
$("#addAngler").on("click", () => {
addAngler(-1);
});
$("#addTeam").on("click", () => {
addTeam(-1);
});
$("#addSection").on("click", () => {
addSection(-1);
});
fetchAndLoadGroups();
});
function fetchAndLoadGroups() {
toggleLoading(true);
const showGroups = localStorage.getItem("showGroups");
const sortAnglers = localStorage.getItem("sortAnglers");
const task = showGroups === "teams" ? "get-teams" : "get-sections";
$.ajax({
type: "POST",
url: pageUrl,
data: {
csrfmiddlewaretoken: csrfMiddlewareToken,
tasks: [task, "get-anglers"],
sortAnglers: sortAnglers,
search: $("#search").val()
},
error: (error) => {
console.error(error);
},
success: (data) => {
loadGroups(data);
}
});
toggleLoading(false);
}
function groupTemplate(group, groupType, oppositeGroupType) {
groupValue = groupType === "teams" ? group.number : group.character
return `
<div class='col-12 col-md-6 col-xl-4 mb-4'>
<div
class='${groupType.toLowerCase()} px-4 py-3 bg-body-tertiary bg-gradient rounded h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover'
data-value='${groupValue}'>
<h3>
<span class='fs-4'>
<span class='fs-6'>${groupType.toUpperCase()}</span>
${groupValue}
</span>
<button class='btn btn-sm btn-light float-end border-0 me-1'>
<i class='bi bi-gear fs-6'></i>
</button>
</h3>
<ul class='list-unstyled anglers mt-3'>
</ul>
</div>
</div>
`
}
function anglerTemplate(angler, oppositeGroupType) {
const fullname = angler.first_name + " " + angler.last_name;
const oppositeGroupValue = oppositeGroupType === "teams"? angler.team_number : angler.section_character;
var oppositeGroup = oppositeGroupType.slice(0, -1);
oppositeGroup = oppositeGroup.charAt(0).toUpperCase() + oppositeGroup.slice(1);
return `
<li class='mb-3 rounded w-100 fluid-hover-zoom'>
<div
class='angler d-flex'
data-first='${angler.first_name}'
data-last='${angler.last_name}'
data-id='${angler.id}'
data-peg-number='${angler.peg_number}'
data-team-id='${angler.team_id}'
data-section-id='${angler.section_id}'>
<div class='px-3 py-2 w-100'>
<div class='badge bg-secondary-subtle text-body fixed-badge' data-bs-title='Peg ${angler.peg_number}' data-bs-toggle='tooltip'>
${angler.peg_number}
</div>
<div class='badge bg-secondary-subtle text-body fixed-badge' data-bs-title='${oppositeGroup} ${oppositeGroupValue}' data-bs-toggle='tooltip'>
${oppositeGroupValue}
</div>
<div class='d-inline-block ms-2 angler-fullname'>
${fullname}
</div>
</div>
<button type='button' class='ms-auto btn btn-light border-0 fs-6 pencil-btn align-self-center me-1 force-contents-center'>
<i class='bi bi-pencil'></i>
</button>
</div>
</li>
`
}
function loadGroups(data) {
const showGroups = localStorage.getItem("showGroups");
const groupType = showGroups === "teams" ? "teams" : "sections";
const oppositeGroupType = showGroups === "teams" ? "sections" : "teams";
const groups = data[groupType];
$("#groups").html("");
groups.forEach((group) => {
$("#groups").append(groupTemplate(group, groupType, oppositeGroupType));
data.anglers.forEach((angler) => {
if (angler.team_id === group.id || angler.section_id === group.id) {
$("#groups").find(".anglers").last().append(
anglerTemplate(angler, oppositeGroupType)
);
}
});
});
activateTooltips();
}
/**
* Shows a loading icon, hides groups and error alerts.
*
* @param {Boolean} loading - Wether or not to show the loading icon
*/
function toggleLoading(loading) {
if (loading) {
$("#groups").hide();
$("#notFound").hide();
$("#loadingSpinner").show();
}
else {
$("#groups").show();
$("#loadingSpinner").hide();
}
}
/**
* Open the modal for adding/editing an angler
*
* @param {Number} anglerId - ID of the angler, if -1 will create a new angler
*/
function addAngler(anglerId) {
$("#anglerModal").modal("show");
}
/**
* Open the modal for adding/editing a team
*
* @param {Number} teamId - ID of the team, if -1 will create a new team
*/
function addTeam(teamId) {
$("#teamModal").modal("show");
}
/**
* Open the modal for adding/editing a section
*
* @param {Number} sectionId - ID of the section, if -1 will create a new section
*/
function addSection(sectionId) {
$("#sectionModal").modal("show");
}

View File

@ -3,6 +3,14 @@ jQuery.expr[':'].icontains = function(a, i, m) {
.indexOf(m[3].toUpperCase()) >= 0;
};
const findByKey = (array, key, value) => {
return array.find(item => item[key] === value);
}
var globalTeamsList = [];
var globalSectionsList = [];
var globalAnglersList = [];
$(document).ready(() => {
teamsLoading(true); // show the loading icon
@ -30,7 +38,7 @@ $(document).ready(() => {
const name = $(this).attr("name");
localStorage.setItem(name, $(`input[name='${name}']:checked`, "#sortForm").val());
fetchAndLoadTeams(...getFilters());
fetchAndLoadPageData();
});
// Load the last saved sort settings TODO: use local storage so it only saves on one reload
@ -93,7 +101,9 @@ $(document).ready(() => {
$("select").select2({theme: "bootstrap-5", minimumResultsForSearch: -1})
// load the teams with default filters
fetchAndLoadTeams(...getFilters());
fetchAndLoadPageData();
// fetchAndLoadTeams(...getFilters());
});
@ -121,10 +131,12 @@ function loadTeams(groups, highlightText="", groupType) {
// Iterate over and add each team
groups.forEach((group) => {
const oppositeGroupType = groupType === "SECTION"? "Team" : "Section";
$("#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'
class='${groupType.toLowerCase()} px-4 py-3 bg-body-tertiary bg-gradient rounded h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover'
data-number='${group.name}'>
<h3>
<span class='fs-4'>
@ -144,7 +156,7 @@ function loadTeams(groups, highlightText="", groupType) {
// While we have the team, iterate over and add it's members
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(
@ -204,19 +216,6 @@ function loadTeams(groups, highlightText="", groupType) {
});
openEditMemberModal($(this).parent().data("member-id"));
});
// Bind new member/team buttons
$("#addTeam").on("click", () => {
openEditTeamModal(-1);
});
$("#addMember").on("click", () => {
openEditMemberModal(-1);
});
$("#addSection").on("click", () => {
openEditSectionModal(-1);
});
}
function fetchAndLoadTeams(search="", sortGroups="", sortMembers="") {
@ -237,16 +236,17 @@ function fetchAndLoadTeams(search="", sortGroups="", sortMembers="") {
});
}
function teamsLoading(show=true) {
$("#teamsNotFound").hide()
if (show) {
$("#teamsContainer").hide();
$("#groupsContainer").hide();
$("#teamsLoadingSpinner").show();
return
}
$("#teamsContainer").show();
$("#groupsContainer").show();
$("#teamsLoadingSpinner").hide();
}
@ -267,32 +267,38 @@ function openEditSectionModal(sectionId) {
teamsLoading(true);
const [search, sortGroups, sortMembers] = getFilters();
const getGroupTask = sortGroups === "teams" ? "get-teams": "get-sections"
const loadFunction = sortGroups === "teams" ? displayTeams : displaySections;
$.ajax({
url: updateSectionUrl,
url: pageUrl,
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"sectionId": sectionId,
"sectionName": $("#editSectionName").val(),
"tasks": ["update-section", getGroupTask, "get-anglers"],
"id": sectionId,
"character": $("#editSectionName").val().toUpperCase(),
"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 (Object.keys(result.form_errors).length > 0) {
for (const errorId in result.form_errors) {
$(errorId).text(result.form_errors[errorId]).show();
}
return;
}
if (!result.form_errors) {
$("#editSectionModal").modal("hide");
}
else {
$("#editSectionName").focus();
$("#editSectionNameError").text(result.form_errors.editSectionName).show();
}
alert(JSON.stringify(result));
alert(sortGroups);
loadFunction(result[sortGroups], result.anglers);
$("#editSectionModal").modal("hide");
}
});
teamsLoading(false);
});
}
@ -310,27 +316,28 @@ function openEditTeamModal(teamId) {
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"teamId": teamId,
"teamNumber": $("#editTeamNumber").val(),
"tasks": ["update-team"],
"id": teamId,
"number": $("#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 (Object.keys(result.form_errors).length > 0) {
alert(JSON.stringify(result));
for (const errorId in result.form_errors) {
$(errorId).focus();
$(errorId).text(result.form_errors[errorId]).show();
}
return;
}
if (!result.form_errors) {
$("#editTeamModal").modal("hide");
}
else {
$("#editTeamNumber").focus();
$("#editTeamNumberError").text(result.form_errors.editTeamNumber).show();
}
$("#editTeamModal").modal("hide");
}
});
teamsLoading(false);
});
}
@ -356,13 +363,17 @@ function openEditMemberModal(memberId) {
// modalTitle = "Edit: " + first + " " + last
// }
// // Load teams as options
// $("#editMemberTeam").html("");
// $(".team").map(function() {
// return $(this).data("number");
// }).get().forEach((team) => {
// $("#editMemberTeam").append($(`<option value='${team}'>Team ${team}</option>`));
// });
// Load teams as options
$("#editMemberTeam").html("");
globalTeamsList.forEach((team) => {
$("#editMemberTeam").append(`<option value='${team.id}'>Team ${team.number}</option>`);
});
$("#editMemberSection").html("");
globalSectionsList.forEach((section) => {
$("#editMemberSection").append(`<option value='${section.id}'>Section ${section.character}</option>`);
});
// // Load data to form
// $("#editMemberName").text(modalTitle);
@ -382,6 +393,7 @@ function openEditMemberModal(memberId) {
});
}
function saveEditMemberModal(memberId) {
if (!$("#editMemberForm").valid()) {
@ -416,4 +428,161 @@ function saveEditMemberModal(memberId) {
$("#editMemberModal").modal("hide");
}
});
}
// REWRITE BEYOND THIS POINT
$(document).ready(() => {
// Bind new member/team buttons
$("#addTeam").on("click", () => {
openEditTeamModal(-1);
});
$("#addMember").on("click", () => {
openEditMemberModal(-1);
});
$("#addSection").on("click", () => {
openEditSectionModal(-1);
});
});
function fetchAndLoadPageData() {
teamsLoading(true);
const [search, sortGroups, sortMembers] = getFilters();
$.ajax({
url: getPageDataUrl,
type: "post",
data: {
"csrfmiddlewaretoken": csrfMiddlewareToken,
"search": search,
"sortGroups": sortGroups,
"sortMembers": sortMembers
},
error: (xhr, textStatus, errorThrown) => {
alert(xhr + " " + textStatus + " " + errorThrown);
},
success: (result) => {
loadPageData(result.teams, result.sections, result.anglers, result.sortGroups);
}
});
teamsLoading(false);
}
function loadPageData(teams, sections, anglers, displayGroups) {
$("#groupsContainer").html("");
globalTeamsList = teams;
globalSectionsList = sections;
globalAnglersList = anglers;
if (displayGroups === "section") {
// displaySections();
}
else if (displayGroups === "team") {
displayTeams();
}
else {
throw new Error(`invalid display groups '${displayGroups}'`);
}
activateTooltips();
}
function displaySections(sections, anglers) {
sections.forEach((section) => {
$("#groupsContainer").append(getGroupTemplate(section.id, "section", section.character));
anglers.forEach((angler) => {
if (angler.section_id != section.id) { return; }
const team = findByKey(globalTeamsList, "id", angler.team_id).number;
$("#groupsContainer").find(".anglers").last().append(getAnglerTemplate(angler, "Team", team));
});
});
}
function displayTeams() {
globalTeamsList.forEach((team) => {
$("#groupsContainer").append(getGroupTemplate(team.id, "team", team.number));
globalAnglersList.forEach((angler) => {
if (angler.team_id != team.id) { return; }
const section = findByKey(globalSectionsList, "id", angler.section_id).character;
$("#groupsContainer").find(".anglers").last().append(getAnglerTemplate(angler, "Section", section));
});
});
}
function getGroupTemplate(groupId, groupType, groupName) {
return`
<div class='col-12 col-md-6 col-xl-4 mb-4'>
<div
class='group px-4 py-3 bg-body-tertiary bg-gradient rounded h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover'
data-id='${groupId}'>
<h3>
<span class='fs-4'>
<span class='fs-6'>${groupType.toUpperCase()}</span>
${groupName}
</span>
<button class='btn btn-sm btn-light float-end border-0 me-1'>
<i class='bi bi-gear fs-6'></i>
</button>
</h3>
<ul class='list-unstyled anglers mt-3'>
</ul>
</div>
</div>
`;
}
function getAnglerTemplate(angler, oppositeGroupType, oppositeGroupName) {
const fullname = angler.first + " " + angler.last;
return `
<li class='mb-3 rounded w-100 fluid-hover-zoom'>
<div
class='angler d-flex'
data-first='${angler.first}'
data-last='${angler.last}'
data-member-id='${angler.id}'
data-peg='${angler.peg}'
data-team-id='${angler.team_id}'
data-section-id='${angler.section_id}'>
<div class='px-3 py-2 w-100'>
<div class='badge bg-secondary-subtle text-body fixed-badge' data-bs-title='Peg ${angler.peg}' data-bs-toggle='tooltip'>
${angler.peg}
</div>
<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 angler-fullname'>
${fullname}
</div>
</div>
<button type='button' class='ms-auto btn btn-light border-0 fs-6 pencil-btn align-self-center me-1 force-contents-center'>
<i class='bi bi-pencil'></i>
</button>
</div>
</li>
`;
}