Compare commits
73 Commits
Author | SHA1 | Date | |
---|---|---|---|
0757c26149 | |||
dcbb27ae2e | |||
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 | |||
bc0166b3ce | |||
|
7adf6b5bf6 | ||
|
e14be7ba75 | ||
|
6e8bc6d861 | ||
|
2739acca81 | ||
|
a00d595ebb | ||
|
e726441285 | ||
|
531d4627da | ||
|
0284db2568 | ||
|
ac5a66b054 | ||
|
bf6d441812 | ||
|
cbbf46bdb9 | ||
|
ac388a6e5b | ||
|
0abd71d498 | ||
|
aa15b6e1ee | ||
|
d1ab33430e | ||
|
a112ba38e9 | ||
|
ea1bdda8c2 | ||
|
ab17d8ded2 | ||
|
2561d97091 | ||
|
5fc0128c7c | ||
|
b4686d5cec | ||
|
8f45b233ec | ||
|
0e4de968db | ||
|
4c09b8470d | ||
|
78b9cae0ab | ||
|
9169541bae | ||
|
e604687add | ||
|
724e6ed544 | ||
|
426f381da2 | ||
|
ba1b46413c | ||
|
8a64b9e519 | ||
|
628764a338 | ||
|
8dfe4eb9f2 | ||
|
e2ad5676c7 | ||
18222e6477 |
5
.gitignore
vendored
@ -1,3 +1,8 @@
|
||||
# Static & media files
|
||||
staticfiles/
|
||||
static/CACHE/
|
||||
media/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
36
.vscode/launch.json
vendored
@ -1,19 +1,19 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Django",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\src\\manage.py",
|
||||
"args": [
|
||||
"runserver"
|
||||
],
|
||||
"django": true,
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Django",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/manage.py",
|
||||
"args": ["runserver"],
|
||||
"django": true,
|
||||
"justMyCode": true,
|
||||
"python": "${workspaceFolder}/venv/bin/python" // Linux
|
||||
// "python": "${workspaceFolder}\\venv\\Scripts\\python", // Windows
|
||||
}
|
||||
]
|
||||
}
|
||||
|
7
apps/api/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
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")
|
||||
]))
|
||||
]
|
16
apps/api/views.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""Views for the API app."""
|
||||
|
||||
from django.shortcuts import render
|
||||
from rest_framework import generics
|
||||
|
||||
from .serializers import VenueSerializer
|
||||
from apps.home import models
|
||||
|
||||
class VenueListView(generics.ListCreateAPIView):
|
||||
serializer_class = VenueSerializer
|
||||
queryset = models.Venue.objects.all().order_by("id")
|
||||
|
||||
|
||||
class VenueDetailsView(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = VenueSerializer
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
483
apps/home/templates/home/index.html
Normal file
@ -0,0 +1,483 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
|
||||
{% compress css %}
|
||||
<link type="text/x-scss" rel="stylesheet" href="{% static 'home/scss/index.scss' %}">
|
||||
{% endcompress %}
|
||||
{% endblock stylesheets %}
|
||||
|
||||
{% block header_buttons %}
|
||||
{% endblock header_buttons %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 bg-body shadow mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center p-2 flex-wrap">
|
||||
<h1 class="fw-bold h4 mb-0 ms-2">Venues & Waters</h1>
|
||||
<div class="input-group w-auto">
|
||||
<div class="input-group-text bg-body pe-0">
|
||||
<i class="bi bi-search"></i>
|
||||
</div>
|
||||
<input type="search" class="form-control border-start-0" placeholder="Search Venues">
|
||||
</div>
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<button class="btn btn-primary rounded-2 me-3" onclick="openVenueModal(-1);">
|
||||
<i class="bi bi-plus-lg me-1"></i>
|
||||
New
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary border-secondary-subtle rounded-2 me-3">
|
||||
<i class="bi bi-upload me-1"></i>
|
||||
Import
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary border-secondary-subtle rounded-2 me-3">
|
||||
<i class="bi bi-sort-alpha-up me-sm-1"></i>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary border-secondary-subtle rounded-2">
|
||||
<i class="bi bi-filter-right me-sm-1"></i>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-row-reverse my-4 mx-lg-4">
|
||||
<div class="col-lg-3 d-flex flex-column">
|
||||
<div id="locationMapContainer" class="w-100 position-relative shadow mb-4 bg-body rounded-3 overflow-hidden" style="height: 300px;">
|
||||
<div id="allLocationsMap" class="w-100 h-100"></div>
|
||||
</div>
|
||||
<div class="bg-body rounded-3 p-4 flex-grow-1 shadow mb-3">
|
||||
<div class="text-body small">
|
||||
placeholder<br>
|
||||
maybe a graph here?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<div class="row gx-3 px-2 mt-0">
|
||||
{% for venue in venues %}
|
||||
<div class="col-xxl-4 col-xl-6 col-md-6 col-sm-12 mb-3">
|
||||
<div class="rounded-2 h-100 p-4 bg-body shadow d-flex flex-column">
|
||||
<header class="d-flex justify-content-between align-items-start mb-3 dropend">
|
||||
<div class="text-truncate">
|
||||
<h4 class="h6 mb-1 text-body-secondary small">
|
||||
{% if venue.venue_type == "FISHERY" %}
|
||||
Fishery
|
||||
{% elif venue.venue_type == "PRIVATE" %}
|
||||
Private
|
||||
{% elif venue.venue_type == "CLUB" %}
|
||||
Club
|
||||
{% endif %}
|
||||
</h4>
|
||||
<h4 class="h6 mb-0 me-2 text-truncate fw-bold">{{ venue.name }}</h4>
|
||||
</div>
|
||||
<button class="border-0 bg-transparent" type="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-three-dots fs-4 d-flex"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu rounded-4 overflow-hidden py-0 shadow-sm" style="width: fit-content; min-width: fit-content;">
|
||||
<li>
|
||||
<button class="dropdown-item hover-fill-primary py-2" onclick="viewVenueModal({{ venue.id }})">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider my-0">
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item py-2" onclick="openVenueModal({{ venue.id }});">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider my-0">
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item py-2 hover-fill-danger" onclick="deleteVenueBtn({{ venue.id }});">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<p class="text-body-secondary venue-description mb-4">{{ venue.description }}</p>
|
||||
<div class="d-flex align-items-center fs-6 mt-auto ">
|
||||
{% if venue.email_address %}
|
||||
<a href="mailto:{{ venue.email_address }}" class="text-reset text-hover-primary me-3" data-bs-toggle="tooltip" data-bs-title="{{ venue.email_address }}">
|
||||
<i class="bi bi-envelope-at"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if venue.phone_number %}
|
||||
<a href="tel:{{ venue.phone_number }}" class="text-reset text-hover-primary me-3" data-bs-toggle="tooltip" data-bs-title="{{ venue.phone_number }}">
|
||||
<i class="bi bi-telephone"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if venue.latitude and venue.longitude %}
|
||||
<a href="https://www.openstreetmap.org/?mlat={{ venue.latitude }}&mlon={{ venue.longitude }}" target="_blank" class="text-reset text-hover-primary me-3" data-bs-toggle="tooltip" data-bs-html="true" data-bs-title="{{ venue.street_address }}<br>{{ venue.city }}, {{ venue.provence }}<br>{{ venue.postal_code }}">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if venue.website_url %}
|
||||
<a href="{{ venue.website_url }}" class="text-reset text-hover-primary me-3" target="_blank" data-bs-toggle="tooltip" data-bs-title="{{ venue.website_url }}">
|
||||
<i class="bi bi-globe2"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="d-inline mx-auto"></div>
|
||||
{% if venue.waters %}
|
||||
<div class="badge bg-info-subtle text-info-emphasis ms-2" data-bs-toggle="tooltip" data-bs-title="There are {{ venue.waters|length }} waters in this venue">
|
||||
{{ venue.waters|length }}
|
||||
<i class="bi bi-droplet-half "></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not venue.latitude or not venue.longitude or not venue.phone_number or not venue.website_url or not venue.email_address %}
|
||||
<div class="badge bg-warning-subtle text-warning-emphasis ms-2" data-bs-toggle="tooltip" data-bs-title="There are missing details for this venue">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not venue.active %}
|
||||
<div class="badge bg-danger-subtle text-danger-emphasis ms-2" data-bs-toggle="tooltip" data-bs-title="This venue is inactive">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="col-xxl-4 col-xl-6 col-md-6 col-sm-12">
|
||||
<div class="fluid-hover-zoom h-100 d-flex flex-column justify-content-center align-items-center" role="button" onclick="openVenueModal(-1);">
|
||||
<i class="bi bi-plus-lg fs-1"></i>
|
||||
<span class="fw-bold">
|
||||
Create new
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="venueModal" class="modal fade" data-venue-id="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-lg-down modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title text-truncate me-5">New Venue</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="venueForm" class="flex-md-grow-1 d-flex" novalidate>
|
||||
<div class="row g-0 flex-md-grow-1">
|
||||
<div class="col-md-1">
|
||||
<div id="sidebarNavigation" class="bg-body-tertiary">
|
||||
<div class="modal-sidebar flex-row flex-md-column align-items-start text-center" role="tablist">
|
||||
<button id="detailsTab" class="modal-sidebar-btn active" type="button" data-bs-toggle="tab" data-bs-target="#detailsContent" role="tab" aria-controls="detailsContent" aria-selected="true">
|
||||
<i class="bi bi-file-earmark-text"></i>
|
||||
</button>
|
||||
<button id="addressTab" class="modal-sidebar-btn" type="button" data-bs-toggle="tab" data-bs-target="#addressContent" role="tab" aria-controls="addressContent" aria-selected="false">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
<button id="contactTab" class="modal-sidebar-btn" type="button" data-bs-toggle="tab" data-bs-target="#contactContent" role="tab" aria-controls="contactContent" aria-selected="false">
|
||||
<i class="bi bi-telephone"></i>
|
||||
</button>
|
||||
<button id="watersTab" class="modal-sidebar-btn" type="button" data-bs-toggle="tab" data-bs-target="#watersContent" role="tab" aria-controls="watersContent" aria-selected="false">
|
||||
<i class="bi bi-droplet-half"></i>
|
||||
</button>
|
||||
<button id="confirmTab" class="modal-sidebar-btn mt-md-auto" type="button" data-bs-toggle="tab" data-bs-target="#confirmContent" role="tab" aria-controls="confirmContent" aria-selected="false">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-11 overflow-auto">
|
||||
<div class="tab-content d-inline-block w-100 h-100 py-3 px-4">
|
||||
<div id="detailsContent" class="tab-pane fade flex-column h-100 show active">
|
||||
<div class="form-floating mb-4">
|
||||
<input type="text" name="venueName" id="venueName" class="form-control" placeholder="">
|
||||
<label for="venueName" class="form-label">Venue Name</label>
|
||||
</div>
|
||||
<div class="form-floating mb-4">
|
||||
<textarea name="venueDesc" id="venueDesc" class="form-control" style="height: 150px; resize: none;" placeholder=""></textarea>
|
||||
<label for="venueDesc" class="form-label">Description</label>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" name="venueType" id="venueFishery" class="form-check-input" value="FISHERY">
|
||||
<label for="venueFishery" class="form-check-label">Fishery</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" name="venueType" id="venueClub" class="form-check-input" value="CLUB">
|
||||
<label for="venueClub" class="form-check-label">Club</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" name="venueType" id="venuePrivate" class="form-check-input" value="PRIVATE">
|
||||
<label for="venuePrivate" class="form-check-label">Private</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="venueActive" id="venueActive" class="form-check-input">
|
||||
<label for="venueActive" class="form-check-label">Venue is active?</label>
|
||||
</div>
|
||||
<div class="mt-5 mt-md-auto d-flex justify-content-end">
|
||||
<button class="btn btn-primary px-3" type="button" onclick="$('#addressTab').click();">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="addressContent" class="tab-pane fade flex-column h-100">
|
||||
<div class="form-floating mb-4">
|
||||
<input type="text" name="venueSearch" id="venueSearch" class="form-control" placeholder="">
|
||||
<label for="venueSearch" class="form-label">
|
||||
<i class="bi bi-search me-1"></i>
|
||||
Post Code or Street Address
|
||||
</label>
|
||||
</div>
|
||||
<div id="venueSearchResults" style="display: none">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venueStreetNum" id="venueStreetNum" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venueStreetNum" class="form-label">Street Number</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venueStreet" id="venueStreet" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venueStreet" class="form-label">Street Address</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venueCity" id="venueCity" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venueCity" class="form-label">Town/City</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venueCounty" id="venueCounty" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venueCounty" class="form-label">County/Region</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venuePost" id="venuePost" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venuePost" class="form-label">Post Code</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venueCountry" id="venueCountry" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venueCountry" class="form-label">Country</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venueLat" id="venueLat" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venueLat" class="form-label">Latitude</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" name="venueLng" id="venueLng" class="form-control-plaintext" placeholder="" readonly>
|
||||
<label for="venueLng" class="form-label">Longitude</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="venueSearchHelper" class="my-auto text-center">
|
||||
<label for="venueSearch" class="fs-4 mx-lg-4 text-body-secondary">Use the search to find your venue.</label>
|
||||
</div>
|
||||
<div class="mt-5 mt-md-auto d-flex justify-content-end">
|
||||
<button class="btn btn-outline-secondary me-3" type="button" onclick="$('#detailsTab').click();">Back</button>
|
||||
<button class="btn btn-primary px-3" type="button" onclick="$('#contactTab').click();">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="contactContent" class="tab-pane fade flex-column h-100">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-floating mb-4">
|
||||
<input type="tel" name="venuePhone" id="venuePhone" class="form-control" placeholder="">
|
||||
<label for="venuePhone" class="form-label">Phone Number</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-floating mb-4">
|
||||
<input type="email" name="venueEmail" id="venueEmail" class="form-control" placeholder="">
|
||||
<label for="venueEmail" class="form-label">Email Address</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-floating mb-4">
|
||||
<input type="url" name="venueWebsite" id="venueWebsite" class="form-control" placeholder="">
|
||||
<label for="venueWebsite" class="form-label">Website URL</label>
|
||||
</div>
|
||||
<div class="form-floating mb-4">
|
||||
<input type="url" name="venueFacebook" id="venueFacebook" class="form-control" placeholder="">
|
||||
<label for="venueFacebook" class="form-label">Facebook URL</label>
|
||||
</div>
|
||||
<div class="form-floating mb-4">
|
||||
<input type="url" name="venueInstagram" id="venueInstagram" class="form-control" placeholder="">
|
||||
<label for="venueInstagram" class="form-label">Instagram URL</label>
|
||||
</div>
|
||||
<div class="form-floating mb-4">
|
||||
<input type="url" name="venueTwitter" id="venueTwitter" class="form-control" placeholder="">
|
||||
<label for="venueTwitter" class="form-label">Twitter URL</label>
|
||||
</div>
|
||||
<div class="mt-5 mt-md-auto d-flex justify-content-end">
|
||||
<button class="btn btn-outline-secondary me-3" type="button" onclick="$('#addressTab').click();">Back</button>
|
||||
<button class="btn btn-primary px-3" type="button" onclick="$('#watersTab').click();">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watersContent" class="tab-pane fade flex-column h-100">
|
||||
waters
|
||||
<div class="mt-5 mt-md-auto d-flex justify-content-end">
|
||||
<button class="btn btn-outline-secondary me-3" type="button" onclick="$('#contactTab').click();">Back</button>
|
||||
<button class="btn btn-primary px-3" type="button" onclick="$('#confirmTab').click();">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="confirmContent" class="tab-pane fade flex-column h-100">
|
||||
<h5 class="mb-3">Preview:</h5>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-floating mb-3 venue-preview-field">
|
||||
<input type="text" class="form-control-plaintext" placeholder="" readonly data-linked-field="#venueName">
|
||||
<label class="form-label">Venue Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3 venue-preview-field">
|
||||
<input type="text" class="form-control-plaintext" placeholder="" readonly data-linked-field="#venueDesc">
|
||||
<label class="form-label">Description</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating mb-3 venue-preview-field">
|
||||
<input type="text" class="form-control-plaintext" placeholder="" readonly data-linked-field="#venueType">
|
||||
<label class="form-label">Venue Type</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mt-md-auto d-flex justify-content-end">
|
||||
<button class="btn btn-outline-secondary me-3" type="button" onclick="$('#watersTab').click();">Back</button>
|
||||
<button id="submitVenueBtn" class="btn btn-primary" type="submit">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div id="venueModal" class="modal fade" data-venue-id="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down ">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Venue</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div id="newVenuePages" class="modal-body">
|
||||
<div id="detailsPage" class="page active">
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" placeholder="">
|
||||
<label for="" class="form-label">Name</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<textarea class="form-control venue-textarea" placeholder=""></textarea>
|
||||
<label for="" class="form-label">Description</label>
|
||||
</div>
|
||||
<label class="form-label">Type</label>
|
||||
<div class="mb-3 group-radio-btns">
|
||||
<input type="radio" name="vType" id="vtFishery" class="btn-check">
|
||||
<label for="vtFishery" class="btn btn-outline-primary">Fishery</label>
|
||||
<input type="radio" name="vType" id="vtClub" class="btn-check">
|
||||
<label for="vtClub" class="btn btn-outline-primary">Club</label>
|
||||
<input type="radio" name="vType" id="vtPrivate" class="btn-check">
|
||||
<label for="vtPrivate" class="btn btn-outline-primary">Private</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="addressPage" class="page">
|
||||
<label class="form-label">Type part of the address or postcode to begin</label>
|
||||
<input type="text" class="form-control" placeholder="E.g. 'CR0 3RL' or '36 Factory Lane'">
|
||||
</div>
|
||||
<div id="contactPage" class="page">
|
||||
<div class="row mb-3">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating">
|
||||
<input type="tel" class="form-control" placeholder="">
|
||||
<label for="">Phone Number</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control" placeholder="">
|
||||
<label for="">Email Address</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="url" class="form-control" placeholder="">
|
||||
<label for="">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
Website
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="url" class="form-control" placeholder="">
|
||||
<label for="">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
Facebook
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="url" class="form-control" placeholder="">
|
||||
<label for="">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
Instagram
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="url" class="form-control" placeholder="">
|
||||
<label for="">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
Twitter
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watersPage" class="page">
|
||||
Waters
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" id="newVenueBack" class="btn btn-secondary" style="display: none">
|
||||
<i class="bi bi-arrow-left me-1"></i>
|
||||
Back
|
||||
</button>
|
||||
<button type="button" id="newVenueNext" class="btn btn-primary">
|
||||
Next
|
||||
<i class="bi bi-arrow-right ms-1"></i>
|
||||
</button>
|
||||
<button type="button" id="newVenueFinished" class="btn btn-primary" style="display: none;">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl, {trigger : 'hover'}))
|
||||
</script>
|
||||
<script>
|
||||
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r, "places"]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
|
||||
key: "AIzaSyDFR2xw2LhMPb6FC8OrfiLrJZRMRLMocvw",
|
||||
v: "weekly",
|
||||
});
|
||||
</script>
|
||||
|
||||
<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 %}+
|
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 %}
|
3
apps/home/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
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()
|
213
core/settings.py
Normal file
@ -0,0 +1,213 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
filename = timezone.now().strftime('%Y-%m-%d_%H-%M-%S')
|
||||
LOGGING["handlers"]["file"]["filename"] = LOGGING_DIR / f"{filename}.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"
|
@ -13,11 +13,11 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from baton.autodiscover import admin
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('baton/', include('baton.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
@ -1,4 +1,7 @@
|
||||
@echo off
|
||||
echo This file is outdated
|
||||
set /p dontCare=Do you want to continue? [y/n]
|
||||
|
||||
echo The purpose of this file is to populate the database with dummy data for testing. Running this WILL DELETE your current sqlite3 database.
|
||||
set /p confirm=Do you want to continue? [y/n]
|
||||
|
11
scripts/createsuperuser.bat
Normal file
@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
cd %~dp0
|
||||
|
||||
call venv/Scripts/activate.bat
|
||||
|
||||
python src/manage.py migrate
|
||||
|
||||
python src/manage.py create_superuser --username admin --password password --noinput --email "admin@mail.com"
|
||||
|
||||
echo "A superuser has been created username='admin' and password='password'"
|
||||
pause
|
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
|
@ -7,6 +7,7 @@ Target Audience Tech Knowledge - Mid to Low
|
||||
Notes:
|
||||
- Pull up the system for an event, input data that is happening in the event.
|
||||
- Live scoreboard, updated when new data is inserted into the database.
|
||||
- Seperate marks per event. Stores finished events in an archive.
|
||||
|
||||
Assuming the number of members are divised equally amongst the teams - the total number of pegs can be calculated by
|
||||
multiplying the total number of teams by the amount of members a team
|
||||
|
@ -1,144 +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 = ["*", "192.168.5.105"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'baton',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# 'rest_framework_datatables'
|
||||
'mainapp',
|
||||
'baton.autodiscover',
|
||||
]
|
||||
|
||||
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/']
|
||||
|
||||
# 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,41 +0,0 @@
|
||||
"""Admin models for the mainapp app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Peg, Member, Team
|
||||
|
||||
|
||||
@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 = ("team_number",)
|
||||
list_display = ("team_number",)
|
||||
search_fields = ("team_number",)
|
||||
|
||||
|
||||
# @admin.register(Section)
|
||||
# class SectionAdmin(admin.ModelAdmin):
|
||||
# """Admin model for the Section model."""
|
||||
|
||||
# list_display = ("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,302 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"first_name": "Robert",
|
||||
"last_name": "Reid",
|
||||
"team": 1,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"first_name": "Ronald",
|
||||
"last_name": "Jones",
|
||||
"team": 1,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"first_name": "Casey",
|
||||
"last_name": "Cohen",
|
||||
"team": 1,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"first_name": "James",
|
||||
"last_name": "Scudder",
|
||||
"team": 2,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"first_name": "Randall",
|
||||
"last_name": "Young",
|
||||
"team": 2,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"first_name": "Helen",
|
||||
"last_name": "Doak",
|
||||
"team": 2,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"first_name": "Brenda",
|
||||
"last_name": "Powell",
|
||||
"team": 3,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"first_name": "Constance",
|
||||
"last_name": "Abild",
|
||||
"team": 3,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"first_name": "Patsy",
|
||||
"last_name": "Branham",
|
||||
"team": 3,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"first_name": "Cheryl",
|
||||
"last_name": "Sears",
|
||||
"team": 4,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"first_name": "Justin",
|
||||
"last_name": "Cramer",
|
||||
"team": 4,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 12,
|
||||
"fields": {
|
||||
"first_name": "Theodore",
|
||||
"last_name": "Wilson",
|
||||
"team": 4,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 13,
|
||||
"fields": {
|
||||
"first_name": "Geneva",
|
||||
"last_name": "Low",
|
||||
"team": 5,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 14,
|
||||
"fields": {
|
||||
"first_name": "John",
|
||||
"last_name": "Burtt",
|
||||
"team": 5,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 15,
|
||||
"fields": {
|
||||
"first_name": "Alfred",
|
||||
"last_name": "Diaz",
|
||||
"team": 5,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 16,
|
||||
"fields": {
|
||||
"first_name": "Arthur",
|
||||
"last_name": "Alton",
|
||||
"team": 6,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 17,
|
||||
"fields": {
|
||||
"first_name": "Vicki",
|
||||
"last_name": "Greer",
|
||||
"team": 6,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 18,
|
||||
"fields": {
|
||||
"first_name": "Lewis",
|
||||
"last_name": "Segovia",
|
||||
"team": 6,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 19,
|
||||
"fields": {
|
||||
"first_name": "Vince",
|
||||
"last_name": "Robinson",
|
||||
"team": 7,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 20,
|
||||
"fields": {
|
||||
"first_name": "Blake",
|
||||
"last_name": "Mueller",
|
||||
"team": 7,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 21,
|
||||
"fields": {
|
||||
"first_name": "Luis",
|
||||
"last_name": "Hazel",
|
||||
"team": 7,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 22,
|
||||
"fields": {
|
||||
"first_name": "Diane",
|
||||
"last_name": "Lloyd",
|
||||
"team": 8,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 23,
|
||||
"fields": {
|
||||
"first_name": "Jamey",
|
||||
"last_name": "Mendes",
|
||||
"team": 8,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 24,
|
||||
"fields": {
|
||||
"first_name": "Virgilio",
|
||||
"last_name": "Nixon",
|
||||
"team": 8,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 25,
|
||||
"fields": {
|
||||
"first_name": "Rodney",
|
||||
"last_name": "White",
|
||||
"team": 9,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 26,
|
||||
"fields": {
|
||||
"first_name": "Kathleen",
|
||||
"last_name": "Ashe",
|
||||
"team": 9,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 27,
|
||||
"fields": {
|
||||
"first_name": "Stephanie",
|
||||
"last_name": "Taylor",
|
||||
"team": 9,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 28,
|
||||
"fields": {
|
||||
"first_name": "John",
|
||||
"last_name": "Brennan",
|
||||
"team": 10,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 29,
|
||||
"fields": {
|
||||
"first_name": "Kenneth",
|
||||
"last_name": "Duff",
|
||||
"team": 10,
|
||||
"peg_number": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.member",
|
||||
"pk": 30,
|
||||
"fields": {
|
||||
"first_name": "Matthew",
|
||||
"last_name": "Whitesell",
|
||||
"team": 10,
|
||||
"peg_number": null
|
||||
}
|
||||
}
|
||||
]
|
@ -1,72 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"section_letter": "D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"section_letter": "P"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"section_letter": "L"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"section_letter": "M"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"section_letter": "Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"section_letter": "Y"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"section_letter": "J"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"section_letter": "W"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"section_letter": "T"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "mainapp.team",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"section_letter": "F"
|
||||
}
|
||||
}
|
||||
]
|
@ -1,52 +0,0 @@
|
||||
import random
|
||||
import names
|
||||
import json
|
||||
from django.core.management.base import BaseCommand
|
||||
from mainapp.models import Member, Team
|
||||
|
||||
|
||||
# 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("num_members", type=int)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
num_members = options["num_members"]
|
||||
teams = Team.objects.all()
|
||||
if not teams:
|
||||
print("No teams found. Please create some teams first.")
|
||||
return
|
||||
|
||||
members = []
|
||||
for team in teams:
|
||||
for _ in range(num_members):
|
||||
first_name = names.get_first_name()
|
||||
last_name = names.get_last_name()
|
||||
member = Member(first_name=first_name, last_name=last_name, team=team)
|
||||
members.append(member)
|
||||
|
||||
Member.objects.bulk_create(members)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Created {num_members} members."))
|
||||
|
||||
# create a 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,64 +0,0 @@
|
||||
"""Command to create test data fixture for teams."""
|
||||
|
||||
import random
|
||||
import json
|
||||
from string import ascii_uppercase
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.utils import IntegrityError
|
||||
from mainapp.models import Team, BLOCKED_SECTION_LETTERS
|
||||
|
||||
|
||||
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):
|
||||
|
||||
existing_teams = Team.objects.all()
|
||||
|
||||
# Available sections
|
||||
available_sections = [
|
||||
char for char in [*ascii_uppercase]
|
||||
if char.lower() not in BLOCKED_SECTION_LETTERS
|
||||
and char not in [team.section_letter for team in existing_teams]
|
||||
]
|
||||
|
||||
if not available_sections:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
f"There are no available sections for new teams."
|
||||
))
|
||||
return
|
||||
|
||||
teams_amount = options["amount_of_teams"]
|
||||
max_teams = len(available_sections)
|
||||
|
||||
if teams_amount > max_teams:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
f"Number of teams is too large [{teams_amount}/{max_teams}]."
|
||||
))
|
||||
return
|
||||
|
||||
# Create the new teams (this will create them in the database)#
|
||||
new_teams = []
|
||||
for i in range(teams_amount):
|
||||
section_letter = random.choice(available_sections)
|
||||
available_sections.remove(section_letter)
|
||||
team = Team.objects.create(section_letter=section_letter)
|
||||
new_teams.append(team)
|
||||
|
||||
teams_fixture = [{
|
||||
"model": "mainapp.team",
|
||||
"pk": team.pk,
|
||||
"fields": {"section_letter": team.section_letter}
|
||||
} 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,39 +0,0 @@
|
||||
# Generated by Django 4.1.5 on 2023-05-07 21:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import mainapp.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Peg',
|
||||
fields=[
|
||||
('peg_number', mainapp.models.ReusableAutoField(default=mainapp.models.ReusableAutoField.get_default, editable=False, primary_key=True, serialize=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('team_number', mainapp.models.ReusableAutoField(default=mainapp.models.ReusableAutoField.get_default, editable=False, primary_key=True, serialize=False)),
|
||||
('section_letter', models.CharField(choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G'), ('H', 'H'), ('I', 'I'), ('J', 'J'), ('K', 'K'), ('L', 'L'), ('M', 'M'), ('N', 'N'), ('O', 'O'), ('P', 'P'), ('Q', 'Q'), ('R', 'R'), ('S', 'S'), ('T', 'T'), ('U', 'U'), ('V', 'V'), ('W', 'W'), ('X', 'X'), ('Y', 'Y'), ('Z', 'Z')], max_length=1, 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)),
|
||||
('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='mainapp.team', validators=[mainapp.models.validate_team_size])),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,131 +0,0 @@
|
||||
from string import ascii_uppercase
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
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 Peg(models.Model):
|
||||
"""Represents a person's peg"""
|
||||
|
||||
peg_number = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"Peg {self.peg_number}"
|
||||
|
||||
|
||||
def validate_team_size(value):
|
||||
if Member.objects.filter(team=value).count() >= 3:
|
||||
raise ValidationError('Team already has maximal amount of members (3)')
|
||||
|
||||
|
||||
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, validators=(validate_team_size,), related_name='members')
|
||||
peg_number = models.PositiveIntegerField(null=True, editable=True, unique=True)
|
||||
# peg = models.OneToOneField("Peg", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
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.team_number}))"
|
||||
|
||||
@property
|
||||
def fullname(self) -> str:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
|
||||
BLOCKED_SECTION_LETTERS = ["i"] # lowercase only, sections cannot be any of these letters#
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
"""Represents a team"""
|
||||
|
||||
team_number = ReusableAutoField(primary_key=True, default=ReusableAutoField.get_default, editable=False)
|
||||
section_letter = models.CharField(max_length=1, unique=True, choices=[
|
||||
(char, char) for char in ascii_uppercase if char.lower() not in BLOCKED_SECTION_LETTERS
|
||||
])
|
||||
|
||||
def __str__(self):
|
||||
return f"Team {self.team_number}"
|
||||
|
||||
@property
|
||||
def members(self) -> list[Member]:
|
||||
Member.objects.filter(team=self)
|
||||
|
||||
|
||||
def validate_section_character(value):
|
||||
"""Validates the section character"""
|
||||
|
||||
value = value.upper()
|
||||
banned_characters = ["I"]
|
||||
|
||||
if value in banned_characters:
|
||||
raise ValidationError(f"The character <{value}> is a prohibited character.")
|
||||
|
||||
|
||||
# class Section(models.Model):
|
||||
# """Represents a section of the scoreboard"""
|
||||
|
||||
# # character field stores a single character but doesnt allow for 'I' or 'i'
|
||||
# character = models.CharField(primary_key=True, max_length=1, validators=(validate_section_character, ))
|
||||
|
||||
# def clean(self):
|
||||
# self.character = self.character.upper()
|
||||
|
||||
# def __str__(self):
|
||||
# return f"Section {self.character}"
|
||||
|
||||
|
||||
#class Scoreboard(models.Model):
|
||||
# class Status(models.IntegerChoices):
|
||||
# ACTIVE = 1, "Active"
|
||||
# INACTIVE = 2, "Inactive"
|
||||
# ARCHIVED = 3, "Archived"
|
||||
#
|
||||
# name = models.CharField(max_length=255)
|
||||
# category = models.CharField(max_length=255)
|
||||
# price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
# cost = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
# status = models.PositiveSmallIntegerField(choices=Status.choices)
|
||||
#
|
||||
# def __str__(self):
|
||||
# return self.name
|
@ -1,11 +0,0 @@
|
||||
import names
|
||||
from string import ascii_lowercase
|
||||
|
||||
created_teams = []
|
||||
for char in ascii_lowercase:
|
||||
created_teams.append({
|
||||
"identifier": char.upper(),
|
||||
"members": [{
|
||||
"name": names.get_full_name()
|
||||
} for i in range(9)]
|
||||
})
|
@ -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">Start Pegging</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,56 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="py-5 text-center container">
|
||||
<div class="row py-lg-5">
|
||||
<div class="col-lg-6 col-md-8 mx-auto">
|
||||
<h1 class="fw-light text-white">Results System</h1>
|
||||
<p class="lead text-white">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi faucibus elementum diam nec semper. Nullam vestibulum enim eu nisi condimentum, vitae suscipit risus imperdiet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="album py-5 bg-light">
|
||||
<div class="container">
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
|
||||
|
||||
<!-- Teams & Members -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="{% url 'teams' %}">
|
||||
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Teams / Members</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scoreboard -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="{% url 'scoreboard' %}">
|
||||
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Scoreboard</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div class="col">
|
||||
<div class="card shadow-sm">
|
||||
<a href="{% url 'results' %}">
|
||||
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Thumbnail" preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">Thumbnail</text></svg>
|
||||
<div class="card-body">
|
||||
<p class="card-text">Results</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -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,160 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Teams & Members |
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-body p-4">
|
||||
<a href="{% url 'index' %}" class="btn btn-company px-4">Back</a>
|
||||
</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">
|
||||
<h3 class="mb-3 mb-xl-0">Teams & Members</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 Member">
|
||||
<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>
|
||||
</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 Members" 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="team_number" name="sortTeams" 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="section_letter" name="sortTeams" 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 members 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="teamsContainer"></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 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">
|
||||
<h3 class="modal-title fs-5" id="editTeamTitle">Team Number Here</h3>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editTeamForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<label for="editTeamNumber" class="form-label">Team Number</label>
|
||||
<input type="number" name="editTeamNumber" class="form-control" id="editTeamNumber">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="editTeamSection" class="form-label">Section Letter</label>
|
||||
<input type="text" name="editTeamSection" class="form-control" id="editTeamSection">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="saveEditTeamModal">Save Changes</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">
|
||||
<h3 class="modal-title fs-5" id="editMemberName">Member Name Here</h3>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editMemberForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editMemberFirstName" class="form-label">Forename</label>
|
||||
<input type="text" name="editMemberFirstName" id="editMemberFirstName" class="form-control" required minlength="1" maxlength="30">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editMemberLastName" class="form-label">Surname</label>
|
||||
<input type="text" name="editMemberLastName" id="editMemberLastName" class="form-control" required minlength="1" maxlength="30">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="editMemberTeam" class="form-label">Team</label>
|
||||
<select name="editMemberTeam" id="editMemberTeam" class="form-select" required></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="editMemberPeg" class="form-label">Peg Number</label>
|
||||
<input type="number" name="editMemberPeg" id="editMemberPeg" class="form-control" min="1" max="9999">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="saveEditModal">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% load static %}
|
||||
{% block scripts %}
|
||||
<script type="text/javascript">
|
||||
const getTeamsUrl = "{% url 'get-teams' %}";
|
||||
const updateMemberUrl = "{% url 'update-member' %}";
|
||||
const csrfMiddlewareToken = "{{ csrf_token }}";
|
||||
</script>
|
||||
<script src="{% static 'js/teams.js' %}"></script>
|
||||
{% endblock scripts %}
|
@ -1,13 +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('teams/', views.teams, name='teams'),
|
||||
|
||||
path('bulk-peg/', views.bulk_create_pegs, name='bulk-peg'),
|
||||
path('get-teams/', views.get_teams, name='get-teams'),
|
||||
path('update-member/', views.update_member, name='update-member')
|
||||
]
|
@ -1,116 +0,0 @@
|
||||
"""Views for the main app."""
|
||||
|
||||
from functools import reduce
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q, Case, When, Value, IntegerField
|
||||
|
||||
from .models import Peg, Team, Member
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'index.html')
|
||||
|
||||
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 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 get_teams(request):
|
||||
"""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_teams = request.POST.get("sortTeams") or "team_number"
|
||||
sort_members = request.POST.get("sortMembers") or "peg_number"
|
||||
|
||||
teams = Team.objects.order_by(sort_teams).all()
|
||||
|
||||
# Filter out teams that don't contain members being searched for
|
||||
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()
|
||||
|
||||
# Create a dictionary for the data that is JSON safe
|
||||
response_data = {"teams": []}
|
||||
for team in teams:
|
||||
team_data = {
|
||||
"team_number": team.team_number,
|
||||
"section_letter": team.section_letter if team.section_letter else None,
|
||||
"members": [
|
||||
{
|
||||
"first": member.first_name,
|
||||
"last": member.last_name,
|
||||
"id": member.id,
|
||||
"team": team.team_number,
|
||||
"peg": member.peg_number,
|
||||
"section": team.section_letter if team.section_letter else None
|
||||
}
|
||||
for member in team.members.order_by(sort_members).all()
|
||||
]
|
||||
}
|
||||
response_data["teams"].append(team_data)
|
||||
|
||||
response_data["sortTeams"] = sort_teams
|
||||
response_data["sortMembers"] = sort_members
|
||||
|
||||
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(team_number=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_teams(request)
|
@ -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 |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM7.519 5.057c-.886 1.418-1.772 2.838-2.542 4.265v1.12H8.85V12h1.26v-1.559h1.007V9.334H10.11V4.002H8.176c-.218.352-.438.703-.657 1.055ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 391 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057c.22-.352.439-.703.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218Z"/>
|
||||
<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: 433 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218Z"/>
|
||||
<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.519 5.057c.22-.352.439-.703.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 440 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057c.22-.352.439-.703.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218Z"/>
|
||||
<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: 507 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-8.006 4.158c1.74 0 2.924-1.119 2.924-2.806 0-1.641-1.178-2.584-2.56-2.584-.897 0-1.442.421-1.612.68h-.064l.193-2.344h3.621V4.002H5.791L5.445 8.63h1.149c.193-.358.668-.809 1.435-.809.85 0 1.582.604 1.582 1.57 0 1.085-.779 1.682-1.57 1.682-.697 0-1.389-.31-1.53-1.031H5.276c.065 1.213 1.149 2.115 2.72 2.115Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 496 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 1 14 0A7 7 0 0 1 1 8Zm15 0A8 8 0 1 0 0 8a8 8 0 0 0 16 0Zm-8.006 4.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 516 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-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.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 523 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square" viewBox="0 0 16 16">
|
||||
<path d="M7.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
<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: 605 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM8.21 3.855c-1.868 0-3.116 1.395-3.116 4.407 0 1.183.228 2.039.597 2.642.569.926 1.477 1.254 2.409 1.254 1.629 0 2.847-1.013 2.847-2.783 0-1.676-1.254-2.555-2.508-2.555-1.125 0-1.752.61-1.98 1.155h-.082c-.012-1.946.727-3.036 1.805-3.036.802 0 1.213.457 1.312.815h1.29c-.06-.908-.962-1.899-2.573-1.899Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 619 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-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 0ZM8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 643 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M8.111 7.863c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582Z"/>
|
||||
<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-2H2Zm6.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 665 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-6-square" viewBox="0 0 16 16">
|
||||
<path d="M8.21 3.855c1.612 0 2.515.99 2.573 1.899H9.494c-.1-.358-.51-.815-1.312-.815-1.078 0-1.817 1.09-1.805 3.036h.082c.229-.545.855-1.155 1.98-1.155 1.254 0 2.508.88 2.508 2.555 0 1.77-1.218 2.783-2.847 2.783-.932 0-1.84-.328-2.409-1.254-.369-.603-.597-1.459-.597-2.642 0-3.012 1.248-4.407 3.117-4.407Zm-.099 4.008c-.92 0-1.564.65-1.564 1.576 0 1.032.703 1.635 1.558 1.635.868 0 1.553-.533 1.553-1.629 0-1.06-.744-1.582-1.547-1.582Z"/>
|
||||
<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: 732 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0ZM5.37 5.11h3.972v.07L6.025 12H7.42l3.258-6.85V4.002H5.369v1.107Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 252 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-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 0ZM5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 281 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-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-2H2Zm3.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 288 B |
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-7-square" viewBox="0 0 16 16">
|
||||
<path d="M5.37 5.11V4.001h5.308V5.15L7.42 12H6.025l3.317-6.82v-.07H5.369Z"/>
|
||||
<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: 370 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-5.03 1.803c0-1.248-.943-1.84-1.646-1.992v-.065c.598-.187 1.336-.72 1.336-1.781 0-1.225-1.084-2.121-2.654-2.121-1.57 0-2.66.896-2.66 2.12 0 1.044.709 1.589 1.33 1.782v.065c-.697.152-1.647.732-1.647 2.003 0 1.39 1.19 2.344 2.953 2.344 1.77 0 2.989-.96 2.989-2.355Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 694 B |
@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-8-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 0Zm-5.03 1.803c0 1.394-1.218 2.355-2.988 2.355-1.763 0-2.953-.955-2.953-2.344 0-1.271.95-1.851 1.647-2.003v-.065c-.621-.193-1.33-.738-1.33-1.781 0-1.225 1.09-2.121 2.66-2.121s2.654.896 2.654 2.12c0 1.061-.738 1.595-1.336 1.782v.065c.703.152 1.647.744 1.647 1.992Zm-4.347-3.71c0 .739.586 1.255 1.383 1.255s1.377-.516 1.377-1.254c0-.733-.58-1.23-1.377-1.23s-1.383.497-1.383 1.23Zm-.281 3.645c0 .838.72 1.412 1.664 1.412.943 0 1.658-.574 1.658-1.412 0-.843-.715-1.424-1.658-1.424-.944 0-1.664.58-1.664 1.424Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 720 B |