showoff 0.16.1 → 0.16.2

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: f799ced051d346d7788de67fc8737fce0f374f2e
4
- data.tar.gz: 315d9a402e3ace5d16c1f6184518c0aeb84495ba
3
+ metadata.gz: bcb7404f722269ce7b4c571c0a7d34c72f76d684
4
+ data.tar.gz: 7a2b4b825af14a88c58fccd9a851f51587894d9f
5
5
  SHA512:
6
- metadata.gz: a8c3c7c3462fbb722ea6097d53264f271ca0e10ab111a71d9fb4f242adfa59f934926a05a42403535b011148c324be0832219f64682de87df3e29b1146f84fcd
7
- data.tar.gz: 564ec5a49755719797eb3f07f759f91e0be067e13fff279b84cdc9441a2d12ed4fc330cd67487aaeba2187a59d363998f4d73b4090730d3fc5829a7335f94c8c
6
+ metadata.gz: 180720b95f90f0948642258fca4c84327da441b85d7b23bb00203272ee05c7ed07c85af8665a5938c5c1b8ca683e98887aa1b98514ae4b59d32c9e8dde41af2e
7
+ data.tar.gz: 38036fa41718cf12953dd2db5a32cdc2b05840c11fea9652dfe8a525b0a095238a97bffbf643ddd5d4ea1ac57b0b2ce5af89929c7710761467e6323169dd3167
data/bin/showoff CHANGED
@@ -4,6 +4,7 @@ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
4
  require 'showoff'
5
5
  require 'showoff/version'
6
6
  require 'rubygems'
7
+ require 'fidget'
7
8
  require 'gli'
8
9
 
9
10
  # See https://github.com/davetron5000/gli/issues/196 for rationale for this silly wrapper
@@ -98,24 +99,24 @@ module Wrapper
98
99
  arg_name 'heroku_name'
99
100
  long_desc 'Creates the configuration files needed to Herokuize a Showoff presentation and then deploys it for you.'
100
101
  command :heroku do |c|
101
-
102
+
102
103
  c.desc 'add password protection to your heroku site'
103
104
  c.flag [:p,:password]
104
-
105
+
105
106
  c.desc 'force overwrite of existing Gemfile/.gems and config.ru files if they exist'
106
107
  c.switch [:f,:force]
107
-
108
+
108
109
  c.action do |global_options,options,args|
109
110
  raise "heroku_name is required" if args.empty?
110
111
  raise "Name must start with a letter and can only contain lowercase letters, numbers, and dashes." unless args.first =~ /^[a-z][a-z1-9-]*$/
111
-
112
+
112
113
  unless system('git remote get-url heroku')
113
114
  ShowOffUtils.command("heroku create #{args[0]}", "Please ensure that the heroku gem is installed and you're logged in.")
114
115
  end
115
-
116
+
116
117
  if ShowOffUtils.heroku(args[0],options[:f],options[:p])
117
118
  ShowOffUtils.command('bundle install', 'Please ensure that the bundler gem is installed.')
118
-
119
+
119
120
  begin
120
121
  ShowOffUtils.command('git add Procfile Gemfile Gemfile.lock config.ru')
121
122
  ShowOffUtils.command('git commit -m "Herokuized by Showoff"')
@@ -129,11 +130,11 @@ module Wrapper
129
130
  puts
130
131
  puts 'When done, please run "git push heroku master"'
131
132
  end
132
-
133
+
133
134
  if options[:p]
134
135
  puts "CAREFUL: you are commiting your access password - anyone with read access to the repo can access the preso\n\n"
135
136
  end
136
-
137
+
137
138
  puts 'Your presentation has been Herokuized. Run `heroku open` to see it.'
138
139
  end
139
140
  end
@@ -159,6 +160,9 @@ module Wrapper
159
160
  c.desc 'Disable content caching'
160
161
  c.switch :nocache
161
162
 
163
+ c.desc 'Prevent the computer from sleeping during your presentation'
164
+ c.switch :nosleep
165
+
162
166
  c.desc 'Port on which to run'
163
167
  c.default_value "9090"
164
168
  c.flag [:p, :port]
@@ -230,6 +234,11 @@ module Wrapper
230
234
 
