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:
2026-04-05 18:44:10 +02:00
parent 6c6ef263be
commit 5acad376b0
18 changed files with 1157 additions and 1068 deletions

View File

@@ -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"

View File

@@ -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),
)