tmc-client 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.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ .DS_Store?
3
+ *.gem
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ script: bundle exec rspec specs
6
+ notifications:
7
+ irc: "irc.cc.hut.fi#tmcee"
8
+ email: false
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+ gem 'json'
3
+ gem 'faraday'
4
+ gem 'pry'
5
+ gem 'fileutils'
6
+ gem 'mocha'
7
+ gem 'highline' # brew install imagemagick
8
+ gem 'rake'
9
+ gem 'rspec'
data/Gemfile.lock ADDED
@@ -0,0 +1,44 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ coderay (1.0.9)
5
+ diff-lcs (1.1.3)
6
+ faraday (0.8.7)
7
+ multipart-post (~> 1.1)
8
+ fileutils (0.7)
9
+ rmagick (>= 2.13.1)
10
+ highline (1.6.18)
11
+ json (1.7.7)
12
+ metaclass (0.0.1)
13
+ method_source (0.8.1)
14
+ mocha (0.13.3)
15
+ metaclass (~> 0.0.1)
16
+ multipart-post (1.2.0)
17
+ pry (0.9.12)
18
+ coderay (~> 1.0.5)
19
+ method_source (~> 0.8)
20
+ slop (~> 3.4)
21
+ rake (10.0.4)
22
+ rmagick (2.13.2)
23
+ rspec (2.12.0)
24
+ rspec-core (~> 2.12.0)
25
+ rspec-expectations (~> 2.12.0)
26
+ rspec-mocks (~> 2.12.0)
27
+ rspec-core (2.12.2)
28
+ rspec-expectations (2.12.1)
29
+ diff-lcs (~> 1.1.3)
30
+ rspec-mocks (2.12.2)
31
+ slop (3.4.4)
32
+
33
+ PLATFORMS
34
+ ruby
35
+
36
+ DEPENDENCIES
37
+ faraday
38
+ fileutils
39
+ highline
40
+ json
41
+ mocha
42
+ pry
43
+ rake
44
+ rspec
data/Readme.md ADDED
File without changes
data/bin/tmc-client ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'tmc-client/client'
5
+
6
+ @c = Client.new
7
+
8
+ commands = {
9
+ list: :list,
10
+ download: :download,
11
+ submit: :submit_exercise,
12
+ update: :update_exercise,
13
+ auth: :auth,
14
+ check: :check,
15
+ init: :init_course,
16
+ status: :status,
17
+ solution: :solution,
18
+ get: :get,
19
+ set: :set
20
+ }
21
+
22
+ command = ARGV[0].to_s
23
+ sub_arguments = ARGV.drop 1
24
+
25
+ unless commands[command.to_sym].nil?
26
+ @c.send(commands[command.to_sym], *sub_arguments) #unless command
27
+ else
28
+ puts "Unknown command"
29
+ end
30
+
31
+
32
+ rescue LoadError => e
33
+ $stderr.puts <<-EOS
34
+ #{'*'*50}
35
+ puts "An error occurred. Make sure the command was triggered in the right directory. Also make sure you have a valid server address and authentication information. If these do not help, please contact an administrator."
36
+ #{'*'*50}
37
+ EOS
38
+
39
+ raise e
40
+
41
+ exit(1)
42
+ end
@@ -0,0 +1,386 @@
1
+ require 'rubygems'
2
+ require 'highline/import'
3
+
4
+ require 'json'
5
+ require 'faraday'
6
+ require 'yaml'
7
+ require 'pry'
8
+ require 'fileutils'
9
+ require 'tempfile'
10
+ require 'pp'
11
+ require_relative 'my_config'
12
+
13
+ class Client
14
+ attr_accessor :courses, :config, :conn, :output, :input
15
+ def initialize(output=$stdout, input=$stdin)
16
+ @config = MyConfig.new
17
+ @output = output
18
+ @input = input
19
+ begin
20
+ @config.server_url ||= request_server_url
21
+ rescue
22
+ request_server_url
23
+ end
24
+ init_connection()
25
+ if @config.auth
26
+ begin
27
+ @courses = JSON.parse get_courses_json
28
+ rescue => e
29
+ auth
30
+ end
31
+ else
32
+ output.puts "No username/password. run tmc auth"
33
+ end
34
+ end
35
+
36
+ # stupid name - this should create connection, but not fetch courses.json!
37
+ def init_connection()
38
+ @conn = Faraday.new(:url => @config.server_url) do |faraday|
39
+ faraday.request :multipart
40
+ faraday.request :url_encoded # form-encode POST params
41
+ #faraday.response :logger # log requests to STDOUT We dont want to do this in production!
42
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
43
+ end
44
+
45
+ if @config.auth
46
+ @conn.headers[Faraday::Request::Authorization::KEY] = @config.auth
47
+ else
48
+ auth
49
+ @conn.headers[Faraday::Request::Authorization::KEY] = @config.auth
50
+ end
51
+
52
+ end
53
+
54
+ def get(*args)
55
+ target = args.first
56
+ case target
57
+ when 'url' then output.puts @config.server_url
58
+ else output.puts "No property selected"
59
+ end
60
+ end
61
+
62
+ def set(*args)
63
+ target = args.first
64
+ case target
65
+ when "url"
66
+ @config.server_url = args[1] if args.count >= 2
67
+ else output.puts "No property selected"
68
+ end
69
+ end
70
+
71
+ def check
72
+ output.puts get_courses_json
73
+ end
74
+
75
+ def get_courses_json
76
+ data = @conn.get('courses.json', {api_version: 5}).body
77
+ raise "Error with autentikation" if data['error']
78
+ data
79
+ end
80
+
81
+ # Path can be relative or absolute
82
+ def is_universal_project?(path)
83
+ File.exists? File.join(path, ".universal")
84
+ end
85
+
86
+ def get_password(prompt="Enter Password")
87
+ ask(prompt) {|q| q.echo = false}
88
+ end
89
+
90
+ def auth
91
+ output.print "Username: "
92
+ username = @input.gets.chomp.strip
93
+ password = get_password("Password (typing is hidden): ")
94
+ @config.auth = nil
95
+ @conn.basic_auth(username, password)
96
+ @config.auth = @conn.headers[Faraday::Request::Authorization::KEY]
97
+ end
98
+
99
+ def request_server_url
100
+ output.print "Server url: "
101
+ @config.server_url = @input.gets.chomp.strip
102
+ end
103
+
104
+ def get_real_name(headers)
105
+ name = headers['content-disposition']
106
+ name.split("\"").compact[-1]
107
+ end
108
+
109
+ def fetch_zip(zip_url)
110
+ @conn.get(zip_url)
111
+ end
112
+
113
+ def get_course_name
114
+ get_my_course['name']
115
+ end
116
+
117
+ def list(mode=:courses)
118
+ mode = mode.to_sym
119
+ case mode
120
+ when :courses
121
+ list_courses
122
+ when :exercises
123
+ list_exercises
124
+ else
125
+ end
126
+ end
127
+
128
+ def list_exercises(course_dir_name=nil)
129
+ course_dir_name = current_directory_name if course_dir_name.nil?
130
+ course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
131
+ raise "Invalid course name" if course.nil?
132
+ course["exercises"].each do |ex|
133
+ output.puts "#{ex['name']} #{ex['deadline']}" if ex["returnable"]
134
+ end
135
+ end
136
+
137
+ def list_courses
138
+ output.puts "No courses available. Make sure your server url is correct and that you have authenticated." if @courses.nil? or @courses["courses"].nil?
139
+ @courses['courses'].each do |course|
140
+ output.puts "#{course['name']}"
141
+ end
142
+ end
143
+
144
+ def current_directory_name
145
+ File.basename(Dir.getwd)
146
+ end
147
+
148
+ def previous_directory_name
149
+ path = Dir.getwd
150
+ directories = path.split("/")
151
+ directories[directories.count - 2]
152
+ end
153
+
154
+ def init_course(course_name)
155
+ FileUtils.mkdir_p(course_name)
156
+ output.print "Would you like to download all available exercises? Yn"
157
+ if ["", "y", "Y"].include? @input.gets.strip.chomp
158
+ Dir.chdir(course_name) do
159
+ course = @courses['courses'].select { |course| course['name'] == course_name }.first
160
+ download_new_exercises
161
+ end
162
+ end
163
+ end
164
+
165
+ def download(*args)
166
+ if args.include? "all" or args.include? "-a" or args.include? "--all"
167
+ download_new_exercises
168
+ else
169
+ download_new_exercise(*args)
170
+ end
171
+ end
172
+
173
+ def solution(exercise_dir_name=nil)
174
+ # Initialize course and exercise names to identify exercise to submit (from json)
175
+ if exercise_dir_name.nil?
176
+ exercise_dir_name = current_directory_name
177
+ course_dir_name = previous_directory_name
178
+ else
179
+ course_dir_name = current_directory_name
180
+ end
181
+
182
+ exercise_dir_name.chomp("/")
183
+ # Find course and exercise ids
184
+ course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
185
+ raise "Invalid course name" if course.nil?
186
+ exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
187
+ raise "Invalid exercise name" if exercise.nil?
188
+
189
+ update_from_zip(exercise['solution_zip_url'], exercise_dir_name, course_dir_name, exercise, course)
190
+ end
191
+
192
+ def update_from_zip(zip_url, exercise_dir_name, course_dir_name, exercise, course)
193
+ zip = fetch_zip(zip_url)
194
+ output.puts "URL: #{zip_url}"
195
+ work_dir = Dir.pwd
196
+ to_dir = if Dir.pwd.chomp("/").split("/").last == exercise_dir_name
197
+ work_dir
198
+ else
199
+ File.join(work_dir, exercise_dir_name)
200
+ end
201
+ Dir.mktmpdir do |tmpdir|
202
+ Dir.chdir tmpdir do
203
+ File.open("tmp.zip", 'wb') {|file| file.write(zip.body)}
204
+ `unzip -n tmp.zip && rm tmp.zip`
205
+ files = Dir.glob('**/*')
206
+ all_selected = false
207
+ files.each do |file|
208
+ next if file == exercise_dir_name or File.directory? file
209
+ output.puts "Want to update #{file}? Yn[A]" unless all_selected
210
+ input = @input.gets.chomp.strip unless all_selected
211
+ all_selected = true if input == "A" or input == "a"
212
+ if all_selected or (["", "y", "Y"].include? input)
213
+ begin
214
+ to = File.join(to_dir,file.split("/")[1..-1].join("/"))
215
+ output.puts "copying #{file} to #{to}"
216
+ unless File.directory? to
217
+ FileUtils.mkdir_p(to.split("/")[0..-2].join("/"))
218
+ else
219
+ FileUtils.mkdir_p(to)
220
+ end
221
+ FileUtils.cp_r(file, to)
222
+ rescue ArgumentError => e
223
+ output.puts "An error occurred #{e}"
224
+ end
225
+ elsif input == "b"
226
+ binding.pry
227
+ else
228
+ output.puts "Skipping file #{file}"
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ def update_automatically_detected_project_from_zip(zip_url, exercise_dir_name, course_dir_name, exercise, course)
236
+ zip = fetch_zip(exercise['zip_url'])
237
+ work_dir = Dir.pwd
238
+ to_dir = if Dir.pwd.chomp("/").split("/").last == exercise_dir_name
239
+ work_dir
240
+ else
241
+ File.join(work_dir, exercise_dir_name)
242
+ end
243
+ Dir.mktmpdir do |tmpdir|
244
+ Dir.chdir tmpdir do
245
+ File.open("tmp.zip", 'wb') {|file| file.write(zip.body)}
246
+ `unzip -n tmp.zip && rm tmp.zip`
247
+ files = Dir.glob('**/*')
248
+
249
+ files.each do |file|
250
+ next if file == exercise_dir_name or file.to_s.include? "src" or File.directory? file
251
+ begin
252
+ to = File.join(to_dir,file.split("/")[1..-1].join("/"))
253
+ output.puts "copying #{file} to #{to}"
254
+ unless File.directory? to
255
+ FileUtils.mkdir_p(to.split("/")[0..-2].join("/"))
256
+ else
257
+ FileUtils.mkdir_p(to)
258
+ end
259
+ FileUtils.cp_r(file, to)
260
+ rescue ArgumentError => e
261
+ output.puts "An error occurred #{e}"
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ def filter_returnable(exercises)
269
+ exercises.collect { |ex| ex['name'] if ex['returnable'] }.compact
270
+ end
271
+
272
+ def download_new_exercises(course_dir_name=nil)
273
+ course_dir_name = current_directory_name if course_dir_name.nil?
274
+ course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
275
+ raise "Invalid course name" if course.nil?
276
+ filter_returnable(course['exercises']).each do |ex_name|
277
+ begin
278
+ download_new_exercise(ex_name)
279
+ rescue
280
+ end
281
+ end
282
+ end
283
+
284
+ def download_new_exercise(exercise_dir_name=nil)
285
+ # Initialize course and exercise names to identify exercise to submit (from json)
286
+ course_dir_name = current_directory_name
287
+ exercise_dir_name.chomp("/")
288
+ # Find course and exercise ids
289
+ course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
290
+ raise "Invalid course name" if course.nil?
291
+ exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
292
+ raise "Invalid exercise name" if exercise.nil?
293
+
294
+ raise "Exercise already downloaded" if File.exists? exercise['name']
295
+ zip = fetch_zip(exercise['zip_url'])
296
+ File.open("tmp.zip", 'wb') {|file| file.write(zip.body)}
297
+ `unzip -n tmp.zip && rm tmp.zip`
298
+ end
299
+
300
+ # Filepath can be either relative or absolute
301
+ def zip_file_content(filepath)
302
+ `zip -r -q tmp_submit.zip #{filepath}`
303
+ #`zip -r -q - #{filepath}`
304
+ end
305
+
306
+ # Call in exercise root
307
+ # Zipping to stdout zip -r -q - tmc
308
+ def submit_exercise(*args)
309
+ # Initialize course and exercise names to identify exercise to submit (from json)
310
+ if args.count == 0 or args.all? { |arg| arg.start_with? "-" }
311
+ exercise_dir_name = current_directory_name
312
+ course_dir_name = previous_directory_name
313
+ zip_file_content(".")
314
+ else
315
+ exercise_dir_name = args.first
316
+ course_dir_name = current_directory_name
317
+ zip_file_content(exercise_dir_name)
318
+ end
319
+
320
+ exercise_dir_name.chomp("/")
321
+ exercise_id = 0
322
+ # Find course and exercise ids
323
+ course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
324
+ raise "Invalid course name" if course.nil?
325
+ exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
326
+ raise "Invalid exercise name" if exercise.nil?
327
+
328
+ # Submit
329
+ payload={:submission => {}}
330
+ payload[:request_review] = true if args.include? "--request-review" or args.include? "-r" or args.include? "--review"
331
+ payload[:paste] = true if args.include? "--paste" or args.include? "-p" or args.include? "--public"
332
+ payload[:submission][:file] = Faraday::UploadIO.new('tmp_submit.zip', 'application/zip')
333
+ response = @conn.post "/exercises/#{exercise['id']}/submissions.json?api_version=5&client=netbeans_plugin&client_version=1", payload
334
+ submission_url = JSON.parse(response.body)['submission_url']
335
+ puts "Submission url: #{submission_url}"
336
+
337
+ if (args & %w{-q --quiet -s --silent}).empty?
338
+ while status(submission_url) == "processing"
339
+ sleep(1)
340
+ end
341
+ end
342
+
343
+ FileUtils.rm 'tmp_submit.zip'
344
+ payload
345
+ end
346
+
347
+ def status(submission_id_or_url)
348
+ url = (submission_id_or_url.include? "submissions") ? submission_id_or_url : "/submissions/#{submission_id_or_url}.json?api_version=5"
349
+ json = JSON.parse(@conn.get(url).body)
350
+ if json['status'] != 'processing'
351
+ puts "Status: #{json['status']}"
352
+ puts "Points: #{json['points'].inspect}"
353
+ puts "Tests:"
354
+ json['test_cases'].each do |test|
355
+ puts "#{test['name']} : #{(test['successful']) ? 'Ok' : 'Fail'}#{(test['message'].nil?) ? '' : (' : ' + test['message'])}"
356
+ end
357
+ end
358
+ json['status']
359
+ end
360
+
361
+ def update_exercise(exercise_dir_name=nil)
362
+ # Initialize course and exercise names to identify exercise to submit (from json)
363
+ is_universal = false
364
+ if exercise_dir_name.nil?
365
+ exercise_dir_name = current_directory_name
366
+ course_dir_name = previous_directory_name
367
+ is_universal = true if File.exists? ".universal"
368
+ else
369
+ course_dir_name = current_directory_name
370
+ is_universal = true if File.exists?(File.join("#{exercise_dir_name}", ".universal"))
371
+ end
372
+
373
+ exercise_dir_name.chomp("/")
374
+ # Find course and exercise ids
375
+ course = @courses['courses'].select { |course| course['name'] == course_dir_name }.first
376
+ raise "Invalid course name" if course.nil?
377
+ exercise = course["exercises"].select { |ex| ex["name"] == exercise_dir_name }.first
378
+ raise "Invalid exercise name" if exercise.nil?
379
+
380
+ if is_universal
381
+ update_from_zip(exercise['zip_url'], exercise_dir_name, course_dir_name, exercise, course)
382
+ else
383
+ update_automatically_detected_project_from_zip(exercise['zip_url'], exercise_dir_name, course_dir_name, exercise, course)
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,2 @@
1
+ ---
2
+ :server_url: http://0.0.0.0
File without changes
@@ -0,0 +1,56 @@
1
+ class MyConfig
2
+ attr_accessor :config
3
+
4
+ def initialize
5
+ @config = get_config
6
+ end
7
+
8
+ def get_config
9
+ begin
10
+ yaml = YAML::load(File.open(File.join(File.dirname(File.expand_path(__FILE__)), "config.yml")))
11
+ rescue
12
+ raise "Error loading config.yml"
13
+ end
14
+ if yaml then yaml else {} end
15
+ end
16
+
17
+ def save_config
18
+ File.open(File.open(File.join(File.dirname(File.expand_path(__FILE__)), "config.yml")), "w") {|f| f.write(config.to_yaml) }
19
+ end
20
+
21
+ def server_url
22
+ @config[:server_url] unless @config.nil?
23
+ end
24
+
25
+ def server_url=(url)
26
+ @config[:server_url] = url
27
+ save_config
28
+ end
29
+
30
+ def course_id
31
+ @config[:course_id]
32
+ end
33
+
34
+ def course_id=(id)
35
+ @config[:course_id] = id
36
+ end
37
+
38
+ def save_course_path
39
+ @config[:save_course_path]
40
+ end
41
+
42
+ def save_course_path=(path)
43
+ path= "#{path}/" unless path[-1] == "/"
44
+ @config[:save_course_path]=path
45
+ save_config
46
+ end
47
+
48
+ def auth=(auth)
49
+ @config[:auth] = auth
50
+ save_config
51
+ end
52
+
53
+ def auth
54
+ @config[:auth] unless @config.nil?
55
+ end
56
+ end
@@ -0,0 +1,208 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+ require 'faraday'
4
+ require 'yaml'
5
+ require 'pry'
6
+ require 'fileutils'
7
+ require 'tempfile'
8
+ require 'pp'
9
+ require_relative 'my_config'
10
+
11
+ class Client
12
+ attr_accessor :courses, :config, :conn
13
+ def initialize()
14
+ @config = MyConfig.new
15
+ response = get_connection(@config.username,@config.password)
16
+ @courses = JSON.parse response.body
17
+ end
18
+
19
+ # stupid name - this should create connection, but not fetch courses.json!
20
+ def get_connection(username, password)
21
+ @conn = Faraday.new(:url => @config.server_url) do |faraday|
22
+ faraday.request :url_encoded # form-encode POST params
23
+ faraday.response :logger # log requests to STDOUT We dont want to do this in production!
24
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
25
+ end
26
+ @conn.basic_auth(username, password) # )
27
+ @conn.get 'courses.json', {api_version: 5}
28
+ end
29
+
30
+ def get_real_name(headers)
31
+ name = headers['content-disposition']
32
+ name.split("\"").compact[-1]
33
+ end
34
+
35
+ def download_file(path)
36
+ ask_course_path unless @config.save_course_path
37
+ loaded_file = @conn.get(path)
38
+ real_name = get_real_name(loaded_file.headers)
39
+ fname= path.split("/")[-1]
40
+ course_name = get_course_name
41
+ path = "#{config.save_course_path}#{course_name}/"
42
+ begin
43
+ FileUtils.mkdir_p(path) unless File.exists? path
44
+ rescue Errno::EISDIR
45
+ binding.pry
46
+ end
47
+ file_path = "#{path}#{real_name}"
48
+ begin
49
+ File.open(file_path, 'wb') {|file| file.write(loaded_file.body)} #unless File.exists? file_path
50
+ rescue Errno::EISDIR
51
+ binding.pry
52
+ end
53
+ result = `unzip -o #{file_path} -d #{path}`
54
+ FileUtils.rm file_path
55
+ end
56
+
57
+ def fetch_zip(zip_url)
58
+ @conn.get(zip_url)
59
+ end
60
+
61
+ def ask_course_path
62
+ puts "where to download?"
63
+ @config.save_course_path=gets.chomp
64
+ end
65
+
66
+ def get_my_course(course_name=config.course_id)
67
+ list = @courses['courses'].select {|course| course['id'] == config.course_id}
68
+ list[0]
69
+ end
70
+
71
+
72
+ def list(mode=nil)
73
+ if mode.nil?
74
+ puts "What would you like to list? Perhaps exercises with list exercises, or active projects with list active?"
75
+ return
76
+ end
77
+
78
+ send("list_#{mode}")
79
+ end
80
+
81
+ def list_exercises
82
+ list = get_my_course['exercises'].map {|ex| ex['name']}
83
+ print_exercises(list)
84
+ end
85
+
86
+ def print_exercises(hash)
87
+ list = hash.each {|ex| puts ex['name']}
88
+ end
89
+
90
+ def list_active
91
+ list = get_my_course['exercises'].select {|ex| ex['returnable'] == true and ex['completed']==false}
92
+ print_exercises(list)
93
+ end
94
+
95
+ def ask_for_course_id
96
+ list_courses
97
+ @config.course_id= gets.chomp
98
+ end
99
+
100
+ def download_all_available
101
+ list = get_my_course['exercises']
102
+ ask_course_id if list.empty?
103
+ list.each do |ex|
104
+ download_file(ex['zip_url'])
105
+ end
106
+ end
107
+
108
+ # Filepath can be either relative or absolute
109
+ def zip_file_content(filepath)
110
+ `zip -r -q - #{filepath}`
111
+ end
112
+
113
+ # Call in exercise root
114
+ # Zipping to stdout zip -r -q - tmc
115
+ def update_exercise(exercise_dir_name=nil)
116
+ # Initialize course and exercise names to identify exercise to submit (from json)
117
+ if exercise_dir_name.nil?
118
+ exercise_dir_name = get_current_directory_name
119
+ course_dir_name = get_previous_directory_name
120
+ else
121
+ course_dir_name = get_current_directory_name
122
+ end
123
+
124
+ exercise_dir_name.chomp("/")
125
+ exercise_id = 0
126
+ zip_url=""
127
+ # Find course and exercise ids
128
+ @courses["courses"].each do |course|
129
+ if course["name"] == course_dir_name
130
+ course["exercises"].each do |exercise|
131
+ if exercise["name"] == exercise_dir_name
132
+ exercise_id = exercise["id"]
133
+ zip_url = exercise["zip_url"]
134
+ end
135
+ end
136
+ end
137
+ end
138
+ raise "UrlNotFound" if zip_url.empty?
139
+ # and now download the zip file and extract it into a tmpdir.
140
+ # Then copy all files except in src to this dir
141
+ zip = fetch_zip(zip_url)
142
+ Dir.mktmpdir do |dir|
143
+ begin
144
+ File.open("#{dir}/tmp.zip", 'wb') {|file| file.write(zip.body)} #unless File.exists? file_path
145
+ rescue Errno::EISDIR
146
+ binding.pry
147
+ end
148
+ result = `unzip -o #{dir}/tmp.zip -d #{dir}/`
149
+ #now its in #{dir}/#{exercise_dir_name}
150
+ from_dir = "#{dir}/#{exercise_dir_name}/"
151
+ to_dir = "#{exercise_dir_name}/"
152
+ copy_updateable_files(from_dir, to_dir)
153
+ end
154
+ end
155
+
156
+ def copy_updateable_files(from_dir, to_dir)
157
+ to_dir = "." if get_current_directory_name == to_dir.chomp("/")
158
+ do_not_update = %w(src)
159
+ Dir.glob("#{from_dir}**") do |file|
160
+ filename = file.split("/")[-1]
161
+ next if do_not_update.include? filename
162
+ FileUtils.rm_rf("#{to_dir}/file")
163
+ FileUtils.cp_r(file,"#{to_dir}/")
164
+ end
165
+ end
166
+
167
+ # Call in exercise root
168
+ >>>>>>> update-action
169
+ def submit_exercise(exercise_dir_name=nil)
170
+ # Initialize course and exercise names to identify exercise to submit (from json)
171
+ if exercise_dir_name.nil?
172
+ exercise_dir_name = current_directory_name
173
+ course_dir_name = previous_directory_name
174
+ zipped = zip_file_content(".")
175
+ else
176
+ #<<<<<<< HEAD
177
+ # course_dir_name = current_directory_name
178
+ # zipped = zip_file_content(exercise_dir_name)
179
+ #=======
180
+ course_dir_name = get_current_directory_name
181
+ # Zip folder
182
+ `zip -r zipped.zip #{exercise_dir_name}`
183
+ end
184
+
185
+ exercise_dir_name.chomp("/")
186
+ exercise_id = 0
187
+ # Find course and exercise ids
188
+ @courses["courses"].each do |course|
189
+ if course["name"] == course_dir_name
190
+ course["exercises"].each do |exercise|
191
+ if exercise["name"] == exercise_dir_name
192
+ exercise_id = exercise["id"]
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ # Submit
199
+ payload = {:submission => zipped}
200
+ @conn.post do |req|
201
+ req.url "/exercises/#{exercise_id}/submissions"
202
+ req.headers["Content-Type"] = "application/zip"
203
+ req.body = "#{payload}"
204
+ end
205
+ payload
206
+ end
207
+
208
+ end
data/lib/tmc-client.rb ADDED
@@ -0,0 +1,30 @@
1
+ require_relative 'tmc-client/client'
2
+
3
+ @c = Client.new
4
+
5
+ commands = {
6
+ list: :list,
7
+ download: :download,
8
+ submit: :submit_exercise,
9
+ update: :update_exercise,
10
+ auth: :auth,
11
+ check: :check,
12
+ init: :init_course,
13
+ status: :status,
14
+ solution: :solution,
15
+ get: :get,
16
+ set: :set
17
+ }
18
+
19
+ command = ARGV[0].to_s
20
+ sub_arguments = ARGV.drop 1
21
+
22
+ begin
23
+ unless commands[command.to_sym].nil?
24
+ @c.send(commands[command.to_sym], *sub_arguments) #unless command
25
+ else
26
+ puts "Unknown command"
27
+ end
28
+ rescue
29
+ puts "An error occurred. Make sure the command was triggered in the right directory. Also make sure you have a valid server address and authentication information. If these do not help, please contact an administrator."
30
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../lib/tmc-client/client.rb'
2
+ require 'rspec'
3
+ require 'mocha/setup'
4
+
5
+ describe Client do
6
+ subject do
7
+ c = Client.new
8
+ end
9
+
10
+ it "should be able to download a zip" do
11
+ received_zip_data = mock("faraday_object")
12
+ received_zip_data.expects(:body).returns(File.read(File.join(File.dirname(File.expand_path(__FILE__)), "ex.zip")))
13
+ subject.expects(:fetch_zip).returns(received_zip_data)
14
+ current_dir = subject.current_directory_name
15
+ subject.courses = { "courses" => [{ "name" => current_dir, "exercises" => [{ "name" => "ex", "returnable" => true }, {"name" => "old", "returnable" => false}] }] }
16
+
17
+ subject.download_new_exercise("ex")
18
+ file_content = File.read("ex.rb")
19
+ file_content.include?("def method").should == true
20
+ `rm ex.rb`
21
+ end
22
+
23
+ it "should not download unreturnable exercises" do
24
+ filtered = subject.filter_returnable([{ "name" => "true", "returnable" => true}, {"name" => "false", "returnable" => false }])
25
+ filtered.count.should == 1
26
+ filtered.first.should == "true"
27
+ end
28
+ end
data/specs/ex.zip ADDED
Binary file
@@ -0,0 +1,27 @@
1
+ require_relative '../lib/tmc-client/client.rb'
2
+ require 'rspec'
3
+ require 'mocha'
4
+
5
+ describe Client do
6
+ subject do
7
+ Client.new
8
+ end
9
+
10
+ it "should print all course names when listing courses" do
11
+ subject.courses = {"courses" => [{ "name" => "test_course1"}, {"name" => "test_course2" }] }
12
+ output = mock("output")
13
+ output.expects(:puts).with("test_course1")
14
+ output.expects(:puts).with("test_course2")
15
+ subject.output = output
16
+ subject.list_courses
17
+ end
18
+
19
+ it "should print all exercise names when listing exercises of a course" do
20
+ subject.courses = {"courses" => [{ "name" => "test_course1", "exercises" => [{"name" => "test_ex1"}, {"name" => "test_ex2"}]}] }
21
+ output = mock("output")
22
+ output.expects(:puts).with("test_ex1")
23
+ output.expects(:puts).with("test_ex2")
24
+ subject.output = output
25
+ subject.list_exercises("test_course1")
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../lib/tmc-client/client.rb'
2
+ require 'rspec'
3
+ require 'mocha'
4
+
5
+ describe Client do
6
+ subject do
7
+ Client.new
8
+ end
9
+
10
+ its(:current_directory_name) { should == `pwd`.split("/").last.chomp }
11
+ its(:previous_directory_name) { should == `pwd`.split("/")[-2].chomp }
12
+
13
+ it "should be able to produce some zip contents" do
14
+ file = Tempfile.new("tmp")
15
+ file.write("content")
16
+ file.close
17
+ subject.zip_file_content(file.path)
18
+ zip_content = File.read("tmp_submit.zip")
19
+ `rm tmp_submit.zip`
20
+ file.unlink
21
+
22
+ zip_content.should_not be_nil
23
+ zip_content.should_not == ""
24
+ end
25
+ end
Binary file
@@ -0,0 +1,43 @@
1
+ require_relative '../lib/tmc-client/client.rb'
2
+ require 'rspec'
3
+ require 'mocha/setup'
4
+
5
+ describe Client do
6
+ subject do
7
+ c = Client.new
8
+ end
9
+
10
+ after(:each) do
11
+ `rm -r update_universal_ex`
12
+ `rm -r update_ex`
13
+ end
14
+
15
+ it "should download all but contents of src folder with normal projects" do
16
+ received_zip_data = mock("faraday_object")
17
+ received_zip_data.expects(:body).returns(File.read(File.join(File.dirname(File.expand_path(__FILE__)), "update_ex.zip")))
18
+ subject.expects(:fetch_zip).returns(received_zip_data)
19
+ current_dir = subject.current_directory_name
20
+ subject.courses = { "courses" => [{ "name" => current_dir, "exercises" => [{ "name" => "update_ex", "returnable" => true }, {"name" => "old", "returnable" => false}] }] }
21
+
22
+ subject.update_exercise("update_ex")
23
+ File.exists?("update_ex/included.txt").should == true
24
+ File.exists?("update_ex/src/not_included.txt").should == false
25
+ end
26
+
27
+ it "should download all files when universal project" do
28
+ FileUtils.mkdir_p(File.join("update_universal_ex", ".universal"))
29
+
30
+ received_zip_data = mock("faraday_object")
31
+ received_zip_data.expects(:body).returns(File.read(File.join(File.dirname(File.expand_path(__FILE__)), "update_universal_ex.zip")))
32
+ input = mock("input")
33
+ input.expects(:gets).returns("A")
34
+ subject.input = input
35
+ subject.expects(:fetch_zip).returns(received_zip_data)
36
+ current_dir = subject.current_directory_name
37
+ subject.courses = { "courses" => [{ "name" => current_dir, "exercises" => [{ "name" => "update_universal_ex", "returnable" => true }, {"name" => "old", "returnable" => false}] }] }
38
+
39
+ subject.update_exercise("update_universal_ex")
40
+ File.exists?("update_universal_ex/included.txt").should == true
41
+ File.exists?("update_universal_ex/src/included.txt").should == true
42
+ end
43
+ end
Binary file
data/tmc ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ ##!/usr/bin/env ruby
3
+ ruby tmc.rb $*
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'tmc-client'
3
+ s.version = '0.0.1'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.summary = 'TestMyCode Commandline client'
6
+ s.authors = ['Jarmo Isotalo', 'Tony Kovanen']
7
+ s.homepage = "https://github.com/TMCee/tmc-client/"
8
+ s.files = `git ls-files`.split("\n")
9
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
10
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
11
+ s.require_paths = ["lib"]
12
+
13
+ s.add_dependency "json"
14
+ s.add_dependency "faraday"
15
+ s.add_dependency "pry"
16
+ s.add_dependency "fileutils"
17
+ s.add_dependency "mocha"
18
+ s.add_dependency "highline"
19
+ s.add_dependency "rake"
20
+ s.add_dependency "rspec"
21
+
22
+ s.add_development_dependency "rspec", "~> 2.8"
23
+
24
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tmc-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jarmo Isotalo
9
+ - Tony Kovanen
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-05-03 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: faraday
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: pry
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: fileutils
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: mocha
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: highline
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rspec
129
+ requirement: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ! '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ type: :runtime
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: rspec
145
+ requirement: !ruby/object:Gem::Requirement
146
+ none: false
147
+ requirements:
148
+ - - ~>
149
+ - !ruby/object:Gem::Version
150
+ version: '2.8'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ none: false
155
+ requirements:
156
+ - - ~>
157
+ - !ruby/object:Gem::Version
158
+ version: '2.8'
159
+ description:
160
+ email:
161
+ executables:
162
+ - tmc-client
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - .gitignore
167
+ - .travis.yml
168
+ - Gemfile
169
+ - Gemfile.lock
170
+ - Readme.md
171
+ - bin/tmc-client
172
+ - lib/tmc-client.rb
173
+ - lib/tmc-client/client.rb
174
+ - lib/tmc-client/config.default.yml
175
+ - lib/tmc-client/config.yml
176
+ - lib/tmc-client/my_config.rb
177
+ - lib/tmc-client/old-client.rb
178
+ - specs/download_spec.rb
179
+ - specs/ex.zip
180
+ - specs/list_spec.rb
181
+ - specs/submit_spec.rb
182
+ - specs/update_ex.zip
183
+ - specs/update_spec.rb
184
+ - specs/update_universal_ex.zip
185
+ - tmc
186
+ - tmc-client-0.0.1.gem
187
+ - tmc-client.gemspec
188
+ homepage: https://github.com/TMCee/tmc-client/
189
+ licenses: []
190
+ post_install_message:
191
+ rdoc_options: []
192
+ require_paths:
193
+ - lib
194
+ required_ruby_version: !ruby/object:Gem::Requirement
195
+ none: false
196
+ requirements:
197
+ - - ! '>='
198
+ - !ruby/object:Gem::Version
199
+ version: '0'
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubyforge_project:
208
+ rubygems_version: 1.8.23
209
+ signing_key:
210
+ specification_version: 3
211
+ summary: TestMyCode Commandline client
212
+ test_files: []