20 Commits

Author SHA1 Message Date
d4ab5005a4 лан, со специализациями вроде бы все. нужно с группами то же самое сделать и можно будет переключаться на приказы 2025-06-19 04:20:59 +04:00
dad73a03f5 пока что только специализации отображаются адекватно, в зависимости от айди текущего декана) 2025-06-19 03:16:18 +04:00
c37dbbf68c оставила только один приказ - буду менять его типы. так, по мелочи - подгружаются пока что ВСЕ направления, группы и т.д., открывается форма создания/редактирования направления. перехожу к созданию системы учета текущего декана 2025-06-19 01:26:17 +04:00
86eb94436f накатила заново миграцию, перехожу к заполнению бд данными 2025-06-16 17:48:01 +04:00
d879fcfde7 доделала еще и приказы под фронт 2025-06-16 16:29:20 +04:00
23cedfe4e1 поправила немного модели на фронте для нормальной десериализации данных 2025-06-16 16:05:23 +04:00
7ba2e811f5 группы отображаются ура )) 2025-06-15 20:03:18 +04:00
a58a7219ee что-то я еще подкорректировала и потихоньку перехожу к совокуплению бэка и фронта 2025-06-11 05:48:06 +04:00
bf4a0c1100 что ж... я добавила ОЧЕНЬ много дтошек, роутеров, в принципе закончила с модельками, теперь осталось закончить с апишкой и снова перейти к фронту и все тестировать 2025-06-11 03:51:08 +04:00
611ea95c12 сделала свою бд, нужно теперь прописывать роуты и... встречать возможные ошибки :_) 2025-06-10 23:46:47 +04:00
2f521dd674 поменяла факултет и связи у декана и факультета 2025-06-10 14:25:59 +04:00
8ca935b1d5 сделала лоджики для студентов, групп и направлений с моковыми данными
починила тип проекта
отображаются списки на форме
2025-06-09 22:54:54 +04:00
988e6db8c4 добавила репозитории, сделала абстрактный класс репозитория. переделала модели на модели без конструктора 2025-06-08 21:09:35 +04:00
24da47b509 потихоньку привожу модели в божеский вид. ну и делаю сёрчи для моделей 2025-06-08 13:50:20 +04:00
c4889aaa73 наброски форм:
главная, фильтрация и редактирование/добавление
2025-06-07 13:18:59 +04:00
88f37d6c46 отличненько, полдела с разбором форм сделано 2025-06-07 01:53:05 +04:00
42636941e8 поправила откуда не возьмись какую-то проблему 2025-06-06 23:53:45 +04:00
81b4238394 что-то непонятное я наделала с отчетом по контингенту. нужно пересмотреть смысОл этого всего 2025-06-05 01:44:33 +04:00
d2f61d8cb5 вроде с моделями просто закончила начальную писанину 2025-06-04 18:12:11 +04:00
aaa4d26a3b создала начальные классы-модели. но еще не все 2025-06-04 15:51:55 +04:00
216 changed files with 9046 additions and 12426 deletions

View File

@@ -1,16 +0,0 @@
# .env
MYSQL_DATABASE=app
MYSQL_USER=appuser
MYSQL_PASSWORD=secret
MYSQL_ROOT_PASSWORD=supersecret
DB_HOST=db
DB_PORT=3306
DB_DRIVER=aiomysql
DB_NAME=app
DB_USER=appuser
DB_PASSWORD=secret
SECRET_KEY=supersecretkey
ALGORITHM=HS256

1
.gitignore vendored
View File

@@ -1 +0,0 @@
.env

View File

@@ -1,20 +0,0 @@
up:
docker compose up -d
down:
docker compose down
remove:
docker compose down -v
restart:
docker compose restart
logs:
docker compose logs
monitor:
docker compose logs -f backend
build:
docker compose build

View File

