Skip to content

Commit

Permalink
Implement tutorial mode for new score board
Browse files Browse the repository at this point in the history
  • Loading branch information
J12934 committed Jul 29, 2023
1 parent 6806edc commit f344070
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 45 deletions.
Expand Up @@ -4,7 +4,7 @@ import { ChallengesUnavailableWarningComponent } from './challenges-unavailable-
import { TranslateModule } from '@ngx-translate/core'
import { DEFAULT_FILTER_SETTING } from '../../types/FilterSetting'

describe('ChallengeCard', () => {
describe('ChallengesUnavailableWarningComponent', () => {
let component: ChallengesUnavailableWarningComponent
let fixture: ComponentFixture<ChallengesUnavailableWarningComponent>

Expand Down
@@ -0,0 +1,10 @@
<div
*ngIf="tutorialModeActive"
class="tutorial-mode-warning-container"
>
<mat-icon>school</mat-icon>
<span class="tutorial-mode-warning-text">
{{ 'INFO_FULL_CHALLENGE_MODE' | translate: {num: allChallenges.length} }}
</span>
</div>

@@ -0,0 +1,13 @@
.tutorial-mode-warning-container {
align-items: center;
background-color: var(--theme-background-dark);
border-radius: 4px;
display: grid;
grid-template-columns: min-content auto;
margin-top: 24px;
padding: 12px;
}

.tutorial-mode-warning-text {
margin-left: 12px;
}
@@ -0,0 +1,78 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'

import { TutorialModeWarningComponent } from './tutorial-mode-warning.component'
import { TranslateModule } from '@ngx-translate/core'

describe('TutorialModeWarningComponent', () => {
let component: TutorialModeWarningComponent
let fixture: ComponentFixture<TutorialModeWarningComponent>

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [TutorialModeWarningComponent]
}).compileComponents()

fixture = TestBed.createComponent(TutorialModeWarningComponent)
component = fixture.componentInstance

component.allChallenges = [
{
category: 'foobar',
name: 'my name',
tutorialOrder: 1,
solved: false
},
{
category: 'foobar',
name: 'my name two',
description: 'lorem ipsum',
tutorialOrder: null,
solved: false
}
] as any

component.applicationConfig = {
challenges: {
restrictToTutorialsFirst: true
}
} as any

fixture.detectChanges()
})

it('should show warning when there are configured and unsolved tutorial challenges exist', () => {
component.ngOnChanges()
expect(component.tutorialModeActive).toBe(true)
})

it('not show if tutorial is not configured', () => {
component.applicationConfig = {
challenges: {
restrictToTutorialsFirst: false
}
} as any
component.ngOnChanges()
expect(component.tutorialModeActive).toBe(false)
})

it('should not show warning when all tutorial mode challenges are solved', () => {
component.allChallenges = [
{
category: 'foobar',
name: 'my name',
tutorialOrder: 1,
solved: true
},
{
category: 'foobar',
name: 'my name two',
description: 'lorem ipsum',
tutorialOrder: null,
solved: false
}
] as any
component.ngOnChanges()
expect(component.tutorialModeActive).toBe(false)
})
})
@@ -0,0 +1,31 @@
import { Component, Input, OnChanges } from '@angular/core'

import { EnrichedChallenge } from '../../types/EnrichedChallenge'
import { Config } from 'src/app/Services/configuration.service'

@Component({
selector: 'tutorial-mode-warning',
templateUrl: './tutorial-mode-warning.component.html',
styleUrls: ['./tutorial-mode-warning.component.scss']
})
export class TutorialModeWarningComponent implements OnChanges {
@Input()
public allChallenges: EnrichedChallenge[]

@Input()
public applicationConfig: Config | null = null

public tutorialModeActive: boolean | null = null

ngOnChanges (): void {
if (!this.applicationConfig?.challenges?.restrictToTutorialsFirst) {
this.tutorialModeActive = false
return
}

const allTutorialChallengesSolved = this.allChallenges
.filter(challenge => challenge.tutorialOrder !== null)
.every(challenge => challenge.solved)
this.tutorialModeActive = !allTutorialChallengesSolved
}
}
Expand Up @@ -16,7 +16,8 @@ const CHALLENGE_1 = {
solved: false,
codingChallengeStatus: 0,
tagList: ['easy'],
disabledEnv: null
disabledEnv: null,
tutorialOrder: 1
} as EnrichedChallenge

