tmc-client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []