ó
    òïÒj+'  ã            	       ó   • S r SSKrSSKrSSKJrJr  \R
                  R                  S5      =(       di    \R                  R                  \R                  R                  \R                  R                  \R                  R                  \5      5      SSS5      5      rS rS	 rSS
 jrS rS rSS jrS rS rS rSS jrSS jrSS jrS rg)zPSQLite storage: users, per-activity progress, attempt history, badges, settings.é    N)ÚdatetimeÚdateÚDB_PATHz..Ú_Datazfrench_grades.dbc                  ó„   • [         R                  " [        SS9n [         R                  U l        U R                  S5        U $ )Né
   )ÚtimeoutzPRAGMA journal_mode = WAL)Úsqlite3Úconnectr   ÚRowÚrow_factoryÚexecute)Úconns    Údb.pyÚget_dbr   
   s0   € Ü�?Š?œ7¨BÑ/€DÜ—{‘{€DÔØ‡L�LÐ,Ô-Ø€Kó    c                  ó~  • [         R                  " [         R                  R                  [        5      SS9  [        5       n U R                  S5        SU R                  S5      R                  5        Vs/ s H  oS   PM	     sn;  a  U R                  S5        U R                  5         U R                  5         g s  snf )NT)Úexist_oka,  
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT UNIQUE NOT NULL,
        role TEXT NOT NULL CHECK(role IN ('kid','parent')),
        password_hash TEXT NOT NULL,
        display_name TEXT NOT NULL,
        created_at TEXT DEFAULT (datetime('now','localtime'))
    );
    CREATE TABLE IF NOT EXISTS progress (
        user_id INTEGER NOT NULL,
        activity_id TEXT NOT NULL,
        lesson_id TEXT NOT NULL,
        best_score INTEGER DEFAULT 0,
        best_total INTEGER DEFAULT 0,
        best_pct INTEGER DEFAULT 0,
        stars INTEGER DEFAULT 0,
        points INTEGER DEFAULT 0,
        attempts INTEGER DEFAULT 0,
        completed INTEGER DEFAULT 0,
        first_done TEXT,
        last_done TEXT,
        PRIMARY KEY (user_id, activity_id)
    );
    CREATE TABLE IF NOT EXISTS attempts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        activity_id TEXT NOT NULL,
        lesson_id TEXT NOT NULL,
        score INTEGER, total INTEGER, pct INTEGER, stars INTEGER,
        points_earned INTEGER DEFAULT 0,
        seconds INTEGER DEFAULT 0,
        created_at TEXT DEFAULT (datetime('now','localtime'))
    );
    CREATE INDEX IF NOT EXISTS idx_attempts_user ON attempts(user_id, created_at);
    CREATE TABLE IF NOT EXISTS badges (
        user_id INTEGER NOT NULL,
        badge_id TEXT NOT NULL,
        earned_at TEXT DEFAULT (datetime('now','localtime')),
        PRIMARY KEY (user_id, badge_id)
    );
    CREATE TABLE IF NOT EXISTS days (
        user_id INTEGER NOT NULL,
        day TEXT NOT NULL,
        points INTEGER DEFAULT 0,
        activities INTEGER DEFAULT 0,
        PRIMARY KEY (user_id, day)
    );
    CREATE TABLE IF NOT EXISTS settings (
        key TEXT PRIMARY KEY,
        value TEXT
    );
    ÚpartialzPRAGMA table_info(attempts)Únamez9ALTER TABLE attempts ADD COLUMN partial INTEGER DEFAULT 0)ÚosÚmakedirsÚpathÚdirnamer   r   Úexecutescriptr   ÚfetchallÚcommitÚclose)ÚcÚrs     r   Úinit_dbr!      s‹   € Ü‡K‚K”—‘—‘¤Ó(°4Ò8Ü‹€AØ‡O�Oð 4ô 4	ðj ¨A¯I©IÐ6SÓ,T×,]Ñ,]Ô,_Ó`Ò,_ q˜6œÑ,_Ñ`Ó`Ø	�	‰	ÐMÔNØ‡H�H„JØ‡G�G…Iùò as   Á4B:c                 ó”   • [        5       nUR                  SU 45      R                  5       nUR                  5         U(       a  US   $ U$ )Nz&SELECT value FROM settings WHERE key=?Úvalue)r   r   Úfetchoner   )ÚkeyÚdefaultr   Úrows       r   Úget_settingr(   W   s?   € Ü‹€AØ
�)‰)Ð<¸s¸fÓ
E×
NÑ
NÓ
P€CØ‡G�G„IÞˆ3ˆw‰<Ð+ GÐ+r   c                 ó’   • [        5       nUR                  SU [        U5      45        UR                  5         UR	                  5         g )NzbINSERT INTO settings (key, value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value)r   r   Ústrr   r   )r%   r#   r   s      r   Úset_settingr+   ^   sF   € Ü‹€AØ‡I�IÐrÐuxÔz}ð  Dó  {Eð  uFô  GØ‡H�H„JØ‡G�G…Ir   c                 ó6   • U S:¼  a  S$ U S:¼  a  S$ U S:¼  a  S$ S$ )NéZ   é   é<   é   é   é   r   © )Úpcts    r   Ú	stars_forr5   f   s)   € Ø�r“	ˆ1ÐH C¨2£I˜qÐH¸¸r»	°1ÐHÀqÐHr   c                 óš  • [        [        U=(       d    S5      S5      nU(       a(  [        [        [        U=(       d    S5      U5      S5      OSnUS:X  a  SO[        US-  U-  5      n[	        U5      n	[        XX-  S-  5      US:X  a  SOS-   n
[        5       nUR                  SX45      R                  5       nU(       a  US   OSnX­:„  nU(       a  X­-
  O	US:¼  a  SOSn[        R                  " 5       R                  S5      nUS	:¼  a  U(       a  U(       a  US
   (       a  SOSnU(       a]  UR                  SU(       d  [        X<S   5      OUU(       a  UOUS   [        XŒS   5      [        XœS   5      [        X­5      UUX4	5        OUR                  SXXXHXšUUU45        UR                  SXXXHXŸ[        [        U=(       d    S5      S5      U(       a  SOS4
5        [        R                  " 5       R                  5       nUR                  SU UU45        UR                  5         UR                  5         [!        XùXŽ[#        U5      S9$ )zKStore one attempt; returns dict(points_earned, stars, pct, best, new_best).r   éd   é   z8SELECT * FROM progress WHERE user_id=? AND activity_id=?Úpointsr/   r0   z%Y-%m-%d %H:%M:%Sé2   Ú	completedr2   z³UPDATE progress SET best_score=?, best_total=?, best_pct=?, stars=?, points=?, attempts=attempts+1,
                     completed=?, last_done=? WHERE user_id=? AND activity_id=?Ú
best_scoreÚ
best_totalÚbest_pctÚstarszÉINSERT INTO progress (user_id, activity_id, lesson_id, best_score, best_total, best_pct, stars, points, attempts,
                     completed, first_done, last_done) VALUES (?,?,?,?,?,?,?,?,1,?,?,?)zŽINSERT INTO attempts (user_id, activity_id, lesson_id, score, total, pct, stars, points_earned, seconds, partial) VALUES (?,?,?,?,?,?,?,?,?,?)i  z´INSERT INTO days (user_id, day, points, activities) VALUES (?,?,?,1)
                 ON CONFLICT(user_id, day) DO UPDATE SET points=points+excluded.points, activities=activities+1)Úpoints_earnedr?   r4   Únew_bestr;   )ÚmaxÚintÚminÚroundr5   r   r   r$   r   ÚnowÚstrftimer   ÚtodayÚ	isoformatr   r   ÚdictÚbool)Úuser_idÚ	lesson_idÚactivity_idÚscoreÚtotalÚ
points_maxÚsecondsr   r4   r?   Ú
points_nowr   ÚprevÚprev_pointsrA   ÚearnedrF   r;   rH   s                      r   Úrecord_attemptrW   j   s3  € ä”�E—J˜Q“ Ó#€EÞ38ŒC””C˜Ÿ
 “O UÓ+¨QÔ/¸a€EØ˜!“‰#¤ u¨s¡{°UÑ':Ó!;€CÜ�c‹N€EÜ�zÑ'¨#Ñ-Ó.°s¸c³z±!ÀqÑI€JÜ‹€AØ�9‰9ÐOÐRYÐQgÓh×qÑqÓs€DÞ$(�$�x’.¨a€KØÑ'€HÞ)1ˆZÒ%¸SÀB»Y¹ÈA€FÜ
�,Š,‹.×
!Ñ
!Ð"5Ó
6€CØ˜R“i®¶T¸dÀ;×>O‘ÐVW€IÞØ	�	‰	ð Sæ9A”3�u <Ñ0Ô1ÀuÖW_ÉeÐeiÐjvÑewÜ�s Ñ,Ó-¬s°5¸w¹-Ó/HÌ#ÈjÓJfÐhqÐsvÐxðNõ	Oð
 	
�	‰	ð [à¨¸5ÀuÐZcÐehÐjmÐnô	pð ‡I�Ið  _Ø Y°uÀ5ÔRUÔVYÐZa×ZfÐefÓVgÐimÓRnÖu|Ñpqð  CDð  EôFä�JŠJ‹L×"Ñ"Ó$€EØ‡I�Ið sØu|ð  Dð  FLð  uMôNà‡H�H„JØ‡G�G„IÜ˜f°sÔY]Ð^gÓYhÑiÐir   c                 ó8  • [        5       nUR                  SU 45      R                  5       n[        UR                  SU 45      R                  5       5      nUR	                  5         0 nU H,  n[        U5      nUR                  US   5      US'   XdUS   '   M.     U$ )z\Best result per activity, plus the fastest perfect run (personal best for timed challenges).z&SELECT * FROM progress WHERE user_id=?zŠSELECT activity_id, MIN(seconds) FROM attempts WHERE user_id=? AND pct=100 AND seconds>0
                             GROUP BY activity_idrN   Ú	best_secs)r   r   r   rJ   r   Úget)rL   r   ÚrowsÚbestÚoutr    Úds          r   Úprogress_mapr_   ‹   s™   € ä‹€AØ�9‰9Ð=À¸zÓJ×SÑSÓU€DÜ�—	‘	ð 5Ø7>°jóBßBJÁ(Ã*óN€Dà‡G�G„IØ
€CÛˆÜ�‹GˆØŸ™ ! MÑ"2Ó3ˆˆ+‰Ø !ˆAˆmÑÓñ ð €Jr   c                 ó:  • [        5       nUR                  SU 45      R                  5       S   nUR                  SU 45      R                  5       nUR                  SU 45      R                  5        Vs/ s H  oDS   PM	     nnUR                  SU 45      R                  5        Vs/ s H  oDS   PM	     nnUR                  SU 45      R                  5       S	   nUR	                  5         S
n[
        R                  " 5       n	[        U5      n
U	R                  5       U
;  a"  U	R                  U	R                  5       S-
  5      n	U	R                  5       U
;   a=  US-  nU	R                  U	R                  5       S-
  5      n	U	R                  5       U
;   a  M=  [        [        U5      US   =(       d    S
US   =(       d    S
US   =(       d    S
US   =(       d    S
[        U5      X†[        U5      [
        R                  " 5       R                  5       U
;   S9
$ s  snf s  snf )NzISELECT COALESCE(SUM(points_earned),0) AS xp FROM attempts WHERE user_id=?Úxpa  SELECT COUNT(*) AS done, COALESCE(SUM(stars),0) AS stars, COALESCE(SUM(points),0) AS points,
                       SUM(CASE WHEN best_pct=100 THEN 1 ELSE 0 END) AS perfect, COALESCE(SUM(attempts),0) AS attempts
                       FROM progress WHERE user_id=? AND completed=1zGSELECT day FROM days WHERE user_id=? AND activities>0 ORDER BY day DESCÚdayz>SELECT badge_id FROM badges WHERE user_id=? ORDER BY earned_atÚbadge_idzBSELECT COALESCE(SUM(seconds),0) AS s FROM attempts WHERE user_id=?Úsr   r2   Údoner?   ÚperfectÚattempts)
ra   re   r?   rf   rg   ÚdaysÚstreakÚbadgesrR   rH   )r   r   r$   r   r   r   rH   ÚsetrI   ÚfromordinalÚ	toordinalrJ   rC   Úlen)rL   r   ra   r'   r    rh   rj   Úsecsri   r^   Údaysets              r   Ústatsrq   š   s  € Ü‹€AØ	
�‰Ð^ÐahÐ`jÓ	k×	tÑ	tÓ	vÐw{Ñ	|€BØ
�)‰)ð HàJQÈóUçU]ÑU]ÓU_ð ð  Ÿi™iÐ(qÐt{Ðs}Ó~÷  Hñ  Hô  Jó  Kò  J˜ˆeŒHñ  J€Dð  KØ%&§Y¡YÐ/oÐryÐq{Ó%|÷  &Fñ  &Fô  &Hó  Iò  &H �
Œmñ  &H€Fð  IØ�9‰9ÐYÐ\cÐ[eÓf×oÑoÓqÐruÑv€DØ‡G�G„IØ€FÜ�
Š
‹€AÜ�‹Y€FØ‡{�{ƒ}˜FÓ"Ø�M‰M˜!Ÿ+™+›-¨!Ñ+Ó,ˆØ
�+‰+‹-˜6Ó
!Ø�!‰ˆØ�M‰M˜!Ÿ+™+›-¨!Ñ+Ó,ˆð �+‰+‹-˜6Õ
!ô ”3�r“7  V¡×!1°¸¸W¹×9JÈÐTWÐXaÑTb×TgÐfgÐruð  wAñ  sB÷  sGð  FGÜ˜“Y vÄcÈ$ÃiÔW[×WaÒWaÓWc×WmÑWmÓWoÐsyÑWyñ{ð {ùò Kùò Is   Á3HÂ&Hc                 óž   • [        5       nUR                  SX45      nUR                  5         UR                  S:„  nUR	                  5         U$ )Nz=INSERT OR IGNORE INTO badges (user_id, badge_id) VALUES (?,?)r   )r   r   r   Úrowcountr   )rL   rc   r   ÚcurÚnews        r   Úaward_badgerv   °   sC   € Ü‹€AØ
�)‰)ÐSÐV]ÐUhÓ
i€CØ‡H�H„JØ
�,‰,˜Ñ
€CØ‡G�G„IØ€Jr   c                 óº   • [        5       nUR                  SX45      R                  5       nUR                  5         U Vs/ s H  n[	        U5      PM     sn$ s  snf )Nz?SELECT * FROM attempts WHERE user_id=? ORDER BY id DESC LIMIT ?)r   r   r   r   rJ   )rL   Úlimitr   r[   r    s        r   Úhistoryry   ¹   sL   € Ü‹€AØ�9‰9ÐVÐY`ÐXhÓi×rÑrÓt€DØ‡G�G„IÙ!Ó"šT˜ŒD�ŽG™TÑ"Ð"ùÒ"s   Á Ac                 óÌ   • [        5       nUR                  SX45      R                  5       nUR                  5         [	        U5       Vs/ s H  n[        U5      PM     sn$ s  snf )NzRSELECT day, points, activities FROM days WHERE user_id=? ORDER BY day DESC LIMIT ?)r   r   r   r   ÚreversedrJ   )rL   Únr   r[   r    s        r   Ú
day_seriesr}   À   sY   € Ü‹€AØ�9‰9ÐiÐlsÐkwÓx÷  Bñ  Bó  D€DØ‡G�G„IÜ% dœ^Ó,š^˜ŒD�ŽG™^Ñ,Ð,ùÒ,s   Á	A!c                 óP  • [        5       nU(       a'  UR                  SX45        UR                  SX45        ONU(       a'  UR                  SX45        UR                  SX45        O S H  nUR                  SU S3U 45        M     UR                  5         UR                  5         g )Nz6DELETE FROM progress WHERE user_id=? AND activity_id=?z6DELETE FROM attempts WHERE user_id=? AND activity_id=?z4DELETE FROM progress WHERE user_id=? AND lesson_id=?z4DELETE FROM attempts WHERE user_id=? AND lesson_id=?)Úprogressrg   rj   rh   zDELETE FROM z WHERE user_id=?)r   r   r   r   )rL   rM   rN   r   Úts        r   Úresetr�   Ç   s�   € Ü‹€AÞØ	�	‰	ÐJÈWÐLbÔcØ	�	‰	ÐJÈWÐLbÕcÞ	Ø	�	‰	ÐHÈ7ÐJ^Ô_Ø	�	‰	ÐHÈ7ÐJ^Õ_ã;ˆAØ�I‰I˜ Q CÐ'7Ð8¸7¸*ÖEñ <à‡H�H„JØ‡G�G…Ir   c                  óÜ  • SSK n U R                  (       d  U R                  * S-  OU R                  * S-  nU(       a  [	        S5      (       a  [        S[        U5      5        g[        5       nUS S3nSS/4S	S
/4SSS/4SS/44 H.  u  pEU H#  nUR                  SU SU SU SU S3	U45        M%     M0     UR                  5         UR                  5         [        S[        U5      5        g)zIRows written before TZ was set are in UTC: shift them once to local time.r   Ni  Ú
tz_shiftedz+dz hoursrg   Ú
created_atrj   Ú	earned_atr   Ú
first_doneÚ	last_doneÚuserszUPDATE z SET z
=datetime(z, ?) WHERE z IS NOT NULL)ÚtimeÚdaylightÚtimezoneÚaltzoner(   r+   r*   r   r   r   r   )r‰   Úoffr   ÚmodÚtableÚcolsÚcols          r   Úmigrate_timezoner’   Ö   s  € ãØ(,¯¯ˆ4�=‰=ˆ.˜DÒ
 ¸D¿L¹L¸=ÈDÑ;P€CÞ”+˜l×+Ñ+Ü�L¤# c£(Ô+ØÜ‹€AØ�ˆH�FÐ
€CØ# l ^Ð4°xÀ+ÀÐ6OÐR\Ð_kÐmxÐ^yÐQzð  ~Eð  HTð  GUð  }Vó  W‰ˆÛˆCØ�I‰I˜ ˜w e¨C¨5°
¸3¸%¸{È3È%È|Ð\Ð_bÐ^dÖeó ñ Wð ‡H�H„JØ‡G�G„IÜ�œc #›hÕ'r   )N)r   F)r/   )é   )NN)Ú__doc__r   r
   r   r   ÚenvironrZ   r   ÚnormpathÚjoinr   ÚabspathÚ__file__r   r   r!   r(   r+   r5   rW   r_   rq   rv   ry   r}   r�   r’   r3   r   r   Ú<module>rš      sÀ   ðá VÛ 	Û ß #à
�*‰*�.‰.˜Ó
#÷  U r§w¡w×'7Ñ'7¸¿¹¿¹ÀRÇWÁWÇ_Á_ÐUW×U\ÑU\×UdÑUdÐemÓUnÓEoÐquÐw~ð  ASó  9Tó  (U€òò;ôH,òòIôjòBò{ò,ô#ô-ôó(r   