Compare commits
35 Commits
bc0166b3ce
...
8d3b20e971
Author | SHA1 | Date | |
---|---|---|---|
8d3b20e971 | |||
cc31d447b8 | |||
c5c1918ee9 | |||
4a69aec513 | |||
17b6b09737 | |||
d1bb8bb62a | |||
385198fcb2 | |||
85f5b5b1b4 | |||
0aa53a8080 | |||
6a3bb974b9 | |||
fb2b483d43 | |||
7762b830ec | |||
5772b0c3eb | |||
00441acf08 | |||
a89cbdd02a | |||
f433da1b20 | |||
3fe2bd4f0f | |||
c37f69f64f | |||
3d64b410d5 | |||
5f8b062058 | |||
0c914eeb06 | |||
b8f40d888a | |||
43d13fed27 | |||
5427c676cc | |||
ed521d2f6c | |||
ecf81a86b3 | |||
0bb4bc9ffe | |||
a44f597790 | |||
33193ef053 | |||
03fd41e359 | |||
5f7ba001ac | |||
33de71ba71 | |||
a5f6f51f11 | |||
32d77d1737 | |||
42a8ae9c20 |
6
.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
# Compressed / cached static files
|
||||
src/static/CACHE/
|
||||
# Static & media files
|
||||
staticfiles/
|
||||
static/CACHE/
|
||||
media/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
@ -3,4 +3,5 @@ from django.apps import AppConfig
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api'
|
||||
name = 'apps.api'
|
||||
label = "apps_api"
|
28
apps/api/serializers.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Serializers for the API app."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.home import models
|
||||
|
||||
|
||||
class VenueAddressSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.VenueAddress
|
||||
exclude = []
|
||||
|
||||
|
||||
class VenueContactsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.VenueContacts
|
||||
exclude = []
|
||||
|
||||
|
||||
class VenueSerializer(serializers.ModelSerializer):
|
||||
|
||||
address = VenueAddressSerializer()
|
||||
contacts = VenueContactsSerializer()
|
||||
|
||||
class Meta:
|
||||
model = models.Venue
|
||||
exclude = []
|
||||
|
14
apps/api/urls.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""URLs for the API app."""
|
||||
|
||||
from django.urls import path, include
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("venues/", include([
|
||||
path("", views.VenueListView.as_view(), name="venues"),
|
||||
path("<int:pk>/", views.VenueDetailsView.as_view(), name="venue-detail")
|
||||
]))
|
||||
]
|
@ -1,14 +1,16 @@
|
||||
"""Views for the API app."""
|
||||
|
||||
from django.shortcuts import render
|
||||
from rest_framework import generics
|
||||
from .serializers import VenueSerializer
|
||||
from mainapp.models import Venue
|
||||
|
||||
# Create your views here.
|
||||
from .serializers import VenueSerializer
|
||||
from apps.home import models
|
||||
|
||||
class VenueListView(generics.ListCreateAPIView):
|
||||
serializer_class = VenueSerializer
|
||||
queryset = Venue.objects.all().order_by("name")
|
||||
|
||||
class VenueDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
queryset = models.Venue.objects.all().order_by("id")
|
||||
|
||||
|
||||
class VenueDetailsView(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = VenueSerializer
|
||||
queryset = Venue.objects.all().order_by("name")
|
||||
queryset = models.Venue.objects.all().order_by("id")
|
60
apps/home/admin.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Admin models for the home app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.Angler)
|
||||
class Angler(admin.ModelAdmin):
|
||||
"""Admin class for the `Angler` model."""
|
||||
|
||||
|
||||
@admin.register(models.AnglerGroup)
|
||||
class AnglerGroup(admin.ModelAdmin):
|
||||
"""Admin class for the `AnglerGroup` model."""
|
||||
|
||||
|
||||
@admin.register(models.Waters)
|
||||
class Waters(admin.ModelAdmin):
|
||||
"""Admin class for the `Waters` model."""
|
||||
|
||||
|
||||
@admin.register(models.VenueAddress)
|
||||
class VenueAddress(admin.ModelAdmin):
|
||||
"""Admin class for the `VenueAddress` model."""
|
||||
|
||||
|
||||
@admin.register(models.VenueContacts)
|
||||
class VenueContacts(admin.ModelAdmin):
|
||||
"""Admin class for the `VenueContacts` model."""
|
||||
|
||||
|
||||
@admin.register(models.Venue)
|
||||
class Venue(admin.ModelAdmin):
|
||||
"""Admin class for the `Venue` model."""
|
||||
|
||||
|
||||
@admin.register(models.Match)
|
||||
class Match(admin.ModelAdmin):
|
||||
"""Admin class for the `Match` model."""
|
||||
|
||||
|
||||
@admin.register(models.LeagueRule)
|
||||
class LeagueRule(admin.ModelAdmin):
|
||||
"""Admin class for the `LeagueRule` model."""
|
||||
|
||||
|
||||
@admin.register(models.League)
|
||||
class League(admin.ModelAdmin):
|
||||
"""Admin class for the `League` model."""
|
||||
|
||||
|
||||
@admin.register(models.Sponsor)
|
||||
class Sponsor(admin.ModelAdmin):
|
||||
"""Admin class for the `Sponsor` model."""
|
||||
|
||||
|
||||
@admin.register(models.LeagueResult)
|
||||
class LeagueResult(admin.ModelAdmin):
|
||||
"""Admin class for the `LeagueResult` model."""
|
@ -3,4 +3,5 @@ from django.apps import AppConfig
|
||||
|
||||
class MainappConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'mainapp'
|
||||
name = 'apps.home'
|
||||
label = "apps_home"
|
218
apps/home/fixtures/dummy_anglers.json
Normal file
@ -0,0 +1,218 @@
|
||||
[
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "William Howard",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Thomas Henderson",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "David Robertson",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "Charles Willis",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Richard Moore",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "James Andrews",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Christopher Pines",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "Darren Boyd",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"name": "Jenson Hammond",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"name": "Dylan Berry",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"name": "Tori Wright",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 12,
|
||||
"fields": {
|
||||
"name": "Rahul Finch",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 13,
|
||||
"fields": {
|
||||
"name": "Gary Whitney",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 14,
|
||||
"fields": {
|
||||
"name": "Martin Sanford",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 15,
|
||||
"fields": {
|
||||
"name": "Kyle Bird",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 16,
|
||||
"fields": {
|
||||
"name": "Edward Brandt",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 17,
|
||||
"fields": {
|
||||
"name": "Layton Holt",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 18,
|
||||
"fields": {
|
||||
"name": "Adam Hayes",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 19,
|
||||
"fields": {
|
||||
"name": "Leo Duran",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 20,
|
||||
"fields": {
|
||||
"name": "Noah Sims",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 21,
|
||||
"fields": {
|
||||
"name": "Louis Holloway",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 22,
|
||||
"fields": {
|
||||
"name": "Sean Foster",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 23,
|
||||
"fields": {
|
||||
"name": "Josh Lambert",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 24,
|
||||
"fields": {
|
||||
"name": "Evan Bass",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 25,
|
||||
"fields": {
|
||||
"name": "Marcus Brooke",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 26,
|
||||
"fields": {
|
||||
"name": "Cory Cohen",
|
||||
"redact": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.angler",
|
||||
"pk": 27,
|
||||
"fields": {
|
||||
"name": "Nadia Graves",
|
||||
"redact": false
|
||||
}
|
||||
}
|
||||
]
|
84
apps/home/fixtures/dummy_venue_and_waters.json
Normal file
@ -0,0 +1,84 @@
|
||||
[
|
||||
{
|
||||
"model": "apps_home.venueaddress",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"street_number": null,
|
||||
"street_address": "Shrewley Common",
|
||||
"town": "Warwick",
|
||||
"provence": "Warwickshire",
|
||||
"post_code": "CV327AN",
|
||||
"satnav_post_code": null,
|
||||
"country": "United Kingdom",
|
||||
"latitude": null,
|
||||
"longitude": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.venuecontacts",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"phone_number": "07795546160",
|
||||
"email_address": "admin@tbftackle.com",
|
||||
"website_url": "http://www.tunnelbarnfarm.co.uk/",
|
||||
"facebook_url": "https://facebook.com/TunnelBarnFarm",
|
||||
"twitter_url": "https://twitter.com/TunnelBarnFarm",
|
||||
"instagram_url": "https://instagram.com/tunnel_barn_farm_fishery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.venue",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Tunnel Barn Farm",
|
||||
"description": "Located in the picturesque village of Shrewley in Warwickshire, Tunnel Barn Farm is the Midlands' premier fishery, with its own onsite Tackle Shop, offering day tickets for pleasure fishermen, daily competitions for match anglers (including and Over 50s and Veterans club) and hosting club matches all year round. We have 9 unique pools that range in size and style and that are stoc",
|
||||
"extra_notes": null,
|
||||
"created_at": "2024-11-05T18:49:53.317Z",
|
||||
"updated_at": "2024-11-05T18:49:53.318Z",
|
||||
"profile_picture": "",
|
||||
"banner_picture": "",
|
||||
"type": 2,
|
||||
"address": 1,
|
||||
"contacts": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.waters",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Canal Pool",
|
||||
"pegs_from": 1,
|
||||
"pegs_to": 24,
|
||||
"map": "",
|
||||
"venue": 1,
|
||||
"type": 0,
|
||||
"fish_types": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.waters",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Jennys Pool",
|
||||
"pegs_from": 1,
|
||||
"pegs_to": 27,
|
||||
"map": "",
|
||||
"venue": 1,
|
||||
"type": 0,
|
||||
"fish_types": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "apps_home.waters",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "New Pool",
|
||||
"pegs_from": 1,
|
||||
"pegs_to": 44,
|
||||
"map": "",
|
||||
"venue": 1,
|
||||
"type": 0,
|
||||
"fish_types": 0
|
||||
}
|
||||
}
|
||||
]
|
231
apps/home/migrations/0001_initial.py
Normal file
@ -0,0 +1,231 @@
|
||||
# Generated by Django 5.0.6 on 2024-11-05 12:49
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Angler',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('redact', models.BooleanField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'angler',
|
||||
'verbose_name_plural': 'anglers',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeagueRule',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('ranking_system', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('points_allocation', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('points_awarded', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('place_secondly_by_weight', models.BooleanField()),
|
||||
('team_places_secondly_by_section', models.BooleanField()),
|
||||
('section_placed_positions', models.IntegerField()),
|
||||
('worst_place_limits', models.BooleanField()),
|
||||
('attendance_points', models.IntegerField()),
|
||||
('did_not_weigh', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('did_not_weigh_value', models.IntegerField()),
|
||||
('left_early', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('left_early_value', models.IntegerField()),
|
||||
('did_not_book', models.IntegerField()),
|
||||
('disqualification', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('disqualification_value', models.IntegerField()),
|
||||
('best_league_sessions', models.IntegerField()),
|
||||
('worst_league_sessions', models.IntegerField()),
|
||||
('match_placed_positions', models.IntegerField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'league rule',
|
||||
'verbose_name_plural': 'league rules',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Match',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.CharField(max_length=384)),
|
||||
('meeting_point', models.CharField(max_length=1024)),
|
||||
('use_metric', models.BooleanField()),
|
||||
('allow_in_tournaments', models.BooleanField()),
|
||||
('start_datetime', models.DateTimeField()),
|
||||
('end_datetime', models.DateTimeField()),
|
||||
('draw_datetime', models.DateTimeField()),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('competitor_type', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'match',
|
||||
'verbose_name_plural': 'matches',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Sponsor',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('url', models.URLField()),
|
||||
('image', models.ImageField(upload_to='')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'sponsor',
|
||||
'verbose_name_plural': 'sponsors',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Venue',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.CharField(max_length=384)),
|
||||
('extra_notes', models.CharField(max_length=1028)),
|
||||
('created_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
|
||||
('updated_at', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
|
||||
('profile_picture', models.ImageField(upload_to='')),
|
||||
('banner_picture', models.ImageField(upload_to='')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(0, 'Fishery'), (1, 'Club'), (2, 'Private')])),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'venue',
|
||||
'verbose_name_plural': 'venues',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VenueAddress',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('street_number', models.IntegerField()),
|
||||
('street_address', models.CharField(max_length=256)),
|
||||
('town', models.CharField(max_length=256)),
|
||||
('county', models.CharField(max_length=256)),
|
||||
('post_code', models.CharField(max_length=32)),
|
||||
('satnav_post_code', models.CharField(max_length=32)),
|
||||
('country', models.CharField(max_length=128)),
|
||||
('latitude', models.DecimalField(decimal_places=16, max_digits=22)),
|
||||
('longitude', models.DecimalField(decimal_places=16, max_digits=22)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'venue address',
|
||||
'verbose_name_plural': 'venue addresses',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VenueContacts',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('phone_number', models.CharField(max_length=64)),
|
||||
('email_address', models.EmailField(max_length=254)),
|
||||
('website_url', models.URLField()),
|
||||
('facebook_url', models.URLField()),
|
||||
('twitter_url', models.URLField()),
|
||||
('instagram_url', models.URLField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'venue contacts',
|
||||
'verbose_name_plural': 'venue contacts',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Waters',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('pegs_from', models.IntegerField()),
|
||||
('pegs_to', models.IntegerField()),
|
||||
('map', models.ImageField(upload_to='')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(0, 'Commercial Water'), (1, 'Natural Still Water'), (2, 'Canal'), (3, 'River'), (4, 'Loch')])),
|
||||
('fish_types', models.PositiveSmallIntegerField(choices=[(0, 'Coarse'), (1, 'Specimen Carp'), (2, 'Game'), (3, 'Predator')])),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'waters',
|
||||
'verbose_name_plural': 'waters',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AnglerGroup',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(0, 'choice 1'), (1, 'choice 2'), (2, 'choice 3')])),
|
||||
('anglers', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apps_home.angler')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'angler group',
|
||||
'verbose_name_plural': 'angler groups',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='League',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128)),
|
||||
('description', models.CharField(max_length=384)),
|
||||
('extra_notes', models.CharField(max_length=1028)),
|
||||
('profile_picture', models.ImageField(upload_to='')),
|
||||
('banner_picture', models.ImageField(upload_to='')),
|
||||
('anglers', models.ManyToManyField(to='apps_home.angler')),
|
||||
('rules', models.ManyToManyField(to='apps_home.leaguerule')),
|
||||
('matches', models.ManyToManyField(to='apps_home.match')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'league',
|
||||
'verbose_name_plural': 'leagues',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LeagueResult',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('total_weight', models.CharField(max_length=64)),
|
||||
('matches', models.IntegerField()),
|
||||
('date', models.DateField(default=django.utils.timezone.now)),
|
||||
('angler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apps_home.angler')),
|
||||
('league', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apps_home.league')),
|
||||
('sponsor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='apps_home.sponsor')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'league result',
|
||||
'verbose_name_plural': 'league results',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='match',
|
||||
name='venue',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apps_home.venue'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='venue',
|
||||
name='address',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apps_home.venueaddress'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='venue',
|
||||
name='contacts',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apps_home.venuecontacts'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='venue',
|
||||
name='waters',
|
||||
field=models.ManyToManyField(to='apps_home.waters'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='match',
|
||||
name='waters',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='apps_home.waters'),
|
||||
),
|
||||
]
|
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.6 on 2024-11-05 13:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apps_home', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='venueaddress',
|
||||
old_name='county',
|
||||
new_name='provence',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venueaddress',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venueaddress',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.6 on 2024-11-05 17:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apps_home', '0002_rename_county_venueaddress_provence_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='venue',
|
||||
name='waters',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waters',
|
||||
name='venue',
|
||||
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='apps_home.venue'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='anglergroup',
|
||||
name='type',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'team type 1'), (1, 'team type 2'), (2, 'team type 3')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='match',
|
||||
name='description',
|
||||
field=models.CharField(max_length=1024),
|
||||
),
|
||||
]
|
@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.0.6 on 2024-11-05 18:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apps_home', '0003_remove_venue_waters_waters_venue_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='anglergroup',
|
||||
name='anglers',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='anglergroup',
|
||||
name='anglers',
|
||||
field=models.ManyToManyField(to='apps_home.angler'),
|
||||
),
|
||||
]
|
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.6 on 2024-11-05 18:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apps_home', '0004_remove_anglergroup_anglers_anglergroup_anglers'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='venuecontacts',
|
||||
name='email_address',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuecontacts',
|
||||
name='facebook_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuecontacts',
|
||||
name='instagram_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuecontacts',
|
||||
name='phone_number',
|
||||
field=models.CharField(blank=True, max_length=64, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuecontacts',
|
||||
name='twitter_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venuecontacts',
|
||||
name='website_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.6 on 2024-11-05 18:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apps_home', '0005_alter_venuecontacts_email_address_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='banner_picture',
|
||||
field=models.ImageField(blank=True, null=True, upload_to=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='description',
|
||||
field=models.CharField(blank=True, max_length=384, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='extra_notes',
|
||||
field=models.CharField(blank=True, max_length=1028, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='profile_picture',
|
||||
field=models.ImageField(blank=True, null=True, upload_to=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venueaddress',
|
||||
name='satnav_post_code',
|
||||
field=models.CharField(blank=True, max_length=32, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venueaddress',
|
||||
name='street_number',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
471
apps/home/models.py
Normal file
@ -0,0 +1,471 @@
|
||||
"""Models for the mainapp."""
|
||||
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
# region Anglers & Groups
|
||||
|
||||
class Angler(models.Model):
|
||||
"""A participant of one or more events.
|
||||
|
||||
Attributes:
|
||||
id (int): Automatically incrementing identifier number.
|
||||
name (str): Full name or label describing the angler.
|
||||
redact (bool): Determines if the `name` attr should be redacted.
|
||||
"""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
redact = models.BooleanField()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "angler"
|
||||
verbose_name_plural = "anglers"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AnglerGroup(models.Model):
|
||||
"""A group of one or more anglers.
|
||||
|
||||
Attributes:
|
||||
id (int): Automatically incrementing identifier number.
|
||||
name (str): A human-readable label to identify this group.
|
||||
anglers (list of `Angler`): The members of this group.
|
||||
type (int): An enum-like value representing the type of group.
|
||||
"""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
anglers = models.ManyToManyField(to=Angler)
|
||||
|
||||
TYPES = (
|
||||
(0, "Set"), # Collection of commonly used anglers to aide in their selection for a match or league.
|
||||
(1, "Team"), # Collection of anglers that acts as a single competing unit in a match or league.
|
||||
(2, "Pair") # Two anglers that acts as a single competing unit in a match or league.
|
||||
)
|
||||
|
||||
type = models.PositiveSmallIntegerField(choices=TYPES)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "angler group"
|
||||
verbose_name_plural = "angler groups"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.anglers.count} anglers)"
|
||||
|
||||
|
||||
# region Venues & Waters
|
||||
|
||||
|
||||
class VenueAddress(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
street_number = models.IntegerField(null=True, blank=True)
|
||||
street_address = models.CharField(max_length=256)
|
||||
town = models.CharField(max_length=256)
|
||||
provence = models.CharField(max_length=256)
|
||||
post_code = models.CharField(max_length=32)
|
||||
satnav_post_code = models.CharField(max_length=32, null=True, blank=True)
|
||||
country = models.CharField(max_length=128)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "venue address"
|
||||
verbose_name_plural = "venue addresses"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.street_address}, {self.town} ({self.country})"
|
||||
|
||||
|
||||
class VenueContacts(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
phone_number = models.CharField(max_length=64, null=True, blank=True)
|
||||
email_address = models.EmailField(null=True, blank=True)
|
||||
website_url = models.URLField(null=True, blank=True)
|
||||
facebook_url = models.URLField(null=True, blank=True)
|
||||
twitter_url = models.URLField(null=True, blank=True)
|
||||
instagram_url = models.URLField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "venue contacts"
|
||||
verbose_name_plural = "venue contacts"
|
||||
|
||||
def __str__(self):
|
||||
return self.email_address or str(self.id)
|
||||
|
||||
|
||||
class Venue(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=384, null=True, blank=True)
|
||||
extra_notes = models.CharField(max_length=1028, null=True, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(default=timezone.now, editable=False)
|
||||
updated_at = models.DateTimeField(default=timezone.now, editable=False)
|
||||
|
||||
profile_picture = models.ImageField(null=True, blank=True)
|
||||
banner_picture = models.ImageField(null=True, blank=True)
|
||||
|
||||
class Types:
|
||||
FISHERY = 0
|
||||
CLUB = 1
|
||||
PRIVATE = 2
|
||||
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=(
|
||||
(Types.FISHERY, "Fishery"),
|
||||
(Types.CLUB, "Club"),
|
||||
(Types.PRIVATE, "Private")
|
||||
)
|
||||
)
|
||||
|
||||
address = models.ForeignKey(to=VenueAddress, on_delete=models.CASCADE)
|
||||
contacts = models.ForeignKey(to=VenueContacts, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "venue"
|
||||
verbose_name_plural = "venues"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.updated_at = timezone.now()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def waters(self):
|
||||
"""Returns all waters belonging to this venue."""
|
||||
return Waters.objects.filter(venue=self)
|
||||
|
||||
|
||||
class Waters(models.Model):
|
||||
"""A body of water belonging to a particular venue.
|
||||
|
||||
Attributes:
|
||||
id (int): Automatically incrementing identifier number.
|
||||
name (str): A human-readable label to identify this item.
|
||||
pegs_from (int): The lowest peg number available.
|
||||
pegs_to (int): The highest peg number available.
|
||||
map (file): An image map showing the body of water.
|
||||
venue (`Venue`): The real world venue where this waters can be found.
|
||||
type (int): The type of waters represented (canal, river, etc...)
|
||||
fish_type (int): The fish categories that can be found in this water.
|
||||
"""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
pegs_from = models.IntegerField()
|
||||
pegs_to = models.IntegerField()
|
||||
map = models.ImageField(null=True, blank=True)
|
||||
venue = models.ForeignKey(to=Venue, on_delete=models.CASCADE)
|
||||
|
||||
class Types:
|
||||
COMMERCIAL = 0
|
||||
NATURAL_STILL = 1
|
||||
CANAL = 2
|
||||
RIVER = 3
|
||||
LOCH = 4
|
||||
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=(
|
||||
(Types.COMMERCIAL, "Commercial Water"),
|
||||
(Types.NATURAL_STILL, "Natural Still Water"),
|
||||
(Types.CANAL, "Canal"),
|
||||
(Types.RIVER, "River"),
|
||||
(Types.LOCH, "Loch")
|
||||
)
|
||||
)
|
||||
|
||||
class FishTypes:
|
||||
COARSE = 0
|
||||
SPECIMEN_CARP = 1
|
||||
GAME = 2
|
||||
PREDATOR = 3
|
||||
|
||||
fish_types = models.PositiveSmallIntegerField(
|
||||
choices=(
|
||||
(FishTypes.COARSE, "Coarse"),
|
||||
(FishTypes.SPECIMEN_CARP, "Specimen Carp"),
|
||||
(FishTypes.GAME, "Game"),
|
||||
(FishTypes.PREDATOR, "Predator")
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "waters"
|
||||
verbose_name_plural = "waters"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
# region Leagues & Matches
|
||||
|
||||
|
||||
class Match(models.Model):
|
||||
"""Represents an a fishing event/competition.
|
||||
|
||||
Attributes:
|
||||
id (int): Automatically incrementing identifier number.
|
||||
name (str): A human-readable label to identify this item.
|
||||
description (str): A detailed description of the event.
|
||||
venue (`Venue`): Where this match is hosted.
|
||||
waters (`Waters`): Which waters will be used in this match, can only use waters from the assigned venue.
|
||||
meeting_point (str): TODO: what is this for?
|
||||
use_metric (bool): Should the metric system be used in measurements, imperial is used if `False`.
|
||||
allow_in_tournaments (bool): Should this match be allowed in tournaments? (TODO: is this correct?)
|
||||
start_datetime (datetime): When the match will begin.
|
||||
end_datetime (datetime): When the match will finish.
|
||||
draw_datetime (datetime): When the draw is made (during the match).
|
||||
type (int): TODO: match types?
|
||||
competitor_type (int): TODO what are these?
|
||||
"""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=1024)
|
||||
|
||||
venue = models.ForeignKey(to=Venue, on_delete=models.CASCADE)
|
||||
waters = models.ForeignKey(to=Waters, on_delete=models.CASCADE) # can only select waters from the matching venue
|
||||
|
||||
meeting_point = models.CharField(max_length=1024)
|
||||
use_metric = models.BooleanField()
|
||||
allow_in_tournaments = models.BooleanField()
|
||||
|
||||
start_datetime = models.DateTimeField()
|
||||
end_datetime = models.DateTimeField()
|
||||
draw_datetime = models.DateTimeField()
|
||||
|
||||
TYPES = (
|
||||
(0, "Club Match"),
|
||||
(1, "Open Match"),
|
||||
(2, "Majors")
|
||||
)
|
||||
type = models.PositiveSmallIntegerField(choices=TYPES)
|
||||
|
||||
# TODO: this might be wrong, maybe should inherit value from related league or league rules
|
||||
COMPETITOR_TYPES = (
|
||||
(0, "Individuals"),
|
||||
(1, "Pairs"),
|
||||
(2, "Teams")
|
||||
)
|
||||
competitor_type = models.PositiveSmallIntegerField(choices=COMPETITOR_TYPES)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "match"
|
||||
verbose_name_plural = "matches"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class LeagueRule(models.Model):
|
||||
"""Rules for leagues.
|
||||
|
||||
Rules can be reused against multiple leagues, and aren't strictly owned by
|
||||
any particular `League` instance.
|
||||
|
||||
Attributes:
|
||||
id (int): Automatically incrementing identifier number.
|
||||
name (str): A human-readable label to identify this item.
|
||||
ranking_system (int): TODO
|
||||
points_allocation (int): Whether people win by having the lowest or heighest points.
|
||||
points_awarded (int): TODO
|
||||
place_secondly_by_weight (bool): TODO
|
||||
team_places_secondly_by_section (bool): TODO
|
||||
section_placed_positions = (int): TODO
|
||||
worst_place_limits (bool): TODO
|
||||
attendance_points (int): TODO
|
||||
did_not_weigh (int): TODO
|
||||
did_not_weigh_value (int): TODO
|
||||
left_early (int): TODO
|
||||
left_early_value (int): TODO
|
||||
did_not_book (int): TODO
|
||||
did_not_book_value (int): TODO
|
||||
disqualification (int): TODO
|
||||
disqualification_value (int): TODO
|
||||
best_league_sessions (int): TODO
|
||||
worst_league_sessions (int): TODO
|
||||
match_placed_positions (int): TODO
|
||||
"""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
|
||||
RANKING_SYSTEMS = (
|
||||
(0, "By Points"),
|
||||
(1, "By Weight")
|
||||
)
|
||||
ranking_system = models.PositiveSmallIntegerField(choices=RANKING_SYSTEMS)
|
||||
|
||||
POINTS_ALLOCATIONS = (
|
||||
(0, "First Place Low"),
|
||||
(1, "First Place High")
|
||||
)
|
||||
points_allocation = models.PositiveSmallIntegerField(choices=POINTS_ALLOCATIONS)
|
||||
|
||||
POINTS_AWARDED = (
|
||||
(0, "Per Section"),
|
||||
(1, "Per Zone"), # TODO: what is a zone?
|
||||
(2, "Per Match")
|
||||
)
|
||||
points_awarded = models.PositiveSmallIntegerField(choices=POINTS_AWARDED)
|
||||
|
||||
# Use fish weight as a fallback for anglers with matching points? Otherwise they get the same placement.
|
||||
place_secondly_by_weight = models.BooleanField()
|
||||
|
||||
# False:
|
||||
# people at the end have the same amount of points, they both come first
|
||||
# True:
|
||||
# two people have same number of points, winner is the section with the highest section placement
|
||||
team_places_secondly_by_section = models.BooleanField()
|
||||
|
||||
# The number of awarded placements per section
|
||||
section_placed_positions = models.IntegerField() # max: 200, min: 0
|
||||
|
||||
# TODO: waiting on james to ask kelly for clarification on this.
|
||||
worst_place_limits = models.BooleanField()
|
||||
|
||||
# Number of points adjusted for attending a match:
|
||||
# if points allocation == 0: adjusted = deducted
|
||||
# if points allocation == 1: adjusted = added
|
||||
attendance_points = models.IntegerField() # max: 20, min: 0
|
||||
|
||||
# Impact on points if an angler hasn't weighed his fish
|
||||
DID_NOT_WEIGHS = (
|
||||
(0, "Threshold Points"), # TODO: waiting on description
|
||||
(1, "Worst Place Points"), # The points given for a DNW are equal to the points of last place
|
||||
(2, "Incremental Points") # A fixed number of points added for a DWN
|
||||
(2, "Absolute Points") # A fixed number of points given for a DNW
|
||||
(2, "Not Scored") # Don't count to overall points
|
||||
)
|
||||
did_not_weigh = models.PositiveSmallIntegerField(choices=DID_NOT_WEIGHS)
|
||||
did_not_weigh_value = models.IntegerField()
|
||||
|
||||
# Same as DNW, except is invoked because the angler left before the match end.
|
||||
LEFT_EARLYS = DID_NOT_WEIGHS
|
||||
left_early = models.PositiveSmallIntegerField(choices=LEFT_EARLYS)
|
||||
left_early_value = models.IntegerField()
|
||||
|
||||
# Same as DNW, except invoked for not showing up.
|
||||
# This is used to balance teams who are missing players.
|
||||
NO_SHOWS = (
|
||||
(0, "Threshold Points"),
|
||||
(1, "Worst Place Points"),
|
||||
(2, "Absolute Points"),
|
||||
(3, "Not Scored")
|
||||
)
|
||||
no_show = models.PositiveSmallIntegerField(choices=NO_SHOWS)
|
||||
no_show_value = models.IntegerField()
|
||||
|
||||
# Same as Now Show, except for angler's who didn't book their place
|
||||
DID_NOT_BOOKS = NO_SHOWS
|
||||
did_not_book = models.PositiveSmallIntegerField(choices=DID_NOT_BOOKS)
|
||||
did_not_book = models.IntegerField()
|
||||
|
||||
# Same as DNW, except invoked when an angler is disqualified.
|
||||
DISQUALIFICATIONS = DID_NOT_WEIGHS
|
||||
disqualification = models.PositiveSmallIntegerField(choices=DISQUALIFICATIONS)
|
||||
disqualification_value = models.IntegerField()
|
||||
|
||||
# TODO
|
||||
best_league_sessions = models.IntegerField() # max: 20, min: 0
|
||||
worst_league_sessions = models.IntegerField() # max: 20, min: 0
|
||||
match_placed_positions = models.IntegerField() # max: 200, min: 0
|
||||
|
||||
class Meta:
|
||||
verbose_name = "league rule"
|
||||
verbose_name_plural = "league rules"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class League(models.Model):
|
||||
"""A League of matches (TODO: should match not be foreignkey linked to league?)
|
||||
|
||||
Attributes:
|
||||
id (int): Automatically incrementing identifier number.
|
||||
name (str): A human-readable label to identify this item.
|
||||
description (str):
|
||||
extra_notes (str):
|
||||
profile_picture (file):
|
||||
banner_picture (file):
|
||||
matches (list of `Match`):
|
||||
anglers (list of `Angler`):
|
||||
rules (list of `LeagueRule`):
|
||||
"""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
description = models.CharField(max_length=384)
|
||||
extra_notes = models.CharField(max_length=1028)
|
||||
|
||||
profile_picture = models.ImageField()
|
||||
banner_picture = models.ImageField()
|
||||
|
||||
matches = models.ManyToManyField(to=Match)
|
||||
anglers = models.ManyToManyField(to=Angler)
|
||||
rules = models.ManyToManyField(to=LeagueRule)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "league"
|
||||
verbose_name_plural = "leagues"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def results(self):
|
||||
return LeagueResult.objects.filter(league=self)
|
||||
|
||||
|
||||
class Sponsor(models.Model):
|
||||
"""Represent those sponsoring.
|
||||
|
||||
Attribute:
|
||||
id (int): Automatically incrementing identifier number.
|
||||
name (str): The sponsor's name.
|
||||
url (str): Link to the sponsor's external resource.
|
||||
image (file): An image representing the sponsor.
|
||||
"""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=128)
|
||||
url = models.URLField()
|
||||
image = models.ImageField()
|
||||
|
||||
class Meta:
|
||||
verbose_name = "sponsor"
|
||||
verbose_name_plural = "sponsors"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class LeagueResult(models.Model):
|
||||
"""An entry representing an angler's result in a league.
|
||||
|
||||
Attributes:
|
||||
TODO
|
||||
"""
|
||||
id = models.AutoField(primary_key=True)
|
||||
|
||||
league = models.ForeignKey(to=League, on_delete=models.CASCADE)
|
||||
angler = models.ForeignKey(to=Angler, on_delete=models.CASCADE)
|
||||
sponsor = models.ForeignKey(to=Sponsor, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
total_weight = models.CharField(max_length=64)
|
||||
matches = models.IntegerField()
|
||||
date = models.DateField(default=timezone.now)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "league result"
|
||||
verbose_name_plural = "league results"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.league.name} - {self.angler.name}"
|
119
apps/home/static/home/js/venues.js
Normal file
@ -0,0 +1,119 @@
|
||||
var venueTypes;
|
||||
|
||||
$(document).ready(async () => {
|
||||
await loadVenueTypes();
|
||||
await loadVenues();
|
||||
});
|
||||
|
||||
const loadVenueTypes = async () => {
|
||||
const response = await $.ajax({
|
||||
url: "/api/venues",
|
||||
method: "OPTIONS"
|
||||
});
|
||||
|
||||
venueTypes = response?.actions?.POST?.type?.choices || [];
|
||||
}
|
||||
|
||||
const loadVenues = async () => {
|
||||
const response = await $.ajax({
|
||||
url: "/api/venues/",
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
for (const venue of response?.results || []) {
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
addVenue(venue);
|
||||
}
|
||||
}
|
||||
|
||||
const addVenue = venue => {
|
||||
let $venue = $($("#venueItemTemplate").html());
|
||||
|
||||
const type = venueTypes.find(type => type.value === venue.type);
|
||||
|
||||
$venue.find(".venue-type").text(type.display_name);
|
||||
$venue.find(".venue-item").data("id", venue.id);
|
||||
$venue.find(".venue-name").text(venue.name);
|
||||
|
||||
$description = $venue.find(".venue-description");
|
||||
$description.text(venue.description);
|
||||
$clamp($description[0], {clamp: 3});
|
||||
|
||||
appendVenueAddress($venue, venue.address);
|
||||
appendVenueContacts($venue, venue.contacts);
|
||||
|
||||
$venue.off("click").on("click", async () => await selectVenue(venue.id));
|
||||
|
||||
$("#venueContainer").append($venue);
|
||||
}
|
||||
|
||||
const appendVenueAddress = ($venue, address) => {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = address?.post_code;
|
||||
addVenueTemplateContact($venue, location, "bi-geo-alt");
|
||||
}
|
||||
|
||||
const appendVenueContacts = ($venue, contacts) => {
|
||||
if (!contacts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const phoneNumber = contacts?.phone_number;
|
||||
addVenueTemplateContact($venue, phoneNumber, "bi-telephone");
|
||||
|
||||
const emailAddress = contacts?.email_address;
|
||||
addVenueTemplateContact($venue, emailAddress, "bi-envelope");
|
||||
|
||||
const websiteUrl = contacts?.website_url;
|
||||
addVenueTemplateContact($venue, websiteUrl, "bi-globe");
|
||||
|
||||
const facebookUrl = contacts?.facebook_url;
|
||||
addVenueTemplateContact($venue, facebookUrl, "bi-facebook");
|
||||
|
||||
const twitterUrl = contacts?.twitter_url;
|
||||
addVenueTemplateContact($venue, twitterUrl, "bi-twitter-x");
|
||||
|
||||
const instagramUrl = contacts?.instagram_url;
|
||||
addVenueTemplateContact($venue, instagramUrl, "bi-instagram");
|
||||
}
|
||||
|
||||
const addVenueTemplateContact = ($venue, value, iconClass) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let $contact = $($("#venueContactTemplate").html())
|
||||
|
||||
$contact.find(".venue-contact-icon")
|
||||
.addClass(iconClass)
|
||||
.attr("data-bs-title", value)
|
||||
.attr("data-bs-toggle", "tooltip")
|
||||
.tooltip();
|
||||
|
||||
$venue.find(".venue-contacts").append($contact);
|
||||
}
|
||||
|
||||
const selectVenue = async id => {
|
||||
const response = await $.ajax({
|
||||
url: `/api/venues/${id}/`,
|
||||
method: "GET"
|
||||
})
|
||||
|
||||
$(".js-createVenueBtn").trigger("click");
|
||||
console.log(JSON.stringify(response, null, 4));
|
||||
}
|
||||
|
||||
$(".js-createVenueBtn").on("click", () => {
|
||||
$("#venueModal").modal("show");
|
||||
});
|
1
apps/home/static/home/scss/index.scss
Normal file
@ -0,0 +1 @@
|
||||
@import "scss/base.scss";
|
1
apps/home/static/home/scss/leagues.scss
Normal file
@ -0,0 +1 @@
|
||||
@import "scss/base.scss";
|
145
apps/home/static/home/scss/venues.scss
Normal file
@ -0,0 +1,145 @@
|
||||
@import "scss/base.scss";
|
||||
|
||||
.search-group {
|
||||
|
||||
max-width: 400px;
|
||||
|
||||
.form-control {
|
||||
|
||||
border-left: none !important;
|
||||
border-top-right-radius: 0.25rem;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
|
||||
&:focus, &:focus-within {
|
||||
|
||||
box-shadow: none;
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
|
||||
background-color: transparent;
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
cursor: text;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.venue-item {
|
||||
|
||||
color: var(--bs-body-color);
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 0.25rem;
|
||||
min-width: 0;
|
||||
|
||||
.venue-type {
|
||||
|
||||
color: var(--bs-tertiary-color);
|
||||
margin-bottom: 0.25rem;
|
||||
@extend small;
|
||||
@extend .text-truncate;
|
||||
|
||||
}
|
||||
|
||||
.venue-name {
|
||||
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--bs-body-color);
|
||||
|
||||
// Single line with ellipsis
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.venue-description {
|
||||
|
||||
color: var(--bs-secondary-color);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
}
|
||||
|
||||
.venue-contacts {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style: none;
|
||||
margin-bottom: 0;
|
||||
padding-left: 0;
|
||||
|
||||
.venue-contact {
|
||||
|
||||
margin-right: 0.75rem;
|
||||
|
||||
.venue-contact-icon { @extend .bi; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.venue-modal {
|
||||
|
||||
.modal-content { min-height: 0 !important; }
|
||||
|
||||
.venue-modal-body {
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
textarea {
|
||||
|
||||
--height: calc(6em + 0.75rem + calc(var(--bs-border-width) * 2));;
|
||||
max-height: var(--height);
|
||||
min-height: var(--height);
|
||||
height: var(--height);
|
||||
|
||||
}
|
||||
|
||||
.venue-modal-sidebar {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
|
||||
.venue-sidebar-btn {
|
||||
|
||||
@extend .btn;
|
||||
|
||||
--bs-btn-active-color: #fff;
|
||||
--bs-btn-active-bg: #04385c;
|
||||
--bs-btn-hover-color: #fff;
|
||||
--bs-btn-hover-bg: #085c8d;
|
||||
|
||||
padding: 0.75rem 1.5rem;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.venue-modal-content {
|
||||
|
||||
padding: 1.5rem;
|
||||
height: 100%;
|
||||
background-color: var(--bs-body-bg);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,13 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
|
||||
{% block title %}
|
||||
Venues |
|
||||
{% endblock title %}
|
||||
|
||||
{% block style %}
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
{% endblock style %}
|
||||
|
||||
{% compress css %}
|
||||
<link type="text/x-scss" rel="stylesheet" href="{% static 'home/scss/index.scss' %}">
|
||||
{% endcompress %}
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block header_buttons %}
|
||||
{% endblock header_buttons %}
|
11
apps/home/templates/home/leagues.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
|
||||
{% block title %} · Leagues{% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{% compress css %}
|
||||
<link type="text/x-scss" rel="stylesheet" href="{% static 'home/scss/leagues.scss' %}">
|
||||
{% endcompress %}
|
||||
{% endblock stylesheets %}
|
116
apps/home/templates/home/venues.html
Normal file
@ -0,0 +1,116 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
|
||||
{% block title %} · Venues{% endblock title %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{% compress css %}
|
||||
<link type="text/x-scss" rel="stylesheet" href="{% static 'home/scss/venues.scss' %}">
|
||||
{% endcompress %}
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xxl">
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group search-group mb-4 mb-md-0">
|
||||
<label for="venueSearch" class="input-group-text">
|
||||
<i class="bi bi-search"></i>
|
||||
</label>
|
||||
<input type="search" id="venueSearch" name="venueSearch" class="form-control" placeholder="Search...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<button type="button" class="js-createVenueBtn btn btn-primary rounded-1">
|
||||
Create a Venue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="venueContainer" class="row gy-0 venue-items"></div>
|
||||
</div>
|
||||
|
||||
<div id="venueModal" class="venue-modal modal fade" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-lg-down modal-lg">
|
||||
<div class="modal-content rounded-1">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title me-5 fw-bold">New Venue</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="venue-modal-body row g-0">
|
||||
<div class="col-md-1">
|
||||
<div class="venue-modal-sidebar">
|
||||
<button type="button" class="venue-sidebar-btn active">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</button>
|
||||
<button type="button" class="venue-sidebar-btn">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
<button type="button" class="venue-sidebar-btn">
|
||||
<i class="bi bi-telephone"></i>
|
||||
</button>
|
||||
<button type="button" class="venue-sidebar-btn">
|
||||
<i class="bi bi-droplet-half"></i>
|
||||
</button>
|
||||
<button type="button" class="venue-sidebar-btn mt-auto">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-11">
|
||||
<div class="venue-modal-content">
|
||||
<div class="form-group">
|
||||
<label for="venueName" class="form-label">Name</label>
|
||||
<input type="text" name="venueName" id="venueName" class="form-control">
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<label for="venueDescription" class="form-label">Description</label>
|
||||
<textarea name="venueDescription" id="venueDescription" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="form-label w-100">Type</label>
|
||||
<div class="btn-group">
|
||||
<input type="radio" name="venueType" id="venueTypeFishery" class="btn-check" value="fishery">
|
||||
<label for="venueTypeFishery" class="btn btn-outline-primary">Fishery</label>
|
||||
<input type="radio" name="venueType" id="venueTypeClub" class="btn-check" value="club">
|
||||
<label for="venueTypeClub" class="btn btn-outline-primary">Club</label>
|
||||
<input type="radio" name="venueType" id="venueTypePrivate" class="btn-check" value="private">
|
||||
<label for="venueTypePrivate" class="btn btn-outline-primary">Private</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-end">
|
||||
<button type="button" class="btn btn-secondary rounded-1 me-3 d-none">
|
||||
Back
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary rounded-1 px-4">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
<script id="venueItemTemplate" type="text/template">
|
||||
<div class="col-md-6 col-lg-4 col-xxl-3 mt-4">
|
||||
<div class="venue-item" role="button">
|
||||
<div class="venue-type"></div>
|
||||
<div class="venue-name"></div>
|
||||
<div class="venue-description"></div>
|
||||
<ul class="venue-contacts"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script id="venueContactTemplate" type="text/template">
|
||||
<li class="venue-contact">
|
||||
<i class="venue-contact-icon"></i>
|
||||
</li>
|
||||
</script>
|
||||
<script src="{% static 'js/clamp.min.js' %}"></script>
|
||||
<script src="{% static 'home/js/venues.js' %}"></script>
|
||||
{% endblock scripts %}
|
13
apps/home/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""URLs for the home app."""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "home"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.IndexView.as_view(), name="index"),
|
||||
path("venues", views.VenueWatersView.as_view(), name="venues-waters"),
|
||||
path("leagues", views.LeagueView.as_view(), name="leagues")
|
||||
]
|
22
apps/home/views.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Views for the home app."""
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class IndexView(TemplateView):
|
||||
"""The index view of the home app."""
|
||||
|
||||
template_name = "home/index.html"
|
||||
|
||||
|
||||
class VenueWatersView(TemplateView):
|
||||
"""Page for indexing Venues & Waters."""
|
||||
|
||||
template_name = "home/venues.html"
|
||||
|
||||
|
||||
class LeagueView(TemplateView):
|
||||
"""Page for indexing Leagues."""
|
||||
|
||||
template_name = "home/leagues.html"
|
@ -11,6 +11,6 @@ import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Results.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
|
||||
application = get_asgi_application()
|
212
core/settings.py
Normal file
@ -0,0 +1,212 @@
|
||||
"""
|
||||
Django settings for Results project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.1.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
import environ
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
VERSION = "0.0.0"
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / "subdir".
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Environment Variables
|
||||
env = environ.Env(DEBUG=(bool, True))
|
||||
environ.Env.read_env(BASE_DIR / ".env")
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env("RESULTS_SYSTEM__SECRET_KEY", default="unsecure-default-secret-key")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env("RESULTS_SYSTEM__DEBUG").lower() == "true"
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
"http://localhost",
|
||||
"http://127.0.0.1",
|
||||
"https://" + env("RESULTS_SYSTEM__HOST", default="127.0.0.1")
|
||||
]
|
||||
ALLOWED_HOSTS = [
|
||||
host.replace("http://", "").replace("https://", "")
|
||||
for host in CSRF_TRUSTED_ORIGINS
|
||||
]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django_extensions",
|
||||
"rest_framework",
|
||||
"compressor",
|
||||
"apps.api",
|
||||
"apps.home",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "core.urls"
|
||||
APPEND_SLASH = True
|
||||
|
||||
LOGIN_URL = "/login/"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "core.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
|
||||
DB_ENGINE = env("RESULTS_SYSTEM__DB_ENGINE", default=None)
|
||||
|
||||
if not DB_ENGINE:
|
||||
db_data = {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": "db.sqlite3"
|
||||
}
|
||||
else:
|
||||
db_data = {
|
||||
"ENGINE": env("DB_ENGINE"),
|
||||
"NAME": env("DB_NAME"),
|
||||
"USER": env("DB_USER"),
|
||||
"PASSWORD": env("DB_PASS"),
|
||||
"HOST": env("DB_HOST"),
|
||||
"PORT": env("DB_PORT")
|
||||
}
|
||||
|
||||
DATABASES = { "default": db_data }
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" },
|
||||
{ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator" },
|
||||
{ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator" },
|
||||
{ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator" },
|
||||
]
|
||||
|
||||
|
||||
# Logging
|
||||
# https://docs.djangoproject.com/en/5.0/topics/logging/
|
||||
|
||||
LOGGING_DIR = BASE_DIR / "logs"
|
||||
LOGGING_DIR.mkdir(exist_ok=True)
|
||||
|
||||
with open(LOGGING_DIR / "config.json", "r", encoding="utf-8") as file:
|
||||
LOGGING = json.load(file)
|
||||
|
||||
LOGGING["handlers"]["file"]["filename"] = LOGGING_DIR / f"{timezone.now()}.log"
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-gb"
|
||||
TIME_ZONE = "Europe/London"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
STATIC_URL = "/static/"
|
||||
STATICFILES_DIRS = (BASE_DIR / "static",)
|
||||
STATICFILES_FINDERS = (
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
"compressor.finders.CompressorFinder",
|
||||
)
|
||||
|
||||
|
||||
# Compressors
|
||||
|
||||
COMPRESS_ENABLED = True
|
||||
COMPRESS_OFFLINE = not DEBUG
|
||||
COMPRESS_PRECOMPILERS = [("text/x-scss", "django_libsass.SassCompiler")]
|
||||
COMPRESS_CSS_FILTERS = ["compressor.filters.css_default.CssAbsoluteIdentifier"]
|
||||
LIBSASS_ADDITIONAL_INCLUDE_PATHS = [str(BASE_DIR / "static/")]
|
||||
|
||||
|
||||
# Media Files
|
||||
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
|
||||
# region Rest Framework
|
||||
# https://www.django-rest-framework.org/
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_THROTTLE_CLASSES": [
|
||||
"rest_framework.throttling.AnonRateThrottle",
|
||||
"rest_framework.throttling.UserRateThrottle"
|
||||
],
|
||||
"DEFAULT_THROTTLE_RATES": {
|
||||
"anon": "100/day",
|
||||
"user": "10000/hour"
|
||||
},
|
||||
"DEFAULT_RENDERER_CLASSES": [
|
||||
"rest_framework.renderers.JSONRenderer",
|
||||
# "rest_framework.renderers.AdminRenderer",
|
||||
"rest_framework.renderers.BrowsableAPIRenderer"
|
||||
],
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
|
||||
"PAGE_SIZE": 100
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
@ -17,7 +17,7 @@ from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('api.urls')),
|
||||
path('', include('mainapp.urls')),
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/", include("apps.api.urls", namespace="api")),
|
||||
path("", include("apps.home.urls", namespace="home")),
|
||||
]
|
@ -11,6 +11,6 @@ import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Results.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
|
||||
application = get_wsgi_application()
|
20
fixtures/dummy_admin_user.json
Normal file
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"model": "auth.user",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"password": "pbkdf2_sha256$720000$sz7T3QpIk5GHLR8ezSb7UU$lF2oPTmIs7fKOaa02/+daObbqJs7eCrSu+VLIRX8H+M=",
|
||||
"last_login": null,
|
||||
"is_superuser": true,
|
||||
"username": "admin",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"email": "admin@mail.com",
|
||||
"is_staff": true,
|
||||
"is_active": true,
|
||||
"date_joined": "2024-11-05T18:15:55.516Z",
|
||||
"groups": [],
|
||||
"user_permissions": []
|
||||
}
|
||||
}
|
||||
]
|
54
logs/config.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "[%(module)s|%(levelname)s] %(message)s"
|
||||
},
|
||||
"detail": {
|
||||
"format": "[%(asctime)s] [%(levelname)s] [%(module)s]: %(message)s"
|
||||
},
|
||||
"complex": {
|
||||
"format": "[%(levelname)s|%(name)s|L%(lineno)d] %(asctime)s %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"file": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.FileHandler",
|
||||
"formatter": "complex"
|
||||
},
|
||||
"stdout": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
"queue_handler": {
|
||||
"respect_handler_level": true,
|
||||
"class": "logging.handlers.QueueHandler",
|
||||
"handlers": [
|
||||
"file",
|
||||
"stdout"
|
||||
]
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "DEBUG",
|
||||
"handlers": [
|
||||
"queue_handler"
|
||||
]
|
||||
},
|
||||
"apps": {
|
||||
"level": "DEBUG"
|
||||
},
|
||||
"django": {
|
||||
"level": "INFO"
|
||||
},
|
||||
"django.request": {
|
||||
"level": "ERROR"
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import sys
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Results.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
BIN
requirements.txt
5
scripts/entity-relationship-diagram.sh
Normal file
@ -0,0 +1,5 @@
|
||||
cd "$(dirname "$(readlink -f "$0")")"/..
|
||||
|
||||
source ./venv/bin/activate
|
||||
|
||||
python manage.py graph_models apps_home | dot -Tpng -o entity-relationship-diagram.png
|
@ -1,155 +0,0 @@
|
||||
"""
|
||||
Django settings for Results project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.1.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-==z9@atc)#1c@%@+txwiie=3qk)9r92antn3b$v#4o8r2q63&d'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# 'rest_framework_datatables'
|
||||
'rest_framework',
|
||||
'mainapp',
|
||||
'api',
|
||||
'compressor',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'Results.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'Results.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||
|
||||
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
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Rest Framework for Data Table
|
||||
|
||||
#REST_FRAMEWORK = {
|
||||
# 'DEFAULT_RENDERER_CLASSES': (
|
||||
# 'rest_framework.renderers.JSONRenderer',
|
||||
# 'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
# 'rest_framework_datatables.renderers.DatatablesRenderer',
|
||||
# ),
|
||||
# 'DEFAULT_FILTER_BACKENDS': (
|
||||
# 'rest_framework_datatables.filters.DatatablesFilterBackend',
|
||||
# ),
|
||||
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework_datatables.pagination.DatatablesPageNumberPagination',
|
||||
# 'PAGE_SIZE': 50,
|
||||
#}
|
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
@ -1,27 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
from mainapp.models import Venue
|
||||
|
||||
class VenueSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Venue
|
||||
fields = (
|
||||
"pk",
|
||||
"name",
|
||||
"description",
|
||||
"extra_notes",
|
||||
"venue_type",
|
||||
"phone_number",
|
||||
"email_address",
|
||||
"website_url",
|
||||
"street_address",
|
||||
"city",
|
||||
"provence",
|
||||
"postal_code",
|
||||
"country",
|
||||
"latitude",
|
||||
"longitude",
|
||||
"twitter_url",
|
||||
"instagram_url",
|
||||
"facebook_url",
|
||||
"active",
|
||||
)
|
@ -1,9 +0,0 @@
|
||||
from django.urls import path, include
|
||||
from .views import VenueListView, VenueDetailView
|
||||
|
||||
urlpatterns = [
|
||||
path('venue/', include([
|
||||
path("", VenueListView.as_view()),
|
||||
path("<int:pk>/", VenueDetailView.as_view())
|
||||
]))
|
||||
]
|
@ -1,52 +0,0 @@
|
||||
"""Admin models for the mainapp app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Venue, Waters
|
||||
# from .models import Member, Team, Section
|
||||
|
||||
|
||||
@admin.register(Venue)
|
||||
class VenueAdmin(admin.ModelAdmin):
|
||||
"""Admin model for the Venue model."""
|
||||
|
||||
@admin.register(Waters)
|
||||
class WatersAdmin(admin.ModelAdmin):
|
||||
"""Admin model for the Waters model"""
|
||||
|
||||
|
||||
|
||||
# @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(Member)
|
||||
# class MemberAdmin(admin.ModelAdmin):
|
||||
# """Admin model for the Member model."""
|
||||
|
||||
# list_display = ("first_name", "last_name", "team")
|
||||
# search_fields = ("first_name", "last_name", "team")
|
||||
# list_filter = ("team",)
|
||||
|
||||
|
||||
# @admin.register(Team)
|
||||
# class TeamAdmin(admin.ModelAdmin):
|
||||
# """Admin model for the Team model."""
|
||||
|
||||
# # 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",)
|
@ -1,23 +0,0 @@
|
||||
from decimal import Decimal
|
||||
from django.db.models import Q
|
||||
import django_filters
|
||||
from .models import Scoreboard
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
query = django_filters.CharFilter(method='universal_search',
|
||||
label="")
|
||||
|
||||
class Meta:
|
||||
model = Scoreboard
|
||||
fields = ['query']
|
||||
|
||||
def universal_search(self, queryset, name, value):
|
||||
if value.replace(".", "", 1).isdigit():
|
||||
value = Decimal(value)
|
||||
return Scoreboard.objects.filter(
|
||||
Q(price=value) | Q(cost=value)
|
||||
)
|
||||
|
||||
return Scoreboard.objects.filter(
|
||||
Q(name__icontains=value) | Q(category__icontains=value)
|
||||
)
|
@ -1,167 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
@ -1,152 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
]
|
@ -1,117 +0,0 @@
|
||||
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."))
|
@ -1,79 +0,0 @@
|
||||
"""Command to create test data fixture for sections."""
|
||||
|
||||
import json
|
||||
from string import ascii_uppercase
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from mainapp.models import Section, SectionValidator
|
||||
|
||||
|
||||
def name_sort_key(name):
|
||||
if len(name) == 1:
|
||||
return (0, name)
|
||||
else:
|
||||
return (1, name[:-1], name[-1])
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a fixture file for Team objects"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("amount_of_sections", type=int, help="Number of sections to create")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
amount_of_sections = options["amount_of_sections"]
|
||||
|
||||
used_names = set()
|
||||
last_name = ""
|
||||
|
||||
for i in range(amount_of_sections):
|
||||
add_character = last_name.endswith("Z")
|
||||
if add_character:
|
||||
last_name = ""
|
||||
|
||||
new_name = last_name if add_character else ""
|
||||
if not new_name.endswith("Z"):
|
||||
new_name = new_name[:-1] if len(new_name) > 1 else new_name
|
||||
|
||||
try:
|
||||
new_name += ascii_uppercase[i]
|
||||
except IndexError:
|
||||
quotient, remainder = divmod(i, 26)
|
||||
new_name += ascii_uppercase[quotient - 1] + ascii_uppercase[remainder]
|
||||
|
||||
while new_name in used_names:
|
||||
if new_name[-1] == "Z":
|
||||
idx = len(new_name) - 2
|
||||
while idx >= 0 and new_name[idx] == "Z":
|
||||
idx -= 1
|
||||
if idx < 0:
|
||||
new_name = "A" * (len(new_name) + 1)
|
||||
else:
|
||||
new_name = new_name[:idx] + chr(ord(new_name[idx]) + 1) + "A" * (len(new_name) - idx - 1)
|
||||
else:
|
||||
new_name = new_name[:-1] + chr(ord(new_name[-1]) + 1)
|
||||
|
||||
used_names.add(new_name)
|
||||
last_name = new_name
|
||||
|
||||
names = sorted([name for name in used_names], key=name_sort_key)
|
||||
|
||||
print([name for name in names])
|
||||
sections = [Section(name=name) for name in names]
|
||||
Section.objects.bulk_create(sections)
|
||||
|
||||
fixture = [{
|
||||
"model": "mainapp.section",
|
||||
"pk": section.pk,
|
||||
"fields": {
|
||||
"name": section.name
|
||||
}
|
||||
} for section in sections]
|
||||
|
||||
for section in sections:
|
||||
section.delete()
|
||||
|
||||
with open("src/mainapp/fixtures/sections_fixture.json", "w") as file:
|
||||
file.write(json.dumps(fixture, indent=4))
|
||||
self.stdout.write(self.style.SUCCESS("Created sections_fixture.json."))
|
@ -1,28 +0,0 @@
|
||||
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()
|
@ -1,34 +0,0 @@
|
||||
"""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,40 +0,0 @@
|
||||
# Generated by Django 4.1.5 on 2023-05-15 11:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Section',
|
||||
fields=[
|
||||
('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=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('number', models.PositiveIntegerField(unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Member',
|
||||
fields=[
|
||||
('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)),
|
||||
('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,45 +0,0 @@
|
||||
# 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,27 +0,0 @@
|
||||
# Generated by Django 4.1.5 on 2023-10-23 22:17
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0002_venue_delete_member_delete_section_delete_team'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Waters',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('description', models.TextField(max_length=255)),
|
||||
('pegs_min', models.IntegerField()),
|
||||
('pegs_max', models.IntegerField()),
|
||||
('water_type', models.CharField(choices=[('CW', 'Commercial Water'), ('NSW', 'Natural Still Water'), ('C', 'Canal'), ('R', 'River'), ('L', 'Loch')], max_length=50)),
|
||||
('fish_type', models.CharField(choices=[('C', 'Coarse'), ('SC', 'Specimen Carp'), ('G', 'Game'), ('P', 'Predator')], max_length=50)),
|
||||
('venue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mainapp.venue')),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.5 on 2023-11-06 20:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0003_waters'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='venue',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, max_length=500),
|
||||
),
|
||||
]
|
@ -1,88 +0,0 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-26 20:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('mainapp', '0004_venue_active_alter_venue_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='country',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='email_address',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='extra_notes',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='facebook_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='instagram_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='phone_number',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='postal_code',
|
||||
field=models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='provence',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='street_address',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='twitter_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='venue_type',
|
||||
field=models.CharField(blank=True, choices=[('FISHERY', 'Fishery'), ('CLUB', 'Club'), ('PRIVATE', 'Private')], max_length=50, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='venue',
|
||||
name='website_url',
|
||||
field=models.URLField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -1,276 +0,0 @@
|
||||
"""Models for the mainapp."""
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
class Venue(models.Model):
|
||||
"""Represents a Venue and Waters."""
|
||||
|
||||
VENUE_TYPES = (
|
||||
("FISHERY", "Fishery"),
|
||||
("CLUB", "Club"),
|
||||
("PRIVATE", "Private")
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255, null=True, blank=True)
|
||||
description = models.TextField(blank=True, max_length=500, null=True,)
|
||||
extra_notes = models.TextField(blank=True, null=True,)
|
||||
venue_type = models.CharField(choices=VENUE_TYPES, max_length=50, null=True, blank=True)
|
||||
|
||||
# Contact information
|
||||
phone_number = models.CharField(max_length=100, null=True, blank=True)
|
||||
email_address = models.EmailField(null=True, blank=True)
|
||||
website_url = models.URLField(null=True, blank=True)
|
||||
|
||||
# Location information
|
||||
street_address = models.CharField(max_length=100, null=True, blank=True)
|
||||
city = models.CharField(max_length=255, null=True, blank=True)
|
||||
provence = models.CharField(max_length=100, null=True, blank=True)
|
||||
postal_code = models.CharField(max_length=20, null=True, blank=True)
|
||||
country = models.CharField(max_length=100, null=True, blank=True)
|
||||
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)
|
||||
|
||||
# Socials information
|
||||
twitter_url = models.URLField(blank=True, null=True)
|
||||
instagram_url = models.URLField(blank=True, null=True)
|
||||
facebook_url = models.URLField(blank=True, null=True)
|
||||
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def waters(self):
|
||||
"""Returns all waters linked to this venue."""
|
||||
|
||||
waters = Waters.objects.filter(venue=self)
|
||||
return waters
|
||||
|
||||
|
||||
class Waters(models.Model):
|
||||
"""Represents the waters of a Venue"""
|
||||
|
||||
WATER_TYPES = (
|
||||
("CW", "Commercial Water"),
|
||||
("NSW", "Natural Still Water"),
|
||||
("C", "Canal"),
|
||||
("R", "River"),
|
||||
("L", "Loch"),
|
||||
)
|
||||
|
||||
FISH_TYPES = (
|
||||
("C", "Coarse"),
|
||||
("SC", "Specimen Carp"),
|
||||
("G", "Game"),
|
||||
("P", "Predator"),
|
||||
)
|
||||
|
||||
venue = models.ForeignKey(Venue, on_delete=models.CASCADE)
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(max_length=255)
|
||||
|
||||
pegs_min = models.IntegerField()
|
||||
pegs_max = models.IntegerField()
|
||||
water_type = models.CharField(choices=WATER_TYPES, max_length=50)
|
||||
fish_type = models.CharField(choices=FISH_TYPES, max_length=50)
|
||||
|
||||
# water_map = models.ImageField()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
# 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 SectionValidator:
|
||||
# """Validation class for the `section` field on the `member` model."""
|
||||
|
||||
# def __init__(self, max_value="ZZZ"):
|
||||
# self.max_value = max_value.upper()
|
||||
# self.alphabet_size = ord("Z") - ord("A") + 1
|
||||
|
||||
# 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 SectionManager(models.Manager):
|
||||
|
||||
# @staticmethod
|
||||
# def get_max_section():
|
||||
# max_section = None
|
||||
# max_number = -1
|
||||
|
||||
# # 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
|
||||
|
||||
|
||||
# class Member(models.Model):
|
||||
# """Represents a member of a team"""
|
||||
|
||||
# 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')
|
||||
|
||||
# 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 Team(models.Model):
|
||||
# """Represents a team"""
|
||||
|
||||
# number = models.PositiveIntegerField(unique=True, null=False, blank=False)
|
||||
|
||||
# def __str__(self):
|
||||
# return f"Team {self.number}"
|
@ -1,31 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{% static 'css/ocean.css' %}">
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<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>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static 'js/ocean.js' %}"></script>
|
||||
{% endblock scripts %}
|
@ -1,200 +0,0 @@
|
||||
{% 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,35 +0,0 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
|
||||
{% block object-tools %}
|
||||
<div class="bg-body pt-2 pb-3 d-flex justify-content-end">
|
||||
<button class="btn btn-primary me-3" data-bs-toggle="modal" data-bs-target="#peggingModal">Add Many Pegs</button>
|
||||
<form method="POST" action="/bulk-peg/">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="pegAmount" value="1" min="1" max="1" required="true">
|
||||
<button type="submit" class="btn btn-secondary">Add One Peg</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal fade" tabindex="-1" id="peggingModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Bulk Add Pegs</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="/bulk-peg/">
|
||||
{% csrf_token %}
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label" for="pegAmount">Number of Pegs</label>
|
||||
<input type="number" class="form-control" id="pegAmount" name="pegAmount" min="1" max="9999" required="true">
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-success me-auto">Add</button>
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" aria-label="Close">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock object-tools %}
|
@ -1,91 +0,0 @@
|
||||
|
||||
<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>
|
@ -1,47 +0,0 @@
|
||||
|
||||
<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>
|
@ -1,48 +0,0 @@
|
||||
|
||||
<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,11 +0,0 @@
|
||||
{% 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,11 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% 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>
|
||||
{% endblock content %}
|
@ -1,245 +0,0 @@
|
||||
{% 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 %}
|
@ -1,45 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{{ venue.name }} |
|
||||
{% endblock title %}
|
||||
|
||||
{% block style %}
|
||||
{% endblock style %}
|
||||
|
||||
{% block header_buttons %}
|
||||
{% endblock header_buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-xl-5 col-lg-6 col-md-10">
|
||||
<div class="m-sm-5 m-3">
|
||||
<h1 class="text-company fw-bold">
|
||||
{{ venue.name}}
|
||||
</h1>
|
||||
<div class="text-body-secondary mb-4">
|
||||
<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="mb-4">
|
||||
<p class="mb-0">Lorem ipsum dolor sit amet consectetur adipisicing elit. Repudiandae eveniet aspernatur neque vero molestias nemo, voluptatum eum quo blanditiis. Voluptate provident earum placeat impedit cumque sapiente praesentium, alias omnis tempore!</p>
|
||||
<!-- <p class="mb-0">{{ venue.description }}</p> -->
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h5 class="text-company">
|
||||
Contact
|
||||
</h5>
|
||||
<div class="text-body-secondary mb-4">
|
||||
<p class="mb-0">{{ venue.phone_number }}</p>
|
||||
<p class="mb-0">{{ venue.email_address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock scripts %}
|
@ -1,480 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
Venues |
|
||||
{% endblock title %}
|
||||
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
{% endblock style %}
|
||||
|
||||
{% block header_buttons %}
|
||||
{% endblock header_buttons %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex flex-column flex-grow-1">
|
||||
|
||||
<div class="m-1 m-sm-4 mb-0 mt-4 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">
|
||||
<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" id="addVenue" onclick="openVenueModal(-1);" data-bs-toggle="tooltip" data-bs-title="Add Venue" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<button class="btn border-secondary-subtle btn-outline-company me-4 rounded-end" id="importVenue" data-bs-toggle="tooltip" data-bs-title="Import Venue" data-bs-custom-class="light-tooltip">
|
||||
<i class="bi bi-upload"></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 m-1 mx-sm-4 my-2">
|
||||
<div class="col-xl-7">
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-1 m-sm-4 mt-0 row row-cols-xxl-5 row-cols-xl-4 row-cols-lg-3 g-3">
|
||||
{% for venue in venues %}
|
||||
<div class="">
|
||||
<div class="card w-100 h-100 border-0 rounded-3 overflow-hidden">
|
||||
<div class="card-body bg-body-tertiary position-relative">
|
||||
<div class="d-flex justify-content-between align-items-stretch h-100">
|
||||
<div>
|
||||
<h5 class="card-title fw-semibold">{{ venue.name }}</h5>
|
||||
<p class="card-text mb-0">{{ venue.street_address }}</p>
|
||||
<p class="card-text mb-0">{{ venue.city }}, {{ venue.provence }}</p>
|
||||
<p class="card-text mb-3">{{ venue.postal_code }}</p>
|
||||
<div class="text-body-secondary">
|
||||
<p class="card-text mb-0">{{ venue.phone_number }}</p>
|
||||
<p class="card-text mb-0">{{ venue.email_address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column justify-content-end align-items-end">
|
||||
<div class="badge company-bg rounded-pill end-0 top-0">
|
||||
6 Waters
|
||||
</div>
|
||||
<button class="mt-auto btn btn-outline-company rounded-circle" style="width: fit-content;" onclick="openVenueModal({{ venue.id }});">
|
||||
<i class="bi bi-gear"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="card w-100 h-100 shadow-sm overflow-hidden flex-row fluid-hover-zoom md-shadow-on-hover" data-venue-id="{{ venue.id }}">
|
||||
<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 class="d-flex flex-column">
|
||||
<div class="btn-group btn-group-vertical m-3 mt-auto">
|
||||
<a class="btn btn-outline-company rounded-top-4" href="{{ venue.id }}">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-company rounded-bottom-4 ms-0" onclick="openVenueModal({{ venue.id }});">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="venueModal" class="modal fade" data-venue-id="-1">
|
||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||
<div class="modal-content overflow-hidden rounded-4" style="min-height: 760px;">
|
||||
<div class="modal-header company-bg justify-content-center">
|
||||
<h4 class="card-title text-light fw-bold mb-0">
|
||||
<span class="create" style="display: none">New Venue</span>
|
||||
<span class="edit" style="display: none">Edit Venue</span>
|
||||
</h4>
|
||||
</div>
|
||||
<form id="venueForm" class="mb-0 needs-validation" novalidate>
|
||||
<div class="modal-body border-bottom-0 p-0 overflow-hidden">
|
||||
<ul id="newVenueTabBtns" class="nav nav-pills mb-4 d-flex w-100 justify-content-center py-2 bg-light" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button id="newVenueDetailsTabBtn" class="nav-link rounded-4 active" data-bs-toggle="pill" data-bs-target="#newVenueDetailsTab" type="button" role="tab" aria-controls="newVenueDetailsTab" aria-selected="true">Details</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button id="newVenueAddressTabBtn" class="nav-link rounded-4" data-bs-toggle="pill" data-bs-target="#newVenueAddressTab" type="button" role="tab" aria-controls="newVenueAddressTab" aria-selected="false">Address</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button id="newVenueContactTabBtn" class="nav-link rounded-4" data-bs-toggle="pill" data-bs-target="#newVenueContactTab" type="button" role="tab" aria-controls="newVenueContactTab" aria-selected="false">Contact</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button id="newVenueWatersTabBtn" class="nav-link rounded-4" data-bs-toggle="pill" data-bs-target="#newVenueWatersTab" type="button" role="tab" aria-controls="newVenueWatersTab" aria-selected="false">Waters</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-2 px-sm-4 pb-0">
|
||||
<div id="newVenueTabs" class="tab-content">
|
||||
<div id="newVenueDetailsTab" class="tab-pane fade show active" role="tabpanel" aria-labelledby="newVenueDetailsTabBtn" tabindex="0">
|
||||
<div class="row g-4 align-items-start mb-3">
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<input name="venueName" id="venueName" type="text" class="form-control" placeholder="" minlength="3" maxlength="100" required>
|
||||
<label for="venueName" class="form-label">
|
||||
Name <strong class="text-danger">*</strong>
|
||||
</label>
|
||||
<div class="invalid-feedback">Please enter the Venue's name</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-7">
|
||||
<div class="form-floating">
|
||||
<select name="venueType" id="venueType" class="form-select" placeholder="" required>
|
||||
<option disabled value="">Choose one ...</option>
|
||||
{% for type in venue_types %}
|
||||
<option value="{{ type.0 }}">{{ type.1 }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="venueType" class="form-label">
|
||||
Type of Venue <strong class="text-danger">*</strong>
|
||||
</label>
|
||||
<div class="invalid-feedback">Please select a Venue type</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<textarea name="venueDescription" id="venueDescription" type="text" class="form-control venue-textarea" placeholder="" maxlength="500"></textarea>
|
||||
<label for="venueDescription" class="form-label">
|
||||
Description
|
||||
</label>
|
||||
<div class="invalid-feedback">Please enter a brief description of the Venue</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<textarea name="venueExtraNotes" id="venueExtraNotes" type="text" class="form-control venue-textarea" placeholder="" maxlength="500"></textarea>
|
||||
<label for="venueExtraNotes" class="form-label">Extra Notes</label>
|
||||
<div class="invalid-feedback">Please enter any additional notes regarding the Venue</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newVenueAddressTab" class="tab-pane fade" role="tabpanel" aria-labelledby="newVenueAddressTabBtn" tabindex="0">
|
||||
<div class="mb-3">
|
||||
<div class="form-floating">
|
||||
<input name="venueStreetAddress" id="venueStreetAddress" type="text" class="form-control" placeholder="" required>
|
||||
<label for="venueStreetAddress" class="form-label">
|
||||
Street Address <strong class="text-danger">*</strong>
|
||||
</label>
|
||||
<div class="invalid-feedback">Please enter a valid Street Address</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-4 align-items-start mb-3">
|
||||
<div class="col-6">
|
||||
<div class="form-floating">
|
||||
<input name="venueCity" id="venueCity" type="text" class="form-control" placeholder="" required>
|
||||
<label for="venueCity" class="form-label">
|
||||
Town or City <strong class="text-danger">*</strong>
|
||||
</label>
|
||||
<div class="invalid-feedback">Please enter a Town or City</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-floating">
|
||||
<input name="venueProvence" id="venueProvence" type="text" class="form-control" placeholder="" required>
|
||||
<label for="venueProvence" class="form-label">
|
||||
Provence <strong class="text-danger">*</strong>
|
||||
</label>
|
||||
<div class="invalid-feedback">Please enter a Provence</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-floating">
|
||||
<input name="venuePostCode" id="venuePostCode" type="text" class="form-control" placeholder="" required>
|
||||
<label for="venuePostCode" class="form-label">
|
||||
Postal Code <strong class="text-danger">*</strong>
|
||||
</label>
|
||||
<div class="invalid-feedback">Please enter a Postal Code</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-floating">
|
||||
<!-- <input name="venueCountry" id="venueCountry" type="text" class="form-control" placeholder="" required> -->
|
||||
<select name="venueCountry" id="venueCountry" class="form-select" placeholder="" disabled required>
|
||||
<option value="UK">United Kingdom</option>
|
||||
</select>
|
||||
<label for="venueCountry" class="form-label">
|
||||
Country <i class="bi bi-lock-fill text-warning-emphasis"></i>
|
||||
</label>
|
||||
<div class="invalid-feedback">Please enter a Country</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none">
|
||||
<input type="hidden" name="venueLatitude" id="venueLatitude">
|
||||
<input type="hidden" name="venueLongitude" id="venueLongitude">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div id="locationMapContainer" class="w-100 position-relative" style="height: 225px;">
|
||||
<div id="locationMap" class="rounded-2 w-100 h-100"></div>
|
||||
<div id="locationMapOverlay" style="display: none;">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-text">(<strong>Disclaimer:</strong> results fetched from clicking the map may be inaccurate)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newVenueContactTab" class="tab-pane fade" role="tabpanel" aria-labelledby="newVenueContactTabBtn" tabindex="0">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-6">
|
||||
<div class="form-floating">
|
||||
<input name="venuePhone" id="venuePhone" type="tel" class="form-control" placeholder="">
|
||||
<label for="venuePhone" class="form-label">Phone Number</label>
|
||||
<div class="invalid-feedback">Bad phone number</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-floating">
|
||||
<input name="venueEmail" id="venueEmail" type="email" class="form-control" placeholder="">
|
||||
<label for="venueEmail" class="form-label">Email Address</label>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<input name="venueWebsite" id="venueWebsite" type="url" class="form-control" placeholder="">
|
||||
<label for="venueWebsite" class="form-label">Website Address</label>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-twitter" style="color: #1DA1F2"></i>
|
||||
</span>
|
||||
<div class="form-floating">
|
||||
<input name="venueTwitter" id="venueTwitter" type="url" class="form-control" placeholder="">
|
||||
<label for="venueTwitter" class="form-label d-flex align-items-center">
|
||||
Twitter Profile Address
|
||||
</label>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-facebook" style="color: #4267B2"></i>
|
||||
</span>
|
||||
<div class="form-floating">
|
||||
<input name="venueFacebook" id="venueFacebook" type="url" class="form-control" placeholder="">
|
||||
<label for="venueFacebook" class="form-label d-flex align-items-center">
|
||||
Facebook Profile Address
|
||||
</label>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-instagram" style="color: #D62976;"></i>
|
||||
</span>
|
||||
<div class="form-floating">
|
||||
<input name="venueInstagram" id="venueInstagram" type="url" class="form-control" placeholder="">
|
||||
<label for="venueInstagram" class="form-label d-flex align-items-center">
|
||||
Instagram Profile Address
|
||||
</label>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newVenueWatersTab" class="tab-pane fade" role="tabpanel" aria-labelledby="newVenueWatersTabBtn" tabindex="0">
|
||||
<div class="pb-5 pt-0 d-flex justify-content-between align-items-center">
|
||||
<button class="btn btn-outline-secondary rounded-4 d-flex align-items-center">
|
||||
<i class="bi bi-info-lg"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-company rounded-4 d-flex align-items-center">
|
||||
<span>Add Waters</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="list-unstyled overflow-y-auto" style="max-height: 450px;">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start mb-4">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">Section A · Coal Wharf, Market Drayton</div>
|
||||
<p class="mb-0">Commercial Water</p>
|
||||
<p class="mb-0 text-body-secondary">Specimen Carp</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-self-stretch">
|
||||
<span class="badge company-bg rounded-pill mb-4">1 · 36</span>
|
||||
<button class="btn btn-outline-secondary small rounded-4 mt-auto">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light border-top-0 px-4 py-3">
|
||||
<!-- <button type="button" class="btn btn-outline-danger rounded-4 me-auto px-3 edit" style="display: none">Delete</button>
|
||||
<button type="button" class="btn btn-outline-secondary me-3 rounded-4 px-3" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" id="saveVenue" class="btn btn-company rounded-4 px-3" form="venueForm">
|
||||
<span class="edit" style="display: none;">Save Edit</span>
|
||||
<span class="create" style="display: none;">Save New</span>
|
||||
</button> -->
|
||||
<button type="button" class="btn btn-outline-danger rounded-4">
|
||||
<i class="bi bi-trash2"></i>
|
||||
</button>
|
||||
<div class="mx-auto">
|
||||
<!-- <button type="button" id="newVenueTabLeft" class="btn btn-company rounded-4 me-1" disabled>
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button> -->
|
||||
<button type="submit" id="saveVenue" class="btn btn-company rounded-4 px-4" form="venueForm">
|
||||
<!-- <i class="bi bi-floppy"></i> -->
|
||||
<span class="edit" style="display: none;">Save Edit</span>
|
||||
<span class="create" style="display: none;">Save New</span>
|
||||
</button>
|
||||
<!-- <button type="button" id="newVenueTabRight" class="btn btn-company rounded-4 ms-1">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button> -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary rounded-4" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toastContainer" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
|
||||
<script src="{% static 'js/mainapp/venues.js' %}" data-csrfmiddlewaretoken="{{ csrf_token }}"></script>
|
||||
{% endblock scripts %}
|
@ -1,24 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
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('venues/get-waters/<int:venue_id>', views.get_venue_waters, name="get-venue-waters"),
|
||||
path('venues/<int:venue_id>', views.venue_details, name="venue-details"),
|
||||
path("venues/api/<int:venue_id>", views.get_venue_details, name="venue-details"),
|
||||
path("venues/api/create", views.create_venue, name="create-venue"),
|
||||
|
||||
# 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,622 +0,0 @@
|
||||
"""Views for the main app."""
|
||||
|
||||
import json
|
||||
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, HttpResponseNotFound
|
||||
from django.core import serializers
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from .models import Venue, Waters
|
||||
# from .models import Team, Member, Section, SectionManager, ReusableAutoField, SectionValidator
|
||||
|
||||
|
||||
def index(request):
|
||||
|
||||
venues = Venue.objects.all()
|
||||
context = {"venues": venues, "venue_types": Venue.VENUE_TYPES}
|
||||
|
||||
return render(request, 'index.html', context)
|
||||
|
||||
def results(request):
|
||||
return render(request, 'results.html')
|
||||
|
||||
def scoreboard(request):
|
||||
return render(request, 'scoreboard.html')
|
||||
|
||||
def teams(request):
|
||||
return render(request, 'teams.html')
|
||||
|
||||
def venues(request):
|
||||
|
||||
venues = Venue.objects.all()
|
||||
context = {"venues": venues, "venue_types": Venue.VENUE_TYPES}
|
||||
|
||||
return render(request, 'venues.html', context)
|
||||
|
||||
def venue_details(request, venue_id):
|
||||
|
||||
try:
|
||||
venue = Venue.objects.get(id=venue_id)
|
||||
except Venue.DoesNotExist:
|
||||
return HttpResponseNotFound("<h1>404 - Venue not found</h1>")
|
||||
|
||||
context = {"venue": venue}
|
||||
|
||||
return render(request, 'venue_details.html', context)
|
||||
|
||||
def create_venue(request):
|
||||
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error", "Method not allowed"}, status=403)
|
||||
|
||||
test = request.POST
|
||||
|
||||
attributes = {
|
||||
name: request.POST.get(name) for name in
|
||||
[field.name for field in Venue._meta.get_fields()]
|
||||
}
|
||||
venue_id = request.POST.get("id")
|
||||
|
||||
if venue_id:
|
||||
venue = Venue.objects.get(id=venue_id)
|
||||
for k, v in attributes.items():
|
||||
setattr(venue, k, v)
|
||||
|
||||
venue.save()
|
||||
return JsonResponse({"success": "successful update"}, status=200)
|
||||
|
||||
del attributes["id"]
|
||||
Venue.objects.create(**attributes)
|
||||
return JsonResponse({"success": "successful creation"}, status=200)
|
||||
|
||||
def get_venue_details(request, venue_id: int):
|
||||
|
||||
try:
|
||||
venue = Venue.objects.get(pk=venue_id)
|
||||
except Venue.DoesNotExist:
|
||||
return JsonResponse({"error": "Venue not found"}, status=404)
|
||||
|
||||
json_venue = model_to_dict(venue)
|
||||
|
||||
return JsonResponse({"data": json_venue})
|
||||
|
||||
def get_venue_waters(request, venue_id: int):
|
||||
|
||||
try:
|
||||
venue = Venue.objects.get(pk=venue_id)
|
||||
except Venue.DoesNotExist:
|
||||
return JsonResponse({"error": "Venue not found"}, status=404)
|
||||
|
||||
waters = Waters.objects.filter(venue=venue)
|
||||
waters_data = [
|
||||
{
|
||||
"name": water.name,
|
||||
"description": water.description,
|
||||
"pegs_min": water.pegs_min,
|
||||
"pegs_max": water.pegs_max,
|
||||
"water_type": water.water_type,
|
||||
"fish_type": water.fish_type
|
||||
}
|
||||
for water in waters
|
||||
]
|
||||
return JsonResponse({"waters", waters_data})
|
||||
|
||||
# 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})
|
@ -1,173 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 479 B |
@ -1,4 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 511 B |
@ -1,4 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 518 B |
@ -1,4 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 585 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 257 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 287 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 294 B |
@ -1,4 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 376 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 870 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 458 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 480 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 487 B |
@ -1,4 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 569 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 608 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318Z"/>
|
||||
<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>
|
Before Width: | Height: | Size: 645 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-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-2H2Zm5.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 637 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318Z"/>
|
||||
<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>
|
Before Width: | Height: | Size: 719 B |