Ilia 1 týždeň pred
commit
bede00681d
59 zmenil súbory, kde vykonal 1986 pridanie a 0 odobranie
  1. 8 0
      .env
  2. 3 0
      .vscode/settings.json
  3. 33 0
      README.md
  4. 53 0
      docker-compose.yml
  5. 27 0
      dockerfile
  6. 22 0
      manage.py
  7. 0 0
      meloman/__init__.py
  8. BIN
      meloman/__pycache__/__init__.cpython-313.pyc
  9. BIN
      meloman/__pycache__/settings.cpython-313.pyc
  10. BIN
      meloman/__pycache__/urls.cpython-313.pyc
  11. BIN
      meloman/__pycache__/wsgi.cpython-313.pyc
  12. 16 0
      meloman/asgi.py
  13. 138 0
      meloman/settings.py
  14. 23 0
      meloman/urls.py
  15. 16 0
      meloman/wsgi.py
  16. 0 0
      mysite/__init__.py
  17. BIN
      mysite/__pycache__/__init__.cpython-313.pyc
  18. BIN
      mysite/__pycache__/admin.cpython-313.pyc
  19. BIN
      mysite/__pycache__/apps.cpython-313.pyc
  20. BIN
      mysite/__pycache__/forms.cpython-313.pyc
  21. BIN
      mysite/__pycache__/models.cpython-313.pyc
  22. BIN
      mysite/__pycache__/urls.cpython-313.pyc
  23. BIN
      mysite/__pycache__/views.cpython-313.pyc
  24. 68 0
      mysite/admin.py
  25. 6 0
      mysite/apps.py
  26. 71 0
      mysite/forms.py
  27. 134 0
      mysite/migrations/0001_initial.py
  28. 13 0
      mysite/migrations/0002_merged_migration.py
  29. 41 0
      mysite/migrations/0003_trigger.py
  30. 14 0
      mysite/migrations/0004_merge_0002_merged_migration_0003_trigger.py
  31. 21 0
      mysite/migrations/0005_meloman_user.py
  32. 0 0
      mysite/migrations/__init__.py
  33. BIN
      mysite/migrations/__pycache__/0001_initial.cpython-313.pyc
  34. BIN
      mysite/migrations/__pycache__/0002_merged_migration.cpython-313.pyc
  35. BIN
      mysite/migrations/__pycache__/0002_trigger.cpython-313.pyc
  36. BIN
      mysite/migrations/__pycache__/0003_trigger.cpython-313.pyc
  37. BIN
      mysite/migrations/__pycache__/0004_merge_0002_merged_migration_0003_trigger.cpython-313.pyc
  38. BIN
      mysite/migrations/__pycache__/0005_meloman_user.cpython-313.pyc
  39. BIN
      mysite/migrations/__pycache__/__init__.cpython-313.pyc
  40. 137 0
      mysite/models.py
  41. 116 0
      mysite/templates/mysite/base.html
  42. 30 0
      mysite/templates/mysite/login.html
  43. 71 0
      mysite/templates/mysite/meloman_list.html
  44. 25 0
      mysite/templates/mysite/musician_confirm_delete.html
  45. 32 0
      mysite/templates/mysite/musician_form.html
  46. 55 0
      mysite/templates/mysite/musician_list.html
  47. 112 0
      mysite/templates/mysite/profile.html
  48. 59 0
      mysite/templates/mysite/rate_song.html
  49. 40 0
      mysite/templates/mysite/recommendations.html
  50. 79 0
      mysite/templates/mysite/register.html
  51. 27 0
      mysite/templates/mysite/song_confirm_delete.html
  52. 65 0
      mysite/templates/mysite/song_details.html
  53. 38 0
      mysite/templates/mysite/song_form.html
  54. 44 0
      mysite/templates/mysite/song_list.html
  55. 54 0
      mysite/templates/mysite/song_list_data.html
  56. 3 0
      mysite/tests.py
  57. 37 0
      mysite/urls.py
  58. 251 0
      mysite/views.py
  59. 4 0
      requirements.txt

+ 8 - 0
.env

