def get_course_date_blocks(course, user, request=None, include_access=False, include_past_dates=False, num_assignments=None): """ Return the list of blocks to display on the course info page, sorted by date. """ block_classes = [ CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate, ] if not course.self_paced and certs_api.get_active_web_certificate(course): block_classes.insert(0, CertificateAvailableDate) blocks = [cls(course, user) for cls in block_classes] if RELATIVE_DATES_FLAG.is_enabled(course.id): blocks.append(CourseExpiredDate(course, user)) blocks.extend(get_course_assignment_date_blocks( course, user, request, num_return=num_assignments, include_access=include_access, include_past_dates=include_past_dates, )) return sorted((b for b in blocks if b.date and (b.is_enabled or include_past_dates)), key=date_block_key_fn)
def dates_banner_should_display(course_key, user): """ Return whether or not the reset banner should display, determined by whether or not a course has any past-due, incomplete sequentials and which enrollment mode is being dealt with for the current user and course. Returns: (missed_deadlines, missed_gated_content): missed_deadlines is True if the user has missed any graded content deadlines missed_gated_content is True if the first content that the user missed was gated content """ if not RELATIVE_DATES_FLAG.is_enabled(course_key): return False, False course_overview = CourseOverview.objects.get(id=str(course_key)) course_end_date = getattr(course_overview, 'end_date', None) is_self_paced = getattr(course_overview, 'self_paced', False) # Only display the banner for self-paced courses if not is_self_paced: return False, False # Only display the banner for enrolled users if not CourseEnrollment.is_enrolled(user, course_key): return False, False # Don't display the banner for course staff is_course_staff = bool( user and course_overview and has_access(user, 'staff', course_overview, course_overview.id)) if is_course_staff: return False, False # Don't display the banner if the course has ended if course_end_date and course_end_date < timezone.now(): return False, False store = modulestore() course_usage_key = store.make_course_usage_key(course_key) block_data = get_course_blocks(user, course_usage_key, include_completion=True) for section_key in block_data.get_children(course_usage_key): for subsection_key in block_data.get_children(section_key): subsection_due_date = block_data.get_xblock_field( subsection_key, 'due', None) if (subsection_due_date and subsection_due_date < timezone.now() and not is_block_structure_complete_for_assignments( block_data, subsection_key)): # Display the banner if the due date for an incomplete graded subsection has passed return True, block_data.get_xblock_field( subsection_key, 'contains_gated_content', False) # Don't display the banner if there were no missed deadlines return False, False
def date(self): if self.course.self_paced and RELATIVE_DATES_FLAG.is_enabled(self.course_id): weeks_to_complete = get_course_run_details(self.course.id, ['weeks_to_complete']).get('weeks_to_complete') if weeks_to_complete: course_duration = datetime.timedelta(weeks=weeks_to_complete) if self.course.end < (self.current_time + course_duration): return self.course.end return None return self.course.end
def _are_relative_dates_enabled(course_key=None): """ Return whether it's OK to consider relative dates. If not, pretend those database entries don't exist. """ try: # It's bad form to depend on LMS code from inside a plugin like this. But we gracefully fail, and this is # temporary code anyway, while we develop this feature. from openedx.features.course_experience import RELATIVE_DATES_FLAG except ImportError: return False return RELATIVE_DATES_FLAG.is_enabled(course_key)
def date(self): """ Returns the course end date, if applicable. For self-paced courses using Personalized Learner Schedules, the end date is only displayed if it is within 365 days. """ if self.course.self_paced and RELATIVE_DATES_FLAG.is_enabled(self.course_id): one_year = datetime.timedelta(days=365) if self.course.end and self.course.end < (self.current_time + one_year): return self.course.end return None return self.course.end
def render_to_fragment(self, request, course_id=None, **kwargs): """ Render the course dates fragment. """ from lms.urls import RESET_COURSE_DEADLINES_NAME from openedx.features.course_experience.urls import COURSE_DATES_FRAGMENT_VIEW_NAME course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) course_date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=2) display_reset_dates_banner = False if RELATIVE_DATES_FLAG.is_enabled(course.id): display_reset_dates_banner = reset_deadlines_banner_should_display( course_key, request) reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME ) if display_reset_dates_banner else None reset_deadlines_redirect_url_base = COURSE_DATES_FRAGMENT_VIEW_NAME if ( reset_deadlines_url) else None context = { 'course_date_blocks': [ block for block in course_date_blocks if block.title != 'current_datetime' ], 'display_reset_dates_banner': display_reset_dates_banner, 'reset_deadlines_url': reset_deadlines_url, 'reset_deadlines_redirect_url_base': reset_deadlines_redirect_url_base, 'reset_deadlines_redirect_url_id_dict': { 'course_id': course_id } } html = render_to_string(self.template_name, context) dates_fragment = Fragment(html) self.add_fragment_resource_urls(dates_fragment) return dates_fragment
def is_enabled(cls, request, course_key): """ The Calendar Sync toggle tool is limited to user enabled through a waffle flag. Staff always has access. """ if not (CALENDAR_SYNC_FLAG.is_enabled(course_key) and RELATIVE_DATES_FLAG.is_enabled(course_key)): return False if CourseEnrollment.is_enrolled(request.user, course_key): if UserCalendarSyncConfig.is_enabled_for_course(request.user, course_key): cls.link_title = _('Unsubscribe from calendar updates') cls.toggle_data['toggle_data'] = UNSUBSCRIBE else: cls.link_title = _('Subscribe to calendar updates') cls.toggle_data['toggle_data'] = SUBSCRIBE return True return False
def handle_calendar_sync_email(sender, instance, created, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument if (CALENDAR_SYNC_FLAG.is_enabled(instance.course_key) and RELATIVE_DATES_FLAG.is_enabled(instance.course_key) and created): user = instance.user email = user.email course_overview = CourseOverview.objects.get(id=instance.course_key) ics_files = generate_ics_files_for_user_course(course_overview, user, instance) send_email_with_attachment([email], ics_files, course_overview.display_name, created) post_save.disconnect(handle_calendar_sync_email, sender=UserCalendarSyncConfig) instance.ics_sequence = instance.ics_sequence + 1 instance.save() post_save.connect(handle_calendar_sync_email, sender=UserCalendarSyncConfig)
def dates_banner_should_display(course_key, request): """ Return whether or not the reset banner should display, determined by whether or not a course has any past-due, incomplete sequentials and which enrollment mode is being dealt with for the current user and course. """ missed_deadlines = False course_enrollment = None if RELATIVE_DATES_FLAG.is_enabled(str(course_key)): course_overview = CourseOverview.objects.get(id=str(course_key)) course_end_date = getattr(course_overview, 'end_date', None) is_self_paced = getattr(course_overview, 'self_paced', False) is_course_staff = bool(request.user and course_overview and has_access( request.user, 'staff', course_overview, course_overview.id)) if is_self_paced and (not is_course_staff) and ( not course_end_date or timezone.now() < course_end_date): course_enrollment = CourseEnrollment.objects.filter( course=course_overview, user=request.user, ).filter(Q(mode=CourseMode.AUDIT) | Q(mode=CourseMode.VERIFIED)).first() if course_enrollment: store = modulestore() course_usage_key = store.make_course_usage_key(course_key) block_data = get_course_blocks(request.user, course_usage_key, include_completion=True) for section_key in block_data.get_children(course_usage_key): if missed_deadlines: break for subsection_key in block_data.get_children(section_key): subsection_due_date = block_data.get_xblock_field( subsection_key, 'due', None) if subsection_due_date and ( not block_data.get_xblock_field( subsection_key, 'complete', False) and block_data.get_xblock_field( subsection_key, 'graded', False) and subsection_due_date < timezone.now()): missed_deadlines = True break return missed_deadlines, getattr(course_enrollment, 'mode', None)
def get_course_date_blocks(course, user, request=None, include_access=False, include_past_dates=False, num_assignments=None): """ Return the list of blocks to display on the course info page, sorted by date. """ blocks = [] if RELATIVE_DATES_FLAG.is_enabled(course.id): blocks.append(CourseExpiredDate(course, user)) blocks.extend( get_course_assignment_date_blocks( course, user, request, num_return=num_assignments, include_access=include_access, include_past_dates=include_past_dates, )) # Adding these in after the assignment blocks so in the case multiple blocks have the same date, # these blocks will be sorted to come after the assignments. See https://openedx.atlassian.net/browse/AA-158 default_block_classes = [ CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate, ] if not course.self_paced and certs_api.get_active_web_certificate(course): block_classes.insert(0, CertificateAvailableDate) blocks.extend([cls(course, user) for cls in default_block_classes]) return sorted( (b for b in blocks if b.date and (b.is_enabled or include_past_dates)), key=date_block_key_fn)
def get_course_date_blocks(course, user, request=None, include_access=False, include_past_dates=False, num_assignments=None): """ Return the list of blocks to display on the course info page, sorted by date. """ blocks = [] if RELATIVE_DATES_FLAG.is_enabled(course.id): blocks.extend( get_course_assignment_date_blocks( course, user, request, num_return=num_assignments, include_access=include_access, include_past_dates=include_past_dates, )) # Adding these in after the assignment blocks so in the case multiple blocks have the same date, # these blocks will be sorted to come after the assignments. See https://openedx.atlassian.net/browse/AA-158 default_block_classes = [ CertificateAvailableDate, CourseEndDate, CourseExpiredDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate, ] blocks.extend([cls(course, user) for cls in default_block_classes]) blocks = filter( lambda b: b.is_allowed and b.date and (include_past_dates or b.is_enabled), blocks) return sorted(blocks, key=date_block_key_fn)
def is_enabled(cls, course, user=None): """Returns true if this tab is enabled.""" if not super().is_enabled(course, user=user): return False return RELATIVE_DATES_FLAG.is_enabled(course.id)
def is_enabled(cls, course, user=None): """Returns true if this tab is enabled.""" return RELATIVE_DATES_FLAG.is_enabled(course.id) and super( DatesTab, cls).is_enabled(course, user=user)
def enabled_for(cls, course): """ Enabled only for Self-Paced courses using Personalized User Schedules. """ return course and course.self_paced and RELATIVE_DATES_FLAG.is_enabled( course.id)
def is_enabled(cls, course, user=None): """Returns true if this tab is enabled.""" # We want to only limit this feature to instructor led courses for now (and limit to relative dates experiment) return not CourseOverview.get_from_id( course.id).self_paced and RELATIVE_DATES_FLAG.is_enabled(course.id)
def is_enabled(cls, course, user=None): """Returns true if this tab is enabled.""" return RELATIVE_DATES_FLAG.is_enabled(course.id)
def _create_courseware_context(self, request): """ Returns and creates the rendering context for the courseware. Also returns the table of contents for the courseware. """ from lms.urls import RESET_COURSE_DEADLINES_NAME course_url_name = default_course_url_name(self.course.id) course_url = reverse( course_url_name, kwargs={'course_id': six.text_type(self.course.id)}) show_search = ( settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH') or (settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF') and self.is_staff)) staff_access = self.is_staff allow_anonymous = check_public_access(self.course, [COURSE_VISIBILITY_PUBLIC]) display_reset_dates_banner = False if not allow_anonymous and RELATIVE_DATES_FLAG.is_enabled( self.course.id): display_reset_dates_banner = reset_deadlines_banner_should_display( self.course_key, request) reset_deadlines_url = reverse(RESET_COURSE_DEADLINES_NAME ) if display_reset_dates_banner else None reset_deadlines_redirect_url_base = COURSE_HOME_VIEW_NAME if reset_deadlines_url else None courseware_context = { 'csrf': csrf(self.request)['csrf_token'], 'course': self.course, 'course_url': course_url, 'chapter': self.chapter, 'section': self.section, 'init': '', 'fragment': Fragment(), 'staff_access': staff_access, 'can_masquerade': self.can_masquerade, 'masquerade': self.masquerade, 'supports_preview_menu': True, 'studio_url': get_studio_url(self.course, 'course'), 'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"), 'bookmarks_api_url': reverse('bookmarks'), 'language_preference': self._get_language_preference(), 'disable_optimizely': not WaffleSwitchNamespace('RET').is_enabled( 'enable_optimizely_in_courseware'), 'section_title': None, 'sequence_title': None, 'disable_accordion': COURSE_OUTLINE_PAGE_FLAG.is_enabled(self.course.id), 'show_search': show_search, 'relative_dates_is_enabled': RELATIVE_DATES_FLAG.is_enabled(self.course.id), 'display_reset_dates_banner': display_reset_dates_banner, 'reset_deadlines_url': reset_deadlines_url, 'reset_deadlines_redirect_url_base': reset_deadlines_redirect_url_base, 'reset_deadlines_redirect_url_id_dict': { 'course_id': str(self.course.id) }, } courseware_context.update( get_experiment_user_metadata_context( self.course, self.effective_user, )) table_of_contents = toc_for_course( self.effective_user, self.request, self.course, self.chapter_url_name, self.section_url_name, self.field_data_cache, ) courseware_context['accordion'] = render_accordion( self.request, self.course, table_of_contents['chapters'], ) courseware_context['course_sock_fragment'] = CourseSockFragmentView( ).render_to_fragment(request, course=self.course) # entrance exam data self._add_entrance_exam_to_context(courseware_context) if self.section: # chromeless data if self.section.chrome: chrome = [ s.strip() for s in self.section.chrome.lower().split(",") ] if 'accordion' not in chrome: courseware_context['disable_accordion'] = True if 'tabs' not in chrome: courseware_context['disable_tabs'] = True # default tab if self.section.default_tab: courseware_context['default_tab'] = self.section.default_tab # section data courseware_context[ 'section_title'] = self.section.display_name_with_default section_context = self._create_section_context( table_of_contents['previous_of_active_section'], table_of_contents['next_of_active_section'], ) courseware_context['fragment'] = self.section.render( self.view, section_context) if self.section.position and self.section.has_children: self._add_sequence_title_to_context(courseware_context) # Courseware MFE link if show_courseware_mfe_link(request.user, staff_access, self.course.id): courseware_context['microfrontend_link'] = self.microfrontend_url else: courseware_context['microfrontend_link'] = None return courseware_context
def _create_courseware_context(self, request): """ Returns and creates the rendering context for the courseware. Also returns the table of contents for the courseware. """ course_url_name = default_course_url_name(self.course.id) course_url = reverse( course_url_name, kwargs={'course_id': six.text_type(self.course.id)}) show_search = ( settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH') or (settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF') and self.is_staff)) staff_access = self.is_staff reset_deadlines_url = reverse( 'openedx.course_experience.reset_course_deadlines', kwargs={'course_id': six.text_type(self.course.id)}) allow_anonymous = allow_public_access(self.course, [COURSE_VISIBILITY_PUBLIC]) display_reset_dates_banner = False if not allow_anonymous and RELATIVE_DATES_FLAG.is_enabled( self.course.id): # pylint: disable=too-many-nested-blocks course_overview = CourseOverview.objects.get( id=str(self.course_key)) end_date = getattr(course_overview, 'end_date') if course_overview.self_paced and (not end_date or timezone.now() < end_date): if (CourseEnrollment.objects.filter( course=course_overview, user=request.user, mode=CourseMode.VERIFIED).exists()): course_block_tree = get_course_outline_block_tree( request, str(self.course_key), request.user) course_sections = course_block_tree.get('children', []) for section in course_sections: if display_reset_dates_banner: break for subsection in section.get('children', []): if (not subsection.get('complete', True) and subsection.get( 'due', timezone.now() + timedelta(1)) < timezone.now()): display_reset_dates_banner = True break courseware_context = { 'csrf': csrf(self.request)['csrf_token'], 'course': self.course, 'course_url': course_url, 'chapter': self.chapter, 'section': self.section, 'init': '', 'fragment': Fragment(), 'staff_access': staff_access, 'can_masquerade': self.can_masquerade, 'masquerade': self.masquerade, 'supports_preview_menu': True, 'studio_url': get_studio_url(self.course, 'course'), 'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"), 'bookmarks_api_url': reverse('bookmarks'), 'language_preference': self._get_language_preference(), 'disable_optimizely': not WaffleSwitchNamespace('RET').is_enabled( 'enable_optimizely_in_courseware'), 'section_title': None, 'sequence_title': None, 'disable_accordion': COURSE_OUTLINE_PAGE_FLAG.is_enabled(self.course.id), 'show_search': show_search, 'relative_dates_is_enabled': RELATIVE_DATES_FLAG.is_enabled(self.course.id), 'reset_deadlines_url': reset_deadlines_url, 'display_reset_dates_banner': display_reset_dates_banner, } courseware_context.update( get_experiment_user_metadata_context( self.course, self.effective_user, )) table_of_contents = toc_for_course( self.effective_user, self.request, self.course, self.chapter_url_name, self.section_url_name, self.field_data_cache, ) courseware_context['accordion'] = render_accordion( self.request, self.course, table_of_contents['chapters'], ) courseware_context['course_sock_fragment'] = CourseSockFragmentView( ).render_to_fragment(request, course=self.course_overview) # entrance exam data self._add_entrance_exam_to_context(courseware_context) if self.section: # chromeless data if self.section.chrome: chrome = [ s.strip() for s in self.section.chrome.lower().split(",") ] if 'accordion' not in chrome: courseware_context['disable_accordion'] = True if 'tabs' not in chrome: courseware_context['disable_tabs'] = True # default tab if self.section.default_tab: courseware_context['default_tab'] = self.section.default_tab # section data courseware_context[ 'section_title'] = self.section.display_name_with_default section_context = self._create_section_context( table_of_contents['previous_of_active_section'], table_of_contents['next_of_active_section'], ) courseware_context['fragment'] = self.section.render( self.view, section_context) if self.section.position and self.section.has_children: self._add_sequence_title_to_context(courseware_context) # Courseware MFE link if show_courseware_mfe_link(request.user, staff_access, self.course.id): if self.section: try: unit_key = UsageKey.from_string( request.GET.get('activate_block_id', '')) # `activate_block_id` is typically a Unit (a.k.a. Vertical), # but it can technically be any block type. Do a check to # make sure it's really a Unit before we use it for the MFE. if unit_key.block_type != 'vertical': unit_key = None except InvalidKeyError: unit_key = None courseware_context[ 'microfrontend_link'] = get_microfrontend_url( self.course.id, self.section.location, unit_key) else: courseware_context[ 'microfrontend_link'] = get_microfrontend_url( self.course.id) else: courseware_context['microfrontend_link'] = None return courseware_context
def is_allowed(self): return RELATIVE_DATES_FLAG.is_enabled(self.course.id)
def enabled_for(cls, course): # pylint: disable=arguments-differ """ Enabled only for Self-Paced courses using Personalized User Schedules. """ return course and course.self_paced and RELATIVE_DATES_FLAG.is_enabled( course.id)