231
235
  "
232
236
 
237
+ if options[:nosleep] and Fidget.current_process
238
+ puts '**** System sleep has been suspended. ****'
239
+ puts
240
+ end
241
+
233
242
  if options[:url]
234
243
  ShowOffUtils.clone(options[:git_url], options[:git_branch], options[:git_path]) do
235
244
  ShowOff.run!(options) do |server|
data/lib/showoff.rb CHANGED
@@ -140,6 +140,7 @@ class ShowOff < Sinatra::Application
140
140
  @section_major = 0
141
141
  @section_minor = 0
142
142
  @section_title = settings.showoff_config['name'] rescue 'Showoff Presentation'
143
+ @@slide_titles = [] # a list of generated slide names, used for cross references later.
143
144
 
144
145
  @logger.debug settings.pres_template
145
146
 
@@ -161,12 +162,16 @@ class ShowOff < Sinatra::Application
161
162
  begin
162
163
  @@counter = JSON.parse(File.read("#{settings.statsdir}/#{settings.viewstats}"))
163
164
 
164
- # port old format stats
165
+ # TODO: remove this logic 4/15/2017: port old format stats
165
166
  unless @@counter.has_key? 'user_agents'
166
- @@counter = { 'user_agents' => {}, 'pageviews' => @@counter }
167
+ @@counter['pageviews'] = @@counter
167
168
  end
169
+
170
+ @@counter['current'] ||= {}
171
+ @@counter['pageviews'] ||= {}
172
+ @@counter['user_agents'] ||= {}
168
173
  rescue
169
- @@counter = { 'user_agents' => {}, 'pageviews' => {} }
174
+ @@counter = { 'user_agents' => {}, 'pageviews' => {}, 'current' => {} }
170
175
  end
171
176
 
172
177
  # keeps track of form responses. In memory to avoid concurrence issues.
@@ -395,11 +400,9 @@ class ShowOff < Sinatra::Application
395
400
 
396
401
  # name the slide. If we've got multiple slides in this file, we'll have a sequence number
397
402
  # include that sequence number to index directly into that content
398
- if seq
399
- content += "<div class=\"content #{classes}\" ref=\"#{name}:#{seq.to_s}\">\n"
400
- else
401
- content += "<div class=\"content #{classes}\" ref=\"#{name}\">\n"
402
- end
403
+ ref = seq ? "#{name}:#{seq.to_s}" : name
404
+ content += "<div class=\"content #{classes}\" ref=\"#{ref}\">\n"
405
+ @@slide_titles << ref
403
406
 
404
407
  # renderers like wkhtmltopdf needs an <h1> tag to use for a section title, but only when printing.
405
408
  if opts[:print]
@@ -492,12 +495,6 @@ class ShowOff < Sinatra::Application
492
495
  # Turn this into a document for munging
493
496
  doc = Nokogiri::HTML::DocumentFragment.parse(result)
494
497
 
495
- if opts[:section]
496
- doc.css('div.notes-section').each do |section|
497
- section.remove unless section.attr('class').split.include? opts[:section]
498
- end
499
- end
500
-
501
498
  filename = File.join(settings.pres_dir, '_notes', "#{name}.md")
502
499
  @logger.debug "personal notes filename: #{filename}"
503
500
  if [nil, 'notes'].include? opts[:section] and File.file? filename
@@ -519,9 +516,68 @@ class ShowOff < Sinatra::Application
519
516
  end
520
517
  end
521
518
 
522
- # Now add a target so we open all external links from notes in a new window
519
+ doc.css('.callout.glossary').each do |item|
520
+ next unless item.content =~ /^([^|]+)\|([^:]+):(.*)$/
521
+ item['data-term'] = $1
522
+ item['data-target'] = $2
523
+ item['data-text'] = $3
524
+ item.content = $3
525
+
526
+ glossary = (item.attr('class').split - ['callout', 'glossary']).first
527
+ address = glossary ? "#{glossary}/#{$2}" : $2
528
+ frag = "<a class=\"processed label\" href=\"glossary://#{address}\">#{$1}</a>"
529
+
530
+ item.children.before(Nokogiri::HTML::DocumentFragment.parse(frag))
531
+ end
532
+
533
+ # Process links
523
534
  doc.css('a').each do |link|
