daigaku 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/README.md +62 -0
- data/Rakefile +2 -0
- data/bin/daigaku +12 -0
- data/daigaku.gemspec +37 -0
- data/lib/daigaku.rb +27 -0
- data/lib/daigaku/chapter.rb +23 -0
- data/lib/daigaku/configuration.rb +86 -0
- data/lib/daigaku/course.rb +23 -0
- data/lib/daigaku/database.rb +64 -0
- data/lib/daigaku/exceptions.rb +19 -0
- data/lib/daigaku/generator.rb +53 -0
- data/lib/daigaku/loadable.rb +23 -0
- data/lib/daigaku/loading/chapters.rb +9 -0
- data/lib/daigaku/loading/courses.rb +9 -0
- data/lib/daigaku/loading/units.rb +9 -0
- data/lib/daigaku/reference_solution.rb +10 -0
- data/lib/daigaku/solution.rb +42 -0
- data/lib/daigaku/task.rb +10 -0
- data/lib/daigaku/terminal.rb +11 -0
- data/lib/daigaku/terminal/cli.rb +59 -0
- data/lib/daigaku/terminal/courses.rb +114 -0
- data/lib/daigaku/terminal/output.rb +72 -0
- data/lib/daigaku/terminal/setup.rb +115 -0
- data/lib/daigaku/terminal/solutions.rb +46 -0
- data/lib/daigaku/terminal/texts/about.txt +19 -0
- data/lib/daigaku/terminal/texts/courses_empty.txt +3 -0
- data/lib/daigaku/terminal/texts/hint_course_download.txt +13 -0
- data/lib/daigaku/terminal/texts/welcome.txt +12 -0
- data/lib/daigaku/terminal/welcome.rb +98 -0
- data/lib/daigaku/test.rb +46 -0
- data/lib/daigaku/test_result.rb +69 -0
- data/lib/daigaku/unit.rb +28 -0
- data/lib/daigaku/version.rb +3 -0
- data/lib/daigaku/views.rb +59 -0
- data/lib/daigaku/views/chapters_menu.rb +91 -0
- data/lib/daigaku/views/courses_menu.rb +87 -0
- data/lib/daigaku/views/main_menu.rb +37 -0
- data/lib/daigaku/views/splash.rb +57 -0
- data/lib/daigaku/views/task_view.rb +206 -0
- data/lib/daigaku/views/top_bar.rb +48 -0
- data/lib/daigaku/views/units_menu.rb +92 -0
- data/lib/daigaku/window.rb +160 -0
- data/spec/daigaku/chapter_spec.rb +76 -0
- data/spec/daigaku/configuration_spec.rb +161 -0
- data/spec/daigaku/course_spec.rb +75 -0
- data/spec/daigaku/database_spec.rb +79 -0
- data/spec/daigaku/generator_spec.rb +82 -0
- data/spec/daigaku/loading/chapters_spec.rb +16 -0
- data/spec/daigaku/loading/courses_spec.rb +16 -0
- data/spec/daigaku/loading/units_spec.rb +21 -0
- data/spec/daigaku/reference_solution_spec.rb +23 -0
- data/spec/daigaku/solution_spec.rb +79 -0
- data/spec/daigaku/task_spec.rb +23 -0
- data/spec/daigaku/terminal/cli_spec.rb +51 -0
- data/spec/daigaku/terminal/courses_spec.rb +60 -0
- data/spec/daigaku/terminal/output_spec.rb +123 -0
- data/spec/daigaku/terminal/setup_spec.rb +10 -0
- data/spec/daigaku/terminal/solutions_spec.rb +8 -0
- data/spec/daigaku/terminal/welcome_spec.rb +12 -0
- data/spec/daigaku/terminal_spec.rb +14 -0
- data/spec/daigaku/test_example_spec.rb +54 -0
- data/spec/daigaku/test_result_spec.rb +81 -0
- data/spec/daigaku/test_spec.rb +48 -0
- data/spec/daigaku/unit_spec.rb +85 -0
- data/spec/daigaku/views/chapters_menu_spec.rb +8 -0
- data/spec/daigaku/views/courses_menu_spec.rb +8 -0
- data/spec/daigaku/views/task_view_spec.rb +7 -0
- data/spec/daigaku/views/units_menu_spec.rb +8 -0
- data/spec/daigaku/views_spec.rb +23 -0
- data/spec/daigaku_spec.rb +57 -0
- data/spec/path_helpers_spec.rb +60 -0
- data/spec/resource_helpers_spec.rb +33 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/macros/content_helpers.rb +129 -0
- data/spec/support/macros/mock_helpers.rb +20 -0
- data/spec/support/macros/path_helpers.rb +133 -0
- data/spec/support/macros/resource_helpers.rb +119 -0
- data/spec/support/macros/test_helpers.rb +6 -0
- metadata +361 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Daigaku
|
2
|
+
module Terminal
|
3
|
+
|
4
|
+
require 'os'
|
5
|
+
require_relative 'output'
|
6
|
+
|
7
|
+
class Solutions < Thor
|
8
|
+
include Terminal::Output
|
9
|
+
|
10
|
+
desc 'solutions open [COURSE NAME]', 'Open the solutions folder of a course in a GUI window'
|
11
|
+
def open(course_name = '')
|
12
|
+
begin
|
13
|
+
path = File.join(Daigaku.config.solutions_path, course_name)
|
14
|
+
|
15
|
+
unless Dir.exist?(path)
|
16
|
+
text = [
|
17
|
+
"The course directory \"#{File.basename(path)}\" is not available in",
|
18
|
+
"\"#{File.dirname(path)}\".\n",
|
19
|
+
'Hint:',
|
20
|
+
'Run "daigaku scaffold" to create empty solution files for all courses.'
|
21
|
+
]
|
22
|
+
say_warning text.join("\n")
|
23
|
+
|
24
|
+
unless Loading::Courses.load(Daigaku.config.courses_path).empty?
|
25
|
+
Terminal::Courses.new.list
|
26
|
+
end
|
27
|
+
|
28
|
+
return
|
29
|
+
end
|
30
|
+
|
31
|
+
if OS.windows?
|
32
|
+
system "explorer '#{path}'"
|
33
|
+
elsif OS.mac?
|
34
|
+
system "open '#{path}'"
|
35
|
+
elsif OS.linux?
|
36
|
+
system "xdg-open '#{path}'"
|
37
|
+
end
|
38
|
+
rescue ConfigurationError => e
|
39
|
+
say_warning e.message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
**********************************************************************
|
2
|
+
◇ ABOUT DAIGAKU! ◇
|
3
|
+
**********************************************************************
|
4
|
+
|
5
|
+
Daigaku is the Japanese word for "university".
|
6
|
+
With Daigaku you can master your way of learning the Ruby programming
|
7
|
+
language with courses that are created by the community.
|
8
|
+
|
9
|
+
Daigaku is a command line tool and a text based interface and provides
|
10
|
+
you with a number of learning tasks and explanations about the Ruby
|
11
|
+
programming language. You will learn Ruby step by step by solving small
|
12
|
+
language-explaining programming tasks.
|
13
|
+
|
14
|
+
Daigaku's command line interface provides several commands which you
|
15
|
+
can use in your terminal to setup the system, download new courses,
|
16
|
+
and navigate through your solutions.
|
17
|
+
|
18
|
+
By typing "daigaku help" in you terminal you are provided with an
|
19
|
+
overview of all available commands.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
You can download a new daigaku course by using the
|
2
|
+
"daigaku courses download [URL] [OPTIONS]" command, e.g.:
|
3
|
+
|
4
|
+
$ daigaku courses download https://github.com/daigaku-ruby/Get_started_with_Ruby/archive/master.zip
|
5
|
+
|
6
|
+
For Github resources you can also use the `--github` (short `-g`) option:
|
7
|
+
|
8
|
+
$ daigaku courses download -g daigaku-ruby/Get_started_with_Ruby
|
9
|
+
|
10
|
+
If you want to quick start with Daiaku's "Get started with Ruby"
|
11
|
+
course just run:
|
12
|
+
|
13
|
+
$ daigaku courses download
|
@@ -0,0 +1,12 @@
|
|
1
|
+
**********************************************************************
|
2
|
+
◇ WELCOME TO DAIGAKU! ◇
|
3
|
+
**********************************************************************
|
4
|
+
|
5
|
+
To get started, you need a folder where you can save your courses and
|
6
|
+
solutions. By default, daigaku uses the same base path for both.
|
7
|
+
Your courses will be saved to a "courses" folder and your solutions
|
8
|
+
can be found in a "solutions" folder.
|
9
|
+
|
10
|
+
The "solutions" path and "courses" path can be changed later on.
|
11
|
+
See which command you can use to update your setup by simply typing
|
12
|
+
"daigaku setup help set" in your command line.
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Daigaku
|
2
|
+
module Terminal
|
3
|
+
|
4
|
+
class Welcome
|
5
|
+
include Terminal::Output
|
6
|
+
|
7
|
+
def self.run
|
8
|
+
self.new.run
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.about
|
12
|
+
self.new.about
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
empty_line
|
17
|
+
say Terminal.text :welcome
|
18
|
+
empty_line
|
19
|
+
say "For now, let's setup the daigaku paths."
|
20
|
+
Daigaku::Terminal::Setup.new.init
|
21
|
+
|
22
|
+
show_setup_list_announcement
|
23
|
+
show_courses_list_announcement
|
24
|
+
|
25
|
+
courses = Loading::Courses.load(Daigaku.config.courses_path)
|
26
|
+
|
27
|
+
if courses.empty?
|
28
|
+
show_courses_download_announcement
|
29
|
+
show_solutions_open_announcement
|
30
|
+
end
|
31
|
+
|
32
|
+
empty_line
|
33
|
+
show_learn_announcement
|
34
|
+
end
|
35
|
+
|
36
|
+
def about
|
37
|
+
empty_line
|
38
|
+
say Terminal.text :about
|
39
|
+
empty_line
|
40
|
+
say %x{daigaku help}
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def show_setup_list_announcement
|
46
|
+
command = 'daigaku setup list'
|
47
|
+
text = [
|
48
|
+
"The courses path and solutions path have been added to your settings.",
|
49
|
+
"Just list your current settings with the \"#{command}\" command:"
|
50
|
+
].join("\n")
|
51
|
+
|
52
|
+
get_command(command, text)
|
53
|
+
end
|
54
|
+
|
55
|
+
def show_courses_list_announcement
|
56
|
+
command = 'daigaku courses list'
|
57
|
+
text = [
|
58
|
+
"Well done. Now, type \"#{command}\" to see what courses are",
|
59
|
+
"available in your daigaku folder:"
|
60
|
+
].join("\n")
|
61
|
+
|
62
|
+
get_command(command, text)
|
63
|
+
end
|
64
|
+
|
65
|
+
def show_courses_download_announcement
|
66
|
+
command = 'daigaku courses download'
|
67
|
+
text = [
|
68
|
+
"Oh! You don't have any courses, yet?",
|
69
|
+
"Just enter \"#{command}\" to download the basic Daigaku course:"
|
70
|
+
].join("\n")
|
71
|
+
|
72
|
+
get_command(command, text)
|
73
|
+
end
|
74
|
+
|
75
|
+
def show_solutions_open_announcement
|
76
|
+
command = 'daigaku solutions open'
|
77
|
+
text = [
|
78
|
+
"When downloading a course, Daigaku scaffolds empty solution files",
|
79
|
+
"for your code on the fly.\n",
|
80
|
+
"Type \"#{command}\" to open your solutions folder:"
|
81
|
+
].join("\n")
|
82
|
+
|
83
|
+
get_command(command, text)
|
84
|
+
end
|
85
|
+
|
86
|
+
def show_learn_announcement
|
87
|
+
command = 'daigaku learn'
|
88
|
+
text = [
|
89
|
+
"Congratulations! You learned the first steps of using daigaku.",
|
90
|
+
"To continue and start learning Ruby type \"#{command}\":"
|
91
|
+
].join("\n")
|
92
|
+
|
93
|
+
get_command(command, text)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
data/lib/daigaku/test.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Daigaku
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
class Test
|
5
|
+
|
6
|
+
attr_reader :path
|
7
|
+
|
8
|
+
CODE_REGEX = /\[\['solution::code'\]\]/
|
9
|
+
|
10
|
+
def initialize(path)
|
11
|
+
@unit_path = path
|
12
|
+
@path = Dir[File.join(path, '*spec.rb')].first
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(solution_code)
|
16
|
+
spec_code = File.read(@path)
|
17
|
+
patched_spec_code = insert_code(spec_code, solution_code.to_s)
|
18
|
+
|
19
|
+
temp_spec = File.join(File.dirname(@path), "temp_#{File.basename(@path)}")
|
20
|
+
create_temp_spec(temp_spec, patched_spec_code)
|
21
|
+
|
22
|
+
result = %x{ rspec --color --format j #{temp_spec} }
|
23
|
+
remove_file(temp_spec)
|
24
|
+
|
25
|
+
TestResult.new(result)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def insert_code(spec, code)
|
31
|
+
spec.gsub(CODE_REGEX, code)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_temp_spec(path, content)
|
35
|
+
base_path = File.dirname(path)
|
36
|
+
FileUtils.mkdir_p(base_path) unless Dir.exist?(base_path)
|
37
|
+
File.open(path, 'w') { |f| f.puts content }
|
38
|
+
end
|
39
|
+
|
40
|
+
def remove_file(path)
|
41
|
+
FileUtils.rm(path) if File.exist?(path)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Daigaku
|
2
|
+
|
3
|
+
class TestResult
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
attr_reader :examples, :example_count, :failure_count
|
7
|
+
|
8
|
+
TEST_PASSED_MESSAGE = "Your code passed all tests. Congratulations!"
|
9
|
+
|
10
|
+
def initialize(result_json)
|
11
|
+
@result = JSON.parse(result_json, symbolize_names: true)
|
12
|
+
|
13
|
+
@example_count = @result[:summary][:example_count]
|
14
|
+
@failure_count = @result[:summary][:failure_count]
|
15
|
+
|
16
|
+
@examples = @result[:examples].map do |example|
|
17
|
+
description = example[:full_description]
|
18
|
+
status = example[:status]
|
19
|
+
exception = example[:exception]
|
20
|
+
message = exception ? exception[:message] : nil
|
21
|
+
|
22
|
+
TestExample.new(description: description, status: status, message: message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def passed?
|
27
|
+
@examples.reduce(true) do |passed, example|
|
28
|
+
passed && example.passed?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def summary
|
33
|
+
if passed?
|
34
|
+
TEST_PASSED_MESSAGE
|
35
|
+
else
|
36
|
+
build_failed_summary
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def build_failed_summary
|
43
|
+
message = examples.map do |example|
|
44
|
+
"#{example.description}\n#{example.status}: #{example.message}"
|
45
|
+
end
|
46
|
+
|
47
|
+
summary = message.map(&:strip).join("\n" * 3)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
class TestExample
|
53
|
+
|
54
|
+
attr_reader :description, :status, :message
|
55
|
+
|
56
|
+
EXAMPLE_PASSED_MESSAGE = "Your code passed this requirement."
|
57
|
+
|
58
|
+
def initialize(args = {})
|
59
|
+
@description = args[:description]
|
60
|
+
@status = args[:status]
|
61
|
+
@message = args[:message] || EXAMPLE_PASSED_MESSAGE
|
62
|
+
end
|
63
|
+
|
64
|
+
def passed?
|
65
|
+
@status == 'passed'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/lib/daigaku/unit.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Daigaku
|
2
|
+
class Unit
|
3
|
+
|
4
|
+
attr_reader :title, :task
|
5
|
+
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
@title = File.basename(path).gsub(/\_+/, ' ')
|
9
|
+
end
|
10
|
+
|
11
|
+
def task
|
12
|
+
@task ||= Task.new(@path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def reference_solution
|
16
|
+
@reference_solution ||= ReferenceSolution.new(@path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def solution
|
20
|
+
@solution = Solution.new(@path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def mastered?
|
24
|
+
solution.verified?
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'curses'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require_relative 'views/top_bar'
|
4
|
+
|
5
|
+
module Daigaku
|
6
|
+
module Views
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
include Curses
|
11
|
+
|
12
|
+
def reset_menu_position
|
13
|
+
@position = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def default_window(height = nil, width = nil, top = 0, left = 0)
|
19
|
+
init_screen
|
20
|
+
|
21
|
+
noecho
|
22
|
+
crmode
|
23
|
+
curs_set(0) # invisible cursor
|
24
|
+
|
25
|
+
height ||= lines
|
26
|
+
width ||= cols + 1
|
27
|
+
|
28
|
+
window = Daigaku::Window.new(height, width, top, left)
|
29
|
+
|
30
|
+
Curses.lines.times do |line|
|
31
|
+
window.setpos(line, 0)
|
32
|
+
window.clear_line
|
33
|
+
end
|
34
|
+
|
35
|
+
window.keypad(true)
|
36
|
+
window.scrollok(true)
|
37
|
+
window.refresh
|
38
|
+
window
|
39
|
+
end
|
40
|
+
|
41
|
+
def top_bar(window)
|
42
|
+
TopBar.new(window)
|
43
|
+
end
|
44
|
+
|
45
|
+
def main_panel(window)
|
46
|
+
top_bar(window).show
|
47
|
+
yield(window) if block_given?
|
48
|
+
end
|
49
|
+
|
50
|
+
def sub_window_below_top_bar(window)
|
51
|
+
top = top_bar(window).height
|
52
|
+
sub_window = window.subwin(window.maxy - top, window.maxx, top, 0)
|
53
|
+
sub_window.keypad(true)
|
54
|
+
sub_window
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Daigaku
|
2
|
+
module Views
|
3
|
+
require 'wisper'
|
4
|
+
|
5
|
+
class ChaptersMenu
|
6
|
+
include Views
|
7
|
+
include Wisper::Publisher
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@position = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def enter_chapters_menu(course)
|
14
|
+
@window = default_window
|
15
|
+
@course = course
|
16
|
+
|
17
|
+
main_panel(@window) do |window|
|
18
|
+
show sub_window_below_top_bar(window)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def reenter_chapters_menu(course, chapter)
|
23
|
+
@course = course
|
24
|
+
@chapter = chapter
|
25
|
+
|
26
|
+
@position = course.chapters.find_index(chapter)
|
27
|
+
enter_chapters_menu(@course)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def show(window)
|
33
|
+
draw(window, @position)
|
34
|
+
interact_with(window)
|
35
|
+
end
|
36
|
+
|
37
|
+
def draw(window, active_index = 0)
|
38
|
+
window.attrset(A_NORMAL)
|
39
|
+
window.setpos(0, 1)
|
40
|
+
window.emphasize @course.title
|
41
|
+
window.write ' - available chapters:'
|
42
|
+
|
43
|
+
menu_items.each_with_index do |item, index|
|
44
|
+
window.setpos(index + 2, 1)
|
45
|
+
window.print_indicator(chapters[index])
|
46
|
+
window.attrset(index == active_index ? A_STANDOUT : A_NORMAL)
|
47
|
+
window.write " #{item.to_s} "
|
48
|
+
end
|
49
|
+
|
50
|
+
window.refresh
|
51
|
+
end
|
52
|
+
|
53
|
+
def interact_with(window)
|
54
|
+
while char = window.getch
|
55
|
+
case char
|
56
|
+
when KEY_UP
|
57
|
+
@position -= 1
|
58
|
+
broadcast(:reset_menu_position)
|
59
|
+
when KEY_DOWN
|
60
|
+
@position += 1
|
61
|
+
broadcast(:reset_menu_position)
|
62
|
+
when 10 # Enter
|
63
|
+
broadcast(:enter_units_menu, @course, chapters[@position])
|
64
|
+
return
|
65
|
+
when 263 # Backspace
|
66
|
+
broadcast(:reenter_courses_menu, @course)
|
67
|
+
return
|
68
|
+
when 27 # ESC
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
|
72
|
+
@position = menu_items.length - 1 if @position < 0
|
73
|
+
@position = 0 if @position >= menu_items.length
|
74
|
+
draw(window, @position)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def chapters
|
79
|
+
@course.chapters
|
80
|
+
end
|
81
|
+
|
82
|
+
def menu_items
|
83
|
+
@menu_items = chapters.map do |chapter|
|
84
|
+
chapter.title
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|