def test_queryExecuteSelect(self): """ Tests if the query.executeSelect function works for an empty query, a query with only filters, a query with only selects and a query with both selects and filters """ # Setup test data self.maxDiff = None d = os.path.dirname(os.path.realpath(__file__)) f1 = open(f'{d}/testStatement.json') f2 = open(f'{d}/testStatement2.json') stms = [json.load(f1), json.load(f2)] # Test for a completely empty query query_empty = lrs.Query() self.assertEqual([{}, {}], list(query_empty.executeSelect(stms))) # Test for a query that contains only select statements query_selectonly = lrs.Query() query_selectonly.select(lrs.Attr.ACTOR, lrs.Attr.VERB, lrs.Attr.ACTIVITY) query_selectonly_correct = [{ lrs.Attr.ACTOR["key"]: '{"name": "User 1", "account": {"homePage": "http://moodle.boxinabox.nl", "name": "actor_ID"}}', lrs.Attr.VERB["key"]: "verb_ID", lrs.Attr.ACTIVITY["key"]: "object_ID" }, { lrs.Attr.ACTOR["key"]: '{"name": "User 1", "account": {"homePage": "http://moodle.boxinabox.nl", "name": "another_actor_ID"}}', lrs.Attr.VERB["key"]: "another_verb_ID", lrs.Attr.ACTIVITY["key"]: "another_object_ID" }] self.assertEqual(query_selectonly_correct, list(query_selectonly.executeSelect(stms))) # Test for a query that contains only filter statements query_filteronly = lrs.Query() query_filteronly.where(lrs.Attr.VERB, lrs.NOT_IS, "verb_ID") self.assertEqual([{}], list(query_filteronly.executeSelect(stms))) # Test for a query that contains both filter and select statements query_combined = lrs.Query() query_combined.select(lrs.Attr.ACTOR, lrs.Attr.VERB, lrs.Attr.ACTIVITY) query_combined.where(lrs.Attr.VERB, lrs.NOT_IS, "verb_ID") query_combined_correct = [{ lrs.Attr.ACTOR["key"]: '{"name": "User 1", "account": {"homePage": "http://moodle.boxinabox.nl", "name": "another_actor_ID"}}', lrs.Attr.VERB["key"]: "another_verb_ID", lrs.Attr.ACTIVITY["key"]: "another_object_ID" }] self.assertEqual(query_combined_correct, list(query_combined.executeSelect(stms)))
def test_getSimpleLrsData_Error(self, lrs_mock): """ Test if getSimpleLrsData handles an exception correctly """ # Setup mock lrs_mock.side_effect = lrs.LearningLockerException( f'mocked exception has been thrown') # Run the test result = lrs.getSimpleLrsData(lrs.Query()) correctResult = {"error": "mocked exception has been thrown"} self.assertEqual(correctResult, result)
def test_queryBuild(self): """ Tests if the query.build function works correctly with and without conflicting constraints """ # Test on non-conflicting query query1 = lrs.Query() query1.where(lrs.Attr.ACTOR, lrs.IS, "actor") query1.where(lrs.Attr.VERB, lrs.IS, "verb") query1.where(lrs.Attr.ACTIVITY, lrs.IS, "activity") query1.where(lrs.Attr.LIMIT, lrs.IS, "limit") query1Result = query1.build() query1Correct = "/data/xAPI/statements?format=ids&agent=actor&verb=verb&activity=activity&limit=limit" self.assertEqual(query1Correct, query1Result) # Test on conflicting query query2 = lrs.Query() query2.where(lrs.Attr.ACTIVITY, lrs.IS, "activity") query2.where(lrs.Attr.CONTEXTACTIVITY, lrs.IS, "contextActivity") query2Result = query2.build() query2Correct = "/data/xAPI/statements?format=ids&related_activities=true&activity=contextActivity" self.assertEqual(query2Correct, query2Result)
def getStatementData(courseID): """ Retrieves and cleans up the xAPI statements from the LRS. :param courseID: ID of the course to analyse \t :type courseID: str \n :param emodID: ID of the emodule of the course to analyse \t :type emodID: str \n :returns: collection of key value pairs of (userID, timestamp) emodule activity. \t :rtype: dict<str,[(Time,str)] \t """ return (lrs.Query( ).select(lrs.Attr.ACTOR, lrs.Attr.TIMESTAMP, lrs.Attr.ACTIVITY).where( lrs.Attr.CONTEXTACTIVITY, lrs.IS, f"{settings.MOODLE_BASE_URL}/course/view.php?id={courseID}").execute())
def getViewHistory(courseid): """ Run the query to get the data required for this analysis from the LRS \n :type courseid: str \n :returns: List of preprocessed statements (dictionaries) from the LRS \n :rtype: list<dict<string:mixed>> \n """ return (lrs.Query() .select(lrs.Attr.ACTIVITY, lrs.Attr.TIMESTAMP) .where(lrs.Attr.VERB, lrs.IS, "http://id.tincanapi.com/verb/viewed") .where(lrs.Attr.CONTEXTACTIVITY, lrs.IS, f"{settings.MOODLE_BASE_URL}/course/view.php?id={courseid}") .where(lrs.Attr.ACTIVITY, lrs.NOT_IS, f"{settings.MOODLE_BASE_URL}/course/view.php?id={courseid}") .where(lrs.Attr.ACTIVITY, lrs.CONTAINS, "view.php?id=") .execute() )
def getDTdata(scenarioID): """ Query the LRS for all the students that completed a specific scenario \n :param scenarioID: The id of the scenario that was completed \t :type scenarioID: string \n :returns: A list of scenario completions (xAPI statements), with for each completion the timestamp, the actor and the result \t :rtype: [dict] \n """ return (lrs.Query().where( lrs.Attr.ACTIVITY, lrs.IS, f"https://en.dialoguetrainer.app/scenario/play/{scenarioID}").where( lrs.Attr.VERB, lrs.IS, "https://adlnet.gov/expapi/verbs/completed").select( lrs.Attr.ACTOR, lrs.Attr.RESULT, lrs.Attr.TIMESTAMP).execute())
def runQuery(scenarioID): """ Run a query that aquires the data from the lrs for one specific dialoguetrainer scenario \n :param scenarioID: The id of the scenario to request the data from \t :type scenarioID: int \n :returns: The data for that scenario or error \t :rtype: [Dict<string, mixed>] | {error} \n """ return ( lrs.Query() .where(lrs.Attr.ACTIVITY, lrs.IS, f"https://en.dialoguetrainer.app/scenario/play/{scenarioID}") .where(lrs.Attr.VERB, lrs.IS, "https://adlnet.gov/expapi/verbs/completed") .select(lrs.Attr.ACTOR, lrs.Attr.RESULT) .execute() )
def runQuery(courseid): """ Run the query to get the data required for this analysis from the LRS \n :type courseid: str \n :returns: List of preprocessed statements (dictionaries) from the LRS \n :rtype: list<dict<string:mixed>> \n """ return (lrs.Query().where( lrs.Attr.CONTEXTACTIVITY, lrs.IS, f"{settings.MOODLE_BASE_URL}/course/view.php?id={courseid}").where( lrs.Attr.VERB, lrs.IS, "http://activitystrea.ms/schema/1.0/submit").where( lrs.Attr.ACTIVITY, lrs.NOT_IS, f"{settings.MOODLE_BASE_URL}/course/view.php?id={courseid}"). where(lrs.Attr.ACTIVITY, lrs.CONTAINS, "/mod/assign/").select(lrs.Attr.ACTIVITY, lrs.Attr.ACTOR).execute())
def test_getSimpleLrsData(self, lrs_mock): """ Test if getSimpleLrsData appends results correctly """ # Setup mocking function and test data d = os.path.dirname(os.path.realpath(__file__)) f1 = open(f'{d}/statementBatch1.json') f2 = open(f'{d}/statementBatch2.json') batch1 = MagicMock() batch1.text = f1.read() batch2 = MagicMock() batch2.text = f2.read() lrs_mock.side_effect = [batch1, batch2] query_empty = lrs.Query() # Run the test result = lrs.getSimpleLrsData(query_empty) self.assertEqual([{}, {}, {}, {}, {}, {}, {}], result)
def test_querySelect(self): """ Tests if the query.select function correctly adds the selects to the query """ # Test if selects are empty on init query = lrs.Query() self.assertEqual([], query.selects) # Test if an invalid select is ignored query.select(lrs.Attr.FORMAT) self.assertEqual([], query.selects) # Tests if a valid select is added to the selects list query.select(lrs.Attr.ACTOR) self.assertEqual([lrs.Attr.ACTOR], query.selects) # Test if additional selects are appended to the selects list query.select(lrs.Attr.VERB) self.assertEqual([lrs.Attr.ACTOR, lrs.Attr.VERB], query.selects)
def test_querySelectData(self): """ Tests if query.selectData gives the correct output for both empty and non empty select """ # Setup test data d = os.path.dirname(os.path.realpath(__file__)) f = open(f'{d}/testStatement.json') stm = json.load(f) query = lrs.Query() # Check if an empty select results in an empty statement self.assertEqual({}, query.selectData(stm)) # Check if a non-empty select actually selects the correct statements query.select(lrs.Attr.VERB, lrs.Attr.ACTIVITY) correctResult = { lrs.Attr.VERB["key"]: "verb_ID", lrs.Attr.ACTIVITY["key"]: "object_ID" } self.assertEqual(correctResult, query.selectData(stm))
def test_queryFilterData(self): """ Tests if the query.filterData function works correctly """ # Setup test data query = lrs.Query() d = os.path.dirname(os.path.realpath(__file__)) f = open(f'{d}/testStatement.json') stm = json.load(f) # Test if filters are indeed empty and if statements are accepted if the filters are empty self.assertEqual([], query.filters) self.assertEqual(True, query.filterData(stm)) # Test if the filters give a correct positive result query.where(lrs.Attr.ACTOR, lrs.CONTAINS, "actor") self.assertEqual(True, query.filterData(stm)) # Test if the filters give a correct negative result query.where(lrs.Attr.VERB, lrs.CONTAINS, "viewed") self.assertEqual(False, query.filterData(stm))
def test_queryWhere(self): """ Tests if the query.where functions works correctly for different cases """ query = lrs.Query() # setup testing data startParams = query.params finalParams1 = copy.deepcopy(startParams) finalParams2 = copy.deepcopy(startParams) finalParams1[lrs.Attr.ACTOR["param"]] = { "attr": lrs.Attr.ACTOR, "val": "test1" } finalParams2[lrs.Attr.ACTOR["param"]] = { "attr": lrs.Attr.ACTOR, "val": "test2" } finalFilters1 = [{ "select": lrs.Attr.ACTIVITY["selector"], "op": lrs.NOT_IS, "val": "object_ID" }] # Normal case testParams1 = query.where(lrs.Attr.ACTOR, lrs.IS, "test1") self.assertEqual(finalParams1, testParams1.params) self.assertEqual([], query.filters) # Param already exists case testParams2 = query.where(lrs.Attr.ACTOR, lrs.IS, "test2") self.assertEqual(finalParams2, testParams2.params) self.assertEqual([], testParams2.filters) # Not a param but a filter case testParams3 = query.where(lrs.Attr.ACTIVITY, lrs.NOT_IS, "object_ID") self.assertEqual(finalParams2, testParams3.params) self.assertEqual(finalFilters1, testParams3.filters)
def getPageViewData(courseid, actorid, untiltime): """ For one student, get all the pages viewed between the untiltime and 14 days earlier \n :param courseid: The course to which the pages should belong \t :type courseid: string \n :param actorid: The full LRS id of the student to get the page views for \t :type actorid: string (stringified json) \n :param untiltime: A "YYYY-MM-DDThh:mm:ssZ" formatted timestamp \t :type untiltime: string (datetime) \n :returns: A dictionary of pages visited pages and for each page the amount of visits \t :rtype: dict \n """ def stmInRange(lower, upper, stm): timestamp = datetime.fromisoformat(stm["timestamp"].replace( ".000Z", "")) return lower < timestamp and timestamp < upper untiltime = datetime.fromisoformat(untiltime.replace("Z", "")) sincetime = untiltime - timedelta(days=14) querydata = (lrs.Query().select( lrs.Attr.ACTIVITY, lrs.Attr.TIMESTAMP).where( lrs.Attr.VERB, lrs.IS, "http://id.tincanapi.com/verb/viewed").where( lrs.Attr.CONTEXTACTIVITY, lrs.IS, f"http://localhost/course/view.php?id={courseid}").where( lrs.Attr.ACTIVITY, lrs.CONTAINS, "/mod/page/view.php").where(lrs.Attr.ACTOR, lrs.IS, actorid).execute()) querydata = filter(lambda stm: stmInRange(sincetime, untiltime, stm), querydata) querydata = map(lambda stm: utils.getIdFromUrl(stm["activity"]), querydata) querydata = utils.groupOn(querydata, utils.id, lambda x: 1, lambda total, x: total + 1) return querydata
def test_queryFixConflict(self): """ Tests if the query.fixConflict function fixes the conflict correctly if there is one and does leaves the query unchanged if there is no conflict """ # Setup query query = lrs.Query() query.where(lrs.Attr.ACTOR, lrs.IS, "actor") # Test if no conflict situation leaves query intact correctparams1 = { "format": { "attr": lrs.Attr.FORMAT, "val": "ids" }, "agent": { "attr": lrs.Attr.ACTOR, "val": "actor" } } self.assertEqual(correctparams1, query.params) query.fixConflict(lrs.Attr.ACTOR, lrs.Attr.CONTEXTACTOR) self.assertEqual(correctparams1, query.params) # Test if a conflict can actually be created query.where(lrs.Attr.CONTEXTACTOR, lrs.IS, "contextActor") correctparams2 = { "format": { "attr": lrs.Attr.FORMAT, "val": "ids" }, "agent": { "attr": lrs.Attr.ACTOR, "val": "actor" }, "related_agents": { "attr": lrs.Attr.CONTEXTACTOR, "val": "contextActor" } } self.assertEqual(correctparams2, query.params) self.assertEqual([], query.filters) # Test if the conflict is fixed correctly query.fixConflict(lrs.Attr.ACTOR, lrs.Attr.CONTEXTACTOR) correctparams3 = { "format": { "attr": lrs.Attr.FORMAT, "val": "ids" }, "related_agents": { "attr": lrs.Attr.CONTEXTACTOR, "val": "contextActor" } } correctfilters = [{ "select": lrs.Attr.ACTOR["selector"], "op": lrs.IS, "val": "actor" }] self.assertEqual(correctparams3, query.params) self.assertEqual(correctfilters, query.filters)