Skip to content

Commit

Permalink
create(exercise): allow specifying author and/or difficulty (#864)
Browse files Browse the repository at this point in the history
This commit adds support for specifying the `author` and/or `difficulty` when creating a new exercise via `configlet create --practice-exercise`
  • Loading branch information
ErikSchierboom authored May 1, 2024
1 parent 0e3196a commit 68b92e8
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 7 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Options for create:
--concept-exercise <slug> The slug of the concept exercise
-e, --exercise <slug> 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 <slug> Only operate on this exercise
Expand Down
17 changes: 16 additions & 1 deletion src/cli.nim
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +90,8 @@ type
optCreateArticle = "article"
optCreateConceptExercise = "conceptExercise"
optCreatePracticeExercise = "practiceExercise"
optCreateAuthor = "author"
optCreateDifficulty = "difficulty"

# Options for `completion`
optCompletionShell = "shell"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 14 additions & 1 deletion src/create/create.nim
Original file line number Diff line number Diff line change
@@ -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]

Expand All @@ -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)
Expand All @@ -36,6 +40,10 @@ proc create*(conf: Conf) =
let msg = "Please specify an exercise to create an article for, using --exercise <slug>"
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)
Expand All @@ -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)
Expand Down
23 changes: 19 additions & 4 deletions src/create/exercises.nim
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
)

Expand Down
80 changes: 79 additions & 1 deletion tests/test_binary_create.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[os, osproc, strformat, strutils, unittest]
import std/[os, osproc, re, strformat, strutils, unittest]
import exec
import "."/[binary_helpers]

Expand Down Expand Up @@ -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...
Expand Down Expand Up @@ -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": "<UUID>",
+ "practices": [],
+ "prerequisites": [],
+ "difficulty": 5
""".unindent().replace("\p", "\n")
let diff = gitDiffConcise(trackDir).replace(re""""uuid": "[^"]+"""", """"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.}

0 comments on commit 68b92e8

Please sign in to comment.