Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.

Commit 1d50078

Browse files
committed
Squashed commit of the following:
commit 937843a65a6e0e65f6ee54216d975b259a3f8b16 Author: Brad Miller <[email protected]> Date: Fri Oct 21 09:11:53 2022 -0500 Turn on ethical ad experiment commit 210ffb8 Author: Brad Miller <[email protected]> Date: Thu Oct 20 11:36:27 2022 -0500 Fix: remove lint commit bce03ca Author: Brad Miller <[email protected]> Date: Thu Oct 20 11:28:25 2022 -0500 Add: docs Also, refactor to extract an add_flashcard function so it is not embedded in the updateLastPage function commit c8f2661 Author: Brad Miller <[email protected]> Date: Thu Oct 20 09:50:21 2022 -0500 New: restore functionality for ad-hoc practice commit ee094ae Author: Brad Miller <[email protected]> Date: Wed Oct 19 16:48:40 2022 -0500 Start the adhoc practice update
1 parent 7f25e7f commit 1d50078

File tree

4 files changed

+227
-5
lines changed

4 files changed

+227
-5
lines changed

bookserver/crud.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from fastapi.exceptions import HTTPException
2525
from pydal.validators import CRYPT
2626
from sqlalchemy import and_, distinct, func, update
27-
from sqlalchemy.sql import select, text
27+
from sqlalchemy.sql import select, text, delete
2828
from starlette.requests import Request
2929

3030
from . import schemas
@@ -48,6 +48,7 @@
4848
CourseAttribute,
4949
CourseInstructor,
5050
CourseInstructorValidator,
51+
CoursePractice,
5152
Courses,
5253
CoursesValidator,
5354
Library,
@@ -70,6 +71,8 @@
7071
UserStateValidator,
7172
UserSubChapterProgress,
7273
UserSubChapterProgressValidator,
74+
UserTopicPractice,
75+
UserTopicPracticeValidator,
7376
runestone_component_dict,
7477
)
7578

@@ -1007,3 +1010,116 @@ async def fetch_library_books():
10071010

10081011
async def create_library_book():
10091012
...
1013+
1014+
1015+
async def fetch_course_practice(course_name: str) -> CoursePractice:
1016+
"""
1017+
Fetch the course_practice row for a given course. The course practice row
1018+
contains the configuration of the practice feature for the given course.
1019+
"""
1020+
query = (
1021+
select(CoursePractice)
1022+
.where(CoursePractice.course_name == course_name)
1023+
.order_by(CoursePractice.id.desc())
1024+
)
1025+
async with async_session() as session:
1026+
res = await session.execute(query)
1027+
return res.scalars().first()
1028+
1029+
1030+
async def fetch_one_user_topic_practice(
1031+
user: AuthUserValidator,
1032+
last_page_chapter: str,
1033+
last_page_subchapter: str,
1034+
qname: str,
1035+
) -> UserTopicPracticeValidator:
1036+
"""
1037+
The user_topic_practice table contains information about each question (flashcard)
1038+
that a student is eligible to see for a given topic in a course.
1039+
A particular question should ony be in the table once per student. This row also contains
1040+
information about scheduling and correctness to help the practice algorithm select the
1041+
best question to show a student.
1042+
"""
1043+
query = select(UserTopicPractice).where(
1044+
(UserTopicPractice.user_id == user.id)
1045+
& (UserTopicPractice.course_name == user.course_name)
1046+
& (UserTopicPractice.chapter_label == last_page_chapter)
1047+
& (UserTopicPractice.sub_chapter_label == last_page_subchapter)
1048+
& (UserTopicPractice.question_name == qname)
1049+
)
1050+
async with async_session() as session:
1051+
res = await session.execute(query)
1052+
rslogger.debug(f"{res=}")
1053+
utp = res.scalars().one_or_none()
1054+
return UserTopicPracticeValidator.from_orm(utp)
1055+
1056+
1057+
async def delete_one_user_topic_practice(qid: int) -> None:
1058+
"""
1059+
Used by ad hoc question selection. If a student un-marks a page as completed then if there
1060+
is a question from the page it will be removed from the set of possible flashcards a student
1061+
can see.
1062+
"""
1063+
query = delete(UserTopicPractice).where(UserTopicPractice.id == qid)
1064+
async with async_session.begin() as session:
1065+
await session.execute(query)
1066+
1067+
1068+
async def create_user_topic_practice(
1069+
user: AuthUserValidator,
1070+
last_page_chapter: str,
1071+
last_page_subchapter: str,
1072+
qname: str,
1073+
now_local: datetime.datetime,
1074+
now: datetime.datetime,
1075+
tz_offset: float,
1076+
):
1077+
"""
1078+
Add a question for the user to practice on
1079+
"""
1080+
async with async_session.begin() as session:
1081+
new_entry = UserTopicPractice(
1082+
user_id=user.id,
1083+
course_name=user.course_name,
1084+
chapter_label=last_page_chapter,
1085+
sub_chapter_label=last_page_subchapter,
1086+
question_name=qname,
1087+
# Treat it as if the first eligible question is the last one asked.
1088+
i_interval=0,
1089+
e_factor=2.5,
1090+
next_eligible_date=now_local.date(),
1091+
# add as if yesterday, so can practice right away
1092+
last_presented=now - datetime.timedelta(1),
1093+
last_completed=now - datetime.timedelta(1),
1094+
creation_time=now,
1095+
timezoneoffset=tz_offset,
1096+
)
1097+
session.add(new_entry)
1098+
1099+
1100+
async def fetch_qualified_questions(
1101+
base_course, chapter_label, sub_chapter_label
1102+
) -> list[QuestionValidator]:
1103+
"""
1104+
Return a list of possible questions for a given chapter and subchapter. These
1105+
questions will all have the practice flag set to true.
1106+
"""
1107+
query = select(Question).where(
1108+
(Question.base_course == base_course)
1109+
& (
1110+
(Question.topic == "{}/{}".format(chapter_label, sub_chapter_label))
1111+
| (
1112+
(Question.chapter == chapter_label)
1113+
& (Question.topic == None) # noqa: E711
1114+
& (Question.subchapter == sub_chapter_label)
1115+
)
1116+
)
1117+
& (Question.practice == True) # noqa: E712
1118+
& (Question.review_flag == False) # noqa: E712
1119+
)
1120+
async with async_session() as session:
1121+
res = await session.execute(query)
1122+
rslogger.debug(f"{res=}")
1123+
questionlist = [QuestionValidator.from_orm(x) for x in res.scalars().fetchall()]
1124+
1125+
return questionlist

