rpw 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b470a6daefa536118bc45ad650345ae904890cfdc21340f4241fe95585840ca
4
- data.tar.gz: 4055764a0110865d0bd1faa4616a269c16beeceee6868ad9f1b5a8188b172f30
3
+ metadata.gz: 7da44e397174a310bc8ba3ed4c2ecb28f2811cf30275a2cb7cd3ec85029ab99a
4
+ data.tar.gz: c096b4539f5570d6994eed51ec758007e08f75632e0e26f7d13e15b531633995
5
5
  SHA512:
6
- metadata.gz: 797f1c5528477c5befc5bed08a2dba98aabf2968be757a682a5f5dda81ae9815bbfa572f4974504eecb478a7bf106abef1562945f3865f09a8429badf68f61b4
7
- data.tar.gz: a948446a6b948793d63dad89880c1e958f21532eaa278b9e232248a2b298a59c8b103e21c4f0ec537fb003d066abf79da56d8d3e97fde3aad49c9c99fbfaf7f3
6
+ metadata.gz: 3557e0688d5141ae96bcfed87ced6539059bbbd0517a3f2bf85910c5b13b99e153d940fbf56d9bd86bf2cf23525f32a9c848d7fa3ee2d58ae664db806854e93b
7
+ data.tar.gz: 0e804e064e64cd66c703238e00d80f75a35871bf724c08203594d4eec1c94db5815d5ac2bacc11b743e008eb1aeec2c39f2b240fefac87aebe2c005bb194ae19
data/.gitignore CHANGED
@@ -1,2 +1,5 @@
1
1
  bundle
2
- *.gem
2
+ *.gem
3
+ .rpw_key
4
+ .rpw_info
5
+ smoketest
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rpw (0.0.1)
4
+ rpw (0.0.2)
5
5
  thor
6
6
  typhoeus
7
7
 
data/HISTORY.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.0.2
2
+
3
+ * first command-complete version
4
+
1
5
  ## 0.0.1
2
6
 
3
7
  * setup command works
data/exe/rpw CHANGED
@@ -4,56 +4,131 @@ require "thor"
4
4
  require_relative "../lib/rpw"
5
5
 
6
6
  module RPW
7
- class CLI < Thor
8
- class_option :verbose, type: :boolean, aliases: "-v"
7
+ class SubCommandBase < Thor
8
+ def self.banner(command, namespace = nil, subcommand = false)
9
+ "#{basename} #{subcommand_prefix} #{command.usage}"
10
+ end
9
11
 
10
- desc "next", "Proceed to the next section of the workshop"
12
+ def self.subcommand_prefix
13
+ name.gsub(%r{.*::}, "").gsub(%r{^[A-Z]}) { |match| match[0].downcase }.gsub(%r{[A-Z]}) { |match| "-#{match[0].downcase}" }
14
+ end
15
+ end
16
+
17
+ class Lesson < SubCommandBase
18
+ desc "next", "Proceed to the next lesson of the workshop"
11
19
 
12
20
  def next
13
- # ...
21
+ say "Proceeding to next lesson..."
22
+ client.next
14
23
  end
15
24
 
16
- desc "setup", "Set up your purchase key in order to download workshop sections"
25
+ desc "complete", "Mark the current lesson as complete"
17
26
 
18
- def setup
19
- say "We'll create a .rpw_key file in the current directory to save your key for future use."
20
- key = ask("Purchase Key: ")
27
+ def complete
28
+ say "Marked current lesson as complete"
29
+ client.complete
30
+ end
21
31
 
22
- client.setup(key)
32
+ desc "list", "Show all available workshop lessons"
23
33
 
24
- say "Successfully authenticated with the RPW server and saved your key."
34
+ def list
35
+ say "All available workshop lessons:"
36
+ client.list.each do |lesson|
37
+ puts "#{" " * lesson["indent"]}[#{lesson["position"]}]: #{lesson["title"]}"
38
+ end
25
39
  end
26
40
 
27
- desc "status", "Show your current workshop progression"
41
+ desc "download [CONTENT | all]", "Download one or all workshop contents"
28
42
 
29
- def status
30
- # ...
43
+ def download(content)
44
+ client.download(content)
31
45
  end
32
46
 
33
- desc "support", "Create a new support ticket, report a bug or issue"
47
+ desc "show [CONTENT]", "Show any workshop lesson"
34
48
 
35
- def support
49
+ def show(content)
50
+ client.show(content)
36
51
  end
37
52
 
38
- desc "show CONTENT", "Show any workshop section (use list to see all section names)"
53
+ private
39
54
 
