teachers_pet 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +7 -0
  3. data/Guardfile +3 -1
  4. data/README.md +53 -28
  5. data/Rakefile +1 -1
  6. data/bin/teachers_pet +4 -0
  7. data/lib/teachers_pet.rb +4 -1
  8. data/lib/teachers_pet/actions/add_to_team.rb +31 -0
  9. data/lib/teachers_pet/actions/base.rb +36 -111
  10. data/lib/teachers_pet/actions/clone_repos.rb +9 -36
  11. data/lib/teachers_pet/actions/create_repos.rb +16 -39
  12. data/lib/teachers_pet/actions/create_student_teams.rb +35 -0
  13. data/lib/teachers_pet/actions/fork_collab.rb +4 -19
  14. data/lib/teachers_pet/actions/open_issue.rb +17 -32
  15. data/lib/teachers_pet/actions/push_files.rb +8 -22
  16. data/lib/teachers_pet/cli.rb +22 -0
  17. data/lib/teachers_pet/client_decorator.rb +59 -0
  18. data/lib/teachers_pet/commands/add_to_team.rb +12 -0
  19. data/lib/teachers_pet/commands/clone_repos.rb +15 -0
  20. data/lib/teachers_pet/commands/create_repos.rb +15 -0
  21. data/lib/teachers_pet/commands/create_student_teams.rb +13 -0
  22. data/lib/teachers_pet/commands/fork_collab.rb +12 -0
  23. data/lib/teachers_pet/commands/open_issue.rb +18 -0
  24. data/lib/teachers_pet/commands/push_files.rb +15 -0
  25. data/lib/teachers_pet/configuration.rb +0 -13
  26. data/lib/teachers_pet/version.rb +1 -1
  27. data/spec/actions/base_spec.rb +13 -0
  28. data/spec/cli_spec.rb +17 -0
  29. data/spec/commands/add_to_team_spec.rb +43 -0
  30. data/spec/commands/clone_repos_spec.rb +33 -0
  31. data/spec/commands/create_repos_spec.rb +55 -0
  32. data/spec/commands/create_student_teams_spec.rb +90 -0
  33. data/spec/commands/fork_collab_spec.rb +95 -0
  34. data/spec/commands/open_issue_spec.rb +63 -0
  35. data/spec/commands/push_files_spec.rb +33 -0
  36. data/spec/fixtures/empty +0 -0
  37. data/spec/fixtures/teams +1 -0
  38. data/spec/spec_helper.rb +13 -53
  39. data/spec/support/command_helpers.rb +11 -0
  40. data/spec/support/common_helpers.rb +52 -0
  41. data/teachers_pet.gemspec +6 -2
  42. metadata +89 -29
  43. data/bin/clone_repos +0 -6
  44. data/bin/create_repos +0 -6
  45. data/bin/create_teams +0 -6
  46. data/bin/fork_collab +0 -6
  47. data/bin/open_issue +0 -6
  48. data/bin/push_files +0 -6
  49. data/lib/teachers_pet/actions/create_teams.rb +0 -101
  50. data/spec/actions/clone_repos_spec.rb +0 -44
  51. data/spec/actions/create_repos_spec.rb +0 -65
  52. data/spec/actions/create_teams_spec.rb +0 -40
  53. data/spec/actions/open_issue_spec.rb +0 -71
@@ -1,45 +1,27 @@
1
- # Author: Mike Helmick
2
- # Script to create assignment repositories for students under the appropriate organization
3
-
4
- $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
5
-
6
- require 'rubygems'
7
- require 'highline/question'
8
- require 'highline/import'
9
- require 'highline/compatibility'
10
- require 'teachers_pet/actions/base'
11
-
12
1
  module TeachersPet
13
2
  module Actions
14
3
  class CreateRepos < Base
15
4
  def read_info
