Skip to content

Commit

Permalink
feat: add instructor dashboard integration (#35)
Browse files Browse the repository at this point in the history
feat: Implement the functionality for instructor dashboard
  • Loading branch information
Ian2012 authored Dec 9, 2023
1 parent bc4ee70 commit 92d381b
Show file tree
Hide file tree
Showing 29 changed files with 614 additions and 35 deletions.
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
[run]
data_file = .coverage
source = feedback
omit = */urls.py
omit =
*/urls.py
*/settings/*
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ __pycache__

# Translations
feedback/conf/locale/*/LC_MESSAGES/*
build/
dist/
7 changes: 7 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include CHANGELOG.rst
include LICENSE.txt
include README.rst
include requirements/base.in
include requirements/constraints.txt
recursive-include feedback/locale *
recursive-include feedback *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.txt
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ quality: ## Run the quality checks
test: ## Run the tests
mkdir -p var
rm -rf .coverage
DJANGO_SETTINGS_MODULE=test_settings python -m coverage run --rcfile=.coveragerc -m pytest
DJANGO_SETTINGS_MODULE=feedback.settings.test python -m coverage run --rcfile=.coveragerc -m pytest

covreport: ## Show the coverage results
python -m coverage report -m --skip-covered
Expand Down
35 changes: 35 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,41 @@ feedback.
.. |Scale where good is in the middle| image:: happy_sad_happy_example.png
.. |Numberical scale| image:: numerical_example.png

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.

Tutor configuration
-------------------

To enable the FeedbackXBlock report in the instructor dashboard, you can use the following tutor inline plugins:

.. code-block:: yaml
name: feedback-xblock-settings
version: 0.1.0
patches:
openedx-common-settings: |
FEATURES["ENABLE_FEEDBACK_INSTRUCTOR_VIEW"] = True
OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.learning.instructor.dashboard.render.started.v1": {
"fail_silently": False,
"pipeline": [
"feedback.extensions.filters.AddFeedbackTab",
]
},
}
To enable this plugin you need to create a file called *feedback-xblock-settings.yml* in your tutor plugins directory of your tutor instance
with the content of the previous code block, and run the following commands.

.. code-block:: bash
tutor plugins enable feedback-xblock-settings
tutor config save
You can find more information about tutor plugins in the Tutor `plugins`_ documentation.

.. _plugins: https://docs.tutor.edly.io/tutorials/plugin.html

Getting Started
===============
Expand Down
4 changes: 4 additions & 0 deletions feedback/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
course resources, and to think and synthesize about their experience
in the course.
"""
import os
from pathlib import Path

ROOT_DIRECTORY = Path(os.path.dirname(os.path.abspath(__file__)))
28 changes: 28 additions & 0 deletions feedback/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
forum_email_notifier Django application initialization.
"""

from django.apps import AppConfig


class FeedbackConfig(AppConfig):
"""
Configuration for the feedback Django application.
"""

name = "feedback"

plugin_app = {
"settings_config": {
"lms.djangoapp": {
"common": {"relative_path": "settings.common"},
"test": {"relative_path": "settings.test"},
"production": {"relative_path": "settings.production"},
},
"cms.djangoapp": {
"common": {"relative_path": "settings.common"},
"test": {"relative_path": "settings.test"},
"production": {"relative_path": "settings.production"},
},
},
}
3 changes: 3 additions & 0 deletions feedback/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Open edX filters extensions module.
"""
196 changes: 196 additions & 0 deletions feedback/extensions/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"""
Open edX Filters needed for instructor dashboard integration.
"""
import pkg_resources
from crum import get_current_request
from django.conf import settings
from django.template import Context, Template
from openedx_filters import PipelineStep
from web_fragments.fragment import Fragment

try:
from cms.djangoapps.contentstore.utils import get_lms_link_for_item
from lms.djangoapps.courseware.block_render import (get_block_by_usage_id,
load_single_xblock)
from openedx.core.djangoapps.enrollments.data import get_user_enrollments
from xmodule.modulestore.django import modulestore
except ImportError:
load_single_xblock = None
get_block_by_usage_id = None
modulestore = None
get_user_enrollments = None
get_lms_link_for_item = None

TEMPLATE_ABSOLUTE_PATH = "/instructor_dashboard/"
BLOCK_CATEGORY = "feedback"
TEMPLATE_CATEGORY = "feedback_instructor"


class AddFeedbackTab(PipelineStep):
"""Add forum_notifier tab to instructor dashboard by adding a new context with feedback data."""

def run_filter(
self, context, template_name
): # pylint: disable=unused-argument, arguments-differ
"""Execute filter that modifies the instructor dashboard context.
Args:
context (dict): the context for the instructor dashboard.
_ (str): instructor dashboard template name.
"""
if not settings.FEATURES.get("ENABLE_FEEDBACK_INSTRUCTOR_VIEW", False):
return {
"context": context,
}

course = context["course"]
template = Template(
self.resource_string(f"static/html/{TEMPLATE_CATEGORY}.html")
)

request = get_current_request()

context.update(
{
"blocks": load_blocks(request, course),
}
)

html = template.render(Context(context))
frag = Fragment(html)
frag.add_css(self.resource_string(f"static/css/{TEMPLATE_CATEGORY}.css"))
frag.add_javascript(
self.resource_string(f"static/js/src/{TEMPLATE_CATEGORY}.js")
)

section_data = {
"fragment": frag,
"section_key": TEMPLATE_CATEGORY,
"section_display_name": "Course Feedback",
"course_id": str(course.id),
"template_path_prefix": TEMPLATE_ABSOLUTE_PATH,
}
context["sections"].append(section_data)

return {"context": context}

def resource_string(self, path):
"""Handy helper for getting resources from our kit."""
data = pkg_resources.resource_string("feedback", path)
return data.decode("utf8")


def load_blocks(request, course):
"""
Load feedback blocks for a given course for all enrolled students.
Arguments:
request (HttpRequest): Django request object.
course (CourseLocator): Course locator object.
"""
course_id = str(course.id)

feedback_blocks = modulestore().get_items(
course.id, qualifiers={"category": BLOCK_CATEGORY}
)

blocks = []

if not feedback_blocks:
return []

students = get_user_enrollments(course_id).values_list("user_id", "user__username")
for feedback_block in feedback_blocks:
block, _ = get_block_by_usage_id(
request,
str(course.id),
str(feedback_block.location),
disable_staff_debug_info=True,
course=course,
)
answers = load_xblock_answers(
request,
students,
str(course.location.course_key),
str(feedback_block.location),
course,
)

vote_aggregate = []
total_votes = 0
total_answers = 0

if not block.vote_aggregate:
block.vote_aggregate = [0] * len(block.get_prompt()["scale_text"])
for index, vote in enumerate(block.vote_aggregate):
vote_aggregate.append(
{
"scale_text": block.get_prompt()["scale_text"][index],
"count": vote,
}
)
total_answers += vote
# We have an inverted scale, so we need to invert the index
# to get the correct average rating.
# Excellent = 1, Very Good = 2, Good = 3, Fair = 4, Poor = 5
# So Excellent = 5, Very Good = 4, Good = 3, Fair = 2, Poor = 1
total_votes += vote * (5 - index)

try:
average_rating = round(total_votes / total_answers, 2)
except ZeroDivisionError:
average_rating = 0

parent, _ = get_block_by_usage_id(
request,
str(course.id),
str(feedback_block.parent),
disable_staff_debug_info=True,
course=course,
)

blocks.append(
{
"display_name": block.display_name,
"prompts": block.prompts,
"vote_aggregate": vote_aggregate,
"answers": answers[-10:],
"parent": parent.display_name,
"average_rating": average_rating,
"url": get_lms_link_for_item(block.location),
}
)
return blocks


def load_xblock_answers(request, students, course_id, block_id, course):
"""
Load answers for a given feedback xblock instance.
Arguments:
request (HttpRequest): Django request object.
students (list): List of enrolled students.
course_id (str): Course ID.
block_id (str): Block ID.
course (CourseDescriptor): Course descriptor.
"""
answers = []
for user_id, username in students:
student_xblock_instance = load_single_xblock(
request, user_id, course_id, block_id, course
)
if student_xblock_instance:
prompt = student_xblock_instance.get_prompt()
if student_xblock_instance.user_freeform:
if student_xblock_instance.user_vote != -1:
vote = prompt["scale_text"][student_xblock_instance.user_vote]
else:
vote = "No vote"
answers.append(
{
"username": username,
"user_vote": vote,
"user_freeform": student_xblock_instance.user_freeform,
}
)

return answers
Empty file added feedback/settings/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions feedback/settings/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Common Django settings for eox_hooks project.
For more information on this file, see
https://docs.djangoproject.com/en/2.22/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.22/ref/settings/
"""
from feedback import ROOT_DIRECTORY

INSTALLED_APPS = [
"feedback",
]


def plugin_settings(settings):
"""
Set of plugin settings used by the Open Edx platform.
More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst
"""
settings.MAKO_TEMPLATE_DIRS_BASE.append(ROOT_DIRECTORY / "templates")
14 changes: 14 additions & 0 deletions feedback/settings/production.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
Common Django settings for eox_hooks project.
For more information on this file, see
https://docs.djangoproject.com/en/2.22/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.22/ref/settings/
"""


def plugin_settings(settings): # pylint: disable=unused-argument
"""
Set of plugin settings used by the Open Edx platform.
More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst
"""
24 changes: 24 additions & 0 deletions feedback/settings/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Common Test settings for eox_hooks project.
For more information on this file, see
https://docs.djangoproject.com/en/2.22/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.22/ref/settings/
"""
from workbench.settings import * # pylint: disable=wildcard-import

from django.conf.global_settings import LOGGING

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'feedback',
'workbench',
]

FEATURES = {
"ENABLE_FEEDBACK_INSTRUCTOR_VIEW": True,
}

SECRET_KEY = 'fake-key'
9 changes: 9 additions & 0 deletions feedback/static/css/feedback_instructor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.feedback-instructor-wrapper {
width: 100%;
display: flex;
flex-direction: column;
}

.go-back {
margin: 16px 0px 16px 0px;
}
Loading

0 comments on commit 92d381b

Please sign in to comment.