bind = require "utils.binding"
bind\bind_static 'executer', require 'facades.executer'
+bind\bind_static 'updater', require 'facades.updater'
bind\bind_static 'crypto', require 'services.crypto'
bind\bind_static 'uuidv4', require 'services.uuid'
bind\bind_static 'queries', require 'services.queries'
@navbar.selected = -1
@scripts = {}
+ @raw_scripts = {}
['index': "/"]: => redirect_to: @url_for 'problem'
['account.account': "/account"]: controller "account.account"
['leaderboard': '/leaderboard']: controller "leaderboard.view"
+ ['leaderboard.update': '/leaderboard/update']: controller "leaderboard.update"
['problem': '/problems']: controller "problem.problem"
['problem.description': '/problems/:problem_name']: controller "problem.problem"
req_secret (os.getenv 'REQ_SECRET')
executer_addr 'http://192.168.0.4:8080'
+ updater_addr 'http://192.168.0.5:5000'
postgres ->
-- Have to use a fixed ip since the container name
@flow 'csrf_validate'
assert_valid @params, {
- { "username", exists: true, min_length: 2, matches_pattern: "^%w+$" }
- { "nickname", exists: true, min_length: 2 }
+ { "username", exists: true, min_length: 1, matches_pattern: "^%w+$" }
+ { "nickname", exists: true, min_length: 1 }
{ "email", exists: true, min_length: 4, matches_pattern: "^%S+@%S+%.%S+$" }
{ "password", exists: true, min_length: 2 }
{ "password_confirmation", exists: true, min_length: 2, equals: @params.password, 'Passwords must be the same' }
for s in *routes.scripts
table.insert @scripts, s
+ if routes.raw_scripts
+ for s in *routes.raw_scripts
+ table.insert @raw_scripts, s
+
return if not routes.middleware
for middleware in *routes.middleware
make_controller
inject:
scoring: 'scoring'
+ updater: 'updater'
middleware: { 'internal_request' }
@scoring\score job.user_id, job.problem_id
@scoring\place!
+ @updater\push_submission_update job.id
+
json: { status: 'success' }
), =>
json: { status: 'error', errors: @errors }
--- /dev/null
+import make_controller from require "controllers.controller"
+Leaderboard = require 'views.ssr.leaderboard'
+
+make_controller
+ layout: false
+ middleware: { 'logged_in', 'competition_started' }
+
+ get: =>
+ @placements = @competition\get_leaderboard!
+
+ leaderboard_widget = Leaderboard @placements
+ leaderboard_widget\include_helper @
+ return { layout: false, status_code: 200, leaderboard_widget\render_to_string! }
middleware: { 'logged_in', 'competition_started' }
scripts: { "pie_chart" }
+ raw_scripts: {
+ "https://polyfill.io/v3/polyfill.min.js?features=es6"
+ "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
+ }
get: capture_errors_json =>
@navbar.selected = 1
status_widget = JobResult @job
status_widget\include_helper @
- return { layout: false, status: status_code, status_widget\render_to_string! }
\ No newline at end of file
+ return { layout: false, status: status_code, status_widget\render_to_string! }
middleware: { 'logged_in' }
scripts: { 'vendor/ace/ace', 'submission_reloader' }
+ raw_scripts: { '/socket.io/socket.io.js' }
get: capture_errors_json =>
@navbar.selected = 2
unless @job.user_id == @user.id
yield_error "You are not allowed to view this submission!"
- render: 'submission.view'
\ No newline at end of file
+ render: 'submission.view'
--- /dev/null
+config = (require 'lapis.config').get!
+http = require 'lapis.nginx.http'
+
+class UpdaterFacade
+ push_submission_update: (job_id) =>
+ http.simple "#{config.updater_addr}/submission_update?submission_ida=#{job_id}"
proxy_http_version 1.1;
proxy_pass $_url;
}
+
+ location ~* \.io {
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_http_version 1.1;
+
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $host;
+ proxy_pass http://192.168.0.5:5000;
+ proxy_redirect off;
+ }
}
}
score_user: (user_id) =>
for p in *@comp_problems
- problem = p\get_problem!
- @score_problem_for_user user_id, problem
+ @score user_id, p.problem_id
- score_problem: (problem_name) =>
- problem = Problems\find short_name: problem_name
+ score_problem: (problem_id) =>
for u in *@users
- @score_problem_for_user u.id, problem
+ @score u.id, problem_id
score_all: =>
for p in *@comp_problems
--- /dev/null
+$(document).ready ->
+ console.log "Hello!"
$.get '/submissions/status', { submission_id: submission_id }, (html, _, data) ->
$('#status-container').html html
- if data.status == 200
- setTimeout updateStatus, 100
-
$(document).ready ->
- updateStatus()
\ No newline at end of file
+ socket = io()
+ socket.emit "request-submission-updates", submission_id
+
+ socket.on 'update', ->
+ updateStatus()
--- /dev/null
+// Generated by CoffeeScript 2.4.1
+(function() {
+ $(document).ready(function() {
+ return console.log("Hello!");
+ });
+
+}).call(this);
+
+//# sourceMappingURL=leaderboard_update.js.map
--- /dev/null
+{
+ "version": 3,
+ "file": "leaderboard_update.js",
+ "sourceRoot": "..",
+ "sources": [
+ "coffee/leaderboard_update.coffee"
+ ],
+ "names": [],
+ "mappings": ";AAAA;EAAA,CAAA,CAAE,QAAF,CAAW,CAAC,KAAZ,CAAkB,QAAA,CAAA,CAAA;WACd,OAAO,CAAC,GAAR,CAAY,QAAZ;EADc,CAAlB;AAAA",
+ "sourcesContent": [
+ "$(document).ready ->\n console.log \"Hello!\"\n"
+ ]
+}
\ No newline at end of file
return $.get('/submissions/status', {
submission_id: submission_id
}, function(html, _, data) {
- $('#status-container').html(html);
- if (data.status === 200) {
- return setTimeout(updateStatus, 100);
- }
+ return $('#status-container').html(html);
});
};
$(document).ready(function() {
- return updateStatus();
+ var socket;
+ socket = io();
+ socket.emit("request-submission-updates", submission_id);
+ return socket.on('update', function() {
+ return updateStatus();
+ });
});
}).call(this);
"coffee/submission_reloader.coffee"
],
"names": [],
- "mappings": ";AAAA;AAAA,MAAA,aAAA,EAAA;;EAAA,aAAA,GAAgB,CAAC,IAAI,eAAJ,CAAoB,MAAM,CAAC,QAAQ,CAAC,MAApC,CAAD,CAA4C,CAAC,GAA7C,CAAiD,eAAjD;;EAEhB,YAAA,GAAe,QAAA,CAAA,CAAA;WACX,CAAC,CAAC,GAAF,CAAM,qBAAN,EAA6B;MAAE,aAAA,EAAe;IAAjB,CAA7B,EAA+D,QAAA,CAAC,IAAD,EAAO,CAAP,EAAU,IAAV,CAAA;MAC3D,CAAA,CAAE,mBAAF,CAAsB,CAAC,IAAvB,CAA4B,IAA5B;MAEA,IAAG,IAAI,CAAC,MAAL,KAAe,GAAlB;eACI,UAAA,CAAW,YAAX,EAAyB,GAAzB,EADJ;;IAH2D,CAA/D;EADW;;EAOf,CAAA,CAAE,QAAF,CAAW,CAAC,KAAZ,CAAkB,QAAA,CAAA,CAAA;WACd,YAAA,CAAA;EADc,CAAlB;AATA",
+ "mappings": ";AAAA;AAAA,MAAA,aAAA,EAAA;;EAAA,aAAA,GAAgB,CAAC,IAAI,eAAJ,CAAoB,MAAM,CAAC,QAAQ,CAAC,MAApC,CAAD,CAA4C,CAAC,GAA7C,CAAiD,eAAjD;;EAEhB,YAAA,GAAe,QAAA,CAAA,CAAA;WACX,CAAC,CAAC,GAAF,CAAM,qBAAN,EAA6B;MAAE,aAAA,EAAe;IAAjB,CAA7B,EAA+D,QAAA,CAAC,IAAD,EAAO,CAAP,EAAU,IAAV,CAAA;aAC3D,CAAA,CAAE,mBAAF,CAAsB,CAAC,IAAvB,CAA4B,IAA5B;IAD2D,CAA/D;EADW;;EAIf,CAAA,CAAE,QAAF,CAAW,CAAC,KAAZ,CAAkB,QAAA,CAAA,CAAA;AACjB,QAAA;IAAA,MAAA,GAAS,EAAA,CAAA;IACT,MAAM,CAAC,IAAP,CAAY,4BAAZ,EAA0C,aAA1C;WAEA,MAAM,CAAC,EAAP,CAAU,QAAV,EAAoB,QAAA,CAAA,CAAA;aACnB,YAAA,CAAA;IADmB,CAApB;EAJiB,CAAlB;AANA",
"sourcesContent": [
- "submission_id = (new URLSearchParams window.location.search).get 'submission_id'\n\nupdateStatus = ->\n $.get '/submissions/status', { submission_id: submission_id }, (html, _, data) ->\n $('#status-container').html html\n\n if data.status == 200\n setTimeout updateStatus, 100\n\n$(document).ready ->\n updateStatus()"
+ "submission_id = (new URLSearchParams window.location.search).get 'submission_id'\n\nupdateStatus = ->\n $.get '/submissions/status', { submission_id: submission_id }, (html, _, data) ->\n $('#status-container').html html\n\n$(document).ready ->\n\tsocket = io()\n\tsocket.emit \"request-submission-updates\", submission_id\n\n\tsocket.on 'update', ->\n\t\tupdateStatus()\n"
]
}
\ No newline at end of file
div class: 'split-1-1', ->
form method: 'POST', ->
input type: 'hidden', name: 'csrf_token', value: @csrf_token
-
+
label for: 'username', 'Username'
p -> input type: 'text', placeholder: 'Username', name: 'username', required: true, ''
- label for: 'password', 'Password'
- p -> input type: 'password', placeholder: 'Password', name: 'password', required: true, ''
+ div class: 'split-2', ->
+ div ->
+ label for: 'password', 'Password'
+ p -> input type: 'password', placeholder: 'Password', name: 'password', required: true, ''
- label for: 'password_confirmation', 'Confirm Password'
- p -> input type: 'password', placeholder: 'Confirm Password', name: 'password_confirmation', required: true, ''
+ div ->
+ label for: 'password_confirmation', 'Confirm Password'
+ p -> input type: 'password', placeholder: 'Confirm Password', name: 'password_confirmation', required: true, ''
label for: 'email', 'Email'
p -> input type: 'text', placeholder: 'Email', name: 'email', required: true, ''
html = require 'lapis.html'
import CompetitionProblems, LeaderboardProblems from require 'models'
+Leaderboard = require 'views.ssr.leaderboard'
-class Leaderboard extends html.Widget
+class LeaderboardView extends html.Widget
content: =>
h1 "#{@competition.name} - Leaderboard"
div class: 'content', ->
- div class: 'leaderboard', ->
- drawn_labels = false
- for place in *@placements
- @problems = place\get_problems!
- CompetitionProblems\include_in @problems, "problem_id",
- as: 'cp'
- flip: true
- local_key: 'problem_id'
- where: { competition_id: @competition.id }
-
- -- Sort the problems by letter
- prob.lnum = (prob.cp.letter\byte 1) for prob in *@problems
- table.sort @problems, (a, b) ->
- a.lnum < b.lnum
-
- unless drawn_labels
- div class: 'placement-labels', ->
- div "Place"
- div "Name"
- div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
- for prob in *@problems
- div "#{prob.cp.letter}"
- div "Score"
- drawn_labels = true
-
- div class: 'placement', ->
- div "#{place.place}"
- div "#{place\get_user!.nickname}"
-
- div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
- for prob in *@problems
- prob_status = switch prob.status
- when LeaderboardProblems.statuses.correct then "correct"
- when LeaderboardProblems.statuses.wrong then "wrong"
- when LeaderboardProblems.statuses.attempted then "attempted"
-
- div class: "#{prob_status}", ->
- div "#{prob.points}"
- div "#{prob.attempts}"
-
- div "#{place.score}"
-
+ widget (Leaderboard @placements)
for s in *@scripts
script type: "text/javascript", src: "/static/js/#{s}.js"
+ for s in *@raw_scripts
+ script type: "text/javascript", src: s
+
body ->
widget Navbar
widget ErrorList
class ProblemsView extends html.Widget
content: =>
- raw '<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
- <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>'
-
div class: 'sidebar-page-container', ->
div class: 'sidebar-problem-list', ->
widget (require 'views.partials.problem_sidebar')
--- /dev/null
+html = require 'lapis.html'
+import CompetitionProblems, LeaderboardProblems from require 'models'
+
+class Leaderboard extends html.Widget
+ new: (@placements) =>
+
+ content: =>
+ div class: 'leaderboard', ->
+ drawn_labels = false
+ for place in *@placements
+ @problems = place\get_problems!
+ CompetitionProblems\include_in @problems, "problem_id",
+ as: 'cp'
+ flip: true
+ local_key: 'problem_id'
+ where: { competition_id: @competition.id }
+ -- Sort the problems by letter
+ prob.lnum = (prob.cp.letter\byte 1) for prob in *@problems
+
+ table.sort @problems, (a, b) ->
+ a.lnum < b.lnum
+
+ unless drawn_labels
+ div class: 'placement-labels', ->
+ div "Place"
+ div "Name"
+ div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
+ for prob in *@problems
+ div "#{prob.cp.letter}"
+ div "Score"
+ drawn_labels = true
+
+ div class: 'placement', ->
+ div "#{place.place}"
+ div "#{place\get_user!.nickname}"
+
+ div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
+ for prob in *@problems
+ prob_status = switch prob.status
+ when LeaderboardProblems.statuses.correct then "correct"
+ when LeaderboardProblems.statuses.wrong then "wrong"
+ when LeaderboardProblems.statuses.attempted then "attempted"
+
+ div class: "#{prob_status}", ->
+ div "#{prob.points}"
+ div "#{prob.attempts}"
+
+ div "#{place.score}"
appnet:
ipv4_address: 192.168.0.4
+ updater:
+ env_file:
+ - config.env
+ build:
+ context: .
+ dockerfile: ./docker/updater/Dockerfile
+ volumes:
+ - ./updater/app:/app/app
+ command: node main.js
+ networks:
+ appnet:
+ ipv4_address: 192.168.0.5
+
postgres:
env_file:
- config.env
--- /dev/null
+FROM node:12.9.1
+
+RUN yarn global add coffeescript
+
+COPY ./updater/package.json /app/package.json
+WORKDIR /app
+RUN yarn
+
+COPY ./updater/main.js /app/main.js
+
+ENV PATH $(yarn global bin):$PATH
rej(-1);
}
- console.log("Updated job: ", job_id, status.status)
resolve(1);
}
)
--- /dev/null
+node_modules/
+.tup/
+yarn.lock
+*.js
--- /dev/null
+include_rules
--- /dev/null
+: foreach *.coffee |> coffee -c -o %B.js %f |> %B.js
\ No newline at end of file
--- /dev/null
+include_rules
--- /dev/null
+express = require 'express'
+app = express()
+server = require('http').createServer(app)
+io = require('socket.io')(server)
+
+class UpdateForwarder
+ constructor: ->
+ @channels = new Map()
+
+ add_channel: (channel_name) ->
+ unless @channels.has channel_name
+ @channels.set channel_name, []
+
+ join_channel: (channel_name, socket) ->
+ return unless @channels.has channel_name
+ @channels.get(channel_name).push(socket)
+
+ leave_channel: (channel_name, socket) ->
+ return unless @channels.has channel_name
+
+ sockets = @channels.get channel_name
+ idx = sockets.indexOf socket
+ sockets.splice idx, 1
+
+ leave: (socket) ->
+ for chan from @channels.values()
+ idx = chan.indexOf socket
+ if idx != -1
+ chan.splice idx, 1
+ return
+
+ push_update: (channel_name, param_match="") ->
+ return unless @channels.has channel_name
+
+ for sock in @channels.get channel_name
+ if param_match != ""
+ if sock.param == param_match
+ sock.emit 'update', {}
+ else
+ sock.emit 'update', {}
+ return
+
+update_forwarder = new UpdateForwarder()
+update_forwarder.add_channel "submission-updates"
+
+io.on 'connection', (socket) ->
+ # data is the submission id
+ socket.on 'request-submission-updates', (data) ->
+ socket.param = data
+ update_forwarder.join_channel "submission-updates", socket
+
+ socket.once 'disconnect', ->
+ update_forwarder.leave socket
+
+app.get '/submission_update', (req, res) ->
+ submission_id = req.query.submission_id
+ update_forwarder.push_update "submission-updates", submission_id
+
+ res.status 200
+ res.end()
+
+main = ->
+ port = 5000
+ console.log "Socket IO server running on port #{port}"
+ server.listen port
+
+module.exports = main
--- /dev/null
+main = require "./app/app.js"
+main()
--- /dev/null
+{
+ "name": "codebox-socketio",
+ "version": "1.0.0",
+ "description": "SocketIO part of Codebox",
+ "main": "main.js",
+ "author": "Brendan Hansen",
+ "license": "MIT",
+ "private": true,
+ "dependencies": {
+ "express": "^4.17.1",
+ "socket.io": "^2.3.0"
+ }
+}