16
- @repository = ask('What repository name should be created for each student?') { |q| q.validate = /\w+/ }
17
- @organization = ask("What is the organization name?") { |q| q.default = TeachersPet::Configuration.organization }
18
- @student_file = self.get_students_file_path
19
- @instructor_file = self.get_instructors_file_path
20
- @public_repos = confirm('Create repositories as public?', false)
21
- @add_init_files = confirm('Add .gitignore and README.md files? (skip this if you are pushing starter files.)', false)
5
+ @repository = self.repository
6
+ @organization = self.organization
7
+ @public_repos = self.public?
22
8
  end
23
9
 
24
10
  def load_files
25
- @students = read_file(@student_file, 'Students')
26
- @instructors = read_file(@instructor_file, 'Instructors')
11
+ @students = self.read_students_file
27
12
  end
28
13
 
29
14
  def create
30
- pub_private_text = @public_repos ? 'public' : 'private'
31
- confirm("Create #{@students.keys.size} #{pub_private_text} repositories for students and give access to instructors?")
32
-
33
15
  # create a repo for each student
34
16
  self.init_client
35
17
 
36
- org_hash = read_organization(@organization)
18
+ org_hash = self.client.organization(@organization)
37
19
  abort('Organization could not be found') if org_hash.nil?
38
20
  puts "Found organization at: #{org_hash[:login]}"
39
21
 
40
22
  # Load the teams - there should be one team per student.
41
23
  # Repositories are given permissions by teams
42
- org_teams = get_teams_by_name(@organization)
24
+ org_teams = self.client.get_teams_by_name(@organization)
43
25
  # For each student - create a repository, and give permissions to their "team"
44
26
  # The repository name is teamName-repository
45
27
  puts "\nCreating assignment repositories for students..."
@@ -50,26 +32,21 @@ module TeachersPet
50
32
  end
51
33
  repo_name = "#{student}-#{@repository}"
52
34
 
53
- if repository?(@organization, repo_name)
35
+ if self.client.repository?(@organization, repo_name)
54
36
  puts " --> Already exists, skipping '#{repo_name}'"
55
37
  next
56
38
  end
57
39
 
58
- git_ignore_template = "C++" ## This is specific to my current class, you'll want to change
59
- git_ignore_template = '' unless @add_init_files
60
40
  puts " --> Creating '#{repo_name}' public? #{@public_repos}"
61
- @client.create_repository(repo_name,
62
- {
63
- :description => "#{@repository} created for #{student}",
64
- :private => !@public_repos,
65
- :has_issues => true, # seems like a resonable default
66
- :has_wiki => false,
67
- :has_downloads => false,
68
- :organization => @organization,
69
- :team_id => org_teams[student][:id],
70
- :auto_init => @add_init_files,
71
- :gitignore_template => git_ignore_template
72
- })
41
+ self.client.create_repository(repo_name,
42
+ description: "#{@repository} created for #{student}",
43
+ private: !@public_repos,
44
+ has_issues: true,
45
+ has_wiki: false,
46
+ has_downloads: false,
47
+ organization: @organization,
48
+ team_id: org_teams[student][:id]
49
+ )
73
50
  end
74
51
  end
75
52
 
@@ -0,0 +1,35 @@
1
+ module TeachersPet
2
+ module Actions
3
+ class CreateStudentTeams < Base
4
+ def create_student_teams
5
+ teams_by_name = self.client.existing_teams_by_name(self.organization)
6
+
7
+ students_list = self.read_students_file
8
+ students_list.each do |key, value|
9
+ if value
10
+ # Create one team per group of students
11
+ team_name = key
12
+ usernames = value
13
+ else
14
+ # Create a team with the same name as the student, with that person as the only member
15
+ team_name = key
16
+ usernames = [value]
17
+ end
18
+
19
+ team = teams_by_name[team_name]
20
+ if team
21
+ puts "Team @#{organization}/#{team_name} already exists."
22
+ else
23
+ team = self.client.create_team(organization, team_name)
24
+ end
25
+ self.client.add_users_to_team(organization, team, usernames)
26
+ end
27
+ end
28
+
29
+ def run
30
+ self.init_client
31
+ self.create_student_teams
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,33 +1,19 @@
1
- $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
2
-
3
- require 'rubygems'
4
- require 'highline/question'
5
- require 'highline/import'
6
- require 'highline/compatibility'
7
- require 'teachers_pet/actions/base'
8
-
9
1
  module TeachersPet
