Skip to content

Commit

Permalink
Code giveaways (#1306)
Browse files Browse the repository at this point in the history
* Add a table for code giveaways

* WIP

* Tests for batch insert

* Fixes

* Implement exporting

* Rework manage page navigation, and add new page for code giveaways

* Plural

* Add some indexes

* Add controllers

* Add controller for csv export

* Implement student endpoints

* WIP

* Enable CSV export

* Fix conditional block

* WIP add block to CMS

* Block in CMS

* WIP student interface

* Refactor

* WIP student frontend

* System test fixes

* System test fixes

* Update snapshots

* WIP test

* WIP requirements

* Sort translations

* Update translations

* Update

* System test fixes
  • Loading branch information
nygrenh authored Sep 3, 2024
1 parent 1e1b97d commit ec7a5be
Show file tree
Hide file tree
Showing 152 changed files with 3,918 additions and 964 deletions.
40 changes: 22 additions & 18 deletions bin/translations-add-new-ones-to-non-en
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
#!/usr/bin/env python3

BASEDIR="$(dirname "${BASH_SOURCE[0]}")"
TRANSLATIONS_EN_PATH="$BASEDIR/../shared-module/packages/common/src/locales/en"
TRANSLATIONS_EN_RELATIVE_PATH=$(realpath --relative-to="$(pwd)" "$TRANSLATIONS_EN_PATH")
FOLDERS_PATH="$BASEDIR/../shared-module/packages/common/src/locales"
FOLDERS_RELATIVE_PATH=$(realpath --relative-to="$(pwd)" "$FOLDERS_PATH")
import os
import glob
import json

BASEDIR = os.path.dirname(os.path.realpath(__file__))
TRANSLATIONS_EN_PATH = os.path.join(BASEDIR, "../shared-module/packages/common/src/locales/en")
Expand All @@ -20,20 +18,26 @@ ensure_program_in_path('jq')
ensure_program_in_path('sponge')

EN_PATHS = glob.glob(f"{TRANSLATIONS_EN_RELATIVE_PATH}/*.json")
FOLDERS_PATH = [name for name in glob.glob(f"{FOLDERS_RELATIVE_PATH}/*") if os.path.isdir(name) and not name.endswith('/en')]
OTHER_LANGUAGE_FOLDERS = [name for name in glob.glob(f"{FOLDERS_RELATIVE_PATH}/*") if os.path.isdir(name) and not name.endswith('/en')]

for original_file in EN_PATHS:
for target_folder in FOLDERS_PATH:
target_folder_language = os.path.basename(target_folder)
target_file = original_file.replace("/en/", f"/{target_folder_language}/")
print(f"Adding translations {original_file} > {target_file}")
with open(original_file) as f1, open(target_file) as f2:
data1 = json.load(f1)
data2 = json.load(f2)
missing_keys = [key for key in data1 if key not in data2]
for key in missing_keys:
data2[key] = data1[key]
with open(target_file, 'w') as f:
json.dump(data2, f, indent=2, ensure_ascii=False)
for target_folder in OTHER_LANGUAGE_FOLDERS:
target_folder_language = os.path.basename(target_folder)
target_file = original_file.replace("/en/", f"/{target_folder_language}/")
print(f"Adding translations {original_file} > {target_file}")

if os.path.exists(target_file):
with open(original_file) as f1, open(target_file) as f2:
data1 = json.load(f1)
data2 = json.load(f2)

missing_keys = [key for key in data1 if key not in data2]
for key in missing_keys:
data2[key] = data1[key]

with open(target_file, 'w') as f:
json.dump(data2, f, indent=2, ensure_ascii=False)
else:
print(f"Target file {target_file} does not exist. Skipping.")

print("\nImportant: remember to translate the new strings before adding them to version control")
97 changes: 97 additions & 0 deletions services/cms/src/blocks/CodeGiveaway/CodeGiveawayBlockEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import styled from "@emotion/styled"
import { useQuery } from "@tanstack/react-query"
import { InnerBlocks, InspectorControls } from "@wordpress/block-editor"
import { BlockEditProps } from "@wordpress/blocks"
import React, { useContext, useMemo } from "react"
import { useTranslation } from "react-i18next"

import PageContext from "../../contexts/PageContext"
import { fetchCourseInstances } from "../../services/backend/course-instances"
import { fetchCourseModulesByCourseId } from "../../services/backend/courses"
import BlockPlaceholderWrapper from "../BlockPlaceholderWrapper"

import { ConditionAttributes } from "."

import InnerBlocksWrapper from "@/components/blocks/InnerBlocksWrapper"
import CourseContext from "@/contexts/CourseContext"
import { fetchCodeGiveawaysByCourseId } from "@/services/backend/code-giveaways"
import DropdownMenu from "@/shared-module/common/components/DropdownMenu"
import SelectField from "@/shared-module/common/components/InputFields/SelectField"
import { assertNotNullOrUndefined } from "@/shared-module/common/utils/nullability"

const ALLOWED_NESTED_BLOCKS = [
"core/heading",
"core/buttons",
"core/button",
"core/paragraph",
"core/image",
"core/embed",
]

const Wrapper = styled.div`
margin-left: 1rem;
margin-right: 1rem;
height: auto;
`

const CodeGiveawayBlockEditor: React.FC<
React.PropsWithChildren<BlockEditProps<ConditionAttributes>>
> = ({ attributes, clientId, setAttributes }) => {
const { t } = useTranslation()
const courseId = useContext(PageContext)?.page.course_id

const codeGivawayQuery = useQuery({
queryKey: [`/code-giveaways/by-course/${courseId}`],
queryFn: () => fetchCodeGiveawaysByCourseId(assertNotNullOrUndefined(courseId)),
enabled: !!courseId,
})

const title = useMemo(() => {
let title = t("code-giveaway")
if (codeGivawayQuery.data) {
const selected = codeGivawayQuery.data.find((o) => o.id === attributes.code_giveaway_id)
if (selected) {
title += ` (${selected.name})`
}
}
return title
}, [attributes.code_giveaway_id, codeGivawayQuery.data, t])

const dropdownOptions = useMemo(() => {
const res = [{ label: t("select-an-option"), value: "" }]
if (!codeGivawayQuery.data) {
return res
}
const additional = codeGivawayQuery.data.map((o) => ({
label: o.name,
value: o.id,
}))
return res.concat(additional)
}, [codeGivawayQuery.data, t])

return (
<BlockPlaceholderWrapper
id={clientId}
title={title}
explanation={t("code-giveaway-explanation")}
>
<InspectorControls>
{codeGivawayQuery.data && (
<Wrapper>
<SelectField
label={t("code-giveaway")}
options={dropdownOptions}
defaultValue={attributes.code_giveaway_id}
onChangeByValue={(value) => setAttributes({ code_giveaway_id: value })}
/>
</Wrapper>
)}
</InspectorControls>
<InnerBlocksWrapper title={t("instructions")}>
<InnerBlocks allowedBlocks={ALLOWED_NESTED_BLOCKS} />
</InnerBlocksWrapper>
</BlockPlaceholderWrapper>
)
}

export default CodeGiveawayBlockEditor
11 changes: 11 additions & 0 deletions services/cms/src/blocks/CodeGiveaway/CodeGiveawayBlockSave.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { InnerBlocks } from "@wordpress/block-editor"

const CodeGiveawayBlockSave: React.FC = () => {
return (
<div>
<InnerBlocks.Content />
</div>
)
}

export default CodeGiveawayBlockSave
28 changes: 28 additions & 0 deletions services/cms/src/blocks/CodeGiveaway/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable i18next/no-literal-string */
import { BlockConfiguration } from "@wordpress/blocks"

import { MOOCFI_CATEGORY_SLUG } from "../../utils/Gutenberg/modifyGutenbergCategories"

import CodeGiveawayBlockEditor from "./CodeGiveawayBlockEditor"
import CodeGiveawayBlockSave from "./CodeGiveawayBlockSave"

export interface ConditionAttributes {
code_giveaway_id: string
}

const ConditionalBlockConfiguration: BlockConfiguration<ConditionAttributes> = {
title: "CodeGiveaway",
description:
"Used to place a code giveaway to a page. Make sure to have created a code giveaway in the manage page.",
category: MOOCFI_CATEGORY_SLUG,
attributes: {
code_giveaway_id: {
type: "string",
default: "",
},
},
edit: CodeGiveawayBlockEditor,
save: CodeGiveawayBlockSave,
}

export default ConditionalBlockConfiguration
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import BlockPlaceholderWrapper from "../BlockPlaceholderWrapper"

import { ConditionAttributes } from "."

import InnerBlocksWrapper from "@/components/blocks/InnerBlocksWrapper"
import CheckBox from "@/shared-module/common/components/InputFields/CheckBox"
import { assertNotNullOrUndefined } from "@/shared-module/common/utils/nullability"

Expand Down Expand Up @@ -66,7 +67,7 @@ const ConditionalBlockEditor: React.FC<
return (
<CheckBox
key={mod.id}
label={mod.name ?? t("default")}
label={mod.name ?? t("label-default")}
value={mod.id}
onChange={() => {
const previuoslyChecked = requiredModules.some((modId) => modId == mod.id)
Expand All @@ -90,7 +91,7 @@ const ConditionalBlockEditor: React.FC<
return (
<CheckBox
key={inst.id}
label={inst.name ?? t("default")}
label={inst.name ?? t("label-default")}
value={inst.id}
onChange={() => {
const previuoslyChecked = requiredInstanceEnrollment.some(
Expand All @@ -112,7 +113,9 @@ const ConditionalBlockEditor: React.FC<
</Wrapper>
)}
</InspectorControls>
<InnerBlocks allowedBlocks={ALLOWED_NESTED_BLOCKS} />
<InnerBlocksWrapper title={t("conditionally-shown-content")}>
<InnerBlocks allowedBlocks={ALLOWED_NESTED_BLOCKS} />
</InnerBlocksWrapper>
</BlockPlaceholderWrapper>
)
}
Expand Down
3 changes: 3 additions & 0 deletions services/cms/src/blocks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Aside from "./Aside"
import Author from "./Author"
import AuthorInnerBlock from "./AuthorInnerBlock"
import ChapterProgress from "./ChapterProgress"
import CodeGiveaway from "./CodeGiveaway"
import ConditionalBlock from "./ConditionalBlock"
import Congratulations from "./Congratulations"
import CourseChapterGrid from "./CourseChapterGrid"
Expand Down Expand Up @@ -68,6 +69,7 @@ export const blockTypeMapForPages = [
["moocfi/exercise-custom-view-block", ExerciseCustomView],
["moocfi/partners", PartnersBlock],
["moocfi/top-level-pages", TopLevelPage],
["moocfi/code-giveaway", CodeGiveaway],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as Array<[string, BlockConfiguration<Record<string, any>>]>

Expand All @@ -87,6 +89,7 @@ export const blockTypeMapForFrontPages = [
["moocfi/map", Map],
["moocfi/conditional-block", ConditionalBlock],
["moocfi/exercise-custom-view-block", ExerciseCustomView],
["moocfi/code-giveaway", CodeGiveaway],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as Array<[string, BlockConfiguration<Record<string, any>>]>

Expand Down
21 changes: 21 additions & 0 deletions services/cms/src/components/blocks/InnerBlocksWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { css } from "@emotion/css"

const InnerBlocksWrapper: React.FC<React.PropsWithChildren<{ title: string }>> = ({
title,
children,
}) => {
return (
<div
className={css`
width: 100%;
border: 1px solid #e2e2e2;
padding: 1rem;
`}
>
<h4>{title}</h4>
{children}
</div>
)
}

export default InnerBlocksWrapper
8 changes: 8 additions & 0 deletions services/cms/src/services/backend/code-giveaways.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { cmsClient } from "./cmsClient"

import { validateResponse } from "@/shared-module/common/utils/fetching"

export const fetchCodeGiveawaysByCourseId = async (courseId: string) => {
const response = await cmsClient.get(`/code-giveaways/by-course/${courseId}`)
return validateResponse(response, Array.isArray)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import AudioPlayer from "./moocfi/AudioPlayer/index"
import AuthorBlock from "./moocfi/AuthorBlock"
import AuthorInnerBlock from "./moocfi/AuthorInnerBlock"
import ChapterProgressBlock from "./moocfi/ChapterProgressBlock"
import CodeGiveawayBlock from "./moocfi/CodeGiveAway"
import ConditionalBlock from "./moocfi/ConditionalBlock"
import CongratulationsBlock from "./moocfi/CongratulationsBlock"
import CourseChapterGridBlock from "./moocfi/CourseChapterGridBlock"
Expand Down Expand Up @@ -159,6 +160,7 @@ export const blockToRendererMap: { [blockName: string]: any } = {
"moocfi/author-inner-block": AuthorInnerBlock,
"moocfi/research-consent-question": ResearchConsentQuestionBlock,
"moocfi/exercise-custom-view-block": ExerciseCustomViewBlock,
"moocfi/code-giveaway": CodeGiveawayBlock,
}

const highlightedBlockStyles = css`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useTranslation } from "react-i18next"

import { claimCodeFromCodeGiveaway } from "@/services/backend"
import Button from "@/shared-module/common/components/Button"
import ErrorBanner from "@/shared-module/common/components/ErrorBanner"
import useToastMutation from "@/shared-module/common/hooks/useToastMutation"
import { assertNotNullOrUndefined } from "@/shared-module/common/utils/nullability"

interface ClaimCodeProps {
codeGiveawayId: string
onClaimed: () => void
}

const ClaimCode: React.FC<ClaimCodeProps> = ({ codeGiveawayId, onClaimed }) => {
const { t } = useTranslation()

const claimCodeMutation = useToastMutation(
() => claimCodeFromCodeGiveaway(assertNotNullOrUndefined(codeGiveawayId)),
{ notify: false },
)

return (
<>
{claimCodeMutation.isError && (
<ErrorBanner error={claimCodeMutation.error} variant="readOnly" />
)}
<Button
onClick={async () => {
await claimCodeMutation.mutateAsync()
onClaimed()
}}
variant="primary"
size="medium"
disabled={claimCodeMutation.isPending}
>
{t("claim-code")}
</Button>
</>
)
}

export default ClaimCode
Loading

0 comments on commit ec7a5be

Please sign in to comment.