diff --git a/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/LoginServiceImpl.kt b/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/LoginServiceImpl.kt index 35319a783..e8cdb8e2f 100644 --- a/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/LoginServiceImpl.kt +++ b/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/LoginServiceImpl.kt @@ -26,7 +26,7 @@ internal class LoginServiceImpl( return performNetworkCall { val response = ktorProvider.ktorClient.post(serverUrl) { url { - appendPathSegments("api", "authenticate") + appendPathSegments("api", "public", "authenticate") } contentType(ContentType.Application.Json) @@ -48,7 +48,7 @@ internal class LoginServiceImpl( return performNetworkCall { ktorProvider.ktorClient.post(serverUrl) { url { - appendPathSegments("api", "saml2") + appendPathSegments("api", "public", "saml2") } setBody(rememberMe) contentType(ContentType.Application.Json) diff --git a/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/ServerDataServiceImpl.kt b/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/ServerDataServiceImpl.kt index 2fcf42446..a48818727 100644 --- a/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/ServerDataServiceImpl.kt +++ b/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/impl/ServerDataServiceImpl.kt @@ -34,7 +34,7 @@ internal class ServerDataServiceImpl(private val ktorProvider: KtorProvider) : S .ktorClient .get(serverUrl) { url { - appendPathSegments("api", "account") + appendPathSegments("api", "public", "account") } contentType(ContentType.Application.Json) diff --git a/core/datastore/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/datastore/AccountService.kt b/core/datastore/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/datastore/AccountService.kt index 1cbeeb98c..913a6fcd8 100644 --- a/core/datastore/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/datastore/AccountService.kt +++ b/core/datastore/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/datastore/AccountService.kt @@ -22,10 +22,7 @@ interface AccountService { sealed class AuthenticationData { object NotLoggedIn : AuthenticationData() - data class LoggedIn(val authToken: String, val username: String) : - AuthenticationData() { - val asBearer = "Bearer $authToken" - } + data class LoggedIn(val authToken: String, val username: String) : AuthenticationData() } suspend fun login(username: String, password: String, rememberMe: Boolean): LoginResponse diff --git a/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/ServerTimeServiceImpl.kt b/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/ServerTimeServiceImpl.kt index 334342cde..ec0c26196 100644 --- a/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/ServerTimeServiceImpl.kt +++ b/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/ServerTimeServiceImpl.kt @@ -134,7 +134,7 @@ internal class ServerTimeServiceImpl( performNetworkCall { ktorProvider.ktorClient.get(serverUrl) { url { - appendPathSegments("time") + appendPathSegments("api", "public", "time") } contentType(ContentType.Application.Json) diff --git a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/ExerciseViewModel.kt b/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/ExerciseViewModel.kt index 56dbeb30f..680e3b971 100644 --- a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/ExerciseViewModel.kt +++ b/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/ExerciseViewModel.kt @@ -1,47 +1,48 @@ package de.tum.informatics.www1.artemis.native_app.feature.exercise_view -import android.annotation.SuppressLint -import android.content.Context -import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest -import de.tum.informatics.www1.artemis.native_app.core.data.* -import de.tum.informatics.www1.artemis.native_app.core.data.service.BuildLogService +import de.tum.informatics.www1.artemis.native_app.core.data.DataState +import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse +import de.tum.informatics.www1.artemis.native_app.core.data.retryOnInternet import de.tum.informatics.www1.artemis.native_app.core.data.service.CourseExerciseService import de.tum.informatics.www1.artemis.native_app.core.data.service.ExerciseService -import de.tum.informatics.www1.artemis.native_app.core.data.service.ResultService +import de.tum.informatics.www1.artemis.native_app.core.data.stateIn import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvider import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.ProgrammingExercise -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.feedback.Feedback import de.tum.informatics.www1.artemis.native_app.core.model.exercise.participation.Participation -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.BuildLogEntry -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.ProgrammingSubmission import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.Result import de.tum.informatics.www1.artemis.native_app.core.ui.authTokenStateFlow import de.tum.informatics.www1.artemis.native_app.core.ui.serverUrlStateFlow import de.tum.informatics.www1.artemis.native_app.core.websocket.LiveParticipationService -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlinx.datetime.Instant -// it does not actually leak, as context will be the application context, which cannot be leaked. -@SuppressLint("StaticFieldLeak") internal class ExerciseViewModel( private val exerciseId: Long, private val serverConfigurationService: ServerConfigurationService, private val accountService: AccountService, private val exerciseService: ExerciseService, private val liveParticipationService: LiveParticipationService, - private val resultService: ResultService, - private val buildLogService: BuildLogService, private val courseExerciseService: CourseExerciseService, - private val networkStatusProvider: NetworkStatusProvider, - private val context: Context + private val networkStatusProvider: NetworkStatusProvider ) : ViewModel() { private val requestReloadExercise = MutableSharedFlow(extraBufferCapacity = 1) @@ -110,19 +111,6 @@ internal class ExerciseViewModel( } .stateIn(viewModelScope, SharingStarted.Eagerly) - val latestIndividualDueDate: StateFlow> = - baseConfigurationFlow - .flatMapLatest { (serverUrl, authToken) -> - retryOnInternet(networkStatusProvider.currentNetworkStatus) { - exerciseService.getLatestDueDate( - exerciseId, - serverUrl, - authToken - ) - } - } - .stateIn(viewModelScope, SharingStarted.Lazily) - /** * The latest result for the exercise. */ @@ -138,98 +126,6 @@ internal class ExerciseViewModel( } .stateIn(viewModelScope, SharingStarted.Lazily) - val feedbackItems: StateFlow>> = - flatMapLatest( - exerciseDataState, - latestResultDataState, - serverConfigurationService.serverUrl, - accountService.authToken - ) { exerciseDataState, latestResultDataState, serverUrl, authToken -> - when (val joinedDataState = exerciseDataState join latestResultDataState) { - is DataState.Success -> { - val (exercise, latestResult) = joinedDataState.data - if (latestResult != null) { - val feedbacks = latestResult.feedbacks - if (feedbacks == null) { - // Load feedback - - val participation = - exercise.studentParticipations.orEmpty().firstOrNull() - - retryOnInternet(networkStatusProvider.currentNetworkStatus) { - resultService.getFeedbackDetailsForResult( - latestResult.participation?.id ?: participation?.id ?: 0, - resultId = latestResult.id ?: 0, - serverUrl = serverUrl, - authToken = authToken - ) - } - .map { feedbackDataState -> - feedbackDataState.bind { feedback -> - createFeedbackItems(exercise, feedback) - } - } - } else { - flowOf( - DataState.Success( - createFeedbackItems( - exercise, - feedbacks - ) - ) - - ) - } - } else emptyFlow() - - } - else -> { - flowOf(joinedDataState.bind { emptyList() }) - } - } - } - .stateIn(viewModelScope, SharingStarted.Lazily) - - /** - * Fetch the build logs only if the exercise is loaded and the exercise is a programming exercise. - * Furthermore, the loaded exercise must already have a submission. - */ - val buildLogs: StateFlow>> = - exerciseDataState - .map { exerciseDataState -> exerciseDataState.bind { it }.orElse(null) } - .filterNotNull() - .filterIsInstance() - .flatMapLatest { exercise -> - val participationId = exercise.studentParticipations.orEmpty().firstOrNull()?.id - ?: return@flatMapLatest emptyFlow() - - latestResultDataState - .filter { - if (it !is DataState.Success) return@filter false - val submission = it.data?.submission - submission is ProgrammingSubmission && submission.buildFailed == true - } - .transformLatest { latestResult -> - val resultId = latestResult.bind { it?.id }.orElse(null) - - //This is reexecuted when exercise is reloaded anyway. And exercise is reloaded - //When server url or auth data changes. - val serverUrl = serverConfigurationService.serverUrl.first() - val authToken = accountService.authToken.first() - - emitAll( - buildLogService - .loadBuildLogs( - participationId, - resultId, - serverUrl, - authToken - ) - ) - } - } - .stateIn(viewModelScope, SharingStarted.Lazily) - val serverUrl: StateFlow = serverUrlStateFlow(serverConfigurationService) val authToken: StateFlow = authTokenStateFlow(accountService) @@ -237,164 +133,6 @@ internal class ExerciseViewModel( requestReloadExercise.tryEmit(Unit) } - private fun createFeedbackItems( - exercise: Exercise, - feedbackList: List - ): List { - val showTestDetails = - exercise is ProgrammingExercise && exercise.showTestNamesToStudents == true - - return if (exercise is ProgrammingExercise) { - feedbackList.map { createProgrammingExerciseFeedbackItem(it, showTestDetails) } - } else { - feedbackList.map { feedback -> - FeedbackItem( - type = FeedbackItemType.Feedback, - category = R.string.result_view_feedback_category_regular, - title = feedback.text, - text = feedback.detailText, - positive = feedback.positive, - credits = feedback.credits, - actualCredits = null - ) - } - } - } - - private fun createProgrammingExerciseFeedbackItem( - feedback: Feedback, - showTestDetails: Boolean - ): FeedbackItem { - return when { - feedback.isSubmissionPolicy -> { - createProgrammingExerciseSubmissionPolicyFeedbackItem(feedback) - } - - feedback.isStaticCodeAnalysis -> { - createProgrammingExerciseScaFeedbackItem(feedback, showTestDetails) - } - - feedback.type == Feedback.FeedbackCreationType.AUTOMATIC -> { - createProgrammingExerciseAutomaticFeedbackItem(feedback, showTestDetails) - } - - (feedback.type == Feedback.FeedbackCreationType.MANUAL - || feedback.type == Feedback.FeedbackCreationType.MANUAL_UNREFERENCED) - && feedback.gradingInstruction != null -> { - createProgrammingExerciseGradingInstructionFeedbackItem(feedback, showTestDetails) - } - - else -> { - createProgrammingExerciseTutorFeedbackItem(feedback, showTestDetails) - } - } - } - - private fun createProgrammingExerciseSubmissionPolicyFeedbackItem( - feedback: Feedback - ): FeedbackItem { - return FeedbackItem( - type = FeedbackItemType.Policy, - category = R.string.result_view_feedback_category_submission_policy, - title = feedback.submissionPolicyTitle, - text = feedback.detailText, - positive = false, - credits = feedback.credits, - actualCredits = null - ) - } - - private fun createProgrammingExerciseScaFeedbackItem( - feedback: Feedback, - showTestDetails: Boolean - ): FeedbackItem { - val issue = feedback.staticCodeAnalysisIssue - - val penalty = issue.penalty - return FeedbackItem( - type = FeedbackItemType.Issue, - category = R.string.result_view_feedback_category_submission_code_issue, - title = "", - text = if (showTestDetails) "${issue.rule}: ${issue.message}" else issue.message, - positive = false, - credits = if (penalty != null) -penalty else feedback.credits, - actualCredits = feedback.credits - ) - } - - /** - * Creates a feedback item from a feedback generated from an automatic test case result. - */ - private fun createProgrammingExerciseAutomaticFeedbackItem( - feedback: Feedback, - showTestDetails: Boolean - ): FeedbackItem { - val title = - if (showTestDetails) { - val positive = feedback.positive - context.getString( - if (positive == null) { - R.string.result_view_feedback_test_title_no_info - } else { - if (positive) { - R.string.result_view_feedback_test_title_passed - } else { - R.string.result_view_feedback_test_title_failed - } - }, feedback.text - ) - } else "" - - return FeedbackItem( - type = FeedbackItemType.Test, - category = if (showTestDetails) R.string.result_view_feedback_category_submission_test - else R.string.result_view_feedback_category_regular, - title = title, - text = feedback.detailText, - positive = feedback.positive, - credits = feedback.credits, - actualCredits = null - ) - } - - /** - * Creates a feedback item for a manual feedback where the tutor used a grading instruction. - */ - private fun createProgrammingExerciseGradingInstructionFeedbackItem( - feedback: Feedback, - showTestDetails: Boolean - ): FeedbackItem { - return FeedbackItem( - type = FeedbackItemType.Feedback, - category = if (showTestDetails) R.string.result_view_feedback_category_submission_tutor - else R.string.result_view_feedback_category_regular, - title = feedback.text, - text = feedback.gradingInstruction?.feedback.orEmpty() + (feedback.detailText ?: ""), - positive = feedback.positive, - credits = feedback.credits, - actualCredits = null - ) - } - - /** - * Creates a feedback item for a regular tutor feedback not using a grading instruction. - */ - private fun createProgrammingExerciseTutorFeedbackItem( - feedback: Feedback, - showTestDetails: Boolean - ): FeedbackItem { - return FeedbackItem( - type = FeedbackItemType.Feedback, - category = if (showTestDetails) R.string.result_view_feedback_category_submission_tutor - else R.string.result_view_feedback_category_regular, - title = feedback.text, - text = feedback.detailText, - positive = feedback.positive, - credits = feedback.credits, - actualCredits = null - ) - } - fun startExercise(onStartedSuccessfully: (participationId: Long) -> Unit) { viewModelScope.launch { val serverUrl = serverConfigurationService.serverUrl.first() @@ -417,25 +155,4 @@ internal class ExerciseViewModel( } } } - - enum class FeedbackItemType { - Issue, - Test, - Feedback, - Policy, - Subsequent, - } - - data class FeedbackItem( - val type: FeedbackItemType, - @StringRes val category: Int, - val title: String?, // this is typically feedback.text - val text: String?, // this is typically feedback.detailText - val positive: Boolean?, - val credits: Float?, - val actualCredits: Float? - ) { - val creditsOrZero: Double = credits?.toDouble() ?: 0.0 - val actualCreditsOrZero: Double = actualCredits?.toDouble() ?: 0.0 - } -} \ No newline at end of file +} diff --git a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/exercise_view_module.kt b/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/exercise_view_module.kt index d3543acda..aae25dfd5 100644 --- a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/exercise_view_module.kt +++ b/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/exercise_view_module.kt @@ -5,7 +5,6 @@ import de.tum.informatics.www1.artemis.native_app.feature.exercise_view.service. import de.tum.informatics.www1.artemis.native_app.feature.exercise_view.service.TextSubmissionService import de.tum.informatics.www1.artemis.native_app.feature.exercise_view.service.impl.TextEditorServiceImpl import de.tum.informatics.www1.artemis.native_app.feature.exercise_view.service.impl.TextSubmissionServiceImpl -import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -18,10 +17,7 @@ val exerciseViewModule = module { get(), get(), get(), - get(), - get(), - get(), - androidContext() + get() ) } diff --git a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/view_result/ResultDetailUi.kt b/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/view_result/ResultDetailUi.kt deleted file mode 100644 index 14efadf81..000000000 --- a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/view_result/ResultDetailUi.kt +++ /dev/null @@ -1,593 +0,0 @@ -package de.tum.informatics.www1.artemis.native_app.feature.exercise_view.view_result - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.ProgrammingExercise -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.BuildLogEntry -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.Result -import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.isPreliminary -import de.tum.informatics.www1.artemis.native_app.core.ui.date.isInFuture -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.ExercisePointsDecimalFormat -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.resultBad -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.resultMedium -import de.tum.informatics.www1.artemis.native_app.core.ui.exercise.resultSuccess -import de.tum.informatics.www1.artemis.native_app.feature.exercise_view.ExerciseViewModel -import de.tum.informatics.www1.artemis.native_app.feature.exercise_view.R -import kotlinx.datetime.Instant -import kotlinx.datetime.toJavaInstant -import java.text.DecimalFormat -import java.text.SimpleDateFormat -import java.util.* -import kotlin.math.round - -@Composable -internal fun ResultDetailUi( - modifier: Modifier, - exercise: Exercise, - latestResult: Result, - feedbackItems: List, - latestIndividualDueDate: Instant?, - buildLogs: List -) { - val showMissingAutomaticFeedbackInformation = latestIndividualDueDate?.isInFuture() ?: false - - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (showMissingAutomaticFeedbackInformation) { - MissingFeedbackInformation( - modifier = Modifier.fillMaxWidth(), - latestIndividualDueDate = latestIndividualDueDate - ) - - Divider() - } - - if (latestResult.isPreliminary.collectAsState(initial = false).value) { - ResultIsPreliminaryWarning( - modifier = Modifier.fillMaxWidth(), - assessmentType = exercise.assessmentType - ) - - Divider() - } - - ScoreSection( - modifier = Modifier.fillMaxWidth(), - exercise = exercise, - latestResult = latestResult - ) - - if (exercise is ProgrammingExercise) { - Divider() - - Text( - text = stringResource(id = R.string.result_view_programming_exercise_chart_section_title), - style = MaterialTheme.typography.headlineMedium - ) - - ScoreResultsCard( - modifier = Modifier.fillMaxWidth(), - exercise = exercise, - feedbackItems = feedbackItems - ) - } - - if (buildLogs.isNotEmpty()) { - Divider() - - Text( - text = stringResource(id = R.string.result_view_build_log_section_title), - style = MaterialTheme.typography.headlineMedium - ) - - buildLogs.forEach { buildLog -> - BuildLogCard(modifier = Modifier.fillMaxWidth(), buildLog = buildLog) - } - } - - if (feedbackItems.isNotEmpty()) { - Divider() - - Text( - text = stringResource(id = R.string.result_view_feedback_section_title), - style = MaterialTheme.typography.headlineMedium - ) - - feedbackItems.forEach { feedbackItem -> - FeedbackCard(modifier = Modifier.fillMaxWidth(), feedbackItem = feedbackItem) - } - } - } -} - -@Composable -private fun MissingFeedbackInformation(modifier: Modifier, latestIndividualDueDate: Instant?) { - val formattedDate = remember(latestIndividualDueDate) { - if (latestIndividualDueDate == null) { - "" - } else { - SimpleDateFormat.getDateTimeInstance( - SimpleDateFormat.MEDIUM, - SimpleDateFormat.SHORT - ) - .format(Date.from(latestIndividualDueDate.toJavaInstant())) - } - } - - Text( - modifier = modifier, - text = stringResource( - id = R.string.result_view_automatic_feedback_missing, - formattedDate - ), - style = MaterialTheme.typography.bodyMedium, - fontStyle = FontStyle.Italic - ) -} - -@Composable -private fun ResultIsPreliminaryWarning( - modifier: Modifier, - assessmentType: Exercise.AssessmentType? -) { - val text = stringResource( - id = if (assessmentType == Exercise.AssessmentType.AUTOMATIC) { - R.string.result_view_preliminary_result_semi_automatic - } else R.string.result_view_preliminary_result_automatic - ) - - Text( - modifier = modifier, - text = text, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Bold - ) -} - -@Composable -private fun ScoreSection( - modifier: Modifier, - exercise: Exercise, - latestResult: Result -) { - Column(modifier = modifier) { - Text( - text = stringResource(id = R.string.result_view_score_section_title), - style = MaterialTheme.typography.headlineMedium - ) - - val userScore = latestResult.score - val achievableScore = exercise.maxPoints - val achievedPoints = userScore?.let { userScore / 100f * (achievableScore ?: 0f) } - - if (userScore != null && achievableScore != null) { - val formattedAchievedPoints = remember(achievedPoints) { - ExercisePointsDecimalFormat.format(achievedPoints) - } - - val formattedAchievablePoints = remember(achievableScore) { - ExercisePointsDecimalFormat.format(achievableScore) - } - - val formattedAchievedPercent = remember(userScore, achievableScore) { - val percent = userScore / 100f - DecimalFormat.getPercentInstance().format(percent) - } - - Text( - text = stringResource( - id = R.string.result_view_score_result, - formattedAchievedPoints, - formattedAchievablePoints, - formattedAchievedPercent - ), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.fillMaxWidth(), - fontWeight = FontWeight.Bold - ) - } - } -} - -@Composable -private fun ScoreResultsCard( - modifier: Modifier, - exercise: Exercise, - feedbackItems: List -) { - val chartValues = remember(feedbackItems) { - val maxPoints = exercise.maxPoints ?: 0f - val maxPointsWithBonus = maxPoints + (exercise.bonusPoints ?: 0f) - - val positiveCredits = - feedbackItems - .filter { it.type != ExerciseViewModel.FeedbackItemType.Test && it.credits != null && it.credits > 0 } - .sumOf { it.creditsOrZero } - - val maxCodeIssueCredits = - if (exercise is ProgrammingExercise) { - val maxStaticCodeAnalysisPenalty = exercise.maxStaticCodeAnalysisPenalty - - if (exercise.staticCodeAnalysisEnabled && maxStaticCodeAnalysisPenalty != null) { - maxPoints.toDouble() * maxStaticCodeAnalysisPenalty.toDouble() / 100.0 - } else Double.MAX_VALUE - } else Double.MAX_VALUE - - val codeIssueCredits = - -feedbackItems - .filter { it.type == ExerciseViewModel.FeedbackItemType.Issue } - .sumOf { it.actualCreditsOrZero } - .coerceAtMost(maxCodeIssueCredits) - val codeIssuePenalties = - feedbackItems - .filter { it.type == ExerciseViewModel.FeedbackItemType.Issue } - .sumOf { it.creditsOrZero } - val negativeCredits = - -feedbackItems - .filter { it.type == ExerciseViewModel.FeedbackItemType.Issue && it.credits != null && it.credits < 0 } - .sumOf { it.creditsOrZero } - - val testCaseCredits = - feedbackItems - .filter { it.type == ExerciseViewModel.FeedbackItemType.Test } - .sumOf { it.creditsOrZero } - .coerceAtMost(maxPointsWithBonus.toDouble()) - - val accuracy = exercise.course?.accuracyOfScores?.toDouble() ?: 1.0 - - val appliedNegativePoints = (codeIssueCredits + negativeCredits).rounded(accuracy) - val receivedNegativePoints = (codeIssuePenalties + negativeCredits).rounded(accuracy) - val positivePoints = (testCaseCredits + positiveCredits).rounded(accuracy) - - ChartValues( - positivePoints, - appliedNegativePoints, - receivedNegativePoints, - maxPoints, - maxPointsWithBonus - ) - } - - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(40.dp) - .clip(RoundedCornerShape(10)) - ) { - @Suppress("LocalVariableName") - val Bar = @Composable { percentage: Float, color: Color -> - if (percentage > 0) { - Box( - modifier = Modifier - .weight(percentage) - .fillMaxHeight() - .background(color = color) - ) - } - } - - Bar(chartValues.positivePointsPercentage.toFloat(), resultSuccess) - Bar(chartValues.warningPointsPercentage.toFloat(), resultMedium) - Bar(chartValues.errorPointsPercentage.toFloat(), resultBad) - Bar(chartValues.nothingPercentage.toFloat(), Color.Gray) - } - - Row(modifier = Modifier.fillMaxWidth()) { - val scoreResultModifier = Modifier.weight(1f) - ScoreResult( - modifier = scoreResultModifier, - title = stringResource(id = R.string.result_view_feedback_overview_category_correct), - points = chartValues.positivePoints, - percentage = chartValues.positivePointsPercentage, - colors = successCardColors - ) - - ScoreResult( - modifier = scoreResultModifier, - title = stringResource(id = R.string.result_view_feedback_overview_category_warning), - points = chartValues.appliedNegativePoints, - percentage = chartValues.warningPointsPercentage, - colors = neutralCardColors - ) - - ScoreResult( - modifier = scoreResultModifier, - title = stringResource(id = R.string.result_view_feedback_overview_category_wrong), - points = chartValues.receivedNegativePoints, - percentage = chartValues.receivedNegativePoints / chartValues.maxPoints, - colors = issueCardColors - ) - } - } - -} - -private val percentageFormat = DecimalFormat.getPercentInstance() - -@Composable -private fun ScoreResult( - modifier: Modifier, - title: String, - points: Double, - percentage: Double, - colors: CardColors -) { - val pointText = remember(points) { - ExercisePointsDecimalFormat.format(points) - } - - val percentageText = remember(percentage) { - percentageFormat.format(percentage) - } - - Box(modifier = modifier) { - OutlinedCard(modifier = Modifier.align(Alignment.Center), colors = colors) { - Column( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - - Text( - text = pointText, - style = MaterialTheme.typography.bodyMedium - ) - - Text( - text = percentageText, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } -} - -private val issueCardColors: CardColors - @Composable get() { - return CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ) - } - -private val neutralCardColors: CardColors - @Composable get() { - return CardDefaults.outlinedCardColors( - contentColor = Color(0xFF5A5208), - containerColor = Color(0xFFEEEBCE) - ) - } - -private val successCardColors: CardColors - @Composable get() { - return CardDefaults.outlinedCardColors( - contentColor = Color(0xFF00801D), - containerColor = Color(0xFFD1E4D4) - ) - } - -@Composable -private fun FeedbackCard(modifier: Modifier, feedbackItem: ExerciseViewModel.FeedbackItem) { - val cardColors = when (feedbackItem.type) { - ExerciseViewModel.FeedbackItemType.Issue -> neutralCardColors - ExerciseViewModel.FeedbackItemType.Test -> - if (feedbackItem.positive == true) successCardColors - else issueCardColors - else -> { - if (feedbackItem.credits == null || feedbackItem.credits == 0f) { - neutralCardColors - } else if (feedbackItem.positive == true || (feedbackItem.credits > 0f)) { - successCardColors - } else { - issueCardColors - } - } - } - - OutlinedCard( - modifier = modifier, - colors = cardColors - ) { - Column( - modifier = Modifier.padding(8.dp) - ) { - Row { - Text( - modifier = Modifier.weight(1f), - text = stringResource(id = feedbackItem.category), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold - ) - - if (feedbackItem.credits != null) { - val text: String = remember(feedbackItem.credits) { - DecimalFormat.getNumberInstance().format(feedbackItem.credits) - } - - Text( - modifier = Modifier, - text = text, -// fontStyle = MaterialTheme.typography.labelMedium - ) - } - } - - if (!feedbackItem.title.isNullOrBlank()) { - Text(text = feedbackItem.title, style = MaterialTheme.typography.titleSmall) - } - - if (feedbackItem.text != null) { - Text( - modifier = Modifier.padding(top = 4.dp), - text = feedbackItem.text, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } -} - -@Composable -private fun BuildLogCard(modifier: Modifier, buildLog: BuildLogEntry) { - val cardColors = when (buildLog.type) { - BuildLogEntry.Type.ERROR -> issueCardColors - BuildLogEntry.Type.WARNING -> neutralCardColors - BuildLogEntry.Type.OTHER -> neutralCardColors - } - - OutlinedCard(modifier = modifier, colors = cardColors) { - var showMoreButtonDisplayed by remember { - mutableStateOf(false) - } - - var showWholeLog by remember { - mutableStateOf(false) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { - AnimatedContent(targetState = showWholeLog) { doShowWholeLog -> - Text( - text = buildLog.log, - style = MaterialTheme.typography.bodySmall, - maxLines = if (doShowWholeLog) Int.MAX_VALUE else 3, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth(), - onTextLayout = { - if (it.hasVisualOverflow) { - showMoreButtonDisplayed = true - } - } - ) - } - - if (showMoreButtonDisplayed) { - Button(onClick = { showWholeLog = !showWholeLog }) { - Text(text = stringResource(id = R.string.result_view_build_log_show_entire_log)) - } - } - } - } -} - -/** - * Rounds the value to the next multiple of accuracy. - */ -private fun Double.rounded(accuracy: Double): Double { - return round(this * (1.0 / accuracy)) * accuracy -} - -private data class ChartValues( - val positivePoints: Double, - val appliedNegativePoints: Double, - val receivedNegativePoints: Double, - val maxPoints: Float, - val maxPointsWithBonus: Float -) { - val positivePointsPercentage: Double = positivePoints / maxPoints - val warningPointsPercentage: Double = appliedNegativePoints / maxPoints - val errorPointsPercentage: Double = receivedNegativePoints / maxPoints - - val nothingPercentage: Double = - 1f - (positivePointsPercentage + warningPointsPercentage + errorPointsPercentage) -} - -private class FeedbackItemProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - ExerciseViewModel.FeedbackItem( - type = ExerciseViewModel.FeedbackItemType.Feedback, - category = R.string.result_view_feedback_category_regular, - title = "Issue with 1a", - text = "1a is wrong || ".repeat(10), - positive = false, - credits = -3f, - actualCredits = null - ), - ExerciseViewModel.FeedbackItem( - type = ExerciseViewModel.FeedbackItemType.Feedback, - category = R.string.result_view_feedback_category_regular, - title = "Do this better", - text = "you coul dhave done something better", - positive = null, - credits = null, - actualCredits = null - ), - ExerciseViewModel.FeedbackItem( - type = ExerciseViewModel.FeedbackItemType.Test, - category = R.string.result_view_feedback_category_regular, - title = "checkSomethingTest Wrong", - text = "You have not changed something to something.", - positive = false, - credits = -3f, - actualCredits = null - ), - ExerciseViewModel.FeedbackItem( - type = ExerciseViewModel.FeedbackItemType.Test, - category = R.string.result_view_feedback_category_regular, - title = "checkSomethingTest Correct", - text = "Good job, successfully does something", - positive = true, - credits = 1f, - actualCredits = null - ), - ) -} - -@Preview -@Composable -private fun FeedbackCardPreview( - @PreviewParameter(FeedbackItemProvider::class) feedbackItem: ExerciseViewModel.FeedbackItem -) { - FeedbackCard( - modifier = Modifier.fillMaxWidth(), - feedbackItem = feedbackItem - ) -} - -private class BuildLogEntryProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - BuildLogEntry( - log = "Short warning log", - type = BuildLogEntry.Type.WARNING - ), - BuildLogEntry( - log = "Very long error log\n".repeat(40), - type = BuildLogEntry.Type.ERROR - ) - ) -} - -@Preview -@Composable -private fun BuildLogCardPreview( - @PreviewParameter(BuildLogEntryProvider::class) entry: BuildLogEntry -) { - BuildLogCard(modifier = Modifier.fillMaxWidth(), buildLog = entry) -} \ No newline at end of file diff --git a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/view_result/ViewResultScreen.kt b/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/view_result/ViewResultScreen.kt index dd91376a6..1ebaf3c14 100644 --- a/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/view_result/ViewResultScreen.kt +++ b/feature/exercise_view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exercise_view/view_result/ViewResultScreen.kt @@ -88,34 +88,5 @@ internal fun ViewResultScreen( ) } } - - - // Commented out because we use the web view for now. -// val latestIndividualDueDate by viewModel.latestIndividualDueDate.collectAsState() -// -// val feedbackItems by viewModel.feedbackItems.collectAsState() -// -// val buildLogs by viewModel.buildLogs.collectAsState() -// -// ExerciseDataStateUi( -// modifier = Modifier -// .fillMaxSize() -// .padding(padding), -// value = latestResultDataState join exerciseDataState, -// onClickRetry = { viewModel.requestReloadExercise() }, -// onSuccess = { (latestResult, exercise) -> -// ResultDetailUi( -// modifier = Modifier -// .fillMaxSize() -// .padding(8.dp) -// .verticalScroll(rememberScrollState()), -// exercise = exercise, -// latestResult = latestResult ?: return@ExerciseDataStateUi, -// feedbackItems = feedbackItems.orElse(emptyList()), -// latestIndividualDueDate = latestIndividualDueDate.orElse(null), -// buildLogs = buildLogs.orElse(emptyList()) -// ) -// } -// ) } -} \ No newline at end of file +} diff --git a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/service/impl/RegisterServiceImpl.kt b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/service/impl/RegisterServiceImpl.kt index aa95e5fff..b41fbe595 100644 --- a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/service/impl/RegisterServiceImpl.kt +++ b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/service/impl/RegisterServiceImpl.kt @@ -16,11 +16,14 @@ internal class RegisterServiceImpl( private val ktorProvider: KtorProvider ) : RegisterService { - override suspend fun register(account: User, serverUrl: String): NetworkResponse { + override suspend fun register( + account: User, + serverUrl: String + ): NetworkResponse { return performNetworkCall { ktorProvider.ktorClient.post(serverUrl) { url { - appendPathSegments("api", "register") + appendPathSegments("api", "public", "register") } setBody(account) diff --git a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt index 7c3fca2d7..70aeaee49 100644 --- a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt @@ -56,6 +56,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.androidx.compose.get +import org.koin.compose.koinInject private const val SETTINGS_DESTINATION = "settings" private const val PUSH_NOTIFICATION_SETTINGS_DESTINATION = "push_notification_settings" @@ -112,14 +113,14 @@ private fun SettingsScreen( ) { val linkOpener = LocalLinkOpener.current - val pushNotificationJobService: PushNotificationJobService = get() - val pushNotificationConfigurationService: PushNotificationConfigurationService = get() + val pushNotificationJobService: PushNotificationJobService = koinInject() + val pushNotificationConfigurationService: PushNotificationConfigurationService = koinInject() - val accountService: AccountService = get() + val accountService: AccountService = koinInject() val authenticationData: AccountService.AuthenticationData? by accountService.authenticationData.collectAsState( initial = null ) - val serverConfigurationService: ServerConfigurationService = get() + val serverConfigurationService: ServerConfigurationService = koinInject() val serverUrl by serverConfigurationService.serverUrl.collectAsState(initial = "") val scope = rememberCoroutineScope() @@ -136,8 +137,8 @@ private fun SettingsScreen( else -> null } - val serverDataService: ServerDataService = get() - val networkStatusProvider: NetworkStatusProvider = get() + val serverDataService: ServerDataService = koinInject() + val networkStatusProvider: NetworkStatusProvider = koinInject() val accountDataFlow: StateFlow?> = remember { flatMapLatest(