40
- def show(content)
55
+ def client
56
+ @client ||= RPW::Client.new
41
57
  end
58
+ end
42
59
 
43
- desc "list", "Show all available workshop content"
60
+ class Progress < SubCommandBase
61
+ desc "set [LESSON]", "Set current lesson to a particular lesson"
44
62
 
45
- def list
63
+ def set(lesson)
64
+ client.set_progress(lesson)
46
65
  end
47
66
 
48
- desc "download [CONTENT | all]", "Download one or all workshop contents"
67
+ desc "reset", "Erase all progress and start over"
49
68
 
50
- def download(content)
69
+ def reset
70
+ yes? "Are you sure you want to reset your progress? (Y/N)"
71
+ client.reset_progress
72
+ end
73
+
74
+ desc "show", "Show current workshop progress"
75
+ def show
76
+ data = client.progress
77
+ say "The Rails Performance Workshop"
78
+ say "You have completed #{data[:completed]} out of #{data[:total]} total sections."
79
+ say "Current lesson: #{data[:current_lesson]["title"]}"
80
+ say "Progress by Section (X == completed, O == current):"
81
+ data[:sections].each do |section|
82
+ say "#{section[:title]}: #{section[:progress]}"
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def client
89
+ @client ||= RPW::Client.new
90
+ end
91
+
92
+ default_task :show
93
+ end
94
+
95
+ class CLI < Thor
96
+ desc "lesson [SUBCOMMAND]", "View and download lessons"
97
+ subcommand "lesson", Lesson
98
+ desc "progress [SUBCOMMAND]", "View and set progress"
99
+ subcommand "progress", Progress
100
+
101
+ def self.exit_on_failure?
102
+ true
103
+ end
104
+
105
+ desc "setup", "Set up purchase key and directories"
106
+
107
+ def setup
108
+ return unless yes? "We're going to (idempotently!) create a few files and directories in the current working directory. Is that OK? (Y/N)"
109
+
110
+ say "We'll create a .rpw_key file in the current directory to save your purchase key and course data."
111
+ key = ask("Your Purchase Key: ")
112
+
113
+ client.setup(key)
114
+
115
+ say "Successfully authenticated with the RPW server and saved your key."
116
+ say "We'll also create a few directories for your course data and progress."
117
+
118
+ client.directory_setup
119
+
120
+ say "Setup complete!"
121
+ end
122
+
123
+ desc "support", "Create a new support ticket, report a bug or issue"
124
+
125
+ def support
51
126
  end
52
127
 
53
- private
128
+ private
54
129
 
55
130
  def client
56
- @client ||= RPW::Client.new
131
+ @client ||= RPW::Client.new
57
132
  end
58
133
  end
59
134
  end
data/lib/rpw.rb CHANGED
@@ -1,63 +1,289 @@
1
1
  require "typhoeus"
2
- require "base64"
2
+ require "json"
3
3
 
4
4
  module RPW
5
+ class Error < StandardError; end
6
+
5
7
  class Gateway
6
- attr_accessor :domain
8
+ attr_accessor :domain
7
9
 
8
- def initialize(domain)
10
+ def initialize(domain, key)
9
11
  @domain = domain
12
+ @key = key
10
13
  end
11
14
 
12
15
  class Error < StandardError; end
13
16
 
14
17
  def authenticate_key(key)
15
- request = Typhoeus::Request.new(
16
- domain + "/license",
17
- method: :get,
18
- headers: { Authorization: "Basic #{Base64.encode64(key + ':')}" }
19
- )
20
-
21
- request.on_complete do |response|
22
- if response.success?
23
- true
24
- else
25
- raise Error, "Server responded: #{response.code} #{response.response_body}"
26
- end
18
+ Typhoeus.get(domain + "/license", userpwd: key + ":").success?
19
+ end
20
+
21
+ def get_content_by_position(position)
22
+ response = Typhoeus.get(domain + "/contents/positional?position=#{position}", userpwd: @key + ":")
23
+ if response.success?
24
+ JSON.parse(response.body)
25
+ else
26
+ puts response.inspect
27
+ raise Error, "There was a problem fetching this content."
27
28
  end
29
+ end
28
30
 
29
- request.run
31
+ def list_content
32
+ response = Typhoeus.get(domain + "/contents", userpwd: @key + ":")
33
+ if response.success?
34
+ JSON.parse(response.body)
35
+ else
36
+ puts response.inspect
37
+ raise Error, "There was a problem fetching this content."
38
+ end
30
39
  end
31
40
 
