Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion platform_plugin_aspects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
import os
from pathlib import Path

__version__ = "1.0.2"
__version__ = "1.1.0"

ROOT_DIRECTORY = Path(os.path.dirname(os.path.abspath(__file__)))
21 changes: 6 additions & 15 deletions platform_plugin_aspects/extensions/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from openedx_filters import PipelineStep
from web_fragments.fragment import Fragment

from platform_plugin_aspects.utils import _, generate_superset_context, get_model
from platform_plugin_aspects.utils import (
_,
generate_superset_context,
get_user_dashboard_locale,
)

TEMPLATE_ABSOLUTE_PATH = "/instructor_dashboard/"
BLOCK_CATEGORY = "aspects"
Expand All @@ -34,20 +38,7 @@ def run_filter(
show_dashboard_link = settings.SUPERSET_SHOW_INSTRUCTOR_DASHBOARD_LINK

user = get_current_user()

try:
user_language = (
get_model("user_preference").get_value(user, "pref-lang") or "en"
)
# If there is no user_preferences model defined this will get thrown
except AttributeError:
user_language = "en"

formatted_language = user_language.lower().replace("-", "_")
if formatted_language not in [
loc.lower().replace("-", "_") for loc in settings.SUPERSET_DASHBOARD_LOCALES
]:
formatted_language = "en"
formatted_language = get_user_dashboard_locale(user)

context["course_id"] = course.id
context = generate_superset_context(
Expand Down
36 changes: 5 additions & 31 deletions platform_plugin_aspects/extensions/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ def setUp(self) -> None:
self.course_id = "course-v1:org+course+run"
self.context = {"course": Mock(id=self.course_id), "sections": []}

@patch("platform_plugin_aspects.extensions.filters.get_model")
def test_run_filter_with_language(
@patch("platform_plugin_aspects.extensions.filters.get_user_dashboard_locale")
def test_run_filter(
self,
mock_get_model,
mock_get_user_dashboard_locale,
):
"""
Check the filter is not executed when there are no LimeSurvey blocks in the course.

Expected result:
- The context is returned without modifications.
"""
mock_get_model.return_value.get_value.return_value = "not-a-language"
mock_get_user_dashboard_locale.return_value = "en"

context = self.filter.run_filter(self.context, self.template_name)

Expand All @@ -47,30 +47,4 @@ def test_run_filter_with_language(
"template_path_prefix": "/instructor_dashboard/",
}.items() <= context["context"]["sections"][0].items()

mock_get_model.assert_called_once()

@patch("platform_plugin_aspects.extensions.filters.get_model")
def test_run_filter_without_language(
self,
mock_get_model,
):
"""
Check the filter is not executed when there are no LimeSurvey blocks in the course.

Expected result:
- The context is returned without modifications.
"""
mock_get_model.return_value.get_value.return_value = None

context = self.filter.run_filter(self.context, self.template_name)

assert {
"course_id": self.course_id,
"section_key": BLOCK_CATEGORY,
"section_display_name": "Reports",
"superset_url": "http://superset-dummy-url/",
"superset_guest_token_url": f"https://lms.url/superset_guest_token/{self.course_id}",
"template_path_prefix": "/instructor_dashboard/",
}.items() <= context["context"]["sections"][0].items()

mock_get_model.assert_called_once()
mock_get_user_dashboard_locale.assert_called_once()
37 changes: 37 additions & 0 deletions platform_plugin_aspects/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,40 @@ def plugin_settings(settings):
"model": "ObjectTag",
},
}

settings.ASPECTS_IN_CONTEXT_DASHBOARDS = {
"course": {
"name": _("Course"),
"slug": "in-context-course",
"uuid": "f2880cc1-63e9-48d7-ac3c-d2ff6f6698e2",
"allow_translations": True,
"course_filter_ids": ["NATIVE_FILTER-QLQbulmHH"],
"block_filter_ids": [],
},
"sequential": {
"name": _("Graded Subsection"),
"slug": "in-context-graded-subsection",
"uuid": "f0321087-6428-4b97-b32e-2dae7d9cc447",
"allow_translations": True,
"course_filter_ids": ["NATIVE_FILTER-oPAuR7ahy"],
"block_filter_ids": ["NATIVE_FILTER-CBWbI7uLq"],
},
"problem": {
"name": _("Problem"),
"slug": "in-context-problem",
"uuid": "98ff33ff-18dd-48f9-8c58-629ae4f4194b",
"allow_translations": True,
"course_filter_ids": ["NATIVE_FILTER-29CPcbirK"],
"block_filter_ids": ["NATIVE_FILTER-TJwItQhUI"],
},
"video": {
"name": _("Video"),
"slug": "in-context-video",
"uuid": "bc6510fb-027f-4026-a333-d0c42d3cc35c",
"allow_translations": True,
"course_filter_ids": ["NATIVE_FILTER-uaxvZkSAg"],
"block_filter_ids": ["NATIVE_FILTER-Fse4rzDW0"],
},
}
settings.IN_CONTEXT_DASHBOARD_COURSE_KEY_COLUMN = "course_key"
settings.IN_CONTEXT_DASHBOARD_BLOCK_ID_COLUMN = "block_id"
4 changes: 4 additions & 0 deletions platform_plugin_aspects/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ def plugin_settings(settings):
"EVENT_SINK_CLICKHOUSE_PII_MODELS",
settings.EVENT_SINK_CLICKHOUSE_PII_MODELS,
)
settings.ASPECTS_IN_CONTEXT_DASHBOARDS = settings.ENV_TOKENS.get(
"ASPECTS_IN_CONTEXT_DASHBOARDS",
settings.ASPECTS_IN_CONTEXT_DASHBOARDS,
)
21 changes: 21 additions & 0 deletions platform_plugin_aspects/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
get_ccx_courses,
get_model,
get_tags_for_block,
get_user_dashboard_locale,
)
from test_utils.helpers import course_factory

