diff --git a/app/src/main/java/de/xikolo/controllers/section/CourseItemsActivity.kt b/app/src/main/java/de/xikolo/controllers/section/CourseItemsActivity.kt index 6a16bfe27..6d857602b 100644 --- a/app/src/main/java/de/xikolo/controllers/section/CourseItemsActivity.kt +++ b/app/src/main/java/de/xikolo/controllers/section/CourseItemsActivity.kt @@ -1,10 +1,7 @@ package de.xikolo.controllers.section import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import android.widget.TextView import androidx.core.app.NavUtils import androidx.core.content.ContextCompat @@ -56,9 +53,14 @@ class CourseItemsActivity : ViewModelActivity() { @BindView(R.id.tabs) lateinit var tabLayout: TabLayout + @BindView(R.id.stub_bottom) + lateinit var stubBottom: ViewStub + private var course: Course? = null private var section: Section? = null + var activeFragment: Fragment? = null + override fun createViewModel(): CourseItemsViewModel { return CourseItemsViewModel(courseId, sectionId) } @@ -69,6 +71,11 @@ class CourseItemsActivity : ViewModelActivity() { setContentView(R.layout.activity_blank_tabs) setupActionBar() + if (stubBottom.parent != null) { + stubBottom.layoutResource = R.layout.view_floating_button + stubBottom.inflate() + } + Crashlytics.setString("course_id", courseId) Crashlytics.setString("section_id", sectionId) @@ -99,6 +106,18 @@ class CourseItemsActivity : ViewModelActivity() { viewPager.adapter = adapter viewPager.offscreenPageLimit = 2 + viewPager.clearOnPageChangeListeners() + viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageSelected(position: Int) { + stubBottom.visibility = View.GONE + activeFragment = adapter.getItem(position) + (activeFragment as? QuizFragment)?.notifyActive() + } + + override fun onPageScrollStateChanged(state: Int) {} + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} + }) + tabLayout.setupWithViewPager(viewPager) tabLayout.addOnTabSelectedListener(adapter) @@ -112,6 +131,15 @@ class CourseItemsActivity : ViewModelActivity() { onItemSelected(index) } + fun updateActionButton(sender: Fragment, text: String, icon: String, click: (View) -> Unit) { + if (sender == activeFragment) { + findViewById(R.id.actionText).text = text + findViewById(R.id.actionIcon).text = icon + findViewById(R.id.actionButton).setOnClickListener(click) + stubBottom.visibility = View.VISIBLE + } + } + private fun onItemSelected(position: Int) { index = position @@ -211,16 +239,17 @@ class CourseItemsActivity : ViewModelActivity() { if (fragment == null) { fragment = if (course?.enrollment?.proctored == true && item.proctored) { ProctoredItemFragment() - } else when (item.contentType) { - Item.TYPE_LTI -> LtiExerciseFragmentAutoBundle.builder(courseId, sectionId, item.id).build() - Item.TYPE_PEER -> PeerAssessmentFragmentAutoBundle.builder(courseId, sectionId, item.id).build() - Item.TYPE_QUIZ -> WebViewFragmentAutoBundle.builder(url) + } else when { + item.contentType == Item.TYPE_LTI -> LtiExerciseFragmentAutoBundle.builder(courseId, sectionId, item.id).build() + item.contentType == Item.TYPE_PEER -> PeerAssessmentFragmentAutoBundle.builder(courseId, sectionId, item.id).build() + item.contentType == Item.TYPE_QUIZ && item.exerciseType == Item.EXERCISE_TYPE_SELFTEST -> QuizFragmentAutoBundle.builder(courseId, sectionId, item.id, item.contentId).build() + item.contentType == Item.TYPE_QUIZ -> WebViewFragmentAutoBundle.builder(url) .inAppLinksEnabled(true) .externalLinksEnabled(false) .build() - Item.TYPE_TEXT -> RichTextFragmentAutoBundle.builder(courseId, sectionId, item.id).build() - Item.TYPE_VIDEO -> VideoPreviewFragmentAutoBundle.builder(courseId, sectionId, item.id).build() - else -> WebViewFragmentAutoBundle.builder(url) + item.contentType == Item.TYPE_TEXT -> RichTextFragmentAutoBundle.builder(courseId, sectionId, item.id).build() + item.contentType == Item.TYPE_VIDEO -> VideoPreviewFragmentAutoBundle.builder(courseId, sectionId, item.id).build() + else -> WebViewFragmentAutoBundle.builder(url) .inAppLinksEnabled(false) .externalLinksEnabled(false) .build() @@ -258,7 +287,6 @@ class CourseItemsActivity : ViewModelActivity() { } override fun onTabReselected(tab: TabLayout.Tab) {} - } } diff --git a/app/src/main/java/de/xikolo/controllers/section/QuizFragment.kt b/app/src/main/java/de/xikolo/controllers/section/QuizFragment.kt new file mode 100644 index 000000000..1f3d4b690 --- /dev/null +++ b/app/src/main/java/de/xikolo/controllers/section/QuizFragment.kt @@ -0,0 +1,475 @@ +package de.xikolo.controllers.section + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView +import butterknife.BindView +import com.yatatsu.autobundle.AutoBundleField +import de.xikolo.R +import de.xikolo.controllers.base.ViewModelFragment +import de.xikolo.extensions.observe +import de.xikolo.extensions.observeOnce +import de.xikolo.models.Quiz +import de.xikolo.models.QuizQuestion +import de.xikolo.models.QuizSubmission +import de.xikolo.models.dao.ItemDao +import de.xikolo.network.jobs.base.NetworkCode +import de.xikolo.network.jobs.base.NetworkStateLiveData +import de.xikolo.utils.extensions.setMarkdownText +import de.xikolo.viewmodels.section.QuizViewModel +import de.xikolo.views.DateTextView +import de.xikolo.views.quiz.* +import io.realm.RealmList +import java.text.DateFormat +import java.util.* + +class QuizFragment : ViewModelFragment() { + + companion object { + val TAG: String = QuizFragment::class.java.simpleName + + const val SNAPSHOTTING_INTERVAL = 20000L + } + + @AutoBundleField + lateinit var courseId: String + + @AutoBundleField + lateinit var sectionId: String + + @AutoBundleField + lateinit var itemId: String + + @AutoBundleField + lateinit var quizId: String + + @BindView(R.id.snapshotContainer) + lateinit var snapshotContainer: ViewGroup + + @BindView(R.id.snapshotMessage) + lateinit var snapshotMessage: TextView + + @BindView(R.id.snapshotCheckmark) + lateinit var snapshotCheckmark: TextView + + @BindView(R.id.snapshotProgress) + lateinit var snapshotProgress: ProgressBar + + @BindView(R.id.message) + lateinit var message: TextView + + @BindView(R.id.questionContainer) + lateinit var questionContainer: ViewGroup + + @BindView(R.id.detailsQuestionCount) + lateinit var questionCount: TextView + + @BindView(R.id.detailsInstructions) + lateinit var instructions: TextView + + @BindView(R.id.details) + lateinit var detailsContainer: View + + @BindView(R.id.detailsPoints) + lateinit var maxPoints: TextView + + @BindView(R.id.detailsTimeLimit) + lateinit var timeLimit: TextView + + @BindView(R.id.detailsAllowedAttempts) + lateinit var allowedAttempts: TextView + + @BindView(R.id.detailsSubmission) + lateinit var submissionDetailsContainer: View + + @BindView(R.id.detailsAchievedPoints) + lateinit var achievedPoints: TextView + + @BindView(R.id.detailsLastSubmitted) + lateinit var lastSubmitted: DateTextView + + override val layoutResource = R.layout.fragment_quiz + + private var questionViewMap: Map> = mapOf() + private var isInSolutionMode = false + + private var snapshotTimer: Timer? = null + + override fun createViewModel(): QuizViewModel { + return QuizViewModel(itemId, quizId) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // The PagerAdapter creates the Fragments multiple times, so they need to be reset here + resetView() + + setSubmissionMode() + + viewModel.item + .observe(viewLifecycleOwner) { item -> + ItemDao.Unmanaged.findContent(item.id)?.let { quiz -> + viewModel.quiz = quiz as Quiz + + message.text = getString(R.string.quiz_selftest_description) + + if (quiz.instructions != "dummy") { + instructions.setMarkdownText(quiz.instructions) + instructions.visibility = View.VISIBLE + } else { + instructions.visibility = View.GONE + } + + maxPoints.text = getString(R.string.quiz_maximum_points, item.maxPoints.toString()) + + timeLimit.text = + if (quiz.timeLimit == 0) { + getString(R.string.quiz_time_limit_none) + } else { + getString(R.string.quiz_time_limit, quiz.timeLimit) + } + + if (quiz.allowedAttempts > 0) { + allowedAttempts.text = getString(R.string.quiz_allowed_attempts, quiz.allowedAttempts) + allowedAttempts.visibility = View.VISIBLE + } else { + allowedAttempts.visibility = View.GONE + } + } + } + + viewModel.questions + .observe(viewLifecycleOwner) { questions -> + questionCount.text = getString(R.string.quiz_question_count, questions.size) + + if (questionViewMap.isEmpty()) { + questionViewMap = buildQuestions(questions) + } + + viewModel.newestSubmission?.removeObservers(viewLifecycleOwner) + viewModel.newestSubmission + ?.observe(viewLifecycleOwner) { submission -> + lockQuestions(questionViewMap) + insertAnswers(questionViewMap, submission) + + if (submission.submitted) { + showSolution(questionViewMap, submission) + setSolutionMode() + + viewModel.item.value?.maxPoints?.let { + achievedPoints.text = getString(R.string.quiz_submission_points, submission.points.toString(), it.toString()) + achievedPoints.visibility = View.VISIBLE + } ?: run { + achievedPoints.visibility = View.GONE + } + + submission.submittedAt?.let { + lastSubmitted.setDate(it) + lastSubmitted.text = getString(R.string.quiz_submission_submitted_at, DateFormat.getDateTimeInstance( + DateFormat.YEAR_FIELD or DateFormat.MONTH_FIELD or DateFormat.DATE_FIELD, + DateFormat.SHORT, + Locale.getDefault() + ).format(it)) + lastSubmitted.visibility = View.VISIBLE + } ?: run { + lastSubmitted.visibility = View.GONE + } + } else { + unlockQuestions(questionViewMap) + } + + showContent() + } + + showContent() + } + } + + override fun onStart() { + super.onStart() + if (!isInSolutionMode) { + startSnapshotting() + } + } + + override fun onStop() { + super.onStop() + if (!isInSolutionMode) { + stopSnapshotting() + snapshot() + } + } + + private fun snapshot() { + if (viewModel.quiz?.newestSubmissionId != null) { + updateQuizSubmission(getAnswers(questionViewMap), false) + } else { + createQuizSubmission(true) + } + } + + private fun startSnapshotting() { + if (snapshotTimer == null) { + snapshotTimer?.cancel() + snapshotTimer = Timer() + snapshotTimer?.scheduleAtFixedRate(object : TimerTask() { + override fun run() { + if (!isInSolutionMode) { + activity?.runOnUiThread { + snapshot() + } + } + } + }, SNAPSHOTTING_INTERVAL, SNAPSHOTTING_INTERVAL) + } + } + + private fun stopSnapshotting() { + snapshotTimer?.cancel() + snapshotTimer = null + } + + private fun resetView() { + questionViewMap = emptyMap() + snapshotProgress.visibility = View.GONE + } + + private fun createQuizSubmission(silent: Boolean = false) { + val quizCreationNetworkState = NetworkStateLiveData() + + if (view != null) { + if (!silent) { + showBlockingProgress() + } + + quizCreationNetworkState + .observeOnce(viewLifecycleOwner) { + when (it.code) { + NetworkCode.SUCCESS -> { + if (!silent) { + hideAnyProgress() + setSubmissionMode() + } + viewModel.onRefresh() + true + } + + NetworkCode.NO_NETWORK -> { + hideAnyProgress() + showNetworkRequired() + true + } + else -> false + } + } + } + + viewModel.createQuizSubmission(quizCreationNetworkState) + } + + private fun updateQuizSubmission(submission: QuizSubmission, submit: Boolean) { + val quizSubmissionNetworkState = NetworkStateLiveData() + + if (view != null) { + if (submit) { + showBlockingProgress() + } + + quizSubmissionNetworkState + .observeOnce(viewLifecycleOwner) { + when (it.code) { + NetworkCode.SUCCESS -> { + if (submit) { + hideAnyProgress() + setSolutionMode() + viewModel.onRefresh() + } else { + snapshotProgress.visibility = View.INVISIBLE + snapshotCheckmark.visibility = View.VISIBLE + snapshotMessage.text = getString(R.string.quiz_snapshot_saved) + } + true + } + NetworkCode.NO_NETWORK -> { + if (submit) { + hideAnyProgress() + showNetworkRequired() + } else { + snapshotMessage.text = getString(R.string.quiz_snapshot_error_network) + } + true + } + NetworkCode.ERROR -> { + if (submit) { + hideAnyProgress() + showErrorMessage() + } else { + snapshotMessage.text = getString(R.string.quiz_snapshot_error) + } + true + } + else -> false + } + } + + snapshotProgress.visibility = View.VISIBLE + snapshotCheckmark.visibility = View.INVISIBLE + snapshotMessage.text = getString(R.string.quiz_snapshot_saving) + } + + viewModel.updateQuizSubmission(submission.apply { + id = viewModel.quiz?.newestSubmissionId!! + quizId = this@QuizFragment.quizId + submitted = submit + }, quizSubmissionNetworkState) + } + + fun notifyActive() { + updateActionButton() + } + + private fun updateActionButton() { + (activity as? CourseItemsActivity)?.let { + if (isInSolutionMode) { + it.updateActionButton( + this, + getString(R.string.quiz_redo), + getString(R.string.icon_reload) + ) { + createQuizSubmission() + resetView() + } + } else { + it.updateActionButton( + this, + getString(R.string.quiz_submit), + getString(R.string.icon_checkmark) + ) { + stopSnapshotting() + updateQuizSubmission(getAnswers(questionViewMap), true) + } + } + } + } + + private fun setSolutionMode() { + isInSolutionMode = true + updateActionButton() + submissionDetailsContainer.visibility = View.VISIBLE + detailsContainer.visibility = View.GONE + snapshotContainer.visibility = View.GONE + } + + private fun setSubmissionMode() { + isInSolutionMode = false + updateActionButton() + detailsContainer.visibility = View.VISIBLE + submissionDetailsContainer.visibility = View.GONE + snapshotContainer.visibility = View.VISIBLE + startSnapshotting() + } + + private fun lockQuestions(questionViewMap: Map>) { + questionViewMap.forEach { entry -> + entry.value.second.lock() + } + } + + private fun unlockQuestions(questionViewMap: Map>) { + questionViewMap.forEach { entry -> + entry.value.second.unlock() + } + } + + private fun insertAnswers(questionViewMap: Map>, submission: QuizSubmission) { + questionViewMap.forEach { entry -> + val answer = submission.answers.find { it.questionId == entry.key } + if (answer != null) { + entry.value.second.insertAnswer(answer) + } + } + } + + private fun showSolution(questionViewMap: Map>, submission: QuizSubmission) { + questionViewMap.forEach { entry -> + val answer = submission.answers.find { it.questionId == entry.key } + entry.value.second.showSolution(answer) + } + } + + private fun getAnswers(questionViewMap: Map>): QuizSubmission { + return QuizSubmission().apply { + answers = RealmList() + questionViewMap.forEach { entry -> + val answer = entry.value.second.getAnswer() + if (answer != null) { + answer.questionId = entry.key + answers.add(answer) + } + } + } + } + + private fun buildQuestions(questions: List): Map> { + val map: MutableMap> = mutableMapOf() + + context?.let { + questionContainer.removeAllViews() + questions.sortedBy { it.position }.forEachIndexed { n, q -> + val questionView = QuestionContainerView(it) + + questionView.numberView.text = getString(R.string.quiz_question_number, n + 1) + questionView.pointsView.text = getString(R.string.quiz_question_points, q.maxPoints) + questionView.questionView.setMarkdownText(q.text) + + when (q.type) { + QuizQuestion.TYPE_SELECT_ONE -> { + val view = SingleChoiceQuestionView(it) + view.shuffleOptions = q.shuffleOptions + view.options = q.options + view.changeListener = { + snapshot() + } + questionView.containerView.addView(view) + + map[q.id!!] = Pair(questionView, view) + } + QuizQuestion.TYPE_SELECT_MULTIPLE -> { + val view = MultiChoiceQuestionView(it) + view.shuffleOptions = q.shuffleOptions + view.options = q.options + view.changeListener = { + snapshot() + } + questionView.containerView.addView(view) + + map[q.id!!] = Pair(questionView, view) + } + QuizQuestion.TYPE_FREE_TEXT -> { + val view = FreeTextQuestionView(it) + view.option = q.options[0]!! + view.changeListener = { + snapshot() + } + questionView.containerView.addView(view) + + map[q.id!!] = Pair(questionView, view) + } + else -> Unit + } + + questionContainer.addView(questionView) + } + } + + return map + } +} diff --git a/app/src/main/java/de/xikolo/models/Item.java b/app/src/main/java/de/xikolo/models/Item.java index 01e51139c..875e91266 100644 --- a/app/src/main/java/de/xikolo/models/Item.java +++ b/app/src/main/java/de/xikolo/models/Item.java @@ -10,7 +10,6 @@ import de.xikolo.R; import de.xikolo.models.base.RealmAdapter; import de.xikolo.models.dao.CourseDao; -import de.xikolo.models.dao.ItemDao; import de.xikolo.models.dao.SectionDao; import de.xikolo.utils.extensions.DateUtil; import io.realm.RealmObject; @@ -58,10 +57,6 @@ public Course getCourse() { return CourseDao.Unmanaged.find(courseId); } - public RealmObject getContent() { - return ItemDao.Unmanaged.findContent(contentId); - } - public static final String TYPE_TEXT = "rich_text"; public static final String TYPE_VIDEO = "video"; public static final String TYPE_QUIZ = "quiz"; diff --git a/app/src/main/java/de/xikolo/models/Quiz.java b/app/src/main/java/de/xikolo/models/Quiz.java index 8be6f6f66..dc741ee5c 100644 --- a/app/src/main/java/de/xikolo/models/Quiz.java +++ b/app/src/main/java/de/xikolo/models/Quiz.java @@ -5,6 +5,8 @@ import de.xikolo.models.base.RealmAdapter; import io.realm.RealmObject; import io.realm.annotations.PrimaryKey; +import moe.banana.jsonapi2.HasMany; +import moe.banana.jsonapi2.HasOne; import moe.banana.jsonapi2.JsonApi; import moe.banana.jsonapi2.Resource; @@ -21,6 +23,8 @@ public class Quiz extends RealmObject { public boolean showWelcomePage; + public String newestSubmissionId; + @JsonApi(type = "quizzes") public static class JsonModel extends Resource implements RealmAdapter { @@ -32,12 +36,15 @@ public static class JsonModel extends Resource implements RealmAdapter { @Json(name = "allowed_attempts") public int allowedAttempts; - @Json(name = "max_points") - public int maxPoints; - @Json(name = "show_welcome_page") public boolean showWelcomePage; + @Json(name = "questions") + public HasMany questions; + + @Json(name = "newest_user_submission") + public HasOne newestSubmission; + @Override public Quiz convertToRealmObject() { Quiz quiz = new Quiz(); @@ -47,6 +54,10 @@ public Quiz convertToRealmObject() { quiz.allowedAttempts = allowedAttempts; quiz.showWelcomePage = showWelcomePage; + if (newestSubmission != null) { + quiz.newestSubmissionId = newestSubmission.get().getId(); + } + return quiz; } diff --git a/app/src/main/java/de/xikolo/models/QuizQuestion.kt b/app/src/main/java/de/xikolo/models/QuizQuestion.kt new file mode 100644 index 000000000..db0fa1fd4 --- /dev/null +++ b/app/src/main/java/de/xikolo/models/QuizQuestion.kt @@ -0,0 +1,82 @@ +package de.xikolo.models + +import com.squareup.moshi.Json + +import de.xikolo.models.base.RealmAdapter +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import moe.banana.jsonapi2.HasOne +import moe.banana.jsonapi2.JsonApi +import moe.banana.jsonapi2.Resource + +open class QuizQuestion : RealmObject() { + + companion object { + const val TYPE_SELECT_MULTIPLE = "select_multiple" + const val TYPE_SELECT_ONE = "select_one" + const val TYPE_FREE_TEXT = "free_text" + } + + @PrimaryKey + var id: String? = null + + var text: String? = null + + var explanation: String? = null + + var type: String? = null + + var position: Int = 0 + + var maxPoints: Float = 0f + + var shuffleOptions: Boolean = false + + var options = RealmList() + + var quizId: String? = null + + @JsonApi(type = "quiz-questions") + class JsonModel : Resource(), RealmAdapter { + + var text: String? = null + + var explanation: String? = null + + @field:Json(name = "type") + var quizType: String = "" + + var position: Int = 0 + + @field:Json(name = "max_points") + var maxPoints: Float = 0.toFloat() + + @field:Json(name = "shuffle_options") + var shuffleOptions: Boolean = false + + var options: List? = null + + var quiz: HasOne? = null + + override fun convertToRealmObject(): QuizQuestion { + val quizQuestion = QuizQuestion() + quizQuestion.id = id + quizQuestion.text = text + quizQuestion.explanation = explanation + quizQuestion.type = quizType + quizQuestion.position = position + quizQuestion.maxPoints = maxPoints + quizQuestion.shuffleOptions = shuffleOptions + quizQuestion.options.addAll(options!!) + + if (quiz != null) { + quizQuestion.quizId = quiz!!.get().id + } + + return quizQuestion + } + + } + +} diff --git a/app/src/main/java/de/xikolo/models/QuizQuestionOption.java b/app/src/main/java/de/xikolo/models/QuizQuestionOption.java new file mode 100644 index 000000000..dc789c7b1 --- /dev/null +++ b/app/src/main/java/de/xikolo/models/QuizQuestionOption.java @@ -0,0 +1,18 @@ +package de.xikolo.models; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; + +public class QuizQuestionOption extends RealmObject { + + @PrimaryKey + public String id; + + public int position; + + public String text; + + public boolean correct; + + public String explanation; +} diff --git a/app/src/main/java/de/xikolo/models/QuizSubmission.kt b/app/src/main/java/de/xikolo/models/QuizSubmission.kt new file mode 100644 index 000000000..a0d344603 --- /dev/null +++ b/app/src/main/java/de/xikolo/models/QuizSubmission.kt @@ -0,0 +1,108 @@ +package de.xikolo.models + +import com.squareup.moshi.Json +import de.xikolo.models.base.JsonAdapter +import de.xikolo.models.base.RealmAdapter +import de.xikolo.utils.extensions.asDate +import de.xikolo.utils.extensions.formattedString +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import moe.banana.jsonapi2.HasOne +import moe.banana.jsonapi2.JsonApi +import moe.banana.jsonapi2.Resource +import java.util.* + +open class QuizSubmission : RealmObject(), JsonAdapter { + + @PrimaryKey + var id: String? = null + + var createdAt: Date? = null + + var submittedAt: Date? = null + + var submitted: Boolean = false + + var points: Float = 0f + + var answers: RealmList = RealmList() + + var quizId: String? = null + + override fun convertToJsonResource(): JsonModel { + val model = JsonModel() + model.id = id + model.createdAt = createdAt?.formattedString + model.submittedAt = submittedAt?.formattedString + model.submitted = submitted + model.points = points + + val modelAnswers = mutableMapOf>() + answers.forEach { answer -> + modelAnswers[answer.questionId!!] = + mapOf("type" to answer.value?.type!!, "data" to + if (answer.value?.type!! == QuizQuestion.TYPE_SELECT_MULTIPLE) { + answer.value?.data!! // map to list of strings + } else { + answer.value?.data?.first()!! // map to single string + } + ) + } + + model.answers = modelAnswers + + model.quiz = HasOne(Quiz.JsonModel().type, quizId) + + return model + } + + @JsonApi(type = "quiz-submissions") + class JsonModel : Resource(), RealmAdapter { + + @field:Json(name = "created_at") + var createdAt: String? = null + + @field:Json(name = "submitted_at") + var submittedAt: String? = null + + var submitted: Boolean = false + + var points: Float = 0f + + // data schema is Map> for free_text and select_one + // and Map> for select_multiple + var answers: Map>? = null + + var quiz: HasOne? = null + + override fun convertToRealmObject(): QuizSubmission { + val quizSubmission = QuizSubmission() + quizSubmission.id = id + quizSubmission.createdAt = createdAt.asDate + quizSubmission.submittedAt = submittedAt.asDate + quizSubmission.submitted = submitted + quizSubmission.points = points + + answers?.forEach { answer -> + quizSubmission.answers.add( + QuizSubmissionAnswer(answer.key, answer.value["type"] as String, + if (answer.value["type"] == QuizQuestion.TYPE_SELECT_MULTIPLE) { + answer.value["data"] as List // data is a list of strings + } else { + listOf(answer.value["data"] as String) // data is a string and will be converted to list here + } + ) + ) + } + + if (quiz != null) { + quizSubmission.quizId = quiz!!.get().id + } + + return quizSubmission + } + + } + +} diff --git a/app/src/main/java/de/xikolo/models/QuizSubmissionAnswer.kt b/app/src/main/java/de/xikolo/models/QuizSubmissionAnswer.kt new file mode 100644 index 000000000..5e14aa1d0 --- /dev/null +++ b/app/src/main/java/de/xikolo/models/QuizSubmissionAnswer.kt @@ -0,0 +1,25 @@ +package de.xikolo.models + +import io.realm.RealmList +import io.realm.RealmObject + + +open class QuizSubmissionAnswer() : RealmObject() { + + constructor(questionId: String, type: String, data: String) : this(questionId, type, listOf(data)) + + constructor(questionId: String, type: String, data: List) : this() { + this.questionId = questionId + this.value = QuizSubmissionAnswerData().apply { + this.type = type + this.data = RealmList().apply { + data.forEach { + add(it) + } + } + } + } + + var questionId: String? = null + var value: QuizSubmissionAnswerData? = null +} diff --git a/app/src/main/java/de/xikolo/models/QuizSubmissionAnswerData.kt b/app/src/main/java/de/xikolo/models/QuizSubmissionAnswerData.kt new file mode 100644 index 000000000..2eccddc46 --- /dev/null +++ b/app/src/main/java/de/xikolo/models/QuizSubmissionAnswerData.kt @@ -0,0 +1,9 @@ +package de.xikolo.models + +import io.realm.RealmList +import io.realm.RealmObject + +open class QuizSubmissionAnswerData : RealmObject() { + var type: String? = null + var data: RealmList = RealmList() +} diff --git a/app/src/main/java/de/xikolo/models/dao/ItemDao.kt b/app/src/main/java/de/xikolo/models/dao/ItemDao.kt index d28057ca2..d16a1ecf9 100644 --- a/app/src/main/java/de/xikolo/models/dao/ItemDao.kt +++ b/app/src/main/java/de/xikolo/models/dao/ItemDao.kt @@ -1,6 +1,8 @@ package de.xikolo.models.dao +import androidx.lifecycle.LiveData import de.xikolo.extensions.asCopy +import de.xikolo.extensions.asLiveData import de.xikolo.models.* import de.xikolo.models.Item.* import de.xikolo.models.dao.base.BaseDao @@ -21,6 +23,19 @@ class ItemDao(realm: Realm) : BaseDao(Item::class, realm) { fun allAccessibleForSection(sectionId: String?) = all("sectionId" to sectionId, "accessible" to true) + + fun findContent(id: String?): LiveData? = + Unmanaged.find(id)?.let { item -> + return when (item.contentType) { + TYPE_TEXT -> realm.where().equalTo("id", item.contentId).findFirstAsync().asLiveData() + TYPE_VIDEO -> realm.where