Merge pull request #28675 from edx/AA-905

Populate the course goals user activity table when a user visits course-specific pages
This commit is contained in:
Matthew Piatetsky
2021-09-20 13:52:27 -04:00
committed by GitHub
16 changed files with 469 additions and 8 deletions

View File

@@ -13,6 +13,8 @@ from opaque_keys.edx.keys import CourseKey
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -309,6 +311,10 @@ class BlocksInCourseView(BlocksView):
response = super().list(request, course_usage_key,
hide_access_denials=hide_access_denials)
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(request.user, course_key, request=request, only_if_mobile_app=True)
calculate_completion = any('completion' in param
for param in request.query_params.getlist('requested_fields', []))
if not calculate_completion:

View File

@@ -3,14 +3,22 @@ Course Goals Models
"""
import uuid
import logging
import pytz
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import ugettext_lazy as _
from edx_django_utils.cache import TieredCache
from model_utils import Choices
from opaque_keys.edx.django.models import CourseKeyField
from simple_history.models import HistoricalRecords
from lms.djangoapps.courseware.masquerade import is_masquerading
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
# Each goal is represented by a goal key and a string description.
GOAL_KEY_CHOICES = Choices(
('certify', _('Earn a certificate')),
@@ -20,6 +28,7 @@ GOAL_KEY_CHOICES = Choices(
)
User = get_user_model()
log = logging.getLogger(__name__)
class CourseGoal(models.Model):
@@ -84,3 +93,64 @@ class UserActivity(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
course_key = CourseKeyField(max_length=255)
date = models.DateField()
@classmethod
def record_user_activity(cls, user, course_key, request=None, only_if_mobile_app=False):
'''
Update the user activity table with a record for this activity.
Since we store one activity per date, we don't need to query the database
for every activity on a given date.
To avoid unnecessary queries, we store a record in a cache once we have an activity for the date,
which times out at the end of that date (in the user's timezone).
The request argument is only used to check if the request is coming from a mobile app.
Once the only_if_mobile_app argument is removed the request argument can be removed as well.
The return value is the id of the object that was created, or retrieved.
A return value of None signifies that there was an issue with the parameters (or the user was masquerading).
'''
if not (user and user.id) or not course_key:
return None
if only_if_mobile_app and request and not is_request_from_mobile_app(request):
return None
if is_masquerading(user, course_key):
return None
user_preferences = get_user_preferences(user)
timezone = pytz.timezone(user_preferences.get('time_zone', 'UTC'))
now = datetime.now(timezone)
date = now.date()
cache_key = 'goals_user_activity_{}_{}_{}'.format(str(user.id), str(course_key), str(date))
cached_value = TieredCache.get_cached_response(cache_key)
if cached_value.is_found:
# Temporary debugging log for testing mobile app connection
if request:
log.info(
'Retrieved cached value with request {} for user and course combination {} {}'.format(
str(request.build_absolute_uri()), str(user.id), str(course_key)
)
)
return cached_value.value, False
activity_object, __ = cls.objects.get_or_create(user=user, course_key=course_key, date=date)
# Cache result until the end of the day to avoid unnecessary database requests
tomorrow = now + timedelta(days=1)
midnight = datetime(year=tomorrow.year, month=tomorrow.month,
day=tomorrow.day, hour=0, minute=0, second=0, tzinfo=timezone)
seconds_until_midnight = (midnight - now).seconds
TieredCache.set_all_tiers(cache_key, activity_object.id, seconds_until_midnight)
# Temporary debugging log for testing mobile app connection
if request:
log.info(
'Set cached value with request {} for user and course combination {} {}'.format(
str(request.build_absolute_uri()), str(user.id), str(course_key)
)
)
return activity_object.id

View File

@@ -0,0 +1,205 @@
"""
Unit tests for user activity methods.
"""
from datetime import datetime, timedelta
import ddt
from django.contrib.auth import get_user_model
from django.test.client import RequestFactory
from django.urls import reverse
from edx_django_utils.cache import TieredCache
from edx_toggles.toggles.testutils import override_waffle_flag
from freezegun import freeze_time
from mock import patch
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.util.testing import UrlResetMixin
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from openedx.core.djangoapps.django_comment_common.models import ForumsConfig
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_SPLIT_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
User = get_user_model()
@ddt.ddt
@override_waffle_flag(RECORD_USER_ACTIVITY_FLAG, active=True)
class UserActivityTests(UrlResetMixin, ModuleStoreTestCase):
"""
Testing Course Goals User Activity
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
def setUp(self):
super().setUp()
self.course = CourseFactory.create(
start=datetime(2020, 1, 1),
end=datetime(2028, 1, 1),
enrollment_start=datetime(2020, 1, 1),
enrollment_end=datetime(2028, 1, 1),
emit_signals=True,
modulestore=self.store,
discussion_topics={"Test Topic": {"id": "test_topic"}},
)
chapter = ItemFactory(parent=self.course, category='chapter')
ItemFactory(parent=chapter, category='sequential')
self.client.login(username=self.user.username, password=self.user_password)
CourseEnrollment.enroll(self.user, self.course.id)
self.request = RequestFactory().get('foo')
self.request.user = self.user
config = ForumsConfig.current()
config.enabled = True
config.save()
def test_mfe_tabs_call_user_activity(self):
'''
New style tabs call one of two metadata endpoints
These in turn call get_course_tab_list, which records user activity
'''
url = reverse('course-home:course-metadata', args=[self.course.id])
with patch.object(UserActivity, 'record_user_activity') as record_user_activity_mock:
self.client.get(url)
record_user_activity_mock.assert_called_once()
with patch.object(UserActivity, 'record_user_activity') as record_user_activity_mock:
url = f'/api/courseware/course/{self.course.id}'
self.client.get(url)
record_user_activity_mock.assert_called_once()
def test_non_mfe_tabs_call_user_activity(self):
'''
Tabs that are not yet part of the learning microfrontend all include the course_navigation.html file
This file calls the get_course_tab_list function, which records user activity
'''
with patch.object(UserActivity, 'record_user_activity') as record_user_activity_mock:
render_to_response('courseware/course_navigation.html', {'course': self.course, 'request': self.request})
record_user_activity_mock.assert_called_once()
def test_when_record_user_activity_does_not_perform_updates(self):
'''
Ensure that record user activity is not called when:
1. user or course are not defined
2. we have already recorded user activity for this user/course on this date
and have a record in the cache
'''
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as activity_cache_set:
UserActivity.record_user_activity(self.user, None)
activity_cache_set.assert_not_called()
UserActivity.record_user_activity(None, self.course.id)
activity_cache_set.assert_not_called()
cache_key = 'goals_user_activity_{}_{}_{}'.format(
str(self.user.id), str(self.course.id), str(datetime.now().date())
)
TieredCache.set_all_tiers(cache_key, 'test', 3600)
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as activity_cache_set:
UserActivity.record_user_activity(self.user, self.course.id)
activity_cache_set.assert_not_called()
# Test that the happy path works to ensure that the measurement in this test isn't broken
user2 = UserFactory()
UserActivity.record_user_activity(user2, self.course.id)
activity_cache_set.assert_called_once()
def test_that_user_activity_cache_works_properly(self):
'''
Ensure that the cache for user activity works properly
1. user or course are not defined
2. we have already recorded user activity for this user/course on this date
and have a record in the cache
'''
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as activity_cache_set:
UserActivity.record_user_activity(self.user, self.course.id)
activity_cache_set.assert_called_once()
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as activity_cache_set:
UserActivity.record_user_activity(self.user, self.course.id)
activity_cache_set.assert_not_called()
now_plus_1_day = datetime.now() + timedelta(days=1)
with freeze_time(now_plus_1_day):
UserActivity.record_user_activity(self.user, self.course.id)
activity_cache_set.assert_called_once()
def test_mobile_argument(self):
'''
Method only records activity if the request is coming from the mobile app
when the only_if_mobile_app argument is true
'''
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as activity_cache_set:
UserActivity.record_user_activity(
self.user, self.course.id, request=self.request, only_if_mobile_app=True
)
activity_cache_set.assert_not_called()
with patch('lms.djangoapps.course_goals.models.is_request_from_mobile_app', return_value=True):
UserActivity.record_user_activity(
self.user, self.course.id, request=self.request, only_if_mobile_app=True
)
activity_cache_set.assert_called_once()
def test_masquerading(self):
'''
Method only records activity if the user is not masquerading
'''
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as activity_cache_set:
UserActivity.record_user_activity(self.user, self.course.id)
activity_cache_set.assert_called_once()
with patch.object(TieredCache, 'set_all_tiers', wraps=TieredCache.set_all_tiers) as activity_cache_set:
with patch('lms.djangoapps.course_goals.models.is_masquerading', return_value=True):
UserActivity.record_user_activity(self.user, self.course.id)
activity_cache_set.assert_not_called()
@ddt.data(
'/api/course_home/v1/dates/{COURSE_ID}',
'/api/mobile/v0.5/course_info/{COURSE_ID}/handouts',
'/api/mobile/v0.5/course_info/{COURSE_ID}/updates',
'/api/course_experience/v1/course_deadlines_info/{COURSE_ID}',
'/api/course_home/v1/dates/{COURSE_ID}',
'/api/courseware/course/{COURSE_ID}',
'/api/discussion/v1/courses/{COURSE_ID}/',
'/api/discussion/v1/course_topics/{COURSE_ID}',
)
def test_mobile_app_user_activity_calls(self, url):
url = url.replace('{COURSE_ID}', str(self.course.id))
with patch.object(UserActivity, 'record_user_activity') as record_user_activity_mock:
with patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}):
self.client.get(url)
record_user_activity_mock.assert_called_once()
def test_mobile_app_user_activity_other_calls(self):
# thread view call
with patch.object(UserActivity, 'record_user_activity') as record_user_activity_mock:
try:
self.client.get(reverse("thread-list"), {'course_id': str(self.course.id)})
except: # pylint: disable=bare-except
pass
record_user_activity_mock.assert_called_once()
# blocks call
with patch.object(UserActivity, 'record_user_activity') as record_user_activity_mock:
url = '/api/courses/v2/blocks/'
self.client.get(url, {'course_id': str(self.course.id), 'username': self.user.username})
record_user_activity_mock.assert_called_once()
# xblock call
with patch.object(UserActivity, 'record_user_activity') as record_user_activity_mock:
url = '/xblock/' + str(self.course.scope_ids.usage_id)
try:
self.client.get(url)
except: # pylint: disable=bare-except
pass
record_user_activity_mock.assert_called_once()

