Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add transformer to files downloaded event #35

Merged
merged 13 commits into from
Jan 5, 2024
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Backend for event-routing-backends library.

This is required since the library has explicit dependencies from openedx platform.
https://github.com/openedx/event-routing-backends
"""
from event_routing_backends.processors.xapi.registry import XApiTransformersRegistry # pylint: disable=import-error
from event_routing_backends.processors.xapi.transformer import XApiTransformer # pylint: disable=import-error


def get_xapi_transformer_registry():
"""Allow to get the XApiTransformersRegistry class from
https://github.com/openedx/event-routing-backends/blob/master/event_routing_backends/processors/xapi/registry.py#L7

Returns:
XApiTransformersRegistry class.
"""
return XApiTransformersRegistry


def get_xapi_transformer():
"""Allow to get the XApiTransformer class from
https://github.com/openedx/event-routing-backends/blob/master/event_routing_backends/processors/xapi/transformer.py#L27

Returns:
XApiTransformer class.
"""
return XApiTransformer
16 changes: 16 additions & 0 deletions filesmanager/edxapp_wrapper/event_routing_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Wrapper for event-routing-backends library.

This contains all the required dependencies from event-routing-backends.

Attributes:
XApiTransformer: Wrapper for the XApiTransformer class.
XApiTransformersRegistry: Wrapper for the XApiTransformersRegistry class.
"""
from importlib import import_module

from django.conf import settings

backend = import_module(settings.FILES_MANAGER_EVENT_ROUTING_BACKEND)

XApiTransformer = backend.get_xapi_transformer()
XApiTransformersRegistry = backend.get_xapi_transformer_registry()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Backend for event-routing-backends library.

This is required since the library has explicit dependencies from openedx platform.
https://github.com/openedx/event-routing-backends
"""
from unittest.mock import Mock


def get_xapi_transformer_registry():
"""Test backend for the XApiTransformersRegistry class.

Returns:
Mock class.
"""
XApiTransformersRegistry = Mock()
XApiTransformersRegistry.register.return_value = lambda x: x

return XApiTransformersRegistry


def get_xapi_transformer():
"""Test backend for the XApiTransformer class.

Returns:
Mock class.
"""
return Mock()
8 changes: 7 additions & 1 deletion filesmanager/filesmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader

from filesmanager.processors.xapi.event_transformers import FilesDownloadedTransformer # pylint: disable=unused-import
from filesmanager.tasks import create_zip_file_task

try:
Expand Down Expand Up @@ -231,9 +232,14 @@ def student_view(self, context=None):
if statici18n_js_url:
frag.add_javascript_url(self.runtime.local_resource_url(self, statici18n_js_url))

user = self.get_current_user()

js_context = {
"xblock_id": self.block_id,
"is_edit_view": False
"is_edit_view": False,
"course_id": self.course_id,
"user_id": user.opt_attrs.get("edx-platform.user_id"),
"username": user.opt_attrs.get("edx-platform.username"),
}

frag.add_javascript(js_content_parsed)
Expand Down
Empty file.
Empty file.
13 changes: 13 additions & 0 deletions filesmanager/processors/xapi/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Constants for xAPI specifications."""

# xAPI verbs
XAPI_VERB_DOWNLOADED = "http://id.tincanapi.com/verb/downloaded"

# xAPI activities
XAPI_ACTIVITY_FILE = "http://activitystrea.ms/schema/1.0/file"

# Languages
EN = "en"

# Display names
DOWNLOADED = "downloaded"
4 changes: 4 additions & 0 deletions filesmanager/processors/xapi/event_transformers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
All xAPI transformers.
"""
from filesmanager.processors.xapi.event_transformers.filesmanager_events import FilesDownloadedTransformer
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Transformers for filesmanager events.

Classes:
FilesDownloadedTransformer: Transformer for the event edunext.xblock.filesmanager.files.downloaded.
"""

from tincan import Activity, ActivityDefinition, LanguageMap, Verb

from filesmanager.edxapp_wrapper.event_routing_backends import XApiTransformer, XApiTransformersRegistry
from filesmanager.processors.xapi import constants


@XApiTransformersRegistry.register("edunext.xblock.filesmanager.files.downloaded")
class FilesDownloadedTransformer(XApiTransformer):
"""
Transformers for event generated when a student download files from xblock.
"""

