cloned from code.mipt.ru
170
.gitignore
vendored
Normal 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
@ -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
@ -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)
|
||||
|
||||
## Концептуальная модель
|
||||
|
||||

|
||||
|
||||
Для редактирования и изменения можно фоспользоваться [.drawio](https://cloud.gazakbayev.net/index.php/s/5XpDS4Zx287bejj?dir=/) файлом сохранения.
|
||||
|
||||
## Логическая модель
|
||||
|
||||

|
||||
|
||||
Логическая модель была создана с использованием языка 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}[а-я]?$'),
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Data Manipulation
|
||||
|
||||
С помощью библиотеку Faker и скриптов на языке Python были сгенерированы данные для дальнейшего взаимодействия с базой.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
*Да, можно посмеяться с того, что фамилии и имена не согласуются, и вообще Faker - генерирует какую-то абсолютно рандомную фигню :)*
|
||||
|
||||
## Requests
|
||||
|
||||
В файле requests.sql приведены достаточно интересные запросы с использованием всех требуемых операторов, подзапросов и оконных функций:
|
||||
|
||||
- Студенты с одинаковыми фамилиями
|
||||
|
||||

|
||||
|
||||
- Средний балл студентов
|
||||
|
||||

|
||||
|
||||
- Топ 5 лучших
|
||||
|
||||

|
||||
|
||||
- Количества у кафедры дисциплин и студентов
|
||||
|
||||

|
||||
|
||||
- ФИ обучающихся (статус Учится)
|
||||
|
||||

|
||||
|
||||
- Прогресс изменения среднего балла по студентам
|
||||
|
||||

|
||||
|
||||
- Список группы с обучающимися, с указанием оценки успеваемости студента в группе "Ниже среднего" или "Выше среднего"
|
||||
|
||||

|
||||
|
||||
- Статистика отчислений по месяцам, основываясь на движении контингента (приказах)
|
||||
|
||||

|
||||
|
||||
- Научные руководители студентов
|
||||
|
||||

|
||||
|
||||
- Вывод преподавателей из ведомости по ФИО, принимающим предметам, средней оценки и уровня халявности
|
||||
|
||||