32
- def get_resource(resource)
41
+ def download_content(content, folder:)
42
+ puts "Downloading #{content["title"]}..."
43
+ downloaded_file = File.open("#{folder}/#{content["s3_key"]}", "w")
44
+ request = Typhoeus::Request.new(content["url"])
45
+ request.on_body do |chunk|
46
+ downloaded_file.write(chunk)
47
+ printf(".") if rand(10) == 0 # lol
48
+ end
49
+ request.on_complete { |response| downloaded_file.close }
50
+ request
33
51
  end
34
52
  end
35
53
 
36
54
  class Client
37
- DOTFILE_NAME = ".rpw_key"
38
- RPW_SERVER_DOMAIN = "https://rpw-licensor.speedshop.co"
39
-
40
- class Error < StandardError; end
55
+ RPW_SERVER_DOMAIN = ENV["RPW_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
41
56
 
42
57
  def setup(key)
43
- # authenticate against server
44
58
  gateway.authenticate_key(key)
59
+ keyfile["key"] = key
60
+ end
61
+
62
+ def directory_setup
63
+ ["video", "quiz", "lab", "text", "cgrp"].each do |path|
64
+ FileUtils.mkdir_p(path) unless File.directory?(path)
65
+ end
66
+
67
+ client_data["completed"] = [] # just to write the file
68
+
69
+ unless File.exist?(".gitignore") && File.read(".gitignore").match(/rpw_key/)
70
+ File.open(".gitignore", "a") do |f|
71
+ f.puts "\n"
72
+ f.puts ".rpw_key\n"
73
+ f.puts ".rpw_info\n"
74
+ f.puts "video\n"
75
+ f.puts "quiz\n"
76
+ f.puts "lab\n"
77
+ f.puts "text\n"
78
+ f.puts "cgrp\n"
79
+ end
80
+ end
81
+ end
82
+
83
+ def next
84
+ content = next_content
85
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
86
+ gateway.download_content(content, folder: content["style"]).run
87
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
88
+ end
89
+ client_data["current_lesson"] = content["position"]
90
+ display_content(content)
91
+ end
92
+
93
+ def complete
94
+ client_data["completed"] ||= []
95
+ client_data["completed"] += [client_data["current_lesson"]]
96
+ end
97
+
98
+ def list
99
+ gateway.list_content
100
+ end
101
+
102
+ def show(content_pos)
103
+ content = gateway.get_content_by_position(content_pos)
104
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
105
+ gateway.download_content(content, folder: content["style"]).run
106
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
107
+ end
108
+ client_data["current_lesson"] = content["position"]
109
+ display_content(content)
110
+ end
111
+
112
+ def download(content_pos)
113
+ if content_pos.downcase == "all"
114
+ to_download = gateway.list_content
115
+ hydra = Typhoeus::Hydra.new(max_concurrency: 5)
116
+ to_download.each do |content|
117
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
118
+ hydra.queue gateway.download_content(content, folder: content["style"])
119
+ end
120
+ end
121
+ hydra.run
122
+ to_download.each { |content| extract_content(content) if content["s3_key"].end_with?(".tar.gz") }
123
+ else
124
+ content = gateway.get_content_by_position(content_pos)
125
+ unless File.exist?(content["style"] + "/" + content["s3_key"])
126
+ gateway.download_content(content, folder: content["style"]).run
127
+ extract_content(content) if content["s3_key"].end_with?(".tar.gz")
128
+ end
129
+ end
130
+ end
131
+
132
+ def progress
133
+ contents = gateway.list_content
134
+ {
135
+ completed: client_data["completed"].size,
136
+ total: contents.size,
137
+ current_lesson: contents.find { |c| c["position"] == client_data["current_lesson"] },
138
+ sections: chart_section_progress(contents)
139
+ }
140
+ end
141
+
142
+ def set_progress(lesson)
143
+ client_data["current_lesson"] = lesson.to_i
144
+ end
145
+
146
+ def reset_progress
147
+ client_data["current_lesson"] = 0
148
+ client_data["completed"] = []
149
+ end
150
+
151
+ private
45
152
 
46
- # write authenticated key
153
+ def chart_section_progress(contents)
154
+ contents.group_by { |c| c["position"] / 100 }
155
+ .each_with_object([]) do |(_, c), memo|
156
+ completed_str = c.map { |l|
157
+ if l["position"] == client_data["current_lesson"]
158
+ "O"
159
+ elsif client_data["completed"].include?(l["position"])
160
+ "X"
161
+ else
162
+ "."
163
+ end
164
+ }.join
165
+ memo << {
166
+ title: c[0]["title"],
167
+ progress: completed_str
168
+ }
169
+ end
170
+ end
171
+
172
+ def next_content
173
+ contents = gateway.list_content
174
+ return contents.first unless client_data["completed"]
175
+ contents.delete_if { |c| client_data["completed"].include? c["position"] }
176
+ contents.min_by { |c| c["position"] }
177
+ end
178
+
179
+ def client_data
180
+ @client_data ||= ClientData.new
181
+ end
182
+
183
+ def keyfile
184
+ @keyfile ||= Keyfile.new
185
+ end
186
+
187
+ def gateway
188
+ @gateway ||= Gateway.new(RPW_SERVER_DOMAIN, keyfile["key"])
189
+ end
190
+
191
+ def extract_content(content)
192
+ folder = content["style"]
193
+ `tar -C #{folder} -xvzf #{folder}/#{content["s3_key"]}`
194
+ end
195
+
196
+ def display_content(content)
197
+ case content["style"]
198
+ when "video"
199
+ puts "Opening video: #{content["title"]}"
200
+ exec("open video/#{content["s3_key"]}")
201
+ when "quiz"
202
+ Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
203
+ when "lab"
204
+ # extract and rm archive
205
+ puts "Lab downloaded to lab/#{content["s3_key"]}, navigate there and look at the README to continue"
206
+ when "text"
207
+ puts "Opening in your editor: #{content["title"]}"
208
+ exec("$EDITOR text/#{content["s3_key"]}")
209
+ when "cgrp"
210
+ puts "The Complete Guide to Rails Performance has been downloaded and extracted to the cgrp directory."
211
+ puts "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
212
+ end
213
+ end
214
+ end
215
+
216
+ require "fileutils"
217
+ require "yaml"
218
+
219
+ class ClientData
220
+ DOTFILE_NAME = ".rpw_info"
221
+
222
+ def [](key)
223
+ make_sure_dotfile_exists
224
+ data[key]
225
+ end
226
+
227
+ def []=(key, value)
228
+ data[key] = value
229
+ begin
230
+ File.open(self.class::DOTFILE_NAME, "w") { |f| f.write(YAML.dump(data)) }
231
+ rescue
232
+ raise Error, "The RPW data file in this directory is not writable. \
233
+ Check your file permissions."
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def data
240
+ @data ||= begin
241
+ yaml = begin
242
+ YAML.safe_load(File.read(self.class::DOTFILE_NAME))
243
+ rescue
244
+ nil
245
+ end
246
+ yaml || {}
247
+ end
248
+ end
249
+
250
+ def make_sure_dotfile_exists
251
+ return true if File.exist?(self.class::DOTFILE_NAME)
47
252
  begin
48
- File.open(DOTFILE_NAME, "w") { |f| f.write(key) }
253
+ FileUtils.touch(self.class::DOTFILE_NAME)
49
254
  rescue
50
- raise Error.new "Could not create dotfile in this directory \
51
- to save your key. Check your file permissions."
255
+ raise Error, "Could not create the RPW data file in this directory \
256
+ Check your file permissions."
52
257
  end
258
+ end
259
+ end
260
+
261
+ class Keyfile < ClientData
262
+ DOTFILE_NAME = ".rpw_key"
263
+ end
264
+
265
+ require "digest"
53
266
 
54
- key
267
+ class Quiz < Thor
268
+ desc "give_quiz FILENAME", ""
269
+ def give_quiz(filename)
270
+ @quiz_data = YAML.safe_load(File.read(filename))
271
+ @quiz_data["questions"].each { |q| question(q) }
55
272
  end
56
273
 
57
274
  private
58
275
 
59
- def gateway
60
- @gateway ||= Gateway.new(RPW_SERVER_DOMAIN)
276
+ def question(data)
277
+ puts data["prompt"]
278
+ data["answer_choices"].each { |ac| puts ac }
279
+ provided_answer = ask("Your answer?")
280
+ answer_digest = Digest::MD5.hexdigest(data["prompt"] + provided_answer)
281
+ if answer_digest == data["answer_digest"]
282
+ say "Correct!"
283
+ else
284
+ say "Incorrect."
285
+ say "I encourage you to try reviewing the material to see what the correct answer is."
286
+ end
61
287
  end
62
288
  end
63
289
  end
@@ -1,3 +1,3 @@
1
- module RPW
2
- VERSION = "0.0.1"
3
- end
1
+ module RPW
2
+ VERSION = "0.0.2"
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rpw
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Berkopec
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-05 00:00:00.000000000 Z
11
+ date: 2020-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor