Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding certificate preview to the rewards modal and skillmap tests #8592

Merged
merged 5 commits into from
Nov 16, 2021
Merged
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
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":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to support this case? the only map that could've been using it (before this current pr changed the syntax) was our own hour of code, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a few right now, i can remove this once release shenanigans are settled but best to keep it for now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good! lgtm otherwise

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