rpw 0.0.5 → 0.0.6

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 nate.berkopec@speedshop.co"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,99 @@
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 :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.increment_current_lesson!(content["position"])
20
+ display_content(content, options[: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
27
+ end
28
+
29
+ desc "list", "Show all available workshop lessons"
30
+ def list
31
+ say "All available workshop lessons:"
32
+ client.list.each do |lesson|
33
+ puts "#{" " * lesson["indent"]}[#{lesson["position"]}]: #{lesson["title"]}"
34
+ end
35
+ end
36
+
37
+ desc "download [CONTENT | all]", "Download one or all workshop contents"
38
+ def download(content_pos)
39
+ to_download = if content_pos.downcase == "all"
40
+ client.list
41
+ else
42
+ [client.show(content_pos)]
43
+ end
44
+ to_download.each { |content| client.download_and_extract(content) }
45
+ end
46
+
47
+ desc "show [CONTENT]", "Show any workshop lesson, shows current lesson w/no arguments"
48
+ option :open
49
+ def show(content_order = :current)
50
+ content = client.show(content_order)
51
+ client.download_and_extract(content)
52
+ display_content(content, options[:open])
53
+ end
54
+
55
+ private
56
+
57
+ def display_content(content, open_after)
58
+ say "Current Lesson: #{content["title"]}"
59
+ openable = false
60
+ case content["style"]
61
+ when "video"
62
+ location = "video/#{content["s3_key"]}"
63
+ openable = true
64
+ when "quiz"
65
+ Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
66
+ when "lab"
67
+ location = "lab/#{content["s3_key"][0..-8]}"
68
+ when "text"
69
+ location = "lab/#{content["s3_key"]}"
70
+ openable = true
71
+ when "cgrp"
72
+ say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
73
+ say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
74
+ end
75
+ if location
76
+ if openable && !open_after
77
+ say "This file can be opened automatically if you use the --open flag next time."
78
+ say "e.g. $ rpw lesson next --open"
79
+ say "Download complete. Open with: $ #{open_command} #{location}"
80
+ elsif open_after && openable
81
+ exec "#{open_command} #{location}"
82
+ end
83
+ end
84
+ end
85
+
86
+ require "rbconfig"
87
+ def open_command
88
+ host_os = RbConfig::CONFIG["host_os"]
89
+ case host_os
90
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
91
+ "start"
92
+ when /darwin|mac os/
93
+ "open"
94
+ else
95
+ "xdg-open"
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,32 @@
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(lesson)
7
+ client.set_progress(lesson)
8
+ end
9
+
10
+ desc "reset", "Erase all progress and start over"
11
+ def reset
12
+ yes? "Are you sure you want to reset your progress? (Y/N)"
13
+ client.reset_progress
14
+ end
15
+
16
+ desc "show", "Show current workshop progress"
17
+ def show
18
+ data = client.progress
19
+ say "The Rails Performance Workshop"
20
+ say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
21
+ say "Current lesson: #{data[:current_lesson]["title"]}" if data[:current_lesson]
22
+ say "Progress by Section (X == completed, O == current):"
23
+ data[:sections].each do |section|
24
+ say "#{section[:title]}: #{section[:progress]}"
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ default_task :show
31
+ end
32
+ 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,168 @@
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
+ contents = gateway.list_content
25
+ return contents.first unless client_data["completed"]
26
+ contents.delete_if { |c| client_data["completed"].include? c["position"] }
27
+ contents.sort_by { |c| c["position"] }[1] # 0 would be the current lesson
28
+ end
29
+
30
+ def list
31
+ gateway.list_content
32
+ end
33
+
34
+ def show(content_pos)
35
+ content_pos = client_data["current_lesson"] if content_pos == :current
36
+ gateway.get_content_by_position(content_pos)
37
+ end
38
+
39
+ def directory_setup(home_dir_ok = true)
40
+ ["video", "quiz", "lab", "text", "cgrp"].each do |path|
41
+ FileUtils.mkdir_p(path) unless File.directory?(path)
42
+ end
43
+
44
+ if home_dir_ok
45
+ ClientData.create_in_home!
46
+ else
47
+ ClientData.create_in_pwd!
48
+ end
49
+
50
+ unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_info/)
51
+ File.open(".gitignore", "a") do |f|
52
+ f.puts "\n"
53
+ f.puts ".rpw_info\n"
54
+ f.puts "video\n"
55
+ f.puts "quiz\n"
56
+ f.puts "lab\n"
57
+ f.puts "text\n"
58
+ f.puts "cgrp\n"
59
+ end
60
+ end
61
+
62
+ File.open("README.md", "w+") do |f|
63
+ f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
64
+ end
65
+ end
66
+
67
+ def increment_current_lesson!(position)
68
+ mark_current_lesson_as_completed
69
+ client_data["current_lesson"] = position
70
+ end
71
+
72
+ def progress
73
+ contents = gateway.list_content
74
+ completed_lessons = client_data["completed"] || []
75
+ {
76
+ completed: completed_lessons.size,
77
+ total: contents.size,
78
+ current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
79
+ sections: chart_section_progress(contents, completed_lessons)
80
+ }
81
+ end
82
+
83
+ def set_progress(lesson)
84
+ client_data["current_lesson"] = lesson.to_i
85
+ end
86
+
87
+ def reset_progress
88
+ client_data["current_lesson"] = 0
89
+ client_data["completed"] = []
90
+ end
91
+
92
+ def latest_version?
93
+ return true unless ClientData.exists?
94
+
95
+ if client_data["last_version_check"]
96
+ return true if client_data["last_version_check"] >= Time.now - (60 * 60 * 24)
97
+ return false if client_data["last_version_check"] == false
98
+ end
99
+
100
+ begin
101
+ latest = gateway.latest_version?
102
+ rescue
103
+ return true
104
+ end
105
+
106
+ client_data["last_version_check"] = if latest
107
+ Time.now
108
+ else
109
+ false
110
+ end
111
+ end
112
+
113
+ def setup?
114
+ return false unless ClientData.exists?
115
+ client_data["key"]
116
+ end
117
+
118
+ def directories_ready?
119
+ ["video", "quiz", "lab", "text", "cgrp"].all? do |path|
120
+ File.directory?(path)
121
+ end
122
+ end
123
+
124
+ def download_and_extract(content)
125
+ location = content["style"] + "/" + content["s3_key"]
126
+ unless File.exist?(location)
127
+ gateway.download_content(content, folder: content["style"])
128
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def mark_current_lesson_as_completed
135
+ reset_progress unless client_data["current_lesson"] && client_data["completed"]
136
+ client_data["completed"] ||= []
137
+ client_data["completed"] += [client_data["current_lesson"] || 0]
138
+ end
139
+
140
+ def chart_section_progress(contents, completed)
141
+ contents.group_by { |c| c["position"] / 100 }
142
+ .each_with_object([]) do |(_, c), memo|
143
+ completed_str = c.map { |l|
144
+ if l["position"] == client_data["current_lesson"]
145
+ "O"
146
+ elsif completed.include?(l["position"])
147
+ "X"
148
+ else
149
+ "."
150
+ end
151
+ }.join
152
+ memo << {
153
+ title: c[0]["title"],
154
+ progress: completed_str
155
+ }
156
+ end
157
+ end
158
+
159
+ def client_data
160
+ @client_data ||= ClientData.new
161
+ end
162
+
163
+ def extract_content(content)
164
+ folder = content["style"]
165
+ `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,73 @@
1
+ require "fileutils"
2
+ require "yaml"
3
+
4
+ module RPW
5
+ class ClientData
6
+ DOTFILE_NAME = ".rpw_info"
7
+
8
+ def initialize
9
+ data # access file to load
10
+ end
11
+
12
+ def [](key)
13
+ data[key]
14
+ end
15
+
16
+ def []=(key, value)
17
+ data
18
+ data[key] = value
19
+
20
+ begin
21
+ File.open(filestore_location, "w") { |f| f.write(YAML.dump(data)) }
22
+ rescue
23
+ raise Error, "The RPW data at #{filestore_location} is not writable. \
24
+ Check your file permissions."
25
+ end
26
+ end
27
+
28
+ def self.create_in_pwd!
29
+ FileUtils.touch(File.expand_path("./" + DOTFILE_NAME))
30
+ end
31
+
32
+ def self.create_in_home!
33
+ unless File.directory?(File.expand_path("~/.rpw/"))
34
+ FileUtils.mkdir(File.expand_path("~/.rpw/"))
35
+ end
36
+
37
+ FileUtils.touch(File.expand_path("~/.rpw/" + DOTFILE_NAME))
38
+ end
39
+
40
+ def self.delete_filestore
41
+ return unless File.exist?(filestore_location)
42
+ FileUtils.remove(filestore_location)
43
+ end
44
+
45
+ def self.exists?
46
+ File.exist? filestore_location
47
+ end
48
+
49
+ def self.filestore_location
50
+ if File.exist?(File.expand_path("./" + DOTFILE_NAME))
51
+ File.expand_path("./" + DOTFILE_NAME)
52
+ else
53
+ File.expand_path("~/.rpw/" + DOTFILE_NAME)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def filestore_location
60
+ self.class.filestore_location
61
+ end
62
+
63
+ def data
64
+ @data ||= begin
65
+ begin
66
+ YAML.safe_load(File.read(filestore_location), permitted_classes: [Time]) || {}
67
+ rescue Errno::ENOENT
68
+ {}
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end