New tab (Metrics) in instructor dashboard
Metrics tab shows student data: -Count of students opened a subsection -Grade distribution per problem for each section/subsection of the course. Implemented for both the old and beta dashboard Controlled by a feature flag 'CLASS_DASHBOARD' Data is aggregated across all students Aggregate data computed from courseware_studentmodule
This commit is contained in:
committed by
David Adams
parent
9a23be5379
commit
3881ffdc0d
Binary file not shown.
@@ -37,8 +37,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1a\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2014-02-27 08:57-0500\n"
|
||||
"PO-Revision-Date: 2014-02-27 13:57:20.220825\n"
|
||||
"POT-Creation-Date: 2014-02-28 13:57-0800\n"
|
||||
"PO-Revision-Date: 2014-02-28 21:57:20.716655\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -1294,6 +1294,40 @@ msgstr "Séärçh Ⱡ'σяєм ιρѕ#"
|
||||
msgid "Copyright"
|
||||
msgstr "Çöpýrïght #"
|
||||
|
||||
#: lms/djangoapps/class_dashboard/dashboard_data.py
|
||||
msgid ""
|
||||
"{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: "
|
||||
"{grade:.0f}/{max_grade:.0f} {questions})"
|
||||
msgstr ""
|
||||
"{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: "
|
||||
"{grade:.0f}/{max_grade:.0f} {questions}) Ⱡ'σяєм ιρѕ#"
|
||||
|
||||
#: lms/djangoapps/class_dashboard/dashboard_data.py
|
||||
#: lms/djangoapps/class_dashboard/dashboard_data.py
|
||||
msgid "students"
|
||||
msgstr "stüdénts #"
|
||||
|
||||
#: lms/djangoapps/class_dashboard/dashboard_data.py
|
||||
#: lms/djangoapps/class_dashboard/dashboard_data.py
|
||||
msgid "questions"
|
||||
msgstr "qüéstïöns #"
|
||||
|
||||
#: lms/djangoapps/class_dashboard/dashboard_data.py
|
||||
msgid ""
|
||||
"{num_students} student(s) opened Subsection {subsection_num}: "
|
||||
"{subsection_name}"
|
||||
msgstr ""
|
||||
"{num_students} stüdént(s) öpénéd Süßséçtïön {subsection_num}: "
|
||||
"{subsection_name} Ⱡ'σяєм ιρѕυ#"
|
||||
|
||||
#: lms/djangoapps/class_dashboard/dashboard_data.py
|
||||
msgid ""
|
||||
"{problem_info_x} {problem_info_n} - {count_grade} {students} "
|
||||
"({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})"
|
||||
msgstr ""
|
||||
"{problem_info_x} {problem_info_n} - {count_grade} {students} "
|
||||
"({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions}) Ⱡ'σяєм ιρѕ#"
|
||||
|
||||
#. Translators: this string includes wiki markup. Leave the ** and the _
|
||||
#. alone.
|
||||
#: lms/djangoapps/course_wiki/views.py
|
||||
@@ -2961,7 +2995,7 @@ msgstr "Délété ärtïçlé Ⱡ'#"
|
||||
|
||||
#: lms/templates/wiki/delete.html
|
||||
#: lms/templates/wiki/plugins/attachments/index.html
|
||||
#: cms/templates/component.html
|
||||
#: cms/templates/component.html cms/templates/studio_xblock_wrapper.html
|
||||
#: lms/templates/discussion/_underscore_templates.html
|
||||
#: lms/templates/discussion/_underscore_templates.html
|
||||
#: lms/templates/discussion/mustache/_inline_thread_show.mustache
|
||||
@@ -3003,6 +3037,7 @@ msgid "You are deleting an article. Please confirm."
|
||||
msgstr "Ýöü äré délétïng än ärtïçlé. Pléäsé çönfïrm. Ⱡ'σяєм ιρѕυм#"
|
||||
|
||||
#: lms/templates/wiki/edit.html cms/templates/component.html
|
||||
#: cms/templates/studio_xblock_wrapper.html
|
||||
#: lms/templates/discussion/_underscore_templates.html
|
||||
#: lms/templates/discussion/_underscore_templates.html
|
||||
#: lms/templates/discussion/_underscore_templates.html
|
||||
@@ -3032,6 +3067,7 @@ msgstr "Prévïéw #"
|
||||
#: lms/templates/help_modal.html lms/templates/login_modal.html
|
||||
#: lms/templates/signup_modal.html
|
||||
#: lms/templates/modal/_modal-settings-language.html
|
||||
#: lms/templates/modal/accessible_confirm.html
|
||||
msgid "Close Modal"
|
||||
msgstr "Çlösé Mödäl Ⱡ#"
|
||||
|
||||
@@ -3048,6 +3084,7 @@ msgstr "Wïkï Prévïéw Ⱡ#"
|
||||
#: lms/templates/dashboard.html lms/templates/dashboard.html
|
||||
#: lms/templates/dashboard.html
|
||||
#: lms/templates/modal/_modal-settings-language.html
|
||||
#: lms/templates/modal/accessible_confirm.html
|
||||
msgid "modal open"
|
||||
msgstr "mödäl öpén Ⱡ#"
|
||||
|
||||
@@ -3713,6 +3750,11 @@ msgstr "Püßlïç Ûsérnämé Ⱡ'#"
|
||||
msgid "Preferred Language"
|
||||
msgstr "Préférréd Längüägé Ⱡ'σ#"
|
||||
|
||||
#: cms/templates/unit_container_xblock_component.html
|
||||
#: lms/templates/wiki/includes/article_menu.html
|
||||
msgid "View"
|
||||
msgstr "Vïéw Ⱡ'σяєм#"
|
||||
|
||||
#: cms/templates/registration/activation_complete.html
|
||||
#: lms/templates/registration/activation_complete.html
|
||||
msgid "Thanks for activating your account."
|
||||
@@ -4172,9 +4214,6 @@ msgstr ""
|
||||
msgid "Change your name"
|
||||
msgstr "Çhängé ýöür nämé Ⱡ'σ#"
|
||||
|
||||
#. Translators: note that {platform} {cert_name_short} will look something
|
||||
#. like: "edX certificate". Please do not change the order of these
|
||||
#. placeholders.
|
||||
#: lms/templates/dashboard.html
|
||||
msgid ""
|
||||
"To uphold the credibility of your {platform} {cert_name_short}, all name "
|
||||
@@ -4183,9 +4222,6 @@ msgstr ""
|
||||
"Tö üphöld thé çrédïßïlïtý öf ýöür {platform} {cert_name_short}, äll nämé "
|
||||
"çhängés wïll ßé löggéd änd réçördéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#"
|
||||
|
||||
#. Translators: note that {platform} {cert_name_short} will look something
|
||||
#. like: "edX certificate". Please do not change the order of these
|
||||
#. placeholders.
|
||||
#: lms/templates/dashboard.html
|
||||
msgid ""
|
||||
"Enter your desired full name, as it will appear on your {platform} "
|
||||
@@ -4847,7 +4883,7 @@ msgstr "Réjéçtéd #"
|
||||
msgid "Pending name changes"
|
||||
msgstr "Péndïng nämé çhängés Ⱡ'σя#"
|
||||
|
||||
#: lms/templates/name_changes.html
|
||||
#: lms/templates/name_changes.html lms/templates/modal/accessible_confirm.html
|
||||
msgid "Confirm"
|
||||
msgstr "Çönfïrm #"
|
||||
|
||||
@@ -5710,20 +5746,10 @@ msgstr "Tögglé Füll Rüßrïç Ⱡ'σ#"
|
||||
msgid "{result_of_task} from grader {number}"
|
||||
msgstr "{result_of_task} fröm grädér {number} Ⱡ'σя#"
|
||||
|
||||
#. Translators: "See full feedback" is the text of
|
||||
#. a link that allows a user to see more detailed
|
||||
#. feedback from a self, peer, or instructor
|
||||
#. graded openended problem
|
||||
#: lms/templates/combinedopenended/open_ended_result_table.html
|
||||
msgid "See full feedback"
|
||||
msgstr "Séé füll féédßäçk Ⱡ'σ#"
|
||||
|
||||
#. Translators: this text forms a link that, when
|
||||
#. clicked, allows a user to respond to the feedback
|
||||
#. the user received on his or her openended problem
|
||||
#. Translators: when "Respond to Feedback" is clicked, a survey
|
||||
#. appears on which a user can respond to the feedback the user
|
||||
#. received on an openended problem
|
||||
#: lms/templates/combinedopenended/open_ended_result_table.html
|
||||
#: lms/templates/combinedopenended/openended/open_ended_evaluation.html
|
||||
msgid "Respond to Feedback"
|
||||
@@ -6137,6 +6163,10 @@ msgstr "DätäDümp #"
|
||||
msgid "Manage Groups"
|
||||
msgstr "Mänägé Gröüps Ⱡ'#"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
msgid "Metrics"
|
||||
msgstr "Métrïçs #"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
msgid "Grade Downloads"
|
||||
msgstr "Grädé Döwnlöäds Ⱡ'#"
|
||||
@@ -6320,6 +6350,8 @@ msgid "Pull enrollment from remote gradebook"
|
||||
msgstr "Püll énröllmént fröm rémöté grädéßöök Ⱡ'σяєм ιρѕ#"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid "Section:"
|
||||
msgstr "Séçtïön: #"
|
||||
|
||||
@@ -6485,6 +6517,40 @@ msgstr "Mäx Ⱡ'σя#"
|
||||
msgid "Points Earned (Num Students)"
|
||||
msgstr "Pöïnts Éärnéd (Nüm Stüdénts) Ⱡ'σяєм #"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid "There is no data available to display at this time."
|
||||
msgstr "Théré ïs nö dätä äväïläßlé tö dïspläý ät thïs tïmé. Ⱡ'σяєм ιρѕυм ∂#"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid ""
|
||||
"Loading the latest graphs for you; depending on your class size, this may "
|
||||
"take a few minutes."
|
||||
msgstr ""
|
||||
"Löädïng thé lätést gräphs för ýöü; dépéndïng ön ýöür çläss sïzé, thïs mäý "
|
||||
"täké ä féw mïnütés. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
msgid "Count of Students that Opened a Subsection"
|
||||
msgstr "Çöünt öf Stüdénts thät Öpénéd ä Süßséçtïön Ⱡ'σяєм ιρѕυ#"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid "Loading..."
|
||||
msgstr "Löädïng... Ⱡ#"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid "Grade Distribution per Problem"
|
||||
msgstr "Grädé Dïstrïßütïön pér Prößlém Ⱡ'σяєм #"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid "There are no problems in this section."
|
||||
msgstr "Théré äré nö prößléms ïn thïs séçtïön. Ⱡ'σяєм ιρѕ#"
|
||||
|
||||
#: lms/templates/courseware/instructor_dashboard.html
|
||||
msgid "Students answering correctly"
|
||||
msgstr "Stüdénts änswérïng çörréçtlý Ⱡ'σяєм #"
|
||||
@@ -6782,12 +6848,10 @@ msgstr "Vïéw Àrçhïvéd Çöürsé Ⱡ'σя#"
|
||||
msgid "View Course"
|
||||
msgstr "Vïéw Çöürsé Ⱡ#"
|
||||
|
||||
#. Translators: The course's name will be added to the end of this sentence.
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid "Are you sure you want to unregister from"
|
||||
msgstr "Àré ýöü süré ýöü wänt tö ünrégïstér fröm Ⱡ'σяєм ιρѕυ#"
|
||||
|
||||
#. Translators: The course's name will be added to the end of this sentence.
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
#: lms/templates/dashboard/_dashboard_course_listing.html
|
||||
msgid ""
|
||||
@@ -7850,6 +7914,14 @@ msgstr ""
|
||||
"çöhörts. Théïr pösts äré märkéd 'Çömmünïtý TÀ'. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,"
|
||||
" ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι#"
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid "Reload Graphs"
|
||||
msgstr "Rélöäd Gräphs Ⱡ'#"
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
|
||||
msgid "Count of Students Opened a Subsection"
|
||||
msgstr "Çöünt öf Stüdénts Öpénéd ä Süßséçtïön Ⱡ'σяєм ιρѕ#"
|
||||
|
||||
#: lms/templates/instructor/instructor_dashboard_2/send_email.html
|
||||
#: lms/templates/instructor/instructor_dashboard_2/send_email.html
|
||||
msgid "Send Email"
|
||||
@@ -8624,6 +8696,27 @@ msgstr ""
|
||||
"héré för pössïßlé üsé ßý ïnställätïöns öf Öpén édX. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт "
|
||||
"αмєт, ¢σηѕє¢тєтυ#"
|
||||
|
||||
#: lms/templates/static_templates/embargo.html
|
||||
msgid "This Course Unavailable In Your Country"
|
||||
msgstr "Thïs Çöürsé Ûnäväïläßlé Ìn Ýöür Çöüntrý Ⱡ'σяєм ιρѕ#"
|
||||
|
||||
#: lms/templates/static_templates/embargo.html
|
||||
msgid ""
|
||||
"Our system indicates that you are trying to access an edX course from an IP "
|
||||
"address associated with a country currently subjected to U.S. economic and "
|
||||
"trade sanctions. Unfortunately, at this time edX must comply with export "
|
||||
"controls, and we cannot allow you to access this particular course. Feel "
|
||||
"free to browse our catalogue to find other courses you may be interested in "
|
||||
"taking."
|
||||
msgstr ""
|
||||
"Öür sýstém ïndïçätés thät ýöü äré trýïng tö äççéss än édX çöürsé fröm än ÌP "
|
||||
"äddréss ässöçïätéd wïth ä çöüntrý çürréntlý süßjéçtéd tö Û.S. éçönömïç änd "
|
||||
"trädé sänçtïöns. Ûnförtünätélý, ät thïs tïmé édX müst çömplý wïth éxpört "
|
||||
"çöntröls, änd wé çännöt ällöw ýöü tö äççéss thïs pärtïçülär çöürsé. Féél "
|
||||
"fréé tö ßröwsé öür çätälögüé tö fïnd öthér çöürsés ýöü mäý ßé ïntéréstéd ïn "
|
||||
"täkïng. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ "
|
||||
"єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυ#"
|
||||
|
||||
#: lms/templates/static_templates/honor.html
|
||||
#: lms/templates/static_templates/honor.html
|
||||
msgid "Honor Code"
|
||||
@@ -9721,10 +9814,6 @@ msgstr ""
|
||||
msgid "You have decided to pay $ "
|
||||
msgstr "Ýöü hävé déçïdéd tö päý $ Ⱡ'σяєм#"
|
||||
|
||||
#: lms/templates/wiki/includes/article_menu.html
|
||||
msgid "View"
|
||||
msgstr "Vïéw Ⱡ'σяєм#"
|
||||
|
||||
#: lms/templates/wiki/includes/article_menu.html
|
||||
#: lms/templates/wiki/includes/article_menu.html
|
||||
#: lms/templates/wiki/includes/article_menu.html
|
||||
@@ -9872,10 +9961,10 @@ msgstr "Löäd Ànöthér Fïlé Ⱡ'σ#"
|
||||
msgid "Content"
|
||||
msgstr "Çöntént #"
|
||||
|
||||
#: cms/templates/asset_index.html cms/templates/course_info.html
|
||||
#: cms/templates/edit-tabs.html cms/templates/index.html
|
||||
#: cms/templates/manage_users.html cms/templates/overview.html
|
||||
#: cms/templates/textbooks.html
|
||||
#: cms/templates/asset_index.html cms/templates/container.html
|
||||
#: cms/templates/course_info.html cms/templates/edit-tabs.html
|
||||
#: cms/templates/index.html cms/templates/manage_users.html
|
||||
#: cms/templates/overview.html cms/templates/textbooks.html
|
||||
msgid "Page Actions"
|
||||
msgstr "Pägé Àçtïöns Ⱡ#"
|
||||
|
||||
@@ -9916,8 +10005,8 @@ msgstr ""
|
||||
"öf ýöür çöürsé. Dö nöt üsé thé Éxtérnäl ÛRL äs ä lïnk välüé wïthïn ýöür "
|
||||
"çöürsé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι#"
|
||||
|
||||
#: cms/templates/asset_index.html cms/templates/overview.html
|
||||
#: cms/templates/settings_graders.html
|
||||
#: cms/templates/asset_index.html cms/templates/container.html
|
||||
#: cms/templates/overview.html cms/templates/settings_graders.html
|
||||
msgid "What can I do on this page?"
|
||||
msgstr "Whät çän Ì dö ön thïs pägé? Ⱡ'σяєм#"
|
||||
|
||||
@@ -9979,23 +10068,43 @@ msgstr ""
|
||||
msgid "Editor"
|
||||
msgstr "Édïtör Ⱡ'σяєм ιρѕ#"
|
||||
|
||||
#: cms/templates/component.html
|
||||
#: cms/templates/component.html cms/templates/studio_xblock_wrapper.html
|
||||
msgid "Duplicate"
|
||||
msgstr "Düplïçäté #"
|
||||
|
||||
#: cms/templates/component.html
|
||||
#: cms/templates/component.html cms/templates/studio_xblock_wrapper.html
|
||||
msgid "Duplicate this component"
|
||||
msgstr "Düplïçäté thïs çömpönént Ⱡ'σяє#"
|
||||
|
||||
#: cms/templates/component.html
|
||||
#: cms/templates/component.html cms/templates/studio_xblock_wrapper.html
|
||||
msgid "Delete this component"
|
||||
msgstr "Délété thïs çömpönént Ⱡ'σя#"
|
||||
|
||||
#: cms/templates/component.html cms/templates/overview.html
|
||||
#: cms/templates/overview.html
|
||||
#: cms/templates/unit_container_xblock_component.html
|
||||
msgid "Drag to reorder"
|
||||
msgstr "Dräg tö réördér Ⱡ'#"
|
||||
|
||||
#: cms/templates/container.html cms/templates/ux/reference/container.html
|
||||
msgid "Container"
|
||||
msgstr "Çöntäïnér #"
|
||||
|
||||
#: cms/templates/container.html cms/templates/studio_vertical_wrapper.html
|
||||
msgid "No Actions"
|
||||
msgstr "Nö Àçtïöns Ⱡ#"
|
||||
|
||||
#: cms/templates/container.html
|
||||
msgid ""
|
||||
"You can view course components that contain other components on this page. "
|
||||
"In the case of experiment blocks, this allows you to confirm that you have "
|
||||
"properly configured your experiment groups."
|
||||
msgstr ""
|
||||
"Ýöü çän vïéw çöürsé çömpönénts thät çöntäïn öthér çömpönénts ön thïs pägé. "
|
||||
"Ìn thé çäsé öf éxpérïmént ßlöçks, thïs ällöws ýöü tö çönfïrm thät ýöü hävé "
|
||||
"pröpérlý çönfïgüréd ýöür éxpérïmént gröüps. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
|
||||
"¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє#"
|
||||
|
||||
#: cms/templates/course_info.html cms/templates/course_info.html
|
||||
msgid "Course Updates"
|
||||
msgstr "Çöürsé Ûpdätés Ⱡ'#"
|
||||
@@ -10205,7 +10314,6 @@ msgstr "Çöürsé Éxpört Ⱡ'#"
|
||||
msgid "About Exporting Courses"
|
||||
msgstr "Àßöüt Éxpörtïng Çöürsés Ⱡ'σяє#"
|
||||
|
||||
#. Translators: ".tar.gz" is a file extension, and should not be translated
|
||||
#: cms/templates/export.html
|
||||
msgid ""
|
||||
"You can export courses and edit them outside of Studio. The exported file is"
|
||||
@@ -10310,7 +10418,6 @@ msgstr ""
|
||||
msgid "Opening the downloaded file"
|
||||
msgstr "Öpénïng thé döwnlöädéd fïlé Ⱡ'σяєм#"
|
||||
|
||||
#. Translators: ".tar.gz" is a file extension, and should not be translated
|
||||
#: cms/templates/export.html
|
||||
msgid ""
|
||||
"Use an archive program to extract the data from the .tar.gz file. Extracted "
|
||||
@@ -10646,8 +10753,6 @@ msgstr ""
|
||||
"çürrént çöürsé, sö ýöü hävé ä ßäçküp çöpý öf ït. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт "
|
||||
"αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя #"
|
||||
|
||||
#. Translators: ".tar.gz" is a file extension, and files with that extension
|
||||
#. are called "gzipped tar files": these terms should not be translated
|
||||
#: cms/templates/import.html
|
||||
msgid ""
|
||||
"The course that you import must be in a .tar.gz file (that is, a .tar file "
|
||||
@@ -10672,8 +10777,6 @@ msgstr ""
|
||||
"ýöür çöürsé üntïl thé ïmpört öpérätïön häs çömplétéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт"
|
||||
" αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂#"
|
||||
|
||||
#. Translators: ".tar.gz" is a file extension, and files with that extension
|
||||
#. are called "gzipped tar files": these terms should not be translated
|
||||
#: cms/templates/import.html
|
||||
msgid "Select a .tar.gz File to Replace Your Course Content"
|
||||
msgstr "Séléçt ä .tär.gz Fïlé tö Répläçé Ýöür Çöürsé Çöntént Ⱡ'σяєм ιρѕυм ∂σ#"
|
||||
@@ -11919,6 +12022,11 @@ msgstr ""
|
||||
"éxäms, änd spéçïfý höw müçh öf ä stüdént's grädé éäçh ässïgnmént týpé ïs "
|
||||
"wörth. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι#"
|
||||
|
||||
#: cms/templates/studio_vertical_wrapper.html
|
||||
#: cms/templates/studio_vertical_wrapper.html
|
||||
msgid "Expand or Collapse"
|
||||
msgstr "Éxpänd ör Çölläpsé Ⱡ'σ#"
|
||||
|
||||
#: cms/templates/textbooks.html cms/templates/textbooks.html
|
||||
#: cms/templates/widgets/header.html
|
||||
msgid "Textbooks"
|
||||
@@ -12137,10 +12245,6 @@ msgstr ""
|
||||
"Àn äçtïvätïön lïnk häs ßéén sént tö {email}, älöng wïth ïnstrüçtïöns för "
|
||||
"äçtïvätïng ýöür äççöünt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
|
||||
|
||||
#: cms/templates/ux/reference/container.html
|
||||
msgid "Container"
|
||||
msgstr "Çöntäïnér #"
|
||||
|
||||
#: cms/templates/widgets/footer.html
|
||||
msgid "All rights reserved."
|
||||
msgstr "Àll rïghts résérvéd. Ⱡ'σя#"
|
||||
|
||||
Binary file not shown.
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.1a\n"
|
||||
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
|
||||
"POT-Creation-Date: 2014-02-27 08:56-0500\n"
|
||||
"PO-Revision-Date: 2014-02-27 13:57:20.650962\n"
|
||||
"POT-Creation-Date: 2014-02-28 13:56-0800\n"
|
||||
"PO-Revision-Date: 2014-02-28 21:57:20.936431\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -1257,6 +1257,15 @@ msgstr "Lïst ïtém #"
|
||||
msgid "Heading"
|
||||
msgstr "Héädïng #"
|
||||
|
||||
#: lms/templates/class_dashboard/all_section_metrics.js
|
||||
#: lms/templates/class_dashboard/all_section_metrics.js
|
||||
msgid "Unable to retrieve data, please try again later."
|
||||
msgstr "Ûnäßlé tö rétrïévé dätä, pléäsé trý ägäïn lätér. Ⱡ'σяєм ιρѕυм #"
|
||||
|
||||
#: lms/templates/class_dashboard/d3_stacked_bar_graph.js
|
||||
msgid "Number of Students"
|
||||
msgstr "Nümßér öf Stüdénts Ⱡ'σ#"
|
||||
|
||||
#: cms/static/coffee/src/main.js
|
||||
msgid ""
|
||||
"This may be happening because of an error with our server or your internet "
|
||||
@@ -1276,6 +1285,7 @@ msgstr "<em>Édïtïng:</em> %s Ⱡ'σ#"
|
||||
|
||||
#: cms/static/coffee/src/views/module_edit.js
|
||||
#: cms/static/coffee/src/views/tabs.js cms/static/coffee/src/views/unit.js
|
||||
#: cms/static/coffee/src/xblock/cms.runtime.v1.js
|
||||
#: cms/static/js/models/section.js cms/static/js/views/asset.js
|
||||
#: cms/static/js/views/course_info_handout.js
|
||||
#: cms/static/js/views/course_info_update.js cms/static/js/views/overview.js
|
||||
|
||||
3
lms/djangoapps/class_dashboard/__init__.py
Normal file
3
lms/djangoapps/class_dashboard/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
init.py file for class_dashboard
|
||||
"""
|
||||
401
lms/djangoapps/class_dashboard/dashboard_data.py
Normal file
401
lms/djangoapps/class_dashboard/dashboard_data.py
Normal file
@@ -0,0 +1,401 @@
|
||||
"""
|
||||
Computes the data to display on the Instructor Dashboard
|
||||
"""
|
||||
|
||||
from courseware import models
|
||||
from django.db.models import Count
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
|
||||
|
||||
def get_problem_grade_distribution(course_id):
|
||||
"""
|
||||
Returns the grade distribution per problem for the course
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Output is a dict, where the key is the problem 'module_id' and the value is a dict with:
|
||||
'max_grade' - max grade for this problem
|
||||
'grade_distrib' - array of tuples (`grade`,`count`).
|
||||
"""
|
||||
|
||||
# Aggregate query on studentmodule table for grade data for all problems in course
|
||||
db_query = models.StudentModule.objects.filter(
|
||||
course_id__exact=course_id,
|
||||
grade__isnull=False,
|
||||
module_type__exact="problem",
|
||||
).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade'))
|
||||
|
||||
prob_grade_distrib = {}
|
||||
|
||||
# Loop through resultset building data for each problem
|
||||
for row in db_query:
|
||||
curr_problem = row['module_state_key']
|
||||
|
||||
# Build set of grade distributions for each problem that has student responses
|
||||
if curr_problem in prob_grade_distrib:
|
||||
prob_grade_distrib[curr_problem]['grade_distrib'].append((row['grade'], row['count_grade']))
|
||||
|
||||
if (prob_grade_distrib[curr_problem]['max_grade'] != row['max_grade']) and \
|
||||
(prob_grade_distrib[curr_problem]['max_grade'] < row['max_grade']):
|
||||
prob_grade_distrib[curr_problem]['max_grade'] = row['max_grade']
|
||||
|
||||
else:
|
||||
prob_grade_distrib[curr_problem] = {
|
||||
'max_grade': row['max_grade'],
|
||||
'grade_distrib': [(row['grade'], row['count_grade'])]
|
||||
}
|
||||
|
||||
return prob_grade_distrib
|
||||
|
||||
|
||||
def get_sequential_open_distrib(course_id):
|
||||
"""
|
||||
Returns the number of students that opened each subsection/sequential of the course
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Outputs a dict mapping the 'module_id' to the number of students that have opened that subsection/sequential.
|
||||
"""
|
||||
|
||||
# Aggregate query on studentmodule table for "opening a subsection" data
|
||||
db_query = models.StudentModule.objects.filter(
|
||||
course_id__exact=course_id,
|
||||
module_type__exact="sequential",
|
||||
).values('module_state_key').annotate(count_sequential=Count('module_state_key'))
|
||||
|
||||
# Build set of "opened" data for each subsection that has "opened" data
|
||||
sequential_open_distrib = {}
|
||||
for row in db_query:
|
||||
sequential_open_distrib[row['module_state_key']] = row['count_sequential']
|
||||
|
||||
return sequential_open_distrib
|
||||
|
||||
|
||||
def get_problem_set_grade_distrib(course_id, problem_set):
|
||||
"""
|
||||
Returns the grade distribution for the problems specified in `problem_set`.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
`problem_set` an array of strings representing problem module_id's.
|
||||
|
||||
Requests from the database the a count of each grade for each problem in the `problem_set`.
|
||||
|
||||
Returns a dict, where the key is the problem 'module_id' and the value is a dict with two parts:
|
||||
'max_grade' - the maximum grade possible for the course
|
||||
'grade_distrib' - array of tuples (`grade`,`count`) ordered by `grade`
|
||||
"""
|
||||
|
||||
# Aggregate query on studentmodule table for grade data for set of problems in course
|
||||
db_query = models.StudentModule.objects.filter(
|
||||
course_id__exact=course_id,
|
||||
grade__isnull=False,
|
||||
module_type__exact="problem",
|
||||
module_state_key__in=problem_set,
|
||||
).values(
|
||||
'module_state_key',
|
||||
'grade',
|
||||
'max_grade',
|
||||
).annotate(count_grade=Count('grade')).order_by('module_state_key', 'grade')
|
||||
|
||||
prob_grade_distrib = {}
|
||||
|
||||
# Loop through resultset building data for each problem
|
||||
for row in db_query:
|
||||
if row['module_state_key'] not in prob_grade_distrib:
|
||||
prob_grade_distrib[row['module_state_key']] = {
|
||||
'max_grade': 0,
|
||||
'grade_distrib': [],
|
||||
}
|
||||
|
||||
curr_grade_distrib = prob_grade_distrib[row['module_state_key']]
|
||||
curr_grade_distrib['grade_distrib'].append((row['grade'], row['count_grade']))
|
||||
|
||||
if curr_grade_distrib['max_grade'] < row['max_grade']:
|
||||
curr_grade_distrib['max_grade'] = row['max_grade']
|
||||
|
||||
return prob_grade_distrib
|
||||
|
||||
|
||||
def get_d3_problem_grade_distrib(course_id):
|
||||
"""
|
||||
Returns problem grade distribution information for each section, data already in format for d3 function.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Returns an array of dicts in the order of the sections. Each dict has:
|
||||
'display_name' - display name for the section
|
||||
'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem
|
||||
"""
|
||||
|
||||
prob_grade_distrib = get_problem_grade_distribution(course_id)
|
||||
d3_data = []
|
||||
|
||||
# Retrieve course object down to problems
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
|
||||
# Iterate through sections, subsections, units, problems
|
||||
for section in course.get_children():
|
||||
curr_section = {}
|
||||
curr_section['display_name'] = own_metadata(section).get('display_name', '')
|
||||
data = []
|
||||
c_subsection = 0
|
||||
for subsection in section.get_children():
|
||||
c_subsection += 1
|
||||
c_unit = 0
|
||||
for unit in subsection.get_children():
|
||||
c_unit += 1
|
||||
c_problem = 0
|
||||
for child in unit.get_children():
|
||||
|
||||
# Student data is at the problem level
|
||||
if child.location.category == 'problem':
|
||||
c_problem += 1
|
||||
stack_data = []
|
||||
|
||||
# Construct label to display for this problem
|
||||
label = "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem)
|
||||
|
||||
# Only problems in prob_grade_distrib have had a student submission.
|
||||
if child.location.url() in prob_grade_distrib:
|
||||
|
||||
# Get max_grade, grade_distribution for this problem
|
||||
problem_info = prob_grade_distrib[child.location.url()]
|
||||
|
||||
# Get problem_name for tooltip
|
||||
problem_name = own_metadata(child).get('display_name', '')
|
||||
|
||||
# Compute percent of this grade over max_grade
|
||||
max_grade = float(problem_info['max_grade'])
|
||||
for (grade, count_grade) in problem_info['grade_distrib']:
|
||||
percent = 0.0
|
||||
if max_grade > 0:
|
||||
percent = (grade * 100.0) / max_grade
|
||||
|
||||
# Construct tooltip for problem in grade distibution view
|
||||
tooltip = _("{label} {problem_name} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format(
|
||||
label=label,
|
||||
problem_name=problem_name,
|
||||
count_grade=count_grade,
|
||||
students=_("students"),
|
||||
percent=percent,
|
||||
grade=grade,
|
||||
max_grade=max_grade,
|
||||
questions=_("questions"),
|
||||
)
|
||||
|
||||
# Construct data to be sent to d3
|
||||
stack_data.append({
|
||||
'color': percent,
|
||||
'value': count_grade,
|
||||
'tooltip': tooltip,
|
||||
})
|
||||
|
||||
problem = {
|
||||
'xValue': label,
|
||||
'stackData': stack_data,
|
||||
}
|
||||
data.append(problem)
|
||||
curr_section['data'] = data
|
||||
|
||||
d3_data.append(curr_section)
|
||||
|
||||
return d3_data
|
||||
|
||||
|
||||
def get_d3_sequential_open_distrib(course_id):
|
||||
"""
|
||||
Returns how many students opened a sequential/subsection for each section, data already in format for d3 function.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Returns an array in the order of the sections and each dict has:
|
||||
'display_name' - display name for the section
|
||||
'data' - data for the d3_stacked_bar_graph function of how many students opened each sequential/subsection
|
||||
"""
|
||||
sequential_open_distrib = get_sequential_open_distrib(course_id)
|
||||
|
||||
d3_data = []
|
||||
|
||||
# Retrieve course object down to subsection
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
|
||||
|
||||
# Iterate through sections, subsections
|
||||
for section in course.get_children():
|
||||
curr_section = {}
|
||||
curr_section['display_name'] = own_metadata(section).get('display_name', '')
|
||||
data = []
|
||||
c_subsection = 0
|
||||
|
||||
# Construct data for each subsection to be sent to d3
|
||||
for subsection in section.get_children():
|
||||
c_subsection += 1
|
||||
subsection_name = own_metadata(subsection).get('display_name', '')
|
||||
|
||||
num_students = 0
|
||||
if subsection.location.url() in sequential_open_distrib:
|
||||
num_students = sequential_open_distrib[subsection.location.url()]
|
||||
|
||||
stack_data = []
|
||||
tooltip = _("{num_students} student(s) opened Subsection {subsection_num}: {subsection_name}").format(
|
||||
num_students=num_students,
|
||||
subsection_num=c_subsection,
|
||||
subsection_name=subsection_name,
|
||||
)
|
||||
|
||||
stack_data.append({
|
||||
'color': 0,
|
||||
'value': num_students,
|
||||
'tooltip': tooltip,
|
||||
})
|
||||
subsection = {
|
||||
'xValue': "SS {0}".format(c_subsection),
|
||||
'stackData': stack_data,
|
||||
}
|
||||
data.append(subsection)
|
||||
|
||||
curr_section['data'] = data
|
||||
d3_data.append(curr_section)
|
||||
|
||||
return d3_data
|
||||
|
||||
|
||||
def get_d3_section_grade_distrib(course_id, section):
|
||||
"""
|
||||
Returns the grade distribution for the problems in the `section` section in a format for the d3 code.
|
||||
|
||||
`course_id` a string that is the course's ID.
|
||||
|
||||
`section` an int that is a zero-based index into the course's list of sections.
|
||||
|
||||
Navigates to the section specified to find all the problems associated with that section and then finds the grade
|
||||
distribution for those problems. Finally returns an object formated the way the d3_stacked_bar_graph.js expects its
|
||||
data object to be in.
|
||||
|
||||
If this is requested multiple times quickly for the same course, it is better to call
|
||||
get_d3_problem_grade_distrib and pick out the sections of interest.
|
||||
|
||||
Returns an array of dicts with the following keys (taken from d3_stacked_bar_graph.js's documentation)
|
||||
'xValue' - Corresponding value for the x-axis
|
||||
'stackData' - Array of objects with key, value pairs that represent a bar:
|
||||
'color' - Defines what "color" the bar will map to
|
||||
'value' - Maps to the height of the bar, along the y-axis
|
||||
'tooltip' - (Optional) Text to display on mouse hover
|
||||
"""
|
||||
|
||||
# Retrieve course object down to problems
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
|
||||
problem_set = []
|
||||
problem_info = {}
|
||||
c_subsection = 0
|
||||
for subsection in course.get_children()[section].get_children():
|
||||
c_subsection += 1
|
||||
c_unit = 0
|
||||
for unit in subsection.get_children():
|
||||
c_unit += 1
|
||||
c_problem = 0
|
||||
for child in unit.get_children():
|
||||
if (child.location.category == 'problem'):
|
||||
c_problem += 1
|
||||
problem_set.append(child.location.url())
|
||||
problem_info[child.location.url()] = {
|
||||
'id': child.location.url(),
|
||||
'x_value': "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem),
|
||||
'display_name': own_metadata(child).get('display_name', ''),
|
||||
}
|
||||
|
||||
# Retrieve grade distribution for these problems
|
||||
grade_distrib = get_problem_set_grade_distrib(course_id, problem_set)
|
||||
|
||||
d3_data = []
|
||||
|
||||
# Construct data for each problem to be sent to d3
|
||||
for problem in problem_set:
|
||||
stack_data = []
|
||||
|
||||
if problem in grade_distrib: # Some problems have no data because students have not tried them yet.
|
||||
max_grade = float(grade_distrib[problem]['max_grade'])
|
||||
for (grade, count_grade) in grade_distrib[problem]['grade_distrib']:
|
||||
percent = 0.0
|
||||
if max_grade > 0:
|
||||
percent = (grade * 100.0) / max_grade
|
||||
|
||||
# Construct tooltip for problem in grade distibution view
|
||||
tooltip = _("{problem_info_x} {problem_info_n} - {count_grade} {students} ({percent:.0f}%: {grade:.0f}/{max_grade:.0f} {questions})").format(
|
||||
problem_info_x=problem_info[problem]['x_value'],
|
||||
count_grade=count_grade,
|
||||
students=_("students"),
|
||||
percent=percent,
|
||||
problem_info_n=problem_info[problem]['display_name'],
|
||||
grade=grade,
|
||||
max_grade=max_grade,
|
||||
questions=_("questions"),
|
||||
)
|
||||
|
||||
stack_data.append({
|
||||
'color': percent,
|
||||
'value': count_grade,
|
||||
'tooltip': tooltip,
|
||||
})
|
||||
|
||||
d3_data.append({
|
||||
'xValue': problem_info[problem]['x_value'],
|
||||
'stackData': stack_data,
|
||||
})
|
||||
|
||||
return d3_data
|
||||
|
||||
|
||||
def get_section_display_name(course_id):
|
||||
"""
|
||||
Returns an array of the display names for each section in the course.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
The ith string in the array is the display name of the ith section in the course.
|
||||
"""
|
||||
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
|
||||
section_display_name = [""] * len(course.get_children())
|
||||
i = 0
|
||||
for section in course.get_children():
|
||||
section_display_name[i] = own_metadata(section).get('display_name', '')
|
||||
i += 1
|
||||
|
||||
return section_display_name
|
||||
|
||||
|
||||
def get_array_section_has_problem(course_id):
|
||||
"""
|
||||
Returns an array of true/false whether each section has problems.
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
The ith value in the array is true if the ith section in the course contains problems and false otherwise.
|
||||
"""
|
||||
|
||||
course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=4)
|
||||
|
||||
b_section_has_problem = [False] * len(course.get_children())
|
||||
i = 0
|
||||
for section in course.get_children():
|
||||
for subsection in section.get_children():
|
||||
for unit in subsection.get_children():
|
||||
for child in unit.get_children():
|
||||
if child.location.category == 'problem':
|
||||
b_section_has_problem[i] = True
|
||||
break # out of child loop
|
||||
if b_section_has_problem[i]:
|
||||
break # out of unit loop
|
||||
if b_section_has_problem[i]:
|
||||
break # out of subsection loop
|
||||
|
||||
i += 1
|
||||
|
||||
return b_section_has_problem
|
||||
180
lms/djangoapps/class_dashboard/test/test_dashboard_data.py
Normal file
180
lms/djangoapps/class_dashboard/test/test_dashboard_data.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Tests for class dashboard (Metrics tab in instructor dashboard)
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from courseware.tests.factories import StudentModuleFactory
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory
|
||||
from capa.tests.response_xml_factory import StringResponseXMLFactory
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from class_dashboard.dashboard_data import (get_problem_grade_distribution, get_sequential_open_distrib,
|
||||
get_problem_set_grade_distrib, get_d3_problem_grade_distrib,
|
||||
get_d3_sequential_open_distrib, get_d3_section_grade_distrib,
|
||||
get_section_display_name, get_array_section_has_problem
|
||||
)
|
||||
from class_dashboard.views import has_instructor_access_for_class
|
||||
|
||||
USER_COUNT = 11
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
class TestGetProblemGradeDistribution(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests related to class_dashboard/dashboard_data.py
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.instructor = AdminFactory.create()
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
self.attempts = 3
|
||||
self.course = CourseFactory.create(
|
||||
display_name=u"test course omega \u03a9",
|
||||
)
|
||||
|
||||
section = ItemFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="chapter",
|
||||
display_name=u"test factory section omega \u03a9",
|
||||
)
|
||||
sub_section = ItemFactory.create(
|
||||
parent_location=section.location,
|
||||
category="sequential",
|
||||
display_name=u"test subsection omega \u03a9",
|
||||
)
|
||||
|
||||
unit = ItemFactory.create(
|
||||
parent_location=sub_section.location,
|
||||
category="vertical",
|
||||
metadata={'graded': True, 'format': 'Homework'},
|
||||
display_name=u"test unit omega \u03a9",
|
||||
)
|
||||
|
||||
self.users = [UserFactory.create() for _ in xrange(USER_COUNT)]
|
||||
|
||||
for user in self.users:
|
||||
CourseEnrollmentFactory.create(user=user, course_id=self.course.id)
|
||||
|
||||
for i in xrange(USER_COUNT - 1):
|
||||
category = "problem"
|
||||
item = ItemFactory.create(
|
||||
parent_location=unit.location,
|
||||
category=category,
|
||||
data=StringResponseXMLFactory().build_xml(answer='foo'),
|
||||
metadata={'rerandomize': 'always'},
|
||||
display_name=u"test problem omega \u03a9 " + str(i)
|
||||
)
|
||||
|
||||
for j, user in enumerate(self.users):
|
||||
StudentModuleFactory.create(
|
||||
grade=1 if i < j else 0,
|
||||
max_grade=1 if i < j else 0.5,
|
||||
student=user,
|
||||
course_id=self.course.id,
|
||||
module_state_key=Location(item.location).url(),
|
||||
state=json.dumps({'attempts': self.attempts}),
|
||||
)
|
||||
|
||||
for j, user in enumerate(self.users):
|
||||
StudentModuleFactory.create(
|
||||
course_id=self.course.id,
|
||||
module_type='sequential',
|
||||
module_state_key=Location(item.location).url(),
|
||||
)
|
||||
|
||||
def test_get_problem_grade_distribution(self):
|
||||
|
||||
prob_grade_distrib = get_problem_grade_distribution(self.course.id)
|
||||
|
||||
for problem in prob_grade_distrib:
|
||||
max_grade = prob_grade_distrib[problem]['max_grade']
|
||||
self.assertEquals(1, max_grade)
|
||||
|
||||
def test_get_sequential_open_distibution(self):
|
||||
|
||||
sequential_open_distrib = get_sequential_open_distrib(self.course.id)
|
||||
|
||||
for problem in sequential_open_distrib:
|
||||
num_students = sequential_open_distrib[problem]
|
||||
self.assertEquals(USER_COUNT, num_students)
|
||||
|
||||
def test_get_problemset_grade_distrib(self):
|
||||
|
||||
prob_grade_distrib = get_problem_grade_distribution(self.course.id)
|
||||
probset_grade_distrib = get_problem_set_grade_distrib(self.course.id, prob_grade_distrib)
|
||||
|
||||
for problem in probset_grade_distrib:
|
||||
max_grade = probset_grade_distrib[problem]['max_grade']
|
||||
self.assertEquals(1, max_grade)
|
||||
|
||||
grade_distrib = probset_grade_distrib[problem]['grade_distrib']
|
||||
sum_attempts = 0
|
||||
for item in grade_distrib:
|
||||
sum_attempts += item[1]
|
||||
self.assertEquals(USER_COUNT, sum_attempts)
|
||||
|
||||
def test_get_d3_problem_grade_distrib(self):
|
||||
|
||||
d3_data = get_d3_problem_grade_distrib(self.course.id)
|
||||
for data in d3_data:
|
||||
for stack_data in data['data']:
|
||||
sum_values = 0
|
||||
for problem in stack_data['stackData']:
|
||||
sum_values += problem['value']
|
||||
self.assertEquals(USER_COUNT, sum_values)
|
||||
|
||||
def test_get_d3_sequential_open_distrib(self):
|
||||
|
||||
d3_data = get_d3_sequential_open_distrib(self.course.id)
|
||||
|
||||
for data in d3_data:
|
||||
for stack_data in data['data']:
|
||||
for problem in stack_data['stackData']:
|
||||
value = problem['value']
|
||||
self.assertEquals(0, value)
|
||||
|
||||
def test_get_d3_section_grade_distrib(self):
|
||||
|
||||
d3_data = get_d3_section_grade_distrib(self.course.id, 0)
|
||||
|
||||
for stack_data in d3_data:
|
||||
sum_values = 0
|
||||
for problem in stack_data['stackData']:
|
||||
sum_values += problem['value']
|
||||
self.assertEquals(USER_COUNT, sum_values)
|
||||
|
||||
def test_get_section_display_name(self):
|
||||
|
||||
section_display_name = get_section_display_name(self.course.id)
|
||||
self.assertMultiLineEqual(section_display_name[0], u"test factory section omega \u03a9")
|
||||
|
||||
def test_get_array_section_has_problem(self):
|
||||
|
||||
b_section_has_problem = get_array_section_has_problem(self.course.id)
|
||||
self.assertEquals(b_section_has_problem[0], True)
|
||||
|
||||
def test_dashboard(self):
|
||||
|
||||
url = reverse('instructor_dashboard', kwargs={'course_id': self.course.id})
|
||||
response = self.client.post(
|
||||
url,
|
||||
{
|
||||
'idash_mode': 'Metrics'
|
||||
}
|
||||
)
|
||||
self.assertContains(response, '<h2>Course Statistics At A Glance</h2>')
|
||||
|
||||
def test_has_instructor_access_for_class(self):
|
||||
"""
|
||||
Test for instructor access
|
||||
"""
|
||||
ret_val = has_instructor_access_for_class(self.instructor, self.course.id)
|
||||
self.assertEquals(ret_val, True)
|
||||
83
lms/djangoapps/class_dashboard/test/test_views.py
Normal file
83
lms/djangoapps/class_dashboard/test/test_views.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Tests for class dashboard (Metrics tab in instructor dashboard)
|
||||
"""
|
||||
from mock import patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import simplejson
|
||||
|
||||
from class_dashboard import views
|
||||
|
||||
|
||||
class TestViews(TestCase):
|
||||
"""
|
||||
Tests related to class_dashboard/views.py
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
self.request_factory = RequestFactory()
|
||||
self.request = self.request_factory.get('')
|
||||
self.request.user = None
|
||||
self.simple_data = {'error': 'error'}
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_problem_grade_distribution_has_access(self, has_access):
|
||||
"""
|
||||
Test returns proper value when have proper access
|
||||
"""
|
||||
has_access.return_value = True
|
||||
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_problem_grade_distribution_no_access(self, has_access):
|
||||
"""
|
||||
Test for no access
|
||||
"""
|
||||
has_access.return_value = False
|
||||
response = views.all_problem_grade_distribution(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_sequential_open_distribution_has_access(self, has_access):
|
||||
"""
|
||||
Test returns proper value when have proper access
|
||||
"""
|
||||
has_access.return_value = True
|
||||
response = views.all_sequential_open_distrib(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_all_sequential_open_distribution_no_access(self, has_access):
|
||||
"""
|
||||
Test for no access
|
||||
"""
|
||||
has_access.return_value = False
|
||||
response = views.all_sequential_open_distrib(self.request, 'test/test/test')
|
||||
|
||||
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_section_problem_grade_distribution_has_access(self, has_access):
|
||||
"""
|
||||
Test returns proper value when have proper access
|
||||
"""
|
||||
has_access.return_value = True
|
||||
response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1')
|
||||
|
||||
self.assertEqual(simplejson.dumps(self.simple_data), response.content)
|
||||
|
||||
@patch('class_dashboard.views.has_instructor_access_for_class')
|
||||
def test_section_problem_grade_distribution_no_access(self, has_access):
|
||||
"""
|
||||
Test for no access
|
||||
"""
|
||||
has_access.return_value = False
|
||||
response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1')
|
||||
|
||||
self.assertEqual("{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", response.content)
|
||||
104
lms/djangoapps/class_dashboard/views.py
Normal file
104
lms/djangoapps/class_dashboard/views.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Handles requests for data, returning a json
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.utils import simplejson
|
||||
from django.http import HttpResponse
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from courseware.access import has_access
|
||||
from class_dashboard import dashboard_data
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def has_instructor_access_for_class(user, course_id):
|
||||
"""
|
||||
Returns true if the `user` is an instructor for the course.
|
||||
"""
|
||||
|
||||
course = get_course_with_access(user, course_id, 'staff', depth=None)
|
||||
return has_access(user, course, 'staff')
|
||||
|
||||
|
||||
def all_sequential_open_distrib(request, course_id):
|
||||
"""
|
||||
Creates a json with the open distribution for all the subsections in the course.
|
||||
|
||||
`request` django request
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Returns the format in dashboard_data.get_d3_sequential_open_distrib
|
||||
"""
|
||||
|
||||
json = {}
|
||||
|
||||
# Only instructor for this particular course can request this information
|
||||
if has_instructor_access_for_class(request.user, course_id):
|
||||
try:
|
||||
json = dashboard_data.get_d3_sequential_open_distrib(course_id)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.error('Generating metrics failed with exception: %s', ex)
|
||||
json = {'error': "error"}
|
||||
else:
|
||||
json = {'error': "Access Denied: User does not have access to this course's data"}
|
||||
|
||||
return HttpResponse(simplejson.dumps(json), mimetype="application/json")
|
||||
|
||||
|
||||
def all_problem_grade_distribution(request, course_id):
|
||||
"""
|
||||
Creates a json with the grade distribution for all the problems in the course.
|
||||
|
||||
`Request` django request
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
Returns the format in dashboard_data.get_d3_problem_grade_distrib
|
||||
"""
|
||||
json = {}
|
||||
|
||||
# Only instructor for this particular course can request this information
|
||||
if has_instructor_access_for_class(request.user, course_id):
|
||||
try:
|
||||
json = dashboard_data.get_d3_problem_grade_distrib(course_id)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.error('Generating metrics failed with exception: %s', ex)
|
||||
json = {'error': "error"}
|
||||
else:
|
||||
json = {'error': "Access Denied: User does not have access to this course's data"}
|
||||
|
||||
return HttpResponse(simplejson.dumps(json), mimetype="application/json")
|
||||
|
||||
|
||||
def section_problem_grade_distrib(request, course_id, section):
|
||||
"""
|
||||
Creates a json with the grade distribution for the problems in the specified section.
|
||||
|
||||
`request` django request
|
||||
|
||||
`course_id` the course ID for the course interested in
|
||||
|
||||
`section` The zero-based index of the section for the course
|
||||
|
||||
Returns the format in dashboard_data.get_d3_section_grade_distrib
|
||||
|
||||
If this is requested multiple times quickly for the same course, it is better to call all_problem_grade_distribution
|
||||
and pick out the sections of interest.
|
||||
"""
|
||||
json = {}
|
||||
|
||||
# Only instructor for this particular course can request this information
|
||||
if has_instructor_access_for_class(request.user, course_id):
|
||||
try:
|
||||
json = dashboard_data.get_d3_section_grade_distrib(course_id, section)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
log.error('Generating metrics failed with exception: %s', ex)
|
||||
json = {'error': "error"}
|
||||
else:
|
||||
json = {'error': "Access Denied: User does not have access to this course's data"}
|
||||
|
||||
return HttpResponse(simplejson.dumps(json), mimetype="application/json")
|
||||
@@ -23,7 +23,7 @@ from django_comment_client.utils import has_forum_access
|
||||
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
|
||||
from student.models import CourseEnrollment
|
||||
from bulk_email.models import CourseAuthorization
|
||||
|
||||
from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem
|
||||
|
||||
from .tools import get_units_with_due_date, title_or_url
|
||||
|
||||
@@ -31,7 +31,7 @@ from .tools import get_units_with_due_date, title_or_url
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def instructor_dashboard_2(request, course_id):
|
||||
"""Display the instructor dashboard for a course."""
|
||||
""" Display the instructor dashboard for a course. """
|
||||
|
||||
course = get_course_by_id(course_id, depth=None)
|
||||
is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE)
|
||||
@@ -64,6 +64,10 @@ def instructor_dashboard_2(request, course_id):
|
||||
is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
|
||||
sections.append(_section_send_email(course_id, access, course))
|
||||
|
||||
# Gate access to Metrics tab by featue flag and staff authorization
|
||||
if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']:
|
||||
sections.append(_section_metrics(course_id, access))
|
||||
|
||||
studio_url = None
|
||||
if is_studio_course:
|
||||
studio_url = get_cms_course_link(course)
|
||||
@@ -228,3 +232,15 @@ def _section_analytics(course_id, access):
|
||||
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_metrics(course_id, access):
|
||||
"""Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'section_key': 'metrics',
|
||||
'section_display_name': ('Metrics'),
|
||||
'access': access,
|
||||
'sub_section_display_name': get_section_display_name(course_id),
|
||||
'section_has_problem': get_array_section_has_problem(course_id)
|
||||
}
|
||||
return section_data
|
||||
|
||||
@@ -53,6 +53,7 @@ from instructor_task.api import (
|
||||
)
|
||||
from instructor_task.views import get_task_completion_info
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
from class_dashboard import dashboard_data
|
||||
from psychometrics import psychoanalyze
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user
|
||||
from student.views import course_from_id
|
||||
@@ -817,6 +818,14 @@ def instructor_dashboard(request, course_id):
|
||||
for analytic_name in DASHBOARD_ANALYTICS:
|
||||
analytics_results[analytic_name] = get_analytics_result(analytic_name)
|
||||
|
||||
#----------------------------------------
|
||||
# Metrics
|
||||
|
||||
metrics_results = {}
|
||||
if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics':
|
||||
metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_id)
|
||||
metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_id)
|
||||
|
||||
#----------------------------------------
|
||||
# offline grades?
|
||||
|
||||
@@ -900,7 +909,8 @@ def instructor_dashboard(request, course_id):
|
||||
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
|
||||
|
||||
'analytics_results': analytics_results,
|
||||
'disable_buttons': disable_buttons
|
||||
'disable_buttons': disable_buttons,
|
||||
'metrics_results': metrics_results,
|
||||
}
|
||||
|
||||
if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
|
||||
|
||||
@@ -1246,6 +1246,11 @@ VERIFY_STUDENT = {
|
||||
"DAYS_GOOD_FOR": 365, # How many days is a verficiation good for?
|
||||
}
|
||||
|
||||
### This enables the Metrics tab for the Instructor dashboard ###########
|
||||
FEATURES['CLASS_DASHBOARD'] = False
|
||||
if FEATURES.get('CLASS_DASHBOARD'):
|
||||
INSTALLED_APPS += ('class_dashboard',)
|
||||
|
||||
######################## CAS authentication ###########################
|
||||
|
||||
if FEATURES.get('AUTH_USE_CAS'):
|
||||
|
||||
@@ -279,10 +279,12 @@ CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_P
|
||||
########################## USER API ########################
|
||||
EDX_API_KEY = None
|
||||
|
||||
|
||||
####################### Shoppingcart ###########################
|
||||
FEATURES['ENABLE_SHOPPING_CART'] = True
|
||||
|
||||
### This enables the Metrics tab for the Instructor dashboard ###########
|
||||
FEATURES['CLASS_DASHBOARD'] = True
|
||||
|
||||
#####################################################################
|
||||
# Lastly, see if the developer has any local overrides.
|
||||
try:
|
||||
|
||||
@@ -269,6 +269,9 @@ PASSWORD_HASHERS = (
|
||||
# 'django.contrib.auth.hashers.CryptPasswordHasher',
|
||||
)
|
||||
|
||||
### This enables the Metrics tab for the Instructor dashboard ###########
|
||||
FEATURES['CLASS_DASHBOARD'] = True
|
||||
|
||||
################### Make tests quieter
|
||||
|
||||
# OpenID spews messages like this to stderr, we don't need to see them:
|
||||
|
||||
@@ -170,6 +170,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
|
||||
,
|
||||
constructor: window.InstructorDashboard.sections.Analytics
|
||||
$element: idash_content.find ".#{CSS_IDASH_SECTION}#analytics"
|
||||
,
|
||||
constructor: window.InstructorDashboard.sections.Metrics
|
||||
$element: idash_content.find ".#{CSS_IDASH_SECTION}#metrics"
|
||||
]
|
||||
|
||||
sections_to_initialize.map ({constructor, $element}) ->
|
||||
|
||||
25
lms/static/coffee/src/instructor_dashboard/metrics.coffee
Normal file
25
lms/static/coffee/src/instructor_dashboard/metrics.coffee
Normal file
@@ -0,0 +1,25 @@
|
||||
# METRICS Section
|
||||
|
||||
# imports from other modules.
|
||||
# wrap in (-> ... apply) to defer evaluation
|
||||
# such that the value can be defined later than this assignment (file load order).
|
||||
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
|
||||
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
|
||||
|
||||
#Metrics Section
|
||||
class Metrics
|
||||
constructor: (@$section) ->
|
||||
@$section.data 'wrapper', @
|
||||
|
||||
|
||||
# handler for when the section title is clicked.
|
||||
onClickTitle: ->
|
||||
|
||||
# export for use
|
||||
# create parent namespaces if they do not already exist.
|
||||
# abort if underscore can not be found.
|
||||
if _?
|
||||
_.defaults window, InstructorDashboard: {}
|
||||
_.defaults window.InstructorDashboard, sections: {}
|
||||
_.defaults window.InstructorDashboard.sections,
|
||||
Metrics: Metrics
|
||||
@@ -121,5 +121,55 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Metrics tab
|
||||
|
||||
.metrics-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
float: left;
|
||||
clear: both;
|
||||
margin-top: 25px;
|
||||
}
|
||||
.metrics-left {
|
||||
position: relative;
|
||||
width: 30%;
|
||||
height: 640px;
|
||||
float: left;
|
||||
margin-right: 2.5%;
|
||||
}
|
||||
.metrics-right {
|
||||
position: relative;
|
||||
width: 65%;
|
||||
height: 295px;
|
||||
float: left;
|
||||
margin-left: 2.5%;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.metrics-tooltip {
|
||||
width: 250px;
|
||||
background-color: lightgray;
|
||||
padding: 3px;
|
||||
}
|
||||
.stacked-bar-graph-legend {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
p.loading {
|
||||
padding-top: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p.nothing {
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
h3.attention {
|
||||
padding: 10px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 5px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -458,6 +458,69 @@ section.instructor-dashboard-content-2 {
|
||||
}
|
||||
|
||||
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#metrics {
|
||||
|
||||
.metrics-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
float: left;
|
||||
clear: both;
|
||||
margin-top: 25px;
|
||||
}
|
||||
.metrics-left {
|
||||
position: relative;
|
||||
width: 30%;
|
||||
height: 640px;
|
||||
float: left;
|
||||
margin-right: 2.5%;
|
||||
}
|
||||
.metrics-left svg {
|
||||
width: 100%;
|
||||
}
|
||||
.metrics-right {
|
||||
position: relative;
|
||||
width: 65%;
|
||||
height: 295px;
|
||||
float: left;
|
||||
margin-left: 2.5%;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.metrics-right svg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metrics-tooltip {
|
||||
width: 250px;
|
||||
background-color: lightgray;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.stacked-bar-graph-legend {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
p.loading {
|
||||
padding-top: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p.nothing {
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
h3.attention {
|
||||
padding: 10px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 5px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
input#graph_reload {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.profile-distribution-widget {
|
||||
margin-bottom: $baseline * 2;
|
||||
|
||||
|
||||
88
lms/templates/class_dashboard/all_section_metrics.js
Normal file
88
lms/templates/class_dashboard/all_section_metrics.js
Normal file
@@ -0,0 +1,88 @@
|
||||
<%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, **kwargs"/>
|
||||
<%!
|
||||
import json
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
$(function () {
|
||||
|
||||
d3.json("${reverse('all_sequential_open_distrib', kwargs=dict(course_id=course_id))}", function(error, json) {
|
||||
var section, paramOpened, barGraphOpened, error;
|
||||
var i, curr_id;
|
||||
var errorMessage = gettext('Unable to retrieve data, please try again later.');
|
||||
|
||||
error = json.error;
|
||||
if (error) {
|
||||
$('.metrics-left .loading').text(errorMessage);
|
||||
return
|
||||
}
|
||||
|
||||
i = 0;
|
||||
for (section in json) {
|
||||
curr_id = "#${id_opened_prefix}"+i;
|
||||
paramOpened = {
|
||||
data: json[section].data,
|
||||
width: $(curr_id).width(),
|
||||
height: $(curr_id).height()-25, // Account for header
|
||||
tag: "opened"+i,
|
||||
bVerticalXAxisLabel : true,
|
||||
bLegend : false,
|
||||
margin: {left:0},
|
||||
};
|
||||
|
||||
barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"),
|
||||
d3.select("#${id_tooltip_prefix}"+i));
|
||||
barGraphOpened.scale.stackColor.range(["#555555","#555555"]);
|
||||
|
||||
if (paramOpened.data.length > 0) {
|
||||
barGraphOpened.drawGraph();
|
||||
|
||||
$('svg').siblings('.loading').remove();
|
||||
} else {
|
||||
$('svg').siblings('.loading').text(errorMessage);
|
||||
}
|
||||
|
||||
i+=1;
|
||||
}
|
||||
});
|
||||
|
||||
d3.json("${reverse('all_problem_grade_distribution', kwargs=dict(course_id=course_id))}", function(error, json) {
|
||||
var section, paramGrade, barGraphGrade, error;
|
||||
var i, curr_id;
|
||||
var errorMessage = gettext('Unable to retrieve data, please try again later.');
|
||||
|
||||
error = json.error;
|
||||
if (error) {
|
||||
$('.metrics-right .loading').text(errorMessage);
|
||||
return
|
||||
}
|
||||
|
||||
i = 0;
|
||||
for (section in json) {
|
||||
curr_id = "#${id_grade_prefix}"+i;
|
||||
paramGrade = {
|
||||
data: json[section].data,
|
||||
width: $(curr_id).width(),
|
||||
height: $(curr_id).height()-25, // Account for header
|
||||
tag: "grade"+i,
|
||||
bVerticalXAxisLabel : true,
|
||||
};
|
||||
|
||||
barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"),
|
||||
d3.select("#${id_tooltip_prefix}"+i));
|
||||
barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]);
|
||||
barGraphGrade.legend.width += 2;
|
||||
|
||||
if ( paramGrade.data.length > 0 ) {
|
||||
barGraphGrade.drawGraph();
|
||||
|
||||
$('svg').siblings('.loading').remove();
|
||||
} else {
|
||||
$('svg').siblings('.loading').text(errorMessage);
|
||||
}
|
||||
|
||||
i+=1;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
428
lms/templates/class_dashboard/d3_stacked_bar_graph.js
vendored
Normal file
428
lms/templates/class_dashboard/d3_stacked_bar_graph.js
vendored
Normal file
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
There are three parameters:
|
||||
(1) Parameter is of type object. Inside can include (* marks required):
|
||||
data* - Array of objects with key, value pairs that represent a single stack of bars:
|
||||
xValue - Corresponding value for the x-axis
|
||||
stackData - Array of objects with key, value pairs that represent a bar:
|
||||
color - Defines what "color" the bar will map to
|
||||
value - Maps to the height of the bar, along the y-axis
|
||||
tooltip - (Optional) Text to display on mouse hover
|
||||
|
||||
height - Height of the SVG the graph will be displayed in (default: 500)
|
||||
|
||||
width - Width of the SVG the graph will be displayed in (default: 500)
|
||||
|
||||
margin - Object with key, value pairs for the graph's margins within the SVG (default for all: 10)
|
||||
top - Top margin
|
||||
bottom - Bottom margin
|
||||
right - Right margin
|
||||
left - Left margin
|
||||
|
||||
yRange - Array of two values, representing the min and max respectively. (default: [0, <calculated max>])
|
||||
|
||||
xRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data)
|
||||
|
||||
colorRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data)
|
||||
|
||||
bVerticalXAxisLabel - Boolean whether to make the labels in the x-axis veritcal (default: false)
|
||||
|
||||
bLegend - Boolean if false does not create the graph with a legend (default: true)
|
||||
|
||||
(2) Parameter is a d3 pointer to the SVG the graph will draw itself in.
|
||||
|
||||
(3) Parameter is a d3 pointer to a div that will be used for the graph's tooltip.
|
||||
|
||||
****Does not actually draw graph.**** Returns an object that includes a function
|
||||
drawGraph, for when ready to draw graph. Reason for this is, because of all
|
||||
the defaults, some changes may be needed before drawing the graph
|
||||
|
||||
returns an object with the following:
|
||||
state - All information that can be put in parameters and adding:
|
||||
margin.axisX - margin to accomodate the x-axis
|
||||
margin.axisY - margin to acommodate the y-axis
|
||||
|
||||
drawGraph - function to call when ready to draw graph
|
||||
|
||||
scale - Object containing three d3 scales
|
||||
x - d3 scale for the x-axis
|
||||
y - d3 scale for the y-axis
|
||||
stackColor - d3 scale for the stack color
|
||||
|
||||
axis - Object containg the graph's two d3 axis
|
||||
x - d3 axis for the x-axis
|
||||
y - d3 axis for the y-axis
|
||||
|
||||
svg - d3 pointer to the svg holding the graph
|
||||
|
||||
svgGroup - object holding the svg groups
|
||||
main - svg group holding all other groups
|
||||
xAxis - svg group holding the x-axis
|
||||
yAxis - svg group holding the x-axis
|
||||
bars - svg groups holding the bars
|
||||
|
||||
yAxisLabel - d3 pointer to the text component that holds the y axis label
|
||||
|
||||
divTooltip - d3 pointer to the div that is used as the tooltip for the graph
|
||||
|
||||
rects - d3 collection of the rects used in the bars
|
||||
|
||||
legend - object containing information for the legend
|
||||
height - height of the legend
|
||||
width - width of the legend (if change, need to update state.margin.axisY also)
|
||||
range - array of values that appears in the legend
|
||||
barHeight - height of a bar in the legend, based on height and length of range
|
||||
*/
|
||||
|
||||
edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) {
|
||||
var graph = {
|
||||
svg : svg,
|
||||
state : {
|
||||
data : undefined,
|
||||
height : 500,
|
||||
width : 500,
|
||||
margin: {top: 10, bottom: 10, right: 10, left: 10},
|
||||
yRange: [0],
|
||||
xRange : undefined,
|
||||
colorRange : undefined,
|
||||
tag : "",
|
||||
bVerticalXAxisLabel : false,
|
||||
bLegend : true,
|
||||
},
|
||||
divTooltip : divTooltip,
|
||||
};
|
||||
|
||||
var state = graph.state;
|
||||
|
||||
// Handle parameters
|
||||
state.data = parameters.data;
|
||||
|
||||
if (parameters.margin != undefined) {
|
||||
for (var key in state.margin) {
|
||||
if ((state.margin.hasOwnProperty(key) &&
|
||||
(parameters.margin[key] != undefined))) {
|
||||
state.margin[key] = parameters.margin[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in state) {
|
||||
if ((key != "data") && (key != "margin")) {
|
||||
if (state.hasOwnProperty(key) && (parameters[key] != undefined)) {
|
||||
state[key] = parameters[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.tag != "")
|
||||
state.tag = state.tag+"-";
|
||||
|
||||
if ((state.xRange == undefined) || (state.yRange.length < 2 ||
|
||||
state.colorRange == undefined)) {
|
||||
var aryXRange = [];
|
||||
var bXIsOrdinal = false;
|
||||
var maxYRange = 0;
|
||||
var aryColorRange = [];
|
||||
var bColorIsOrdinal = false;
|
||||
|
||||
for (var stackKey in state.data) {
|
||||
var stack = state.data[stackKey];
|
||||
aryXRange.push(stack.xValue);
|
||||
if (isNaN(stack.xValue))
|
||||
bXIsOrdinal = true;
|
||||
|
||||
var valueTotal = 0;
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
valueTotal += bar.value;
|
||||
|
||||
if (isNaN(bar.color))
|
||||
bColorIsOrdinal = true;
|
||||
|
||||
if (aryColorRange.indexOf(bar.color) < 0)
|
||||
aryColorRange.push(bar.color);
|
||||
}
|
||||
if (maxYRange < valueTotal)
|
||||
maxYRange = valueTotal;
|
||||
}
|
||||
|
||||
if (state.xRange == undefined){
|
||||
if (bXIsOrdinal)
|
||||
state.xRange = aryXRange;
|
||||
else
|
||||
state.xRange = [
|
||||
Math.min.apply(null,aryXRange),
|
||||
Math.max.apply(null,aryXRange)
|
||||
];
|
||||
}
|
||||
|
||||
if (state.yRange.length < 2)
|
||||
state.yRange[1] = maxYRange;
|
||||
|
||||
if (state.colorRange == undefined){
|
||||
if (bColorIsOrdinal)
|
||||
state.colorRange = aryColorRange;
|
||||
else
|
||||
state.colorRange = [
|
||||
Math.min.apply(null,aryColorRange),
|
||||
Math.max.apply(null,aryColorRange)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Find needed spacing for axes
|
||||
var tmpEl = graph.svg.append("text").text(state.yRange[1]+"1234")
|
||||
.attr("id",state.tag+"stacked-bar-graph-long-str");
|
||||
state.margin.axisY = document.getElementById(state.tag+"stacked-bar-graph-long-str")
|
||||
.getComputedTextLength()+state.margin.left;
|
||||
|
||||
var longestXAxisStr = "";
|
||||
if (isNaN(state.xRange[0])) {
|
||||
for (var i in state.xRange) {
|
||||
if (longestXAxisStr.length < state.xRange[i].length)
|
||||
longestXAxisStr = state.xRange[i]+"1234";
|
||||
}
|
||||
} else {
|
||||
longestXAxisStr = state.xRange[1]+"1234";
|
||||
}
|
||||
|
||||
tmpEl.text(longestXAxisStr);
|
||||
if (state.bVerticalXAxisLabel) {
|
||||
state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str")
|
||||
.getComputedTextLength()+state.margin.bottom;
|
||||
} else {
|
||||
state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str")
|
||||
.clientHeight+state.margin.bottom;
|
||||
}
|
||||
|
||||
tmpEl.remove();
|
||||
|
||||
// Add y0 and y1 of the y-axis based on the count and order of the colorRange.
|
||||
// First, case if color is a number range
|
||||
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
|
||||
!(isNaN(state.colorRange[1]))) {
|
||||
for (var stackKey in state.data) {
|
||||
var stack = state.data[stackKey];
|
||||
stack.stackData.sort(function(a,b) { return a.color - b.color; });
|
||||
|
||||
var currTotal = 0;
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
bar.y0 = currTotal;
|
||||
currTotal += bar.value;
|
||||
bar.y1 = currTotal;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var stackKey in state.data) {
|
||||
var stack = state.data[stackKey];
|
||||
|
||||
var tmpStackData = [];
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
tmpStackData[state.colorRange.indexOf(bar.color)] = bar;
|
||||
}
|
||||
stack.stackData = tmpStackData;
|
||||
|
||||
var currTotal = 0;
|
||||
for (var barKey in stack.stackData) {
|
||||
var bar = stack.stackData[barKey];
|
||||
bar.y0 = currTotal;
|
||||
currTotal += bar.value;
|
||||
bar.y1 = currTotal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add information to create legend
|
||||
if (state.bLegend) {
|
||||
graph.legend = {
|
||||
height : (state.height-state.margin.top-state.margin.axisX),
|
||||
width : 30,
|
||||
range : state.colorRange,
|
||||
};
|
||||
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
|
||||
!(isNaN(state.colorRange[1]))) {
|
||||
graph.legend.range = [];
|
||||
|
||||
var i = 0;
|
||||
var min = state.colorRange[0];
|
||||
var max = state.colorRange[1];
|
||||
while (i <= 10) {
|
||||
graph.legend.range[i] = min+((max-min)/10)*i;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
graph.legend.barHeight = graph.legend.height/graph.legend.range.length;
|
||||
|
||||
// Shifting the axis over to make room
|
||||
graph.state.margin.axisY += graph.legend.width;
|
||||
}
|
||||
|
||||
// Make the scales
|
||||
graph.scale = {
|
||||
x: d3.scale.ordinal()
|
||||
.domain(graph.state.xRange)
|
||||
.rangeRoundBands([
|
||||
(graph.state.margin.axisY),
|
||||
(graph.state.width-graph.state.margin.right)],
|
||||
.3),
|
||||
|
||||
y: d3.scale.linear()
|
||||
.domain(graph.state.yRange) // yRange is the range of the y-axis values
|
||||
.range([
|
||||
(graph.state.height-graph.state.margin.axisX),
|
||||
graph.state.margin.top
|
||||
]),
|
||||
|
||||
stackColor: d3.scale.ordinal()
|
||||
.domain(graph.state.colorRange)
|
||||
.range(["#ffeeee","#ffebeb","#ffd8d8","#ffc4c4","#ffb1b1","#ff9d9d","#ff8989","#ff7676","#ff6262","#ff4e4e","#ff3b3b"])
|
||||
};
|
||||
|
||||
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
|
||||
!(isNaN(state.colorRange[1]))) {
|
||||
graph.scale.stackColor = d3.scale.linear()
|
||||
.domain(state.colorRange)
|
||||
.range(["#e13f29","#17a74d"]);
|
||||
}
|
||||
|
||||
// Setup axes
|
||||
graph.axis = {
|
||||
x: d3.svg.axis()
|
||||
.scale(graph.scale.x),
|
||||
y: d3.svg.axis()
|
||||
.scale(graph.scale.y),
|
||||
}
|
||||
|
||||
graph.axis.x.orient("bottom");
|
||||
graph.axis.y.orient("left");
|
||||
|
||||
// Draw graph function, to call when ready.
|
||||
graph.drawGraph = function() {
|
||||
var graph = this;
|
||||
|
||||
// Steup SVG
|
||||
graph.svg.attr("id", graph.state.tag+"stacked-bar-graph")
|
||||
.attr("class", "stacked-bar-graph")
|
||||
.attr("width", graph.state.width)
|
||||
.attr("height", graph.state.height);
|
||||
graph.svgGroup = {};
|
||||
|
||||
graph.svgGroup.main = graph.svg.append("g");
|
||||
|
||||
// Draw Bars
|
||||
graph.svgGroup.bars = graph.svgGroup.main.selectAll(".stacked-bar")
|
||||
.data(graph.state.data)
|
||||
.enter().append("g")
|
||||
.attr("class", "stacked-bar")
|
||||
.attr("transform", function(d) {
|
||||
return "translate("+graph.scale.x(d.xValue)+",0)";
|
||||
});
|
||||
|
||||
graph.rects = graph.svgGroup.bars.selectAll("rect")
|
||||
.data(function(d) { return d.stackData; })
|
||||
.enter().append("rect")
|
||||
.attr("width", function(d) {
|
||||
return graph.scale.x.rangeBand()
|
||||
})
|
||||
.attr("y", function(d) { return graph.scale.y(d.y1); })
|
||||
.attr("height", function(d) {
|
||||
return graph.scale.y(d.y0) - graph.scale.y(d.y1);
|
||||
})
|
||||
.style("fill", function(d) { return graph.scale.stackColor(d.color); })
|
||||
.style("stroke", "white")
|
||||
.style("stroke-width", "0.5px");
|
||||
|
||||
// Setup tooltip
|
||||
if (graph.divTooltip != undefined) {
|
||||
graph.divTooltip
|
||||
.style("position", "absolute")
|
||||
.style("z-index", "10")
|
||||
.style("visibility", "hidden");
|
||||
}
|
||||
|
||||
graph.rects
|
||||
.on("mouseover", function(d) {
|
||||
var pos = d3.mouse(graph.divTooltip.node().parentNode);
|
||||
var left = pos[0]+10;
|
||||
var top = pos[1]-10;
|
||||
var width = $('#'+graph.divTooltip.attr("id")).width();
|
||||
|
||||
graph.divTooltip.style("visibility", "visible")
|
||||
.text(d.tooltip);
|
||||
|
||||
if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width())
|
||||
left -= (width+30);
|
||||
|
||||
graph.divTooltip.style("top", top+"px")
|
||||
.style("left", left+"px");
|
||||
})
|
||||
.on("mouseout", function(d){
|
||||
graph.divTooltip.style("visibility", "hidden")
|
||||
});
|
||||
|
||||
// Add legend
|
||||
if (graph.state.bLegend) {
|
||||
graph.svgGroup.legendG = graph.svgGroup.main.append("g")
|
||||
.attr("class","stacked-bar-graph-legend")
|
||||
.attr("transform","translate("+graph.state.margin.left+","+
|
||||
graph.state.margin.top+")");
|
||||
graph.svgGroup.legendGs = graph.svgGroup.legendG.selectAll(".stacked-bar-graph-legend-g")
|
||||
.data(graph.legend.range)
|
||||
.enter().append("g")
|
||||
.attr("class","stacked-bar-graph-legend-g")
|
||||
.attr("id",function(d,i) { return graph.state.tag+"legend-"+i; })
|
||||
.attr("transform", function(d,i) {
|
||||
return "translate(0,"+
|
||||
(graph.state.height-graph.state.margin.axisX-((i+1)*(graph.legend.barHeight))) + ")";
|
||||
});
|
||||
|
||||
graph.svgGroup.legendGs.append("rect")
|
||||
.attr("class","stacked-bar-graph-legend-rect")
|
||||
.attr("height", graph.legend.barHeight)
|
||||
.attr("width", graph.legend.width)
|
||||
.style("fill", graph.scale.stackColor)
|
||||
.style("stroke", "white");
|
||||
|
||||
graph.svgGroup.legendGs.append("text")
|
||||
.attr("class","axis-label")
|
||||
.attr("transform", function(d) {
|
||||
var str = "translate("+(graph.legend.width/2)+","+
|
||||
(graph.legend.barHeight/2)+")";
|
||||
return str;
|
||||
})
|
||||
.attr("dy", ".35em")
|
||||
.attr("dx", "-1px")
|
||||
.style("text-anchor", "middle")
|
||||
.text(function(d,i) { return d; });
|
||||
}
|
||||
|
||||
|
||||
// Draw Axes
|
||||
graph.svgGroup.xAxis = graph.svgGroup.main.append("g")
|
||||
.attr("class","stacked-bar-graph-axis")
|
||||
.attr("id",graph.state.tag+"x-axis");
|
||||
|
||||
var tmpS = "translate(0,"+(graph.state.height-graph.state.margin.axisX)+")";
|
||||
if (graph.state.bVerticalXAxisLabel) {
|
||||
graph.axis.x.orient("left");
|
||||
tmpS = "rotate(270), translate(-"+(graph.state.height-graph.state.margin.axisX)+",0)";
|
||||
}
|
||||
graph.svgGroup.xAxis.attr("transform", tmpS)
|
||||
.call(graph.axis.x);
|
||||
|
||||
graph.svgGroup.yAxis = graph.svgGroup.main.append("g")
|
||||
.attr("class","stacked-bar-graph-axis")
|
||||
.attr("id",graph.state.tag+"y-axis")
|
||||
.attr("transform","translate("+
|
||||
(graph.state.margin.axisY)+",0)")
|
||||
.call(graph.axis.y);
|
||||
graph.yAxisLabel = graph.svgGroup.yAxis.append("text")
|
||||
.attr("dy","1em")
|
||||
.attr("transform","rotate(-90)")
|
||||
.style("text-anchor","end")
|
||||
.text(gettext("Number of Students"));
|
||||
};
|
||||
|
||||
return graph;
|
||||
};
|
||||
@@ -144,6 +144,9 @@ function goto( mode)
|
||||
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_ANALYTICS'):
|
||||
| <a href="#" onclick="goto('Analytics');" class="${modeflag.get('Analytics')}">${_("Analytics")}</a>
|
||||
%endif
|
||||
%if settings.FEATURES.get('CLASS_DASHBOARD'):
|
||||
| <a href="#" onclick="goto('Metrics');" class="${modeflag.get('Metrics')}">${_("Metrics")}</a>
|
||||
%endif
|
||||
]
|
||||
</h2>
|
||||
|
||||
@@ -669,6 +672,46 @@ function goto( mode)
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%if modeflag.get('Metrics'):
|
||||
%if not any (metrics_results.values()):
|
||||
<p>${_("There is no data available to display at this time.")}</p>
|
||||
%else:
|
||||
<%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/>
|
||||
<%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/>
|
||||
|
||||
<script>
|
||||
${d3_stacked_bar_graph.body()}
|
||||
</script>
|
||||
|
||||
<div id="metrics"></div>
|
||||
|
||||
<h3 class="attention">${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}</h3>
|
||||
|
||||
%for i in range(0,len(metrics_results['section_display_name'])):
|
||||
<div class="metrics-container" id="metrics_section_${i}">
|
||||
<h2>${_("Section:")} ${metrics_results['section_display_name'][i]}</h2>
|
||||
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
|
||||
<div class="metrics-left" id="metric_opened_${i}">
|
||||
<h3>${_("Count of Students that Opened a Subsection")}</h3>
|
||||
<p class="loading"><i class="icon-spinner icon-spin icon-large"></i>${_("Loading...")}</p>
|
||||
</div>
|
||||
<div class="metrics-right" id="metric_grade_${i}">
|
||||
<h3>${_("Grade Distribution per Problem")}</h3>
|
||||
%if not metrics_results['section_has_problem'][i]:
|
||||
<p>${_("There are no problems in this section.")}</p>
|
||||
%else:
|
||||
<p class="loading"><i class="icon-spinner icon-spin icon-large"></i>${_("Loading...")}</p>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
%endfor
|
||||
<script>
|
||||
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)}
|
||||
</script>
|
||||
|
||||
%endif
|
||||
%endif
|
||||
|
||||
%if modeflag.get('Analytics In Progress'):
|
||||
|
||||
##This is not as helpful as it could be -- let's give full point distribution
|
||||
|
||||
80
lms/templates/instructor/instructor_dashboard_2/metrics.html
Normal file
80
lms/templates/instructor/instructor_dashboard_2/metrics.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%page args="section_data"/>
|
||||
|
||||
<script>
|
||||
${d3_stacked_bar_graph.body()}
|
||||
</script>
|
||||
|
||||
%if not any (section_data.values()):
|
||||
<p>${_("There is no data available to display at this time.")}</p>
|
||||
%else:
|
||||
<%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/>
|
||||
<%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/>
|
||||
|
||||
<h3 class="attention" id="graph_load">${_("Loading the latest graphs for you; depending on your class size, this may take a few minutes.")}</h3>
|
||||
<input type="button" id="graph_reload" value="${_("Reload Graphs")}" />
|
||||
|
||||
%for i in range(0,len(section_data['sub_section_display_name'])):
|
||||
<div class="metrics-container" id="metrics_section_${i}">
|
||||
<h2>${_("Section:")} ${section_data['sub_section_display_name'][i]}</h2>
|
||||
<div class="metrics-tooltip" id="metric_tooltip_${i}"></div>
|
||||
<div class="metrics-left" id="metric_opened_${i}">
|
||||
<h3>${_("Count of Students Opened a Subsection")}</h3>
|
||||
</div>
|
||||
<div class="metrics-right" id="metric_grade_${i}" data-section-has-problem=${section_data['section_has_problem'][i]}>
|
||||
<h3>${_("Grade Distribution per Problem")}</h3>
|
||||
</div>
|
||||
</div>
|
||||
%endfor
|
||||
<script>
|
||||
$(function () {
|
||||
var firstLoad = true;
|
||||
|
||||
loadGraphs = function() {
|
||||
$('#graph_load').show();
|
||||
$('#graph_reload').hide();
|
||||
$('.loading').remove();
|
||||
|
||||
var nothingText = "${_('There are no problems in this section.')}";
|
||||
var loadingText = "${_('Loading...')}";
|
||||
var nothingP = '<p class="nothing">' + nothingText + '</p>';
|
||||
var loading = '<p class="loading"><i class="icon-spinner icon-spin icon-large"></i>' + loadingText + '</p>';
|
||||
|
||||
$('.metrics-left').each(function() {
|
||||
$(this).append(loading);
|
||||
});
|
||||
$('.metrics-right p.nothing').remove();
|
||||
$('.metrics-right').each(function() {
|
||||
if ($(this).data('section-has-problem') === "False") {
|
||||
$(this).append(nothingP);
|
||||
} else {
|
||||
$(this).append(loading);
|
||||
}
|
||||
});
|
||||
$('.metrics-left svg, .metrics-right svg').remove();
|
||||
|
||||
${all_section_metrics.body("metric_opened_","metric_grade_","metric_attempts_","metric_tooltip_",course.id)}
|
||||
|
||||
setTimeout(function() {
|
||||
$('#graph_load, #graph_reload').toggle();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
$('.instructor-nav a').click(function () {
|
||||
if ($(this).data('section') === "metrics" && firstLoad) {
|
||||
loadGraphs();
|
||||
firstLoad = false;
|
||||
}
|
||||
});
|
||||
|
||||
$('#graph_reload').click(function () {
|
||||
loadGraphs();
|
||||
});
|
||||
|
||||
if (window.location.hash === "#view-metrics") {
|
||||
$('.instructor-nav a[data-section="metrics"]').click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
%endif
|
||||
13
lms/urls.py
13
lms/urls.py
@@ -375,6 +375,19 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA
|
||||
include('instructor.views.api_urls'))
|
||||
)
|
||||
|
||||
if settings.FEATURES.get('CLASS_DASHBOARD'):
|
||||
urlpatterns += (
|
||||
# Json request data for metrics for entire course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_sequential_open_distrib$',
|
||||
'class_dashboard.views.all_sequential_open_distrib', name="all_sequential_open_distrib"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/all_problem_grade_distribution$',
|
||||
'class_dashboard.views.all_problem_grade_distribution', name="all_problem_grade_distribution"),
|
||||
|
||||
# Json request data for metrics for particular section
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/problem_grade_distribution/(?P<section>\d+)$',
|
||||
'class_dashboard.views.section_problem_grade_distrib', name="section_problem_grade_distrib"),
|
||||
)
|
||||
|
||||
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
|
||||
## Jasmine and admin
|
||||
urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
|
||||
|
||||
Reference in New Issue
Block a user