From 92ce8259908443b8b84b53725ff839851e2091db Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 14 Nov 2012 18:55:20 -0500 Subject: [PATCH 01/22] Analytics experiments --- lms/envs/logsettings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lms/envs/logsettings.py b/lms/envs/logsettings.py index 2b001b0517..c877308997 100644 --- a/lms/envs/logsettings.py +++ b/lms/envs/logsettings.py @@ -79,7 +79,7 @@ def get_logger_config(log_dir, 'level': 'INFO' }, 'tracking': { - 'handlers': ['tracking'], + 'handlers': ['tracking', 'http'], 'level': 'DEBUG', 'propagate': False, }, @@ -121,6 +121,12 @@ def get_logger_config(log_dir, 'maxBytes': 1024 * 1024 * 2, 'backupCount': 5, }, + 'http' : { + 'level': 'DEBUG', + 'class': 'logging.handlers.HTTPHandler', + 'host':'127.0.0.1:14141', + 'url':'/logger', + } }) else: logger_config['handlers'].update({ From 660481bee5a60f0c062aca3b0d5fdfc2f57dc14e Mon Sep 17 00:00:00 2001 From: Piotr Mitros Date: Wed, 14 Nov 2012 20:33:29 -0500 Subject: [PATCH 02/22] Better URL --- lms/envs/logsettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/envs/logsettings.py b/lms/envs/logsettings.py index c877308997..0c0e0a577e 100644 --- a/lms/envs/logsettings.py +++ b/lms/envs/logsettings.py @@ -125,7 +125,7 @@ def get_logger_config(log_dir, 'level': 'DEBUG', 'class': 'logging.handlers.HTTPHandler', 'host':'127.0.0.1:14141', - 'url':'/logger', + 'url':'/an_evt', } }) else: From d1fe2c3361d2d516a80eecc67df2bbfd6998860a Mon Sep 17 00:00:00 2001 From: JM Van Thong Date: Mon, 26 Nov 2012 18:18:18 -0500 Subject: [PATCH 03/22] Added analytics tab in instructor dashbaord, and call to the analytics back-end to display sample data. --- lms/djangoapps/instructor/views.py | 10 ++++++++++ lms/envs/common.py | 3 +++ lms/envs/dev.py | 7 +++++++ .../courseware/instructor_dashboard.html | 18 +++++++++++++++++- 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f985cc43a0..31faf8acb6 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -12,6 +12,7 @@ from django.http import HttpResponse from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control from mitxmako.shortcuts import render_to_response +import requests from courseware import grades from courseware.access import has_access, get_access_group_name @@ -266,8 +267,16 @@ def instructor_dashboard(request, course_id): if idash_mode=='Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_id) + #---------------------------------------- + # analytics + analytics_json = None + if idash_mode == 'Analytics': + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_daily_activity?sid=2") + #analytics_html = req.text + analytics_json = req.json + #---------------------------------------- # context for rendering context = {'course': course, @@ -282,6 +291,7 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), + 'analytics_json' : analytics_json, } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/envs/common.py b/lms/envs/common.py index dd9013bcb3..5e22e70307 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -81,6 +81,9 @@ MITX_FEATURES = { 'AUTH_USE_OPENID': False, 'AUTH_USE_MIT_CERTIFICATES' : False, 'AUTH_USE_OPENID_PROVIDER': False, + + # analytics experiments + 'ENABLE_INSTRUCTOR_ANALYTICS' : False, } # Used for A/B testing diff --git a/lms/envs/dev.py b/lms/envs/dev.py index bf72284425..0db028866a 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -21,6 +21,8 @@ MITX_FEATURES['SUBDOMAIN_BRANDING'] = True MITX_FEATURES['FORCE_UNIVERSITY_DOMAIN'] = None # show all university courses if in dev (ie don't use HTTP_HOST) MITX_FEATURES['ENABLE_MANUAL_GIT_RELOAD'] = True MITX_FEATURES['ENABLE_PSYCHOMETRICS'] = False # real-time psychometrics (eg item response theory analysis in instructor dashboard) +MITX_FEATURES['ENABLE_INSTRUCTOR_ANALYTICS'] = True + WIKI_ENABLED = True @@ -177,3 +179,8 @@ PIPELINE_SASS_ARGUMENTS = '-r {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.for MITX_FEATURES['ENABLE_PEARSON_HACK_TEST'] = True PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_PASSWORD = "12345" + +########################## ANALYTICS TESTING ######################## + +ANALYTICS_SERVER_URL = "http://127.0.0.1:14141/" + diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 74bc25fcbe..de619a6144 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -57,7 +57,11 @@ function goto( mode) Psychometrics | %endif Admin | - Forum Admin ] + Forum Admin + %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_ANALYTICS'): + | Analytics + %endif + ]
${djangopid}
@@ -165,6 +169,18 @@ function goto( mode) +##----------------------------------------------------------------------------- +%if modeflag.get('Analytics'): + + % for r in analytics_json: + + + + % endfor +
${r['day']}${r['student_count']}
+ +%endif + ##----------------------------------------------------------------------------- %if modeflag.get('Psychometrics') is None: From 5ecacb0103b64729a49ce3d1e032ec098c1c831d Mon Sep 17 00:00:00 2001 From: JM Van Thong Date: Thu, 29 Nov 2012 12:06:57 -0500 Subject: [PATCH 04/22] Implemented 3 analytics to the instructor dashboard: StudentsEnrolled, StudentsPerHomework, DailyActivityAnalyzer. --- lms/djangoapps/instructor/views.py | 15 ++++++++-- .../courseware/instructor_dashboard.html | 29 ++++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 31faf8acb6..666a5d2025 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -271,12 +271,19 @@ def instructor_dashboard(request, course_id): # analytics analytics_json = None + students_enrolled_json = None + daily_activity_json = None if idash_mode == 'Analytics': - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_daily_activity?sid=2") - #analytics_html = req.text + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id) analytics_json = req.json - + + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id) + students_enrolled_json = req.json + + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&from=2012-11-19&to=2012-11-27") + daily_activity_json = req.json + #---------------------------------------- # context for rendering context = {'course': course, @@ -292,6 +299,8 @@ def instructor_dashboard(request, course_id): 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), 'analytics_json' : analytics_json, + 'students_enrolled_json' : students_enrolled_json, + 'daily_activity_json' : daily_activity_json, } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index de619a6144..0d12fbc444 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -171,13 +171,34 @@ function goto( mode) ##----------------------------------------------------------------------------- %if modeflag.get('Analytics'): - - % for r in analytics_json: + +

Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']} +

+ +

+ Students who attempted at least one exercise: +

+ + % for k,v in analytics_json['data'].items(): - + % endfor -
ModuleNumber of students
${r['day']}${r['student_count']}${k} ${v}
+ +

+ +

+ Daily activity: + + + % for k,v in daily_activity_json['data'].items(): + + + + % endfor +
DayNumber of students
${k} ${v}
+

