mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 08:50:58 -05:00
Replace course guide PDFs with solutions pages
This commit is contained in:
parent
5fc184da67
commit
a7e290fffe
9 changed files with 234 additions and 27 deletions
|
@ -149,6 +149,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'teachers/classes': go('courses/TeacherClassesView', { teachersOnly: true })
|
||||
'teachers/classes/:classroomID': go('courses/TeacherClassView', { teachersOnly: true })
|
||||
'teachers/courses': go('courses/TeacherCoursesView')
|
||||
'teachers/course-solution/:courseID/:language': go('teachers/TeacherCourseSolutionView')
|
||||
'teachers/demo': go('teachers/RequestQuoteView')
|
||||
'teachers/enrollments': redirect('/teachers/licenses')
|
||||
'teachers/licenses': go('courses/EnrollmentsView', { teachersOnly: true })
|
||||
|
|
|
@ -337,8 +337,13 @@
|
|||
|
||||
common:
|
||||
back: "Back" # When used as an action verb, like "Navigate backward"
|
||||
coming_soon: "Coming soon!"
|
||||
continue: "Continue" # When used as an action verb, like "Continue forward"
|
||||
default_code: "Default Code"
|
||||
loading: "Loading..."
|
||||
overview: "Overview"
|
||||
solution: "Solution"
|
||||
intro: "Intro"
|
||||
saving: "Saving..."
|
||||
sending: "Sending..."
|
||||
send: "Send"
|
||||
|
@ -1316,15 +1321,14 @@
|
|||
students_assigned: "students assigned"
|
||||
length: "Length:"
|
||||
title: "Courses" # Flat style redesign
|
||||
subtitle: "Review course guidelines, solutions, and levels"
|
||||
subtitle: "Review course overviews and levels" # {change}
|
||||
changelog: "View latest changes to course levels."
|
||||
select_language: "Select language"
|
||||
select_level: "Select level"
|
||||
play_level: "Play Level"
|
||||
concepts_covered: "Concepts covered"
|
||||
print_guide: "Print Guide (PDF)"
|
||||
view_guide_online: "View Guide Online (PDF)"
|
||||
last_updated: "Last updated:"
|
||||
view_guide_online: "Level Overviews and Solutions" # {change}
|
||||
grants_lifetime_access: "Grants access to all Courses."
|
||||
enrollment_credits_available: "Licenses Available:"
|
||||
description: "Description" # ClassroomSettingsModal
|
||||
|
@ -1391,6 +1395,8 @@
|
|||
select_this_hero: "Select this Hero"
|
||||
|
||||
teacher:
|
||||
course_solution: "Course Solution"
|
||||
level_overview_solutions: "Level Overview and Solutions"
|
||||
teacher_dashboard: "Teacher Dashboard" # Navbar
|
||||
my_classes: "My Classes"
|
||||
courses: "Course Guides"
|
||||
|
|
31
app/styles/teachers/teacher-course-solution-view.sass
Normal file
31
app/styles/teachers/teacher-course-solution-view.sass
Normal file
|
@ -0,0 +1,31 @@
|
|||
#teacher-course-solution-view
|
||||
|
||||
background-color: white
|
||||
color: black
|
||||
font-family: sans-serif
|
||||
margin: 0px
|
||||
padding: 24px
|
||||
|
||||
hr
|
||||
display: block
|
||||
border-style: inset
|
||||
border-width: 1px
|
||||
|
||||
h1, h2, h3, h4, h5
|
||||
color: black
|
||||
font-family: sans-serif
|
||||
font-variant: normal
|
||||
margin: 20px 0px
|
||||
|
||||
h4
|
||||
color: gray
|
||||
|
||||
img
|
||||
display: block
|
||||
margin: auto
|
||||
|
||||
p
|
||||
margin: 16px 0px
|
||||
|
||||
.page-break-before
|
||||
page-break-before: always
|
|
@ -17,9 +17,9 @@ block content
|
|||
.container
|
||||
h1(data-i18n="courses.title")
|
||||
h2(data-i18n="courses.subtitle")
|
||||
p
|
||||
a(href="https://discourse.codecombat.com/t/course-level-changelog/7352" data-i18n="courses.changelog")
|
||||
|
||||
//- p
|
||||
//- a(href="https://discourse.codecombat.com/t/course-level-changelog/7352" data-i18n="courses.changelog")
|
||||
|
||||
.courses.container
|
||||
- var courses = view.courses.models;
|
||||
- var courseIndex = 0;
|
||||
|
@ -29,7 +29,7 @@ block content
|
|||
.course.row
|
||||
.col-sm-9
|
||||
+course-info(course)
|
||||
if me.isTeacher()
|
||||
if me.isTeacher() || me.isAdmin()
|
||||
.col-sm-3
|
||||
.play-level-form(data-course-id=course.id)
|
||||
.form-group
|
||||
|
@ -85,22 +85,12 @@ mixin course-info(course)
|
|||
if course.get('concepts').indexOf(concept) !== course.get('concepts').length - 1
|
||||
span.spr ,
|
||||
|
||||
if me.isTeacher() || view.ownedClassrooms.size()
|
||||
if view.guideLinks[course.id]
|
||||
//- a.btn.btn-primary(href=view.guideLinks[course.id] class=(me.isTeacher() ? '': 'disabled'))
|
||||
//- span(data-i18n="courses.print_guide")
|
||||
a.guide-btn.btn.btn-primary(href=view.guideLinks[course.id].python data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide Python" class=(me.isTeacher() ? '': 'disabled'))
|
||||
if me.isTeacher() || view.ownedClassrooms.size() || me.isAdmin()
|
||||
p
|
||||
a.guide-btn.btn.btn-primary(href=("/teachers/course-solution/" + course.id + "/python") data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide Python" class=(me.isTeacher() || me.isAdmin() ? '': 'disabled'))
|
||||
span(data-i18n="courses.view_guide_online")
|
||||
| — Python
|
||||
a.guide-btn.btn.btn-primary(href=view.guideLinks[course.id].javascript data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide JavaScript" class=(me.isTeacher() ? '': 'disabled'))
|
||||
p
|
||||
a.guide-btn.btn.btn-primary(href=("/teachers/course-solution/" + course.id + "/javascript") data-course-id=course.id data-course-name=course.get('name') data-event-action="Classes Guides Guide JavaScript" class=(me.isTeacher() || me.isAdmin() ? '': 'disabled'))
|
||||
span(data-i18n="courses.view_guide_online")
|
||||
| — JavaScript
|
||||
else
|
||||
i.small
|
||||
| (
|
||||
span(data-i18n='teacher.guides_coming_soon')
|
||||
| )
|
||||
if campaign && campaign.get('levelsUpdated')
|
||||
p.small.m-t-2
|
||||
span.spr(data-i18n="courses.last_updated")
|
||||
span= moment(campaign.get('levelsUpdated')).format('LL')
|
||||
|
|
52
app/templates/teachers/teacher-course-solution-view.jade
Normal file
52
app/templates/teachers/teacher-course-solution-view.jade
Normal file
|
@ -0,0 +1,52 @@
|
|||
block content
|
||||
|
||||
if !me.isTeacher() && !me.isAdmin()
|
||||
a(href="/")
|
||||
img#nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
|
||||
h2.text-center(data-i18n="teacher.teacher_account_required")
|
||||
else
|
||||
.text-center
|
||||
img(src="http://direct.codecombat.com/images/pages/base/logo.png")
|
||||
if view.course.loaded
|
||||
h1 #{view.course.get('name')}
|
||||
h3 #{view.prettyLanguage}
|
||||
i= view.course.get('description')
|
||||
br
|
||||
br
|
||||
|
||||
if view.levels
|
||||
each level, index in view.levels.models
|
||||
h2.page-break-before ##{index + 1}. #{level.get('name')}
|
||||
h3(data-i18n="teacher.level_overview_solutions")
|
||||
i #{level.get('description')}
|
||||
div
|
||||
h4.text-center(data-i18n="common.intro")
|
||||
if level.get('intro')
|
||||
p!= level.get('intro')
|
||||
else
|
||||
.text-center
|
||||
i(data-i18n="common.coming_soon")
|
||||
h4.text-center(data-i18n="common.default_code")
|
||||
if level.get('begin')
|
||||
pre!= level.get('begin')
|
||||
else
|
||||
.text-center
|
||||
i(data-i18n="common.coming_soon")
|
||||
div.overview
|
||||
br
|
||||
h4.text-center(data-i18n="common.overview")
|
||||
if level.get('guide')
|
||||
p!= level.get('guide')
|
||||
else
|
||||
.text-center
|
||||
i(data-i18n="common.coming_soon")
|
||||
h4.text-center
|
||||
span= level.get('name')
|
||||
span.spl(data-i18n="common.solution")
|
||||
if level.get('solution')
|
||||
pre!= level.get('solution')
|
||||
else
|
||||
.text-center
|
||||
i(data-i18n="common.coming_soon")
|
||||
hr
|
||||
br
|
47
app/views/teachers/TeacherCourseSolutionView.coffee
Normal file
47
app/views/teachers/TeacherCourseSolutionView.coffee
Normal file
|
@ -0,0 +1,47 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
Course = require 'models/Course'
|
||||
Level = require 'models/Level'
|
||||
|
||||
module.exports = class TeacherCourseSolutionView extends RootView
|
||||
id: 'teacher-course-solution-view'
|
||||
template: require 'templates/teachers/teacher-course-solution-view'
|
||||
|
||||
getTitle: -> $.i18n.t('teacher.course_solution')
|
||||
|
||||
initialize: (options, @courseID, @language) ->
|
||||
if me.isTeacher() or me.isAdmin()
|
||||
@prettyLanguage = @camelCaseLanguage(@language)
|
||||
@course = new Course(_id: @courseID)
|
||||
@supermodel.trackRequest(@course.fetch())
|
||||
@levels = new CocoCollection([], { url: "/db/course/#{@courseID}/level-solutions", model: Level})
|
||||
@supermodel.loadCollection(@levels, 'levels', {cache: false})
|
||||
super(options)
|
||||
|
||||
camelCaseLanguage: (language) ->
|
||||
return language if _.isEmpty(language)
|
||||
return 'JavaScript' if language is 'javascript'
|
||||
language.charAt(0).toUpperCase() + language.slice(1)
|
||||
|
||||
hideWrongLanguage: (s) ->
|
||||
return '' unless s
|
||||
s.replace /```([a-z]+)[^`]+```/gm, (a, l) =>
|
||||
return '' if l isnt @language
|
||||
a
|
||||
|
||||
onLoaded: ->
|
||||
for level in @levels?.models
|
||||
articles = level.get('documentation').specificArticles;
|
||||
if articles
|
||||
guide = articles.filter((x) => x.name == "Overview").pop()
|
||||
level.set 'guide', marked(@hideWrongLanguage(guide.body)) if guide
|
||||
intro = articles.filter((x) => x.name == "Intro").pop()
|
||||
level.set 'intro', marked(@hideWrongLanguage(intro.body)) if intro
|
||||
heroPlaceholder = level.get('thangs').filter((x) => x.id == 'Hero Placeholder').pop()
|
||||
comp = heroPlaceholder?.components.filter((x) => x.original.toString() == '524b7b5a7fc0f6d51900000e' ).pop()
|
||||
programmableMethod = comp?.config.programmableMethods.plan;
|
||||
if programmableMethod
|
||||
level.set 'begin', _.template(programmableMethod.languages[@language] or programmableMethod.source)(programmableMethod.context)
|
||||
solution = programmableMethod.solutions?.find (x) => x.language is @language
|
||||
level.set 'solution', _.template(solution?.source)(programmableMethod.context)
|
||||
@render?()
|
|
@ -1,4 +1,5 @@
|
|||
errors = require '../commons/errors'
|
||||
log = require 'winston'
|
||||
wrap = require 'co-express'
|
||||
database = require '../commons/database'
|
||||
mongoose = require 'mongoose'
|
||||
|
@ -11,6 +12,24 @@ Level = require '../models/Level'
|
|||
parse = require '../commons/parse'
|
||||
|
||||
module.exports =
|
||||
|
||||
fetchLevelSolutions: wrap (req, res) ->
|
||||
unless req.user?.isTeacher() or req.user?.isAdmin()
|
||||
log.debug "courses.fetchLevelSolutions: level solutions only for teachers, (#{req.user?.id})"
|
||||
throw new errors.Forbidden()
|
||||
|
||||
course = yield database.getDocFromHandle(req, Course)
|
||||
throw new errors.NotFound('Course not found.') unless course
|
||||
|
||||
campaign = yield Campaign.findById course.get('campaignID')
|
||||
throw new errors.NotFound('Campaign not found.') unless campaign
|
||||
|
||||
levelOriginals = (mongoose.Types.ObjectId(levelID) for levelID of campaign.get('levels'))
|
||||
query = { original: { $in: levelOriginals }, slug: { $exists: true }}
|
||||
select = {documentation: 1, intro: 1, name: 1, slug: 1, thangs: 1}
|
||||
levels = yield Level.find(query).select(select).lean()
|
||||
res.status(200).send(levels)
|
||||
|
||||
fetchNextLevel: wrap (req, res) ->
|
||||
levelOriginal = req.params.levelOriginal
|
||||
if not database.isID(levelOriginal)
|
||||
|
@ -18,7 +37,7 @@ module.exports =
|
|||
|
||||
course = yield database.getDocFromHandle(req, Course)
|
||||
if not course
|
||||
throw new errors.NotFound('Course Instance not found.')
|
||||
throw new errors.NotFound('Course not found.')
|
||||
|
||||
campaign = yield Campaign.findById course.get('campaignID')
|
||||
if not campaign
|
||||
|
@ -43,7 +62,6 @@ module.exports =
|
|||
|
||||
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})
|
||||
|
||||
|
||||
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
|
||||
dbq.select(parse.getProjectFromReq(req))
|
||||
level = yield dbq
|
||||
|
|
|
@ -82,6 +82,7 @@ module.exports.setup = (app) ->
|
|||
Course = require '../models/Course'
|
||||
app.get('/db/course', mw.courses.get(Course))
|
||||
app.get('/db/course/:handle', mw.rest.getByHandle(Course))
|
||||
app.get('/db/course/:handle/level-solutions', mw.courses.fetchLevelSolutions)
|
||||
app.get('/db/course/:handle/levels/:levelOriginal/next', mw.courses.fetchNextLevel)
|
||||
|
||||
app.get('/db/course_instance/-/non-hoc', mw.auth.checkHasPermission(['admin']), mw.courseInstances.fetchNonHoc)
|
||||
|
|
|
@ -109,9 +109,9 @@ describe 'GET /db/course/:handle/levels/:levelOriginal/next', ->
|
|||
[res, body] = yield request.postAsync {uri: classroomsURL, json: data }
|
||||
expect(res.statusCode).toBe(201)
|
||||
@classroom = yield Classroom.findById(res.body._id)
|
||||
|
||||
|
||||
url = getURL('/db/course')
|
||||
|
||||
|
||||
done()
|
||||
|
||||
it 'returns the next level for the course in the linked classroom', utils.wrap (done) ->
|
||||
|
@ -119,7 +119,7 @@ describe 'GET /db/course/:handle/levels/:levelOriginal/next', ->
|
|||
expect(res.statusCode).toBe(200)
|
||||
expect(res.body.original).toBe(@levelB.original.toString())
|
||||
done()
|
||||
|
||||
|
||||
it 'returns empty object if the given level is the last level in its course', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/levels/#{@levelB.id}/next"), json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
|
@ -130,3 +130,64 @@ describe 'GET /db/course/:handle/levels/:levelOriginal/next', ->
|
|||
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseB.id}/levels/#{@levelA.id}/next"), json: true }
|
||||
expect(res.statusCode).toBe(404)
|
||||
done()
|
||||
|
||||
describe 'GET /db/course/:handle/level-solutions', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.clearModels [User, Classroom, Course, Level, Campaign]
|
||||
admin = yield utils.initAdmin()
|
||||
yield utils.loginUser(admin)
|
||||
|
||||
levelJSON = { name: 'A', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
||||
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
||||
expect(res.statusCode).toBe(200)
|
||||
@levelA = yield Level.findById(res.body._id)
|
||||
paredLevelA = _.pick(res.body, 'name', 'original', 'type')
|
||||
|
||||
levelJSON = { name: 'B', permissions: [{access: 'owner', target: admin.id}], type: 'course' }
|
||||
[res, body] = yield request.postAsync({uri: getURL('/db/level'), json: levelJSON})
|
||||
expect(res.statusCode).toBe(200)
|
||||
@levelB = yield Level.findById(res.body._id)
|
||||
paredLevelB = _.pick(res.body, 'name', 'original', 'type')
|
||||
|
||||
campaignJSONA = { name: 'Campaign A', levels: {} }
|
||||
campaignJSONA.levels[paredLevelA.original] = paredLevelA
|
||||
campaignJSONA.levels[paredLevelB.original] = paredLevelB
|
||||
[res, body] = yield request.postAsync({uri: getURL('/db/campaign'), json: campaignJSONA})
|
||||
@campaignA = yield Campaign.findById(res.body._id)
|
||||
|
||||
@courseA = Course({name: 'Course A', campaignID: @campaignA._id, releasePhase: 'released'})
|
||||
yield @courseA.save()
|
||||
|
||||
done()
|
||||
|
||||
describe 'when admin', ->
|
||||
|
||||
it 'returns level solutions', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/level-solutions"), json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toEqual(2)
|
||||
expect(body[0].slug).toEqual('a')
|
||||
done()
|
||||
|
||||
describe 'when teacher', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
teacher = yield utils.initUser({role: 'teacher'})
|
||||
yield utils.loginUser(teacher)
|
||||
done()
|
||||
|
||||
it 'returns level solutions', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/level-solutions"), json: true }
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(body.length).toEqual(2)
|
||||
expect(body[1].slug).toEqual('b')
|
||||
done()
|
||||
|
||||
describe 'when anonymous', ->
|
||||
beforeEach utils.wrap (done) ->
|
||||
yield utils.logout()
|
||||
done()
|
||||
|
||||
it 'returns 403', utils.wrap (done) ->
|
||||
[res, body] = yield request.getAsync { uri: utils.getURL("/db/course/#{@courseA.id}/level-solutions"), json: true }
|
||||
expect(res.statusCode).toBe(403)
|
||||
done()
|
||||
|
|
Loading…
Reference in a new issue