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
14 changes: 14 additions & 0 deletions tools/migrations/26-02-16--add_translation_search.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Translation search history table
-- Tracks successful searches made in the Translation Tab for history view
-- Only logs when a translation was found (meaning exists)
CREATE TABLE translation_search (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
meaning_id INT NOT NULL,
search_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

FOREIGN KEY (user_id) REFERENCES user(id),
FOREIGN KEY (meaning_id) REFERENCES meaning(id),

INDEX idx_user_time (user_id, search_time DESC)
);
46 changes: 45 additions & 1 deletion zeeguu/api/endpoints/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from zeeguu.core.crowd_translations import (
get_own_past_translation,
)
from zeeguu.core.model import Bookmark, User, Meaning, UserWord, UserMweOverride
from zeeguu.core.model import Bookmark, User, Meaning, UserWord, UserMweOverride, TranslationSearch
from zeeguu.core.model.article import Article
from zeeguu.core.model.bookmark_context import BookmarkContext
from zeeguu.core.model.context_identifier import ContextIdentifier
Expand Down Expand Up @@ -176,6 +176,7 @@ def get_one_translation(from_lang_code, to_lang_code):
def get_multiple_translations(from_lang_code, to_lang_code):
"""
Returns a list of possible translations from multiple services.
Also saves Meaning records and logs to translation history.

:return: json array with translations from Azure, Microsoft, and Google
"""
Expand All @@ -187,9 +188,52 @@ def get_multiple_translations(from_lang_code, to_lang_code):

translations = get_all_translations(word_str, context, from_lang_code, to_lang_code, is_separated_mwe, full_sentence_context)

# Save meanings for each translation
first_meaning = None
for t in translations:
translation_text = t.get("translation", "")
if translation_text:
meaning = Meaning.find_or_create(
db_session,
word_str,
from_lang_code,
translation_text,
to_lang_code,
)
t["meaning_id"] = meaning.id
if first_meaning is None:
first_meaning = meaning

# Log search to history only if we found a translation
if first_meaning:
try:
user = User.find_by_id(flask.g.user_id)
TranslationSearch.log_search(db_session, user, first_meaning)
db_session.commit()
except Exception as e:
db_session.rollback()
zeeguu_log(f"[TRANSLATION] Failed to log search history: {e}")

return json_result(dict(translations=translations))


@api.route("/translation_history", methods=["GET"])
@cross_domain
@requires_session
def get_translation_history():
"""
Returns recent translation searches for the current user.
Used by the Translation Tab's history view.

:return: json array with recent searches
"""
user = User.find_by_id(flask.g.user_id)
limit = request.args.get("limit", 50, type=int)

searches = TranslationSearch.get_history(user, limit=limit)
return json_result([s.as_dict() for s in searches])


@api.route(
"/get_translations_stream/<from_lang_code>/<to_lang_code>", methods=["POST"]
)
Expand Down
6 changes: 6 additions & 0 deletions zeeguu/api/test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ def __init__(self, client):
print(response.data)
print(self.session)

# Mark email as verified for tests
from zeeguu.core.model import User
user = User.find(self.email)
user.email_verified = True
db_session.commit()

def append_session(self, url):
if "?" in url:
return url + "&session=" + self.session
Expand Down
3 changes: 2 additions & 1 deletion zeeguu/api/test/test_teacher_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ def test_student_does_not_have_access_to_cohort(client):
student_session = response.data.decode("utf-8")

# Ensure student user can't access /cohorts_info
# 403 Forbidden: authenticated but not authorized (not a teacher)
response = client.client.get(f"/cohorts_info?session={student_session}")
assert response.status_code == 401
assert response.status_code == 403


FRENCH_B1_COHORT = {
Expand Down
3 changes: 3 additions & 0 deletions zeeguu/core/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,6 @@
# stats caching
from .monthly_active_users_cache import MonthlyActiveUsersCache
from .monthly_activity_stats_cache import MonthlyActivityStatsCache

# translation history
from .translation_search import TranslationSearch
69 changes: 69 additions & 0 deletions zeeguu/core/model/translation_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from datetime import datetime
from sqlalchemy import desc

from zeeguu.core.model.db import db
from zeeguu.core.model.meaning import Meaning
from zeeguu.core.model.user import User


class TranslationSearch(db.Model):
"""
Tracks successful translation searches made in the Translation Tab.
Only logs searches where a translation was found (meaning exists).
"""

__tablename__ = "translation_search"

id = db.Column(db.Integer, primary_key=True)

user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False)
user = db.relationship(User)

meaning_id = db.Column(db.Integer, db.ForeignKey(Meaning.id), nullable=False)
meaning = db.relationship(Meaning)

search_time = db.Column(db.DateTime, nullable=False, default=datetime.now)

def __init__(self, user: User, meaning: Meaning):
self.user = user
self.meaning = meaning
self.search_time = datetime.now()

def __repr__(self):
return f"TranslationSearch({self.meaning.origin.content})"

@classmethod
def log_search(cls, session, user: User, meaning: Meaning):
"""
Log a translation search to history.

Note: Does not commit - caller is responsible for committing.
"""
search = cls(user=user, meaning=meaning)
session.add(search)
return search

@classmethod
def get_history(cls, user: User, limit: int = 50):
"""
Get recent translation searches for a user.
Returns most recent searches first.
"""
return (
cls.query.filter(cls.user_id == user.id)
.order_by(desc(cls.search_time))
.limit(limit)
.all()
)

def as_dict(self):
"""Return dictionary representation for API response."""
return {
"id": self.id,
"search_word": self.meaning.origin.content,
"translation": self.meaning.translation.content,
"from_language": self.meaning.origin.language.code,
"to_language": self.meaning.translation.language.code,
"meaning_id": self.meaning.id,
"search_time": self.search_time.isoformat(),
}