524
- link.set_attribute('target', '_blank') unless link['href'].start_with? '#'
535
+ next if link['href'].start_with? '#'
536
+ next if link['class'].split.include? 'processed' rescue nil
537
+
538
+ # If these are glossary links, populate the notes/handouts sections
539
+ if link['href'].start_with? 'glossary://'
540
+ doc.add_child '<div class="notes-section notes"></div>' if doc.css('div.notes-section.notes').empty?
541
+ doc.add_child '<div class="notes-section handouts"></div>' if doc.css('div.notes-section.handouts').empty?
542
+
543
+ term = link.content
544
+ text = link['title']
545
+ href = link['href']
546
+ href.slice!('glossary://')
547
+
548
+ parts = href.split('/')
549
+ target = parts.pop
550
+ name = parts.pop # either the glossary name or nil
551
+
552
+ link['class'] = 'term'
553
+
554
+ label = link.clone
555
+ label['class'] = 'label processed'
556
+
557
+ frag = Nokogiri::HTML::DocumentFragment.parse('<p></p>')
558
+ definition = frag.children.first
559
+ definition['class'] = "callout glossary #{name}"
560
+ definition['data-term'] = term
561
+ definition['data-target'] = target
562
+ definition['data-text'] = text
563
+ definition.content = text
564
+ definition.children.before(label)
565
+
566
+ [doc.css('div.notes-section.notes'), doc.css('div.notes-section.handouts')].each do |section|
567
+ section.first.add_child(definition.clone)
568
+ end
569
+
570
+ else
571
+ # Add a target so we open all external links from notes in a new window
572
+ link.set_attribute('target', '_blank')
573
+ end
574
+ end
575
+
576
+ # finally, remove any sections we don't want to print
577
+ if opts[:section]
578
+ doc.css('div.notes-section').each do |section|
579
+ section.remove unless section.attr('class').split.include? opts[:section]
580
+ end
525
581
  end
526
582
 
527
583
  doc.to_html
@@ -552,31 +608,87 @@ class ShowOff < Sinatra::Application
552
608
  end
553
609
 
554
610
  def process_content_for_all_slides(content, num_slides, opts={})
611
+ # this has to be text replacement for now, since the string can appear in any context
555
612
  content.gsub!("~~~NUM_SLIDES~~~", num_slides.to_s)
613
+ doc = Nokogiri::HTML::DocumentFragment.parse(content)
556
614
 
557
615
  # Should we build a table of contents?
558
616
  if opts[:toc]
559
- frag = Nokogiri::HTML::DocumentFragment.parse ""
560
- toc = Nokogiri::XML::Node.new('div', frag)
561
- toc['id'] = 'toc'
562
- frag.add_child(toc)
563
-
564
- Nokogiri::HTML(content).css('div.subsection > h1:not(.section_title)').each do |section|
565
- entry = Nokogiri::XML::Node.new('div', frag)
566
- entry['class'] = 'tocentry'
567
- toc.add_child(entry)
568
-
569
- link = Nokogiri::XML::Node.new('a', frag)
570
- link['href'] = "##{section.parent.parent['id']}"
571
- link.content = section.content
572
- entry.add_child(link)
617
+ toc = Nokogiri::HTML::DocumentFragment.parse("<p id=\"toc\"></p>")
618
+
619
+ doc.css('div.subsection > h1:not(.section_title)').each do |section|
620
+ href = section.parent.parent['id']
621
+ frag = "<div class=\"tocentry\"><a href=\"##{href}\">#{section.content}</a></div>"
622
+ link = Nokogiri::HTML::DocumentFragment.parse(frag)
623
+
624
+ toc.children.first.add_child(link)
573
625
  end
574
626
 
575
627
  # swap out the tag, if found, with the table of contents
576
- content.gsub!("~~~TOC~~~", frag.to_html)
628
+ doc.at('p:contains("~~~TOC~~~")').replace(toc)
577
629
  end
578
630
 
