teachers_pet 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2d30fbc91197f42ea30df5cd1a1347cc5da3b082
4
- data.tar.gz: 3f740ae9fab9e2a13a09399db11cc6bc84e57529
3
+ metadata.gz: 34f31b6d378cbb283cead5e47e9a115b04fc48dd
4
+ data.tar.gz: c67ba2a5cb528af7e6a956b00162bbfe4b6d789b
5
5
  SHA512:
6
- metadata.gz: 9af701ab906d0122f95778c8c0096dcbd4a9b22326d01b028d18dd1e2e01962944e90c96f19436c4e529a5d56efe749ca2c7911457c36b752e709b4c11acba49
7
- data.tar.gz: e6ac88733408986a70c83fb5e7a6cdc131510f8a8a82c9b42d25cf1399c18309d425ada3c37c57f20c9e7deba78ce6c842416686ae90bc2fbacf4bccbb7315b5
6
+ metadata.gz: 8abd43bf0409d38576a414321598658e8cc04edf95b3e1e3b287887b437e6e5da38ab548c52e544e89e78eb0e63cd3b531bd2f1534495f30777a4b2dead499d4
7
+ data.tar.gz: 3e3137a1d275ea9381356aaa39bf3e784ecbdb11c88ecb213e96e2f2681350d46cc8240f21752ca2d2730831b4355d0058150a5a3a6488c6644d11c1a02fe649
data/.gitignore CHANGED
@@ -1,8 +1,14 @@
1
+ # project specific
2
+ /instructors
3
+ /students
4
+ /teams
5
+
1
6
  *.gem
2
7
  *.rbc
3
8
  .bundle
4
9
  .config
5
10
  coverage
11
+ Gemfile.lock
6
12
  InstalledFiles
7
13
  lib/bundler/man
8
14
  pkg
@@ -16,3 +22,13 @@ tmp
16
22
  .yardoc
17
23
  _yardoc
18
24
  doc/
