General rewrite of sections + UI changes
General rewrite of sections: sections are rewritten to be their own model UI Changes: updated teams page for sections changes, also rewrote home page.
This commit is contained in:
parent
e2ad5676c7
commit
8dfe4eb9f2
File diff suppressed because it is too large
Load Diff
3502
src/mainapp/fixtures/sections_fixture.json
Normal file
3502
src/mainapp/fixtures/sections_fixture.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,71 +2,151 @@
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"section_letter": "M"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"section_letter": "K"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"section_letter": "Q"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"section_letter": "V"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"section_letter": "U"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"section_letter": "H"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"section_letter": "A"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"section_letter": "D"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"section_letter": "P"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"section_letter": "G"
|
||||
}
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 11,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 12,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 13,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 14,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 15,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 16,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 17,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 18,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 19,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 20,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 21,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 22,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 23,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 24,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 25,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 26,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 27,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 28,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 29,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 30,
|
||||
"fields": {}
|
||||
}
|
||||
]
|
@ -1,84 +1,117 @@
|
||||
import names
|
||||
import json
|
||||
import random
|
||||
from typing import Optional, List, Set
|
||||
from string import ascii_uppercase
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q, Count
|
||||
|
||||
from mainapp.models import Member, Team, SectionValidator
|
||||
from mainapp.models import Member, Team, SectionValidator, Section
|
||||
|
||||
|
||||
# TODO: refactor this file like create_teams_fixtures.py
|
||||
|
||||
|
||||
# SECTION_LETTERS = ascii_uppercase
|
||||
|
||||
|
||||
# def get_next_available_section(member: Member, section_validator: SectionValidator) -> Optional[str]:
|
||||
# """Returns the next available section for a member."""
|
||||
# member_sections = member.sections.all().values_list('name', flat=True)
|
||||
# used_sections = set(member_sections)
|
||||
|
||||
# if not member_sections:
|
||||
# return "A"
|
||||
|
||||
# used_sections = sorted(used_sections)
|
||||
|
||||
# last_section = member_sections[-1]
|
||||
# last_section_index = SECTION_LETTERS.index(last_section)
|
||||
|
||||
# for i in range(last_section_index + 1, len(SECTION_LETTERS)):
|
||||
# section = SECTION_LETTERS[i]
|
||||
# if section_validator.is_valid_section(section) and section not in used_sections:
|
||||
# return section
|
||||
|
||||
# for i in range(last_section_index - 1, -1, -1):
|
||||
# section = SECTION_LETTERS[i]
|
||||
# if section_validator.is_valid_section(section) and section not in used_sections:
|
||||
# return section
|
||||
|
||||
# return None
|
||||
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a fixture with randomly generated Member objects"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("num_members", type=int)
|
||||
parser.add_argument("members_per_team", type=int)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
num_members = options["num_members"]
|
||||
members_per_team = options["members_per_team"]
|
||||
|
||||
teams = Team.objects.all()
|
||||
|
||||
if not teams:
|
||||
print("No teams found. Please create some teams first.")
|
||||
return
|
||||
raise Exception("no teams")
|
||||
|
||||
members = []
|
||||
new_members = []
|
||||
for team in teams:
|
||||
for _ in range(num_members):
|
||||
first_name = names.get_first_name()
|
||||
last_name = names.get_last_name()
|
||||
member = Member.objects.create(first_name=first_name, last_name=last_name, team=team)
|
||||
members.append(member)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Created {num_members} members."))
|
||||
i = members_per_team
|
||||
while i > 0:
|
||||
|
||||
# create a members fixture file
|
||||
members_fixture = []
|
||||
for member in members:
|
||||
member_fixture = {
|
||||
team_member_ids = team.members.values_list('id', flat=True)
|
||||
|
||||
print(team_member_ids)
|
||||
|
||||
section = random.choice([
|
||||
sec for sec in
|
||||
Section.objects.exclude(members__id__in=team_member_ids)
|
||||
])
|
||||
|
||||
member = Member.objects.create(
|
||||
first_name=names.get_first_name(),
|
||||
last_name=names.get_last_name(),
|
||||
section=section,
|
||||
team=team
|
||||
)
|
||||
|
||||
if not section.is_joinable(member):
|
||||
member.delete()
|
||||
i += 1
|
||||
continue
|
||||
|
||||
new_members.append(member)
|
||||
print(member)
|
||||
|
||||
i -= 1
|
||||
|
||||
fixtures = []
|
||||
for member in new_members:
|
||||
member.delete()
|
||||
|
||||
fixture = {
|
||||
"model": "mainapp.member",
|
||||
"pk": member.pk,
|
||||
"fields": {
|
||||
"first_name": member.first_name,
|
||||
"last_name": member.last_name,
|
||||
"team": member.team_id,
|
||||
"peg_number": member.peg_number,
|
||||
"section": member.section_id,
|
||||
"peg_number": member.peg_number
|
||||
}
|
||||
}
|
||||
members_fixture.append(member_fixture)
|
||||
fixtures.append(fixture)
|
||||
|
||||
with open("src/mainapp/fixtures/members_fixture.json", "w") as f:
|
||||
f.write(json.dumps(members_fixture, indent=2))
|
||||
f.write(json.dumps(fixtures, indent=2))
|
||||
self.stdout.write(self.style.SUCCESS("Created members_fixture.json."))
|
||||
|
||||
# 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.objects.create(first_name=first_name, last_name=last_name, team=team)
|
||||
# members.append(member)
|
||||
|
||||
# self.stdout.write(self.style.SUCCESS(f"Created {num_members} members."))
|
||||
|
||||
# # 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,
|
||||
# "peg_number": member.peg_number,
|
||||
# }
|
||||
# }
|
||||
# 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."))
|
||||
|
79
src/mainapp/management/commands/create_sections_fixture.py
Normal file
79
src/mainapp/management/commands/create_sections_fixture.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Command to create test data fixture for sections."""
|
||||
|
||||
import json
|
||||
from string import ascii_uppercase
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from mainapp.models import Section, SectionValidator
|
||||
|
||||
|
||||
def name_sort_key(name):
|
||||
if len(name) == 1:
|
||||
return (0, name)
|
||||
else:
|
||||
return (1, name[:-1], name[-1])
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a fixture file for Team objects"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("amount_of_sections", type=int, help="Number of sections to create")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
amount_of_sections = options["amount_of_sections"]
|
||||
|
||||
used_names = set()
|
||||
last_name = ""
|
||||
|
||||
for i in range(amount_of_sections):
|
||||
add_character = last_name.endswith("Z")
|
||||
if add_character:
|
||||
last_name = ""
|
||||
|
||||
new_name = last_name if add_character else ""
|
||||
if not new_name.endswith("Z"):
|
||||
new_name = new_name[:-1] if len(new_name) > 1 else new_name
|
||||
|
||||
try:
|
||||
new_name += ascii_uppercase[i]
|
||||
except IndexError:
|
||||
quotient, remainder = divmod(i, 26)
|
||||
new_name += ascii_uppercase[quotient - 1] + ascii_uppercase[remainder]
|
||||
|
||||
while new_name in used_names:
|
||||
if new_name[-1] == "Z":
|
||||
idx = len(new_name) - 2
|
||||
while idx >= 0 and new_name[idx] == "Z":
|
||||
idx -= 1
|
||||
if idx < 0:
|
||||
new_name = "A" * (len(new_name) + 1)
|
||||
else:
|
||||
new_name = new_name[:idx] + chr(ord(new_name[idx]) + 1) + "A" * (len(new_name) - idx - 1)
|
||||
else:
|
||||
new_name = new_name[:-1] + chr(ord(new_name[-1]) + 1)
|
||||
|
||||
used_names.add(new_name)
|
||||
last_name = new_name
|
||||
|
||||
names = sorted([name for name in used_names], key=name_sort_key)
|
||||
|
||||
print([name for name in names])
|
||||
sections = [Section(name=name) for name in names]
|
||||
Section.objects.bulk_create(sections)
|
||||
|
||||
fixture = [{
|
||||
"model": "mainapp.section",
|
||||
"pk": section.pk,
|
||||
"fields": {
|
||||
"name": section.name
|
||||
}
|
||||
} for section in sections]
|
||||
|
||||
for section in sections:
|
||||
section.delete()
|
||||
|
||||
with open("src/mainapp/fixtures/sections_fixture.json", "w") as file:
|
||||
file.write(json.dumps(fixture, indent=4))
|
||||
self.stdout.write(self.style.SUCCESS("Created sections_fixture.json."))
|
@ -1,12 +1,10 @@
|
||||
"""Command to create test data fixture for teams."""
|
||||
|
||||
import random
|
||||
import json
|
||||
from string import ascii_uppercase
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.utils import IntegrityError
|
||||
from mainapp.models import Team, BLOCKED_SECTION_LETTERS
|
||||
|
||||
from mainapp.models import Team
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -16,43 +14,15 @@ class Command(BaseCommand):
|
||||
parser.add_argument("amount_of_teams", type=int, help="Number of teams to create")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
existing_teams = Team.objects.all()
|
||||
|
||||
# Available sections
|
||||
available_sections = [
|
||||
char for char in [*ascii_uppercase]
|
||||
if char.lower() not in BLOCKED_SECTION_LETTERS
|
||||
and char not in [team.section_letter for team in existing_teams]
|
||||
]
|
||||
|
||||
if not available_sections:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
f"There are no available sections for new teams."
|
||||
))
|
||||
return
|
||||
|
||||
teams_amount = options["amount_of_teams"]
|
||||
max_teams = len(available_sections)
|
||||
|
||||
if teams_amount > max_teams:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
f"Number of teams is too large [{teams_amount}/{max_teams}]."
|
||||
))
|
||||
return
|
||||
|
||||
# Create the new teams (this will create them in the database)#
|
||||
new_teams = []
|
||||
for i in range(teams_amount):
|
||||
section_letter = random.choice(available_sections)
|
||||
available_sections.remove(section_letter)
|
||||
team = Team.objects.create(section_letter=section_letter)
|
||||
new_teams.append(team)
|
||||
new_teams = [Team.objects.create() for i in range(teams_amount)]
|
||||
|
||||
teams_fixture = [{
|
||||
"model": "mainapp.team",
|
||||
"pk": team.pk,
|
||||
"fields": {"section_letter": team.section_letter}
|
||||
"fields": {}
|
||||
} for team in new_teams]
|
||||
|
||||
# Remove the teams from the database
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Generated by Django 4.1.5 on 2023-05-09 22:52
|
||||
# Generated by Django 4.1.5 on 2023-05-10 08:30
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@ -17,13 +17,13 @@ class Migration(migrations.Migration):
|
||||
name='Section',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(editable=False, max_length=3, unique=True)),
|
||||
('name', models.CharField(max_length=3, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('team_number', mainapp.models.ReusableAutoField(default=mainapp.models.ReusableAutoField.get_default, editable=False, primary_key=True, serialize=False)),
|
||||
('name', mainapp.models.ReusableAutoField(default=mainapp.models.ReusableAutoField.get_default, editable=False, primary_key=True, serialize=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@ -34,7 +34,7 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='mainapp.team', validators=[mainapp.models.validate_team_size])),
|
||||
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='mainapp.team')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.5 on 2023-05-09 23:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='team',
|
||||
old_name='team_number',
|
||||
new_name='name',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='section',
|
||||
name='name',
|
||||
field=models.CharField(max_length=3, unique=True),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
from string import ascii_uppercase
|
||||
"""Models for the mainapp."""
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -7,7 +7,7 @@ from django.db import models
|
||||
|
||||
|
||||
class ReusableAutoField(models.PositiveIntegerField):
|
||||
"""A django auto field that can reuse deleted primary keys"""
|
||||
"""A django auto field that can reuse deleted primary keys."""
|
||||
|
||||
def get_next_available_id(self, model_cls, using=None):
|
||||
"""
|
||||
@ -31,15 +31,6 @@ class ReusableAutoField(models.PositiveIntegerField):
|
||||
return self.get_next_available_id(self.model)
|
||||
|
||||
|
||||
# class Peg(models.Model):
|
||||
# """Represents a person's peg"""
|
||||
|
||||
# peg_number = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
|
||||
|
||||
# def __str__(self):
|
||||
# return f"Peg {self.peg_number}"
|
||||
|
||||
|
||||
class SectionValidator:
|
||||
"""Validation class for the `section` field on the `member` model."""
|
||||
|
||||
@ -124,13 +115,54 @@ class Section(models.Model):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
def get_members(self):
|
||||
return Member.objects.filter(section=self)
|
||||
|
||||
@property
|
||||
def section_after(self):
|
||||
"""Returns the next section after this one, or none if not found."""
|
||||
sections = [sec for sec in Section.objects.all()]
|
||||
sections.sort(key=lambda sec: sec.name)
|
||||
try:
|
||||
return sections[sections.index(self) + 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def validate_team_size(value):
|
||||
if Member.objects.filter(team=value).count() >= 3:
|
||||
raise ValidationError('Team already has maximal amount of members (3)')
|
||||
@property
|
||||
def section_before(self):
|
||||
"""Returns the last section before this one, or none if not found."""
|
||||
sections = [sec for sec in Section.objects.all()]
|
||||
sections.sort(key=lambda sec: sec.name)
|
||||
try:
|
||||
return sections[sections.index(self) - 1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def is_joinable(self, member, /) -> bool:
|
||||
"""Returns boolean if a member is able to join this section."""
|
||||
|
||||
if not member.team:
|
||||
return True
|
||||
|
||||
teammates = [teammate for teammate in Member.objects.filter(team=member.team)]
|
||||
|
||||
section_after = self.section_after.get_members if self.section_after else []
|
||||
section_before = self.section_before.get_members if self.section_before else []
|
||||
|
||||
for teammate in teammates:
|
||||
if teammate in section_after:
|
||||
return False
|
||||
|
||||
if teammate in section_before:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return any(
|
||||
teammate in section_after or teammate in section_before
|
||||
for teammate in teammates
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
@ -138,9 +170,9 @@ class Member(models.Model):
|
||||
|
||||
first_name = models.CharField(max_length=255)
|
||||
last_name = models.CharField(max_length=255)
|
||||
team = models.ForeignKey("Team", on_delete=models.SET_NULL, null=True, blank=True, validators=(validate_team_size,), related_name='members')
|
||||
team = models.ForeignKey("Team", on_delete=models.SET_NULL, null=True, blank=True, related_name='members')
|
||||
peg_number = models.PositiveIntegerField(null=True, editable=True, unique=True)
|
||||
section = models.ForeignKey(to=Section, on_delete=models.SET_NULL, null=True, swappable=True)
|
||||
section = models.ForeignKey(to=Section, on_delete=models.SET_NULL, null=True, swappable=True, related_name='members')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -153,46 +185,22 @@ class Member(models.Model):
|
||||
if peg_numbers:
|
||||
self.peg_number = min (peg_numbers)
|
||||
|
||||
# def clean(self):
|
||||
# super().clean()
|
||||
|
||||
# validator = SectionValidator(max_value="ZZZ")
|
||||
# if not validator.is_valid(self.section):
|
||||
# raise ValidationError("Invalid section value.")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name} (team {self.team.team_number}))"
|
||||
return f"{self.first_name} {self.last_name} (team {self.team.name}) [section {self.section.name}]"
|
||||
|
||||
@property
|
||||
def fullname(self) -> str:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
|
||||
BLOCKED_SECTION_LETTERS = ["i"] # lowercase only, sections cannot be any of these letters#
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
"""Represents a team"""
|
||||
|
||||
name = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
|
||||
# section_letter = models.CharField(max_length=1, unique=True, choices=[
|
||||
# (char, char) for char in ascii_uppercase if char.lower() not in BLOCKED_SECTION_LETTERS
|
||||
# ])
|
||||
|
||||
def __str__(self):
|
||||
return f"Team {self.name}"
|
||||
|
||||
@property
|
||||
def members(self) -> list[Member]:
|
||||
def get_members(self) -> list[Member]:
|
||||
return Member.objects.filter(team=self)
|
||||
|
||||
|
||||
# def validate_section_character(value):
|
||||
# """Validates the section character"""
|
||||
|
||||
# value = value.upper()
|
||||
# banned_characters = ["I"]
|
||||
|
||||
# if value in banned_characters:
|
||||
# raise ValidationError(f"The character <{value}> is a prohibited character.")
|
||||
|
||||
|
@ -1,53 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="py-5 text-center container">
|
||||
<div class="row py-lg-5">
|
||||
<div class="col-lg-6 col-md-8 mx-auto">
|
||||
<h1 class="fw-light text-white">Results System</h1>
|
||||
<p class="lead text-white">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi faucibus elementum diam nec semper. Nullam vestibulum enim eu nisi condimentum, vitae suscipit risus imperdiet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="album py-5 bg-light">
|
||||
<div class="py-5 bg-body mt-5">
|
||||
<div class="container">
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="fw-light text-white mt-5 pb-5 mb-5 bg-body text-body d-inline-block">
|
||||
   Angling Trust Results   
|
||||
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-4 justify-contentpaper">
|
||||
|
||||
<!-- Teams & Members -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="{% url 'teams' %}">
|
||||
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Teams / Members</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% url 'members' %}" 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">
|
||||
Member Management
|
||||
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
|
||||
</h3>
|
||||
</div> -->
|
||||
<div class="fluid-hover-zoom d-flex flex-wrap justify-content-center shadow-sm md-shadow on-hover bg-body-tertiary rounded text-center p-5 h-100">
|
||||
<h3 class="h2 w-100">
|
||||
Member Management
|
||||
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
|
||||
</h3>
|
||||
<p class="mt-3">
|
||||
Create and manage all members, teams and sections of an event
|
||||
</p>
|
||||
<button class="btn btn-lg btn-company mt-3 px-4 ">
|
||||
Visit Member Management
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Scoreboard -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="{% url 'scoreboard' %}">
|
||||
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Scoreboard</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% url 'scoreboard' %}" 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-list-stars w-100 text-center" style="font-size: 5rem;"></i>
|
||||
<h3 class=" fs-3">
|
||||
   Scoreboard   
|
||||
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
|
||||
</h3>
|
||||
</div> -->
|
||||
<div class="fluid-hover-zoom d-flex flex-wrap justify-content-center shadow-sm md-shadow on-hover bg-body-tertiary rounded text-center p-5 h-100">
|
||||
<h3 class="h2 w-100">
|
||||
Live Scoreboard
|
||||
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
|
||||
</h3>
|
||||
<p class="mt-3">
|
||||
View a live scoreboard of the event, good for a dedicated scoreboard display
|
||||
</p>
|
||||
<button class="btn btn-lg btn-company mt-3 px-4 mt-auto">
|
||||
Visit Live Scoreboard
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="{% url 'results' %}">
|
||||
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Results</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% url 'results' %}" 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-journal-text w-100 text-center" style="font-size: 5rem;"></i>
|
||||
<h3 class=" fs-3">
|
||||
   Results   
|
||||
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
|
||||
</h3>
|
||||
</div> -->
|
||||
<div class="fluid-hover-zoom d-flex flex-wrap justify-content-center shadow-sm md-shadow on-hover bg-body-tertiary rounded text-center p-5 h-100">
|
||||
<h3 class="h2 w-100">
|
||||
Competition Results
|
||||
<div class="border-company border-bottom border-2 w-100 pt-1"></div>
|
||||
</h3>
|
||||
<p class="mt-3">
|
||||
An organised output of the competition outcome and member performance
|
||||
</p>
|
||||
<button class="btn btn-lg btn-company mt-3 px-4 mt-auto">
|
||||
Visit Competition Results
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -1,9 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Teams & Members |
|
||||
Member Management |
|
||||
{% 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 %}
|
||||
<div class="bg-body p-4">
|
||||
<a href="{% url 'index' %}" class="btn btn-company px-4">Back</a>
|
||||
@ -11,8 +16,11 @@
|
||||
|
||||
<div class="container my-4 p-4 pb-0 bg-body rounded">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12 col-xl-4">
|
||||
<h3 class="mb-3 mb-xl-0">Teams & Members</h3>
|
||||
<div class="col-12 col-xl-4 d-flex">
|
||||
<h3 class="mb-3 mb-xl-0">
|
||||
Member Management
|
||||
<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">
|
||||
@ -22,6 +30,9 @@
|
||||
<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">
|
||||
@ -84,12 +95,26 @@
|
||||
<div class="modal fade" id="editTeamModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title fs-5" id="editTeamTitle">Team Number Here</h3>
|
||||
<div class="modal-header border-0">
|
||||
<h3 class="modal-title fs-5" id="editTeamTitle">
|
||||
Team Number Here
|
||||
<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>
|
||||
<div class="modal-body">
|
||||
<form id="editTeamForm">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-tag"></i>
|
||||
</span>
|
||||
<div class="form-floating">
|
||||
<input type="number" class="form-control" name="editTeamName" id="editTeamName">
|
||||
<label for="editTeamName">Number</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <form id="editTeamForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<label for="editTeamNumber" class="form-label">Team Number</label>
|
||||
@ -100,11 +125,10 @@
|
||||
<input type="text" name="editTeamSection" class="form-control" id="editTeamSection">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form> -->
|
||||
</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" id="saveEditTeamModal">Save Changes</button>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-company" id="saveEditTeamModal">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -114,35 +138,71 @@
|
||||
<div class="modal fade" id="editMemberModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title fs-5" id="editMemberName">Member Name Here</h3>
|
||||
<div class="modal-header border-0">
|
||||
<h3 class="modal-title fs-5 color" id="editMemberName">
|
||||
Member Name Here
|
||||
<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>
|
||||
<div class="modal-body">
|
||||
<form id="editMemberForm">
|
||||
<form>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md">
|
||||
<div class="form-floating">
|
||||
<input class="form-control" type="text" name="editMemberForename" id="editMemberForename" placeholder="Forename" value="First Name">
|
||||
<label for="editMemberForename">Forename</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-floating">
|
||||
<input class="form-control" type="text" name="editMemberSurname" id="editMemberSurname" placeholder="Surname" value="Last Name">
|
||||
<label for="editMemberSurname">Surname</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-people"></i>
|
||||
</span>
|
||||
<select class="form-select" name="editMemberTeam" id="editMemberTeam">
|
||||
<option value="" class="font-ssp">Team 1</option>
|
||||
<option value="" class="font-ssp">Team 2</option>
|
||||
<option value="" class="font-ssp">Team 3</option>
|
||||
<option value="" class="font-ssp">Team 4</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-layers"></i>
|
||||
</span>
|
||||
<select class="form-select" name="editMemberSection" id="editMemberSection">
|
||||
<option value="">Section A</option>
|
||||
<option value="">Section B</option>
|
||||
<option value="">Section C</option>
|
||||
<option value="">Section D</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editMemberFirstName" class="form-label">Forename</label>
|
||||
<input type="text" name="editMemberFirstName" id="editMemberFirstName" class="form-control" required minlength="1" maxlength="30">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editMemberLastName" class="form-label">Surname</label>
|
||||
<input type="text" name="editMemberLastName" id="editMemberLastName" class="form-control" required minlength="1" maxlength="30">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="editMemberTeam" class="form-label">Team</label>
|
||||
<select name="editMemberTeam" id="editMemberTeam" class="form-select" required></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="editMemberPeg" class="form-label">Peg Number</label>
|
||||
<input type="number" name="editMemberPeg" id="editMemberPeg" class="form-control" min="1" max="9999">
|
||||
<div class="col-md-6 d-flex">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-tag"></i>
|
||||
</span>
|
||||
<span class="border border-end-0 d-flex align-items-center">
|
||||
<small class="text-muted mx-2" style="margin-right: 1px;">Peg</small>
|
||||
</span>
|
||||
<input type="number" name="editMemberPeg" id="editMemberPeg" class="form-control ps-0 border-start-0 shadow-none" 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>
|
||||
</form>
|
||||
</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" id="saveEditModal">Save Changes</button>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-company" id="saveEditModal">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -151,6 +211,9 @@
|
||||
|
||||
{% load static %}
|
||||
{% 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 getTeamsUrl = "{% url 'get-teams' %}";
|
||||
const updateMemberUrl = "{% url 'update-member' %}";
|
||||
|
@ -5,7 +5,7 @@ urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('results/', views.results, name='results'),
|
||||
path('scoreboard/', views.scoreboard, name='scoreboard'),
|
||||
path('teams/', views.teams, name='teams'),
|
||||
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'),
|
||||
|
@ -31,6 +31,12 @@ def teams(request):
|
||||
# # return to previous page
|
||||
# return redirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
def name_sort_key(section):
|
||||
if len(section.name) == 1:
|
||||
return (0, section.name)
|
||||
else:
|
||||
return (1, section.name[:-1], section.name[-1])
|
||||
|
||||
def get_teams(request):
|
||||
"""Returns a JsonResponse containing a dictionary with a k/v pair for a list of teams.
|
||||
|
||||
@ -64,6 +70,9 @@ def get_teams(request):
|
||||
)
|
||||
groups = groups.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:
|
||||
@ -86,8 +95,6 @@ def get_teams(request):
|
||||
response_data["sortGroups"] = sort_groups
|
||||
response_data["sortMembers"] = sort_members
|
||||
|
||||
print(sort_groups, sort_members)
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
def update_member(request):
|
||||
@ -105,7 +112,7 @@ def update_member(request):
|
||||
|
||||
# Get the member and team
|
||||
member = Member.objects.get(id=member_id)
|
||||
team = Team.objects.get(team_number=team_number)
|
||||
team = Team.objects.get(name=team_number)
|
||||
|
||||
# Update the member
|
||||
member.first_name = first
|
||||
|
@ -57,6 +57,8 @@ $(document).ready(() => {
|
||||
}
|
||||
});
|
||||
|
||||
$("select").select2({theme: "bootstrap-5", minimumResultsForSearch: -1})
|
||||
|
||||
// load the teams with default filters
|
||||
fetchAndLoadTeams(...getFilters());
|
||||
|
||||
@ -213,58 +215,58 @@ function teamsLoading(show=true) {
|
||||
|
||||
function openEditTeamModal(teamNumber) {
|
||||
|
||||
// Team data
|
||||
const team = $(`.team[data-number='${teamNumber}']`);
|
||||
const section = team.data("section");
|
||||
// // Team data
|
||||
// const team = $(`.team[data-number='${teamNumber}']`);
|
||||
// const section = team.data("section");
|
||||
|
||||
// Load data to form
|
||||
$("#editTeamTitle").text(`Team ${teamNumber}`);
|
||||
// // Load data to form
|
||||
// $("#editTeamTitle").text(`Team ${teamNumber}`);
|
||||
|
||||
$("#editTeamNumber").val(teamNumber);
|
||||
$('#editTeamSection').val(section);
|
||||
// $("#editTeamNumber").val(teamNumber);
|
||||
// $('#editTeamSection').val(section);
|
||||
|
||||
$("#editTeamModal").modal("show");
|
||||
}
|
||||
|
||||
function openEditMemberModal(memberId) {
|
||||
|
||||
var modalTitle = "Add Member";
|
||||
var first = "";
|
||||
var last = "";
|
||||
var teamNumber = $(".team").map(function() { return $(this).data("number"); }).get(); // TODO: shorten this
|
||||
var pegNumber = 0;
|
||||
var firstPlaceholder = "Forename";
|
||||
var lastPlaceholder = "Surname";
|
||||
// var modalTitle = "Add Member";
|
||||
// var first = "";
|
||||
// var last = "";
|
||||
// var teamNumber = $(".team").map(function() { return $(this).data("number"); }).get(); // TODO: shorten this
|
||||
// var pegNumber = 0;
|
||||
// var firstPlaceholder = "Forename";
|
||||
// var lastPlaceholder = "Surname";
|
||||
|
||||
// Member data
|
||||
if (memberId !== -1) {
|
||||
const member = $(`.team-member[data-member-id='${memberId}']`);
|
||||
first = member.data("first");
|
||||
last = member.data("last");
|
||||
teamNumber = member.data("team-number");
|
||||
pegNumber = member.data("peg-number");
|
||||
firstPlaceholder = first;
|
||||
lastPlaceholder = last;
|
||||
modalTitle = "Edit: " + first + " " + last
|
||||
}
|
||||
// // Member data
|
||||
// if (memberId !== -1) {
|
||||
// const member = $(`.team-member[data-member-id='${memberId}']`);
|
||||
// first = member.data("first");
|
||||
// last = member.data("last");
|
||||
// teamNumber = member.data("team-number");
|
||||
// pegNumber = member.data("peg-number");
|
||||
// firstPlaceholder = first;
|
||||
// lastPlaceholder = last;
|
||||
// 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("");
|
||||
// $(".team").map(function() {
|
||||
// return $(this).data("number");
|
||||
// }).get().forEach((team) => {
|
||||
// $("#editMemberTeam").append($(`<option value='${team}'>Team ${team}</option>`));
|
||||
// });
|
||||
|
||||
// Load data to form
|
||||
$("#editMemberName").text(modalTitle);
|
||||
$("#editMemberFirstName").val(first);
|
||||
$("#editMemberLastName").val(last);
|
||||
$("#editMemberTeam").val(teamNumber);
|
||||
$("#editMemberPeg").val(pegNumber);
|
||||
// // Load data to form
|
||||
// $("#editMemberName").text(modalTitle);
|
||||
// $("#editMemberFirstName").val(first);
|
||||
// $("#editMemberLastName").val(last);
|
||||
// $("#editMemberTeam").val(teamNumber);
|
||||
// $("#editMemberPeg").val(pegNumber);
|
||||
|
||||
$("#editMemberFirstName").attr("placeholder", firstPlaceholder);
|
||||
$("#editMemberLastName").attr("placeholder", lastPlaceholder);
|
||||
// $("#editMemberFirstName").attr("placeholder", firstPlaceholder);
|
||||
// $("#editMemberLastName").attr("placeholder", lastPlaceholder);
|
||||
|
||||
$("#editMemberModal").modal("show");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user