diff --git a/src/mainapp/admin.py b/src/mainapp/admin.py index 3ab2193..7a9093b 100644 --- a/src/mainapp/admin.py +++ b/src/mainapp/admin.py @@ -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) diff --git a/src/mainapp/management/__init__.py b/src/mainapp/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mainapp/management/commands/__init__.py b/src/mainapp/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mainapp/management/commands/create_members_fixture.py b/src/mainapp/management/commands/create_members_fixture.py new file mode 100644 index 0000000..351cdf1 --- /dev/null +++ b/src/mainapp/management/commands/create_members_fixture.py @@ -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.")) diff --git a/src/mainapp/management/commands/create_teams_fixture.py b/src/mainapp/management/commands/create_teams_fixture.py new file mode 100644 index 0000000..1211396 --- /dev/null +++ b/src/mainapp/management/commands/create_teams_fixture.py @@ -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")) diff --git a/src/mainapp/migrations/0001_initial.py b/src/mainapp/migrations/0001_initial.py index dd0dc47..e2e00e4 100644 --- a/src/mainapp/migrations/0001_initial.py +++ b/src/mainapp/migrations/0001_initial.py @@ -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])), ], ), ] diff --git a/src/mainapp/migrations/0002_member_peg_alter_member_team.py b/src/mainapp/migrations/0002_member_peg_alter_member_team.py deleted file mode 100644 index 2f276d6..0000000 --- a/src/mainapp/migrations/0002_member_peg_alter_member_team.py +++ /dev/null @@ -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]), - ), - ] diff --git a/src/mainapp/models.py b/src/mainapp/models.py index 3fabee1..7ae25cb 100644 --- a/src/mainapp/models.py +++ b/src/mainapp/models.py @@ -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""" diff --git a/src/mainapp/templates/teams.html b/src/mainapp/templates/teams.html index d498aec..eba6989 100644 --- a/src/mainapp/templates/teams.html +++ b/src/mainapp/templates/teams.html @@ -62,7 +62,7 @@ @@ -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 = $( - `
-
-

${team.identifier}

+ $("#teamsContainer").append( + `
+
+

${team.team_number}

` ); - 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( `
  • - ${member.name} + data-first='${member.first}' + data-last='${member.last}' + data-member-id='${member.id}' + data-team-number='${member.team}' + > + ${fullname}
  • ` - ) + ); }); }); + + // 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($(``)); + }); + + // 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"); + } + }); } {% endblock scripts %} \ No newline at end of file diff --git a/src/mainapp/urls.py b/src/mainapp/urls.py index 9be72d3..5cef79f 100644 --- a/src/mainapp/urls.py +++ b/src/mainapp/urls.py @@ -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') ] \ No newline at end of file diff --git a/src/mainapp/views.py b/src/mainapp/views.py index a949dd7..04fe6a3 100644 --- a/src/mainapp/views.py +++ b/src/mainapp/views.py @@ -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)