const CHALLENGE_2 = {
Expand All @@ -33,7 +34,8 @@ const CHALLENGE_2 = {
solved: true,
codingChallengeStatus: 2,
tagList: ['easy'],
disabledEnv: null
disabledEnv: null,
tutorialOrder: 2
} as EnrichedChallenge

const CHALLENGE_3 = {
Expand All @@ -50,13 +52,14 @@ const CHALLENGE_3 = {
solved: true,
codingChallengeStatus: 1,
tagList: ['hard'],
disabledEnv: 'docker'
disabledEnv: 'docker',
tutorialOrder: null
} as EnrichedChallenge

describe('filterChallenges', () => {
it('should filter empty list', () => {
expect(filterChallenges([], { ...DEFAULT_FILTER_SETTING })).toEqual([])
expect(filterChallenges([], { categories: ['foo', 'bar'], difficulties: [1, 2, 3, 5, 6], tags: ['hard'], status: 'solved', searchQuery: 'foobar', showDisabledChallenges: true })).toEqual([])
expect(filterChallenges([], { categories: ['foo', 'bar'], difficulties: [1, 2, 3, 5, 6], tags: ['hard'], status: 'solved', searchQuery: 'foobar', showDisabledChallenges: true, restrictToTutorialChallengesFirst: true })).toEqual([])
})

it('should filter challenges based on categories properly', () => {
Expand Down Expand Up @@ -128,4 +131,32 @@ describe('filterChallenges', () => {
{ ...DEFAULT_FILTER_SETTING, showDisabledChallenges: false }
).map((challenge) => challenge.key)).toEqual(jasmine.arrayWithExactContents(['challenge-1', 'challenge-2']))
})

it('should only show unsolved tutorial of first difficulty if no challenges are solved', () => {
expect(filterChallenges(
[CHALLENGE_1, { ...CHALLENGE_2, solved: false }, CHALLENGE_3],
{ ...DEFAULT_FILTER_SETTING, restrictToTutorialChallengesFirst: true }
).map((challenge) => challenge.key)).toEqual(jasmine.arrayWithExactContents(['challenge-1']))
})

it('should only show tutorial challenges when restrictToTutorialChallengesFirst is set', () => {
expect(filterChallenges(
[CHALLENGE_1, { ...CHALLENGE_2, solved: false, difficulty: 1, tutorialOrder: null }, CHALLENGE_3],
{ ...DEFAULT_FILTER_SETTING, restrictToTutorialChallengesFirst: true }
).map((challenge) => challenge.key)).toEqual(jasmine.arrayWithExactContents(['challenge-1']))
})

it('should only show unsolved tutorial of first difficulty and solved ones of easier difficulties', () => {
expect(filterChallenges(
[{ ...CHALLENGE_1, solved: true }, { ...CHALLENGE_2, solved: false }, CHALLENGE_3],
{ ...DEFAULT_FILTER_SETTING, restrictToTutorialChallengesFirst: true }
).map((challenge) => challenge.key)).toEqual(jasmine.arrayWithExactContents(['challenge-1', 'challenge-2']))
})

it('should only show ignore tutorial mode when all tutorial challenges are solved', () => {
expect(filterChallenges(
[{ ...CHALLENGE_1, solved: true }, { ...CHALLENGE_2, solved: true }, CHALLENGE_3],
{ ...DEFAULT_FILTER_SETTING, restrictToTutorialChallengesFirst: true }
).map((challenge) => challenge.key)).toEqual(jasmine.arrayWithExactContents(['challenge-1', 'challenge-2', 'challenge-3']))
})
})
115 changes: 81 additions & 34 deletions frontend/src/app/score-board-preview/helpers/challenge-filtering.ts
@@ -1,29 +1,38 @@
import { EnrichedChallenge } from '../types/EnrichedChallenge'
import { FilterSetting, SolvedStatus } from '../types/FilterSetting'

export function filterChallenges (challenges: EnrichedChallenge[], filterSetting: FilterSetting): EnrichedChallenge[] {
console.log('filterSetting.showDisabledChallenges', filterSetting.showDisabledChallenges)
return challenges
export function filterChallenges (
challenges: EnrichedChallenge[],
filterSetting: FilterSetting
): EnrichedChallenge[] {
console.log(
'filterSetting.showDisabledChallenges',
filterSetting.showDisabledChallenges
)
return (
challenges
// filter by category
.filter((challenge) => {
if (filterSetting.categories.length === 0) {
return true
}
return filterSetting.categories.includes(challenge.category)
})
// filter by difficulty
.filter((challenge) => {
if (filterSetting.difficulties.length === 0) {
return true
}
return filterSetting.difficulties.includes(challenge.difficulty)
})
// filter by tags
.filter((challenge) => {
if (filterSetting.tags.length === 0) {
return true
}
return challenge.tagList.some((tag) => filterSetting.tags.includes(tag))
if (filterSetting.categories.length === 0) {
return true
}
return filterSetting.categories.includes(challenge.category)
})
// filter by difficulty
.filter((challenge) => {
if (filterSetting.difficulties.length === 0) {
return true
}
return filterSetting.difficulties.includes(challenge.difficulty)
})
// filter by tags
.filter((challenge) => {
if (filterSetting.tags.length === 0) {
return true
}
return challenge.tagList.some((tag) =>
filterSetting.tags.includes(tag)
)
})
// filter by status
.filter((challenge) => {
Expand All @@ -44,22 +53,60 @@ export function filterChallenges (challenges: EnrichedChallenge[], filterSetting
if (filterSetting.searchQuery === null) {
return true
}
return challenge.name.toLowerCase().includes(filterSetting.searchQuery.toLowerCase()) ||
challenge.originalDescription.toLowerCase().includes(filterSetting.searchQuery.toLowerCase())
return (
challenge.name
.toLowerCase()
.includes(filterSetting.searchQuery.toLowerCase()) ||
challenge.originalDescription
.toLowerCase()
.includes(filterSetting.searchQuery.toLowerCase())
)
})
}
// filter by tutorial challenges
.filter((challenge) => {
if (!filterSetting.restrictToTutorialChallengesFirst) {
return true
}

function getCompleteChallengeStatus (challenge: EnrichedChallenge): SolvedStatus {
if (!challenge.solved) {
return 'unsolved'
}
const tutorialChallenges = challenges.filter(
(challenge) => challenge.tutorialOrder !== null
)
const allTutorialChallengesSolved = tutorialChallenges.every((challenge) => challenge.solved)

if (!challenge.hasCodingChallenge) {
return challenge.solved ? 'solved' : 'unsolved'
} else {
if (challenge.codingChallengeStatus === 2) {
return 'solved'
if (allTutorialChallengesSolved) {
return true
} else if (!allTutorialChallengesSolved && challenge.tutorialOrder === null) {
// there are still unsolved tutorial challenges, but this challenge is not a tutorial challenge so we don't want to show it
return false
}

// find --include difficulty of unsolved tutorial challenges, we only want to show ones matching it or easier
const difficultiesOfUnsolvedTutorialChallenges = tutorialChallenges
.filter((challenge) => !challenge.solved)
.map((challenge) => challenge.difficulty)
const easiestDifficultyOfUnsolvedTutorialChallenges = Math.min(...difficultiesOfUnsolvedTutorialChallenges)

if (challenge.difficulty <= easiestDifficultyOfUnsolvedTutorialChallenges) {
return true
}
return 'partially-solved'
return false
})
)
}

function getCompleteChallengeStatus (
challenge: EnrichedChallenge
): SolvedStatus {
if (!challenge.solved) {
return 'unsolved'
}

if (!challenge.hasCodingChallenge) {
return challenge.solved ? 'solved' : 'unsolved'
} else {
if (challenge.codingChallengeStatus === 2) {
return 'solved'
}
return 'partially-solved'
}
}
Expand Up @@ -5,6 +5,7 @@
</div>

<filter-settings
*ngIf="applicationConfiguration?.challenges.restrictToTutorialsFirst === false"
[filterSetting]="filterSetting"
(filterSettingChange)="onFilterSettingUpdate($event)"
[allChallenges]="allChallenges"
Expand All @@ -17,6 +18,11 @@
(filterSettingChange)="onFilterSettingUpdate($event)"
></challenges-unavailable-warning>

<tutorial-mode-warning
[allChallenges]="allChallenges"
[applicationConfig]="applicationConfiguration"
></tutorial-mode-warning>

<div class="challenges" *ngIf="filteredChallenges.length > 0; else emptyChallenges">
<challenge-card
*ngFor="let challenge of filteredChallenges; trackBy:getChallengeKey"
Expand Down

0 comments on commit f344070

Please sign in to comment.