25
+
26
+ # OS generated files #
27
+ ######################
28
+ .DS_Store
29
+ .DS_Store?
30
+ ._*
31
+ .Spotlight-V100
32
+ .Trashes
33
+ ehthumbs.db
34
+ Thumbs.db
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,19 @@
1
+ ## Development
2
+
3
+ ```bash
4
+ git clone https://github.com/education/teachers_pet.git
5
+ cd teachers_pet
6
+ bundle install
7
+ # then run actions using
8
+ bundle exec ./bin/COMMAND
9
+ ```
10
+
11
+ ## Running tests
12
+
13
+ ```bash
14
+ bundle
15
+ # then
16
+ bundle exec rspec
17
+ # or
18
+ bundle exec guard
19
+ ```
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ # More info at https://github.com/guard/guard#readme
2
+
3
+ guard :rspec, all_on_start: true, cmd: 'bundle exec rspec' do
4
+ watch(%r{^(lib|spec)/.*})
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 GitHub Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # teachers_pet [![Build Status](https://travis-ci.org/education/teachers_pet.svg?branch=master)](https://travis-ci.org/education/teachers_pet)
2
+
3
+ Command line tools to help teachers use GitHub in their classrooms.
4
+
5
+ ## Philosophy
6
+
7
+ Each class is an 'organization' on GitHub. This allows the instructors (GitHub organization Owners) to create, push, pull, and administer all repositories. This achieves two goals:
8
+
9
+ * Instructors can push code starter code to all students
10
+ * Instructors can easily browse/pull student code at any time during the assignment to assist in questions, check on progress
11
+
12
+ Each student is given a team in the organization. The team name is the same as the student's GitHub username. The course instructors are also added as team members for each team (see the goals above).
13
+
14
+ ## Installation
15
+
16
+ [Install Ruby 1.9.3+](https://www.ruby-lang.org/en/installation/), then run
17
+
18
+ ```bash
19
+ gem install teachers_pet
20
+ ```
21
+
22
+ To use the latest-and-greatest code from this repository, see the instructions in [CONTRIBUTING.md](CONTRIBUTING.md).
23
+
24
+ ## Basic Setup
25
+
26
+ * Create an organization (you will be an owner by default). The organization should reflect the name of your course.
27
+ * Create a `students` file (you will be prompted for the path later)
28
+ * Individual assignments: one username per line
29
+ * Group assignments: one team per line in the format `teamName username username username`
30
+ * Add the GitHub username of all instructors to an 'instructors' file (one per line)
31
+ * Run `create_teams`
32
+
33
+ ## Passwords / OAuth
34
+
35
+ The scripts will ask for your GitHub password in order to run. If you have [two factor authentication](https://help.github.com/articles/about-two-factor-authentication) enabled, [create a personal access token](https://help.github.com/articles/creating-an-access-token-for-command-line-use) (replace `github.com` with your host for GitHub Enterprise):
36
+
37
+ https://github.com/settings/tokens/new?description=teachers_pet&scopes=repo%2Cpublic_repo%2Cwrite%3Aorg%2Crepo%3Astatus%2Cread%3Aorg%2Cuser%2Cadmin%3Aorg
38
+
39
+ Then, add the following line to your `.bash_profile`:
40
+
41
+ ```bash
42
+ export ghe_oauth="YOUR_TOKEN_HERE"
43
+ ```
44
+
45
+ ## Actions
46
+
47
+ ### Creating assignments
48
+
49
+ For each assignment, run `create_repos` to create a repository for each student. The repositories are technically created per team, but if you use `create_teams` first, then there will be one team per student.
50
+
51
+ ### Collaborator access
52
+
53
+ Give collaborator access to everyone who has forked your repository.
54
+
55
+ ```bash
56
+ fork_collab
57
+ ```
58
+
59
+ ### Pushing starter files
60
+
61
+ This is the workflow that we use. Create a private repository on GitHub. Clone it to your machine and place in all the necessary starter files (.gitignore and build files, like Makefile are highly recommended). Commit and push this repository to the origin.
62
+
63
+ While in the directory for the starter file repository, run the `push_repos` script.
64
+
65
+ This works by creating a git remote for each repository and thing doing a push to that repository.
66
+
67
+ ### Pulling repositories for grading
68
+
69
+ When grading, use the `clone_repos` script to clone all the repositories in the organization that match the username-repository naming scheme that is generated when `create_repos` is run.
70
+
71
+ ## Related projects
72
+
73
+ * https://education.github.com/guide
74
+ * https://github.com/hogbait/6170_repo_management
75
+ * https://github.com/mikehelmick/edugit-scripts
76
+ * https://github.com/UCSB-CS-Using-GitHub-In-Courses/github-acad-scripts
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/clone_repos ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'teachers_pet/actions/clone_repos'
5
+
6
+ TeachersPet::Actions::CloneRepos.new.run
data/bin/create_repos ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'teachers_pet/actions/create_repos'
5
+
6
+ TeachersPet::Actions::CreateRepos.new.run
data/bin/create_teams ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'teachers_pet/actions/create_teams'
5
+
6
+ TeachersPet::Actions::CreateTeams.new.run
data/bin/fork_collab ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'teachers_pet/actions/fork_collab'
5
+
6
+ TeachersPet::Actions::ForkCollab.new.run
data/bin/push_files ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'teachers_pet/actions/push_files'
5
+
6
+ TeachersPet::Actions::PushFiles.new.run
@@ -0,0 +1,160 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..')
2
+
3
+ require 'configuration'
4
+
5
+ ## Common code for the edugit scripts.
6
+ module TeachersPet
7
+ module Actions
8
+ class Base
9
+ def get_auth_method
10
+ auth_method = nil
11
+
12
+ choose do |menu|
13
+ menu.prompt = "Login via oAuth or Password? "
14
+ menu.choice :oAuth do
15
+ auth_method = 'oauth'
16
+ end
17
+ menu.choice :Password do
18
+ auth_method = 'password'
19
+ end
20
+ end
21
+
22
+ auth_method
23
+ end
24
+
25
+ def config_github
26
+ return unless @username.nil?
27
+ @api_endpoint = ask('What is the API endpoint?') { |q| q.default = Configuration.apiEndpoint }
28
+ @web_endpoint = ask("What is the Web endpoint?") { |q| q.default = Configuration.webEndpoint }
29
+
30
+ @username = ask('What is your username? (You must be an owner for the organization)?') { |q| q.default = ENV['USER'] }
31
+
32
+ @authmethod = self.get_auth_method
33
+
34
+ if @authmethod == 'oauth'
35
+ @oauthtoken = ask('What is your oAuth token?') { |q| q.default = ENV['ghe_oauth'] }
36
+ end
37
+ if @authmethod == 'password'
38
+ @password = ask('What is your password?') { |q| q.echo = false }
39
+ end
40
+ end
41
+
42
+ def init_client
43
+ self.config_github
44
+ puts "=" * 50
45
+ puts "Authenticating to GitHub..."
46
+ Octokit.configure do |c|
47
+ c.api_endpoint = @api_endpoint
48
+ c.web_endpoint = @web_endpoint
49
+ # Organizations can get big, pull in all pages
50
+ c.auto_paginate = true
51
+ end
52
+
53
+ if @authmethod == 'password'
54
+ @client = Octokit::Client.new(:login => @username, :password => @password)
55
+ end
56
+ if @authmethod == 'oauth'
57
+ @client = Octokit::Client.new(:login => @username, :access_token => @oauthtoken)
58
+ end
59
+ end
60
+
61
+ def read_organization(organization)
62
+ abort("GitHub client not initialized") if @client.nil?
63
+ @client.organization(organization)
64
+ end
65
+
66
+ def execute(command)
67
+ `#{command}`
68
+ end
69
+
70
+ protected
71
+ def repository?(organization, repo_name)
72
+ begin
73
+ @client.repository("#{organization}/#{repo_name}")
74
+ rescue
75
+ return false
76
+ end
77
+ end
78
+
79
+ def get_students_file_path
80
+ ask("What is the filename of the list of students?") { |q| q.default = TeachersPet::Configuration.studentsFile }
81
+ end
82
+
83
+ def get_instructors_file_path
84
+ ask("What is the filename of the list of instructors?") { |q| q.default = TeachersPet::Configuration.instructorsFile }
85
+ end
86
+
87
+ def get_existing_repos_by_names(organization)
88
+ repos = Hash.new
89
+ response = @client.organization_repositories(organization)
90
+ print " Org repo names"
91
+ response.each do |repo|
92
+ repos[repo[:name]] = repo
93
+ print " '#{repo[:name]}'"
94
+ end
95
+ print "\n";
96
+ return repos
97
+ end
98
+
99
+ def get_teams_by_name(organization)
100
+ org_teams = @client.organization_teams(organization)
101
+ teams = Hash.new
102
+ org_teams.each do |team|
103
+ teams[team[:name]] = team
104
+ end
105
+ return teams
106
+ end
107
+
108
+ def get_team_member_logins(team_id)
109
+ @client.team_members(team_id).map do |member|
110
+ member[:login]
111
+ end
112
+ end
113
+
114
+ def read_file(filename, type)
115
+ map = Hash.new
116
+ puts "Loading #{type}:"
117
+ File.open(filename).each_line do |team|
118
+ # Team can be a single user, or a team name and multiple users
119
+ # Trim whitespace, otherwise issues occur
120
+ team.strip!
121
+ items = team.split(' ')
122
+ items.each do |item|
123
+ abort("No users can be named 'owners' (in any case)") if 'owners'.eql?(item.downcase)
124
+ end
125
+
126
+ if map[items[0]].nil?
127
+ map[items[0]] = Array.new
128
+ puts " -> #{items[0]}"
129
+ if (items.size > 1)
130
+ print " \\-> members: "
131
+ 1.upto(items.size - 1) do |i|
132
+ print "#{items[i]} "
133
+ map[items[0]] << items[i]
134
+ end
135
+ print "\n"
136
+ else
137
+ map[items[0]] << items[0]
138
+ end
139
+ end
140
+ end
141
+ return map
142
+ end
143
+
144
+ def confirm(message, abort_on_no = true)
145
+ # confirm
146
+ confirmed = false
147
+ choose do |menu|
148
+ menu.prompt = message
149
+
150
+ menu.choice :yes do confirmed = true end
151
+ menu.choice :no do confirmed = false end
152
+ end
153
+ if abort_on_no && !confirmed
154
+ abort("Creation canceled by user")
155
+ end
156
+ return confirmed
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,89 @@
1
+ # Author: Mike Helmick
2
+ # Clones all student repositories for a particular assignment
3
+ #
4
+ # Currently this will clone all student repositories into the current
5
+
6
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
7
+
8
+ require 'rubygems'
9
+ require 'highline/question'
10
+ require 'highline/import'
11
+ require 'highline/compatibility'
12
+ require 'octokit'
13
+ require 'teachers_pet/actions/base'
14
+
15
+ module TeachersPet
16
+ module Actions
17
+ class CloneRepos < Base
18
+ def read_info
19
+ @repository = ask('What repository name should be cloned for each student?') { |q| q.validate = /\w+/ }
20
+ @organization = ask("What is the organization name?") { |q| q.default = TeachersPet::Configuration.organization }
21
+ @student_file = self.get_students_file_path
22
+ end
23
+
24
+ def load_files
25
+ @students = read_file(@student_file, 'Students')
26
+ end
27
+
28
+ def get_clone_method
29
+ cloneMethod = 'https'
30
+ choose do |menu|
31
+ menu.prompt = "Clone via? "
32
+ menu.choice :ssh do
33
+ cloneMethod = 'ssh'
34
+ end
35
+ menu.choice :https do
36
+ cloneMethod = 'https'
37
+ end
38
+ end
39
+
40
+ cloneMethod
41
+ end
42
+
43
+ def create
44
+ cloneMethod = self.get_clone_method
45
+
46
+ confirm("Clone all repositories?")
47
+
48
+ # create a repo for each student
49
+ self.init_client
50
+
51
+ org_hash = read_organization(@organization)
52
+ abort('Organization could not be found') if org_hash.nil?
53
+ puts "Found organization at: #{org_hash[:url]}"
54
+
55
+ # Load the teams - there should be one team per student.
56
+ org_teams = get_teams_by_name(@organization)
57
+ # For each student - pull the repository if it exists
58
+ puts "\nCloning assignment repositories for students..."
59
+ @students.keys.each do |student|
60
+ unless org_teams.key?(student)
61
+ puts(" ** ERROR ** - no team for #{student}")
62
+ next
63
+ end
64
+ repo_name = "#{student}-#{@repository}"
65
+
66
+ unless repository?(@organization, repo_name)
67
+ puts " ** ERROR ** - Can't find expected repository '#{repo_name}'"
68
+ next
69
+ end
70
+
71
+
72
+ sshEndpoint = @web_endpoint.gsub("https://","git@").gsub("/",":")
73
+ command = "git clone #{sshEndpoint}#{@organization}/#{repo_name}.git"
74
+ if cloneMethod.eql?('https')
75
+ command = "git clone #{@web_endpoint}#{@organization}/#{repo_name}.git"
76
+ end
77
+ puts " --> Cloning: '#{command}'"
78
+ self.execute(command)
79
+ end
80
+ end
81
+
82
+ def run
83
+ self.read_info
84
+ self.load_files
85
+ self.create
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,82 @@
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 'octokit'
11
+ require 'teachers_pet/actions/base'
12
+
13
+ module TeachersPet
14
+ module Actions
15
+ class CreateRepos < Base
16
+ def read_info
17
+ @repository = ask('What repository name should be created for each student?') { |q| q.validate = /\w+/ }
18
+ @organization = ask("What is the organization name?") { |q| q.default = TeachersPet::Configuration.organization }
19
+ @student_file = self.get_students_file_path
20
+ @instructor_file = self.get_instructors_file_path
21
+ @add_init_files = confirm('Add .gitignore and README.md files? (skip this if you are pushing starter files.)', false)
22
+ end
23
+
24
+ def load_files
25
+ @students = read_file(@student_file, 'Students')
26
+ @instructors = read_file(@instructor_file, 'Instructors')
27
+ end
28
+
29
+ def create
30
+ confirm("Create #{@students.keys.size} repositories for students and give access to instructors?")
31
+
32
+ # create a repo for each student
33
+ self.init_client
34
+
35
+ org_hash = read_organization(@organization)
36
+ abort('Organization could not be found') if org_hash.nil?
37
+ puts "Found organization at: #{org_hash[:login]}"
38
+
39
+ # Load the teams - there should be one team per student.
40
+ # Repositories are given permissions by teams
41
+ org_teams = get_teams_by_name(@organization)
42
+ # For each student - create a repository, and give permissions to their "team"
43
+ # The repository name is teamName-repository
44
+ puts "\nCreating assignment repositories for students..."
45
+ @students.keys.sort.each do |student|
46
+ unless org_teams.key?(student)
47
+ puts(" ** ERROR ** - no team for #{student}")
48
+ next
49
+ end
50
+ repo_name = "#{student}-#{@repository}"
51
+
52
+ if repository?(@organization, repo_name)
53
+ puts " --> Already exists, skipping '#{repo_name}'"
54
+ next
55
+ end
56
+
57
+ git_ignore_template = "C++" ## This is specific to my current class, you'll want to change
58
+ git_ignore_template = '' unless @add_init_files
59
+ puts " --> Creating '#{repo_name}'"
60
+ @client.create_repository(repo_name,
61
+ {
62
+ :description => "#{@repository} created for #{student}",
63
+ :private => !TeachersPet::Configuration.reposPublic, ## Current default, repositories are private to the student & instructors
64
+ :has_issues => true, # seems like a resonable default
65
+ :has_wiki => false,
66
+ :has_downloads => false,
67
+ :organization => @organization,
68
+ :team_id => org_teams[student][:id],
69
+ :auto_init => @add_init_files,
70
+ :gitignore_template => git_ignore_template
71
+ })
72
+ end
73
+ end
74
+
75
+ def run
76
+ self.read_info
77
+ self.load_files
78
+ self.create
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,102 @@
1
+ # Author: Mike Helmick
2
+ # Creates "teams" for an origanization. In this scenario - each team consists of
3
+ # one student, and any instructors for the course.
4
+
5
+ # The students and instructors files contain 1 userid per line.
6
+ # - For teams, the students file should be "teamName studentName studentName"
7
+ # We recommend that instructors also be created as students for ease of testing.
8
+
9
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', '..')
10
+
11
+ require 'rubygems'
12
+ require 'highline/question'
13
+ require 'highline/import'
14
+ require 'highline/compatibility'
15
+ require 'octokit'
16
+
17
+ require 'teachers_pet/actions/base'
18
+
19
+ module TeachersPet
20
+ module Actions
21
+ class CreateTeams < Base
22
+ def read_info
23
+ @organization = ask("What is the organization name?") { |q| q.default = TeachersPet::Configuration.organization }
24
+ @student_file = self.get_students_file_path
25
+ @instructor_file = self.get_instructors_file_path
26
+ end
27
+
28
+ def load_files
29
+ @students = read_file(@student_file, 'Students')
30
+ @instructors = read_file(@instructor_file, 'Instructors')
31
+ end
32
+
33
+ def create
34
+ self.init_client
35
+ confirm("Create teams for #{@students.size} students/teams?")
36
+
37
+ existing = Hash.new
38
+ teams = @client.organization_teams(@organization)
39
+ teams.each { |team| existing[team[:name]] = team }
40
+
41
+ puts "\nDetermining which students need teams created..."
42
+ todo = Hash.new
43
+ @students.keys.each do |team|
44
+ if existing[team].nil?
45
+ puts " -> #{team}"
46
+ todo[team] = true
47
+ end
48
+ end
49
+
50
+ if todo.empty?
51
+ puts "\nAll teams exist"
52
+ else
53
+ puts "\nCreating team..."
54
+ todo.keys.each do |team|
55
+ puts " -> '#{team}' ..."
56
+ @client.create_team(@organization,
57
+ {
58
+ :name => team,
59
+ :permission => 'push'
60
+ })
61
+ end
62
+ end
63
+
64
+ puts "\nAdjusting team memberships"
65
+ teams = @client.organization_teams(@organization)
66
+ teams.each do |team|
67
+ team_members = get_team_member_logins(team[:id])
68
+ if team[:name].eql?('Owners')
69
+ puts "*** OWNERS *** - Ensuring instructors are owners"
70
+ @instructors.keys.each do |instructor|
71
+ unless team_members.include?(instructor)
72
+ @client.add_team_member(team[:id], instructor)
73
+ puts " -> '#{instructor}' has been made an owner for this course"
74
+ else
75
+ puts " -> '#{instructor}' is already an owner"
76
+ end
77
+ end
78
+ elsif @students.key?(team[:name])
79
+ puts "Validating membership for team '#{team[:name]}'"
80
+ # If there isn't a team member that is the same name as the team, and we already know
81
+ # there is a student with the same name, add that student to the team.
82
+ #unless team_members.include?(team[:name])
83
+ @students[team[:name]].each do |student|
84
+ puts " -> Adding '#{team[:name]}' to the team"
85
+ @client.add_team_member(team[:id], student)
86
+ end
87
+ # Originally, instructors were added to the student's team, but that isn't needed
88
+ # since instructors are addded to the Owners team that can see all repositories.
89
+ else
90
+ puts "*** Team name '#{team[:name]}' does not match any students, ignoring. ***"
91
+ end
92
+ end
93
+ end
94
+
95
+ def run
96
+ self.read_info
97
+ self.load_files
98
+ self.create
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,45 @@
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 'octokit'
8
+ require 'teachers_pet/actions/base'
9
+
10
+ module TeachersPet
11
+ module Actions
12
+ class ForkCollab < Base
13
+ def read_info
14
+ @repository = ask("Which repository? (name/owner)")
15
+ end
16
+
17
+ def get_forks
18
+ @client.forks(@repository)
19
+ end
20
+
21
+ def promote
22
+ self.init_client
23
+
24
+ forks = self.get_forks
25
+
26
+ confirm("Are you sure you want to add #{forks.count} users as collaborators on '#{@repository}'?")
27
+
28
+ forks.each do |fork|
29
+ login = fork.owner.login
30
+ if fork.owner.type == "User"
31
+ result = @client.add_collab(@repository, login)
32
+ puts "#{login} - #{result}"
33
+ else
34
+ puts "#{login} - false (Organization)"
35
+ end
36
+ end
37
+ end
38
+
39
+ def run
40
+ self.read_info
41
+ self.promote
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,75 @@
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 'octokit'
8
+ require 'teachers_pet/actions/base'
9
+
10
+ # This script should be run within a working directory that is a git repository.
11
+ # It will add a remote that is the name of each student team to your repository
12
+
13
+ module TeachersPet
14
+ module Actions
15
+ class PushFiles < Base
16
+ def read_info
17
+ @repository = ask('What repository name should pushed to for each student?') { |q| q.validate = /\w+/ }
18
+
19
+ @organization = ask("What is the organization name?") { |q| q.default = TeachersPet::Configuration.organization }
20
+ @student_file = self.get_students_file_path
21
+ @sshEndpoint = ask('What is the ssh endpoint?') { |q| q.default = TeachersPet::Configuration.sshEndpoint }
22
+ end
23
+
24
+ def load_files
25
+ @students = read_file(@student_file, 'Students')
26
+ end
27
+
28
+ def push
29
+ confirm("Push files to student repositories?")
30
+ self.init_client
31
+
32
+ org_hash = read_organization(@organization)
33
+ abort('Organization could not be found') if org_hash.nil?
34
+ puts "Found organization at: #{org_hash[:url]}"
35
+
36
+ # Load the teams - there should be one team per student.
37
+ # Repositories are given permissions by teams
38
+ org_teams = get_teams_by_name(@organization)
39
+
40
+ # For each student - if an appropraite repository exists,
41
+ # add it to the list.
42
+ remotes_to_add = Hash.new
43
+ @students.keys.sort.each do |student|
44
+ unless org_teams.key?(student)
45
+ puts(" ** ERROR ** - no team for #{student}")
46
+ next
47
+ end
48
+ repo_name = "#{student}-#{@repository}"
49
+
50
+ unless repository?(@organization, repo_name)
51
+ puts(" ** ERROR ** - no repository called #{repo_name}")
52
+ end
53
+ if TeachersPet::Configuration.remoteSsh
54
+ remotes_to_add[student] = "git@#{@sshEndpoint}:#{@organization}/#{repo_name}.git"
55
+ else
56
+ remotes_to_add[student] = "#{@web_endpoint}#{@organization}/#{repo_name}.git"
57
+ end
58
+ end
59
+
60
+ puts "Adding remotes and pushing files to student repositories."
61
+ remotes_to_add.keys.each do |remote|
62
+ puts "#{remote} --> #{remotes_to_add[remote]}"
63
+ `git remote add #{remote} #{remotes_to_add[remote]}`
64
+ `git push #{remote} master`
65
+ end
66
+ end
67
+
68
+ def run
69
+ self.read_info
70
+ self.load_files
71
+ self.push
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,67 @@
1
+ # Configuration for the edugit scripts. Modify this as desired to change
2
+ # your defaults in the prompts
3
+ module TeachersPet
4
+ class Configuration
5
+
6
+ # For github.com: 'https://api.github.com/'
7
+ # For GitHub Enterprise: 'https://yourServer.com/api/v3'
8
+ @@API_ENDPOINT = 'https://api.github.com/'
9
+
10
+ def self.apiEndpoint
11
+ @@API_ENDPOINT
12
+ end
13
+
14
+ # For github.com: https://www.github.com/'
15
+ # For GitHub Enterprise: ''https://yourServer.com/'
16
+ @@WEB_ENDPOINT = 'https://www.github.com/'
17
+
18
+ def self.webEndpoint
19
+ @@WEB_ENDPOINT
20
+ end
21
+
22
+ # The name of your organization
23
+ @@ORGANIZATION = 'edugit-test'
24
+
25
+ def self.organization
26
+ @@ORGANIZATION
27
+ end
28
+
29
+ # The name fo the file that contains the team definitions / students
30
+ @@STUDENTS_FILE = './students'
31
+
32
+ def self.studentsFile
33
+ @@STUDENTS_FILE
34
+ end
35
+
36
+ # The name of the file that contains the GitHub usernames of the instructors
37
+ @@INSTRUCTORS_FILE = './instructors'
38
+
39
+ def self.instructorsFile
40
+ @@INSTRUCTORS_FILE
41
+ end
42
+
43
+ # The default is to create the repositories as public.
44
+ # If you are using GitHub enterprise or a paid account, it make sense to make this private
45
+ @@REPOS_PUBLIC = true
46
+
47
+ def self.reposPublic
48
+ @@REPOS_PUBLIC
49
+ end
50
+
51
+ # github.com - set to 'github.com'
52
+ # GitHub Enterprise - 'yourserver.com'
53
+ @@SSH_ENDPOINT = 'github.com'
54
+
55
+ def self.sshEndpoint
56
+ @@SSH_ENDPOINT
57
+ end
58
+
59
+ ## It is best to push remotes vis SSH, you can swith this to false to push as HTTPS
60
+ @@REMOTE_SSH = true
61
+
62
+ def self.remoteSsh
63
+ @@REMOTE_SSH
64
+ end
65
+
66
+ end
67
+ end
@@ -1,3 +1,3 @@
1
1
  module TeachersPet
2
- VERSION = '0.0.1'
2
+ VERSION = '0.1.0'
3
3
  end
data/lib/teachers_pet.rb CHANGED
@@ -1,2 +1,4 @@
1
+ Dir.glob(File.join(File.dirname(__FILE__), '**', '*.rb'), &method(:require))
2
+
1
3
  module TeachersPet
2
4
  end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe TeachersPet::Actions::CloneRepos do
4
+ let(:action) { TeachersPet::Actions::CloneRepos.new }
5
+
6
+ def respond(question, response)
7
+ action.stub(:ask).with(question).and_return(response)
8
+ end
9
+
10
+ before do
11
+ # fallback
12
+ action.stub(:ask){|question| raise("can't ask \"#{question}\"") }
13
+ action.stub(:choose){ raise("can't choose()") }
14
+
15
+ action.stub(:confirm)
16
+ end
17
+
18
+ it "runs" do
19
+ respond("What repository name should be cloned for each student?", 'testrepo')
20
+ respond("What is the organization name?", 'testorg')
21
+ respond("What is the filename of the list of students?", students_list_fixture_path)
22
+ action.stub(get_clone_method: 'https')
23
+ respond("What is the organization name?", "testorg")
24
+ stub_github_config
25
+
26
+ request_stubs = []
27
+
28
+ request_stubs << stub_get_json('https://testteacher:abc123@api.github.com/orgs/testorg',
29
+ login: 'testorg',
30
+ url: 'https://api.github.com/orgs/testorg'
31
+ )
32
+ request_stubs << stub_get_json('https://testteacher:abc123@api.github.com/orgs/testorg/teams?per_page=100', student_teams)
33
+ student_usernames.each do |username|
34
+ request_stubs << stub_get_json("https://testteacher:abc123@api.github.com/repos/testorg/#{username}-testrepo", {})
35
+ action.should_receive(:execute).with("git clone https://www.github.com/testorg/#{username}-testrepo.git").once
36
+ end
37
+
38
+ action.run
39
+
40
+ request_stubs.each do |request_stub|
41
+ expect(request_stub).to have_been_requested.once
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe TeachersPet::Actions::CreateTeams do
4
+ let(:action) { TeachersPet::Actions::CreateTeams.new }
5
+
6
+ before do
7
+ # fallback
8
+ action.stub(:ask){|question| raise("can't ask \"#{question}\"") }
9
+ action.stub(:choose){ raise("can't choose()") }
10
+
11
+ action.stub(:confirm)
12
+ end
13
+
14
+ it "creates teams if none exist" do
15
+ respond("What is the organization name?", 'testorg')
16
+ respond("What is the filename of the list of students?", students_list_fixture_path)
17
+ respond("What is the filename of the list of instructors?", instructors_list_fixture_path)
18
+ stub_github_config
19
+
20
+ teams_stub = stub_get_json('https://testteacher:abc123@api.github.com/orgs/testorg/teams?per_page=100', [])
21
+
22
+ request_stubs = []
23
+ student_usernames.each do |student|
24
+ request_stubs << stub_request(:post, 'https://testteacher:abc123@api.github.com/orgs/testorg/teams').
25
+ with(body: {
26
+ name: student,
27
+ permission: 'push'
28
+ }.to_json)
29
+ end
30
+
31
+ action.run
32
+
33
+ expect(teams_stub).to have_been_requested.twice
34
+ request_stubs.each do |request_stub|
35
+ expect(request_stub).to have_been_requested.once
36
+ end
37
+ end
38
+
39
+ it "handles existing teams"
40
+ end
@@ -0,0 +1,3 @@
1
+ testinstructor1
2
+ testinstructor2
3
+ testinstructor3
@@ -0,0 +1,3 @@
1
+ teststudent1
2
+ teststudent2
3
+ teststudent3
@@ -0,0 +1,56 @@
1
+ require 'csv'
2
+ require 'json'
3
+ require 'webmock/rspec'
4
+
5
+ require_relative File.join('..', 'lib', 'teachers_pet')
6
+
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ config.filter_run :focus
11
+ config.order = 'random'
12
+ end
13
+
14
+
15
+ def stub_get_json(url, response)
16
+ stub_request(:get, url).to_return(
17
+ headers: {
18
+ 'Content-Type' => 'application/json'
19
+ },
20
+ body: response.to_json
21
+ )
22
+ end
23
+
24
+ def students_list_fixture_path
25
+ File.join(File.dirname(__FILE__), 'fixtures', 'students')
26
+ end
27
+
28
+ def instructors_list_fixture_path
29
+ File.join(File.dirname(__FILE__), 'fixtures', 'instructors')
30
+ end
31
+
32
+ def student_usernames
33
+ CSV.read(students_list_fixture_path).flatten
34
+ end
35
+
36
+ def student_teams
37
+ student_usernames.each_with_index.map do |username, i|
38
+ {
39
+ url: "https://api.github.com/teams/#{i}",
40
+ name: username,
41
+ id: i
42
+ }
43
+ end
44
+ end
45
+
46
+ def respond(question, response)
47
+ action.stub(:ask).with(question).and_return(response)
48
+ end
49
+
50
+ def stub_github_config
51
+ respond("What is the API endpoint?", TeachersPet::Configuration.apiEndpoint)
52
+ respond("What is the Web endpoint?", TeachersPet::Configuration.webEndpoint)
53
+ respond("What is your username? (You must be an owner for the organization)?", 'testteacher')
54
+ action.stub(get_auth_method: 'password')
55
+ respond("What is your password?", 'abc123')
56
+ end
data/teachers_pet.gemspec CHANGED
@@ -1,20 +1,29 @@
1
- require File.expand_path("../lib/teachers_pet/version", __FILE__)
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'teachers_pet/version'
2
5
 
3
6
  Gem::Specification.new do |s|
4
7
  s.name = 'teachers_pet'
5
8
  s.version = TeachersPet::VERSION
6
- s.summary = "Command line tools for teachers"
7
- s.description = "A simple hello world gem"
9
+ s.summary = "Command line tools to help teachers use GitHub in their classrooms"
8
10
  s.authors = ["John Britton"]
9
11
  s.email = 'public@johndbritton.com'
10
- s.homepage = 'http://github.com/johndbritton/teachers_pet'
12
+ s.homepage = 'http://github.com/education/teachers_pet'
11
13
  s.license = 'MIT'
12
14
 
13
- s.files = `git ls-files`.split $/
15
+ s.bindir = 'bin'
16
+ s.files = `git ls-files -z`.split("\x0")
17
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
14
19
  s.require_paths = ['lib']
15
20
 
16
- s.bindir = 'bin'
17
- s.executables << 'forkcollab'
21
+ s.add_dependency 'highline', '~> 1.6.21'
22
+ s.add_dependency 'octokit', '~> 3.1.0'
18
23
 
19
- s.add_dependency 'octokit'
24
+ s.add_development_dependency 'bundler', '~> 1.5'
25
+ s.add_development_dependency 'guard-rspec', '~> 4.2'
26
+ s.add_development_dependency 'rake'
27
+ s.add_development_dependency 'rspec', '~> 2.14'
28
+ s.add_development_dependency 'webmock', '~> 1.17'
20
29
  end
metadata CHANGED
@@ -1,42 +1,154 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teachers_pet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Britton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-25 00:00:00.000000000 Z
11
+ date: 2014-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: highline
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.6.21
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.6.21
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: octokit
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
- - - '>='
31
+ - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '0'
33
+ version: 3.1.0
20
34
  type: :runtime
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
- - - '>='
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
25
74
  - !ruby/object:Gem::Version
26
75
  version: '0'
27
- description: A simple hello world gem
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.14'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.14'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.17'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.17'
111
+ description:
28
112
  email: public@johndbritton.com
29
113
  executables:
30
- - forkcollab
114
+ - clone_repos
115
+ - create_repos
116
+ - create_teams
117
+ - fork_collab
118
+ - push_files
31
119
  extensions: []
32
120
  extra_rdoc_files: []
33
121
  files:
34
- - .gitignore
35
- - bin/forkcollab
122
+ - ".gitignore"
123
+ - ".rspec"
124
+ - ".travis.yml"
125
+ - CONTRIBUTING.md
126
+ - Gemfile
127
+ - Guardfile
128
+ - LICENSE
129
+ - README.md
130
+ - Rakefile
131
+ - bin/clone_repos
132
+ - bin/create_repos
133
+ - bin/create_teams
134
+ - bin/fork_collab
135
+ - bin/push_files
36
136
  - lib/teachers_pet.rb
137
+ - lib/teachers_pet/actions/base.rb
138
+ - lib/teachers_pet/actions/clone_repos.rb
139
+ - lib/teachers_pet/actions/create_repos.rb
140
+ - lib/teachers_pet/actions/create_teams.rb
141
+ - lib/teachers_pet/actions/fork_collab.rb
142
+ - lib/teachers_pet/actions/push_files.rb
143
+ - lib/teachers_pet/configuration.rb
37
144
  - lib/teachers_pet/version.rb
145
+ - spec/actions/clone_repos_spec.rb
146
+ - spec/actions/create_teams_spec.rb
147
+ - spec/fixtures/instructors
148
+ - spec/fixtures/students
149
+ - spec/spec_helper.rb
38
150
  - teachers_pet.gemspec
39
- homepage: http://github.com/johndbritton/teachers_pet
151
+ homepage: http://github.com/education/teachers_pet
40
152
  licenses:
41
153
  - MIT
42
154
  metadata: {}
@@ -46,18 +158,24 @@ require_paths:
46
158
  - lib
47
159
  required_ruby_version: !ruby/object:Gem::Requirement
48
160
  requirements:
49
- - - '>='
161
+ - - ">="
50
162
  - !ruby/object:Gem::Version
51
163
  version: '0'
52
164
  required_rubygems_version: !ruby/object:Gem::Requirement
53
165
  requirements:
54
- - - '>='
166
+ - - ">="
55
167
  - !ruby/object:Gem::Version
56
168
  version: '0'
57
169
  requirements: []
58
170
  rubyforge_project:
59
- rubygems_version: 2.0.3
171
+ rubygems_version: 2.2.2
60
172
  signing_key:
61
173
  specification_version: 4
62
- summary: Command line tools for teachers
63
- test_files: []
174
+ summary: Command line tools to help teachers use GitHub in their classrooms
175
+ test_files:
176
+ - spec/actions/clone_repos_spec.rb
177
+ - spec/actions/create_teams_spec.rb
178
+ - spec/fixtures/instructors
179
+ - spec/fixtures/students
180
+ - spec/spec_helper.rb
181
+ has_rdoc:
data/bin/forkcollab DELETED
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'io/console'
4
- require 'octokit'
5
-
6
- puts "Create a personal access token: https://github.com/settings/applications"
7
- print "Token: "
8
- token = STDIN.noecho(&:gets).chomp
9
- puts
10
-
11
- print "Repository (name/owner): "
12
- nwo = gets.chomp
13
-
14
- client = Octokit::Client.new :access_token => token
15
-
16
- forks = client.forks(nwo)
17
-
18
- puts "Are you sure you want to add #{forks.count} users as collaborators on '#{nwo}'?"
19
- print "Please type the name of the repository to confirm: "
20
- confirm = gets.chomp
21
-
22
- if confirm == nwo
23
- forks.each do |fork|
24
- if fork.owner.type == "User"
25
- result = client.add_collab(nwo, fork.owner)
26
- puts "#{fork.owner.login} - #{result}"
27
- else
28
- puts "#{fork.owner.login} - false (Organization)"
29
- end
30
- end
31
- end