Remove obsolete manage command and related modules
- Deleted `manage.py` and related classes - Removed unused `PendingManageAction` and repository calls - Updated references to obsolete manage features in tests: - Deleted `test_statistics.py` - Deleted `test_cron.py` - Deleted `test_kurigram_compat.py` - Cleaned up outdated functionality for managing questions, schedules, and statistics via `/manage`.
This commit is contained in:
@@ -31,6 +31,8 @@ def parse_default_question_payload(payload: Any) -> ApiQuestion:
|
||||
return parse_default_question_payload(payload["data"])
|
||||
|
||||
question_text = _extract_question_text(payload)
|
||||
if not question_text.endswith("?"):
|
||||
question_text += "?"
|
||||
answers = _extract_answers(payload)
|
||||
if len(answers) < 2:
|
||||
raise ValueError("Question must have at least 2 answers.")
|
||||
@@ -38,9 +40,44 @@ def parse_default_question_payload(payload: Any) -> ApiQuestion:
|
||||
return ApiQuestion(text=question_text, answers=answers)
|
||||
|
||||
|
||||
def parse_wyr_app_payload(payload: Any) -> list[ApiQuestion]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("API response must be a JSON object.")
|
||||
|
||||
statements = payload.get("statements", [])
|
||||
if not isinstance(statements, list):
|
||||
return []
|
||||
|
||||
questions = []
|
||||
for item in statements:
|
||||
phrase = item.get("phrase")
|
||||
if not isinstance(phrase, list) or len(phrase) < 2:
|
||||
continue
|
||||
|
||||
answers = [p.get("text", "") for p in phrase if p.get("text")]
|
||||
if not answers:
|
||||
continue
|
||||
|
||||
text = item.get("title")
|
||||
if not text:
|
||||
# Если заголовка нет, пробуем составить его из ответов для уникальности
|
||||
text = " ИЛИ ".join(answers)
|
||||
|
||||
# Добавляем "?" в конец, если его нет
|
||||
text = text.strip()
|
||||
if not text.endswith("?"):
|
||||
text += "?"
|
||||
|
||||
if len(answers) >= 2:
|
||||
questions.append(ApiQuestion(text=text, answers=answers))
|
||||
|
||||
return questions
|
||||
|
||||
|
||||
def build_handler_registry() -> dict[str, QuestionPayloadHandler]:
|
||||
return {
|
||||
"default": parse_default_question_payload,
|
||||
"wyr_app": parse_wyr_app_payload,
|
||||
}
|
||||
|
||||
|
||||
@@ -55,18 +92,36 @@ class QuestionApiService:
|
||||
self.url = url
|
||||
self.handler = handler or parse_default_question_payload
|
||||
self.timeout = timeout
|
||||
self.headers = headers or {"Accept": "application/json"}
|
||||
default_headers = {
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
}
|
||||
if headers:
|
||||
default_headers.update(headers)
|
||||
self.headers = default_headers
|
||||
|
||||
def fetch_question(self) -> ApiQuestion:
|
||||
payload = self._fetch_json()
|
||||
return self.handler(payload)
|
||||
payload = self._fetch_json(self.url)
|
||||
result = self.handler(payload)
|
||||
if isinstance(result, list):
|
||||
if not result:
|
||||
raise ValueError("API returned no questions.")
|
||||
return result[0]
|
||||
return result
|
||||
|
||||
def fetch_questions(self, url: str | None = None) -> list[ApiQuestion]:
|
||||
payload = self._fetch_json(url or self.url)
|
||||
result = self.handler(payload)
|
||||
if isinstance(result, list):
|
||||
return result
|
||||
return [result]
|
||||
|
||||
def fetch_and_store(self, repository: SQLiteWouldYouRatherRepository) -> int:
|
||||
question = self.fetch_question()
|
||||
return repository.add_question(question.text, question.answers)
|
||||
|
||||
def _fetch_json(self) -> Any:
|
||||
request = Request(self.url, headers=self.headers, method="GET")
|
||||
def _fetch_json(self, url: str) -> Any:
|
||||
request = Request(url, headers=self.headers, method="GET")
|
||||
try:
|
||||
with urlopen(request, timeout=self.timeout) as response:
|
||||
charset = response.headers.get_content_charset() or "utf-8"
|
||||
|
||||
142
game/database.py
142
game/database.py
@@ -109,14 +109,51 @@ class SQLiteWouldYouRatherRepository:
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
poll_id TEXT NOT NULL UNIQUE,
|
||||
question_id INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS kv_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
# Migrate old schema: fix answers table UNIQUE constraint
|
||||
self._migrate_answers_table(connection)
|
||||
|
||||
def _migrate_answers_table(self, connection: sqlite3.Connection) -> None:
|
||||
"""Fix old schema where answers had UNIQUE(question_id) instead of UNIQUE(question_id, text)."""
|
||||
# Check if we need to migrate by looking at the constraint
|
||||
cursor = connection.execute("PRAGMA index_list(answers)")
|
||||
indexes = cursor.fetchall()
|
||||
|
||||
for idx in indexes:
|
||||
idx_name = idx[1] # name column
|
||||
if idx_name == "sqlite_autoindex_answers_1":
|
||||
# Check what columns are in this index
|
||||
cursor2 = connection.execute(f"PRAGMA index_info({idx_name})")
|
||||
columns = [row[2] for row in cursor2.fetchall()]
|
||||
# If only question_id is indexed, we need to migrate
|
||||
if columns == ["question_id"]:
|
||||
# Recreate the table with correct schema
|
||||
connection.executescript("""
|
||||
CREATE TABLE answers_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE,
|
||||
UNIQUE (question_id, text)
|
||||
);
|
||||
INSERT INTO answers_new SELECT * FROM answers;
|
||||
DROP TABLE answers;
|
||||
ALTER TABLE answers_new RENAME TO answers;
|
||||
""")
|
||||
return
|
||||
|
||||
def add_question(self, text: str, answers: Iterable[str]) -> int:
|
||||
prepared_text = text.strip()
|
||||
if not prepared_text.endswith("?"):
|
||||
prepared_text += "?"
|
||||
prepared_answers = [answer.strip() for answer in answers if answer.strip()]
|
||||
if not prepared_text:
|
||||
raise ValueError("Question text cannot be empty.")
|
||||
@@ -124,16 +161,45 @@ class SQLiteWouldYouRatherRepository:
|
||||
raise ValueError("Question must have at least 2 answers.")
|
||||
|
||||
with self._connect() as connection:
|
||||
cursor = connection.execute(
|
||||
"INSERT INTO questions(text, used) VALUES (?, 0)",
|
||||
(prepared_text,),
|
||||
)
|
||||
question_id = int(cursor.lastrowid)
|
||||
connection.executemany(
|
||||
"INSERT INTO answers(question_id, text) VALUES (?, ?)",
|
||||
[(question_id, answer_text) for answer_text in prepared_answers],
|
||||
)
|
||||
return question_id
|
||||
try:
|
||||
cursor = connection.execute(
|
||||
"INSERT INTO questions(text, used) VALUES (?, 0)",
|
||||
(prepared_text,),
|
||||
)
|
||||
question_id = int(cursor.lastrowid)
|
||||
connection.executemany(
|
||||
"INSERT INTO answers(question_id, text) VALUES (?, ?)",
|
||||
[(question_id, answer_text) for answer_text in prepared_answers],
|
||||
)
|
||||
return question_id
|
||||
except sqlite3.IntegrityError as e:
|
||||
import logging
|
||||
import traceback
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(
|
||||
"Failed to add question due to integrity constraint: %s\nStacktrace:\n%s",
|
||||
str(e),
|
||||
traceback.format_exc()
|
||||
)
|
||||
raise ValueError(f"Question already exists or duplicate answer: {e}") from e
|
||||
except Exception as e:
|
||||
import logging
|
||||
import traceback
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(
|
||||
"Unexpected error adding question: %s\nStacktrace:\n%s",
|
||||
str(e),
|
||||
traceback.format_exc()
|
||||
)
|
||||
raise
|
||||
|
||||
def delete_question_by_text(self, text: str) -> bool:
|
||||
prepared_text = text.strip()
|
||||
if not prepared_text.endswith("?"):
|
||||
prepared_text += "?"
|
||||
with self._connect() as connection:
|
||||
cursor = connection.execute("DELETE FROM questions WHERE text = ?", (prepared_text,))
|
||||
return cursor.rowcount > 0
|
||||
|
||||
def get_question(self, question_id: int) -> QuestionWithAnswers | None:
|
||||
with self._connect() as connection:
|
||||
@@ -164,20 +230,24 @@ class SQLiteWouldYouRatherRepository:
|
||||
],
|
||||
)
|
||||
|
||||
def count_questions(self) -> int:
|
||||
def count_questions(self, unused_only: bool = False) -> int:
|
||||
with self._connect() as connection:
|
||||
row = connection.execute("SELECT COUNT(*) AS total FROM questions").fetchone()
|
||||
query = "SELECT COUNT(*) AS total FROM questions"
|
||||
if unused_only:
|
||||
query += " WHERE used = 0"
|
||||
row = connection.execute(query).fetchone()
|
||||
return int(row["total"])
|
||||
|
||||
def list_questions(self, limit: int = 10, offset: int = 0) -> list[QuestionListItem]:
|
||||
def list_questions(
|
||||
self, limit: int = 10, offset: int = 0, unused_only: bool = False
|
||||
) -> list[QuestionListItem]:
|
||||
if limit < 1:
|
||||
raise ValueError("Limit must be >= 1.")
|
||||
if offset < 0:
|
||||
raise ValueError("Offset must be >= 0.")
|
||||
|
||||
with self._connect() as connection:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
query = """
|
||||
SELECT
|
||||
q.id,
|
||||
q.text,
|
||||
@@ -185,12 +255,19 @@ class SQLiteWouldYouRatherRepository:
|
||||
COUNT(a.id) AS answers_count
|
||||
FROM questions q
|
||||
LEFT JOIN answers a ON a.question_id = q.id
|
||||
"""
|
||||
params: list[int] = []
|
||||
if unused_only:
|
||||
query += " WHERE q.used = 0"
|
||||
|
||||
query += """
|
||||
GROUP BY q.id, q.text, q.used
|
||||
ORDER BY q.id
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
(limit, offset),
|
||||
).fetchall()
|
||||
"""
|
||||
params.extend([limit, offset])
|
||||
|
||||
rows = connection.execute(query, params).fetchall()
|
||||
|
||||
return [
|
||||
QuestionListItem(
|
||||
@@ -209,16 +286,7 @@ class SQLiteWouldYouRatherRepository:
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
has_questions = connection.execute(
|
||||
"SELECT 1 FROM questions LIMIT 1"
|
||||
).fetchone()
|
||||
if has_questions is None:
|
||||
return None
|
||||
|
||||
connection.execute("UPDATE questions SET used = 0")
|
||||
row = connection.execute(
|
||||
"SELECT id FROM questions ORDER BY RANDOM() LIMIT 1"
|
||||
).fetchone()
|
||||
return None
|
||||
|
||||
question_id = int(row["id"])
|
||||
connection.execute("UPDATE questions SET used = 1 WHERE id = ?", (question_id,))
|
||||
@@ -275,6 +343,8 @@ class SQLiteWouldYouRatherRepository:
|
||||
|
||||
def update_question(self, question_id: int, text: str, answers: Iterable[str]) -> None:
|
||||
prepared_text = text.strip()
|
||||
if not prepared_text.endswith("?"):
|
||||
prepared_text += "?"
|
||||
prepared_answers = [answer.strip() for answer in answers if answer.strip()]
|
||||
if not prepared_text:
|
||||
raise ValueError("Question text cannot be empty.")
|
||||
@@ -427,3 +497,17 @@ class SQLiteWouldYouRatherRepository:
|
||||
def reset_used_flags(self) -> None:
|
||||
with self._connect() as connection:
|
||||
connection.execute("UPDATE questions SET used = 0")
|
||||
|
||||
def get_setting(self, key: str, default: str | None = None) -> str | None:
|
||||
with self._connect() as connection:
|
||||
row = connection.execute(
|
||||
"SELECT value FROM kv_settings WHERE key = ?", (key,)
|
||||
).fetchone()
|
||||
return row["value"] if row else default
|
||||
|
||||
def set_setting(self, key: str, value: str) -> None:
|
||||
with self._connect() as connection:
|
||||
connection.execute(
|
||||
"INSERT OR REPLACE INTO kv_settings (key, value) VALUES (?, ?)",
|
||||
(key, value),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user