Skip to content

Commit 92d381b

Browse files
authored
feat: add instructor dashboard integration (#35)
feat: Implement the functionality for instructor dashboard
1 parent bc4ee70 commit 92d381b

29 files changed

+614
-35
lines changed

.coveragerc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
[run]
33
data_file = .coverage
44
source = feedback
5-
omit = */urls.py
5+
omit =
6+
*/urls.py
7+
*/settings/*

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ __pycache__
99

1010
# Translations
1111
feedback/conf/locale/*/LC_MESSAGES/*
12+
build/
13+
dist/

MANIFEST.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
include CHANGELOG.rst
2+
include LICENSE.txt
3+
include README.rst
4+
include requirements/base.in
5+
include requirements/constraints.txt
6+
recursive-include feedback/locale *
7+
recursive-include feedback *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.txt

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ quality: ## Run the quality checks
2727
test: ## Run the tests
2828
mkdir -p var
2929
rm -rf .coverage
30-
DJANGO_SETTINGS_MODULE=test_settings python -m coverage run --rcfile=.coveragerc -m pytest
30+
DJANGO_SETTINGS_MODULE=feedback.settings.test python -m coverage run --rcfile=.coveragerc -m pytest
3131

3232
covreport: ## Show the coverage results
3333
python -m coverage report -m --skip-covered

README.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,41 @@ feedback.
3434
.. |Scale where good is in the middle| image:: happy_sad_happy_example.png
3535
.. |Numberical scale| image:: numerical_example.png
3636

37+
The instructors can view reports in their course instructor dashboard. The reports shows the count for every score, the average sentiment score, and the last 10 feedback comments.
38+
39+
Tutor configuration
40+
-------------------
41+
42+
To enable the FeedbackXBlock report in the instructor dashboard, you can use the following tutor inline plugins:
43+
44+
.. code-block:: yaml
45+
46+
name: feedback-xblock-settings
47+
version: 0.1.0
48+
patches:
49+
openedx-common-settings: |
50+
FEATURES["ENABLE_FEEDBACK_INSTRUCTOR_VIEW"] = True
51+
OPEN_EDX_FILTERS_CONFIG = {
52+
"org.openedx.learning.instructor.dashboard.render.started.v1": {
53+
"fail_silently": False,
54+
"pipeline": [
55+
"feedback.extensions.filters.AddFeedbackTab",
56+
]
57+
},
58+
}
59+
60+
To enable this plugin you need to create a file called *feedback-xblock-settings.yml* in your tutor plugins directory of your tutor instance
61+
with the content of the previous code block, and run the following commands.
62+
63+
.. code-block:: bash
64+
65+
tutor plugins enable feedback-xblock-settings
66+
tutor config save
67+
68+
69+
You can find more information about tutor plugins in the Tutor `plugins`_ documentation.
70+
71+
.. _plugins: https://docs.tutor.edly.io/tutorials/plugin.html
3772

3873
Getting Started
3974
===============

feedback/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
course resources, and to think and synthesize about their experience
44
in the course.
55
"""
6+
import os
7+
from pathlib import Path
8+
9+
ROOT_DIRECTORY = Path(os.path.dirname(os.path.abspath(__file__)))

feedback/apps.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
forum_email_notifier Django application initialization.
3+
"""
4+
5+
from django.apps import AppConfig
6+
7+
8+
class FeedbackConfig(AppConfig):
9+
"""
10+
Configuration for the feedback Django application.
11+
"""
12+
13+
name = "feedback"
14+
15+
plugin_app = {
16+
"settings_config": {
17+
"lms.djangoapp": {
18+
"common": {"relative_path": "settings.common"},
19+
"test": {"relative_path": "settings.test"},
20+
"production": {"relative_path": "settings.production"},
21+
},
22+
"cms.djangoapp": {
23+
"common": {"relative_path": "settings.common"},
24+
"test": {"relative_path": "settings.test"},
25+
"production": {"relative_path": "settings.production"},
26+
},
27+
},
28+
}

feedback/extensions/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Open edX filters extensions module.
3+
"""

feedback/extensions/filters.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""
2+
Open edX Filters needed for instructor dashboard integration.
3+
"""
4+
import pkg_resources
5+
from crum import get_current_request
6+
from django.conf import settings
7+
from django.template import Context, Template
8+
from openedx_filters import PipelineStep
9+
from web_fragments.fragment import Fragment
10+
11+
try:
12+
from cms.djangoapps.contentstore.utils import get_lms_link_for_item
13+
from lms.djangoapps.courseware.block_render import (get_block_by_usage_id,
14+
load_single_xblock)
15+
from openedx.core.djangoapps.enrollments.data import get_user_enrollments
16+
from xmodule.modulestore.django import modulestore
17+
except ImportError:
18+
load_single_xblock = None
19+
get_block_by_usage_id = None
20+
modulestore = None
21+
get_user_enrollments = None
22+
get_lms_link_for_item = None
23+
24+
TEMPLATE_ABSOLUTE_PATH = "/instructor_dashboard/"
25+
BLOCK_CATEGORY = "feedback"
26+
TEMPLATE_CATEGORY = "feedback_instructor"
27+
28+
29+
class AddFeedbackTab(PipelineStep):
30+
"""Add forum_notifier tab to instructor dashboard by adding a new context with feedback data."""
31+
32+
def run_filter(
33+
self, context, template_name
34+
): # pylint: disable=unused-argument, arguments-differ
35+
"""Execute filter that modifies the instructor dashboard context.
36+
Args:
37+
context (dict): the context for the instructor dashboard.
38+
_ (str): instructor dashboard template name.
39+
"""
40+
if not settings.FEATURES.get("ENABLE_FEEDBACK_INSTRUCTOR_VIEW", False):
41+
return {
42+
"context": context,
43+
}
44+
45+
course = context["course"]
46+
template = Template(
47+
self.resource_string(f"static/html/{TEMPLATE_CATEGORY}.html")
48+
)
49+
50+
request = get_current_request()
51+
52+
context.update(
53+
{
54+
"blocks": load_blocks(request, course),
55+
}
56+
)
57+
58+
html = template.render(Context(context))
59+
frag = Fragment(html)
60+
frag.add_css(self.resource_string(f"static/css/{TEMPLATE_CATEGORY}.css"))
61+
frag.add_javascript(
62+
self.resource_string(f"static/js/src/{TEMPLATE_CATEGORY}.js")
63+
)
64+
65+
section_data = {
66+
"fragment": frag,
67+
"section_key": TEMPLATE_CATEGORY,
68+
"section_display_name": "Course Feedback",
69+
"course_id": str(course.id),
70+
"template_path_prefix": TEMPLATE_ABSOLUTE_PATH,
71+
}
72+
context["sections"].append(section_data)
73+
74+
return {"context": context}
75+
76+
def resource_string(self, path):
77+
"""Handy helper for getting resources from our kit."""
78+
data = pkg_resources.resource_string("feedback", path)
79+
return data.decode("utf8")
80+
81+
82+
def load_blocks(request, course):
83+
"""
84+
Load feedback blocks for a given course for all enrolled students.
85+
86+
Arguments:
87+
request (HttpRequest): Django request object.
88+
course (CourseLocator): Course locator object.
89+
"""
90+
course_id = str(course.id)
91+
92+
feedback_blocks = modulestore().get_items(
93+
course.id, qualifiers={"category": BLOCK_CATEGORY}
94+
)
95+
96+
blocks = []
97+
98+
if not feedback_blocks:
99+
return []
100+
101+
students = get_user_enrollments(course_id).values_list("user_id", "user__username")
102+
for feedback_block in feedback_blocks:
103+
block, _ = get_block_by_usage_id(
104+
request,
105+
str(course.id),
106+
str(feedback_block.location),
107+
disable_staff_debug_info=True,
108+
course=course,
109+
)
110+
answers = load_xblock_answers(
111+
request,
112+
students,
113+
str(course.location.course_key),
114+
str(feedback_block.location),
115+
course,
116+
)
117+
118+
vote_aggregate = []
119+
total_votes = 0
120+
total_answers = 0
121+
122+
if not block.vote_aggregate:
123+
block.vote_aggregate = [0] * len(block.get_prompt()["scale_text"])
124+
for index, vote in enumerate(block.vote_aggregate):
125+
vote_aggregate.append(
126+
{
127+
"scale_text": block.get_prompt()["scale_text"][index],
128+
"count": vote,
129+
}
130+
)
131+
total_answers += vote
132+
# We have an inverted scale, so we need to invert the index
133+
# to get the correct average rating.
134+
# Excellent = 1, Very Good = 2, Good = 3, Fair = 4, Poor = 5
135+
# So Excellent = 5, Very Good = 4, Good = 3, Fair = 2, Poor = 1
136+
total_votes += vote * (5 - index)
137+
138+
try:
139+
average_rating = round(total_votes / total_answers, 2)
140+
except ZeroDivisionError:
141+
average_rating = 0
142+
143+
parent, _ = get_block_by_usage_id(
144+
request,
145+
str(course.id),
146+
str(feedback_block.parent),
147+
disable_staff_debug_info=True,
148+
course=course,
149+
)
150+
151+
blocks.append(
152+
{
153+
"display_name": block.display_name,
154+
"prompts": block.prompts,
155+
"vote_aggregate": vote_aggregate,
156+
"answers": answers[-10:],
157+
"parent": parent.display_name,
158+
"average_rating": average_rating,
159+
"url": get_lms_link_for_item(block.location),
160+
}
161+
)
162+
return blocks
163+
164+
165+
def load_xblock_answers(request, students, course_id, block_id, course):
166+
"""
167+
Load answers for a given feedback xblock instance.
168+
169+
Arguments:
170+
request (HttpRequest): Django request object.
171+
students (list): List of enrolled students.
172+
course_id (str): Course ID.
173+
block_id (str): Block ID.
174+
course (CourseDescriptor): Course descriptor.
175+
"""
176+
answers = []
177+
for user_id, username in students:
178+
student_xblock_instance = load_single_xblock(
179+
request, user_id, course_id, block_id, course
180+
)
181+
if student_xblock_instance:
182+
prompt = student_xblock_instance.get_prompt()
183+
if student_xblock_instance.user_freeform:
184+
if student_xblock_instance.user_vote != -1:
185+
vote = prompt["scale_text"][student_xblock_instance.user_vote]
186+
else:
187+
vote = "No vote"
188+
answers.append(
189+
{
190+
"username": username,
191+
"user_vote": vote,
192+
"user_freeform": student_xblock_instance.user_freeform,
193+
}
194+
)
195+
196+
return answers

feedback/settings/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)