Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: playing with offline problem #2551

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions common/djangoapps/student/models/course_enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,71 @@ class UnenrollmentNotAllowed(CourseEnrollmentException):
pass


class CourseEnrollmentQuerySet(models.QuerySet):
"""
Custom queryset for CourseEnrollment with Table-level filter methods.
"""

def active(self):
"""
Returns a queryset of CourseEnrollment objects for courses that are currently active.
"""
return self.filter(is_active=True)

def without_certificates(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that do not have a certificate.
"""
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=user_username
).values_list('course_id', flat=True)
return self.exclude(course_id__in=course_ids_with_certificates)

def with_certificates(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that have a certificate.
"""
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=user_username
).values_list('course_id', flat=True)
return self.filter(course_id__in=course_ids_with_certificates)

def in_progress(self, user_username, time_zone=UTC):
"""
Returns a queryset of CourseEnrollment objects for courses that are currently in progress.
"""
now = datetime.now(time_zone)
return self.active().without_certificates(user_username).filter(
Q(course__start__lte=now, course__end__gte=now)
| Q(course__start__isnull=True, course__end__isnull=True)
| Q(course__start__isnull=True, course__end__gte=now)
| Q(course__start__lte=now, course__end__isnull=True),
)

def completed(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that have been completed.
"""
return self.active().with_certificates(user_username)

def expired(self, user_username, time_zone=UTC):
"""
Returns a queryset of CourseEnrollment objects for courses that have expired.
"""
now = datetime.now(time_zone)
return self.active().without_certificates(user_username).filter(course__end__lt=now)


class CourseEnrollmentManager(models.Manager):
"""
Custom manager for CourseEnrollment with Table-level filter methods.
"""

def get_queryset(self):
return CourseEnrollmentQuerySet(self.model, using=self._db)

def is_small_course(self, course_id):
"""
Returns false if the number of enrollments are one greater than 'max_enrollments' else true
Expand Down
152 changes: 152 additions & 0 deletions lms/djangoapps/course_home_api/dates/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Dates Tab Views
"""

from django.conf import settings
from edx_django_utils import monitoring as monitoring_utils
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
Expand All @@ -21,8 +22,152 @@
from lms.djangoapps.courseware.masquerade import setup_masquerade
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from xmodule.modulestore.django import modulestore


def enclosing_sequence_for_gating_checks(block):
seq_tags = ['sequential']
if block.location.block_type in seq_tags:
return None

ancestor = block
while ancestor and ancestor.location.block_type not in seq_tags:
ancestor = ancestor.get_parent() # Note: CourseBlock's parent is None

if ancestor:
return block.runtime.get_block(ancestor.location)
return None


def xblock_view_handler(request, xblock, check_if_enrolled=True, disable_staff_debug_info=False):
"""
Helper function to render an XBlock and return the rendered HTML content.
"""
from edx_django_utils.monitoring import set_custom_attribute, set_custom_attributes_for_course_key
from lms.djangoapps.courseware.courses import get_course_with_access
from lms.djangoapps.courseware.block_render import get_block, get_block_by_usage_id, get_block_for_descriptor
from xmodule.modulestore.exceptions import ItemNotFoundError, NoPathToItem
from openedx.features.course_experience.url_helpers import (
get_courseware_url,
get_learning_mfe_home_url,
is_request_from_learning_mfe
)
from openedx.core.lib.mobile_utils import is_request_from_mobile_app
from openedx.features.course_experience.utils import dates_banner_should_display
from lms.djangoapps.courseware.masquerade import is_masquerading_as_specific_student, setup_masquerade
from lms.djangoapps.courseware.views.views import get_optimization_flags_for_content
from lms.djangoapps.edxnotes.helpers import is_feature_enabled
from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
from common.djangoapps.edxmako.shortcuts import marketing_link, render_to_response, render_to_string
usage_key = xblock.usage_key

usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key))
course_key = usage_key.course_key

# Gathering metrics to make performance measurements easier.
set_custom_attributes_for_course_key(course_key)
set_custom_attribute('usage_key', str(usage_key))
set_custom_attribute('block_type', usage_key.block_type)

staff_access = has_access(request.user, 'staff', course_key)

with modulestore().bulk_operations(course_key):
# verify the user has access to the course, including enrollment check
try:
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=check_if_enrolled)
except:
return None

_course_masquerade, request.user = setup_masquerade(
request,
course_key,
staff_access,
)

UserActivity.record_user_activity(
request.user, usage_key.course_key, request=request, only_if_mobile_app=True
)

recheck_access = request.GET.get('recheck_access') == '1'
try:
block, _ = get_block_by_usage_id(
request,
str(course_key),
str(usage_key),
disable_staff_debug_info=disable_staff_debug_info,
course=course,
will_recheck_access=recheck_access,
)
except:
return

student_view_context = request.GET.dict()
student_view_context['show_bookmark_button'] = request.GET.get('show_bookmark_button', '0') == '1'
student_view_context['show_title'] = request.GET.get('show_title', '1') == '1'

is_learning_mfe = is_request_from_learning_mfe(request)
student_view_context['hide_access_error_blocks'] = is_learning_mfe and recheck_access
is_mobile_app = is_request_from_mobile_app(request)
student_view_context['is_mobile_app'] = is_mobile_app

enable_completion_on_view_service = False
completion_service = block.runtime.service(block, 'completion')
if completion_service and completion_service.completion_tracking_enabled():
if completion_service.blocks_to_mark_complete_on_view({block}):
enable_completion_on_view_service = True
student_view_context['wrap_xblock_data'] = {
'mark-completed-on-view-after-delay': completion_service.get_complete_on_view_delay_ms()
}

missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user)

ancestor_sequence_block = enclosing_sequence_for_gating_checks(block)
if False:
context = {'specific_masquerade': is_masquerading_as_specific_student(request.user, course_key)}
if ancestor_sequence_block.descendants_are_gated(context):
return redirect(
reverse(
'render_xblock',
kwargs={'usage_key_string': str(ancestor_sequence_block.location)}
)
)

if False:
seq_block = ancestor_sequence_block if ancestor_sequence_block else block
if getattr(seq_block, 'is_time_limited', None):
if not _check_sequence_exam_access(request, seq_block.location):
return HttpResponseForbidden("Access to exam content is restricted")

fragment = block.render('student_view', context=student_view_context)
optimization_flags = get_optimization_flags_for_content(block, fragment)

context = {
'fragment': fragment,
'course': course,
'block': block,
'disable_accordion': True,
'allow_iframing': True,
'disable_header': True,
'disable_footer': True,
'disable_window_wrap': True,
'enable_completion_on_view_service': enable_completion_on_view_service,
'edx_notes_enabled': is_feature_enabled(course, request.user),
'staff_access': staff_access,
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
'missed_deadlines': missed_deadlines,
'missed_gated_content': missed_gated_content,
'has_ended': course.has_ended(),
'web_app_course_url': get_learning_mfe_home_url(course_key=course.id, url_fragment='home'),
'on_courseware_page': True,
'verified_upgrade_link': verified_upgrade_deadline_link(request.user, course=course),
'is_learning_mfe': is_learning_mfe,
'is_mobile_app': is_mobile_app,
'render_course_wide_assets': True,

**optimization_flags,
}
return render_to_string('courseware/courseware-chromeless.html', context)

class DatesTabView(RetrieveAPIView):
"""
**Use Cases**
Expand Down Expand Up @@ -75,6 +220,13 @@ class DatesTabView(RetrieveAPIView):
def get(self, request, *args, **kwargs):
course_key_string = kwargs.get('course_key_string')
course_key = CourseKey.from_string(course_key_string)
import pdb; pdb.set_trace()
for xblock in modulestore().get_items(course_key, qualifiers={'category': 'problem'}):
try:
response_xblock = xblock_view_handler(request, xblock)
xblock.update_info_api(response_xblock)
except:
pass

# Enable NR tracing for this view based on course
monitoring_utils.set_custom_attribute('course_id', course_key_string)
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/discussion/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def create_message_context(comment, site):
'course_id': str(thread.course_id),
'comment_id': comment.id,
'comment_body': comment.body,
'comment_body_text': comment.body_text,
'comment_author_id': comment.user_id,
'comment_created_at': comment.created_at, # comment_client models dates are already serialized
'thread_id': thread.id,
Expand Down
5 changes: 5 additions & 0 deletions lms/djangoapps/discussion/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ def _build_message_context(context): # lint-amnesty, pylint: disable=missing-fu
'thread_username': thread_author.username,
'comment_username': comment_author.username,
'post_link': post_link,
'push_notification_extra_context': {
'notification_type': 'forum_comment',
'thread_id': context['thread_id'],
'comment_id': context['comment_id'],
},
'comment_created_at': date.deserialize(context['comment_created_at']),
'thread_created_at': date.deserialize(context['thread_created_at'])
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load i18n %}
{% blocktrans trimmed %}{{ comment_username }} replied to {{ thread_title }}:{% endblocktrans %}
{{ comment_body_text }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load i18n %}

