Commit 52b8d0c0 by David Adams

Merge pull request #2346 from edx/dcadams/student_metrics_tab

New tab (Metrics) in instructor dashboard
parents 9a23be53 3881ffdc
...@@ -37,8 +37,8 @@ msgid "" ...@@ -37,8 +37,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 0.1a\n" "Project-Id-Version: 0.1a\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-02-27 08:57-0500\n" "POT-Creation-Date: 2014-02-28 13:57-0800\n"
"PO-Revision-Date: 2014-02-27 13:57:20.220825\n" "PO-Revision-Date: 2014-02-28 21:57:20.716655\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n" "Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
...@@ -1294,6 +1294,40 @@ msgstr "Séärçh Ⱡ'σяєм ιρѕ#" ...@@ -1294,6 +1294,40 @@ msgstr "Séärçh Ⱡ'σяєм ιρѕ#"
msgid "Copyright" msgid "Copyright"
msgstr "Çöpýrïght #" 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 _ #. Translators: this string includes wiki markup. Leave the ** and the _
#. alone. #. alone.
#: lms/djangoapps/course_wiki/views.py #: lms/djangoapps/course_wiki/views.py
...@@ -2961,7 +2995,7 @@ msgstr "Délété ärtïçlé Ⱡ'#" ...@@ -2961,7 +2995,7 @@ msgstr "Délété ärtïçlé Ⱡ'#"
#: lms/templates/wiki/delete.html #: lms/templates/wiki/delete.html
#: lms/templates/wiki/plugins/attachments/index.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/_underscore_templates.html #: lms/templates/discussion/_underscore_templates.html
#: lms/templates/discussion/mustache/_inline_thread_show.mustache #: lms/templates/discussion/mustache/_inline_thread_show.mustache
...@@ -3003,6 +3037,7 @@ msgid "You are deleting an article. Please confirm." ...@@ -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. Ⱡ'σяєм ιρѕυм#" msgstr "Ýöü äré délétïng än ärtïçlé. Pléäsé çönfïrm. Ⱡ'σяєм ιρѕυм#"
#: lms/templates/wiki/edit.html cms/templates/component.html #: 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 #: 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 #" ...@@ -3032,6 +3067,7 @@ msgstr "Prévïéw #"
#: lms/templates/help_modal.html lms/templates/login_modal.html #: lms/templates/help_modal.html lms/templates/login_modal.html
#: lms/templates/signup_modal.html #: lms/templates/signup_modal.html
#: lms/templates/modal/_modal-settings-language.html #: lms/templates/modal/_modal-settings-language.html
#: lms/templates/modal/accessible_confirm.html
msgid "Close Modal" msgid "Close Modal"
msgstr "Çlösé Mödäl Ⱡ#" msgstr "Çlösé Mödäl Ⱡ#"
...@@ -3048,6 +3084,7 @@ msgstr "Wïkï Prévïéw Ⱡ#" ...@@ -3048,6 +3084,7 @@ msgstr "Wïkï Prévïéw Ⱡ#"
#: lms/templates/dashboard.html lms/templates/dashboard.html #: lms/templates/dashboard.html lms/templates/dashboard.html
#: lms/templates/dashboard.html #: lms/templates/dashboard.html
#: lms/templates/modal/_modal-settings-language.html #: lms/templates/modal/_modal-settings-language.html
#: lms/templates/modal/accessible_confirm.html
msgid "modal open" msgid "modal open"
msgstr "mödäl öpén Ⱡ#" msgstr "mödäl öpén Ⱡ#"
...@@ -3713,6 +3750,11 @@ msgstr "Püßlïç Ûsérnämé Ⱡ'#" ...@@ -3713,6 +3750,11 @@ msgstr "Püßlïç Ûsérnämé Ⱡ'#"
msgid "Preferred Language" msgid "Preferred Language"
msgstr "Préférréd Längüägé Ⱡ'σ#" 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 #: cms/templates/registration/activation_complete.html
#: lms/templates/registration/activation_complete.html #: lms/templates/registration/activation_complete.html
msgid "Thanks for activating your account." msgid "Thanks for activating your account."
...@@ -4172,9 +4214,6 @@ msgstr "" ...@@ -4172,9 +4214,6 @@ msgstr ""
msgid "Change your name" msgid "Change your name"
msgstr "Çhängé ýöür nämé Ⱡ'σ#" 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 #: lms/templates/dashboard.html
msgid "" msgid ""
"To uphold the credibility of your {platform} {cert_name_short}, all name " "To uphold the credibility of your {platform} {cert_name_short}, all name "
...@@ -4183,9 +4222,6 @@ msgstr "" ...@@ -4183,9 +4222,6 @@ msgstr ""
"Tö üphöld thé çrédïßïlïtý öf ýöür {platform} {cert_name_short}, äll nämé " "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. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" "ç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 #: lms/templates/dashboard.html
msgid "" msgid ""
"Enter your desired full name, as it will appear on your {platform} " "Enter your desired full name, as it will appear on your {platform} "
...@@ -4847,7 +4883,7 @@ msgstr "Réjéçtéd #" ...@@ -4847,7 +4883,7 @@ msgstr "Réjéçtéd #"
msgid "Pending name changes" msgid "Pending name changes"
msgstr "Péndïng nämé çhängés Ⱡ'σя#" 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" msgid "Confirm"
msgstr "Çönfïrm #" msgstr "Çönfïrm #"
...@@ -5710,20 +5746,10 @@ msgstr "Tögglé Füll Rüßrïç Ⱡ'σ#" ...@@ -5710,20 +5746,10 @@ msgstr "Tögglé Füll Rüßrïç Ⱡ'σ#"
msgid "{result_of_task} from grader {number}" msgid "{result_of_task} from grader {number}"
msgstr "{result_of_task} fröm grädér {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 #: lms/templates/combinedopenended/open_ended_result_table.html
msgid "See full feedback" msgid "See full feedback"
msgstr "Séé füll féédßäçk Ⱡ'σ#" 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/open_ended_result_table.html
#: lms/templates/combinedopenended/openended/open_ended_evaluation.html #: lms/templates/combinedopenended/openended/open_ended_evaluation.html
msgid "Respond to Feedback" msgid "Respond to Feedback"
...@@ -6138,6 +6164,10 @@ msgid "Manage Groups" ...@@ -6138,6 +6164,10 @@ msgid "Manage Groups"
msgstr "Mänägé Gröüps Ⱡ'#" msgstr "Mänägé Gröüps Ⱡ'#"
#: lms/templates/courseware/instructor_dashboard.html #: lms/templates/courseware/instructor_dashboard.html
msgid "Metrics"
msgstr "Métrïçs #"
#: lms/templates/courseware/instructor_dashboard.html
msgid "Grade Downloads" msgid "Grade Downloads"
msgstr "Grädé Döwnlöäds Ⱡ'#" msgstr "Grädé Döwnlöäds Ⱡ'#"
...@@ -6320,6 +6350,8 @@ msgid "Pull enrollment from remote gradebook" ...@@ -6320,6 +6350,8 @@ msgid "Pull enrollment from remote gradebook"
msgstr "Püll énröllmént fröm rémöté grädéßöök Ⱡ'σяєм ιρѕ#" 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/courseware/instructor_dashboard.html
#: lms/templates/instructor/instructor_dashboard_2/metrics.html
msgid "Section:" msgid "Section:"
msgstr "Séçtïön: #" msgstr "Séçtïön: #"
...@@ -6486,6 +6518,40 @@ msgid "Points Earned (Num Students)" ...@@ -6486,6 +6518,40 @@ msgid "Points Earned (Num Students)"
msgstr "Pöïnts Éärnéd (Nüm Stüdénts) Ⱡ'σяєм #" msgstr "Pöïnts Éärnéd (Nüm Stüdénts) Ⱡ'σяєм #"
#: lms/templates/courseware/instructor_dashboard.html #: 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" msgid "Students answering correctly"
msgstr "Stüdénts änswérïng çörréçtlý Ⱡ'σяєм #" msgstr "Stüdénts änswérïng çörréçtlý Ⱡ'σяєм #"
...@@ -6782,12 +6848,10 @@ msgstr "Vïéw Àrçhïvéd Çöürsé Ⱡ'σя#" ...@@ -6782,12 +6848,10 @@ msgstr "Vïéw Àrçhïvéd Çöürsé Ⱡ'σя#"
msgid "View Course" msgid "View Course"
msgstr "Vïéw Çöürsé Ⱡ#" 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 #: lms/templates/dashboard/_dashboard_course_listing.html
msgid "Are you sure you want to unregister from" msgid "Are you sure you want to unregister from"
msgstr "Àré ýöü süré ýöü wänt tö ünrégïstér fröm Ⱡ'σяєм ιρѕυ#" 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
#: lms/templates/dashboard/_dashboard_course_listing.html #: lms/templates/dashboard/_dashboard_course_listing.html
msgid "" msgid ""
...@@ -7850,6 +7914,14 @@ msgstr "" ...@@ -7850,6 +7914,14 @@ msgstr ""
"çöhörts. Théïr pösts äré märkéd 'Çömmünïtý TÀ'. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт," "çöhörts. Théïr pösts äré märkéd 'Çömmünïtý TÀ'. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,"
" ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι#" " ¢σηѕє¢тєтυя α∂ιριѕι¢ιη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
#: lms/templates/instructor/instructor_dashboard_2/send_email.html #: lms/templates/instructor/instructor_dashboard_2/send_email.html
msgid "Send Email" msgid "Send Email"
...@@ -8624,6 +8696,27 @@ msgstr "" ...@@ -8624,6 +8696,27 @@ msgstr ""
"héré för pössïßlé üsé ßý ïnställätïöns öf Öpén édX. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " "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
#: lms/templates/static_templates/honor.html #: lms/templates/static_templates/honor.html
msgid "Honor Code" msgid "Honor Code"
...@@ -9722,10 +9815,6 @@ msgid "You have decided to pay $ " ...@@ -9722,10 +9815,6 @@ msgid "You have decided to pay $ "
msgstr "Ýöü hävé déçïdéd tö päý $ Ⱡ'σяєм#" msgstr "Ýöü hävé déçïdéd tö päý $ Ⱡ'σяєм#"
#: lms/templates/wiki/includes/article_menu.html #: 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
#: lms/templates/wiki/includes/article_menu.html #: 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é Ⱡ'σ#" ...@@ -9872,10 +9961,10 @@ msgstr "Löäd Ànöthér Fïlé Ⱡ'σ#"
msgid "Content" msgid "Content"
msgstr "Çöntént #" msgstr "Çöntént #"
#: cms/templates/asset_index.html cms/templates/course_info.html #: cms/templates/asset_index.html cms/templates/container.html
#: cms/templates/edit-tabs.html cms/templates/index.html #: cms/templates/course_info.html cms/templates/edit-tabs.html
#: cms/templates/manage_users.html cms/templates/overview.html #: cms/templates/index.html cms/templates/manage_users.html
#: cms/templates/textbooks.html #: cms/templates/overview.html cms/templates/textbooks.html
msgid "Page Actions" msgid "Page Actions"
msgstr "Pägé Àçtïöns Ⱡ#" msgstr "Pägé Àçtïöns Ⱡ#"
...@@ -9916,8 +10005,8 @@ msgstr "" ...@@ -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 " "öf ýöür çöürsé. Dö nöt üsé thé Éxtérnäl ÛRL äs ä lïnk välüé wïthïn ýöür "
"çöürsé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι#" "çöürsé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι#"
#: cms/templates/asset_index.html cms/templates/overview.html #: cms/templates/asset_index.html cms/templates/container.html
#: cms/templates/settings_graders.html #: cms/templates/overview.html cms/templates/settings_graders.html
msgid "What can I do on this page?" msgid "What can I do on this page?"
msgstr "Whät çän Ì dö ön thïs pägé? Ⱡ'σяєм#" msgstr "Whät çän Ì dö ön thïs pägé? Ⱡ'σяєм#"
...@@ -9979,23 +10068,43 @@ msgstr "" ...@@ -9979,23 +10068,43 @@ msgstr ""
msgid "Editor" msgid "Editor"
msgstr "Édïtör Ⱡ'σяєм ιρѕ#" msgstr "Édïtör Ⱡ'σяєм ιρѕ#"
#: cms/templates/component.html #: cms/templates/component.html cms/templates/studio_xblock_wrapper.html
msgid "Duplicate" msgid "Duplicate"
msgstr "Düplïçäté #" msgstr "Düplïçäté #"
#: cms/templates/component.html #: cms/templates/component.html cms/templates/studio_xblock_wrapper.html
msgid "Duplicate this component" msgid "Duplicate this component"
msgstr "Düplïçäté thïs çömpönént Ⱡ'σяє#" 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" msgid "Delete this component"
msgstr "Délété thïs çömpönént Ⱡ'σя#" msgstr "Délété thïs çömpönént Ⱡ'σя#"
#: cms/templates/component.html cms/templates/overview.html #: cms/templates/component.html cms/templates/overview.html
#: cms/templates/overview.html #: cms/templates/overview.html
#: cms/templates/unit_container_xblock_component.html
msgid "Drag to reorder" msgid "Drag to reorder"
msgstr "Dräg tö réördér Ⱡ'#" 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 #: cms/templates/course_info.html cms/templates/course_info.html
msgid "Course Updates" msgid "Course Updates"
msgstr "Çöürsé Ûpdätés Ⱡ'#" msgstr "Çöürsé Ûpdätés Ⱡ'#"
...@@ -10205,7 +10314,6 @@ msgstr "Çöürsé Éxpört Ⱡ'#" ...@@ -10205,7 +10314,6 @@ msgstr "Çöürsé Éxpört Ⱡ'#"
msgid "About Exporting Courses" msgid "About Exporting Courses"
msgstr "Àßöüt Éxpörtïng Çöürsés Ⱡ'σяє#" msgstr "Àßöüt Éxpörtïng Çöürsés Ⱡ'σяє#"
#. Translators: ".tar.gz" is a file extension, and should not be translated
#: cms/templates/export.html #: cms/templates/export.html
msgid "" msgid ""
"You can export courses and edit them outside of Studio. The exported file is" "You can export courses and edit them outside of Studio. The exported file is"
...@@ -10310,7 +10418,6 @@ msgstr "" ...@@ -10310,7 +10418,6 @@ msgstr ""
msgid "Opening the downloaded file" msgid "Opening the downloaded file"
msgstr "Öpénïng thé döwnlöädéd fïlé Ⱡ'σяєм#" 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 #: cms/templates/export.html
msgid "" msgid ""
"Use an archive program to extract the data from the .tar.gz file. Extracted " "Use an archive program to extract the data from the .tar.gz file. Extracted "
...@@ -10646,8 +10753,6 @@ msgstr "" ...@@ -10646,8 +10753,6 @@ msgstr ""
"çürrént çöürsé, sö ýöü hävé ä ßäçküp çöpý öf ït. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " "çürrént çöürsé, sö ýöü hävé ä ßäçküp çöpý öf ït. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт "
"αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя #" "αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιη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 #: cms/templates/import.html
msgid "" msgid ""
"The course that you import must be in a .tar.gz file (that is, a .tar file " "The course that you import must be in a .tar.gz file (that is, a .tar file "
...@@ -10672,8 +10777,6 @@ msgstr "" ...@@ -10672,8 +10777,6 @@ msgstr ""
"ýöür çöürsé üntïl thé ïmpört öpérätïön häs çömplétéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт" "ýöür çöürsé üntïl thé ïmpört öpérätïön häs çömplétéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт"
" αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂#" " αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιη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 #: cms/templates/import.html
msgid "Select a .tar.gz File to Replace Your Course Content" 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 Ⱡ'σяєм ιρѕυм ∂σ#" msgstr "Séléçt ä .tär.gz Fïlé tö Répläçé Ýöür Çöürsé Çöntént Ⱡ'σяєм ιρѕυм ∂σ#"
...@@ -11919,6 +12022,11 @@ msgstr "" ...@@ -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 " "é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. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι#" "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/textbooks.html cms/templates/textbooks.html
#: cms/templates/widgets/header.html #: cms/templates/widgets/header.html
msgid "Textbooks" msgid "Textbooks"
...@@ -12137,10 +12245,6 @@ msgstr "" ...@@ -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 " "À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. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#" "äçtïvätïng ýöür äççöünt. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,#"
#: cms/templates/ux/reference/container.html
msgid "Container"
msgstr "Çöntäïnér #"
#: cms/templates/widgets/footer.html #: cms/templates/widgets/footer.html
msgid "All rights reserved." msgid "All rights reserved."
msgstr "Àll rïghts résérvéd. Ⱡ'σя#" msgstr "Àll rïghts résérvéd. Ⱡ'σя#"
......
...@@ -7,8 +7,8 @@ msgid "" ...@@ -7,8 +7,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 0.1a\n" "Project-Id-Version: 0.1a\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" "Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-02-27 08:56-0500\n" "POT-Creation-Date: 2014-02-28 13:56-0800\n"
"PO-Revision-Date: 2014-02-27 13:57:20.650962\n" "PO-Revision-Date: 2014-02-28 21:57:20.936431\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n" "Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
...@@ -1257,6 +1257,15 @@ msgstr "Lïst ïtém #" ...@@ -1257,6 +1257,15 @@ msgstr "Lïst ïtém #"
msgid "Heading" msgid "Heading"
msgstr "Héädïng #" 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 #: cms/static/coffee/src/main.js
msgid "" msgid ""
"This may be happening because of an error with our server or your internet " "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 Ⱡ'σ#" ...@@ -1276,6 +1285,7 @@ msgstr "<em>Édïtïng:</em> %s Ⱡ'σ#"
#: cms/static/coffee/src/views/module_edit.js #: 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/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/models/section.js cms/static/js/views/asset.js
#: cms/static/js/views/course_info_handout.js #: cms/static/js/views/course_info_handout.js
#: cms/static/js/views/course_info_update.js cms/static/js/views/overview.js #: cms/static/js/views/course_info_update.js cms/static/js/views/overview.js
......
"""
init.py file for class_dashboard
"""
"""
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
"""
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)
"""
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)
"""
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 ...@@ -23,7 +23,7 @@ from django_comment_client.utils import has_forum_access
from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR from django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from student.models import CourseEnrollment from student.models import CourseEnrollment
from bulk_email.models import CourseAuthorization 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 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 ...@@ -31,7 +31,7 @@ from .tools import get_units_with_due_date, title_or_url
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
def instructor_dashboard_2(request, course_id): 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) course = get_course_by_id(course_id, depth=None)
is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE) is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE)
...@@ -64,6 +64,10 @@ def instructor_dashboard_2(request, course_id): ...@@ -64,6 +64,10 @@ def instructor_dashboard_2(request, course_id):
is_studio_course and CourseAuthorization.instructor_email_enabled(course_id): is_studio_course and CourseAuthorization.instructor_email_enabled(course_id):
sections.append(_section_send_email(course_id, access, course)) 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 studio_url = None
if is_studio_course: if is_studio_course:
studio_url = get_cms_course_link(course) studio_url = get_cms_course_link(course)
...@@ -228,3 +232,15 @@ def _section_analytics(course_id, access): ...@@ -228,3 +232,15 @@ def _section_analytics(course_id, access):
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}), 'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
} }
return section_data 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 ( ...@@ -53,6 +53,7 @@ from instructor_task.api import (
) )
from instructor_task.views import get_task_completion_info from instructor_task.views import get_task_completion_info
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
from class_dashboard import dashboard_data
from psychometrics import psychoanalyze from psychometrics import psychoanalyze
from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user
from student.views import course_from_id from student.views import course_from_id
...@@ -818,6 +819,14 @@ def instructor_dashboard(request, course_id): ...@@ -818,6 +819,14 @@ def instructor_dashboard(request, course_id):
analytics_results[analytic_name] = get_analytics_result(analytic_name) 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? # offline grades?
if use_offline: if use_offline:
...@@ -900,7 +909,8 @@ def instructor_dashboard(request, course_id): ...@@ -900,7 +909,8 @@ def instructor_dashboard(request, course_id):
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), 'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
'analytics_results': analytics_results, 'analytics_results': analytics_results,
'disable_buttons': disable_buttons 'disable_buttons': disable_buttons,
'metrics_results': metrics_results,
} }
if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'): if settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA_DASHBOARD'):
......
...@@ -1246,6 +1246,11 @@ VERIFY_STUDENT = { ...@@ -1246,6 +1246,11 @@ VERIFY_STUDENT = {
"DAYS_GOOD_FOR": 365, # How many days is a verficiation good for? "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 ########################### ######################## CAS authentication ###########################
if FEATURES.get('AUTH_USE_CAS'): if FEATURES.get('AUTH_USE_CAS'):
......
...@@ -279,10 +279,12 @@ CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_P ...@@ -279,10 +279,12 @@ CC_PROCESSOR['CyberSource']['PURCHASE_ENDPOINT'] = os.environ.get('CYBERSOURCE_P
########################## USER API ######################## ########################## USER API ########################
EDX_API_KEY = None EDX_API_KEY = None
####################### Shoppingcart ########################### ####################### Shoppingcart ###########################
FEATURES['ENABLE_SHOPPING_CART'] = True 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. # Lastly, see if the developer has any local overrides.
try: try:
......
...@@ -269,6 +269,9 @@ PASSWORD_HASHERS = ( ...@@ -269,6 +269,9 @@ PASSWORD_HASHERS = (
# 'django.contrib.auth.hashers.CryptPasswordHasher', # 'django.contrib.auth.hashers.CryptPasswordHasher',
) )
### This enables the Metrics tab for the Instructor dashboard ###########
FEATURES['CLASS_DASHBOARD'] = True
################### Make tests quieter ################### Make tests quieter
# OpenID spews messages like this to stderr, we don't need to see them: # OpenID spews messages like this to stderr, we don't need to see them:
......
...@@ -170,6 +170,9 @@ setup_instructor_dashboard_sections = (idash_content) -> ...@@ -170,6 +170,9 @@ setup_instructor_dashboard_sections = (idash_content) ->
, ,
constructor: window.InstructorDashboard.sections.Analytics constructor: window.InstructorDashboard.sections.Analytics
$element: idash_content.find ".#{CSS_IDASH_SECTION}#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}) -> sections_to_initialize.map ({constructor, $element}) ->
......
# 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 @@ ...@@ -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 { ...@@ -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 { .profile-distribution-widget {
margin-bottom: $baseline * 2; margin-bottom: $baseline * 2;
......
<%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;
}
});
});
\ No newline at end of file
/*
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;
};
\ No newline at end of file
...@@ -144,6 +144,9 @@ function goto( mode) ...@@ -144,6 +144,9 @@ function goto( mode)
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_ANALYTICS'): %if settings.FEATURES.get('ENABLE_INSTRUCTOR_ANALYTICS'):
| <a href="#" onclick="goto('Analytics');" class="${modeflag.get('Analytics')}">${_("Analytics")}</a> | <a href="#" onclick="goto('Analytics');" class="${modeflag.get('Analytics')}">${_("Analytics")}</a>
%endif %endif
%if settings.FEATURES.get('CLASS_DASHBOARD'):
| <a href="#" onclick="goto('Metrics');" class="${modeflag.get('Metrics')}">${_("Metrics")}</a>
%endif
] ]
</h2> </h2>
...@@ -669,6 +672,46 @@ function goto( mode) ...@@ -669,6 +672,46 @@ function goto( mode)
%endif %endif
%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'): %if modeflag.get('Analytics In Progress'):
##This is not as helpful as it could be -- let's give full point distribution ##This is not as helpful as it could be -- let's give full point distribution
......
<%! 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
...@@ -375,6 +375,19 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA ...@@ -375,6 +375,19 @@ if settings.COURSEWARE_ENABLED and settings.FEATURES.get('ENABLE_INSTRUCTOR_BETA
include('instructor.views.api_urls')) 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'): if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
## Jasmine and admin ## Jasmine and admin
urlpatterns += (url(r'^admin/', include(admin.site.urls)),) urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment