Skip to content

Commit

Permalink
feat: add BFFE endpoint for Learning Assistant to get all necessary d…
Browse files Browse the repository at this point in the history
…ata to function (#140)

This commit adds a back-end-for-frontend (BFFE) endpoint for the Learning Assistant to get all the necessary data it needs to function.

The response from this endpoint includes the following information.

* whether the Learning Assistant is enabled
* message history information, if the learner is eligible to use the Learning Assistant
* audit trial information
  • Loading branch information
MichaelRoytman authored Dec 4, 2024
1 parent bcb23ca commit ed0061e
Show file tree
Hide file tree
Showing 15 changed files with 864 additions and 85 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ Change Log
Unreleased
**********

4.5.0 - 2024-12-04
******************
* Add local setup to readme
* Add a BFFE chat summary endpoint for Learning Assistant, including information about whether the Learning Assistant is
enabled, Learning Assistant message history, and Learning Assistant audit trial data.

4.4.7 - 2024-11-25
******************
Expand Down
2 changes: 1 addition & 1 deletion learning_assistant/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
Plugin for a learning assistant backend, intended for use within edx-platform.
"""

__version__ = '4.4.7'
__version__ = '4.5.0'

default_app_config = 'learning_assistant.apps.LearningAssistantConfig' # pylint: disable=invalid-name
104 changes: 93 additions & 11 deletions learning_assistant/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Library for the learning_assistant app.
"""
import datetime
import logging
from datetime import datetime, timedelta

Expand All @@ -11,8 +12,13 @@
from jinja2 import BaseLoader, Environment
from opaque_keys import InvalidKeyError

from learning_assistant.constants import ACCEPTED_CATEGORY_TYPES, AUDIT_TRIAL_MAX_DAYS, CATEGORY_TYPE_MAP
from learning_assistant.data import LearningAssistantCourseEnabledData
try:
from common.djangoapps.course_modes.models import CourseMode
except ImportError:
CourseMode = None

from learning_assistant.constants import ACCEPTED_CATEGORY_TYPES, CATEGORY_TYPE_MAP
from learning_assistant.data import LearningAssistantAuditTrialData, LearningAssistantCourseEnabledData
from learning_assistant.models import (
LearningAssistantAuditTrial,
LearningAssistantCourseEnabled,
Expand Down Expand Up @@ -231,22 +237,98 @@ def get_message_history(courserun_key, user, message_count):
return message_history


def audit_trial_is_expired(user, upgrade_deadline):
def get_audit_trial_expiration_date(start_date):
"""
Given a user (User), get or create the corresponding LearningAssistantAuditTrial trial object.
Given a start date of an audit trial, calculate the expiration date of the audit trial.
Arguments:
* start_date (datetime): the start date of the audit trial
Returns:
* expiration_date (datetime): the expiration date of the audit trial
"""
# If the upgrade deadline has passed, return "True" for expired
DAYS_SINCE_UPGRADE_DEADLINE = datetime.now() - upgrade_deadline
if DAYS_SINCE_UPGRADE_DEADLINE >= timedelta(days=0):
return True
default_trial_length_days = 14

trial_length_days = getattr(settings, 'LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS', default_trial_length_days)

if trial_length_days is None:
trial_length_days = default_trial_length_days

# If LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS is set to a negative number, assume it should be 0.
# pylint: disable=consider-using-max-builtin
if trial_length_days < 0:
trial_length_days = 0

expiration_datetime = start_date + timedelta(days=trial_length_days)
return expiration_datetime


def get_audit_trial(user):
"""
Given a user, return the associated audit trial data.
Arguments:
* user (User): the user
Returns:
* audit_trial_data (LearningAssistantAuditTrialData): the audit trial data
* user_id (int): the user's id
* start_date (datetime): the start date of the audit trial
* expiration_date (datetime): the expiration date of the audit trial
* None: if no audit trial exists for the user
"""
try:
audit_trial = LearningAssistantAuditTrial.objects.get(user=user)
except LearningAssistantAuditTrial.DoesNotExist:
return None

return LearningAssistantAuditTrialData(
user_id=user.id,
start_date=audit_trial.start_date,
expiration_date=get_audit_trial_expiration_date(audit_trial.start_date),
)


def get_or_create_audit_trial(user):
"""
Given a user, return the associated audit trial data, creating a new audit trial for the user if one does not exist.
Arguments:
* user (User): the user
Returns:
* audit_trial_data (LearningAssistantAuditTrialData): the audit trial data
* user_id (int): the user's id
* start_date (datetime): the start date of the audit trial
* expiration_date (datetime): the expiration date of the audit trial
"""
audit_trial, _ = LearningAssistantAuditTrial.objects.get_or_create(
user=user,
defaults={
"start_date": datetime.now(),
},
)

# If the user's trial is past its expiry date, return "True" for expired. Else, return False
DAYS_SINCE_TRIAL_START_DATE = datetime.now() - audit_trial.start_date
return DAYS_SINCE_TRIAL_START_DATE >= timedelta(days=AUDIT_TRIAL_MAX_DAYS)
return LearningAssistantAuditTrialData(
user_id=user.id,
start_date=audit_trial.start_date,
expiration_date=get_audit_trial_expiration_date(audit_trial.start_date),
)


def audit_trial_is_expired(audit_trial_data, courserun_key):
"""
Given a user (User), get or create the corresponding LearningAssistantAuditTrial trial object.
"""
course_mode = CourseMode.objects.get(course=courserun_key)

upgrade_deadline = course_mode.expiration_datetime()

# If the upgrade deadline has passed, return True for expired. Upgrade deadline is an optional attribute of a
# CourseMode, so if it's None, then do not return True.
days_until_upgrade_deadline = datetime.now() - upgrade_deadline if upgrade_deadline else None
if days_until_upgrade_deadline is not None and days_until_upgrade_deadline >= timedelta(days=0):
return True

# If the user's trial is past its expiry date, return True for expired. Else, return False.
return audit_trial_data is None or audit_trial_data.expiration_date <= datetime.now()
13 changes: 13 additions & 0 deletions learning_assistant/data.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Data classes for the Learning Assistant application.
"""
from datetime import datetime

from attrs import field, frozen, validators
from opaque_keys.edx.keys import CourseKey

Expand All @@ -13,3 +15,14 @@ class LearningAssistantCourseEnabledData:

course_key: CourseKey = field(validator=validators.instance_of(CourseKey))
enabled: bool = field(validator=validators.instance_of(bool))


@frozen
class LearningAssistantAuditTrialData:
"""
Data class representing an audit learner's trial of the Learning Assistant.
"""

user_id: int = field(validator=validators.instance_of(int))
start_date: datetime = field(validator=validators.optional(validators.instance_of(datetime)))
expiration_date: datetime = field(validator=validators.optional(validators.instance_of(datetime)))
12 changes: 11 additions & 1 deletion learning_assistant/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from django.urls import re_path

from learning_assistant.constants import COURSE_ID_PATTERN
from learning_assistant.views import CourseChatView, LearningAssistantEnabledView, LearningAssistantMessageHistoryView
from learning_assistant.views import (
CourseChatView,
LearningAssistantChatSummaryView,
LearningAssistantEnabledView,
LearningAssistantMessageHistoryView,
)

app_name = 'learning_assistant'

Expand All @@ -24,4 +29,9 @@
LearningAssistantMessageHistoryView.as_view(),
name='message-history',
),
re_path(
fr'learning_assistant/v1/course_id/{COURSE_ID_PATTERN}/chat-summary',
LearningAssistantChatSummaryView.as_view(),
name='chat-summary',
),
]
Loading

0 comments on commit ed0061e

Please sign in to comment.