+ %endif From 1732fc4804c6f5847c2a19451319ca71118346cf Mon Sep 17 00:00:00 2001 From: JM Van Thong Date: Mon, 3 Dec 2012 18:44:19 -0500 Subject: [PATCH 05/22] Improve analytics table style and added scroll. --- .../courseware/instructor_dashboard.html | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 0d12fbc444..6d696a3558 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -33,6 +33,10 @@ table.stat_table td { border-color: #666666; background-color: #ffffff; } +.divScroll { + height: 200px; + overflow: scroll; +} a.selectedmode { background-color: yellow; } @@ -176,21 +180,25 @@ function goto( mode)

- Students who attempted at least one exercise: - - +

Students who attempted at least one exercise:

+ +
+
ModuleNumber of students
+ % for k,v in analytics_json['data'].items(): % endfor
ModuleNumber of students
${k} ${v}
+ +

- Daily activity: - - +

Daily activity:

+
DayNumber of students
+ % for k,v in daily_activity_json['data'].items(): From 39d48b1966fca0d3b5629907f5060875be573478 Mon Sep 17 00:00:00 2001 From: JM Van Thong Date: Tue, 4 Dec 2012 12:00:27 -0500 Subject: [PATCH 06/22] Change daily activity to be course specific. --- lms/djangoapps/instructor/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 666a5d2025..ffada9a482 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -281,7 +281,7 @@ def instructor_dashboard(request, course_id): req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id) students_enrolled_json = req.json - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&from=2012-11-19&to=2012-11-27") + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&from=2012-11-19&to=2012-12-04&course_id=%s" % course_id) daily_activity_json = req.json #---------------------------------------- From b10b6e2b3f1ce98d74eb24f51519e2f121334526 Mon Sep 17 00:00:00 2001 From: JM Van Thong Date: Wed, 5 Dec 2012 23:21:00 -0500 Subject: [PATCH 07/22] Added more analytics to instructor dashboard. --- lms/djangoapps/instructor/views.py | 24 ++++++++++++++++- .../courseware/instructor_dashboard.html | 26 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index ffada9a482..f638d55f61 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -5,6 +5,8 @@ import csv import logging import os import urllib +import datetime +from datetime import datetime, timedelta from django.conf import settings from django.contrib.auth.models import User, Group @@ -273,15 +275,33 @@ def instructor_dashboard(request, course_id): analytics_json = None students_enrolled_json = None daily_activity_json = None + students_daily_activity_json = None + students_per_problem_correct_json = None if idash_mode == 'Analytics': req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id) analytics_json = req.json + # get the day + to_day = datetime.today().date() + from_day = to_day - timedelta(days=7) + + # number of students active in the past 7 days (including current day) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day)) + students_daily_activity_json = req.json + + # number of students per problem who have problem graded correct + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) + students_per_problem_correct_json = req.json + + # number of students enrolled in this course req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id) students_enrolled_json = req.json - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&from=2012-11-19&to=2012-12-04&course_id=%s" % course_id) + # number of students active in the past 7 days (including current day) --- online version! + to_day = datetime.today().date() + from_day = to_day - timedelta(days=7) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&course_id=%s&from=%s&to=%s" % (course_id,from_day, to_day)) daily_activity_json = req.json #---------------------------------------- @@ -301,6 +321,8 @@ def instructor_dashboard(request, course_id): 'analytics_json' : analytics_json, 'students_enrolled_json' : students_enrolled_json, 'daily_activity_json' : daily_activity_json, + 'students_daily_activity_json' : students_daily_activity_json, + 'students_per_problem_correct_json' : students_per_problem_correct_json, } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 6d696a3558..f15647ccd2 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -179,6 +179,30 @@ function goto( mode)

Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']}

+

+

Daily activity for the past 7 days:

+
DayNumber of students
${k} ${v}
+ + % for k,v in students_daily_activity_json['data'].items(): + + + + % endfor +
DayNumber of students
${k} ${v}
+

+ +

+

Number of students with correct problems for the past 7 days:

+ + + % for k,v in students_per_problem_correct_json['data'].items(): + + + + % endfor +
ProblemNumber of students
${k} ${v}
+

+

Students who attempted at least one exercise:

@@ -196,7 +220,7 @@ function goto( mode)

-

Daily activity:

+

Daily activity (online version):

% for k,v in daily_activity_json['data'].items(): From 27b8f01ffac2e8aa47e499e8ac0a5846e5b68227 Mon Sep 17 00:00:00 2001 From: JM Van Thong Date: Fri, 7 Dec 2012 17:26:33 -0500 Subject: [PATCH 08/22] Added the following analyzers: StudentsActive, OverallGradeDistribution, StudentsDropoffPerDay. --- lms/djangoapps/instructor/views.py | 43 +++++++++---- .../courseware/instructor_dashboard.html | 63 ++++++++++++++----- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index f638d55f61..5c743561a9 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -274,36 +274,52 @@ def instructor_dashboard(request, course_id): analytics_json = None students_enrolled_json = None + students_active_json = None daily_activity_json = None students_daily_activity_json = None students_per_problem_correct_json = None + overall_grade_distribution = None + dropoff_per_day = None if idash_mode == 'Analytics': - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id) - analytics_json = req.json - # get the day + # get current day to_day = datetime.today().date() from_day = to_day - timedelta(days=7) - # number of students active in the past 7 days (including current day) - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day)) - students_daily_activity_json = req.json - - # number of students per problem who have problem graded correct - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) - students_per_problem_correct_json = req.json - # number of students enrolled in this course req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id) students_enrolled_json = req.json - # number of students active in the past 7 days (including current day) --- online version! + # number of students active in the past 7 days (including current day), i.e. with at least one activity for the period + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day)) + students_active_json = req.json + + # number of students per problem who have problem graded correct + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) + students_per_problem_correct_json = req.json + + # grade distribution for the course + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=OverallGradeDistribution&course_id=%s" % (course_id,)) + overall_grade_distribution = req.json + + # number of students distribution drop off per day + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day)) + dropoff_per_day = req.json + + # the following is ++incorrect++ use of studentmodule table + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day)) + students_daily_activity_json = req.json + + # number of students active in the past 7 days (including current day) --- online version! experimental to_day = datetime.today().date() from_day = to_day - timedelta(days=7) req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&course_id=%s&from=%s&to=%s" % (course_id,from_day, to_day)) daily_activity_json = req.json + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id) + analytics_json = req.json + #---------------------------------------- # context for rendering context = {'course': course, @@ -320,9 +336,12 @@ def instructor_dashboard(request, course_id): 'djangopid' : os.getpid(), 'analytics_json' : analytics_json, 'students_enrolled_json' : students_enrolled_json, + 'students_active_json' : students_active_json, 'daily_activity_json' : daily_activity_json, 'students_daily_activity_json' : students_daily_activity_json, 'students_per_problem_correct_json' : students_per_problem_correct_json, + 'overall_grade_distribution' : overall_grade_distribution, + 'dropoff_per_day' : dropoff_per_day, } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index f15647ccd2..b06eb43944 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -176,23 +176,16 @@ function goto( mode) ##----------------------------------------------------------------------------- %if modeflag.get('Analytics'): -

Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']} +

+ Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']} +

+

+ Number of active students for the past 7 days: ${students_active_json['data']['nb_students_active']}

-

Daily activity for the past 7 days:

-
DayNumber of students
- - % for k,v in students_daily_activity_json['data'].items(): - - - - % endfor -
DayNumber of students
${k} ${v}
-

+

Number of active students per problems who have this problem graded as correct:

-

-

Number of students with correct problems for the past 7 days:

% for k,v in students_per_problem_correct_json['data'].items(): @@ -204,7 +197,38 @@ function goto( mode)

-

Students who attempted at least one exercise:

+

Grade distribution:

+ +
+
ProblemNumber of students
+ + % for k,v in overall_grade_distribution['data'].items(): + + + + % endfor +
GradeNumber of students
${k} ${v}
+ + +

+ +

+

Number of students who dropped off per day before becoming inactive:

+ +
+ + + % for k,v in dropoff_per_day['data'].items(): + + + + % endfor +
DayNumber of students
${k} ${v}
+
+

+ +

+

Students who attempted at least one exercise:

@@ -231,6 +255,17 @@ function goto( mode)

+

+

Daily activity for the past 7 days:

+ + + % for k,v in students_daily_activity_json['data'].items(): + + + + % endfor +
DayNumber of students
${k} ${v}
+

