Class: Courseware

Inherits:
Object
  • Object
show all
Defined in:
lib/courseware.rb,
lib/courseware/help.rb,
lib/courseware/utils.rb,
lib/courseware/version.rb

Defined Under Namespace

Classes: Cache, Composer, Dummy, Generator, Manager, Printer, Repository

Constant Summary collapse

VERSION =
'0.6.2'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config, configfile) ⇒ Courseware

Returns a new instance of Courseware.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/courseware.rb', line 10

def initialize(config, configfile)
  @config     = config
  @configfile = configfile
  @cache      = Courseware::Cache.new(config)
  @generator  = Courseware::Generator.new(config)
  @composer   = Courseware::Composer.new(config)

  if Courseware::Repository.repository?
    @repository = Courseware::Repository.new(config)
    @manager    = Courseware::Manager.new(config, @repository)
  else
    require 'courseware/dummy'
    @repository = Courseware::Dummy.new(config)
    @manager    = Courseware::Dummy.new(config)
    $logger.debug "Running outside a valid git repository."
  end
end

Class Method Details

.bailout?(message, required = false) ⇒ Boolean

Returns:

  • (Boolean)


38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/courseware/utils.rb', line 38

def self.bailout?(message, required=false)
  if required
    print "#{message} Continue? [y/N]: "
    options = ['y', 'yes']
  else
    print "#{message} Continue? [Y/n]: "
    options = [ 'y', 'yes', '']
  end
  unless options.include? STDIN.gets.strip.downcase
    if block_given?
      yield
    end
    raise "User cancelled"
  end
end

.choose(message, options, default = nil) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/courseware/utils.rb', line 68

def self.choose(message, options, default = nil)
  body  = ""
  options.each_with_index { |item, index| body << "\n[#{index}] #{item}" }
  dialog(message, body)

  ans = nil
  loop do
    if default
      print "Choose an option by typing its number [#{default}]: "
      ans = STDIN.gets.strip
      ans = (ans == "") ? default : Integer(ans) rescue nil
    else
      print "Choose an option by typing its number: "
      ans = Integer(STDIN.gets.strip) rescue nil
    end

    break if (0...options.size).include? ans
  end

  ans
end

.choose_variantObject



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/courseware/utils.rb', line 90

def self.choose_variant
  variants = Dir.glob('*.json')
  return :none if variants.empty?
  return 'showoff.json' if variants == ['showoff.json']

  maxlen = variants.max { |x,y| x.size <=> y.size }.size - 4 # accomodate for the extension we're stripping


  # Ensures that the default `showoff.json` is listed first
  variants.unshift "showoff.json"
  variants.uniq!

  options = variants.map do |variant|
    data = JSON.parse(File.read(variant))
    name = (variant == 'showoff.json') ? 'default' : File.basename(variant, '.json')
    desc = data['description']

    "%#{maxlen}s: %s" % [name, desc]
  end

  idx = Courseware.choose("This course has several variants available:", options, 0)
  variants[idx]
end

.confirm(message, default = true) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/courseware/utils.rb', line 25

def self.confirm(message, default=true)
  return default unless STDIN.tty?

  if default
    print "#{message} [Y/n]: "
    answers = [ 'y', 'yes', '' ]
  else
    print "#{message} [N/y]: "
    answers = [ 'y', 'yes' ]
  end
  answers.include? STDIN.gets.strip.downcase
end

.dialog(header, body = nil, width = 80) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/courseware/utils.rb', line 54

def self.dialog(header, body=nil, width=80)
  width -= 6

  puts '################################################################################'
  puts "## #{header[0..width].center(width)} ##"
  if body
    puts '##----------------------------------------------------------------------------##'
    body.wrap(width).split("\n").each do |line|
      printf "## %-#{width}s ##\n", line
    end
  end
  puts '################################################################################'
end

.get_component(initial) ⇒ Object



119
120
121
122
123
124
125
# File 'lib/courseware/utils.rb', line 119

