Add solutions
authorMarius Gavrilescu <marius@ieval.ro>
Tue, 9 Dec 2014 16:54:24 +0000 (18:54 +0200)
committerMarius Gavrilescu <marius@ieval.ro>
Tue, 9 Dec 2014 16:54:24 +0000 (18:54 +0200)
MANIFEST
js/01-zepto-jquery.js [new file with mode: 0644]
js/10-bootstrap-modal.js [new file with mode: 0644]
lib/Plack/App/Gruntmaster.pm
lib/Plack/App/Gruntmaster/HTML.pm
tmpl/pb_entry.en
tmpl/sol.en [new file with mode: 0644]

index 8c81dff225b7367e13d5ce17939283fc608a7cdf..14d91b8ab9cd9fdb756e06cc50ce1f7441afb72d 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -11,7 +11,9 @@ css/themes/cosmo.css
 css/themes/cyborg.css
 css/themes/slate.css
 js/00-zepto.js
+js/01-zepto-jquery.js
 js/10-bootstrap-dropdown.js
+js/10-bootstrap-modal.js
 js/90-custom.js
 js/90-form.js
 lib/Plack/App/Gruntmaster.pm
@@ -31,6 +33,7 @@ tmpl/log_entry.en
 tmpl/pb.en
 tmpl/pb_entry.en
 tmpl/skel.en
+tmpl/sol.en
 tmpl/st.en
 tmpl/us.en
 tmpl/us_entry.en