10
2
  module Actions
11
3
  class ForkCollab < Base
12
- def read_info
13
- @repository = ask("Which repository? (owner/repo)")
14
- end
15
-
16
4
  def get_forks
17
- @client.forks(@repository)
5
+ self.client.forks(self.repository)
18
6
  end
19
7
 
20
8
  def promote
21
9
  self.init_client
22
-
23
10
  forks = self.get_forks
24
-
25
- confirm("Are you sure you want to add #{forks.count} users as collaborators on '#{@repository}'?")
26
-
27
11
  forks.each do |fork|
28
12
  login = fork.owner.login
29
13
  if fork.owner.type == "User"
30
- result = @client.add_collab(@repository, login)
14
+ unless self.dry_run?
15
+ result = self.client.add_collab(self.repository, login)
16
+ end
31
17
  puts "#{login} - #{result}"
32
18
  else
33
19
  puts "#{login} - false (Organization)"
@@ -36,7 +22,6 @@ module TeachersPet
36
22
  end
37
23
 
38
24
  def run
39
- self.read_info
40
25
  self.promote
41
26
  end
42
27
  end
@@ -1,49 +1,34 @@
1
- # Opens a single issue in each repository. The body of the issue is loaded
2
- # from a file and the user is prompted for the title.
3
-
4
- $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
5
-
6
- require 'rubygems'
7
- require 'highline/question'
8
- require 'highline/import'
9
- require 'teachers_pet/actions/base'
10
-
11
1
  module TeachersPet
12
2
  module Actions
13
3
  class OpenIssue < Base
14
4
  def read_info
15
- @repository = ask("What repository will the issue be raised in?") { |q| q.validate = /\w+/ }
16
- @organization = ask("What is the organization name?") { |q| q.default = TeachersPet::Configuration.organization }
17
-
18
- @issue = {}
19
- @issue[:title] = ask("What is the title of the issue?")
20
- @issue_file = ask("What is the path to the file containing the issue body?")
21
-
22
- # Add labels to issue
23
- options = {}
24
- options[:labels] = ask("Optionally add any labels, seperated by commas:")
25
- @issue[:options] = options
26
-
27
- @student_file = self.get_students_file_path
28
- @instructor_file = self.get_instructors_file_path
5
+ @repository = self.repository
6
+ @organization = self.organization
7
+
8
+ @issue = {
9
+ title: self.title,
10
+ options: {
11
+ labels: self.labels
12
+ }
13
+ }
14
+ @issue_file = self.body
29
15
  end
30
16
 
31
17
  def load_files
32
- @students = read_file(@student_file, 'Students')
33
- @instructors = read_file(@instructor_file, 'Instructors')
18
+ @students = self.read_students_file
34
19
  @issue[:body] = File.open(@issue_file).read
35
20
  end
36
21
 
37
22
  def create
38
- confirm("Create issue '#{@issue[:title]}' in #{@students.keys.size} student repositories - '#{@repository}'?")
23
+ # confirm("Create issue '#{@issue[:title]}' in #{@students.keys.size} student repositories - '#{@repository}'?")
39
24
  self.init_client
40
25
 
41
- org_hash = read_organization(@organization)
26
+ org_hash = self.client.organization(@organization)
42
27
  abort('Organization could not be found') if org_hash.nil?
43
28
  puts "Found organization at: #{org_hash[:login]}"
44
29
 
45
- org_teams = get_teams_by_name(@organization)
46
-
30
+ org_teams = self.client.get_teams_by_name(@organization)
31
+
47
32
  puts "\nCreating issue in repositories..."
