diff --git a/src/mainapp/migrations/0001_initial.py b/src/mainapp/migrations/0001_initial.py
index ab3aa24..764396b 100644
--- a/src/mainapp/migrations/0001_initial.py
+++ b/src/mainapp/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 4.1.5 on 2023-05-12 09:13
+# Generated by Django 4.1.5 on 2023-05-15 11:20
from django.db import migrations, models
import django.db.models.deletion
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
name='Team',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('number', models.PositiveIntegerField()),
+ ('number', models.PositiveIntegerField(unique=True)),
],
),
migrations.CreateModel(
diff --git a/src/mainapp/templates/anglers.html b/src/mainapp/templates/anglers.html
index a20e0c2..34e4f78 100644
--- a/src/mainapp/templates/anglers.html
+++ b/src/mainapp/templates/anglers.html
@@ -10,8 +10,52 @@
{% endblock style %}
+{% block header_buttons %}
+Back
+{% endblock header_buttons %}
+
{% block content %}
+
\ No newline at end of file
diff --git a/src/mainapp/views.py b/src/mainapp/views.py
index 70abaa6..9af1da4 100644
--- a/src/mainapp/views.py
+++ b/src/mainapp/views.py
@@ -4,7 +4,7 @@ from functools import reduce
from django.shortcuts import render, redirect
from django.http import JsonResponse
-from django.db.models import Q, Case, When, Value, IntegerField
+from django.db.models import Q, Case, When, Value, IntegerField, Min
from django.db.utils import IntegrityError
from django.views import View
from django.views.decorators.http import require_GET, require_POST
@@ -57,7 +57,10 @@ class ManageAnglersView(View):
HttpRequest: A render of the Manage Anglers page.
"""
- return render(request, self.template_name)
+ anglers = Member.objects.order_by("first_name", "last_name")
+ context = {"anglers": anglers}
+
+ return render(request, self.template_name, context)
def post(self, request: HttpRequest, *args, **kwargs) -> JsonResponse:
"""Handle POST requests to the Manage Anglers page.
@@ -99,6 +102,11 @@ class ManageAnglersView(View):
"get-teams": self.get_teams,
"get-sections": self.get_sections,
"get-anglers": self.get_anglers,
+ "delete-team": self.delete_team,
+ "delete-section": self.delete_section,
+ "delete-angler": self.delete_angler,
+ "get-nextTeamNumber": self.get_next_team_number,
+ "get-nextPegNumber": self.get_next_peg_number,
}
handler = task_handlers.get(task)
@@ -127,7 +135,7 @@ class ManageAnglersView(View):
team.save()
result["team"] = {"id": team.id, "number": team.number}
except IntegrityError:
- result["form_errors"]["#editTeamNumberError"] = "A Team with this number already exists"
+ result["form_errors"]["#teamNumber"] = "A Team with this number already exists"
return result
@@ -188,9 +196,13 @@ class ManageAnglersView(View):
angler.team = team
angler.section = section
- angler.save()
+ try:
+ angler.save()
+ except IntegrityError:
+ result["form_errors"]["#anglerPeg"] = "An Angler with this peg number already exists"
+
result["angler"] = {
- "id": id,
+ "id": angler.id,
"forename": forename,
"surname": surname,
"peg_number": peg_number,
@@ -205,7 +217,7 @@ class ManageAnglersView(View):
def get_teams(self, request) -> dict[str]:
"""Returns a dictionary of all teams."""
- search = request.GET.get("search")
+ search = request.POST.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.
@@ -222,7 +234,7 @@ class ManageAnglersView(View):
def get_sections(self, request) -> dict[str]:
"""Returns a dictionary of all sections."""
- search = request.GET.get("search")
+ search = request.POST.get("search")
sections = Section.objects.order_by("character").all()
if search:
@@ -238,8 +250,9 @@ class ManageAnglersView(View):
def get_anglers(self, request) -> dict[str]:
"""Returns a dictionary of all anglers."""
- search = request.GET.get("search")
+ search = request.POST.get("search")
anglers = Member.objects.order_by("first_name").all()
+ order_by = "peg_number" if request.POST.get("sortAnglers") == "pegs" else "first_name"
if search:
search_terms = search.split()
@@ -260,10 +273,65 @@ class ManageAnglersView(View):
"team_number": angler.team.number,
"section_character": angler.section.character
}
- for angler in anglers
+ for angler in anglers.order_by(order_by).all()
]
}
+ def delete_team(self, request) -> dict:
+ """Deletes a team."""
+
+ team_id = request.POST.get("team_id")
+ if not team_id:
+ raise ValueError("Invalid team ID")
+
+ teams = Team.objects.get(id=team_id)
+ teams.delete()
+
+ return {}
+
+ def delete_section(self, request) -> dict:
+ """Deletes a section."""
+
+ section_id = request.POST.get("section_id")
+ if not section_id:
+ raise ValueError("Invalid section ID")
+
+ sections = Section.objects.get(id=section_id)
+ sections.delete()
+
+ return {}
+
+ def delete_angler(self, request) -> dict:
+ """Delete an angler."""
+
+ angler_id = request.POST.get("angler_id")
+ if not angler_id:
+ raise ValueError("Invalid angler ID")
+
+ angler = Member.objects.get(id=angler_id)
+ angler.delete()
+
+ return {}
+
+ def get_next_team_number(self, request) -> dict[str, int]:
+ """Returns the next available team number."""
+
+ next_team_number = 1
+
+ while Team.objects.filter(number=next_team_number).exists():
+ next_team_number += 1
+
+ return {"nextTeamNumber": next_team_number}
+
+ def get_next_peg_number(self, request) -> dict[str, int]:
+ """Returns the next available peg number."""
+
+ next_peg_number = 1
+
+ while Member.objects.filter(peg_number=next_peg_number).exists():
+ next_peg_number += 1
+
+ return {"nextPegNumber": next_peg_number}
@@ -412,8 +480,8 @@ def update_team(request):
if not request.POST:
return
- team_id = request.POST.get("teamId")
- team_number = request.POST.get("teamNumber")
+ team_id = request.POST.get("id")
+ team_number = request.POST.get("number")
try:
if team_id == "-1":
diff --git a/src/static/css/custom.css b/src/static/css/custom.css
index 840d149..e046bdb 100644
--- a/src/static/css/custom.css
+++ b/src/static/css/custom.css
@@ -58,6 +58,10 @@
font-family: "Source Sans Pro";
}
+.font-helvetica {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
.pencil-btn {
width: 32px;
height: 32px;
@@ -118,4 +122,154 @@ input[type='radio']:checked {
background-color: #04385c;
border: 1px solid #04385c;
box-shadow: none;
-}
\ No newline at end of file
+}
+
+#sidebar {
+ width: 80px;
+}
+
+#webContent {
+ margin-left: 80px;
+}
+
+.light-tooltip {
+ --bs-tooltip-bg: #04385c;
+}
+
+#sidebar .nav-item > a:hover {
+ background-color: rgba(0, 0, 0, 0.15);
+}
+
+html, body {
+ height: 100%;
+}
+
+#waterContainer {
+ background-color: rgba(2, 40, 110, 0.70);
+ max-height: calc(100vh - 70px);
+ overflow: hidden;
+}
+
+:root {
+ --drip-time: 4s;
+ --drip-length: 600px;
+}
+
+#drop {
+ position: relative;
+ width: 20px;
+ height: 20px;
+ top: -30px;
+ margin: 0 auto;
+ background: #FFF;
+ -moz-border-radius: 20px;
+ -webkit-border-radius: 20px;
+ border-radius: 20px;
+ -moz-animation-name: drip;
+ -webkit-animation-name: drip;
+ animation-name: drip;
+ -moz-animation-timing-function: cubic-bezier(1,0,.91,.19);
+ -webkit-animation-timing-function: cubic-bezier(1,0,.91,.19);
+ animation-timing-function: cubic-bezier(1,0,.91,.19);
+ -moz-animation-duration: var(--drip-time);
+ -webkit-animation-duration: var(--drip-time);
+ animation-duration: var(--drip-time);
+ -moz-animation-iteration-count: infinite;
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+ margin: 0 auto;
+}
+
+#drop:before {
+ content: "";
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-bottom: 30px solid rgba(255,255,255,1);
+ top: -22px;
+}
+
+#wave {
+ position: relative;
+ opacity: 0;
+ top: 0;
+ width: 2px;
+ height: 1px;
+ border: #FFF 7px solid;
+ -moz-border-radius: 300px / 150px;
+ -webkit-border-radius: 300px / 150px;
+ border-radius: 300px / 150px;
+ -moz-animation-name: ripple;
+ -webkit-animation-name: ripple;
+ animation-name: ripple;
+ -moz-animation-delay: var(--drip-time);
+ -webkit-animation-delay: var(--drip-time);
+ animation-delay: var(--drip-time);
+ -moz-animation-duration: var(--drip-time);
+ -webkit-animation-duration: var(--drip-time);
+ animation-duration: var(--drip-time);
+ -moz-animation-iteration-count: infinite;
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+ margin: var(--drip-length) auto 0 auto;
+}
+
+#wave:after {
+ content: "";
+ position: absolute;
+ opacity: 0;
+ top: -5px;
+ left: -5px;
+ width: 2px;
+ height: 1px;
+ border: #FFF 5px solid;
+ -moz-border-radius: 300px / 150px;
+ -webkit-border-radius: 300px / 150px;
+ border-radius: 300px / 150px;
+ -moz-animation-name: ripple-2;
+ -webkit-animation-name: ripple-2;
+ animation-name: ripple-2;
+ -moz-animation-duration: var(--drip-time);
+ -webkit-animation-duration: var(--drip-time);
+ animation-duration: var(--drip-time);
+ -moz-animation-iteration-count: infinite;
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+}
+
+@keyframes ripple {
+ from {
+ opacity: 1;
+ }
+ to {
+ width: 600px;
+ height: 300px;
+ border-width: 1px;
+ top: -100px;
+ opacity: 0;
+ }
+}
+
+@keyframes ripple-2 {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ width: 200px;
+ height: 100px;
+ border-width: 1px;
+ top: 100px;
+ left: 200px;
+ }
+}
+
+@keyframes drip {
+ to {
+ top: var(--drip-length);
+ }
+}
diff --git a/src/static/css/index.css b/src/static/css/index.css
index 7691a02..49b6c03 100644
--- a/src/static/css/index.css
+++ b/src/static/css/index.css
@@ -53,7 +53,7 @@
-webkit-overflow-scrolling: touch;
}
-.card{
+/* .card{
border-radius: 4px;
background: #fff;
box-shadow: 0 6px 10px rgba(0,0,0,.08), 0 0 6px rgba(0,0,0,.05);
@@ -64,4 +64,4 @@
.card:hover{
transform: scale(1.05);
box-shadow: 0 10px 20px rgba(0,0,0,.12), 0 4px 8px rgba(0,0,0,.06);
-}
\ No newline at end of file
+} */
\ No newline at end of file
diff --git a/src/static/img/at-logo.png b/src/static/img/at-logo.png
new file mode 100644
index 0000000..8e95044
Binary files /dev/null and b/src/static/img/at-logo.png differ
diff --git a/src/static/js/custom.js b/src/static/js/custom.js
index f77b435..8b8b6fd 100644
--- a/src/static/js/custom.js
+++ b/src/static/js/custom.js
@@ -8,7 +8,9 @@ $(document).ready(() => {
activateTooltips();
pageLoadMotionEffects();
- $("select").select2({theme: "bootstrap-5", minimumResultsForSearch: -1})
+ // $("select").select2({theme: "bootstrap-5", minimumResultsForSearch: -1})
+
+ $("#headerTitle").text(document.title);
});
@@ -58,3 +60,8 @@ jQuery.expr[':'].icontains = function(a, i, m) {
const findByKey = (array, key, value) => {
return array.find(item => item[key] === value);
}
+
+function isEmpty(obj) {
+ return Object.keys(obj).length === 0;
+}
+
\ No newline at end of file
diff --git a/src/static/js/mainapp/anglers.js b/src/static/js/mainapp/anglers.js
index 10ad73b..be3c450 100644
--- a/src/static/js/mainapp/anglers.js
+++ b/src/static/js/mainapp/anglers.js
@@ -27,6 +27,16 @@ $(document).ready(() => {
fetchAndLoadGroups();
});
+ const showGroupsValue = localStorage.getItem("showGroups");
+ if (showGroupsValue !== null) {
+ $("#sortForm input[name='showGroups']").val([showGroupsValue]);
+ }
+
+ const sortAnglersValue = localStorage.getItem("sortAnglers");
+ if (sortAnglersValue !== null) {
+ $("#sortForm input[name='sortAnglers']").val([sortAnglersValue]);
+ }
+
// Modals
$("#addAngler").on("click", () => {
@@ -51,7 +61,7 @@ function fetchAndLoadGroups() {
const showGroups = localStorage.getItem("showGroups");
const sortAnglers = localStorage.getItem("sortAnglers");
const task = showGroups === "teams" ? "get-teams" : "get-sections";
-
+ const search = $("#search").val()
$.ajax({
type: "POST",
url: pageUrl,
@@ -59,7 +69,7 @@ function fetchAndLoadGroups() {
csrfmiddlewaretoken: csrfMiddlewareToken,
tasks: [task, "get-anglers"],
sortAnglers: sortAnglers,
- search: $("#search").val()
+ search: isEmptyOrSpaces(search) ? "" : search,
},
error: (error) => {
console.error(error);
@@ -74,18 +84,20 @@ function fetchAndLoadGroups() {
function groupTemplate(group, groupType, oppositeGroupType) {
groupValue = groupType === "teams" ? group.number : group.character
+ const nonPluralGroupType = groupType.slice(0, -1);
+ const editFunction = groupType === "teams"? "addTeam" : "addSection";
return `
+ class='${nonPluralGroupType.toLowerCase()} px-4 py-3 bg-body-tertiary bg-gradient rounded h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover'
+ data-id='${group.id}' data-value='${groupValue}'>
- ${groupType.toUpperCase()}
+ ${nonPluralGroupType.toUpperCase()}
${groupValue}
-
+
@@ -104,7 +116,7 @@ function anglerTemplate(angler, oppositeGroupType) {
oppositeGroup = oppositeGroup.charAt(0).toUpperCase() + oppositeGroup.slice(1);
return `
-
+
-
+
@@ -140,10 +152,15 @@ function loadGroups(data) {
$("#groups").html("");
+ if (!groups.length) {
+ $("#notFound").show();
+ return;
+ }
+
groups.forEach((group) => {
$("#groups").append(groupTemplate(group, groupType, oppositeGroupType));
data.anglers.forEach((angler) => {
- if (angler.team_id === group.id || angler.section_id === group.id) {
+ if ((groupType == "teams" && angler.team_id === group.id) || (groupType == "sections" && angler.section_id === group.id)) {
$("#groups").find(".anglers").last().append(
anglerTemplate(angler, oppositeGroupType)
);
@@ -152,6 +169,13 @@ function loadGroups(data) {
});
activateTooltips();
+
+ if (!isEmptyOrSpaces($("#search").val())) {
+ $("#groups").find('.angler-fullname').each(function() {
+ var regex = new RegExp($("#search").val(), 'gi');
+ $(this).html($(this).text().replace(regex, '
$& '));
+ });
+ }
}
/**
@@ -177,7 +201,182 @@ function toggleLoading(loading) {
* @param {Number} anglerId - ID of the angler, if -1 will create a new angler
*/
function addAngler(anglerId) {
+
+ // Reset the form values
+ $("#anglerTitle").text("Add New Angler");
+ $("#anglerForename").val("");
+ $("#anglerSurname").val("");
+ $("#anglerTeam").empty().trigger("change");
+ $("#anglerSection").empty().trigger("change");
+ $("#anglerPeg").val(1);
+ $("#anglerForm .error").text("").hide();
+ $("#anglerDelete").hide();
+
+ function validateSection() {
+ const sectionId = $("#anglerSection").val();
+
+ var valid = true;
+
+ // Ensure that no teammates are in this section
+ const teamId = $("#anglerTeam").val();
+ $(`.team[data-id='${teamId}'] .angler`).each(function() {
+ if ($(this).data("section-id") == sectionId && $(this).data("id") != anglerId) {
+ valid = false;
+ return false;
+ }
+ else {
+ $("#anglerSectionError").text("").hide();
+ }
+ });
+
+ return valid;
+ }
+
+ $("#anglerSection").change(() => {validateSection()});
+
+ $.ajax({
+ type: "POST",
+ url: pageUrl,
+ data: {
+ csrfmiddlewaretoken: csrfMiddlewareToken,
+ tasks: ["get-teams", "get-sections"]
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ success: (data) => {
+ data.teams.forEach((team) => {
+ $("#anglerTeam").append(`
Team ${team.number} `);
+ });
+ data.sections.forEach((section) => {
+ $("#anglerSection").append(`
Section ${section.character} `);
+ });
+
+ if (anglerId !== -1) {
+ const angler = $(`.angler[data-id='${anglerId}']`)
+ const forename = angler.data("first");
+ const surname = angler.data("last");
+ const peg = angler.data("peg-number");
+ const teamId = angler.data("team-id");
+ const sectionId = angler.data("section-id");
+
+ $("#anglerTitle").text(`Edit ${forename} ${surname}`);
+ $("#anglerForename").val(forename);
+ $("#anglerSurname").val(surname);
+ $("#anglerTeam").val(teamId).trigger("change");
+ $("#anglerSection").val(sectionId).trigger("change");
+ $("#anglerPeg").val(peg);
+ $("#anglerDelete").show();
+ }
+ }
+ });
+
$("#anglerModal").modal("show");
+
+ $("#anglerForm").off("submit").on("submit", (e) => {
+ e.preventDefault();
+
+ $("#anglerForm .error").text("").hide();
+
+ const forename = $("#anglerForename").val();
+ const surname = $("#anglerSurname").val();
+ const teamId = $("#anglerTeam").val();
+ const sectionId = $("#anglerSection").val();
+ const peg = $("#anglerPeg").val();
+ const validSection = validateSection();
+1
+ var formErrors = !(forename && surname && teamId && sectionId && peg && validSection);
+
+ // This form validation is not very scalable
+ if (!forename) {
+ $("#anglerForenameError").text("Please enter a forename").show();
+ }
+ if (!surname) {
+ $("#anglerSurnameError").text("Please enter a surname").show();
+ }
+ if (!teamId) {
+ $("#anglerTeamError").text("Please select a team").show();
+ }
+ if (!sectionId) {
+ $("#anglerSectionError").text("Please select a section").show();
+ }
+ if (!peg) {
+ $("#anglerPegError").text("Please enter a peg number").show();
+ }
+ if (!validSection) {
+ $("#anglerSectionError").text("Anglers cannot share a section with a teammate").show();
+ }
+ if (formErrors) {
+ return;
+ }
+
+ $.ajax({
+ type: "POST",
+ url: pageUrl,
+ data: {
+ csrfmiddlewaretoken: csrfMiddlewareToken,
+ tasks: ["update-angler"],
+ angler_id: anglerId,
+ forename: forename,
+ surname: surname,
+ team_id: teamId,
+ section_id: sectionId,
+ peg_number: peg
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ success: (data) => {
+ if (isEmpty(data.form_errors)) {
+ fetchAndLoadGroups();
+ $("#anglerModal").modal("hide");
+ return;
+ }
+
+ for(var inputId in data.form_errors) {
+ var errorMessage = data.form_errors[inputId];
+ $(`${inputId}Error`).text(errorMessage).show();
+ }
+ }
+ });
+ });
+
+ $("#anglerDelete").off("click").on("click", () => {
+ $.ajax({
+ type: "POST",
+ url: pageUrl,
+ data: {
+ csrfmiddlewaretoken: csrfMiddlewareToken,
+ tasks: ["delete-angler"],
+ angler_id: anglerId
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ success: (data) => {
+ fetchAndLoadGroups();
+ $("#anglerModal").modal("hide");
+ alert("Angler Deleted");
+ }
+ });
+ });
+
+ $("#anglerPegNext").off("click").on("click", () => {
+ $.ajax({
+ type: "POST",
+ url: pageUrl,
+ data: {
+ csrfmiddlewaretoken: csrfMiddlewareToken,
+ tasks: ["get-nextPegNumber"]
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ success: (data) => {
+ $("#anglerPeg").val(data.nextPegNumber);
+ }
+ });
+ });
}
/**
@@ -186,7 +385,96 @@ function addAngler(anglerId) {
* @param {Number} teamId - ID of the team, if -1 will create a new team
*/
function addTeam(teamId) {
+
+ // Reset the form values
+ $("#teamNumberError").text("").hide();
+
+ if (teamId !== -1) {
+ const team = $(`.team[data-id='${teamId}']`)
+ const number = team.data("value");
+
+ $("#teamTitle").text(`Edit Team #${number}`);
+ $("#teamNumber").val(number);
+ $("#teamDelete").show();
+ }
+ else {
+ $("#teamTitle").text("Add New Team");
+ $("#teamNumber").val(1);
+ $("#teamDelete").hide();
+ }
+
$("#teamModal").modal("show");
+
+ $("#teamForm").off("submit").on("submit", (e) => {
+ e.preventDefault();
+
+ const number = $("#teamNumber").val();
+ if (!number) {
+ $("#teamNumberError").text("Please enter a team number").show();
+ return;
+ }
+
+ $.ajax({
+ type: "POST",
+ url: pageUrl,
+ data: {
+ csrfmiddlewaretoken: csrfMiddlewareToken,
+ tasks: ["update-team"],
+ id: teamId,
+ number: number
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ success: (data) => {
+ if (isEmpty(data.form_errors)) {
+ fetchAndLoadGroups();
+ $("#teamModal").modal("hide");
+ return;
+ }
+ for(var inputId in data.form_errors) {
+ var errorMessage = data.form_errors[inputId];
+ $(`${inputId}Error`).text(errorMessage).show();
+ }
+ }
+ });
+ });
+
+ $("#teamDelete").off("click").on("click", () => {
+ $.ajax({
+ type: "POST",
+ url: pageUrl,
+ data: {
+ csrfmiddlewaretoken: csrfMiddlewareToken,
+ tasks: ["delete-team"],
+ team_id: teamId
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ success: (data) => {
+ fetchAndLoadGroups();
+ $("#teamModal").modal("hide");
+ }
+ });
+ });
+
+ $("#teamNumberNext").off("click").on("click", () => {
+ $.ajax({
+ type: "POST",
+ url: pageUrl,
+ data: {
+ csrfmiddlewaretoken: csrfMiddlewareToken,
+ tasks: ["get-nextTeamNumber"]
+ },
+ error: (error) => {
+ console.error(error);
+ },
+ success: (data) => {
+ $("#teamNumber").val(data.nextTeamNumber);
+ }
+ });
+ });
}
/**
diff --git a/src/templates/base.html b/src/templates/base.html
index 94a1532..1cb3765 100644
--- a/src/templates/base.html
+++ b/src/templates/base.html
@@ -3,7 +3,7 @@
-
{% block title %}{% endblock title %}AT Results
+
{% block title %}{% endblock title %}Angling Trust Results
{% load static %}
@@ -14,9 +14,60 @@
{% block style %}
{% endblock style %}
-
- {% block content %}
- {% endblock content %}
+
+
+
+
+
+
+
+
+ {% block header_buttons %}
+ {% endblock header_buttons %}
+
+
+
+ {% block content %}
+ {% endblock content %}
+
+
+
+
+