Expand Down Expand Up @@ -271,3 +272,23 @@ def mock_tag(taxonomy, value, i, parent=None):
course_tags = get_tags_for_block(course.location)
assert course_tags == [1, 2, 3, 4, 5]
mock_get_object_tags.assert_called_once_with(course.location)

@patch("platform_plugin_aspects.utils.get_model")
def test_get_user_dashboard_locale(self, mock_get_model):
"""Test that get_user_dashboard_locale gets user language with fallback to 'en'."""
mock_get_model.return_value.get_value.return_value = "es-419"
user = Mock()
assert get_user_dashboard_locale(user) == "es_419"
mock_get_model.assert_called_once()
mock_get_model.reset_mock()

mock_get_model.return_value.get_value.return_value = None
user = Mock()
assert get_user_dashboard_locale(user) == "en"
mock_get_model.assert_called_once()
mock_get_model.reset_mock()

mock_get_model.return_value.get_value.return_value = "not-a-language"
user = Mock()
assert get_user_dashboard_locale(user) == "en"
mock_get_model.assert_called_once()
118 changes: 115 additions & 3 deletions platform_plugin_aspects/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ def setUp(self):
super().setUp()
self.client = APIClient()
self.superset_guest_token_url = f"/superset_guest_token/{COURSE_ID}"
self.superset_in_context_dashboard_course_url = (
f"/superset_in_context_dashboard/{COURSE_ID}"
)
self.superset_in_context_dashboard_block_url = (
"/superset_in_context_dashboard/"
"block-v1:org+course+run+type@problem+block@e25d8eac15224f91bd3aa22bfe28a602"
)
self.user = User.objects.create(
username="user",
email="user@example.com",
Expand All @@ -54,8 +61,9 @@ def test_guest_token_invalid_course_id(self):
@patch("platform_plugin_aspects.views.get_model")
def test_guest_token_course_not_found(self, mock_get_model):
mock_model_get = Mock(side_effect=ObjectDoesNotExist)
mock_model_only = Mock(return_value=Mock(get=mock_model_get))
mock_get_model.return_value = Mock(
objects=Mock(get=mock_model_get),
objects=Mock(only=mock_model_only),
DoesNotExist=ObjectDoesNotExist,
)

Expand Down Expand Up @@ -102,8 +110,9 @@ def test_guest_token_with_course_overview(
mock_has_object_permission.return_value = True
mock_generate_guest_token.return_value = "test-token"
mock_model_get = Mock(return_value=Mock(display_name="Course Title"))
mock_model_only = Mock(return_value=Mock(get=mock_model_get))
mock_get_model.return_value = Mock(
objects=Mock(get=mock_model_get),
objects=Mock(only=mock_model_only),
DoesNotExist=ObjectDoesNotExist,
)

Expand All @@ -117,6 +126,109 @@ def test_guest_token_with_course_overview(
mock_generate_guest_token.assert_called_once_with(
user=self.user,
course=CourseKey.from_string(COURSE_ID),
dashboards=settings.ASPECTS_INSTRUCTOR_DASHBOARDS,
dashboards=(
settings.ASPECTS_INSTRUCTOR_DASHBOARDS
+ list(settings.ASPECTS_IN_CONTEXT_DASHBOARDS.values())
),
filters=DEFAULT_FILTERS_FORMAT,
)

def test_in_context_dashboard_requires_authorization(self):
response = self.client.get(self.superset_in_context_dashboard_course_url)
self.assertEqual(response.status_code, 403)

def test_in_context_dashboard_requires_course_access(self):
self.client.login(username="user", password="password")
response = self.client.get(self.superset_in_context_dashboard_course_url)
self.assertEqual(response.status_code, 403)

def test_in_context_dashboard_invalid_usage_key(self):
# Will fail as it is not a well-formed block id.
superset_in_context_dashboard_course_url = (
"/superset_in_context_dashboard/block-v1:org+course+run"
)
self.client.login(username="user", password="password")
response = self.client.get(superset_in_context_dashboard_course_url)
self.assertEqual(response.status_code, 404)

@patch("platform_plugin_aspects.views.get_model")
def test_in_context_dashboard_course_not_found(self, mock_get_model):
mock_model_get = Mock(side_effect=ObjectDoesNotExist)
mock_model_only = Mock(return_value=Mock(get=mock_model_get))
mock_get_model.return_value = Mock(
objects=Mock(only=mock_model_only),
DoesNotExist=ObjectDoesNotExist,
)

self.client.login(username="user", password="password")
response = self.client.get(self.superset_in_context_dashboard_course_url)
self.assertEqual(response.status_code, 404)
mock_model_get.assert_called_once()

@patch("platform_plugin_aspects.views.get_model")
def test_in_context_dashboard_block_course_not_found(self, mock_get_model):
mock_model_get = Mock(side_effect=ObjectDoesNotExist)
mock_model_only = Mock(return_value=Mock(get=mock_model_get))
mock_get_model.return_value = Mock(
objects=Mock(only=mock_model_only),
DoesNotExist=ObjectDoesNotExist,
)

self.client.login(username="user", password="password")
response = self.client.get(self.superset_in_context_dashboard_block_url)
self.assertEqual(response.status_code, 404)
mock_model_get.assert_called_once()

@patch.object(IsCourseStaffInstructor, "has_object_permission")
def test_in_context_dashboard_block_no_dashboard(self, mock_has_object_permission):
mock_has_object_permission.return_value = True
# Will be not found as test settings do not include dashboard for video blocks.
superset_in_context_dashboard_video_block_url = (
"/superset_in_context_dashboard/"
"block-v1:org+course+run+type@video+block@e25d8eac15224f91bd3aa22bfe28a602"
)
self.client.login(username="user", password="password")
response = self.client.get(superset_in_context_dashboard_video_block_url)
self.assertEqual(response.status_code, 404)

@patch.object(IsCourseStaffInstructor, "has_object_permission")
@patch("platform_plugin_aspects.views.get_localized_uuid")
def test_in_context_dashboard_course(
self, mock_get_localized_uuid, mock_has_object_permission
):
mock_has_object_permission.return_value = True
mock_get_localized_uuid.return_value = "00000000-0000-0000-0000-000000000000"

self.client.login(username="user", password="password")
response = self.client.get(self.superset_in_context_dashboard_course_url)

mock_has_object_permission.assert_called_once()

dashboard_uuid = settings.ASPECTS_IN_CONTEXT_DASHBOARDS["course"]["uuid"]
mock_get_localized_uuid.assert_called_once_with(dashboard_uuid, "en")

self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["dashboardId"], "00000000-0000-0000-0000-000000000000")
self.assertEqual(data["defaultCourseRun"], "run")

@patch.object(IsCourseStaffInstructor, "has_object_permission")
@patch("platform_plugin_aspects.views.get_localized_uuid")
def test_in_context_dashboard_block(
self, mock_get_localized_uuid, mock_has_object_permission
):
mock_has_object_permission.return_value = True
mock_get_localized_uuid.return_value = "00000000-0000-0000-0000-000000000000"

self.client.login(username="user", password="password")
response = self.client.get(self.superset_in_context_dashboard_block_url)

mock_has_object_permission.assert_called_once()

dashboard_uuid = settings.ASPECTS_IN_CONTEXT_DASHBOARDS["problem"]["uuid"]
mock_get_localized_uuid.assert_called_once_with(dashboard_uuid, "en")

self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["dashboardId"], "00000000-0000-0000-0000-000000000000")
self.assertEqual(data["defaultCourseRun"], "run")
11 changes: 10 additions & 1 deletion platform_plugin_aspects/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,23 @@

# Copied from openedx.core.constants
COURSE_ID_PATTERN = r"(?P<course_id>[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)"
# Copied from lms.envs.common
USAGE_ID_PATTERN = (
r"(?P<usage_id>(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))"
)

app_url_patterns = (
[
re_path(
rf"superset_guest_token/{COURSE_ID_PATTERN}/?$",
views.SupersetView.as_view(),
views.SupersetTokenView.as_view(),
name="superset_guest_token",
),
re_path(
rf"superset_in_context_dashboard/{USAGE_ID_PATTERN}/?$",
views.SupersetInContextDashboardView.as_view(),
name="superset_in_context_dashboard",
),
],
"platform_plugin_aspects",
)
Expand Down
Loading
Loading