puppet-courseware-manager 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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,5 @@
1
+ class Courseware::Dummy
2
+ def method_missing(meth, *args, &block)
3
+ raise "Cannot call #{meth} without a working courseware repository"
4
+ end
5
+ 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
+