puppet-courseware-manager 0.5.0

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