Skip to content

Commit

Permalink
Adding certificate preview to the rewards modal and skillmap tests (#…
Browse files Browse the repository at this point in the history
…8592)

* Adding certificate preview to the rewards modal and skillmap tests

* Fix lint and remove extra css

* Add the ability to specify a name for the badge
  • Loading branch information
riknoll authored Nov 16, 2021
1 parent 2b70988 commit 98d1858
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 35 deletions.
8 changes: 7 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,10 @@ const copySkillmapHtml = () => rimraf("webapp/public/skillmap.html")

const skillmap = gulp.series(cleanSkillmap, buildSkillmap, gulp.series(copySkillmapCss, copySkillmapJs, copySkillmapHtml));

const buildSkillmapTests = () => compileTsProject("skillmap/tests", "built/tests");
const runSkillmapTests = () => exec("./node_modules/.bin/mocha ./built/tests/tests/skillmapParser.spec.js", true)

const testSkillmap = gulp.series(buildSkillmapTests, runSkillmapTests);

/********************************************************
Tests and Linting
Expand Down Expand Up @@ -652,7 +656,8 @@ const testAll = gulp.series(
testpytraces,
testtutorials,
testlanguageservice,
karma
karma,
testSkillmap
)

function testTask(testFolder, testFile) {
Expand Down Expand Up @@ -723,6 +728,7 @@ exports.webapp = gulp.series(
browserifyWebapp
)

exports.skillmapTest = testSkillmap;
exports.updatestrings = updatestrings;
exports.updateblockly = copyBlockly;
exports.lint = lint
Expand Down
2 changes: 1 addition & 1 deletion react-common/components/profile/BadgeInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const badgeDescription = (badge: pxt.auth.Badge) => {
case "skillmap-completion":
return <span>{jsxLF(
lf("Completing {0}"),
<a target="_blank" href={sourceURLToSkillmapURL(badge.sourceURL)}>{pxt.U.rlf(badge.title)}</a>
<a target="_blank" rel="noopener noreferrer" href={sourceURLToSkillmapURL(badge.sourceURL)}>{pxt.U.rlf(badge.title)}</a>
)}</span>
}
}
Expand Down
1 change: 1 addition & 0 deletions react-common/styles/profile/profile.css
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@
margin-right: 0;
text-align: center;
color: var(--header-text-color);
overflow: hidden;
}

.profile-initials-portrait {
Expand Down
1 change: 1 addition & 0 deletions skillmap/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ module.exports = {
"parserOptions": {
"project": "skillmap/tsconfig.json",
},
"ignorePatterns": ["tests/**/*.spec.ts", "public/**/*", "build/**/*"]
}
9 changes: 7 additions & 2 deletions skillmap/src/components/AppModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,11 @@ export class AppModalImpl extends React.Component<AppModalProps, AppModalState>

return <Modal title={title} onClose={this.handleOnClose} actions={buttons}>
{lf("Use the button below to get your completion certificate!")}
{reward.previewUrl &&
<div className="certificate-reward">
<img src={reward.previewUrl} alt={lf("certificate Preview")} />
</div>
}
</Modal>
}

Expand All @@ -523,7 +528,7 @@ export class AppModalImpl extends React.Component<AppModalProps, AppModalState>
if (signedIn) {
message = jsxLF(
lf("You’ve received the {0} Badge! Find it in the badges section of your {1}."),
<span>{pxt.U.rlf(skillMap!.displayName)}</span>,
<span>{pxt.U.rlf(badge!.title)}</span>,
<a onClick={goToBadges}>{lf("User Profile")}</a>
);
buttons.push(
Expand Down Expand Up @@ -570,10 +575,10 @@ export class AppModalImpl extends React.Component<AppModalProps, AppModalState>


return <Modal title={title} onClose={this.handleOnClose} actions={buttons}>
{message}
<div className="badge-modal-image">
<Badge badge={badge!} />
</div>
{message}
</Modal>
}
}
Expand Down
2 changes: 2 additions & 0 deletions skillmap/src/lib/skillMap.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,13 @@ type MapReward = MapRewardCertificate | MapCompletionBadge;
interface MapRewardCertificate {
type: "certificate";
url: string;
previewUrl?: string;
}

