Anglers page refactor pt.1
This commit is contained in:
parent
ba1b46413c
commit
426f381da2
@ -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
@ -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)
|
||||
|
28
src/mainapp/management/commands/create_superuser.py
Normal file
28
src/mainapp/management/commands/create_superuser.py
Normal 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()
|
@ -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')),
|
||||
],
|
||||
),
|
||||
|
@ -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}"
|
||||
|
@ -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)]
|
||||
})
|
147
src/mainapp/templates/anglers.html
Normal file
147
src/mainapp/templates/anglers.html
Normal 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 %}
|
@ -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">
|
||||
|
83
src/mainapp/templates/modals/angler.html
Normal file
83
src/mainapp/templates/modals/angler.html
Normal 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>
|
47
src/mainapp/templates/modals/section.html
Normal file
47
src/mainapp/templates/modals/section.html
Normal 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>
|
47
src/mainapp/templates/modals/team.html
Normal file
47
src/mainapp/templates/modals/team.html
Normal 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>
|
@ -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>
|
||||
|
@ -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'),
|
||||
]
|
@ -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
|
||||
|
0
src/static/css/mainapp/anglers.css
Normal file
0
src/static/css/mainapp/anglers.css
Normal 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);
|
||||
}
|
||||
|
199
src/static/js/mainapp/anglers.js
Normal file
199
src/static/js/mainapp/anglers.js
Normal 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");
|
||||
}
|
@ -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>
|
||||
`;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user