Add course complete visual state for student CourseDetailsView

Also including a few misc tweaks to CourseDetailsView and the end-of-course HeroVictoryModal state.
This commit is contained in:
Nick Winter 2015-12-02 09:52:52 -08:00
parent cccf61e2e8
commit c77e1c0fa2
6 changed files with 165 additions and 44 deletions

View file

@ -1360,6 +1360,15 @@
campaigns: "Campaigns" campaigns: "Campaigns"
poll: "Poll" poll: "Poll"
user_polls_record: "Poll Voting History" user_polls_record: "Poll Voting History"
course: "Course"
courses: "Courses"
course_instance: "Course Instance"
courses_instances: "Course Instances"
classroom: "Classroom"
classrooms: "Classrooms"
clan: "Clan"
clans: "Clans"
members: "Members"
concepts: concepts:
advanced_strings: "Advanced Strings" advanced_strings: "Advanced Strings"

View file

@ -140,3 +140,11 @@
.settings-name-input .settings-name-input
width: 50% width: 50%
.jumbotron
.btn
white-space: normal
min-height: 200px
h1
font-size: 48px

View file

@ -38,7 +38,8 @@ block content
if !view.owner.isNew() && view.getOwnerName() if !view.owner.isNew() && view.getOwnerName()
span.spl.spr - Teacher: span.spl.spr - Teacher:
a(href="/user/#{view.owner.id}") //a(href="/user/#{view.owner.id}") // Don't link to profiles until we improve them
span
strong= view.getOwnerName() strong= view.getOwnerName()
h1 h1
@ -51,10 +52,60 @@ block content
each line in courseInstance.get('description').split('\n') each line in courseInstance.get('description').split('\n')
div= line div= line
if view.courseComplete && !view.teacherMode
.jumbotron
.row
if view.singlePlayerMode && !me.isAnonymous()
.col-md-3
.col-md-6
a.btn.btn-lg.btn-success(href="/play")
h1 Play the Campaign
p Youre ready to take the next step! Explore hundreds of challenging levels, learn advanced programming skills, and compete in multiplayer arenas!
.col-md-3
else if view.singlePlayerMode && me.isAnonymous()
.col-md-6
a.btn.btn-lg.btn-success.signup-button
h1 Create an Account
p Sign up for a FREE CodeCombat account and gain access to more levels, more programming skills, and more fun!
.col-md-6
a.btn.btn-lg.btn-success(href="/play")
h1 Preview Campaign
p Take a sneak peek at all that CodeCombat has to offer before signing up for your FREE account.
else if !view.singlePlayerMode
.col-md-6
if view.arenaLevel
a.btn.btn-lg.btn-success.btn-play-level(data-level-slug=view.arenaLevel.slug, data-level-id=view.arenaLevel.original)
h1
span Arena
| :
span.spl= view.arenaLevel.name
p= view.arenaLevel.description.replace(/!\[.*?\)/, '')
else
a.btn.btn-lg.btn-success.disabled
h1 Arena Coming Soon
p We are working on a multiplayer arena for classrooms at the end of #{course.get('name')}.
.col-md-6
if view.nextCourseInstance
a.btn.btn-lg.btn-success(href="/courses/#{view.nextCourse.id}/#{view.nextCourseInstance.id}")
h1= view.nextCourse.get('name')
p= view.nextCourse.get('description')
else if view.nextCourse
a.btn.btn-lg.btn-success.disabled
h1= view.nextCourse.get('name')
p
em NOT ENROLLED
p Ask your teacher to enroll you in the next course.
else
a.btn.btn-lg.btn-success(disabled=!view.nextCourse ? "disabled" : "")
h1 Next Course
p
em COMING SOON
p We are hard at work making more courses for you!
if !me.isAnonymous() if !me.isAnonymous()
div.well.well-sm(role='tabpanel') div.well.well-sm(role='tabpanel')
ul.nav.nav-pills(role='tablist') ul.nav.nav-pills(role='tablist')
if adminMode if view.teacherMode
li.active(role='presentation') li.active(role='presentation')
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress") a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
li(role='presentation') li(role='presentation')
@ -65,7 +116,7 @@ block content
li(role='presentation') li(role='presentation')
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress") a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
.tab-content .tab-content
if adminMode if view.teacherMode
.tab-pane.active#progress(role='tabpanel') .tab-pane.active#progress(role='tabpanel')
+progress-tab +progress-tab
.tab-pane#levels(role='tabpanel') .tab-pane#levels(role='tabpanel')
@ -241,7 +292,7 @@ mixin progress-members-popup-completed(i, level, session)
p p
span.spr(data-i18n="courses.completed") span.spr(data-i18n="courses.completed")
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')} span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
if adminMode if view.teacherMode || me.isAdmin()
strong(data-i18n="clans.view_solution") strong(data-i18n="clans.view_solution")
mixin progress-members-popup-started(i, level, session) mixin progress-members-popup-started(i, level, session)
@ -253,7 +304,7 @@ mixin progress-members-popup-started(i, level, session)
p p
span.spr(data-i18n="clans.last_played") span.spr(data-i18n="clans.last_played")
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')} span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
if adminMode if view.teacherMode || me.isAdmin()
strong(data-i18n="clans.view_solution") strong(data-i18n="clans.view_solution")
mixin levels-tab mixin levels-tab
@ -271,7 +322,7 @@ mixin levels-tab
each level, levelID in campaign.get('levels') each level, levelID in campaign.get('levels')
tr tr
td td
if lastLevelCompleted || adminMode if lastLevelCompleted || view.teacherMode
- var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play'; - var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play';
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID) button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID)
td td