%endif From e40b0d0d081e0a1a7e40c29349e5f224aef6ffdd Mon Sep 17 00:00:00 2001 From: JM Van Thong Date: Fri, 14 Dec 2012 17:16:57 -0500 Subject: [PATCH 09/22] Fixed data order when loading json record from request. --- lms/djangoapps/instructor/views.py | 32 +++++++++---------- .../courseware/instructor_dashboard.html | 18 ++--------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 5c743561a9..5da0209ace 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -7,6 +7,8 @@ import os import urllib import datetime from datetime import datetime, timedelta +import json +from collections import OrderedDict from django.conf import settings from django.contrib.auth.models import User, Group @@ -272,11 +274,10 @@ def instructor_dashboard(request, course_id): #---------------------------------------- # analytics - analytics_json = None + attempted_problems = None students_enrolled_json = None students_active_json = None daily_activity_json = None - students_daily_activity_json = None students_per_problem_correct_json = None overall_grade_distribution = None dropoff_per_day = None @@ -287,29 +288,32 @@ def instructor_dashboard(request, course_id): to_day = datetime.today().date() from_day = to_day - timedelta(days=7) + # WARNING: do not use req.json because the preloaded json doesn't preserve the order of the original record + # number of students enrolled in this course req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id) - students_enrolled_json = req.json + students_enrolled_json = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students active in the past 7 days (including current day), i.e. with at least one activity for the period req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day)) - students_active_json = req.json + students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students per problem who have problem graded correct req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) - students_per_problem_correct_json = req.json + students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) - # grade distribution for the course + # grade distribution for the course +++ this is not the desired distribution +++ req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=OverallGradeDistribution&course_id=%s" % (course_id,)) - overall_grade_distribution = req.json + overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students distribution drop off per day req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day)) - dropoff_per_day = req.json + dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict) + + # number of students per problem who attempted this problem at least once + req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsAttemptedProblems&course_id=%s" % course_id) + attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict) - # the following is ++incorrect++ use of studentmodule table - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDailyActivity&course_id=%s&from=%s" % (course_id,from_day)) - students_daily_activity_json = req.json # number of students active in the past 7 days (including current day) --- online version! experimental to_day = datetime.today().date() @@ -317,9 +321,6 @@ def instructor_dashboard(request, course_id): req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&course_id=%s&from=%s&to=%s" % (course_id,from_day, to_day)) daily_activity_json = req.json - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerHomework&course_id=%s" % course_id) - analytics_json = req.json - #---------------------------------------- # context for rendering context = {'course': course, @@ -334,14 +335,13 @@ def instructor_dashboard(request, course_id): 'plots': plots, # psychometrics 'course_errors': modulestore().get_item_errors(course.location), 'djangopid' : os.getpid(), - 'analytics_json' : analytics_json, 'students_enrolled_json' : students_enrolled_json, 'students_active_json' : students_active_json, 'daily_activity_json' : daily_activity_json, - 'students_daily_activity_json' : students_daily_activity_json, 'students_per_problem_correct_json' : students_per_problem_correct_json, 'overall_grade_distribution' : overall_grade_distribution, 'dropoff_per_day' : dropoff_per_day, + 'attempted_problems' : attempted_problems, } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index b06eb43944..e347ab2c60 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -177,10 +177,10 @@ function goto( mode) %if modeflag.get('Analytics'):

- Number of students enrolled: ${students_enrolled_json['data']['nb_students_enrolled']} + Number of students enrolled: ${students_enrolled_json['data']['value']}

- Number of active students for the past 7 days: ${students_active_json['data']['nb_students_active']} + Number of active students for the past 7 days: ${students_active_json['data']['value']}

@@ -233,7 +233,7 @@ function goto( mode)

- % for k,v in analytics_json['data'].items(): + % for k,v in attempted_problems['data'].items(): @@ -255,18 +255,6 @@ function goto( mode)
ModuleNumber of students
${k} ${v}

-

-

Daily activity for the past 7 days:

- - - % for k,v in students_daily_activity_json['data'].items(): - - - - % endfor -
DayNumber of students
${k} ${v}
-