@@ -1,90 +0,0 @@
# Git
.git
.gitignore
.gitattributes
# CI
.codeclimate.yml
.travis.yml
.taskcluster.yml
# Docker
docker-compose.y[a]ml
compose.y[a]ml
Dockerfile
.docker
.dockerignore
# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# 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/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Virtual environment
.env
.venv/
venv/
# PyCharm
.idea
# Python mode for VIM
.ropeproject
**/.ropeproject
# Vim swap files
**/*.swp
# VS Code
.vscode/

View File

@@ -1,9 +0,0 @@
DB_HOST=db
DB_PORT=3306
DB_DRIVER=mysql+aiomysql
DB_NAME=app
DB_USER=appuser
DB_PASSWORD=secret
SECRET_KEY=gV64m9aIzFG4qpgVphvQbPQrtAO0nM-7YwwOvu0XPt5KJOjAy4AfgLkqJXYEt
ALGORITHM=HS256

View File

@@ -1,2 +1,2 @@
[flake8]
max-line-length=110
max-line-length = 101

16
backend/.gitignore vendored
View File

@@ -106,7 +106,6 @@ ipython_config.py
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
@@ -168,19 +167,6 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
@@ -188,7 +174,7 @@ cython_debug/
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# Cursor is an AI-powered code editor.`.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore

View File

@@ -1,8 +1,24 @@
FROM python:3.12.3-slim
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Установка зависимостей для MySQL
RUN apt-get update && apt-get install -y \
default-libmysqlclient-dev \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Установка Poetry
RUN pip install poetry
# Копируем зависимости
COPY pyproject.toml poetry.lock* ./
# Устанавливаем зависимости
RUN poetry config virtualenvs.create false && \
poetry install --no-root --no-interaction --no-ansi
# Копируем остальные файлы
COPY . .
ENTRYPOINT ["/app/entrypoint.sh"]
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,17 +0,0 @@
up:
docker compose up -d
down:
docker compose down
remove:
docker compose down -v
dev:
uvicorn app.main:app --reload
makemigration:
alembic revision --autogenerate
migrate:
alembic upgrade head

View File

@@ -1,216 +1 @@
# Шаблон приложения FastAPI с аутентификацией и авторизацией
Этот проект представляет собой готовый шаблон для разработки масштабируемых веб-приложений на основе **FastAPI** с
полноценной системой аутентификации и авторизации. Проект включает модульную архитектуру, поддерживает гибкое
логирование с **loguru**, и взаимодействие с базой данных через **SQLAlchemy** с асинхронной поддержкой. Система
миграций **Alembic** упрощает работу со схемой базы данных.
## Стек технологий
- **Веб-фреймворк**: FastAPI
- **ORM**: SQLAlchemy с асинхронной поддержкой через aiosqlite
- **База данных**: SQLite (легко заменяемая на другую SQL-СУБД)
- **Система миграций**: Alembic
- **Авторизация/Аутентификация**: bcrypt для хеширования паролей, python-jose для защиты данных с использованием JWT
## Зависимости проекта
- `fastapi[all]==0.115.0` - высокопроизводительный веб-фреймворк
- `pydantic==2.9.2` - валидация данных
- `uvicorn==0.31.0` - ASGI-сервер
- `jinja2==3.1.4` - шаблонизатор
- `SQLAlchemy==2.0.35` - ORM для работы с базами данных
- `aiosqlite==0.20.0` - асинхронная поддержка SQLite
- `alembic==1.13.3` - управление миграциями базы данных
- `bcrypt==4.0.1` и `passlib[bcrypt]==1.7.4` - хеширование паролей
- `python-jose==3.3.0` - работа с JWT токенами
- `loguru==0.7.2` - красивое и удобное логирование
## Структура проекта
Проект построен с учётом модульной архитектуры, что позволяет легко расширять приложение и упрощает его поддержку.
Каждый модуль отвечает за отдельные задачи, такие как авторизация или управление данными.
### Основная структура проекта
```
├── app/
│ ├── auth/ # Модуль авторизации и аутентификации
│ │ ├── dao.py # Data Access Object для работы с БД
│ │ ├── models.py # Модели данных для авторизации
│ │ ├── router.py # Роутеры FastAPI для маршрутизации
│ │ ├── schemas.py # Схемы для валидации данных
│ │ └── utils.py # Вспомогательные функции для авторизации
│ ├── dao/ # Общие DAO для приложения
│ │ ├── database.py # Подключение к базе данных и управление сессиями
│ │ └── base.py # Базовый класс DAO для работы с БД
│ ├── dependencies # Зависимости в проекте
│ │ ├── auth_dep.py # Зависимости для авторизации
│ │ └── dao_dep.py # Зависимости для сессий SQLAlchemy
│ ├── migration/ # Миграции базы данных
│ │ ├── versions/ # Файлы миграций
│ │ ├── env.py # Настройки среды для Alembic
│ │ ├── README # Документация по миграциям
│ │ └── script.py.mako # Шаблон для генерации миграций
│ ├── static/ # Статические файлы приложения
│ │ └── .gitkeep # Пустой файл для сохранения папки в Git
│ ├── config.py # Конфигурация приложения
│ ├── exceptions.py # Исключения для обработки ошибок
│ ├── main.py # Основной файл для запуска приложения
├── data/ # Папка для хранения файла БД
│ └── db.sqlite3 # Файл базы данных SQLite
├── .env # Конфигурация окружения
├── alembic.ini # Конфигурация Alembic
├── README.md # Документация проекта
└── requirements.txt # Зависимости проекта
```
Обновленный раздел с подробным описанием основных модулей:
---
### Основные модули
#### **app/auth** - Модуль для аутентификации и авторизации
Модуль отвечает за управление процессами аутентификации (входа пользователей) и авторизации (проверки доступа).
Основные файлы:
- **`dao.py`**: Объект доступа к данным пользователей. Содержит методы для работы с базой данных (создание, обновление,
поиск пользователей и т. д.).
- **`dependencies.py`**: Внедрение зависимостей, таких как проверка токенов и авторизация для защищённых маршрутов.
- **`models.py`**: Определяет ORM-модели данных для пользователей (например, таблица Users в базе данных).
- **`router.py`**: Роутер для маршрутизации запросов, связанных с аутентификацией. Определяет эндпоинты для входа,
регистрации и проверки доступа.
- **`schemas.py`**: Определяет Pydantic-схемы для валидации входных данных и структуры ответов (например, формат данных
для регистрации пользователей).
- **`utils.py`**: Вспомогательные функции для работы с токенами (создание, проверка JWT) и шифрование паролей.
---
#### **app/dao** - Базовый слой доступа к данным (Data Access Layer)
Модуль содержит абстракции для работы с базой данных. Используется для управления подключениями и реализации
CRUD-операций.
- **`base.py`**: Базовый класс DAO, предоставляющий общие методы для работы с базой данных, такие как добавление,
обновление, удаление и поиск записей.
- **`database.py`**: Отвечает за подключение к базе данных, управление сессиями SQLAlchemy, а также создание
асинхронного подключения (например, через `aiosqlite`).
---
#### **app/migration** - Управление миграциями базы данных с Alembic
Модуль упрощает управление схемой базы данных и позволяет безопасно вносить изменения.
- **`versions/`**: Хранятся файлы миграций, автоматически создаваемые Alembic.
- **`env.py`**: Основной файл конфигурации для Alembic. Определяет подключение к базе данных и взаимодействие с ORM.
- **`script.py.mako`**: Шаблон для генерации новых файлов миграций.
---
#### **app/dependencies** - Управление миграциями базы данных с Alembic
Модуль содержит зависимости, которые используются в проекте.
- **`auth_dep.py`**: Зависимости, связанные с авторизацией пользователя в системе
- **`dao_dep.py`**: Зависимости, связанные с управллением сессией SQLAlchemy и с работой с дочерними классами BaseDao
---
#### **config.py** - Настройки и конфигурация приложения
- Определяет параметры приложения, загружаемые из файла `.env`. Например:
- `SECRET_KEY`: Секретный ключ для подписания JWT.
- `ALGORITHM`: Алгоритм хеширования токенов.
- `DATABASE_URL`: URL для подключения к базе данных.
- Обеспечивает удобное управление конфигурацией для разных окружений (локальное, тестовое, продакшн).
---
#### **main.py** - Основной файл для запуска приложения
- **Инициализация приложения**: Настраивает FastAPI-приложение, включая параметры, такие как название, версия, и
описание.
- **Подключение роутеров**: Регистрирует маршруты, определённые в модулях приложения, например:
- `app.auth.router` для маршрутов авторизации.
- Любые дополнительные модули (например, `app.users.router`).
- **Настройка зависимостей**: Внедряет глобальные зависимости, такие как подключение к базе данных или параметры
конфигурации.
- **Настройка middleware**: Добавляет промежуточные слои для обработки запросов (например, CORS, сжатие, обработка
ошибок).
- **Обработка ошибок**: Определяет глобальные обработчики исключений, чтобы возвращать понятные ответы при возникновении
ошибок (например, 401 Unauthorized или 500 Internal Server Error).
- **Запуск сервера**: Используется для старта приложения с помощью ASGI-сервера (Uvicorn).
## Настройка аутентификации и авторизации
Для аутентификации используется JSON Web Token (JWT) с bcrypt для хеширования паролей и python-jose для генерации и
проверки токенов. Это обеспечивает безопасное хранение данных и защищает API-эндпоинты.
## Запуск приложения
1. Клонируйте репозиторий:
```bash
git clone https://github.com/Yakvenalex/FastApiWithAuthSample.git .
```
2. Установите зависимости:
```bash
pip install -r requirements.txt
```
3. Создайте и настройте `.env` файл:
```env
SECRET_KEY=supersecretkey
ALGORITHM=HS256
```
4. Запустите приложение с Uvicorn:
```bash
uvicorn app.main:app --reload --port 8005
```
При необходимости замените port на нужный.
## Миграции базы данных
1. Инициализируйте Alembic:
```bash
cd app
alembic init -t async migration
```
Затем переместите `alembic.ini` в корень проекта.
2. В `alembic.ini` установите `script_location` как `app/migration`.
3. Создайте миграцию:
```bash
alembic revision --autogenerate -m "Initial migration"
```
4. Примените миграции:
```bash
alembic upgrade head
```
## Лучшие практики
- Разделяйте функциональность приложения на модули для удобства тестирования и поддержки.
- Обрабатывайте ошибки с четкими ответами и HTTP-кодами.
- Проводите миграции с Alembic для управления схемой базы данных.
- Используйте переменные окружения для безопасного хранения конфиденциальных данных.
---
Этот шаблон является мощной и удобной основой для разработки приложений на FastAPI с поддержкой аутентификации,
авторизации и структурированной архитектуры, готовой к масштабированию.
# Бекенд

View File

@@ -1,84 +1,8 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = app/migration
script_location = alembic
; sqlalchemy.url = mysql+pymysql://myappuser:mypassword@db/myapp
sqlalchemy.url = mysql+pymysql://myappuser:mypassword@localhost/myapp
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migration/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migration/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

1
backend/alembic/README Normal file
View File

@@ -0,0 +1 @@
Generic single-database configuration.

52
backend/alembic/env.py Normal file
View File

@@ -0,0 +1,52 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
import os
import sys
# Добавляем путь к проекту
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app.models import Base
from app.config import settings
config = context.config
config.set_main_option("sqlalchemy.url", settings.database_url)
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -19,8 +19,10 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,46 @@
"""create users
Revision ID: 40906c8e083f
Revises:
Create Date: 2025-05-18 18:11:23.585211
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '40906c8e083f'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=255), nullable=True),
sa.Column('username', sa.String(length=50), nullable=True),
sa.Column('hashed_password', sa.String(length=255), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
# ### end Alembic commands ###

View File

@@ -1 +0,0 @@
from .router import router

View File

@@ -1,72 +0,0 @@
from fastapi import APIRouter, Response, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.utils import authenticate_user, set_tokens
from app.dao import UsersDAO
from app.dependencies.auth_dep import (
get_current_user,
check_refresh_token,
)
from app.dependencies.dao_dep import get_session_with_commit, get_session_without_commit
from app.exceptions import UserAlreadyExistsException, IncorrectEmailOrPasswordException
from app.models import User
from app.schemas import UserCreateSchema, UserAuthSchema, EmailSchema, UserAddDBSchema, UserSchema
router = APIRouter()
@router.post("/register")
async def register_user(
user_data: UserCreateSchema, session: AsyncSession = Depends(get_session_with_commit)
) -> dict:
# Проверка существования пользователя
user_dao = UsersDAO(session)
existing_user = await user_dao.find_one_or_none(
filters=EmailSchema(email=user_data.email)
)
if existing_user:
raise UserAlreadyExistsException
# Подготовка данных для добавления
user_data_dict = user_data.model_dump()
# Добавление пользователя
await user_dao.add(values=UserAddDBSchema(**user_data_dict))
return {"message": "Вы успешно зарегистрированы!"}
@router.post("/login")
async def auth_user(
response: Response,
user_data: UserAuthSchema,
session: AsyncSession = Depends(get_session_without_commit),
) -> dict:
users_dao = UsersDAO(session)
user = await users_dao.find_one_or_none(filters=EmailSchema(email=user_data.email))
if not (user and await authenticate_user(user=user, password=user_data.password)):
raise IncorrectEmailOrPasswordException
set_tokens(response, user.id)
return {"message": "Авторизация успешна!"}
@router.post("/logout")
async def logout(response: Response):
response.delete_cookie("user_access_token")
response.delete_cookie("user_refresh_token")
return {"message": "Пользователь успешно вышел из системы"}
@router.get("/me")
async def get_me(user_data: User = Depends(get_current_user)) -> UserSchema:
return UserSchema.model_validate(user_data)
@router.post("/refresh")
async def process_refresh_token(
response: Response, user: User = Depends(check_refresh_token)
):
set_tokens(response, user.id)
return {"message": "Токены успешно обновлены"}

View File

@@ -1,70 +0,0 @@
from passlib.context import CryptContext
from jose import jwt
from datetime import datetime, timedelta, timezone
from fastapi.responses import Response
from app.config import settings
def create_tokens(data: dict) -> dict:
# Текущее время в UTC
now = datetime.now(timezone.utc)
# AccessToken - 30 минут
access_expire = now + timedelta(minutes=30)
access_payload = data.copy()
access_payload.update({"exp": int(access_expire.timestamp()), "type": "access"})
access_token = jwt.encode(
access_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
# RefreshToken - 7 дней
refresh_expire = now + timedelta(days=7)
refresh_payload = data.copy()
refresh_payload.update({"exp": int(refresh_expire.timestamp()), "type": "refresh"})
refresh_token = jwt.encode(
refresh_payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
return {"access_token": access_token, "refresh_token": refresh_token}
async def authenticate_user(user, password):
if (
not user
or verify_password(plain_password=password, hashed_password=user.password)
is False
):
return None
return user
def set_tokens(response: Response, user_id: int):
new_tokens = create_tokens(data={"sub": str(user_id)})
access_token = new_tokens.get("access_token")
refresh_token = new_tokens.get("refresh_token")
response.set_cookie(
key="user_access_token",
value=access_token,
httponly=True,
secure=True,
samesite="lax",
)
response.set_cookie(
key="user_refresh_token",
value=refresh_token,
httponly=True,
secure=True,
samesite="lax",
)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)

View File

@@ -1,24 +1,14 @@
import os
from pydantic_settings import BaseSettings
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if os.path.exists(os.path.join(BASE_DIR, ".env")):
import dotenv
dotenv.load_dotenv(os.path.join(BASE_DIR, ".env"))
class Settings(BaseSettings):
BASE_DIR: str = BASE_DIR
DB_HOST: str = os.getenv("DB_HOST")
DB_PORT: str = os.getenv("DB_PORT")
DB_DRIVER: str = os.getenv("DB_DRIVER")
DB_NAME: str = os.getenv("DB_NAME")
DB_USER: str = os.getenv("DB_USER")
DB_PASSWORD: str = os.getenv("DB_PASSWORD")
SECRET_KEY: str = os.getenv("SECRET_KEY")
ALGORITHM: str = os.getenv("ALGORITHM")
database_url: str = "mysql+pymysql://myappuser:mypassword@localhost/myapp"
secret_key: str = "your-secret-key"
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
class Config:
env_file = ".env"
# Получаем параметры для загрузки переменных среды
settings = Settings()
database_url = f"{settings.DB_DRIVER}://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}/{settings.DB_NAME}"

View File

@@ -1,3 +0,0 @@
from .database import async_session_maker
from .user import UsersDAO

View File

@@ -1,176 +0,0 @@
from typing import List, TypeVar, Generic, Type
from loguru import logger
from pydantic import BaseModel
from sqlalchemy import update as sqlalchemy_update, delete as sqlalchemy_delete, func
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.models import Model
T = TypeVar("T", bound=Model)
class BaseDAO(Generic[T]):
model: Type[T] = None
def __init__(self, session: AsyncSession):
self._session = session
if self.model is None:
raise ValueError("Модель должна быть указана в дочернем классе")
async def find_one_or_none_by_id(self, data_id: int):
try:
query = select(self.model).filter_by(id=data_id)
result = await self._session.execute(query)
record = result.scalar_one_or_none()
log_message = (
f"Запись {self.model.__name__} с ID {data_id} "
+ "{'найдена' if record else 'не найдена'}."
)
logger.info(log_message)
return record
except SQLAlchemyError as e:
logger.error(f"Ошибка при поиске записи с ID {data_id}: {e}")
raise
async def find_one_or_none(self, filters: BaseModel):
filter_dict = filters.model_dump(exclude_unset=True)
logger.info(
f"Поиск одной записи {self.model.__name__} по фильтрам: {filter_dict}"
)
try:
query = select(self.model).filter_by(**filter_dict)
result = await self._session.execute(query)
record = result.scalar_one_or_none()
log_message = f"Запись {'найдена' if record else 'не найдена'} по фильтрам: {filter_dict}"
logger.info(log_message)
return record
except SQLAlchemyError as e:
logger.error(f"Ошибка при поиске записи по фильтрам {filter_dict}: {e}")
raise
async def find_all(self, filters: BaseModel | None = None):
filter_dict = filters.model_dump(exclude_unset=True) if filters else {}
logger.info(
f"Поиск всех записей {self.model.__name__} по фильтрам: {filter_dict}"
)
try:
query = select(self.model).filter_by(**filter_dict)
result = await self._session.execute(query)
records = result.scalars().all()
logger.info(f"Найдено {len(records)} записей.")
return records
except SQLAlchemyError as e:
logger.error(
f"Ошибка при поиске всех записей по фильтрам {filter_dict}: {e}"
)
raise
async def add(self, values: BaseModel):
values_dict = values.model_dump(exclude_unset=True)
logger.info(
f"Добавление записи {self.model.__name__} с параметрами: {values_dict}"
)
try:
new_instance = self.model(**values_dict)
self._session.add(new_instance)
logger.info(f"Запись {self.model.__name__} успешно добавлена.")
await self._session.flush()
return new_instance
except SQLAlchemyError as e:
logger.error(f"Ошибка при добавлении записи: {e}")
raise
async def add_many(self, instances: List[BaseModel]):
values_list = [item.model_dump(exclude_unset=True) for item in instances]
logger.info(
f"Добавление нескольких записей {self.model.__name__}. Количество: {len(values_list)}"
)
try:
new_instances = [self.model(**values) for values in values_list]
self._session.add_all(new_instances)
logger.info(f"Успешно добавлено {len(new_instances)} записей.")
await self._session.flush()
return new_instances
except SQLAlchemyError as e:
logger.error(f"Ошибка при добавлении нескольких записей: {e}")
raise
async def update(self, filters: BaseModel, values: BaseModel):
filter_dict = filters.model_dump(exclude_unset=True)
values_dict = values.model_dump(exclude_unset=True)
logger.info(
f"Обновление записей {self.model.__name__} по фильтру: {filter_dict} с параметрами: {values_dict}"
)
try:
query = (
sqlalchemy_update(self.model)
.where(*[getattr(self.model, k) == v for k, v in filter_dict.items()])
.values(**values_dict)
.execution_options(synchronize_session="fetch")
)
result = await self._session.execute(query)
logger.info(f"Обновлено {result.rowcount} записей.")
await self._session.flush()
return result.rowcount
except SQLAlchemyError as e:
logger.error(f"Ошибка при обновлении записей: {e}")
raise
async def delete(self, filters: BaseModel):
filter_dict = filters.model_dump(exclude_unset=True)
logger.info(f"Удаление записей {self.model.__name__} по фильтру: {filter_dict}")
if not filter_dict:
logger.error("Нужен хотя бы один фильтр для удаления.")
raise ValueError("Нужен хотя бы один фильтр для удаления.")
try:
query = sqlalchemy_delete(self.model).filter_by(**filter_dict)
result = await self._session.execute(query)
logger.info(f"Удалено {result.rowcount} записей.")
await self._session.flush()
return result.rowcount
except SQLAlchemyError as e:
logger.error(f"Ошибка при удалении записей: {e}")
raise
async def count(self, filters: BaseModel | None = None):
filter_dict = filters.model_dump(exclude_unset=True) if filters else {}
logger.info(
f"Подсчет количества записей {self.model.__name__} по фильтру: {filter_dict}"
)
try:
query = select(func.count(self.model.id)).filter_by(**filter_dict)
result = await self._session.execute(query)
count = result.scalar()
logger.info(f"Найдено {count} записей.")
return count
except SQLAlchemyError as e:
logger.error(f"Ошибка при подсчете записей: {e}")
raise
async def bulk_update(self, records: List[BaseModel]):
logger.info(f"Массовое обновление записей {self.model.__name__}")
try:
updated_count = 0
for record in records:
record_dict = record.model_dump(exclude_unset=True)
if "id" not in record_dict:
continue
update_data = {k: v for k, v in record_dict.items() if k != "id"}
stmt = (
sqlalchemy_update(self.model)
.filter_by(id=record_dict["id"])
.values(**update_data)
)
result = await self._session.execute(stmt)
updated_count += result.rowcount
logger.info(f"Обновлено {updated_count} записей")
await self._session.flush()
return updated_count
except SQLAlchemyError as e:
logger.error(f"Ошибка при массовом обновлении: {e}")
raise

View File

@@ -1,13 +0,0 @@
from sqlalchemy.ext.asyncio import (
async_sessionmaker,
create_async_engine,
AsyncSession,
)
from app.config import database_url
engine = create_async_engine(url=database_url)
async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)

View File

@@ -1,6 +0,0 @@
from app.models import User
from .base import BaseDAO
class UsersDAO(BaseDAO):
model = User

26
backend/app/database.py Normal file
View File

@@ -0,0 +1,26 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from .config import settings
SQLALCHEMY_DATABASE_URL = settings.database_url
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
pool_pre_ping=True,
pool_recycle=3600,
pool_size=10,
max_overflow=20,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -1,86 +0,0 @@
from datetime import datetime, timezone
from fastapi import Request, Depends
from jose import jwt, JWTError, ExpiredSignatureError
from sqlalchemy.ext.asyncio import AsyncSession
from app.dao import UsersDAO
from app.models import User
from app.config import settings
from app.dependencies.dao_dep import get_session_without_commit
from app.exceptions import (
TokenNoFound,
NoJwtException,
TokenExpiredException,
NoUserIdException,
ForbiddenException,
UserNotFoundException,
)
def get_access_token(request: Request) -> str:
"""Извлекаем access_token из кук."""
token = request.cookies.get("user_access_token")
if not token:
raise TokenNoFound
return token
def get_refresh_token(request: Request) -> str:
"""Извлекаем refresh_token из кук."""
token = request.cookies.get("user_refresh_token")
if not token:
raise TokenNoFound
return token
async def check_refresh_token(
token: str = Depends(get_refresh_token),
session: AsyncSession = Depends(get_session_without_commit),
) -> User:
"""Проверяем refresh_token и возвращаем пользователя."""
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
user_id = payload.get("sub")
if not user_id:
raise NoJwtException
user = await UsersDAO(session).find_one_or_none_by_id(data_id=int(user_id))
if not user:
raise NoJwtException
return user
except JWTError:
raise NoJwtException
async def get_current_user(
token: str = Depends(get_access_token),
session: AsyncSession = Depends(get_session_without_commit),
) -> User:
"""Проверяем access_token и возвращаем пользователя."""
try:
# Декодируем токен
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
except ExpiredSignatureError:
raise TokenExpiredException
except JWTError:
# Общая ошибка для токенов
raise NoJwtException
expire: str = payload.get("exp")
expire_time = datetime.fromtimestamp(int(expire), tz=timezone.utc)
if (not expire) or (expire_time < datetime.now(timezone.utc)):
raise TokenExpiredException
user_id: str = payload.get("sub")
if not user_id:
raise NoUserIdException
user = await UsersDAO(session).find_one_or_none_by_id(data_id=int(user_id))
if not user:
raise UserNotFoundException
return user

View File

@@ -1,30 +0,0 @@
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from app.dao import async_session_maker
async def get_session_with_commit() -> AsyncGenerator[AsyncSession, None]:
"""Асинхронная сессия с автоматическим коммитом."""
async with async_session_maker() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
async def get_session_without_commit() -> AsyncGenerator[AsyncSession, None]:
"""Асинхронная сессия без автоматического коммита."""
async with async_session_maker() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()

View File

@@ -1,58 +0,0 @@
from fastapi import status, HTTPException
# Пользователь уже существует
UserAlreadyExistsException = HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Пользователь уже существует"
)
# Пользователь не найден
UserNotFoundException = HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Пользователь не найден"
)
# Отсутствует идентификатор пользователя
UserIdNotFoundException = HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Отсутствует идентификатор пользователя",
)
# Неверная почта или пароль
IncorrectEmailOrPasswordException = HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Неверная почта или пароль"
)
# Токен истек
TokenExpiredException = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Токен истек"
)
# Некорректный формат токена
InvalidTokenFormatException = HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Некорректный формат токена"
)
# Токен отсутствует в заголовке
TokenNoFound = HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Токен отсутствует в заголовке"
)
# Невалидный JWT токен
NoJwtException = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Токен не валидный"
)
# Не найден ID пользователя
NoUserIdException = HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Не найден ID пользователя"
)
# Недостаточно прав
ForbiddenException = HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Недостаточно прав"
)
TokenInvalidFormatException = HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный формат токена. Ожидается 'Bearer <токен>'",
)

View File

@@ -1,67 +1,16 @@
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from loguru import logger
from . import models
from .database import engine
from app.routers import users_router, auth_router
from app.auth import router as auth_router
from app.routers import all_routers
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
app.include_router(users_router, prefix="/api/users", tags=["users"])
app.include_router(auth_router, prefix="/api/auth", tags=["auth"])
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[dict, None]:
"""Управление жизненным циклом приложения."""
logger.info("Инициализация приложения...")
yield
logger.info("Завершение работы приложения...")
def create_app() -> FastAPI:
"""
Создание и конфигурация FastAPI приложения.
Returns:
Сконфигурированное приложение FastAPI
"""
app = FastAPI(
title="Стартовая сборка FastAPI",
description=(
"Стартовая сборка с интегрированной SQLAlchemy 2 для разработки FastAPI приложений с продвинутой "
"архитектурой, включающей авторизацию, аутентификацию и управление ролями пользователей."
),
version="1.0.0",
lifespan=lifespan,
)
# Настройка CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Монтирование статических файлов
app.mount("/static", StaticFiles(directory="app/static"), name="static")
# Регистрация роутеров
register_routers(app)
return app
def register_routers(app: FastAPI) -> None:
"""Регистрация роутеров приложения."""
# Подключение роутеров
app.include_router(auth_router, prefix="/auth", tags=["Auth"])
for router in all_routers:
app.include_router(router)
# Создание экземпляра приложения
app = create_app()
@app.get("/")
def read_root():
return {"message": "Hello from FastAPI!"}

View File

@@ -1 +0,0 @@
Generic single-database configuration with an async dbapi.

17
backend/app/models.py Normal file
View File

@@ -0,0 +1,17 @@
from sqlalchemy import Column, Integer, String, Boolean
from sqlalchemy.sql.sqltypes import TIMESTAMP
from sqlalchemy.sql.expression import text
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(255), unique=True, index=True)
username = Column(String(50), unique=True, index=True)
hashed_password = Column(String(255))
is_active = Column(Boolean, default=True)
created_at = Column(
TIMESTAMP(timezone=True), nullable=False, server_default=text("now()")
)

View File

@@ -1,6 +0,0 @@
from .facult import Facult
from .grade_book import GradeBook
from .group import Group
from .model import Model
from .specialization import Specialization
from .user import User

View File

@@ -1,8 +0,0 @@
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from .model import Model
class Discipline(Model):
name: Mapped[str] = mapped_column(String(256), nullable=False, unique=True)

View File

@@ -1,10 +0,0 @@
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .model import Model
class Facult(Model):
name: Mapped[str] = mapped_column(String(256), nullable=False, unique=True)
dean_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
dean: Mapped["User"] = relationship("User", lazy="joined")

View File

@@ -1,29 +0,0 @@
import enum
from sqlalchemy import ForeignKey, Enum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .model import Model
class StudentStatus(enum.Enum):
STUDY = "Обучается"
ACADEM = "Академ"
EXPLUSION = "Отчислен"
GRADUATED = "Выпущен"
class GradeBook(Model):
student_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
student: Mapped["User"] = relationship("User", lazy="joined")
specialization_id: Mapped[int] = mapped_column(ForeignKey("specializations.id"), nullable=False)
specialization: Mapped["Specialization"] = relationship(
"Specialization", lazy="joined"
)
status: Mapped[StudentStatus] = mapped_column(
Enum(StudentStatus), nullable=False, default=StudentStatus.STUDY
)
group_id: Mapped[int] = mapped_column(ForeignKey("groups.id"))
group: Mapped["Group"] = relationship(
"Group", lazy="joined"
)

View File

@@ -1,15 +0,0 @@
from sqlalchemy import ForeignKey, Integer, CheckConstraint, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .model import Model
class Group(Model):
course: Mapped[int] = mapped_column(Integer, CheckConstraint("course > 0"), nullable=False)
specialization_id: Mapped[int] = mapped_column(ForeignKey("specializations.id"))
specialization: Mapped["Specialization"] = relationship(
"Specialization", lazy="joined"
)
max_students_count: Mapped[int] = mapped_column(Integer, CheckConstraint("max_students_count >= 0"), nullable=False)
number: Mapped[int] = mapped_column(Integer, CheckConstraint("number > 0"), nullable=False)
__table_args__ = (UniqueConstraint("course", "specialization_id", "number", name="_spec_parallel_groups_unique"),)

View File

@@ -1,49 +0,0 @@
import uuid
from datetime import datetime
from decimal import Decimal
from sqlalchemy import Integer, inspect
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, declared_attr
class Model(AsyncAttrs, DeclarativeBase):
__abstract__ = True
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower() + "s"
def to_dict(self, exclude_none: bool = False):
"""
Преобразует объект модели в словарь.
Args:
exclude_none (bool): Исключать ли None значения из результата
Returns:
dict: Словарь с данными объекта
"""
result = {}
for column in inspect(self.__class__).columns:
value = getattr(self, column.key)
# Преобразование специальных типов данных
if isinstance(value, datetime):
value = value.isoformat()
elif isinstance(value, Decimal):
value = float(value)
elif isinstance(value, uuid.UUID):
value = str(value)
# Добавляем значение в результат
if not exclude_none or value is not None:
result[column.key] = value
return result
def __repr__(self) -> str:
"""Строковое представление объекта для удобства отладки."""
return f"<{self.__class__.__name__}(id={self.id})>"

View File

@@ -1,9 +0,0 @@
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from .model import Model
class Specialization(Model):
name: Mapped[str] = mapped_column(String(256), nullable=False, unique=True)
code: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)

View File

@@ -1,28 +0,0 @@
import enum
from sqlalchemy import String, Enum
from sqlalchemy.orm import Mapped, mapped_column
from .model import Model
class UserRole(enum.Enum):
GUEST = "Гость"
STUDENT = "Студент"
TEACHER = "Преподаватель"
DEAN = "Декан"
RECTOR = "Ректор"
ADMIN = "Админ"
class User(Model):
email: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
password: Mapped[str] = mapped_column(String(256), nullable=False)
name: Mapped[str] = mapped_column(String(50), nullable=False)
surname: Mapped[str] = mapped_column(String(50), nullable=False)
patronymic: Mapped[str | None] = mapped_column(
String(50), nullable=True, default=None
)
role: Mapped[UserRole] = mapped_column(
Enum(UserRole), nullable=False, default=UserRole.GUEST
)

View File

@@ -1,5 +1,4 @@
from .auth import router as auth_router
from .users import router as users_router
all_routers = [
users_router
]
__all__ = ["auth_router", "users_router"]

View File

@@ -0,0 +1,79 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from .. import schemas, models, database
from ..config import settings
router = APIRouter(tags=["auth"])
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def verify_password(plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode, settings.secret_key, algorithm=settings.algorithm
)
return encoded_jwt
async def get_current_user(
token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
token, settings.secret_key, algorithms=[settings.algorithm]
)
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = schemas.TokenData(email=email)
except JWTError:
raise credentials_exception
user = db.query(models.User).filter(models.User.email == token_data.email).first()
if user is None:
raise credentials_exception
return user
@router.post("/token", response_model=schemas.Token)
async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(database.get_db),
):
user = db.query(models.User).filter(models.User.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
access_token = create_access_token(
data={"sub": user.email}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

View File

@@ -1,58 +1,39 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.dao import UsersDAO
from app.dependencies.dao_dep import get_session_with_commit, get_session_without_commit
from app.exceptions import UserAlreadyExistsException
from app.schemas import UserSchema, EmailSchema, UserAddDBSchema
from . import auth
from .. import schemas, models, database
router = APIRouter(prefix="/users", tags=["Users"])
router = APIRouter()
@router.get("/{user_id}")
async def get_user_by_id(user_id: int, session: AsyncSession = Depends(get_session_without_commit)) -> UserSchema:
user = await UsersDAO(session).find_one_or_none_by_id(data_id=user_id)
if not user:
raise HTTPException(status_code=404)
return UserSchema.model_validate(user)
@router.post("/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(database.get_db)):
db_user = db.query(models.User).filter(models.User.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
hashed_password = auth.get_password_hash(user.password)
db_user = models.User(email=user.email, hashed_password=hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.get("")
async def get_all_users(session: AsyncSession = Depends(get_session_without_commit)) -> list[UserSchema]:
users_dao = UsersDAO(session)
users = await users_dao.find_all()
return [UserSchema.model_validate(user) for user in users]
@router.post("")
async def create_user(user_data, session: AsyncSession = Depends(get_session_with_commit)) -> dict:
# Проверка существования пользователя
user_dao = UsersDAO(session)
existing_user = await user_dao.find_one_or_none(
filters=EmailSchema(email=user_data.email)
@router.post("/token", response_model=schemas.Token)
def login_for_access_token(
user: schemas.UserCreate, db: Session = Depends(database.get_db)
):
db_user = db.query(models.User).filter(models.User.email == user.email).first()
if not db_user or not auth.verify_password(user.password, db_user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if existing_user:
raise UserAlreadyExistsException
# Подготовка данных для добавления
user_data_dict = user_data.model_dump()
user_data_dict.pop("confirm_password", None)
# Добавление пользователя
await user_dao.add(values=UserAddDBSchema(**user_data_dict))
return {"message": "Вы успешно зарегистрированы!"}
@router.put("/{user_id}")
async def update_user(user_id: int, user_data, session: AsyncSession = Depends(get_session_with_commit)):
pass
@router.delete("/{user_id}")
async def delete_user(user_id: int, session: AsyncSession = Depends(get_session_with_commit)):
pass
access_token = auth.create_access_token(
data={"sub": db_user.email},
expires_delta=timedelta(minutes=auth.ACCESS_TOKEN_EXPIRE_MINUTES),
)
return {"access_token": access_token, "token_type": "bearer"}

26
backend/app/schemas.py Normal file
View File

@@ -0,0 +1,26 @@
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
email: EmailStr
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
class Config:
orm_mode = True
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
email: str | None = None

View File

@@ -1,2 +0,0 @@
from .general import EmailSchema
from .user import UserBaseSchema, UserSchema, UserAddDBSchema, UserCreateSchema, UserAuthSchema

View File

@@ -1,27 +0,0 @@
from typing import Self
from pydantic import (
BaseModel,
ConfigDict,
EmailStr,
Field, model_validator,
)
from app.auth.utils import get_password_hash
class EmailSchema(BaseModel):
email: EmailStr = Field(description="Электронная почта")
model_config = ConfigDict(from_attributes=True)
class PasswordSchema(BaseModel):
password: str = Field(
min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков"
)
model_config = ConfigDict(from_attributes=True)
@model_validator(mode="after")
def check_password(self) -> Self:
self.password = get_password_hash(self.password)
return self

View File

@@ -1,40 +0,0 @@
from pydantic import (
Field,
)
from app.models.user import UserRole
from .general import EmailSchema, PasswordSchema
class UserBaseSchema(EmailSchema):
name: str = Field(
min_length=3, max_length=50, description="Имя, от 3 до 50 символов"
)
surname: str = Field(
min_length=3, max_length=50, description="Фамилия, от 3 до 50 символов"
)
patronymic: str = Field(
min_length=3,
max_length=50,
description="Отчество, от 3 до 50 символов",
default=None,
)
role: UserRole = Field(description="Роль пользователя в системе", default=UserRole.GUEST)
class UserCreateSchema(PasswordSchema, UserBaseSchema):
pass
class UserAddDBSchema(UserBaseSchema):
password: str = Field(min_length=5, description="Пароль в формате HASH-строки")
class UserAuthSchema(EmailSchema):
password: str = Field(
min_length=5, max_length=50, description="Пароль, от 5 до 50 знаков"
)
class UserSchema(UserBaseSchema):
id: int = Field(description="Идентификатор пользователя")

View File

@@ -1,6 +0,0 @@
#!/bin/bash
set -e
alembic upgrade head
exec "$@"

7
backend/poetry.lock generated Normal file
View File

@@ -0,0 +1,7 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
package = []
[metadata]
lock-version = "2.0"
python-versions = "*"
content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8"

29
backend/pyproject.toml Normal file
View File

@@ -0,0 +1,29 @@
[project]
name = "backend"
version = "0.1.0"
description = ""
authors = [
{name = "Your Name",email = "you@example.com"}
]
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi[all] (>=0.115.12,<0.116.0)",
"sqlalchemy (>=2.0.41,<3.0.0)",
"pymysql (>=1.1.1,<2.0.0)",
"passlib (>=1.7.4,<2.0.0)",
"python-jose (>=3.4.0,<4.0.0)",
"pydantic (>=2.11.4,<3.0.0)",
"alembic (>=1.15.2,<2.0.0)",
"dotenv (>=0.9.9,<0.10.0)",
"pydantic-settings (>=2.9.1,<3.0.0)",
"cryptography (>=45.0.2,<46.0.0)",
"bcrypt (>=4.3.0,<5.0.0)"
]
[tool.poetry]
package-mode = false
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,58 +0,0 @@
aiomysql==0.2.0
aiosqlite==0.20.0
alembic==1.13.3
annotated-types==0.7.0
anyio==4.9.0
bcrypt==4.0.1
certifi==2025.4.26
cffi==1.17.1
click==8.2.1
cryptography==45.0.3
dnspython==2.7.0
ecdsa==0.19.1
email_validator==2.2.0
fastapi==0.115.0
fastapi-cli==0.0.7
greenlet==3.2.3
h11==0.16.0
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.4
loguru==0.7.2
Mako==1.3.10
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mdurl==0.1.2
mysql-connector-python==9.3.0
orjson==3.10.18
passlib==1.7.4
pyasn1==0.6.1
pycparser==2.22
pydantic==2.9.2
pydantic-extra-types==2.10.5
pydantic-settings==2.5.2
pydantic_core==2.23.4
Pygments==2.19.1
PyMySQL==1.1.1
python-dotenv==1.1.0
python-jose==3.3.0
python-multipart==0.0.20
PyYAML==6.0.2
rich==14.0.0
rich-toolkit==0.14.7
rsa==4.9.1
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.35
starlette==0.38.6
typer==0.16.0
typing_extensions==4.14.0
ujson==5.10.0
uvicorn==0.31.0
# uvloop==0.21.0
watchfiles==1.0.5
websockets==15.0.1

View File

@@ -2,35 +2,28 @@ services:
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
MYSQL_USER: ${MYSQL_USER:-appuser}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-secret}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-supersecret}
MYSQL_DATABASE: myapp
MYSQL_USER: myappuser
MYSQL_PASSWORD: mypassword
MYSQL_ROOT_PASSWORD: rootpassword
ports:
- 127.0.0.1:3306:3306
- "127.0.0.1:3306:3306"
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 5s
retries: 10
backend:
build: ./backend
ports:
- 127.0.0.1:8000:8000
- "127.0.0.1:8000:8000"
environment:
DB_HOST: ${DB_HOST:-db}
DB_PORT: ${DB_PORT:-3306}
DB_DRIVER: ${DB_DRIVER:-mysql}
DB_NAME: ${DB_NAME:-app}
DB_USER: ${DB_USER:-appuser}
DB_PASSWORD: ${DB_PASSWORD:-secret}
SECRET_KEY: ${SECRET_KEY:-supersecretkey}
ALGORITHM: ${ALGORITHM:-HS256}
DATABASE_URL: mysql+pymysql://myappuser:mypassword@db/myapp
depends_on:
- db
adminer:
image: adminer:latest
ports:
- 127.0.0.1:8080:8080
db:
condition: service_healthy
volumes:
mysql_data:

400
contingent_report_desktop/.gitignore vendored Normal file
View File

@@ -0,0 +1,400 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Controller\Controller.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.9.34714.143
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App_contingent_university", "App_contingent_university.csproj", "{4AAF178E-2E63-49C7-A054-1CF8618B9DB1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataModels", "..\DataModels\DataModels.csproj", "{9C5F04AA-81FA-4EDD-88D7-8A774DAF58C1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controller", "..\Controller\Controller.csproj", "{ABF0591E-2C5D-426A-B542-30A89162E1A8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4AAF178E-2E63-49C7-A054-1CF8618B9DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4AAF178E-2E63-49C7-A054-1CF8618B9DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4AAF178E-2E63-49C7-A054-1CF8618B9DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4AAF178E-2E63-49C7-A054-1CF8618B9DB1}.Release|Any CPU.Build.0 = Release|Any CPU
{9C5F04AA-81FA-4EDD-88D7-8A774DAF58C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C5F04AA-81FA-4EDD-88D7-8A774DAF58C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C5F04AA-81FA-4EDD-88D7-8A774DAF58C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C5F04AA-81FA-4EDD-88D7-8A774DAF58C1}.Release|Any CPU.Build.0 = Release|Any CPU
{ABF0591E-2C5D-426A-B542-30A89162E1A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABF0591E-2C5D-426A-B542-30A89162E1A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABF0591E-2C5D-426A-B542-30A89162E1A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABF0591E-2C5D-426A-B542-30A89162E1A8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6C5452B1-7500-4C58-9F7A-340A4254757F}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,83 @@
namespace App_contingent_university.Forms
{
partial class FormAuth
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
buttonEntrance = new Button();
textBoxEmail = new TextBox();
textBoxPassword = new TextBox();
SuspendLayout();
//
// buttonEntrance
//
buttonEntrance.Location = new Point(144, 176);
buttonEntrance.Name = "buttonEntrance";
buttonEntrance.Size = new Size(94, 29);
buttonEntrance.TabIndex = 0;
buttonEntrance.Text = "Вход";
buttonEntrance.UseVisualStyleBackColor = true;
buttonEntrance.Click += buttonEntrance_Click;
//
// textBoxEmail
//
textBoxEmail.Anchor = AnchorStyles.Top;
textBoxEmail.Location = new Point(102, 36);
textBoxEmail.Name = "textBoxEmail";
textBoxEmail.Size = new Size(170, 27);
textBoxEmail.TabIndex = 1;
//
// textBoxPassword
//
textBoxPassword.Anchor = AnchorStyles.Top;
textBoxPassword.Location = new Point(104, 95);
textBoxPassword.Name = "textBoxPassword";
textBoxPassword.Size = new Size(170, 27);
textBoxPassword.TabIndex = 2;
//
// FormAuth
//
AutoScaleDimensions = new SizeF(8F, 20F);
AutoScaleMode = AutoScaleMode.Font;
BackColor = Color.CornflowerBlue;
ClientSize = new Size(378, 217);
Controls.Add(textBoxPassword);
Controls.Add(textBoxEmail);
Controls.Add(buttonEntrance);
Name = "FormAuth";
Text = "Авторизация";
ResumeLayout(false);
PerformLayout();
}
#endregion
private Button buttonEntrance;
private TextBox textBoxEmail;
private TextBox textBoxPassword;
}
}

View File

@@ -0,0 +1,66 @@
using Controller.Contracts;
using Controller.Repository;
using DataModels.Models;
using Microsoft.Extensions.DependencyInjection;
namespace App_contingent_university.Forms
{
public partial class FormAuth : Form
{
private readonly DeanRepository _deanRepository;
public FormAuth(DeanRepository deanRepository)
{
_deanRepository = deanRepository;
InitializeComponent();
textBoxEmail.PlaceholderText = "dean1@gmail.com";
textBoxPassword.PlaceholderText = "password123456";
textBoxEmail.Text = "dean1@gmail.com";
textBoxPassword.Text = "123456";
}
private async void buttonEntrance_Click(object sender, EventArgs e)
{
string email = textBoxEmail.Text;
string password = textBoxPassword.Text;
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password))
{
MessageBox.Show("Введите email и пароль", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
var search = new DeanSearch
{
Email = email,
Password = password
};
var deans = await _deanRepository.GetFilteredList(search);
var dean = deans.FirstOrDefault();
if (dean == null)
{
MessageBox.Show("Неверные учетные данные", "Ошибка авторизации",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
StaticSession.Login(dean.Id, dean.FacultId ?? 0);
// Устанавливаем результат и закрываем форму
DialogResult = DialogResult.OK;
Close(); // После этого управление вернется в Program.Main()
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка авторизации: {ex.Message}", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,176 @@
namespace App_contingent_university.Forms
{
partial class FormFilterStudents
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.buttonOK = new System.Windows.Forms.Button();
this.labelF = new System.Windows.Forms.Label();
this.textBoxLastName = new System.Windows.Forms.TextBox();
this.textBoxName = new System.Windows.Forms.TextBox();
this.labelI = new System.Windows.Forms.Label();
this.textBoxPatronimic = new System.Windows.Forms.TextBox();
this.labelPatronimic = new System.Windows.Forms.Label();
this.comboBoxSpec = new System.Windows.Forms.ComboBox();
this.labelSpec = new System.Windows.Forms.Label();
this.labelGroup = new System.Windows.Forms.Label();
this.comboBoxGroup = new System.Windows.Forms.ComboBox();
this.SuspendLayout();
//
// buttonOK
//
this.buttonOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.buttonOK.BackColor = System.Drawing.Color.Orange;
this.buttonOK.ForeColor = System.Drawing.Color.Black;
this.buttonOK.Location = new System.Drawing.Point(154, 284);
this.buttonOK.Name = "buttonOK";
this.buttonOK.Size = new System.Drawing.Size(113, 37);
this.buttonOK.TabIndex = 0;
this.buttonOK.Text = "Применить";
this.buttonOK.UseVisualStyleBackColor = false;
//
// labelF
//
this.labelF.AutoSize = true;
this.labelF.Location = new System.Drawing.Point(12, 26);
this.labelF.Name = "labelF";
this.labelF.Size = new System.Drawing.Size(66, 16);
this.labelF.TabIndex = 1;
this.labelF.Text = "Фамилия";
//
// textBoxLastName
//
this.textBoxLastName.Location = new System.Drawing.Point(107, 20);
this.textBoxLastName.Name = "textBoxLastName";
this.textBoxLastName.Size = new System.Drawing.Size(160, 22);
this.textBoxLastName.TabIndex = 2;
//
// textBoxName
//
this.textBoxName.Location = new System.Drawing.Point(107, 56);
this.textBoxName.Name = "textBoxName";
this.textBoxName.Size = new System.Drawing.Size(160, 22);
this.textBoxName.TabIndex = 4;
//
// labelI
//
this.labelI.AutoSize = true;
this.labelI.Location = new System.Drawing.Point(12, 62);
this.labelI.Name = "labelI";
this.labelI.Size = new System.Drawing.Size(33, 16);
this.labelI.TabIndex = 3;
this.labelI.Text = "Имя";
//
// textBoxPatronimic
//
this.textBoxPatronimic.Location = new System.Drawing.Point(107, 94);
this.textBoxPatronimic.Name = "textBoxPatronimic";
this.textBoxPatronimic.Size = new System.Drawing.Size(160, 22);
this.textBoxPatronimic.TabIndex = 6;
//
// labelPatronimic
//
this.labelPatronimic.AutoSize = true;
this.labelPatronimic.Location = new System.Drawing.Point(12, 100);
this.labelPatronimic.Name = "labelPatronimic";
this.labelPatronimic.Size = new System.Drawing.Size(70, 16);
this.labelPatronimic.TabIndex = 5;
this.labelPatronimic.Text = "Отчество";
//
// comboBoxSpec
//
this.comboBoxSpec.FormattingEnabled = true;
this.comboBoxSpec.Location = new System.Drawing.Point(123, 134);
this.comboBoxSpec.Name = "comboBoxSpec";
this.comboBoxSpec.Size = new System.Drawing.Size(145, 24);
this.comboBoxSpec.TabIndex = 7;
//
// labelSpec
//
this.labelSpec.AutoSize = true;
this.labelSpec.Location = new System.Drawing.Point(12, 142);
this.labelSpec.Name = "labelSpec";
this.labelSpec.Size = new System.Drawing.Size(97, 16);
this.labelSpec.TabIndex = 8;
this.labelSpec.Text = "Направление";
//
// labelGroup
//
this.labelGroup.AutoSize = true;
this.labelGroup.Location = new System.Drawing.Point(12, 183);
this.labelGroup.Name = "labelGroup";
this.labelGroup.Size = new System.Drawing.Size(54, 16);
this.labelGroup.TabIndex = 10;
this.labelGroup.Text = "Группа";
//
// comboBoxGroup
//
this.comboBoxGroup.FormattingEnabled = true;
this.comboBoxGroup.Location = new System.Drawing.Point(125, 180);
this.comboBoxGroup.Name = "comboBoxGroup";
this.comboBoxGroup.Size = new System.Drawing.Size(142, 24);
this.comboBoxGroup.TabIndex = 9;
//
// FormFilterStudents
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.LightSteelBlue;
this.ClientSize = new System.Drawing.Size(280, 333);
this.Controls.Add(this.labelGroup);
this.Controls.Add(this.comboBoxGroup);
this.Controls.Add(this.labelSpec);
this.Controls.Add(this.comboBoxSpec);
this.Controls.Add(this.textBoxPatronimic);
this.Controls.Add(this.labelPatronimic);
this.Controls.Add(this.textBoxName);
this.Controls.Add(this.labelI);
this.Controls.Add(this.textBoxLastName);
this.Controls.Add(this.labelF);
this.Controls.Add(this.buttonOK);
this.Name = "FormFilterStudents";
this.Text = "Фильтр";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button buttonOK;
private System.Windows.Forms.Label labelF;
private System.Windows.Forms.TextBox textBoxLastName;
private System.Windows.Forms.TextBox textBoxName;
private System.Windows.Forms.Label labelI;
private System.Windows.Forms.TextBox textBoxPatronimic;
private System.Windows.Forms.Label labelPatronimic;
private System.Windows.Forms.ComboBox comboBoxSpec;
private System.Windows.Forms.Label labelSpec;
private System.Windows.Forms.Label labelGroup;
private System.Windows.Forms.ComboBox comboBoxGroup;
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App_contingent_university.Forms
{
public partial class FormFilterStudents : Form
{
public FormFilterStudents()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,343 @@
namespace App_contingent_university.Forms
{
partial class FormMain
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
MainPages = new TabControl();
StudentsPage = new TabPage();
progressBarStudents = new ProgressBar();
dataGridViewStudents = new DataGridView();
menuStrip1 = new MenuStrip();
фильтрацияToolStripMenuItem = new ToolStripMenuItem();
SpecPage = new TabPage();
progressBarSpecs = new ProgressBar();
dataGridViewSpecs = new DataGridView();
menuStrip2 = new MenuStrip();
добавитьНаправлениеToolStripMenuItem = new ToolStripMenuItem();
GroupsPage = new TabPage();
progressBarGroups = new ProgressBar();
dataGridViewGroups = new DataGridView();
menuStrip3 = new MenuStrip();
toolStripMenuItem2 = new ToolStripMenuItem();
добавитьГруппуToolStripMenuItem = new ToolStripMenuItem();
OrdersPage = new TabPage();
menuStrip4 = new MenuStrip();
создатьПриказToolStripMenuItem = new ToolStripMenuItem();
создатьПриказToolStripMenuItem1 = new ToolStripMenuItem();
MainPages.SuspendLayout();
StudentsPage.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewStudents).BeginInit();
menuStrip1.SuspendLayout();
SpecPage.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewSpecs).BeginInit();
menuStrip2.SuspendLayout();
GroupsPage.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewGroups).BeginInit();
menuStrip3.SuspendLayout();
OrdersPage.SuspendLayout();
menuStrip4.SuspendLayout();
SuspendLayout();
//
// MainPages
//
MainPages.Controls.Add(StudentsPage);
MainPages.Controls.Add(SpecPage);
MainPages.Controls.Add(GroupsPage);
MainPages.Controls.Add(OrdersPage);
MainPages.Dock = DockStyle.Fill;
MainPages.Location = new Point(0, 0);
MainPages.Margin = new Padding(3, 4, 3, 4);
MainPages.Name = "MainPages";
MainPages.SelectedIndex = 0;
MainPages.Size = new Size(1082, 751);
MainPages.TabIndex = 0;
MainPages.SelectedIndexChanged += MainPages_SelectedIndexChanged;
MainPages.VisibleChanged += MainPages_SelectedIndexChanged;
MainPages.KeyDown += MainPages_KeyDown;
//
// StudentsPage
//
StudentsPage.Controls.Add(progressBarStudents);
StudentsPage.Controls.Add(dataGridViewStudents);
StudentsPage.Controls.Add(menuStrip1);
StudentsPage.Location = new Point(4, 29);
StudentsPage.Margin = new Padding(3, 4, 3, 4);
StudentsPage.Name = "StudentsPage";
StudentsPage.Padding = new Padding(3, 4, 3, 4);
StudentsPage.Size = new Size(1074, 718);
StudentsPage.TabIndex = 0;
StudentsPage.Text = "Студенты";
StudentsPage.UseVisualStyleBackColor = true;
//
// progressBarStudents
//
progressBarStudents.Location = new Point(475, 345);
progressBarStudents.Name = "progressBarStudents";
progressBarStudents.Size = new Size(125, 29);
progressBarStudents.TabIndex = 5;
//
// dataGridViewStudents
//
dataGridViewStudents.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
dataGridViewStudents.BackgroundColor = Color.AliceBlue;
dataGridViewStudents.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dataGridViewStudents.Dock = DockStyle.Fill;
dataGridViewStudents.Location = new Point(3, 32);
dataGridViewStudents.Margin = new Padding(3, 4, 3, 4);
dataGridViewStudents.Name = "dataGridViewStudents";
dataGridViewStudents.RowHeadersVisible = false;
dataGridViewStudents.RowHeadersWidth = 51;
dataGridViewStudents.RowTemplate.Height = 24;
dataGridViewStudents.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dataGridViewStudents.Size = new Size(1068, 682);
dataGridViewStudents.TabIndex = 2;
//
// menuStrip1
//
menuStrip1.ImageScalingSize = new Size(20, 20);
menuStrip1.Items.AddRange(new ToolStripItem[] { фильтрацияToolStripMenuItem });
menuStrip1.Location = new Point(3, 4);
menuStrip1.Name = "menuStrip1";
menuStrip1.Size = new Size(1068, 28);
menuStrip1.TabIndex = 3;
menuStrip1.Text = "menuStrip1";
//
// фильтрацияToolStripMenuItem
//
фильтрацияToolStripMenuItem.Name = "фильтрацияToolStripMenuItem";
фильтрацияToolStripMenuItem.Size = new Size(108, 24);
фильтрацияToolStripMenuItem.Text = "Фильтрация";
//
// SpecPage
//
SpecPage.Controls.Add(progressBarSpecs);
SpecPage.Controls.Add(dataGridViewSpecs);
SpecPage.Controls.Add(menuStrip2);
SpecPage.Location = new Point(4, 29);
SpecPage.Margin = new Padding(3, 4, 3, 4);
SpecPage.Name = "SpecPage";
SpecPage.Padding = new Padding(3, 4, 3, 4);
SpecPage.Size = new Size(1074, 718);
SpecPage.TabIndex = 1;
SpecPage.Text = "Направления";
SpecPage.UseVisualStyleBackColor = true;
//
// progressBarSpecs
//
progressBarSpecs.Location = new Point(475, 345);
progressBarSpecs.Name = "progressBarSpecs";
progressBarSpecs.Size = new Size(125, 29);
progressBarSpecs.TabIndex = 5;
//
// dataGridViewSpecs
//
dataGridViewSpecs.AllowUserToAddRows = false;
dataGridViewSpecs.AllowUserToDeleteRows = false;
dataGridViewSpecs.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
dataGridViewSpecs.BackgroundColor = Color.AliceBlue;
dataGridViewSpecs.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dataGridViewSpecs.Dock = DockStyle.Fill;
dataGridViewSpecs.Location = new Point(3, 32);
dataGridViewSpecs.Margin = new Padding(3, 4, 3, 4);
dataGridViewSpecs.Name = "dataGridViewSpecs";
dataGridViewSpecs.RowHeadersVisible = false;
dataGridViewSpecs.RowHeadersWidth = 51;
dataGridViewSpecs.RowTemplate.Height = 24;
dataGridViewSpecs.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dataGridViewSpecs.Size = new Size(1068, 682);
dataGridViewSpecs.TabIndex = 2;
dataGridViewSpecs.CellDoubleClick += dataGridViewSpecs_CellDoubleClick;
dataGridViewSpecs.KeyDown += DataGridViewSpecs_KeyDown;
//
// menuStrip2
//
menuStrip2.ImageScalingSize = new Size(20, 20);
menuStrip2.Items.AddRange(new ToolStripItem[] { добавитьНаправлениеToolStripMenuItem });
menuStrip2.Location = new Point(3, 4);
menuStrip2.Name = "menuStrip2";
menuStrip2.Size = new Size(1068, 28);
menuStrip2.TabIndex = 3;
menuStrip2.Text = "menuStrip2";
//
// добавитьНаправлениеToolStripMenuItem
//
добавитьНаправлениеToolStripMenuItem.Name = обавитьНаправлениеToolStripMenuItem";
добавитьНаправлениеToolStripMenuItem.Size = new Size(187, 24);
добавитьНаправлениеToolStripMenuItem.Text = "Добавить направление";
добавитьНаправлениеToolStripMenuItem.Click += добавитьНаправлениеToolStripMenuItem_Click;
//
// GroupsPage
//
GroupsPage.Controls.Add(progressBarGroups);
GroupsPage.Controls.Add(dataGridViewGroups);
GroupsPage.Controls.Add(menuStrip3);
GroupsPage.Location = new Point(4, 29);
GroupsPage.Margin = new Padding(3, 4, 3, 4);
GroupsPage.Name = "GroupsPage";
GroupsPage.Size = new Size(1074, 718);
GroupsPage.TabIndex = 2;
GroupsPage.Text = "Группы";
GroupsPage.UseVisualStyleBackColor = true;
//
// progressBarGroups
//
progressBarGroups.Location = new Point(475, 345);
progressBarGroups.Name = "progressBarGroups";
progressBarGroups.Size = new Size(125, 29);
progressBarGroups.TabIndex = 4;
//
// dataGridViewGroups
//
dataGridViewGroups.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
dataGridViewGroups.BackgroundColor = Color.AliceBlue;
dataGridViewGroups.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dataGridViewGroups.Dock = DockStyle.Fill;
dataGridViewGroups.Location = new Point(0, 28);
dataGridViewGroups.Margin = new Padding(3, 4, 3, 4);
dataGridViewGroups.Name = "dataGridViewGroups";
dataGridViewGroups.RowHeadersVisible = false;
dataGridViewGroups.RowHeadersWidth = 51;
dataGridViewGroups.RowTemplate.Height = 24;
dataGridViewGroups.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dataGridViewGroups.Size = new Size(1074, 690);
dataGridViewGroups.TabIndex = 2;
//
// menuStrip3
//
menuStrip3.ImageScalingSize = new Size(20, 20);
menuStrip3.Items.AddRange(new ToolStripItem[] { toolStripMenuItem2, добавитьГруппуToolStripMenuItem });
menuStrip3.Location = new Point(0, 0);
menuStrip3.Name = "menuStrip3";
menuStrip3.Size = new Size(1074, 28);
menuStrip3.TabIndex = 3;
menuStrip3.Text = "menuStrip3";
//
// toolStripMenuItem2
//
toolStripMenuItem2.Name = "toolStripMenuItem2";
toolStripMenuItem2.Size = new Size(108, 24);
toolStripMenuItem2.Text = "Фильтрация";
//
// добавитьГруппуToolStripMenuItem
//
добавитьГруппуToolStripMenuItem.Name = обавитьГруппуToolStripMenuItem";
добавитьГруппуToolStripMenuItem.Size = new Size(141, 24);
добавитьГруппуToolStripMenuItem.Text = "Добавить группу";
//
// OrdersPage
//
OrdersPage.BackColor = Color.LightSteelBlue;
OrdersPage.Controls.Add(menuStrip4);
OrdersPage.Location = new Point(4, 29);
OrdersPage.Margin = new Padding(3, 4, 3, 4);
OrdersPage.Name = "OrdersPage";
OrdersPage.Size = new Size(1074, 718);
OrdersPage.TabIndex = 3;
OrdersPage.Text = "Приказы";
//
// menuStrip4
//
menuStrip4.ImageScalingSize = new Size(20, 20);
menuStrip4.Items.AddRange(new ToolStripItem[] { создатьПриказToolStripMenuItem, создатьПриказToolStripMenuItem1 });
menuStrip4.Location = new Point(0, 0);
menuStrip4.Name = "menuStrip4";
menuStrip4.Size = new Size(1074, 28);
menuStrip4.TabIndex = 2;
menuStrip4.Text = "menuStrip4";
//
// создатьПриказToolStripMenuItem
//
создатьПриказToolStripMenuItem.Name = "создатьПриказToolStripMenuItem";
создатьПриказToolStripMenuItem.Size = new Size(108, 24);
создатьПриказToolStripMenuItem.Text = "Фильтрация";
//
// создатьПриказToolStripMenuItem1
//
создатьПриказToolStripMenuItem1.Name = "создатьПриказToolStripMenuItem1";
создатьПриказToolStripMenuItem1.Size = new Size(131, 24);
создатьПриказToolStripMenuItem1.Text = "Создать приказ";
//
// FormMain
//
AutoScaleDimensions = new SizeF(8F, 20F);
AutoScaleMode = AutoScaleMode.Font;
AutoSize = true;
ClientSize = new Size(1082, 751);
Controls.Add(MainPages);
Margin = new Padding(3, 4, 3, 4);
Name = "FormMain";
Text = "Главная";
KeyDown += FormMain_KeyDown;
MainPages.ResumeLayout(false);
StudentsPage.ResumeLayout(false);
StudentsPage.PerformLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewStudents).EndInit();
menuStrip1.ResumeLayout(false);
menuStrip1.PerformLayout();
SpecPage.ResumeLayout(false);
SpecPage.PerformLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewSpecs).EndInit();
menuStrip2.ResumeLayout(false);
menuStrip2.PerformLayout();
GroupsPage.ResumeLayout(false);
GroupsPage.PerformLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewGroups).EndInit();
menuStrip3.ResumeLayout(false);
menuStrip3.PerformLayout();
OrdersPage.ResumeLayout(false);
OrdersPage.PerformLayout();
menuStrip4.ResumeLayout(false);
menuStrip4.PerformLayout();
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TabControl MainPages;
private System.Windows.Forms.TabPage StudentsPage;
private System.Windows.Forms.TabPage SpecPage;
private System.Windows.Forms.TabPage GroupsPage;
private System.Windows.Forms.TabPage OrdersPage;
private System.Windows.Forms.DataGridView dataGridViewStudents;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem фильтрацияToolStripMenuItem;
private System.Windows.Forms.DataGridView dataGridViewSpecs;
private System.Windows.Forms.MenuStrip menuStrip2;
private System.Windows.Forms.DataGridView dataGridViewGroups;
private System.Windows.Forms.MenuStrip menuStrip3;
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem2;
private System.Windows.Forms.ToolStripMenuItem добавитьНаправлениеToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem добавитьГруппуToolStripMenuItem;
private ProgressBar progressBarGroups;
private MenuStrip menuStrip4;
private ToolStripMenuItem создатьПриказToolStripMenuItem;
private ToolStripMenuItem создатьПриказToolStripMenuItem1;
private ProgressBar progressBarStudents;
private ProgressBar progressBarSpecs;
}
}

View File

@@ -0,0 +1,340 @@
using Controller.BusinessLogic;
using Controller.Contracts;
using Controller.Repository;
using DataModels.Models;
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App_contingent_university.Forms
{
public partial class FormMain : Form
{
private readonly StudentLogic _studentLogic;
private readonly GroupLogic _groupLogic;
private readonly SpecializationLogic _specializationLogic;
private readonly OrderLogic _orderLogic;
private readonly StudentRepository studentRepository;
private readonly GroupRepository groupRepository;
private readonly SpecializationRepository specializationRepository;
private readonly OrderRepository orderRepository;
private readonly FacultReposirory facultReposirory;
private int? _id;
public int Id
{
set { _id = value; }
}
private string _currentSearchText = string.Empty;
private FormSearch _searchForm;
public FormMain(StudentLogic studentLogic, GroupLogic groupLogic, SpecializationLogic specializationLogic,
OrderLogic orderLogic,
StudentRepository studentRepository, GroupRepository groupRepository, SpecializationRepository specializationRepository,
OrderRepository orderRepository, FacultReposirory facultReposirory
)
{
_studentLogic = studentLogic;
_groupLogic = groupLogic;
_specializationLogic = specializationLogic;
_orderLogic = orderLogic;
this.studentRepository = studentRepository;
this.groupRepository = groupRepository;
this.specializationRepository = specializationRepository;
this.orderRepository = orderRepository;
this.facultReposirory = facultReposirory;
InitializeComponent();
dataGridViewSpecs.KeyDown += DataGridViewSpecs_KeyDown;
this.KeyPreview = true;
this.KeyDown += FormMain_KeyDown;
}
protected async override void OnShown(EventArgs e)
{
base.OnShown(e);
if (!StaticSession.IsLogin)
{
MessageBox.Show("Требуется авторизация");
Close();
return;
}
var facult = await facultReposirory.Get(StaticSession.CurrentFacultyId);
// Устанавливаем заголовок с информацией о пользователе
this.Text = $"Контингент университета (Факультет {facult.Name})";
// Загружаем данные
LoadInitialData();
}
private async void LoadInitialData()
{
await LoadSpecializationData();
await LoadGroupsData();
await LoadStudentsData();
}
private bool _studentsLoaded = false;
private bool _groupsLoaded = false;
private bool _specsLoaded = false;
private async void MainPages_SelectedIndexChanged(object sender, EventArgs e)
{
if (MainPages.SelectedTab == StudentsPage && !_studentsLoaded)
{
await LoadStudentsData();
_studentsLoaded = true;
}
else if (MainPages.SelectedTab == GroupsPage && !_groupsLoaded)
{
await LoadGroupsData();
_groupsLoaded = true;
}
else if (MainPages.SelectedTab == SpecPage && !_specsLoaded)
{
await LoadSpecializationData();
_specsLoaded = true;
}
else if (MainPages.SelectedTab == OrdersPage)
{
// Дополнительная логика для этой вкладки
}
}
private async Task ShowLoading(bool show, ProgressBar progressBar)
{
if (progressBar.InvokeRequired)
{
progressBar.Invoke(new Action(() => ShowLoading(show, progressBar)));
return;
}
progressBar.Visible = show;
progressBar.Style = show ? ProgressBarStyle.Marquee : ProgressBarStyle.Blocks;
if (show) progressBar.BringToFront();
}
private async Task LoadStudentsData()
{
await ShowLoading(true, progressBarStudents);
try
{
var studLogic = new StudentLogic(studentRepository, groupRepository, specializationRepository);
var students = await studLogic.GetViewModel();
dataGridViewStudents.DataSource = students;
dataGridViewStudents.AutoGenerateColumns = true;
}
finally
{
await ShowLoading(false, progressBarStudents);
}
}
private async void DataGridViewSpecs_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Delete && dataGridViewSpecs.SelectedRows.Count > 0)
{
// Получаем выбранную строку
var selectedRow = dataGridViewSpecs.SelectedRows[0];
var specId = (int)selectedRow.Cells["Id"].Value;
var specName = selectedRow.Cells["Name"].Value?.ToString() ?? "без названия";
// Запрос подтверждения
var result = MessageBox.Show(
$"Вы действительно хотите удалить направление '{specName}' (\nЭто действие нельзя отменить!",
"Подтверждение удаления",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning,
MessageBoxDefaultButton.Button2);
if (result == DialogResult.Yes)
{
try
{
await ShowLoading(true, progressBarSpecs);
// Удаляем специализацию
var deleted = await specializationRepository.Remove(specId);
if (deleted != null)
{
// Обновляем данные
await LoadSpecializationData();
MessageBox.Show("Направление успешно удалено", "Успех",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
MessageBox.Show("Не удалось удалить направление", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка при удалении: {ex.Message}", "Ошибка",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
await ShowLoading(false, progressBarSpecs);
}
}
}
}
private async Task LoadGroupsData()
{
await ShowLoading(true, progressBarGroups);
try
{
var groupLogic = new GroupLogic(specializationRepository, groupRepository);
var groups = await groupLogic.GetViewModel();
dataGridViewGroups.DataSource = groups;
dataGridViewGroups.AutoGenerateColumns = true;
}
finally
{
await ShowLoading(false, progressBarGroups);
}
}
private async Task LoadSpecializationData()
{
await ShowLoading(true, progressBarSpecs);
try
{
var byFacultModel = new SpecializationSearch
{
FacultId = StaticSession.CurrentFacultyId,
Name = _currentSearchText
};
var specLogic = new SpecializationLogic(specializationRepository);
var specs = await specLogic.GetViewModel(byFacultModel);
dataGridViewSpecs.DataSource = specs;
dataGridViewSpecs.AutoGenerateColumns = true;
}
finally
{
await ShowLoading(false, progressBarSpecs);
}
}
private void добавитьНаправлениеToolStripMenuItem_Click(object sender, EventArgs e)
{
if (dataGridViewSpecs.SelectedRows.Count > 0)
{
var service = Program.ServiceProvider?.GetService(typeof(FormAddUpdSpec));
if (service is FormAddUpdSpec form)
{
if (form.ShowDialog() == DialogResult.OK)
{
LoadSpecializationData();
form.Dispose();
}
}
}
}
private void dataGridViewSpecs_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
{
var selectedRow = dataGridViewSpecs.Rows[e.RowIndex];
var specId = (int)selectedRow.Cells["Id"].Value;
var service = Program.ServiceProvider?.GetService(typeof(FormAddUpdSpec));
if (service is FormAddUpdSpec form)
{
form.Id = specId;
if (form.ShowDialog() == DialogResult.OK)
{
LoadSpecializationData();
}
}
}
}
private void FormMain_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.F2 && !e.Control)
{
ShowSearchForm();
}
else if (e.KeyCode == Keys.F2 && e.Control)
{
ResetSearch();
}
}
private void ShowSearchForm()
{
if (_searchForm == null || _searchForm.IsDisposed)
{
_searchForm = new FormSearch();
_searchForm.OnSearchTextChanged += (s, e) =>
{
_currentSearchText = _searchForm.SearchText;
LoadSpecializationData();
};
_searchForm.FormClosed += (s, e) =>
{
if (_searchForm.DialogResult == DialogResult.OK && _searchForm.ResetSearch)
{
ResetSearch();
}
};
}
_searchForm.Show();
_searchForm.Activate();
}
// Сброс поиска
private void ResetSearch()
{
_currentSearchText = string.Empty;
LoadSpecializationData();
}
private void MainPages_KeyDown(object sender, KeyEventArgs e)
{
if (MainPages.SelectedTab == StudentsPage && !_studentsLoaded)
{
}
else if (MainPages.SelectedTab == GroupsPage && !_groupsLoaded)
{
}
else if (MainPages.SelectedTab == SpecPage && !_specsLoaded)
{
if (e.KeyCode == Keys.Insert)
{
var service = Program.ServiceProvider?.GetService(typeof(FormAddUpdSpec));
if (service is FormAddUpdSpec form)
{
if (form.ShowDialog() == DialogResult.OK)
{
LoadSpecializationData();
form.Dispose();
}
}
}
}
else if (MainPages.SelectedTab == OrdersPage)
{
// Дополнительная логика для этой вкладки
}
}
}
}

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="menuStrip2.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>153, 17</value>
</metadata>
<metadata name="menuStrip3.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>289, 17</value>
</metadata>
<metadata name="menuStrip4.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>425, 17</value>
</metadata>
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="menuStrip2.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>153, 17</value>
</metadata>
<metadata name="menuStrip3.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>289, 17</value>
</metadata>
<metadata name="menuStrip4.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>425, 17</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>25</value>
</metadata>
</root>

View File

@@ -0,0 +1,59 @@
namespace App_contingent_university.Forms
{
partial class FormSearch
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
textBoxSearch = new TextBox();
SuspendLayout();
//
// textBoxSearch
//
textBoxSearch.Location = new Point(16, 10);
textBoxSearch.Name = "textBoxSearch";
textBoxSearch.Size = new Size(293, 27);
textBoxSearch.TabIndex = 0;
textBoxSearch.TextChanged += textBoxSearch_TextChanged;
//
// FormSearch
//
AutoScaleDimensions = new SizeF(8F, 20F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(321, 46);
Controls.Add(textBoxSearch);
Name = "FormSearch";
Text = "Поиск";
KeyDown += FormSearch_KeyDown;
ResumeLayout(false);
PerformLayout();
}
#endregion
private TextBox textBoxSearch;
}
}

View File

@@ -0,0 +1,42 @@
namespace App_contingent_university.Forms
{
public partial class FormSearch : Form
{
public string SearchText { get; private set; } = string.Empty;
public bool ResetSearch { get; private set; } = false;
public FormSearch()
{
InitializeComponent();
this.KeyPreview = true;
}
private void textBoxSearch_TextChanged(object sender, EventArgs e)
{
SearchText = textBoxSearch.Text;
OnSearchTextChanged?.Invoke(this, EventArgs.Empty);
}
private void FormSearch_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
DialogResult = DialogResult.OK;
Close();
}
else if (e.KeyCode == Keys.F2 && e.Control)
{
ResetSearch = true;
DialogResult = DialogResult.OK;
Close();
}
else if (e.KeyCode == Keys.Escape)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
public event EventHandler OnSearchTextChanged;
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,108 @@
namespace App_contingent_university.Forms
{
partial class FormSpecsFilter
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.buttonOK = new System.Windows.Forms.Button();
this.textBoxCode = new System.Windows.Forms.TextBox();
this.labelCode = new System.Windows.Forms.Label();
this.textBoxName = new System.Windows.Forms.TextBox();
this.labelName = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// buttonOK
//
this.buttonOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.buttonOK.BackColor = System.Drawing.Color.Orange;
this.buttonOK.ForeColor = System.Drawing.Color.Black;
this.buttonOK.Location = new System.Drawing.Point(158, 100);
this.buttonOK.Name = "buttonOK";
this.buttonOK.Size = new System.Drawing.Size(113, 37);
this.buttonOK.TabIndex = 1;
this.buttonOK.Text = "Применить";
this.buttonOK.UseVisualStyleBackColor = false;
//
// textBoxCode
//
this.textBoxCode.Location = new System.Drawing.Point(107, 48);
this.textBoxCode.Name = "textBoxCode";
this.textBoxCode.Size = new System.Drawing.Size(160, 22);
this.textBoxCode.TabIndex = 8;
//
// labelCode
//
this.labelCode.AutoSize = true;
this.labelCode.Location = new System.Drawing.Point(12, 54);
this.labelCode.Name = "labelCode";
this.labelCode.Size = new System.Drawing.Size(31, 16);
this.labelCode.TabIndex = 7;
this.labelCode.Text = "Код";
//
// textBoxName
//
this.textBoxName.Location = new System.Drawing.Point(107, 12);
this.textBoxName.Name = "textBoxName";
this.textBoxName.Size = new System.Drawing.Size(160, 22);
this.textBoxName.TabIndex = 6;
//
// labelName
//
this.labelName.AutoSize = true;
this.labelName.Location = new System.Drawing.Point(12, 18);
this.labelName.Name = "labelName";
this.labelName.Size = new System.Drawing.Size(73, 16);
this.labelName.TabIndex = 5;
this.labelName.Text = "Название";
//
// FormSpecsFilter
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.LightSteelBlue;
this.ClientSize = new System.Drawing.Size(283, 149);
this.Controls.Add(this.textBoxCode);
this.Controls.Add(this.labelCode);
this.Controls.Add(this.textBoxName);
this.Controls.Add(this.labelName);
this.Controls.Add(this.buttonOK);
this.Name = "FormSpecsFilter";
this.Text = "Фильтр";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button buttonOK;
private System.Windows.Forms.TextBox textBoxCode;
private System.Windows.Forms.Label labelCode;
private System.Windows.Forms.TextBox textBoxName;
private System.Windows.Forms.Label labelName;
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App_contingent_university.Forms
{
public partial class FormSpecsFilter : Form
{
public FormSpecsFilter()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,113 @@
namespace App_contingent_university.Forms
{
partial class FormAddUpdSpec
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
textBoxCode = new TextBox();
labelCode = new Label();
textBoxName = new TextBox();
labelName = new Label();
buttonOK = new Button();
SuspendLayout();
//
// textBoxCode
//
textBoxCode.Location = new Point(109, 60);
textBoxCode.Margin = new Padding(3, 4, 3, 4);
textBoxCode.Name = "textBoxCode";
textBoxCode.Size = new Size(160, 27);
textBoxCode.TabIndex = 13;
//
// labelCode
//
labelCode.AutoSize = true;
labelCode.Location = new Point(14, 68);
labelCode.Name = "labelCode";
labelCode.Size = new Size(35, 20);
labelCode.TabIndex = 12;
labelCode.Text = "Код";
//
// textBoxName
//
textBoxName.Location = new Point(109, 15);
textBoxName.Margin = new Padding(3, 4, 3, 4);
textBoxName.Name = "textBoxName";
textBoxName.Size = new Size(160, 27);
textBoxName.TabIndex = 11;
//
// labelName
//
labelName.AutoSize = true;
labelName.Location = new Point(14, 22);
labelName.Name = "labelName";
labelName.Size = new Size(77, 20);
labelName.TabIndex = 10;
labelName.Text = "Название";
//
// buttonOK
//
buttonOK.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
buttonOK.BackColor = Color.Orange;
buttonOK.ForeColor = Color.Black;
buttonOK.Location = new Point(163, 168);
buttonOK.Margin = new Padding(3, 4, 3, 4);
buttonOK.Name = "buttonOK";
buttonOK.Size = new Size(113, 46);
buttonOK.TabIndex = 9;
buttonOK.Text = "Сохранить";
buttonOK.UseVisualStyleBackColor = false;
buttonOK.Click += buttonOK_Click;
//
// FormAddUpdSpec
//
AutoScaleDimensions = new SizeF(8F, 20F);
AutoScaleMode = AutoScaleMode.Font;
BackColor = Color.LightSteelBlue;
ClientSize = new Size(288, 229);
Controls.Add(textBoxCode);
Controls.Add(labelCode);
Controls.Add(textBoxName);
Controls.Add(labelName);
Controls.Add(buttonOK);
Margin = new Padding(3, 4, 3, 4);
Name = "FormAddUpdSpec";
Text = "Создать/редактировать";
Load += FormAddUpdSpec_Load;
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.TextBox textBoxCode;
private System.Windows.Forms.Label labelCode;
private System.Windows.Forms.TextBox textBoxName;
private System.Windows.Forms.Label labelName;
private System.Windows.Forms.Button buttonOK;
}
}

View File

@@ -0,0 +1,110 @@
using Controller.BusinessLogic;
using Controller.Repository;
using DataModels.Models;
namespace App_contingent_university.Forms
{
public partial class FormAddUpdSpec : Form
{
private readonly SpecializationRepository specializationRepository;
private readonly SpecializationLogic specializationLogic;
private int? _id;
public int Id
{
set
{
_id = value;
}
}
public FormAddUpdSpec(SpecializationLogic logic, SpecializationRepository repo)
{
specializationLogic = logic;
specializationRepository = repo;
InitializeComponent();
}
private async Task LoadData()
{
if (!_id.HasValue) return;
try
{
// Добавляем индикатор загрузки
textBoxCode.Text = "Загрузка...";
textBoxName.Text = "Загрузка...";
// Явно ожидаем загрузку
var spec = await specializationRepository.Get(_id.Value);
if (spec != null)
{
textBoxCode.Text = spec.Code ?? "Не указано";
textBoxName.Text = spec.Name ?? "Не указано";
if (spec.FacultyId == 0) // Проверка на недопустимое значение
{
MessageBox.Show("Ошибка: не указан факультет");
return;
}
}
else
{
MessageBox.Show("Специализация не найдена");
}
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка загрузки: {ex.Message}");
textBoxCode.Text = "Ошибка";
textBoxName.Text = "Ошибка";
}
}
private async void buttonOK_Click(object sender, EventArgs e)
{
try
{
if (_id.HasValue)
{
// Получаем существующую специализацию
var existingSpec = await specializationRepository.Get(_id.Value);
if (existingSpec == null)
{
MessageBox.Show("Специализация не найдена");
return;
}
// Обновляем поля
existingSpec.Code = textBoxCode.Text;
existingSpec.Name = textBoxName.Text;
existingSpec.FacultyId = StaticSession.CurrentFacultyId;
await specializationRepository.Update(existingSpec);
}
else
{
// Создаем новую специализацию
var newSpec = new Specialization
{
Code = textBoxCode.Text,
Name = textBoxName.Text,
FacultyId = StaticSession.CurrentFacultyId,
};
await specializationRepository.Add(newSpec);
}
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
MessageBox.Show($"Ошибка: {ex.Message}");
}
}
private void FormAddUpdSpec_Load(object sender, EventArgs e)
{
LoadData();
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,153 @@
namespace App_contingent_university.Forms
{
partial class FormUpdStudent
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.labelGroup = new System.Windows.Forms.Label();
this.comboBoxGroup = new System.Windows.Forms.ComboBox();
this.textBoxPatronimic = new System.Windows.Forms.TextBox();
this.labelPatronimic = new System.Windows.Forms.Label();
this.textBoxName = new System.Windows.Forms.TextBox();
this.labelI = new System.Windows.Forms.Label();
this.textBoxLastName = new System.Windows.Forms.TextBox();
this.labelF = new System.Windows.Forms.Label();
this.buttonOK = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// labelGroup
//
this.labelGroup.AutoSize = true;
this.labelGroup.Location = new System.Drawing.Point(572, 7);
this.labelGroup.Name = "labelGroup";
this.labelGroup.Size = new System.Drawing.Size(54, 16);
this.labelGroup.TabIndex = 16;
this.labelGroup.Text = "Группа";
//
// comboBoxGroup
//
this.comboBoxGroup.FormattingEnabled = true;
this.comboBoxGroup.Location = new System.Drawing.Point(575, 26);
this.comboBoxGroup.Name = "comboBoxGroup";
this.comboBoxGroup.Size = new System.Drawing.Size(142, 24);
this.comboBoxGroup.TabIndex = 15;
//
// textBoxPatronimic
//
this.textBoxPatronimic.Location = new System.Drawing.Point(389, 28);
this.textBoxPatronimic.Name = "textBoxPatronimic";
this.textBoxPatronimic.Size = new System.Drawing.Size(160, 22);
this.textBoxPatronimic.TabIndex = 14;
//
// labelPatronimic
//
this.labelPatronimic.AutoSize = true;
this.labelPatronimic.Location = new System.Drawing.Point(386, 9);
this.labelPatronimic.Name = "labelPatronimic";
this.labelPatronimic.Size = new System.Drawing.Size(70, 16);
this.labelPatronimic.TabIndex = 13;
this.labelPatronimic.Text = "Отчество";
//
// textBoxName
//
this.textBoxName.Location = new System.Drawing.Point(204, 28);
this.textBoxName.Name = "textBoxName";
this.textBoxName.Size = new System.Drawing.Size(160, 22);
this.textBoxName.TabIndex = 12;
//
// labelI
//
this.labelI.AutoSize = true;
this.labelI.Location = new System.Drawing.Point(201, 9);
this.labelI.Name = "labelI";
this.labelI.Size = new System.Drawing.Size(33, 16);
this.labelI.TabIndex = 11;
this.labelI.Text = "Имя";
//
// textBoxLastName
//
this.textBoxLastName.Location = new System.Drawing.Point(17, 28);
this.textBoxLastName.Name = "textBoxLastName";
this.textBoxLastName.Size = new System.Drawing.Size(160, 22);
this.textBoxLastName.TabIndex = 18;
//
// labelF
//
this.labelF.AutoSize = true;
this.labelF.Location = new System.Drawing.Point(14, 9);
this.labelF.Name = "labelF";
this.labelF.Size = new System.Drawing.Size(66, 16);
this.labelF.TabIndex = 17;
this.labelF.Text = "Фамилия";
//
// buttonOK
//
this.buttonOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.buttonOK.BackColor = System.Drawing.Color.Orange;
this.buttonOK.ForeColor = System.Drawing.Color.Black;
this.buttonOK.Location = new System.Drawing.Point(604, 81);
this.buttonOK.Name = "buttonOK";
this.buttonOK.Size = new System.Drawing.Size(113, 37);
this.buttonOK.TabIndex = 19;
this.buttonOK.Text = "Сохранить";
this.buttonOK.UseVisualStyleBackColor = false;
//
// FormUpdStudent
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.LightSteelBlue;
this.ClientSize = new System.Drawing.Size(746, 130);
this.Controls.Add(this.buttonOK);
this.Controls.Add(this.textBoxLastName);
this.Controls.Add(this.labelF);
this.Controls.Add(this.labelGroup);
this.Controls.Add(this.comboBoxGroup);
this.Controls.Add(this.textBoxPatronimic);
this.Controls.Add(this.labelPatronimic);
this.Controls.Add(this.textBoxName);
this.Controls.Add(this.labelI);
this.Name = "FormUpdStudent";
this.Text = "Редактировать студента";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label labelGroup;
private System.Windows.Forms.ComboBox comboBoxGroup;
private System.Windows.Forms.TextBox textBoxPatronimic;
private System.Windows.Forms.Label labelPatronimic;
private System.Windows.Forms.TextBox textBoxName;
private System.Windows.Forms.Label labelI;
private System.Windows.Forms.TextBox textBoxLastName;
private System.Windows.Forms.Label labelF;
private System.Windows.Forms.Button buttonOK;
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App_contingent_university.Forms
{
public partial class FormUpdStudent : Form
{
public FormUpdStudent()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,70 @@
using App_contingent_university.Forms;
using Controller.BusinessLogic;
using Controller.Repository;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Windows.Forms;
namespace App_contingent_university
{
internal static class Program
{
public static IServiceProvider? ServiceProvider { get; private set; }
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// Настройка сервисов
var services = ConfigureServices();
ServiceProvider = services.BuildServiceProvider();
// Сначала показываем форму аутентификации
using (var authScope = ServiceProvider.CreateScope())
{
var authForm = authScope.ServiceProvider.GetRequiredService<FormAuth>();
if (authForm.ShowDialog() == DialogResult.OK)
{
// После успешной аутентификации показываем главную форму
using (var mainScope = ServiceProvider.CreateScope())
{
var mainForm = mainScope.ServiceProvider.GetRequiredService<FormMain>();
Application.Run(mainForm);
}
}
}
}
static ServiceCollection ConfigureServices()
{
var services = new ServiceCollection();
services.AddHttpClient();
// Регистрация репозиториев
services.AddScoped<StudentRepository>();
services.AddScoped<GroupRepository>();
services.AddScoped<SpecializationRepository>();
services.AddScoped<OrderRepository>();
services.AddScoped<DeanRepository>();
services.AddScoped<FacultReposirory>();
// Регистрация бизнес-логики
services.AddScoped<StudentLogic>();
services.AddScoped<GroupLogic>();
services.AddScoped<SpecializationLogic>();
services.AddScoped<OrderLogic>();
services.AddScoped<DeanLogic>();
// Регистрация форм
services.AddScoped<FormMain>();
services.AddScoped<FormAddUpdSpec>();
services.AddScoped<FormAuth>();
services.AddScoped<FormSearch>();
return services;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// Общие сведения об этой сборке предоставляются следующим набором
// набора атрибутов. Измените значения этих атрибутов для изменения сведений,
// связанных со сборкой.
[assembly: AssemblyTitle("App_contingent_university")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("App_contingent_university")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми
// для компонентов COM. Если необходимо обратиться к типу в этой сборке через
// COM, следует установить атрибут ComVisible в TRUE для этого типа.
[assembly: ComVisible(false)]
// Следующий GUID служит для идентификации библиотеки типов, если этот проект будет видимым для COM
[assembly: Guid("4aaf178e-2e63-49c7-a054-1cf8618b9db1")]
// Сведения о версии сборки состоят из указанных ниже четырех значений:
//
// Основной номер версии
// Дополнительный номер версии
// Номер сборки
// Редакция
//
// Можно задать все значения или принять номера сборки и редакции по умолчанию
// используя "*", как показано ниже:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Этот код создан программным средством.
// Версия среды выполнения: 4.0.30319.42000
//
// Изменения в этом файле могут привести к неправильному поведению и будут утрачены, если
// код создан повторно.
// </auto-generated>
//------------------------------------------------------------------------------
namespace App_contingent_university.Properties
{
/// <summary>
/// Класс ресурсов со строгим типом для поиска локализованных строк и пр.
/// </summary>
// Этот класс был автоматически создан при помощи StronglyTypedResourceBuilder
// класс с помощью таких средств, как ResGen или Visual Studio.
// Для добавления или удаления члена измените файл .ResX, а затем перезапустите ResGen
// с параметром /str или заново постройте свой VS-проект.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// Возврат кэшированного экземпляра ResourceManager, используемого этим классом.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("App_contingent_university.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Переопределяет свойство CurrentUICulture текущего потока для всех
/// подстановки ресурсов с помощью этого класса ресурсов со строгим типом.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace App_contingent_university.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -0,0 +1,62 @@
using System.Text.Json;
using System.Net.Http.Json;
namespace Controller
{
public abstract class AbstractRepository<T, TSearch> where T : class where TSearch : class
{
private readonly HttpClient _httpClient;
public abstract string BaseUrl { get; set; }
public AbstractRepository(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<T> Get(int id)
{
var response = await _httpClient.GetAsync($"{BaseUrl}/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>()
?? throw new JsonException($"Ошибка при десериализации ПРИ ПОЛУЧЕНИИ объекта {typeof(T)}");
}
public async Task<List<T>> GetAll()
{
var response = await _httpClient.GetAsync(BaseUrl);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<T>>()
?? new List<T>();
}
public async Task<List<T>> GetFilteredList(TSearch model)
{
if (model == null)
{
return new List<T>();
}
var query = BuildQueryString(model);
var response = await _httpClient.GetAsync($"{BaseUrl}/filter{query}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<T>>()
?? new List<T>();
}
public async Task<T> Add(T entity)
{
var response = await _httpClient.PostAsJsonAsync(BaseUrl, entity);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>()
?? throw new JsonException($"Ошибка при десериализации ПРИ СОЗДАНИИ объекта {typeof(T)}");
}
public async Task<T> Remove(int id)
{
var response = await _httpClient.DeleteAsync($"{BaseUrl}/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<T>()
?? throw new JsonException($"Ошибка при десериализации ПРИ УДАЛЕНИИ объекта {typeof(T)}");
}
public abstract Task<T> Update(T entity);
public abstract string BuildQueryString(TSearch model);
}
}

View File

@@ -0,0 +1,9 @@
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class ContingentReportLogic
{
}
}

View File

@@ -0,0 +1,9 @@
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class DeanLogic
{
}
}

View File

@@ -0,0 +1,9 @@
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class FacultLogic
{
}
}

View File

@@ -0,0 +1,51 @@
using Controller.Repository;
using Controller.ViewModels;
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class GroupLogic
{
private readonly SpecializationRepository _specializationRepository;
private readonly GroupRepository _groupRepository;
public GroupLogic(SpecializationRepository rep, GroupRepository groupRepository)
{
_specializationRepository = rep;
_groupRepository = groupRepository;
}
public async Task<List<GroupViewModel>> GetViewModel()
{
var groups = await _groupRepository.GetAll();
var results = new List<GroupViewModel>();
foreach (var group in groups)
{
var spec = group.SpecializationId.HasValue
? await _specializationRepository.Get(group.SpecializationId.Value) : null;
Console.WriteLine(group.SpecializationId);
results.Add(new GroupViewModel(group, spec));
}
return results;
//var specs = new List<Specialization>
//{
// new Specialization{Id = 1, Code = "1234", Name = "Программная инженерия"},
// new Specialization{Id = 2, Code = "5678", Name = "Информатика"},
//};
//var groups = new List<Group>
//{
// new Group{Id = 1, Course = 3, Name = "ПИ", Number = 1, CountStudents = 29, SpecializationId = 1},
// new Group{Id = 2, Course = 3, Name = "ПМ", Number = 4, CountStudents = 24, SpecializationId = 2},
//};
}
}
}

View File

@@ -0,0 +1,9 @@
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class LearningPlanLogic
{
}
}

View File

@@ -0,0 +1,8 @@

namespace Controller.BusinessLogic
{
public class OrderLogic
{
}
}

View File

@@ -0,0 +1,39 @@
using Controller.Contracts;
using Controller.Repository;
using Controller.ViewModels;
using DataModels.Enums;
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class SpecializationLogic
{
private readonly SpecializationRepository _specializationRepository;
public SpecializationLogic(SpecializationRepository rep)
{
_specializationRepository = rep;
}
public async Task<List<SpecialisationViewModel>> GetViewModel(SpecializationSearch model)
{
if (model == null)
{
model = new SpecializationSearch { FacultId = StaticSession.CurrentFacultyId };
}
var specs = await _specializationRepository.GetFilteredList(model);
if (!string.IsNullOrEmpty(model.Search))
{
var searchTerm = model.Name.ToLower();
specs = specs.Where(s =>
(s.Name != null && s.Name.ToLower().Contains(searchTerm)) ||
(s.Code != null && s.Code.ToLower().Contains(searchTerm))
).ToList();
}
// Преобразуем в ViewModel
return specs.Select(s => new SpecialisationViewModel(s)).ToList();
}
}
}

View File

@@ -0,0 +1,73 @@
using Controller.Repository;
using Controller.ViewModels;
using DataModels.Enums;
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class StudentLogic
{
private readonly StudentRepository _studentRepository;
private readonly GroupRepository _groupRepository;
private readonly SpecializationRepository _specializationRepository;
public StudentLogic(StudentRepository studentRepository, GroupRepository groupRepository, SpecializationRepository specializationRepository)
{
_studentRepository = studentRepository;
_groupRepository = groupRepository;
_specializationRepository = specializationRepository;
}
public async Task<List<StudentViewModel>> GetViewModel()
{
var students = await _studentRepository.GetAll();
//var students = new List<Student>
//{
// new Student { Id = 1, Name = "Иванов", GroupId = 1, SpecializationId = null, Status = Status.AcademicLeave },
// new Student { Id = 2, Name = "Петров", GroupId = null, SpecializationId = null, Status = Status.Studying },
// new Student { Id = 3, Name = "Сидорова", GroupId = 2, SpecializationId = null }
//};
var result = new List<StudentViewModel>();
//var groups = new List<Group>
// {
// new Group { Id = 1, Name = "Группа 101", Course = 1, SpecializationId = 1, MaxStudentCount = 25, Number = 101 },
// new Group { Id = 2, Name = "Группа 202", Course = 2, SpecializationId = 2, MaxStudentCount = 30, Number = 202 },
// new Group { Id = 3, Name = "Группа 303", Course = 3, SpecializationId = 3, MaxStudentCount = 20, Number = 303 }
// };
// Моковые данные специализаций
//var specializations = new List<Specialization>
// {
// new Specialization { Id = 1, Name = "Информатика", Code = "INF" },
// new Specialization { Id = 2, Name = "Математика", Code = "MATH" },
// new Specialization { Id = 3, Name = "Физика", Code = "PHYS" }
// };
foreach (var student in students)
{
var group = student.GroupId.HasValue
? await _groupRepository.Get(student.GroupId.Value) : null;
var specialization = student.SpecializationId.HasValue
? await _specializationRepository.Get(student.SpecializationId.Value) : null;
//var group = student.GroupId.HasValue
// ? groups.FirstOrDefault(g => g.Id == student.GroupId.Value)
// : null;
//var specialization = student.SpecializationId.HasValue
// ? specializations.FirstOrDefault(s => s.Id == student.SpecializationId.Value)
// : group?.SpecializationId != null
// ? specializations.FirstOrDefault(s => s.Id == group.SpecializationId)
// : null;
result.Add(new StudentViewModel(student, group, specialization));
}
return result;
}
}
}

View File

@@ -0,0 +1,9 @@
using DataModels.Models;
namespace Controller.BusinessLogic
{
public class UserLogic
{
}
}

View File

@@ -0,0 +1,8 @@
namespace Controller.Contracts
{
public class DeanSearch
{
public string? Email { get; set; }
public string? Password { get; set;}
}
}

View File

@@ -0,0 +1,7 @@
namespace Controller.Contracts
{
public class FacultSearch
{
public string? Name { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,11 @@
namespace Controller.Contracts
{
public class GroupSearch
{
public string? Name { get; set; } = string.Empty;
public int? Number { get; set; }
public int? Course { get; set; }
public int? MaxStudentCount { get; set; }
public int? SpecializationId { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Controller.Contracts
{
public class LearningPlanSearch
{
public int? SpecId { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using DataModels.Enums;
namespace Controller.Contracts
{
public class OrderSearch
{
public OrderType? OrderType { get; set; }
public int? Number { get; set; }
public DateTime? Date { get; set; } = DateTime.Now;
}
}

View File

@@ -0,0 +1,10 @@
namespace Controller.Contracts
{
public class SpecializationSearch
{
public string? Name { get; set; } = string.Empty;
public string? Code { get; set; } = string.Empty;
public string? Search { get; set; } = string.Empty;
public int? FacultId{ get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using DataModels.Enums;
namespace Controller.Contracts
{
public class StudentSearch
{
public string? LastName { get; set; } = string.Empty;
public string? Name { get; set; } = string.Empty;
public string? Patronymic { get; set; } = string.Empty;
public int? SpecializationId { get; set; }
public int? GroupId { get; set; }
public Status? Status { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More