{% blocktrans %}Response to {{ thread_title }}{% endblocktrans %}
8 changes: 7 additions & 1 deletion lms/djangoapps/discussion/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ def test_send_discussion_email_notification(self, user_subscribed):
expected_message_context.update({
'comment_author_id': self.comment_author.id,
'comment_body': comment['body'],
'comment_body_text': comment.body_text,
'comment_created_at': ONE_HOUR_AGO,
'comment_id': comment['id'],
'comment_username': self.comment_author.username,
Expand All @@ -283,7 +284,12 @@ def test_send_discussion_email_notification(self, user_subscribed):
'thread_commentable_id': thread['commentable_id'],
'post_link': f'https://{site.domain}{self.mock_permalink.return_value}',
'site': site,
'site_id': site.id
'site_id': site.id,
'push_notification_extra_context': {
'notification_type': 'forum_comment',
'thread_id': thread['id'],
'comment_id': comment['id'],
},
})
expected_recipient = Recipient(self.thread_author.id, self.thread_author.email)
actual_message = self.mock_ace_send.call_args_list[0][0][0]
Expand Down
5 changes: 5 additions & 0 deletions lms/djangoapps/mobile_api/course_info/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Common constants for the `course_info` API.
"""

BLOCK_STRUCTURE_CACHE_TIMEOUT = 60 * 60 # 1 hour
43 changes: 43 additions & 0 deletions lms/djangoapps/mobile_api/course_info/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Common utils for the `course_info` API.
"""

