Разработка программного модуля информационной системы «Игра «Собачья академия» #3
13 changed files with 637 additions and 51 deletions
|
|
@ -23,3 +23,6 @@ DATABASE_URL = "sqlite:///database/DogAcademy.db" # Обновлено на п
|
||||||
|
|
||||||
# Иконки
|
# Иконки
|
||||||
SETTINGS_IMG = "assets/settings.png"
|
SETTINGS_IMG = "assets/settings.png"
|
||||||
|
|
||||||
|
# Уровни уведомлений (для дальнейшей настройки)
|
||||||
|
NOTIFICATION_LEVEL = "info" # Возможные значения: "info", "warning", "error"
|
||||||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
from database.db_session import get_session
|
from database.db_session import get_session
|
||||||
from database.models import Auth
|
from database.models import Auth, Notifications, Users
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
def create_user(login, password):
|
def create_user(login, password):
|
||||||
|
|
@ -26,3 +26,20 @@ def check_user(login, password):
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def log_db_event(event_message, root):
|
||||||
|
# Логирование события с базы данных
|
||||||
|
try:
|
||||||
|
# Пример добавления события в лог
|
||||||
|
with open('logs/database_logs.txt', 'a') as log_file:
|
||||||
|
log_file.write(event_message + "\n")
|
||||||
|
|
||||||
|
# Уведомление для администратора
|
||||||
|
notification = Notifications(root)
|
||||||
|
notification.show_info("Событие", f"Событие успешно записано: {event_message}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Если ошибка при записи в лог
|
||||||
|
notification = Notifications(root)
|
||||||
|
notification.show_error("Ошибка", f"Ошибка при записи в лог: {str(e)}")
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
# database/db_session.py
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from config import DATABASE_URL
|
from config import DATABASE_URL
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from sqlalchemy import Column, Integer, String, ForeignKey, Text
|
from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
@ -26,6 +27,8 @@ class Users(Base):
|
||||||
# Связи
|
# Связи
|
||||||
auth = relationship("Auth", back_populates="user") # Обратная связь с Auth
|
auth = relationship("Auth", back_populates="user") # Обратная связь с Auth
|
||||||
dog = relationship("Dogs", back_populates="users") # Связь с таблицей Dogs
|
dog = relationship("Dogs", back_populates="users") # Связь с таблицей Dogs
|
||||||
|
game_sessions = relationship("GameSession", back_populates="user") # Связь с таблицей GameSession
|
||||||
|
notifications = relationship("Notifications", back_populates="user") # Связь с уведомлениями
|
||||||
|
|
||||||
|
|
||||||
class Dogs(Base):
|
class Dogs(Base):
|
||||||
|
|
@ -47,9 +50,36 @@ class Questions(Base):
|
||||||
__tablename__ = 'questions'
|
__tablename__ = 'questions'
|
||||||
question_id = Column(Integer, primary_key=True)
|
question_id = Column(Integer, primary_key=True)
|
||||||
dog_id = Column(Integer, ForeignKey('dogs.dog_id'))
|
dog_id = Column(Integer, ForeignKey('dogs.dog_id'))
|
||||||
question_text = Column(Text, nullable=False)
|
question_text = Column(Text, nullable=False) # Исправлено поле
|
||||||
image_url = Column(String)
|
image_url = Column(String)
|
||||||
helpful_info = Column(Text)
|
helpful_info = Column(Text)
|
||||||
|
incorrect_attempts = Column(Integer, default=0)
|
||||||
|
|
||||||
# Связь с таблицей Dogs
|
# Связь с таблицей Dogs
|
||||||
dog = relationship("Dogs", back_populates="questions")
|
dog = relationship("Dogs", back_populates="questions")
|
||||||
|
|
||||||
|
|
||||||
|
class GameSession(Base):
|
||||||
|
__tablename__ = 'game_sessions'
|
||||||
|
session_id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey('users.user_id'))
|
||||||
|
level = Column(Integer, nullable=False)
|
||||||
|
score = Column(Integer, default=0)
|
||||||
|
duration = Column(Integer) # Время игры в секундах
|
||||||
|
start_time = Column(DateTime, default=func.now()) # Исправлено
|
||||||
|
end_time = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
# Связь с таблицей Users
|
||||||
|
user = relationship("Users", back_populates="game_sessions")
|
||||||
|
|
||||||
|
class Notifications(Base):
|
||||||
|
__tablename__ = 'notifications'
|
||||||
|
notification_id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey('users.user_id'))
|
||||||
|
message = Column(Text, nullable=False)
|
||||||
|
timestamp = Column(DateTime, default=func.now())
|
||||||
|
is_read = Column(Integer, default=0) # 0 - не прочитано, 1 - прочитано
|
||||||
|
|
||||||
|
# Связь с таблицей Users
|
||||||
|
user = relationship("Users", back_populates="notifications")
|
||||||
|
|
||||||
|
|
|
||||||
BIN
ishodniki/admin_panel.psd
Normal file
BIN
ishodniki/admin_panel.psd
Normal file
Binary file not shown.
329
logs/logfile.log
Normal file
329
logs/logfile.log
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
2024-11-20 12:59:15 - BEGIN (implicit)
|
||||||
|
2024-11-20 12:59:15 - SELECT count(*) AS count_1
|
||||||
|
FROM (SELECT users.user_id AS users_user_id, users.dog_id AS users_dog_id, users.username AS users_username, users.level AS users_level, users.achievement AS users_achievement
|
||||||
|
FROM users) AS anon_1
|
||||||
|
2024-11-20 12:59:15 - [generated in 0.00054s] ()
|
||||||
|
2024-11-20 12:59:15 - SELECT game_sessions.level AS game_sessions_level, count(game_sessions.session_id) AS count_1
|
||||||
|
FROM game_sessions GROUP BY game_sessions.level
|
||||||
|
2024-11-20 12:59:15 - [generated in 0.00030s] ()
|
||||||
|
2024-11-20 13:00:56 - BEGIN (implicit)
|
||||||
|
2024-11-20 13:00:56 - PRAGMA main.table_info("auth")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA temp.table_info("auth")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA main.table_info("users")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA temp.table_info("users")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA main.table_info("dogs")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA temp.table_info("dogs")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA main.table_info("questions")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA temp.table_info("questions")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA main.table_info("game_sessions")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 - PRAGMA temp.table_info("game_sessions")
|
||||||
|
2024-11-20 13:00:56 - [raw sql] ()
|
||||||
|
2024-11-20 13:00:56 -
|
||||||
|
CREATE TABLE auth (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
login VARCHAR NOT NULL,
|
||||||
|
password VARCHAR NOT NULL,
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
UNIQUE (login)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:00:56 - [no key 0.00011s] ()
|
||||||
|
2024-11-20 13:00:56 -
|
||||||
|
CREATE TABLE dogs (
|
||||||
|
dog_id INTEGER NOT NULL,
|
||||||
|
breed VARCHAR,
|
||||||
|
characteristics TEXT,
|
||||||
|
behavior TEXT,
|
||||||
|
care_info TEXT,
|
||||||
|
admin_comments TEXT,
|
||||||
|
PRIMARY KEY (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:00:56 - [no key 0.00017s] ()
|
||||||
|
2024-11-20 13:00:56 -
|
||||||
|
CREATE TABLE users (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
dog_id INTEGER,
|
||||||
|
username VARCHAR NOT NULL,
|
||||||
|
level INTEGER,
|
||||||
|
achievement TEXT,
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES auth (user_id),
|
||||||
|
FOREIGN KEY(dog_id) REFERENCES dogs (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:00:56 - [no key 0.00019s] ()
|
||||||
|
2024-11-20 13:00:56 -
|
||||||
|
CREATE TABLE questions (
|
||||||
|
question_id INTEGER NOT NULL,
|
||||||
|
dog_id INTEGER,
|
||||||
|
question_text TEXT NOT NULL,
|
||||||
|
image_url VARCHAR,
|
||||||
|
helpful_info TEXT,
|
||||||
|
incorrect_attempts INTEGER,
|
||||||
|
PRIMARY KEY (question_id),
|
||||||
|
FOREIGN KEY(dog_id) REFERENCES dogs (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:00:56 - [no key 0.00017s] ()
|
||||||
|
2024-11-20 13:00:57 -
|
||||||
|
CREATE TABLE game_sessions (
|
||||||
|
session_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
level INTEGER NOT NULL,
|
||||||
|
score INTEGER,
|
||||||
|
duration INTEGER,
|
||||||
|
start_time DATETIME,
|
||||||
|
end_time DATETIME,
|
||||||
|
PRIMARY KEY (session_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:00:57 - [no key 0.00023s] ()
|
||||||
|
2024-11-20 13:00:57 - COMMIT
|
||||||
|
2024-11-20 13:01:13 - BEGIN (implicit)
|
||||||
|
2024-11-20 13:01:13 - SELECT count(*) AS count_1
|
||||||
|
FROM (SELECT users.user_id AS users_user_id, users.dog_id AS users_dog_id, users.username AS users_username, users.level AS users_level, users.achievement AS users_achievement
|
||||||
|
FROM users) AS anon_1
|
||||||
|
2024-11-20 13:01:13 - [generated in 0.00032s] ()
|
||||||
|
2024-11-20 13:01:13 - SELECT game_sessions.level AS game_sessions_level, count(game_sessions.session_id) AS count_1
|
||||||
|
FROM game_sessions GROUP BY game_sessions.level
|
||||||
|
2024-11-20 13:01:13 - [generated in 0.00025s] ()
|
||||||
|
2024-11-20 13:01:13 - SELECT questions.question_text AS questions_question_text, questions.incorrect_attempts AS questions_incorrect_attempts
|
||||||
|
FROM questions ORDER BY questions.incorrect_attempts DESC
|
||||||
|
2024-11-20 13:01:13 - [generated in 0.00028s] ()
|
||||||
|
2024-11-20 13:01:13 - SELECT avg(game_sessions.duration) AS avg_1
|
||||||
|
FROM game_sessions
|
||||||
|
2024-11-20 13:01:13 - [generated in 0.00023s] ()
|
||||||
|
2024-11-20 13:01:13 - ROLLBACK
|
||||||
|
2024-11-20 13:01:13 - BEGIN (implicit)
|
||||||
|
2024-11-20 13:01:13 - SELECT game_sessions.start_time AS game_sessions_start_time
|
||||||
|
FROM game_sessions
|
||||||
|
2024-11-20 13:01:13 - [generated in 0.00028s] ()
|
||||||
|
2024-11-20 13:01:13 - ROLLBACK
|
||||||
|
2024-11-20 13:40:04 - BEGIN (implicit)
|
||||||
|
2024-11-20 13:40:04 - PRAGMA main.table_info("auth")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA temp.table_info("auth")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA main.table_info("users")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA temp.table_info("users")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA main.table_info("dogs")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA temp.table_info("dogs")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA main.table_info("questions")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA temp.table_info("questions")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA main.table_info("game_sessions")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA temp.table_info("game_sessions")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA main.table_info("notifications")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 - PRAGMA temp.table_info("notifications")
|
||||||
|
2024-11-20 13:40:04 - [raw sql] ()
|
||||||
|
2024-11-20 13:40:04 -
|
||||||
|
CREATE TABLE auth (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
login VARCHAR NOT NULL,
|
||||||
|
password VARCHAR NOT NULL,
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
UNIQUE (login)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:40:04 - [no key 0.00011s] ()
|
||||||
|
2024-11-20 13:40:04 -
|
||||||
|
CREATE TABLE dogs (
|
||||||
|
dog_id INTEGER NOT NULL,
|
||||||
|
breed VARCHAR,
|
||||||
|
characteristics TEXT,
|
||||||
|
behavior TEXT,
|
||||||
|
care_info TEXT,
|
||||||
|
admin_comments TEXT,
|
||||||
|
PRIMARY KEY (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:40:04 - [no key 0.00019s] ()
|
||||||
|
2024-11-20 13:40:04 -
|
||||||
|
CREATE TABLE users (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
dog_id INTEGER,
|
||||||
|
username VARCHAR NOT NULL,
|
||||||
|
level INTEGER,
|
||||||
|
achievement TEXT,
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES auth (user_id),
|
||||||
|
FOREIGN KEY(dog_id) REFERENCES dogs (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:40:04 - [no key 0.00023s] ()
|
||||||
|
2024-11-20 13:40:04 -
|
||||||
|
CREATE TABLE questions (
|
||||||
|
question_id INTEGER NOT NULL,
|
||||||
|
dog_id INTEGER,
|
||||||
|
question_text TEXT NOT NULL,
|
||||||
|
image_url VARCHAR,
|
||||||
|
helpful_info TEXT,
|
||||||
|
incorrect_attempts INTEGER,
|
||||||
|
PRIMARY KEY (question_id),
|
||||||
|
FOREIGN KEY(dog_id) REFERENCES dogs (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:40:04 - [no key 0.00020s] ()
|
||||||
|
2024-11-20 13:40:04 -
|
||||||
|
CREATE TABLE game_sessions (
|
||||||
|
session_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
level INTEGER NOT NULL,
|
||||||
|
score INTEGER,
|
||||||
|
duration INTEGER,
|
||||||
|
start_time DATETIME,
|
||||||
|
end_time DATETIME,
|
||||||
|
PRIMARY KEY (session_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:40:04 - [no key 0.00020s] ()
|
||||||
|
2024-11-20 13:40:05 -
|
||||||
|
CREATE TABLE notifications (
|
||||||
|
notification_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
timestamp DATETIME,
|
||||||
|
is_read INTEGER,
|
||||||
|
PRIMARY KEY (notification_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 13:40:05 - [no key 0.00017s] ()
|
||||||
|
2024-11-20 13:40:05 - COMMIT
|
||||||
|
2024-11-20 17:07:30 - BEGIN (implicit)
|
||||||
|
2024-11-20 17:07:30 - PRAGMA main.table_info("auth")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA temp.table_info("auth")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA main.table_info("users")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA temp.table_info("users")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA main.table_info("dogs")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA temp.table_info("dogs")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA main.table_info("questions")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA temp.table_info("questions")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA main.table_info("game_sessions")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA temp.table_info("game_sessions")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA main.table_info("notifications")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 - PRAGMA temp.table_info("notifications")
|
||||||
|
2024-11-20 17:07:30 - [raw sql] ()
|
||||||
|
2024-11-20 17:07:30 -
|
||||||
|
CREATE TABLE auth (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
login VARCHAR NOT NULL,
|
||||||
|
password VARCHAR NOT NULL,
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
UNIQUE (login)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 17:07:30 - [no key 0.00010s] ()
|
||||||
|
2024-11-20 17:07:30 -
|
||||||
|
CREATE TABLE dogs (
|
||||||
|
dog_id INTEGER NOT NULL,
|
||||||
|
breed VARCHAR,
|
||||||
|
characteristics TEXT,
|
||||||
|
behavior TEXT,
|
||||||
|
care_info TEXT,
|
||||||
|
admin_comments TEXT,
|
||||||
|
PRIMARY KEY (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 17:07:30 - [no key 0.00025s] ()
|
||||||
|
2024-11-20 17:07:30 -
|
||||||
|
CREATE TABLE users (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
dog_id INTEGER,
|
||||||
|
username VARCHAR NOT NULL,
|
||||||
|
level INTEGER,
|
||||||
|
achievement TEXT,
|
||||||
|
PRIMARY KEY (user_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES auth (user_id),
|
||||||
|
FOREIGN KEY(dog_id) REFERENCES dogs (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 17:07:30 - [no key 0.00023s] ()
|
||||||
|
2024-11-20 17:07:30 -
|
||||||
|
CREATE TABLE questions (
|
||||||
|
question_id INTEGER NOT NULL,
|
||||||
|
dog_id INTEGER,
|
||||||
|
question_text TEXT NOT NULL,
|
||||||
|
image_url VARCHAR,
|
||||||
|
helpful_info TEXT,
|
||||||
|
incorrect_attempts INTEGER,
|
||||||
|
PRIMARY KEY (question_id),
|
||||||
|
FOREIGN KEY(dog_id) REFERENCES dogs (dog_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 17:07:30 - [no key 0.00019s] ()
|
||||||
|
2024-11-20 17:07:30 -
|
||||||
|
CREATE TABLE game_sessions (
|
||||||
|
session_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
level INTEGER NOT NULL,
|
||||||
|
score INTEGER,
|
||||||
|
duration INTEGER,
|
||||||
|
start_time DATETIME,
|
||||||
|
end_time DATETIME,
|
||||||
|
PRIMARY KEY (session_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 17:07:30 - [no key 0.00038s] ()
|
||||||
|
2024-11-20 17:07:30 -
|
||||||
|
CREATE TABLE notifications (
|
||||||
|
notification_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
timestamp DATETIME,
|
||||||
|
is_read INTEGER,
|
||||||
|
PRIMARY KEY (notification_id),
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
2024-11-20 17:07:30 - [no key 0.00018s] ()
|
||||||
|
2024-11-20 17:07:30 - COMMIT
|
||||||
|
|
@ -2,22 +2,45 @@ import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
import csv
|
import csv
|
||||||
from src.utils import clear_frame
|
from src.utils import clear_frame
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
BACKGROUND_COLOR = "#403d49"
|
||||||
|
TEXT_COLOR = "#b2acc0"
|
||||||
|
HEADER_COLOR = "#2f2b38"
|
||||||
|
BUTTON_COLOR = "#444444"
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
filename="logs/logfile.log",
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def log_action(action, user):
|
||||||
|
logging.info(f"{action} - Пользователь: {user}")
|
||||||
|
|
||||||
|
|
||||||
def show_logs(frame):
|
def show_logs(frame):
|
||||||
"""Отображение логов действий пользователей."""
|
"""Отображение логов действий пользователей в тёмной теме."""
|
||||||
clear_frame(frame)
|
clear_frame(frame)
|
||||||
tk.Label(frame, text="Логи действий", font=("Comic Sans MS", 16)).pack()
|
tk.Label(frame, text="Логи действий", font=("Comic Sans MS", 16), bg=BACKGROUND_COLOR, fg=TEXT_COLOR).pack(pady=10)
|
||||||
|
|
||||||
table = ttk.Treeview(frame, columns=("Время", "Действие", "Пользователь"), show="headings")
|
# Настройка таблицы с логами
|
||||||
|
table = ttk.Treeview(frame, columns=("Время", "Действие", "Пользователь"), show="headings", style="Dark.Treeview")
|
||||||
table.heading("Время", text="Время")
|
table.heading("Время", text="Время")
|
||||||
table.heading("Действие", text="Действие")
|
table.heading("Действие", text="Действие")
|
||||||
table.heading("Пользователь", text="Пользователь")
|
table.heading("Пользователь", text="Пользователь")
|
||||||
table.pack(fill="both", expand=True)
|
table.pack(fill="both", expand=True)
|
||||||
|
|
||||||
# Добавьте логи для примера
|
# Добавление логов для примера
|
||||||
table.insert("", "end", values=("2024-11-19 12:30", "Добавление вопроса", "admin"))
|
table.insert("", "end", values=("2024-11-19 12:30", "Добавление вопроса", "admin"))
|
||||||
table.insert("", "end", values=("2024-11-19 13:00", "Удаление пользователя", "moderator"))
|
table.insert("", "end", values=("2024-11-19 13:00", "Удаление пользователя", "moderator"))
|
||||||
|
|
||||||
|
# Применение стиля к таблице
|
||||||
|
style = ttk.Style()
|
||||||
|
style.configure("Dark.Treeview", background=BACKGROUND_COLOR, foreground=TEXT_COLOR, fieldbackground=BACKGROUND_COLOR)
|
||||||
|
style.configure("Dark.Treeview.Heading", background=HEADER_COLOR, foreground=TEXT_COLOR)
|
||||||
|
|
||||||
def export_logs():
|
def export_logs():
|
||||||
data = [
|
data = [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from database.models import Questions
|
from database.models import Questions, Notifications
|
||||||
from database.db_session import engine
|
from database.db_session import engine
|
||||||
from src.utils import clear_frame
|
from src.utils import clear_frame
|
||||||
|
|
||||||
|
|
@ -34,3 +34,19 @@ def manage_questions(frame):
|
||||||
table.insert("", "end", values=(question.id, question.text, question.answer))
|
table.insert("", "end", values=(question.id, question.text, question.answer))
|
||||||
|
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
def add_user_to_db(user_data, root):
|
||||||
|
# Логика добавления пользователя в базу данных
|
||||||
|
try:
|
||||||
|
# Пример кода для добавления в базу (зависит от реализации вашей базы данных)
|
||||||
|
# db_session.add(user_data)
|
||||||
|
# db_session.commit()
|
||||||
|
|
||||||
|
# Если добавление прошло успешно
|
||||||
|
notification = Notifications(root)
|
||||||
|
notification.show_info("Успех", f"Пользователь {user_data['username']} успешно добавлен!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Если возникла ошибка
|
||||||
|
notification = Notifications(root)
|
||||||
|
notification.show_error("Ошибка", f"Ошибка при добавлении пользователя: {str(e)}")
|
||||||
21
src/admin_functions/notification.py
Normal file
21
src/admin_functions/notification.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
|
class Notification:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
def show_info(self, title, message):
|
||||||
|
"""Отображение информационного уведомления"""
|
||||||
|
messagebox.showinfo(title, message)
|
||||||
|
|
||||||
|
def show_warning(self, title, message):
|
||||||
|
"""Отображение предупреждения"""
|
||||||
|
messagebox.showwarning(title, message)
|
||||||
|
|
||||||
|
def show_error(self, title, message):
|
||||||
|
"""Отображение ошибки"""
|
||||||
|
messagebox.showerror(title, message)
|
||||||
|
|
||||||
|
def show_notification(self, title, message):
|
||||||
|
"""Отображение общего уведомления"""
|
||||||
|
self.show_info(title, message)
|
||||||
0
src/admin_functions/security.py
Normal file
0
src/admin_functions/security.py
Normal file
|
|
@ -3,22 +3,96 @@ from tkinter import ttk
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
from src.utils import clear_frame
|
from src.utils import clear_frame
|
||||||
|
from database.db_session import get_session
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from database.models import Users, GameSession, Questions # Пример моделей
|
||||||
|
|
||||||
def show_statistics(frame):
|
def show_statistics(frame):
|
||||||
"""Отображение статистики пользователей и уровней."""
|
"""Отображение статистики."""
|
||||||
clear_frame(frame)
|
clear_frame(frame)
|
||||||
tk.Label(frame, text="Статистика пользователей", font=("Comic Sans MS", 16)).pack()
|
tk.Label(frame, text="Статистика", font=("Comic Sans MS", 16), bg="#403d49", fg="#b2acc0").pack(pady=10)
|
||||||
|
|
||||||
# Пример: график с количеством пользователей
|
# Таблица с общей статистикой
|
||||||
fig, ax = plt.subplots()
|
table = ttk.Treeview(frame, columns=("Metric", "Value"), show="headings", height=5)
|
||||||
ax.bar(["Level 1", "Level 2", "Level 3"], [10, 15, 8]) # Пример данных
|
table.heading("Metric", text="Параметр")
|
||||||
ax.set_title("Популярность уровней")
|
table.heading("Value", text="Значение")
|
||||||
ax.set_xlabel("Уровни")
|
table.pack(pady=10, padx=20, fill="x", expand=True)
|
||||||
ax.set_ylabel("Количество прохождений")
|
|
||||||
|
# Получение данных для таблицы
|
||||||
|
stats = gather_statistics()
|
||||||
|
for metric, value in stats.items():
|
||||||
|
table.insert("", tk.END, values=(metric, value))
|
||||||
|
|
||||||
|
# График активности пользователей
|
||||||
|
tk.Label(frame, text="Активность пользователей", font=("Comic Sans MS", 14), bg="#403d49", fg="#b2acc0").pack(pady=10)
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(6, 4))
|
||||||
|
time_labels, activity_values = get_user_activity()
|
||||||
|
ax.plot(time_labels, activity_values, marker="o")
|
||||||
|
ax.set_title("Активность пользователей по времени")
|
||||||
|
ax.set_xlabel("Время")
|
||||||
|
ax.set_ylabel("Количество действий")
|
||||||
|
ax.grid()
|
||||||
|
|
||||||
canvas = FigureCanvasTkAgg(fig, master=frame)
|
canvas = FigureCanvasTkAgg(fig, master=frame)
|
||||||
canvas.get_tk_widget().pack(fill="both", expand=True)
|
canvas.get_tk_widget().pack(fill="both", expand=True)
|
||||||
canvas.draw()
|
canvas.draw()
|
||||||
|
|
||||||
|
def gather_statistics():
|
||||||
|
"""Собирает основные метрики для таблицы статистики."""
|
||||||
|
session = get_session()
|
||||||
|
|
||||||
|
# Количество зарегистрированных пользователей
|
||||||
|
user_count = session.query(Users).count()
|
||||||
|
|
||||||
|
# Популярные уровни
|
||||||
|
level_data = session.query(GameSession.level, func.count(GameSession.session_id)).group_by(GameSession.level).all()
|
||||||
|
popular_levels = sorted(level_data, key=lambda x: x[1], reverse=True)[:3]
|
||||||
|
|
||||||
|
# Трудные вопросы
|
||||||
|
question_data = session.query(Questions.question_text, Questions.incorrect_attempts).order_by(Questions.incorrect_attempts.desc()).all()
|
||||||
|
hardest_questions = question_data[:3]
|
||||||
|
|
||||||
|
# Средняя продолжительность игры
|
||||||
|
avg_duration = session.query(func.avg(GameSession.duration)).scalar() or 0
|
||||||
|
|
||||||
|
# Состояние базы данных
|
||||||
|
db_size = get_database_size()
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"Количество пользователей": user_count,
|
||||||
|
"Популярные уровни": ", ".join([f"Уровень {lvl} ({cnt} раз)" for lvl, cnt in popular_levels]),
|
||||||
|
"Трудные вопросы": ", ".join([f"'{text}' ({cnt} ошибок)" for text, cnt in hardest_questions]),
|
||||||
|
"Средняя продолжительность игры": f"{avg_duration:.2f} секунд",
|
||||||
|
"Объем базы данных": f"{db_size} КБ"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_user_activity():
|
||||||
|
"""Генерирует данные для графика активности пользователей."""
|
||||||
|
session = get_session()
|
||||||
|
activity_data = session.query(GameSession.start_time).all()
|
||||||
|
|
||||||
|
activity_by_hour = {}
|
||||||
|
for time in activity_data:
|
||||||
|
hour = time.start_time.hour
|
||||||
|
activity_by_hour[hour] = activity_by_hour.get(hour, 0) + 1
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
if not activity_by_hour:
|
||||||
|
return ["Нет данных"], [0]
|
||||||
|
|
||||||
|
hours = sorted(activity_by_hour.keys())
|
||||||
|
activity = [activity_by_hour[hour] for hour in hours]
|
||||||
|
hours = [f"{hour}:00" for hour in hours]
|
||||||
|
return hours, activity
|
||||||
|
|
||||||
|
def get_database_size():
|
||||||
|
"""Возвращает размер базы данных в КБ."""
|
||||||
|
import os
|
||||||
|
db_path = "database/db.sqlite"
|
||||||
|
if os.path.exists(db_path):
|
||||||
|
return round(os.path.getsize(db_path) / 1024, 2)
|
||||||
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ from PIL import Image, ImageTk
|
||||||
from config import SETTINGS_IMG
|
from config import SETTINGS_IMG
|
||||||
from src.admin_functions import db_management, admin_logging, statistics, content, knowledge_base
|
from src.admin_functions import db_management, admin_logging, statistics, content, knowledge_base
|
||||||
from src.utils import clear_frame # Импортируем общую функцию для очистки фрейма
|
from src.utils import clear_frame # Импортируем общую функцию для очистки фрейма
|
||||||
|
from database.db_session import get_session
|
||||||
|
from database.models import Notifications
|
||||||
|
from src.admin_functions.notification import Notification
|
||||||
|
|
||||||
|
|
||||||
# Конфигурация цветов из config.py
|
# Конфигурация цветов из config.py
|
||||||
|
|
@ -14,12 +17,26 @@ MENU_COLOR = "#2f2b38"
|
||||||
MENU_OPACITY = 0.9 # Прозрачность меню
|
MENU_OPACITY = 0.9 # Прозрачность меню
|
||||||
|
|
||||||
class AdminApp:
|
class AdminApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root, master):
|
||||||
self.root = root
|
self.root = root
|
||||||
|
self.master = master
|
||||||
self.root.title("Админ-Панель")
|
self.root.title("Админ-Панель")
|
||||||
self.root.geometry("1920x1080")
|
self.root.geometry("1920x1080")
|
||||||
self.root.config(bg=BACKGROUND_COLOR)
|
self.root.config(bg=BACKGROUND_COLOR)
|
||||||
|
|
||||||
|
self.notification = Notification(self.master)
|
||||||
|
|
||||||
|
def edit_database(self):
|
||||||
|
# Логика редактирования базы данных
|
||||||
|
# Например, успешное редактирование
|
||||||
|
self.notification.show_info("Успех", "База данных успешно обновлена!")
|
||||||
|
|
||||||
|
def show_error(self, message):
|
||||||
|
self.notification.show_error("Ошибка", message)
|
||||||
|
|
||||||
|
def show_warning(self, message):
|
||||||
|
self.notification.show_warning("Предупреждение", message)
|
||||||
|
|
||||||
# Верхняя панель
|
# Верхняя панель
|
||||||
self.top_bar = tk.Frame(self.root, bg=TOP_BAR_COLOR, height=60)
|
self.top_bar = tk.Frame(self.root, bg=TOP_BAR_COLOR, height=60)
|
||||||
self.top_bar.pack(side="top", fill="x")
|
self.top_bar.pack(side="top", fill="x")
|
||||||
|
|
@ -71,43 +88,94 @@ class AdminApp:
|
||||||
|
|
||||||
def toggle_menu(self):
|
def toggle_menu(self):
|
||||||
"""Показ или скрытие меню."""
|
"""Показ или скрытие меню."""
|
||||||
print(
|
if self.menu_visible:
|
||||||
f"Кнопка меню нажата. Меню сейчас {'видимо' if self.menu_visible else 'скрыто'}") # Отладка
|
self.menu_frame.lower()
|
||||||
if self.menu_visible: # Используем флаг для проверки состояния
|
|
||||||
print("Скрываем меню") # Отладка
|
|
||||||
self.menu_frame.lower() # Скрываем меню
|
|
||||||
self.menu_visible = False
|
self.menu_visible = False
|
||||||
else:
|
else:
|
||||||
print("Показываем меню") # Отладка
|
self.menu_frame.lift()
|
||||||
self.menu_frame.lift() # Показываем меню
|
|
||||||
self.menu_visible = True
|
self.menu_visible = True
|
||||||
self.populate_menu() # Наполнение меню элементами
|
self.populate_menu()
|
||||||
|
|
||||||
def populate_menu(self):
|
def populate_menu(self):
|
||||||
# Очистка меню
|
# Очистка меню
|
||||||
for widget in self.menu_frame.winfo_children():
|
for widget in self.menu_frame.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
# Создание пунктов меню
|
|
||||||
self.create_menu_section("Работа с базой данных", [
|
# Список разделов и их элементов
|
||||||
|
menu_sections = [
|
||||||
|
("Работа с базой данных", [
|
||||||
("Редактирование пользователей", db_management.edit_users),
|
("Редактирование пользователей", db_management.edit_users),
|
||||||
("Управление вопросами", db_management.manage_questions),
|
("Управление вопросами", db_management.manage_questions),
|
||||||
("Просмотр таблиц", db_management.view_tables),
|
("Просмотр таблиц", db_management.view_tables),
|
||||||
])
|
]),
|
||||||
self.create_menu_section("Управление игровым контентом", [
|
("Управление игровым контентом", [
|
||||||
("Создание и настройка уровней", content.manage_levels),
|
("Создание и настройка уровней", content.manage_levels),
|
||||||
("Настройка параметров собаки", content.manage_dog_params),
|
("Настройка параметров собаки", content.manage_dog_params),
|
||||||
])
|
]),
|
||||||
self.create_menu_section("Управление интерфейсом пользователя", [
|
("Управление интерфейсом пользователя", [
|
||||||
("Изменение цветовой схемы, фона и логотипа", self.change_ui_settings),
|
|
||||||
("Добавление подсказок в интерфейс", self.manage_ui_tips),
|
("Добавление подсказок в интерфейс", self.manage_ui_tips),
|
||||||
])
|
]),
|
||||||
self.create_menu_section("Работа с базой знаний", [
|
("Работа с базой знаний", [
|
||||||
("Добавление информации", knowledge_base.add_info),
|
("Добавление информации", knowledge_base.add_info),
|
||||||
("Редактирование записей", knowledge_base.edit_records),
|
("Редактирование записей", knowledge_base.edit_records),
|
||||||
("Удаление записей", knowledge_base.delete_records),
|
("Удаление записей", knowledge_base.delete_records),
|
||||||
("Просмотр базы знаний", knowledge_base.view_knowledge_base),
|
("Просмотр базы знаний", knowledge_base.view_knowledge_base),
|
||||||
("Генерация вопросов", knowledge_base.generate_questions),
|
("Генерация вопросов", knowledge_base.generate_questions),
|
||||||
])
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Определяем максимальную ширину текста для настройки ширины меню и кнопок
|
||||||
|
max_text_length = max(
|
||||||
|
len(title) for title, items in menu_sections
|
||||||
|
) + max(
|
||||||
|
max(len(text) for text, _ in items) for _, items in menu_sections
|
||||||
|
)
|
||||||
|
menu_width = max(300, max_text_length * 10) # Устанавливаем минимальную ширину
|
||||||
|
|
||||||
|
# Обновляем ширину меню
|
||||||
|
self.menu_frame.config(width=menu_width)
|
||||||
|
|
||||||
|
# Высота одной кнопки и отступов
|
||||||
|
button_height = 40
|
||||||
|
button_spacing = 10
|
||||||
|
section_spacing = 15
|
||||||
|
|
||||||
|
total_height = 0
|
||||||
|
|
||||||
|
for title, items in menu_sections:
|
||||||
|
# Заголовок раздела
|
||||||
|
section_label = tk.Label(
|
||||||
|
self.menu_frame,
|
||||||
|
text=title,
|
||||||
|
bg=MENU_COLOR,
|
||||||
|
fg=TEXT_COLOR,
|
||||||
|
font=("Comic Sans MS", 14, "bold"),
|
||||||
|
anchor="center" # Выравнивание по центру
|
||||||
|
)
|
||||||
|
section_label.pack(fill="x", padx=10, pady=5)
|
||||||
|
total_height += button_height + section_spacing
|
||||||
|
|
||||||
|
# Кнопки раздела
|
||||||
|
for text, command in items:
|
||||||
|
item_button = tk.Button(
|
||||||
|
self.menu_frame,
|
||||||
|
text=text,
|
||||||
|
bg=BUTTON_COLOR,
|
||||||
|
fg=TEXT_COLOR,
|
||||||
|
font=("Comic Sans MS", 12),
|
||||||
|
width=int(menu_width / 10) - 3, # Ширина кнопок зависит от ширины меню
|
||||||
|
height=1,
|
||||||
|
activebackground=BUTTON_COLOR,
|
||||||
|
activeforeground=TEXT_COLOR,
|
||||||
|
bd=0,
|
||||||
|
anchor="w", # Выравнивание текста по левому краю
|
||||||
|
command=lambda: command(self.main_frame) # Вызываем функцию и передаём фрейм
|
||||||
|
)
|
||||||
|
item_button.pack(fill="x", padx=20, pady=5)
|
||||||
|
total_height += button_height + button_spacing
|
||||||
|
|
||||||
|
# Подстройка высоты меню
|
||||||
|
self.menu_frame.config(height=total_height)
|
||||||
|
|
||||||
def create_menu_section(self, title, items):
|
def create_menu_section(self, title, items):
|
||||||
section_label = tk.Label(self.menu_frame, text=title, bg=MENU_COLOR, fg=TEXT_COLOR, font=("Comic Sans MS", 14, "bold"))
|
section_label = tk.Label(self.menu_frame, text=title, bg=MENU_COLOR, fg=TEXT_COLOR, font=("Comic Sans MS", 14, "bold"))
|
||||||
|
|
@ -150,8 +218,14 @@ class AdminApp:
|
||||||
tk.Label(frame, text="Здесь будут подсказки для интерфейса", bg=BACKGROUND_COLOR, fg=TEXT_COLOR, font=("Comic Sans MS", 16)).pack()
|
tk.Label(frame, text="Здесь будут подсказки для интерфейса", bg=BACKGROUND_COLOR, fg=TEXT_COLOR, font=("Comic Sans MS", 16)).pack()
|
||||||
|
|
||||||
def show_notifications(self, frame):
|
def show_notifications(self, frame):
|
||||||
clear_frame(frame)
|
clear_frame(frame) # Очищаем текущий экран
|
||||||
tk.Label(frame, text="Здесь будут уведомления", bg=BACKGROUND_COLOR, fg=TEXT_COLOR, font=("Comic Sans MS", 16)).pack()
|
session = get_session()
|
||||||
|
notifications = session.query(Notifications).filter_by(
|
||||||
|
is_read=0).all() # Получаем все непрочитанные уведомления
|
||||||
|
for notification in notifications:
|
||||||
|
tk.Label(frame, text=notification.message, bg=BACKGROUND_COLOR, fg=TEXT_COLOR,
|
||||||
|
font=("Comic Sans MS", 16)).pack()
|
||||||
|
session.close()
|
||||||
|
|
||||||
def show_security(self, frame):
|
def show_security(self, frame):
|
||||||
clear_frame(frame)
|
clear_frame(frame)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue