def get_page_type(self): if self.content_type in CourseOffering.communication_types(): return Page.PAGE_TYPE_COMMUNICATION elif self.content_type in CourseOffering.assessment_types(): return Page.PAGE_TYPE_ASSESSMENT else: return Page.PAGE_TYPE_CONTENT
def get(self, request, *args, **kwargs): # Setup list of days in course with initial values student_id = request.GET.get('student_id') resource_id = request.GET.get('resource_id') day_dict = {} our_tz = get_current_timezone() course_span = request.course_offering.end_date - request.course_offering.start_date for day_offset in range(course_span.days + 1): day = request.course_offering.start_date + timedelta(days=day_offset) day_dict[day] = { 'day': day, 'content_visits': 0, 'communication_visits': 0, 'assessment_visits': 0, 'single_events': [], 'submission_events': [], } # Get page visits optionally filtered by student and page page_visit_qs = PageVisit.objects.filter(page__course_offering=request.course_offering) if student_id: page_visit_qs = page_visit_qs.filter(lms_user_id=student_id) if resource_id: page_visit_qs = page_visit_qs.filter(page_id=resource_id) # Add the page visits to their corresponding entry for page_visit in page_visit_qs.values('visited_at', 'page__content_type'): visit_date = page_visit['visited_at'].astimezone(our_tz).date() if page_visit['page__content_type'] in CourseOffering.communication_types(): day_dict[visit_date]['communication_visits'] += 1 elif page_visit['page__content_type'] in CourseOffering.assessment_types(): day_dict[visit_date]['assessment_visits'] += 1 else: day_dict[visit_date]['content_visits'] += 1 # Add the single and submission events to their corresponding entry for single_event in CourseSingleEvent.objects.filter(course_offering=request.course_offering): day_dict[single_event.event_date.date()]['single_events'].append(single_event.title) for submission_event in CourseSubmissionEvent.objects.filter(course_offering=request.course_offering): day_dict[submission_event.start_date.date()]['submission_events'].append("{} (start)".format(submission_event.title)) day_dict[submission_event.end_date.date()]['submission_events'].append("{} (end)".format(submission_event.title)) # Convert to array and serialize data = [v for v in day_dict.values()] data.sort(key=lambda day_data: day_data['day']) serializer = DailyPageVisitsSerializer(data, many=True) return Response(serializer.data)
def get(self, request, format=None): course_offering = self.request.course_offering course_start_dt = course_offering.start_datetime events_by_week_for_all_pages = [0] * course_offering.no_weeks non_content_types = CourseOffering.communication_types( ) + CourseOffering.assessment_types() page_queryset = Page.objects.filter( course_offering=course_offering).exclude( content_type__in=non_content_types).values( 'id', 'title', 'parent_id', 'content_type') total_events = 0 for page in page_queryset: events_for_this_page = PageVisit.objects.filter(page_id=page['id']) events_by_week_for_this_page = [0] * course_offering.no_weeks total_events += events_for_this_page.count() for event in events_for_this_page: # Calculate the time since start of course for this event. From this we can calculate the week. # (Note, it's ok for courses not to start on Monday. In this case, the boundary of the course week won't # be the same as the inter-week boundary). td = event.visited_at - course_start_dt week = td.days // 7 # Integer division, the first week after the course starts is week 0. try: # try is here to catch invalid week indexes caused by events outside of the course offering. events_by_week_for_this_page[week] += 1 events_by_week_for_all_pages[week] += 1 except IndexError: pass page['weeks'] = events_by_week_for_this_page page['total'] = sum( events_by_week_for_this_page) # Add along the row. # Total things for all pages and weeks. Will appear in bottom right corner. events_by_week_for_all_pages.append(total_events) # Calculate percentages. for page in page_queryset: page['percent'] = Decimal(page['total'] * 100 / total_events) if total_events else 0 results = { 'page_set': list(page_queryset), 'totals_by_week': events_by_week_for_all_pages, } serializer = CoursePagesetAndTotalsSerializer(data=results) return Response(serializer.initial_data)
def get(self, request, event_id, format=None): course_offering = self.request.course_offering repeating_event = get_object_or_404(CourseRepeatingEvent, pk=event_id, course_offering=course_offering) our_tz = get_current_timezone() course_start_dt = course_offering.start_datetime non_content_types = CourseOffering.communication_types( ) + CourseOffering.assessment_types() page_queryset = Page.objects.filter( course_offering=course_offering).exclude( content_type__in=non_content_types).values( 'id', 'title', 'parent_id', 'content_type') for page in page_queryset: page_visits_for_this_page = PageVisit.objects.filter( page_id=page['id']) visit_pairs_by_week = [[0, 0] for i in range(course_offering.no_weeks)] for visit in page_visits_for_this_page: # Calculate the time since start of course for this page visit. From this we can calculate the week. # (Note, it's ok for courses not to start on Monday. In this case, the boundary of the course week won't # be the same as the isoweek boundary). td = visit.visited_at - course_start_dt week = td.days // 7 # Integer division, the first week after the course starts is week 0. visit_date_in_local_tz = visit.visited_at.astimezone( our_tz).date() day_of_week_of_visit = visit_date_in_local_tz.weekday() try: # Is the date of the visit within the range defined by the course offering? if day_of_week_of_visit < repeating_event.day_of_week: visit_pairs_by_week[week][0] += 1 else: visit_pairs_by_week[week][1] += 1 except IndexError: pass page['weeks'] = visit_pairs_by_week serializer = CourseContentPageEventSerializer(page_queryset, many=True) return Response(serializer.data)
def get(self, request, format=None): course_offering = self.request.course_offering users_set = LMSUser.objects.filter( course_offering=course_offering).order_by('lms_user_id') users_out = [] assessments_set = Page.objects.filter( course_offering=course_offering, content_type__in=CourseOffering.assessment_types()).order_by( 'pk').values('id', 'title') page_ids = tuple(a['id'] for a in assessments_set) for user in users_set: most_recent_attempts = { } # Dict of attempts for this student, keyed by assessment id # Find all the attempts for this student attempts = SubmissionAttempt.objects.filter(lms_user__id=user.id, page__in=page_ids) # Iterate over the attempts, recording the most recent attempt in the dict for attempt in attempts: page_id = attempt.page_id # If we don't have an attempt for this assessment, or this is a more recent attempt, store it. if page_id not in most_recent_attempts or attempt.attempted_at > most_recent_attempts[ page_id].attempted_at: most_recent_attempts[page_id] = attempt # Take the dict of most recent attempts, and extract the grades. No attempt gives a None grade. grades = { str(k): { 'pk': k, 'grade': v.grade } for k, v in most_recent_attempts.items() } users_out.append({ 'pk': user.id, 'name': user.full_name(), 'grades': grades }) results = { 'assessments': [{ 'pk': assessment['id'], 'title': assessment['title'] } for assessment in assessments_set], 'users': users_out, } serializer = AssessmentUsersAndGradesSerializer(data=results) sd = serializer.initial_data return Response(sd)
def get_queryset(self): course_offering = self.request.course_offering # Was a week number specified? (week number is 1-based) week_num = self.kwargs.get('week_num') if week_num is not None: # Filter for assessment accesses within the week range_start = course_offering.start_datetime + datetime.timedelta( weeks=int(week_num) - 1) range_end = range_start + datetime.timedelta(weeks=1) else: # Filter for assessment accesses within the extent of the courseoffering range_start = course_offering.start_datetime range_end = range_start + datetime.timedelta( weeks=course_offering.no_weeks) dt_range = (range_start, range_end) # Get the page list. If only we could do all this at the db level. page_qs = Page.objects.filter( course_offering=course_offering, content_type__in=CourseOffering.assessment_types()).values( 'id', 'title', 'content_type') # Augment all the pages with how many submission attempts related to that page for the window of interest. for page in page_qs: page['attempts'] = SubmissionAttempt.objects.filter( page=page['id'], attempted_at__range=dt_range).count() # Sort by number of submission attempts and trim down to top 10 rows # WARNING: This is messing with the internals of the query set page_qs._result_cache = sorted(page_qs._result_cache, key=lambda p: p['attempts'], reverse=True)[0:self.max_rows_to_return] # Now calculate the data for the userviews and average score columns. # FIXME: This isn't a great way to do it. Would be good to do it as part of the query above, in a way that didn't involve iteration or sets. for page in page_qs: # The userviews value is the number of distinct users to make a submission attempt in the time period. page['userviews'] = LMSUser.objects.values('id').filter( submissionattempt__page=page['id'], submissionattempt__attempted_at__range=dt_range).distinct( ).count() # The average score is the average of scores for submission attempts in the time period. # FIXME: Check for average-related exceptions here. avg = SubmissionAttempt.objects.filter( page=page['id'], attempted_at__range=dt_range).aggregate(Avg('grade')) page['average_score'] = avg['grade__avg'] return page_qs
def get(self, request, student_id, format=None): communications = Page.objects.filter( course_offering=self.request.course_offering, content_type__in=CourseOffering.communication_types()).values( 'id', 'title', 'content_type') for communication in communications: communication['user_views'] = PageVisit.objects.filter( lms_user_id=student_id, page_id=communication['id']).count() communication['posts'] = SummaryPost.objects.filter( lms_user_id=student_id, page_id=communication['id']).count() serializer = StudentCommunicationSerializer(data=communications, many=True) sd = serializer.initial_data return Response(sd)
def get_queryset(self): course_offering = self.request.course_offering # Was a week number specified? (week number is 1-based) week_num = self.kwargs.get('week_num') if week_num is not None: # Filter for pagevisits within the week range_start = course_offering.start_datetime + datetime.timedelta( weeks=int(week_num) - 1) range_end = range_start + datetime.timedelta(weeks=1) else: # Filter for pagevisits within the extent of the courseoffering range_start = course_offering.start_datetime range_end = range_start + datetime.timedelta( weeks=course_offering.no_weeks) dt_range = (range_start, range_end) # Get the page list. If only we could do all this at the db level. page_qs = Page.objects.filter( course_offering=course_offering, content_type__in=CourseOffering.communication_types()).values( 'id', 'title', 'content_type') # Augment all the pages with how many page views related to that page for the window of interest. for page in page_qs: page['pageviews'] = PageVisit.objects.filter( page=page['id'], visited_at__range=dt_range).count() # Sort by number of page views and trim down to top 10 rows # WARNING: This is messing with the internals of the query set page_qs._result_cache = sorted(page_qs._result_cache, key=lambda p: p['pageviews'], reverse=True)[0:self.max_rows_to_return] # Now calculate the data for the userviews and posts columns. # FIXME: This isn't a great way to do it. Would be good to do it as part of the query above, in a way that didn't involve iteration or sets. for page in page_qs: # The userviews value is the number of distinct users to access this page in the time period. page['userviews'] = LMSUser.objects.filter( pagevisit__page=page['id'], pagevisit__visited_at__range=dt_range).distinct().count() # The posts value is the number of posts to this page in the window of interest. page['posts'] = SummaryPost.objects.filter( page=page['id'], posted_at__range=dt_range).count() return page_qs
def _process_posts(self, posts_data): """ Extracts posts from the course import files and updates/inserts into the page visits table """ if set(posts_data.fieldnames) != set(self.POSTS_FIELDNAMES): raise LMSImportFileError(self.POSTS_FILE, 'Posts data columns do not match {}'.format(self.POSTS_FIELDNAMES)) for row in posts_data: try: user = LMSUser.objects.get(lms_user_id=row['user_key'], course_offering=self.course_offering) except LMSUser.DoesNotExist: self._add_error('Unable to find user {} for post'.format(row['user_key'])) continue values = { 'title': row['thread'], 'content_type': self.FORUM_CONTENT_TYPE, 'parent_id': None, } page, _ = Page.objects.get_or_create(content_id=row['forum_key'], is_forum=True, course_offering=self.course_offering, defaults=values) # In case page was found already, check it's content type to ensure it is a communication type if page.content_type not in CourseOffering.communication_types(): self._add_error('Resource {} for post is not a communication type'.format(page.content_id)) continue try: posted_at = dateparse.parse_datetime(row['timestamp']) if posted_at is None: self._add_error('Timestamp {} for post is not a valid format'.format(row['timestamp'])) continue except ValueError: self._add_error('Timestamp {} for post is not a valid datetime'.format(row['timestamp'])) continue if posted_at < self.course_offering.start_datetime or posted_at > self.course_offering.end_datetime: self._add_non_critical_error('Timestamp {} for post is outside course offering start/end'.format(row['timestamp'])) continue try: post = SummaryPost.objects.create(lms_user=user, page=page, posted_at=posted_at) except IntegrityError as e: self._add_error('Integrity Error in post insert: {}'.format(e))
def _process_submission_attempts(self, submissions_data): """ Extracts submission attempts from the course import files and updates/inserts into the submission attempts table """ if set(submissions_data.fieldnames) != set(self.SUBMISSIONS_FIELDNAMES): raise LMSImportFileError(self.SUBMISSIONS_FILE, 'Submissions data columns do not match {}'.format(self.SUBMISSIONS_FIELDNAMES)) for row in submissions_data: try: user = LMSUser.objects.get(lms_user_id=row['user_key'], course_offering=self.course_offering) except LMSUser.DoesNotExist: self._add_error('Unable to find user {} for submission attempt'.format(row['user_key'])) continue try: page = Page.objects.get(content_id=row['content_key'], is_forum=False, course_offering=self.course_offering) if page.content_type not in CourseOffering.assessment_types(): self._add_non_critical_error('Resource {} for submission attempt is not an assessment type'.format(page.content_id)) continue except Page.DoesNotExist: self._add_error('Unable to find page {} for submission attempt'.format(row['content_key'])) continue try: attempted_at = dateparse.parse_datetime(row['timestamp']) if attempted_at is None: self._add_error('Timestamp {} for submission attempt is not a valid format'.format(row['timestamp'])) continue except ValueError: self._add_error('Timestamp {} for submission attempt is not a valid datetime'.format(row['timestamp'])) continue if attempted_at < self.course_offering.start_datetime or attempted_at > self.course_offering.end_datetime: self._add_non_critical_error('Timestamp {} for submission attempt is outside course offering start/end'.format(row['timestamp'])) continue try: submission = SubmissionAttempt.objects.create(lms_user=user, page=page, attempted_at=attempted_at, grade=row['user_grade']) except IntegrityError as e: self._add_error('Integrity Error in submission attempt insert: {}'.format(e))
def get(self, request, student_id, format=None): assessments = Page.objects.filter( course_offering=self.request.course_offering, content_type__in=CourseOffering.assessment_types()).values( 'id', 'title', 'content_type') for assessment in assessments: assessment['user_views'] = PageVisit.objects.filter( lms_user_id=student_id, page_id=assessment['id']).count() attempts = SubmissionAttempt.objects.filter( lms_user_id=student_id, page_id=assessment['id']).values_list('grade', flat=True) assessment['attempts'] = len(attempts) assessment['average_grade'] = Decimal( sum(attempts) / assessment['attempts']) if assessment['attempts'] else 0 serializer = StudentAssessmentSerializer(data=assessments, many=True) sd = serializer.initial_data return Response(sd)
def get(self, request, format=None): course_offering = self.request.course_offering course_start_dt = course_offering.start_datetime students_by_week_for_all_pages = [ set() for i in range(course_offering.no_weeks) ] page_queryset = Page.objects.filter( course_offering=course_offering, content_type__in=CourseOffering.communication_types()).values( 'id', 'title', 'content_type') for page in page_queryset: page_visits_for_this_page = PageVisit.objects.filter( page_id=page['id']) students_by_week_for_this_page = [ set() for i in range(course_offering.no_weeks) ] for visit in page_visits_for_this_page: # Calculate the time since start of course for this page visit. From this we can calculate the week. # (Note, it's ok for courses not to start on Monday. In this case, the boundary of the course week won't # be the same as the isoweek boundary). td = visit.visited_at - course_start_dt week = td.days // 7 # Integer division, the first week after the course starts is week 0. try: # try is here to catch invalid week indexes caused by events outside of the course offering. students_by_week_for_this_page[week].add(visit.lms_user.pk) students_by_week_for_all_pages[week].add(visit.lms_user.pk) except IndexError: pass # At this point we have a list of sets of students for the page. Take the union of all the sets to find # the set of students who visited this page at any time over the duration of the course offering. set_of_students_for_this_page = set.union( *students_by_week_for_this_page) # For each week bin, convert the set of students for that weekinto a count. students_by_week_for_this_page = list( len(students) for students in students_by_week_for_this_page) page['weeks'] = students_by_week_for_this_page page['total'] = len(set_of_students_for_this_page) grand_total_uniques = len(set.union(*students_by_week_for_all_pages)) # For each week bin, convert the set of students for that weekinto a count. students_by_week_for_all_pages = list( len(students) for students in students_by_week_for_all_pages) # Total page students for all pages and weeks. Will appear in bottom right corner. students_by_week_for_all_pages.append(grand_total_uniques) # Calculate percentages. for page in page_queryset: page['percent'] = (Decimal(page['total'] * 100 / grand_total_uniques) if grand_total_uniques else 0) results = { 'page_set': page_queryset, 'totals_by_week': students_by_week_for_all_pages, } serializer = CoursePagesetAndTotalsSerializer(data=results) sd = serializer.initial_data return Response(sd)
def get(self, request, *args, **kwargs): week_num = kwargs.get('week_num') week_start = request.course_offering.start_datetime + datetime.timedelta( weeks=int(week_num) - 1) dt_range = (week_start, week_start + datetime.timedelta(weeks=1)) our_tz = get_current_timezone() day_dict = {} for day_offset in range(7): day = (week_start + datetime.timedelta(days=day_offset)).date() day_dict[day] = { 'day': day, # Temporary values to calculate final data - will be stripped out by the serializer 'page_list': [], # Final calculated values returned by the endpoint 'unique_visits': 0, 'content_visits': 0, 'communication_visits': 0, 'assessment_visits': 0, 'repeating_events': [], } # Add the page visits to their corresponding entry for page_visit in PageVisit.objects.filter( page__course_offering=request.course_offering, visited_at__range=dt_range).values('page_id', 'visited_at', 'page__content_type'): visit_date = page_visit['visited_at'].astimezone(our_tz).date() day_dict[visit_date]['page_list'].append(page_visit['page_id']) if page_visit[ 'page__content_type'] in CourseOffering.communication_types( ): day_dict[visit_date]['communication_visits'] += 1 elif page_visit[ 'page__content_type'] in CourseOffering.assessment_types(): day_dict[visit_date]['assessment_visits'] += 1 else: day_dict[visit_date]['content_visits'] += 1 # Add the repeating events to their corresponding entry for repeating_event in CourseRepeatingEvent.objects.filter( course_offering=request.course_offering, start_week__lte=week_num, end_week__gte=week_num): repeat_event_date = week_start + datetime.timedelta( days=repeating_event.day_of_week) day_dict[repeat_event_date.date()]['repeating_events'].append( repeating_event.title) # Turn the list of pages visited for each day into a count of unique visits for day in day_dict: day_dict[day]['unique_visits'] = len( set(day_dict[day]['page_list'])) # Convert to array and serialize data = [v for v in day_dict.values()] data.sort(key=lambda day_data: day_data['day']) serializer = WeeklyPageVisitsSerializer(data, many=True) return Response(serializer.data)