Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
88 changes: 77 additions & 11 deletions mobile-app/lib/models/learn/curriculum_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ class Block {
return BlockLabel.fromValue(type);
}

final challengeOrder = _scopedChallengeOrder(
data['challengeOrder'] as List,
superBlockDashedName,
);

return Block(
superBlock: SuperBlock(
dashedName: superBlockDashedName,
Expand All @@ -242,31 +247,92 @@ class Block {
dashedName: dashedName,
description: finalDescription,
order: data['order'],
challenges: (data['challengeOrder'] as List)
challenges: challengeOrder
.map<ChallengeOrder>(
(dynamic challenge) => ChallengeOrder(
id: challenge[0] ?? challenge['id'],
title: challenge[1] ?? challenge['title'],
id: _challengeId(challenge),
title: _challengeTitle(challenge),
),
)
.toList(),
challengeTiles: (data['challengeOrder'] as List)
challengeTiles: challengeOrder
.map<ChallengeListTile>(
(dynamic challenge) => ChallengeListTile(
id: challenge[0] ?? challenge['id'],
name: challenge[1] ?? challenge['title'],
dashedName: challenge[1] ??
challenge['title']
.toLowerCase()
.replaceAll(' ', '-')
.replaceAll(RegExp(r"[@':]"), ''),
id: _challengeId(challenge),
name: _challengeTitle(challenge),
dashedName: _challengeDashedName(challenge),
),
)
.toList(),
);
}
}

String _challengeId(dynamic challenge) {
if (challenge is List) {
return challenge[0];
}

return challenge['id'];
}

String _challengeTitle(dynamic challenge) {
if (challenge is List) {
return challenge[1];
}

return challenge['title'];
}

String _challengeDashedName(dynamic challenge) {
if (challenge is Map && challenge['dashedName'] != null) {
return challenge['dashedName'];
}

return _challengeTitle(challenge)
.toLowerCase()
.replaceAll(' ', '-')
.replaceAll(RegExp(r"[@':]"), '');
}

List<dynamic> _scopedChallengeOrder(
List<dynamic> challengeOrder,
String superBlockDashedName,
) {
final seenIds = <String>{};
final scopedChallengeOrder = <dynamic>[];

for (final challenge in challengeOrder) {
if (!_belongsToSuperBlock(challenge, superBlockDashedName)) {
continue;
}

final id = _challengeId(challenge);

if (seenIds.add(id)) {
scopedChallengeOrder.add(challenge);
}
}

return scopedChallengeOrder;
}

bool _belongsToSuperBlock(dynamic challenge, String superBlockDashedName) {
if (challenge is! Map) {
return true;
}

// Prefer superBlock when it is present because it is the direct curriculum
// route segment the mobile app uses to fetch block and challenge data.
final challengeSuperBlock =
challenge['superBlock'] ?? challenge['superblock'];
if (challengeSuperBlock != null) {
return challengeSuperBlock == superBlockDashedName;
}

return true;
}

class ChallengeListTile {
final String id;
final String name;
Expand Down
104 changes: 104 additions & 0 deletions mobile-app/test/unit/curriculum_model_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:freecodecamp/models/learn/curriculum_model.dart';

void main() {
group('Block.fromJson', () {
test('filters challenge order entries from other superblocks', () {
final block = Block.fromJson(
{
'name': 'Build a Greeting Bot',
'blockLayout': 'challenge-grid',
'blockLabel': 'workshop',
'challengeOrder': [
{
'id': 'js-step-1',
'title': 'Step 1',
'superBlock': 'javascript-v9',
},
{
'id': 'js-step-2',
'title': 'Step 2',
'superBlock': 'javascript-v9',
},
{
'id': 'rwd-step-1',
'title': 'Step 1',
'superBlock': 'responsive-web-design-v9',
},
{
'id': 'rwd-step-2',
'title': 'Step 2',
'superBlock': 'responsive-web-design-v9',
},
],
},
[],
'workshop-greeting-bot',
'javascript-v9',
'JavaScript Certification',
);

expect(block.challenges.map((challenge) => challenge.id), [
'js-step-1',
'js-step-2',
]);
expect(block.challengeTiles.map((challenge) => challenge.id), [
'js-step-1',
'js-step-2',
]);
});

test('deduplicates repeated challenge ids', () {
final block = Block.fromJson(
{
'name': 'Build a Greeting Bot',
'blockLayout': 'challenge-grid',
'blockLabel': 'workshop',
'challengeOrder': [
{'id': 'step-1', 'title': 'Step 1'},
{'id': 'step-2', 'title': 'Step 2'},
{'id': 'step-1', 'title': 'Step 1'},
{'id': 'step-2', 'title': 'Step 2'},
],
},
[],
'workshop-greeting-bot',
'javascript-v9',
'JavaScript Certification',
);

expect(block.challenges.map((challenge) => challenge.id), [
'step-1',
'step-2',
]);
expect(block.challengeTiles.length, 2);
});

test('supports legacy list challenge order entries', () {
final block = Block.fromJson(
{
'name': 'Legacy Block',
'blockLayout': 'challenge-list',
'blockLabel': 'legacy',
'challengeOrder': [
['step-1', 'Step 1'],
['step-2', 'Step 2'],
],
},
[],
'legacy-block',
'responsive-web-design',
'Responsive Web Design',
);

expect(block.challenges.map((challenge) => challenge.id), [
'step-1',
'step-2',
]);
expect(block.challengeTiles.map((challenge) => challenge.dashedName), [
'step-1',
'step-2',
]);
});
});
}
Loading