bookserver/models.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,3 +756,46 @@ class Library(Base, IdMixin):
756756

757757

758758
LibraryValidator = sqlalchemy_to_pydantic(Library)
759+
760+
761+
# Tables for practice feature
762+
#
763+
class CoursePractice(Base, IdMixin):
764+
__tablename__ = "course_practice"
765+
auth_user_id = Column(ForeignKey("auth_user.id", ondelete="CASCADE"))
766+
course_name = Column(String(512))
767+
start_date = Column(Date)
768+
end_date = Column(Date)
769+
max_practice_days = Column(Integer)
770+
max_practice_questions = Column(Integer)
771+
day_points = Column(Float(53))
772+
question_points = Column(Float(53))
773+
questions_to_complete_day = Column(Integer)
774+
graded = Column(Integer)
775+
spacing = Column(Integer)
776+
interleaving = Column(Integer)
777+
flashcard_creation_method = Column(Integer)
778+
779+
780+
CoursePracticeValidator = sqlalchemy_to_pydantic(CoursePractice)
781+
782+
783+
class UserTopicPractice(Base, IdMixin):
784+
__tablename__ = "user_topic_practice"
785+
786+
user_id = Column(ForeignKey("auth_user.id", ondelete="CASCADE"))
787+
course_name = Column(String(512))
788+
chapter_label = Column(String(512))
789+
sub_chapter_label = Column(String(512))
790+
question_name = Column(String(512))
791+
i_interval = Column(Integer, nullable=False)
792+
e_factor = Column(Float(53), nullable=False)
793+
last_presented = Column(DateTime)
794+
last_completed = Column(DateTime)
795+
creation_time = Column(DateTime)
796+
q = Column(Integer, nullable=False, default=0)
797+
next_eligible_date = Column(Date)
798+
timezoneoffset = Column(Integer)
799+
800+
801+
UserTopicPracticeValidator = sqlalchemy_to_pydantic(UserTopicPractice)

bookserver/routers/books.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ async def serve_page(
289289
and course_row.base_course != "csawesome"
290290
and course_row.base_course != "mobilecsp"
291291
and course_row.courselevel != "high"
292+
and course_row.course_name != course_row.base_course
292293
and "supporter" not in course_attrs
293294
):
294295
show_rs_banner = True
@@ -324,6 +325,7 @@ async def serve_page(
324325
pagepath=pagepath,
325326
canonical_host=canonical_host,
326327
show_rs_banner=show_rs_banner,
328+
show_ethical_ad=True,
327329
**course_attrs,
328330
)
329331
# See `templates <https://fastapi.tiangolo.com/advanced/templates/>`_.

