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

Fix #110: Filter questions required for assessment in QuestionTrainingController #227

Merged
merged 25 commits into from
Oct 25, 2019
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9594954
Initial check in for the question data controller
vinitamurthi Oct 8, 2019
6ee7aec
Removed progress controller test
vinitamurthi Oct 8, 2019
4596e86
Review changes
vinitamurthi Oct 9, 2019
f5c23c8
Review changes
vinitamurthi Oct 10, 2019
53d367d
Pass data providers to the assessment controller, and set caps in the…
vinitamurthi Oct 11, 2019
86630b6
Fixed merge conflicts
vinitamurthi Oct 11, 2019
a8adf93
Removed unnecessary imports/variables
vinitamurthi Oct 11, 2019
a7a10f3
Add fake question data for stubbed interfaces
vinitamurthi Oct 11, 2019
459e3e9
Remove duplicate questions while fetching in training controller
vinitamurthi Oct 11, 2019
73c49df
Comment explaining the filter function
vinitamurthi Oct 11, 2019
91cf8e6
Improve duplicate checking - check it while filtering instead of afte…
vinitamurthi Oct 11, 2019
54546d7
Add linked skill id values
vinitamurthi Oct 11, 2019
be375fd
Review changes
vinitamurthi Oct 15, 2019
64b8265
add a new module for questions constants
vinitamurthi Oct 18, 2019
75405a2
fix merge conflicts
vinitamurthi Oct 18, 2019
4878d10
Review changes
vinitamurthi Oct 18, 2019
c37f14b
add a test to verify questions were fetched properly
vinitamurthi Oct 18, 2019
d1bd4a6
reformatted code
vinitamurthi Oct 18, 2019
1f96f0c
Merge remote-tracking branch 'origin/develop' into question_data
vinitamurthi Oct 25, 2019
38147cb
Review changes
vinitamurthi Oct 25, 2019
0d1d65e
remove mockmaker
vinitamurthi Oct 25, 2019
10dbeff
new line for questionstrainingconstants
vinitamurthi Oct 25, 2019
5b768b2
Add another test to ensure that a different test is created when the …
vinitamurthi Oct 25, 2019
3cb73e1
Review changes
vinitamurthi Oct 25, 2019
f2b5bc1
Remove unused imports
vinitamurthi Oct 25, 2019
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
444 changes: 444 additions & 0 deletions domain/src/main/assets/sample_questions.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
package org.oppia.domain.exploration

import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import org.oppia.app.model.AnswerGroup
import org.oppia.app.model.Exploration
import org.oppia.app.model.Interaction
import org.oppia.app.model.InteractionObject
import org.oppia.app.model.Outcome
import org.oppia.app.model.RuleSpec
import org.oppia.app.model.State
import org.oppia.app.model.StringList
import org.oppia.app.model.SubtitledHtml
import org.oppia.app.model.Voiceover
import org.oppia.app.model.VoiceoverMapping
import org.oppia.domain.util.JsonAssetRetriever
import org.oppia.domain.util.StateRetriever
import java.io.IOException
import javax.inject.Inject

Expand All @@ -24,7 +15,9 @@ const val TEST_EXPLORATION_ID_6 = "test_exp_id_6"
// to depend on this utility.

