diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 4c67779cb..b594e43ba 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -258,3 +258,14 @@ module.exports = class Level extends CocoModel else options.url = "/db/course/#{courseID}/levels/#{levelOriginalID}/next" @fetch(options) + + getSolutions: -> + return [] unless hero = _.find (@get("thangs") ? []), id: 'Hero Placeholder' + return [] unless config = _.find(hero.components ? [], (x) -> x.config?.programmableMethods?.plan)?.config + solutions = _.cloneDeep config.programmableMethods.plan.solutions ? [] + for solution in solutions + try + solution.source = _.template(solution.source)(config?.programmableMethods?.plan.context) + catch e + console.error "Problem with template and solution comments for", @get('slug'), e + solutions diff --git a/app/templates/editor/verifier/verifier-view.jade b/app/templates/editor/verifier/verifier-view.jade index a3a58c0eb..ff9c014e5 100644 --- a/app/templates/editor/verifier/verifier-view.jade +++ b/app/templates/editor/verifier/verifier-view.jade @@ -2,80 +2,101 @@ extends /templates/base-flat block content .container - div.row(style="margin-top: 20px") + div.row.verifier-row div.col-sm-3 - p.alert.alert-success(style="padding: 5px") + p.alert.alert-success | Passed: #{view.passed} div.col-sm-3 - p.alert.alert-warning(style="padding: 5px") + p.alert.alert-warning | Test Problem: #{view.problem} div.col-sm-3 - p.alert.alert-danger(style="padding: 5px") + p.alert.alert-danger | Failed: #{view.failed} div.col-sm-3 - p.alert.alert-info(style="padding: 5px") + p.alert.alert-info | To Run: #{view.testCount - view.passed - view.problem - view.failed} - if view.levelIDs + if view.levelsByCampaign + .form.form-inline + .row + each campaignInfo, campaign in view.levelsByCampaign + .form-group.campaign-mix + - var campaignID = "campaign-" + campaign + "-checkbox"; + input(id=campaignID, type="checkbox", checked=campaignInfo.checked, disabled=!!view.tests) + label(for=campaignID)= campaign + ': ' + campaignInfo.levels.length + .row + each codeLanguage in view.codeLanguages + .form-group.code-language-mix + - var codeLanguageID = "code-language-" + codeLanguage.id + "-checkbox"; + input(id=codeLanguageID, type="checkbox", checked=codeLanguage.checked, disabled=!!view.tests) + label(for=codeLanguageID)= codeLanguage.id + .pull-right + button.btn.btn-primary#go-button(disabled=!!view.tests) Start Tests + + if view.levelsToLoad && !view.tests .progress - .progress-bar.progress-bar-success(role="progressbar" style="width: #{100*view.passed/view.testCount}%") - .progress-bar.progress-bar-warning(role="progressbar" style="width: #{100*view.problem/view.testCount}%") - .progress-bar.progress-bar-danger(role="progressbar" style="width: #{100*view.failed/view.testCount}%") + .progress-bar.progress-bar-success(role="progressbar" style="width: #{100*(1 - view.levelsToLoad/view.initialLevelsToLoad)}%") + if view.tests + if view.levelIDs + .progress + .progress-bar.progress-bar-success(role="progressbar" style="width: #{100*view.passed/view.testCount}%") + .progress-bar.progress-bar-warning(role="progressbar" style="width: #{100*view.problem/view.testCount}%") + .progress-bar.progress-bar-danger(role="progressbar" style="width: #{100*view.failed/view.testCount}%") - each test, id in view.tests - - if (test.state == 'no-solution') - - continue; - if test.level - .pull-right - - var last = test.level.get('slug') + view.linksQueryString - a.btn.btn-primary(href="/editor/verifier/" + last) Focus - a.btn.btn-success(href="/play/level/" + last) Play - a.btn.btn-warning(href="/editor/level/" + last) Edit - a.btn.btn-default(data-target='#verifier-test-' + id, data-toggle="collapse") Toggle + each test, id in view.tests + - if (test.state == 'no-solution') + - continue; + if test.level + .pull-right + - var last = test.level.get('slug') + view.linksQueryString + a.btn.btn-primary(href="/editor/verifier/" + last) Focus + a.btn.btn-success(href="/play/level/" + last) Play + a.btn.btn-warning(href="/editor/level/" + last) Edit + a.btn.btn-default(data-target='#verifier-test-' + id, data-toggle="collapse") Toggle - if !test.goals - h2(style='color: orange')= test.level.get('name') - small= ' in ' + test.language + '' - else if test.isSuccessful() - h2(style='color: green')= test.level.get('name') - small= ' in ' + test.language + '' - else - h2(style='color: red')= test.level.get('name') - small= ' in ' + test.language + '' + if !test.goals + h2.test-running= test.level.get('name') + small= ' in ' + test.language + '' + else if test.isSuccessful() + h2.test-success= test.level.get('name') + small= ' in ' + test.language + '' + else + h2.test-failed= test.level.get('name') + small= ' in ' + test.language + '' - div.row(class=(test.isSuccessful() && id > 1 ? 'collapse' : 'collapse in'), id='verifier-test-' + id) - div.col-xs-8 - if test.solution - pre #{test.solution.source} - else - h4 Error Loading Test - pre #{test.error} - div.col-xs-4.well - if test.goals - if test.frames == test.solution.frameCount - div(style='color: green') ✓ Frames: #{test.frames} + div.row(class=(test.isSuccessful() && id > 1 ? 'collapse' : 'collapse in'), id='verifier-test-' + id) + div.col-xs-8 + if test.solution + pre #{test.solution.source} else - div(style='color: red') ✘ Frames: #{test.frames} vs #{test.solution.frameCount} - - each v,k in test.goals || [] - if !test.solution.goals - div(style='color: orange') ? #{k} (#{v.status}) - else if v.status == test.solution.goals[k] - div(style='color: green') ✓ #{k} (#{v.status}) + h4 Error Loading Test + pre #{test.error} + div.col-xs-4.well + if test.goals + if test.frames == test.solution.frameCount + div.test-success ✓ Frames: #{test.frames} else - div(style='color: red') ✘ #{k} (#{v.status} vs #{test.solution.goals[k]}) - else - h3 Pending.... + div.test-failed ✘ Frames: #{test.frames} vs #{test.solution.frameCount} - if test.error - pre(style="color: red") #{test.error} - - if test.userCodeProblems.length - h4(style="color: red") User Code Problems - pre(style="color: red") #{JSON.stringify(test.userCodeProblems, null, 2)} + each v,k in test.goals || [] + if !test.solution.goals + div.test-running ? #{k} (#{v.status}) + else if v.status == test.solution.goals[k] + div.test-success ✓ #{k} (#{v.status}) + else + div.test-failed ✘ #{k} (#{v.status} vs #{test.solution.goals[k]}) + else + h3 Pending.... - else - h1 Loading Level... + if test.error + pre.test-faile #{test.error} - // TODO: show last frame hash + if test.userCodeProblems.length + h4.test-failed User Code Problems + pre.test-failed #{JSON.stringify(test.userCodeProblems, null, 2)} + + else + h1 Loading Level... + + // TODO: show last frame hash diff --git a/app/views/editor/verifier/VerifierTest.coffee b/app/views/editor/verifier/VerifierTest.coffee index 024c4f3ba..0826cad0c 100644 --- a/app/views/editor/verifier/VerifierTest.coffee +++ b/app/views/editor/verifier/VerifierTest.coffee @@ -42,15 +42,8 @@ module.exports = class VerifierTest extends CocoClass @register() configureSession: (session, level) => - # TODO: reach into and find hero and get the config from the solution try - hero = _.find level.get("thangs"), id: "Hero Placeholder" - config = _.find(hero.components, (x) -> x.config?.programmableMethods?.plan).config - programmable = config.programmableMethods.plan - solution = _.find (programmable.solutions ? []), language: session.get('codeLanguage') - solution.source = _.template(solution.source)(config?.programmableMethods?.plan.context) - session.solution = solution - + session.solution = _.find level.getSolutions(), language: session.get('codeLanguage') session.set 'heroConfig', session.solution.heroConfig session.set 'code', {'hero-placeholder': plan: session.solution.source} state = session.get 'state' diff --git a/app/views/editor/verifier/VerifierView.coffee b/app/views/editor/verifier/VerifierView.coffee index d3a618c7a..80dbfb019 100644 --- a/app/views/editor/verifier/VerifierView.coffee +++ b/app/views/editor/verifier/VerifierView.coffee @@ -4,66 +4,102 @@ RootView = require 'views/core/RootView' template = require 'templates/editor/verifier/verifier-view' VerifierTest = require './VerifierTest' SuperModel = require 'models/SuperModel' +Campaigns = require 'collections/Campaigns' +Level = require 'models/Level' module.exports = class VerifierView extends RootView className: 'style-flat' template: template id: 'verifier-view' + events: + 'click #go-button': 'onClickGoButton' + constructor: (options, @levelID) -> super options # TODO: sort tests by unexpected result first @passed = 0 @failed = 0 @problem = 0 + @testCount = 0 - testLevels = [ - 'dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'kounter-kithwise', 'crawlways-of-kithgard', - 'enemy-mine', 'illusory-interruption', 'forgetful-gemsmith', 'signs-and-portents', 'favorable-odds', - 'true-names', 'the-prisoner', 'banefire', 'the-raised-sword', 'kithgard-librarian', 'fire-dancing', - 'loop-da-loop', 'haunted-kithmaze', 'riddling-kithmaze', 'descending-further', 'the-second-kithmaze', - 'dread-door', 'cupboards-of-kithgard', 'hack-and-dash', 'known-enemy', 'master-of-names', 'lowly-kithmen', - 'closing-the-distance', 'tactical-strike', 'the-skeleton', 'a-mayhem-of-munchkins', 'the-final-kithmaze', - 'the-gauntlet', 'radiant-aura', 'kithgard-gates', 'destroying-angel', 'deadly-dungeon-rescue', - 'breakout', 'attack-wisely', 'kithgard-mastery', 'kithgard-apprentice', 'robot-ragnarok', - 'defense-of-plainswood', 'peasant-protection', 'forest-fire-dancing', 'course-winding-trail', - 'patrol-buster', 'endangered-burl', 'thumb-biter', 'gems-or-death', 'village-guard', 'thornbush-farm', - 'back-to-back', 'ogre-encampment', 'woodland-cleaver', 'shield-rush', 'range-finder', 'munchkin-swarm', - 'stillness-in-motion', 'the-agrippa-defense', 'backwoods-bombardier', 'coinucopia', 'copper-meadows', - 'drop-the-flag', 'mind-the-trap', 'signal-corpse', 'rich-forager', + if @levelID + @levelIDs = [@levelID] + @testLanguages = ['python', 'javascript', 'java', 'lua', 'coffeescript'] + @startTestingLevels() + else + @campaigns = new Campaigns() + @supermodel.trackRequest @campaigns.fetch(data: {project: 'slug,type,levels'}) + @campaigns.comparator = (m) -> + ['intro', 'course-2', 'course-3', 'course-4', 'course-5', 'course-6', 'course-8', + 'dungeon', 'forest', 'desert', 'mountain', 'glacier', 'volcano'].indexOf(m.get('slug')) - 'the-mighty-sand-yak', 'oasis', 'sarven-road', 'sarven-gaps', 'thunderhooves', 'minesweeper', - 'medical-attention', 'sarven-sentry', 'keeping-time', 'hoarding-gold', 'decoy-drill', 'continuous-alchemy', - 'dust', 'desert-combat', 'sarven-savior', 'lurkers', 'preferential-treatment', 'sarven-shepherd', - 'shine-getter', + onLoaded: -> + super() + return if @levelID + @filterCampaigns() + @filterCodeLanguages() + @render() - 'a-fine-mint', 'borrowed-sword', 'cloudrip-commander', 'crag-tag', - 'hunters-and-prey', 'hunting-party', - 'leave-it-to-cleaver', 'library-tactician', 'mad-maxer', 'mad-maxer-strikes-back', - 'mirage-maker', 'mixed-unit-tactics', 'mountain-mercenaries', - 'noble-sacrifice', 'odd-sandstorm', 'ogre-gorge-gouger', 'reaping-fire', - 'return-to-thornbush-farm', 'ring-bearer', 'sand-snakes', - 'slalom', 'steelclaw-gap', 'the-geometry-of-flowers', - 'the-two-flowers', 'timber-guard', 'toil-and-trouble', 'village-rover', - 'vital-powers', 'zoo-keeper', - ] + filterCampaigns: -> + @levelsByCampaign = {} + for campaign in @campaigns.models when campaign.get('type') in ['course', 'hero'] and campaign.get('slug') isnt 'picoctf' + @levelsByCampaign[campaign.get('slug')] ?= {levels: [], checked: true} + campaignInfo = @levelsByCampaign[campaign.get('slug')] + for levelID, level of campaign.get('levels') when level.type not in ['hero-ladder', 'course-ladder', 'game-dev'] + campaignInfo.levels.push level.slug + filterCodeLanguages: -> + defaultLanguages = utils.getQueryVariable('languages', 'python,javascript').split(/, ?/) + @codeLanguages ?= ({id: c, checked: c in defaultLanguages} for c in ['python', 'javascript', 'java', 'lua', 'coffeescript']) + + onClickGoButton: (e) -> + @filterCampaigns() + @levelIDs = [] + for campaign, campaignInfo of @levelsByCampaign + if @$("#campaign-#{campaign}-checkbox").is(':checked') + for level in campaignInfo.levels + @levelIDs.push level unless level in @levelIDs + else + campaignInfo.checked = false + @testLanguages = [] + for codeLanguage in @codeLanguages + if @$("#code-language-#{codeLanguage.id}-checkbox").is(':checked') + codeLanguage.checked = true + @testLanguages.push codeLanguage.id + else + codeLanguage.checked = false + @startTestingLevels() + + startTestingLevels: -> + @levelsToLoad = @initialLevelsToLoad = @levelIDs.length + for levelID in @levelIDs + level = @supermodel.getModel(Level, levelID) or new Level _id: levelID + if level.loaded + @onLevelLoaded() + else + @listenToOnce @supermodel.loadModel(level).model, 'sync', @onLevelLoaded + + onLevelLoaded: (level) -> + if --@levelsToLoad is 0 + @onTestLevelsLoaded() + else + @render() + + onTestLevelsLoaded: -> defaultCores = 2 cores = Math.max(window.navigator.hardwareConcurrency, defaultCores) - #testLevels = testLevels.slice 0, 15 @linksQueryString = window.location.search - @levelIDs = if @levelID then [@levelID] else testLevels - languages = utils.getQueryVariable 'languages', 'python,javascript' #supermodel = if @levelID then @supermodel else undefined @tests = [] - @taskList = [] - @tasksList = _.flatten _.map @levelIDs, (v) -> - # TODO: offer good interface for choosing which languages, better performance for skipping missing solutions - #_.map ['python', 'javascript', 'coffeescript', 'lua'], (l) -> - _.map languages.split(','), (l) -> - #_.map ['javascript'], (l) -> - level: v, language: l + @tasksList = [] + for levelID in @levelIDs + level = @supermodel.getModel(Level, levelID) + solutions = level?.getSolutions() + for codeLanguage in @testLanguages + if not solutions or _.find(solutions, language: codeLanguage) + @tasksList.push level: levelID, language: codeLanguage @testCount = @tasksList.length chunks = _.groupBy @tasksList, (v,i) -> i%cores