skp 0.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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +109 -0
- data/.gitignore +9 -0
- data/.ruby_version +1 -0
- data/CODE_OF_CONDUCT.md +78 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +63 -0
- data/HISTORY.md +3 -0
- data/LICENSE +674 -0
- data/README.md +4 -0
- data/Rakefile +11 -0
- data/exe/skp +5 -0
- data/lib/skp/README.md +60 -0
- data/lib/skp/cli/bannerlord.rb +53 -0
- data/lib/skp/cli/key.rb +17 -0
- data/lib/skp/cli/quiz.rb +28 -0
- data/lib/skp/cli/sub_command_base.rb +18 -0
- data/lib/skp/cli.rb +269 -0
- data/lib/skp/client.rb +158 -0
- data/lib/skp/client_data.rb +73 -0
- data/lib/skp/gateway.rb +57 -0
- data/lib/skp/version.rb +3 -0
- data/lib/skp.rb +8 -0
- data/skp.gemspec +31 -0
- metadata +126 -0
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "rake/testtask"
|
2
|
+
require "rubygems/package_task"
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "standard/rake"
|
5
|
+
|
6
|
+
gemspec = Gem::Specification.load("skp.gemspec")
|
7
|
+
Gem::PackageTask.new(gemspec).define
|
8
|
+
|
9
|
+
Rake::TestTask.new(:test)
|
10
|
+
|
11
|
+
task default: [:standard, :test]
|
data/exe/skp
ADDED
data/lib/skp/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
## Installation Requirements
|
2
|
+
|
3
|
+
This client assumes you're using Ruby 2.6 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 (requires HEVC/h265 support)
|
11
|
+
|
12
|
+
## Slack Invite
|
13
|
+
|
14
|
+
The Slack channel is your best resource for questions about Ruby performance
|
15
|
+
or other material in the workshop. Nate is almost always monitoring that channel.
|
16
|
+
|
17
|
+
If you encounter a **bug or other software problem**, please email support@speedshop.co.
|
18
|
+
|
19
|
+
If you purchased the Workshop yourself, you will receive a Slack channel invitation
|
20
|
+
shortly. If you are attending the Workshop as part of a group and your license key
|
21
|
+
was provided to you, you need to register your key to get an invite:
|
22
|
+
|
23
|
+
```
|
24
|
+
$ skp key register [YOUR_EMAIL_ADDRESS]
|
25
|
+
```
|
26
|
+
|
27
|
+
Please note you can only register your key once.
|
28
|
+
|
29
|
+
## Important Commands
|
30
|
+
|
31
|
+
Here are some important commands for you to know:
|
32
|
+
|
33
|
+
```
|
34
|
+
$ skp next | Proceed to the next part of the workshop.
|
35
|
+
$ skp complete | Mark current lesson as complete.
|
36
|
+
$ skp list | List all workshop lessons. Shows progress.
|
37
|
+
$ skp download | Download all lessons. Useful for offline access.
|
38
|
+
$ skp show | Show any particular workshop lesson.
|
39
|
+
$ skp current | Opens the current lesson.
|
40
|
+
```
|
41
|
+
|
42
|
+
Generally, you'll just be doing a lot of `$ skp next`! It's the same thing as `$ skp complete && skp show`.
|
43
|
+
|
44
|
+
#### --no-open
|
45
|
+
|
46
|
+
By default, `$ skp next` (and `$ skp show` and `$ skp current`) will try to open the content it downloads. If you
|
47
|
+
either don't like this, or for some reason it doesn't work, use `$ skp next --no-open`.
|
48
|
+
|
49
|
+
## Working Offline
|
50
|
+
|
51
|
+
By default, the course will download each piece of content as you progress through
|
52
|
+
the course. However, you can use `skp download` to download all content
|
53
|
+
at once, and complete the workshop entirely offline.
|
54
|
+
|
55
|
+
Videos in this workshop are generally about 100MB each, which means the entire
|
56
|
+
course is about a 3 to 4GB download.
|
57
|
+
|
58
|
+
## Bugs and Support
|
59
|
+
|
60
|
+
If you encounter any problems, please email support@speedshop.co for the fastest possible response.
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module SKP
|
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
|
+
,---.o | | o o
|
25
|
+
`---..,---|,---.|__/ .,---. .,---.
|
26
|
+
||| ||---'| \ || | || |
|
27
|
+
`---'``---'`---'` ```---| `` '
|
28
|
+
|
|
29
|
+
,---. | o
|
30
|
+
|---',---.,---.,---.|--- .,---.,---.
|
31
|
+
| | ,---|| | || |---'
|
32
|
+
` ` `---^`---'`---'``---'`---'
|
33
|
+
#{reset})
|
34
|
+
end
|
35
|
+
|
36
|
+
def banner
|
37
|
+
%(
|
38
|
+
+hmNMMMMMm/` -ymMMNh/
|
39
|
+
sMMMMMMMMMy +MMMMMMMMy ,---.o | | o o
|
40
|
+
yMMMMMMMMMMy` yMMMMMMMMN `---..,---|,---.|__/ .,---. .,---.
|
41
|
+
`dMMMMMMMMMMm:-dMMMMMMm: ||| ||---'| \ || | || |
|
42
|
+
`sNMMMMMMMMMMs.:+sso:` `---'``---'`---'` ```---| `` '
|
43
|
+
:dMMMMMMMMMMm/ |
|
44
|
+
:oss+:.sNMMMMMMMMMMy` ,---. | o
|
45
|
+
/mMMMMMMd-:mMMMMMMMMMMd. |---',---.,---.,---.|--- .,---.,---.
|
46
|
+
NMMMMMMMMy `hMMMMMMMMMMh | | ,---|| | || |---'
|
47
|
+
yMMMMMMMM+ `dMMMMMMMMMy ` ` `---^`---'`---'``---'`---'
|
48
|
+
/hNMMmy- `/mMMMMMNmy/
|
49
|
+
#{reset})
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/skp/cli/key.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module SKP
|
2
|
+
class Key < SubCommandBase
|
3
|
+
desc "register [EMAIL_ADDRESS]", "Change email registered with Speedshop. One-time only."
|
4
|
+
def register(email)
|
5
|
+
unless client.setup?
|
6
|
+
say "You have not yet set up the client. Run $ skp start"
|
7
|
+
exit(1)
|
8
|
+
end
|
9
|
+
if client.register_email(email)
|
10
|
+
say "Key registered with #{email}. You should receive a Slack invite soon."
|
11
|
+
else
|
12
|
+
say "Key has already been registered. If you believe this is in error,"\
|
13
|
+
" please email support@speedshop.co"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/skp/cli/quiz.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "thor"
|
3
|
+
|
4
|
+
module SKP
|
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,18 @@
|
|
1
|
+
module SKP
|
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 ||= SKP::Client.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/skp/cli.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "thor/hollaback"
|
3
|
+
require "skp"
|
4
|
+
require "skp/cli/bannerlord"
|
5
|
+
require "skp/cli/sub_command_base"
|
6
|
+
require "skp/cli/key"
|
7
|
+
require "cli/ui"
|
8
|
+
|
9
|
+
CLI::UI::StdoutRouter.enable
|
10
|
+
|
11
|
+
module SKP
|
12
|
+
class CLI < Thor
|
13
|
+
class_before :check_version
|
14
|
+
|
15
|
+
desc "key register [EMAIL_ADDRESS]", "Change email registered w/Speedshop"
|
16
|
+
subcommand "key", Key
|
17
|
+
|
18
|
+
def self.exit_on_failure?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "start", "Tutorial and onboarding"
|
23
|
+
def start
|
24
|
+
warn_if_already_started
|
25
|
+
|
26
|
+
print_banner
|
27
|
+
say "\u{1F48E} Welcome to Sidekiq in Practice. \u{1F48E}"
|
28
|
+
say ""
|
29
|
+
say "This is skp, the command line client for this workshop."
|
30
|
+
say ""
|
31
|
+
say "This client will download files from the internet into the current"
|
32
|
+
say "working directory, so it's best to run this client from a new directory"
|
33
|
+
say "that you'll use as your 'scratch space' for working on the Workshop."
|
34
|
+
say ""
|
35
|
+
|
36
|
+
ans = ::CLI::UI.confirm "Create files and folders in this directory? (no will quit)"
|
37
|
+
|
38
|
+
exit(1) unless ans
|
39
|
+
|
40
|
+
say ""
|
41
|
+
|
42
|
+
ans = ::CLI::UI::Prompt.ask("Where should we save your course progress?",
|
43
|
+
options: [
|
44
|
+
"here",
|
45
|
+
"my home directory (~/.skp)"
|
46
|
+
])
|
47
|
+
|
48
|
+
client.directory_setup((ans == "my home directory (~/.skp)"))
|
49
|
+
|
50
|
+
key = ::CLI::UI::Prompt.ask("Your Purchase Key: ")
|
51
|
+
|
52
|
+
unless client.setup(key)
|
53
|
+
say "That is not a valid key. Please try again."
|
54
|
+
exit(0)
|
55
|
+
end
|
56
|
+
|
57
|
+
say ""
|
58
|
+
say "Successfully authenticated with the SKP server and saved your key."
|
59
|
+
say ""
|
60
|
+
say "Setup complete!"
|
61
|
+
say ""
|
62
|
+
say "To learn how to use this command-line client, consult ./README.md,"
|
63
|
+
say "which we just created."
|
64
|
+
say ""
|
65
|
+
say "Once you've read that and you're ready to get going: $ skp next"
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "next", "Proceed to the next lesson of the workshop"
|
69
|
+
option :"no-open", type: :boolean
|
70
|
+
def next
|
71
|
+
exit_with_no_key
|
72
|
+
content = client.next
|
73
|
+
|
74
|
+
if content.nil?
|
75
|
+
SKP::CLI.new.print_banner
|
76
|
+
say "Congratulations!"
|
77
|
+
say "You have completed Sidekiq in Practice."
|
78
|
+
exit(0)
|
79
|
+
end
|
80
|
+
|
81
|
+
say "Proceeding to next lesson: #{content["title"]}"
|
82
|
+
client.download_and_extract(content)
|
83
|
+
client.complete(content["position"])
|
84
|
+
display_content(content, !options[:"no-open"])
|
85
|
+
end
|
86
|
+
|
87
|
+
desc "current", "Open the current lesson"
|
88
|
+
option :"no-open", type: :boolean
|
89
|
+
def current
|
90
|
+
exit_with_no_key
|
91
|
+
content = client.current
|
92
|
+
say "Opening: #{content["title"]}"
|
93
|
+
client.download_and_extract(content)
|
94
|
+
display_content(content, !options[:"no-open"])
|
95
|
+
end
|
96
|
+
|
97
|
+
desc "complete", "Mark the current lesson as complete"
|
98
|
+
def complete
|
99
|
+
say "Marked current lesson as complete"
|
100
|
+
client.complete(nil)
|
101
|
+
end
|
102
|
+
|
103
|
+
desc "list", "Show all available workshop lessons"
|
104
|
+
def list
|
105
|
+
::CLI::UI::Frame.open("{{*}} {{bold:All Lessons}}", color: :green)
|
106
|
+
|
107
|
+
frame_open = false
|
108
|
+
client.list.each do |lesson|
|
109
|
+
if lesson["title"].start_with?("Section")
|
110
|
+
::CLI::UI::Frame.close(nil) if frame_open
|
111
|
+
::CLI::UI::Frame.open(lesson["title"])
|
112
|
+
frame_open = true
|
113
|
+
next
|
114
|
+
end
|
115
|
+
|
116
|
+
no_data = client.send(:client_data)["completed"].nil?
|
117
|
+
completed = client.send(:client_data)["completed"]&.include?(lesson["position"])
|
118
|
+
|
119
|
+
str = if no_data
|
120
|
+
""
|
121
|
+
elsif completed
|
122
|
+
"\u{2705} "
|
123
|
+
else
|
124
|
+
"\u{274C} "
|
125
|
+
end
|
126
|
+
|
127
|
+
case lesson["style"]
|
128
|
+
when "video"
|
129
|
+
puts str + ::CLI::UI.fmt("{{red:#{lesson["title"]}}}")
|
130
|
+
when "quiz"
|
131
|
+
# puts ::CLI::UI.fmt "{{green:#{" " + lesson["title"]}}}"
|
132
|
+
when "lab"
|
133
|
+
puts str + ::CLI::UI.fmt("{{yellow:#{" " + lesson["title"]}}}")
|
134
|
+
when "text"
|
135
|
+
puts str + ::CLI::UI.fmt("{{magenta:#{" " + lesson["title"]}}}")
|
136
|
+
else
|
137
|
+
puts str + ::CLI::UI.fmt("{{magenta:#{" " + lesson["title"]}}}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
::CLI::UI::Frame.close(nil)
|
142
|
+
::CLI::UI::Frame.close(nil, color: :green)
|
143
|
+
end
|
144
|
+
|
145
|
+
desc "download", "Download all workshop contents"
|
146
|
+
def download
|
147
|
+
exit_with_no_key
|
148
|
+
total = client.list.size
|
149
|
+
client.list.each do |content|
|
150
|
+
current = client.list.index(content) + 1
|
151
|
+
puts "Downloading #{content["title"]} (#{current}/#{total})"
|
152
|
+
client.download_and_extract(content)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
desc "show", "Show any individal workshop lesson"
|
157
|
+
option :"no-open", type: :boolean
|
158
|
+
option :quizzes, type: :boolean
|
159
|
+
def show
|
160
|
+
exit_with_no_key
|
161
|
+
title = ::CLI::UI::Prompt.ask(
|
162
|
+
"Which lesson would you like to view?",
|
163
|
+
options: client.list.reject { |l| !options[:quizzes] && l["title"] == "Quiz" }.map { |l| " " * l["indent"] + l["title"] }
|
164
|
+
)
|
165
|
+
title.strip!
|
166
|
+
content_order = client.list.find { |l| l["title"] == title }["position"]
|
167
|
+
content = client.show(content_order)
|
168
|
+
client.download_and_extract(content)
|
169
|
+
display_content(content, !options[:"no-open"])
|
170
|
+
end
|
171
|
+
|
172
|
+
desc "set_progress", "Set current lesson to a particular lesson"
|
173
|
+
def set_progress
|
174
|
+
title = ::CLI::UI::Prompt.ask(
|
175
|
+
"Which lesson would you like to set your progress to? All prior lessons will be marked complete",
|
176
|
+
options: client.list.reject { |l| l["title"] == "Quiz" }.map { |l| " " * l["indent"] + l["title"] }
|
177
|
+
)
|
178
|
+
title.strip!
|
179
|
+
content_order = client.list.find { |l| l["title"] == title }["position"]
|
180
|
+
content = client.set_progress(content_order, all_prior: true)
|
181
|
+
say "Setting current progress to #{content.last["title"]}"
|
182
|
+
end
|
183
|
+
|
184
|
+
desc "reset", "Erase all progress and start over"
|
185
|
+
def reset
|
186
|
+
return unless ::CLI::UI.confirm("Are you sure you want to erase all of your progress?", default: false)
|
187
|
+
say "Resetting progress."
|
188
|
+
client.set_progress(nil)
|
189
|
+
end
|
190
|
+
|
191
|
+
no_commands do
|
192
|
+
def print_banner
|
193
|
+
SKP::Bannerlord.print_banner
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def exit_with_no_key
|
200
|
+
unless client.setup?
|
201
|
+
say "You have not yet set up the client. Run $ skp start"
|
202
|
+
exit(1)
|
203
|
+
end
|
204
|
+
unless client.directories_ready?
|
205
|
+
say "You are not in your workshop scratch directory, or you have not yet"
|
206
|
+
say "set up the client. Change directory or run $ skp start"
|
207
|
+
exit(1)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def client
|
212
|
+
@client ||= SKP::Client.new
|
213
|
+
end
|
214
|
+
|
215
|
+
def display_content(content, open_after)
|
216
|
+
openable = false
|
217
|
+
case content["style"]
|
218
|
+
when "video"
|
219
|
+
location = "video/#{content["s3_key"]}"
|
220
|
+
openable = true
|
221
|
+
when "quiz"
|
222
|
+
Quiz.start(["give_quiz", "quiz/" + content["s3_key"]])
|
223
|
+
when "lab"
|
224
|
+
location = "lab/#{content["s3_key"][0..-8]}"
|
225
|
+
openable = true
|
226
|
+
when "text"
|
227
|
+
location = "text/#{content["s3_key"]}"
|
228
|
+
openable = true
|
229
|
+
when "cgrp"
|
230
|
+
say "The Complete Guide to Rails Performance has been downloaded and extracted to the ./cgrp directory."
|
231
|
+
say "All source code for the CGRP is in the src directory, PDF and other compiled formats are in the release directory."
|
232
|
+
say "You can check it out now, or to continue: $ skp next "
|
233
|
+
end
|
234
|
+
if location
|
235
|
+
if openable && !open_after
|
236
|
+
say "Download complete. Open with: $ #{open_command} #{location}"
|
237
|
+
elsif open_after && openable
|
238
|
+
exec "#{open_command} #{location}"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
require "rbconfig"
|
244
|
+
def open_command
|
245
|
+
host_os = RbConfig::CONFIG["host_os"]
|
246
|
+
case host_os
|
247
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
248
|
+
"start"
|
249
|
+
when /darwin|mac os/
|
250
|
+
"open"
|
251
|
+
else
|
252
|
+
"xdg-open"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def warn_if_already_started
|
257
|
+
return unless client.setup?
|
258
|
+
exit(0) unless ::CLI::UI.confirm "You have already started the workshop. Continuing "\
|
259
|
+
"this command will wipe all of your current progress. Continue?", default: false
|
260
|
+
end
|
261
|
+
|
262
|
+
def check_version
|
263
|
+
unless client.latest_version?
|
264
|
+
say "WARNING: You are running an old version of skp."
|
265
|
+
say "WARNING: Please run `$ gem install skp`"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
data/lib/skp/client.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "skp/cli/quiz"
|
3
|
+
|
4
|
+
module SKP
|
5
|
+
class Client
|
6
|
+
SKP_SERVER_DOMAIN = ENV["SKP_SERVER_DOMAIN"] || "https://rpw-licensor.speedshop.co"
|
7
|
+
attr_reader :gateway
|
8
|
+
|
9
|
+
def initialize(gateway = nil)
|
10
|
+
@gateway = gateway || Gateway.new(SKP_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 current
|
29
|
+
return list.first unless client_data["completed"]
|
30
|
+
list.sort_by { |c| c["position"] }.find { |c| c["position"] == current_position }
|
31
|
+
end
|
32
|
+
|
33
|
+
def list
|
34
|
+
@list ||= begin
|
35
|
+
if client_data["content_cache_generated"] &&
|
36
|
+
client_data["content_cache_generated"] >= Time.now - 60 * 60
|
37
|
+
|
38
|
+
client_data["content_cache"]
|
39
|
+
else
|
40
|
+
begin
|
41
|
+
client_data["content_cache"] = gateway.list_content
|
42
|
+
client_data["content_cache_generated"] = Time.now
|
43
|
+
client_data["content_cache"]
|
44
|
+
rescue
|
45
|
+
client_data["content_cache"] || (raise Error.new("No internet connection"))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def show(content_pos)
|
52
|
+
list.find { |l| l["position"] == content_pos }
|
53
|
+
end
|
54
|
+
|
55
|
+
def directory_setup(home_dir_ok = true)
|
56
|
+
["video", "quiz", "lab", "text", "cgrp"].each do |path|
|
57
|
+
FileUtils.mkdir_p(path) unless File.directory?(path)
|
58
|
+
end
|
59
|
+
|
60
|
+
if home_dir_ok
|
61
|
+
ClientData.create_in_home!
|
62
|
+
else
|
63
|
+
ClientData.create_in_pwd!
|
64
|
+
end
|
65
|
+
|
66
|
+
unless File.exist?(".gitignore") && File.read(".gitignore").match(/skp_info/)
|
67
|
+
File.open(".gitignore", "a") do |f|
|
68
|
+
f.puts "\n"
|
69
|
+
f.puts ".skp_info\n"
|
70
|
+
f.puts "video\n"
|
71
|
+
f.puts "quiz\n"
|
72
|
+
f.puts "lab\n"
|
73
|
+
f.puts "text\n"
|
74
|
+
f.puts "cgrp\n"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
File.open("README.md", "w+") do |f|
|
79
|
+
f.puts File.read(File.join(File.dirname(__FILE__), "README.md"))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_progress(pos, all_prior: false)
|
84
|
+
client_data["completed"] = [] && return if pos.nil?
|
85
|
+
if all_prior
|
86
|
+
lessons = list.select { |l| l["position"] <= pos }
|
87
|
+
client_data["completed"] = lessons.map { |l| l["position"] }
|
88
|
+
lessons
|
89
|
+
else
|
90
|
+
lesson = list.find { |l| l["position"] == pos }
|
91
|
+
client_data["completed"] += [pos]
|
92
|
+
lesson
|
93
|
+
end
|
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 client_data
|
150
|
+
@client_data ||= ClientData.new
|
151
|
+
end
|
152
|
+
|
153
|
+
def extract_content(content)
|
154
|
+
folder = content["style"]
|
155
|
+
`tar -C #{folder} -xzf #{folder}/#{content["s3_key"]}`
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
module SKP
|
5
|
+
class ClientData
|
6
|
+
DOTFILE_NAME = ".skp_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 SKP 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("~/.skp/"))
|
34
|
+
FileUtils.mkdir(File.expand_path("~/.skp/"))
|
35
|
+
end
|
36
|
+
|
37
|
+
FileUtils.touch(File.expand_path("~/.skp/" + 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("~/.skp/" + 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
|