rpw 0.0.4 → 1.1.0

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.
@@ -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