48
33
  @students.keys.sort.each do |student|
49
34
  unless org_teams.key?(student)
@@ -52,13 +37,13 @@ module TeachersPet
52
37
  end
53
38
  repo_name = "#{student}-#{@repository}"
54
39
 
55
- unless repository?(@organization, repo_name)
40
+ unless self.client.repository?(@organization, repo_name)
56
41
  puts " --> Repository not found, skipping '#{repo_name}'"
57
42
  next
58
43
  end
59
44
 
60
45
  # Create the issue with octokit
61
- @client.create_issue("#{@organization}/#{repo_name}", @issue[:title], @issue[:body], @issue[:options])
46
+ self.client.create_issue("#{@organization}/#{repo_name}", @issue[:title], @issue[:body], @issue[:options])
62
47
  end
63
48
  end
64
49
 
@@ -1,40 +1,26 @@
1
- $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
2
-
3
- require 'rubygems'
4
- require 'highline/question'
5
- require 'highline/import'
6
- require 'highline/compatibility'
7
- require 'teachers_pet/actions/base'
8
-
9
- # This script should be run within a working directory that is a git repository.
10
- # It will add a remote that is the name of each student team to your repository
11
-
12
1
  module TeachersPet
13
2
  module Actions
14
3
  class PushFiles < Base
15
4
  def read_info
16
- @repository = ask('What repository name should pushed to for each student?') { |q| q.validate = /\w+/ }
17
-
18
- @organization = ask("What is the organization name?") { |q| q.default = TeachersPet::Configuration.organization }
19
- @student_file = self.get_students_file_path
20
- @sshEndpoint = ask('What is the ssh endpoint?') { |q| q.default = TeachersPet::Configuration.sshEndpoint }
5
+ @repository = self.repository
6
+ @organization = self.organization
7
+ @sshEndpoint = self.ssh
21
8
  end
22
9
 
23
10
  def load_files
24
- @students = read_file(@student_file, 'Students')
11
+ @students = self.read_students_file
25
12
  end
26
13
 
27
14
  def push
28
- confirm("Push files to student repositories?")
29
15
  self.init_client
30
16
 
31
- org_hash = read_organization(@organization)
17
+ org_hash = self.client.organization(@organization)
32
18
  abort('Organization could not be found') if org_hash.nil?
33
19
  puts "Found organization at: #{org_hash[:url]}"
34
20
 
35
21
  # Load the teams - there should be one team per student.
36
22
  # Repositories are given permissions by teams
37
- org_teams = get_teams_by_name(@organization)
23
+ org_teams = self.client.get_teams_by_name(@organization)
38
24
 
39
25
  # For each student - if an appropraite repository exists,
40
26
  # add it to the list.
@@ -46,13 +32,13 @@ module TeachersPet
46
32
  end
47
33
  repo_name = "#{student}-#{@repository}"
48
34
 
49
- unless repository?(@organization, repo_name)
35
+ unless self.client.repository?(@organization, repo_name)
50
36
  puts(" ** ERROR ** - no repository called #{repo_name}")
51
37
  end
52
38
  if TeachersPet::Configuration.remoteSsh
53
39
  remotes_to_add[student] = "git@#{@sshEndpoint}:#{@organization}/#{repo_name}.git"
54
40
  else
55
- remotes_to_add[student] = "#{@web_endpoint}#{@organization}/#{repo_name}.git"
41
+ remotes_to_add[student] = "#{self.web}#{@organization}/#{repo_name}.git"
56
42
  end
57
43
  end
58
44
 
