rpw 0.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ module RPW
2
+ class Bannerlord
3
+ class << self
4
+ def print_banner
5
+ puts r
6
+ if `tput cols 80`.to_i < 80
7
+ puts small_banner
8
+ else
9
+ puts banner
10
+ end
11
+ puts reset
12
+ end
13
+
14
+ def r
15
+ "\e[31m"
16
+ end
17
+
18
+ def reset
19
+ "\e[0m"
20
+ end
21
+
22
+ def small_banner
23
+ %(
24
+ _____ _ _____ _ _
25
+ |_ _| |_ ___ | __ |___|_| |___
26
+ | | | | -_| | -| .'| | |_ -|
27
+ |_| |_|_|___| |__|__|__,|_|_|___|
28
+ _____ ___
29
+ | _ |___ ___| _|___ ___ _____ ___ ___ ___ ___
30
+ | __| -_| _| _| . | _| | .'| | _| -_|
31
+ |__| |___|_| |_| |___|_| |_|_|_|__,|_|_|___|___|
32
+ _ _ _ _ _
33
+ | | | |___ ___| |_ ___| |_ ___ ___
34
+ | | | | . | _| '_|_ -| | . | . |
35
+ |_____|___|_| |_,_|___|_|_|___| _|
36
+ |_|
37
+ #{reset})
38
+ end
39
+
40
+ def banner
41
+ %(
42
+ _____ _ _____ _ _
43
+ +hmNMMMMMm/` -ymMMNh/ |_ _| |_ ___ | __ |___|_| |___
44
+ sMMMMMMMMMy +MMMMMMMMy | | | | -_| | -| .'| | |_ -|
45
+ yMMMMMMMMMMy` yMMMMMMMMN |_| |_|_|___| |__|__|__,|_|_|___|
46
+ `dMMMMMMMMMMm:-dMMMMMMm: _____ ___
47
+ `sNMMMMMMMMMMs.:+sso:` | _ |___ ___| _|___ ___ _____ ___ ___ ___ ___
48
+ :dMMMMMMMMMMm/ | __| -_| _| _| . | _| | .'| | _| -_|
49
+ :oss+:.sNMMMMMMMMMMy` |__| |___|_| |_| |___|_| |_|_|_|__,|_|_|___|___|
50
+ /mMMMMMMd-:mMMMMMMMMMMd. _ _ _ _ _
51
+ NMMMMMMMMy `hMMMMMMMMMMh | | | |___ ___| |_ ___| |_ ___ ___
52
+ yMMMMMMMM+ `dMMMMMMMMMy | | | | . | _| '_|_ -| | . | . |
53
+ /hNMMmy- `/mMMMMMNmy/ |_____|___|_| |_,_|___|_|_|___| _|
54
+ |_|
55
+ #{reset})
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ module RPW
2
+ class Key < SubCommandBase
3
+ class_before :exit_with_no_key
4
+
5
+ desc "register [EMAIL_ADDRESS]", "Change email registered with Speedshop. One-time only."
6
+ def register(email)
7
+ if client.register_email(email)
8
+ say "Key registered with #{email}. You should receive a Slack invite soon."
9
+ else
10
+ say "Key has already been registered. If you believe this is in error,"\
11
+ " please email support@speedshop.co"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,129 @@
1
+ module RPW
2
+ class Lesson < SubCommandBase
3
+ desc "next", "Proceed to the next lesson of the workshop"
4
+ option :"no-open"
5
+ def next
6
+ exit_with_no_key
7
+ say "Proceeding to next lesson..."
8
+ content = client.next
9
+
10
+ if content.nil?
11
+ RPW::CLI.new.print_banner
12
+ say "Congratulations!"
13
+ say "You have completed the Rails Performance Workshop."
14
+ exit(0)
15
+ end
16
+
17
+ client.download_and_extract(content)
18
+ client.complete(content["position"])
19
+ display_content(content, !options[:"no-open"])
20
+ end
21
+
22
+ desc "complete", "Mark the current lesson as complete"
23
+ def complete
24
+ say "Marked current lesson as complete"
25
+ client.complete(nil)
26
+ end
27
+
28
+ desc "list", "Show all available workshop lessons"
29
+ def list
30
+ ::CLI::UI::Frame.open("{{*}} {{bold:All Lessons}}", color: :green)
31
+
32
+ frame_open = false
33
+ client.list.each do |lesson|
34
+ if lesson["title"].start_with?("Section")
35
+ ::CLI::UI::Frame.close(nil) if frame_open
36
+ ::CLI::UI::Frame.open(lesson["title"])
37
+ frame_open = true
38
+ next
39
+ end
40
+
41
+ case lesson["style"]
42
+ when "video"
43
+ puts ::CLI::UI.fmt "{{red:#{lesson["title"]}}}"
44
+ when "quiz"
45
+ # puts ::CLI::UI.fmt "{{green:#{" " + lesson["title"]}}}"
46
+ when "lab"
47
+ puts ::CLI::UI.fmt "{{yellow:#{" " + lesson["title"]}}}"
48
+ when "text"
49
+ puts ::CLI::UI.fmt "{{magenta:#{" " + lesson["title"]}}}"
50
+ else
51
+ puts ::CLI::UI.fmt "{{magenta:#{" " + lesson["title"]}}}"
52
+ end
53
+ end
54
+
55
+ ::CLI::UI::Frame.close(nil)
56
+ ::CLI::UI::Frame.close(nil, color: :green)
57
+ end
58
+
59
+ desc "download", "Download all workshop contents"
60
+ def download
61
+ exit_with_no_key
62
+ total = client.list.size
63
+ client.list.each do |content|
64
+ current = client.list.index(content)
65
+ puts "Downloading #{content["title"]} (#{current}/#{total})"
66
+ client.download_and_extract(content)
67
+ end
68
+ end
69
+
70
+ desc "show", "Show any individal workshop lesson"
71
+ option :"no-open"
72
+ def show
73
+ exit_with_no_key
74
+ title = ::CLI::UI::Prompt.ask(
75
+ "Which lesson would you like to view?",
76
+ options: client.list.reject { |l| l["title"] == "Quiz" }.map { |l| " " * l["indent"] + l["title"] }
77
+ )
78
+ title.strip!
79
+ content_order = client.list.find { |l| l["title"] == title }["position"]
80
+ content = client.show(content_order)
81
+ client.download_and_extract(content)
82
+ display_content(content, !options[:"no-open"])
83
+ end
84
+
85
+ private
86
+
87
+ def display_content(content, open_after)
88
+ say "Current Lesson: #{content["title"]}"
89
+ openable = false
90
+ case content["style"]
91
+ when "video"
92
+ location = "video/#{content["s3_key"]}"
93
+ openable = true
94
+ when "quiz"
95
+ Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
96
+ when "lab"
97
+ location = "lab/#{content["s3_key"][0..-8]}"
98
+ openable = true
99
+ when "text"
100
+ location = "text/#{content["s3_key"]}"
101
+ openable = true
102
+ when "cgrp"
103
+ say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
104
+ say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
105
+ say "You can check it out now, or to continue: $ rpw lesson next "
106
+ end
107
+ if location
108
+ if openable && !open_after
109
+ say "Download complete. Open with: $ #{open_command} #{location}"
110
+ elsif open_after && openable
111
+ exec "#{open_command} #{location}"
112
+ end
113
+ end
114
+ end
115
+
116
+ require "rbconfig"
117
+ def open_command
118
+ host_os = RbConfig::CONFIG["host_os"]
119
+ case host_os
120
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
121
+ "start"
122
+ when /darwin|mac os/
123
+ "open"
124
+ else
125
+ "xdg-open"
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,38 @@
1
+ module RPW
2
+ class Progress < SubCommandBase
3
+ desc "set [LESSON]", "Set current lesson to a particular lesson"
4
+ def set(pos)
5
+ lesson = client.set_progress(pos.to_i)
6
+ say "Set current progress to #{lesson["title"]}"
7
+ end
8
+
9
+ desc "reset", "Erase all progress and start over"
10
+ def reset
11
+ return unless ::CLI::UI.confirm("Are you sure you want to erase all of your progress?", default: false)
12
+ say "Resetting progress."
13
+ client.set_progress(nil)
14
+ end
15
+
16
+ desc "show", "Show current workshop progress"
17
+ def show
18
+ data = client.progress
19
+ ::CLI::UI::Frame.open("The Rails Performance Workshop", timing: false, color: :red) do
20
+ say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
21
+ say ""
22
+ say "Current lesson: #{data[:current_lesson]["title"]}" if data[:current_lesson]
23
+ say ""
24
+ ::CLI::UI::Frame.open("Progress", timing: false, color: :red) do
25
+ puts ::CLI::UI.fmt "{{i}} (X == completed, O == current)"
26
+ say ""
27
+ data[:sections].each do |section|
28
+ say "#{section[:title]}: #{section[:progress]}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ default_task :show
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ require "digest"
2
+ require "thor"
3
+
4
+ module RPW
5
+ class Quiz < Thor
6
+ desc "give_quiz FILENAME", ""
7
+ def give_quiz(filename)
8
+ @quiz_data = YAML.safe_load(File.read(filename))
9
+ @quiz_data["questions"].each { |q| question(q) }
10
+ end
11
+
12
+ private
13
+
14
+ def question(data)
15
+ puts data["prompt"]
16
+ data["answer_choices"].each { |ac| puts ac }
17
+ provided_answer = ::CLI::UI::Prompt.ask("Your answer?", options: %w[A B C D])
18
+ answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer)
19
+ if answer_digest == data["answer_digest"]
20
+ say "Correct!"
21
+ else
22
+ say "Incorrect."
23
+ say "I encourage you to try reviewing the material to see what the correct answer is."
24
+ end
25
+ say ""
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module RPW
2
+ class SubCommandBase < Thor
3
+ def self.banner(command, namespace = nil, subcommand = false)
4
+ "#{basename} #{subcommand_prefix} #{command.usage}"
5
+ end
6
+
7
+ def self.subcommand_prefix
8
+ name.gsub(%r{.*::}, "").gsub(%r{^[A-Z]}) { |match| match[0].downcase }
9
+ .gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
10
+ end
11
+
12
+ no_commands do
13
+ def client
14
+ @client ||= RPW::Client.new
15
+ end
16
+
17
+ def exit_with_no_key
18
+ unless client.setup?
19
+ say "You have not yet set up the client. Run $ rpw start"
20
+ exit(1)
21
+ end
22
+ unless client.directories_ready?
23
+ say "You are not in your workshop scratch directory, or you have not yet"
24
+ say "set up the client. Change directory or run $ rpw start"
25
+ exit(1)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,177 @@
1
+ require "fileutils"
2
+ require "rpw/cli/quiz"
3
+
4
+ module RPW
5
+ class Client
6
+ RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
7
+ attr_reader :gateway
8
+
9
+ def initialize(gateway = nil)
10
+ @gateway = gateway || Gateway.new(RPW_SERVER_DOMAIN, client_data["key"])
11
+ end
12
+
13
+ def setup(key)
14
+ success = gateway.authenticate_key(key)
15
+ client_data["key"] = key if success
16
+ success
17
+ end
18
+
19
+ def register_email(email)
20
+ gateway.register_email(email)
21
+ end
22
+
23
+ def next
24
+ return list.first unless client_data["completed"]
25
+ list.sort_by { |c| c["position"] }.find { |c| c["position"] > current_position }
26
+ end
27
+
28
+ def list
29
+ @list ||= begin
30
+ if client_data["content_cache_generated"] &&
31
+ client_data["content_cache_generated"] >= Time.now - 60 * 60
32
+
33
+ client_data["content_cache"]
34
+ else
35
+ begin
36
+ client_data["content_cache"] = gateway.list_content
37
+ client_data["content_cache_generated"] = Time.now
38
+ client_data["content_cache"]
39
+ rescue
40
+ client_data["content_cache"] || (raise Error.new("No internet connection"))
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def show(content_pos)
47
+ list.find { |l| l["position"] == content_pos }
48
+ end
49
+
50
+ def directory_setup(home_dir_ok = true)
51
+ ["video", "quiz", "lab", "text", "cgrp"].each do |path|
52
+ FileUtils.mkdir_p(path) unless File.directory?(path)
53
+ end
54
+
55
+ if home_dir_ok
56
+ ClientData.create_in_home!
57
+ else
58
+ ClientData.create_in_pwd!
59
+ end
60
+
61
+ unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_info/)
62
+ File.open(".gitignore", "a") do |f|
63
+ f.puts "\n"
64
+ f.puts ".rpw_info\n"
65
+ f.puts "video\n"
66
+ f.puts "quiz\n"
67
+ f.puts "lab\n"
68
+ f.puts "text\n"
69
+ f.puts "cgrp\n"
70
+ end
71
+ end
72
+
73
+ File.open("README.md", "w+") do |f|
74
+ f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
75
+ end
76
+ end
77
+
78
+ def progress
79
+ completed_lessons = client_data["completed"] || []
80
+ {
81
+ completed: completed_lessons.size,
82
+ total: list.size,
83
+ current_lesson: list.find { |c| c["position"] == current_position },
84
+ sections: chart_section_progress(list, completed_lessons)
85
+ }
86
+ end
87
+
88
+ def set_progress(pos)
89
+ client_data["completed"] = [] && return if pos.nil?
90
+ lesson = list.find { |l| l["position"] == pos }
91
+ raise Error.new("No such lesson - use the IDs in $ rpw lesson list") unless lesson
92
+ client_data["completed"] += [pos]
93
+ lesson
94
+ end
95
+
96
+ def latest_version?
97
+ return true unless ClientData.exists?
98
+ return true if client_data["last_version_check"] &&
99
+ client_data["last_version_check"] >= Time.now - (60 * 60)
100
+
101
+ begin
102
+ latest = gateway.latest_version?
103
+ rescue
104
+ return true
105
+ end
106
+
107
+ client_data["last_version_check"] = if latest
108
+ Time.now
109
+ else
110
+ false
111
+ end
112
+ end
113
+
114
+ def setup?
115
+ return false unless ClientData.exists?
116
+ client_data["key"]
117
+ end
118
+
119
+ def directories_ready?
120
+ ["video", "quiz", "lab", "text", "cgrp"].all? do |path|
121
+ File.directory?(path)
122
+ end
123
+ end
124
+
125
+ def download_and_extract(content)
126
+ location = content["style"] + "/" + content["s3_key"]
127
+ unless File.exist?(location)
128
+ gateway.download_content(content, folder: content["style"])
129
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
130
+ end
131
+ end
132
+
133
+ def complete(position)
134
+ if client_data["completed"]
135
+ # we actually have to put the _next_ lesson on the completed stack
136
+ set_progress(self.next["position"])
137
+ else
138
+ client_data["completed"] = []
139
+ set_progress(position)
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def current_position
146
+ @current_position ||= client_data["completed"]&.last || 0
147
+ end
148
+
149
+ def chart_section_progress(contents, completed)
150
+ contents.group_by { |c| c["position"] / 100 }
151
+ .each_with_object([]) do |(_, c), memo|
152
+ completed_str = c.map { |l|
153
+ if l["position"] == current_position
154
+ "O"
155
+ elsif completed.include?(l["position"])
156
+ "X"
157
+ else
158
+ "."
159
+ end
160
+ }.join
161
+ memo << {
162
+ title: c[0]["title"],
163
+ progress: completed_str
164
+ }
165
+ end
166
+ end
167
+
168
+ def client_data
169
+ @client_data ||= ClientData.new
170
+ end
171
+
172
+ def extract_content(content)
173
+ folder = content["style"]
174
+ `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
175
+ end
176
+ end
177
+ end