- %endif ##----------------------------------------------------------------------------- From d230c1b03ce73896f7bb3e08d67ccba35a158d94 Mon Sep 17 00:00:00 2001 From: jmvt Date: Fri, 11 Jan 2013 17:14:29 -0500 Subject: [PATCH 10/22] Added example of geographic student distribution over world map. --- .../jquery-jvectormap-1.1.1.css | 37 ++++++++++ .../jquery-jvectormap-1.1.1.min.js | 7 ++ .../jquery-jvectormap-world-mill-en.js | 1 + lms/djangoapps/instructor/views.py | 3 +- .../courseware/instructor_dashboard.html | 71 +++++++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css create mode 100644 common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js create mode 100644 common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js diff --git a/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css new file mode 100644 index 0000000000..3d9c8844bb --- /dev/null +++ b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.css @@ -0,0 +1,37 @@ +.jvectormap-label { + position: absolute; + display: none; + border: solid 1px #CDCDCD; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background: #292929; + color: white; + font-family: sans-serif, Verdana; + font-size: smaller; + padding: 3px; +} + +.jvectormap-zoomin, .jvectormap-zoomout { + position: absolute; + left: 10px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background: #292929; + padding: 3px; + color: white; + width: 10px; + height: 10px; + cursor: pointer; + line-height: 10px; + text-align: center; +} + +.jvectormap-zoomin { + top: 10px; +} + +.jvectormap-zoomout { + top: 30px; +} \ No newline at end of file diff --git a/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js new file mode 100644 index 0000000000..17450a0983 --- /dev/null +++ b/common/static/js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js @@ -0,0 +1,7 @@ +/** + * jVectorMap version 1.1 + * + * Copyright 2011-2012, Kirill Lebedev + * Licensed under the MIT license. + * + */(function(e){var t={set:{colors:1,values:1,backgroundColor:1,scaleColors:1,normalizeFunction:1,focus:1},get:{selectedRegions:1,selectedMarkers:1,mapObject:1,regionName:1}};e.fn.vectorMap=function(e){var n,r,i,n=this.children(".jvectormap-container").data("mapObject");if(e==="addMap")jvm.WorldMap.maps[arguments[1]]=arguments[2];else{if(!(e!=="set"&&e!=="get"||!t[e][arguments[1]]))return r=arguments[1].charAt(0).toUpperCase()+arguments[1].substr(1),n[e+r].apply(n,Array.prototype.slice.call(arguments,2));e=e||{},e.container=this,n=new jvm.WorldMap(e)}return this}})(jQuery),function(e){function r(t){var n=t||window.event,r=[].slice.call(arguments,1),i=0,s=!0,o=0,u=0;return t=e.event.fix(n),t.type="mousewheel",n.wheelDelta&&(i=n.wheelDelta/120),n.detail&&(i=-n.detail/3),u=i,n.axis!==undefined&&n.axis===n.HORIZONTAL_AXIS&&(u=0,o=-1*i),n.wheelDeltaY!==undefined&&(u=n.wheelDeltaY/120),n.wheelDeltaX!==undefined&&(o=-1*n.wheelDeltaX/120),r.unshift(t,i,o,u),(e.event.dispatch||e.event.handle).apply(this,r)}var t=["DOMMouseScroll","mousewheel"];if(e.event.fixHooks)for(var n=t.length;n;)e.event.fixHooks[t[--n]]=e.event.mouseHooks;e.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],r,!1);else this.onmousewheel=r},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],r,!1);else this.onmousewheel=null}},e.fn.extend({mousewheel:function(e){return e?this.bind("mousewheel",e):this.trigger("mousewheel")},unmousewheel:function(e){return this.unbind("mousewheel",e)}})}(jQuery);var jvm={inherits:function(e,t){function n(){}n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e,e.parentClass=t},mixin:function(e,t){var n;for(n in t.prototype)t.prototype.hasOwnProperty(n)&&(e.prototype[n]=t.prototype[n])},min:function(e){var t=Number.MAX_VALUE,n;if(e instanceof Array)for(n=0;nt&&(t=e[n]);else for(n in e)e[n]>t&&(t=e[n]);return t},keys:function(e){var t=[],n;for(n in e)t.push(n);return t},values:function(e){var t=[],n,r;for(r=0;r')}}catch(e){jvm.VMLElement.prototype.createElement=function(e){return document.createElement("<"+e+' xmlns="urn:schemas-microsoft.com:vml" class="rvml">')}}document.createStyleSheet().addRule(".rvml","behavior:url(#default#VML)"),jvm.VMLElement.VMLInitialized=!0},jvm.VMLElement.prototype.getElementCtr=function(e){return jvm["VML"+e]},jvm.VMLElement.prototype.addClass=function(e){jvm.$(this.node).addClass(e)},jvm.VMLElement.prototype.applyAttr=function(e,t){this.node[e]=t},jvm.VMLElement.prototype.getBBox=function(){var e=jvm.$(this.node);return{x:e.position().left/this.canvas.scale,y:e.position().top/this.canvas.scale,width:e.width()/this.canvas.scale,height:e.height()/this.canvas.scale}},jvm.VMLGroupElement=function(){jvm.VMLGroupElement.parentClass.call(this,"group"),this.node.style.left="0px",this.node.style.top="0px",this.node.coordorigin="0 0"},jvm.inherits(jvm.VMLGroupElement,jvm.VMLElement),jvm.VMLGroupElement.prototype.add=function(e){this.node.appendChild(e.node)},jvm.VMLCanvasElement=function(e,t,n){this.classPrefix="VML",jvm.VMLCanvasElement.parentClass.call(this,"group"),jvm.AbstractCanvasElement.apply(this,arguments),this.node.style.position="absolute"},jvm.inherits(jvm.VMLCanvasElement,jvm.VMLElement),jvm.mixin(jvm.VMLCanvasElement,jvm.AbstractCanvasElement),jvm.VMLCanvasElement.prototype.setSize=function(e,t){var n,r,i,s;this.width=e,this.height=t,this.node.style.width=e+"px",this.node.style.height=t+"px",this.node.coordsize=e+" "+t,this.node.coordorigin="0 0";if(this.rootElement){n=this.rootElement.node.getElementsByTagName("shape");for(i=0,s=n.length;i=0)e-=t[i],i++;return i==this.scale.length-1?e=this.vectorToNum(this.scale[i]):e=this.vectorToNum(this.vectorAdd(this.scale[i],this.vectorMult(this.vectorSubtract(this.scale[i+1],this.scale[i]),e/t[i]))),e},vectorToNum:function(e){var t=0,n;for(n=0;nt&&(t=e[i]),r").css({width:"100%",height:"100%"}).addClass("jvectormap-container"),this.params.container.append(this.container),this.container.data("mapObject",this),this.container.css({position:"relative",overflow:"hidden"}),this.defaultWidth=this.mapData.width,this.defaultHeight=this.mapData.height,this.setBackgroundColor(this.params.backgroundColor),this.onResize=function(){t.setSize()},jvm.$(window).resize(this.onResize);for(n in jvm.WorldMap.apiEvents)this.params[n]&&this.container.bind(jvm.WorldMap.apiEvents[n]+".jvectormap",this.params[n]);this.canvas=new jvm.VectorCanvas(this.container[0],this.width,this.height),"ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch?this.params.bindTouchEvents&&this.bindContainerTouchEvents():this.bindContainerEvents(),this.bindElementEvents(),this.createLabel(),this.bindZoomButtons(),this.createRegions(),this.createMarkers(this.params.markers||{}),this.setSize(),this.params.focusOn&&(typeof this.params.focusOn=="object"?this.setFocus.call(this,this.params.focusOn.scale,this.params.focusOn.x,this.params.focusOn.y):this.setFocus.call(this,this.params.focusOn)),this.params.selectedRegions&&this.setSelectedRegions(this.params.selectedRegions),this.params.selectedMarkers&&this.setSelectedMarkers(this.params.selectedMarkers),this.params.series&&this.createSeries()},jvm.WorldMap.prototype={transX:0,transY:0,scale:1,baseTransX:0,baseTransY:0,baseScale:1,width:0,height:0,setBackgroundColor:function(e){this.container.css("background-color",e)},resize:function(){var e=this.baseScale;this.width/this.height>this.defaultWidth/this.defaultHeight?(this.baseScale=this.height/this.defaultHeight,this.baseTransX=Math.abs(this.width-this.defaultWidth*this.baseScale)/(2*this.baseScale)):(this.baseScale=this.width/this.defaultWidth,this.baseTransY=Math.abs(this.height-this.defaultHeight*this.baseScale)/(2*this.baseScale)),this.scale*=this.baseScale/e,this.transX*=this.baseScale/e,this.transY*=this.baseScale/e},setSize:function(){this.width=this.container.width(),this.height=this.container.height(),this.resize(),this.canvas.setSize(this.width,this.height),this.applyTransform()},reset:function(){var e,t;for(e in this.series)for(t=0;tt?this.transY=t:this.transYe?this.transX=e:this.transXt[1].pageX?o=t[1].pageX+(t[0].pageX-t[1].pageX)/2:o=t[0].pageX+(t[1].pageX-t[0].pageX)/2,t[0].pageY>t[1].pageY?u=t[1].pageY+(t[0].pageY-t[1].pageY)/2:u=t[0].pageY+(t[1].pageY-t[0].pageY)/2),i=e.originalEvent.touches[0].pageX,s=e.originalEvent.touches[0].pageY}),jvm.$(this.container).bind("touchmove",function(e){var t;if(r.scale!=r.baseScale)return e.originalEvent.touches.length==1&&i&&s?(t=e.originalEvent.touches[0],r.transX-=(i-t.pageX)/r.scale,r.transY-=(s-t.pageY)/r.scale,r.applyTransform(),r.label.hide(),i=t.pageX,s=t.pageY):(i=!1,s=!1),!1})},bindElementEvents:function(){var e=this,t;this.container.mousemove(function(){t=!0}),this.container.delegate("[class~='jvectormap-element']","mouseover mouseout",function(t){var n=this,r=jvm.$(this).attr("class").indexOf("jvectormap-region")===-1?"marker":"region",i=r=="region"?jvm.$(this).attr("data-code"):jvm.$(this).attr("data-index"),s=r=="region"?e.regions[i].element:e.markers[i].element,o=r=="region"?e.mapData.paths[i].name:e.markers[i].config.name||"",u=jvm.$.Event(r+"LabelShow.jvectormap"),a=jvm.$.Event(r+"Over.jvectormap");t.type=="mouseover"?(e.container.trigger(a,[i]),a.isDefaultPrevented()||s.setHovered(!0),e.label.text(o),e.container.trigger(u,[e.label,i]),u.isDefaultPrevented()||(e.label.show(),e.labelWidth=e.label.width(),e.labelHeight=e.label.height())):(s.setHovered(!1),e.label.hide(),e.container.trigger(r+"Out.jvectormap",[i]))}),this.container.delegate("[class~='jvectormap-element']","mousedown",function(e){t=!1}),this.container.delegate("[class~='jvectormap-element']","mouseup",function(n){var r=this,i=jvm.$(this).attr("class").indexOf("jvectormap-region")===-1?"marker":"region",s=i=="region"?jvm.$(this).attr("data-code"):jvm.$(this).attr("data-index"),o=jvm.$.Event(i+"Click.jvectormap"),u=i=="region"?e.regions[s].element:e.markers[s].element;if(!t){e.container.trigger(o,[s]);if(i==="region"&&e.params.regionsSelectable||i==="marker"&&e.params.markersSelectable)o.isDefaultPrevented()||(e.params[i+"sSelectableOne"]&&e.clearSelected(i+"s"),u.setSelected(!u.isSelected))}})},bindZoomButtons:function(){var e=this;jvm.$("
").addClass("jvectormap-zoomin").text("+").appendTo(this.container),jvm.$("
").addClass("jvectormap-zoomout").html("−").appendTo(this.container),this.container.find(".jvectormap-zoomin").click(function(){e.setScale(e.scale*e.params.zoomStep,e.width/2,e.height/2)}),this.container.find(".jvectormap-zoomout").click(function(){e.setScale(e.scale/e.params.zoomStep,e.width/2,e.height/2)})},createLabel:function(){var e=this;this.label=jvm.$("
").addClass("jvectormap-label").appendTo(jvm.$("body")),this.container.mousemove(function(t){var n=t.pageX-15-e.labelWidth,r=t.pageY-15-e.labelHeight;n<5&&(n=t.pageX+15),r<5&&(r=t.pageY+15),e.label.is(":visible")&&e.label.css({left:n,top:r})})},setScale:function(e,t,n,r){var i,s=jvm.$.Event("zoom.jvectormap");e>this.params.zoomMax*this.baseScale?e=this.params.zoomMax*this.baseScale:ei[0].x&&ei[0].y&&t + + <%include file="/courseware/course_navigation.html" args="active_page='instructor'" /> @@ -40,6 +42,45 @@ table.stat_table td { a.selectedmode { background-color: yellow; } + +.jvectormap-label { + position: absolute; + display: none; + border: solid 1px #CDCDCD; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background: #292929; + color: white; + font-family: sans-serif, Verdana; + font-size: smaller; + padding: 3px; +} + +.jvectormap-zoomin, .jvectormap-zoomout { + position: absolute; + left: 10px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background: #292929; + padding: 3px; + color: white; + width: 10px; + height: 10px; + cursor: pointer; + line-height: 10px; + text-align: center; +} + +.jvectormap-zoomin { + top: 10px; +} + +.jvectormap-zoomout { + top: 30px; +} + + +
+ +

Number of active students per problems who have this problem graded as correct:

From a9d3736cfbbed09e3ad9b9bcee30530b7f63bc92 Mon Sep 17 00:00:00 2001 From: jmvt Date: Wed, 16 Jan 2013 15:09:54 -0500 Subject: [PATCH 11/22] Replace all calls to analytics service to use the get methods (pulls from mongodb). Added error handling. --- lms/djangoapps/instructor/views.py | 36 ++-- .../courseware/instructor_dashboard.html | 166 +++++++++++------- 2 files changed, 129 insertions(+), 73 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 37fd133590..6c59200786 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -289,31 +289,43 @@ def instructor_dashboard(request, course_id): from_day = to_day - timedelta(days=7) # WARNING: do not use req.json because the preloaded json doesn't preserve the order of the original record + # use instead: json.loads(req.content, object_pairs_hook=OrderedDict) # number of students enrolled in this course - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsEnrolled&course_id=%s" % course_id) - students_enrolled_json = json.loads(req.content, object_pairs_hook=OrderedDict) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsEnrolled&course_id=%s" % course_id) + if req.content != 'None': + students_enrolled_json = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students active in the past 7 days (including current day), i.e. with at least one activity for the period #req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day)) - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s" % (course_id,)) # default is active past 7 days - students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsActive&course_id=%s" % (course_id,)) # default is active past 7 days + if req.content != 'None': + students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students per problem who have problem graded correct - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) - students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s" % (course_id,)) + if req.content != 'None': + students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) + + # number of students per problem who have problem graded correct <<< THIS IS FOR ACTIVE STUDENTS +# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) +# if req.content != 'None': +# students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) # grade distribution for the course +++ this is not the desired distribution +++ - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=OverallGradeDistribution&course_id=%s" % (course_id,)) - overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=OverallGradeDistribution&course_id=%s" % (course_id,)) + if req.content != 'None': + overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students distribution drop off per day - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day)) - dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day)) + if req.content != 'None': + dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students per problem who attempted this problem at least once - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsAttemptedProblems&course_id=%s" % course_id) - attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict) + req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsAttemptedProblems&course_id=%s" % course_id) + if req.content != 'None': + attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students active in the past 7 days (including current day) --- online version! experimental diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 94d05792f3..29f1602721 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -218,10 +218,29 @@ function goto( mode) %if modeflag.get('Analytics'):

- Number of students enrolled: ${students_enrolled_json['data']['value']} + Number of students enrolled: + % if students_enrolled_json is not None: + % if students_enrolled_json['status'] == 'success': + ${students_enrolled_json['data']['value']} + % else: + ${students_enrolled_json['error']} + % endif + % else: + null data + % endif

+

- Number of active students for the past 7 days: ${students_active_json['data']['value']} + Number of active students for the past 7 days: + % if students_active_json is not None: + % if students_active_json['status'] == 'success': + ${students_active_json['data']['value']} + % else: + ${students_active_json['error']} + % endif + % else: + null data + % endif

@@ -253,78 +272,103 @@ function goto( mode)

-

Number of active students per problems who have this problem graded as correct:

+ % if students_per_problem_correct_json is not None: + % if students_per_problem_correct_json['status'] == 'success': +
- + % for k,v in students_per_problem_correct_json['data'].items(): - - - - % endfor -
ProblemNumber of students
ProblemNumber of students
${k} ${v}
-

- -

-

Grade distribution:

- -
- - - % for k,v in overall_grade_distribution['data'].items(): - - - + % endfor
GradeNumber of students
${k} ${v}
${k} ${v}
- + % else: + ${students_per_problem_correct_json['error']} + % endif + % else: + null data + % endif

-

-

Number of students who dropped off per day before becoming inactive:

+##

+##

Students who attempted at least one exercise:

+## +## % if attempted_problems is not None: +## % if attempted_problems['status'] == 'success': +##
+## +## +## % for k,v in attempted_problems['data'].items(): +## +## % endfor +##
ModuleNumber of students
${k} ${v}
+##
+## % else: +## ${attempted_problems['error']} +## % endif +## % else: +## null data +## % endif +##

+## +##

+##

Number of students who dropped off per day before becoming inactive:

+## +## % if dropoff_per_day is not None: +## % if dropoff_per_day['status'] == 'success': +##
+## +## +## % for k,v in dropoff_per_day['data'].items(): +## +## % endfor +##
DayNumber of students
${k} ${v}
+##
+## % else: +## ${dropoff_per_day['error']} +## % endif +## % else: +## null data +## % endif +##

+## +##

+##

Grade distribution:

+## +## % if overall_grade_distribution is not None: +## % if overall_grade_distribution['status'] == 'success': +##
+## +## +## % for k,v in overall_grade_distribution['data'].items(): +## +## % endfor +##
GradeNumber of students
${k} ${v}
+##
+## % else: +## ${dropoff_per_day['error']} +## % endif +## % else: +## null data +## % endif +##

-
- - - % for k,v in dropoff_per_day['data'].items(): - - - - % endfor -
DayNumber of students
${k} ${v}
-
-

-

-

Students who attempted at least one exercise:

+##

+##

Daily activity (online version):

+## +## +## % for k,v in daily_activity_json['data'].items(): +## +## +## +## % endfor +##
DayNumber of students
${k} ${v}
+##

-
- - - % for k,v in attempted_problems['data'].items(): - - - - % endfor -
ModuleNumber of students
${k} ${v}
-
- -

- -

-

Daily activity (online version):

- - - % for k,v in daily_activity_json['data'].items(): - - - - % endfor -
DayNumber of students
${k} ${v}
-

%endif From 4f005044d62cb557e916f6e10235df3d332ce776 Mon Sep 17 00:00:00 2001 From: jmvt Date: Wed, 23 Jan 2013 10:17:17 -0500 Subject: [PATCH 12/22] Added time of query display. --- .../courseware/instructor_dashboard.html | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 3260ee569b..967088de19 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -348,7 +348,7 @@ function goto( mode) Number of students enrolled: % if students_enrolled_json is not None: % if students_enrolled_json['status'] == 'success': - ${students_enrolled_json['data']['value']} + ${students_enrolled_json['data']['value']} as of ${students_enrolled_json['time']} % else: ${students_enrolled_json['error']} % endif @@ -361,7 +361,7 @@ function goto( mode) Number of active students for the past 7 days: % if students_active_json is not None: % if students_active_json['status'] == 'success': - ${students_active_json['data']['value']} + ${students_active_json['data']['value']} as of ${students_active_json['time']} % else: ${students_active_json['error']} % endif @@ -400,7 +400,7 @@ function goto( mode)

-

Number of active students per problems who have this problem graded as correct:

+

Number of students per problem who have this problem graded as correct, as of ${students_per_problem_correct_json['time']}

% if students_per_problem_correct_json is not None: % if students_per_problem_correct_json['status'] == 'success': @@ -420,28 +420,30 @@ function goto( mode) % endif

-##

-##

Students who attempted at least one exercise:

-## -## % if attempted_problems is not None: -## % if attempted_problems['status'] == 'success': -##
-## -## -## % for k,v in attempted_problems['data'].items(): -## -## % endfor -##
ModuleNumber of students
${k} ${v}
-##
-## % else: -## ${attempted_problems['error']} -## % endif -## % else: -## null data -## % endif -##

-## -##

+

+ Students per module who attempted at least one problem + + % if attempted_problems is not None: + , as of ${attempted_problems['time']} + % if attempted_problems['status'] == 'success': +

+ + + % for k,v in attempted_problems['data'].items(): + + % endfor +
ModuleNumber of students
${k} ${v}
+
+ % else: + ${attempted_problems['error']} + % endif + % else: + : null data + % endif + +

+

+ ##

Number of students who dropped off per day before becoming inactive:

## ## % if dropoff_per_day is not None: From d6984cced8682d98d11658338aae6d166cf6059d Mon Sep 17 00:00:00 2001 From: jmvt Date: Mon, 4 Feb 2013 16:08:37 -0500 Subject: [PATCH 13/22] Updated code to handle new analytics result format. --- .../courseware/instructor_dashboard.html | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 967088de19..8053f67a88 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -345,10 +345,10 @@ function goto( mode) %if modeflag.get('Analytics'):

- Number of students enrolled: + Number of students enrolled for ${students_enrolled_json['data'][0]['course_id']}: % if students_enrolled_json is not None: % if students_enrolled_json['status'] == 'success': - ${students_enrolled_json['data']['value']} as of ${students_enrolled_json['time']} + ${students_enrolled_json['data'][0]['count']} as of ${students_enrolled_json['time']} % else: ${students_enrolled_json['error']} % endif @@ -358,10 +358,10 @@ function goto( mode)

- Number of active students for the past 7 days: + Number of students active for ${students_active_json['data'][0]['course_id']} for the past 7 days: % if students_active_json is not None: % if students_active_json['status'] == 'success': - ${students_active_json['data']['value']} as of ${students_active_json['time']} + ${students_active_json['data'][0]['count']} as of ${students_active_json['time']} % else: ${students_active_json['error']} % endif @@ -407,8 +407,8 @@ function goto( mode)

