puppet-courseware-manager 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.txt +60 -0
- data/LICENSE +202 -0
- data/README.txt +100 -0
- data/bin/courseware +146 -0
- data/lib/courseware.rb +165 -0
- data/lib/courseware/cache.rb +35 -0
- data/lib/courseware/composer.rb +135 -0
- data/lib/courseware/dummy.rb +5 -0
- data/lib/courseware/generator.rb +91 -0
- data/lib/courseware/help.rb +108 -0
- data/lib/courseware/manager.rb +129 -0
- data/lib/courseware/manager/validators.rb +72 -0
- data/lib/courseware/printer.rb +224 -0
- data/lib/courseware/repository.rb +189 -0
- data/lib/courseware/utils.rb +118 -0
- data/lib/courseware/version.rb +4 -0
- metadata +107 -0
data/lib/courseware.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
class Courseware
|
2
|
+
require 'courseware/cache'
|
3
|
+
require 'courseware/composer'
|
4
|
+
require 'courseware/generator'
|
5
|
+
require 'courseware/manager'
|
6
|
+
require 'courseware/printer'
|
7
|
+
require 'courseware/repository'
|
8
|
+
require 'courseware/utils'
|
9
|
+
|
10
|
+
def initialize(config, configfile)
|
11
|
+
@config = config
|
12
|
+
@configfile = configfile
|
13
|
+
@cache = Courseware::Cache.new(config)
|
14
|
+
@generator = Courseware::Generator.new(config)
|
15
|
+
@composer = Courseware::Composer.new(config)
|
16
|
+
|
17
|
+
if Courseware::Repository.repository?
|
18
|
+
@repository = Courseware::Repository.new(config)
|
19
|
+
@manager = Courseware::Manager.new(config, @repository)
|
20
|
+
else
|
21
|
+
require 'courseware/dummy'
|
22
|
+
@repository = Courseware::Dummy.new
|
23
|
+
@manager = Courseware::Dummy.new
|
24
|
+
$logger.debug "Running outside a valid git repository."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def options(opts)
|
29
|
+
raise ArgumentError, "One or two arguments expected, not #{opts.inspect}" unless opts.size.between?(1,2)
|
30
|
+
if opts.include? :section
|
31
|
+
section = opts[:section]
|
32
|
+
setting, value = opts.reject {|key, value| key == :section }.first
|
33
|
+
@config[section][setting] = value
|
34
|
+
else
|
35
|
+
setting, value = opts.first
|
36
|
+
@config[setting] = value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def print(subject)
|
41
|
+
$logger.debug "Printing #{subject}"
|
42
|
+
|
43
|
+
#TODO: This should not be duplicated!
|
44
|
+
opts = {
|
45
|
+
:course => @manager.coursename,
|
46
|
+
:prefix => @manager.prefix,
|
47
|
+
:version => @repository.current(@manager.prefix),
|
48
|
+
:variant => Courseware.choose_variant,
|
49
|
+
}
|
50
|
+
Courseware::Printer.new(@config, opts) do |printer|
|
51
|
+
subject.each do |item|
|
52
|
+
case item
|
53
|
+
when :handouts
|
54
|
+
printer.handouts
|
55
|
+
|
56
|
+
when :exercises
|
57
|
+
printer.exercises
|
58
|
+
|
59
|
+
when :solutions
|
60
|
+
printer.solutions
|
61
|
+
|
62
|
+
when :guide
|
63
|
+
printer.guide
|
64
|
+
|
65
|
+
else
|
66
|
+
$logger.error "The #{item} document type does not exist."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def wordcount(subject)
|
73
|
+
@manager.wordcount(subject)
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate(subject)
|
77
|
+
$logger.debug "Generating #{subject}"
|
78
|
+
if subject.first == :skeleton
|
79
|
+
subject.shift
|
80
|
+
subject.each do |course|
|
81
|
+
@generator.skeleton course.to_s
|
82
|
+
end
|
83
|
+
else
|
84
|
+
subject.each do |item|
|
85
|
+
case item
|
86
|
+
when :config
|
87
|
+
@generator.saveconfig @configfile
|
88
|
+
|
89
|
+
when :styles
|
90
|
+
course = @manager.coursename
|
91
|
+
prefix = @manager.prefix
|
92
|
+
@generator.styles(course, @repository.current(prefix))
|
93
|
+
|
94
|
+
when :links
|
95
|
+
@generator.links
|
96
|
+
|
97
|
+
when :metadata
|
98
|
+
@generator.metadata
|
99
|
+
|
100
|
+
when :rakefile
|
101
|
+
@generator.rakefile
|
102
|
+
|
103
|
+
when :shared
|
104
|
+
@generator.shared
|
105
|
+
|
106
|
+
else
|
107
|
+
$logger.error "I don't know how to generate #{item}!"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate(subject)
|
115
|
+
$logger.debug "Validating #{subject}"
|
116
|
+
subject.each do |item|
|
117
|
+
case item
|
118
|
+
when :obsolete
|
119
|
+
@manager.obsolete
|
120
|
+
|
121
|
+
when :missing
|
122
|
+
@manager.missing
|
123
|
+
|
124
|
+
when :lint
|
125
|
+
@manager.lint
|
126
|
+
|
127
|
+
else
|
128
|
+
$logger.error "I don't know how to do that yet!"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
$logger.warn "Found #{@manager.errors} errors and #{@manager.warnings} warnings."
|
133
|
+
exit @manager.errors + @manager.warnings
|
134
|
+
end
|
135
|
+
|
136
|
+
def release(subject)
|
137
|
+
case subject
|
138
|
+
|
139
|
+
when :major, :minor, :point
|
140
|
+
$logger.debug "Creating a #{subject} release."
|
141
|
+
@manager.release subject
|
142
|
+
|
143
|
+
when :notes
|
144
|
+
$logger.debug "Generating release notes."
|
145
|
+
@manager.releasenotes
|
146
|
+
|
147
|
+
else
|
148
|
+
$logger.error "I don't know how to do that yet!"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def compose(subject)
|
153
|
+
@composer.build(subject)
|
154
|
+
end
|
155
|
+
|
156
|
+
def package(subject)
|
157
|
+
@composer.package(subject)
|
158
|
+
end
|
159
|
+
|
160
|
+
def debug
|
161
|
+
require 'pry'
|
162
|
+
binding.pry
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
class Courseware::Cache
|
3
|
+
|
4
|
+
def initialize(config)
|
5
|
+
@config = config
|
6
|
+
|
7
|
+
clone
|
8
|
+
update
|
9
|
+
end
|
10
|
+
|
11
|
+
def clone
|
12
|
+
templates = File.join(@config[:cachedir], 'templates')
|
13
|
+
|
14
|
+
FileUtils.mkdir_p(@config[:cachedir]) unless File.exists? @config[:cachedir]
|
15
|
+
system('git', 'clone', @config[:templates], templates) unless File.exists? templates
|
16
|
+
end
|
17
|
+
|
18
|
+
def update
|
19
|
+
$logger.debug "Updating template cache..."
|
20
|
+
git('templates', 'pull', '--quiet', 'origin', 'master')
|
21
|
+
git('templates', 'reset', '--quiet', '--hard', 'HEAD')
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear
|
25
|
+
FileUtils.rm_rf @config[:cachedir]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def git(repo, *args)
|
30
|
+
worktree = File.join(@config[:cachedir], repo)
|
31
|
+
gitdir = File.join(worktree, '.git')
|
32
|
+
system('git', '--git-dir', gitdir, '--work-tree', worktree, *args)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
class Courseware::Composer
|
2
|
+
def initialize(config, repository=nil)
|
3
|
+
@config = config
|
4
|
+
@repository = repository || Courseware::Repository.new(config)
|
5
|
+
|
6
|
+
if File.exists?(@config[:presfile])
|
7
|
+
@showoff = JSON.parse(File.read(@config[:presfile]))
|
8
|
+
@coursename = @showoff['name']
|
9
|
+
@prefix = @showoff['name'].gsub(' ', '_')
|
10
|
+
@sections = @showoff['sections']
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(subject)
|
15
|
+
courselevel!
|
16
|
+
|
17
|
+
if subject.nil?
|
18
|
+
display_tags
|
19
|
+
raise "Please re-run this task with a list of tags to include."
|
20
|
+
end
|
21
|
+
|
22
|
+
subject.each do |tag|
|
23
|
+
key = "tag:#{tag}"
|
24
|
+
@showoff['sections'].each do |section|
|
25
|
+
raise 'All sections must be represented as Hashes to customize.' unless section.is_a? Hash
|
26
|
+
|
27
|
+
if section.include? key
|
28
|
+
raise "A section in showoff.json refers to #{section[key]}, which does not exist." unless File.exist? section[key]
|
29
|
+
section['include'] = section[key]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# normalize the output and trim unused tags
|
35
|
+
@showoff['sections'].each do |section|
|
36
|
+
section.select! { |k,v| k == 'include' }
|
37
|
+
end
|
38
|
+
@showoff['sections'].reject! { |section| section.empty? }
|
39
|
+
|
40
|
+
name = Courseware.question('What would you like to call this variant?', 'custom').gsub(/\W+/, '_')
|
41
|
+
desc = Courseware.question("Enter a customized description if you'd like:")
|
42
|
+
|
43
|
+
@showoff['description'] = desc if desc
|
44
|
+
|
45
|
+
File.write("#{name}.json", JSON.pretty_generate(@showoff))
|
46
|
+
puts "Run your presentation with `showoff serve -f #{name}.json` or `rake present`"
|
47
|
+
end
|
48
|
+
|
49
|
+
def package(subject)
|
50
|
+
courselevel!
|
51
|
+
on_release!
|
52
|
+
pristine!
|
53
|
+
|
54
|
+
subject ||= Courseware.choose_variant
|
55
|
+
subject = subject.to_s
|
56
|
+
content = JSON.parse(File.read(subject))
|
57
|
+
variant = File.basename(subject, '.json')
|
58
|
+
current = @repository.current(@coursename)
|
59
|
+
|
60
|
+
if variant == 'showoff'
|
61
|
+
variant = @prefix
|
62
|
+
output = @prefix
|
63
|
+
else
|
64
|
+
output = "#{@prefix}-#{variant}"
|
65
|
+
end
|
66
|
+
|
67
|
+
FileUtils.rm_rf "build/#{variant}"
|
68
|
+
FileUtils.mkdir_p "build/#{variant}"
|
69
|
+
FileUtils.cp subject, "build/#{variant}/showoff.json"
|
70
|
+
|
71
|
+
content['sections'].each do |section|
|
72
|
+
path = section['include']
|
73
|
+
next if path.nil?
|
74
|
+
|
75
|
+
dir = File.dirname path
|
76
|
+
FileUtils.mkdir_p "build/#{variant}/#{dir}"
|
77
|
+
FileUtils.cp path, "build/#{variant}/#{path}"
|
78
|
+
|
79
|
+
files = JSON.parse(File.read(path))
|
80
|
+
files.each do |file|
|
81
|
+
FileUtils.cp "#{dir}/#{file}", "build/#{variant}/#{dir}/#{file}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# support is special
|
86
|
+
FileUtils.cp_r '../_support', "build/#{variant}/_support"
|
87
|
+
FileUtils.rm_f "build/#{variant}/_support/*.pem"
|
88
|
+
FileUtils.rm_f "build/#{variant}/_support/*.pub"
|
89
|
+
FileUtils.rm_f "build/#{variant}/_support/aws_credentials"
|
90
|
+
|
91
|
+
# duplicate from cwd to build/variant everything not already copied
|
92
|
+
Dir.glob('*').each do |thing|
|
93
|
+
next if thing == 'build'
|
94
|
+
next if File.extname(thing) == '.json'
|
95
|
+
next if File.exist? "build/#{variant}/#{thing}"
|
96
|
+
FileUtils.ln_s "../../#{thing}", "build/#{variant}/#{thing}"
|
97
|
+
end
|
98
|
+
|
99
|
+
system("tar -C build --dereference -czf build/#{output}-#{current}.tar.gz #{variant}")
|
100
|
+
if Courseware.confirm("Would you like to clean up the output build directory?")
|
101
|
+
FileUtils.rm_rf "build/#{variant}"
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def display_tags
|
108
|
+
courselevel!
|
109
|
+
|
110
|
+
raise "This course has no tags to choose from." unless @showoff['tags']
|
111
|
+
|
112
|
+
puts
|
113
|
+
puts 'Available tags:'
|
114
|
+
@showoff['tags'].each do |tag, desc|
|
115
|
+
printf " * %-10s: %s\n", tag, desc
|
116
|
+
end
|
117
|
+
puts
|
118
|
+
end
|
119
|
+
|
120
|
+
def courselevel!
|
121
|
+
raise 'This task must be run from within a course directory' unless @repository.courselevel?
|
122
|
+
end
|
123
|
+
|
124
|
+
def pristine!
|
125
|
+
raise 'Your working directory has local modifications or untracked files' unless @repository.clean?
|
126
|
+
end
|
127
|
+
|
128
|
+
def on_release!
|
129
|
+
count = @repository.outstanding_commits(@prefix)
|
130
|
+
unless count == 0
|
131
|
+
raise "There have been #{count} commits since release. Either make a release or check out a tagged release."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
class Courseware::Generator
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def saveconfig(configfile)
|
12
|
+
$logger.info "Saving configuration to #{configfile}"
|
13
|
+
File.write(configfile, @config.to_yaml)
|
14
|
+
end
|
15
|
+
|
16
|
+
def skeleton(dest)
|
17
|
+
source = File.join(@config[:cachedir], 'templates', 'skeleton')
|
18
|
+
FileUtils.cp_r(source, dest)
|
19
|
+
|
20
|
+
Dir.chdir(dest) do
|
21
|
+
course = metadata()
|
22
|
+
styles(course, '0.0.1')
|
23
|
+
|
24
|
+
default = Courseware::Repository.repository? ? 'P' : 'S'
|
25
|
+
case Courseware.question('Should this be a (S)tandalone course or share assets with a (P)arent repository?', default)
|
26
|
+
when 's','S'
|
27
|
+
shared
|
28
|
+
$logger.warn "Don't forget to create the GitHub repository for your new course following the EDU standard."
|
29
|
+
else
|
30
|
+
links
|
31
|
+
$logger.warn "Don't forget to add, commit, & push this new course directory to the repository."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def shared
|
37
|
+
source = File.join(@config[:cachedir], 'templates', 'shared')
|
38
|
+
FileUtils.cp_r(source, '.')
|
39
|
+
end
|
40
|
+
|
41
|
+
def rakefile
|
42
|
+
raise 'Update the toplevel Rakefile instead.' if File.symlink? 'Rakefile'
|
43
|
+
source = File.join(@config[:cachedir], 'templates', 'Rakefile')
|
44
|
+
FileUtils.cp(source, 'Rakefile')
|
45
|
+
end
|
46
|
+
|
47
|
+
def styles(course=nil, version=nil)
|
48
|
+
File.open(@config[:stylesheet], 'w') do |f|
|
49
|
+
$logger.info "Updating stylesheet for #{course} version #{version}."
|
50
|
+
template = File.join(@config[:cachedir], 'templates', 'showoff.css.erb')
|
51
|
+
f.write ERB.new(File.read(template), nil, '-').result(binding)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def links
|
56
|
+
filename = File.join(@config[:cachedir], 'templates', 'links.json')
|
57
|
+
JSON.parse(File.read(filename)).each do |file, target|
|
58
|
+
$logger.info "Linking #{file} -> #{target}"
|
59
|
+
FileUtils.rm_rf(file) if File.exists?(file)
|
60
|
+
FileUtils.ln_sf(target, file)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def metadata
|
65
|
+
location = File.basename Dir.pwd
|
66
|
+
if File.exists?(@config[:presfile])
|
67
|
+
metadata = JSON.parse(File.read(@config[:presfile]))
|
68
|
+
coursename = metadata['name']
|
69
|
+
description = metadata['description']
|
70
|
+
component = metadata['issues'].match(/components=(\d*)/)[1]
|
71
|
+
else
|
72
|
+
template = File.join(@config[:cachedir], 'templates', 'showoff.json')
|
73
|
+
metadata = JSON.parse(File.read(template))
|
74
|
+
coursename = location.capitalize
|
75
|
+
description = nil
|
76
|
+
component = nil
|
77
|
+
end
|
78
|
+
coursename = Courseware.question('Choose a short name for this course:', coursename)
|
79
|
+
description = Courseware.question('Please enter a description of the course:', description)
|
80
|
+
component = Courseware.get_component(component)
|
81
|
+
|
82
|
+
metadata['name'] = coursename
|
83
|
+
metadata['description'] = description
|
84
|
+
metadata['edit'] = "https://github.com/puppetlabs/#{@config[:github][:repository]}/edit/qa/review/#{coursename}/#{location}/"
|
85
|
+
metadata['issues'] = "http://tickets.puppetlabs.com/secure/CreateIssueDetails!init.jspa?pid=10302&issuetype=1&components=#{component}&priority=6&summary="
|
86
|
+
|
87
|
+
$logger.info "Updating presentation file #{@config[:presfile]}"
|
88
|
+
File.write(@config[:presfile], JSON.pretty_generate(metadata))
|
89
|
+
coursename
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
class Courseware
|
2
|
+
def self.help
|
3
|
+
IO.popen("less", "w") do |less|
|
4
|
+
less.puts <<-EOF.gsub(/^ {6}/, '')
|
5
|
+
Courseware Manager
|
6
|
+
|
7
|
+
SYNOPSIS
|
8
|
+
courseware [-c CONFIG] [-d] <verb> [subject] [subject] ...
|
9
|
+
|
10
|
+
DESCRIPTION
|
11
|
+
Manage the development lifecycle of Puppet courseware. This tool is not
|
12
|
+
required for presenting the material or for contributing minor updates.
|
13
|
+
|
14
|
+
The following verbs are recognized:
|
15
|
+
|
16
|
+
* print
|
17
|
+
Render course material as PDF files. This verb accepts one or more of the
|
18
|
+
following arguments, where the default is all.
|
19
|
+
|
20
|
+
Arguments (optional):
|
21
|
+
handouts: Generate handout notes
|
22
|
+
exercises: Generate the lab exercise manual
|
23
|
+
solutions: Generate the solution guide
|
24
|
+
guide: Generate the instructor guide
|
25
|
+
|
26
|
+
* watermark
|
27
|
+
Render watermarked PDF files. Accepts same arguements as the `print` verb.
|
28
|
+
|
29
|
+
* generate or update
|
30
|
+
Build new or update certain kinds of configuration. By default, this will
|
31
|
+
update the stylesheet.
|
32
|
+
|
33
|
+
Arguments (optional):
|
34
|
+
skeleton <name>: Build a new course directory named <name> and generate
|
35
|
+
required metadata for a Showoff presentation.
|
36
|
+
|
37
|
+
config: Write current configuration to a `courseware.yaml` file.
|
38
|
+
|
39
|
+
styles: Generate or update the stylesheet for the current version.
|
40
|
+
|
41
|
+
links: Ensure that all required symlinks are in place.
|
42
|
+
|
43
|
+
metadata: Interactively generate or update the `showoff.json` file.
|
44
|
+
|
45
|
+
* validate
|
46
|
+
Runs validation checks on the presentation. Defaults to running all the checks.
|
47
|
+
|
48
|
+
Validators:
|
49
|
+
obsolete: Lists all unreferenced images and slides. This reference checks
|
50
|
+
all slides and all CSS stylesheets. Case sensitive.
|
51
|
+
|
52
|
+
missing: Lists all slides that are missing. Note that this does not check
|
53
|
+
for missing image files yet. Case sensitive.
|
54
|
+
|
55
|
+
lint: Runs a markdown linter on each slide file, using our own style
|
56
|
+
definition.
|
57
|
+
|
58
|
+
* release [type]
|
59
|
+
Orchestrate a courseware release. Defaults to `point`.
|
60
|
+
|
61
|
+
All instructors are expected to deliver the most current point release, except
|
62
|
+
in extraordinary cases. We follow Semver, as closely as it can be adapted to
|
63
|
+
classroom usage. Instructors can trust that updates with high potential to cause
|
64
|
+
classroom disruptions will never make it into a point release.
|
65
|
+
|
66
|
+
http://semver.org
|
67
|
+
|
68
|
+
Release types:
|
69
|
+
major: This is a major release with "breaking" changes, such as a major
|
70
|
+
product update, or significant classroom workflow changes. This
|
71
|
+
is not necessarily tied to product releases. Instructors should
|
72
|
+
expect to spend significant time studying the new material thoroughly.
|
73
|
+
|
74
|
+
minor: This indicates a significant change in content. Instructors
|
75
|
+
should take extra time to review updates in minor releases.
|
76
|
+
The release cadence is roughly once a quarter, give or take.
|
77
|
+
|
78
|
+
point: Release early and release often. Changes made in the regular
|
79
|
+
maintenance cycle will typically fit into this category.
|
80
|
+
|
81
|
+
notes: Display release notes since last release and copy to clipboard.
|
82
|
+
|
83
|
+
* wordcount [type]
|
84
|
+
Display a simple wordcount of one or more content types.
|
85
|
+
|
86
|
+
Arguments (optional):
|
87
|
+
handouts: Counts words in the student handout guide
|
88
|
+
exercises: Counts words in the lab exercise manual
|
89
|
+
solutions: Counts words in the solution guide
|
90
|
+
|
91
|
+
* compose [comma,separated,list,of,tags]
|
92
|
+
Generates a variant of the complete course, using tags defined in `showoff.json`.
|
93
|
+
The practical effect of this action is to generate a new presentation `.json` file,
|
94
|
+
which can be displayed directly by passing the `-f` flag to Showoff, or by choosing
|
95
|
+
a variant in the classroom `rake present` task.
|
96
|
+
|
97
|
+
* package [variant.json]
|
98
|
+
Package up a standalone form of a given variant of the presentation. You can pass
|
99
|
+
in a `variant.json` file, or choose from a menu. Tarballs will be saved into the
|
100
|
+
`build` directory and you can optionally retain the full working directory.
|
101
|
+
|
102
|
+
* help
|
103
|
+
You're looking at it.
|
104
|
+
EOF
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|