579
- content
631
+ doc.css('.slide.glossary .content').each do |glossary|
632
+ name = (glossary.attr('class').split - ['content', 'glossary']).first
633
+ list = Nokogiri::HTML::DocumentFragment.parse('<ul class="glossary terms"></ul>')
634
+ seen = []
635
+
636
+ doc.css('.callout.glossary').each do |item|
637
+ target = (item.attr('class').split - ['callout', 'glossary']).first
638
+
639
+ # if the name matches or if we didn't name it to begin with.
640
+ next unless target == name
641
+
642
+ # the definition can exist in multiple places, so de-dup it here
643
+ term = item.attr('data-term')
644
+ next if seen.include? term
645
+ seen << term
646
+
647
+ # excrutiatingly find the parent slide content and grab the ref
648
+ # in a library less shitty, this would be something like
649
+ # $(this).parent().siblings('.content').attr('ref')
650
+ href = nil
651
+ item.ancestors('.slide').first.traverse do |element|
652
+ next if element['class'].nil?
653
+ next unless element['class'].split.include? 'content'
654
+
655
+ href = element.attr('ref').gsub('/', '_')
656
+ end
657
+
658
+ text = item.attr('data-text')
659
+ link = item.attr('data-target')
660
+ page = glossary.attr('ref')
661
+ anchor = "#{page}+#{link}"
662
+ next if href.nil? or text.nil? or link.nil?
663
+
664
+ frag = "<li><a id=\"#{anchor}\" class=\"label\">#{term}</a>#{text}<a href=\"##{href}\" class=\"return\">↩</a></li>"
665
+ item = Nokogiri::HTML::DocumentFragment.parse(frag)
666
+
667
+ list.children.first.add_child(item)
668
+ end
669
+
670
+ glossary.add_child(list)
671
+ end
672
+
673
+ # now fix all the links to point to the glossary page
674
+ doc.css('a').each do |link|
675
+ next if link['href'].nil?
676
+ next unless link['href'].start_with? 'glossary://'
677
+
678
+ href = link['href']
679
+ href.slice!('glossary://')
680
+
681
+ parts = href.split('/')
682
+ target = parts.pop
683
+ name = parts.pop # either the glossary name or nil
684
+
685
+ classes = name.nil? ? ".slide.glossary" : ".slide.glossary.#{name}"
686
+ href = doc.at("#{classes} .content").attr('ref') rescue nil
687
+
688
+ link['href'] = "##{href}+#{target}"
689
+ end
690
+
691
+ doc.to_html
580
692
  end
581
693
 
582
694
  # Find any lines that start with a <p>.(something), remove the ones tagged with
@@ -991,6 +1103,14 @@ class ShowOff < Sinatra::Application
991
1103
  # Provide a button in the sidebar for interactive editing if configured
992
1104
  @edit = settings.showoff_config['edit'] if @review
993
1105
 
1106
+ # store a cookie to tell clients apart. More reliable than using IP due to proxies, etc.
1107
+ unless request.cookies['client_id']
1108
+ @client_id = guid()
1109
+ response.set_cookie('client_id', @client_id)
1110
+ else
1111
+ @client_id = request.cookies['client_id']
1112
+ end
1113
+
994
1114
  erb :index
995
1115
  end
996
1116
 
@@ -1046,6 +1166,7 @@ class ShowOff < Sinatra::Application
1046
1166
 
1047
1167
  # if we have a cache and we're not asking to invalidate it
1048
1168
  return @@cache if (@@cache and params['cache'] != 'clear')
1169
+ @@slide_titles = []
1049
1170
  content = get_slides_html(:static=>static)
1050
1171
 
1051
1172
  # allow command line cache disabling
@@ -1081,12 +1202,79 @@ class ShowOff < Sinatra::Application
1081
1202
  erb :download
1082
1203
  end
1083
1204
 
