Packages

Mix tasks for creating and running interactive workshops for teaching people how to program in Elixir, and other things.

Current section

Files

Jump to
workshop lib tasks new exercise.ex
Raw

lib/tasks/new/exercise.ex

defmodule Mix.Tasks.New.Exercise do
use Mix.Task
import Mix.Generator
import Mix.Utils, only: [camelize: 1]
alias Workshop.Exercise
alias Workshop.Exercises
@shortdoc "Create a new exercise for a workshop"
@moduledoc """
Creates a new exercise for a workshop.
It expects a name for the new exercise
mix new.workshop NAME
The path will be named after the given NAME. Given `my_exercise` it will
result in a workshop named *My Exercise*.
The exercises will be ordered by a weight set as a module attribute in the
generated *name/exercise.exs* file. It will increment by 1000 for every new
exercise, which should give ample slots to move exercises around after
they have been created, if the exercise order should change.
"""
@spec run(OptionParser.argv) :: :ok
def run(argv) do
Workshop.start([], [])
{opts, argv, _} = OptionParser.parse(argv, switches: [])
path = Workshop.Session.get(:exercises_folder)
# update current working dir
File.cd!(path)
case argv do
[] -> Mix.raise "Expected NAME to be given. Please use `mix new.exercise NAME`"
[name|_] ->
name = Path.basename(Path.expand(name))
unless Exercise.valid_name?(name) do
Mix.raise "Exercise name must start with a letter and have only lowercase " <>
"letters, numbers and underscore, got: #{inspect name}"
end
if Exercise.name_taken?(name) do
Mix.raise "The name #{inspect name} has already been taken. Please use " <>
"another name for this exercise."
end
mod = camelize(name)
title = snake_case_to_headline(name)
weight = get_next_exercise_weight
exercise_folder = Path.expand("#{name}", path)
case File.mkdir_p(exercise_folder) do
:ok ->
File.cd!(exercise_folder, fn ->
do_generate_exercise(path, title, mod, weight, opts)
Mix.shell.info """
The new exercise has been created in:
#{exercise_folder}
If you want to change the precedence of this exercise you can simply
change the assigned @weight in the created exercise.exs-file.
"""
end)
end
end
end
# calculate the next weight value for the next exercise
@weight_increment 1000
defp get_next_exercise_weight do
case Enum.reverse(Exercises.list_by_weight!) do
[{weight, _} | _] ->
weight + @weight_increment
[] ->
@weight_increment
end
end
defp snake_case_to_headline(name) do
name
|> String.split("_")
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end
defp do_generate_exercise(name, title, mod, weight, _opts) do
assigns = [name: name, title: title, module: mod, weight: weight]
create_file "exercise.exs", exercise_template(assigns)
create_directory "files"
create_directory "solution"
create_directory "test"
create_file "test/test_helper.exs", test_helper_template(assigns)
create_file "test/check.exs", check_template(assigns)
end
embed_template :exercise, """
defmodule Workshop.Exercise.<%= @module %> do
use Workshop.Exercise
@title "<%= @title %>"
@weight <%= @weight %>
@description \"""
@todo, write this exercise
# What's next?
Type `mix workshop.check` to check your solution. If you pass you can proceed
to the next exercise by typing `mix workshop.next`. This is all done in the
terminal.
If you are confused you could try `mix workshop.hint`. Otherwise ask your
instructor or follow the directions on `mix workshop.help`.
\"""
@hint [
"@todo, write a couple of hints for the solving this exercise"
]
end
"""
embed_template :test_helper, """
defmodule Workshop.Exercise.<%= @module %>Check.Helper do
def exec(solution_dir) do
# Locate and load and perhaps start the users solution.
# The following example assumes that the user solution is located
# in a file called *exercise.exs*:
# "exercise.exs"
# |> Path.expand(solution_dir)
# |> Code.require_file
# load and run the solution checker
Code.require_file("check.exs", __DIR__)
Workshop.Exercise.<%= @module %>Check.run()
end
end
"""
embed_template :check, """
defmodule Workshop.Exercise.<%= @module %>Check do
use Workshop.Validator
verify "verify something" do
# return value can be `:ok`, `{:warning, message}`, or `{:error, message}`
:ok
end
verify "verify something else" do
:ok
end
end
"""
end