bookserver/routers/rslogging.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# Standard library
1212
# ----------------
1313
import json
14-
from datetime import datetime
14+
from datetime import datetime, timedelta
1515
import re
1616
from typing import Optional
1717

@@ -31,11 +31,17 @@
3131
create_user_chapter_progress_entry,
3232
create_user_state_entry,
3333
create_user_sub_chapter_progress_entry,
34+
create_user_topic_practice,
3435
EVENT2TABLE,
36+
delete_one_user_topic_practice,
3537
fetch_last_page,
38+
fetch_course,
39+
fetch_course_practice,
40+
fetch_one_user_topic_practice,
3641
fetch_user_chapter_progress,
3742
fetch_user_sub_chapter_progress,
3843
fetch_user,
44+
fetch_qualified_questions,
3945
is_server_feedback,
4046
update_sub_chapter_progress,
4147
update_user_state,
@@ -257,12 +263,17 @@ async def same_class(user1: AuthUserValidator, user2: str) -> bool:
257263
# --------------
258264
# see :ref:`processPageState`
259265
@router.post("/updatelastpage")
260-
async def updatelastpage(request: Request, request_data: LastPageDataIncoming):
266+
async def updatelastpage(
267+
request: Request,
268+
request_data: LastPageDataIncoming,
269+
RS_info: Optional[str] = Cookie(None),
270+
):
261271
if request_data.last_page_url is None:
262272
return # todo: log request_data, request.args and request.env.path_info
263273
if request.state.user:
264274
lpd = request_data.dict()
265275
rslogger.debug(f"{lpd=}")
276+
user = request.state.user
266277

267278
# last_page_url is going to be .../ns/books/published/course/chapter/subchapter.html
268279
# We will treat the second to last element as the chapter and the final element
@@ -290,11 +301,61 @@ async def updatelastpage(request: Request, request_data: LastPageDataIncoming):
290301
rslogger.debug("Not Authorized for update last page")
291302
raise HTTPException(401)
292303

293-
# todo: practice stuff came after this -- it does not belong here. But it needs
294-
# to be ported somewhere....
304+
# If practice is self paced then when a student marks a page as complete
305+
# we need to add flashcards.
306+
307+
practice_settings = await fetch_course_practice(user.course_name)
308+
if RS_info:
309+
values = json.loads(RS_info)
310+
tz_offset = float(values["tz_offset"])
311+
else:
312+
tz_offset = 0
313+
314+
if practice_settings and practice_settings.flashcard_creation_method == 0:
315+
await add_flashcard(request_data, lpd, user, tz_offset)
316+
295317
return make_json_response(detail="Success")
296318

297319

320+
async def add_flashcard(
321+
request_data: LastPageDataIncoming,
322+
lpd: dict,
323+
user: AuthUserValidator,
324+
tz_offset: float,
325+
) -> None:
326+
327+
rslogger.debug("Updating Practice Questions")
328+
# Since each authenticated user has only one active course, we retrieve the course this way.
329+
course = await fetch_course(user.course_name)
330+
331+
# We only retrieve questions to be used in flashcards if they are marked for practice purpose.
332+
questions = await fetch_qualified_questions(
333+
course.base_course, lpd["last_page_chapter"], lpd["last_page_subchapter"]
334+
)
335+
if len(questions) > 0:
336+
now = datetime.utcnow()
337+
now_local = now - timedelta(hours=tz_offset)
338+
existing_flashcards = await fetch_one_user_topic_practice(
339+
user,
340+
lpd["last_page_chapter"],
341+
lpd["last_page_subchapter"],
342+
questions[0].name,
343+
)
344+
# There is at least one qualified question in this subchapter, so insert a flashcard for the subchapter.
345+
if request_data.completion_flag == 1 and existing_flashcards is None:
346+
await create_user_topic_practice(
347+
user,
348+
lpd["last_page_chapter"],
349+
lpd["last_page_subchapter"],
350+
questions[0].name,
351+
now_local,
352+
now,
353+
tz_offset,
354+
)
355+
if request_data.completion_flag == 0 and existing_flashcards is not None:
356+
await delete_one_user_topic_practice(existing_flashcards.id)
357+
358+
298359
# _getCompletionStatus
299360
# --------------------
300361
@router.get("/getCompletionStatus")

0 commit comments

Comments
 (0)