From 156b1f700f0fde5758d483d284fc7215494a0b16 Mon Sep 17 00:00:00 2001 From: nicra Date: Wed, 25 Mar 2026 10:31:41 +0100 Subject: [PATCH 01/16] Now with gamification feature flag --- zeeguu/core/user_feature_toggles.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index 13762a59..e7a44726 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -16,6 +16,7 @@ def _feature_map(): "new_topics": _new_topics, "daily_feedback": _daily_feedback, "hide_recommendations": _hide_recommendations, + "gamification": _gamification, } @@ -78,8 +79,18 @@ def _hide_recommendations(user): return False COHORTS_WITH_HIDDEN_RECOMMENDATIONS = {564} - + # ...existing code... for user_cohort in user.cohorts: if user_cohort.cohort_id in COHORTS_WITH_HIDDEN_RECOMMENDATIONS: return True return False + +# Gamification feature flag logic +from model.user import User +def _gamification(user: User): + """ + Enable gamification features for users whose invitation code is exactly 'gamification'. + """ + GAMIFICATION_INVITE_CODE = "gamification" + return user.invitation_code == GAMIFICATION_INVITE_CODE + From 18258066e17ffeacac772c98ed6b0e3e13d6a500 Mon Sep 17 00:00:00 2001 From: nicra Date: Wed, 25 Mar 2026 10:44:19 +0100 Subject: [PATCH 02/16] Working on the gamification feature flag --- zeeguu/core/user_feature_toggles.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index e7a44726..ddd8d39c 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -86,11 +86,18 @@ def _hide_recommendations(user): return False # Gamification feature flag logic -from model.user import User +from .model.user import User def _gamification(user: User): """ Enable gamification features for users whose invitation code is exactly 'gamification'. """ - GAMIFICATION_INVITE_CODE = "gamification" + from datetime import datetime, date + GAMIFICATION_INVITE_CODE = "gamification" # I guess we can decide on the invitation code + GAMIFICATION_START_DATE = date(2026, 4, 1) # Start after the first of April 2026 + + # Start gamification features after the GAMIFICATION_START_DATE + if datetime.now().date() > GAMIFICATION_START_DATE: + return False + return user.invitation_code == GAMIFICATION_INVITE_CODE From 9ee3ff0a8d4b816b200c68fdb75173311d00da79 Mon Sep 17 00:00:00 2001 From: nicra Date: Wed, 25 Mar 2026 10:45:07 +0100 Subject: [PATCH 03/16] Deleted comment --- zeeguu/core/user_feature_toggles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index ddd8d39c..7632239a 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -79,7 +79,6 @@ def _hide_recommendations(user): return False COHORTS_WITH_HIDDEN_RECOMMENDATIONS = {564} - # ...existing code... for user_cohort in user.cohorts: if user_cohort.cohort_id in COHORTS_WITH_HIDDEN_RECOMMENDATIONS: return True From 7c1794886e9717d1178515aee932ecafc3024e72 Mon Sep 17 00:00:00 2001 From: nicra Date: Wed, 25 Mar 2026 10:58:10 +0100 Subject: [PATCH 04/16] working on feature flags for all gamification features --- zeeguu/core/user_feature_toggles.py | 42 +++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index 7632239a..3f09f380 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -17,6 +17,9 @@ def _feature_map(): "daily_feedback": _daily_feedback, "hide_recommendations": _hide_recommendations, "gamification": _gamification, + "badges": _badges, + "friends": _friends, + "leaderboards": _leaderboards, } @@ -86,17 +89,40 @@ def _hide_recommendations(user): # Gamification feature flag logic from .model.user import User + def _gamification(user: User): """ - Enable gamification features for users whose invitation code is exactly 'gamification'. + Enable general gamification features for users whose invitation code is exactly 'gamification'. + """ + return _gamification_flag_logic(user) + +def _badges(user: User): + """ + Enable badges feature for users whose invitation code is exactly 'gamification'. + """ + return _gamification_flag_logic(user) + +def _friends(user: User): + """ + Enable friends feature for users whose invitation code is exactly 'gamification'. + """ + return _gamification_flag_logic(user) + +def _leaderboards(user: User): + """ + Enable leaderboards feature for users whose invitation code is exactly 'gamification'. + """ + return _gamification_flag_logic(user) + +def _gamification_flag_logic(user: User): + """ + Shared logic for enabling gamification-related features. """ - from datetime import datetime, date - GAMIFICATION_INVITE_CODE = "gamification" # I guess we can decide on the invitation code - GAMIFICATION_START_DATE = date(2026, 4, 1) # Start after the first of April 2026 - - # Start gamification features after the GAMIFICATION_START_DATE + from datetime import datetime, date + GAMIFICATION_INVITE_CODE = "gamification" + GAMIFICATION_START_DATE = date(2026, 4, 1) + # Only enable before the start date if datetime.now().date() > GAMIFICATION_START_DATE: return False - - return user.invitation_code == GAMIFICATION_INVITE_CODE + return getattr(user, "invitation_code", None) == GAMIFICATION_INVITE_CODE From 7544574e0e64da3931ad64a2915f4a53cde1c3e6 Mon Sep 17 00:00:00 2001 From: nicra Date: Thu, 26 Mar 2026 09:16:47 +0100 Subject: [PATCH 05/16] Invite code for all features --- zeeguu/core/user_feature_toggles.py | 63 ++++++++++++++++++----------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index 3f09f380..321216aa 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -89,7 +89,8 @@ def _hide_recommendations(user): # Gamification feature flag logic from .model.user import User - +from datetime import datetime, date +GAMIFICATION_START_DATE = date(2026, 4, 1) def _gamification(user: User): """ Enable general gamification features for users whose invitation code is exactly 'gamification'. @@ -97,32 +98,46 @@ def _gamification(user: User): return _gamification_flag_logic(user) def _badges(user: User): - """ - Enable badges feature for users whose invitation code is exactly 'gamification'. - """ - return _gamification_flag_logic(user) + """ + Enable badges feature for users whose invitation code is exactly 'gamification'. + """ + BADGES_INVITE_CODE = "badges_invite_code" + if not _has_gamification_started(): + return False + + return user.invitation_code == BADGES_INVITE_CODE def _friends(user: User): - """ - Enable friends feature for users whose invitation code is exactly 'gamification'. - """ - return _gamification_flag_logic(user) + """ + Enable friends feature for users whose invitation code is exactly 'gamification'. + """ + FRIENDS_INVITE_CODE = "friends_invite_code" + if not _has_gamification_started(): + return False + + return user.invitation_code == FRIENDS_INVITE_CODE def _leaderboards(user: User): - """ - Enable leaderboards feature for users whose invitation code is exactly 'gamification'. - """ - return _gamification_flag_logic(user) + """ + Enable leaderboards feature for users whose invitation code is exactly 'gamification'. + """ + LEADERBOARDS_INVITE_CODE = "leaderboards_invite_code" + if not _has_gamification_started(): + return False + + return user.invitation_code == LEADERBOARDS_INVITE_CODE def _gamification_flag_logic(user: User): - """ - Shared logic for enabling gamification-related features. - """ - from datetime import datetime, date - GAMIFICATION_INVITE_CODE = "gamification" - GAMIFICATION_START_DATE = date(2026, 4, 1) - # Only enable before the start date - if datetime.now().date() > GAMIFICATION_START_DATE: - return False - return getattr(user, "invitation_code", None) == GAMIFICATION_INVITE_CODE - + """ + Shared logic for enabling gamification-related features. + """ + + GAMIFICATION_INVITE_CODE = "gamification" + # Only enable before the start date + if not _has_gamification_started(): + return False + + return user.invitation_code == GAMIFICATION_INVITE_CODE + +def _has_gamification_started(): + return datetime.now().date() >= GAMIFICATION_START_DATE \ No newline at end of file From b0b2470cea75466af1590d362dbf8f76e15b85e4 Mon Sep 17 00:00:00 2001 From: nicra Date: Thu, 26 Mar 2026 14:27:37 +0100 Subject: [PATCH 06/16] Is dev for the gamification feature --- zeeguu/core/user_feature_toggles.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index 321216aa..6597586e 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -133,7 +133,10 @@ def _gamification_flag_logic(user: User): """ GAMIFICATION_INVITE_CODE = "gamification" - # Only enable before the start date + if user.is_dev: + return True + + # Only enable after the start date if not _has_gamification_started(): return False From 3d25288c3bd6edc8d7215f631cbfa8d577712195 Mon Sep 17 00:00:00 2001 From: xXPinkmagicXx Date: Tue, 31 Mar 2026 13:27:27 +0200 Subject: [PATCH 07/16] clean up logic for gamification --- zeeguu/core/user_feature_toggles.py | 65 +++++++---------------------- 1 file changed, 14 insertions(+), 51 deletions(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index 6597586e..32aecaed 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -16,10 +16,7 @@ def _feature_map(): "new_topics": _new_topics, "daily_feedback": _daily_feedback, "hide_recommendations": _hide_recommendations, - "gamification": _gamification, - "badges": _badges, - "friends": _friends, - "leaderboards": _leaderboards, + "gamification": _gamification } @@ -90,57 +87,23 @@ def _hide_recommendations(user): # Gamification feature flag logic from .model.user import User from datetime import datetime, date + +from zeeguu.core.model import user GAMIFICATION_START_DATE = date(2026, 4, 1) def _gamification(user: User): """ Enable general gamification features for users whose invitation code is exactly 'gamification'. """ - return _gamification_flag_logic(user) - -def _badges(user: User): - """ - Enable badges feature for users whose invitation code is exactly 'gamification'. - """ - BADGES_INVITE_CODE = "badges_invite_code" - if not _has_gamification_started(): - return False - - return user.invitation_code == BADGES_INVITE_CODE - -def _friends(user: User): - """ - Enable friends feature for users whose invitation code is exactly 'gamification'. - """ - FRIENDS_INVITE_CODE = "friends_invite_code" - if not _has_gamification_started(): - return False - - return user.invitation_code == FRIENDS_INVITE_CODE - -def _leaderboards(user: User): - """ - Enable leaderboards feature for users whose invitation code is exactly 'gamification'. - """ - LEADERBOARDS_INVITE_CODE = "leaderboards_invite_code" - if not _has_gamification_started(): - return False - - return user.invitation_code == LEADERBOARDS_INVITE_CODE - -def _gamification_flag_logic(user: User): - """ - Shared logic for enabling gamification-related features. - """ - - GAMIFICATION_INVITE_CODE = "gamification" - if user.is_dev: - return True - - # Only enable after the start date - if not _has_gamification_started(): - return False - - return user.invitation_code == GAMIFICATION_INVITE_CODE + + GAMIFICATION_INVITE_CODE = "gamification" + if user.is_dev: + return True + + # Only enable after the start date + if not _has_gamification_started(): + return False + + return user.invitation_code == GAMIFICATION_INVITE_CODE def _has_gamification_started(): - return datetime.now().date() >= GAMIFICATION_START_DATE \ No newline at end of file + return datetime.now().date() >= GAMIFICATION_START_DATE \ No newline at end of file From 094b345996e79067f0cb99f8bad9a406db44df2f Mon Sep 17 00:00:00 2001 From: xXPinkmagicXx Date: Thu, 2 Apr 2026 13:48:55 +0200 Subject: [PATCH 08/16] update the gamification flag logic --- zeeguu/core/user_feature_toggles.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index 32aecaed..e05bf52a 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -86,24 +86,26 @@ def _hide_recommendations(user): # Gamification feature flag logic from .model.user import User +from .model.cohort import Cohort from datetime import datetime, date - -from zeeguu.core.model import user GAMIFICATION_START_DATE = date(2026, 4, 1) def _gamification(user: User): """ - Enable general gamification features for users whose invitation code is exactly 'gamification'. + Enable general gamification features for users whose invitation with the gamification invite code, + or who are in the gamification cohort. This includes features like badges, friends, and leaderboards. """ - GAMIFICATION_INVITE_CODE = "gamification" + GAMIFICATION_INVITE_CODE = "CD8HGKKJ" if user.is_dev: return True - # Only enable after the start date - if not _has_gamification_started(): - return False - - return user.invitation_code == GAMIFICATION_INVITE_CODE + if user.invitation_code.lower() == GAMIFICATION_INVITE_CODE.lower(): + return True + + # Find gamification cohort by invite code + gamification_cohort = Cohort.find_by_code(GAMIFICATION_INVITE_CODE) + if gamification_cohort and user.is_member_of_cohort(gamification_cohort.id): + return True -def _has_gamification_started(): - return datetime.now().date() >= GAMIFICATION_START_DATE \ No newline at end of file + # Disabled for everyone else + return False From f284eb04553611e85791777152c5cde9669588c8 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Tue, 24 Mar 2026 17:24:41 +0100 Subject: [PATCH 09/16] Add upgrade_to_teacher tool script Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/upgrade_to_teacher.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tools/upgrade_to_teacher.py diff --git a/tools/upgrade_to_teacher.py b/tools/upgrade_to_teacher.py new file mode 100644 index 00000000..a01b0fb0 --- /dev/null +++ b/tools/upgrade_to_teacher.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +from zeeguu.api.app import create_app +from zeeguu.core.model import db + +app = create_app() +app.app_context().push() + +from zeeguu.core.model.user import User +from zeeguu.core.model.teacher import Teacher + +email = input("User email: ") + +user = User.find(email) +if not user: + print(f"No user found with email: {email}") + exit(1) + +print(f"Found user: {user.name} (id={user.id})") + +if Teacher.exists(user): + print("User is already a teacher.") + exit(0) + +teacher = Teacher(user) +db.session.add(teacher) +db.session.commit() + +print(f"User '{user.name}' has been upgraded to teacher.") From 0e52cdde1741d59ef547988387671ea7ad6ec6b5 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Thu, 26 Mar 2026 12:18:58 +0100 Subject: [PATCH 10/16] Fix article extraction to use readability-cleaned HTML Article.find_or_create() was using np_article.html (raw newspaper HTML) instead of np_article.htmlContent (readability server output). This caused navigation menus, headers, and footers to appear as bullet-point lists in shared articles. Co-Authored-By: Claude Opus 4.6 (1M context) --- zeeguu/core/model/article.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zeeguu/core/model/article.py b/zeeguu/core/model/article.py index ab8cf958..1cff0bf8 100644 --- a/zeeguu/core/model/article.py +++ b/zeeguu/core/model/article.py @@ -1350,8 +1350,8 @@ def find_or_create( canonical_url, html_content=html_content ) - # newspaper Article objects use .html, not .htmlContent - html_content = np_article.html + # Use readability-cleaned HTML (not raw np_article.html) + html_content = np_article.htmlContent article_text = np_article.text # Full article text from readability server title = np_article.title authors = ", ".join(np_article.authors or []) From 563467386bdfc08586e6bebe86e404e019345491 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 27 Mar 2026 17:04:53 +0100 Subject: [PATCH 11/16] Fix Anthropic JSON parsing: accept literal newlines from LLM The LLM returns JSON with literal newlines inside string values (instead of escaped \n), which json.loads rejects in strict mode. This caused fallback to DeepSeek (~90s vs ~3s for Anthropic). Using json.loads(strict=False) accepts these control characters. Co-Authored-By: Claude Opus 4.6 (1M context) --- zeeguu/core/llm_services/simplification_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zeeguu/core/llm_services/simplification_service.py b/zeeguu/core/llm_services/simplification_service.py index c4fbd409..63b77e56 100644 --- a/zeeguu/core/llm_services/simplification_service.py +++ b/zeeguu/core/llm_services/simplification_service.py @@ -575,7 +575,9 @@ def clean_text(text): import json import markdown2 try: - result = json.loads(result_text) + # LLMs often return JSON with literal newlines inside + # string values (instead of \n), which strict JSON rejects + result = json.loads(result_text, strict=False) # Convert markdown content to HTML if "content" in result and result["content"]: From fa60ee518b2cf49c658346fe33554442d8f0aadb Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 27 Mar 2026 17:12:11 +0100 Subject: [PATCH 12/16] Support withContent=false in find_or_create_article Skip expensive tokenization when caller only needs article metadata (id, language, title). Used by SharedArticleHandler to show the language choice modal immediately without waiting for NLP processing. Co-Authored-By: Claude Opus 4.6 (1M context) --- zeeguu/api/endpoints/article.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zeeguu/api/endpoints/article.py b/zeeguu/api/endpoints/article.py index 43eed856..23f1bab2 100644 --- a/zeeguu/api/endpoints/article.py +++ b/zeeguu/api/endpoints/article.py @@ -39,6 +39,7 @@ def find_or_create_article(): title = request.form.get("title", "") if pre_extracted else None author = request.form.get("author", "") if pre_extracted else None image_url = request.form.get("imageUrl", "") if pre_extracted else None + with_content = request.form.get("withContent", "true") == "true" print("-- url: " + url) print("-- pre_extracted: " + str(pre_extracted)) @@ -67,7 +68,7 @@ def find_or_create_article(): article.assess_cefr_level(db_session) print("-- article CEFR level assessed") - uai = UserArticle.user_article_info(user, article, with_content=True) + uai = UserArticle.user_article_info(user, article, with_content=with_content) print("-- returning user article info: ", json.dumps(uai)[:50]) return json_result(uai) except NoResultFound as e: From fd67d5843b52f8bc4993a19600bc89fe8edff5f2 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Fri, 27 Mar 2026 17:30:13 +0100 Subject: [PATCH 13/16] Add lightweight /detect_article_info endpoint for share modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downloads and detects language + title without creating the article in the DB. Returns instantly for already-existing articles. Reverts the withContent flag — no longer needed since the share flow now uses this lightweight endpoint instead of find_or_create_article. Co-Authored-By: Claude Opus 4.6 (1M context) --- zeeguu/api/endpoints/article.py | 56 +++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/zeeguu/api/endpoints/article.py b/zeeguu/api/endpoints/article.py index 23f1bab2..915550f3 100644 --- a/zeeguu/api/endpoints/article.py +++ b/zeeguu/api/endpoints/article.py @@ -39,7 +39,6 @@ def find_or_create_article(): title = request.form.get("title", "") if pre_extracted else None author = request.form.get("author", "") if pre_extracted else None image_url = request.form.get("imageUrl", "") if pre_extracted else None - with_content = request.form.get("withContent", "true") == "true" print("-- url: " + url) print("-- pre_extracted: " + str(pre_extracted)) @@ -63,12 +62,11 @@ def find_or_create_article(): print("-- article found or created: " + str(article.id)) # Assess CEFR level for user-initiated article reading - # (only assess if article doesn't already have an assessment) if not article.cefr_assessment or not article.cefr_assessment.llm_cefr_level: article.assess_cefr_level(db_session) print("-- article CEFR level assessed") - uai = UserArticle.user_article_info(user, article, with_content=with_content) + uai = UserArticle.user_article_info(user, article, with_content=True) print("-- returning user article info: ", json.dumps(uai)[:50]) return json_result(uai) except NoResultFound as e: @@ -92,6 +90,58 @@ def find_or_create_article(): flask.abort(500) +# --------------------------------------------------------------------------- +@api.route("/detect_article_info", methods=("POST",)) +# --------------------------------------------------------------------------- +@cross_domain +@requires_session +def detect_article_info(): + """ + Lightweight endpoint: downloads a URL, detects language and title. + Does NOT create an article in the DB. + Used by the share flow to show the language choice modal fast. + + Expects: url (str) + Returns: {language, title, url} + """ + url = request.form.get("url", "") + if not url: + flask.abort(400, "URL required") + + from zeeguu.core.model.url import Url + + canonical_url = Url.extract_canonical_url(url) + + # Check if article already exists — if so, return instantly + existing = Article.find(canonical_url) + if existing: + return json_result({ + "id": existing.id, + "language": existing.language.code, + "title": existing.title, + "url": canonical_url, + "exists": True, + }) + + # Download and detect language without creating article + try: + from zeeguu.core.content_retriever import readability_download_and_parse + + np_article = readability_download_and_parse(canonical_url) + lang = np_article.meta_lang + title = np_article.title + + return json_result({ + "language": lang, + "title": title, + "url": canonical_url, + "exists": False, + }) + except Exception as e: + log(f"detect_article_info failed for {url}: {e}") + flask.abort(422, "Could not parse article") + + # --------------------------------------------------------------------------- @api.route("/make_personal_copy", methods=("POST",)) # --------------------------------------------------------------------------- From ab24d8b53dc9c90308e78c1c085b1340c09804d1 Mon Sep 17 00:00:00 2001 From: Mircea Lungu Date: Sun, 29 Mar 2026 14:43:44 +0200 Subject: [PATCH 14/16] Show non-simplified articles only for legacy users via feature toggle Co-Authored-By: Claude Opus 4.6 (1M context) --- zeeguu/core/model/user_article.py | 4 +++- zeeguu/core/user_feature_toggles.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/zeeguu/core/model/user_article.py b/zeeguu/core/model/user_article.py index 6ab1fb5b..122f441a 100644 --- a/zeeguu/core/model/user_article.py +++ b/zeeguu/core/model/user_article.py @@ -654,8 +654,10 @@ def article_infos(cls, user, articles, select_appropriate=True): # Don't show original articles that aren't simplified — # they'd open externally, which defeats the purpose + # (legacy users with the feature toggle can still see them) if not article.parent_article_id and not article.uploader_id: - continue + if not user.has_feature("show_non_simplified_articles"): + continue if article.id in seen_ids: continue diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index e05bf52a..a0bdb39d 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -16,6 +16,7 @@ def _feature_map(): "new_topics": _new_topics, "daily_feedback": _daily_feedback, "hide_recommendations": _hide_recommendations, + "show_non_simplified_articles": _show_non_simplified_articles, "gamification": _gamification } @@ -69,6 +70,17 @@ def _extension_experiment_1(user): ) +def _show_non_simplified_articles(user): + """Show non-simplified (original) articles for legacy users. + + Most users only see simplified articles. These legacy users + were active before simplification was standard and still expect + to see original articles in their feed. + """ + LEGACY_USER_IDS = {4607, 4626} + return user.id in LEGACY_USER_IDS + + def _hide_recommendations(user): """Hide recommended articles for students in specific cohorts. From c2210511983e4c7647a0ebc1f3da833682411a99 Mon Sep 17 00:00:00 2001 From: xXPinkmagicXx Date: Thu, 2 Apr 2026 14:30:20 +0200 Subject: [PATCH 15/16] fix invitation code check --- zeeguu/core/user_feature_toggles.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index a0bdb39d..0ca00d61 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -110,8 +110,10 @@ def _gamification(user: User): GAMIFICATION_INVITE_CODE = "CD8HGKKJ" if user.is_dev: return True - - if user.invitation_code.lower() == GAMIFICATION_INVITE_CODE.lower(): + + # Invitation code can be None + invitation_code = user.invitation_code or "" + if invitation_code.lower() == GAMIFICATION_INVITE_CODE.lower(): return True # Find gamification cohort by invite code From 871fcabe48634c5765915b30657bce375881b305 Mon Sep 17 00:00:00 2001 From: xXPinkmagicXx Date: Thu, 2 Apr 2026 16:24:18 +0200 Subject: [PATCH 16/16] Try except logic for gamification cohort --- zeeguu/core/user_feature_toggles.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zeeguu/core/user_feature_toggles.py b/zeeguu/core/user_feature_toggles.py index 0ca00d61..fd5e04e4 100644 --- a/zeeguu/core/user_feature_toggles.py +++ b/zeeguu/core/user_feature_toggles.py @@ -97,6 +97,8 @@ def _hide_recommendations(user): return False # Gamification feature flag logic +from sqlalchemy.exc import NoResultFound + from .model.user import User from .model.cohort import Cohort from datetime import datetime, date @@ -116,8 +118,12 @@ def _gamification(user: User): if invitation_code.lower() == GAMIFICATION_INVITE_CODE.lower(): return True - # Find gamification cohort by invite code - gamification_cohort = Cohort.find_by_code(GAMIFICATION_INVITE_CODE) + # Find gamification cohort by invite code, if it exists. + try: + gamification_cohort = Cohort.find_by_code(GAMIFICATION_INVITE_CODE) + except NoResultFound: + gamification_cohort = None + if gamification_cohort and user.is_member_of_cohort(gamification_cohort.id): return True