- % for k,v in students_per_problem_correct_json['data'].items(): - + % for row in students_per_problem_correct_json['data']: + % endfor
ProblemNumber of students
${k} ${v}
${row['module_id']} ${row['count']}
@@ -420,6 +420,7 @@ function goto( mode) % endif

+

Students per module who attempted at least one problem @@ -429,8 +430,8 @@ function goto( mode)

- % for k,v in attempted_problems['data'].items(): - + % for row in attempted_problems['data']: + % endfor
ModuleNumber of students
${k} ${v}
${row['module_id']} ${row['count']}
@@ -444,6 +445,27 @@ function goto( mode)

+

+

Grade distribution:

+ + % if overall_grade_distribution is not None: + % if overall_grade_distribution['status'] == 'success': +
+ + + % for row in overall_grade_distribution['data']: + + % endfor +
GradeNumber of students
${row['overall_grade']} ${row['count']}
+
+ % else: + ${dropoff_per_day['error']} + % endif + % else: + null data + % endif +

+ ##

Number of students who dropped off per day before becoming inactive:

## ## % if dropoff_per_day is not None: @@ -464,26 +486,6 @@ function goto( mode) ## % endif ##

## -##

-##

Grade distribution:

-## -## % if overall_grade_distribution is not None: -## % if overall_grade_distribution['status'] == 'success': -##
-## -## -## % for k,v in overall_grade_distribution['data'].items(): -## -## % endfor -##
GradeNumber of students
${k} ${v}
-##
-## % else: -## ${dropoff_per_day['error']} -## % endif -## % else: -## null data -## % endif -##

##

From c144795588c420602d499f9361d8ec54e04b1bb0 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 7 Mar 2013 10:01:56 -0500 Subject: [PATCH 14/22] Re-enable a couple of analytics on the dashboard to test layout --- lms/djangoapps/instructor/views.py | 80 ++++++++++--------- .../courseware/instructor_dashboard.html | 27 +++++++ 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 568a5c64b3..68f55c3683 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -8,6 +8,7 @@ import logging import os import re import requests +from requests.status_codes import codes import urllib import datetime from datetime import datetime, timedelta @@ -612,40 +613,46 @@ def instructor_dashboard(request, course_id): # use instead: json.loads(req.content, object_pairs_hook=OrderedDict) # number of students enrolled in this course - req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsEnrolled&course_id=%s" % course_id) - if req.content != 'None': - students_enrolled_json = json.loads(req.content, object_pairs_hook=OrderedDict) - - # number of students active in the past 7 days (including current day), i.e. with at least one activity for the period - #req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day)) - req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsActive&course_id=%s" % (course_id,)) # default is active past 7 days - if req.content != 'None': - students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict) + res = requests.get(settings.ANALYTICS_SERVER_URL + + "get?aname=StudentsEnrolled&course_id=%s" % course_id) + if res.status_code == codes.OK: + students_enrolled_json = json.loads(res.content, + object_pairs_hook=OrderedDict) + else: + students_enrolled_json = None +# # number of students active in the past 7 days (including current day), i.e. with at least one activity for the period +# #req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day)) +# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsActive&course_id=%s" % (course_id,)) # default is active past 7 days +# if req.content != 'None': +# students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict) +# # number of students per problem who have problem graded correct - req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s" % (course_id,)) - if req.content != 'None': - students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) - - # number of students per problem who have problem graded correct <<< THIS IS FOR ACTIVE STUDENTS -# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) -# if req.content != 'None': -# students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) + res = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s" % (course_id,)) + if res.status_code == codes.OK: + students_per_problem_correct_json = json.loads(res.content, object_pairs_hook=OrderedDict) + else: + students_per_problem_correct_json = None - # grade distribution for the course +++ this is not the desired distribution +++ - req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=OverallGradeDistribution&course_id=%s" % (course_id,)) - if req.content != 'None': - overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict) - - # number of students distribution drop off per day - req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day)) - if req.content != 'None': - dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict) - - # number of students per problem who attempted this problem at least once - req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsAttemptedProblems&course_id=%s" % course_id) - if req.content != 'None': - attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict) +# # number of students per problem who have problem graded correct <<< THIS IS FOR ACTIVE STUDENTS +# # req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) +# # if req.content != 'None': +# # students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) +# +# # grade distribution for the course +++ this is not the desired distribution +++ +# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=OverallGradeDistribution&course_id=%s" % (course_id,)) +# if req.content != 'None': +# overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict) +# +# # number of students distribution drop off per day +# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day)) +# if req.content != 'None': +# dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict) +# +# # number of students per problem who attempted this problem at least once +# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsAttemptedProblems&course_id=%s" % course_id) +# if req.content != 'None': +# attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict) # number of students active in the past 7 days (including current day) --- online version! experimental @@ -684,12 +691,13 @@ def instructor_dashboard(request, course_id): # The following are specific analytics metrics that should be # put in their own space... 'students_enrolled_json' : students_enrolled_json, - 'students_active_json' : students_active_json, - 'daily_activity_json' : daily_activity_json, + # 'students_active_json' : students_active_json, + # 'daily_activity_json' : daily_activity_json, 'students_per_problem_correct_json' : students_per_problem_correct_json, - 'overall_grade_distribution' : overall_grade_distribution, - 'dropoff_per_day' : dropoff_per_day, - 'attempted_problems' : attempted_problems, + # 'overall_grade_distribution' : overall_grade_distribution, + # 'dropoff_per_day' : dropoff_per_day, + # 'attempted_problems' : attempted_problems + } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index b737cf6978..8a0c62c6f2 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -371,6 +371,33 @@ function goto( mode) %if modeflag.get('Analytics'): +

+ Students enrolled: + % if students_enrolled_json and "data" in students_enrolled_json: + ${students_enrolled_json['data'][0]['students']} + + % endif +

+ +

+

Number of students per problem who have this problem graded as correct, as of ${students_per_problem_correct_json['time']}

+ + % if students_per_problem_correct_json and "data" in students_per_problem_correct_json: +
+ + + % for row in students_per_problem_correct_json['data']: + + % endfor +
ProblemNumber of students
${row['module_id']} ${row['count']}
+
+ % endif +

+ +%endif + +%if modeflag.get('Analytics 2'): +

Number of students enrolled for ${students_enrolled_json['data'][0]['course_id']}: % if students_enrolled_json is not None: From c43d4b3469d4beeeb86c2f4bd99028a9caee69e9 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Thu, 7 Mar 2013 23:39:10 -0500 Subject: [PATCH 15/22] re-added Students with correct answers to analytics dashboard --- lms/templates/courseware/instructor_dashboard.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 8a0c62c6f2..a138ca71c3 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -380,8 +380,7 @@ function goto( mode)

-

Number of students per problem who have this problem graded as correct, as of ${students_per_problem_correct_json['time']}

- +

Students answering correctly

% if students_per_problem_correct_json and "data" in students_per_problem_correct_json:
@@ -396,7 +395,7 @@ function goto( mode) %endif -%if modeflag.get('Analytics 2'): +%if modeflag.get('Analytics In Progress'):

