Merge branch 'jones-dev' into james-dev
169
.gitignore
vendored
@ -1,8 +1,163 @@
|
||||
/venv
|
||||
venv
|
||||
# Compressed / cached static files
|
||||
src/static/CACHE/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
pyvenv.cfg
|
||||
*.pyc
|
||||
venv/pyvenv.cfg
|
||||
src/db.sqlite3
|
||||
*.sqlite3
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
19
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Django",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/manage.py",
|
||||
"args": ["runserver"],
|
||||
"django": true,
|
||||
"justMyCode": true,
|
||||
"python": "${workspaceFolder}/venv/bin/python" // Linux
|
||||
// "python": "${workspaceFolder}\\venv\\Scripts\\python", // Windows
|
||||
}
|
||||
]
|
||||
}
|
26
create_dummy_data.bat
Normal file
@ -0,0 +1,26 @@
|
||||
@echo off
|
||||
echo This file is outdated
|
||||
set /p dontCare=Do you want to continue? [y/n]
|
||||
|
||||
echo The purpose of this file is to populate the database with dummy data for testing. Running this WILL DELETE your current sqlite3 database.
|
||||
set /p confirm=Do you want to continue? [y/n]
|
||||
|
||||
if /i "%confirm%"=="y" (
|
||||
cd /d %~dp0
|
||||
|
||||
if exist src\db.sqlite3 (
|
||||
del src\db.sqlite3
|
||||
)
|
||||
|
||||
call venv\Scripts\activate.bat
|
||||
|
||||
python src/manage.py migrate
|
||||
python src/manage.py create_teams_fixture 10
|
||||
python src/manage.py loaddata teams_fixture
|
||||
python src/manage.py create_members_fixture 3
|
||||
python src/manage.py loaddata members_fixture
|
||||
|
||||
deactivate
|
||||
) else (
|
||||
echo Exiting script...
|
||||
)
|
11
createsuperuser.bat
Normal file
@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
cd %~dp0
|
||||
|
||||
call venv/Scripts/activate.bat
|
||||
|
||||
python src/manage.py migrate
|
||||
|
||||
python src/manage.py create_superuser --username admin --password password --noinput --email "admin@mail.com"
|
||||
|
||||
echo "A superuser has been created username='admin' and password='password'"
|
||||
pause
|
BIN
requirements.txt
106
specs.md
Normal file
@ -0,0 +1,106 @@
|
||||
# Project Name: AT Results
|
||||
## Overview
|
||||
|
||||
This project involves the creation of a website for the Angling Trust to manage members, teams, and other aspects of their fishing events. The website will serve as a central hub for Angling Trust staff to access and manage data related to events, participants, and teams. The website will be designed to streamline the event management process, allowing staff to easily add and edit event details, manage participants and teams, and generate reports in the form of scoreboards.
|
||||
|
||||
## Client Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- The website should allow staff to manage participant and team registrations, including adding and editing participant and team details.
|
||||
|
||||
- The website should allow staff to display a scoreboard on event participation, team rosters, and other relevant data.
|
||||
|
||||
### Content Requirements
|
||||
|
||||
- The website should contain event details, including participants and their assigned teams.
|
||||
|
||||
- The website should contain participant and team details, including names..
|
||||
|
||||
### Design Requirements
|
||||
|
||||
- The website should follow the Angling Trust branding and colour scheme.
|
||||
|
||||
- The website must be easy to navigate and use. Displayed instructions must be clear and interfaces must be intuitive.
|
||||
|
||||
- The website must be responsive and mobile-friendly.
|
||||
|
||||
- Staff should be able to add and edit: members, teams and sections.
|
||||
|
||||
- Staff should be able to assign: members to teams and members to sections.
|
||||
|
||||
- An error should be displayed when a staff member tries to assign a member to a section that contains another member in the same team.
|
||||
|
||||
- An error should appear if a staff member tries to change a peg number to that of another member's peg number, or, should recieve the option to swap the peg numbers.
|
||||
|
||||
- Members that aren't yet assigned to a team should be shown as such, and scoreboards should not include these members.
|
||||
|
||||
- Members that aren't yet assigned to a section should be shown as such, and scoreboards should not include these members.
|
||||
|
||||
- Staff should be able to prohibit certain alphabetic characters from being used as section identifiers.
|
||||
|
||||
### Technical Requirements
|
||||
|
||||
- The website should be built using a modern web development framework.
|
||||
|
||||
- The website should be hosted on a reliable and secure server.
|
||||
|
||||
- The website should incorporate appropriate security measures, such as encryption and authentication.
|
||||
|
||||
#### Teams
|
||||
|
||||
- Represents a team of members
|
||||
|
||||
- Must have a name represented by a whole number.
|
||||
|
||||
- No members on a team can have be on the same section.
|
||||
|
||||
- Teams may be limited to a set amount of members, add a setting to allow for this.
|
||||
|
||||
#### Members
|
||||
|
||||
- Members must represent a participant in the event
|
||||
|
||||
- Members should have a first and last name
|
||||
|
||||
- Members should have a unique whole number as their peg.
|
||||
|
||||
- A member must be assigned to a team.
|
||||
|
||||
- Members must be assigned to a section
|
||||
|
||||
- A member cannot be assigned to a section that already contains one of their teammates.
|
||||
|
||||
#### Section
|
||||
|
||||
- Sections represent a fishing area
|
||||
|
||||
- Sections must be uniquely identified by an alphabetical character.
|
||||
|
||||
- Unique identifiers should use an additional character should all other options be used.
|
||||
|
||||
- A section cannot have 2 members assigned to it that share the same team.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- A fully functional website that meets the client's functional, content, design, and technical requirements.
|
||||
|
||||
- A user manual or other documentation that explains how to use and maintain the website.
|
||||
|
||||
## Timeline
|
||||
|
||||
- Project kickoff: September 2022.
|
||||
|
||||
- No specific end date set.
|
||||
|
||||
## Budget
|
||||
|
||||
- Total project budget is a Costco hotdog with no onions.
|
||||
|
||||
## Stakeholders
|
||||
|
||||
- Angling Trust staff members responsible for managing events and overseeing participant and team registrations.
|
||||
|
||||
## Approval
|
||||
|
||||
- A project demo presentation was carried out for Angling Trust staff
|
@ -42,6 +42,7 @@ INSTALLED_APPS = [
|
||||
# 'rest_framework_datatables'
|
||||
'mainapp',
|
||||
'baton.autodiscover',
|
||||
'compressor',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -59,7 +60,7 @@ ROOT_URLCONF = 'Results.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'mainapp/templates'],
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@ -122,6 +123,16 @@ USE_TZ = True
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [BASE_DIR / 'static/']
|
||||
STATICFILES_FINDERS = ["compressor.finders.CompressorFinder"]
|
||||
|
||||
# Compressors
|
||||
|
||||
COMPRESS_OFFLINE = True
|
||||
LIBSASS_OUTPUT_STYLE = "compressed"
|
||||
COMPRESS_PRECOMPILERS = (
|
||||
("text/x-scss", "django_libsass.SassCompiler"),
|
||||
)
|
||||
COMPRESS_ROOT = BASE_DIR / 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
BIN
src/db.sqlite3
@ -2,40 +2,47 @@
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Peg, Member, Team, Section
|
||||
from .models import Venue
|
||||
# from .models import Member, Team, Section
|
||||
|
||||
|
||||
@admin.register(Peg)
|
||||
class PegAdmin(admin.ModelAdmin):
|
||||
"""Admin model for the Peg model."""
|
||||
|
||||
change_list_template = "entities/bulk_pegging.html"
|
||||
readonly_fields = ("peg_number",)
|
||||
list_display = ("peg_number",)
|
||||
search_fields = ("peg_number",)
|
||||
@admin.register(Venue)
|
||||
class VenueAdmin(admin.ModelAdmin):
|
||||
"""Admin model for the Venue model."""
|
||||
|
||||
|
||||
@admin.register(Member)
|
||||
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")
|
||||
# @admin.register(Peg)
|
||||
# class PegAdmin(admin.ModelAdmin):
|
||||
# """Admin model for the Peg model."""
|
||||
|
||||
# change_list_template = "entities/bulk_pegging.html"
|
||||
# readonly_fields = ("peg_number",)
|
||||
# list_display = ("peg_number",)
|
||||
# search_fields = ("peg_number",)
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
"""Admin model for the Team model."""
|
||||
# @admin.register(Member)
|
||||
# class MemberAdmin(admin.ModelAdmin):
|
||||
# """Admin model for the Member model."""
|
||||
|
||||
readonly_fields = ("team_number",)
|
||||
list_display = ("team_number",)
|
||||
search_fields = ("team_number",)
|
||||
# list_display = ("first_name", "last_name", "team")
|
||||
# search_fields = ("first_name", "last_name", "team")
|
||||
# list_filter = ("team",)
|
||||
|
||||
|
||||
@admin.register(Section)
|
||||
class SectionAdmin(admin.ModelAdmin):
|
||||
"""Admin model for the Section model."""
|
||||
# @admin.register(Team)
|
||||
# class TeamAdmin(admin.ModelAdmin):
|
||||
# """Admin model for the Team model."""
|
||||
|
||||
list_display = ("character",)
|
||||
search_fields = ("character",)
|
||||
# # 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 = ("id", "character",)
|
||||
# search_fields = ("character",)
|
||||
|
167
src/mainapp/fixtures/members_fixture.json
Normal file
@ -0,0 +1,167 @@
|
||||
[
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Duane",
|
||||
"last_name": "Hamilton",
|
||||
"team": 1,
|
||||
"section": 3,
|
||||
"peg_number": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Stacey",
|
||||
"last_name": "Strunk",
|
||||
"team": 1,
|
||||
"section": 10,
|
||||
"peg_number": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Leah",
|
||||
"last_name": "Bishop",
|
||||
"team": 1,
|
||||
"section": 6,
|
||||
"peg_number": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Frances",
|
||||
"last_name": "Day",
|
||||
"team": 2,
|
||||
"section": 16,
|
||||
"peg_number": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Hector",
|
||||
"last_name": "Story",
|
||||
"team": 2,
|
||||
"section": 8,
|
||||
"peg_number": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Deborah",
|
||||
"last_name": "Groseclose",
|
||||
"team": 2,
|
||||
"section": 11,
|
||||
"peg_number": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Elaine",
|
||||
"last_name": "Clarke",
|
||||
"team": 3,
|
||||
"section": 10,
|
||||
"peg_number": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Minnie",
|
||||
"last_name": "Stigall",
|
||||
"team": 3,
|
||||
"section": 7,
|
||||
"peg_number": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Dennis",
|
||||
"last_name": "Stolar",
|
||||
"team": 3,
|
||||
"section": 14,
|
||||
"peg_number": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Darlene",
|
||||
"last_name": "Beckman",
|
||||
"team": 4,
|
||||
"section": 5,
|
||||
"peg_number": 11
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Charles",
|
||||
"last_name": "Boone",
|
||||
"team": 4,
|
||||
"section": 12,
|
||||
"peg_number": 12
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Cecil",
|
||||
"last_name": "Lamus",
|
||||
"team": 4,
|
||||
"section": 13,
|
||||
"peg_number": 13
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Morton",
|
||||
"last_name": "Quinn",
|
||||
"team": 5,
|
||||
"section": 14,
|
||||
"peg_number": 14
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Stuart",
|
||||
"last_name": "Bristol",
|
||||
"team": 5,
|
||||
"section": 15,
|
||||
"peg_number": 15
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": null,
|
||||
"fields": {
|
||||
"first_name": "Jeffrey",
|
||||
"last_name": "Ferretti",
|
||||
"team": 5,
|
||||
"section": 10,
|
||||
"peg_number": 16
|
||||
}
|
||||
}
|
||||
]
|
3502
src/mainapp/fixtures/sections_fixture.json
Normal file
152
src/mainapp/fixtures/teams_fixture.json
Normal file
@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 1,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 2,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 3,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 4,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 5,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 6,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 7,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 8,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 9,
|
||||
"fields": {}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 10,
|
||||
"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": {}
|
||||
}
|
||||
]
|
0
src/mainapp/management/__init__.py
Normal file
0
src/mainapp/management/commands/__init__.py
Normal file
117
src/mainapp/management/commands/create_members_fixture.py
Normal file
@ -0,0 +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, Section
|
||||
|
||||
|
||||
# TODO: refactor this file like create_teams_fixtures.py
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a fixture with randomly generated Member objects"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("members_per_team", type=int)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
members_per_team = options["members_per_team"]
|
||||
|
||||
teams = Team.objects.all()
|
||||
|
||||
if not teams:
|
||||
raise Exception("no teams")
|
||||
|
||||
new_members = []
|
||||
for team in teams:
|
||||
|
||||
i = members_per_team
|
||||
while i > 0:
|
||||
|
||||
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,
|
||||
"section": member.section_id,
|
||||
"peg_number": member.peg_number
|
||||
}
|
||||
}
|
||||
fixtures.append(fixture)
|
||||
|
||||
with open("src/mainapp/fixtures/members_fixture.json", "w") as f:
|
||||
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
@ -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."))
|
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()
|
34
src/mainapp/management/commands/create_teams_fixture.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""Command to create test data fixture for teams."""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from mainapp.models import Team
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a fixture file for Team objects"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("amount_of_teams", type=int, help="Number of teams to create")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
teams_amount = options["amount_of_teams"]
|
||||
|
||||
# Create the new teams (this will create them in the database)#
|
||||
new_teams = [Team.objects.create() for i in range(teams_amount)]
|
||||
|
||||
teams_fixture = [{
|
||||
"model": "mainapp.team",
|
||||
"pk": team.pk,
|
||||
"fields": {}
|
||||
} for team in new_teams]
|
||||
|
||||
# Remove the teams from the database
|
||||
for team in new_teams:
|
||||
team.delete()
|
||||
|
||||
with open("src/mainapp/fixtures/teams_fixture.json", "w") as file:
|
||||
file.write(json.dumps(teams_fixture, indent=4))
|
||||
self.stdout.write(self.style.SUCCESS("Created teams_fixture.json."))
|
@ -1,9 +1,7 @@
|
||||
# Generated by Django 4.1.5 on 2023-01-26 14:24
|
||||
# Generated by Django 4.1.5 on 2023-05-15 11:20
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mainapp.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -11,27 +9,21 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Peg',
|
||||
fields=[
|
||||
('peg_number', mainapp.models.ReusableAutoField(default=mainapp.models.ReusableAutoField.get_default, editable=False, primary_key=True, serialize=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Section',
|
||||
fields=[
|
||||
('character', models.CharField(max_length=1, primary_key=True, serialize=False, validators=[mainapp.models.validate_section_character])),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('character', 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)),
|
||||
('section', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='mainapp.section')),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('number', models.PositiveIntegerField(unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@ -40,8 +32,9 @@ 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)),
|
||||
('peg_number', models.PositiveIntegerField(null=True, unique=True)),
|
||||
('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,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]),
|
||||
),
|
||||
]
|
@ -0,0 +1,45 @@
|
||||
# Generated by Django 4.1.5 on 2023-10-22 21:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Venue',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('extra_notes', models.TextField(blank=True)),
|
||||
('venue_type', models.CharField(choices=[('FISHERY', 'Fishery'), ('CLUB', 'Club'), ('PRIVATE', 'Private')], max_length=50)),
|
||||
('phone_number', models.CharField(max_length=100)),
|
||||
('email_address', models.EmailField(max_length=254)),
|
||||
('website_url', models.URLField()),
|
||||
('street_address', models.CharField(max_length=100)),
|
||||
('city', models.CharField(max_length=255)),
|
||||
('provence', models.CharField(max_length=100)),
|
||||
('postal_code', models.CharField(max_length=20)),
|
||||
('country', models.CharField(max_length=100)),
|
||||
('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('twitter_url', models.URLField(blank=True)),
|
||||
('instagram_url', models.URLField(blank=True)),
|
||||
('facebook_url', models.URLField(blank=True)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Member',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Section',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Team',
|
||||
),
|
||||
]
|
@ -1,106 +1,234 @@
|
||||
"""Models for the mainapp."""
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# products/models.py
|
||||
from django.db import models
|
||||
|
||||
class Venue(models.Model):
|
||||
"""Represents a Venue and Waters."""
|
||||
|
||||
class ReusableAutoField(models.PositiveIntegerField):
|
||||
"""A django auto field that can reuse deleted primary keys"""
|
||||
|
||||
def get_next_available_id(self, model_cls, using=None):
|
||||
"""
|
||||
Returns the next available id for the given model class.
|
||||
"""
|
||||
all_ids = set(range(1, model_cls._default_manager.count()+1))
|
||||
used_ids = set(model_cls._default_manager.all().values_list('pk', flat=True))
|
||||
available_ids = all_ids - used_ids
|
||||
VENUE_TYPES = (
|
||||
("FISHERY", "Fishery"),
|
||||
("CLUB", "Club"),
|
||||
("PRIVATE", "Private")
|
||||
)
|
||||
|
||||
if available_ids:
|
||||
return min(available_ids)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
extra_notes = models.TextField(blank=True)
|
||||
venue_type = models.CharField(choices=VENUE_TYPES, max_length=50)
|
||||
|
||||
if used_ids:
|
||||
return max(used_ids) + 1
|
||||
# Contact information
|
||||
phone_number = models.CharField(max_length=100)
|
||||
email_address = models.EmailField()
|
||||
website_url = models.URLField()
|
||||
|
||||
return 1
|
||||
# Location information
|
||||
street_address = models.CharField(max_length=100)
|
||||
city = models.CharField(max_length=255)
|
||||
provence = models.CharField(max_length=100)
|
||||
postal_code = models.CharField(max_length=20)
|
||||
country = models.CharField(max_length=100)
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
|
||||
def get_default(self):
|
||||
"""Returns the default value for this field"""
|
||||
|
||||
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)
|
||||
# Socials information
|
||||
twitter_url = models.URLField(blank=True)
|
||||
instagram_url = models.URLField(blank=True)
|
||||
facebook_url = models.URLField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Peg {self.peg_number}"
|
||||
return self.name
|
||||
|
||||
|
||||
def validate_team_size(value):
|
||||
if Member.objects.filter(team=value).count() >= 3:
|
||||
raise ValidationError('Team already has maximal amount of members (3)')
|
||||
# class ReusableAutoField(models.PositiveIntegerField):
|
||||
# """A django auto field that can reuse deleted primary keys."""
|
||||
|
||||
# def get_next_available_id(self, model_cls, using=None):
|
||||
# """
|
||||
# Returns the next available id for the given model class.
|
||||
# """
|
||||
# all_ids = set(range(1, model_cls._default_manager.count()+1))
|
||||
# used_ids = set(model_cls._default_manager.all().values_list('pk', flat=True))
|
||||
# available_ids = all_ids - used_ids
|
||||
|
||||
# if available_ids:
|
||||
# return min(available_ids)
|
||||
|
||||
# if used_ids:
|
||||
# return max(used_ids) + 1
|
||||
|
||||
# return 1
|
||||
|
||||
# def get_default(self):
|
||||
# """Returns the default value for this field"""
|
||||
|
||||
# return self.get_next_available_id(self.model)
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
"""Represents a member of a team"""
|
||||
# class SectionValidator:
|
||||
# """Validation class for the `section` field on the `member` 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)
|
||||
# def __init__(self, max_value="ZZZ"):
|
||||
# self.max_value = max_value.upper()
|
||||
# self.alphabet_size = ord("Z") - ord("A") + 1
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name} (team {self.team.team_number}))"
|
||||
# def is_valid(self, section: str, team_sections: list[str]=None) -> bool:
|
||||
# """Returns boolean if the section passed is valid."""
|
||||
# section = section.upper()
|
||||
|
||||
# if not self._is_alphanumeric(section):
|
||||
# return False
|
||||
|
||||
# if not self._is_in_alphabet(section[0]):
|
||||
# return False
|
||||
|
||||
# if not self._is_in_range(section):
|
||||
# return False
|
||||
|
||||
# if team_sections:
|
||||
# if not self._is_unique(section, team_sections):
|
||||
# return False
|
||||
|
||||
# if not self._is_not_adjacent(section, team_sections):
|
||||
# return False
|
||||
|
||||
# return True
|
||||
|
||||
# def _is_alphanumeric(self, section: str) -> bool:
|
||||
# """Returns boolean if all characters in the passed string are alphanumerical."""
|
||||
# return all(c.isalnum() for c in section)
|
||||
|
||||
# def _is_in_alphabet(self, c) -> bool:
|
||||
# """Returns boolean if the passed character is alphabetical."""
|
||||
# return "A" <= c <= "Z"
|
||||
|
||||
# def _is_in_range(self, section) -> bool:
|
||||
# """Returns boolean if the passed section less or equal to the max value."""
|
||||
# section_value = self._section_value(section)
|
||||
# max_value = self._section_value(self.max_value)
|
||||
|
||||
# return section_value <= max_value
|
||||
|
||||
# def _is_unique(self, section, team_sections) -> bool:
|
||||
# """Returns boolean if the passed section is unique amongst `team_sections`."""
|
||||
# return section not in team_sections
|
||||
|
||||
# def _is_not_adjacent(self, section, team_sections) -> bool:
|
||||
# """Returns boolean if the passed section is not adjacent to any `team_sections`."""
|
||||
# for team_section in team_sections:
|
||||
# team_section_value = self._section_value(team_section)
|
||||
# section_value = self._section_value(section)
|
||||
# if abs(team_section_value - section_value) <= 1:
|
||||
# return False
|
||||
|
||||
# return True
|
||||
|
||||
# def _section_value(self, section):
|
||||
# """Returns the value of the passed section."""
|
||||
# n = len(section)
|
||||
# value = sum((ord(c) - ord("A") + 1) * self.alphabet_size ** (n - i - 1) for i, c in enumerate(section))
|
||||
# return value
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
"""Represents a team"""
|
||||
# class SectionManager(models.Manager):
|
||||
|
||||
team_number = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
|
||||
section = models.OneToOneField("Section", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
# @staticmethod
|
||||
# def get_max_section():
|
||||
# max_section = None
|
||||
# max_number = -1
|
||||
|
||||
def __str__(self):
|
||||
return f"Team {self.team_number}"
|
||||
# # Iterate through all sections in the database
|
||||
# for section in Section.objects.all():
|
||||
# section_name = section.name
|
||||
# section_number = 0
|
||||
|
||||
# # Calculate the section number based on the section name
|
||||
# for i, char in enumerate(section_name):
|
||||
# section_number += (ord(char) - ord('A') + 1) * (26 ** (len(section_name) - i - 1))
|
||||
|
||||
# # Check if this section has a higher number than the current maximum
|
||||
# if section_number > max_number:
|
||||
# max_number = section_number
|
||||
# max_section = section_name
|
||||
|
||||
# return max_section
|
||||
|
||||
# @staticmethod
|
||||
# def find_next_section(current_section):
|
||||
# if not current_section:
|
||||
# return 'A'
|
||||
|
||||
# # Split current section name into a list of characters
|
||||
# chars = list(current_section)
|
||||
|
||||
# # Increment the last character
|
||||
# chars[-1] = chr(ord(chars[-1]) + 1)
|
||||
|
||||
# # Check if the last character is "Z", and carry over to the next character if necessary
|
||||
# for i in range(len(chars) - 1, -1, -1):
|
||||
# if chars[i] > 'Z':
|
||||
# chars[i] = 'A'
|
||||
# if i == 0:
|
||||
# # If the first character needs to be incremented, add a new character "A"
|
||||
# chars.insert(0, 'A')
|
||||
# else:
|
||||
# # Increment the previous character
|
||||
# chars[i - 1] = chr(ord(chars[i - 1]) + 1)
|
||||
# else:
|
||||
# break
|
||||
|
||||
# # Join the characters back into a string and return the result
|
||||
# return ''.join(chars)
|
||||
|
||||
# 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."""
|
||||
|
||||
# character = models.CharField(max_length=3, unique=True, null=False)
|
||||
|
||||
# objects = SectionManager()
|
||||
|
||||
# def clean(self):
|
||||
# super().clean()
|
||||
# self.character = self.character.upper()
|
||||
|
||||
# def __str__(self) -> str:
|
||||
# return self.character
|
||||
|
||||
|
||||
def validate_section_character(value):
|
||||
"""Validates the section character"""
|
||||
# class Member(models.Model):
|
||||
# """Represents a member of a team"""
|
||||
|
||||
value = value.upper()
|
||||
banned_characters = ["I"]
|
||||
# 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, 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, related_name='members')
|
||||
|
||||
if value in banned_characters:
|
||||
raise ValidationError(f"The character <{value}> is a prohibited character.")
|
||||
# def __init__(self, *args, **kwargs):
|
||||
# super().__init__(*args, **kwargs)
|
||||
|
||||
# # If the peg_number field is not set, we assign it the smallest
|
||||
# # available positive integer, excluding any used values.
|
||||
# if not self.peg_number:
|
||||
# used_peg_numbers = Member.objects.exclude(id=self.id).exclude(peg_number=None).values_list("peg_number", flat=True)
|
||||
# peg_numbers = set(range(1, Member.objects.count() + 1)) - set(used_peg_numbers)
|
||||
# if peg_numbers:
|
||||
# self.peg_number = min (peg_numbers)
|
||||
|
||||
# def __str__(self):
|
||||
# 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 Section(models.Model):
|
||||
"""Represents a section of the scoreboard"""
|
||||
# class Team(models.Model):
|
||||
# """Represents a team"""
|
||||
|
||||
# character field stores a single character but doesnt allow for 'I' or 'i'
|
||||
character = models.CharField(primary_key=True, max_length=1, validators=(validate_section_character, ))
|
||||
# number = models.PositiveIntegerField(unique=True, null=False, blank=False)
|
||||
|
||||
def clean(self):
|
||||
self.character = self.character.upper()
|
||||
|
||||
def __str__(self):
|
||||
return f"Section {self.character}"
|
||||
|
||||
|
||||
#class Scoreboard(models.Model):
|
||||
# class Status(models.IntegerChoices):
|
||||
# ACTIVE = 1, "Active"
|
||||
# INACTIVE = 2, "Inactive"
|
||||
# ARCHIVED = 3, "Archived"
|
||||
#
|
||||
# name = models.CharField(max_length=255)
|
||||
# category = models.CharField(max_length=255)
|
||||
# price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
# cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
# status = models.PositiveSmallIntegerField(choices=Status.choices)
|
||||
#
|
||||
# def __str__(self):
|
||||
# return self.name
|
||||
# def __str__(self):
|
||||
# return f"Team {self.number}"
|
||||
|
200
src/mainapp/templates/anglers.html
Normal file
@ -0,0 +1,200 @@
|
||||
{% 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 header_buttons %}
|
||||
<a href="/" class="btn btn-company px-4">Back</a>
|
||||
{% endblock header_buttons %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
|
||||
<div class="m-4 mb-0 row">
|
||||
<div class="col-xl-4 col-md-8 mb-md-0 mb-4">
|
||||
<div class="input-group">
|
||||
<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" name="search" id="search" class="form-control border-secondary-subtle shadow-none" placeholder="Search Anglers">
|
||||
<button type="button" class="btn btn-outline-company border-secondary-subtle rounded-end" id="searchButton"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-4">
|
||||
<div class="input-group d-flex justify-content-md-end">
|
||||
<button class="btn border-secondary-subtle btn-outline-company me-4 rounded-end" id="addAngler" data-bs-toggle="tooltip" data-bs-title="Add Angler" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<button class="btn border-secondary-subtle btn-outline-company rounded-start" id="ContractView" data-bs-toggle="tooltip" data-bs-title="Compact View" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-arrows-angle-contract"></i>
|
||||
</button>
|
||||
<button class="btn border-secondary-subtle btn-outline-company" id="expandView" data-bs-toggle="tooltip" data-bs-title="Enlarged View" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-arrows-angle-expand"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-4 my-2">
|
||||
<div class="col-xl-7">
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-4 mt-0 row">
|
||||
{% for angler in anglers %}
|
||||
<div class="col-xl-3 col-lg-4 col-sm-6 mb-4">
|
||||
<div class="card w-100 h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ angler.first_name }}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-body-secondary">{{ angler.last_name }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
<!-- 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 -->
|
||||
{% endcomment %}
|
||||
|
||||
{% 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 %}
|
@ -1,63 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||
<title>Home</title>
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
<!-- Styles -->
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/index.css' %}">
|
||||
</head>
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static 'css/ocean.css' %}">
|
||||
{% endblock style %}
|
||||
|
||||
<body>
|
||||
<main>
|
||||
{% 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 class="flex-grow-1 overflow-hidden">
|
||||
<div id="ocean">
|
||||
<div class="bubble bubble--1"></div>
|
||||
<div class="bubble bubble--2"></div>
|
||||
<div class="bubble bubble--3"></div>
|
||||
<div class="bubble bubble--4"></div>
|
||||
<div class="bubble bubble--5"></div>
|
||||
<div class="bubble bubble--6"></div>
|
||||
<div class="bubble bubble--7"></div>
|
||||
<div class="bubble bubble--8"></div>
|
||||
<div class="bubble bubble--9"></div>
|
||||
<div class="bubble bubble--10"></div>
|
||||
<div class="bubble bubble--11"></div>
|
||||
<div class="bubble bubble--12"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="album py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="teams.html">
|
||||
<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>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="scoreboard.html">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="results.html">
|
||||
<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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% block scripts %}
|
||||
<script src="{% static 'js/ocean.js' %}"></script>
|
||||
{% endblock scripts %}
|
||||
|
91
src/mainapp/templates/modals/angler.html
Normal file
@ -0,0 +1,91 @@
|
||||
|
||||
<div class="modal fade" id="anglerModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<form id="anglerForm">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header border-0">
|
||||
<h3 class="modal-title fs-5 color">
|
||||
<span class="pe-3" id="anglerTitle">Angler Modal Title</span>
|
||||
<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">
|
||||
|
||||
<!-- Angler Name -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating">
|
||||
<input class="form-control" type="text" name="anglerForename" id="anglerForename" placeholder="Forename" requred maxlength="30">
|
||||
<label for="anglerForename">Forename</label>
|
||||
</div>
|
||||
<div id="anglerForenameError" class="text-danger my-2 error" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating">
|
||||
<input class="form-control" type="text" name="anglerSurname" id="anglerSurname" placeholder="Surname" requred maxlength="30">
|
||||
<label for="anglerSurname">Surname</label>
|
||||
</div>
|
||||
<div id="anglerSurnameError" class="text-danger my-2 error" style="display: none;"></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" required></select>
|
||||
</div>
|
||||
<div id="anglerTeamError" class="text-danger my-2 error" style="display: none;"></div>
|
||||
<!-- End Angler Team -->
|
||||
|
||||
<!-- Angler Section -->
|
||||
<div class="col-12 mb-3">
|
||||
<div class="input-group">
|
||||
<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" required></select>
|
||||
</div>
|
||||
<div id="anglerSectionError" class="text-danger my-2 error" style="display: none;"></div>
|
||||
</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" required>
|
||||
</div>
|
||||
<button type="button" id="anglerPegNext" 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 class="col-md-7 mt-2 pe-md-3 text-danger error" id="anglerPegError" style="display:none;"></div>
|
||||
</div>
|
||||
<!-- End Angler Peg Number -->
|
||||
|
||||
</div>
|
||||
<!-- End Modal Body -->
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer border-0 d-flex">
|
||||
<button type="button" class="btn btn-outline-danger me-auto" style="display: hidden;" id="anglerDelete">Delete Angler</button>
|
||||
<button type="submit" class="btn btn-company px-4" id="saveAngler">Save</button>
|
||||
</div>
|
||||
<!-- End Modal Footer -->
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
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>
|
48
src/mainapp/templates/modals/team.html
Normal file
@ -0,0 +1,48 @@
|
||||
|
||||
<div class="modal fade" id="teamModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<form id="teamForm">
|
||||
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header border-0">
|
||||
<h3 class="modal-title fs-5">
|
||||
<span id="teamTitle" class="pe-3">Team Modal Title</span>
|
||||
<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">
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
</div>
|
||||
<!-- End Modal Body -->
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer border-0 d-flex">
|
||||
<button class="btn btn-outline-danger me-auto" id="teamDelete" style="display: none;">Delete Team</button>
|
||||
<button type="submit" class="btn btn-company px-4" id="saveTeam">Save</button>
|
||||
</div>
|
||||
<!-- End Modal Footer -->
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,9 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||
<title>Results</title>
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Results |
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-dark-subtle p-4">
|
||||
<a href="{% url 'index' %}" class="btn btn-primary">Back</a>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,49 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||
<title>Scoreboard</title>
|
||||
<style>
|
||||
table {
|
||||
border-spacing: 0px;
|
||||
}
|
||||
</style>
|
||||
{% extends "base.html" %}
|
||||
|
||||
<body>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
<table class="table table-responsive table-striped table-hover table-bordered border-primary" contenteditable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">First</th>
|
||||
<th scope="col">Last</th>
|
||||
<th scope="col">Handle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">1</th>
|
||||
<td>Mark</td>
|
||||
<td>Otto</td>
|
||||
<td>@mdo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">2</th>
|
||||
<td>Jacob</td>
|
||||
<td>Thornton</td>
|
||||
<td>@fat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">3</th>
|
||||
<td colspan="2">Larry the Bird</td>
|
||||
<td>@twitter</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% block title %}
|
||||
Scoreboard |
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-dark-subtle p-4">
|
||||
<a href="{% url 'index' %}" class="btn btn-primary">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
{% endblock content %}
|
@ -1,8 +1,245 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
|
||||
<title>Teams</title>
|
||||
{% 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 %}
|
||||
<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>
|
||||
|
||||
<div class="container my-4 p-4 pb-0 bg-body rounded">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12 col-xl-4 d-flex">
|
||||
<h3 class="mb-3 mb-xl-0">
|
||||
Manage Anglers
|
||||
<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="addMember" 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">
|
||||
<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>
|
||||
<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">Sort groups by</h3>
|
||||
<div>
|
||||
<div class="d-inline-block form-check">
|
||||
<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="sections" name="sortGroups" id="sortTeamsSection">
|
||||
<label for="sortTeamsSection" 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="first_name" name="sortMembers" id="sortMembersName">
|
||||
<label for="sortMembersName" 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="peg_number" name="sortMembers" id="sortMembersPeg">
|
||||
<label for="sortMembersPeg" class="form-check-label">Peg Number</label>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pb-4" id="teamsNotFound">
|
||||
<div class="alert alert-danger m-0" role="alert">
|
||||
No teams found under that search
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Edit Section Modal -->
|
||||
<div class="modal fade" id="editSectionModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0">
|
||||
<h3 class="modal-title fs-5" id="editTeamTitle">
|
||||
Section Character 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">
|
||||
<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="editSectionName" id="editSectionName" class="form-control" minlength="1" maxlength="3" value="A">
|
||||
</div>
|
||||
<button type="button" id="editSectionNameButton" 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="editSectionNameError" style="display: hidden"></div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-company px-4" id="saveEditSectionModal">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Team Modal -->
|
||||
<div class="modal fade" id="editTeamModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<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">
|
||||
<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="editTeamNumber" id="editTeamNumber" class="form-control" min="1" max="9999" value="1">
|
||||
</div>
|
||||
<button type="button" id="editTeamNumberButton" 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="editTeamNumberError" style="display: hidden"></div>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-company px-4" id="saveEditTeamModal">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Member Modal -->
|
||||
<div class="modal fade" id="editMemberModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0">
|
||||
<h3 class="modal-title fs-5 color" id="editMemberName">
|
||||
Angler 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>
|
||||
<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" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Team">
|
||||
<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" data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="Section">
|
||||
<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 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="editMemberPeg" id="editMemberPeg" 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>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-company px-4" id="saveEditModal">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% 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/teams.js' %}"></script>
|
||||
{% endblock scripts %}
|
181
src/mainapp/templates/venues.html
Normal file
@ -0,0 +1,181 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
Venues and Waters |
|
||||
{% endblock title %}
|
||||
|
||||
{% block style %}
|
||||
{% endblock style %}
|
||||
|
||||
{% block header_buttons %}
|
||||
<!-- <a href="/" class="btn btn-company d-flex align-items-center">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
<span class="ms-2">Add Venue and Waters</span>
|
||||
</a> -->
|
||||
{% endblock header_buttons %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
|
||||
<div class="m-4 mb-0 row">
|
||||
<div class="col-xl-4 col-md-8 mb-md-0 mb-4">
|
||||
<div class="input-group">
|
||||
<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" name="search" id="search" class="form-control border-secondary-subtle shadow-none" placeholder="Search Venues and Waters">
|
||||
<button type="button" class="btn btn-outline-company border-secondary-subtle rounded-end" id="searchButton"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-4">
|
||||
<div class="input-group d-flex justify-content-md-end">
|
||||
<button class="btn border-secondary-subtle btn-outline-company me-4 rounded-end" id="addVenue" data-bs-toggle="modal" data-bs-target="#venueModal" data-bs-toggle="tooltip" data-bs-title="Add Venues and Waters" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<button class="btn border-secondary-subtle btn-outline-company rounded-start" id="ContractView" data-bs-toggle="tooltip" data-bs-title="List View" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-list-task"></i>
|
||||
</button>
|
||||
<button class="btn border-secondary-subtle btn-outline-company" id="expandView" data-bs-toggle="tooltip" data-bs-title="Grid View" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-grid"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mx-4 my-2">
|
||||
<div class="col-xl-7">
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-4 mt-0 row">
|
||||
|
||||
<!-- <div class="col-xl-3 col-lg-4 col-sm-6 mb-4">
|
||||
<div class="card w-100 h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover overflow-hidden">
|
||||
<div class="card-badge-container">
|
||||
<div class="card-badge"></div>
|
||||
</div>
|
||||
<div class="card-body d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="card-title text-company mb-0">Generic Location Name</h5>
|
||||
<div class="fw-bold mb-3">
|
||||
<p class="mb-0">Private</p>
|
||||
</div>
|
||||
<div class="text-body-secondary mb-3">
|
||||
<p class="mb-0">4 Branshaw Grove</p>
|
||||
<p class="mb-0">Newquay, Cornwall</p>
|
||||
<p class="mb-0">TR7 1LN</p>
|
||||
</div>
|
||||
<div class="text-body-secondary mb-3">
|
||||
<p class="mb-0">07796 081958</p>
|
||||
<p class="mb-0">branshaw.grove@mail.co.uk</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
{% for venue in venues %}
|
||||
<div class="col-xl-3 col-lg-4 col-sm-6 mb-4">
|
||||
<div class="card w-100 h-100 fluid-hover-zoom shadow-sm md-shadow-on-hover overflow-hidden">
|
||||
<div class="card-badge-container">
|
||||
<div class="card-badge"></div>
|
||||
</div>
|
||||
<div class="card-body d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<h5 class="card-title text-company mb-0">{{ venue.name }}</h5>
|
||||
<div class="fw-bold mb-3">
|
||||
<p class="mb-0">{{ venue.type }}</p>
|
||||
</div>
|
||||
<div class="text-body-secondary mb-3">
|
||||
<p class="mb-0">{{ venue.street_address }}</p>
|
||||
<p class="mb-0">{{ venue.city }}, {{ venue.provence }}</p>
|
||||
<p class="mb-0">{{ venue.postal_code }}</p>
|
||||
</div>
|
||||
<div class="text-body-secondary mb-3">
|
||||
<p class="mb-0">{{ venue.phone_number }}</p>
|
||||
<p class="mb-0">{{ venue.email_address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="venueModal" class="modal fade">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content overflow-hidden">
|
||||
<div class="modal-body p-0">
|
||||
<div class="card-badge-container">
|
||||
<div class="card-badge"></div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<h5 class="card-title text-company mb-4">New Venue and Waters</h5>
|
||||
<div class="row g-3 align-items-center mb-3">
|
||||
<div class="col-6">
|
||||
<label for="" class="form-label">Name</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="" class="form-label">Type</label>
|
||||
<select name="" id="" class="form-select">
|
||||
<option value="-1">placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="" class="form-label">Description</label>
|
||||
<textarea type="text" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label for="" class="form-label">Street Address</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="row g-3 align-items-center mb-3">
|
||||
<div class="col-6">
|
||||
<label for="" class="form-label">City/Town</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="" class="form-label">Provence</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="" class="form-label">Postal Code</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-6">
|
||||
<label for="" class="form-label">Phone Number</label>
|
||||
<input type="tel" class="form-control">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="" class="form-label">Email Address</label>
|
||||
<input type="email" class="form-control">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="" class="form-label">Website Address</label>
|
||||
<input type="url" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary px-3" data-dismiss="modal">Cancel</button>
|
||||
<button class="btn btn-company" data-dismiss="modal">Save New</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock scripts %}
|
@ -3,8 +3,18 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('results.html', views.results, name='results'),
|
||||
path('scoreboard.html', views.scoreboard, name='scoreboard'),
|
||||
path('teams.html', views.teams, name='teams'),
|
||||
path('bulk-peg/', views.bulk_create_pegs, name='bulk-peg')
|
||||
path('results/', views.results, name='results'),
|
||||
path('scoreboard/', views.scoreboard, name='scoreboard'),
|
||||
path('members/', views.teams, name='members'),
|
||||
path('venues/', views.venues, name='venues'),
|
||||
|
||||
# path('bulk-peg/', views.bulk_create_pegs, name='bulk-peg'),
|
||||
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'),
|
||||
|
||||
# Rewrite
|
||||
path('anglers/', views.ManageAnglersView.as_view(), name='anglers'),
|
||||
]
|
@ -1,6 +1,18 @@
|
||||
from django.shortcuts import render, redirect
|
||||
"""Views for the main app."""
|
||||
|
||||
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, Min
|
||||
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 Venue
|
||||
# from .models import Team, Member, Section, SectionManager, ReusableAutoField, SectionValidator
|
||||
|
||||
from .models import Peg
|
||||
|
||||
def index(request):
|
||||
return render(request, 'index.html')
|
||||
@ -14,12 +26,522 @@ def scoreboard(request):
|
||||
def teams(request):
|
||||
return render(request, 'teams.html')
|
||||
|
||||
def bulk_create_pegs(request):
|
||||
"""Bulk create pegs"""
|
||||
def venues(request):
|
||||
|
||||
number_of_pegs = request.POST.get("pegAmount")
|
||||
for i in range(int(number_of_pegs)):
|
||||
Peg.objects.create()
|
||||
venues = Venue.objects.all()
|
||||
context = {"venues": venues}
|
||||
|
||||
# return to previous page
|
||||
return redirect(request.META.get('HTTP_REFERER'))
|
||||
return render(request, 'venues.html', context)
|
||||
|
||||
# def bulk_create_pegs(request):
|
||||
# """Bulk create pegs"""
|
||||
|
||||
# number_of_pegs = request.POST.get("pegAmount")
|
||||
# for i in range(int(number_of_pegs)):
|
||||
# Peg.objects.create()
|
||||
|
||||
# # 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])
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
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,
|
||||
"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)
|
||||
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"]["#teamNumber"] = "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
|
||||
|
||||
try:
|
||||
angler.save()
|
||||
except IntegrityError:
|
||||
result["form_errors"]["#anglerPeg"] = "An Angler with this peg number already exists"
|
||||
|
||||
result["angler"] = {
|
||||
"id": angler.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.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.
|
||||
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.POST.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.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()
|
||||
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.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}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_angler_page_data(request, **kwargs):
|
||||
"""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")
|
||||
sort_groups = request.POST.get("sortGroups") or "team"
|
||||
sort_members = request.POST.get("sortMembers") or "peg_number"
|
||||
|
||||
teams = Team.objects.order_by("number").all()
|
||||
sections = Section.objects.order_by("character").all()
|
||||
|
||||
if search:
|
||||
search_terms = search.split()
|
||||
members = Member.objects.filter(
|
||||
reduce(
|
||||
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
|
||||
]
|
||||
)
|
||||
)
|
||||
teams = teams.filter(members__in=members).distinct()
|
||||
sections = sections.filter(members__in=members).distinct()
|
||||
|
||||
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
|
||||
|
||||
for key, value in kwargs.items():
|
||||
response_data[key] = value
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def update_member(request):
|
||||
"""Update a member. Returns a JsonResponse with the updated teams."""
|
||||
|
||||
if not request.POST:
|
||||
return
|
||||
|
||||
# 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")
|
||||
peg_number = request.POST.get("pegNumber")
|
||||
|
||||
# Get the member and team
|
||||
member = Member.objects.get(id=member_id)
|
||||
team = Team.objects.get(name=team_number)
|
||||
|
||||
# Update the member
|
||||
member.first_name = first
|
||||
member.last_name = last
|
||||
member.team = team
|
||||
member.peg_number = peg_number
|
||||
|
||||
member.save()
|
||||
|
||||
return get_angler_page_data(request)
|
||||
|
||||
def update_section(request):
|
||||
"""Update a section, returns JsonResponse with updated teams data."""
|
||||
|
||||
if not request.POST:
|
||||
return
|
||||
|
||||
section_id = request.POST.get("sectionId")
|
||||
section_name = request.POST.get("sectionName")
|
||||
|
||||
validator = SectionValidator()
|
||||
if not validator.is_valid(section_name):
|
||||
json_response = get_angler_page_data(request, form_errors={
|
||||
"editSectionName": "This is an invalid section"
|
||||
})
|
||||
return json_response
|
||||
|
||||
if section_id == "-1":
|
||||
section = Section(character=section_name)
|
||||
else:
|
||||
section = Section.objects.get(id=section_id)
|
||||
section.character = section_name
|
||||
|
||||
try:
|
||||
section.save()
|
||||
except IntegrityError:
|
||||
json_response = get_angler_page_data(request, form_errors={
|
||||
"editSectionName": "A Section with this character already exists"
|
||||
})
|
||||
return json_response
|
||||
|
||||
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."""
|
||||
|
||||
if not request.POST:
|
||||
return
|
||||
|
||||
team_id = request.POST.get("id")
|
||||
team_number = request.POST.get("number")
|
||||
|
||||
try:
|
||||
if team_id == "-1":
|
||||
team = Team.objects.create(number=team_number)
|
||||
else:
|
||||
team = Team.objects.get(id=team_id)
|
||||
team.number = team_number
|
||||
team.save()
|
||||
except IntegrityError as error:
|
||||
json_response = get_angler_page_data(request, form_errors={
|
||||
"editTeamNumber": "A Team with this number already exists"
|
||||
})
|
||||
return json_response
|
||||
|
||||
return get_angler_page_data(request)
|
||||
|
||||
def get_next_peg() -> int:
|
||||
pass
|
||||
|
||||
def get_next_section() -> str:
|
||||
|
||||
section_name = SectionManager.get_max_section()
|
||||
return SectionManager.find_next_section(section_name)
|
||||
|
||||
|
||||
def get_next_team() -> int:
|
||||
|
||||
field = ReusableAutoField
|
||||
field.model = Team
|
||||
|
||||
return field().get_default()
|
||||
|
||||
|
||||
def get_next_identifier(request):
|
||||
"""Get the next available identifer (peg, section character, etc.) for an object."""
|
||||
|
||||
if not request.POST:
|
||||
return
|
||||
|
||||
item = request.POST.get("item")
|
||||
|
||||
match item:
|
||||
case "member_peg":
|
||||
result = get_next_peg()
|
||||
|
||||
case "section_name":
|
||||
result = get_next_section()
|
||||
|
||||
case "team_number":
|
||||
result = get_next_team()
|
||||
|
||||
case _:
|
||||
raise ValueError(f"Bad identifier item: {item}")
|
||||
|
||||
return JsonResponse({"identifier": result})
|
||||
|
6
src/static/css/bootstrap.min.css
vendored
Normal file
137
src/static/css/custom.css
Normal file
@ -0,0 +1,137 @@
|
||||
|
||||
.ul-cols-2 {
|
||||
column-count: 2;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
.ul-cols-2 li {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.spinner-border-lg {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.fluid-hover-zoom {
|
||||
transition:
|
||||
.3s transform cubic-bezier(.155,1.105,.295,1.12),
|
||||
.3s box-shadow,
|
||||
.3s -webkit-transform cubic-bezier(.155,1.105,.295,1.12);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.fluid-hover-zoom:hover {
|
||||
transform: scale(1.035);
|
||||
}
|
||||
|
||||
.no-transform {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.shadow-on-hover:not(:hover) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.team-column {
|
||||
opacity: 0;
|
||||
transition: 5s opacity ease-in;
|
||||
}
|
||||
|
||||
.fixed-badge {
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.font-montserrat {
|
||||
font-family: Montserrat;
|
||||
}
|
||||
|
||||
.font-raleway {
|
||||
font-family: Raleway;
|
||||
}
|
||||
|
||||
.font-ssp {
|
||||
font-family: "Source Sans Pro";
|
||||
}
|
||||
|
||||
.font-helvetica {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.pencil-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.force-contents-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.md-shadow-on-hover:hover {
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 8px 16px 0px !important;
|
||||
}
|
||||
|
||||
.text-company {
|
||||
color: #04385c;
|
||||
}
|
||||
|
||||
.company-bg {
|
||||
background-color: #04385c !important;
|
||||
}
|
||||
|
||||
.underline-company {
|
||||
text-decoration: underline solid #04385c;
|
||||
}
|
||||
|
||||
.border-company {
|
||||
border-color: #04385c !important;
|
||||
}
|
||||
|
||||
.btn-company {
|
||||
color: white;
|
||||
background-color: #04385c;
|
||||
}
|
||||
|
||||
.btn-company:hover {
|
||||
color: white;
|
||||
background-color: #002d51;
|
||||
}
|
||||
|
||||
.btn-outline-company {
|
||||
border: 1px solid #04385c;
|
||||
}
|
||||
|
||||
.btn-outline-company:hover {
|
||||
color: white;
|
||||
background-color: #04385c;
|
||||
border: 1px solid #04385c;
|
||||
}
|
||||
|
||||
|
||||
.hover-only-bg:not(:hover) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
input[type='radio']:checked {
|
||||
background-color: #04385c;
|
||||
border: 1px solid #04385c;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.light-tooltip {
|
||||
--bs-tooltip-bg: #04385c;
|
||||
}
|
||||
|
||||
html, body {
|
||||
overflow: hidden !important;
|
||||
max-width: 100vw;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
body {
|
||||
/* body {
|
||||
background-color: #0d5979;
|
||||
}
|
||||
} */
|
||||
|
||||
.bd-placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
@ -53,7 +53,7 @@ body {
|
||||
-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 @@ body {
|
||||
.card:hover{
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,.12), 0 4px 8px rgba(0,0,0,.06);
|
||||
}
|
||||
} */
|
0
src/static/css/mainapp/anglers.css
Normal file
173
src/static/css/ocean.css
Normal file
@ -0,0 +1,173 @@
|
||||
#ocean {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-image: linear-gradient(0deg, #182848, #2980b9)
|
||||
}
|
||||
|
||||
.bubble {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 100%;
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
bottom: -30px;
|
||||
opacity: 0.2;
|
||||
animation: bubble 15s ease-in-out infinite, sideWays 4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.fish {
|
||||
top: 50%;
|
||||
left: 20%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 100%;
|
||||
position: absolute;
|
||||
background-color: yellow;
|
||||
bottom: -30px;
|
||||
opacity: 0.6;
|
||||
animation: swimHorizontal 35s ease-in-out, swimVertical 4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.fish--1 {
|
||||
top: 75%;
|
||||
}
|
||||
|
||||
@keyframes swimHorizontal {
|
||||
0% {
|
||||
margin-left: 1500px;
|
||||
}
|
||||
100% {
|
||||
margin-left: -1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swimVertical {
|
||||
0% {
|
||||
margin-top: 0;
|
||||
}
|
||||
100% {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bubble {
|
||||
0% {
|
||||
transform: translateY(0%);
|
||||
opacity: 0.06;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-120vh);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sideWays {
|
||||
0% {
|
||||
margin-left: 0px;
|
||||
}
|
||||
100% {
|
||||
margin-left: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble--1 {
|
||||
left: 10%;
|
||||
animation-delay: 0.5s;
|
||||
animation-duration: 16s;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.bubble--2 {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
left: 40%;
|
||||
animation-delay: 1s;
|
||||
animation-duration: 10s;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.bubble--3 {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
left: 30%;
|
||||
animation-delay: 5s;
|
||||
animation-duration: 20s;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.bubble--4 {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
left: 40%;
|
||||
animation-delay: 8s;
|
||||
animation-duration: 17s;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.bubble--5 {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
left: 60%;
|
||||
animation-delay: 10s;
|
||||
animation-duration: 15s;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.bubble--6 {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
left: 80%;
|
||||
animation-delay: 3s;
|
||||
animation-duration: 30s;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.bubble--7 {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
left: 90%;
|
||||
animation-delay: -7s;
|
||||
animation-duration: 25s;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.bubble--9 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
left: 50%;
|
||||
bottom: 30px;
|
||||
animation-delay: -5s;
|
||||
animation-duration: 19s;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.bubble--10 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
left: 30%;
|
||||
bottom: 30px;
|
||||
animation-delay: -21s;
|
||||
animation-duration: 16s;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.bubble--11 {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
left: 60%;
|
||||
bottom: 30px;
|
||||
animation-delay: -13.75s;
|
||||
animation-duration: 20s;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.bubble--11 {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
left: 90%;
|
||||
bottom: 30px;
|
||||
animation-delay: -10.5s;
|
||||
animation-duration: 19s;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
BIN
src/static/fonts/Montserrat/Black.ttf
Normal file
BIN
src/static/fonts/Montserrat/BlackItalic.ttf
Normal file
BIN
src/static/fonts/Montserrat/Bold.ttf
Normal file
BIN
src/static/fonts/Montserrat/BoldItalic.ttf
Normal file
BIN
src/static/fonts/Montserrat/ExtraBold.ttf
Normal file
BIN
src/static/fonts/Montserrat/ExtraBoldItalic.ttf
Normal file
BIN
src/static/fonts/Montserrat/ExtraLight.ttf
Normal file
BIN
src/static/fonts/Montserrat/ExtraLightItalic.ttf
Normal file
BIN
src/static/fonts/Montserrat/Italic.ttf
Normal file
BIN
src/static/fonts/Montserrat/Light.ttf
Normal file
BIN
src/static/fonts/Montserrat/LightItalic.ttf
Normal file
BIN
src/static/fonts/Montserrat/Medium.ttf
Normal file
BIN
src/static/fonts/Montserrat/MediumItalic.ttf
Normal file
BIN
src/static/fonts/Montserrat/Regular.ttf
Normal file
BIN
src/static/fonts/Montserrat/SemiBold.ttf
Normal file
BIN
src/static/fonts/Montserrat/SemiBoldItalic.ttf
Normal file
BIN
src/static/fonts/Montserrat/Thin.ttf
Normal file
BIN
src/static/fonts/Montserrat/ThinItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/Black.ttf
Normal file
BIN
src/static/fonts/Raleway/BlackItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/Bold.ttf
Normal file
BIN
src/static/fonts/Raleway/BoldItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/ExtraBold.ttf
Normal file
BIN
src/static/fonts/Raleway/ExtraBoldItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/ExtraLight.ttf
Normal file
BIN
src/static/fonts/Raleway/ExtraLightItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/Italic.ttf
Normal file
BIN
src/static/fonts/Raleway/Light.ttf
Normal file
BIN
src/static/fonts/Raleway/LightItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/Medium.ttf
Normal file
BIN
src/static/fonts/Raleway/MediumItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/Regular.ttf
Normal file
BIN
src/static/fonts/Raleway/SemiBold.ttf
Normal file
BIN
src/static/fonts/Raleway/SemiBoldItalic.ttf
Normal file
BIN
src/static/fonts/Raleway/Thin.ttf
Normal file
BIN
src/static/fonts/Raleway/ThinItalic.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/Black.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/BlackItalic.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/Bold.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/BoldItalic.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/ExtraLight.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/ExtraLightItalic.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/Italic.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/Light.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/LightItalic.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/Regular.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/SemiBold.ttf
Normal file
BIN
src/static/fonts/SourceSansPro/SemiBoldItalic.ttf
Normal file
4
src/static/icons/0-circle-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 479 B |
4
src/static/icons/0-circle.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99ZM8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 511 B |
4
src/static/icons/0-square-fill.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2Zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 518 B |
4
src/static/icons/0-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99ZM8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2Zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 585 B |
3
src/static/icons/1-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312V4.002Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 257 B |
3
src/static/icons/1-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8Zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383h1.312Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 287 B |
3
src/static/icons/1-square-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2Zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383h1.312Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 294 B |
4
src/static/icons/1-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square" viewBox="0 0 16 16">
|
||||
<path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383h1.312Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2Zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 376 B |
3
src/static/icons/123.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
|
||||
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961h1.174Zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057h1.138Zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75v.96Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 870 B |
3
src/static/icons/2-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 458 B |
3
src/static/icons/2-circle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8Zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 480 B |
3
src/static/icons/2-square-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2Zm4.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 487 B |
4
src/static/icons/2-square.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square" viewBox="0 0 16 16">
|
||||
<path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306Z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2Zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
3
src/static/icons/3-circle-fill.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-8.082.414c.92 0 1.535.54 1.541 1.318.012.791-.615 1.36-1.588 1.354-.861-.006-1.482-.469-1.54-1.066H5.104c.047 1.177 1.05 2.144 2.754 2.144 1.653 0 2.954-.937 2.93-2.396-.023-1.278-1.031-1.846-1.734-1.916v-.07c.597-.1 1.505-.739 1.482-1.876-.03-1.177-1.043-2.074-2.637-2.062-1.675.006-2.59.984-2.625 2.12h1.248c.036-.556.557-1.054 1.348-1.054.785 0 1.348.486 1.348 1.195.006.715-.563 1.237-1.342 1.237h-.838v1.072h.879Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 608 B |