def self.get_component(initial)
  puts 'The component ID for this course can be found at:'
  puts ' * https://tickets.puppetlabs.com/browse/COURSES/?selectedTab=com.atlassian.jira.jira-projects-plugin:components-panel'
  puts
  # grab the number ending the response--either the ID from the URL or the whole string
  question('Please enter the component ID or copy & paste in its URL:', initial)[/(\d*)$/]
end

.grep(match, filename) ⇒ Object



127
128
129
# File 'lib/courseware/utils.rb', line 127

def self.grep(match, filename)
  File.read(filename) =~ Regexp.new(match)
end

.helpObject



2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/courseware/help.rb', line 2

def self.help
  IO.popen("less", "w") do |less|
    less.puts <<-EOF.gsub(/^ {6}/, '')
                           Courseware Manager

      SYNOPSIS
        courseware [-c CONFIG] [-d] <verb> [subject] [subject] ...

      DESCRIPTION
        Manage the development lifecycle of Puppet courseware. This tool is not
        required for presenting the material or for contributing minor updates.

        The following verbs are recognized:

        * print
            Render course material as PDF files. This verb accepts one or more of the
            following arguments, where the default is all.

            Arguments (optional):
               handouts: Generate handout notes
              exercises: Generate the lab exercise manual
              solutions: Generate the solution guide
                  guide: Generate the instructor guide

        * watermark
            Render watermarked PDF files. Accepts same arguements as the `print` verb.

        * generate or update
            Build new or update certain kinds of configuration. By default, this will
            update the stylesheet.

            Arguments (optional):
              skeleton <name>: Build a new course directory named <name> and generate
                               required metadata for a Showoff presentation.

                       config: Write current configuration to a `courseware.yaml` file.

                       styles: Generate or update the stylesheet for the current version.

                        links: Ensure that all required symlinks are in place.

                     metadata: Interactively generate or update the `showoff.json` file.

        * validate
            Runs validation checks on the presentation. Defaults to running all the checks.

            Validators:
                obsolete: Lists all unreferenced images and slides. This reference checks all
                          slides and all CSS stylesheets. This validation is case sensitive
                          and should be run from the toplevel courseware root directory.

                 missing: Lists all slides and images that are missing. This validation is case
                          sensitive and should be run from within an individual course directory.

                    lint: Runs a markdown linter on each slide file, using our own style
                          definition. This should be run within a course directory.

        * release [type]
            Orchestrate a courseware release. Defaults to `point`.

            All instructors are expected to deliver the most current point release, except
            in extraordinary cases. We follow Semver, as closely as it can be adapted to
            classroom usage. Instructors can trust that updates with high potential to cause
            classroom disruptions will never make it into a point release.

                                     http://semver.org

            Release types:
                    major: This is a major release with "breaking" changes, such as a major
                           product update, or significant classroom workflow changes. This
                           is not necessarily tied to product releases. Instructors should
                           expect to spend significant time studying the new material thoroughly.

                    minor: This indicates a significant change in content. Instructors
                           should take extra time to review updates in minor releases.
                           The release cadence is roughly once a quarter, give or take.

                    point: Release early and release often. Changes made in the regular
                           maintenance cycle will typically fit into this category.

                    notes: Display release notes since last release and copy to clipboard.

        * wordcount [type]
            Display a simple wordcount of one or more content types.

            Arguments (optional):
               handouts: Counts words in the student handout guide
              exercises: Counts words in the lab exercise manual
              solutions: Counts words in the solution guide

        * compose [comma,separated,list,of,tags]
            Generates a variant of the complete course, using tags defined in `showoff.json`.
            The practical effect of this action is to generate a new presentation `.json` file,
            which can be displayed directly by passing the `-f` flag to Showoff, or by choosing
            a variant in the classroom `rake present` task.

        * package [variant.json]
            Package up a standalone form of a given variant of the presentation. You can pass
            in a `variant.json` file, or choose from a menu. Tarballs will be saved into the
            `build` directory and you can optionally retain the full working directory.

        * help
            You're looking at it.
    EOF
  end
end

.increment(version, type = :point) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/courseware/utils.rb', line 131

def self.increment(version, type=:point)
  major, minor, point = version.split('.')
  case type
  when :major
    major.sub!(/^v/, '')  # chop off the v if needed
    "v#{major.to_i + 1}.0.0"

  when :minor
    "#{major}.#{minor.to_i + 1}.0"

  when :point
    "#{major}.#{minor}.#{point.to_i + 1}"

  end