/** Internal class for actually retrieving an exploration object for uses in domain controllers. */
class ExplorationRetriever @Inject constructor(private val context: Context) {
class ExplorationRetriever @Inject constructor(
private val jsonAssetRetriever: JsonAssetRetriever,
private val stateRetriever: StateRetriever) {
/** Loads and returns an exploration for the specified exploration ID, or fails. */
@Suppress("RedundantSuspendModifier") // Force callers to call this on a background thread.
internal suspend fun loadExploration(explorationId: String): Exploration {
Expand All @@ -38,7 +31,7 @@ class ExplorationRetriever @Inject constructor(private val context: Context) {
// Returns an exploration given an assetName
private fun loadExplorationFromAsset(assetName: String): Exploration {
try {
val explorationObject = loadJsonFromAsset(assetName) ?: return Exploration.getDefaultInstance()
val explorationObject = jsonAssetRetriever.loadJsonFromAsset(assetName) ?: return Exploration.getDefaultInstance()
return Exploration.newBuilder()
.setTitle(explorationObject.getString("title"))
.setLanguageCode(explorationObject.getString("language_code"))
Expand All @@ -51,243 +44,16 @@ class ExplorationRetriever @Inject constructor(private val context: Context) {
}
}

// Returns a JSON Object if it exists, else returns null
private fun getJsonObject(parentObject: JSONObject, key: String): JSONObject? {
return parentObject.optJSONObject(key)
}

// Loads the JSON string from an asset and converts it to a JSONObject
@Throws(IOException::class)
private fun loadJsonFromAsset(assetName: String): JSONObject? {
val assetManager = context.assets
val jsonContents = assetManager.open(assetName).bufferedReader().use { it.readText() }
return JSONObject(jsonContents)
}

// Creates the states map from JSON
private fun createStatesFromJsonObject(statesJsonObject: JSONObject?): MutableMap<String, State> {
val statesMap: MutableMap<String, State> = mutableMapOf()
val statesKeys = statesJsonObject?.keys() ?: return statesMap
val statesIterator = statesKeys.iterator()
while (statesIterator.hasNext()) {
val key = statesIterator.next()
statesMap[key] = createStateFromJson(key, statesJsonObject.getJSONObject(key))
statesMap[key] = stateRetriever.createStateFromJson(key, statesJsonObject.getJSONObject(key))
}
return statesMap
}

// Creates a single state object from JSON
private fun createStateFromJson(stateName: String, stateJson: JSONObject?): State {
val state = State.newBuilder()
.setName(stateName)
.setContent(
SubtitledHtml.newBuilder().setHtml(
stateJson?.getJSONObject("content")?.getString("html")
).setContentId(
stateJson?.getJSONObject("content")?.optString("content_id")
)
)
.setInteraction(createInteractionFromJson(stateJson?.getJSONObject("interaction")))

if (stateJson != null && stateJson.has("recorded_voiceovers")) {
addVoiceOverMappings(stateJson.getJSONObject("recorded_voiceovers"), state)
}

return state.build()
}

// Adds VoiceoverMappings to state builder
private fun addVoiceOverMappings(recordedVoiceovers: JSONObject, stateBuilder: State.Builder) {
val voiceoverMappingJson = recordedVoiceovers.getJSONObject("voiceovers_mapping")
voiceoverMappingJson?.let {
for (key in it.keys()) {
val voiceoverMapping = VoiceoverMapping.newBuilder()
val voiceoverJson = it.getJSONObject(key)
for (lang in voiceoverJson.keys()) {
voiceoverMapping.putVoiceoverMapping(lang, createVoiceOverFromJson(voiceoverJson.getJSONObject(lang)))
}
stateBuilder.putRecordedVoiceovers(key, voiceoverMapping.build())
}
}
}

// Creates a Voiceover from Json
private fun createVoiceOverFromJson(voiceoverJson: JSONObject): Voiceover {
return Voiceover.newBuilder()
.setNeedsUpdate(voiceoverJson.getBoolean("needs_update"))
.setFileName(voiceoverJson.getString("filename"))
.build()
}

// Creates an interaction from JSON
private fun createInteractionFromJson(interactionJson: JSONObject?): Interaction {
if (interactionJson == null) {
return Interaction.getDefaultInstance()
}
return Interaction.newBuilder()
.setId(interactionJson.getString("id"))
.addAllAnswerGroups(
createAnswerGroupsFromJson(
interactionJson.getJSONArray("answer_groups"),
interactionJson.getString("id")
)
)
.addAllConfirmedUnclassifiedAnswers(
createAnswerGroupsFromJson(
interactionJson.getJSONArray("confirmed_unclassified_answers"),
interactionJson.getString("id")
)
)
.setDefaultOutcome(
createOutcomeFromJson(
getJsonObject(interactionJson, "default_outcome")
)
)
.putAllCustomizationArgs(
createCustomizationArgsMapFromJson(
getJsonObject(interactionJson, "customization_args")
)
)
.build()
}

// Creates the list of answer group objects from JSON
private fun createAnswerGroupsFromJson(
answerGroupsJson: JSONArray?, interactionId: String
): MutableList<AnswerGroup> {
val answerGroups = mutableListOf<AnswerGroup>()
if (answerGroupsJson == null) {
return answerGroups
}
for (i in 0 until answerGroupsJson.length()) {
answerGroups.add(
createSingleAnswerGroupFromJson(
answerGroupsJson.getJSONObject(i), interactionId
)
)
}
return answerGroups
}

// Creates a single answer group object from JSON
private fun createSingleAnswerGroupFromJson(
answerGroupJson: JSONObject, interactionId: String
): AnswerGroup {
return AnswerGroup.newBuilder()
.setOutcome(
createOutcomeFromJson(answerGroupJson.getJSONObject("outcome"))
)
.addAllRuleSpecs(
createRuleSpecsFromJson(
answerGroupJson.getJSONArray("rule_specs"), interactionId
)
)
.build()
}

// Creates an outcome object from JSON
private fun createOutcomeFromJson(outcomeJson: JSONObject?): Outcome {
if (outcomeJson == null) {
return Outcome.getDefaultInstance()
}
return Outcome.newBuilder()
.setDestStateName(outcomeJson.getString("dest"))
.setFeedback(
SubtitledHtml.newBuilder()
.setHtml(outcomeJson.getString("feedback"))
)
.setLabelledAsCorrect(outcomeJson.getBoolean("labelled_as_correct"))
.build()
}

// Creates the list of rule spec objects from JSON
private fun createRuleSpecsFromJson(
ruleSpecJson: JSONArray?, interactionId: String
): MutableList<RuleSpec> {
val ruleSpecList = mutableListOf<RuleSpec>()
if (ruleSpecJson == null) {
return ruleSpecList
}
for (i in 0 until ruleSpecJson.length()) {
val ruleSpecBuilder = RuleSpec.newBuilder()
ruleSpecBuilder.ruleType = ruleSpecJson.getJSONObject(i).getString("rule_type")
val inputsJson = ruleSpecJson.getJSONObject(i).getJSONObject("inputs")
val inputKeysIterator = inputsJson.keys()
while (inputKeysIterator.hasNext()) {
val inputName = inputKeysIterator.next()
ruleSpecBuilder.putInput(inputName, createInputFromJson(inputsJson, inputName, interactionId))
}
ruleSpecList.add(ruleSpecBuilder.build())
}
return ruleSpecList
}

// Creates an input interaction object from JSON
private fun createInputFromJson(
inputJson: JSONObject?, keyName: String, interactionId: String
): InteractionObject {
if (inputJson == null) {
return InteractionObject.getDefaultInstance()
}
return when (interactionId) {
"MultipleChoiceInput" -> InteractionObject.newBuilder()
.setNonNegativeInt(inputJson.getInt(keyName))
.build()
"TextInput" -> InteractionObject.newBuilder()
.setNormalizedString(inputJson.getString(keyName))
.build()
"NumericInput" -> InteractionObject.newBuilder()
.setReal(inputJson.getDouble(keyName))
.build()
else -> throw IllegalStateException("Encountered unexpected interaction ID: $interactionId")
}
}

// Creates a customization arg mapping from JSON
private fun createCustomizationArgsMapFromJson(
customizationArgsJson: JSONObject?
): MutableMap<String, InteractionObject> {
val customizationArgsMap: MutableMap<String, InteractionObject> = mutableMapOf()
if (customizationArgsJson == null) {
return customizationArgsMap
}
val customizationArgsKeys = customizationArgsJson.keys() ?: return customizationArgsMap
val customizationArgsIterator = customizationArgsKeys.iterator()
while (customizationArgsIterator.hasNext()) {
val key = customizationArgsIterator.next()
customizationArgsMap[key] = createCustomizationArgValueFromJson(
customizationArgsJson.getJSONObject(key).get("value")
)
}
return customizationArgsMap
}

// Creates a customization arg value interaction object from JSON
private fun createCustomizationArgValueFromJson(customizationArgValue: Any): InteractionObject {
val interactionObjectBuilder = InteractionObject.newBuilder()
when (customizationArgValue) {
is String -> return interactionObjectBuilder
.setNormalizedString(customizationArgValue).build()
is Int -> return interactionObjectBuilder
.setSignedInt(customizationArgValue).build()
is Double -> return interactionObjectBuilder
.setReal(customizationArgValue).build()
is List<*> -> if (customizationArgValue.size > 0) {
return interactionObjectBuilder.setSetOfHtmlString(
createStringList(customizationArgValue)
).build()
}
}
return InteractionObject.getDefaultInstance()
}

@Suppress("UNCHECKED_CAST") // Checked cast in the if statement
private fun createStringList(value: List<*>): StringList {
val stringList = mutableListOf<String>()
if (value[0] is String) {
stringList.addAll(value as List<String>)
return StringList.newBuilder().addAllHtml(stringList).build()
}
return StringList.getDefaultInstance()
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package org.oppia.domain.question

import androidx.lifecycle.LiveData
import org.oppia.app.model.Question
import org.oppia.util.data.AsyncResult
import org.oppia.util.data.DataProvider
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -18,10 +17,9 @@ import javax.inject.Singleton
@Singleton
class QuestionAssessmentProgressController @Inject constructor(
) {
fun beginQuestionTrainingSession(questionsList: LiveData<AsyncResult<List<Question>>>) {
fun beginQuestionTrainingSession(questionsList: DataProvider<List<Question>>) {
}

fun finishQuestionTrainingSession() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.oppia.domain.question

import dagger.Module
import dagger.Provides
import javax.inject.Qualifier

/** Provider to return any constants required during the training session. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should be on the Module instead of this annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@Qualifier
annotation class QuestionCountPerTrainingSession

@Qualifier
annotation class QuestionTrainingSeed

@Module
class QuestionModule {
@Provides
@QuestionCountPerTrainingSession
fun provideQuestionCountPerTrainingSession(): Int = 10

@Provides
@QuestionTrainingSeed
fun provideQuestionTrainingSeed(): Long = System.currentTimeMillis()
}
Loading