Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tools/migrations/26-02-13--add_exercise_report.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE exercise_report (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
bookmark_id INT NOT NULL,
exercise_source_id INT NOT NULL,
reason ENUM('word_not_shown', 'context_confusing', 'wrong_translation', 'context_wrong', 'other') NOT NULL,
comment TEXT DEFAULT NULL,
context_used TEXT DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved BOOLEAN DEFAULT FALSE,
FOREIGN KEY (user_id) REFERENCES user(id),
FOREIGN KEY (bookmark_id) REFERENCES bookmark(id),
FOREIGN KEY (exercise_source_id) REFERENCES exercise_source(id),
UNIQUE KEY unique_user_bookmark_source (user_id, bookmark_id, exercise_source_id)
);
87 changes: 87 additions & 0 deletions zeeguu/api/endpoints/exercises.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,90 @@ def _user_words_as_json_result(user_words):
log("Failed to commit UserWord deletions")

return json_result(dicts)


# ====================================
# Report Exercise Issue
# ====================================


@api.route("/report_exercise_issue", methods=["POST"])
@cross_domain
@requires_session
def report_exercise_issue():
"""
Report a problem with an exercise.

Request body (JSON):
{
"bookmark_id": 123,
"exercise_source": "FindWordInContextCloze",
"reason": "word_not_shown", // word_not_shown, context_confusing, wrong_translation, context_wrong, other
"comment": "Optional details", // optional
"context_used": "The context sentence..." // optional
}

Response (200):
{
"success": true,
"message": "Report submitted",
"report_id": 123
}
"""
from zeeguu.logging import log
from zeeguu.core.model import ExerciseReport, ExerciseSource

user = User.find_by_id(flask.g.user_id)
data = request.get_json()

if not data:
return json_result({"error": "JSON body required"}), 400

bookmark_id = data.get("bookmark_id")
exercise_source_name = data.get("exercise_source", "").strip()
reason = data.get("reason", "").strip()
comment = (data.get("comment") or "").strip() or None
context_used = (data.get("context_used") or "").strip() or None

if not bookmark_id or not exercise_source_name or not reason:
return json_result({"error": "bookmark_id, exercise_source, and reason are required"}), 400

valid_reasons = ["word_not_shown", "context_confusing", "wrong_translation", "context_wrong", "other"]
if reason not in valid_reasons:
return json_result({"error": f"reason must be one of: {valid_reasons}"}), 400

# Find the bookmark
bookmark = Bookmark.find(bookmark_id)
if not bookmark:
return json_result({"error": "Bookmark not found"}), 404

# Find or create the exercise source
exercise_source = ExerciseSource.find_or_create(db_session, exercise_source_name)

# Check if already reported
existing = ExerciseReport.find_by_user_bookmark_source(
user.id, bookmark.id, exercise_source.id
)
if existing:
return json_result({
"success": True,
"message": "You have already reported this exercise",
"report_id": existing.id,
"already_reported": True
})

try:
report = ExerciseReport.create(
db_session, user, bookmark, exercise_source, reason, comment, context_used
)
log(f"User {user.id} reported exercise issue for bookmark {bookmark.id}: {reason}")

return json_result({
"success": True,
"message": "Thanks for the feedback!",
"report_id": report.id
})
except Exception as e:
log(f"Error creating exercise report: {e}")
traceback.print_exc()
return json_result({"error": "Failed to submit report"}), 500
1 change: 1 addition & 0 deletions zeeguu/core/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from .exercise import Exercise
from .exercise_outcome import ExerciseOutcome
from .exercise_source import ExerciseSource
from .exercise_report import ExerciseReport

# user logging
from .user_activitiy_data import UserActivityData
Expand Down
83 changes: 83 additions & 0 deletions zeeguu/core/model/exercise_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from datetime import datetime

from zeeguu.core.model.db import db


class ExerciseReport(db.Model):
"""
User reports for broken or problematic exercises.

Used to flag issues like missing word in cloze, bad translations,
confusing context, etc.
"""

__tablename__ = "exercise_report"

id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
bookmark_id = db.Column(db.Integer, db.ForeignKey("bookmark.id"), nullable=False)
exercise_source_id = db.Column(
db.Integer, db.ForeignKey("exercise_source.id"), nullable=False
)
reason = db.Column(
db.Enum(
"word_not_shown",
"context_confusing",
"wrong_translation",
"context_wrong",
"other",
),
nullable=False,
)
comment = db.Column(db.Text, nullable=True)
context_used = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.now)
resolved = db.Column(db.Boolean, default=False)

user = db.relationship("User")
bookmark = db.relationship("Bookmark", backref="exercise_reports")
exercise_source = db.relationship("ExerciseSource")

def __init__(
self, user, bookmark, exercise_source, reason, comment=None, context_used=None
):
self.user_id = user.id
self.bookmark_id = bookmark.id
self.exercise_source_id = exercise_source.id
self.reason = reason
self.comment = comment
self.context_used = context_used

@classmethod
def create(
cls, session, user, bookmark, exercise_source, reason, comment=None, context_used=None
):
report = cls(user, bookmark, exercise_source, reason, comment, context_used)
session.add(report)
session.commit()
return report

@classmethod
def find_by_user_bookmark_source(cls, user_id, bookmark_id, exercise_source_id):
return cls.query.filter_by(
user_id=user_id,
bookmark_id=bookmark_id,
exercise_source_id=exercise_source_id,
).first()

@classmethod
def count_for_bookmark(cls, bookmark_id):
return cls.query.filter_by(bookmark_id=bookmark_id, resolved=False).count()

def as_dictionary(self):
return {
"id": self.id,
"user_id": self.user_id,
"bookmark_id": self.bookmark_id,
"exercise_source_id": self.exercise_source_id,
"reason": self.reason,
"comment": self.comment,
"context_used": self.context_used,
"created_at": self.created_at.isoformat() if self.created_at else None,
"resolved": self.resolved,
}