1205
+ def stats_data()
1206
+ data = {}
1207
+ begin
1208
+
1209
+ # what are viewers looking at right now?
1210
+ now = Time.now.to_i # let's throw away viewers who haven't done anything in 5m
1211
+ active = @@counter['current'].select {|client, view| (now - view[1]).abs < 300 }
1212
+
1213
+ # percentage of stray viewers
1214
+ stray = active.select {|client, view| view[0] != @@current[:name] }
1215
+ stray_p = ((stray.size.to_f / active.size.to_f) * 100).to_i rescue 0
1216
+ data['stray_p'] = stray_p
1217
+
1218
+ # percentage of idle viewers
1219
+ idle = @@counter['current'].size - active.size
1220
+ idle_p = ((idle.to_f / @@counter['current'].size.to_f) * 100).to_i rescue 0
1221
+ data['idle_p'] = idle_p
1222
+
1223
+ viewers = @@slide_titles.map do |slide|
1224
+ count = active.select {|client, view| view[0] == slide }.size
1225
+ flags = (slide == @@current[:name]) ? 'current' : nil
1226
+ [count, slide, nil, flags]
1227
+ end
1228
+
1229
+ # trim the ends, if nobody's looking we don't much care.
1230
+ viewers.pop while viewers.last[0] == 0
1231
+ viewers.shift while viewers.first[0] == 0
1232
+ viewmax = viewers.max_by {|view| view[0] }.first
1233
+
1234
+ data['viewers'] = viewers
1235
+ data['viewmax'] = viewmax
1236
+ rescue => e
1237
+ @logger.warn "Not enough data to generate pageviews."
1238
+ @logger.debug e.message
1239
+ @logger.debug e.backtrace.first
1240
+ end
1241
+
1242
+ begin
1243
+ # current elapsed time for the zoomline view
1244
+ elapsed = @@slide_titles.map do |slide|
1245
+ if @@counter['pageviews'][slide].nil?
1246
+ time = 0
1247
+ else
1248
+ time = @@counter['pageviews'][slide].inject(0) do |outer, (viewer, views)|
1249
+ outer += views.inject(0) { |inner, view| inner += view['elapsed'] }
1250
+ end
1251
+ end
1252
+ string = Time.at(time).gmtime.strftime('%M:%S')
1253
+ flags = (slide == @@current[:name]) ? 'current' : nil
1254
+
1255
+ [ time, slide, string, flags ]
1256
+ end
1257
+ maxtime = elapsed.max_by {|view| view[0] }.first
1258
+
1259
+ data['elapsed'] = elapsed
1260
+ data['maxtime'] = maxtime
1261
+ rescue => e
1262
+ # expected if this is loaded before a presentation has been compiled
1263
+ @logger.warn "Not enough data to generate elapsed time."
1264
+ @logger.debug e.message
1265
+ @logger.debug e.backtrace.first
1266
+ end
1267
+
1268
+ data.to_json
1269
+ end
1270
+
1084
1271
  def stats()
1085
- if request.env['REMOTE_HOST'] == 'localhost'
1272
+ if localhost?
1086
1273
  # the presenter should have full stats in the erb
1087
1274
  @counter = @@counter['pageviews']
1088
1275
  end
1089
1276
 
1277
+ # for the full page view. Maybe to be disappeared
1090
1278
  @all = Hash.new
1091
1279
  @@counter['pageviews'].each do |slide, stats|
1092
1280
  @all[slide] = 0
@@ -1095,10 +1283,6 @@ class ShowOff < Sinatra::Application
1095
1283
  end
1096
1284
  end
1097
1285
 
1098
- # most and least five viewed slides
1099
- @least = @all.sort_by {|slide, time| time}[0..4]
1100
- @most = @all.sort_by {|slide, time| -time}[0..4]
1101
-
1102
1286
  erb :stats
1103
1287
  end
1104
1288
 
@@ -1131,7 +1315,7 @@ class ShowOff < Sinatra::Application
1131
1315
  end
1132
1316
 
1133
1317
 
1134
- def self.do_static(args, opts = {})
1318
+ def self.do_static(args, opts = {})
1135
1319
  args ||= [] # handle nil arguments
1136
1320
  what = args[0] || "index"
1137
1321
  opt = args[1]
@@ -1319,18 +1503,20 @@ class ShowOff < Sinatra::Application
1319
1503
  (0..15).to_a.map{|a| rand(16).to_s(16)}.join
1320
1504
  end
1321
1505
 
1322
- def valid_cookie?
1506
+ def valid_presenter_cookie?
1507
+ return false if @@cookie.nil?
1323
1508
  (request.cookies['presenter'] == @@cookie)
