belajar 0.1.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/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/README.md +5 -0
- data/Rakefile +5 -0
- data/belajar.gemspec +38 -0
- data/bin/belajar +12 -0
- data/lib/belajar.rb +24 -0
- data/lib/belajar/chapter.rb +23 -0
- data/lib/belajar/configuration.rb +89 -0
- data/lib/belajar/congratulator.rb +13 -0
- data/lib/belajar/course.rb +80 -0
- data/lib/belajar/exceptions.rb +19 -0
- data/lib/belajar/generator.rb +60 -0
- data/lib/belajar/github_client.rb +30 -0
- data/lib/belajar/loadable.rb +23 -0
- data/lib/belajar/loading/chapters.rb +9 -0
- data/lib/belajar/loading/courses.rb +9 -0
- data/lib/belajar/loading/units.rb +9 -0
- data/lib/belajar/reference_solution.rb +18 -0
- data/lib/belajar/solution.rb +53 -0
- data/lib/belajar/storeable.rb +32 -0
- data/lib/belajar/task.rb +10 -0
- data/lib/belajar/terminal.rb +12 -0
- data/lib/belajar/terminal/cli.rb +59 -0
- data/lib/belajar/terminal/courses.rb +179 -0
- data/lib/belajar/terminal/output.rb +78 -0
- data/lib/belajar/terminal/setup.rb +115 -0
- data/lib/belajar/terminal/solutions.rb +46 -0
- data/lib/belajar/terminal/texts/about.txt +19 -0
- data/lib/belajar/terminal/texts/congratulations.txt +12 -0
- data/lib/belajar/terminal/texts/courses_empty.txt +3 -0
- data/lib/belajar/terminal/texts/hint_course_download.txt +13 -0
- data/lib/belajar/terminal/texts/welcome.txt +12 -0
- data/lib/belajar/terminal/welcome.rb +98 -0
- data/lib/belajar/test.rb +46 -0
- data/lib/belajar/test_result.rb +86 -0
- data/lib/belajar/unit.rb +28 -0
- data/lib/belajar/version.rb +3 -0
- data/lib/belajar/views.rb +51 -0
- data/lib/belajar/views/chapters_menu.rb +60 -0
- data/lib/belajar/views/courses_menu.rb +52 -0
- data/lib/belajar/views/main_menu.rb +37 -0
- data/lib/belajar/views/menu.rb +89 -0
- data/lib/belajar/views/splash.rb +57 -0
- data/lib/belajar/views/task_view.rb +236 -0
- data/lib/belajar/views/top_bar.rb +40 -0
- data/lib/belajar/views/units_menu.rb +61 -0
- data/lib/belajar/window.rb +215 -0
- data/spec/belajar/chapter_spec.rb +76 -0
- data/spec/belajar/configuration_spec.rb +161 -0
- data/spec/belajar/congratulator_spec.rb +24 -0
- data/spec/belajar/course_spec.rb +201 -0
- data/spec/belajar/generator_spec.rb +82 -0
- data/spec/belajar/github_client_spec.rb +53 -0
- data/spec/belajar/loading/chapters_spec.rb +16 -0
- data/spec/belajar/loading/courses_spec.rb +16 -0
- data/spec/belajar/loading/units_spec.rb +21 -0
- data/spec/belajar/reference_solution_spec.rb +41 -0
- data/spec/belajar/solution_spec.rb +86 -0
- data/spec/belajar/storeable_spec.rb +35 -0
- data/spec/belajar/task_spec.rb +23 -0
- data/spec/belajar/terminal/cli_spec.rb +51 -0
- data/spec/belajar/terminal/courses_spec.rb +293 -0
- data/spec/belajar/terminal/output_spec.rb +151 -0
- data/spec/belajar/terminal/setup_spec.rb +10 -0
- data/spec/belajar/terminal/solutions_spec.rb +8 -0
- data/spec/belajar/terminal/welcome_spec.rb +12 -0
- data/spec/belajar/terminal_spec.rb +24 -0
- data/spec/belajar/test_example_spec.rb +54 -0
- data/spec/belajar/test_result_spec.rb +91 -0
- data/spec/belajar/test_spec.rb +48 -0
- data/spec/belajar/unit_spec.rb +85 -0
- data/spec/belajar/views/chapters_menu_spec.rb +6 -0
- data/spec/belajar/views/courses_menu_spec.rb +6 -0
- data/spec/belajar/views/menu_spec.rb +19 -0
- data/spec/belajar/views/task_view_spec.rb +7 -0
- data/spec/belajar/views/units_menu_spec.rb +6 -0
- data/spec/belajar/views_spec.rb +21 -0
- data/spec/belajar_spec.rb +51 -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 +25 -0
- data/spec/support/macros/path_helpers.rb +139 -0
- data/spec/support/macros/resource_helpers.rb +157 -0
- data/spec/support/macros/test_helpers.rb +6 -0
- metadata +385 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Loadable
|
3
|
+
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
def load(path)
|
7
|
+
if Dir.exist?(path)
|
8
|
+
dirs = Dir.entries(path).select do |entry|
|
9
|
+
!entry.match(/\./)
|
10
|
+
end
|
11
|
+
|
12
|
+
dirs.sort.map do |dir|
|
13
|
+
dir_path = File.join(path, dir)
|
14
|
+
class_name = self.to_s.demodulize.singularize
|
15
|
+
"Belajar::#{class_name}".constantize.new(dir_path)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
Array.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Belajar
|
2
|
+
class ReferenceSolution
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
def initialize(path)
|
6
|
+
@path = Dir[File.join(path, '*solution.rb')].first
|
7
|
+
@code = File.read(@path).strip if @path
|
8
|
+
end
|
9
|
+
|
10
|
+
def code
|
11
|
+
@code.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def code_lines
|
15
|
+
code.lines.map(&:chomp)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Belajar
|
2
|
+
class Solution
|
3
|
+
|
4
|
+
FILE_SUFFIX = '_solution.rb'
|
5
|
+
|
6
|
+
attr_reader :code, :path, :errors
|
7
|
+
|
8
|
+
def initialize(unit_path)
|
9
|
+
@unit_path = unit_path
|
10
|
+
@path = solution_path(unit_path)
|
11
|
+
@code = File.read(@path).strip if File.file?(@path)
|
12
|
+
@verified = get_store_state
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify!
|
16
|
+
result = Belajar::Test.new(@unit_path).run(self.code)
|
17
|
+
set_store_state(result.passed?)
|
18
|
+
result
|
19
|
+
end
|
20
|
+
|
21
|
+
def verified?
|
22
|
+
!!@verified
|
23
|
+
end
|
24
|
+
|
25
|
+
def store_key
|
26
|
+
unless @store_key
|
27
|
+
part_path = path.split('/')[-3..-1].join('/').gsub(FILE_SUFFIX, '')
|
28
|
+
@store_key = Storeable.key(part_path, prefix: 'verified')
|
29
|
+
end
|
30
|
+
|
31
|
+
@store_key
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def solution_path(path)
|
37
|
+
local_path = Belajar.config.solutions_path
|
38
|
+
sub_dirs = Storeable.key(path.split('/')[-3..-2].join('/').gsub(FILE_SUFFIX, ''))
|
39
|
+
file = Storeable.key(File.basename(path)) + FILE_SUFFIX
|
40
|
+
|
41
|
+
File.join(local_path, sub_dirs, file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_store_state(verified)
|
45
|
+
@verified = verified
|
46
|
+
QuickStore.store.set(store_key, verified?)
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_store_state
|
50
|
+
QuickStore.store.get(store_key)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Storeable
|
3
|
+
|
4
|
+
LEADING_NUMBERS = /^\d+[\_\-\s]+/
|
5
|
+
PART_JOINTS = /[\_\-\s]+/
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def key(text, options = {})
|
9
|
+
separator = QuickStore.config.key_separator
|
10
|
+
prefix = options[:prefix]
|
11
|
+
suffix = clean(options[:suffix])
|
12
|
+
suffixes = options[:suffixes]
|
13
|
+
suffixes_items = suffixes ? suffixes.map { |s| clean(s) }.compact : nil
|
14
|
+
|
15
|
+
[prefix, clean(text), suffix || suffixes_items].compact.join(separator)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def clean(text)
|
21
|
+
if text
|
22
|
+
parts = text.to_s.split(QuickStore.config.key_separator).map do |key|
|
23
|
+
key.gsub(LEADING_NUMBERS, '').gsub(PART_JOINTS, '_').downcase
|
24
|
+
end
|
25
|
+
|
26
|
+
parts.join(QuickStore.config.key_separator)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/belajar/task.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Terminal
|
3
|
+
|
4
|
+
# text should be of a width of 70 columns or less
|
5
|
+
def self.text(file_name)
|
6
|
+
texts_path = File.expand_path('../terminal/texts', __FILE__)
|
7
|
+
file = File.join(texts_path, "#{file_name.to_s}.txt")
|
8
|
+
(File.exist?(file) ? File.read(file).to_s : '')
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module Belajar
|
4
|
+
module Terminal
|
5
|
+
|
6
|
+
require_relative 'courses'
|
7
|
+
require_relative 'solutions'
|
8
|
+
require_relative 'setup'
|
9
|
+
require_relative 'output'
|
10
|
+
|
11
|
+
class CLI < Thor
|
12
|
+
include Terminal::Output
|
13
|
+
|
14
|
+
desc 'courses [COMMAND]', 'Handle belajar courses'
|
15
|
+
subcommand 'courses', Terminal::Courses
|
16
|
+
|
17
|
+
desc 'solutions [COMMAND]', 'Handle your solutions'
|
18
|
+
subcommand 'solutions', Terminal::Solutions
|
19
|
+
|
20
|
+
desc 'setup [COMMAND]', 'Change belajar setup'
|
21
|
+
subcommand 'setup', Terminal::Setup
|
22
|
+
|
23
|
+
def self.start
|
24
|
+
Belajar.config.import!
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
desc 'about', 'About belajar'
|
29
|
+
def about
|
30
|
+
Welcome.about
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'welcome', 'Setup belajar the first time and learn some important commands.'
|
34
|
+
def welcome
|
35
|
+
Welcome.run
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'scaffold', 'Scaffold solution files for your courses.'
|
39
|
+
def scaffold
|
40
|
+
generator = Generator.new
|
41
|
+
generator.prepare
|
42
|
+
|
43
|
+
courses_path = Belajar.config.courses_path
|
44
|
+
solutions_path = Belajar.config.solutions_path
|
45
|
+
|
46
|
+
generator.scaffold(courses_path, solutions_path)
|
47
|
+
|
48
|
+
say_info "You will find your solution files in\n#{solutions_path}."
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'learn', 'Go to belajar to learn Ruby!'
|
52
|
+
def learn
|
53
|
+
courses = Loading::Courses.load(Belajar.config.courses_path)
|
54
|
+
courses.empty? ? Courses.new.list : Belajar.start
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module Belajar
|
2
|
+
module Terminal
|
3
|
+
|
4
|
+
require 'os'
|
5
|
+
require 'open-uri'
|
6
|
+
require 'zip'
|
7
|
+
require_relative 'output'
|
8
|
+
|
9
|
+
class Courses < Thor
|
10
|
+
include Terminal::Output
|
11
|
+
|
12
|
+
desc 'list', 'List your available belajar courses'
|
13
|
+
def list
|
14
|
+
courses = Loading::Courses.load(Belajar.config.courses_path)
|
15
|
+
say_info courses_list_text(courses)
|
16
|
+
end
|
17
|
+
|
18
|
+
method_option :github, type: :string, aliases: '-g', desc: 'Download Github repository'
|
19
|
+
desc 'download [URL] [OPTIONS]', 'Download a new belajar course from [URL]'
|
20
|
+
def download(url = nil, action = 'downloaded')
|
21
|
+
use_initial_course = url.nil? && options[:github].nil?
|
22
|
+
url = GithubClient.master_zip_url(Belajar.config.initial_course) if use_initial_course
|
23
|
+
url = GithubClient.master_zip_url(options[:github]) if options[:github]
|
24
|
+
|
25
|
+
url_given = (url =~ /\A#{URI::regexp(['http', 'https'])}\z/)
|
26
|
+
github = use_initial_course || options[:github] || url.match(/github\.com/)
|
27
|
+
|
28
|
+
raise Download::NoUrlError unless url_given
|
29
|
+
raise Download::NoZipFileUrlError unless File.basename(url) =~ /\.zip/
|
30
|
+
|
31
|
+
courses_path = Belajar.config.courses_path
|
32
|
+
FileUtils.makedirs(courses_path) unless Dir.exist?(courses_path)
|
33
|
+
|
34
|
+
file_name = File.join(courses_path, url.split('/').last)
|
35
|
+
|
36
|
+
File.open(file_name, 'w') { |file| file << open(url).read }
|
37
|
+
course = Course.unzip(file_name, github_repo: github)
|
38
|
+
|
39
|
+
if github
|
40
|
+
user_and_repo = url.match(/github.com\/(.*)\/archive\/master.zip/).captures.first
|
41
|
+
store_repo_data(options[:github] || user_and_repo)
|
42
|
+
end
|
43
|
+
|
44
|
+
QuickStore.store.set(course.key(:url), url)
|
45
|
+
QuickStore.store.set(course.key(:updated_at), Time.now.to_s)
|
46
|
+
scaffold_solutions
|
47
|
+
|
48
|
+
say_info "Successfully #{action} the course \"#{course.title}\"!"
|
49
|
+
rescue Download::NoUrlError => e
|
50
|
+
print_download_warning(url, "\"#{url}\" is not a valid URL!")
|
51
|
+
rescue Download::NoZipFileUrlError => e
|
52
|
+
print_download_warning(url, "\"#{url}\" is not a URL of a *.zip file!")
|
53
|
+
rescue Exception => e
|
54
|
+
print_download_warning(url, e.message)
|
55
|
+
ensure
|
56
|
+
FileUtils.rm(file_name) if File.exist?(file_name.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
method_option :all, type: :boolean, aliases: '-a', desc: 'Update all courses'
|
60
|
+
desc 'update [COURSE_NAME] [OPTIONS]', 'Update Daigak courses.'
|
61
|
+
def update(course_name = nil)
|
62
|
+
if options[:all]
|
63
|
+
courses = Loading::Courses.load(Belajar.config.courses_path)
|
64
|
+
courses.each { |course| update_course(course) }
|
65
|
+
elsif course_name
|
66
|
+
path = File.join(Belajar.config.courses_path, course_name)
|
67
|
+
|
68
|
+
unless Dir.exist?(path)
|
69
|
+
print_course_not_available(course_name)
|
70
|
+
return
|
71
|
+
end
|
72
|
+
|
73
|
+
update_course(Course.new(course_name))
|
74
|
+
else
|
75
|
+
system 'belajar course help update'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
method_option :all, type: :boolean, aliases: '-a', desc: 'Delete all courses'
|
80
|
+
desc 'delete [COURSE_NAME] [OPTIONS]', 'Delete Belajar courses.'
|
81
|
+
def delete(course_name = nil)
|
82
|
+
if options[:all]
|
83
|
+
get_confirm('Are you shure you want to delete all courses?') do
|
84
|
+
course_dirs = Dir[File.join(Belajar.config.courses_path, '*')]
|
85
|
+
|
86
|
+
course_dirs.each do |dir|
|
87
|
+
FileUtils.remove_dir(dir)
|
88
|
+
QuickStore.store.delete(Storeable.key(File.basename(dir), prefix: 'courses'))
|
89
|
+
end
|
90
|
+
|
91
|
+
say_info "All belajar courses were successfully deleted."
|
92
|
+
end
|
93
|
+
elsif course_name
|
94
|
+
path = File.join(Belajar.config.courses_path, course_name)
|
95
|
+
|
96
|
+
unless Dir.exist?(path)
|
97
|
+
print_course_not_available(course_name)
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
get_confirm("Are you shure you want to delete the course \"#{course_name}\"?") do
|
102
|
+
FileUtils.remove_dir(path)
|
103
|
+
QuickStore.store.delete(Storeable.key(course_name, prefix: 'courses'))
|
104
|
+
say_info "The course \"#{course_name}\" was successfully deleted."
|
105
|
+
end
|
106
|
+
else
|
107
|
+
system 'belajar courses help delete'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def courses_list_text(courses)
|
114
|
+
if courses.empty?
|
115
|
+
text = Terminal.text :courses_empty
|
116
|
+
else
|
117
|
+
text = [
|
118
|
+
"Available belajar courses:\n",
|
119
|
+
*courses.map { |course| "* #{File.basename(course.path)}\n" }
|
120
|
+
].join("\n")
|
121
|
+
end
|
122
|
+
|
123
|
+
"#{text}\n#{Terminal.text :hint_course_download}"
|
124
|
+
end
|
125
|
+
|
126
|
+
def store_repo_data(user_and_repo)
|
127
|
+
parts = (user_and_repo ||= Belajar.config.initial_course).split('/')
|
128
|
+
author = parts.first
|
129
|
+
course = parts.second
|
130
|
+
|
131
|
+
course = Course.new(course)
|
132
|
+
QuickStore.store.set(course.key(:author), author)
|
133
|
+
QuickStore.store.set(course.key(:github), user_and_repo)
|
134
|
+
end
|
135
|
+
|
136
|
+
def scaffold_solutions
|
137
|
+
generator = Generator.new
|
138
|
+
generator.prepare
|
139
|
+
generator.scaffold(Belajar.config.courses_path, Belajar.config.solutions_path)
|
140
|
+
end
|
141
|
+
|
142
|
+
def print_download_warning(url, text)
|
143
|
+
message = [
|
144
|
+
"Error while downloading course from URL \"#{url}\"",
|
145
|
+
"#{text}\n",
|
146
|
+
Terminal.text(:hint_course_download)
|
147
|
+
].join("\n")
|
148
|
+
|
149
|
+
say_warning message
|
150
|
+
end
|
151
|
+
|
152
|
+
def update_course(course)
|
153
|
+
url = QuickStore.store.get(course.key(:url))
|
154
|
+
github_repo = QuickStore.store.get(course.key(:github))
|
155
|
+
updated = GithubClient.updated?(github_repo)
|
156
|
+
|
157
|
+
if !github_repo || updated
|
158
|
+
download(url, 'updated') if url
|
159
|
+
else
|
160
|
+
say_info "Course \"#{course.title}\" is still up to date."
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def print_course_not_available(course_name)
|
165
|
+
text = [
|
166
|
+
"The course \"#{course_name}\" is not available in",
|
167
|
+
"\"#{Belajar.config.courses_path}\".\n",
|
168
|
+
]
|
169
|
+
|
170
|
+
say_warning text.join("\n")
|
171
|
+
|
172
|
+
unless Loading::Courses.load(Belajar.config.courses_path).empty?
|
173
|
+
Terminal::Courses.new.list
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'active_support/concern'
|
3
|
+
require 'colorize'
|
4
|
+
|
5
|
+
module Belajar
|
6
|
+
module Terminal
|
7
|
+
|
8
|
+
module Output
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
private
|
13
|
+
|
14
|
+
def say(text)
|
15
|
+
output = text.split("\n").map {|line| "\t#{line}" }.join("\n")
|
16
|
+
$stdout.puts output
|
17
|
+
end
|
18
|
+
|
19
|
+
def empty_line
|
20
|
+
$stdout.puts ''
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(string)
|
24
|
+
$stdout.print "\n\t#{string} "
|
25
|
+
$stdin.gets.strip
|
26
|
+
end
|
27
|
+
|
28
|
+
def say_info(text)
|
29
|
+
say_box(text, ' ℹ', :light_blue)
|
30
|
+
end
|
31
|
+
|
32
|
+
def say_warning(text)
|
33
|
+
say_box(text, '⚠ ', :light_red)
|
34
|
+
end
|
35
|
+
|
36
|
+
def say_box(text, symbol, color)
|
37
|
+
empty_line
|
38
|
+
say line.send(color)
|
39
|
+
empty_line
|
40
|
+
|
41
|
+
indented_text = text.split("\n").join("\n#{' ' * (symbol.length + 1)}")
|
42
|
+
say indented_text.prepend("#{symbol} ").send(color)
|
43
|
+
|
44
|
+
empty_line
|
45
|
+
say line.send(color)
|
46
|
+
empty_line
|
47
|
+
end
|
48
|
+
|
49
|
+
def line(symbol = '-')
|
50
|
+
symbol * 70
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_command(command, description)
|
54
|
+
say description
|
55
|
+
|
56
|
+
loop do
|
57
|
+
cmd = get '>'
|
58
|
+
|
59
|
+
unless cmd == command
|
60
|
+
say "This was something else. Try \"#{command}\"."
|
61
|
+
next
|
62
|
+
end
|
63
|
+
|
64
|
+
system cmd
|
65
|
+
break
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_confirm(description)
|
70
|
+
say_warning description
|
71
|
+
confirm = get '(yes|no)'
|
72
|
+
yield if confirm == 'yes' && block_given?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|