Integrated models for teams & members page

This commit is contained in:
Corban-Lee 2023-05-06 18:57:45 +01:00
parent 8394de0dcb
commit 8afd3fa905
11 changed files with 254 additions and 83 deletions

View File

@ -19,9 +19,9 @@ class PegAdmin(admin.ModelAdmin):
class MemberAdmin(admin.ModelAdmin):
"""Admin model for the Member model."""
list_display = ("first_name", "last_name", "user", "team", "peg")
search_fields = ("first_name", "last_name", "user", "team", "peg")
list_filter = ("team", "peg")
list_display = ("first_name", "last_name", "team")
search_fields = ("first_name", "last_name", "team")
list_filter = ("team",)
@admin.register(Team)

View File

View File

@ -0,0 +1,68 @@
import random
import names
import json
from django.core.management.base import BaseCommand
from mainapp.models import Member, Team
class Command(BaseCommand):
help = "Creates a fixture with randomly generated Member objects"
def add_arguments(self, parser):
parser.add_argument("num_members", type=int)
def handle(self, *args, **options):
num_members = options["num_members"]
teams = Team.objects.all()
if not teams:
print("No teams found. Please create some teams first.")
return
members = []
for team in teams:
for _ in range(num_members):
first_name = names.get_first_name()
last_name = names.get_last_name()
member = Member(first_name=first_name, last_name=last_name, team=team)
members.append(member)
Member.objects.bulk_create(members)
self.stdout.write(self.style.SUCCESS(f"Created {num_members} members."))
# create a team fixture file
teams_fixture = []
for team in teams:
team_fixture = {
"model": "mainapp.team",
"pk": team.pk,
"fields": {
"team_number": team.team_number,
"section": team.section_id
}
}
teams_fixture.append(team_fixture)
with open("src/mainapp/fixtures/teams_fixture.json", "w") as f:
f.write(json.dumps(teams_fixture, indent=2))
self.stdout.write(self.style.SUCCESS("Created teams_fixture.json."))
# create a members fixture file
members_fixture = []
for member in members:
member_fixture = {
"model": "mainapp.member",
"pk": member.pk,
"fields": {
"first_name": member.first_name,
"last_name": member.last_name,
"team": member.team_id
}
}
members_fixture.append(member_fixture)
with open("src/mainapp/fixtures/members_fixture.json", "w") as f:
f.write(json.dumps(members_fixture, indent=2))
self.stdout.write(self.style.SUCCESS("Created members_fixture.json."))

View File

@ -0,0 +1,37 @@
from django.core.management.base import BaseCommand
from mainapp.models import Team
import random
class Command(BaseCommand):
help = "Creates a fixture file for Team objects"
def add_arguments(self, parser):
parser.add_argument("num_teams", type=int, help="Number of teams to create")
def handle(self, *args, **options):
num_teams = options["num_teams"]
teams = []
for i in range(num_teams):
team = Team.objects.create()
teams.append(team)
for team in teams:
self.stdout.write(self.style.SUCCESS(f"Created team {team.team_number}"))
filename = f"src/mainapp/fixtures/teams_{num_teams}.json"
with open(filename, "w") as f:
self.stdout.write(f"Writing fixture to {filename}")
f.write("[\n")
for i, team in enumerate(teams):
f.write(" {\n")
f.write(f' "model": "mainapp.team",\n')
f.write(f' "pk": {team.team_number},\n')
f.write(' "fields": {}\n')
f.write(" }")
if i < len(teams) - 1:
f.write(",")
f.write("\n")
f.write("]\n")
self.stdout.write(self.style.SUCCESS("Fixture created successfully"))

View File

@ -1,6 +1,5 @@
# Generated by Django 4.1.5 on 2023-01-26 14:24
# Generated by Django 4.1.5 on 2023-05-06 15:53
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import mainapp.models
@ -11,7 +10,6 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
@ -40,8 +38,7 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mainapp.team')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mainapp.team', validators=[mainapp.models.validate_team_size])),
],
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 4.1.5 on 2023-01-26 14:31
from django.db import migrations, models
import django.db.models.deletion
import mainapp.models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='member',
name='peg',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mainapp.peg'),
),
migrations.AlterField(
model_name='member',
name='team',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mainapp.team', validators=[mainapp.models.validate_team_size]),
),
]