interface MapCompletionBadge {
type: "completion-badge";
imageUrl: string;
displayName?: string;
}

interface MapRewardNode extends BaseNode {
Expand Down
152 changes: 124 additions & 28 deletions skillmap/src/lib/skillMapParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@

const testMap = ``

interface MarkdownSection {
export interface MarkdownSection {
headerKind: "single" | "double" | "triple";
header: string;
attributes: { [index: string]: string };
listAttributes?: { [index: string]: string[] };
listAttributes?: { [index: string]: MarkdownList };
}

export interface MarkdownList {
key: string;
items: (string | MarkdownList)[];
}

export function test() {
Expand Down Expand Up @@ -41,14 +46,17 @@ export function parseSkillMap(text: string): { maps: SkillMap[], metadata?: Page
return { maps: parsed, metadata };
}

function getSectionsFromText(text: string) {
export function getSectionsFromText(text: string) {
const lines = text.split("\n");

let sections: MarkdownSection[] = [];
let currentSection: MarkdownSection | null = null;

let currentKey: string | null = null;
let currentValue: string | null = null;
let listStack: MarkdownList[] = [];

let currentIndent = 0;

for (const line of lines) {
if (!line.trim()) {
Expand Down Expand Up @@ -77,26 +85,60 @@ function getSectionsFromText(text: string) {
}

if (currentSection) {
const keyMatch = /^[*-]\s+(?:([^:]+):)?(.*)$/.exec(line);
const subkeyMatch = /^ {4}([*-])\s+(.*)$/.exec(line);
const indent = countIndent(line);
const trimmedLine = line.trim();

const keyMatch = /^[*-]\s+(?:([^:]+):)?(.*)$/.exec(trimmedLine);
if (!keyMatch) continue;

// We ignore indent changes of 1 space to make the list authoring a little
// bit friendlier. Likewise, indents can be any length greater than 1 space
if (Math.abs(indent - currentIndent) > 1 && currentKey) {
if (indent > currentIndent) {
const newList = {
key: currentKey,
items: []
};

if (listStack.length) {
listStack[listStack.length - 1].items.push(newList);
}
else {
if (!currentSection.listAttributes) currentSection.listAttributes = {};
currentSection.listAttributes[currentKey] = newList;
}
currentKey = null;
listStack.push(newList);
}
else {
const prev = listStack.pop();

if (currentKey && currentValue) {
prev?.items.push((currentKey + ":" + currentValue).trim())
currentValue = null;
}
}

currentIndent = indent;
}

if (keyMatch) {
if (keyMatch[1]) {
if (currentKey && currentValue) {
currentSection.attributes[currentKey] = currentValue.trim();
if (listStack.length) {
listStack[listStack.length - 1].items.push((currentKey + ":" + currentValue).trim());
}
else {
currentSection.attributes[currentKey] = currentValue.trim();
}
}

currentKey = keyMatch[1].toLowerCase();
currentValue = keyMatch[2];
}
else if (currentKey) {
currentValue += keyMatch[2];
}
} else if (subkeyMatch && currentKey) {
if (!currentSection.listAttributes) currentSection.listAttributes = {};
if (!currentSection.listAttributes[currentKey]) currentSection.listAttributes[currentKey] = [];
if (subkeyMatch[2]) {
currentSection.listAttributes[currentKey].push(subkeyMatch[2].trim());
}
}
}
}
Expand All @@ -108,10 +150,17 @@ function getSectionsFromText(text: string) {
function pushSection() {
if (currentSection) {
if (currentKey && currentValue) {
currentSection.attributes[currentKey] = currentValue.trim();
if (listStack.length) {
listStack[listStack.length - 1].items.push((currentKey + ":" + currentValue).trim());
}
else {
currentSection.attributes[currentKey] = currentValue.trim();
}
}
sections.push(currentSection);
}

listStack = [];
}
}

Expand Down Expand Up @@ -276,7 +325,7 @@ function inflateMapReward(section: MarkdownSection, base: Partial<MapRewardNode>

if (section.listAttributes?.["actions"]) {
const parsedActions: MapCompletionAction[] = [];
const actions = section.listAttributes["actions"];
const actions = section.listAttributes["actions"].items.filter(a => typeof a === "string") as string[];
for (const action of actions) {
let [kind, ...rest] = action.split(":");
const valueMatch = /\s*\[\s*(.*)\s*\](?:\(([^\s]+)\))?/gi.exec(rest.join(":"));
Expand Down Expand Up @@ -317,22 +366,58 @@ function inflateMapReward(section: MarkdownSection, base: Partial<MapRewardNode>
if (section.listAttributes?.["rewards"]) {
const parsedRewards: MapReward[] = [];
const rewards = section.listAttributes["rewards"];
for (const reward of rewards) {
let [kind, ...value] = reward.split(":");

switch (kind) {
case "certificate":
parsedRewards.push({
for (const reward of rewards.items) {
if (typeof reward === "string") {
let [kind, ...value] = reward.split(":");

switch (kind) {
case "certificate":
parsedRewards.push({
type: "certificate",
url: value.join(":").trim()
});
break;
case "completion-badge":
parsedRewards.push({
type: "completion-badge",
imageUrl: value.join(":").trim()
});
break;
}
}
else {
if (reward.key === "certificate") {
const props = reward.items.filter(i => typeof i === "string") as string[]
const cert: Partial<MapRewardCertificate> = {
type: "certificate",
url: value.join(":").trim()
});
break;
case "completion-badge":
parsedRewards.push({
};

for (const prop of props) {
let [kind, ...value] = prop.split(":");

if (kind === "url") cert.url = value.join(":").trim();
if (kind === "previewurl" || kind === "preview") cert.previewUrl = value.join(":").trim();
}

if (!cert.url) error(`Certificate in activity ${section.header} is missing url attribute`);
parsedRewards.push(cert as MapRewardCertificate);
}
else if (reward.key === "completion-badge") {
const props = reward.items.filter(i => typeof i === "string") as string[]
const badge: Partial<MapCompletionBadge> = {
type: "completion-badge",
imageUrl: value.join(":").trim()
});
break;
};

for (const prop of props) {
let [kind, ...value] = prop.split(":");

if (kind === "imageurl" || kind === "image") badge.imageUrl = value.join(":").trim();
if (kind === "displayname" || kind === "name") badge.displayName = value.join(":").trim();
}

if (!badge.imageUrl) error(`completion-badge in activity ${section.header} is missing imageurl attribute`);
parsedRewards.push(badge as MapCompletionBadge);
}
}
}

Expand Down Expand Up @@ -500,5 +585,16 @@ function error(message: string): never {
throw(message);
}

// Handles tabs and spaces, but a mix of them might end up with strange results. Not much
// we can do about that so just treat 1 tab as 4 spaces
function countIndent(line: string) {
let indent = 0;
for (let i = 0; i < line.length; i++) {
if (line.charAt(i) === " ") indent++;
else if (line.charAt(i) === "\t") indent += 4;
else return indent;
}
return 0;
}


5 changes: 3 additions & 2 deletions skillmap/src/lib/skillMapUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,13 @@ export function getCompletedBadges(user: UserState, pageSource: string, map: Ski
}

export function getCompletionBadge(pageSource: string, map: SkillMap, node: MapRewardNode): pxt.auth.Badge {
const badge = node.rewards.filter(b => b.type === "completion-badge")[0] as MapCompletionBadge;
return {
id: `skillmap-completion-${map.mapId}`,
image: (node.rewards.filter(b => b.type === "completion-badge")[0] as MapCompletionBadge)?.imageUrl,
image: badge?.imageUrl,
sourceURL: pageSource,
type: "skillmap-completion",
title: map.displayName
title: badge?.displayName || map.displayName
};
}

Expand Down
17 changes: 17 additions & 0 deletions skillmap/src/styles/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,23 @@
max-width: 100%;
}

/* CERTIFICATE MODAL */
.certificate-reward {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1rem
}

.certificate-reward img {
max-width: 50%;
}

/* BADGE MODAL */
.badge-modal-image {
margin-top: 1rem;
}

/* COMPLETION MODAL */
.completion-reward {
display: flex;
Expand Down
Loading

0 comments on commit 98d1858

Please sign in to comment.