diff --git a/adaptive_hockey_federation/adaptive_hockey_federation/settings.py b/adaptive_hockey_federation/adaptive_hockey_federation/settings.py index 6be753f9..e29bffcf 100644 --- a/adaptive_hockey_federation/adaptive_hockey_federation/settings.py +++ b/adaptive_hockey_federation/adaptive_hockey_federation/settings.py @@ -75,7 +75,7 @@ LANGUAGE_CODE = 'ru' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Europe/Moscow' USE_I18N = True @@ -88,3 +88,4 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +AUTH_USER_MODEL = 'users.User' diff --git a/adaptive_hockey_federation/main/admin.py b/adaptive_hockey_federation/main/admin.py index 6af52da2..7cc80a82 100644 --- a/adaptive_hockey_federation/main/admin.py +++ b/adaptive_hockey_federation/main/admin.py @@ -1,3 +1,104 @@ -from django.contrib import admin # noqa +from django.contrib import admin +from main.models import ( + Anamnesis, + Discipline, + Health, + Location, + Player, + Position, + RespiratoryFailure, + Role, + Team, +) -# Register your models here. + +class TeamInline(admin.TabularInline): + model = Team.players.through + extra = 0 + verbose_name = 'Команда' + verbose_name_plural = 'Команды' + autocomplete_fields = ['team'] + + +class HealthInline(admin.TabularInline): + model = Health + extra = 0 + verbose_name = 'Медицинская карта' + verbose_name_plural = 'Медицинская карта' + + +class PlayerInline(admin.TabularInline): + model = Player.team.through + extra = 0 + verbose_name = 'Игрок' + verbose_name_plural = 'Игроки' + autocomplete_fields = ['player'] + + +@admin.register(Player) +class PlayerAdmin(admin.ModelAdmin): + inlines = [TeamInline, HealthInline] + fields = ['name', 'surname', 'patronymic', 'birth_date'] + list_display = ['name', 'surname'] + search_fields = ['surname', 'name'] + + +@admin.register(Anamnesis) +class AnamnesisAdmin(admin.ModelAdmin): + search_fields = ['name'] + + def get_model_perms(self, request): + """Прячем из меню админки данную модель""" + return {} + + +@admin.register(Discipline) +class DisciplineAdmin(admin.ModelAdmin): + search_fields = ['name'] + + def get_model_perms(self, request): + """Прячем из меню админки данную модель""" + return {} + + +@admin.register(Team) +class TeamAdmin(admin.ModelAdmin): + search_fields = ['name'] + autocomplete_fields = ['location', 'discipline'] + inlines = [PlayerInline] + + +@admin.register(Location) +class LocationAdmin(admin.ModelAdmin): + search_fields = ['name'] + + def get_model_perms(self, request): + """Прячем из меню админки данную модель""" + return {} + + +@admin.register(Position) +class PositionAdmin(admin.ModelAdmin): + search_fields = ['name'] + + def get_model_perms(self, request): + """Прячем из меню админки данную модель""" + return {} + + +@admin.register(Role) +class RoleAdmin(admin.ModelAdmin): + search_fields = ['name'] + + def get_model_perms(self, request): + """Прячем из меню админки данную модель""" + return {} + + +@admin.register(RespiratoryFailure) +class RespiratoryFailureAdmin(admin.ModelAdmin): + search_fields = ['name'] + + def get_model_perms(self, request): + """Прячем из меню админки данную модель""" + return {} diff --git a/adaptive_hockey_federation/main/migrations/0001_initial.py b/adaptive_hockey_federation/main/migrations/0001_initial.py new file mode 100644 index 00000000..53c78927 --- /dev/null +++ b/adaptive_hockey_federation/main/migrations/0001_initial.py @@ -0,0 +1,161 @@ +# Generated by Django 4.2.6 on 2023-11-25 14:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Anamnesis', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True, verbose_name='Название')), + ], + options={ + 'verbose_name': 'Диагноз', + 'verbose_name_plural': 'Диагнозы', + 'ordering': ('name',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Discipline', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True, verbose_name='Название')), + ], + options={ + 'verbose_name': 'Дисциплина', + 'verbose_name_plural': 'Дисциплины', + 'ordering': ('name',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Location', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True, verbose_name='Название')), + ], + options={ + 'verbose_name': 'Территория', + 'verbose_name_plural': 'Территории', + 'ordering': ('name',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Player', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, verbose_name='Имя')), + ('surname', models.CharField(max_length=256, verbose_name='Фамилия')), + ('patronymic', models.CharField(blank=True, max_length=256, null=True, verbose_name='Отчество')), + ('birth_date', models.DateField()), + ('sex', models.CharField(blank=True, choices=[('male', 'Мужской'), ('female', 'Женский')], max_length=6, null=True, verbose_name='Пол')), + ], + options={ + 'verbose_name': 'Игрок', + 'verbose_name_plural': 'Игроки', + 'ordering': ('surname', 'name', 'patronymic'), + 'abstract': False, + 'default_related_name': 'players', + }, + ), + migrations.CreateModel( + name='Position', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True, verbose_name='Название')), + ], + options={ + 'verbose_name': 'Игровая позиция', + 'verbose_name_plural': 'Игровые позиции', + 'ordering': ('name',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RespiratoryFailure', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True, verbose_name='Название')), + ], + options={ + 'verbose_name': 'Класс ХДН', + 'verbose_name_plural': 'Классы ХДН', + 'ordering': ('name',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256, unique=True, verbose_name='Название')), + ], + options={ + 'verbose_name': 'Игровая позиция', + 'verbose_name_plural': 'Игровые позиции', + 'ordering': ('name',), + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('discipline', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.discipline', verbose_name='Дисциплина команды')), + ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.location', verbose_name='Локация команды')), + ], + options={ + 'verbose_name': 'Команда', + 'verbose_name_plural': 'Команды', + 'default_related_name': 'teams', + }, + ), + migrations.CreateModel( + name='PlayerTeam', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.CharField(max_length=256, verbose_name='Игровой номер')), + ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='player_teams', to='main.player', verbose_name='Игрок')), + ('position', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.position', verbose_name='Позиция игрока')), + ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.role', verbose_name='Статус игрока')), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='team_players', to='main.team', verbose_name='Команда')), + ], + ), + migrations.AddField( + model_name='player', + name='team', + field=models.ManyToManyField(through='main.PlayerTeam', to='main.team', verbose_name='Команда'), + ), + migrations.CreateModel( + name='Health', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_permanent', models.BooleanField(default=False, verbose_name='Класс ХДН подтверждён перманентно')), + ('revision', models.CharField(blank=True, max_length=256, null=True, verbose_name='Пересмотр класса ХДН')), + ('wheelchair', models.BooleanField(default=False, verbose_name='На коляске')), + ('anamnesis', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.anamnesis', verbose_name='Диагноз')), + ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='health', to='main.player', verbose_name='Игрок')), + ('respiratory_failure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='respiratory_failure_players', to='main.respiratoryfailure', verbose_name='Класс ХДН')), + ], + ), + migrations.AddConstraint( + model_name='team', + constraint=models.UniqueConstraint(fields=('name', 'location', 'discipline'), name='team_location_unique'), + ), + migrations.AddConstraint( + model_name='player', + constraint=models.UniqueConstraint(fields=('name', 'surname', 'patronymic', 'birth_date'), name='player_unique'), + ), + ] diff --git a/adaptive_hockey_federation/main/models.py b/adaptive_hockey_federation/main/models.py index 974b0572..2feaeb5e 100644 --- a/adaptive_hockey_federation/main/models.py +++ b/adaptive_hockey_federation/main/models.py @@ -1,78 +1,267 @@ -from django.db import models +from django.db.models import ( + BooleanField, CASCADE, CharField, DateField, ForeignKey, ManyToManyField, + Model, SET_NULL, UniqueConstraint, +) +SEX_CHOICES = ( + ('male', 'Мужской'), + ('female', 'Женский'), +) -class Team(models.Model): - """Класс команды +NAME_FIELD_LENGTH = 256 +BASE_PERSON_FIELD_LENGTH = 256 + + +class BaseUniqueName(Model): + """ + Абстрактный класс с уникальным полем "Название" и методом отображения """ - name = models.CharField( - max_length=256, + name = CharField( + max_length=NAME_FIELD_LENGTH, verbose_name='Название', + unique=True, ) class Meta: - verbose_name = 'Команда' - verbose_name_plural = 'Команды' + ordering = ('name',) + abstract = True def __str__(self): return self.name -class Position(models.Model): - """Позиция в команде +class Location(BaseUniqueName): """ - name = models.CharField( - max_length=256, - verbose_name='Название', + Локация привязки команды (город, область, край) + или места проведения соревнования + """ + + class Meta(BaseUniqueName.Meta): + verbose_name = 'Территория' + verbose_name_plural = 'Территории' + + +class Discipline(BaseUniqueName): + """ + Классификация дисциплины (следж-хоккей, хоккей для незрячих, спец. хоккей) + """ + + class Meta(BaseUniqueName.Meta): + verbose_name = 'Дисциплина' + verbose_name_plural = 'Дисциплины' + + +class Team(Model): + """ + Модель команды. + """ + name = CharField(max_length=NAME_FIELD_LENGTH, ) + location = ForeignKey( + to=Location, + on_delete=SET_NULL, + blank=True, + null=True, + verbose_name='Локация команды', + ) + discipline = ForeignKey( + to=Discipline, + on_delete=SET_NULL, + blank=True, + null=True, + verbose_name='Дисциплина команды', ) class Meta: - verbose_name = 'Позиция' - verbose_name_plural = 'Позиции' + default_related_name = 'teams' + verbose_name = 'Команда' + verbose_name_plural = 'Команды' + constraints = [ + UniqueConstraint( + name='team_location_unique', + fields=['name', 'location', 'discipline'], + ) + ] def __str__(self): + if self.location: + return f'{self.name} - {self.location}' return self.name -class BaseUserInfo(models.Model): - """Класс игрока +class Position(BaseUniqueName): + """ + Игровая позиция игрока в команде """ - CLS_CHOICES = [ - ('A', 'A'), - ('A2', 'A2'), - ('B', 'B'), - ('B2', 'B2'), - ('C', 'C'), - ('C2', 'C2'), - ] - name = models.CharField( - max_length=56, + + class Meta(BaseUniqueName.Meta): + verbose_name = 'Игровая позиция' + verbose_name_plural = 'Игровые позиции' + + +class Role(BaseUniqueName): + """ + Роль игрока в команде (капитан, ассистент) + """ + + class Meta(BaseUniqueName.Meta): + verbose_name = 'Игровая позиция' + verbose_name_plural = 'Игровые позиции' + + +class Anamnesis(BaseUniqueName): + """ + Диагноз общий + """ + + class Meta(BaseUniqueName.Meta): + verbose_name = 'Диагноз' + verbose_name_plural = 'Диагнозы' + + +class RespiratoryFailure(BaseUniqueName): + """ + Класс хронической дыхательной недостаточности + """ + + class Meta(BaseUniqueName.Meta): + verbose_name = 'Класс ХДН' + verbose_name_plural = 'Классы ХДН' + + +class BasePerson(Model): + """ + Абстрактная модель с базовой персональной информацией + """ + name = CharField( + max_length=BASE_PERSON_FIELD_LENGTH, verbose_name='Имя', ) - surname = models.CharField( - max_length=56, + surname = CharField( + max_length=BASE_PERSON_FIELD_LENGTH, + verbose_name='Фамилия', ) - date_of_birth = models.DateTimeField( - verbose_name='Дата рождения', + patronymic = CharField( + max_length=BASE_PERSON_FIELD_LENGTH, + blank=True, + default='', + verbose_name='Отчество', ) - team = models.ForeignKey( - to=Team, - on_delete=models.SET_NULL, + + class Meta: + ordering = ('surname', 'name', 'patronymic') + abstract = True + + def __str__(self): + return ' '.join([self.surname, self.name, self.patronymic]) + + +class Player(BasePerson): + """ + Модель игрока. Связь с командой "многие ко многим" на случай включения + игрока в сборную, помимо основного состава. + """ + birth_date = DateField() + sex = CharField( + max_length=max(len(sex) for sex, _ in SEX_CHOICES), + choices=SEX_CHOICES, blank=True, null=True, - verbose_name='Команда', + verbose_name='Пол' ) - position = models.ForeignKey( + team = ManyToManyField( to=Team, - on_delete=models.SET_NULL, + through='PlayerTeam', + verbose_name='Команда' + ) + + class Meta(BasePerson.Meta): + default_related_name = 'players' + verbose_name = 'Игрок' + verbose_name_plural = 'Игроки' + constraints = [ + UniqueConstraint( + name='player_unique', + fields=['name', 'surname', 'patronymic', 'birth_date'], + ) + ] + + +class Health(Model): + """ + Информация по хронической дыхательной недостаточности. + """ + player = ForeignKey( + to=Player, + related_name='health', + on_delete=CASCADE, + verbose_name='Игрок', + ) + respiratory_failure = ForeignKey( + to=RespiratoryFailure, + related_name='respiratory_failure_players', + on_delete=CASCADE, + verbose_name='Класс ХДН', + ) + is_permanent = BooleanField( + default=False, + verbose_name='Класс ХДН подтверждён перманентно', + ) + revision = CharField( + max_length=NAME_FIELD_LENGTH, + blank=True, + null=True, + verbose_name='Пересмотр класса ХДН', + ) + anamnesis = ForeignKey( + to=Anamnesis, + on_delete=SET_NULL, blank=True, null=True, - verbose_name='Позиция', + verbose_name='Диагноз', ) - classification = models.CharField( - max_length=255, - choices=CLS_CHOICES, - default=None, + wheelchair = BooleanField( + default=False, + verbose_name='На коляске' ) def __str__(self): - return self.name + return (f'Медицинские показатели' + f' игрока {self.player.surname} {self.player.name}') + + +class PlayerTeam(Model): + """ + Связь "многие ко многим" игрока с командой с добавлением данных игрока + в этой команде. + """ + player = ForeignKey( + to=Player, + related_name='player_teams', + on_delete=CASCADE, + verbose_name='Игрок', + + ) + team = ForeignKey( + to=Team, + related_name='team_players', + on_delete=CASCADE, + verbose_name='Команда', + ) + position = ForeignKey( + to=Position, + on_delete=SET_NULL, + blank=True, + null=True, + verbose_name='Позиция игрока', + ) + role = ForeignKey( + to=Role, + on_delete=SET_NULL, + blank=True, + null=True, + verbose_name='Статус игрока', + ) + number = CharField( + max_length=NAME_FIELD_LENGTH, + verbose_name='Игровой номер', + ) diff --git a/adaptive_hockey_federation/users/admin.py b/adaptive_hockey_federation/users/admin.py index 6af52da2..344accaa 100644 --- a/adaptive_hockey_federation/users/admin.py +++ b/adaptive_hockey_federation/users/admin.py @@ -1,3 +1,27 @@ -from django.contrib import admin # noqa +from django.contrib import admin -# Register your models here. +from users.models import User + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + # date_hierarchy = 'date_joined' + list_display = ( + 'id', + 'username', + 'email', + 'phone', + 'first_name', + 'last_name', + 'role', + 'team', + ) + fields = [ + 'username', + 'email', + 'phone', + ('first_name', 'last_name'), + 'role', + 'team', + ] + empty_value_display = '-пусто-' diff --git a/adaptive_hockey_federation/users/apps.py b/adaptive_hockey_federation/users/apps.py index 72b14010..9bd2beed 100644 --- a/adaptive_hockey_federation/users/apps.py +++ b/adaptive_hockey_federation/users/apps.py @@ -4,3 +4,4 @@ class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' + verbose_name = 'Пользователи' diff --git a/adaptive_hockey_federation/users/migrations/0001_initial.py b/adaptive_hockey_federation/users/migrations/0001_initial.py new file mode 100644 index 00000000..110071e8 --- /dev/null +++ b/adaptive_hockey_federation/users/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.6 on 2023-11-25 14:50 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('main', '0001_initial'), + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('phone', models.CharField(max_length=20)), + ('role', models.CharField(choices=[('user', 'Пользователь'), ('agent', 'Представитель команды'), ('moderator', 'Модератор'), ('admin', 'Администратор')], default='user', max_length=9)), + ('first_name', models.CharField(blank=True, max_length=256, null=True)), + ('last_name', models.CharField(blank=True, max_length=256, null=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('team', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='main.team', verbose_name='Команда')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'Пользователь', + 'verbose_name_plural': 'Пользователи', + 'ordering': ('username',), + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/adaptive_hockey_federation/users/models.py b/adaptive_hockey_federation/users/models.py index 9d57c559..9bc5aa77 100644 --- a/adaptive_hockey_federation/users/models.py +++ b/adaptive_hockey_federation/users/models.py @@ -1,3 +1,68 @@ -from django.db import models # noqa +from django.contrib.auth.models import AbstractUser +from django.db.models import SET_NULL, CharField, ForeignKey +from main.models import Team -# Create your models here. +NAME_MAX_LENGTH = 256 +EMAIL_MAX_LENGTH = 256 +PHONE_MAX_LENGTH = 20 + +ROLE_USER = 'user' +ROLE_AGENT = 'agent' +ROLE_MODERATOR = 'moderator' +ROLE_ADMIN = 'admin' + +ROLES_CHOICES = ( + (ROLE_USER, 'Пользователь'), + (ROLE_AGENT, 'Представитель команды'), + (ROLE_MODERATOR, 'Модератор'), + (ROLE_ADMIN, 'Администратор'), +) + + +class User(AbstractUser): + phone = CharField( + max_length=PHONE_MAX_LENGTH, + ) + role = CharField( + choices=ROLES_CHOICES, + default=ROLE_USER, + max_length=max(len(role) for role, _ in ROLES_CHOICES) + ) + first_name = CharField( + max_length=NAME_MAX_LENGTH, + default='', + null=True, + ) + last_name = CharField( + max_length=NAME_MAX_LENGTH, + default='', + null=True, + ) + team = ForeignKey( + to=Team, + on_delete=SET_NULL, + related_name='users', + verbose_name='Команда', + blank=True, + null=True, + ) + + class Meta: + verbose_name = 'Пользователь' + verbose_name_plural = 'Пользователи' + ordering = ('username',) + + def __str__(self): + return self.username + + @property + def is_agent(self): + return self.role == ROLE_AGENT + + @property + def is_moderator(self): + return self.role == ROLE_MODERATOR + + @property + def is_admin(self): + return (self.role == ROLE_ADMIN) or self.is_staff