From 68b92e89a5e7490c02eea88203bf6e83809084d9 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Wed, 1 May 2024 12:07:13 +0200 Subject: [PATCH] create(exercise): allow specifying `author` and/or `difficulty` (#864) This commit adds support for specifying the `author` and/or `difficulty` when creating a new exercise via `configlet create --practice-exercise` --- README.md | 2 + src/cli.nim | 17 +++++++- src/create/create.nim | 15 ++++++- src/create/exercises.nim | 23 +++++++++-- tests/test_binary_create.nim | 80 +++++++++++++++++++++++++++++++++++- 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b5a3c5f5..4f6e938e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ Options for create: --concept-exercise The slug of the concept exercise -e, --exercise Only operate on this exercise -o, --offline Do not update the cached 'problem-specifications' data + -a, --author The author of the exercise, approach or article + -d, --difficulty The difficulty of the exercise (default: 1) Options for fmt: -e, --exercise Only operate on this exercise diff --git a/src/cli.nim b/src/cli.nim index 50ed3272..675e5cf6 100644 --- a/src/cli.nim +++ b/src/cli.nim @@ -1,4 +1,4 @@ -import std/[os, parseutils, strformat, strutils, terminal] +import std/[options, os, parseutils, strformat, strutils, terminal] import pkg/supersnappy import patched_libs/parseopt3 @@ -47,6 +47,8 @@ type # in object variants. exerciseCreate*: string offlineCreate*: bool + author*: string + difficulty*: Option[int] of actFmt: # We can't name these fields `exercise`, `update`, and `yes` because we # use those names in `actSync`, and Nim doesn't yet support duplicate @@ -88,6 +90,8 @@ type optCreateArticle = "article" optCreateConceptExercise = "conceptExercise" optCreatePracticeExercise = "practiceExercise" + optCreateAuthor = "author" + optCreateDifficulty = "difficulty" # Options for `completion` optCompletionShell = "shell" @@ -244,6 +248,8 @@ func genHelpText: string = optCreateArticle: "The slug of the article", optCreateConceptExercise: "The slug of the concept exercise", optCreatePracticeExercise: "The slug of the practice exercise", + optCreateAuthor: "The author of the exercise, approach or article", + optCreateDifficulty: "The difficulty of the exercise (default: 1)", optCompletionShell: &"Choose the shell type (required)\n" & &"{paddingOpt}{allowedValues(Shell)}", optFmtSyncCreateExercise: "Only operate on this exercise", @@ -552,6 +558,15 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) = setActionOpt(practiceExerciseSlug, val) of optInfoSyncCreateOffline: setActionOpt(offlineCreate, true) + of optCreateAuthor: + setActionOpt(author, val) + of optCreateDifficulty: + var num = -1 + discard parseSaturatedNatural(val, num) + if num notin 1..10: + showError(&"value for {formatOpt(kind, key)} is not between " & + &" 1 to 10: {val}") + setActionOpt(difficulty, some(num)) else: discard of actFmt: diff --git a/src/create/create.nim b/src/create/create.nim index cfcbc951..f572af72 100644 --- a/src/create/create.nim +++ b/src/create/create.nim @@ -1,4 +1,4 @@ -import std/[os, strformat] +import std/[options, os, strformat] import ".."/[cli, helpers, sync/sync, types_track_config] import "."/[approaches, articles, exercises] @@ -13,6 +13,10 @@ proc create*(conf: Conf) = let msg = &"Both --approach and --article were provided. Please specify only one." stderr.writeLine msg quit QuitFailure + if conf.action.difficulty.isSome: + let msg = "The difficulty argument is not supported for approaches" + stderr.writeLine msg + quit QuitFailure let trackConfigPath = conf.trackDir / "config.json" let trackConfig = parseFile(trackConfigPath, TrackConfig) let trackExerciseSlugs = getSlugs(trackConfig.exercises, conf, trackConfigPath) @@ -36,6 +40,10 @@ proc create*(conf: Conf) = let msg = "Please specify an exercise to create an article for, using --exercise " stderr.writeLine msg quit QuitFailure + if conf.action.difficulty.isSome: + let msg = "The difficulty argument is not supported for articles" + stderr.writeLine msg + quit QuitFailure let trackConfigPath = conf.trackDir / "config.json" let trackConfig = parseFile(trackConfigPath, TrackConfig) let trackExerciseSlugs = getSlugs(trackConfig.exercises, conf, trackConfigPath) @@ -55,6 +63,11 @@ proc create*(conf: Conf) = createArticle(Slug(conf.action.articleSlug), userExercise, exerciseDir) elif conf.action.conceptExerciseSlug.len > 0: + if conf.action.difficulty.isSome: + let msg = "The difficulty argument is not supported for concept exercises" + stderr.writeLine msg + quit QuitFailure + createConceptExercise(conf) elif conf.action.practiceExerciseSlug.len > 0: createPracticeExercise(conf) diff --git a/src/create/exercises.nim b/src/create/exercises.nim index 21bc05f0..b78fccf7 100644 --- a/src/create/exercises.nim +++ b/src/create/exercises.nim @@ -1,7 +1,7 @@ import std/[sets, options, os, strformat] -import ".."/[cli, helpers, logger, fmt/track_config, sync/probspecs, sync/sync, - sync/sync_filepaths, sync/sync_metadata, types_exercise_config, - types_track_config, uuid/uuid] +import ".."/[cli, helpers, logger, fmt/exercises, fmt/track_config, + sync/probspecs, sync/sync, sync/sync_common, sync/sync_filepaths, + sync/sync_metadata, types_exercise_config, types_track_config, uuid/uuid] proc verifyExerciseDoesNotExist(conf: Conf, slug: string): tuple[trackConfig: TrackConfig, trackConfigPath: string, exercise: Slug] = let trackConfigPath = conf.trackDir / "config.json" @@ -53,10 +53,25 @@ proc syncExercise(conf: Conf, slug: Slug,) = ) discard syncImpl(syncConf) +proc setAuthor(conf: Conf, slug: Slug, trackDir: string, exerciseKind: ExerciseKind) = + let configPath = trackDir / "exercises" / $exerciseKind / $slug / ".meta" / "config.json" + var exerciseConfig = ExerciseConfig.init(exerciseKind, configPath) + let formattedConfig = + case exerciseKind + of ekConcept: + exerciseConfig.c.authors.add conf.action.author + prettyExerciseConfig(exerciseConfig.c, pmFmt) + of ekPractice: + exerciseConfig.p.authors.add conf.action.author + prettyExerciseConfig(exerciseConfig.p, pmFmt) + writeFile(configPath, formattedConfig) + proc createFiles(conf: Conf, slug: Slug, trackConfig: TrackConfig, trackDir: string, exerciseKind: ExerciseKind) = withLevel(verQuiet): syncExercise(conf, slug) syncFiles(trackConfig, conf.trackDir, slug, exerciseKind) + if conf.action.author.len > 0: + setAuthor(conf, slug, trackDir, exerciseKind) proc createConceptExercise*(conf: Conf) = var (trackConfig, trackConfigPath, userExercise) = verifyExerciseDoesNotExist(conf, conf.action.conceptExerciseSlug) @@ -105,7 +120,7 @@ proc createPracticeExercise*(conf: Conf) = uuid: $genUuid(), practices: OrderedSet[string](), prerequisites: OrderedSet[string](), - difficulty: 1, + difficulty: conf.action.difficulty.get(1), status: sMissing ) diff --git a/tests/test_binary_create.nim b/tests/test_binary_create.nim index 7e26c1c3..c717486c 100644 --- a/tests/test_binary_create.nim +++ b/tests/test_binary_create.nim @@ -1,4 +1,4 @@ -import std/[os, osproc, strformat, strutils, unittest] +import std/[os, osproc, re, strformat, strutils, unittest] import exec import "."/[binary_helpers] @@ -45,6 +45,12 @@ proc main = """.unindent() execAndCheck(1, &"{createBase} --concept-exercise=hangman", expectedOutput) + test "concept exercise with difficulty (prints the expected output, and exits with 1)": + const expectedOutput = fmt""" + The difficulty argument is not supported for concept exercises + """.unindent() + execAndCheck(1, &"{createBase} --concept-exercise=bar --difficulty 4", expectedOutput) + test "create concept exercise (creates the exercise files, and exits with 0)": const expectedOutput = fmt""" Updating cached 'problem-specifications' data... @@ -127,5 +133,77 @@ proc main = """.unindent() testStatusThenReset(trackDir, expectedStatus) + test "create practice exercise with difficulty (creates the exercise files, and exits with 0)": + const expectedOutput = fmt""" + Updating cached 'problem-specifications' data... + Created practice exercise 'foo'. + """.unindent() + execAndCheck(0, &"{createBase} --practice-exercise=foo --difficulty=5", expectedOutput) + + const expectedDiff = """ + --- config.json + +++ config.json + + }, + + { + + "slug": "foo", + + "name": "foo", + + "uuid": "", + + "practices": [], + + "prerequisites": [], + + "difficulty": 5 + """.unindent().replace("\p", "\n") + let diff = gitDiffConcise(trackDir).replace(re""""uuid": "[^"]+"""", """"uuid": """"") + check diff == expectedDiff + + const expectedStatus = """ + M config.json + A exercises/practice/foo/.docs/instructions.md + A exercises/practice/foo/.meta/config.json + A exercises/practice/foo/.meta/example.ex + A exercises/practice/foo/lib/foo.ex + A exercises/practice/foo/test/foo_test.exs + """.unindent() + testStatusThenReset(trackDir, expectedStatus) + + test "create practice exercise with author (creates the exercise files, and exits with 0)": + const expectedOutput = fmt""" + Updating cached 'problem-specifications' data... + Created practice exercise 'foo'. + """.unindent() + execAndCheck(0, &"{createBase} --practice-exercise=foo --author=bar", expectedOutput) + + const expectedConfig = """ + { + "authors": [ + "bar" + ], + "files": { + "solution": [ + "lib/foo.ex" + ], + "test": [ + "test/foo_test.exs" + ], + "example": [ + ".meta/example.ex" + ] + }, + "blurb": "" + } + """.dedent(6).replace("\p", "\n") + let configPath = trackDir / "exercises" / "practice" / "foo" / ".meta" / "config.json" + let config = readFile(configPath) + check config == expectedConfig + + const expectedStatus = """ + M config.json + A exercises/practice/foo/.docs/instructions.md + A exercises/practice/foo/.meta/config.json + A exercises/practice/foo/.meta/example.ex + A exercises/practice/foo/lib/foo.ex + A exercises/practice/foo/test/foo_test.exs + """.unindent() + testStatusThenReset(trackDir, expectedStatus) + main() {.used.}