View File

@@ -1,11 +1,10 @@
"""
Unit tests for course_goals.api methods.
Unit tests for course_goals.views methods.
"""
from unittest import mock
from django.contrib.auth import get_user_model
from django.test.utils import override_settings
from django.urls import reverse
from rest_framework.test import APIClient
@@ -19,8 +18,6 @@ from xmodule.modulestore.tests.factories import CourseFactory
EVENT_NAME_ADDED = 'edx.course.goal.added'
EVENT_NAME_UPDATED = 'edx.course.goal.updated'
User = get_user_model()
class TestCourseGoalsAPI(SharedModuleStoreTestCase):
"""

View File

@@ -19,3 +19,14 @@ WAFFLE_FLAG_NAMESPACE = LegacyWaffleFlagNamespace(name='course_goals')
# .. toggle_target_removal_date: 2021-09-01
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-859
COURSE_GOALS_NUMBER_OF_DAYS_GOALS = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'number_of_days_goals', __name__)
# .. toggle_name: course_goals.record_user_activity
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: This flag enables populating user activity for tracking a user's progress towards course goals
# .. toggle_warnings: None
# .. toggle_use_cases: temporary
# .. toggle_creation_date: 2021-09-16
# .. toggle_target_removal_date: 2021-11-16
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-905
RECORD_USER_ACTIVITY_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'record_user_activity', __name__)

View File

@@ -13,6 +13,8 @@ from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_api.api import course_detail
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_home_api.course_metadata.serializers import CourseHomeMetadataSerializer
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs
@@ -114,6 +116,10 @@ class CourseHomeMetadataView(RetrieveAPIView):
request.user, enrollment, course, user_timezone if not None else browser_timezone
)
# Record course goals user activity for (web) learning mfe course tabs
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
UserActivity.record_user_activity(request.user, course_key)
data = {
'course_id': course.id,
'username': username,

View File

@@ -12,6 +12,8 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.course_home_api.dates.serializers import DatesTabSerializer
from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active
from lms.djangoapps.courseware.access import has_access
@@ -93,6 +95,10 @@ class DatesTabView(RetrieveAPIView):
reset_masquerade_data=True,
)
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(request.user, course.id, request=request, only_if_mobile_app=True)
if not CourseEnrollment.is_enrolled(request.user, course_key) and not is_staff:
return Response('User not enrolled.', status=401)

View File

@@ -54,6 +54,8 @@ from lms.djangoapps.ccx.custom_exception import CCXLocatorValidationException
from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.course_home_api.toggles import (
course_home_legacy_is_active,
course_home_mfe_progress_tab_is_active
@@ -1730,6 +1732,12 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
staff_access,
)
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(
request.user, usage_key.course_key, request=request, only_if_mobile_app=True
)
# get the block, which verifies whether the user has access to the block.
recheck_access = request.GET.get('recheck_access') == '1'
block, _ = get_module_by_usage_id(

View File

@@ -18,6 +18,8 @@ from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet
from xmodule.modulestore.django import modulestore
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.instructor.access import update_forum_role
from openedx.core.djangoapps.django_comment_common import comment_client
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings, Role
@@ -90,6 +92,9 @@ class CourseView(DeveloperErrorViewMixin, APIView):
def get(self, request, course_id):
"""Implements the GET method as described in the class docstring."""
course_key = CourseKey.from_string(course_id) # TODO: which class is right?
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(request.user, course_key, request=request, only_if_mobile_app=True)
return Response(get_course(request, course_key))
@@ -132,6 +137,9 @@ class CourseTopicsView(DeveloperErrorViewMixin, APIView):
course_key,
set(topic_ids.strip(',').split(',')) if topic_ids else None,
)
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(request.user, course_key, request=request, only_if_mobile_app=True)
return Response(response)
@@ -322,6 +330,13 @@ class ThreadViewSet(DeveloperErrorViewMixin, ViewSet):
form = ThreadListGetForm(request.GET)
if not form.is_valid():
raise ValidationError(form.errors)
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(
request.user, form.cleaned_data["course_id"], request=request, only_if_mobile_app=True
)
return get_thread_list(
request,
form.cleaned_data["course_id"],

View File

@@ -5,13 +5,21 @@ Tests for course_info
import ddt
from django.conf import settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from milestones.tests.utils import MilestonesTestCaseMixin
from rest_framework.test import APIClient
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin
from lms.djangoapps.mobile_api.utils import API_V1, API_V05
from xmodule.html_module import CourseInfoBlock
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.xml_importer import import_course_from_xml
@@ -219,3 +227,54 @@ class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTes
self.course.mobile_available = True
self.store.update_item(self.course, self.user.id)
self.login_and_enroll()
@override_waffle_flag(RECORD_USER_ACTIVITY_FLAG, active=True)
class TestCourseGoalsUserActivityAPI(MobileAPITestCase, SharedModuleStoreTestCase):
"""
Testing the Course Goals User Activity API.
"""
def setUp(self):
super().setUp()
self.apiUrl = reverse('record_user_activity', args=['v1'])
self.login_and_enroll()
def test_record_activity(self):
'''
Test the happy path of recording user activity
'''
post_data = {
'course_key': self.course.id,
'user_id': self.user.id,
}
response = self.client.post(self.apiUrl, post_data)
assert response.status_code == 200
def test_invalid_parameters(self):
'''
Ensure that we check that parameters meet the requirements
and return a 400 otherwise.
'''
post_data = {
'course_key': self.course.id,
}
response = self.client.post(self.apiUrl, post_data)
assert response.status_code == 400
post_data = {
'user_id': self.user.id,
}
response = self.client.post(self.apiUrl, post_data)
assert response.status_code == 400
post_data = {
'user_id': self.user.id,
'course_key': 'invalidcoursekey',
}
response = self.client.post(self.apiUrl, post_data)
assert response.status_code == 400

View File

@@ -6,7 +6,7 @@ URLs for course_info API
from django.conf import settings
from django.conf.urls import url
from .views import CourseHandoutsList, CourseUpdatesList
from .views import CourseHandoutsList, CourseUpdatesList, CourseGoalsRecordUserActivity
urlpatterns = [
url(
@@ -19,4 +19,5 @@ urlpatterns = [
CourseUpdatesList.as_view(),
name='course-updates-list'
),
url(r'^record_user_activity$', CourseGoalsRecordUserActivity.as_view(), name='record_user_activity'),
]

View File

@@ -2,16 +2,22 @@
Views for course info API
"""
from rest_framework import generics
from django.contrib.auth import get_user_model
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.views import APIView
from common.djangoapps.static_replace import make_static_urls_absolute
from lms.djangoapps.courseware.courses import get_course_info_section_module
from lms.djangoapps.course_goals.models import UserActivity
from openedx.core.lib.xblock_utils import get_course_update_items
from ..decorators import mobile_course_access, mobile_view
User = get_user_model()
@mobile_view()
class CourseUpdatesList(generics.ListAPIView):
@@ -107,3 +113,46 @@ def apply_wrappers_to_content(content, module, request):
content = module.system.replace_jump_to_id_urls(content)
return make_static_urls_absolute(request, content)
@mobile_view()
class CourseGoalsRecordUserActivity(APIView):
"""
API that allows the mobile_apps to record activity for course goals to the user activity table
"""
def post(self, request, *args, **kwargs):
"""
Handle the POST request
Populate the user activity table.
"""
user_id = request.data.get('user_id')
course_key = request.data.get('course_key')
if not user_id or not course_key:
return Response(
'User id and course key are required',
status=status.HTTP_400_BAD_REQUEST,
)
try:
user_id = int(user_id)
user = User.objects.get(id=user_id)
except User.DoesNotExist:
return Response(
'Provided user id does not correspond to an existing user',
status=status.HTTP_400_BAD_REQUEST,
)
try:
course_key = CourseKey.from_string(course_key)
except InvalidKeyError:
return Response(
'Provided course key is not valid',
status=status.HTTP_400_BAD_REQUEST,
)
# Populate user activity for tracking progress towards a user's course goals
UserActivity.record_user_activity(user, course_key)
return Response(status=(200))