import logging
from typing import List, Optional, Union

from django.core.cache import cache

from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.grades.api import CourseGradeFactory
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager

log = logging.getLogger(__name__)


def calculate_progress(
user: 'User', # noqa: F821
course_id: 'CourseLocator', # noqa: F821
cache_timeout: int,
) -> Optional[List[Union['ReadSubsectionGrade', 'ZeroSubsectionGrade']]]: # noqa: F821
"""
Calculate the progress of the user in the course.
"""
is_staff = bool(has_access(user, 'staff', course_id))

try:
cache_key = f'course_block_structure_{str(course_id)}_{user.id}'
collected_block_structure = cache.get(cache_key)
if not collected_block_structure:
collected_block_structure = get_block_structure_manager(course_id).get_collected()
cache.set(cache_key, collected_block_structure, cache_timeout)

course_grade = CourseGradeFactory().read(user, collected_block_structure=collected_block_structure)

# recalculate course grade from visible grades (stored grade was calculated over all grades, visible or not)
course_grade.update(visible_grades_only=True, has_staff_access=is_staff)
subsection_grades = list(course_grade.subsection_grades.values())
except Exception as err: # pylint: disable=broad-except
log.warning(f'Could not get grades for the course: {course_id}, error: {err}')
return []

return subsection_grades
Loading
Loading