def has_course_goal_permission(request, course_id, user_access): """ Returns whether the user can access the course goal functionality. Only authenticated users that are enrolled in a verifiable course can use this feature. """ course_key = CourseKey.from_string(course_id) has_verified_mode = CourseMode.has_verified_mode(CourseMode.modes_for_course_dict(course_key)) return user_access['is_enrolled'] and has_verified_mode and ENABLE_COURSE_GOALS.is_enabled(course_key) \ and settings.FEATURES.get('ENABLE_COURSE_GOALS')
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 a user activity record was not stored or retrieved ''' if not ENABLE_COURSE_GOALS.is_enabled(course_key): return None 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 timezone = get_user_timezone_or_last_seen_timezone_or_utc(user) 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: 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
def handle_goal(goal, today, sunday_date, monday_date): """Sends an email reminder for a single CourseGoal, if it passes all our checks""" if not ENABLE_COURSE_GOALS.is_enabled(goal.course_key): return False enrollment = CourseEnrollment.get_enrollment(goal.user, goal.course_key, select_related=['course']) # If you're not actively enrolled in the course or your enrollment was this week if not enrollment or not enrollment.is_active or enrollment.created.date( ) >= monday_date: return False audit_access_expiration_date = get_user_course_expiration_date( goal.user, enrollment.course_overview) # If an audit user's access expires this week, exclude them from the email since they may not # be able to hit their goal anyway if audit_access_expiration_date and audit_access_expiration_date.date( ) <= sunday_date: return False cert = get_certificate_for_user_id(goal.user, goal.course_key) # If a user has a downloadable certificate, we will consider them as having completed # the course and opt them out of receiving emails if cert and cert.status == CertificateStatuses.downloadable: return False # Check the number of days left to successfully hit their goal week_activity_count = UserActivity.objects.filter( user=goal.user, course_key=goal.course_key, date__gte=monday_date, ).count() required_days_left = goal.days_per_week - week_activity_count # The weekdays are 0 indexed, but we want this to be 1 to match required_days_left. # Essentially, if today is Sunday, days_left_in_week should be 1 since they have Sunday to hit their goal. days_left_in_week = SUNDAY_WEEKDAY - today.weekday() + 1 # We want to email users in the morning of their timezone user_timezone = get_user_timezone_or_last_seen_timezone_or_utc( goal.user) now_in_users_timezone = datetime.now(user_timezone) if not 9 <= now_in_users_timezone.hour < 12: return False if required_days_left == days_left_in_week: send_ace_message(goal) CourseGoalReminderStatus.objects.update_or_create( goal=goal, defaults={'email_reminder_sent': True}) return True return False
def course_goals(self): """ Returns a dict of course goals """ if COURSE_GOALS_NUMBER_OF_DAYS_GOALS.is_enabled(): course_goals = { 'goal_options': [], 'selected_goal': None } user_is_enrolled = CourseEnrollment.is_enrolled(self.effective_user, self.course_key) if (user_is_enrolled and ENABLE_COURSE_GOALS.is_enabled(self.course_key)): selected_goal = get_course_goal(self.effective_user, self.course_key) if selected_goal: course_goals['selected_goal'] = { 'days_per_week': selected_goal.days_per_week, 'subscribed_to_reminders': selected_goal.subscribed_to_reminders, } return course_goals
def course_goals(self): """ Returns a dict of course goals """ course_goals = { 'selected_goal': None, 'weekly_learning_goal_enabled': False, } user_is_enrolled = CourseEnrollment.is_enrolled(self.effective_user, self.course_key) if (user_is_enrolled and ENABLE_COURSE_GOALS.is_enabled(self.course_key)): course_goals['weekly_learning_goal_enabled'] = True selected_goal = get_course_goal(self.effective_user, self.course_key) if selected_goal: course_goals['selected_goal'] = { 'days_per_week': selected_goal.days_per_week, 'subscribed_to_reminders': selected_goal.subscribed_to_reminders, } return course_goals
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, ) if not ENABLE_COURSE_GOALS.is_enabled(course_key): log.warning('For this mobile request, user activity is not enabled for this user {} and course {}'.format( str(user_id), str(course_key)) ) return Response(status=(200)) # Populate user activity for tracking progress towards a user's course goals UserActivity.record_user_activity(user, course_key) return Response(status=(200))
def get(self, request, *args, **kwargs): # pylint: disable=too-many-statements course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) masquerade_object, request.user = setup_masquerade( request, course_key, staff_access=has_access(request.user, 'staff', course_key), reset_masquerade_data=True, ) user_is_masquerading = is_masquerading( request.user, course_key, course_masquerade=masquerade_object) course_overview = get_course_overview_or_404(course_key) enrollment = CourseEnrollment.get_enrollment(request.user, course_key) enrollment_mode = getattr(enrollment, 'mode', None) allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled( course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] dates_tab_link = get_learning_mfe_home_url(course_key=course.id, url_fragment='dates') # Set all of the defaults access_expiration = None cert_data = None course_blocks = None course_goals = { 'selected_goal': None, 'weekly_learning_goal_enabled': False, } course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) dates_widget = { 'course_date_blocks': [], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } enroll_alert = { 'can_enroll': True, 'extra_text': None, } handouts_html = None offer_data = None resume_course = { 'has_visited_course': False, 'url': None, } welcome_message_html = None is_enrolled = enrollment and enrollment.is_active is_staff = bool(has_access(request.user, 'staff', course_key)) show_enrolled = is_enrolled or is_staff enable_proctored_exams = False if show_enrolled: course_blocks = get_course_outline_block_tree( request, course_key_string, request.user) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) dates_widget['course_date_blocks'] = [ block for block in date_blocks if not isinstance(block, TodaysDate) ] handouts_html = get_course_info_section(request, request.user, course, 'handouts') welcome_message_html = get_current_update_for_user(request, course) offer_data = generate_offer_data(request.user, course_overview) access_expiration = get_access_expiration_data( request.user, course_overview) cert_data = get_cert_data(request.user, course, enrollment.mode) if is_enrolled else None enable_proctored_exams = course_overview.enable_proctored_exams if (is_enrolled and ENABLE_COURSE_GOALS.is_enabled(course_key)): course_goals['weekly_learning_goal_enabled'] = True selected_goal = get_course_goal(request.user, course_key) if selected_goal: course_goals['selected_goal'] = { 'days_per_week': selected_goal.days_per_week, 'subscribed_to_reminders': selected_goal.subscribed_to_reminders, } try: resume_block = get_key_to_last_completed_block( request.user, course.id) resume_course['has_visited_course'] = True resume_path = reverse('jump_to', kwargs={ 'course_id': course_key_string, 'location': str(resume_block) }) resume_course['url'] = request.build_absolute_uri(resume_path) except UnavailableCompletionData: start_block = get_start_block(course_blocks) resume_course['url'] = start_block['lms_web_url'] elif allow_public_outline or allow_public or user_is_masquerading: course_blocks = get_course_outline_block_tree( request, course_key_string, None) if allow_public or user_is_masquerading: handouts_html = get_course_info_section( request, request.user, course, 'handouts') if not is_enrolled: if CourseMode.is_masters_only(course_key): enroll_alert['can_enroll'] = False enroll_alert['extra_text'] = _( 'Please contact your degree administrator or ' '{platform_name} Support if you have questions.').format( platform_name=settings.PLATFORM_NAME) elif course_is_invitation_only(course): enroll_alert['can_enroll'] = False # Sometimes there are sequences returned by Course Blocks that we # don't actually want to show to the user, such as when a sequence is # composed entirely of units that the user can't access. The Learning # Sequences API knows how to roll this up, so we use it determine which # sequences we should remove from course_blocks. # # The long term goal is to remove the Course Blocks API call entirely, # so this is a tiny first step in that migration. if course_blocks: user_course_outline = get_user_course_outline( course_key, request.user, datetime.now(tz=timezone.utc)) available_seq_ids = { str(usage_key) for usage_key in user_course_outline.sequences } # course_blocks is a reference to the root of the course, so we go # through the chapters (sections) to look for sequences to remove. for chapter_data in course_blocks['children']: chapter_data['children'] = [ seq_data for seq_data in chapter_data['children'] if (seq_data['id'] in available_seq_ids or # Edge case: Sometimes we have weird course structures. # We expect only sequentials here, but if there is # another type, just skip it (don't filter it out). seq_data['type'] != 'sequential') ] if 'children' in chapter_data else [] user_has_passing_grade = False if not request.user.is_anonymous: user_grade = CourseGradeFactory().read(request.user, course) if user_grade: user_has_passing_grade = user_grade.passed data = { 'access_expiration': access_expiration, 'cert_data': cert_data, 'course_blocks': course_blocks, 'course_goals': course_goals, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enable_proctored_exams': enable_proctored_exams, 'enroll_alert': enroll_alert, 'enrollment_mode': enrollment_mode, 'handouts_html': handouts_html, 'has_ended': course.has_ended(), 'offer': offer_data, 'resume_course': resume_course, 'user_has_passing_grade': user_has_passing_grade, 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_overview'] = course_overview context['enable_links'] = show_enrolled or allow_public context['enrollment'] = enrollment serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)