View File

@@ -10,6 +10,8 @@ from opaque_keys.edx.keys import CourseKey
from rest_framework import status
from rest_framework.response import Response
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.courseware.courses import get_course_with_access
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
@@ -41,6 +43,11 @@ def mobile_course_access(depth=0):
depth=depth,
check_if_enrolled=True,
)
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(
request.user, course_id, request=request, only_if_mobile_app=True
)
except CoursewareAccessException as error:
return Response(data=error.to_json(), status=status.HTTP_404_NOT_FOUND)
except CourseAccessRedirect as error:

View File

@@ -6,6 +6,9 @@
<%!
from lms.djangoapps.courseware.masquerade import is_masquerading_as_student
from lms.djangoapps.courseware.tabs import get_course_tab_list
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from django.conf import settings
from django.urls import reverse
from django.utils.translation import ugettext as _
@@ -39,6 +42,10 @@ masquerading_as_student = is_masquerading_as_student(request.user, course.id)
% if disable_tabs is UNDEFINED or not disable_tabs:
<%
tab_list = get_course_tab_list(request.user, course)
# Record course goals user activity for (web) courseware and course tabs that are outside of the learning mfe
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
UserActivity.record_user_activity(user, course.id)
%>
% if uses_bootstrap:
<nav class="navbar course-tabs pb-0 navbar-expand" aria-label="${_('Course')}">