@@ -0,0 +1,22 @@
1
+ require 'thor'
2
+
3
+ module TeachersPet
4
+ class Cli < Thor
5
+ # TODO figure out a way to display options as groups
6
+
7
+ def self.common_options
8
+ check_unknown_options!
9
+
10
+ option :username, default: ENV['USER']
11
+ option :password
12
+ option :token, default: ENV['TEACHERS_PET_GITHUB_TOKEN'], desc: "Provide a token instead of a username+password to authenticate via OAuth. See https://github.com/education/teachers_pet#authentication."
13
+
14
+ option :api, banner: 'ORIGIN', default: Configuration.apiEndpoint, desc: "The API endpoint of your GitHub Enterprise instance, if you have one."
15
+ option :web, banner: 'ORIGIN', default: Configuration.webEndpoint, desc: "The URL of your GitHub Enterprise instance, if you have one."
16
+ end
17
+
18
+ def self.students_option
19
+ option :students, default: TeachersPet::Configuration.studentsFile, banner: 'PATH', desc: "The path to the file containing the list of students"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ module TeachersPet
2
+ class ClientDecorator < SimpleDelegator
3
+ def repository?(organization, repo_name)
4
+ begin
5
+ self.repository("#{organization}/#{repo_name}")
6
+ rescue
7
+ return false
8
+ end
9
+ end
10
+
11
+ def get_teams_by_name(organization)
12
+ org_teams = self.organization_teams(organization)
13
+ teams = Hash.new
14
+ org_teams.each do |team|
15
+ teams[team[:name]] = team
16
+ end
17
+ return teams
18
+ end
19
+
20
+ def get_team_member_logins(team_id)
21
+ self.team_members(team_id).map do |member|
22
+ member[:login]
23
+ end
24
+ end
25
+
26
+ def existing_teams_by_name(organization)
27
+ results = Hash.new
28
+ teams = self.organization_teams(organization)
29
+ teams.each do |team|
30
+ results[team[:name]] = team
31
+ end
32
+
33
+ results
34
+ end
35
+
36
+ def create_team(organization, name)
37
+ puts "Creating team @#{organization}/#{name} ..."
38
+ super(organization,
39
+ name: name,
40
+ permission: 'push'
41
+ )
42
+ end
43
+
44
+ def add_users_to_team(organization, team, usernames)
45
+ # Minor optimization, mostly for testing
46
+ if usernames.any?
47
+ team_members = self.get_team_member_logins(team[:id])
48
+ usernames.each do |username|
49
+ if team_members.include?(username)
50
+ puts " -> @#{username} is already on @#{organization}/#{team[:name]}"
51
+ else
52
+ self.add_team_member(team[:id], username)
53
+ puts " -> @#{username} has been added to @#{organization}/#{team[:name]}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,12 @@
1
+ module TeachersPet
2
+ class Cli
3
+ option :organization, required: true
4
+ option :members, required: true, banner: 'PATH', desc: "The path to the file containing the list of members to add. The filename will be used as the name of the team, e.g. `path/to/instructors.csv` will use the 'instructors' team."
5
+ common_options
6
+
7
+ desc 'add_to_team', "Adds each user in the list to the team specified my the filename. Creates the team on the specified organization if it doesn't already exist."
8
+ def add_to_team
9
+ TeachersPet::Actions::AddToTeam.new(options).run
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module TeachersPet
2
+ class Cli
3
+ option :organization, required: true
4
+ option :repository, required: true
5
+ option :clone_method, default: 'https', desc: "'https' or 'ssh'"
6
+
7
+ students_option
8
+ common_options
9
+
10
+ desc 'clone_repos', "Clone all student repositories for a particular assignment into the current directory."
11
+ def clone_repos
12
+ TeachersPet::Actions::CloneRepos.new(options).run
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module TeachersPet
2
+ class Cli
3
+ option :organization, required: true
4
+ option :repository, required: true
5
+ option :public, type: :boolean, default: false, desc: "Make the repositories public"
6
+
7
+ students_option
8
+ common_options
9
+
10
+ desc 'create_repos', "Create assignment repositories for students."
11
+ def create_repos
12
+ TeachersPet::Actions::CreateRepos.new(options).run
13
+ end
14
+ end
15
+ end