|
||||
|
||||
## Техническая итерация : Индексы
|
||||
|
||||
Созданы индексы `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
@ -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("Генерация тестовых данных завершена!")
|
||||
|
||||
|
||||
# /$$$$$$ /$$ /$$ /$$
|
||||
# /$$__ $$ | $$ |__/ | $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
|
||||
# /$$ \ $$
|
||||
# | $$$$$$/
|
||||
# \______/
|
||||
# /$$ /$$ /$$
|
||||
# | $$$ /$$$ | $$
|
||||
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
|
||||
|
||||
# /$$ /$$ /$$ /$$
|
||||
# | $$ | $$ | $$ | $$
|
||||
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
|
||||
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
|
||||
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
|
||||
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
|
||||
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
|
||||
# /$$ | $$ /$$ \ $$ /$$ | $$
|
||||
# | $$$$$$/ | $$$$$$/ | $$$$$$/
|
||||
# \______/ \______/ \______/
|
47
analysis/debug_data/database_creds.py
Normal 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",
|
||||
}
|
||||
|
||||
# /$$$$$$ /$$ /$$ /$$
|
||||
# /$$__ $$ | $$ |__/ | $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
|
||||
# /$$ \ $$
|
||||
# | $$$$$$/
|
||||
# \______/
|
||||
# /$$ /$$ /$$
|
||||
# | $$$ /$$$ | $$
|
||||
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
|
||||
|
||||
# /$$ /$$ /$$ /$$
|
||||
# | $$ | $$ | $$ | $$
|
||||
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
|
||||
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
|
||||
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
|
||||
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
|
||||
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
|
||||
# /$$ | $$ /$$ \ $$ /$$ | $$
|
||||
# | $$$$$$/ | $$$$$$/ | $$$$$$/
|
||||
# \______/ \______/ \______/
|
70
analysis/debug_data/database_values.py
Normal 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 = ["ЗАЧИСЛЕН", "ВОССТАНОВЛЕН", "ОТЧИСЛЕН", "ПЕРЕВЕДЁН"]
|
||||
|
||||
|
||||
# /$$$$$$ /$$ /$$ /$$
|
||||
# /$$__ $$ | $$ |__/ | $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
|
||||
# /$$ \ $$
|
||||
# | $$$$$$/
|
||||
# \______/
|
||||
# /$$ /$$ /$$
|
||||
# | $$$ /$$$ | $$
|
||||
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
|
||||
|
||||
# /$$ /$$ /$$ /$$
|
||||
# | $$ | $$ | $$ | $$
|
||||
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
|
||||
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
|
||||
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
|
||||
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
|
||||
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
|
||||
# /$$ | $$ /$$ \ $$ /$$ | $$
|
||||
# | $$$$$$/ | $$$$$$/ | $$$$$$/
|
||||
# \______/ \______/ \______/
|
50
analysis/debug_data/limits.py
Normal 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
|
||||
|
||||
# /$$$$$$ /$$ /$$ /$$
|
||||
# /$$__ $$ | $$ |__/ | $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
|
||||
# /$$ \ $$
|
||||
# | $$$$$$/
|
||||
# \______/
|
||||
# /$$ /$$ /$$
|
||||
# | $$$ /$$$ | $$
|
||||
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
|
||||
|
||||
# /$$ /$$ /$$ /$$
|
||||
# | $$ | $$ | $$ | $$
|
||||
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
|
||||
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
|
||||
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
|
||||
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
|
||||
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
|
||||
# /$$ | $$ /$$ \ $$ /$$ | $$
|
||||
# | $$$$$$/ | $$$$$$/ | $$$$$$/
|
||||
# \______/ \______/ \______/
|
257
analysis/manager/__init__.py
Normal 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()
|
||||
|
||||
|
||||
# /$$$$$$ /$$ /$$ /$$
|
||||
# /$$__ $$ | $$ |__/ | $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ /$$__ $$| $$__ $$|_ $$_/ | $$| $$__ $$ /$$__ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ | $$ \ $$| $$ \ $$ | $$ | $$| $$ \ $$| $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$ $$| $$ | $$| $$ | $$ | $$ /$$| $$| $$ | $$| $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$ | $$$$/| $$| $$ | $$| $$$$$$$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# \______/ \______/ |__/ |__/ \___/ |__/|__/ |__/ \____ $$ \_______/|__/ |__/ \___/
|
||||
# /$$ \ $$
|
||||
# | $$$$$$/
|
||||
# \______/
|
||||
# /$$ /$$ /$$
|
||||
# | $$$ /$$$ | $$
|
||||
# | $$$$ /$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$/$$$$ /$$$$$$ /$$$$$$$ /$$$$$$
|
||||
# | $$ $$/$$ $$ /$$__ $$| $$ /$$//$$__ $$| $$_ $$_ $$ /$$__ $$| $$__ $$|_ $$_/
|
||||
# | $$ $$$| $$| $$ \ $$ \ $$/$$/| $$$$$$$$| $$ \ $$ \ $$| $$$$$$$$| $$ \ $$ | $$
|
||||
# | $$\ $ | $$| $$ | $$ \ $$$/ | $$_____/| $$ | $$ | $$| $$_____/| $$ | $$ | $$ /$$
|
||||
# | $$ \/ | $$| $$$$$$/ \ $/ | $$$$$$$| $$ | $$ | $$| $$$$$$$| $$ | $$ | $$$$/
|
||||
# |__/ |__/ \______/ \_/ \_______/|__/ |__/ |__/ \_______/|__/ |__/ \___/
|
||||
|
||||
# /$$ /$$ /$$ /$$
|
||||
# | $$ | $$ | $$ | $$
|
||||
# | $$$$$$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$$$ /$$$$$$ | $$ /$$| $$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$__ $$| $$ | $$ /$$__ $$ |____ $$|____ /$$/ |____ $$| $$ /$$/| $$__ $$ |____ $$| $$ | $$ /$$__ $$| $$ /$$/| $$__ $$ /$$__ $$|_ $$_/
|
||||
# | $$ \ $$| $$ | $$ | $$ \ $$ /$$$$$$$ /$$$$/ /$$$$$$$| $$$$$$/ | $$ \ $$ /$$$$$$$| $$ | $$| $$$$$$$$ \ $$/$$/ | $$ \ $$| $$$$$$$$ | $$
|
||||
# | $$ | $$| $$ | $$ | $$ | $$ /$$__ $$ /$$__/ /$$__ $$| $$_ $$ | $$ | $$ /$$__ $$| $$ | $$| $$_____/ \ $$$/ | $$ | $$| $$_____/ | $$ /$$
|
||||
# | $$$$$$$/| $$$$$$$ | $$$$$$$| $$$$$$$ /$$$$$$$$| $$$$$$$| $$ \ $$| $$$$$$$/| $$$$$$$| $$$$$$$| $$$$$$$ \ $//$$| $$ | $$| $$$$$$$ | $$$$/
|
||||
# |_______/ \____ $$ \____ $$ \_______/|________/ \_______/|__/ \__/|_______/ \_______/ \____ $$ \_______/ \_/|__/|__/ |__/ \_______/ \___/
|
||||
# /$$ | $$ /$$ \ $$ /$$ | $$
|
||||
# | $$$$$$/ | $$$$$$/ | $$$$$$/
|
||||
# \______/ \______/ \______/
|
BIN
docs/dbeaver/dbeaver_ddl.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
docs/dbeaver/dbeaver_dml_persons.png
Normal file
After Width: | Height: | Size: 206 KiB |
BIN
docs/dbeaver/dbeaver_dml_students.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
docs/dbeaver/dbeaver_r_1.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
docs/dbeaver/dbeaver_r_10.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/dbeaver/dbeaver_r_2.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/dbeaver/dbeaver_r_3.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
docs/dbeaver/dbeaver_r_4.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
docs/dbeaver/dbeaver_r_5.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
docs/dbeaver/dbeaver_r_6.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
docs/dbeaver/dbeaver_r_7.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
docs/dbeaver/dbeaver_r_8.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
docs/dbeaver/dbeaver_r_9.png
Normal file
After Width: | Height: | Size: 100 KiB |
1257
docs/dml_fill.sql
Normal file
77
docs/funcs_and_procs.sql
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
110
docs/Концептуальная модель.drawio
Normal 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>
|
BIN
docs/Концептуальная модель.png
Normal file
After Width: | Height: | Size: 46 KiB |
193
docs/Логическая модель.dbml
Normal 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]
|
||||
}
|
BIN
docs/Логическая модель.jpg
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/Физическая модель.pdf
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
Faker==37.0.1
|
||||
psycopg2==2.9.10
|
||||
tzdata==2025.1
|