Merge pull request #3604 from Zerrien/taskViewAbstraction

Task view abstraction
This commit is contained in:
Josh Callebaut 2016-05-18 15:15:19 -07:00
commit d6586d2e94
16 changed files with 429 additions and 79 deletions

View file

@ -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)

View file

@ -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'

View file

@ -0,0 +1,6 @@
#artisans-view
text-align: center
a
font-size: xx-large
img
border-radius: 8px

View file

@ -0,0 +1,12 @@
#level-tasks-view
#levelTable
width: 100%
.tasksTable
width: 100%
.tasks
width: 87.5%
.taskOwner
width: 12.5%

View file

@ -0,0 +1,9 @@
#solution-problems-view
.problemType
width: 87.5%
.problemValue
width: 12.5%
.problemsTable
width: 100%

View file

@ -9,4 +9,4 @@
width: 87.5%
.taskOwner
width: 12.5%
width: 12.5%

View 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

View 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

View 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

View 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

View file

@ -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

View 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'

View 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

View 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

View 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

View file

@ -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