cloned from code.mipt.ru

This commit is contained in:
Ахмет Газакбаев 2025-07-15 16:59:24 +05:00
commit d7ab972517
35 changed files with 3261 additions and 0 deletions

170
.gitignore vendored Normal file
View File

@ -0,0 +1,170 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 ahmet
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

167
README.md Normal file
View File

@ -0,0 +1,167 @@
# Движение контингента
Данный проект является упрощенным вариантом базы данных обучающихся, которая используется образовательными учреждениями для контроля контингента.
> Все исходники в процессе создания сохраняются в [облаке](https://cloud.gazakbayev.net/index.php/s/5XpDS4Zx287bejj)
**Этапы проекта:**
- [x] Описание проекта.
- [x] Концептуальная модель.
- [x] Логическая модель.
- [x] Физическая модель.
- [x] Реализация схемы DDL.
- [x] Заполнение схемы DML.
- [x] Составление осмысленных запросов.
**Технические итерации к выполнению:**
- [x] Индексы (indexes)
- [x] Представления (views)
- [x] Функции и Процедуры (funcs_and_procs)
- [x] Триггеры (triggers)
## Концептуальная модель
![Концептуальная модель](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/Концептуальная%20модель.png&fileId=58551&x=1920&y=1080&a=true&etag=8a8f4beea72faadc1719431bd98e3e92)
Для редактирования и изменения можно фоспользоваться [.drawio](https://cloud.gazakbayev.net/index.php/s/5XpDS4Zx287bejj?dir=/) файлом сохранения.
## Логическая модель
![Логическая модель](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/Логическая%20модель.jpg&fileId=58597&x=1920&y=1080&a=true&etag=3e95f4e275f2aeea1d869724298b47f6)
Логическая модель была создана с использованием языка DBML, а также вспомогательных инструментов (среди прочих, drawdb).
## Физическая модель
Физическая модель располагается по следующим ссылкам, в зависимости от нужного формата:
- Таблица-источник, а так же описание форматирования: [docs.google.com](https://docs.google.com/spreadsheets/d/1tKWyQ4CsW6sF9kxRUULqEuPtOp6bD87Umzqr1hbn7I8/edit?usp=sharing)
- PDF-версия: [cloud.gazakbayev.net](https://cloud.gazakbayev.net/index.php/s/5XpDS4Zx287bejj?dir=/&openfile=true)
## Data Definition
По вышеприведённым моделям был составлен скрипт schema.sql, которая создаёт отношения с проверкой условий, описанных в физической модели.
Например,
```
access_card VARCHAR(20) NOT NULL CHECK (access_card ~ '^[A-F0-9]{2}(:[A-F0-9]{2}){5}$'),
access_level VARCHAR(10) NOT NULL CHECK (access_level IN ('КАМПУС', 'ОБЩЕЖИТИЯ', 'ПОЛНЫЙ'))
```
```
group_id VARCHAR(10) PRIMARY KEY CHECK (group_id ~ '^[АБМ]\d{2}-\d{3}[а-я]?$'),
```
![DDL](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_ddl.png&fileId=58554&x=1920&y=1080&a=true&etag=d5aa1cbf64917e9a923c020654a1dc75)
## Data Manipulation
С помощью библиотеку Faker и скриптов на языке Python были сгенерированы данные для дальнейшего взаимодействия с базой.
![DML1](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_dml_persons.png&fileId=58553&x=1920&y=1080&a=true&etag=8f5891b2415e6bda3499db299447e09b)
![DML2](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_dml_students.png&fileId=58581&x=1920&y=1080&a=true&etag=58b5092cb8d7060275e6ba467439f082)
*Да, можно посмеяться с того, что фамилии и имена не согласуются, и вообще Faker - генерирует какую-то абсолютно рандомную фигню :)*
## Requests
В файле requests.sql приведены достаточно интересные запросы с использованием всех требуемых операторов, подзапросов и оконных функций:
- Студенты с одинаковыми фамилиями
![r1](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_1.png&fileId=58586&x=1920&y=1080&a=true&etag=6bc33e441500fd0ac88f5c78ec20530e)
- Средний балл студентов
![r2](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_2.png&fileId=58588&x=1920&y=1080&a=true&etag=23d36a8e27317e4a58b7fabcce366fe2)
- Топ 5 лучших
![r3](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_3.png&fileId=58596&x=1920&y=1080&a=true&etag=dc0b2d7cdc2a7962704c657880648e17)
- Количества у кафедры дисциплин и студентов
![r4](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_4.png&fileId=58595&x=1920&y=1080&a=true&etag=d630c03a1981f6fe1a410e4633a6f163)
- ФИ обучающихся (статус Учится)
![r5](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_5.png&fileId=58598&x=1920&y=1080&a=true&etag=a1039656f3377b649fd32458e255a183)
- Прогресс изменения среднего балла по студентам
![r6](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_6.png&fileId=58599&x=1920&y=1080&a=true&etag=7196fca2ec33400e83aa1ae2294023d9)
- Список группы с обучающимися, с указанием оценки успеваемости студента в группе "Ниже среднего" или "Выше среднего"
![r7](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_7.png&fileId=58603&x=1920&y=1080&a=true&etag=ee123479c8d5aae1d5c7ace612973a55)
- Статистика отчислений по месяцам, основываясь на движении контингента (приказах)
![r8](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_8.png&fileId=58601&x=1920&y=1080&a=true&etag=5c10d03144f63209aeba3af3861dbc24)
- Научные руководители студентов
![r9](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_9.png&fileId=58602&x=1920&y=1080&a=true&etag=85600ee19d022be8ca4af3adaafa5c2b)
- Вывод преподавателей из ведомости по ФИО, принимающим предметам, средней оценки и уровня халявности
![r10](https://cloud.gazakbayev.net/index.php/apps/files_sharing/publicpreview/5XpDS4Zx287bejj?file=/dbeaver/dbeaver_r_10.png&fileId=58587&x=1920&y=1080&a=true&etag=aad8451f67c41b3e1d4a48fcb9baa899)
## Техническая итерация : Индексы
Созданы индексы `idx_physicals_fulltext_name` и `idx_movement_recent`.
Первый ускоряет полнотекстовый поиск, позволяя быстро находить лица, содержащие нужные имена в Personals. Например,
```sql
SELECT passport_no, name, surname
FROM Physicals
WHERE to_tsvector('russian', name || ' ' || surname) @@ to_tsquery('russian', 'Петров:*');
```
```sql
SELECT passport_no, name, surname
FROM Physicals
WHERE to_tsvector('russian', name || ' ' || surname) @@ plainto_tsquery('russian', 'Иван');
```
Второй ускоряет поиск по приказам из движения контингента, затрагивая интервал - последние 30 дней.
## Техническая итерация : Представления
В проекте представлено два представления:
`vw_student_performance` - представление, предоставляющее доступ к ID студенческого, ФИО студента и количество его проваленных/пройденных экзаменов/зачётных мероприятий, а также выводящий средний балл студентов.
`vw_birthdays_month` - просто забавное представление, выдающее список людей (студентов, преподавателей и пр.), у которых в этом месяце день рождения. Можно будет использовать для создания календаря дней рождений.
## Техническая итерация : Функции и Процедуры
`fn_low_enrollment(threshold INTEGER) -> TABLE` - функция, выводящая дисциплины с малым количеством студентов (основываясь на закрытых ведомостях). В аргумент принимает threshold - порог количества студентов для того, чтобы считать дисциплину малопосещаемой.
`fn_generate_transcript(p_student INTEGER) -> TEXT` - функция для вывода листа оценок студента. В аргумент принимает ID студента, выводя транскрипт, например:
```sql
SELECT fn_generate_transcript(42) AS transcript;
-- Transcript for student 42
-- Name: Иван Иванов
-- Group: А01-001
-- Status: УЧИТСЯ
--
-- 2024-12-15 - Математический анализ: 7
-- 2025-01-20 - Программирование: 9
-- 2025-03-05 - Математический анализ: 5
```
`sp_graduate_by_admission_year(p_year INTEGER) -> None` - процедура, присваивающая статус "Выпущен" занесением записи в Movements всему году поступления, указанным в p_year (срабатывает trigger 3).
## Техническая итерация : Триггеры
**Триггер 1** - предотвращает удаление группы с находящимися там студентами.
**Триггер 2** - стилизует вводимую электронную почту под lowercase, унифицируя тем самым все записи, облегчая восприятие почты.
**Триггер 3** - по обновлению статуса в Movement, проивзодит изменения в основном объекте студента Students.

401
analysis/data_generator.py Normal file
View File

@ -0,0 +1,401 @@
#
# "contingent-movement" project;
# author: gazakbayev.net
# ver: 1.0
#
from faker import Faker
import random
from datetime import datetime, timedelta
from manager import *
Database.initialize()
fake = Faker('ru_RU')
class DataStorage:
physicals = []
supervisors = []
faculties = []
departments = []
programs = []
groups = []
students = []
disciplines = []
def generate_physicals(count=30):
for _ in range(count):
access_level = random.choice(['КАМПУС', 'ОБЩЕЖИТИЯ', 'ПОЛНЫЙ'])
access_card = ":".join(f"{random.randint(0, 255):02X}" for _ in range(6))
data = [
fake.unique.passport_number(),
fake.first_name(),
fake.last_name(),
fake.date_of_birth(minimum_age=18, maximum_age=80),
fake.phone_number(),
fake.email(),
fake.country(),
fake.address(),
access_card,
access_level
]
DataStorage.physicals.append(data[0])
physicals_write(data)
def generate_supervisors(count=5):
if not DataStorage.physicals:
generate_physicals(10)
for _ in range(count):
data = [
random.choice(DataStorage.physicals),
random.randint(1, 30),
round(random.uniform(0.3, 1.0), 2),
random.choice(['МЛАДШИЙ НАУЧНЫЙ СОТРУДНИК', 'СТАРШИЙ НАУЧНЫЙ СОТРУДНИК',
'ВЕДУЩИЙ НАУЧНЫЙ СОТРУДНИК', 'ГЛАВНЫЙ НАУЧНЫЙ СОТРУДНИК', 'НАУЧНЫЙ СОТРУДНИК'])
]
DataStorage.supervisors.append(data[0])
supervisors_write(data)
def generate_faculties(count=3):
faculty_names = [
"Факультет компьютерных наук",
"Физико-математический факультет",
"Факультет экономики",
"Юридический факультет",
"Филологический факультет"
]
if not DataStorage.physicals:
generate_physicals(10)
for i in range(count):
head = random.choice(DataStorage.physicals)
vice = random.choice([p for p in DataStorage.physicals if p != head] + [None])
data = [
faculty_names[i],
faculty_names[i][:3].upper(),
head,
vice,
fake.address()
]
DataStorage.faculties.append(data)
faculties_write(data)
def generate_departments(count=5):
department_names = [
"Кафедра программной инженерии",
"Кафедра искусственного интеллекта",
"Кафедра теоретической физики",
"Кафедра прикладной математики",
"Кафедра экономической теории",
"Кафедра системного анализа",
"Кафедра кибербезопасности",
"Кафедра биоинформатики"
]
if not DataStorage.faculties:
generate_faculties()
if not DataStorage.physicals:
generate_physicals(10)
actual_count = min(count, len(department_names))
for i in range(actual_count):
head = random.choice(DataStorage.physicals)
vice = random.choice([p for p in DataStorage.physicals if p != head] + [None])
secretary = random.choice([p for p in DataStorage.physicals if p != head and p != vice] + [None])
data = [
department_names[i],
department_names[i][:3].upper(),
fake.date_between(start_date='-30y', end_date='-5y'),
head,
vice,
secretary,
random.choice(range(1, len(DataStorage.faculties)+1))
]
DataStorage.departments.append(data)
departments_write(data)
for i in range(actual_count, count):
dept_name = f"Кафедра {fake.unique.word().capitalize()}"
head = random.choice(DataStorage.physicals)
vice = random.choice([p for p in DataStorage.physicals if p != head] + [None])
secretary = random.choice([p for p in DataStorage.physicals if p != head and p != vice] + [None])
data = [
dept_name,
dept_name[:3].upper(),
fake.date_between(start_date='-30y', end_date='-5y'),
head,
vice,
secretary,
random.choice(range(1, len(DataStorage.faculties)+1))
]
DataStorage.departments.append(data)
departments_write(data)
def generate_programs(count=8):
program_names = [
"Программная инженерия",
"Искусственный интеллект и машинное обучение",
"Теоретическая физика",
"Прикладная математика и информатика",
"Экономика и финансы",
"Юриспруденция",
"Филология и лингвистика",
"Бизнес-информатика",
"Биоинженерия",
"Кибербезопасность",
"Международные отношения",
"Психология"
]
actual_count = min(count, len(program_names))
for i in range(actual_count):
parent_id = random.choice([None] + list(range(1, i+1)))
data = [
f"SPEC-{fake.unique.bothify(text='??-####')}",
random.choice(['BACHELOR', 'MAGISTER', 'ASPIRANT']),
program_names[i],
parent_id
]
DataStorage.programs.append(data)
programs_write(data)
for i in range(actual_count, count):
program_name = f"{fake.word().capitalize()} {fake.word().capitalize()}"
parent_id = random.choice([None] + list(range(1, len(DataStorage.programs)+1)))
data = [
f"SPEC-{fake.unique.bothify(text='??-####')}",
random.choice(['BACHELOR', 'MAGISTER', 'ASPIRANT']),
program_name,
parent_id
]
DataStorage.programs.append(data)
programs_write(data)
def generate_groups(count=5):
if not DataStorage.faculties or not DataStorage.programs or not DataStorage.departments:
generate_faculties()
generate_programs()
generate_departments()
group_prefixes = ['А', 'Б', 'М']
cyrillic_lower = [chr(c) for c in range(1072, 1104)]
for _ in range(count):
prefix = random.choice(group_prefixes)
year = fake.numerify(text='##')
num = fake.numerify(text='###')
letter = random.choice([''] + cyrillic_lower)
group_id = f"{prefix}{year}-{num}{letter}"
study_starts = fake.date_between(start_date='-4y', end_date='today')
study_ends = study_starts + timedelta(days=1460)
data = [
group_id,
random.choice(range(1, len(DataStorage.faculties)+1)),
random.choice(range(1, len(DataStorage.programs)+1)),
random.choice(range(1, len(DataStorage.departments)+1)),
study_starts,
study_ends
]
DataStorage.groups.append(data)
groups_write(data)
def generate_students(count=10):
if not DataStorage.physicals or not DataStorage.groups or not DataStorage.supervisors:
generate_physicals(150)
generate_groups()
generate_supervisors()
statuses = ['УЧИТСЯ', 'В АКАДЕМИЧЕСКОМ ОТПУСКЕ', 'ОТЧИСЛЕН']
education_forms = ['ОЧНАЯ', 'ЗАОЧНАЯ', 'ВЕЧЕРНЯЯ']
for _ in range(count):
data = [
random.choice(DataStorage.physicals),
random.choice([g[0] for g in DataStorage.groups]),
random.choice(DataStorage.supervisors + [None]),
random.choice(education_forms),
random.choices(statuses, weights=[0.85, 0.1, 0.05])[0]
]
DataStorage.students.append(data)
students_write(data)
def generate_family(count=20):
if not DataStorage.students:
generate_students()
kinships = ['MOTHER', 'FATHER', 'BROTHER', 'SISTER', 'ANOTHER']
for _ in range(count):
student = random.choice(DataStorage.students)[0]
kinship = random.choice(kinships)
data = [
student,
fake.first_name(),
fake.last_name(),
kinship,
fake.phone_number(),
fake.address()
]
family_write(data)
def generate_disciplines(count=7):
if not DataStorage.departments:
generate_departments()
discipline_names = [
"Программирование на Python",
"Базы данных",
"Машинное обучение",
"Теоретическая механика",
"Дифференциальные уравнения",
"Эконометрика",
"Гражданское право",
"История литературы"
]
for _ in range(count):
academic_hours = random.randint(36, 72)
general_hours = academic_hours + random.randint(1, 36)
data = [
f"{random.choice(discipline_names)} {fake.numerify(text='###')}",
random.choice(range(1, len(DataStorage.departments)+1)),
random.randint(2, 6),
academic_hours,
general_hours,
random.choice([True, False])
]
DataStorage.disciplines.append(data)
disciplines_write(data)
def generate_statements(count=300):
if not DataStorage.students or not DataStorage.disciplines or not DataStorage.physicals:
generate_students()
generate_disciplines()
generate_physicals()
for _ in range(count):
data = [
random.choice(range(1, len(DataStorage.students)+1)),
random.choice(range(1, len(DataStorage.disciplines)+1)),
random.choice(DataStorage.physicals),
random.randint(0, 2),
random.randint(3, 10),
fake.date_between(start_date='-2y', end_date='today')
]
statements_write(data)
def generate_movement(count=50):
if not DataStorage.students or not DataStorage.groups:
generate_students()
generate_groups()
movement_types = ['ЗАЧИСЛЕН', 'ВОССТАНОВЛЕН', 'ОТЧИСЛЕН', 'В АКАДЕМИЧЕСКИЙ ОТПУСК', 'ПЕРЕВОД В ДРУГУЮ ГРУППУ']
statuses = ['УЧИТСЯ', 'В АКАДЕМИЧЕСКОМ ОТПУСКЕ', 'ОТЧИСЛЕН']
for _ in range(count):
movement_type = random.choice(movement_types)
if movement_type == 'ПЕРЕВОД В ДРУГУЮ ГРУППУ':
new_group = random.choice([g[0] for g in DataStorage.groups if g[0] != random.choice([g[0] for g in DataStorage.groups])])
new_status = 'УЧИТСЯ'
elif movement_type == 'В АКАДЕМИЧЕСКИЙ ОТПУСК':
new_group = None
new_status = 'В АКАДЕМИЧЕСКОМ ОТПУСКЕ'
else:
new_group = None
new_status = random.choice(statuses)
data = [
random.choice(range(1, len(DataStorage.students)+1)),
movement_type,
new_group,
new_status,
fake.date_between(start_date='-2y', end_date='today')
]
movement_write(data)
def generate_files(count=100):
if not DataStorage.students:
generate_students()
extensions = ['PNG', 'JPEG', 'PDF']
for _ in range(count):
data = [
random.choice(range(1, len(DataStorage.students)+1)),
fake.file_name(),
fake.sentence(),
random.choice(extensions),
round(random.uniform(0.1, 20.0), 2),
f"/uploads/{fake.unique.uuid4()}"
]
files_write(data)
import debug_data.limits as lim
def generate_all_data():
generate_physicals(lim.PHYSICALS)
generate_supervisors(lim.SUPERVISORS)
generate_faculties(lim.FACULTIES)
generate_departments(lim.DEPARTMENTS)
generate_programs(lim.PROGRAMS)
generate_groups(lim.GROUPS)
generate_students(lim.STUDENTS)
generate_family(lim.FAMILY)
generate_disciplines(lim.DISCIPLINES)
generate_statements(lim.STATEMENTS)
generate_movement(lim.MOVEMENTS)
generate_files(lim.FILES)
if __name__ == "__main__":
generate_all_data()
print("Генерация тестовых данных завершена!")
# /$$$$$$ /$$ /$$ /$$
# /$$__ $$ | $$ |__/ | $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
# /$$ \ $$
# | $$$$$$/
# \______/
# /$$ /$$ /$$
# | $$$ /$$$ | $$
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
# /$$ /$$ /$$ /$$
# | $$ | $$ | $$ | $$
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
# /$$ | $$ /$$ \ $$ /$$ | $$
# | $$$$$$/ | $$$$$$/ | $$$$$$/
# \______/ \______/ \______/

View File

@ -0,0 +1,47 @@
#
# "contingent-movement" project
# author: gazakbayev.net
# ver: 1.0
#
import os
CREDS = {
"dbname": "edu",
"user": "edu",
"password": os.getenv("EDU_DB_PASSWORD"),
"host": "demo.labinn.ru",
"port": "5432",
}
# /$$$$$$ /$$ /$$ /$$
# /$$__ $$ | $$ |__/ | $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
# /$$ \ $$
# | $$$$$$/
# \______/
# /$$ /$$ /$$
# | $$$ /$$$ | $$
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
# /$$ /$$ /$$ /$$
# | $$ | $$ | $$ | $$
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
# /$$ | $$ /$$ \ $$ /$$ | $$
# | $$$$$$/ | $$$$$$/ | $$$$$$/
# \______/ \______/ \______/

View File

@ -0,0 +1,70 @@
#
# "contingent-movement" project;
# author: gazakbayev.net
# ver: 1.0
#
FACULTIES = [
("Физтех-школа прикладной математики и информатики", "ФПМИ"),
("Физтех-школа радиотехники и компьютерных технологий", "ФРКТ"),
("Физтех-школа биологической и медицинской физики", "ФБМФ"),
("Физтех-школа Электроники, Фотоники и Молекулярной Физики", "ФЭФМ"),
("Факультет общей и прикладной физики", "ФОПФ")
]
PROGRAMS = [
("01.03.02", "BACHELOR", "Прикладная математика и информатика"),
("10.03.01", "BACHELOR", "Информационная безопасность"),
("12.04.02", "MAGISTER", "Фотоника и оптоинформатика"),
("03.06.01", "ASPIRANT", "Физика и астрономия"),
("27.03.03", "BACHELOR", "Системный анализ и управление"),
("09.04.01", "MAGISTER", "Информатика и вычислительная техника"),
("02.06.01", "ASPIRANT", "Математика и механика"),
("11.04.03", "MAGISTER", "Конструирование и технология электронных средств")
]
QUALIFICATIONS = [
"МЛАДШИЙ НАУЧНЫЙ СОТРУДНИК", "СТАРШИЙ НАУЧНЫЙ СОТРУДНИК",
"ВЕДУЩИЙ НАУЧНЫЙ СОТРУДНИК", "ГЛАВНЫЙ НАУЧНЫЙ СОТРУДНИК", "НАУЧНЫЙ СОТРУДНИК"
]
DEGREES = ["BACHELOR", "MAGISTER", "ASPIRANT"]
ACCESS_LEVELS = ["КАМПУС", "ОБЩЕЖИТИЯ", "ПОЛНЫЙ"]
KINSHIP_TYPES = ["МАТЬ", "ОТЕЦ", "БРАТ", "СЕСТРА", "ДРУГИЕ"]
EDUCATION_FORMS = ["ОЧНАЯ", "ЗАОЧНАЯ", "ВЕЧЕРНЯЯ"]
STATUSES = ["УЧИТСЯ", "В АКАДЕМИЧЕСКОМ ОТПУСКЕ", "ОТЧИСЛЕН"]
FILE_EXTENSIONS = ["PNG", "JPEG", "PDF"]
MOVEMENT_TYPES = ["ЗАЧИСЛЕН", "ВОССТАНОВЛЕН", "ОТЧИСЛЕН", "ПЕРЕВЕДЁН"]
# /$$$$$$ /$$ /$$ /$$
# /$$__ $$ | $$ |__/ | $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
# /$$ \ $$
# | $$$$$$/
# \______/
# /$$ /$$ /$$
# | $$$ /$$$ | $$
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
# /$$ /$$ /$$ /$$
# | $$ | $$ | $$ | $$
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
# /$$ | $$ /$$ \ $$ /$$ | $$
# | $$$$$$/ | $$$$$$/ | $$$$$$/
# \______/ \______/ \______/

View File

@ -0,0 +1,50 @@
#
# "contingent-movement" project
# author: gazakbayev.net
# ver: 1.0
#
PHYSICALS = 300
SUPERVISORS = 10
FACULTIES = 4
DEPARTMENTS = 5
PROGRAMS = 8
GROUPS = 6
STUDENTS = 40
FAMILY = 100
DISCIPLINES = 7
STATEMENTS = 500
MOVEMENTS = 100
FILES = 100
# /$$$$$$ /$$ /$$ /$$
# /$$__ $$ | $$ |__/ | $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
# /$$ \ $$
# | $$$$$$/
# \______/
# /$$ /$$ /$$
# | $$$ /$$$ | $$
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
# /$$ /$$ /$$ /$$
# | $$ | $$ | $$ | $$
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
# /$$ | $$ /$$ \ $$ /$$ | $$
# | $$$$$$/ | $$$$$$/ | $$$$$$/
# \______/ \______/ \______/

View File

@ -0,0 +1,257 @@
#
# "contingent-movement" project;
# author: gazakbayev.net
# ver: 1.0
#
import psycopg2
from debug_data.database_creds import CREDS
class Database:
@staticmethod
def __connection__():
return psycopg2.connect(**CREDS)
@staticmethod
def initialize():
conn = Database.__connection__()
try:
with conn.cursor() as cur, open("../docs/schema.sql", "r", encoding="utf-8") as f:
cur.execute(f.read())
conn.commit()
print("[Contingent-movement] Initialized database with relations.")
except Exception as e:
print(f"[Contingent-movement] [Contingent Movement] Error: {e}")
conn.rollback()
finally:
conn.close()
def physicals_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Physicals (passport_no, name, surname, birthday, phone, mail, citizenship, address, access_card, access_level)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Physicals: {e}")
finally:
conn.close()
def supervisors_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Supervisors (person, experience, defended_ratio, qualification)
VALUES (%s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Supervisors: {e}")
finally:
conn.close()
def faculties_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Faculties (name, acronym, head, vice, address)
VALUES (%s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Faculties: {e}")
finally:
conn.close()
def departments_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Departments (name, acronym, founded, head, vice, secretary, faculty_id)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Departments: {e}")
finally:
conn.close()
def programs_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Programs (specification, degree, name, parent_id)
VALUES (%s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Programs: {e}")
finally:
conn.close()
def groups_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Groups (group_id, faculty_id, program_id, department_id, study_starts, study_ends)
VALUES (%s, %s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Groups: {e}")
finally:
conn.close()
def students_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
# 1. Сначала проверяем существование всех внешних ключей
cur.execute("""
SELECT 1 FROM physicals WHERE passport_no = %s
UNION ALL
SELECT 1 FROM groups WHERE group_id = %s
UNION ALL
SELECT 1 FROM physicals WHERE passport_no = %s OR %s IS NULL
""", [data[0], data[1], data[2], data[2]])
if len(cur.fetchall()) < 2 + (1 if data[2] is not None else 0):
raise ValueError("Invalid foreign key references")
cur.execute("""
INSERT INTO Students (person, group_id, supervisor, education_form, status)
VALUES (%s, %s, %s, %s, %s)
RETURNING id
""", data)
inserted_id = cur.fetchone()[0]
conn.commit()
return inserted_id
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Students: {e}")
return None
finally:
conn.close()
def family_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Family (person, name, surname, kinship, phone, address)
VALUES (%s, %s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Family: {e}")
finally:
conn.close()
def files_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Files (student_id, name, description, extension, size, path)
VALUES (%s, %s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Files: {e}")
finally:
conn.close()
def disciplines_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Disciplines (name, department_id, credit_units, academic_hours, general_hours, is_annual)
VALUES (%s, %s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Disciplines: {e}")
finally:
conn.close()
def statements_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Statements (student_id, discipline_id, examiner, try_no, grade, conducted_at)
VALUES (%s, %s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Statements: {e}")
finally:
conn.close()
def movement_write(data: list):
conn = Database.__connection__()
try:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO Movement (student_id, type, new_group, new_status, issued_at)
VALUES (%s, %s, %s, %s, %s)
""", data)
conn.commit()
except Exception as e:
conn.rollback()
print(f"[Contingent Movement] Error inserting into Movement: {e}")
finally:
conn.close()
# /$$$$$$ /$$ /$$ /$$
# /$$__ $$ | $$ |__/ | $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
# /$$ \ $$
# | $$$$$$/
# \______/
# /$$ /$$ /$$
# | $$$ /$$$ | $$
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
# /$$ /$$ /$$ /$$
# | $$ | $$ | $$ | $$
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
# /$$ | $$ /$$ \ $$ /$$ | $$
# | $$$$$$/ | $$$$$$/ | $$$$$$/
# \______/ \______/ \______/

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

1257
docs/dml_fill.sql Normal file

File diff suppressed because it is too large Load Diff

77
docs/funcs_and_procs.sql Normal file
View File

@ -0,0 +1,77 @@
CREATE OR REPLACE FUNCTION fn_low_enrollment(threshold INTEGER)
RETURNS TABLE(
discipline_id INTEGER,
discipline_name VARCHAR,
department_name VARCHAR,
enrolled BIGINT
) AS $$
BEGIN
RETURN QUERY
SELECT d.id,
d.name AS discipline_name,
dept.name AS department_name,
COUNT(st.id) AS enrolled
FROM Disciplines d
LEFT JOIN Departments dept ON d.department_id = dept.id
LEFT JOIN Statements st ON st.discipline_id = d.id
GROUP BY d.id, d.name, dept.name
HAVING COUNT(st.id) < threshold;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION fn_generate_transcript(p_student INTEGER)
RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
student_info RECORD;
transcript TEXT;
BEGIN
SELECT p.name || ' ' || p.surname AS full_name,
s.group_id,
s.status
INTO student_info
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
WHERE s.id = p_student;
transcript := 'Transcript for student ' || p_student || E'\n'
|| 'Name: ' || student_info.full_name || E'\n'
|| 'Group: ' || student_info.group_id || E'\n'
|| 'Status: ' || student_info.status || E'\n' || E'\n';
FOR rec IN
SELECT d.name AS discipline_name,
st.grade,
to_char(st.conducted_at,'YYYY-MM-DD') AS date
FROM Statements st
JOIN Disciplines d ON st.discipline_id = d.id
WHERE st.student_id = p_student
ORDER BY st.conducted_at
LOOP
transcript := transcript || rec.date || ' - ' || rec.discipline_name || ': ' || rec.grade || E'\n';
END LOOP;
RETURN transcript;
END;
$$;
CREATE OR REPLACE PROCEDURE sp_graduate_by_admission_year(p_year INTEGER)
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT s.id, g.study_starts
FROM Students s
JOIN Groups g ON s.group_id = g.group_id
WHERE EXTRACT(YEAR FROM g.study_starts) = p_year
AND s.status = 'УЧИТСЯ'
LOOP
INSERT INTO Movement(student_id, type, new_status, issued_at)
VALUES (rec.id, 'ОТЧИСЛЕН', 'ОТЧИСЛЕН', NOW());
END LOOP;
END;
$$;

9
docs/indexes.sql Normal file
View File

@ -0,0 +1,9 @@
CREATE INDEX idx_physicals_fulltext_name ON Physicals
USING GIN (to_tsvector('russian', name || ' ' || surname));
-- GIN (Generalized Inverted Index) хранит для каждой лексемы список указателей на строки,
-- в которых она встречается. Это ускоряет полнотекстовый поиск, так как позволяет быстро
-- находить все документы, содержащие нужные слова, без полного сканирования таблицы.
CREATE INDEX idx_movement_recent ON Movement(issued_at)
WHERE issued_at >= '2024-09-01'::timestamp;

192
docs/requests.sql Normal file
View File

@ -0,0 +1,192 @@
-- #
-- # "contingent-movement" project;
-- # author: gazakbayev.net
-- # ver: 1.0
-- #
-- 1 Студенты с одинаковыми фамилиями
WITH student_info AS (
SELECT
s.id as student_id,
p.passport_no,
p.surname,
p.name,
g.group_id,
f.name as faculty_name
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
JOIN Groups g ON s.group_id = g.group_id
JOIN Faculties f ON g.faculty_id = f.id
)
SELECT
a.surname,
a.name as student1_name,
b.name as student2_name,
a.group_id as group1,
b.group_id as group2,
a.faculty_name as faculty1,
b.faculty_name as faculty2
FROM student_info a
JOIN student_info b ON a.surname = b.surname
AND a.student_id < b.student_id
ORDER BY a.surname, a.name;
-- 2 Средний балл студентов
SELECT
s.id AS student_id,
p.name,
p.surname,
ROUND(AVG(st.grade)::numeric, 2) AS average_grade
FROM
Students s
JOIN
Physicals p ON s.person = p.passport_no
LEFT JOIN
Statements st ON s.id = st.student_id
GROUP BY
s.id, p.name, p.surname
ORDER BY
average_grade DESC NULLS LAST;
-- 3 Топ 5 лучших
SELECT
s.id,
p.surname,
p.name,
ROUND(AVG(st.grade)::numeric, 2) AS avg_grade,
DENSE_RANK() OVER (ORDER BY AVG(st.grade) DESC) AS rank
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
JOIN Statements st ON s.id = st.student_id
GROUP BY s.id, p.surname, p.name
ORDER BY avg_grade DESC
LIMIT 5;
-- 4 Количества у кафедры дисциплин и студентов
SELECT
d.name AS department,
COUNT(DISTINCT disc.id) AS discipline_count,
COUNT(DISTINCT s.id) AS student_count
FROM Departments d
LEFT JOIN Disciplines disc ON d.id = disc.department_id
LEFT JOIN Groups g ON d.id = g.department_id
LEFT JOIN Students s ON g.group_id = s.group_id
GROUP BY d.id
HAVING COUNT(DISTINCT s.id) > 0
ORDER BY student_count DESC;
-- 5 ФИ обучающихся (статус Учится)
SELECT
p.surname,
p.name
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
WHERE EXISTS (
SELECT 1 FROM Statements st
WHERE st.student_id = s.id
)
AND s.status = 'УЧИТСЯ';
-- 6 Прогресс изменения среднего балла по студентам
SELECT
s.id,
p.surname,
p.name,
g.group_id,
st.conducted_at,
st.grade,
AVG(st.grade) OVER (PARTITION BY s.id ORDER BY st.conducted_at
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_avg
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
JOIN Groups g ON s.group_id = g.group_id
JOIN Statements st ON s.id = st.student_id
ORDER BY s.id, st.conducted_at;
-- 7 Список группы с обучающимися, с указанием оценки успеваемости студента в группе "Ниже среднего" или "Выше среднего"
WITH group_stats AS (
SELECT
s.group_id,
AVG(st.grade) AS group_avg
FROM Students s
JOIN Statements st ON s.id = st.student_id
GROUP BY s.group_id
)
SELECT
s.id,
p.surname,
p.name,
g.group_id,
ROUND(AVG(st.grade)::numeric, 2) AS student_avg,
gs.group_avg,
CASE
WHEN AVG(st.grade) > gs.group_avg THEN 'Выше среднего'
WHEN AVG(st.grade) < gs.group_avg THEN 'Ниже среднего'
ELSE 'Средний'
END AS comparison
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
JOIN Groups g ON s.group_id = g.group_id
JOIN Statements st ON s.id = st.student_id
JOIN group_stats gs ON g.group_id = gs.group_id
GROUP BY s.id, p.surname, p.name, g.group_id, gs.group_avg
ORDER BY g.group_id, comparison;
-- 8 Статистика отчислений по месяцам, основываясь на движении контингента (приказах)
SELECT
DATE_TRUNC('month', m.issued_at) AS month,
COUNT(*) AS dropouts,
SUM(COUNT(*)) OVER (ORDER BY DATE_TRUNC('month', m.issued_at)) AS cumulative_total
FROM Movement m
WHERE m.type = 'ОТЧИСЛЕН'
GROUP BY DATE_TRUNC('month', m.issued_at)
ORDER BY month;
-- 9 Научные руководители студентов
SELECT
s.id AS student_id,
p.name AS student_name,
p.surname AS student_surname,
s.supervisor AS supervisor_id,
ph.name AS supervisor_name,
ph.surname AS supervisor_surname
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
LEFT JOIN Supervisors sup ON s.supervisor = sup.person
LEFT JOIN Physicals ph ON sup.person = ph.passport_no
WHERE s.supervisor IS NOT NULL
ORDER BY student_surname;
-- 10 Вывод преподавателей из ведомости по ФИО, принимающим предметам, средней оценки и уровня халявности:
SELECT
p.surname AS "Фамилия",
p.name AS "Имя",
STRING_AGG(DISTINCT d.name, ', ' ORDER BY d.name) AS "Принимает дисциплины",
ROUND(AVG(st.grade), 2) AS "Средняя оценка",
CASE
WHEN AVG(st.grade) < 3 THEN 'Не халявный'
WHEN AVG(st.grade) < 5 THEN 'Хороший'
WHEN AVG(st.grade) < 8 THEN 'Халявный'
ELSE 'Ультра халявный'
END AS "Уровень халявности"
FROM
Statements st
JOIN
Physicals p ON st.examiner = p.passport_no
JOIN
Disciplines d ON st.discipline_id = d.id
GROUP BY
p.passport_no, p.surname, p.name
HAVING
COUNT(*) >= 5
ORDER BY
"Средняя оценка" DESC;

126
docs/schema.sql Normal file
View File

@ -0,0 +1,126 @@
-- #
-- # "contingent-movement" project;
-- # author: gazakbayev.net
-- # ver: 1.0
-- #
CREATE TABLE Physicals (
passport_no VARCHAR(30) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
surname VARCHAR(255) NOT NULL,
birthday TIMESTAMP NOT NULL,
phone VARCHAR(20) NOT NULL,
mail VARCHAR(255) NOT NULL,
citizenship VARCHAR(50) NOT NULL,
address VARCHAR(255) NOT NULL,
access_card VARCHAR(20) NOT NULL CHECK (access_card ~ '^[A-F0-9]{2}(:[A-F0-9]{2}){5}$'),
access_level VARCHAR(10) NOT NULL CHECK (access_level IN ('КАМПУС', 'ОБЩЕЖИТИЯ', 'ПОЛНЫЙ'))
);
CREATE TABLE Supervisors (
id SERIAL PRIMARY KEY,
person VARCHAR(32) REFERENCES Physicals(passport_no),
experience INTEGER NOT NULL CHECK (experience >= 0),
defended_ratio REAL NOT NULL CHECK (defended_ratio >= 0 AND defended_ratio <= 1),
qualification VARCHAR(30) NOT NULL CHECK (qualification IN ('МЛАДШИЙ НАУЧНЫЙ СОТРУДНИК', 'СТАРШИЙ НАУЧНЫЙ СОТРУДНИК', 'ВЕДУЩИЙ НАУЧНЫЙ СОТРУДНИК', 'ГЛАВНЫЙ НАУЧНЫЙ СОТРУДНИК', 'НАУЧНЫЙ СОТРУДНИК')),
valid_from TIMESTAMP NOT NULL,
valid_to TIMESTAMP
);
CREATE TABLE Faculties (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
acronym VARCHAR(10) NOT NULL,
head VARCHAR(32) NOT NULL REFERENCES Physicals(passport_no),
vice VARCHAR(32) REFERENCES Physicals(passport_no),
address VARCHAR(255) NOT NULL
);
CREATE TABLE Departments (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
acronym VARCHAR(10) NOT NULL,
founded TIMESTAMP NOT NULL,
head VARCHAR(32) NOT NULL REFERENCES Physicals(passport_no),
vice VARCHAR(32) REFERENCES Physicals(passport_no),
secretary VARCHAR(32) REFERENCES Physicals(passport_no),
faculty_id INTEGER REFERENCES Faculties(id)
);
CREATE TABLE Programs (
id SERIAL PRIMARY KEY,
specification VARCHAR(20) NOT NULL,
degree VARCHAR(30) NOT NULL CHECK (degree IN ('BACHELOR', 'MAGISTER', 'ASPIRANT')),
name VARCHAR(255) NOT NULL,
parent_id INTEGER REFERENCES Programs(id)
);
CREATE TABLE Groups (
group_id VARCHAR(10) PRIMARY KEY CHECK (group_id ~ '^[АБМ]\d{2}-\d{3}[а-я]?$'),
faculty_id INTEGER NOT NULL REFERENCES Faculties(id),
program_id INTEGER NOT NULL REFERENCES Programs(id),
department_id INTEGER REFERENCES Departments(id),
study_starts TIMESTAMP NOT NULL DEFAULT NOW(),
study_ends TIMESTAMP NOT NULL DEFAULT NOW() + INTERVAL '4 years'
);
CREATE TABLE Students (
id SERIAL PRIMARY KEY,
person VARCHAR(32) NOT NULL UNIQUE REFERENCES Physicals(passport_no),
group_id VARCHAR(10) NOT NULL REFERENCES Groups(group_id),
supervisor_id INTEGER REFERENCES Supervisors(id),
education_form VARCHAR(255) NOT NULL CHECK (education_form IN ('ОЧНАЯ', 'ЗАОЧНАЯ', 'ВЕЧЕРНЯЯ')),
status VARCHAR(30) NOT NULL CHECK (status IN ('УЧИТСЯ', 'В АКАДЕМИЧЕСКОМ ОТПУСКЕ', 'ОТЧИСЛЕН'))
);
CREATE TABLE Family (
id SERIAL PRIMARY KEY,
person VARCHAR(32) NOT NULL REFERENCES Physicals(passport_no),
name VARCHAR(255) NOT NULL,
surname VARCHAR(255) NOT NULL,
kinship VARCHAR(10) NOT NULL CHECK (kinship IN ('MOTHER', 'FATHER', 'BROTHER', 'SISTER', 'ANOTHER')),
phone VARCHAR(20) NOT NULL UNIQUE,
address VARCHAR(255) NOT NULL,
CONSTRAINT unique_student_kinship UNIQUE (person, kinship)
);
CREATE TABLE Files (
id SERIAL PRIMARY KEY,
student_id INTEGER NOT NULL REFERENCES Students(id),
name VARCHAR(255) NOT NULL,
description VARCHAR(255) NOT NULL,
extension VARCHAR(5) NOT NULL CHECK (extension IN ('PNG', 'JPEG', 'PDF')),
size NUMERIC(10,2) NOT NULL CHECK (size <= 20),
path VARCHAR(255) NOT NULL UNIQUE,
loaded_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE Disciplines (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
department_id INTEGER NOT NULL REFERENCES Departments(id),
credit_units INTEGER NOT NULL,
academic_hours INTEGER NOT NULL,
general_hours INTEGER NOT NULL CHECK (general_hours > academic_hours),
is_annual BOOLEAN NOT NULL
);
CREATE TABLE Statements (
id SERIAL PRIMARY KEY,
student_id INTEGER NOT NULL REFERENCES Students(id),
discipline_id INTEGER NOT NULL REFERENCES Disciplines(id),
examiner VARCHAR(32) NOT NULL REFERENCES Physicals(passport_no),
try_no INTEGER NOT NULL CHECK (try_no BETWEEN 0 AND 2),
grade INTEGER NOT NULL CHECK (grade BETWEEN 1 AND 10),
conducted_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE Movement (
id SERIAL PRIMARY KEY,
student_id INTEGER NOT NULL REFERENCES Students(id),
type VARCHAR(30) NOT NULL CHECK (type IN ('ЗАЧИСЛЕН', 'ВОССТАНОВЛЕН', 'ОТЧИСЛЕН', 'В АКАДЕМИЧЕСКИЙ ОТПУСК', 'ПЕРЕВОД В ДРУГУЮ ГРУППУ')),
new_group VARCHAR(10) REFERENCES Groups(group_id),
new_status VARCHAR(30) CHECK (new_status IN ('УЧИТСЯ', 'В АКАДЕМИЧЕСКОМ ОТПУСКЕ', 'ОТЧИСЛЕН')),
issued_at TIMESTAMP NOT NULL DEFAULT NOW()
);

101
docs/triggers.sql Normal file
View File

@ -0,0 +1,101 @@
-- Trigger 1
CREATE OR REPLACE FUNCTION trg_prevent_group_delete()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE cnt INTEGER;
BEGIN
SELECT COUNT(*) INTO cnt FROM Students WHERE group_id = OLD.group_id AND status = 'УЧИТСЯ';
IF cnt > 0 THEN
RAISE EXCEPTION 'Cannot delete group %: % active students exist', OLD.group_id, cnt;
END IF;
RETURN OLD;
END;
$$;
CREATE TRIGGER prevent_group_delete
BEFORE DELETE ON Groups
FOR EACH ROW
EXECUTE PROCEDURE trg_prevent_group_delete();
-- Trigger 2
CREATE OR REPLACE FUNCTION trg_lowercase_mail()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
NEW.mail := lower(NEW.mail);
RETURN NEW;
END;
$$;
CREATE TRIGGER lowercase_mail
BEFORE INSERT OR UPDATE ON Physicals
FOR EACH ROW
EXECUTE PROCEDURE trg_lowercase_mail();
-- Trigger 3
CREATE OR REPLACE FUNCTION trg_sync_student_status()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
UPDATE Students
SET status = NEW.new_status
WHERE id = NEW.student_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER sync_student_status
AFTER INSERT ON Movement
FOR EACH ROW
EXECUTE PROCEDURE trg_sync_student_status();
-- Trigger SCD 2
CREATE OR REPLACE FUNCTION trg_supervisors_history()
RETURNS trigger AS $$
BEGIN
NEW.valid_from = NOW();
INSERT INTO Supervisors (
person,
experience,
defended_ratio,
qualification,
valid_from,
valid_to
) VALUES (
OLD.person,
OLD.experience,
OLD.defended_ratio,
OLD.qualification,
OLD.valid_from,
NOW()
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_supervisors_scd
BEFORE UPDATE ON Supervisors
FOR EACH ROW
EXECUTE PROCEDURE trg_supervisors_history();
CREATE OR REPLACE FUNCTION check_unique_supervisor()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
SELECT 1 FROM Supervisors
WHERE person = NEW.person
AND experience = NEW.experience
AND defended_ratio = NEW.defended_ratio
AND qualification = NEW.qualification
AND valid_from = NEW.valid_from
AND (valid_to = NEW.valid_to OR (valid_to IS NULL AND NEW.valid_to IS NULL))
) THEN
RAISE EXCEPTION 'Дублирующая запись в таблице Supervisors. Все поля (кроме id) должны быть уникальными.';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER unique_supervisor_trigger
BEFORE INSERT ON Supervisors
FOR EACH ROW
EXECUTE FUNCTION check_unique_supervisor();

22
docs/views.sql Normal file
View File

@ -0,0 +1,22 @@
-- STUDENT PERFORMANCE --
CREATE OR REPLACE VIEW vw_student_performance AS
SELECT s.id AS student_id,
p.name || ' ' || p.surname AS full_name,
s.group_id,
COUNT(st.id) FILTER (WHERE st.grade >= 3) AS passes,
COUNT(st.id) FILTER (WHERE st.grade < 3) AS fails,
ROUND(AVG(st.grade)::numeric, 2) AS avg_grade
FROM Students s
JOIN Physicals p ON s.person = p.passport_no
LEFT JOIN Statements st ON s.id = st.student_id
GROUP BY s.id, p.name, p.surname, s.group_id;
-- BIRTHDAYS ON THIS MONTH --
CREATE VIEW vw_birthdays_month AS
SELECT passport_no,
name || ' ' || surname AS full_name,
birthday,
EXTRACT(DAY FROM birthday) AS day_of_month
FROM Physicals
WHERE EXTRACT(MONTH FROM birthday) = EXTRACT(MONTH FROM NOW())
ORDER BY day_of_month;

View File

@ -0,0 +1,110 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 YaBrowser/25.2.0.0 Safari/537.36" version="26.1.1">
<diagram name="Концептуальная модель" id="uXQN3Tqoo_ou7SFrJxsO">
<mxGraphModel dx="954" dy="603" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="M9otMMDKzzFARfVN8Q2O-27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;endArrow=ERone;endFill=0;startArrow=ERmany;startFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-10" target="M9otMMDKzzFARfVN8Q2O-22" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=circle;endFill=0;endSize=3;startArrow=ERmany;startFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-10" target="M9otMMDKzzFARfVN8Q2O-16" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=ERone;startFill=0;endArrow=ERone;endFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-10" target="M9otMMDKzzFARfVN8Q2O-18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-10" value="Students" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="200" y="440" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-23" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=ERone;endFill=0;startArrow=ERmany;startFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-11" target="M9otMMDKzzFARfVN8Q2O-10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-11" value="Family" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="10" y="530" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=ERmany;startFill=0;endArrow=none;" parent="1" source="M9otMMDKzzFARfVN8Q2O-13" target="M9otMMDKzzFARfVN8Q2O-10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-13" value="Files" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="10" y="590" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-41" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=none;startFill=0;startArrow=ERmany;" parent="1" source="M9otMMDKzzFARfVN8Q2O-14" target="M9otMMDKzzFARfVN8Q2O-10" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-14" value="Movement" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="90" y="230" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-15" target="M9otMMDKzzFARfVN8Q2O-18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-42" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endArrow=none;startFill=0;startArrow=ERmany;" parent="1" source="M9otMMDKzzFARfVN8Q2O-15" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="200" y="490" as="targetPoint" />
<Array as="points">
<mxPoint x="70" y="200" />
<mxPoint x="70" y="490" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-44" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;startArrow=ERmany;startFill=0;endArrow=ERone;endFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-15" target="M9otMMDKzzFARfVN8Q2O-17" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-15" value="Statements" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="90" y="180" width="100" height="40" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;endArrow=none;startFill=0;startArrow=ERone;" parent="1" source="M9otMMDKzzFARfVN8Q2O-16" target="M9otMMDKzzFARfVN8Q2O-18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-16" value="Supervisors" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="200" y="360" width="120" height="40" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;endArrow=ERone;endFill=0;startArrow=ERmany;startFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-17" target="M9otMMDKzzFARfVN8Q2O-19" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-17" value="Disciplines" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="550" y="150" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-18" value="Physicals" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="330" y="160" width="150" height="180" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-38" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.75;entryDx=0;entryDy=0;startArrow=ERmany;startFill=0;endArrow=ERmany;endFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-19" target="M9otMMDKzzFARfVN8Q2O-18" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-19" value="Departments" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="640" y="290" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="xU2I2qLbYURmXa2A43uc-1" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=oval;startFill=0;endArrow=ERmany;endFill=0;" edge="1" parent="1" target="M9otMMDKzzFARfVN8Q2O-19">
<mxGeometry relative="1" as="geometry">
<mxPoint x="700" y="406" as="sourcePoint" />
<Array as="points">
<mxPoint x="700" y="380" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-20" value="Faculties" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="640" y="410" width="120" height="50" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;endArrow=ERone;endFill=0;endSize=6;startArrow=ERone;startFill=0;startSize=6;" parent="1" source="M9otMMDKzzFARfVN8Q2O-21" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="650" y="600.4000000000001" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-21" value="Programs" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="560" y="550" width="120" height="50" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.25;entryY=0;entryDx=0;entryDy=0;endArrow=ERone;endFill=0;startArrow=ERmany;startFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-22" target="M9otMMDKzzFARfVN8Q2O-21" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-36" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=ERmany;startFill=0;endArrow=ERone;endFill=0;" parent="1" source="M9otMMDKzzFARfVN8Q2O-22" target="M9otMMDKzzFARfVN8Q2O-20" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-37" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;startArrow=ERmany;startFill=0;endArrow=circle;endFill=0;endSize=3;" parent="1" source="M9otMMDKzzFARfVN8Q2O-22" target="M9otMMDKzzFARfVN8Q2O-19" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="M9otMMDKzzFARfVN8Q2O-22" value="Groups" style="rounded=1;arcSize=10;whiteSpace=wrap;html=1;align=center;" parent="1" vertex="1">
<mxGeometry x="390" y="420" width="150" height="100" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,193 @@
Table Students {
id integer [pk, increment, not null, unique, note: 'Номер студенческого билета']
person_id integer [not null, unique, note: 'Номер физического лица']
group varchar [not null, note: 'Номер учебной группы']
supervisor integer [note: 'Номер научного руководителя']
education_form varchar [not null, default: 'Очная', note: 'Форма обучения студента']
status varchar [not null, default: 'Учится', note: 'Статус обучающегося']
}
Table Persons {
id integer [pk, increment, not null, unique]
name varchar [not null]
surname varchar [not null]
birthday timestamp [not null]
phone varchar [not null]
mail varchar [default: null]
passport_no varchar [not null]
citizenship varchar [not null]
address varchar [not null]
access_card varchar [not null]
access_level varchar [not null]
}
Table Groups {
group varchar [pk, not null, unique]
faculty_id integer [not null]
program_id integer [not null]
department_id integer [not null]
study_starts timestamp [not null]
study_ends timestamp [not null]
}
Table Family {
id integer [pk, increment, not null, unique]
student_id integer [not null]
name varchar [not null]
surname varchar [not null]
kinship varchar [not null, note: 'Степень родства: mthr, fthr, brth, sstr']
phone varchar [not null]
address varchar [not null]
}
Table Files {
id integer [pk, increment, not null, unique]
student_id integer [not null]
name varchar [not null]
description varchar [not null]
extension varchar [not null, note: 'available: png, jpeg, pdf']
size numeric [not null, note: 'size in mb']
path varchar [not null, note: 'path to file /var/www/student_files/<file_id>.<ext>']
loaded_at timestamp [not null]
}
Table Supervisors {
id integer [pk, increment, not null, unique]
person_id integer [not null]
experience integer [not null]
defended_ratio real [not null]
qualification varchar [not null]
}
Table Movement {
id integer [pk, increment, not null, unique]
student_id integer [not null]
type varchar [not null]
new_group varchar
new_status varchar
issued_at timestamp [not null]
}
Table Departments {
id integer [pk, increment, not null, unique]
name varchar [not null]
acronym varchar [not null]
founded timestamp [not null]
head integer [not null]
vice integer
secretary integer
faculty_id integer
}
Table Programs {
id integer [pk, increment, not null, unique]
specification varchar [not null, note: 'Key from Russian register']
degree varchar [not null]
name varchar [not null]
parent_id integer [not null]
}
Table Faculties {
id integer [pk, increment, not null, unique]
name varchar [not null]
acronym varchar [not null]
head integer [not null]
vice integer
address varchar [not null]
}
Table Statements {
id integer [pk, increment, not null, unique]
student_id integer [not null]
discipline_id integer [not null]
examiner_id integer [not null]
try_no integer [not null]
grade integer [not null]
conducted_at timestamp [not null]
}
Table Disciplines {
id integer [pk, increment, not null, unique]
name varchar [not null]
department_id integer [not null]
credit_units integer [not null]
academic_hours integer [not null]
general_hours integer [not null]
is_annual boolean [not null]
}
Ref "Студент к физлицу" {
Persons.id - Students.person_id [delete: no action]
}
Ref "Загружен для" {
Files.student_id > Students.id [delete: no action]
}
Ref "Руководитель к физлицу" {
Departments.head - Persons.id [delete: no action]
}
Ref "Заместитель к физлицу" {
Departments.vice - Persons.id [delete: no action]
}
Ref "Секретарь к физлицу" {
Departments.secretary - Persons.id [delete: no action]
}
Ref "К научному руководителю" {
Students.supervisor > Supervisors.id [delete: no action]
}
Ref "В учебной группе" {
Students.group > Groups.group [delete: no action]
}
Ref "План/программа группы" {
Groups.program_id > Programs.id [delete: no action]
}
Ref "Группа факультета" {
Groups.faculty_id > Faculties.id [delete: no action]
}
Ref "Имеет базовую кафедру" {
Groups.department_id > Departments.id [delete: no action]
}
Ref "Ведомость по студенту" {
Statements.student_id > Students.id [delete: no action]
}
Ref "Ведомость по дисциплине" {
Statements.discipline_id > Disciplines.id [delete: no action]
}
Ref "Экзаменатор к физлицу" {
Statements.examiner_id > Persons.id [delete: no action]
}
Ref "Дисциплина к кафедре" {
Disciplines.department_id > Departments.id [delete: no action]
}
Ref "Подразделение факультета" {
Departments.faculty_id > Faculties.id [delete: no action]
}
Ref "Приказ по студенту" {
Movement.student_id > Students.id [delete: no action]
}
Ref "Научный руководитель к физлицу" {
Supervisors.person_id - Persons.id [delete: no action]
}
Ref "Дочерний к программе" {
Programs.parent_id - Programs.id [delete: no action]
}
Ref "Родственник студента" {
Family.student_id > Students.id [delete: no action]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Faker==37.0.1
psycopg2==2.9.10
tzdata==2025.1

Binary file not shown.