Number of students enrolled for ${students_enrolled_json['data'][0]['course_id']}: From 3bb3cf3ca396d4aaf494d849ffc78acfa8756c90 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Sat, 9 Mar 2013 18:52:20 -0500 Subject: [PATCH 16/22] Consolidate some of the analytic fetching logic in the instructor dashboard. --- lms/djangoapps/instructor/views.py | 99 +++++-------------- lms/envs/dev.py | 2 +- .../courseware/instructor_dashboard.html | 79 +++++++-------- 3 files changed, 61 insertions(+), 119 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 68f55c3683..ea1e0ba350 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -594,72 +594,34 @@ def instructor_dashboard(request, course_id): #---------------------------------------- # analytics + def get_analytics_result(analytics_name): + url = settings.ANALYTICS_SERVER_URL + \ + "get?aname={}&course_id={}".format(analytics_name, course_id) + res = requests.get(url) + if res.status_code == codes.OK: + # WARNING: do not use req.json because the preloaded json doesn't + # preserve the order of the original record use instead: + # json.loads(req.content, object_pairs_hook=OrderedDict) + return json.loads(res.content, object_pairs_hook=OrderedDict) + else: + log.error("Error fetching %s, code: %s, msg: %s", + (url, res.status_code, res.content)) + return None - attempted_problems = None - students_enrolled_json = None - students_active_json = None - daily_activity_json = None - students_per_problem_correct_json = None - overall_grade_distribution = None - dropoff_per_day = None + analytics_results = {} if idash_mode == 'Analytics': - - # get current day - to_day = datetime.today().date() - from_day = to_day - timedelta(days=7) - - # WARNING: do not use req.json because the preloaded json doesn't preserve the order of the original record - # use instead: json.loads(req.content, object_pairs_hook=OrderedDict) - - # number of students enrolled in this course - res = requests.get(settings.ANALYTICS_SERVER_URL + - "get?aname=StudentsEnrolled&course_id=%s" % course_id) - if res.status_code == codes.OK: - students_enrolled_json = json.loads(res.content, - object_pairs_hook=OrderedDict) - else: - students_enrolled_json = None - -# # number of students active in the past 7 days (including current day), i.e. with at least one activity for the period -# #req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=StudentsActive&course_id=%s&from=%s" % (course_id,from_day)) -# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsActive&course_id=%s" % (course_id,)) # default is active past 7 days -# if req.content != 'None': -# students_active_json = json.loads(req.content, object_pairs_hook=OrderedDict) -# - # number of students per problem who have problem graded correct - res = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s" % (course_id,)) - if res.status_code == codes.OK: - students_per_problem_correct_json = json.loads(res.content, object_pairs_hook=OrderedDict) - else: - students_per_problem_correct_json = None - -# # number of students per problem who have problem graded correct <<< THIS IS FOR ACTIVE STUDENTS -# # req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsPerProblemCorrect&course_id=%s&from=%s" % (course_id,from_day)) -# # if req.content != 'None': -# # students_per_problem_correct_json = json.loads(req.content, object_pairs_hook=OrderedDict) -# -# # grade distribution for the course +++ this is not the desired distribution +++ -# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=OverallGradeDistribution&course_id=%s" % (course_id,)) -# if req.content != 'None': -# overall_grade_distribution = json.loads(req.content, object_pairs_hook=OrderedDict) -# -# # number of students distribution drop off per day -# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsDropoffPerDay&course_id=%s&from=%s" % (course_id,from_day)) -# if req.content != 'None': -# dropoff_per_day = json.loads(req.content, object_pairs_hook=OrderedDict) -# -# # number of students per problem who attempted this problem at least once -# req = requests.get(settings.ANALYTICS_SERVER_URL + "get?aname=StudentsAttemptedProblems&course_id=%s" % course_id) -# if req.content != 'None': -# attempted_problems = json.loads(req.content, object_pairs_hook=OrderedDict) - - - # number of students active in the past 7 days (including current day) --- online version! experimental - to_day = datetime.today().date() - from_day = to_day - timedelta(days=7) - req = requests.get(settings.ANALYTICS_SERVER_URL + "get_analytics?aname=DailyActivityAnalyzer&course_id=%s&from=%s&to=%s" % (course_id,from_day, to_day)) - daily_activity_json = req.json + DASHBOARD_ANALYTICS = [ + "StudentsAttemptedProblems", # num students who tried given problem + "StudentsDailyActivity", # active students by day + "StudentsDropoffPerDay", # active students dropoff by day + "OverallGradeDistribution", # overall point distribution for course + "StudentsActive", # num students active in time period (default = 1wk) + "StudentsEnrolled", # num students enrolled + "StudentsPerProblemCorrect", # foreach problem, num students correct + ] + for analytic_name in DASHBOARD_ANALYTICS: + analytics_results[analytic_name] = get_analytics_result(analytic_name) #---------------------------------------- # offline grades? @@ -687,16 +649,7 @@ def instructor_dashboard(request, course_id): 'offline_grade_log': offline_grades_available(course_id), 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), - - # The following are specific analytics metrics that should be - # put in their own space... - 'students_enrolled_json' : students_enrolled_json, - # 'students_active_json' : students_active_json, - # 'daily_activity_json' : daily_activity_json, - 'students_per_problem_correct_json' : students_per_problem_correct_json, - # 'overall_grade_distribution' : overall_grade_distribution, - # 'dropoff_per_day' : dropoff_per_day, - # 'attempted_problems' : attempted_problems + 'analytics_results' : analytics_results, } return render_to_response('courseware/instructor_dashboard.html', context) diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 3b047ba66e..bfa2b3f32d 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -220,5 +220,5 @@ PEARSON_TEST_PASSWORD = "12345" ########################## ANALYTICS TESTING ######################## -ANALYTICS_SERVER_URL = "http://127.0.0.1:14141/" +ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/" diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index a138ca71c3..a053cdaa81 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -371,57 +371,46 @@ function goto( mode) %if modeflag.get('Analytics'): -

- Students enrolled: - % if students_enrolled_json and "data" in students_enrolled_json: - ${students_enrolled_json['data'][0]['students']} - - % endif -

+ %if analytics_results.get("StudentsEnrolled"): +

+ Students enrolled: + ${analytics_results["StudentsEnrolled"]['data'][0]['students']} +

+ %endif + + %if analytics_results.get("StudentsActive"): +

+ Students active in the last week: + ${analytics_results["StudentsActive"]['data'][0]['active']} +

+ + %endif + + ##This is not as helpful as it could be -- let's give full point distribution + ##instead. + %if analytics_results.get("StudentsPerProblemCorrect"): +

Students answering correctly

+
+
+ + + + + %for row in analytics_results['StudentsPerProblemCorrect']['data']: + + + + + %endfor +
ProblemNumber of students
${row['module_id'].split('/')[-1]}${row['count']}
+
+ %endif -

-

Students answering correctly

- % if students_per_problem_correct_json and "data" in students_per_problem_correct_json: -
- - - % for row in students_per_problem_correct_json['data']: - - % endfor -
ProblemNumber of students
${row['module_id']} ${row['count']}
-
- % endif -

%endif %if modeflag.get('Analytics In Progress'): -

- Number of students enrolled for ${students_enrolled_json['data'][0]['course_id']}: - % if students_enrolled_json is not None: - % if students_enrolled_json['status'] == 'success': - ${students_enrolled_json['data'][0]['count']} as of ${students_enrolled_json['time']} - % else: - ${students_enrolled_json['error']} - % endif - % else: - null data - % endif -

- -

- Number of students active for ${students_active_json['data'][0]['course_id']} for the past 7 days: - % if students_active_json is not None: - % if students_active_json['status'] == 'success': - ${students_active_json['data'][0]['count']} as of ${students_active_json['time']} - % else: - ${students_active_json['error']} - % endif - % else: - null data - % endif -