@@ -0,0 +1,8 @@
+# .env
+DB_NAME=music_db
+DB_USER=postgres
+DB_PASSWORD=postgres
+DB_HOST=db
+DB_PORT=5432
+SECRET_KEY=django-insecure-2vnmw64)5vxpo3qg%$ppgbfqv-(=fa$oi6_ipdcdbdcocxi+%(
+DEBUG=True

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "python-envs.defaultEnvManager": "ms-python.python:system"
+}

+ 33 - 0
README.md

@@ -0,0 +1,33 @@
+# Music Collection — Музыкальная коллекция
+
+## Аннотация
+Music Collection — это веб-приложение для каталогизации музыкальных произведений, сбора пользовательских оценок и формирования рейтингов. Система позволяет управлять информацией об исполнителях, альбомах, песнях и жанрах, а также предоставляет пользователям возможность оценивать песни и получать персональные рекомендации.
+
+## Введение
+### Основные возможности
+- Регистрация и авторизация пользователей
+- Разграничение прав доступа (администратор, менеджер, читатель)
+- CRUD-операции для исполнителей, альбомов и песен
+- Система выставления оценок (1-10) с расчетом среднего рейтинга
+- Персональные рекомендации на основе предпочтений
+- Профиль пользователя с расчетными атрибутами (возраст, статистика оценок)
+- Интерактивный поиск по названию и исполнителю
+- Административная панель Django
+
+## Назначение и условия применения
+### Назначение
+Система предназначена для любителей музыки, коллекционеров и музыкальных критиков, желающих структурировать информацию о музыкальных произведениях, делиться мнениями и находить новые песни на основе оценок других пользователей.
+
+### Условия применения
+- Операционная система: Linux (Ubuntu 22.04+), Windows 10/11, macOS
+- Установленный Docker и Docker Compose (рекомендуемый способ)
+- Или Python 3.11+, PostgreSQL 15+ (для ручной установки)
+- Доступ к интернету для установки зависимостей
+
+## Установка
+### Установка через Docker (рекомендуемый способ)
+
+- [x] **Шаг 1:** Клонируйте репозиторий
+  ```bash
+  git clone https://gogs.itiscaf.ru/username/music_collection.git
+  cd music_collection

+ 53 - 0
docker-compose.yml

@@ -0,0 +1,53 @@
+# docker-compose.yml
+version: '3.8'
+
+services:
+  db:
+    image: postgres:15
+    container_name: music_db
+    environment:
+      POSTGRES_DB: music_db
+      POSTGRES_USER: postgres
+      POSTGRES_PASSWORD: postgres
+    ports:
+      - "5432:5432"
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
+    networks:
+      - music_network
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U postgres"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+
+  web:
+    build: .
+    container_name: music_web
+    command: >
+      sh -c "python manage.py migrate &&
+             python manage.py loaddata fixtures/initial_data.json &&
+             python manage.py runserver 0.0.0.0:8000"
+    volumes:
+      - .:/app
+    ports:
+      - "8000:8000"
+    environment:
+      - DB_NAME=music_db
+      - DB_USER=postgres
+      - DB_PASSWORD=postgres
+      - DB_HOST=db
+      - DB_PORT=5432
+    depends_on:
+      db:
+        condition: service_healthy
+    networks:
+      - music_network
+
+volumes:
+  postgres_data:
+
+networks:
+  music_network:
+    driver: bridge

+ 27 - 0
dockerfile

@@ -0,0 +1,27 @@
+# Dockerfile
+FROM python:3.11-slim
+
+# Установка зависимостей системы
+RUN apt-get update && apt-get install -y \
+    gcc \
+    postgresql-client \
+    && rm -rf /var/lib/apt/lists/*
+
+# Установка рабочей директории
+WORKDIR /app
+
+# Копирование зависимостей и установка
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Копирование кода приложения
+COPY . .
+
+# Сбор статики
+RUN python manage.py collectstatic --noinput
+
+# Открытие порта
+EXPOSE 8000
+
+# Запуск приложения
+CMD ["gunicorn", "meloman.wsgi:application", "--bind", "0.0.0.0:8000"]

+ 22 - 0
manage.py

@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    """Run administrative tasks."""
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'meloman.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()

+ 0 - 0
meloman/__init__.py


BIN
meloman/__pycache__/__init__.cpython-313.pyc


BIN
meloman/__pycache__/settings.cpython-313.pyc


BIN
meloman/__pycache__/urls.cpython-313.pyc


BIN
meloman/__pycache__/wsgi.cpython-313.pyc


+ 16 - 0
meloman/asgi.py

@@ -0,0 +1,16 @@
+"""
+ASGI config for meloman project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'meloman.settings')
+
+application = get_asgi_application()

+ 138 - 0
meloman/settings.py

@@ -0,0 +1,138 @@
+"""
+Django settings for meloman project.
+
+Generated by 'django-admin startproject' using Django 5.2.6.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/5.2/ref/settings/
+"""
+
+from pathlib import Path
+
+# 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/5.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-2vnmw64)5vxpo3qg%$ppgbfqv-(=fa$oi6_ipdcdbdcocxi+%('
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'mysite'
+]
+
+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 = 'meloman.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'meloman.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
+# DATABASES = {
+#     'default': {
+#         'ENGINE': 'django.db.backends.sqlite3',
+#         'NAME': BASE_DIR / 'db.sqlite3',
+#     }
+# }
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql',
+        'NAME': 'dbpractice',
+        'HOST': '245e1-rw.db.pub.dbaas.postgrespro.ru',
+        'PORT': '5432',
+        'USER': 'baryshnikov_ii',
+        'PASSWORD': 'VY%6e%dG2j$'        
+    }
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/5.2/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/5.2/topics/i18n/
+
+LANGUAGE_CODE = 'ru-RU'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
+STATIC_URL = 'static/'
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+# Настройка перенаправления после входа
+LOGIN_REDIRECT_URL = '/'
+LOGOUT_REDIRECT_URL = '/login/'
+LOGIN_URL = '/login/'

+ 23 - 0
meloman/urls.py

@@ -0,0 +1,23 @@
+"""
+URL configuration for meloman project.
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/5.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+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 django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('', include('mysite.urls'))
+]

+ 16 - 0
meloman/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for meloman project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'meloman.settings')
+
+application = get_wsgi_application()

+ 0 - 0
mysite/__init__.py


BIN
mysite/__pycache__/__init__.cpython-313.pyc


BIN
mysite/__pycache__/admin.cpython-313.pyc


BIN
mysite/__pycache__/apps.cpython-313.pyc


BIN
mysite/__pycache__/forms.cpython-313.pyc


BIN
mysite/__pycache__/models.cpython-313.pyc


BIN
mysite/__pycache__/urls.cpython-313.pyc


BIN
mysite/__pycache__/views.cpython-313.pyc


+ 68 - 0
mysite/admin.py

@@ -0,0 +1,68 @@
+# admin.py
+from django.contrib import admin
+from django.db.models import Avg, Count
+from .models import Musician, Genre, Album, Song, Meloman, Rating, Preference
+
+# Сначала зарегистрируйте модели, используемые в autocomplete_fields
+@admin.register(Genre)
+class GenreAdmin(admin.ModelAdmin):
+    list_display = ('genre_name',)
+    search_fields = ('genre_name',)  # Важно для autocomplete!
+    
+@admin.register(Album)
+class AlbumAdmin(admin.ModelAdmin):
+    list_display = ('album_name', 'musician')
+    search_fields = ('album_name', 'musician__name')  # Важно для autocomplete!
+    list_select_related = ('musician',)
+    
+@admin.register(Musician)
+class MusicianAdmin(admin.ModelAdmin):
+    list_display = ('name', 'city', 'birth_year')
+    search_fields = ('name', 'city')
+
+@admin.register(Song)
+class SongAdmin(admin.ModelAdmin):
+    list_display = ('title', 'album', 'genre', 'duration_formatted', 'avg_rating')
+    list_filter = ('genre', 'album__musician')
+    search_fields = ('title',)
+    autocomplete_fields = ['album', 'genre']  # Теперь работает, т.к. админы зарегистрированы
+    
+    def get_queryset(self, request):
+        return super().get_queryset(request).annotate(
+            avg_rating_value=Avg('rating__score')
+        )
+    
+    def avg_rating(self, obj):
+        if hasattr(obj, 'avg_rating_value') and obj.avg_rating_value:
+            return f"{obj.avg_rating_value:.1f} ★"
+        return "Нет оценок"
+    avg_rating.short_description = "Средний рейтинг"
+    
+    def duration_formatted(self, obj):
+        return obj.duration_formatted if obj.duration_sec else "00:00"
+    duration_formatted.short_description = "Длительность"
+
+@admin.register(Meloman)
+class MelomanAdmin(admin.ModelAdmin):
+    list_display = ('full_name', 'gender', 'city', 'age')
+    search_fields = ('full_name', 'city')
+    list_filter = ('gender', 'city')
+    
+    def age(self, obj):
+        return obj.age if obj.age else "—"
+    age.short_description = "Возраст"
+
+@admin.register(Rating)
+class RatingAdmin(admin.ModelAdmin):
+    list_display = ('song', 'meloman', 'score', 'rating_date')
+    list_filter = ('score', 'rating_date')
+    search_fields = ('song__title', 'meloman__full_name')
+    autocomplete_fields = ['song', 'meloman']
+    list_select_related = ('song', 'meloman')
+
+@admin.register(Preference)
+class PreferenceAdmin(admin.ModelAdmin):
+    list_display = ('meloman', 'genre')
+    list_filter = ('genre',)
+    search_fields = ('meloman__full_name', 'genre__genre_name')
+    autocomplete_fields = ['meloman', 'genre']

+ 6 - 0
mysite/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MysiteConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'mysite'

+ 71 - 0
mysite/forms.py

@@ -0,0 +1,71 @@
+from django import forms
+from .models import Musician, Song, Rating, Meloman
+from django.contrib.auth.models import User
+from django.contrib.auth.forms import UserCreationForm
+
+class MusicianForm(forms.ModelForm):
+    class Meta:
+        model = Musician
+        fields = ['name', 'city', 'birth_year']
+        widgets = {
+            'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Название исполнителя'}),
+            'city': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Город'}),
+            'birth_year': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Год основания'}),
+        }
+        labels = {
+            'name': 'Имя исполнителя',
+            'city': 'Город',
+            'birth_year': 'Год рождения/основания',
+        }
+
+class SongForm(forms.ModelForm):
+    class Meta:
+        model = Song
+        fields = ['title', 'duration_sec', 'album', 'genre']
+        widgets = {
+            'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Название песни'}),
+            'duration_sec': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'Длительность в секундах'}),
+            'album': forms.Select(attrs={'class': 'form-control'}),
+            'genre': forms.Select(attrs={'class': 'form-control'}),
+        }
+
+class RatingForm(forms.ModelForm):
+    class Meta:
+        model = Rating
+        fields = ['score']
+        widgets = {
+            'score': forms.NumberInput(attrs={'min': 1, 'max': 10, 'class': 'form-control', 'style': 'width: 100px;'})
+        }
+        
+
+class RegistrationForm(UserCreationForm):
+    email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control'}))
+    full_name = forms.CharField(max_length=150, required=True, widget=forms.TextInput(attrs={'class': 'form-control'}))
+    city = forms.CharField(max_length=100, required=False, widget=forms.TextInput(attrs={'class': 'form-control'}))
+    birth_year = forms.IntegerField(required=False, widget=forms.NumberInput(attrs={'class': 'form-control'}))
+    gender = forms.ChoiceField(choices=[('M', 'Мужской'), ('F', 'Женский')], required=False, widget=forms.Select(attrs={'class': 'form-control'}))
+    
+    class Meta:
+        model = User
+        fields = ['username', 'email', 'password1', 'password2']
+        widgets = {
+            'username': forms.TextInput(attrs={'class': 'form-control'}),
+            'email': forms.EmailInput(attrs={'class': 'form-control'}),
+            'password1': forms.PasswordInput(attrs={'class': 'form-control'}),
+            'password2': forms.PasswordInput(attrs={'class': 'form-control'}),
+        }
+    
+    def save(self, commit=True):
+        user = super().save(commit=False)
+        user.email = self.cleaned_data['email']
+        if commit:
+            user.save()
+            # Создаем профиль меломана
+            Meloman.objects.create(
+                user=user,
+                full_name=self.cleaned_data['full_name'],
+                city=self.cleaned_data.get('city', ''),
+                birth_year=self.cleaned_data.get('birth_year'),
+                gender=self.cleaned_data.get('gender')
+            )
+        return user

+ 134 - 0
mysite/migrations/0001_initial.py

@@ -0,0 +1,134 @@
+# Generated by Django 5.2.6 on 2026-06-11 10:56
+
+import django.core.validators
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Genre',
+            fields=[
+                ('genre_id', models.AutoField(primary_key=True, serialize=False)),
+                ('genre_name', models.CharField(max_length=50, unique=True)),
+            ],
+            options={
+                'verbose_name': 'Жанр',
+                'verbose_name_plural': 'Жанры',
+                'db_table': 'genre',
+            },
+        ),
+        migrations.CreateModel(
+            name='Meloman',
+            fields=[
+                ('meloman_id', models.AutoField(primary_key=True, serialize=False)),
+                ('full_name', models.CharField(max_length=150)),
+                ('gender', models.CharField(blank=True, choices=[('M', 'Мужской'), ('F', 'Женский')], max_length=1, null=True)),
+                ('city', models.CharField(blank=True, max_length=100, null=True)),
+                ('birth_year', models.IntegerField(blank=True, null=True)),
+            ],
+            options={
+                'verbose_name': 'Меломан',
+                'verbose_name_plural': 'Меломанцы',
+                'db_table': 'meloman',
+            },
+        ),
+        migrations.CreateModel(
+            name='Musician',
+            fields=[
+                ('musician_id', models.AutoField(primary_key=True, serialize=False)),
+                ('name', models.CharField(max_length=100, verbose_name='Имя исполнителя')),
+                ('city', models.CharField(blank=True, max_length=100, null=True)),
+                ('birth_year', models.IntegerField(blank=True, null=True)),
+            ],
+            options={
+                'verbose_name': 'Исполнитель',
+                'verbose_name_plural': 'Исполнители',
+                'db_table': 'musician',
+            },
+        ),
+        migrations.CreateModel(
+            name='Album',
+            fields=[
+                ('album_id', models.AutoField(primary_key=True, serialize=False)),
+                ('album_name', models.CharField(max_length=150)),
+                ('release_date', models.DateField(blank=True, null=True)),
+                ('musician', models.ForeignKey(db_column='musician_id', on_delete=django.db.models.deletion.CASCADE, to='mysite.musician')),
+            ],
+            options={
+                'verbose_name': 'Альбом',
+                'verbose_name_plural': 'Альбомы',
+                'db_table': 'album',
+            },
+        ),
+        migrations.CreateModel(
+            name='Preference',
+            fields=[
+                ('preference_id', models.AutoField(primary_key=True, serialize=False)),
+                ('genre', models.ForeignKey(db_column='genre_id', on_delete=django.db.models.deletion.CASCADE, to='mysite.genre')),
+                ('meloman', models.ForeignKey(db_column='meloman_id', on_delete=django.db.models.deletion.CASCADE, to='mysite.meloman')),
+            ],
+            options={
+                'verbose_name': 'Предпочтение',
+                'verbose_name_plural': 'Предпочтения',
+                'db_table': 'preference',
+            },
+        ),
+        migrations.CreateModel(
+            name='Song',
+            fields=[
+                ('song_id', models.AutoField(primary_key=True, serialize=False)),
+                ('title', models.CharField(max_length=150)),
+                ('duration_sec', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(1)])),
+                ('album', models.ForeignKey(db_column='album_id', on_delete=django.db.models.deletion.CASCADE, to='mysite.album')),
+                ('genre', models.ForeignKey(db_column='genre_id', null=True, on_delete=django.db.models.deletion.SET_NULL, to='mysite.genre')),
+            ],
+            options={
+                'verbose_name': 'Песня',
+                'verbose_name_plural': 'Песни',
+                'db_table': 'song',
+            },
+        ),
+        migrations.CreateModel(
+            name='Rating',
+            fields=[
+                ('rating_id', models.AutoField(primary_key=True, serialize=False)),
+                ('score', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])),
+                ('rating_date', models.DateField(auto_now_add=True)),
+                ('meloman', models.ForeignKey(db_column='meloman_id', on_delete=django.db.models.deletion.CASCADE, to='mysite.meloman')),
+                ('song', models.ForeignKey(db_column='song_id', on_delete=django.db.models.deletion.CASCADE, to='mysite.song')),
+            ],
+            options={
+                'verbose_name': 'Оценка',
+                'verbose_name_plural': 'Оценки',
+                'db_table': 'rating',
+            },
+        ),
+        migrations.AddIndex(
+            model_name='album',
+            index=models.Index(fields=['musician'], name='idx_album_musician'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='preference',
+            unique_together={('meloman', 'genre')},
+        ),
+        migrations.AddIndex(
+            model_name='song',
+            index=models.Index(fields=['album'], name='idx_song_album'),
+        ),
+        migrations.AddIndex(
+            model_name='song',
+            index=models.Index(fields=['genre'], name='idx_song_genre'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='rating',
+            unique_together={('meloman', 'song')},
+        ),
+    ]

+ 13 - 0
mysite/migrations/0002_merged_migration.py

@@ -0,0 +1,13 @@
+# Generated by Django 5.2.6 on 2026-06-11 11:00
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('mysite', '0001_initial'),
+    ]
+
+    operations = [
+    ]

+ 41 - 0
mysite/migrations/0003_trigger.py

@@ -0,0 +1,41 @@
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('mysite', '0001_initial'),
+    ]
+    operations = [
+        migrations.RunSQL("""
+            CREATE OR REPLACE VIEW song_details_view AS
+            SELECT 
+                s.song_id,
+                s.title,
+                a.album_name,
+                m.name AS musician_name,
+                s.duration_sec,
+                g.genre_name,
+                ROUND(AVG(r.score), 2) AS avg_rating
+            FROM song s
+            JOIN album a ON s.album_id = a.album_id
+            JOIN musician m ON a.musician_id = m.musician_id
+            LEFT JOIN genre g ON s.genre_id = g.genre_id
+            LEFT JOIN rating r ON s.song_id = r.song_id
+            GROUP BY s.song_id, a.album_name, m.name, g.genre_name;
+        """),
+        
+        migrations.RunSQL("""
+            CREATE OR REPLACE FUNCTION update_rating_timestamp()
+            RETURNS TRIGGER AS $$
+            BEGIN
+                NEW.rating_date = CURRENT_DATE;
+                RETURN NEW;
+            END;
+            $$ LANGUAGE plpgsql;
+            
+            CREATE TRIGGER trg_update_rating_date
+            BEFORE UPDATE ON rating
+            FOR EACH ROW
+            EXECUTE FUNCTION update_rating_timestamp();
+        """),
+    ]

+ 14 - 0
mysite/migrations/0004_merge_0002_merged_migration_0003_trigger.py

@@ -0,0 +1,14 @@
+# Generated by Django 5.2.6 on 2026-06-11 11:01
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('mysite', '0002_merged_migration'),
+        ('mysite', '0003_trigger'),
+    ]
+
+    operations = [
+    ]

+ 21 - 0
mysite/migrations/0005_meloman_user.py

@@ -0,0 +1,21 @@
+# Generated by Django 5.2.6 on 2026-06-13 05:21
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('mysite', '0004_merge_0002_merged_migration_0003_trigger'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='meloman',
+            name='user',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='meloman', to=settings.AUTH_USER_MODEL),
+        ),
+    ]

+ 0 - 0
mysite/migrations/__init__.py


BIN
mysite/migrations/__pycache__/0001_initial.cpython-313.pyc


BIN
mysite/migrations/__pycache__/0002_merged_migration.cpython-313.pyc


BIN
mysite/migrations/__pycache__/0002_trigger.cpython-313.pyc


BIN
mysite/migrations/__pycache__/0003_trigger.cpython-313.pyc


BIN
mysite/migrations/__pycache__/0004_merge_0002_merged_migration_0003_trigger.cpython-313.pyc


BIN
mysite/migrations/__pycache__/0005_meloman_user.cpython-313.pyc


BIN
mysite/migrations/__pycache__/__init__.cpython-313.pyc


+ 137 - 0
mysite/models.py

@@ -0,0 +1,137 @@
+from django.db import models
+from django.core.validators import MinValueValidator, MaxValueValidator
+from django.utils import timezone
+from django.contrib.auth.models import User
+
+
+class Musician(models.Model):
+    musician_id = models.AutoField(primary_key=True)
+    name = models.CharField(max_length=100, verbose_name="Имя исполнителя")
+    city = models.CharField(max_length=100, blank=True, null=True)
+    birth_year = models.IntegerField(null=True, blank=True)
+    
+    class Meta:
+        db_table = 'musician'
+        verbose_name = 'Исполнитель'
+        verbose_name_plural = 'Исполнители'
+    
+    def __str__(self):
+        return self.name
+
+
+class Genre(models.Model):
+    genre_id = models.AutoField(primary_key=True)
+    genre_name = models.CharField(max_length=50, unique=True)
+    
+    class Meta:
+        db_table = 'genre'
+        verbose_name = 'Жанр'
+        verbose_name_plural = 'Жанры'
+    
+    def __str__(self):
+        return self.genre_name
+
+
+class Album(models.Model):
+    album_id = models.AutoField(primary_key=True)
+    album_name = models.CharField(max_length=150)
+    release_date = models.DateField(null=True, blank=True)
+    musician = models.ForeignKey(Musician, on_delete=models.CASCADE, db_column='musician_id')
+    
+    class Meta:
+        db_table = 'album'
+        verbose_name = 'Альбом'
+        verbose_name_plural = 'Альбомы'
+        indexes = [
+            models.Index(fields=['musician'], name='idx_album_musician'),
+        ]
+    
+    def __str__(self):
+        return f"{self.album_name} - {self.musician.name}"
+
+
+class Song(models.Model):
+    song_id = models.AutoField(primary_key=True)
+    title = models.CharField(max_length=150)
+    duration_sec = models.IntegerField(validators=[MinValueValidator(1)], null=True, blank=True)
+    album = models.ForeignKey(Album, on_delete=models.CASCADE, db_column='album_id')
+    genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, null=True, db_column='genre_id')
+    
+    class Meta:
+        db_table = 'song'
+        verbose_name = 'Песня'
+        verbose_name_plural = 'Песни'
+        indexes = [
+            models.Index(fields=['album'], name='idx_song_album'),
+            models.Index(fields=['genre'], name='idx_song_genre'),
+        ]
+    
+    def __str__(self):
+        return self.title
+    
+    @property
+    def duration_formatted(self):
+        """Расчетный атрибут: форматированная длительность"""
+        if not self.duration_sec:
+            return "00:00"
+        minutes = self.duration_sec // 60
+        seconds = self.duration_sec % 60
+        return f"{minutes}:{seconds:02d}"
+
+
+class Meloman(models.Model):
+    GENDER_CHOICES = [
+        ('M', 'Мужской'),
+        ('F', 'Женский'),
+    ]
+    
+    meloman_id = models.AutoField(primary_key=True)
+    full_name = models.CharField(max_length=150)
+    gender = models.CharField(max_length=1, choices=GENDER_CHOICES, blank=True, null=True)
+    city = models.CharField(max_length=100, blank=True, null=True)
+    birth_year = models.IntegerField(null=True, blank=True)
+    user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True, related_name='meloman')
+    
+    class Meta:
+        db_table = 'meloman'
+        verbose_name = 'Меломан'
+        verbose_name_plural = 'Меломанцы'
+    
+    def __str__(self):
+        return self.full_name
+    
+    @property
+    def age(self):
+        """Расчетный атрибут: возраст меломана"""
+        if not self.birth_year:
+            return None
+        return timezone.now().year - self.birth_year
+
+
+class Rating(models.Model):
+    rating_id = models.AutoField(primary_key=True)
+    meloman = models.ForeignKey(Meloman, on_delete=models.CASCADE, db_column='meloman_id')
+    song = models.ForeignKey(Song, on_delete=models.CASCADE, db_column='song_id')
+    score = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
+    rating_date = models.DateField(auto_now_add=True)
+    
+    class Meta:
+        db_table = 'rating'
+        verbose_name = 'Оценка'
+        verbose_name_plural = 'Оценки'
+        unique_together = ['meloman', 'song']  # один меломан — одна оценка на песню
+    
+    def __str__(self):
+        return f"{self.meloman.full_name} - {self.song.title}: {self.score}"
+
+
+class Preference(models.Model):
+    preference_id = models.AutoField(primary_key=True)
+    meloman = models.ForeignKey(Meloman, on_delete=models.CASCADE, db_column='meloman_id')
+    genre = models.ForeignKey(Genre, on_delete=models.CASCADE, db_column='genre_id')
+    
+    class Meta:
+        db_table = 'preference'
+        verbose_name = 'Предпочтение'
+        verbose_name_plural = 'Предпочтения'
+        unique_together = ['meloman', 'genre']

+ 116 - 0
mysite/templates/mysite/base.html

@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>{% block title %}Music Database{% endblock %}</title>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
+    <style>
+        .rating-stars { color: #ffc107; }
+        .navbar { margin-bottom: 20px; }
+        footer { margin-top: 50px; padding: 20px; background: #f8f9fa; }
+        .table-actions { white-space: nowrap; }
+        .avg-rating-high { color: #28a745; font-weight: bold; }
+        .avg-rating-medium { color: #ffc107; font-weight: bold; }
+        .avg-rating-low { color: #dc3545; font-weight: bold; }
+        .logout-btn:hover {
+            background-color: #dc3545;
+            border-radius: 5px;
+        }
+    </style>
+</head>
+<body>
+    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
+        <div class="container">
+            <a class="navbar-brand" href="{% url 'mysite:song_list' %}">🎵 Music DB</a>
+            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            <div class="collapse navbar-collapse" id="navbarNav">
+                <ul class="navbar-nav me-auto">
+                    {% if user.is_authenticated %}
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:song_list' %}">🎵 Главная</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:musician_list' %}">🎤 Исполнители</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:song_details' %}">📊 Детальный список</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:song_list_data' %}">📝 Управление песнями</a>
+                        </li>
+                        {% if user.is_superuser %}
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:meloman_list' %}">👥 Меломанцы</a>
+                        </li>
+                        {% endif %}
+                    {% endif %}
+                </ul>
+                <ul class="navbar-nav">
+                    {% if user.is_authenticated %}
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:profile' %}">👤 {{ user.username }}</a>
+                        </li>
+                        <li class="nav-item">
+                            <form method="post" action="{% url 'mysite:logout' %}" style="display: inline;">
+                                {% csrf_token %}
+                                <button type="submit" class="btn btn-link nav-link" 
+                                        style="background: none; border: none; cursor: pointer; color: rgba(255,255,255,0.75); text-decoration: none;"
+                                        onmouseover="this.style.color='#dc3545'" 
+                                        onmouseout="this.style.color='rgba(255,255,255,0.75)'"
+                                        onclick="return confirm('Вы уверены, что хотите выйти?')">
+                                    🚪 Выйти
+                                </button>
+                            </form>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:recommendations' %}">🎯 Рекомендации</a>
+                        </li>
+                    {% else %}
+                        <li class="nav-item">
+                            <a class="nav-link" href="{% url 'mysite:login' %}">🔐 Войти</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link btn btn-outline-success text-white" href="{% url 'mysite:register' %}" 
+                            style="margin-left: 10px; border-color: #28a745;">
+                                📝 Регистрация
+                            </a>
+                        </li>
+                    {% endif %}
+                </ul>
+            </div>
+        </div>
+    </nav>
+    
+    <div class="container mt-4">
+        {% if messages %}
+            {% for message in messages %}
+                <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
+                    {{ message }}
+                    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+                </div>
+            {% endfor %}
+        {% endif %}
+        
+        {% block content %}{% endblock %}
+    </div>
+    
+    <footer class="text-center mt-5 py-3 bg-light">
+        <p>© 2025 Music Database | Расчетные атрибуты: средний рейтинг, возраст, длительность</p>
+    </footer>
+    
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
+    <script>
+        // Подтверждение выхода
+        document.querySelectorAll('.nav-link.text-danger').forEach(link => {
+            link.addEventListener('click', function(e) {
+                if (!confirm('Вы уверены, что хотите выйти из системы?')) {
+                    e.preventDefault();
+                }
+            });
+        });
+    </script>
+</body>
+</html>

+ 30 - 0
mysite/templates/mysite/login.html

@@ -0,0 +1,30 @@
+<!-- music/templates/music/login.html -->
+{% extends 'mysite/base.html' %}
+
+{% block title %}Вход в систему{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-4">
+        <div class="card">
+            <div class="card-header bg-primary text-white">
+                <h4 class="mb-0">Авторизация</h4>
+            </div>
+            <div class="card-body">
+                <form method="post">
+                    {% csrf_token %}
+                    <div class="mb-3">
+                        <label for="username" class="form-label">Имя пользователя</label>
+                        <input type="text" name="username" class="form-control" required>
+                    </div>
+                    <div class="mb-3">
+                        <label for="password" class="form-label">Пароль</label>
+                        <input type="password" name="password" class="form-control" required>
+                    </div>
+                    <button type="submit" class="btn btn-primary w-100">Войти</button>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 71 - 0
mysite/templates/mysite/meloman_list.html

@@ -0,0 +1,71 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Список меломанов{% endblock %}
+
+{% block content %}
+<h2>👥 Меломанцы</h2>
+<p class="text-muted">Список зарегистрированных пользователей с расчетными атрибутами</p>
+
+{% if melomans %}
+    <div class="table-responsive">
+        <table class="table table-striped table-hover">
+            <thead class="table-dark">
+                <tr>
+                    <th>ID</th>
+                    <th>ФИО</th>
+                    <th>Пол</th>
+                    <th>Город</th>
+                    <th>Возраст</th>
+                    <th>Количество оценок</th>
+                    <th>Средний балл</th>
+                    <th>Любимые жанры</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for meloman in melomans %}
+                <tr>
+                    <td>{{ meloman.meloman_id }}</td>
+                    <td><strong>{{ meloman.full_name }}</strong></td>
+                    <td>{{ meloman.get_gender_display|default:"—" }}</td>
+                    <td>{{ meloman.city|default:"—" }}</td>
+                    <td>
+                        {% if meloman.age %}
+                            {{ meloman.age }} лет
+                        {% else %}
+                            —
+                        {% endif %}
+                    </td>
+                    <td>{{ meloman.ratings_count|default:0 }}</td>
+                    <td>
+                        {% if meloman.avg_score %}
+                            <span class="
+                                {% if meloman.avg_score >= 8 %}avg-rating-high
+                                {% elif meloman.avg_score >= 5 %}avg-rating-medium
+                                {% else %}avg-rating-low{% endif %}
+                            ">
+                                {{ meloman.avg_score|floatformat:1 }}
+                            </span>
+                        {% else %}
+                            —
+                        {% endif %}
+                    </td>
+                    <td>
+                        {% with genres=meloman.preference_set.all %}
+                            {% if genres %}
+                                {{ genres|join:", " }}
+                            {% else %}
+                                —
+                            {% endif %}
+                        {% endwith %}
+                    </td>
+                </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    </div>
+{% else %}
+    <div class="alert alert-info text-center">
+        👥 Меломанов пока нет.
+    </div>
+{% endif %}
+{% endblock %}

+ 25 - 0
mysite/templates/mysite/musician_confirm_delete.html

@@ -0,0 +1,25 @@
+<!-- music/templates/music/musician_confirm_delete.html -->
+{% extends 'mysite/base.html' %}
+
+{% block title %}Удаление исполнителя{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-6">
+        <div class="card border-danger">
+            <div class="card-header bg-danger text-white">
+                <h4 class="mb-0">⚠️ Подтверждение удаления</h4>
+            </div>
+            <div class="card-body">
+                <p>Вы действительно хотите удалить исполнителя <strong>{{ musician.name }}</strong>?</p>
+                <p class="text-danger">Внимание! Будут удалены все альбомы и песни этого исполнителя.</p>
+                <form method="post">
+                    {% csrf_token %}
+                    <button type="submit" class="btn btn-danger">Да, удалить</button>
+                    <a href="{% url 'mysite:musician_list' %}" class="btn btn-secondary">Отмена</a>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 32 - 0
mysite/templates/mysite/musician_form.html

@@ -0,0 +1,32 @@
+<!-- music/templates/music/musician_form.html -->
+{% extends 'mysite/base.html' %}
+
+{% block title %}{{ form.instance.pk|yesno:"Редактирование,Добавление" }} исполнителя{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-6">
+        <div class="card">
+            <div class="card-header bg-primary text-white">
+                <h4 class="mb-0">{{ form.instance.pk|yesno:"✏️ Редактирование,➕ Добавление" }} исполнителя</h4>
+            </div>
+            <div class="card-body">
+                <form method="post">
+                    {% csrf_token %}
+                    {% for field in form %}
+                    <div class="mb-3">
+                        <label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
+                        {{ field }}
+                        {% if field.errors %}
+                        <div class="text-danger">{{ field.errors }}</div>
+                        {% endif %}
+                    </div>
+                    {% endfor %}
+                    <button type="submit" class="btn btn-primary">Сохранить</button>
+                    <a href="{% url 'mysite:musician_list' %}" class="btn btn-secondary">Отмена</a>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 55 - 0
mysite/templates/mysite/musician_list.html

@@ -0,0 +1,55 @@
+<!-- music/templates/music/musician_list.html -->
+{% extends 'mysite/base.html' %}
+
+{% block title %}Исполнители{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>🎤 Исполнители</h2>
+    <a href="{% url 'mysite:musician_create' %}" class="btn btn-success">+ Добавить исполнителя</a>
+</div>
+
+<table class="table table-striped">
+    <thead>
+        <tr>
+            <th>ID</th>
+            <th>Имя</th>
+            <th>Город</th>
+            <th>Год основания</th>
+            <th>Количество альбомов</th>
+            <th>Действия</th>
+        </tr>
+    </thead>
+    <tbody>
+        {% for musician in musicians %}
+        <tr>
+            <td>{{ musician.musician_id }}</td>
+            <td>{{ musician.name }}</td>
+            <td>{{ musician.city|default:"—" }}</td>
+            <td>{{ musician.birth_year|default:"—" }}</td>
+            <td>{{ musician.albums_count }}</td>
+            <td>
+                <a href="{% url 'mysite:musician_update' musician.musician_id %}" class="btn btn-sm btn-warning">✏️</a>
+                <a href="{% url 'mysite:musician_delete' musician.musician_id %}" class="btn btn-sm btn-danger">🗑️</a>
+            </td>
+        </tr>
+        {% endfor %}
+    </tbody>
+</table>
+
+{% if is_paginated %}
+<nav>
+    <ul class="pagination">
+        {% if page_obj.has_previous %}
+        <li class="page-item"><a class="page-link" href="?page=1">Первая</a></li>
+        <li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a></li>
+        {% endif %}
+        <li class="page-item active"><span class="page-link">{{ page_obj.number }}</span></li>
+        {% if page_obj.has_next %}
+        <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a></li>
+        <li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a></li>
+        {% endif %}
+    </ul>
+</nav>
+{% endif %}
+{% endblock %}

+ 112 - 0
mysite/templates/mysite/profile.html

@@ -0,0 +1,112 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Мой профиль{% endblock %}
+
+{% block content %}
+<h2>👤 Мой профиль</h2>
+
+<div class="row">
+    <div class="col-md-4">
+        <div class="card shadow mb-4">
+            <div class="card-header bg-primary text-white">
+                <h5 class="mb-0">Личная информация</h5>
+            </div>
+            <div class="card-body">
+                <p><strong>ФИО:</strong> {{ meloman.full_name }}</p>
+                <p><strong>Пол:</strong> {{ meloman.get_gender_display|default:"—" }}</p>
+                <p><strong>Город:</strong> {{ meloman.city|default:"—" }}</p>
+                <p><strong>Год рождения:</strong> {{ meloman.birth_year|default:"—" }}</p>
+                <p><strong>Возраст:</strong> {{ age|default:"—" }} лет</p>
+            </div>
+        </div>
+    </div>
+    
+    <div class="col-md-4">
+        <div class="card shadow mb-4">
+            <div class="card-header bg-success text-white">
+                <h5 class="mb-0">Статистика оценок</h5>
+            </div>
+            <div class="card-body text-center">
+                <h1 class="display-4">{{ total_ratings }}</h1>
+                <p>всего оценок</p>
+                <hr>
+                <h3>
+                    {% if avg_user_rating %}
+                        <span class="
+                            {% if avg_user_rating >= 8 %}avg-rating-high
+                            {% elif avg_user_rating >= 5 %}avg-rating-medium
+                            {% else %}avg-rating-low{% endif %}
+                        ">
+                            {{ avg_user_rating|floatformat:1 }}
+                        </span>
+                    {% else %}
+                        —
+                    {% endif %}
+                </h3>
+                <p>средний балл пользователя</p>
+            </div>
+        </div>
+    </div>
+    
+    <div class="col-md-4">
+        <div class="card shadow mb-4">
+            <div class="card-header bg-warning text-dark">
+                <h5 class="mb-0">Любимые жанры</h5>
+            </div>
+            <div class="card-body">
+                {% if favorite_genres %}
+                    <ul class="list-group">
+                        {% for pref in favorite_genres %}
+                            <li class="list-group-item">🎸 {{ pref.genre.genre_name }}</li>
+                        {% endfor %}
+                    </ul>
+                {% else %}
+                    <p class="text-muted text-center">Нет выбранных жанров</p>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="card shadow">
+    <div class="card-header bg-info text-white">
+        <h5 class="mb-0">📝 Последние оценки</h5>
+    </div>
+    <div class="card-body">
+        {% if recent_ratings %}
+            <div class="table-responsive">
+                <table class="table table-sm">
+                    <thead>
+                        <tr>
+                            <th>Песня</th>
+                            <th>Исполнитель</th>
+                            <th>Оценка</th>
+                            <th>Дата</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {% for rating in recent_ratings %}
+                        <tr>
+                            <td>{{ rating.song.title }}</td>
+                            <td>{{ rating.song.album.musician.name }}</td>
+                            <td>
+                                <span class="
+                                    {% if rating.score >= 8 %}avg-rating-high
+                                    {% elif rating.score >= 5 %}avg-rating-medium
+                                    {% else %}avg-rating-low{% endif %}
+                                ">
+                                    {{ rating.score }} / 10
+                                </span>
+                            </td>
+                            <td>{{ rating.rating_date }}</td>
+                        </tr>
+                        {% endfor %}
+                    </tbody>
+                </table>
+            </div>
+        {% else %}
+            <p class="text-muted text-center">Вы еще не поставили ни одной оценки</p>
+        {% endif %}
+    </div>
+</div>
+{% endblock %}

+ 59 - 0
mysite/templates/mysite/rate_song.html

@@ -0,0 +1,59 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Оценка песни{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-5">
+        <div class="card shadow">
+            <div class="card-header bg-primary text-white text-center">
+                <h4 class="mb-0">⭐ Оценка песни</h4>
+            </div>
+            <div class="card-body">
+                <div class="text-center mb-4">
+                    <h3>{{ song.title }}</h3>
+                    <p class="text-muted">
+                        {{ song.album.musician.name }} — {{ song.album.album_name }}
+                    </p>
+                    <p>⏱️ Длительность: {{ song.duration_formatted }}</p>
+                </div>
+                
+                <form method="post">
+                    {% csrf_token %}
+                    
+                    <div class="mb-4 text-center">
+                        <label class="form-label fw-bold">Ваша оценка (1-10)</label>
+                        <div class="d-flex justify-content-center gap-2 flex-wrap">
+                            {% for i in "12345678910"|make_list %}
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="radio" name="score" value="{{ forloop.counter }}" id="score{{ forloop.counter }}"
+                                    {% if form.score.value|stringformat:"s" == forloop.counter|stringformat:"s" %}checked{% endif %}>
+                                <label class="form-check-label" for="score{{ forloop.counter }}">{{ forloop.counter }}</label>
+                            </div>
+                            {% endfor %}
+                        </div>
+                        {% if form.score.errors %}
+                            <div class="text-danger small">{{ form.score.errors }}</div>
+                        {% endif %}
+                    </div>
+                    
+                    <div class="d-flex justify-content-between">
+                        <button type="submit" class="btn btn-primary">💾 Сохранить оценку</button>
+                        <a href="{% url 'mysite:song_list' %}" class="btn btn-secondary">❌ Отмена</a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+
+<style>
+    .form-check-inline {
+        margin-right: 5px;
+    }
+    .form-check-input:checked + .form-check-label {
+        font-weight: bold;
+        color: #ffc107;
+    }
+</style>
+{% endblock %}

+ 40 - 0
mysite/templates/mysite/recommendations.html

@@ -0,0 +1,40 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Рекомендации{% endblock %}
+
+{% block content %}
+<h2>🎵 Рекомендации для вас</h2>
+<p class="text-muted">Песни, подобранные на основе ваших музыкальных предпочтений</p>
+
+{% if error %}
+    <div class="alert alert-warning">{{ error }}</div>
+{% endif %}
+
+{% if recommendations %}
+    <div class="row">
+        {% for song in recommendations %}
+        <div class="col-md-6 mb-3">
+            <div class="card h-100 shadow-sm">
+                <div class="card-body">
+                    <h5 class="card-title">{{ song.song_title }}</h5>
+                    <p class="card-text">
+                        <strong>🎤 Исполнитель:</strong> {{ song.musician_name }}<br>
+                        <strong>🎸 Жанр:</strong> {{ song.genre_name }}<br>
+                        <strong>⭐ Средний рейтинг:</strong> 
+                        {% if song.avg_rating %}
+                            {{ song.avg_rating|floatformat:1 }} / 10
+                        {% else %}
+                            Нет оценок
+                        {% endif %}
+                    </p>
+                </div>
+            </div>
+        </div>
+        {% endfor %}
+    </div>
+{% else %}
+    <div class="alert alert-info text-center">
+        ⚡ Нет рекомендаций. Поставьте оценки песням или укажите любимые жанры!
+    </div>
+{% endif %}
+{% endblock %}

+ 79 - 0
mysite/templates/mysite/register.html

@@ -0,0 +1,79 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Регистрация{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-6">
+        <div class="card shadow">
+            <div class="card-header bg-success text-white">
+                <h4 class="mb-0">📝 Регистрация нового пользователя</h4>
+            </div>
+            <div class="card-body">
+                <form method="post">
+                    {% csrf_token %}
+                    
+                    <div class="mb-3">
+                        <label for="id_username" class="form-label">Имя пользователя</label>
+                        {{ form.username }}
+                        {% if form.username.errors %}
+                            <div class="text-danger small">{{ form.username.errors }}</div>
+                        {% endif %}
+                    </div>
+                    
+                    <div class="mb-3">
+                        <label for="id_email" class="form-label">Email</label>
+                        {{ form.email }}
+                        {% if form.email.errors %}
+                            <div class="text-danger small">{{ form.email.errors }}</div>
+                        {% endif %}
+                    </div>
+                    
+                    <div class="mb-3">
+                        <label for="id_full_name" class="form-label">Полное имя</label>
+                        {{ form.full_name }}
+                    </div>
+                    
+                    <div class="row">
+                        <div class="col-md-6 mb-3">
+                            <label for="id_city" class="form-label">Город</label>
+                            {{ form.city }}
+                        </div>
+                        <div class="col-md-6 mb-3">
+                            <label for="id_birth_year" class="form-label">Год рождения</label>
+                            {{ form.birth_year }}
+                        </div>
+                    </div>
+                    
+                    <div class="mb-3">
+                        <label for="id_gender" class="form-label">Пол</label>
+                        {{ form.gender }}
+                    </div>
+                    
+                    <div class="mb-3">
+                        <label for="id_password1" class="form-label">Пароль</label>
+                        {{ form.password1 }}
+                        {% if form.password1.errors %}
+                            <div class="text-danger small">{{ form.password1.errors }}</div>
+                        {% endif %}
+                    </div>
+                    
+                    <div class="mb-3">
+                        <label for="id_password2" class="form-label">Подтверждение пароля</label>
+                        {{ form.password2 }}
+                        {% if form.password2.errors %}
+                            <div class="text-danger small">{{ form.password2.errors }}</div>
+                        {% endif %}
+                    </div>
+                    
+                    <button type="submit" class="btn btn-success w-100">Зарегистрироваться</button>
+                </form>
+                <hr>
+                <p class="text-center mb-0">
+                    Уже есть аккаунт? <a href="{% url 'mysite:login' %}">Войдите</a>
+                </p>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 27 - 0
mysite/templates/mysite/song_confirm_delete.html

@@ -0,0 +1,27 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Удаление песни{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-5">
+        <div class="card border-danger shadow">
+            <div class="card-header bg-danger text-white">
+                <h4 class="mb-0">⚠️ Подтверждение удаления</h4>
+            </div>
+            <div class="card-body text-center">
+                <p class="lead">Вы действительно хотите удалить песню</p>
+                <h4 class="text-danger">"{{ song.title }}"</h4>
+                <p class="text-muted">Исполнитель: {{ song.album.musician.name }}</p>
+                <p class="text-danger small">Внимание! Это действие невозможно отменить. Все оценки этой песни будут также удалены.</p>
+                <hr>
+                <form method="post">
+                    {% csrf_token %}
+                    <button type="submit" class="btn btn-danger">🗑️ Да, удалить</button>
+                    <a href="{% url 'mysite:song_list_data' %}" class="btn btn-secondary">❌ Отмена</a>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 65 - 0
mysite/templates/mysite/song_details.html

@@ -0,0 +1,65 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Детальный список песен{% endblock %}
+
+{% block content %}
+<h2>📊 Детальный список песен</h2>
+<p class="text-muted">Данные из представления song_details_view со средним рейтингом</p>
+
+{% if songs %}
+    <div class="table-responsive">
+        <table class="table table-striped table-hover">
+            <thead class="table-dark">
+                <tr>
+                    <th>ID</th>
+                    <th>Название песни</th>
+                    <th>Исполнитель</th>
+                    <th>Альбом</th>
+                    <th>Жанр</th>
+                    <th>Длительность</th>
+                    <th>Средний рейтинг</th>
+                    <th>Количество оценок</th>
+                </tr>
+            </thead>
+            <tbody>
+                {% for song in songs %}
+                <tr>
+                    <td>{{ song.song_id }}</td>
+                    <td><strong>{{ song.title }}</strong></td>
+                    <td>{{ song.musician_name }}</td>
+                    <td>{{ song.album_name }}</td>
+                    <td>{{ song.genre_name|default:"—" }}</td>
+                    <td>
+                        {% if song.duration_sec %}
+                            {% with minutes=song.duration_sec|divisibleby:60 %}
+                                {{ song.duration_sec|divisibleby:60 }}:{{ song.duration_sec|stringformat:"02d" }}
+                            {% endwith %}
+                        {% else %}
+                            —
+                        {% endif %}
+                    </td>
+                    <td>
+                        {% if song.avg_rating %}
+                            <span class="
+                                {% if song.avg_rating >= 8 %}avg-rating-high
+                                {% elif song.avg_rating >= 5 %}avg-rating-medium
+                                {% else %}avg-rating-low{% endif %}
+                            ">
+                                {{ song.avg_rating|floatformat:1 }} / 10
+                            </span>
+                        {% else %}
+                            <span class="text-muted">Нет оценок</span>
+                        {% endif %}
+                    </td>
+                    <td>{{ song.ratings_count|default:0 }}</td>
+                </tr>
+                {% endfor %}
+            </tbody>
+        </table>
+    </div>
+{% else %}
+    <div class="alert alert-info text-center">
+        Нет данных о песнях
+    </div>
+{% endif %}
+{% endblock %}

+ 38 - 0
mysite/templates/mysite/song_form.html

@@ -0,0 +1,38 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}{{ form.instance.pk|yesno:"Редактирование,Добавление" }} песни{% endblock %}
+
+{% block content %}
+<div class="row justify-content-center">
+    <div class="col-md-6">
+        <div class="card shadow">
+            <div class="card-header bg-primary text-white">
+                <h4 class="mb-0">{{ form.instance.pk|yesno:"✏️ Редактирование,➕ Добавление" }} песни</h4>
+            </div>
+            <div class="card-body">
+                <form method="post">
+                    {% csrf_token %}
+                    
+                    {% for field in form %}
+                    <div class="mb-3">
+                        <label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
+                        {{ field }}
+                        {% if field.help_text %}
+                            <div class="form-text text-muted small">{{ field.help_text }}</div>
+                        {% endif %}
+                        {% if field.errors %}
+                            <div class="text-danger small">{{ field.errors }}</div>
+                        {% endif %}
+                    </div>
+                    {% endfor %}
+                    
+                    <div class="d-flex justify-content-between">
+                        <button type="submit" class="btn btn-primary">💾 Сохранить</button>
+                        <a href="{% url 'mysite:song_list_data' %}" class="btn btn-secondary">❌ Отмена</a>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}

+ 44 - 0
mysite/templates/mysite/song_list.html

@@ -0,0 +1,44 @@
+<!-- music/templates/music/song_list.html -->
+{% extends 'mysite/base.html' %}
+
+{% block title %}Каталог песен{% endblock %}
+
+{% block content %}
+<h2>🎵 Каталог песен</h2>
+
+<form method="get" class="mb-4">
+    <div class="input-group">
+        <input type="text" name="search" class="form-control" placeholder="Поиск по названию или исполнителю" value="{{ search_query }}">
+        <button type="submit" class="btn btn-primary">🔍 Найти</button>
+    </div>
+</form>
+
+<div class="row">
+    {% for song in songs %}
+    <div class="col-md-6 mb-3">
+        <div class="card">
+            <div class="card-body">
+                <h5 class="card-title">{{ song.title }}</h5>
+                <p class="card-text">
+                    <strong>Исполнитель:</strong> {{ song.album.musician.name }}<br>
+                    <strong>Альбом:</strong> {{ song.album.album_name }}<br>
+                    <strong>Жанр:</strong> {{ song.genre.genre_name|default:"—" }}<br>
+                    <strong>Длительность:</strong> {{ song.duration_formatted }}<br>
+                    <strong>⭐ Средний рейтинг:</strong> 
+                    {% if song.avg_rating %}
+                        {{ song.avg_rating|floatformat:1 }} / 10 ({{ song.ratings_count }} оценок)
+                    {% else %}
+                        Нет оценок
+                    {% endif %}
+                </p>
+                {% if user.is_authenticated %}
+                <a href="{% url 'mysite:rate_song' song.song_id %}" class="btn btn-sm btn-outline-primary">Оценить</a>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+    {% empty %}
+    <p class="text-center">Песен не найдено</p>
+    {% endfor %}
+</div>
+{% endblock %}

+ 54 - 0
mysite/templates/mysite/song_list_data.html

@@ -0,0 +1,54 @@
+{% extends 'mysite/base.html' %}
+
+{% block title %}Управление песнями{% endblock %}
+
+{% block content %}
+<div class="d-flex justify-content-between align-items-center mb-4">
+    <h2>📝 Управление песнями</h2>
+    <a href="{% url 'mysite:song_create' %}" class="btn btn-success">➕ Добавить песню</a>
+</div>
+
+{% if songs %}
+    <table class="table table-striped table-hover">
+        <thead class="table-dark">
+            <tr>
+                <th>ID</th>
+                <th>Название</th>
+                <th>Исполнитель</th>
+                <th>Альбом</th>
+                <th>Жанр</th>
+                <th>Длительность</th>
+                <th>Средний рейтинг</th>
+                <th>Действия</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for song in songs %}
+            <tr>
+                <td>{{ song.song_id }}</td>
+                <td>{{ song.title }}</td>
+                <td>{{ song.album.musician.name }}</td>
+                <td>{{ song.album.album_name }}</td>
+                <td>{{ song.genre.genre_name|default:"—" }}</td>
+                <td>{{ song.duration_formatted }}</td>
+                <td>
+                    {% if song.avg_rating %}
+                        {{ song.avg_rating|floatformat:1 }}
+                    {% else %}
+                        —
+                    {% endif %}
+                </td>
+                <td class="table-actions">
+                    <a href="{% url 'mysite:song_update' song.song_id %}" class="btn btn-sm btn-warning">✏️</a>
+                    <a href="{% url 'mysite:song_delete' song.song_id %}" class="btn btn-sm btn-danger">🗑️</a>
+                </td>
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{% else %}
+    <div class="alert alert-info text-center">
+        📝 Песен пока нет. Добавьте первую песню!
+    </div>
+{% endif %}
+{% endblock %}

+ 3 - 0
mysite/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 37 - 0
mysite/urls.py

@@ -0,0 +1,37 @@
+# mysite/urls.py
+from django.urls import path
+from django.contrib.auth.views import LoginView, LogoutView
+from . import views
+
+app_name = 'mysite'
+
+urlpatterns = [
+    # Главная страница
+    path('', views.SongListView.as_view(), name='song_list'),
+    
+    path('register/', views.register, name='register'),
+    
+    # Авторизация
+    path('login/', LoginView.as_view(template_name='mysite/login.html'), name='login'),
+    path('logout/', LogoutView.as_view(next_page='mysite:login'), name='logout'),
+    
+    # Исполнители
+    path('musicians/', views.MusicianListView.as_view(), name='musician_list'),
+    path('musicians/create/', views.MusicianCreateView.as_view(), name='musician_create'),
+    path('musicians/<int:pk>/update/', views.MusicianUpdateView.as_view(), name='musician_update'),
+    path('musicians/<int:pk>/delete/', views.MusicianDeleteView.as_view(), name='musician_delete'),
+    
+    # Песни
+    path('songs/', views.SongListDataView.as_view(), name='song_list_data'),
+    path('songs/create/', views.SongCreateView.as_view(), name='song_create'),
+    path('songs/<int:pk>/update/', views.SongUpdateView.as_view(), name='song_update'),
+    path('songs/<int:pk>/delete/', views.SongDeleteView.as_view(), name='song_delete'),
+    
+    # Меломанцы
+    path('melomans/', views.MelomanListView.as_view(), name='meloman_list'),
+    path('profile/', views.MelomanProfileView.as_view(), name='profile'),
+    path('rate/<int:song_id>/', views.rate_song, name='rate_song'),
+    
+    path('recommendations/', views.RecommendationsView.as_view(), name='recommendations'),
+    path('song-details/', views.SongDetailsView.as_view(), name='song_details'),
+]

+ 251 - 0
mysite/views.py

@@ -0,0 +1,251 @@
+# mysite/views.py
+from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView
+from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
+from django.contrib.auth import logout
+from django.shortcuts import render, redirect, get_object_or_404
+from django.urls import reverse_lazy
+from django.db.models import Avg, Count
+from django.contrib import messages
+from .models import Musician, Song, Meloman, Rating, Album, Genre
+from .forms import MusicianForm, SongForm, RatingForm, RegistrationForm
+from django.views.generic import TemplateView
+
+
+# Регистрация
+def register(request):
+    if request.method == 'POST':
+        form = RegistrationForm(request.POST)
+        if form.is_valid():
+            form.save()
+            messages.success(request, 'Регистрация прошла успешно! Теперь вы можете войти в систему.')
+            return redirect('mysite:login')
+    else:
+        form = RegistrationForm()
+    return render(request, 'mysite/register.html', {'form': form})
+
+# Выход из системы
+def custom_logout(request):
+    logout(request)
+    messages.success(request, 'Вы успешно вышли из системы')
+    return redirect('mysite:login')
+
+# Главная страница со списком песен
+class SongListView(ListView):
+    model = Song
+    template_name = 'mysite/song_list.html'
+    context_object_name = 'songs'
+    
+    def get_queryset(self):
+        return Song.objects.select_related('album__musician', 'genre').annotate(
+            avg_rating=Avg('rating__score'),
+            ratings_count=Count('rating')
+        )
+    
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        search_query = self.request.GET.get('search', '')
+        if search_query:
+            context['songs'] = context['songs'].filter(
+                title__icontains=search_query
+            ) | context['songs'].filter(
+                album__musician__name__icontains=search_query
+            )
+            context['search_query'] = search_query
+        return context
+
+# Список исполнителей
+class MusicianListView(LoginRequiredMixin, ListView):
+    model = Musician
+    template_name = 'mysite/musician_list.html'
+    context_object_name = 'musicians'
+    paginate_by = 10
+    
+    def get_queryset(self):
+        return Musician.objects.annotate(albums_count=Count('album'))
+
+class MusicianCreateView(LoginRequiredMixin, CreateView):
+    model = Musician
+    form_class = MusicianForm
+    template_name = 'mysite/musician_form.html'
+    success_url = reverse_lazy('mysite:musician_list')
+    
+    def form_valid(self, form):
+        messages.success(self.request, 'Исполнитель успешно добавлен')
+        return super().form_valid(form)
+
+class MusicianUpdateView(LoginRequiredMixin, UpdateView):
+    model = Musician
+    form_class = MusicianForm
+    template_name = 'mysite/musician_form.html'
+    success_url = reverse_lazy('mysite:musician_list')
+    
+    def form_valid(self, form):
+        messages.success(self.request, 'Данные исполнителя обновлены')
+        return super().form_valid(form)
+
+class MusicianDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
+    model = Musician
+    template_name = 'mysite/musician_confirm_delete.html'
+    success_url = reverse_lazy('mysite:musician_list')
+    
+    def test_func(self):
+        return self.request.user.is_superuser
+    
+    def delete(self, request, *args, **kwargs):
+        messages.success(request, 'Исполнитель удален')
+        return super().delete(request, *args, **kwargs)
+
+# Список песен для CRUD
+class SongListDataView(LoginRequiredMixin, ListView):
+    model = Song
+    template_name = 'mysite/song_list_data.html'
+    context_object_name = 'songs'
+    
+    def get_queryset(self):
+        return Song.objects.select_related('album__musician', 'genre').annotate(
+            avg_rating=Avg('rating__score')
+        )
+
+class SongCreateView(LoginRequiredMixin, CreateView):
+    model = Song
+    form_class = SongForm
+    template_name = 'mysite/song_form.html'
+    success_url = reverse_lazy('mysite:song_list_data')
+    
+    def form_valid(self, form):
+        messages.success(self.request, 'Песня успешно добавлена')
+        return super().form_valid(form)
+
+class SongUpdateView(LoginRequiredMixin, UpdateView):
+    model = Song
+    form_class = SongForm
+    template_name = 'mysite/song_form.html'
+    success_url = reverse_lazy('mysite:song_list_data')
+    
+    def form_valid(self, form):
+        messages.success(self.request, 'Данные песни обновлены')
+        return super().form_valid(form)
+
+class SongDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
+    model = Song
+    template_name = 'mysite/song_confirm_delete.html'
+    success_url = reverse_lazy('mysite:song_list_data')
+    
+    def test_func(self):
+        return self.request.user.is_superuser
+
+# Список меломанов
+class MelomanListView(LoginRequiredMixin, ListView):
+    model = Meloman
+    template_name = 'mysite/meloman_list.html'
+    context_object_name = 'melomans'
+    
+    def get_queryset(self):
+        return Meloman.objects.annotate(
+            ratings_count=Count('rating'),
+            avg_score=Avg('rating__score')
+        )
+    
+    def dispatch(self, request, *args, **kwargs):
+        if not request.user.is_superuser:
+            return redirect('mysite:song_list')
+        return super().dispatch(request, *args, **kwargs)
+
+# Профиль меломана
+class MelomanProfileView(LoginRequiredMixin, DetailView):
+    model = Meloman
+    template_name = 'mysite/profile.html'
+    context_object_name = 'meloman'
+    
+    def get_object(self):
+        try:
+            return Meloman.objects.get(user=self.request.user)
+        except Meloman.DoesNotExist:
+            return None
+    
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        meloman = self.get_object()
+        if meloman:
+            context['total_ratings'] = meloman.rating_set.count()
+            context['avg_user_rating'] = meloman.rating_set.aggregate(Avg('score'))['score__avg']
+            context['favorite_genres'] = meloman.preference_set.select_related('genre').all()
+            context['age'] = meloman.age
+            context['recent_ratings'] = meloman.rating_set.select_related('song').order_by('-rating_date')[:5]
+        return context
+
+# Оценка песни
+def rate_song(request, song_id):
+    if not request.user.is_authenticated:
+        return redirect('mysite:login')
+    
+    song = get_object_or_404(Song, pk=song_id)
+    
+    try:
+        meloman = Meloman.objects.get(user=request.user)
+    except Meloman.DoesNotExist:
+        messages.error(request, 'У вас нет профиля меломана. Обратитесь к администратору.')
+        return redirect('mysite:song_list')
+    
+    rating, created = Rating.objects.get_or_create(
+        meloman=meloman,
+        song=song,
+        defaults={'score': 5}
+    )
+    
+    if request.method == 'POST':
+        form = RatingForm(request.POST, instance=rating)
+        if form.is_valid():
+            form.save()
+            messages.success(request, f'Оценка для "{song.title}" сохранена!')
+            return redirect('mysite:song_list')
+    else:
+        form = RatingForm(instance=rating)
+    
+    return render(request, 'mysite/rate_song.html', {'form': form, 'song': song})
+
+
+from django.db import connection
+
+class SongDetailsView(ListView):
+    """Использование представления song_details_view"""
+    template_name = 'mysite/song_details.html'
+    context_object_name = 'songs'
+    
+    def get_queryset(self):
+        with connection.cursor() as cursor:
+            cursor.execute("""
+                SELECT song_id, title, musician_name, album_name, 
+                       genre_name, duration_sec, avg_rating, ratings_count
+                FROM song_details_view
+                ORDER BY avg_rating DESC NULLS LAST
+            """)
+            columns = [col[0] for col in cursor.description]
+            return [dict(zip(columns, row)) for row in cursor.fetchall()]
+        
+
+class RecommendationsView(LoginRequiredMixin, TemplateView):
+    """Использование функции recommend_songs"""
+    template_name = 'mysite/recommendations.html'
+    
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        
+        try:
+            meloman = Meloman.objects.get(user=self.request.user)
+            meloman_id = meloman.meloman_id
+        except Meloman.DoesNotExist:
+            context['recommendations'] = []
+            context['error'] = 'Профиль меломана не найден'
+            return context
+        
+        with connection.cursor() as cursor:
+            cursor.execute("""
+                SELECT * FROM recommend_songs(%s, %s)
+            """, [meloman_id, 10])
+            
+            columns = [col[0] for col in cursor.description]
+            recommendations = [dict(zip(columns, row)) for row in cursor.fetchall()]
+        
+        context['recommendations'] = recommendations
+        return context

+ 4 - 0
requirements.txt

@@ -0,0 +1,4 @@
+Django==4.2.7
+psycopg2-binary==2.9.9
+gunicorn==21.2.0
+python-dotenv==1.0.0