end

.parse_showoff(filename) ⇒ Object

TODO: we could use some validation here



115
116
117
# File 'lib/courseware/utils.rb', line 115

def self.parse_showoff(filename)
  JSON.parse(File.read(filename))
end

.question(message, default = nil, required = false) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/courseware/utils.rb', line 5

def self.question(message, default=nil, required=false)
  unless STDIN.tty?
    raise "The question '#{message}' requires an answer and cannot be run non-interactively." if required
    return default
  end

  loop do
    if default
      print "#{message} [#{default}] "
    else
      print "#{message} "
    end

    answer = STDIN.gets.strip
    next if required and answer.empty?

    return answer.empty? ? default : answer
  end
end

Instance Method Details

#compose(subject) ⇒ Object



151
152
153
# File 'lib/courseware.rb', line 151

def compose(subject)
  @composer.build(subject)
end

#debugObject



159
160
161
162
# File 'lib/courseware.rb', line 159

def debug
  require 'pry'
  binding.pry
end

#generate(subject) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/courseware.rb', line 75

def generate(subject)
  $logger.debug "Generating #{subject}"
  if subject.first == :skeleton
    subject.shift
    subject.each do |course|
      @generator.skeleton course.to_s
    end
  else
    subject.each do |item|
      case item
      when :config
        @generator.saveconfig @configfile

      when :styles
        course = @manager.coursename
        prefix = @manager.prefix
        @generator.styles(course, @repository.current(prefix))

      when :links
        @generator.links

      when :metadata
        @generator.

      when :rakefile
        @generator.rakefile

      when :shared
        @generator.shared

      else
        $logger.error "I don't know how to generate #{item}!"
      end
    end
  end

end

#options(opts) ⇒ Object

Raises:

  • (ArgumentError)


28
29
30
31
32
33
34
35
36
37
38
# File 'lib/courseware.rb', line 28

def options(opts)
  raise ArgumentError, "One or two arguments expected, not #{opts.inspect}" unless opts.size.between?(1,2)
  if opts.include? :section
    section = opts[:section]
    setting, value = opts.reject {|key, value| key == :section }.first
    @config[section][setting] = value
  else
    setting, value = opts.first
    @config[setting] = value
  end
end

#package(subject) ⇒ Object



155
156
157
# File 'lib/courseware.rb', line 155

def package(subject)
  @composer.package(subject)
end


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/courseware.rb', line 40

def print(subject)
  $logger.debug "Printing #{subject}"

  #TODO: This should not be duplicated!
  opts = {
    :course  => @manager.coursename,
    :prefix  => @manager.prefix,
    :version => @repository.current(@manager.prefix),
  }
  Courseware::Printer.new(@config, opts) do |printer|
    subject.each do |item|
      case item
      when :handouts
        printer.handouts

      when :exercises
        printer.exercises

      when :solutions
        printer.solutions

      when :guide
        printer.guide

      else
        $logger.error "The #{item} document type does not exist."
      end
    end
  end
end

#release(subject) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/courseware.rb', line 135

def release(subject)
  case subject

  when :major, :minor, :point
    $logger.debug "Creating a #{subject} release."
    @manager.release subject

  when :notes
    $logger.debug "Generating release notes."
    @manager.releasenotes

  else
    $logger.error "I don't know how to do that yet!"
  end
end

#validate(subject) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/courseware.rb', line 113

def validate(subject)
  $logger.debug "Validating #{subject}"
  subject.each do |item|
    case item
    when :obsolete
      @manager.obsolete

    when :missing
      @manager.missing

    when :lint
      @manager.lint

    else
      $logger.error "I don't know how to do that yet!"
    end
  end

  $logger.warn "Found #{@manager.errors} errors and #{@manager.warnings} warnings."
  exit @manager.errors + @manager.warnings
end

#wordcount(subject) ⇒ Object



71
72
73
# File 'lib/courseware.rb', line 71

def wordcount(subject)
  @manager.wordcount(subject)
end