diff --git a/.clang-format b/.clang-format index 457581e..2a510b2 100644 --- a/.clang-format +++ b/.clang-format @@ -1,8 +1,8 @@ --- BasedOnStyle: Google +AccessModifierOffset: -4 -AccessModifierOffset: -2 AlignTrailingComments: true IndentPPDirectives: AfterHash IndentWidth: 4 @@ -15,11 +15,14 @@ BreakBeforeTernaryOperators: true ColumnLimit: 100 ConstructorInitializerAllOnOneLineOrOnePerLine: true -IncludeBlocks: Regroup +IncludeBlocks: Regroup + SortIncludes: true SpaceBeforeParens: ControlStatements AllowShortFunctionsOnASingleLine: InlineOnly AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false -... \ No newline at end of file + +... + diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml new file mode 100644 index 0000000..ca3d77d --- /dev/null +++ b/.github/workflows/clang-format-check.yml @@ -0,0 +1,53 @@ +name: Clang Format Check + +on: + push: + branches: + - '**' + +jobs: + clang-format: + runs-on: ubuntu-latest + + steps: + # Шаг 1: Checkout репозитория + - name: Checkout repository + uses: actions/checkout@v3 + + # Шаг 2: Установка зависимостей + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang-format clang-tidy libboost-all-dev \ + libcurl4-openssl-dev libpq-dev \ + nlohmann-json3-dev libpqxx-dev + + # Шаг 3: Установка TgBot + - name: Install TgBot + run: | + git clone https://github.com/reo7sp/tgbot-cpp.git + cd tgbot-cpp + mkdir build + cd build + cmake .. + make -j$(nproc) + sudo make install + + # Шаг 4: Кэширование директории сборки + - name: Cache Build Directory + uses: actions/cache@v3 + with: + path: build + key: ${{ runner.os }}-build-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-build- + + # Шаг 5: Конфигурация CMake + - name: Configure CMake + run: cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DTgBot_DIR=/usr/local/lib/cmake/TgBot + + # Шаг 6: Запуск Clang Format + - name: Run Clang Format + run: | + find include source -name '*.cpp' -o -name '*.h' | xargs clang-format -i + git diff --exit-code || (echo "Clang format check failed! Run clang-format and commit changes."; exit 1) diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml new file mode 100644 index 0000000..8acdfbe --- /dev/null +++ b/.github/workflows/clang-tidy.yml @@ -0,0 +1,64 @@ +name: Static Analysis Checks + +on: + push: + branches: + - '**' + +defaults: + run: + working-directory: . + +jobs: + install-deps: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang-format clang-tidy libboost-all-dev \ + libcurl4-openssl-dev libpq-dev \ + nlohmann-json3-dev libpqxx-dev + + - name: Install TgBot + run: | + git clone https://github.com/reo7sp/tgbot-cpp.git + cd tgbot-cpp + mkdir build + cd build + cmake .. + make -j$(nproc) + sudo make install + + - name: Cache Build Directory + uses: actions/cache@v3 + with: + path: build + key: ${{ runner.os }}-build-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-build- + + - name: Configure CMake + run: cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DTgBot_DIR=/usr/local/lib/cmake/TgBot + + + clang-tidy: + needs: install-deps + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Restore Cached Build + uses: actions/cache@v3 + with: + path: build + key: ${{ runner.os }}-build-${{ github.sha }} + + - name: Run Clang Tidy + run: | + # Найдем все исходные файлы (.cpp и .h) в нужных папках (source, include), исключая build + clang-tidy source/*.cpp include/*.hpp diff --git a/.github/workflows/cppcheck.yml b/.github/workflows/cppcheck.yml new file mode 100644 index 0000000..ac0c884 --- /dev/null +++ b/.github/workflows/cppcheck.yml @@ -0,0 +1,27 @@ +name: CppCheck Analysis + +on: + push: + branches: + - '**' + +jobs: + cppcheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y cppcheck + + - name: Configure CMake + run: cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Run CppCheck + run: | + # Указываем CppCheck проверять только исходные файлы в папке source или include + cppcheck --enable=all --inconclusive --quiet --error-exitcode=1 source/ include/ diff --git a/.gitignore b/.gitignore index 0c18535..09d1547 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +BotToken.hpp +/build +.vscode/ .idea/ -build/ + # Prerequisites *.d diff --git a/CMakeLists.txt b/CMakeLists.txt index 81f6226..434c885 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,31 +1,60 @@ -cmake_minimum_required(VERSION 3.10) -project(TelegramBot) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - -find_package(Boost REQUIRED COMPONENTS system) -find_package(TgBot REQUIRED) - -target_include_directories(include) - -file(GLOB SOURCES source/*.cpp) - -add_executable(my_bot ${SOURCES}) - -target_link_libraries(my_bot Boost::system TgBot::TgBot) - - -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - target_compile_options(my_bot PRIVATE - -g - -fsanitize=address,undefined - -fno-omit-frame-pointer - ) - target_link_options(my_bot PRIVATE - -fsanitize=address,undefined - -fno-omit-frame-pointer - ) -endif() \ No newline at end of file +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +project(MyProject) + +include_directories(include) + +file(GLOB SOURCES source/*.cpp) + +add_executable(main ${SOURCES}) + +include(cmake/CPM.cmake) + +CPMAddPackage( + NAME Boost + VERSION 1.86.0 + URL https://github.com/boostorg/boost/releases/download/boost-1.86.0/boost-1.86.0-cmake.tar.xz + URL_HASH SHA256=2c5ec5edcdff47ff55e27ed9560b0a0b94b07bd07ed9928b476150e16b0efc57 + OPTIONS "BOOST_ENABLE_CMAKE ON" "BOOST_SKIP_INSTALL_RULES ON" + "BUILD_SHARED_LIBS OFF" "BOOST_INCLUDE_LIBRARIES system" +) + +CPMAddPackage( + NAME TgBot + GIT_REPOSITORY https://github.com/reo7sp/tgbot-cpp.git + GIT_TAG master +) + +CPMAddPackage( + NAME nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.2 +) + +CPMAddPackage( + NAME libcurl + GIT_REPOSITORY https://github.com/curl/curl.git + GIT_TAG curl-8_1_2 + OPTIONS "BUILD_CURL_EXE OFF" "BUILD_TESTING OFF" +) + +CPMAddPackage( + NAME libpqxx + GIT_REPOSITORY https://github.com/jtv/libpqxx.git + GIT_TAG 7.6.0 +) + +CPMAddPackage( + NAME fmt + GIT_REPOSITORY https://github.com/fmtlib/fmt.git + GIT_TAG 10.2.1 # можно заменить на нужную версию +) + +target_link_libraries(main + Boost::system + TgBot + nlohmann_json::nlohmann_json + curl + pqxx + fmt::fmt +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..34a1f1b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Melnikov Kirill + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..9d87d01 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.40.7) +set(CPM_HASH_SUM "c0fc82149e00c43a21febe7b2ca57b2ffea2b8e88ab867022c21d6b81937eb50") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/include/OfficeStaff.hpp b/include/OfficeStaff.hpp new file mode 100644 index 0000000..446d9b4 --- /dev/null +++ b/include/OfficeStaff.hpp @@ -0,0 +1,16 @@ +#ifndef OFFECESTAFF_HPP_ +#define OFFECESTAFF_HPP_ + +#include "User.hpp" + +namespace mtd { +class OfficeStaff : public User { +public: + OfficeStaff(int64_t chat_id); + TgBot::InlineKeyboardMarkup::Ptr GetInlineKeyboard() override; + TgBot::InlineKeyboardMarkup::Ptr GetMenu() override; + TgBot::InlineKeyboardMarkup::Ptr BackButton() override; +}; +} // namespace mtd + +#endif diff --git a/include/Student.hpp b/include/Student.hpp new file mode 100644 index 0000000..4264fe4 --- /dev/null +++ b/include/Student.hpp @@ -0,0 +1,16 @@ +#ifndef STUDENT_GPP_ +#define STUDENT_GPP_ + +#include "User.hpp" + +namespace mtd { +class Student : public User { +public: + Student(int64_t chat_id); + TgBot::InlineKeyboardMarkup::Ptr GetInlineKeyboard() override; + TgBot::InlineKeyboardMarkup::Ptr GetMenu() override; + TgBot::InlineKeyboardMarkup::Ptr BackButton() override; +}; +} // namespace mtd + +#endif \ No newline at end of file diff --git a/include/Teacher.hpp b/include/Teacher.hpp new file mode 100644 index 0000000..6cab586 --- /dev/null +++ b/include/Teacher.hpp @@ -0,0 +1,16 @@ +#ifndef TEACHER_HPP_ +#define TEACHER_HPP_ + +#include "User.hpp" + +namespace mtd { +class Teacher : public User { +public: + Teacher(int64_t chat_id); + TgBot::InlineKeyboardMarkup::Ptr GetInlineKeyboard() override; + TgBot::InlineKeyboardMarkup::Ptr GetMenu() override; + TgBot::InlineKeyboardMarkup::Ptr BackButton() override; +}; +} // namespace mtd + +#endif \ No newline at end of file diff --git a/include/Tutor.hpp b/include/Tutor.hpp new file mode 100644 index 0000000..d60fe6f --- /dev/null +++ b/include/Tutor.hpp @@ -0,0 +1,16 @@ +#ifndef TUTOR_HPP_ +#define TUTOR_HPP_ + +#include "User.hpp" + +namespace mtd { +class Tutor : public User { +public: + Tutor(int64_t chat_id); + TgBot::InlineKeyboardMarkup::Ptr GetInlineKeyboard() override; + TgBot::InlineKeyboardMarkup::Ptr GetMenu() override; + TgBot::InlineKeyboardMarkup::Ptr BackButton() override; +}; +} // namespace mtd + +#endif \ No newline at end of file diff --git a/include/User.hpp b/include/User.hpp new file mode 100644 index 0000000..44385c1 --- /dev/null +++ b/include/User.hpp @@ -0,0 +1,37 @@ +#ifndef USER_HPP_ +#define USER_HPP_ + +#include + +#include +#include + +#include "sop.hpp" + +namespace mtd { +enum class UserRole { STUDENT, TEACHER, OFFICE_STAFF, TUTOR, NONE }; +enum class UserState { MENU, BUTTONS, INFORMATION, STUDENT_SOP, TUTOR_SOP, CREATE_SOP, TUTOR_ADD_DECLARATION, NONE }; + +class User { + int64_t chat_id = 0; + UserRole role = UserRole::NONE; + UserState state = UserState::NONE; + int step = 0; + std::vector evaluations; + +public: + explicit User(int64_t chat_id, UserRole role); + virtual ~User(); + Feedback feedback; + int64_t id() const; + UserRole GetRole() const; + UserState &GetState(); + int &GetStep(); + std::vector &GetEvaluations(); + virtual TgBot::InlineKeyboardMarkup::Ptr GetInlineKeyboard() = 0; + virtual TgBot::InlineKeyboardMarkup::Ptr GetMenu() = 0; + virtual TgBot::InlineKeyboardMarkup::Ptr BackButton() = 0; +}; +} // namespace mtd + +#endif \ No newline at end of file diff --git a/include/sop.hpp b/include/sop.hpp new file mode 100644 index 0000000..20daa8a --- /dev/null +++ b/include/sop.hpp @@ -0,0 +1,61 @@ +#ifndef SOP_HPP_ +#define SOP_HPP_ + +#include +#include +#include +#include + +namespace mtd { +struct Discipline { + std::string name_subject; + std::string lector; + std::vector seminarians; +}; +// эта структура -- кусочек сопа для какого-то предмета +// например, если говорим про ОМП, то это часть типа практика группы 2 +// или оценивание лекции +/* + Данная структура находится в каждого студента в классе, + он заполняет чать сопа, из этой структуры все перекидывается в другие две (которые ниже), + потом в эту структуру записываются новые данные (у каждого студента своя) +*/ +struct Feedback { + int grade = 0; // оценка преподавания + int grade_home_work = -1; // проверка дз (-1 -- это лекция, нет дз) + std::string advantages; // что нравится + std::string disadvantages; // что не нравится + int step = 0; // Вроде бесполезная херня, но лучше пока не трогать + int index = 0; // Чтобы итерироваться по практикам +}; + +// Для каждого студента саздается одна такая по каждому прежмету +struct LectionFeedback { + int grade; + std::string advantages; // что нравится + std::string disadvantages; // что не нравится +}; + +// таки создается для каждого студента столько, сколько всего групп +struct PracticeFeedback { + std::string name_teacher; + int grade; + int grade_for_homework; + std::string advantages; // что нравится + std::string disadvantages; // что не нравится +}; + +struct Subject { + std::string name_subject; + std::vector lections_result; // все фидбеки по лекциям + std::vector practice_result; // по практикам + std::vector comments; // дополнительные комментарии к предмету +}; + +// Если ты это читаешь, то искренне сочувствую тебе. Может, тебе удастся что-то понять +// Я тебе не советую этого делать, лучше иди погуляй, потрогай траву, посмотри на небо, на людей +// Подумай, программирование -- это реально то, на что ты готов потратить свою короткую жизнь + +} // namespace mtd + +#endif \ No newline at end of file diff --git a/include/statistics.hpp b/include/statistics.hpp new file mode 100644 index 0000000..68f9be6 --- /dev/null +++ b/include/statistics.hpp @@ -0,0 +1,8 @@ +#ifndef STATISTICS_H +#define STATISTICS_H + +#include "sop.hpp" // если у вас есть этот файл с описанием структур + +std::string generate_statistics(const mtd::Subject& subject); + +#endif // STATISTICS_H \ No newline at end of file diff --git a/source/OfficeStaff.cpp b/source/OfficeStaff.cpp new file mode 100644 index 0000000..006a81f --- /dev/null +++ b/source/OfficeStaff.cpp @@ -0,0 +1,67 @@ +#include "OfficeStaff.hpp" + +namespace mtd { +OfficeStaff::OfficeStaff(int64_t chat_id) : User(chat_id, UserRole::OFFICE_STAFF) { +} +TgBot::InlineKeyboardMarkup::Ptr OfficeStaff::GetInlineKeyboard() { + TgBot::InlineKeyboardMarkup::Ptr inlineKeyboard(new TgBot::InlineKeyboardMarkup); + + TgBot::InlineKeyboardButton::Ptr timeTable(new TgBot::InlineKeyboardButton); + timeTable->text = "📅 Расписание"; + timeTable->callbackData = "office_staff_time_table"; + + TgBot::InlineKeyboardButton::Ptr declaration(new TgBot::InlineKeyboardButton); + declaration->text = "📢 Объявления"; + declaration->callbackData = "office_staff_declaration"; + + TgBot::InlineKeyboardButton::Ptr connectWithTeacher(new TgBot::InlineKeyboardButton); + connectWithTeacher->text = "📩 Связь с преподавателем"; + connectWithTeacher->callbackData = "office_staff_connect_with_teacher"; + + TgBot::InlineKeyboardButton::Ptr help(new TgBot::InlineKeyboardButton); + help->text = "❓ Помощь"; + help->callbackData = "office_staff_help"; + + // ЗДЕСЬ КАКОЙ_ТО ПИЗДЕЦ + TgBot::InlineKeyboardButton::Ptr addInfo(new TgBot::InlineKeyboardButton); + addInfo->text = "Добавить информацию"; + addInfo->callbackData = "office_add_info"; + + TgBot::InlineKeyboardButton::Ptr back(new TgBot::InlineKeyboardButton); + back->text = "🔙 Назад"; + back->callbackData = "office_staff_back"; + + inlineKeyboard->inlineKeyboard.push_back({timeTable}); + inlineKeyboard->inlineKeyboard.push_back({declaration}); + inlineKeyboard->inlineKeyboard.push_back({connectWithTeacher}); + inlineKeyboard->inlineKeyboard.push_back({help}); + inlineKeyboard->inlineKeyboard.push_back({addInfo}); + inlineKeyboard->inlineKeyboard.push_back({back}); + return inlineKeyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr OfficeStaff::GetMenu() { + TgBot::InlineKeyboardMarkup::Ptr inline_keyboard(new TgBot::InlineKeyboardMarkup); + + TgBot::InlineKeyboardButton::Ptr buttons(new TgBot::InlineKeyboardButton); + buttons->text = "кнопочки"; + buttons->callbackData = "office_staff_buttons"; + + TgBot::InlineKeyboardButton::Ptr information(new TgBot::InlineKeyboardButton); + information->text = "Информация"; + information->callbackData = "office_staff_information"; + + inline_keyboard->inlineKeyboard.push_back({buttons}); + inline_keyboard->inlineKeyboard.push_back({information}); + return inline_keyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr OfficeStaff::BackButton() { + TgBot::InlineKeyboardMarkup::Ptr keyboard(new TgBot::InlineKeyboardMarkup); + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = "🔙 Назад"; + button->callbackData = "office_staff_back"; + keyboard->inlineKeyboard.push_back({button}); + return keyboard; +} +} // namespace mtd \ No newline at end of file diff --git a/source/Student.cpp b/source/Student.cpp new file mode 100644 index 0000000..b7d2709 --- /dev/null +++ b/source/Student.cpp @@ -0,0 +1,67 @@ +#include "Student.hpp" + +namespace mtd { +Student::Student(int64_t chat_id) : User(chat_id, UserRole::STUDENT) { +} +TgBot::InlineKeyboardMarkup::Ptr Student::GetInlineKeyboard() { + TgBot::InlineKeyboardMarkup::Ptr inlineKeyboard(new TgBot::InlineKeyboardMarkup); + + TgBot::InlineKeyboardButton::Ptr timeTable(new TgBot::InlineKeyboardButton); + timeTable->text = "📅 Расписание"; + timeTable->callbackData = "student_time_table"; + + TgBot::InlineKeyboardButton::Ptr declaration(new TgBot::InlineKeyboardButton); + declaration->text = "📢 Объявления"; + declaration->callbackData = "student_declaration"; + + TgBot::InlineKeyboardButton::Ptr connectWithTeacher(new TgBot::InlineKeyboardButton); + connectWithTeacher->text = "📩 Связь с преподавателем"; + connectWithTeacher->callbackData = "student_connect_with_teacher"; + + TgBot::InlineKeyboardButton::Ptr help(new TgBot::InlineKeyboardButton); + help->text = "❓ Помощь"; + help->callbackData = "student_help"; + + TgBot::InlineKeyboardButton::Ptr back(new TgBot::InlineKeyboardButton); + back->text = "🔙 Назад"; + back->callbackData = "student_back"; + + TgBot::InlineKeyboardButton::Ptr sop(new TgBot::InlineKeyboardButton); + sop->text = "Пройти СОП"; + sop->callbackData = "student_sop"; + + inlineKeyboard->inlineKeyboard.push_back({timeTable}); + inlineKeyboard->inlineKeyboard.push_back({declaration}); + inlineKeyboard->inlineKeyboard.push_back({connectWithTeacher}); + inlineKeyboard->inlineKeyboard.push_back({help}); + inlineKeyboard->inlineKeyboard.push_back({sop}); + inlineKeyboard->inlineKeyboard.push_back({back}); + + return inlineKeyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr Student::GetMenu() { + TgBot::InlineKeyboardMarkup::Ptr inline_keyboard(new TgBot::InlineKeyboardMarkup); + + TgBot::InlineKeyboardButton::Ptr buttons(new TgBot::InlineKeyboardButton); + buttons->text = "кнопочки"; + buttons->callbackData = "student_buttons"; + + TgBot::InlineKeyboardButton::Ptr information(new TgBot::InlineKeyboardButton); + information->text = "Информация"; + information->callbackData = "student_information"; + + inline_keyboard->inlineKeyboard.push_back({buttons}); + inline_keyboard->inlineKeyboard.push_back({information}); + return inline_keyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr Student::BackButton() { + TgBot::InlineKeyboardMarkup::Ptr keyboard(new TgBot::InlineKeyboardMarkup); + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = "🔙 Назад"; + button->callbackData = "student_back"; + keyboard->inlineKeyboard.push_back({button}); + return keyboard; +} +} // namespace mtd \ No newline at end of file diff --git a/source/Teacher.cpp b/source/Teacher.cpp new file mode 100644 index 0000000..eca9acf --- /dev/null +++ b/source/Teacher.cpp @@ -0,0 +1,47 @@ +#include "Teacher.hpp" + +namespace mtd { +Teacher::Teacher(int64_t chat_id) : User(chat_id, UserRole::TEACHER) { +} +TgBot::InlineKeyboardMarkup::Ptr Teacher::GetInlineKeyboard() { + TgBot::InlineKeyboardMarkup::Ptr inlineKeyboard(new TgBot::InlineKeyboardMarkup); + + TgBot::InlineKeyboardButton::Ptr declaration(new TgBot::InlineKeyboardButton); + declaration->text = "Уведомление студентам"; + declaration->callbackData = "teacher_declaration"; + inlineKeyboard->inlineKeyboard.push_back({declaration}); + + TgBot::InlineKeyboardButton::Ptr back(new TgBot::InlineKeyboardButton); + back->text = "🔙 Назад"; + back->callbackData = "teacher_back"; + inlineKeyboard->inlineKeyboard.push_back({back}); + + return inlineKeyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr Teacher::GetMenu() { + TgBot::InlineKeyboardMarkup::Ptr inlineKeyboard(new TgBot::InlineKeyboardMarkup); + + TgBot::InlineKeyboardButton::Ptr buttons(new TgBot::InlineKeyboardButton); + buttons->text = "кнопочки"; + buttons->callbackData = "teacher_buttons"; + + TgBot::InlineKeyboardButton::Ptr information(new TgBot::InlineKeyboardButton); + information->text = "Информация"; + information->callbackData = "teachert_information"; + + inlineKeyboard->inlineKeyboard.push_back({buttons}); + inlineKeyboard->inlineKeyboard.push_back({information}); + + return inlineKeyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr Teacher::BackButton() { + TgBot::InlineKeyboardMarkup::Ptr keyboard(new TgBot::InlineKeyboardMarkup); + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = "🔙 Назад"; + button->callbackData = "teacher_back"; + keyboard->inlineKeyboard.push_back({button}); + return keyboard; +} +} // namespace mtd \ No newline at end of file diff --git a/source/Tutor.cpp b/source/Tutor.cpp new file mode 100644 index 0000000..a020d61 --- /dev/null +++ b/source/Tutor.cpp @@ -0,0 +1,68 @@ +#include "Tutor.hpp" + +namespace mtd { +Tutor::Tutor(int64_t chat_id) : User(chat_id, UserRole::TUTOR) { +} + +TgBot::InlineKeyboardMarkup::Ptr Tutor::GetInlineKeyboard() { + TgBot::InlineKeyboardMarkup::Ptr inlineKeyboard(new TgBot::InlineKeyboardMarkup); + + // Добавление кнопки для добавления предмета + TgBot::InlineKeyboardButton::Ptr addSubject(new TgBot::InlineKeyboardButton); + addSubject->text = "Добавить предмет"; + addSubject->callbackData = "tutor_add_subject"; + inlineKeyboard->inlineKeyboard.push_back({addSubject}); + + // Добавление кнопки для объявления + TgBot::InlineKeyboardButton::Ptr addDeclaration(new TgBot::InlineKeyboardButton); + addDeclaration->text = "Сделать объявление"; + addDeclaration->callbackData = "tutor_add_declaration"; + inlineKeyboard->inlineKeyboard.push_back({addDeclaration}); + + // Добавление кнопки для добавления людей в группы + TgBot::InlineKeyboardButton::Ptr addPeople(new TgBot::InlineKeyboardButton); + addPeople->text = "Добавить людей в группы"; + addPeople->callbackData = "tutor_add_people"; + inlineKeyboard->inlineKeyboard.push_back({addPeople}); + + // Добавление кнопки для создания SOP + TgBot::InlineKeyboardButton::Ptr createSop(new TgBot::InlineKeyboardButton); + createSop->text = "Создать SOP"; + createSop->callbackData = "tutor_create_sop"; + inlineKeyboard->inlineKeyboard.push_back({createSop}); + + TgBot::InlineKeyboardButton::Ptr view_sop(new TgBot::InlineKeyboardButton); + view_sop->text = "Посмотреть СОП"; + view_sop->callbackData = "tutor_view_sop"; + inlineKeyboard->inlineKeyboard.push_back({view_sop}); + + // Добавление кнопки "Назад" + TgBot::InlineKeyboardButton::Ptr back(new TgBot::InlineKeyboardButton); + back->text = "🔙 Назад"; + back->callbackData = "tutor_back"; + inlineKeyboard->inlineKeyboard.push_back({back}); + + return inlineKeyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr Tutor::GetMenu() { + TgBot::InlineKeyboardMarkup::Ptr inlineKeyboard(new TgBot::InlineKeyboardMarkup); + + // Пример кнопки меню (можно добавить другие кнопки по необходимости) + TgBot::InlineKeyboardButton::Ptr buttons(new TgBot::InlineKeyboardButton); + buttons->text = "Меню для куратора"; + buttons->callbackData = "tutor_buttons"; + + inlineKeyboard->inlineKeyboard.push_back({buttons}); + return inlineKeyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr Tutor::BackButton() { + TgBot::InlineKeyboardMarkup::Ptr keyboard(new TgBot::InlineKeyboardMarkup); + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = "🔙 Назад"; + button->callbackData = "tutor_back"; + keyboard->inlineKeyboard.push_back({button}); + return keyboard; +} +} // namespace mtd diff --git a/source/User.cpp b/source/User.cpp new file mode 100644 index 0000000..e6ad1f8 --- /dev/null +++ b/source/User.cpp @@ -0,0 +1,23 @@ +#include "User.hpp" + +namespace mtd { +User::User(int64_t chat_id, UserRole role) : chat_id(chat_id), role(role), state(UserState::NONE) { +} +User::~User() { +} +int &User::GetStep() { + return step; +} +std::vector &User::GetEvaluations() { + return evaluations; +} +int64_t User::id() const { + return chat_id; +} +UserRole User::GetRole() const { + return role; +} +UserState &User::GetState() { + return state; +} +} // namespace mtd \ No newline at end of file diff --git a/source/main.cpp b/source/main.cpp new file mode 100644 index 0000000..1221d70 --- /dev/null +++ b/source/main.cpp @@ -0,0 +1,367 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +// #include "BotToken.hpp" +#include "OfficeStaff.hpp" +#include "Student.hpp" +#include "Teacher.hpp" +#include "Tutor.hpp" +#include "User.hpp" +#include "sop.hpp" +#include "statistics.hpp" + +mtd::Discipline t; +mtd::Subject OMP; +std::vector declarations; +std::vector quotes + = {"Учись так, как будто тебе предстоит жить вечно.", "Знание — сила.", + "Мотивация приходит во время действия.", "Каждый день — шанс стать лучше.", + "Не бойся ошибаться — бойся не попробовать."}; + +TgBot::InlineKeyboardMarkup::Ptr CompleteButton() { + TgBot::InlineKeyboardMarkup::Ptr keyboard(new TgBot::InlineKeyboardMarkup); + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = "Завершить"; + button->callbackData = "complete_button"; + keyboard->inlineKeyboard.push_back({button}); + return keyboard; +} + +TgBot::InlineKeyboardMarkup::Ptr get_raiting_scale() { + TgBot::InlineKeyboardMarkup::Ptr keyboard(new TgBot::InlineKeyboardMarkup); + // Создаем кнопки от 1 до 10 + for (int i = 1; i < 11; ++i) { + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = std::to_string(i); + button->callbackData = std::to_string(i); // Колбек-данные соответствуют цифре + keyboard->inlineKeyboard.push_back({button}); // Добавляем кнопки в строку клавиатуры + } + TgBot::InlineKeyboardButton::Ptr button(new TgBot::InlineKeyboardButton); + button->text = "Не моя группа"; + button->callbackData = "-1"; + keyboard->inlineKeyboard.push_back({button}); + return keyboard; +} + +void StudentCallBackQuery(TgBot::Bot &bot, TgBot::CallbackQuery::Ptr &query, + std::shared_ptr user) { + int64_t ChatId = user->id(); + if (query->data == "1" || query->data == "2" || query->data == "3" || query->data == "4" + || query->data == "5" || query->data == "6" || query->data == "7" || query->data == "8" + || query->data == "9" || query->data == "10" || query->data == "-1") { + if (user->GetState() == mtd::UserState::STUDENT_SOP) { + if (user->GetStep() == 0) { // оценка лектора + user->feedback.grade = std::stoi(query->data); + bot.getApi().sendMessage(ChatId, "вы оценили лекции на " + query->data + + "\nНапишите, что вам нравится в лекциях"); + user->GetStep()++; + } else if (user->GetStep() == 3) { + user->feedback.grade = std::stoi(query->data); + user->GetStep()++; + bot.getApi().sendMessage(ChatId, "Оцените домашку", 0, 0, get_raiting_scale()); + } else if (user->GetStep() == 4) { + user->feedback.grade_home_work = std::stoi(query->data); + user->GetStep()++; + bot.getApi().sendMessage(ChatId, "Что нравится"); + } + } + } + + //--------------------------------- + //--------------------------------- + if (query->data == "student_buttons") { + bot.getApi().sendMessage(ChatId, "fsfsdf", 0, 0, user->GetInlineKeyboard()); + } else if (query->data == "student_sop" && user->GetStep() == 0) { + bot.getApi().sendMessage(ChatId, + "Оцените лекции по " + t.name_subject + " (" + t.lector + ")", 0, + 0, get_raiting_scale()); + user->GetState() = mtd::UserState::STUDENT_SOP; + } else if (query->data == "student_time_table") { + bot.getApi().sendMessage(ChatId, "Ссылка на расписание", 0, 0, user->GetInlineKeyboard()); + } else if (query->data == "student_declaration") { + std::string result_string; + for (auto declaration : declarations) { + result_string += declaration + "\n"; + } + if (result_string.empty()) { + result_string = "Объявлений нет"; + } + bot.getApi().sendMessage(ChatId, result_string, 0, 0, user->BackButton()); + } else if (query->data == "student_connect_with_teacher") { + bot.getApi().sendMessage(ChatId, "Списочек с контактами преподавателя", 0, 0, + user->GetInlineKeyboard()); + } else if (query->data == "student_help") { + bot.getApi().sendMessage(ChatId, "Помощь", 0, 0, user->GetInlineKeyboard()); + } else if (query->data == "student_back") { + bot.getApi().sendMessage(ChatId, "Меню", 0, 0, user->GetMenu()); + } else if (query->data == "student_information") { + bot.getApi().sendMessage(ChatId, "Какая-то информация", 0, 0, user->BackButton()); + } +} + +void OfficeStaffCallBackQuery(TgBot::Bot &bot, TgBot::CallbackQuery::Ptr &query, + std::shared_ptr user) { + int64_t ChatId = user->id(); + if (query->data == "office_staff_buttons") { + bot.getApi().sendMessage(ChatId, "Кнопочки", 0, 0, user->GetInlineKeyboard()); + } else if (query->data == "office_staff_time_table") { + bot.getApi().sendMessage(ChatId, "Ссылка на расписание для office staff", 0, 0, + user->GetInlineKeyboard()); + } else if (query->data == "office_staff_declaration") { + bot.getApi().sendMessage(ChatId, "Актуальные объявления для office staff", 0, 0, + user->GetInlineKeyboard()); + } else if (query->data == "office_staff_connect_with_teacher") { + bot.getApi().sendMessage(ChatId, "Списочек с контактами преподавателя для office staff", 0, + 0, user->GetInlineKeyboard()); + } else if (query->data == "office_staff_help") { + bot.getApi().sendMessage(ChatId, "Помощь для office staff", 0, 0, + user->GetInlineKeyboard()); + } else if (query->data == "office_staff_back") { + bot.getApi().sendMessage(ChatId, "Меню для office staff", 0, 0, user->GetMenu()); + } else if (query->data == "office_staff_information") { + bot.getApi().sendMessage(ChatId, "Какая-то информация для office staff", 0, 0, + user->BackButton()); + } else if (query->data == "office_add_info") { + bot.getApi().sendMessage(ChatId, "Здесь вы можете ввести информацию", 0, 0, + user->GetInlineKeyboard()); + } +} + +void TeacherCallBackQuery(TgBot::Bot &bot, TgBot::CallbackQuery::Ptr &query, + std::shared_ptr user) { + int64_t ChatId = user->id(); + if (query->data == "teacher_declaration") { + bot.getApi().sendMessage(ChatId, "Вы преподаватель и вы можите сделать объявление", 0, 0, + user->BackButton()); + } else if (query->data == "teacher_back") { + bot.getApi().sendMessage(ChatId, "Меню", 0, 0, user->GetMenu()); + } else if (query->data == "teacher_buttons") { + bot.getApi().sendMessage(ChatId, "Кнопочки для преподов", 0, 0, user->GetInlineKeyboard()); + } else if (query->data == "teachert_information") { + bot.getApi().sendMessage(ChatId, "Какая-то полезная инва для преподов", 0, 0, + user->BackButton()); + } +} + +void TutorCallBackQuery(TgBot::Bot &bot, TgBot::CallbackQuery::Ptr &query, + std::shared_ptr user) { + int64_t ChatId = user->id(); + if (query->data == "tutor_buttons") { + bot.getApi().sendMessage(ChatId, "fsdf", 0, 0, user->GetInlineKeyboard()); + } else if (query->data == "tutor_add_subject") { + bot.getApi().sendMessage(ChatId, "Введите название нового предмета", 0, 0, + user->GetInlineKeyboard()); + } else if (query->data == "tutor_view_sop") { + bot.getApi().sendMessage(ChatId, generate_statistics(OMP), 0, 0, user->GetMenu()); + + } else if (query->data == "tutor_add_people") { + bot.getApi().sendMessage(ChatId, "Введите ID студентов и групп, которых нужно добавить", 0, + 0, user->GetInlineKeyboard()); + } else if (query->data == "tutor_create_sop") { + user->GetStep() = 0; + user->GetState() = mtd::UserState::CREATE_SOP; + bot.getApi().sendMessage(ChatId, "Название предмета"); + } else if (query->data == "tutor_back") { + bot.getApi().sendMessage(ChatId, "Меню", 0, 0, user->GetMenu()); + } else if (query->data == "tutor_add_declaration") { + bot.getApi().sendMessage(ChatId, "Введите объявление"); + user->GetState() = mtd::UserState::TUTOR_ADD_DECLARATION; + } +} + +int main() { + OMP.name_subject = "ОМП"; + TgBot::Bot bot("7472835556:AAGGxuQuWDgYb9rskK3tn7YG660YEg7OgKM"); + std::map> users; + std::set NewUsers; + std::mutex MutexForUsers; + + bot.getEvents().onCommand( + "start", [&bot, &users, &MutexForUsers, &NewUsers](TgBot::Message::Ptr message) { + std::lock_guard lock(MutexForUsers); + NewUsers.insert(message->chat->id); + std::cout << "User connect: " << message->chat->id << '\n'; + TgBot::InlineKeyboardMarkup::Ptr keyboard(new TgBot::InlineKeyboardMarkup); + TgBot::InlineKeyboardButton::Ptr b1(new TgBot::InlineKeyboardButton); + b1->text = "Студент"; + b1->callbackData = "student"; + + TgBot::InlineKeyboardButton::Ptr b2(new TgBot::InlineKeyboardButton); + b2->text = "Очебный офис"; + b2->callbackData = "office_staff"; + + TgBot::InlineKeyboardButton::Ptr b3(new TgBot::InlineKeyboardButton); + b3->text = "Преподаватель"; + b3->callbackData = "teacher"; + + TgBot::InlineKeyboardButton::Ptr b4(new TgBot::InlineKeyboardButton); + b4->text = "Куратор"; + b4->callbackData = "tutor"; + + keyboard->inlineKeyboard.push_back({b1}); + keyboard->inlineKeyboard.push_back({b2}); + keyboard->inlineKeyboard.push_back({b3}); + keyboard->inlineKeyboard.push_back({b4}); + + bot.getApi().sendMessage(message->chat->id, "Кто ты?", 0, 0, keyboard); + }); + + bot.getEvents().onCallbackQuery( + [&bot, &users, &MutexForUsers, &NewUsers](TgBot::CallbackQuery::Ptr query) { + std::lock_guard lock(MutexForUsers); + int64_t ChatId = query->message->chat->id; + + if (NewUsers.find(ChatId) != NewUsers.end()) { + if (query->data == "student") { + auto student_ptr = std::make_shared(ChatId); + users.insert({ChatId, student_ptr}); + } else if (query->data == "office_staff") { + auto office_staff_ptr = std::make_shared(ChatId); + users.insert({ChatId, office_staff_ptr}); + } else if (query->data == "teacher") { + auto teacher_ptr = std::make_shared(ChatId); + users.insert({ChatId, teacher_ptr}); + } else if (query->data == "tutor") { + auto tutor_ptr = std::make_shared(ChatId); + users.insert({ChatId, tutor_ptr}); + } + + bot.getApi().sendMessage(ChatId, "Меню", 0, 0, users[ChatId]->GetMenu()); + NewUsers.erase(ChatId); + return; + } + if (users.find(ChatId) == users.end()) { + std::cout << "=== Error ===\n"; + return; + } + auto &user = users[ChatId]; + + if (query->data == "complete_button") { + std::cout << "sfdfdfdf\n"; + bot.getApi().sendMessage(ChatId, "_", 0, 0, user->GetMenu()); + return; + } + + if (user->GetRole() == mtd::UserRole::STUDENT) { + StudentCallBackQuery(bot, query, user); + } else if (user->GetRole() == mtd::UserRole::OFFICE_STAFF) { + OfficeStaffCallBackQuery(bot, query, user); + } else if (user->GetRole() == mtd::UserRole::TEACHER) { + TeacherCallBackQuery(bot, query, user); + } else if (user->GetRole() == mtd::UserRole::TUTOR) { + TutorCallBackQuery(bot, query, user); + } + }); + + bot.getEvents().onAnyMessage([&bot, &users, &MutexForUsers](TgBot::Message::Ptr message) { + std::lock_guard lock(MutexForUsers); + int64_t ChatId = message->chat->id; + + if (users.find(ChatId) == users.end()) { + std::cout << "=== Error 2 ===\n"; + return; + } + auto &user = users[ChatId]; + + if (user->GetState() == mtd::UserState::TUTOR_SOP) { + int64_t student_ChatId = static_cast(std::stoll(message->text)); + std::string s; + for (const auto i : users[student_ChatId]->GetEvaluations()) { + s += std::to_string(i) + " "; + } + bot.getApi().sendMessage(ChatId, "Оценки этого студента:\n" + s, 0, 0, + user->BackButton()); + return; + } else if (user->GetState() == mtd::UserState::TUTOR_ADD_DECLARATION) { + declarations.push_back(message->text); + bot.getApi().sendMessage(ChatId, + "Объявление успешно создано и сделана рассылка пользователям", + 0, 0, user->GetMenu()); + for (const auto &iter : users) { + if (iter.first == ChatId) { + continue; + } + std::string dec = "Объвяление от " + message->chat->firstName + " " + + message->chat->lastName + ":\n" + message->text; + bot.getApi().sendMessage(iter.first, dec, 0, 0, iter.second->GetMenu()); + } + return; + } else if (user->GetState() == mtd::UserState::STUDENT_SOP) { + if (user->GetStep() == -1) { // конец + OMP.comments.push_back(message->text); + bot.getApi().sendMessage(ChatId, "Меню", 0, 0, user->GetMenu()); + } + if (user->GetStep() == 1) { + user->feedback.advantages = message->text; + bot.getApi().sendMessage(ChatId, "Что не нравится в лекциях?"); + user->GetStep()++; + } else if (user->GetStep() == 2) { + if (user->feedback.index == 0) { + user->feedback.disadvantages = message->text; + mtd::LectionFeedback q; + q.grade = user->feedback.grade; + q.advantages = user->feedback.advantages; + q.disadvantages = user->feedback.disadvantages; + OMP.lections_result.push_back(q); + } + user->GetStep()++; + if (user->feedback.index < t.seminarians.size()) { + bot.getApi().sendMessage( + ChatId, "Оцените практику с " + t.seminarians[user->feedback.index], 0, 0, + get_raiting_scale()); + } else { + user->GetStep() = -1; + bot.getApi().sendMessage(ChatId, "Введите какой-нибудь коммент"); + } + + } else if (user->GetStep() == 5) { + user->feedback.advantages = message->text; + bot.getApi().sendMessage(ChatId, "Что не нравится"); + user->GetStep()++; + } else if (user->GetStep() == 6) { + user->feedback.disadvantages = message->text; + mtd::PracticeFeedback q; + q.grade = user->feedback.grade; + q.grade_for_homework = user->feedback.grade_home_work; + q.advantages = user->feedback.advantages; + q.disadvantages = user->feedback.disadvantages; + q.name_teacher = t.seminarians[user->feedback.index]; + OMP.practice_result.push_back(q); + user->feedback.index++; + user->GetStep() = 2; + bot.getApi().sendMessage(ChatId, "Если хотите продолжить, введите что-то"); + } + } else if (user->GetState() == mtd::UserState::CREATE_SOP) { + if (user->GetStep() == 0) { + t.name_subject = message->text; + user->GetStep()++; + bot.getApi().sendMessage(ChatId, "Как зовут лектора"); + } else if (user->GetStep() == 1) { + t.lector = message->text; + user->GetStep()++; + bot.getApi().sendMessage(ChatId, "Как зовут практика", 0, 0, CompleteButton()); + } else if (user->GetStep() == 2) { + t.seminarians.push_back(message->text); + bot.getApi().sendMessage(ChatId, "Как зовут практика", 0, 0, CompleteButton()); + } + } + }); + try { + std::cout << "Bot is running...\n"; + TgBot::TgLongPoll longPoll(bot); + while (true) { + longPoll.start(); + } + } catch (const TgBot::TgException &e) { + std::cerr << "Error: " << e.what() << "\n"; + } + + return 0; +} diff --git a/source/statistics.cpp b/source/statistics.cpp new file mode 100644 index 0000000..bc1d317 --- /dev/null +++ b/source/statistics.cpp @@ -0,0 +1,71 @@ +#include "statistics.hpp" + +#include + +float calculate_average_grade(const std::vector& lections, + const std::vector& practices) { + int total_grade = 0; + int total_count = 0; + + // Суммируем оценки по лекциям + for (const auto& lection : lections) { + total_grade += lection.grade; + total_count++; + } + + // Суммируем оценки по практикам + for (const auto& practice : practices) { + total_grade += practice.grade; + total_count++; + } + + return total_count > 0 ? static_cast(total_grade) / total_count : 0.0f; +} + +std::string generate_statistics(const mtd::Subject& subject) { + std::stringstream ss; + + ss << "Статистика по предмету: " << subject.name_subject << "\n\n"; + + ss << "Лекции:\n"; + if (subject.lections_result.empty()) { + ss << "Нет отзывов по лекциям.\n"; + } else { + for (const auto& lection : subject.lections_result) { + ss << "Оценка: " << lection.grade << "\n"; + ss << "Что нравится: " << (lection.advantages.empty() ? "Нет" : lection.advantages) + << "\n"; + ss << "Что не нравится: " + << (lection.disadvantages.empty() ? "Нет" : lection.disadvantages) << "\n\n"; + } + } + + ss << "Практики:\n"; + if (subject.practice_result.empty()) { + ss << "Нет отзывов по практикам.\n"; + } else { + for (const auto& practice : subject.practice_result) { + ss << "Преподаватель: " << practice.name_teacher << "\n"; + ss << "Оценка: " << practice.grade << "\n"; + ss << "Оценка за домашку: " << practice.grade_for_homework << "\n"; + ss << "Что нравится: " << (practice.advantages.empty() ? "Нет" : practice.advantages) + << "\n"; + ss << "Что не нравится: " + << (practice.disadvantages.empty() ? "Нет" : practice.disadvantages) << "\n\n"; + } + } + + if (!subject.comments.empty()) { + ss << "Комментарии:\n"; + for (const auto& comment : subject.comments) { + ss << "- " << comment << "\n"; + } + } else { + ss << "Нет комментариев.\n"; + } + + float average_grade = calculate_average_grade(subject.lections_result, subject.practice_result); + ss << "\nСредняя оценка по предмету: " << average_grade << "\n"; + + return ss.str(); +}