View File

@@ -23,6 +23,8 @@ from lms.djangoapps.edxnotes.helpers import is_feature_enabled
from lms.djangoapps.certificates.api import get_certificate_url
from lms.djangoapps.certificates.models import GeneratedCertificate
from lms.djangoapps.course_api.api import course_detail
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.access_response import (
CoursewareMicrofrontendDisabledAccessError,
@@ -458,11 +460,15 @@ class CoursewareInformation(RetrieveAPIView):
username = self.request.GET.get('username', '') or self.request.user.username
else:
username = self.request.user.username
course_key = CourseKey.from_string(self.kwargs['course_key_string'])
overview = CoursewareMeta(
CourseKey.from_string(self.kwargs['course_key_string']),
course_key,
self.request,
username=username,
)
# Record course goals user activity for learning mfe courseware on web
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
UserActivity.record_user_activity(self.request.user, course_key)
return overview

View File

@@ -20,6 +20,8 @@ from edx_rest_framework_extensions.auth.session.authentication import SessionAut
from opaque_keys.edx.keys import CourseKey
from lms.djangoapps.course_api.api import course_detail
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_goals.toggles import RECORD_USER_ACTIVITY_FLAG
from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active
from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.courseware.courses import get_course_with_access
@@ -152,5 +154,11 @@ class CourseDeadlinesMobileView(RetrieveAPIView):
# Although this course data is not used this method will return 404 if course does not exist
get_course_with_access(request.user, 'load', course_key)
if RECORD_USER_ACTIVITY_FLAG.is_enabled():
# Record user activity for tracking progress towards a user's course goals (for mobile app)
UserActivity.record_user_activity(
request.user, course_key, request=request, only_if_mobile_app=True
)
serializer = self.get_serializer({})
return Response(serializer.data)