View File

@ -49,13 +49,16 @@ class Member(models.Model):
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
user = models.OneToOneField("auth.User", on_delete=models.CASCADE)
team = models.ForeignKey("Team", on_delete=models.SET_NULL, null=True, blank=True, validators=(validate_team_size,))
peg = models.OneToOneField("Peg", on_delete=models.SET_NULL, null=True, blank=True)
team = models.ForeignKey("Team", on_delete=models.SET_NULL, null=True, blank=True, validators=(validate_team_size,), related_name='members')
# peg = models.OneToOneField("Peg", on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self):
return f"{self.first_name} {self.last_name} (team {self.team.team_number}))"
@property
def fullname(self) -> str:
return f"{self.first_name} {self.last_name}"
class Team(models.Model):
"""Represents a team"""
@ -66,6 +69,10 @@ class Team(models.Model):
def __str__(self):
return f"Team {self.team_number}"
@property
def members(self) -> list[Member]:
Member.objects.filter(team=self)
def validate_section_character(value):
"""Validates the section character"""

View File

@ -62,7 +62,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn btn-primary" id="saveEditModal">Save Changes</button>
</div>
</div>
</div>
@ -101,7 +101,6 @@
*
* **/
function isEmptyOrSpaces(string) {
console.log(string === null || string.match(/^ *$/) !== null);
return string === null || string.match(/^ *$/) !== null;
}
@ -109,43 +108,53 @@
$("#teamsContainer").html(""); // Clear the previous listed teams
if (teams.length < 1) {
$("#teamsNotFound").show()
$("#teamsNotFound").show();
}
// Iterate over and add each team
teams.forEach((team, index) => {
const teamColumn = $(
`<div class='col-12 col-md-6 col-xl-4 mb-4 team-column'>
<div class='p-4 bg-body-tertiary rounded h-100 fluid-hover-zoom shadow-sm shadow-on-hover'>
<h4>${team.identifier}</h4>
$("#teamsContainer").append(
`<div class='col-12 col-md-6 col-xl-4 mb-4'>
<div
class='team p-4 bg-body-tertiary rounded h-100 fluid-hover-zoom shadow-sm shadow-on-hover'
data-number='${team.team_number}'
>
<h4>${team.team_number}</h4>
<ul class='list-unstyled ul-cols-2 team-members'>
</ul>
</div>
</div>`
);
teamColumn.css("opacity", 1);
$("#teamsContainer").append(teamColumn);
// While we have the team, iterate over and add it's members
// Whilimage.pnge we have the team, iterate over and add it's members
team.members.forEach((member) => {
const fullname = member.first + " " + member.last;
$("#teamsContainer").find(".team-members").last().append(
`<li>
<span
type='button'
class='team-member'
onclick='javascript:openEditModal("${member.name}");'
>
${member.name}
data-first='${member.first}'
data-last='${member.last}'
data-member-id='${member.id}'
data-team-number='${member.team}'
>
${fullname}
</span>
</li>`
)
);
});
});
// Highlight all instances where the text matches the search critera
if (!isEmptyOrSpaces(highlightText)) {
$("#teamsContainer").find(`.team-member:icontains('${highlightText}')`).addClass("border-bottom text-primary fw-bold");
}
$(".team-member").on("click", function() {
openEditModal($(this).data("member-id"));
});
}
function fetchAndLoadTeams(search="") {
@ -158,7 +167,7 @@
},
error: (xhr, textStatus, errorThrown) => { alert(errorThrown); },
success: (result) => {
teamsLoading(false)
teamsLoading(false);
loadTeams(result.teams, search);
}
});
@ -177,17 +186,62 @@
$("#teamsLoadingSpinner").hide();
}
function openEditModal(name) {
function openEditModal(memberId) {
var [first, last] = name.split(" ");
var teamIdentifier = $(`.team-member:contains('${name}')`).parent().parent().parent().find("h4").text();
// Member data
const member = $(`.team-member[data-member-id='${memberId}']`);
const first = member.data("first");
const last = member.data("last");
const teamNumber = member.data("team-number");
$("#editMemberName").text(name);
// Load teams as options
$("#editMemberTeam").html("");
$(".team").map(function() {
return $(this).data("number");
}).get().forEach((team) => {
$("#editMemberTeam").append($(`<option value='${team}'>${team}</option>`));
});
// Load data to form
$("#editMemberName").text(first + " " + last);
$("#editMemberFirstName").val(first);
$("#editMemberLastName").val(last);
$("#editMemberTeam").val(teamIdentifier)
$("#editMemberTeam").val(teamNumber);
$("#editMemberModal").modal("show");
// Update the submit button
$("#saveEditModal").off("click").on("click", () => { saveEditModal(memberId); });
}
function saveEditModal(memberId) {
// Grab the updated data
const first = $("#editMemberFirstName").val();
const last = $("#editMemberLastName").val();
const teamNumber = $("#editMemberTeam").val();
const search = $("#search").val();
teamsLoading(true);
$.ajax({
url: "{% url 'update-member' %}",
type: "post",
data: {
"csrfmiddlewaretoken": "{{ csrf_token }}",
"memberId": memberId,
"first": first,
"last": last,
"teamNumber": teamNumber,
"search": !isEmptyOrSpaces(search) ? search : null
},
error: (xhr, textStatus, errorThrown) => { alert(errorThrown); },
success: (result) => {
teamsLoading(false);
loadTeams(result.teams, search);
$("#editMemberModal").modal("hide");
}
});
}
</script>
{% endblock scripts %}

View File

@ -9,4 +9,5 @@ urlpatterns = [
path('bulk-peg/', views.bulk_create_pegs, name='bulk-peg'),
path('get-teams/', views.get_teams, name='get-teams'),
path('update-member/', views.update_member, name='update-member')
]

View File

@ -1,11 +1,13 @@
"""Views for the main app."""
from string import ascii_lowercase
from functools import reduce
from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.db.models import Q
from .models import Peg, Team, Member
from .models import Peg
def index(request):
return render(request, 'index.html')
@ -30,39 +32,69 @@ def bulk_create_pegs(request):
return redirect(request.META.get('HTTP_REFERER'))
def get_teams(request):
"""Returns list of teams."""
"""Returns a JsonResponse containing a dictionary with a k/v pair for a list of teams.
Args:
request: the web request object.
Returns:
JsonResponse: dictionary of teams like so {'teams': [{}, {}, {}]}.
"""
if not request.POST:
return
search = request.POST.get("search")
teams = Team.objects.order_by("team_number").all()
from .teams_demo_test import created_teams
# Filter out teams that don't contain members being searched for
if search:
unfiltered_teams = created_teams.copy()
created_teams = [
team for team in unfiltered_teams
if any(
search.lower() in name for name in
[member["name"].lower() for member in team["members"]]
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)
created_teams.sort(reverse=False, key=lambda team: team["identifier"])
return JsonResponse({"teams": created_teams})
# Create a dictionary for the data that is JSON safe
response_data = {"teams": []}
for team in teams:
team_data = {
"team_number": team.team_number,
"section": team.section.name if team.section else None,
"members": [
{"first": member.first_name, "last": member.last_name, "id": member.id, "team": team.team_number}
for member in team.members.all()
]
}
response_data["teams"].append(team_data)
# if not search:
# created_teams.sort(reverse=False, key=lambda team: team["identifier"])
# return JsonResponse({"teams": created_teams})
return JsonResponse(response_data)
# # Create a new list only containing teams that meet the search criteria
# filtered_teams = []
# for team in created_teams:
# member_names = [member["name"].lower() for member in team["members"]]
# if any(search.lower() in name for name in member_names):
# filtered_teams.append(team)
def update_member(request):
"""Update a member. Returns a JsonResponse with the updated teams."""
# filtered_teams.sort(reverse=False, key=lambda team: team["identifier"])
if not request.POST:
return
# return JsonResponse({"teams": filtered_teams})
# Get the updated values
member_id = request.POST.get("memberId")
first = request.POST.get("first")
last = request.POST.get("last")
team_number = request.POST.get("teamNumber")
# Get the member and team
member = Member.objects.get(id=member_id)
team = Team.objects.get(team_number=team_number)
# Update the member
member.first_name = first
member.last_name = last
member.team = team
member.save()
return get_teams(request)