daigaku 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|