rpw 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ ## Installation Requirements
2
+
3
+ This client assumes you're using Ruby 2.3 or later.
4
+
5
+ This client assumes you have `tar` installed and available on your PATH.
6
+
7
+ The way that the client opens files (using `open` or `xdg-open` depending on platform) assumes you have your default program for the following filetypes set correctly:
8
+
9
+ * .md for Markdown (XCode by default on Mac: you probably want to change that!)
10
+ * .mp4 for videos
11
+
12
+ ## Slack Invite
13
+
14
+ If you purchased the Workshop yourself, you will receive a Slack channel invitation
15
+ shortly. If you are attending the Workshop as part of a group and your license key
16
+ was provided to you, you need to register your key to get an invite:
17
+
18
+ ```
19
+ $ rpw key register [YOUR_EMAIL_ADDRESS]
20
+ ```
21
+
22
+ Please note you can only register your key once.
23
+
24
+ The Slack channel is your best resource for questions about Rails Performance
25
+ or other material in the workshop. Nate is almost always monitoring that channel.
26
+
27
+ If you encounter a **bug or other software problem**, please email support@speedshop.co.
28
+
29
+ ## Important Commands
30
+
31
+ Here are some important commands for you to know:
32
+
33
+ ```
34
+ $ rpw lesson next | Proceed to the next part of the workshop.
35
+ $ rpw lesson complete | Mark current lesson as complete.
36
+ $ rpw lesson list | List all workshop lessons. Note each lesson is preceded with an ID.
37
+ $ rpw lesson download | Download any or all lessons. Use the IDs from "list".
38
+ $ rpw lesson show | Show any particular workshop lesson. Use the IDs from "list".
39
+ $ rpw progress | Show where you're currently at in the workshop.
40
+ ```
41
+
42
+ Generally, you'll just be doing a lot of `$ rpw lesson next`!
43
+
44
+ By default, `$ rpw lesson next` will try to open the content it downloads. If you
45
+ either don't like this, or for some reason it doesn't work, use `$ rpw lesson next --no-open`.
46
+
47
+ ## Working Offline
48
+
49
+ By default, the course will download each piece of content as you progress through
50
+ the course. However, you can use `rpw lesson download all` to download all content
51
+ at once, and complete the workshop entirely offline.
52
+
53
+ Videos in this workshop are generally about 100MB each, which means the entire
54
+ course is about a 3 to 4GB download.
55
+
56
+ ## Bugs and Support
57
+
58
+ If you encounter any problems, please email support@speedshop.co for the fastest possible response.
@@ -0,0 +1,94 @@
1
+ require "thor"
2
+ require "thor/hollaback"
3
+ require "rpw"
4
+ require "rpw/cli/bannerlord"
5
+ require "rpw/cli/sub_command_base"
6
+ require "rpw/cli/key"
7
+ require "rpw/cli/lesson"
8
+ require "rpw/cli/progress"
9
+
10
+ module RPW
11
+ class CLI < Thor
12
+ class_before :check_version
13
+ class_before :check_setup
14
+
15
+ desc "key register [EMAIL_ADDRESS]", "Change email registered w/Speedshop"
16
+ subcommand "key", Key
17
+ desc "lesson [SUBCOMMAND]", "View and download lessons"
18
+ subcommand "lesson", Lesson
19
+ desc "progress [SUBCOMMAND]", "View and set progress"
20
+ subcommand "progress", Progress
21
+
22
+ def self.exit_on_failure?
23
+ true
24
+ end
25
+
26
+ desc "start", "Tutorial and onboarding"
27
+ def start
28
+ warn_if_already_started
29
+
30
+ print_banner
31
+ say "Welcome to the Rails Performance Workshop."
32
+ say ""
33
+ say "This is rpw, the command line client for this workshop."
34
+ say ""
35
+ say "This client will download files from the internet into the current"
36
+ say "working directory, so it's best to run this client from a new directory"
37
+ say "that you'll use as your 'scratch space' for working on the Workshop."
38
+ say ""
39
+ say "We will create a handful of new files and folders in the current directory."
40
+ return unless yes? "Is this OK? (y/N) (N will quit)"
41
+ puts ""
42
+ say "We'll also create a .rpw_info file at #{File.expand_path("~/.rpw")} to save your purchase key."
43
+ home_dir_ok = yes?("Is this OK? (y/N) (N will create it in the current directory)")
44
+ client.directory_setup(home_dir_ok)
45
+
46
+ key = ask("Your Purchase Key: ")
47
+
48
+ unless client.setup(key)
49
+ say "That is not a valid key. Please try again."
50
+ exit(0)
51
+ end
52
+
53
+ puts ""
54
+ say "Successfully authenticated with the RPW server and saved your key."
55
+ puts ""
56
+ say "Setup complete!"
57
+ puts ""
58
+ say "To learn how to use this command-line client, consult ./README.md, which we just created."
59
+ say "Once you've read that and you're ready to get going: $ rpw lesson next"
60
+ end
61
+
62
+ no_commands do
63
+ def print_banner
64
+ RPW::Bannerlord.print_banner
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def client
71
+ @client ||= RPW::Client.new
72
+ end
73
+
74
+ def warn_if_already_started
75
+ return unless client.setup?
76
+ exit(0) unless yes? "You have already started the workshop. Continuing "\
77
+ "this command will wipe all of your current progress. Continue? (y/N)"
78
+ end
79
+
80
+ def check_version
81
+ unless client.latest_version?
82
+ say "WARNING: You are running an old version of rpw."
83
+ say "WARNING: Please run `$ gem install rpw`"
84
+ end
85
+ end
86
+
87
+ def check_setup
88
+ unless client.setup? || current_command_chain == [:start]
89
+ say "WARNING: You do not have a purchase key set. Run `$ rpw start`"
90
+ exit(0)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -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,174 @@
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
+ reset_progress unless client_data["completed"]
136
+ # we actually have to put the _next_ lesson on the completed stack
137
+ set_progress(self.next["position"])
138
+ end
139
+
140
+ private
141
+
142
+ def current_position
143
+ @current_position ||= client_data["completed"]&.last || 0
144
+ end
145
+
146
+ def chart_section_progress(contents, completed)
147
+ contents.group_by { |c| c["position"] / 100 }
148
+ .each_with_object([]) do |(_, c), memo|
149
+ completed_str = c.map { |l|
150
+ if l["position"] == current_position
151
+ "O"
152
+ elsif completed.include?(l["position"])
153
+ "X"
154
+ else
155
+ "."
156
+ end
157
+ }.join
158
+ memo << {
159
+ title: c[0]["title"],
160
+ progress: completed_str
161
+ }
162
+ end
163
+ end
164
+
165
+ def client_data
166
+ @client_data ||= ClientData.new
167
+ end
168
+
169
+ def extract_content(content)
170
+ folder = content["style"]
171
+ `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
172
+ end
173
+ end
174
+ end