1324
1509
  end
1325
1510
 
1326
1511
  post '/form/:id' do |id|
1327
- @logger.warn("Saving form answers from ip:#{request.ip} for id:##{id}")
1512
+ client_id = request.cookies['client_id']
1513
+ @logger.warn("Saving form answers from ip:#{request.ip} with ID of #{client_id} for id:##{id}")
1328
1514
 
1329
1515
  form = params.reject { |k,v| ['splat', 'captures', 'id'].include? k }
1330
1516
 
1331
1517
  # make sure we've got a bucket for this form, then save our answers
1332
1518
  @@forms[id] ||= {}
1333
- @@forms[id][request.ip] = form
1519
+ @@forms[id][client_id] = form
1334
1520
 
1335
1521
  form.to_json
1336
1522
  end
@@ -1445,13 +1631,13 @@ class ShowOff < Sinatra::Application
1445
1631
  begin
1446
1632
  control = JSON.parse(data)
1447
1633
 
1448
- @logger.warn "#{control.inspect}"
1634
+ @logger.debug "#{control.inspect}"
1449
1635
 
1450
1636
  case control['message']
1451
1637
  when 'update'
1452
1638
  # websockets don't use the same auth standards
1453
1639
  # we use a session cookie to identify the presenter
1454
- if valid_cookie?
1640
+ if valid_presenter_cookie?
1455
1641
  name = control['name']
1456
1642
  slide = control['slide'].to_i
1457
1643
  increment = control['increment'].to_i rescue 0
@@ -1472,29 +1658,37 @@ class ShowOff < Sinatra::Application
1472
1658
 
1473
1659
  when 'register'
1474
1660
  # save a list of presenters
1475
- if valid_cookie?
1661
+ if valid_presenter_cookie?
1476
1662
  remote = request.env['REMOTE_HOST'] || request.env['REMOTE_ADDR']
1477
1663
  settings.presenters << ws
1478
1664
  @logger.warn "Registered new presenter: #{remote}"
1479
1665
  end
1480
1666
 
1481
1667
  when 'track'
1482
- remote = valid_cookie? ? 'presenter' : (request.env['REMOTE_HOST'] || request.env['REMOTE_ADDR'])
1668
+ remote = valid_presenter_cookie? ? 'presenter' : request.cookies['client_id']
1483
1669
  slide = control['slide']
1484
- time = control['time'].to_f
1485
1670
 
1486
- # record the UA of the client if we haven't seen it before
1487
- @@counter['user_agents'][remote] ||= request.user_agent
1671
+ if control.has_key? 'time'
1672
+ time = control['time'].to_f
1673
+
1674
+ # record the UA of the client if we haven't seen it before
1675
+ @@counter['user_agents'][remote] ||= request.user_agent
1488
1676
 
1489
- views = @@counter['pageviews']
1490
- # a bucket for this slide
1491
- views[slide] ||= Hash.new
1492
- # a bucket of slideviews for this address
1493
- views[slide][remote] ||= Array.new
1494
- # and add this slide viewing to the bucket
1495
- views[slide][remote] << { 'elapsed' => time, 'timestamp' => Time.now.to_i, 'presenter' => @@current[:name] }
1677
+ views = @@counter['pageviews']
1678
+ # a bucket for this slide
1679
+ views[slide] ||= Hash.new
1680
+ # a bucket of slideviews for this address
1681
+ views[slide][remote] ||= Array.new
1682
+ # and add this slide viewing to the bucket
1683
+ views[slide][remote] << { 'elapsed' => time, 'timestamp' => Time.now.to_i, 'presenter' => @@current[:name] }
1684
+
1685
+ @logger.debug "Logged #{time} on slide #{slide} for #{remote}"
1686
+
1687
+ else
1688
+ @@counter['current'][remote] = [slide, Time.now.to_i]
1689
+ @logger.debug "Recorded current slide #{slide} for #{remote}"
1690
+ end
1496
1691
 
1497
- @logger.debug "Logged #{time} on slide #{slide} for #{remote}"
1498
1692
 
1499
1693
  when 'position'
1500
1694
  ws.send( { 'current' => @@current[:number] }.to_json ) unless @@cookie.nil?