_verb = Verb(
id=constants.XAPI_VERB_DOWNLOADED,
display=LanguageMap({constants.EN: constants.DOWNLOADED}),
)

def get_object(self):
"""
Get object for xAPI transformed event related to files download from xblock.

Returns:
`Activity`
"""
return Activity(
id=self.get_object_iri("xblock", self.get_data("data.xblock_id", True)),
definition=ActivityDefinition(
type=constants.XAPI_ACTIVITY_FILE,
),
)
3 changes: 2 additions & 1 deletion filesmanager/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
]


def plugin_settings(settings): # pylint: disable=unused-argument
def plugin_settings(settings):
BryanttV marked this conversation as resolved.
Show resolved Hide resolved
"""
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.FILES_MANAGER_EVENT_ROUTING_BACKEND = 'filesmanager.edxapp_wrapper.backends.event_routing_backends_p_v1'
8 changes: 7 additions & 1 deletion filesmanager/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
"""


def plugin_settings(settings): # pylint: disable=unused-argument
def plugin_settings(settings):
BryanttV marked this conversation as resolved.
Show resolved Hide resolved
"""
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.FILES_MANAGER_EVENT_ROUTING_BACKEND = getattr(
BryanttV marked this conversation as resolved.
Show resolved Hide resolved
settings, "ENV_TOKENS", {}
).get(
"FILES_MANAGER_EVENT_ROUTING_BACKEND",
settings.FILES_MANAGER_EVENT_ROUTING_BACKEND,
)
2 changes: 1 addition & 1 deletion filesmanager/static/html/bundle.js

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions react-app/components/FileManager/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import ErrorMessage from '@components/ErrorMessage';
import { sendTrackingLogEvent } from '@services/analyticsService';

import { useCustomFileMap, useFiles, useFolderChain, useFileActionHandler } from './hooks';
import { convertFileMapToTree } from './utils';
import { convertFileMapToTree, getMetadataFiles } from './utils';
import { prepareCustomFileMap, defaultFileActions, customFileActions, openFileAction } from './constants';

ChonkyActions.ToggleHiddenFiles.button.toolbar = false;
Expand All @@ -34,19 +34,24 @@ const FileManager = (props) => {
const [, setIsFetchLoading] = useState(false);
const [downloadFileErrorMessage, setDownloadFileErrorMessage] = useState(null);
const [reloadPage, setReloadPage] = useState(false);
const downloadFilesData = useRef(null);

const onFileDownloaded = () => {
setDownloadFileErrorMessage(null);
BryanttV marked this conversation as resolved.
Show resolved Hide resolved
const { isStudioView } = xBlockContext;
if(!isStudioView) {
/* change this as you need it */
sendTrackingLogEvent('edx.course.tool.accessed', {
course_id: 'courseId',
is_staff: 'administrator',
tool_name: 'analyticsId',
const fileContents = downloadFilesData.current;
const filesMetadata = getMetadataFiles(fileContents);
const { isStudioView, xblockId, courseId, userId, userName } = xBlockContext;
if (!isStudioView) {
sendTrackingLogEvent('edunext.xblock.filesmanager.files.downloaded', {
BryanttV marked this conversation as resolved.
Show resolved Hide resolved
course_id: courseId,
xblock_id: xblockId,
user_id: userId,
username: userName,
files_downloaded_metadata: filesMetadata,
created_at: new Date().toISOString()
});
}
}
};

const onError = () => {
const errorMessage = gettext('There was an error downloading the file');
Expand All @@ -69,6 +74,7 @@ const FileManager = (props) => {
isDir,
} = fileData;
const { hostname, port, protocol } = window.location;
downloadFilesData.current = fileData;
const fullUrl = port ? `${protocol}//${hostname}:${port}${url}` : `${protocol}//${hostname}${url}`;
if (isDir){
downloadFiles([fileData])
Expand Down
33 changes: 33 additions & 0 deletions react-app/components/FileManager/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const convertTreeToNewFileMap = (node, parent = null, newFileMapObject, isSaved
id: node.id,
name: node.name,
isDir: isDirectory,
path: node.path || '',
metadata: node.metadata || {},
isSaved
};
Expand Down Expand Up @@ -177,3 +178,35 @@ export const getFilesFromATree = (node) => {

return files;
};

/**
* Recursively extracts asset keys from a node object and its children.
* @param {object} node - The node object containing asset information.
* @param {string} [node.name] - The name of the node.
* @param {string} [node.path] - The path of the node.
* @param {object} [node.metadata] - The metadata object containing asset_key and external_url.
* @param {string} [node.metadata.asset_key] - The asset key associated with the node.
* @param {string} [node.metadata.external_url] - The external URL associated with the node.
* @returns {Array<object>} - An array of objects containing asset keys and related information.
*/
export const getMetadataFiles = (node) => {
const assetKeys = [];

const { name, path, metadata } = node;
if (metadata && metadata.asset_key) {
assetKeys.push({
name: name || '',
path: path || '',
asset_key: metadata.asset_key || '',
url: metadata.external_url || ''
});
}

if (node.children && node.children.length) {
node.children.forEach((child) => {
assetKeys.push(...getMetadataFiles(child));
});
}

return assetKeys;
BryanttV marked this conversation as resolved.
Show resolved Hide resolved
};
4 changes: 4 additions & 0 deletions react-app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ function FilesManagerXBlock(runtime, _, context) {
xBlockContext.element = elementSelector;
xBlockContext.context = context;
xBlockContext.isStudioView = elementSelector && typeRuntime !== 'LmsRuntime';
xBlockContext.xblockId = context.xblock_id;
xBlockContext.courseId = context.course_id;
xBlockContext.userId = context.user_id;
xBlockContext.userName = context.username;
xBlockContext.isEditView = context.is_edit_view;
const rootElement = document.getElementById('files-manager-app-root');
const root = ReactDOM.createRoot(rootElement);
Expand Down
1 change: 1 addition & 0 deletions react-app/services/analyticsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const sendTrackingLogEvent = (eventName, properties) => {
const snakeEventData = decamelizeKeys(properties);
const serverData = {
event_type: eventName,
courserun_key: properties.course_id,
event: JSON.stringify(snakeEventData),
page: pageHref,
};
Expand Down
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ XBlock
xblock-utils
celery
edx-opaque-keys
tincan
23 changes: 14 additions & 9 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#
amqp==5.2.0
# via kombu
aniso8601==9.0.1
# via tincan
appdirs==1.4.4
# via fs
asgiref==3.7.2
Expand All @@ -16,9 +18,9 @@ backports-zoneinfo[tzdata]==0.2.1
# kombu
billiard==4.2.0
# via celery
boto3==1.29.6
boto3==1.34.9
# via fs-s3fs
botocore==1.32.6
botocore==1.34.9
# via
# boto3
# s3transfer
Expand Down Expand Up @@ -66,7 +68,7 @@ kombu==5.3.4
# via celery
lazy==1.6
# via xblock
lxml==4.9.3
lxml==4.9.4
# via
# edx-i18n-tools
# xblock
Expand All @@ -79,15 +81,15 @@ markupsafe==2.1.3
# via
# mako
# xblock
openedx-django-pyfs==3.4.0
openedx-django-pyfs==3.4.1
# via xblock
path==16.7.1
path==16.9.0
# via edx-i18n-tools
pbr==6.0.0
# via stevedore
polib==1.2.0
# via edx-i18n-tools
prompt-toolkit==3.0.41
prompt-toolkit==3.0.43
# via click-repl
pymongo==3.13.0
# via edx-opaque-keys
Expand All @@ -99,12 +101,13 @@ python-dateutil==2.8.2
pytz==2023.3.post1
# via
# django
# tincan
# xblock
pyyaml==6.0.1
# via
# edx-i18n-tools
# xblock
s3transfer==0.7.0
s3transfer==0.10.0
# via boto3
simplejson==3.19.2
# via
Expand All @@ -119,7 +122,9 @@ sqlparse==0.4.4
# via django
stevedore==5.1.0
# via edx-opaque-keys
typing-extensions==4.8.0
tincan==1.0.0
# via -r requirements/base.in
typing-extensions==4.9.0
# via
# asgiref
# edx-opaque-keys
Expand All @@ -145,7 +150,7 @@ web-fragments==2.1.0
# xblock-utils
webob==1.8.7
# via xblock
xblock[django]==1.8.1
xblock[django]==1.9.0
# via
# -r requirements/base.in
# xblock-utils
Expand Down
Loading
Loading