rpw 0.0.3 → 1.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.
@@ -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,101 @@
1
+ module RPW
2
+ class Lesson < SubCommandBase
3
+ class_before :exit_with_no_key
4
+
5
+ desc "next", "Proceed to the next lesson of the workshop"
6
+ option :"no-open"
7
+ def next
8
+ say "Proceeding to next lesson..."
9
+ content = client.next
10
+
11
+ if content.nil?
12
+ RPW::CLI.new.print_banner
13
+ say "Congratulations!"
14
+ say "You have completed the Rails Performance Workshop."
15
+ exit(0)
16
+ end
17
+
18
+ client.download_and_extract(content)
19
+ client.complete(content["position"])
20
+ display_content(content, !options[:"no-open"])
21
+ end
22
+
23
+ desc "complete", "Mark the current lesson as complete"
24
+ def complete
25
+ say "Marked current lesson as complete"
26
+ client.complete(nil)
27
+ end
28
+
29
+ desc "list", "Show all available workshop lessons"
30
+ def list
31
+ say "All available workshop lessons:"
32
+ say "Use [ID] for the show/download command"
33
+ say "[ID]: Lesson Name"
34
+ client.list.each do |lesson|
35
+ puts "[#{lesson["position"]}]:#{" " * lesson["indent"]} #{lesson["title"]}"
36
+ end
37
+ end
38
+
39
+ desc "download [CONTENT | all]", "Download one or all workshop contents"
40
+ def download(content_pos)
41
+ to_download = if content_pos.downcase == "all"
42
+ client.list
43
+ else
44
+ [client.show(content_pos)]
45
+ end
46
+ to_download.each { |content| client.download_and_extract(content) }
47
+ end
48
+
49
+ desc "show [CONTENT]", "Show any workshop lesson, shows current lesson w/no arguments"
50
+ option :"no-open"
51
+ def show(content_order = :current)
52
+ content = client.show(content_order)
53
+ client.download_and_extract(content)
54
+ display_content(content, !options[:"no-open"])
55
+ end
56
+
57
+ private
58
+
59
+ def display_content(content, open_after)
60
+ say "Current Lesson: #{content["title"]}"
61
+ openable = false
62
+ case content["style"]
63
+ when "video"
64
+ location = "video/#{content["s3_key"]}"
65
+ openable = true
66
+ when "quiz"
67
+ Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
68
+ when "lab"
69
+ location = "lab/#{content["s3_key"][0..-8]}"
70
+ openable = true
71
+ when "text"
72
+ location = "text/#{content["s3_key"]}"
73
+ openable = true
74
+ when "cgrp"
75
+ say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
76
+ say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
77
+ say "You can check it out now, or to continue: $ rpw lesson next "
78
+ end
79
+ if location
80
+ if openable && !open_after
81
+ say "Download complete. Open with: $ #{open_command} #{location}"
82
+ elsif open_after && openable
83
+ exec "#{open_command} #{location}"
84
+ end
85
+ end
86
+ end
87
+
88
+ require "rbconfig"
89
+ def open_command
90
+ host_os = RbConfig::CONFIG["host_os"]
91
+ case host_os
92
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
93
+ "start"
94
+ when /darwin|mac os/
95
+ "open"
96
+ else
97
+ "xdg-open"
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,34 @@
1
+ module RPW
2
+ class Progress < SubCommandBase
3
+ class_before :exit_with_no_key
4
+
5
+ desc "set [LESSON]", "Set current lesson to a particular lesson"
6
+ def set(pos)
7
+ lesson = client.set_progress(pos.to_i)
8
+ say "Set current progress to #{lesson["title"]}"
9
+ end
10
+
11
+ desc "reset", "Erase all progress and start over"
12
+ def reset
13
+ return unless yes? "Are you sure you want to reset your progress? (Y/N)"
14
+ say "Resetting progress."
15
+ client.set_progress(nil)
16
+ end
17
+
18
+ desc "show", "Show current workshop progress"
19
+ def show
20
+ data = client.progress
21
+ say "The Rails Performance Workshop"
22
+ say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
23
+ say "Current lesson: #{data[:current_lesson]["title"]}" if data[:current_lesson]
24
+ say "Progress by Section (X == completed, O == current):"
25
+ data[:sections].each do |section|
26
+ say "#{section[:title]}: #{section[:progress]}"
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ default_task :show
33
+ end
34
+ 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 = ask("Your answer?")
18
+ answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer.upcase)
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,178 @@
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
+ content_pos = current_position if content_pos == :current
48
+ gateway.get_content_by_position(content_pos)
49
+ end
50
+
51
+ def directory_setup(home_dir_ok = true)
52
+ ["video", "quiz", "lab", "text", "cgrp"].each do |path|
53
+ FileUtils.mkdir_p(path) unless File.directory?(path)
54
+ end
55
+
56
+ if home_dir_ok
57
+ ClientData.create_in_home!
58
+ else
59
+ ClientData.create_in_pwd!
60
+ end
61
+
62
+ unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_info/)
63
+ File.open(".gitignore", "a") do |f|
64
+ f.puts "\n"
65
+ f.puts ".rpw_info\n"
66
+ f.puts "video\n"
67
+ f.puts "quiz\n"
68
+ f.puts "lab\n"
69
+ f.puts "text\n"
70
+ f.puts "cgrp\n"
71
+ end
72
+ end
73
+
74
+ File.open("README.md", "w+") do |f|
75
+ f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
76
+ end
77
+ end
78
+
79
+ def progress
80
+ completed_lessons = client_data["completed"] || []
81
+ {
82
+ completed: completed_lessons.size,
83
+ total: list.size,
84
+ current_lesson: list.find { |c| c["position"] == current_position },
85
+ sections: chart_section_progress(list, completed_lessons)
86
+ }
87
+ end
88
+
89
+ def set_progress(pos)
90
+ client_data["completed"] = [] && return if pos.nil?
91
+ lesson = list.find { |l| l["position"] == pos }
92
+ raise Error.new("No such lesson - use the IDs in $ rpw lesson list") unless lesson
93
+ client_data["completed"] += [pos]
94
+ lesson
95
+ end
96
+
97
+ def latest_version?
98
+ return true unless ClientData.exists?
99
+ return true if client_data["last_version_check"] &&
100
+ client_data["last_version_check"] >= Time.now - (60 * 60)
101
+
102
+ begin
103
+ latest = gateway.latest_version?
104
+ rescue
105
+ return true
106
+ end
107
+
108
+ client_data["last_version_check"] = if latest
109
+ Time.now
110
+ else
111
+ false
112
+ end
113
+ end
114
+
115
+ def setup?
116
+ return false unless ClientData.exists?
117
+ client_data["key"]
118
+ end
119
+
120
+ def directories_ready?
121
+ ["video", "quiz", "lab", "text", "cgrp"].all? do |path|
122
+ File.directory?(path)
123
+ end
124
+ end
125
+
126
+ def download_and_extract(content)
127
+ location = content["style"] + "/" + content["s3_key"]
128
+ unless File.exist?(location)
129
+ gateway.download_content(content, folder: content["style"])
130
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
131
+ end
132
+ end
133
+
134
+ def complete(position)
135
+ if client_data["completed"]
136
+ # we actually have to put the _next_ lesson on the completed stack
137
+ set_progress(self.next["position"])
138
+ else
139
+ client_data["completed"] = []
140
+ set_progress(position)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def current_position
147
+ @current_position ||= client_data["completed"]&.last || 0
148
+ end
149
+
150
+ def chart_section_progress(contents, completed)
151
+ contents.group_by { |c| c["position"] / 100 }
152
+ .each_with_object([]) do |(_, c), memo|
153
+ completed_str = c.map { |l|
154
+ if l["position"] == current_position
155
+ "O"
156
+ elsif completed.include?(l["position"])
157
+ "X"
158
+ else
159
+ "."
160
+ end
161
+ }.join
162
+ memo << {
163
+ title: c[0]["title"],
164
+ progress: completed_str
165
+ }
166
+ end
167
+ end
168
+
169
+ def client_data
170
+ @client_data ||= ClientData.new
171
+ end
172
+
173
+ def extract_content(content)
174
+ folder = content["style"]
175
+ `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
176
+ end
177
+ end
178
+ end