Student distribution per country, all courses, Sep-12 to Oct-17, 1 server (shown here as an example): From cc82ffe10ab9f5c29d44a198c37e843b662b370a Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Sun, 10 Mar 2013 00:48:46 -0500 Subject: [PATCH 17/22] Give a more comprehensive grade distribution of problems analytic --- lms/djangoapps/instructor/views.py | 7 +- .../courseware/instructor_dashboard.html | 124 +++++++----------- 2 files changed, 55 insertions(+), 76 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index ea1e0ba350..24d29c9f2f 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -612,13 +612,14 @@ def instructor_dashboard(request, course_id): if idash_mode == 'Analytics': DASHBOARD_ANALYTICS = [ - "StudentsAttemptedProblems", # num students who tried given problem + #"StudentsAttemptedProblems", # num students who tried given problem "StudentsDailyActivity", # active students by day "StudentsDropoffPerDay", # active students dropoff by day - "OverallGradeDistribution", # overall point distribution for course + #"OverallGradeDistribution", # overall point distribution for course "StudentsActive", # num students active in time period (default = 1wk) "StudentsEnrolled", # num students enrolled - "StudentsPerProblemCorrect", # foreach problem, num students correct + #"StudentsPerProblemCorrect", # foreach problem, num students correct, + "ProblemGradeDistribution", # foreach problem, grade distribution ] for analytic_name in DASHBOARD_ANALYTICS: analytics_results[analytic_name] = get_analytics_result(analytic_name) diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index a053cdaa81..24ceade496 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -383,9 +383,59 @@ function goto( mode) Students active in the last week: ${analytics_results["StudentsActive"]['data'][0]['active']}

- %endif + %if analytics_results.get("StudentsDropoffPerDay"): +

Student activity day by day

+
+ + + + + + %for row in analytics_results['StudentsDropoffPerDay']['data']: + + ## For now, just discard the non-date portion + + + + %endfor +
DayStudents
${row['last_day'].split("T")[0]}${row['num_students']}
+
+ %endif +
+ %if analytics_results.get("ProblemGradeDistribution"): +

Answer distribution for problems

+
+ + + + + + + %for row in analytics_results['ProblemGradeDistribution']['data']: + + + + %endfor + + %endfor +
ProblemMaxPoints Earned (Num Students)
${row['module_id'].split('/')[-1]}${max(grade_record['max_grade'] for grade_record in row["grade_info"])} + %for grade_record in row["grade_info"]: + + %if isinstance(grade_record["grade"], float): + ${"{grade:.2f}".format(**grade_record)} + %else: + ${"{grade}".format(**grade_record)} + %endif + (${grade_record["num_students"]}) +
+
+ %endif +%endif + +%if modeflag.get('Analytics In Progress'): + ##This is not as helpful as it could be -- let's give full point distribution ##instead. %if analytics_results.get("StudentsPerProblemCorrect"): @@ -406,12 +456,6 @@ function goto( mode)
%endif - -%endif - -%if modeflag.get('Analytics In Progress'): - -

Student distribution per country, all courses, Sep-12 to Oct-17, 1 server (shown here as an example):

@@ -441,72 +485,6 @@ function goto( mode)
-

-

Number of students per problem who have this problem graded as correct, as of ${students_per_problem_correct_json['time']}

- - % if students_per_problem_correct_json is not None: - % if students_per_problem_correct_json['status'] == 'success': -
- - - % for row in students_per_problem_correct_json['data']: - - % endfor -
ProblemNumber of students
${row['module_id']} ${row['count']}
-
- % else: - ${students_per_problem_correct_json['error']} - % endif - % else: - null data - % endif -

- - -

- Students per module who attempted at least one problem - - % if attempted_problems is not None: - , as of ${attempted_problems['time']} - % if attempted_problems['status'] == 'success': -

- - - % for row in attempted_problems['data']: - - % endfor -
ModuleNumber of students
${row['module_id']} ${row['count']}
-
- % else: - ${attempted_problems['error']} - % endif - % else: - : null data - % endif - -

-

- -

-

Grade distribution:

- - % if overall_grade_distribution is not None: - % if overall_grade_distribution['status'] == 'success': -
- - - % for row in overall_grade_distribution['data']: - - % endfor -
GradeNumber of students
${row['overall_grade']} ${row['count']}
-
- % else: - ${dropoff_per_day['error']} - % endif - % else: - null data - % endif -

##

Number of students who dropped off per day before becoming inactive:

## From 4d4869b1766bc951c645abe9e491b7fe9e414f58 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Mon, 11 Mar 2013 01:24:25 -0400 Subject: [PATCH 18/22] Remove HTTP-based logging leftover from early experiments. --- common/lib/logsettings.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/common/lib/logsettings.py b/common/lib/logsettings.py index 9af8b08fb1..8fc2bb9db1 100644 --- a/common/lib/logsettings.py +++ b/common/lib/logsettings.py @@ -88,7 +88,7 @@ def get_logger_config(log_dir, }, 'loggers': { 'tracking': { - 'handlers': ['tracking', 'http'], + 'handlers': ['tracking'], 'level': 'DEBUG', 'propagate': False, }, @@ -120,12 +120,6 @@ def get_logger_config(log_dir, 'maxBytes': 1024 * 1024 * 2, 'backupCount': 5, }, - 'http' : { - 'level': 'DEBUG', - 'class': 'logging.handlers.HTTPHandler', - 'host':'127.0.0.1:14141', - 'url':'/an_evt', - } }) else: # for production environments we will only From a0dd6d9832bfb7974827738461c7d57fa41a636a Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Mon, 11 Mar 2013 10:05:36 -0400 Subject: [PATCH 19/22] add entry for ANALYTICS_SERVER_URL to aws.py --- lms/envs/aws.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 9089bc92ed..2320bee225 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -110,3 +110,6 @@ PEARSON = AUTH_TOKENS.get("PEARSON") # Datadog for events! DATADOG_API = AUTH_TOKENS.get("DATADOG_API") + +# Analytics dashboard server +ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL") From e135a4052b68d86846f458cef847a1bbffec7f14 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Wed, 13 Mar 2013 15:16:36 -0400 Subject: [PATCH 20/22] Better error checking with Analytics are unavailable. --- lms/djangoapps/instructor/views.py | 2 +- lms/templates/courseware/instructor_dashboard.html | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 24d29c9f2f..e5bef24902 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -605,7 +605,7 @@ def instructor_dashboard(request, course_id): return json.loads(res.content, object_pairs_hook=OrderedDict) else: log.error("Error fetching %s, code: %s, msg: %s", - (url, res.status_code, res.content)) + url, res.status_code, res.content) return None analytics_results = {} diff --git a/lms/templates/courseware/instructor_dashboard.html b/lms/templates/courseware/instructor_dashboard.html index 24ceade496..eda8c8a391 100644 --- a/lms/templates/courseware/instructor_dashboard.html +++ b/lms/templates/courseware/instructor_dashboard.html @@ -371,6 +371,10 @@ function goto( mode) %if modeflag.get('Analytics'): + %if not any(analytics_results.values()): +

No Analytics are available at this time.

+ %endif + %if analytics_results.get("StudentsEnrolled"):

Students enrolled: From 33c2507abc1e427ebc724953226b16e5e676d253 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Wed, 13 Mar 2013 15:27:33 -0400 Subject: [PATCH 21/22] Guard against connection errors to the Analytics service. --- lms/djangoapps/instructor/views.py | 13 ++++++++++--- lms/envs/dev.py | 1 - 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index e5bef24902..956a3747f0 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -595,13 +595,20 @@ def instructor_dashboard(request, course_id): #---------------------------------------- # analytics def get_analytics_result(analytics_name): + """Return data for an Analytic piece, or None if it doesn't exist. It + logs and swallows errors. + """ url = settings.ANALYTICS_SERVER_URL + \ "get?aname={}&course_id={}".format(analytics_name, course_id) - res = requests.get(url) + try: + res = requests.get(url) + except Exception: + log.exception("Error trying to access analytics at %s", url) + return None + if res.status_code == codes.OK: # WARNING: do not use req.json because the preloaded json doesn't - # preserve the order of the original record use instead: - # json.loads(req.content, object_pairs_hook=OrderedDict) + # preserve the order of the original record (hence OrderedDict). return json.loads(res.content, object_pairs_hook=OrderedDict) else: log.error("Error fetching %s, code: %s, msg: %s", diff --git a/lms/envs/dev.py b/lms/envs/dev.py index bfa2b3f32d..75e74fed08 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -221,4 +221,3 @@ PEARSON_TEST_PASSWORD = "12345" ########################## ANALYTICS TESTING ######################## ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/" - From 8d4e0f83866e311c43f33a67df6a6661d0899f14 Mon Sep 17 00:00:00 2001 From: David Ormsbee Date: Wed, 13 Mar 2013 16:55:40 -0400 Subject: [PATCH 22/22] Add ANALYTICS_API_KEY to config files and dashboard analytics call --- lms/djangoapps/instructor/views.py | 4 +++- lms/envs/aws.py | 1 + lms/envs/dev.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index 956a3747f0..91ea8793f1 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -599,7 +599,9 @@ def instructor_dashboard(request, course_id): logs and swallows errors. """ url = settings.ANALYTICS_SERVER_URL + \ - "get?aname={}&course_id={}".format(analytics_name, course_id) + "get?aname={}&course_id={}&apikey={}".format(analytics_name, + course_id, + settings.ANALYTICS_API_KEY) try: res = requests.get(url) except Exception: diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 2320bee225..cc9247b876 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -113,3 +113,4 @@ DATADOG_API = AUTH_TOKENS.get("DATADOG_API") # Analytics dashboard server ANALYTICS_SERVER_URL = ENV_TOKENS.get("ANALYTICS_SERVER_URL") +ANALYTICS_API_KEY = AUTH_TOKENS.get("ANALYTICS_API_KEY", "") diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 75e74fed08..f5b30c948b 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -221,3 +221,4 @@ PEARSON_TEST_PASSWORD = "12345" ########################## ANALYTICS TESTING ######################## ANALYTICS_SERVER_URL = "http://127.0.0.1:9000/" +ANALYTICS_API_KEY = "" \ No newline at end of file