diff --git a/js/01-zepto-jquery.js b/js/01-zepto-jquery.js
new file mode 100644 (file)
index 0000000..1fe7da3
--- /dev/null
@@ -0,0 +1 @@
+jQuery = Zepto;
diff --git a/js/10-bootstrap-modal.js b/js/10-bootstrap-modal.js
new file mode 100644 (file)
index 0000000..93891aa
--- /dev/null
@@ -0,0 +1,324 @@
+/* ========================================================================
+ * Bootstrap: modal.js v3.3.1
+ * http://getbootstrap.com/javascript/#modals
+ * ========================================================================
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // MODAL CLASS DEFINITION
+  // ======================
+
+  var Modal = function (element, options) {
+    this.options        = options
+    this.$body          = $(document.body)
+    this.$element       = $(element)
+    this.$backdrop      =
+    this.isShown        = null
+    this.scrollbarWidth = 0
+
+    if (this.options.remote) {
+      this.$element
+        .find('.modal-content')
+        .load(this.options.remote, $.proxy(function () {
+          this.$element.trigger('loaded.bs.modal')
+        }, this))
+    }
+  }
+
+  Modal.VERSION  = '3.3.1'
+
+  Modal.TRANSITION_DURATION = 300
+  Modal.BACKDROP_TRANSITION_DURATION = 150
+
+  Modal.DEFAULTS = {
+    backdrop: true,
+    keyboard: true,
+    show: true
+  }
+
+  Modal.prototype.toggle = function (_relatedTarget) {
+    return this.isShown ? this.hide() : this.show(_relatedTarget)
+  }
+
+  Modal.prototype.show = function (_relatedTarget) {
+    var that = this
+    var e    = $.Event('show.bs.modal', { relatedTarget: _relatedTarget })
+
+    this.$element.trigger(e)
+
+    if (this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = true
+
+    this.checkScrollbar()
+    this.setScrollbar()
+    this.$body.addClass('modal-open')
+
+    this.escape()
+    this.resize()
+
+    this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this))
+
+    this.backdrop(function () {
+      var transition = $.support.transition && that.$element.hasClass('fade')
+
+      if (!that.$element.parent().length) {
+        that.$element.appendTo(that.$body) // don't move modals dom position
+      }
+
+      that.$element
+        .show()
+        .scrollTop(0)
+
+      if (that.options.backdrop) that.adjustBackdrop()
+      that.adjustDialog()
+
+      if (transition) {
+        that.$element[0].offsetWidth // force reflow
+      }
+
+      that.$element
+        .addClass('in')
+        .attr('aria-hidden', false)
+
+      that.enforceFocus()
+
+      var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget })
+
+      transition ?
+        that.$element.find('.modal-dialog') // wait for modal to slide in
+          .one('bsTransitionEnd', function () {
+            that.$element.trigger('focus').trigger(e)
+          })
+          .emulateTransitionEnd(Modal.TRANSITION_DURATION) :
+        that.$element.trigger('focus').trigger(e)
+    })
+  }
+
+  Modal.prototype.hide = function (e) {
+    if (e) e.preventDefault()
+
+    e = $.Event('hide.bs.modal')
+
+    this.$element.trigger(e)
+
+    if (!this.isShown || e.isDefaultPrevented()) return
+
+    this.isShown = false
+
+    this.escape()
+    this.resize()
+
+    $(document).off('focusin.bs.modal')
+
+    this.$element
+      .removeClass('in')
+      .attr('aria-hidden', true)
+      .off('click.dismiss.bs.modal')
+
+    $.support.transition && this.$element.hasClass('fade') ?
+      this.$element
+        .one('bsTransitionEnd', $.proxy(this.hideModal, this))
+        .emulateTransitionEnd(Modal.TRANSITION_DURATION) :
+      this.hideModal()
+  }
+
+  Modal.prototype.enforceFocus = function () {
+    $(document)
+      .off('focusin.bs.modal') // guard against infinite focus loop
+      .on('focusin.bs.modal', $.proxy(function (e) {
+        if (this.$element[0] !== e.target && !this.$element.has(e.target).length) {
+          this.$element.trigger('focus')
+        }
+      }, this))
+  }
+
+  Modal.prototype.escape = function () {
+    if (this.isShown && this.options.keyboard) {
+      this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) {
+        e.which == 27 && this.hide()
+      }, this))
+    } else if (!this.isShown) {
+      this.$element.off('keydown.dismiss.bs.modal')
+    }
+  }
+
+  Modal.prototype.resize = function () {
+    if (this.isShown) {
+      $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this))
+    } else {
+      $(window).off('resize.bs.modal')
+    }
+  }
+
+  Modal.prototype.hideModal = function () {
+    var that = this
+    this.$element.hide()
+    this.backdrop(function () {
+      that.$body.removeClass('modal-open')
+      that.resetAdjustments()
+      that.resetScrollbar()
+      that.$element.trigger('hidden.bs.modal')
+    })
+  }
+
+  Modal.prototype.removeBackdrop = function () {
+    this.$backdrop && this.$backdrop.remove()
+    this.$backdrop = null
+  }
+
+  Modal.prototype.backdrop = function (callback) {
+    var that = this
+    var animate = this.$element.hasClass('fade') ? 'fade' : ''
+
+    if (this.isShown && this.options.backdrop) {
+      var doAnimate = $.support.transition && animate
+
+      this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />')
+        .prependTo(this.$element)
+        .on('click.dismiss.bs.modal', $.proxy(function (e) {
+          if (e.target !== e.currentTarget) return
+          this.options.backdrop == 'static'
+            ? this.$element[0].focus.call(this.$element[0])
+            : this.hide.call(this)
+        }, this))
+
+      if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
+
+      this.$backdrop.addClass('in')
+
+      if (!callback) return
+
+      doAnimate ?
+        this.$backdrop
+          .one('bsTransitionEnd', callback)
+          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
+        callback()
+
+    } else if (!this.isShown && this.$backdrop) {
+      this.$backdrop.removeClass('in')
+
+      var callbackRemove = function () {
+        that.removeBackdrop()
+        callback && callback()
+      }
+      $.support.transition && this.$element.hasClass('fade') ?
+        this.$backdrop
+          .one('bsTransitionEnd', callbackRemove)
+          .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) :
+        callbackRemove()
+
+    } else if (callback) {
+      callback()
+    }
+  }
+
+  // these following methods are used to handle overflowing modals
+
+  Modal.prototype.handleUpdate = function () {
+    if (this.options.backdrop) this.adjustBackdrop()
+    this.adjustDialog()
+  }
+
+  Modal.prototype.adjustBackdrop = function () {
+    this.$backdrop
+      .css('height', 0)
+      .css('height', this.$element[0].scrollHeight)
+  }
+
+  Modal.prototype.adjustDialog = function () {
+    var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
+
+    this.$element.css({
+      paddingLeft:  !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
+      paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
+    })
+  }
+
+  Modal.prototype.resetAdjustments = function () {
+    this.$element.css({
+      paddingLeft: '',
+      paddingRight: ''
+    })
+  }
+
+  Modal.prototype.checkScrollbar = function () {
+    this.bodyIsOverflowing = document.body.scrollHeight > document.documentElement.clientHeight
+    this.scrollbarWidth = this.measureScrollbar()
+  }
+
+  Modal.prototype.setScrollbar = function () {
+    var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10)
+    if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth)
+  }
+
+  Modal.prototype.resetScrollbar = function () {
+    this.$body.css('padding-right', '')
+  }
+
+  Modal.prototype.measureScrollbar = function () { // thx walsh
+    var scrollDiv = document.createElement('div')
+    scrollDiv.className = 'modal-scrollbar-measure'
+    this.$body.append(scrollDiv)
+    var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
+    this.$body[0].removeChild(scrollDiv)
+    return scrollbarWidth
+  }
+
+
+  // MODAL PLUGIN DEFINITION
+  // =======================
+
+  function Plugin(option, _relatedTarget) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.modal')
+      var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option)
+
+      if (!data) $this.data('bs.modal', (data = new Modal(this, options)))
+      if (typeof option == 'string') data[option](_relatedTarget)
+      else if (options.show) data.show(_relatedTarget)
+    })
+  }
+
+  var old = $.fn.modal
+
+  $.fn.modal             = Plugin
+  $.fn.modal.Constructor = Modal
+
+
+  // MODAL NO CONFLICT
+  // =================
+
+  $.fn.modal.noConflict = function () {
+    $.fn.modal = old
+    return this
+  }
+
+
+  // MODAL DATA-API
+  // ==============
+
+  $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) {
+    var $this   = $(this)
+    var href    = $this.attr('href')
+    var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7
+    var option  = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
+
+    if ($this.is('a')) e.preventDefault()
+
+    $target.one('show.bs.modal', function (showEvent) {
+      if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown
+      $target.one('hidden.bs.modal', function () {
+        $this.is(':visible') && $this.trigger('focus')
+      })
+    })
+    Plugin.call($target, option, this)
+  })
+
+}(jQuery);
index 4dd497f742c966d5ac164f67cd65dfab89ec950b..c8b04f3873a9bace214ef2620b3e6a87259dc0cf 100644 (file)
@@ -161,6 +161,10 @@ sub dispatch_request{
                        forbid problem->is_private;
                        response pb_entry => problem->name, db->problem_entry($_{problem}, $_{contest}, remote_user && remote_user->id);
                },
+               sub (/sol/:problem) {
+                       forbid !problem->is_in_archive;
+                       response sol => 'Solution of ' . problem->name, {solution => db->problem($_{problem})->solution};
+               },
 
                sub (/) { redispatch_to '/index' },
                sub (/:article) { [200, ['Content-Type' => 'text/html'], [render_article $_{article}, 'en']] }
index 04127828de03ee7704f2deb2887d0eba9142a2ee..ff998b96a4e654b999a5cebb5d60618506c63182 100644 (file)
@@ -118,11 +118,14 @@ sub process_pb_entry {
        my ($tree, %args) = @_;
        $tree->fid('owner')->edit_href(sub{s/owner_id/$args{owner}/});
        $tree->fid('job_log')->edit_href(sub{s/problem_id/$args{id}/});
+       $tree->fid('solution')->edit_href(sub{s/problem_id/$args{id}/});
        $tree->content_handler(
                statement => literal $args{statement},
                author    =>         $args{author},
                owner     =>         $args{owner_name} || $args{owner});
        if ($args{contest_stop}) {
+               $tree->fid('solution')->detach;
+               $tree->fid('solution_modal')->detach;
                my $countdown = $tree->fid('countdown');
                $countdown->attr('data-start' => $args{contest_start});
                $countdown->attr('data-stop' => $args{contest_stop});
@@ -133,6 +136,7 @@ sub process_pb_entry {
                $tree->fid('score')->replace_content(Gruntmaster::Data::calc_score($args{value}, $args{time} - $args{contest_start}, 0, $args{contest_stop} - $args{contest_start}));
        } else {
                $_->detach for $tree->fclass('rc'); # requires contest
+               $tree->fid('solution_modal')->fclass('modal-body')->replace_content(literal $args{solution});
        }
        if ($args{cansubmit}) {
                $tree->look_down(name => 'problem')->attr(value => $args{id});
@@ -144,6 +148,11 @@ sub process_pb_entry {
        }
 }
 
+sub process_sol {
+       my ($tree, %args) = @_;
+       $tree->content_handler(solution => literal $args{solution});
+}
+
 sub process_pb {
        my ($tree, %args) = @_;
        my $titer = sub {
index 4e0d324cbe18870b5d53ca252c95e0d04e53ece3..a3d7700b308888756cebafe174f3ac0e2924ac6d 100644 (file)
@@ -11,7 +11,8 @@
 <dt class="rc">Contest ends in</dt> <dd id="countdown" data-start="..." data-time="..." data-stop="..." class="rc">01:30</dd>
 </dl>
 
-<a href="/pb/problem_id/log/" id="job_log">Job log</a>
+<a href="/pb/problem_id/log/" id="job_log">Job log</a><br>
+<a href="/sol/problem_id" id="solution" data-toggle="modal" data-target="#solution_modal">Solution</a>
 
 <div id="submit">
 <h1>Submit solution</h1>
 </div>
 </div>
 </div>
+
+<div class="modal fade" id="solution_modal" role="dialog" aria-hidden="true" tabindex="-1">
+<div class="modal-dialog">
+<div class="modal-content">
+<div class="modal-header">
+<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+<h4 class="modal-title">Solution</h4>
+</div>
+<div class="modal-body"></div>
+</div>
+</div>
+</div>
diff --git a/tmpl/sol.en b/tmpl/sol.en
new file mode 100644 (file)
index 0000000..452d076
--- /dev/null
@@ -0,0 +1 @@
+<div id="solution"></div>
This page took 0.019851 seconds and 4 git commands to generate.