mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 08:50:58 -05:00
Merge pull request #3604 from Zerrien/taskViewAbstraction
Task view abstraction
This commit is contained in:
commit
d6586d2e94
16 changed files with 429 additions and 79 deletions
|
@ -12,4 +12,7 @@ module.exports = class LevelCollection extends CocoCollection
|
|||
fetchForClassroomAndCourse: (classroomID, courseID, options={}) ->
|
||||
options.url = "/db/classroom/#{classroomID}/courses/#{courseID}/levels"
|
||||
@fetch(options)
|
||||
|
||||
|
||||
fetchForCampaign: (campaignSlug, options={}) ->
|
||||
options.url = "/db/campaign/#{campaignSlug}/levels"
|
||||
@fetch(options)
|
||||
|
|
|
@ -43,6 +43,12 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'admin/pending-patches': go('admin/PendingPatchesView')
|
||||
'admin/codelogs': go('admin/CodeLogsView')
|
||||
|
||||
'artisans': go('artisans/ArtisansView')
|
||||
|
||||
'artisans/level-tasks': go('artisans/LevelTasksView')
|
||||
'artisans/solution-problems': go('artisans/SolutionProblemsView')
|
||||
'artisans/thang-tasks': go('artisans/ThangTasksView')
|
||||
|
||||
'beta': go('HomeView')
|
||||
|
||||
'careers': => window.location.href = 'https://jobs.lever.co/codecombat'
|
||||
|
|
6
app/styles/artisans/artisans-view.sass
Normal file
6
app/styles/artisans/artisans-view.sass
Normal file
|
@ -0,0 +1,6 @@
|
|||
#artisans-view
|
||||
text-align: center
|
||||
a
|
||||
font-size: xx-large
|
||||
img
|
||||
border-radius: 8px
|
12
app/styles/artisans/level-tasks-view.sass
Normal file
12
app/styles/artisans/level-tasks-view.sass
Normal file
|
@ -0,0 +1,12 @@
|
|||
#level-tasks-view
|
||||
#levelTable
|
||||
width: 100%
|
||||
|
||||
.tasksTable
|
||||
width: 100%
|
||||
|
||||
.tasks
|
||||
width: 87.5%
|
||||
|
||||
.taskOwner
|
||||
width: 12.5%
|
9
app/styles/artisans/solution-problems-view.sass
Normal file
9
app/styles/artisans/solution-problems-view.sass
Normal file
|
@ -0,0 +1,9 @@
|
|||
#solution-problems-view
|
||||
.problemType
|
||||
width: 87.5%
|
||||
|
||||
.problemValue
|
||||
width: 12.5%
|
||||
|
||||
.problemsTable
|
||||
width: 100%
|
|
@ -9,4 +9,4 @@
|
|||
width: 87.5%
|
||||
|
||||
.taskOwner
|
||||
width: 12.5%
|
||||
width: 12.5%
|
13
app/templates/artisans/artisans-view.jade
Normal file
13
app/templates/artisans/artisans-view.jade
Normal file
|
@ -0,0 +1,13 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
img(src='/images/pages/user/artisan.png')
|
||||
div
|
||||
a(href='/artisans/thang-tasks')
|
||||
|Thang Tasks
|
||||
div
|
||||
a(href="/artisans/level-tasks")
|
||||
|Level Tasks
|
||||
div
|
||||
a(href="/artisans/solution-problems")
|
||||
|Solution Problems
|
36
app/templates/artisans/level-tasks-view.jade
Normal file
36
app/templates/artisans/level-tasks-view.jade
Normal file
|
@ -0,0 +1,36 @@
|
|||
// DNT
|
||||
extends /templates/base
|
||||
|
||||
block content
|
||||
#level-tasks-view
|
||||
div
|
||||
a(href='/artisans')
|
||||
span.glyphicon.glyphicon-chevron-left
|
||||
span Artisans Home
|
||||
br
|
||||
input.searchInput#name-search(placeholder='Filter: Level Name')
|
||||
br
|
||||
input.searchInput#desc-search(placeholder='Filter: Task Description')
|
||||
hr
|
||||
div#level-table
|
||||
if view.processedLevels
|
||||
table.table.table-striped
|
||||
tr
|
||||
th Level Name
|
||||
th Task List
|
||||
for level in view.processedLevels
|
||||
if view.hasIncompleteTasks(level)
|
||||
+levelRow(level)
|
||||
else
|
||||
div No view.processedLevels
|
||||
|
||||
mixin levelRow(level)
|
||||
tr
|
||||
td.taskOwner
|
||||
a(href= 'level/' + level.slug)= level.name
|
||||
td.tasks
|
||||
table.table-striped.table-hover.tasksTable
|
||||
for task in (level.tasks || [])
|
||||
if !task.complete
|
||||
tr
|
||||
td= task.name
|
26
app/templates/artisans/solution-problems-view.jade
Normal file
26
app/templates/artisans/solution-problems-view.jade
Normal file
|
@ -0,0 +1,26 @@
|
|||
// DNT
|
||||
extends /templates/base
|
||||
|
||||
block content
|
||||
#solution-problems-view
|
||||
div
|
||||
a(href='/artisans')
|
||||
span.glyphicon.glyphicon-chevron-left
|
||||
span Artisans Home
|
||||
br
|
||||
div Total number of problems: #{view.problemCount}
|
||||
hr
|
||||
table.table.table-striped#level-table
|
||||
tr
|
||||
th Level Name
|
||||
th Solution Problems
|
||||
for level in (view.parsedLevels || [])
|
||||
if (level.problems || []).length != 0
|
||||
tr
|
||||
td(style="width:10%")= level.level.get('name')
|
||||
td
|
||||
table.table-striped.table-hover.problemsTable
|
||||
for problem in (level.problems || [])
|
||||
tr(style="width:100%")
|
||||
td.problemValue= problem.value
|
||||
td.problemType= problem.type
|
35
app/templates/artisans/thang-tasks-view.jade
Normal file
35
app/templates/artisans/thang-tasks-view.jade
Normal file
|
@ -0,0 +1,35 @@
|
|||
// DNT
|
||||
extends /templates/base
|
||||
|
||||
block content
|
||||
#thang-tasks-view
|
||||
div
|
||||
a(href='/artisans')
|
||||
span.glyphicon.glyphicon-chevron-left
|
||||
span Artisans Home
|
||||
input.inputSearch#name-search(placeholder='Filter: Thang Name')
|
||||
br
|
||||
input.inputSearch#desc-search(placeholder='Filter: Task Description')
|
||||
hr
|
||||
div#thang-table
|
||||
if view.processedThangs
|
||||
table.table.table-striped
|
||||
tr
|
||||
th Thang Name
|
||||
th Task List
|
||||
for thang in view.processedThangs
|
||||
if view.hasIncompleteTasks(thang)
|
||||
+thangRow(thang)
|
||||
else
|
||||
span No view.processedThangs
|
||||
|
||||
mixin thangRow(thang)
|
||||
tr
|
||||
td.taskOwner
|
||||
a(href= 'thang/' + thang.get('slug'))= thang.get('name')
|
||||
td.tasks
|
||||
table.table-striped.table-hover.tasksTable
|
||||
for task in (thang.tasks || [])
|
||||
if !task.complete
|
||||
tr
|
||||
td= task.name
|
|
@ -1,29 +0,0 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
#thang-tasks-view
|
||||
input#nameSearch(placeholder='Filter: Thang Name')
|
||||
br
|
||||
input#descSearch(placeholder='Filter: Task Description')
|
||||
hr
|
||||
if view.processedThangs
|
||||
table.table.table-striped#thangTable
|
||||
tr
|
||||
th Thang Name
|
||||
th Task List
|
||||
for thang in view.processedThangs
|
||||
if view.hasIncompleteTasks(thang)
|
||||
+thangRow(thang)
|
||||
else
|
||||
span No view.processedThangs
|
||||
|
||||
mixin thangRow(thang)
|
||||
tr
|
||||
td.taskOwner
|
||||
a(href= 'thang/' + thang.get('slug'))= thang.get('name')
|
||||
td.tasks
|
||||
table.table-striped.table-hover.tasksTable
|
||||
for task in (thang.tasks || [])
|
||||
if !task.complete
|
||||
tr
|
||||
td= task.name
|
6
app/views/artisans/ArtisansView.coffee
Normal file
6
app/views/artisans/ArtisansView.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/artisans/artisans-view'
|
||||
|
||||
module.exports = class ArtisansView extends RootView
|
||||
template: template
|
||||
id: 'artisans-view'
|
58
app/views/artisans/LevelTasksView.coffee
Normal file
58
app/views/artisans/LevelTasksView.coffee
Normal file
|
@ -0,0 +1,58 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/artisans/level-tasks-view'
|
||||
|
||||
Campaigns = require 'collections/Campaigns'
|
||||
|
||||
Campaign = require 'models/Campaign'
|
||||
|
||||
module.exports = class LevelTasksView extends RootView
|
||||
template: template
|
||||
id: 'level-tasks-view'
|
||||
events:
|
||||
'input .searchInput': 'processLevels'
|
||||
'change .searchInput': 'processLevels'
|
||||
|
||||
excludedCampaigns = [
|
||||
'picoctf', 'auditions'
|
||||
]
|
||||
|
||||
levels: {}
|
||||
processedLevels: {}
|
||||
|
||||
initialize: () ->
|
||||
@processLevels = _.debounce(@processLevels, 250)
|
||||
|
||||
@campaigns = new Campaigns()
|
||||
@listenTo(@campaigns, 'sync', @onCampaignsLoaded)
|
||||
@supermodel.trackRequest(@campaigns.fetch(
|
||||
data:
|
||||
project: 'name,slug,levels,tasks'
|
||||
))
|
||||
|
||||
onCampaignsLoaded: (campCollection) ->
|
||||
@levels = {}
|
||||
for campaign in campCollection.models
|
||||
campaignSlug = campaign.get 'slug'
|
||||
continue if campaignSlug in excludedCampaigns
|
||||
levels = campaign.get 'levels'
|
||||
for key, level of levels
|
||||
levelSlug = level.slug
|
||||
@levels[levelSlug] = level
|
||||
@processLevels()
|
||||
|
||||
processLevels: () ->
|
||||
@processedLevels = {}
|
||||
for key, level of @levels
|
||||
continue unless ///#{$('#name-search')[0].value}///i.test level.name
|
||||
filteredTasks = level.tasks.filter (elem) ->
|
||||
# Similar case-insensitive search of input vs description (name).
|
||||
return ///#{$('#desc-search')[0].value}///i.test elem.name
|
||||
@processedLevels[key] = {
|
||||
tasks: filteredTasks
|
||||
name: level.name
|
||||
}
|
||||
@renderSelectors '#level-table'
|
||||
|
||||
# Jade helper
|
||||
hasIncompleteTasks: (level) ->
|
||||
return level.tasks and level.tasks.filter((_elem) -> return not _elem.complete).length > 0
|
171
app/views/artisans/SolutionProblemsView.coffee
Normal file
171
app/views/artisans/SolutionProblemsView.coffee
Normal file
|
@ -0,0 +1,171 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/artisans/solution-problems-view'
|
||||
|
||||
Level = require 'models/Level'
|
||||
Campaign = require 'models/Campaign'
|
||||
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Campaigns = require 'collections/Campaigns'
|
||||
Levels = require 'collections/Levels'
|
||||
|
||||
module.exports = class SolutionProblemsView extends RootView
|
||||
template: template
|
||||
id: 'solution-problems-view'
|
||||
excludedCampaigns = [
|
||||
# Misc. campaigns
|
||||
'picoctf', 'auditions'
|
||||
|
||||
# Campaign-version campaigns
|
||||
#'dungeon', 'forest', 'desert', 'mountain', 'glacier'
|
||||
|
||||
# Test campaigns
|
||||
'dungeon-branching-test', 'forest-branching-test', 'desert-branching-test'
|
||||
|
||||
# Course-version campaigns
|
||||
#'intro', 'course-2', 'course-3', 'course-4', 'course-5', 'course-6'
|
||||
]
|
||||
excludedSimulationLevels = [
|
||||
# Course Arenas
|
||||
'wakka-maul', 'cross-bones'
|
||||
]
|
||||
excludedSolutionLevels = [
|
||||
# Multiplayer Levels
|
||||
'cavern-survival'
|
||||
'dueling-grounds', 'multiplayer-treasure-grove'
|
||||
'harrowland'
|
||||
'zero-sum'
|
||||
'ace-of-coders', 'capture-their-flag'
|
||||
]
|
||||
simulationRequirements = [
|
||||
'seed'
|
||||
'succeeds'
|
||||
'heroConfig'
|
||||
'frameCount'
|
||||
'goals'
|
||||
]
|
||||
includedLanguages = [
|
||||
'python', 'javascript', 'java', 'lua', 'coffeescript'
|
||||
]
|
||||
# TODO: Phase the following out:
|
||||
excludedLanguages = [
|
||||
'java', 'lua', 'coffeescript'
|
||||
]
|
||||
excludedLevelSnippets = [
|
||||
'treasure', 'brawl', 'siege'
|
||||
]
|
||||
|
||||
unloadedCampaigns: 0
|
||||
campaignLevels: {}
|
||||
loadedLevels: {}
|
||||
parsedLevels: []
|
||||
problemCount: 0
|
||||
|
||||
initialize: ->
|
||||
@campaigns = new Campaigns([])
|
||||
@listenTo(@campaigns, 'sync', @onCampaignsLoaded)
|
||||
@supermodel.trackRequest(@campaigns.fetch(
|
||||
data:
|
||||
project:'slug'
|
||||
))
|
||||
|
||||
onCampaignsLoaded: (campCollection) ->
|
||||
for campaign in campCollection.models
|
||||
campaignSlug = campaign.get('slug')
|
||||
continue if campaignSlug in excludedCampaigns
|
||||
@unloadedCampaigns++
|
||||
|
||||
@campaignLevels[campaignSlug] = new Levels()
|
||||
@listenTo(@campaignLevels[campaignSlug], 'sync', @onLevelsLoaded)
|
||||
@supermodel.trackRequest(@campaignLevels[campaignSlug].fetchForCampaign(campaignSlug,
|
||||
data:
|
||||
project: 'thangs,name,slug,campaign'
|
||||
))
|
||||
|
||||
onLevelsLoaded: (lvlCollection) ->
|
||||
for level in lvlCollection.models
|
||||
@loadedLevels[level.get('slug')] = level
|
||||
if --@unloadedCampaigns is 0
|
||||
@onAllLevelsLoaded()
|
||||
|
||||
onAllLevelsLoaded: ->
|
||||
for levelSlug, level of @loadedLevels
|
||||
unless level?
|
||||
console.error 'Level Slug doesn\'t have associated Level', levelSlug
|
||||
continue
|
||||
continue if levelSlug in excludedSolutionLevels
|
||||
isBad = false
|
||||
for word in excludedLevelSnippets
|
||||
if levelSlug.indexOf(word) isnt -1
|
||||
isBad = true
|
||||
continue if isBad
|
||||
thangs = level.get 'thangs'
|
||||
component = null
|
||||
thangs = _.filter(thangs, (elem) ->
|
||||
return _.findWhere(elem.components, (elem2) ->
|
||||
if elem2.config?.programmableMethods?
|
||||
component = elem2
|
||||
return true
|
||||
)
|
||||
)
|
||||
|
||||
if thangs.length > 1
|
||||
unless levelSlug in excludedSimulationLevels
|
||||
console.warn 'Level has more than 1 programmableMethod Thangs', levelSlug
|
||||
continue
|
||||
unless component?
|
||||
console.error 'Level doesn\'t have programmableMethod Thang', levelSlug
|
||||
continue
|
||||
|
||||
plan = component.config.programmableMethods.plan
|
||||
solutions = plan.solutions or []
|
||||
problems = []
|
||||
problems = problems.concat(@findMissingSolutions solutions)
|
||||
unless levelSlug in excludedSimulationLevels
|
||||
for solution in solutions
|
||||
problems = problems.concat(@findSimulationProblems solution)
|
||||
problems = problems.concat(@findPass solution)
|
||||
problems = problems.concat(@findIdenticalToSource solution, plan)
|
||||
@problemCount += problems.length
|
||||
@parsedLevels.push
|
||||
level: level
|
||||
problems: problems
|
||||
|
||||
@renderSelectors '#level-table'
|
||||
|
||||
findMissingSolutions: (solutions) ->
|
||||
problems = []
|
||||
for lang in includedLanguages
|
||||
if _.findWhere(solutions, (elem) -> return elem.language is lang)
|
||||
# TODO: Phase the following out:
|
||||
else if lang not in excludedLanguages
|
||||
problems.push
|
||||
type: 'Missing solution language'
|
||||
value: lang
|
||||
problems
|
||||
|
||||
findSimulationProblems: (solution) ->
|
||||
problems = []
|
||||
for req in simulationRequirements
|
||||
unless solution[req]?
|
||||
problems.push
|
||||
type: 'Solution is not simulatable'
|
||||
value: solution.language
|
||||
break
|
||||
problems
|
||||
|
||||
findPass: (solution) ->
|
||||
problems = []
|
||||
if solution.source.search(/pass\n/) isnt -1
|
||||
problems.push
|
||||
type: 'Solution contains pass'
|
||||
value: solution.language
|
||||
problems
|
||||
|
||||
findIdenticalToSource: (solution, plan) ->
|
||||
problems = []
|
||||
source = if solution.lang is 'javascript' then plan.source else plan.languages[solution.language]
|
||||
if solution.source is source
|
||||
problems.push
|
||||
type: 'Solution matches sample code'
|
||||
value: solution.language
|
||||
problems
|
46
app/views/artisans/ThangTasksView.coffee
Normal file
46
app/views/artisans/ThangTasksView.coffee
Normal file
|
@ -0,0 +1,46 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/artisans/thang-tasks-view'
|
||||
|
||||
ThangType = require 'models/ThangType'
|
||||
|
||||
ThangTypes = require 'collections/ThangTypes'
|
||||
|
||||
module.exports = class ThangTasksView extends RootView
|
||||
template: template
|
||||
id: 'thang-tasks-view'
|
||||
events:
|
||||
'input input': 'processThangs'
|
||||
'change input': 'processThangs'
|
||||
|
||||
thangs: {}
|
||||
processedThangs: {}
|
||||
|
||||
initialize: () ->
|
||||
@processThangs = _.debounce(@processThangs, 250)
|
||||
|
||||
@thangs = new ThangTypes()
|
||||
@listenTo(@thangs, 'sync', @onThangsLoaded)
|
||||
@supermodel.trackRequest(@thangs.fetch(
|
||||
data:
|
||||
project: 'name,tasks,slug'
|
||||
))
|
||||
|
||||
onThangsLoaded: (thangCollection) ->
|
||||
@processThangs()
|
||||
|
||||
processThangs: ->
|
||||
@processedThangs = @thangs.filter (_elem) ->
|
||||
# Case-insensitive search of input vs name.
|
||||
return ///#{$('#name-search')[0].value}///i.test _elem.get('name')
|
||||
for thang in @processedThangs
|
||||
thang.tasks = _.filter thang.attributes.tasks, (_elem) ->
|
||||
# Similar case-insensitive search of input vs description (name).
|
||||
return ///#{$('#desc-search')[0].value}///i.test _elem.name
|
||||
@renderSelectors '#thang-table'
|
||||
|
||||
sortThangs: (a, b) ->
|
||||
a.get('name').localeCompare(b.get('name'))
|
||||
|
||||
# Jade helper
|
||||
hasIncompleteTasks: (thang) ->
|
||||
return thang.tasks and thang.tasks.filter((_elem) -> return not _elem.complete).length > 0
|
|
@ -1,48 +0,0 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/editor/thangTasksView'
|
||||
ThangType = require 'models/ThangType'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
|
||||
module.exports = class ThangTasksView extends RootView
|
||||
template: template
|
||||
id: 'thang-tasks-view'
|
||||
events:
|
||||
'input input': 'searchUpdate'
|
||||
'change input': 'searchUpdate'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@thangs = new CocoCollection([],
|
||||
url: '/db/thang.type?project=name,tasks,slug'
|
||||
model: ThangType
|
||||
comparator: @sortThangs
|
||||
)
|
||||
@lastLoad = (new Date()).getTime()
|
||||
@listenTo(@thangs, 'sync', @onThangsLoaded)
|
||||
@supermodel.loadCollection(@thangs, 'thangs')
|
||||
|
||||
searchUpdate: ->
|
||||
if not @lastLoad? or (new Date()).getTime() - @lastLoad > 60 * 1000 * 1 # Update only after a minute from last update.
|
||||
@thangs.fetch()
|
||||
@listenTo(@thangs, 'sync', @onThangsLoaded)
|
||||
@supermodel.loadCollection(@thangs, 'thangs')
|
||||
@lastLoad = (new Date()).getTime()
|
||||
else
|
||||
@onThangsLoaded()
|
||||
|
||||
onThangsLoaded: ->
|
||||
@processedThangs = @thangs.filter (_elem) ->
|
||||
# Case-insensitive search of input vs name.
|
||||
return ///#{$('#nameSearch')[0].value}///i.test _elem.get('name')
|
||||
for thang in @processedThangs
|
||||
thang.tasks = _.filter thang.attributes.tasks, (_elem) ->
|
||||
# Similar case-insensitive search of input vs description (name).
|
||||
return ///#{$('#descSearch')[0].value}///i.test _elem.name
|
||||
@renderSelectors '#thangTable'
|
||||
|
||||
sortThangs: (a, b) ->
|
||||
a.get('name').localeCompare(b.get('name'))
|
||||
|
||||
# Jade helper
|
||||
hasIncompleteTasks: (thang) ->
|
||||
return thang.tasks and thang.tasks.filter((_elem) -> return not _elem.complete).length > 0
|
Loading…
Reference in a new issue