View file

@ -12,10 +12,11 @@ block modal-body-content
#victory-text= victoryText #victory-text= victoryText
if isCourseLevel if isCourseLevel
if currentCourseName .course-name-container
p if currentCourseName
span.spr.level-title(data-i18n="play_level.course") p
span.level-name= currentCourseName span.spr.level-title(data-i18n="play_level.course")
span.level-name= currentCourseName
.container-fluid .container-fluid
.row .row
.col-md-6 .col-md-6
@ -26,6 +27,10 @@ block modal-body-content
if nextLevelName if nextLevelName
.level-title(data-i18n="play_level.next_level") .level-title(data-i18n="play_level.next_level")
.level-name= nextLevelName.replace('Course: ', '') .level-name= nextLevelName.replace('Course: ', '')
else
.level-title(data-i18n="play_level.course")
.level-name(data-i18n="play_level.victory_title_suffix")
br br
#level-feedback #level-feedback

View file

@ -15,6 +15,9 @@ autoplayedOnce = false
module.exports = class CourseDetailsView extends RootView module.exports = class CourseDetailsView extends RootView
id: 'course-details-view' id: 'course-details-view'
template: template template: template
teacherMode: false
singlePlayerMode: false
memberSort: 'nameAsc'
events: events:
'change .progress-expand-checkbox': 'onCheckExpandedProgress' 'change .progress-expand-checkbox': 'onCheckExpandedProgress'
@ -31,8 +34,6 @@ module.exports = class CourseDetailsView extends RootView
@courseID ?= options.courseID @courseID ?= options.courseID
@courseInstanceID ?= options.courseInstanceID @courseInstanceID ?= options.courseInstanceID
@classroom = new Classroom() @classroom = new Classroom()
@adminMode = me.isAdmin()
@memberSort = 'nameAsc'
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID @course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
@listenTo @course, 'sync', @onCourseSync @listenTo @course, 'sync', @onCourseSync
@prepaid = new Prepaid() @prepaid = new Prepaid()
@ -43,7 +44,6 @@ module.exports = class CourseDetailsView extends RootView
getRenderData: -> getRenderData: ->
context = super() context = super()
context.adminMode = @adminMode ? false
context.campaign = @campaign context.campaign = @campaign
context.conceptsCompleted = @conceptsCompleted ? {} context.conceptsCompleted = @conceptsCompleted ? {}
context.course = @course if @course?.loaded context.course = @course if @course?.loaded
@ -64,11 +64,19 @@ module.exports = class CourseDetailsView extends RootView
context.document = document context.document = document
context context
afterRender: ->
super()
if @supermodel.finished() and @courseComplete and me.isAnonymous() and @options.justBeatLevel
# TODO: Make an intermediate modal that tells them they've finished HoC and has some snazzy stuff for convincing players to sign up instead of just throwing up the bare AuthModal
AuthModal = require 'views/core/AuthModal'
@openModalView new AuthModal showSignupRationale: true
onCourseSync: -> onCourseSync: ->
return if @destroyed
# console.log 'onCourseSync' # console.log 'onCourseSync'
if me.isAnonymous() and (not me.get('hourOfCode') and not @course.get('hourOfCode')) if me.isAnonymous() and (not me.get('hourOfCode') and not @course.get('hourOfCode'))
@noCourseInstance = true @noCourseInstance = true
@render?() @render()
return return
return if @campaign? return if @campaign?
campaignID = @course.get('campaignID') campaignID = @course.get('campaignID')
@ -78,24 +86,36 @@ module.exports = class CourseDetailsView extends RootView
@onCampaignSync() @onCampaignSync()
else else
@supermodel.loadModel @campaign, 'campaign' @supermodel.loadModel @campaign, 'campaign'
@render?() @render()
onCampaignSync: -> onCampaignSync: ->
return if @destroyed
# console.log 'onCampaignSync' # console.log 'onCampaignSync'
if @courseInstanceID if @courseInstanceID
@loadCourseInstance(@courseInstanceID) @loadCourseInstance(@courseInstanceID)
else unless me.isAnonymous() else unless me.isAnonymous()
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance}) @loadCourseInstances()
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
@supermodel.loadCollection(@courseInstances, 'course_instances')
@levelConceptMap = {} @levelConceptMap = {}
for levelID, level of @campaign.get('levels') for levelID, level of @campaign.get('levels')
@levelConceptMap[levelID] ?= {} @levelConceptMap[levelID] ?= {}
for concept in level.concepts for concept in level.concepts
@levelConceptMap[levelID][concept] = true @levelConceptMap[levelID][concept] = true
@render?() if level.type is 'course-ladder'
@arenaLevel = level
@render()
loadCourseInstances: ->
@courseInstances = new CocoCollection [], {url: "/db/user/#{me.id}/course_instances", model: CourseInstance, comparator: 'courseID'}
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
@supermodel.loadCollection @courseInstances, 'course_instances'
loadAllCourses: ->
@allCourses = new CocoCollection [], {url: "/db/course", model: Course, comparator: '_id'}
@listenToOnce @allCourses, 'sync', @onAllCoursesSync
@supermodel.loadCollection @allCourses, 'courses'
loadCourseInstance: (courseInstanceID) -> loadCourseInstance: (courseInstanceID) ->
return if @destroyed
# console.log 'loadCourseInstance' # console.log 'loadCourseInstance'
return if @courseInstance? return if @courseInstance?
@courseInstanceID = courseInstanceID @courseInstanceID = courseInstanceID
@ -107,23 +127,29 @@ module.exports = class CourseDetailsView extends RootView
@courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model @courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model
onCourseInstancesSync: -> onCourseInstancesSync: ->
return if @destroyed
# console.log 'onCourseInstancesSync' # console.log 'onCourseInstancesSync'
if @courseInstances.models.length is 1 @findNextCourseInstance()
@loadCourseInstance(@courseInstances.models[0].id) if not @courseInstance
else # We are loading these to find the one we want to display.
if @courseInstances.models.length is 0 if @courseInstances.models.length is 1
@noCourseInstance = true @loadCourseInstance(@courseInstances.models[0].id)
else else
@noCourseInstanceSelected = true if @courseInstances.models.length is 0
@render?() @noCourseInstance = true
else
@noCourseInstanceSelected = true
@render()
onCourseInstanceSync: -> onCourseInstanceSync: ->
return if @destroyed
# console.log 'onCourseInstanceSync' # console.log 'onCourseInstanceSync'
if @courseInstance.get('classroomID') if @courseInstance.get('classroomID')
@classroom = new Classroom({_id: @courseInstance.get('classroomID')}) @classroom = new Classroom({_id: @courseInstance.get('classroomID')})
@supermodel.loadModel @classroom, 'classroom' @supermodel.loadModel @classroom, 'classroom'
@adminMode = true if @courseInstance.get('ownerID') is me.id and @courseInstance.get('name') isnt 'Single Player' @singlePlayerMode = @courseInstance.get('name') is 'Single Player'
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator:'_id' }) @teacherMode = @courseInstance.get('ownerID') is me.id and not @singlePlayerMode
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator: '_id' })
@listenToOnce @levelSessions, 'sync', @onLevelSessionsSync @listenToOnce @levelSessions, 'sync', @onLevelSessionsSync
@supermodel.loadCollection @levelSessions, 'level_sessions', cache: false @supermodel.loadCollection @levelSessions, 'level_sessions', cache: false
@members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' }) @members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' })
@ -131,19 +157,22 @@ module.exports = class CourseDetailsView extends RootView
@supermodel.loadCollection @members, 'members', cache: false @supermodel.loadCollection @members, 'members', cache: false
@owner = new User({_id: @courseInstance.get('ownerID')}) @owner = new User({_id: @courseInstance.get('ownerID')})
@supermodel.loadModel @owner, 'user' @supermodel.loadModel @owner, 'user'
if @adminMode and prepaidID = @courseInstance.get('prepaidID') if @teacherMode and prepaidID = @courseInstance.get('prepaidID')
@prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID @prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID
@listenTo @prepaid, 'sync', @onPrepaidSync @listenTo @prepaid, 'sync', @onPrepaidSync
if @prepaid.loaded if @prepaid.loaded
@onPrepaidSync() @onPrepaidSync()
else else
@supermodel.loadModel @prepaid, 'prepaid' @supermodel.loadModel @prepaid, 'prepaid'
@render?() @render()
onPrepaidSync: -> onPrepaidSync: ->
@render?() return if @destroyed
# TODO: why do we rerender here? Template doesn't use prepaid.
@render()
onLevelSessionsSync: -> onLevelSessionsSync: ->
return if @destroyed
# console.log 'onLevelSessionsSync' # console.log 'onLevelSessionsSync'
@instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0 @instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0
@memberStats = {} @memberStats = {}
@ -197,39 +226,57 @@ module.exports = class CourseDetailsView extends RootView
@conceptsCompleted[concept] ?= 0 @conceptsCompleted[concept] ?= 0
@conceptsCompleted[concept]++ @conceptsCompleted[concept]++
if @memberStats[me.id]?.totalLevelsCompleted >= _.size @campaign.get('levels') if @memberStats[me.id]?.totalLevelsCompleted >= _.size(@campaign.get('levels')) - 1 # Don't need to complete arena
@courseComplete = true @courseComplete = true
@loadCourseInstances() unless @courseInstances # Find the next course instance to do.
@render?() @render()
# If we just joined a single-player course for Hour of Code, we automatically play. # If we just joined a single-player course for Hour of Code, we automatically play.
if @instanceStats.totalLevelsCompleted is 0 and @instanceStats.totalPlayTime is 0 and @courseInstance.get('members').length is 1 and me.get('hourOfCode') and not @adminMode and not autoplayedOnce if @instanceStats.totalLevelsCompleted is 0 and @instanceStats.totalPlayTime is 0 and @singlePlayerMode and not autoplayedOnce
autoplayedOnce = true autoplayedOnce = true
@$el.find('button.btn-play-level').click() @$el.find('button.btn-play-level').click()
onMembersSync: -> onMembersSync: ->
return if @destroyed
# console.log 'onMembersSync' # console.log 'onMembersSync'
@memberUserMap = {} @memberUserMap = {}
for user in @members.models for user in @members.models
@memberUserMap[user.id] = user @memberUserMap[user.id] = user
@sortMembers() @sortMembers()
@render?() @render()
onAllCoursesSync: ->
@findNextCourseInstance()
findNextCourseInstance: ->
@nextCourseInstance = _.find @courseInstances.models, (ci) =>
# Sorted by courseID
ci.get('classroomID') is @courseInstance.get('classroomID') and ci.id isnt @courseInstance.id and ci.get('courseID') > @course.id
if @nextCourseInstance
nextCourseID = @nextCourseInstance.get('courseID')
@nextCourse = @supermodel.getModel(Course, nextCourseID) or new Course _id: nextCourseID
@nextCourse = @supermodel.loadModel(@nextCourse, 'course').model
else if @allCourses?.loaded
@nextCourse = _.find @allCourses.models, (course) => course.id > @course.id
else
@loadAllCourses()
onCheckExpandedProgress: (e) -> onCheckExpandedProgress: (e) ->
@showExpandedProgress = $('.progress-expand-checkbox').prop('checked') @showExpandedProgress = $('.progress-expand-checkbox').prop('checked')
# TODO: why does render reset the checkbox to be unchecked? # TODO: why does render reset the checkbox to be unchecked?
@render?() @render()
$('.progress-expand-checkbox').attr('checked', @showExpandedProgress) $('.progress-expand-checkbox').attr('checked', @showExpandedProgress)
onClickMemberHeader: (e) -> onClickMemberHeader: (e) ->
@memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc' @memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc'
@sortMembers() @sortMembers()
@render?() @render()
onClickProgressHeader: (e) -> onClickProgressHeader: (e) ->
@memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc' @memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc'
@sortMembers() @sortMembers()
@render?() @render()
onClickPlayLevel: (e) -> onClickPlayLevel: (e) ->
levelSlug = $(e.target).data('level-slug') levelSlug = $(e.target).data('level-slug')
@ -237,7 +284,7 @@ module.exports = class CourseDetailsView extends RootView
level = @campaign.get('levels')[levelID] level = @campaign.get('levels')[levelID]
if level.type is 'course-ladder' if level.type is 'course-ladder'
route = '/play/ladder/' + levelSlug route = '/play/ladder/' + levelSlug
route += '/course/' + @courseInstance.id if @courseInstance.get('members').length > 1 # No league for solo courses route += '/course/' + @courseInstance.id unless @singlePlayerMode # No league for solo courses
Backbone.Mediator.publish 'router:navigate', route: route Backbone.Mediator.publish 'router:navigate', route: route
else else
Backbone.Mediator.publish 'router:navigate', { Backbone.Mediator.publish 'router:navigate', {
@ -255,7 +302,7 @@ module.exports = class CourseDetailsView extends RootView
@loadCourseInstance(courseInstanceID) @loadCourseInstance(courseInstanceID)
onClickProgressLevelCell: (e) -> onClickProgressLevelCell: (e) ->
return unless @adminMode return unless @teacherMode or me.isAdmin()
levelID = $(e.currentTarget).data('level-id') levelID = $(e.currentTarget).data('level-id')
levelSlug = $(e.currentTarget).data('level-slug') levelSlug = $(e.currentTarget).data('level-slug')
userID = $(e.currentTarget).data('user-id') userID = $(e.currentTarget).data('user-id')

View file

@ -63,9 +63,10 @@ module.exports = class HeroVictoryModal extends ModalView
else else
@readyToContinue = true @readyToContinue = true
@playSound 'victory' @playSound 'victory'
if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel') if @level.get('type', true) is 'course'
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}" if nextLevel = @level.get('nextLevel')
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model @nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model
if @courseID if @courseID
@course = new Course().setURL "/db/course/#{@courseID}" @course = new Course().setURL "/db/course/#{@courseID}"
@course = @supermodel.loadModel(@course, 'course').model @course = @supermodel.loadModel(@course, 'course').model