Merge branch 'master' into vagrant-brunch-v2

# Conflicts:
#	package.json
This commit is contained in:
duybkict 2016-05-26 10:50:40 +07:00
commit 6e36a4abc4
124 changed files with 3173 additions and 1825 deletions
app
collections
core
lib
locale
models
schemas
styles
templates
views
bower.jsonconfig.coffeepackage.json
scripts
server

View file

@ -0,0 +1,7 @@
StripeCoupon = require 'models/StripeCoupon'
CocoCollection = require 'collections/CocoCollection'
module.exports = class StripeCoupons extends CocoCollection
model: StripeCoupon
url: '/stripe/coupons'

View file

@ -29,3 +29,9 @@ module.exports = class Users extends CocoCollection
@remove @filter (user) -> @remove @filter (user) ->
user.get('deleted') user.get('deleted')
true true
search: (term) ->
return @slice() unless term
term = term.toLowerCase()
return @filter (user) ->
user.broadName().toLowerCase().indexOf(term) > -1 or (user.get('email') ? '').indexOf(term) > -1

View file

@ -73,8 +73,8 @@ module.exports = class CocoRouter extends Backbone.Router
'Courses': go('courses/CoursesView') # , { studentsOnly: true }) 'Courses': go('courses/CoursesView') # , { studentsOnly: true })
'courses/students': redirect('/courses') 'courses/students': redirect('/courses')
'courses/teachers': redirect('/teachers/classes') 'courses/teachers': redirect('/teachers/classes')
'courses/purchase': redirect('/teachers/enrollments') 'courses/purchase': redirect('/teachers/licenses')
'courses/enroll(/:courseID)': redirect('/teachers/enrollments') 'courses/enroll(/:courseID)': redirect('/teachers/licenses')
'courses/update-account': go('courses/CoursesUpdateAccountView') 'courses/update-account': go('courses/CoursesUpdateAccountView')
'courses/:classroomID': go('courses/ClassroomView') #, { studentsOnly: true }) 'courses/:classroomID': go('courses/ClassroomView') #, { studentsOnly: true })
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView') 'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView')
@ -146,7 +146,8 @@ module.exports = class CocoRouter extends Backbone.Router
'teachers/classes/:classroomID': go('courses/TeacherClassView') #, { teachersOnly: true }) 'teachers/classes/:classroomID': go('courses/TeacherClassView') #, { teachersOnly: true })
'teachers/courses': go('courses/TeacherCoursesView') 'teachers/courses': go('courses/TeacherCoursesView')
'teachers/demo': go('teachers/RequestQuoteView') 'teachers/demo': go('teachers/RequestQuoteView')
'teachers/enrollments': go('courses/EnrollmentsView') #, { teachersOnly: true }) 'teachers/enrollments': redirect('/teachers/licenses')
'teachers/licenses': go('courses/EnrollmentsView') #, { teachersOnly: true })
'teachers/freetrial': go('teachers/RequestQuoteView') 'teachers/freetrial': go('teachers/RequestQuoteView')
'teachers/quote': go('teachers/RequestQuoteView') 'teachers/quote': go('teachers/RequestQuoteView')
'teachers/signup': -> 'teachers/signup': ->
@ -159,6 +160,7 @@ module.exports = class CocoRouter extends Backbone.Router
'test(/*subpath)': go('TestView') 'test(/*subpath)': go('TestView')
'user/:slugOrID': go('user/MainUserView') 'user/:slugOrID': go('user/MainUserView')
'user/:userID/verify/:verificationCode': go('user/EmailVerifiedView')
'*name/': 'removeTrailingSlash' '*name/': 'removeTrailingSlash'
'*name': go('NotFoundView') '*name': go('NotFoundView')

View file

@ -60,6 +60,12 @@ module.exports.logoutUser = ->
res = $.post('/auth/logout', {}, callback) res = $.post('/auth/logout', {}, callback)
res.fail(genericFailure) res.fail(genericFailure)
module.exports.sendRecoveryEmail = (email, options={}) ->
options = _.merge(options,
{method: 'POST', url: '/auth/reset', data: { email }}
)
$.ajax(options)
onSetVolume = (e) -> onSetVolume = (e) ->
return if e.volume is me.get('volume') return if e.volume is me.get('volume')
me.set('volume', e.volume) me.set('volume', e.volume)

View file

@ -1,10 +1,19 @@
module.exports.sendContactMessage = (contactMessageObject, modal) -> module.exports = {
modal?.find('.sending-indicator').show() sendContactMessage: (contactMessageObject, modal) ->
jqxhr = $.post '/contact', contactMessageObject, (response) -> # deprecated
return unless modal modal?.find('.sending-indicator').show()
modal.find('.sending-indicator').hide() return $.post '/contact', contactMessageObject, (response) ->
modal.find('#contact-message').val('Thanks!') return unless modal
_.delay -> modal.find('.sending-indicator').hide()
modal.find('#contact-message').val('') modal.find('#contact-message').val('Thanks!')
modal.modal 'hide' _.delay ->
, 1000 modal.find('#contact-message').val('')
modal.modal 'hide'
, 1000
send: (options={}) ->
options.type = 'POST'
options.url = '/contact'
$.ajax(options)
}

View file

@ -238,10 +238,8 @@ codeLanguages =
javascript: 'ace/mode/javascript' javascript: 'ace/mode/javascript'
coffeescript: 'ace/mode/coffee' coffeescript: 'ace/mode/coffee'
python: 'ace/mode/python' python: 'ace/mode/python'
clojure: 'ace/mode/clojure'
lua: 'ace/mode/lua' lua: 'ace/mode/lua'
java: 'ace/mode/java' java: 'ace/mode/java'
io: 'ace/mode/text'
class CodeLanguagesObjectTreema extends TreemaNode.nodeMap.object class CodeLanguagesObjectTreema extends TreemaNode.nodeMap.object
childPropertiesAvailable: -> childPropertiesAvailable: ->

View file

@ -267,9 +267,7 @@ module.exports.aceEditModes = aceEditModes =
'coffeescript': 'ace/mode/coffee' 'coffeescript': 'ace/mode/coffee'
'python': 'ace/mode/python' 'python': 'ace/mode/python'
'java': 'ace/mode/java' 'java': 'ace/mode/java'
'clojure': 'ace/mode/clojure'
'lua': 'ace/mode/lua' 'lua': 'ace/mode/lua'
'io': 'ace/mode/text'
'java': 'ace/mode/java' 'java': 'ace/mode/java'
module.exports.initializeACE = (el, codeLanguage) -> module.exports.initializeACE = (el, codeLanguage) ->
@ -294,13 +292,9 @@ module.exports.initializeACE = (el, codeLanguage) ->
session.setNewLineMode 'unix' session.setNewLineMode 'unix'
return editor return editor
module.exports.capitalLanguages = capitalLanguages = module.exports.capitalLanguages = capitalLanguages =
'javascript': 'JavaScript' 'javascript': 'JavaScript'
'coffeescript': 'CoffeeScript' 'coffeescript': 'CoffeeScript'
'python': 'Python' 'python': 'Python'
'java': 'Java' 'java': 'Java'
'clojure': 'Clojure'
'lua': 'Lua' 'lua': 'Lua'
'io': 'Io'

View file

@ -126,8 +126,7 @@ module.exports = class LevelLoader extends CocoClass
@sessionResource = @supermodel.loadModel(session, 'level_session', {cache: false}) @sessionResource = @supermodel.loadModel(session, 'level_session', {cache: false})
@session = @sessionResource.model @session = @sessionResource.model
if @opponentSessionID if @opponentSessionID
opponentURL = "/db/level.session/#{@opponentSessionID}" opponentURL = "/db/level.session/#{@opponentSessionID}?interpret=true"
opponentURL += "?interpret=true" if @spectateMode or utils.getQueryVariable 'esper'
opponentSession = new LevelSession().setURL opponentURL opponentSession = new LevelSession().setURL opponentURL
opponentSession.project = session.project if @headless opponentSession.project = session.project if @headless
@opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session', {cache: false}) @opponentSessionResource = @supermodel.loadModel(opponentSession, 'opponent_session', {cache: false})
@ -158,6 +157,8 @@ module.exports = class LevelLoader extends CocoClass
code[if session.get('team') is 'humans' then 'hero-placeholder' else 'hero-placeholder-1'].plan = uncompressed code[if session.get('team') is 'humans' then 'hero-placeholder' else 'hero-placeholder-1'].plan = uncompressed
session.set 'code', code session.set 'code', code
session.unset 'interpret' session.unset 'interpret'
if session.get('codeLanguage') in ['io', 'clojure']
session.set 'codeLanguage', 'python'
if session is @session if session is @session
@addSessionBrowserInfo session @addSessionBrowserInfo session
# hero-ladder games require the correct session team in level:loaded # hero-ladder games require the correct session team in level:loaded

View file

@ -7,14 +7,6 @@ module.exports.createAetherOptions = (options) ->
throw new Error 'Specify a function name to create an Aether instance' unless options.functionName throw new Error 'Specify a function name to create an Aether instance' unless options.functionName
throw new Error 'Specify a code language to create an Aether instance' unless options.codeLanguage throw new Error 'Specify a code language to create an Aether instance' unless options.codeLanguage
useInterpreter = options.useInterpreter
defaultToEsper = true #switch options.codeLanguage
# when 'python' then me.level() < 15 # Esper currently works well until using range()
# when 'javascript' then me.level() < 22 # Esper currently works well until using hero.myFn = function() pattern
# when 'lua' then me.level() < 10 # Functions don't work in Esper yet, can't play forest function levels
# when 'coffeescript' then false # CoffeeScript has a toNative error if it ever finishes plan(), and also @fn = -> pattern doesn't work
# when 'clojure' then false # No Clojure support
useInterpreter ?= !!utils.getQueryVariable 'esper', defaultToEsper
aetherOptions = aetherOptions =
functionName: options.functionName functionName: options.functionName
protectAPI: not options.skipProtectAPI protectAPI: not options.skipProtectAPI
@ -37,7 +29,7 @@ module.exports.createAetherOptions = (options) ->
#functionParameters: # TODOOOOO #functionParameters: # TODOOOOO
executionLimit: 3 * 1000 * 1000 executionLimit: 3 * 1000 * 1000
language: options.codeLanguage language: options.codeLanguage
useInterpreter: useInterpreter useInterpreter: true
parameters = functionParameters[options.functionName] parameters = functionParameters[options.functionName]
unless parameters unless parameters
console.warn "Unknown method #{options.functionName}: please add function parameters to lib/aether_utils.coffee." console.warn "Unknown method #{options.functionName}: please add function parameters to lib/aether_utils.coffee."

View file

@ -11,9 +11,14 @@ module.exports =
instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id }) instance = courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
continue if not instance continue if not instance
instance.numCompleted = 0 instance.numCompleted = 0
instance.numStarted = 0 instance.started = false
levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true}) levels = classroom.getLevels({courseID: course.id, withoutLadderLevels: true})
for userID in instance.get('members') for userID in instance.get('members')
instance.started ||= _.any levels.models, (level) ->
return false if level.isLadder()
session = _.find classroom.sessions.models, (session) ->
session.get('creator') is userID and session.get('level').original is level.get('original')
session?
levelCompletes = _.map levels.models, (level) -> levelCompletes = _.map levels.models, (level) ->
return true if level.isLadder() return true if level.isLadder()
#TODO: Hella slow! Do the mapping first! #TODO: Hella slow! Do the mapping first!
@ -23,8 +28,6 @@ module.exports =
session?.completed() session?.completed()
if _.every levelCompletes if _.every levelCompletes
instance.numCompleted += 1 instance.numCompleted += 1
if _.any levelCompletes
instance.numStarted += 1
calculateEarliestIncomplete: (classroom, courses, courseInstances, students) -> calculateEarliestIncomplete: (classroom, courses, courseInstances, students) ->
# Loop through all the combinations of things, return the first one that somebody hasn't finished # Loop through all the combinations of things, return the first one that somebody hasn't finished

View file

@ -443,7 +443,7 @@ module.exports = class Simulator extends CocoClass
aether.transpile '' aether.transpile ''
createAether: (methodName, method, useProtectAPI, codeLanguage) -> createAether: (methodName, method, useProtectAPI, codeLanguage) ->
aetherOptions = createAetherOptions functionName: methodName, codeLanguage: codeLanguage, skipProtectAPI: not useProtectAPI, useInterpreter: true aetherOptions = createAetherOptions functionName: methodName, codeLanguage: codeLanguage, skipProtectAPI: not useProtectAPI
return new Aether aetherOptions return new Aether aetherOptions
class SimulationTask class SimulationTask

View file

@ -145,21 +145,15 @@ module.exports.thangNames = thangNames =
'Ofgar' 'Ofgar'
'Randall' 'Randall'
] ]
'Raven': [ 'Raven Pet': [
# Animal # Animal
'Baltimore'
'Columbia'
'Dawnstar'
'Nevermore' 'Nevermore'
] ]
'Cougar': [ 'Cougar Pet': [
# Animal # Animal
'Guenhwyvar'
'Kitty' 'Kitty'
'Shasta'
'Simbia'
] ]
'Frog': [ 'Frog Pet': [
# Animal # Animal
'Bighead' 'Bighead'
'Bufo' 'Bufo'

View file

@ -1,4 +1,4 @@
module.exports = nativeDescription: "English", englishDescription: "English", translation: module.exports = nativeDescription: "English", englishDescription: "English", translation:
home: home:
slogan: "Learn to Code by Playing a Game" slogan: "Learn to Code by Playing a Game"
no_ie: "CodeCombat does not run in Internet Explorer 8 or older. Sorry!" # Warning that only shows up in IE8 and older no_ie: "CodeCombat does not run in Internet Explorer 8 or older. Sorry!" # Warning that only shows up in IE8 and older
@ -132,6 +132,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
help_suff: "and we'll get in touch!" help_suff: "and we'll get in touch!"
modal: modal:
cancel: "Cancel"
close: "Close" close: "Close"
okay: "Okay" okay: "Okay"
@ -645,9 +646,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
python_blurb: "Simple yet powerful, great for beginners and experts." python_blurb: "Simple yet powerful, great for beginners and experts."
javascript_blurb: "The language of the web. (Not the same as Java.)" javascript_blurb: "The language of the web. (Not the same as Java.)"
coffeescript_blurb: "Nicer JavaScript syntax." coffeescript_blurb: "Nicer JavaScript syntax."
clojure_blurb: "A modern Lisp."
lua_blurb: "Game scripting language." lua_blurb: "Game scripting language."
io_blurb: "Simple but obscure."
java_blurb: "(Subscriber Only) Android and enterprise." java_blurb: "(Subscriber Only) Android and enterprise."
status: "Status" status: "Status"
hero_type: "Type" hero_type: "Type"
@ -861,7 +860,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
signup_with: "Sign up with:" signup_with: "Sign up with:"
connect_with: "Connect with:" connect_with: "Connect with:"
conversion_warning: "WARNING: Your current account is a <em>Student Account</em>. Once you submit this form, your account will be updated to a Teacher Account." conversion_warning: "WARNING: Your current account is a <em>Student Account</em>. Once you submit this form, your account will be updated to a Teacher Account."
learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account." learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign licenses and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account." # {change}
create_account: "Create a Teacher Account" create_account: "Create a Teacher Account"
create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. <strong>Set up a class</strong>, add your students, and <strong>monitor their progress</strong>!" create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. <strong>Set up a class</strong>, add your students, and <strong>monitor their progress</strong>!"
convert_account_title: "Update to Teacher Account" convert_account_title: "Update to Teacher Account"
@ -1115,13 +1114,13 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
get_free: "Get FREE course" get_free: "Get FREE course"
enroll_paid: "Enroll Students in Paid Courses" enroll_paid: "Enroll Students in Paid Courses"
you_have1: "You have" you_have1: "You have"
you_have2: "unused paid enrollments" you_have2: "unused student licenses" # {change}
use_one: "Use 1 paid enrollment for" use_one: "Use 1 student license for" # {change}
use_multiple: "Use paid enrollments for the following students:" use_multiple: "Use licenses for the following students:" # {change}
already_enrolled: "already enrolled" already_enrolled: "already enrolled"
licenses_remaining: "licenses remaining:" licenses_remaining: "licenses remaining:"
insufficient_enrollments: "insufficient paid enrollments" insufficient_enrollments: "insufficient student licenses" # {change}
get_enrollments: "Get More Enrollments" get_enrollments: "Get More Licenses" # {change}
change_language: "Change Course Language" change_language: "Change Course Language"
keep_using: "Keep Using" keep_using: "Keep Using"
switch_to: "Switch To" switch_to: "Switch To"
@ -1130,7 +1129,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
back_courses: "Back to my courses" back_courses: "Back to my courses"
edit_details: "Edit class details" edit_details: "Edit class details"
enrolled_courses: "enrolled in paid courses:" enrolled_courses: "enrolled in paid courses:"
purchase_enrollments: "Purchase Enrollments" purchase_enrollments: "Purchase Student Licenses" # {change}
remove_student: "remove student" remove_student: "remove student"
assign: "Assign" assign: "Assign"
to_assign: "to assign paid courses." to_assign: "to assign paid courses."
@ -1175,6 +1174,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
view_class: "view class" view_class: "view class"
view_levels: "view levels" view_levels: "view levels"
join_class: "Join A Class" join_class: "Join A Class"
join_class_2: "Join class"
ask_teacher_for_code: "Ask your teacher if you have a CodeCombat class code! If so, enter it below:" ask_teacher_for_code: "Ask your teacher if you have a CodeCombat class code! If so, enter it below:"
enter_c_code: "<Enter Class Code>" enter_c_code: "<Enter Class Code>"
join: "Join" join: "Join"
@ -1201,10 +1201,10 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
return_to_course_man: "Return to course management." return_to_course_man: "Return to course management."
students_not_enrolled: "students not enrolled" students_not_enrolled: "students not enrolled"
total_all_classes: "Total Across All Classes" total_all_classes: "Total Across All Classes"
how_many_enrollments: "How many additional paid enrollments do you need?" how_many_enrollments: "How many additional student licenses do you need?" # {change}
each_student_access: "Each student in a class will get access to Courses 2-4 once they are enrolled in paid courses. You may assign each course to each student individually." each_student_access: "Each student in a class will get access to Courses 2-4 once they are enrolled in paid courses. You may assign each course to each student individually."
purchase_now: "Purchase Now" purchase_now: "Purchase Now"
enrollments: "enrollments" enrollments: "licenses" # {change}
remove_student1: "Remove Student" remove_student1: "Remove Student"
are_you_sure: "Are you sure you want to remove this student from this class?" are_you_sure: "Are you sure you want to remove this student from this class?"
remove_description1: "Student will lose access to this classroom and assigned classes. Progress and gameplay is NOT lost, and the student can be added back to the classroom at any time." remove_description1: "Student will lose access to this classroom and assigned classes. Progress and gameplay is NOT lost, and the student can be added back to the classroom at any time."
@ -1232,20 +1232,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
getting_started_1: "Create a new class by clicking the green 'Create New Class' button below." getting_started_1: "Create a new class by clicking the green 'Create New Class' button below."
getting_started_2: "Once you've created a class, click the blue 'Add Students' button." getting_started_2: "Once you've created a class, click the blue 'Add Students' button."
getting_started_3: "You'll see student's progress below as they sign up and join your class." getting_started_3: "You'll see student's progress below as they sign up and join your class."
additional_resources: "Additional Resources"
additional_resources_1_pref: "Download/print our"
additional_resources_1_mid: "Course 1"
additional_resources_1_mid2: "and"
additional_resources_1_mid3: "Course 2"
additional_resources_1_suff: "teacher's guides with solutions for each level."
additional_resources_2_pref: "Complete our"
additional_resources_2_suff: "to get two free enrollments for the rest of our paid courses."
additional_resources_3_pref: "Visit our"
additional_resources_3_mid: "Teacher Forums"
additional_resources_3_suff: "to connect to fellow educators who are using CodeCombat."
additional_resources_4_pref: "Check out our"
additional_resources_4_mid: "Schools Page"
additional_resources_4_suff: "to learn more about CodeCombat's classroom offerings."
educator_wiki_pref: "Or check out our new" educator_wiki_pref: "Or check out our new"
educator_wiki_mid: "educator wiki" educator_wiki_mid: "educator wiki"
educator_wiki_suff: "to browse the guide online." educator_wiki_suff: "to browse the guide online."
@ -1253,8 +1239,8 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
no_classes: "No classes yet!" no_classes: "No classes yet!"
create_new_class1: "create new class" create_new_class1: "create new class"
available_courses: "Available Courses" available_courses: "Available Courses"
unused_enrollments: "Unused enrollments available:" unused_enrollments: "Unused licenses available:" # {change}
students_access: "All students get access to Introduction to Computer Science for free. One enrollment per student is required to assign them to paid CodeCombat courses. A single student does not need multiple enrollments to access all paid courses." students_access: "All students get access to Introduction to Computer Science for free. One license per student is required to assign them to paid CodeCombat courses. A single student does not need multiple licenses to access all paid courses." # {change}
active_courses: "active courses" active_courses: "active courses"
no_students: "No students yet!" no_students: "No students yet!"
add_students1: "add students" add_students1: "add students"
@ -1273,7 +1259,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
view_guide_online: "View Guide Online (PDF)" view_guide_online: "View Guide Online (PDF)"
last_updated: "Last updated:" last_updated: "Last updated:"
grants_lifetime_access: "Grants access to all Courses." grants_lifetime_access: "Grants access to all Courses."
enrollment_credits_available: "Enrollment Credits Available:" enrollment_credits_available: "Licenses Available:" # {change}
description: "Description" # ClassroomSettingsModal description: "Description" # ClassroomSettingsModal
language_select: "Select a language" language_select: "Select a language"
language_cannot_change: "Language cannot be changed once students join a class." language_cannot_change: "Language cannot be changed once students join a class."
@ -1321,12 +1307,21 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
update_account_update_student: "Update to Student" update_account_update_student: "Update to Student"
update_account_not_sure: "Not sure which one to choose? Email" update_account_not_sure: "Not sure which one to choose? Email"
update_account_confirm_update_student: "Are you sure you want to update your account to a Student experience?\n\nYou will not be able to manage any classes that you have previously created or create new classes. Your previously created classes will be removed from CodeCombat and cannot be restored." update_account_confirm_update_student: "Are you sure you want to update your account to a Student experience?\n\nYou will not be able to manage any classes that you have previously created or create new classes. Your previously created classes will be removed from CodeCombat and cannot be restored."
instructor: "Instructor: "
youve_been_invited_1: "You've been invited to join "
youve_been_invited_2: ", where you'll learn "
youve_been_invited_3: " with your classmates in CodeCombat."
by_joining_1: "By joining "
by_joining_2: "will be able to help reset your password if you forget or lose it. You can also verify your email address so that you can reset the password yourself!"
sent_verification: "We've sent a verification email to:"
you_can_edit: "You can edit your email address in "
account_settings: "Account Settings"
teacher: teacher:
teacher_dashboard: "Teacher Dashboard" # Navbar teacher_dashboard: "Teacher Dashboard" # Navbar
my_classes: "My Classes" my_classes: "My Classes"
courses: "Courses" courses: "Course Guides" # {change}
enrollments: "Enrollments" enrollments: "Student Licenses" # {change}
resources: "Resources" resources: "Resources"
help: "Help" help: "Help"
students: "Students" # Shared students: "Students" # Shared
@ -1337,7 +1332,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
teacher_account_required: "A teacher account is required to access this content." teacher_account_required: "A teacher account is required to access this content."
create_teacher_account: "Create Teacher Account" create_teacher_account: "Create Teacher Account"
what_is_a_teacher_account: "What's a Teacher Account?" what_is_a_teacher_account: "What's a Teacher Account?"
teacher_account_explanation: "A CodeCombat Teacher account allows you to set up classrooms, monitor students progress as they work through courses, manage enrollments and access resources to aid in your curriculum-building." teacher_account_explanation: "A CodeCombat Teacher account allows you to set up classrooms, monitor students progress as they work through courses, manage licenses and access resources to aid in your curriculum-building." # {change}
current_classes: "Current Classes" current_classes: "Current Classes"
archived_classes: "Archived Classes" archived_classes: "Archived Classes"
archived_classes_blurb: "Classes can be archived for future reference. Unarchive a class to view it in the Current Classes list again." archived_classes_blurb: "Classes can be archived for future reference. Unarchive a class to view it in the Current Classes list again."
@ -1362,6 +1357,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
course_progress: "Course Progress" course_progress: "Course Progress"
not_applicable: "N/A" not_applicable: "N/A"
edit: "edit" edit: "edit"
edit_2: "Edit"
remove: "remove" remove: "remove"
latest_completed: "Latest Completed" latest_completed: "Latest Completed"
sort_by: "Sort by" sort_by: "Sort by"
@ -1388,14 +1384,14 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
enroll_the_following_students: "Enroll the following students" enroll_the_following_students: "Enroll the following students"
all_students: "All Students" all_students: "All Students"
enroll_students: "Enroll Students" enroll_students: "Enroll Students"
not_enough_enrollments: "Not enough Enrollments available." not_enough_enrollments: "Not enough licenses available." # {change}
enrollments_blurb_1: "Students taking Computer Science" # Enrollments page enrollments_blurb_1: "Students taking Computer Science" # {change}
enrollments_blurb_2: "require enrollments to access the courses." enrollments_blurb_2: "require a license to access the courses." # {change}
credits_available: "Credits Available" credits_available: "Licenses Available" # {change}
total_unique_students: "Total Unique Students" total_unique_students: "Total Students" # {change}
total_enrolled_students: "Total Enrolled Students" total_enrolled_students: "Enrolled Students" # {change}
unenrolled_students: "Unenrolled Students" unenrolled_students: "Unenrolled Students"
add_enrollment_credits: "Add Enrollment Credits" add_enrollment_credits: "Add Licenses" # {change}
purchasing: "Purchasing..." purchasing: "Purchasing..."
purchased: "Purchased!" purchased: "Purchased!"
purchase_now: "Purchase Now" purchase_now: "Purchase Now"
@ -1406,6 +1402,27 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
bulk_pricing_blurb: "Purchasing for more than 25 students? Contact us to discuss next steps." bulk_pricing_blurb: "Purchasing for more than 25 students? Contact us to discuss next steps."
total_unenrolled: "Total unenrolled" total_unenrolled: "Total unenrolled"
export_student_progress: "Export Student Progress (CSV)" export_student_progress: "Export Student Progress (CSV)"
send_email_to: "Send Recover Password Email to:"
email_sent: "Email sent"
send_recovery_email: "Send recovery email"
enter_new_password_below: "Enter new password below:"
change_password: "Change Password"
changed: "Changed"
available_credits: "Available Licenses" # {change}
pending_credits: "Pending Licenses" # {change}
credits: "licenses" # {change}
start_date: "start date:"
end_date: "end date:"
num_enrollments_needed: "Number of licenses needed:" # {change}
get_enrollments_blurb: " We'll help you build a solution that meets the needs of your class, school or district."
enroll_request_sent_blurb1: "Thanks! Your request has been sent."
enroll_request_sent_blurb2: "Our classroom success team will be in touch shortly to help you find the best solution for your students' needs!"
enroll_request_sent_blurb3: "Please reach out to <a href='mailto:schools@codecombat.com'>schools@codecombat.com</a> if you have additional questions at this time."
request_sent: "Request Sent!"
enrollment_status: "Enrollment Status"
status_expired: "Expired on {{date}}"
status_not_enrolled: "Not Enrolled"
status_enrolled: "Expires on {{date}}"
classes: classes:
archmage_title: "Archmage" archmage_title: "Archmage"
@ -1563,7 +1580,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
scribe_introduction_url_mozilla: "Mozilla Developer Network" scribe_introduction_url_mozilla: "Mozilla Developer Network"
scribe_introduction_suf: " has built. If your idea of fun is articulating the concepts of programming in Markdown form, then this class might be for you." scribe_introduction_suf: " has built. If your idea of fun is articulating the concepts of programming in Markdown form, then this class might be for you."
scribe_attribute_1: "Skill in words is pretty much all you need. Not only grammar and spelling, but able to convey complicated ideas to others." scribe_attribute_1: "Skill in words is pretty much all you need. Not only grammar and spelling, but able to convey complicated ideas to others."
contact_us_url: "Contact us" contact_us_url: "Contact Us" # {change}
scribe_join_description: "tell us a little about yourself, your experience with programming and what sort of things you'd like to write about. We'll go from there!" scribe_join_description: "tell us a little about yourself, your experience with programming and what sort of things you'd like to write about. We'll go from there!"
scribe_subscribe_desc: "Get emails about article writing announcements." scribe_subscribe_desc: "Get emails about article writing announcements."
diplomat_introduction_pref: "So, if there's one thing we learned from the " diplomat_introduction_pref: "So, if there's one thing we learned from the "
@ -1706,6 +1723,15 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
card: "Card" card: "Card"
status_unsubscribed_active: "You're not subscribed and won't be billed, but your account is still active for now." status_unsubscribed_active: "You're not subscribed and won't be billed, but your account is still active for now."
status_unsubscribed: "Get access to new levels, heroes, items, and bonus gems with a CodeCombat subscription!" status_unsubscribed: "Get access to new levels, heroes, items, and bonus gems with a CodeCombat subscription!"
not_yet_verified: "Not yet verified."
resend_email: "Resend email"
email_sent: "Email sent! Check your inbox."
verifying_email: "Verifying your email address..."
successfully_verified: "You've successfully verified your email address!"
back_to_student_page: "Go back to student things"
back_to_teacher_page: "Go to My Classes"
back_to_game: "Go play some more levels!"
verify_error: "Something went wrong when verifying your email :("
account_invoices: account_invoices:
amount: "Amount in US dollars" amount: "Amount in US dollars"

View file

@ -102,7 +102,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
blog: "Блог" blog: "Блог"
forum: "Форум" forum: "Форум"
account: "Налог" account: "Налог"
# my_account: "My Account" my_account: "Мој налог"
profile: "Профил" profile: "Профил"
stats: "Статистика" stats: "Статистика"
code: "Код" code: "Код"
@ -629,7 +629,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
currently_free: "Тренутно имате бесплатну претплату" currently_free: "Тренутно имате бесплатну претплату"
currently_free_until: "Тренутно имате претплату до" currently_free_until: "Тренутно имате претплату до"
was_free_until: "Имали сте бесплатну претплату до" was_free_until: "Имали сте бесплатну претплату до"
# managed_subs: "Managed Subscriptions" managed_subs: "Успешне претплате"
subscribing: "Претплата је у току..." subscribing: "Претплата је у току..."
current_recipients: "Тренутни примаоци" current_recipients: "Тренутни примаоци"
unsubscribing: "Одјава претплате" unsubscribing: "Одјава претплате"
@ -648,7 +648,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
clojure_blurb: "Модерни Lisp." clojure_blurb: "Модерни Lisp."
lua_blurb: "Скриптни језик за игре." lua_blurb: "Скриптни језик за игре."
io_blurb: "Једноставан, али непознат." io_blurb: "Једноставан, али непознат."
# java_blurb: "(Subscriber Only) Android and enterprise." java_blurb: "(Само за претплатнике) Андроид и предузетништво."
status: "Статус" status: "Статус"
hero_type: "Врста" hero_type: "Врста"
weapons: "Оружја" weapons: "Оружја"
@ -808,75 +808,75 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
next: "Следеће" next: "Следеће"
location_title: "Налазимо се у центру Сан Франциска:" location_title: "Налазимо се у центру Сан Франциска:"
# teachers: teachers:
# who_for_title: "Who is CodeCombat for?" who_for_title: "За кога је CodeCombat?"
# who_for_1: "We recommend CodeCombat for students aged 9 and up. No prior programming experience is needed. We've designed CodeCombat to appeal to both boys and girls." who_for_1: "Ми препоручујемо CodeCombat ученицима старости 9 година и више. Претходно искуство у програмирању није потребно. Дизајнирали смо CodeCombat да се допадне и дечацима и девојчицама."
# who_for_2: "Our Courses system allows teachers to set up classrooms, track progress and assign additional content to students through a dedicated interface." who_for_2: "Наш систем Курсеви омогућава учитељима да подесе разреде, прате напредак и доделе додатни материјал ученицима кроз наменски интерфејс."
# more_info_title: "Where can I find more information?" more_info_title: "Где могу да нађем више информација?"
# more_info_1: "Our" more_info_1: "Наш"
# more_info_2: "teachers forum" more_info_2: "форум за учитеље"
# more_info_3: "is a good place to connect with fellow educators who are using CodeCombat." more_info_3: "је добро место да се повежеш са колегама едукаторима који користе CodeCombat."
# teachers_quote: teachers_quote:
# name: "Demo Form" name: "Демо формулар"
# title: "Request a Demo" title: "Затражи демо верзију"
# subtitle: "Get your students started in less than an hour. You'll be able to <strong>create a class, add students, and monitor their progress</strong> as they learn computer science." subtitle: "Нека твоји ученици почну за мање од једног сата. Моћи ћеш да <strong>направиш разред, додаш ученике и пратиш њихов напредак</strong> док уче компјутерске науке."
# email_exists: "User exists with this email." email_exists: "Корисник постоји са овим мејлом."
# phone_number: "Phone number" phone_number: "Број телефона"
# phone_number_help: "Where can we reach you during the workday?" phone_number_help: "Где можемо да те добијемо током радног дана?"
# primary_role_label: "Your Primary Role" primary_role_label: "Твоја примарна улога"
# role_default: "Select Role" role_default: "Изабери улогу"
# primary_role_default: "Select Primary Role" primary_role_default: "Изабери примарну улогу"
# purchaser_role_default: "Select Purchaser Role" purchaser_role_default: "Изабери куповну улогу"
# tech_coordinator: "Technology coordinator" tech_coordinator: "Технички координатор"
# advisor: "Advisor" advisor: "Саветннк"
# principal: "Principal" principal: "Директор"
# superintendent: "Superintendent" superintendent: "Управник"
# parent: "Parent" parent: "Родитељ"
# purchaser_role_label: "Your Purchaser Role" purchaser_role_label: "Твоја куповна улога"
# influence_advocate: "Influence/Advocate" influence_advocate: "Утицај/заступник"
# evaluate_recommend: "Evaluate/Recommend" evaluate_recommend: "Евалуација/препорука"
# approve_funds: "Approve Funds" approve_funds: "Одобрење средстава"
# no_purchaser_role: "No role in purchase decisions" no_purchaser_role: "Без улоге у куповним одлукама"
# organization_label: "Name of School/District" organization_label: "Име школе/округа"
# city: "City" city: "Град"
# state: "State" state: "Савезна држава"
# country: "Country" country: "Држава"
# num_students_help: "How many do you anticipate enrolling in CodeCombat?" num_students_help: "Колико ученика очекујеш да се упишу на CodeCombat?"
# num_students_default: "Select Range" num_students_default: "Изабери опсег"
# education_level_label: "Education Level of Students" education_level_label: "Образовни ниво ученика"
# education_level_help: "Choose as many as apply." education_level_help: "Изабери колико год важи."
# elementary_school: "Elementary School" elementary_school: "Основна школа"
# high_school: "High School" high_school: "Средња школа"
# please_explain: "(please explain)" please_explain: "(објасните)"
# middle_school: "Middle School" middle_school: "Основна школа (виши разреди)"
# college_plus: "College or higher" college_plus: "Факултет или више"
# anything_else: "Anything else we should know?" anything_else: "Још нешто што треба да знамо?"
# thanks_header: "Request Received!" thanks_header: "Захтев је примљен!"
# thanks_sub_header: "Thanks for expressing interest in CodeCombat for your school." thanks_sub_header: "Хвала на интересовању за CodeCombat за твоју школу."
# thanks_p: "We'll be in touch soon! If you need to get in contact, you can reach us at:" thanks_p: "Бићемо у контакту ускоро! Ако желиш да ступиш у контакт, можеш нас добити на:"
# back_to_classes: "Back to Classes" back_to_classes: "Назад на разреде"
# finish_signup: "Finish creating your teacher account:" finish_signup: "Заврши креирање свој учитељског налога:"
# finish_signup_p: "Create an account to set up a class, add your students, and monitor their progress as they learn computer science." finish_signup_p: "Направи налог да оснујеш разред, додаш своје ученике и пратиш њихов напредак док уче компјутерске науке."
# signup_with: "Sign up with:" signup_with: "Пријави се са:"
# connect_with: "Connect with:" connect_with: "Повежи се са:"
# conversion_warning: "WARNING: Your current account is a <em>Student Account</em>. Once you submit this form, your account will be updated to a Teacher Account." # conversion_warning: "WARNING: Your current account is a <em>Student Account</em>. Once you submit this form, your account will be updated to a Teacher Account."
# learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account." # learn_more_modal: "Teacher accounts on CodeCombat have the ability to monitor student progress, assign enrollments and manage classrooms. Teacher accounts cannot be a part of a classroom - if you are currently enrolled in a class using this account, you will no longer be able to access it once you update to a Teacher Account."
# create_account: "Create a Teacher Account" create_account: "Направи учитељски налог"
# create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. <strong>Set up a class</strong>, add your students, and <strong>monitor their progress</strong>!" # create_account_subtitle: "Get access to teacher-only tools for using CodeCombat in the classroom. <strong>Set up a class</strong>, add your students, and <strong>monitor their progress</strong>!"
# convert_account_title: "Update to Teacher Account" # convert_account_title: "Update to Teacher Account"
# not: "Not" # not: "Not"
# setup_a_class: "Set Up a Class" setup_a_class: "Подеси разред"
# versions: versions:
# save_version_title: "Save New Version" save_version_title: "Сачувај нову верзију"
# new_major_version: "New Major Version" new_major_version: "Нова главна верзија"
# submitting_patch: "Submitting Patch..." submitting_patch: "Подношење измене..."
# cla_prefix: "To save changes, first you must agree to our" cla_prefix: "Да би сачувао измене, прво мораш да се сложиш са нашимr"
# cla_url: "CLA" cla_url: "CLA"
# cla_suffix: "." cla_suffix: "."
# cla_agree: "I AGREE" cla_agree: "СЛАЖЕМ СЕ"
# owner_approve: "An owner will need to approve it before your changes will become visible." owner_approve: "Власник ће морати да одобри твоје измене пре него што постану видљиве."
contact: contact:
contact_us: "Контактирај CodeCombat" contact_us: "Контактирај CodeCombat"
@ -900,18 +900,18 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
autosave: "Измене се чувају аутоматски" autosave: "Измене се чувају аутоматски"
me_tab: "Ја" me_tab: "Ја"
picture_tab: "Фотографија" picture_tab: "Фотографија"
# delete_account_tab: "Delete Your Account" delete_account_tab: "Избриши свој налог"
# wrong_email: "Wrong Email" wrong_email: "Погрешан мејл"
# wrong_password: "Wrong Password" wrong_password: "Погрешна шифра"
# upload_picture: "Upload a picture" upload_picture: "Постави слику"
# delete_this_account: "Delete this account permanently" delete_this_account: "Избриши овај налог заувек"
# reset_progress_tab: "Reset All Progress" reset_progress_tab: "Ресетуј цео напредак"
# reset_your_progress: "Clear all your progress and start over" reset_your_progress: "Избриши цео свој напредак и почни поново"
# god_mode: "God Mode" # god_mode: "God Mode"
password_tab: "Шифра" password_tab: "Шифра"
emails_tab: "Мејлови" emails_tab: "Мејлови"
# admin: "Admin" # admin: "Admin"
# manage_subscription: "Click here to manage your subscription." manage_subscription: "Кликни овде да би управљао својом претплатом."
new_password: "Нова Шифра" new_password: "Нова Шифра"
new_password_verify: "Потврди" new_password_verify: "Потврди"
# type_in_email: "Type in your email to confirm account deletion." # type_in_email: "Type in your email to confirm account deletion."
@ -925,8 +925,8 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity." # email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
# email_any_notes: "Any Notifications" # email_any_notes: "Any Notifications"
# email_any_notes_description: "Disable to stop all activity notification emails." # email_any_notes_description: "Disable to stop all activity notification emails."
# email_news: "News" email_news: "Вести"
# email_recruit_notes: "Job Opportunities" email_recruit_notes: "Пословне могућности"
# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job." # email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
contributor_emails: "Мејлови реда сарадника" contributor_emails: "Мејлови реда сарадника"
contribute_prefix: "Тражимо људе који би нам се придружили! Погледај " contribute_prefix: "Тражимо људе који би нам се придружили! Погледај "
@ -936,7 +936,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian
error_saving: "Чување грешке..." error_saving: "Чување грешке..."
saved: "Измене су сачуване" saved: "Измене су сачуване"
password_mismatch: "Шифре се не слажу." password_mismatch: "Шифре се не слажу."
# password_repeat: "Please repeat your password." password_repeat: "Понови своју шифру."
# keyboard_shortcuts: # keyboard_shortcuts:
# keyboard_shortcuts: "Keyboard Shortcuts" # keyboard_shortcuts: "Keyboard Shortcuts"

View file

@ -136,14 +136,14 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
okay: "OK" okay: "OK"
not_found: not_found:
page_not_found: "không tìm thấy trang" page_not_found: "Không tìm thấy trang"
diplomat_suggestion: diplomat_suggestion:
title: "Hãy giúp chúng tôi phiên dịch CodeCombat!" # This shows up when a player switches to a non-English language using the language selector. title: "Hãy giúp chúng tôi phiên dịch CodeCombat!" # This shows up when a player switches to a non-English language using the language selector.
sub_heading: "Chúng tôi cần kỹ năng ngoại ngữ của bạn." sub_heading: "Chúng tôi cần kỹ năng ngoại ngữ của bạn."
pitch_body: "Chúng tôi xây dựng Codecombat bằng Tiếng Anh, tuy nhiên có rất nhiều bạn trẻ trên toàn thế giới đều muốn tham gia. Các bạn trẻ Việt Nam cũng muốn chơi với nội dung Tiếng Việt, nếu như bạn có thể đọc và viết thành thạo cả 2 ngôn ngữ xin hãy đăng kí làm dịch thuật cho chúng tôi." pitch_body: "Chúng tôi xây dựng Codecombat bằng Tiếng Anh, tuy nhiên có rất nhiều bạn trẻ trên toàn thế giới đều muốn tham gia. Các bạn trẻ Việt Nam cũng muốn chơi với nội dung Tiếng Việt, nếu như bạn có thể đọc và viết thành thạo cả 2 ngôn ngữ xin hãy đăng kí làm dịch thuật cho chúng tôi."
missing_translations: "Bạn sẽ tiếp tục thấy Tiếng Anh cho đến khi chúng tôi dịch tất cả nội dung qua Tiếng Việt." missing_translations: "Bạn sẽ tiếp tục thấy Tiếng Anh cho đến khi chúng tôi dịch tất cả nội dung qua Tiếng Việt."
learn_more: "Tìm hiểu thêm để tham gia làm Phiên Dịch Viên" learn_more: "Tìm hiểu thêm để tham gia trở thành Phiên Dịch Viên"
subscribe_as_diplomat: "Trở thành Phiên Dịch Viên" subscribe_as_diplomat: "Trở thành Phiên Dịch Viên"
play: play:
@ -152,22 +152,22 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
spectate: "Quan sát" # Ladder page spectate: "Quan sát" # Ladder page
players: "người chơi" # Hover over a level on /play players: "người chơi" # Hover over a level on /play
hours_played: "Thời gian chơi" # Hover over a level on /play hours_played: "Thời gian chơi" # Hover over a level on /play
items: "Trang b" # Tooltip on item shop button from /play items: "Trang B" # Tooltip on item shop button from /play
unlock: "Mua" # For purchasing items and heroes unlock: "Mua" # For purchasing items and heroes
confirm: "Xác nhận" confirm: "Xác nhận"
owned: "Đã có" # For items you own owned: "Đã có" # For items you own
locked: "Bị khóa" locked: "Bị khóa"
purchasable: "Có thể mua" # For a hero you unlocked but haven't purchased purchasable: "Có thể mua" # For a hero you unlocked but haven't purchased
available: "Khả dụng" available: "Khả dụng"
skills_granted: "Đã nhận được Kĩ Năng" # Property documentation details skills_granted: "Kỹ năng nhận được" # Property documentation details
heroes: "Các Tướng" # Tooltip on hero shop button from /play heroes: "Tướng" # Tooltip on hero shop button from /play
achievements: "Thành tích" # Tooltip on achievement list button from /play achievements: "Thành Tích" # Tooltip on achievement list button from /play
account: "Tài khoản" # Tooltip on account button from /play account: "Tài khoản" # Tooltip on account button from /play
settings: "Tùy Chỉnh" # Tooltip on settings button from /play settings: "Tùy Chỉnh" # Tooltip on settings button from /play
poll: "Bỏ phiếu" # Tooltip on poll button from /play poll: "Bỏ phiếu" # Tooltip on poll button from /play
next: "Tiếp" # Go from choose hero to choose inventory before playing a level next: "Tiếp" # Go from choose hero to choose inventory before playing a level
change_hero: "Đổi Tướng" # Go back from choose inventory to choose hero change_hero: "Đổi Tướng" # Go back from choose inventory to choose hero
buy_gems: "Mua ngọc" buy_gems: "Mua Ngọc"
subscription_required: "Cần đăng kí" subscription_required: "Cần đăng kí"
anonymous: "Người chơi ẩn danh" anonymous: "Người chơi ẩn danh"
level_difficulty: "Độ khó: " level_difficulty: "Độ khó: "
@ -251,7 +251,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
finishing: "Sắp hoàn tất" finishing: "Sắp hoàn tất"
sign_in_with_facebook: "Đăng nhập với Facebook" sign_in_with_facebook: "Đăng nhập với Facebook"
sign_in_with_gplus: "Đăng nhập với G+" sign_in_with_gplus: "Đăng nhập với G+"
signup_switch: "Bạn muốn tạo tài khoản mới?" signup_switch: "Bạn muốn tạo tài khoản mới?"
signup: signup:
email_announcements: "Nhận thông báo bằng email" email_announcements: "Nhận thông báo bằng email"
@ -289,9 +289,9 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
common: common:
back: "Trở lại" # When used as an action verb, like "Navigate backward" back: "Trở lại" # When used as an action verb, like "Navigate backward"
continue: "Tiếp tục" # When used as an action verb, like "Continue forward" continue: "Tiếp tục" # When used as an action verb, like "Continue forward"
loading: "Đang tải..." loading: "Đang Tải..."
saving: "Đang lưu..." saving: "Đang Lưu..."
sending: "Đang gửi..." sending: "Đang Gửi..."
send: "Gửi" send: "Gửi"
cancel: "Hủy" cancel: "Hủy"
save: "Lưu" save: "Lưu"
@ -345,7 +345,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
message: "Tin nhắn" message: "Tin nhắn"
code: "Code" code: "Code"
ladder: "Thang điểm" ladder: "Thang điểm"
when: "Khi nào" when: "Thời gian"
opponent: "Đối thủ" opponent: "Đối thủ"
rank: "Hạng" rank: "Hạng"
score: "Điểm" score: "Điểm"
@ -358,7 +358,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
player: "Người chơi" player: "Người chơi"
player_level: "Cấp độ" # Like player level 5, not like level: Dungeons of Kithgard player_level: "Cấp độ" # Like player level 5, not like level: Dungeons of Kithgard
warrior: "Chiến binh" warrior: "Chiến binh"
ranger: "Cung thủ" ranger: "Xạ thủ"
wizard: "Phù thủy" wizard: "Phù thủy"
first_name: "Tên" first_name: "Tên"
last_name: "Họ" last_name: "Họ"
@ -420,8 +420,8 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
victory_review_placeholder: "Màn chơi vừa rồi như thế nào?" victory_review_placeholder: "Màn chơi vừa rồi như thế nào?"
victory_hour_of_code_done: "Bạn xong chưa?" victory_hour_of_code_done: "Bạn xong chưa?"
victory_hour_of_code_done_yes: "Đúng vậy, tôi đã hoàn tất thời gian lập trình!" victory_hour_of_code_done_yes: "Đúng vậy, tôi đã hoàn tất thời gian lập trình!"
victory_experience_gained: "Đã tăng XP" victory_experience_gained: "XP nhận được"
victory_gems_gained: "Nhận được Ngọc" victory_gems_gained: "Ngọc nhận được"
victory_new_item: "Vật phẩm mới" victory_new_item: "Vật phẩm mới"
victory_viking_code_school: "Thật tuyệt vời, bạn vừa vượt qua một màn chơi khó khủng khiếp! Không lâu nữa bạn sẽ trở thành một lập trình viên thôi. Bạn vừa được nhận thẳng vào trường Viking Code School, nơi bạn có thể nâng tầm kĩ năng của mình và trở thành lập trình viên web chuyên nghiệp trong 14 tuần." victory_viking_code_school: "Thật tuyệt vời, bạn vừa vượt qua một màn chơi khó khủng khiếp! Không lâu nữa bạn sẽ trở thành một lập trình viên thôi. Bạn vừa được nhận thẳng vào trường Viking Code School, nơi bạn có thể nâng tầm kĩ năng của mình và trở thành lập trình viên web chuyên nghiệp trong 14 tuần."
victory_become_a_viking: "Trở thành Viking" victory_become_a_viking: "Trở thành Viking"
@ -442,7 +442,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
skip_tutorial: "Bỏ qua (esc)" skip_tutorial: "Bỏ qua (esc)"
keyboard_shortcuts: "Các phím tắt" keyboard_shortcuts: "Các phím tắt"
loading_ready: "Sẵn sàng!" loading_ready: "Sẵn sàng!"
loading_start: "Bắt đầu màn này" loading_start: "Bắt đầu màn chơi"
problem_alert_title: "Hãy sửa lại Code của bạn" problem_alert_title: "Hãy sửa lại Code của bạn"
time_current: "Bây giờ:" time_current: "Bây giờ:"
time_total: "Tối đa:" time_total: "Tối đa:"
@ -542,12 +542,12 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
view_other_solutions: "Xem xếp hạng" # {change} view_other_solutions: "Xem xếp hạng" # {change}
scores: "Điểm" scores: "Điểm"
top_players: "Người chơi dẫn đầu xếp theo" top_players: "Người chơi dẫn đầu xếp theo"
day: "Hôm nay" day: "Hôm Nay"
week: "Tuần này" week: "Tuần Này"
all: "Tất c" all: "Tất C"
time: "Thời gian" time: "Thời Gian"
damage_taken: "Sát thương nhận vào" damage_taken: "Sát thương nhận vào"
damage_dealt: "Sát thương gây ra" damage_dealt: "Mức Sát Thương"
difficulty: "Độ khó" difficulty: "Độ khó"
gold_collected: "Vàng đã thu thập" gold_collected: "Vàng đã thu thập"
@ -560,7 +560,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
equipped: "(đã trang bị)" equipped: "(đã trang bị)"
locked: "(khóa)" locked: "(khóa)"
restricted: "(bị giới hạn ở màn này)" restricted: "(bị giới hạn ở màn này)"
equip: "Mặc trang bị" equip: "Mặc"
unequip: "Cởi ra" unequip: "Cởi ra"
buy_gems: buy_gems:
@ -577,7 +577,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
price: "x{{gems}} / tháng" price: "x{{gems}} / tháng"
subscribe: subscribe:
comparison_blurb: "Tăng cường kĩ năng bằng việc đăng kí theo dõi CodeCombat!" comparison_blurb: "Tăng cường kĩ năng bằng cách mua gói dịch vụ nâng cao của CodeCombat!"
feature1: "__levelsCount__+ màn chơi cơ bản trên __worldsCount__ bản đồ thế giới" feature1: "__levelsCount__+ màn chơi cơ bản trên __worldsCount__ bản đồ thế giới"
feature2: "__heroesCount__ <strong>tướng mới</strong> mạnh mẽ với những kĩ năng đặc biệt!" feature2: "__heroesCount__ <strong>tướng mới</strong> mạnh mẽ với những kĩ năng đặc biệt!"
feature3: "__bonusLevelsCount__+ màn chơi thêm" feature3: "__bonusLevelsCount__+ màn chơi thêm"
@ -589,7 +589,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
free: "Miễn phí" free: "Miễn phí"
month: "tháng" month: "tháng"
must_be_logged: "Trước tiên bạn phải đăng nhập. Hãy tạo một tài khoản mới hoặc đăng nhập ở menu phía trên." must_be_logged: "Trước tiên bạn phải đăng nhập. Hãy tạo một tài khoản mới hoặc đăng nhập ở menu phía trên."
subscribe_title: "Đăng kí theo dõi" subscribe_title: "Mua gói nâng cao"
unsubscribe: "Ngừng theo dõi" unsubscribe: "Ngừng theo dõi"
confirm_unsubscribe: "Xác nhận ngừng theo dõi" confirm_unsubscribe: "Xác nhận ngừng theo dõi"
never_mind: "Đừng bận tâm, tôi vẫn yêu bạn" never_mind: "Đừng bận tâm, tôi vẫn yêu bạn"
@ -598,8 +598,8 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
thank_you: "Cảm ơn bạn đã ủng hộ CodeCombat." thank_you: "Cảm ơn bạn đã ủng hộ CodeCombat."
sorry_to_see_you_go: "Thật đáng tiếc khi phải chia tay bạn! Hãy góp ý để chúng tôi có thể cái thiện tốt hơn." sorry_to_see_you_go: "Thật đáng tiếc khi phải chia tay bạn! Hãy góp ý để chúng tôi có thể cái thiện tốt hơn."
unsubscribe_feedback_placeholder: "Ồ, chúng tôi đã làm gì sai ư?" unsubscribe_feedback_placeholder: "Ồ, chúng tôi đã làm gì sai ư?"
parent_button: "Hãy hỏi phụ huynh bạn" parent_button: "Hỏi phụ huynh bạn"
parent_email_description: "Chúng tôi sẽ email cho họ để họ có thể mua cho bạn một gói dịch vụ của CodeCombat." parent_email_description: "Chúng tôi sẽ email cho họ để họ có thể mua cho bạn một gói dịch vụ nâng cao của CodeCombat."
parent_email_input_invalid: "Địa chỉ email không hợp lệ." parent_email_input_invalid: "Địa chỉ email không hợp lệ."
parent_email_input_label: "Địa chỉ email của phụ huynh" parent_email_input_label: "Địa chỉ email của phụ huynh"
parent_email_input_placeholder: "Hãy nhập địa chi email của phụ huynh bạn" parent_email_input_placeholder: "Hãy nhập địa chi email của phụ huynh bạn"
@ -608,13 +608,13 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
parent_email_title: "Địa chỉ email của phụ huynh bạn là gì?" parent_email_title: "Địa chỉ email của phụ huynh bạn là gì?"
parents: "Dành cho Phụ huynh" parents: "Dành cho Phụ huynh"
parents_title: "Xin chào: Con của bạn muốn học lập trình. Bạn đồng ý chứ?" # {change} parents_title: "Xin chào: Con của bạn muốn học lập trình. Bạn đồng ý chứ?" # {change}
parents_blurb1: "Con của bạn đã hoàn thiện __nLevels__ cấp độ lập trình đầu tiên với CodeCombat. Hãy giúp con bạn theo đuổi giấc mơ lập trình bằng cách đăng kí thêm khóa học." parents_blurb1: "Con của bạn đã hoàn thiện __nLevels__ cấp độ lập trình cơ bản với CodeCombat. Hãy giúp con bạn theo đuổi giấc mơ lập trình bằng cách đăng kí thêm khóa học."
parents_blurb1a: "Lập trình là một kỹ năng cần thiết mà con bạn chắc chắn sẽ cần khi trưởng thành. Tới 2020, kỹ năng phần mếm cơ bản sẽ được dùng trong 77% các ngành nghề, và khắp nơi trên thế giới hiện nay đều đang có nhu cầu cao tìm kiếm những kĩ sư phần mềm. Bạn có biết Công Nghệ Thông Tin đang là bằng cấp đại học đem lại mức lương cao nhất?" parents_blurb1a: "Lập trình là một kỹ năng cần thiết mà con bạn chắc chắn sẽ cần khi trưởng thành. Tính đến 2020, kỹ năng phần mếm cơ bản sẽ được dùng trong 77% các ngành nghề, và khắp mọi nơi trên thế giới hiện nay đều đang có nhu cầu cao tìm kiếm kĩ sư phần mềm. Bạn có biết Công Nghệ Thông Tin đang là bằng cấp đại học đem lại mức lương cao nhất?"
parents_blurb2: "Chỉ với ${{price}} USD/tháng, con bạn sẽ nhận được những thử thách mới mỗi tháng và sẽ nhận được sự hỗ trợ đặc biệt từ các lập trình viên chuyên nghiệp." # {change} parents_blurb2: "Chỉ với ${{price}} USD/tháng, con bạn sẽ nhận được những thử thách mới mỗi tháng và sẽ nhận được sự hỗ trợ đặc biệt từ các lập trình viên chuyên nghiệp." # {change}
parents_blurb3: "Không hề có rủi ro: Nếu bạn không hài lòng bạn có thể nhận lại 100% số tiền mình bỏ ra chỉ với 1 cú nhấp chuốt." parents_blurb3: "Không hề có rủi ro: Nếu bạn không hài lòng bạn có thể nhận lại 100% số tiền mình bỏ ra chỉ với 1 cú click chuốt."
payment_methods: "Phương thức thanh toán" payment_methods: "Phương thức thanh toán"
payment_methods_title: "Những phương thức thanh toán được chấp nhận." payment_methods_title: "Những phương thức thanh toán được chấp nhận."
payment_methods_blurb1: "Hiện tại chúng tôi chấp nhận thẻ tín dụng và Alipay. Bạn cũng có thể sử dụng PayPal để chuyển {{three_month_price}} USD tới nick@codecombat.com và ghi rõ email tài khoản để mua dịch vụ trong 3 tháng, hoặc ${{year_price}} để mua dịch vụ trong 1 năm." payment_methods_blurb1: "Hiện tại chúng tôi chấp nhận thẻ tín dụng và Alipay. Bạn cũng có thể sử dụng PayPal để chuyển ${{three_month_price}} tới nick@codecombat.com và ghi rõ email tài khoản để mua dịch vụ trong 3 tháng, hoặc ${{year_price}} để mua dịch vụ trong 1 năm."
payment_methods_blurb2: "Nếu bạn cần một phương thức thanh toán khác, hãy liên hệ" payment_methods_blurb2: "Nếu bạn cần một phương thức thanh toán khác, hãy liên hệ"
sale_button: "Ưu đãi!" sale_button: "Ưu đãi!"
sale_button_title: "Tiết kiệm $21 khi mua gói dịch vụ 1 năm" sale_button_title: "Tiết kiệm $21 khi mua gói dịch vụ 1 năm"
@ -644,7 +644,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
experimental: "Thử" experimental: "Thử"
python_blurb: "Đơn giản nhưng mạnh mẽ, tốt cho cả những người mới bắt đầu và chuyên gia." python_blurb: "Đơn giản nhưng mạnh mẽ, tốt cho cả những người mới bắt đầu và chuyên gia."
javascript_blurb: "Ngôn ngữ của thế giới web. (Không giống với Java đâu nhé.)" javascript_blurb: "Ngôn ngữ của thế giới web. (Không giống với Java đâu nhé.)"
coffeescript_blurb: "JavaScript viết bằng cú pháp tốt hơn." coffeescript_blurb: "JavaScript vi cú pháp tốt hơn."
clojure_blurb: "Lisp thời đại mới." clojure_blurb: "Lisp thời đại mới."
lua_blurb: "Ngôn ngữ được ưa chuông để làm game." lua_blurb: "Ngôn ngữ được ưa chuông để làm game."
io_blurb: "Đơn giản nhưng ít người biết đến." io_blurb: "Đơn giản nhưng ít người biết đến."
@ -659,7 +659,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
health: "Sinh lực" health: "Sinh lực"
speed: "Tốc độ" speed: "Tốc độ"
regeneration: "Hồi sinh lực" regeneration: "Hồi sinh lực"
range: "Tầm đánh/Tầm nhìn" # As in "attack or visual range" range: "Tầm xa" # As in "attack or visual range"
blocks: "Đỡ" # As in "this shield blocks this much damage" blocks: "Đỡ" # As in "this shield blocks this much damage"
# backstab: "Backstab" # As in "this dagger does this much backstab damage" # backstab: "Backstab" # As in "this dagger does this much backstab damage"
skills: "Kĩ năng" skills: "Kĩ năng"
@ -707,14 +707,14 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
granularity_change_history: "Lịch sử" granularity_change_history: "Lịch sử"
options: options:
general_options: "Tùy chỉnh chung" # Check out the Options tab in the Game Menu while playing a level general_options: "Tùy Chỉnh Chung" # Check out the Options tab in the Game Menu while playing a level
volume_label: "Âm lượng" volume_label: "Âm Lượng"
music_label: "Âm nhạc" music_label: "Âm Nhạc"
music_description: "Bật/tắt nhạc nền." music_description: "Bật/tắt nhạc nền."
editor_config_title: "Cấu hình Editor" editor_config_title: "Cấu Hình Editor"
# editor_config_keybindings_label: "Key Bindings" editor_config_keybindings_label: "Các phím tắt"
editor_config_keybindings_default: "Mặc định (Ace)" editor_config_keybindings_default: "Mặc định (Ace)"
editor_config_keybindings_description: "Thêm shortcuts chung cho các công cụ editor." editor_config_keybindings_description: "Hệ thống phím tắt chung cho các công cụ editor."
editor_config_livecompletion_label: "Gợi ý tự động" editor_config_livecompletion_label: "Gợi ý tự động"
editor_config_livecompletion_description: "Hiển thị gợi ý tự động trong khi gõ phím." editor_config_livecompletion_description: "Hiển thị gợi ý tự động trong khi gõ phím."
editor_config_invisibles_label: "Hiện kí tự ẩn" editor_config_invisibles_label: "Hiện kí tự ẩn"
@ -899,14 +899,14 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
not_logged_in: "Đăng nhập hoặc tạo tài khoản để thay đổi cài đặt." not_logged_in: "Đăng nhập hoặc tạo tài khoản để thay đổi cài đặt."
autosave: "Tự động lưu thay đổi" autosave: "Tự động lưu thay đổi"
me_tab: "Tôi" me_tab: "Tôi"
picture_tab: "Ảnh đại diện" picture_tab: "Ảnh Đại Diện"
delete_account_tab: "Xóa tài khoản" delete_account_tab: "Xóa Tài Khoản"
wrong_email: "Email không đúng" wrong_email: "Email không đúng"
wrong_password: "Mật khẩu không đúng" wrong_password: "Mật khẩu không đúng"
upload_picture: "Tải ảnh lên" upload_picture: "Tải ảnh lên"
delete_this_account: "Xóa tài khoản này vĩnh viễn" delete_this_account: "Xóa tài khoản này vĩnh viễn"
# reset_progress_tab: "Reset All Progress" reset_progress_tab: "Xóa Mọi Tiến Trình"
# reset_your_progress: "Clear all your progress and start over" reset_your_progress: "Xóa mọi tiến trình của bạn và bắt đầu lại từ đầu"
# god_mode: "God Mode" # god_mode: "God Mode"
password_tab: "Mật khẩu" password_tab: "Mật khẩu"
emails_tab: "Email" emails_tab: "Email"
@ -914,9 +914,9 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
manage_subscription: "Nhấn vào đây để chỉnh sửa gói đăng ký." manage_subscription: "Nhấn vào đây để chỉnh sửa gói đăng ký."
new_password: "Mật khẩu mới" new_password: "Mật khẩu mới"
new_password_verify: "Xác nhận" new_password_verify: "Xác nhận"
# type_in_email: "Type in your email to confirm account deletion." type_in_email: "Nhập email của bạn để xác nhận xóa tài khoản."
# type_in_email_progress: "Type in your email to confirm deleting your progress." type_in_email_progress: "Nhập email của bạn để xác nhận xóa tiến trình."
# type_in_password: "Also, type in your password." type_in_password: "Nhập lại mật khẩu của bạn."
email_subscriptions: "Thuê bao Email" email_subscriptions: "Thuê bao Email"
# email_subscriptions_none: "No Email Subscriptions." # email_subscriptions_none: "No Email Subscriptions."
email_announcements: "Thông báo" email_announcements: "Thông báo"
@ -928,8 +928,8 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# email_news: "News" # email_news: "News"
email_recruit_notes: "Cơ hội việc làm" email_recruit_notes: "Cơ hội việc làm"
email_recruit_notes_description: "Nếu bạn chơi trò này rất giỏi, chúng tôi có thể sẽ liên lạc với bạn về cơ hội nghề nghiệp." email_recruit_notes_description: "Nếu bạn chơi trò này rất giỏi, chúng tôi có thể sẽ liên lạc với bạn về cơ hội nghề nghiệp."
# contributor_emails: "Contributor Class Emails" contributor_emails: "Email tham gia đóng góp"
contribute_prefix: "Chúng tôi đang tìm thêm người vào nhóm của chúng tôi! Hãy kiểm " contribute_prefix: "Chúng tôi đang tìm thêm người để cùng tham gia với chúng tôi! Hãy vào "
contribute_page: "trang đóng góp" contribute_page: "trang đóng góp"
contribute_suffix: " để tìm hiểu thêm." contribute_suffix: " để tìm hiểu thêm."
# email_toggle: "Toggle All" # email_toggle: "Toggle All"
@ -980,78 +980,78 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
clans: clans:
clan: "Clan" clan: "Clan"
# clans: "Clans" clans: "Các Clan"
new_name: "Tên mới cho bè đảng" new_name: "Tên Clan mới"
new_description: "Miêu tả cho bè đảng mới này" new_description: "Mô tả Clan mới"
make_private: "Tạo bè đảng kín" make_private: "Make clan private"
# subs_only: "subscribers only" subs_only: "chỉ dành cho các subscriber"
create_clan: "Tạo một clan mới" create_clan: "Tạo một clan mới"
# private_preview: "Preview" private_preview: "Xem trước"
# private_clans: "Private Clans" private_clans: "Các Clan kín"
# public_clans: "Public Clans" public_clans: "Các Clan mở"
# my_clans: "My Clans" my_clans: "Các Clan của tôi"
# clan_name: "Clan Name" clan_name: "Tên Clan"
name: "Tên" name: "Tên"
# chieftain: "Chieftain" chieftain: "Thủ lĩnh"
# type: "Type" type: "Loại"
# edit_clan_name: "Edit Clan Name" edit_clan_name: "Sửa tên Clan"
# edit_clan_description: "Edit Clan Description" edit_clan_description: "Sửa mô tả Clan"
# edit_name: "edit name" edit_name: "sửa tên"
# edit_description: "edit description" edit_description: "sửa mô tả"
# private: "(private)" private: "(kín)"
# summary: "Summary" summary: "Tóm tắt"
# average_level: "Average Level" average_level: "Cấp độ trng bình"
# average_achievements: "Average Achievements" # average_achievements: "Average Achievements"
# delete_clan: "Delete Clan" delete_clan: "Xóa Clan"
# leave_clan: "Leave Clan" leave_clan: "Rời Clan"
# join_clan: "Join Clan" join_clan: "Tham gia Clan"
# invite_1: "Invite:" invite_1: "Mời:"
# invite_2: "*Invite players to this Clan by sending them this link." invite_2: "*Mời người chơi khác tham dự Clan này bằng cách gửi họ đường link này."
members: "Những thành viên" members: "Thành viên"
# progress: "Progress" progress: "Tiến trình"
# not_started_1: "not started" not_started_1: "chưa băt đầu"
# started_1: "started" started_1: "đã bắt đầu"
# complete_1: "complete" complete_1: "hoàn thành"
# exp_levels: "Expand levels" # exp_levels: "Expand levels"
# rem_hero: "Remove Hero" # rem_hero: "Remove Hero"
# status: "Status" # status: "Status"
# complete_2: "Complete" complete_2: "Hoàn thành"
# started_2: "Started" started_2: "Đã bắt đầu"
# not_started_2: "Not Started" not_started_2: "Chưa bắt đầu"
# view_solution: "Click to view solution." view_solution: "Click để xem lời giải."
# view_attempt: "Click to view attempt." # view_attempt: "Click to view attempt."
# latest_achievement: "Latest Achievement" # latest_achievement: "Latest Achievement"
# playtime: "Playtime" playtime: "Thời gian chơi"
# last_played: "Last played" last_played: "Lần chơi cuối"
# leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances." # leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances."
# track_concepts1: "Track concepts" # track_concepts1: "Track concepts"
# track_concepts2a: "learned by each student" # track_concepts2a: "learned by each student"
# track_concepts2b: "learned by each member" # track_concepts2b: "learned by each member"
# track_concepts3a: "Track levels completed for each student" # track_concepts3a: "Track levels completed for each student"
# track_concepts3b: "Track levels completed for each member" # track_concepts3b: "Track levels completed for each member"
# track_concepts4a: "See your students'" track_concepts4a: "Xem các học viên của bạn'"
# track_concepts4b: "See your members'" track_concepts4b: "Xem các thành viên của bạn'"
# track_concepts5: "solutions" track_concepts5: "lời giải"
# track_concepts6a: "Sort students by name or progress" track_concepts6a: "Sắp xếp học viên theo tên hoặc tiến trình"
# track_concepts6b: "Sort members by name or progress" track_concepts6b: "Sắp xếp thành viên theo tên hoặc tiến trình"
# track_concepts7: "Requires invitation" # track_concepts7: "Requires invitation"
# track_concepts8: "to join" # track_concepts8: "to join"
# private_require_sub: "Private clans require a subscription to create or join." private_require_sub: "Các Clan kín cần mua subscription để tạo hoặc tham gia."
courses: courses:
course: "Khóa học" course: "Khóa học"
courses: "Những khóa học" courses: "Những khóa học"
create_new_class: "Tạo khóa học mới" create_new_class: "Tạo Lớp Học Mới"
not_enrolled: "Bạn không đăng kí vào khóa học này" not_enrolled: "Bạn không đăng kí vào khóa học này"
# visit_pref: "Please visit the" visit_pref: "Hãy ghé thăm trang"
# visit_suf: "page to enroll." visit_suf: "để tham gia."
# select_class: "Select one of your classes" select_class: "Chọn một trong các lớp học của bạn"
# unnamed: "*unnamed*" unnamed: "*unnamed*"
# select: "Select" # select: "Select"
# unnamed_class: "Unnamed Class" unnamed_class: "Lớp học chưa đặt tên"
# edit_settings: "edit class settings" edit_settings: "thay đổi tùy chỉnh lớp học"
# edit_settings1: "Edit Class Settings" edit_settings1: "Thay đổi tùy chỉnh lớp học"
progress: "Tiến trình của khóa học" progress: "Tiến trình của lớp học"
add_students: "Thêm học sinh" add_students: "Thêm học sinh"
stats: "Thống kê" stats: "Thống kê"
total_students: "Tổng số học sinh:" total_students: "Tổng số học sinh:"
@ -1073,9 +1073,9 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# invite_link_p_2: "Or have us email them directly:" # invite_link_p_2: "Or have us email them directly:"
# capacity_used: "Course slots used:" # capacity_used: "Course slots used:"
# enter_emails: "Enter student emails to invite, one per line" # enter_emails: "Enter student emails to invite, one per line"
# send_invites: "Send Invites" send_invites: "Gửi lời mời"
# creating_class: "Creating class..." creating_class: "Đang tạo lớp..."
# purchasing_course: "Purchasing course..." purchasing_course: "Đang mua khóa học..."
buy_course: "Mua khóa học" buy_course: "Mua khóa học"
buy_course1: "Mua khóa học này" buy_course1: "Mua khóa học này"
select_all_courses: "Lựa chọn 'Tất cả khóa học' để nhận giảm giá 50%!" select_all_courses: "Lựa chọn 'Tất cả khóa học' để nhận giảm giá 50%!"
@ -1083,7 +1083,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
number_programming_students: "Số Lượng Học Viên" number_programming_students: "Số Lượng Học Viên"
number_total_students: "Tổng Số Học Viên tại Trường/Quận" number_total_students: "Tổng Số Học Viên tại Trường/Quận"
enter_number_students: "Hãy nhập vào số lượng học sinh bạn cần cho lớp học này." enter_number_students: "Hãy nhập vào số lượng học sinh bạn cần cho lớp học này."
# name_class: "Name your class" name_class: "Đặt tên lớp của bạn"
# displayed_course_page: "This will be displayed on the course page for you and your students. It can be changed later." # displayed_course_page: "This will be displayed on the course page for you and your students. It can be changed later."
buy: "Mua" buy: "Mua"
# purchasing_for: "You are purchasing a license for" # purchasing_for: "You are purchasing a license for"
@ -1273,25 +1273,25 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# last_updated: "Last updated:" # last_updated: "Last updated:"
# grants_lifetime_access: "Grants access to all Courses." # grants_lifetime_access: "Grants access to all Courses."
# enrollment_credits_available: "Enrollment Credits Available:" # enrollment_credits_available: "Enrollment Credits Available:"
description: "Miêu tả" # ClassroomSettingsModal description: "Mô tả" # ClassroomSettingsModal
language_select: "Lựa chọn một ngôn ngữ" language_select: "Lựa chọn một ngôn ngữ"
language_cannot_change: "Không thể đổi ngôn ngữ sau khi học viên đã gia nhập lớp học." language_cannot_change: "Không thể đổi ngôn ngữ sau khi học viên đã gia nhập lớp học."
learn_p: "Học Python" learn_p: "Học Python"
learn_j: "Học JavaScript" learn_j: "Học JavaScript"
# avg_student_exp_label: "Average Student Programming Experience" avg_student_exp_label: "Kinh nghiệm lập trình trung bình của học viên"
# avg_student_exp_desc: "This will help us understand how to pace courses better." avg_student_exp_desc: "Điều này sẽ giúp chúng tôi điều chỉnh tốc độ các khóa học tốt hơn."
# avg_student_exp_select: "Select the best option" avg_student_exp_select: "Chọn lựa chọn đúng nhất"
# avg_student_exp_none: "No Experience - little to no experience" avg_student_exp_none: "Không Kinh Nghiệm - ít hoặc không có kinh nghiệm"
# avg_student_exp_beginner: "Beginner - some exposure or block-based" avg_student_exp_beginner: "Mới Bắt Đầu - đã tiếp cận hoặc đã được giới thiệu"
# avg_student_exp_intermediate: "Intermediate - some experience with typed code" avg_student_exp_intermediate: "Khá - có chút kinh nghiệm viết code"
# avg_student_exp_advanced: "Advanced - extensive experience with typed code" avg_student_exp_advanced: "Nâng Cao - có nhiều kình nghiệm viết code"
# avg_student_exp_varied: "Varied Levels of Experience" avg_student_exp_varied: "Nhiều Trình Độ"
# student_age_range_label: "Student Age Range" student_age_range_label: "Lứa tuổi học viên"
# student_age_range_younger: "Younger than 6" student_age_range_younger: "Nhỏ hơn 6 tuổi"
# student_age_range_older: "Older than 18" student_age_range_older: "Lớn hơn 18 tuổi"
# student_age_range_to: "to" student_age_range_to: "tới"
# create_class: "Create Class" create_class: "Tạo Lớp"
# class_name: "Class Name" class_name: "Tên lớp"
# teacher_account_restricted: "Your account is a teacher account, and so cannot access student content." # teacher_account_restricted: "Your account is a teacher account, and so cannot access student content."
teacher: teacher:
@ -1318,7 +1318,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# unarchive_class: "unarchive class" # unarchive_class: "unarchive class"
# no_students_yet: "This class has no students yet." # no_students_yet: "This class has no students yet."
add_students: "Thêm Học Viên" add_students: "Thêm Học Viên"
# create_new_class: "Create a New Class" create_new_class: "Tạo Lớp học mới"
# class_overview: "Class Overview" # View Class page # class_overview: "Class Overview" # View Class page
# avg_playtime: "Average level playtime" # avg_playtime: "Average level playtime"
# total_playtime: "Total play time" # total_playtime: "Total play time"
@ -1530,13 +1530,13 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
adventurer_forum_url: "diễn đàn" adventurer_forum_url: "diễn đàn"
adventurer_join_suf: "vì vậy nếu bạn muốn liên lạc thông qua những kênh trên, hãy đăng ký ở đó!" adventurer_join_suf: "vì vậy nếu bạn muốn liên lạc thông qua những kênh trên, hãy đăng ký ở đó!"
adventurer_subscribe_desc: "Nhận email khi có màn chơi mới cần kiểm thử." adventurer_subscribe_desc: "Nhận email khi có màn chơi mới cần kiểm thử."
# scribe_introduction_pref: "CodeCombat isn't just going to be a bunch of levels. It will also include a resource for knowledge, a wiki of programming concepts that levels can hook into. That way rather than each Artisan having to describe in detail what a comparison operator is, they can simply link their level to the Article describing them that is already written for the player's edification. Something along the lines of what the " scribe_introduction_pref: "CodeCombat không chỉ là một đống các màn chơi. Nó cũng sẽ là một nguồn tài nguyên tri thức, một cuốn thư viện về các khái niệm lập trình, chính những thứ mà được sử dụng để xây dựng các màn chơi. Như vậy các Thợ Thủ Công không phải bỏ công giải thích đi giải thích lại những khái niệm cơ bản như toán tử là gì, họ có thể một cách đơn giản dẫn link từ các màn chơi của họ sang các bài viết dành cho người chơi. Việc này tương tự như những thứ mà "
# scribe_introduction_url_mozilla: "Mozilla Developer Network" scribe_introduction_url_mozilla: "Cộng Đồng Lập Trình Mozilla"
# scribe_introduction_suf: " has built. If your idea of fun is articulating the concepts of programming in Markdown form, then this class might be for you." scribe_introduction_suf: " đã xây dựng. Nếu bạn có thể truyền tải được các tư tưởng lập trình dưới dạng Markdown, thì lớp nhân vật này có thể phù hợp với bạn."
# scribe_attribute_1: "Skill in words is pretty much all you need. Not only grammar and spelling, but able to convey complicated ideas to others." scribe_attribute_1: "Khả năng hành văn gần như là thứ duy nhất bạn cần. Không chỉ cần ngữ pháp và đánh vần thành thạo, mà bạn còn cần khả năng truyền tải những thông điệp phức tạp tới người khác."
contact_us_url: "Liên hệ với chúng tôi" contact_us_url: "Liên hệ với chúng tôi"
# scribe_join_description: "tell us a little about yourself, your experience with programming and what sort of things you'd like to write about. We'll go from there!" scribe_join_description: "kể cho chung tôi một chút về bạn, kinh nghiệm lập trình của bạn và bạn hứng thú viết về điều gì. Chúng ta sẽ cùng bắt đầu từ đó!"
# scribe_subscribe_desc: "Get emails about article writing announcements." scribe_subscribe_desc: "Nhận email về những thông tin viết bài."
diplomat_introduction_pref: "Nếu như bạn hỏi chúng tôi đã nhận được gì kể từ khi " diplomat_introduction_pref: "Nếu như bạn hỏi chúng tôi đã nhận được gì kể từ khi "
diplomat_launch_url: "bắt đầu vào tháng Mười" diplomat_launch_url: "bắt đầu vào tháng Mười"
diplomat_introduction_suf: "thì đó chính là niềm quan tâm rất lớn với CodeCombat đến từ nhiều quốc gia trên thế giới! Chúng tôi đang xây dựng một đội ngũ phiên dịch viên đầy nhiệt huyết để đưa CodeCombat đến với mọi nơi trên thế giới. Nếu bạn muốn cập nhật những nội dung mới nhất đồng thời muốn truyền tải chúng tới quốc gia của bạn, thì lớp nhân vật này có thể sẽ phù hợp với bạn." diplomat_introduction_suf: "thì đó chính là niềm quan tâm rất lớn với CodeCombat đến từ nhiều quốc gia trên thế giới! Chúng tôi đang xây dựng một đội ngũ phiên dịch viên đầy nhiệt huyết để đưa CodeCombat đến với mọi nơi trên thế giới. Nếu bạn muốn cập nhật những nội dung mới nhất đồng thời muốn truyền tải chúng tới quốc gia của bạn, thì lớp nhân vật này có thể sẽ phù hợp với bạn."
@ -1548,7 +1548,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
diplomat_github_url: "ở trên GitHub" diplomat_github_url: "ở trên GitHub"
diplomat_join_suf_github: ", chỉnh sửa online, và submit một pull request. Đồng thời, đánh dấu vào ô phía dưới để theo dõi những thông tin cập nhật về việc phát triển đa ngôn ngữ!" diplomat_join_suf_github: ", chỉnh sửa online, và submit một pull request. Đồng thời, đánh dấu vào ô phía dưới để theo dõi những thông tin cập nhật về việc phát triển đa ngôn ngữ!"
diplomat_subscribe_desc: "Nhận email về việc phát triển đa ngôn ngữ và màn chơi mới cần dịch thuật." diplomat_subscribe_desc: "Nhận email về việc phát triển đa ngôn ngữ và màn chơi mới cần dịch thuật."
# ambassador_introduction: "This is a community we're building, and you are the connections. We've got forums, emails, and social networks with lots of people to talk with and help get acquainted with the game and learn from. If you want to help people get involved and have fun, and get a good feel of the pulse of CodeCombat and where we're going, then this class might be for you." ambassador_introduction: "Đây là cộng đồng mà chúng tôi đang gây dựng, và bạn là những người kết nối. Chúng tôi có các diễn đàn, email, và các mạng xã hội với rất nhiêu người để nói chuyện và giúp đỡ làm quen và học từ game. Nếu bạn muốn giúp đỡ người khác tham gia chơi, và cùng tham gia CodeCombat trên con đường chúng tôi đang hướng đến, thì lớp nhân vật này có thể phù hợp với bạn."
ambassador_attribute_1: "Kỹ năng giao tiếp. Có thể nhận định được vấn đề của người chơi đang gặp phải và giúp họ giải quyết. Đồng thời, thông báo cho chúng tôi biết ý kiến của người chơi, những gì họ thích và không thích và những điều họ mong muốn!" ambassador_attribute_1: "Kỹ năng giao tiếp. Có thể nhận định được vấn đề của người chơi đang gặp phải và giúp họ giải quyết. Đồng thời, thông báo cho chúng tôi biết ý kiến của người chơi, những gì họ thích và không thích và những điều họ mong muốn!"
ambassador_join_desc: "kể cho chúng tôi một chút về bạn, bạn đã làm gì và bạn hứng thú làm gì. Chúng ta sẽ cùng bắt đầu từ đó!" ambassador_join_desc: "kể cho chúng tôi một chút về bạn, bạn đã làm gì và bạn hứng thú làm gì. Chúng ta sẽ cùng bắt đầu từ đó!"
ambassador_join_note_strong: "Chú thích" ambassador_join_note_strong: "Chú thích"
@ -1564,7 +1564,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
helpful_ambassadors: "Những Sứ Giả đầy hữu ích của chúng tôi:" helpful_ambassadors: "Những Sứ Giả đầy hữu ích của chúng tôi:"
ladder: ladder:
# please_login: "Please log in first before playing a ladder game." please_login: "Hãy đăng nhập để tham gia thi đấu."
my_matches: "Những trận đấu của tôi" my_matches: "Những trận đấu của tôi"
# simulate: "Simulate" # simulate: "Simulate"
# simulation_explanation: "By simulating games you can get your game ranked faster!" # simulation_explanation: "By simulating games you can get your game ranked faster!"
@ -1574,7 +1574,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# games_simulated_for: "Games simulated for you:" # games_simulated_for: "Games simulated for you:"
# games_in_queue: "Games currently in the queue:" # games_in_queue: "Games currently in the queue:"
# games_simulated: "Games simulated" # games_simulated: "Games simulated"
# games_played: "Games played" games_played: "Các game đã chơi"
ratio: "Tỷ lệ" ratio: "Tỷ lệ"
leaderboard: "Bạng xếp hạng" leaderboard: "Bạng xếp hạng"
# battle_as: "Battle as " # battle_as: "Battle as "
@ -1593,40 +1593,40 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# code_being_simulated: "Your new code is being simulated by other players for ranking. This will refresh as new matches come in." # code_being_simulated: "Your new code is being simulated by other players for ranking. This will refresh as new matches come in."
# no_ranked_matches_pre: "No ranked matches for the " # no_ranked_matches_pre: "No ranked matches for the "
# no_ranked_matches_post: " team! Play against some competitors and then come back here to get your game ranked." # no_ranked_matches_post: " team! Play against some competitors and then come back here to get your game ranked."
# choose_opponent: "Choose an Opponent" choose_opponent: "Chọn một đối thủ"
# select_your_language: "Select your language!" select_your_language: "Lựa chọn ngôn ngữ của bạn!"
# tutorial_play: "Play Tutorial" tutorial_play: "Chạy hướng dẫn"
# tutorial_recommended: "Recommended if you've never played before" # tutorial_recommended: "Recommended if you've never played before"
# tutorial_skip: "Skip Tutorial" # tutorial_skip: "Skip Tutorial"
# tutorial_not_sure: "Not sure what's going on?" # tutorial_not_sure: "Not sure what's going on?"
# tutorial_play_first: "Play the Tutorial first." # tutorial_play_first: "Play the Tutorial first."
# simple_ai: "Simple CPU" # simple_ai: "Simple CPU"
# warmup: "Warmup" warmup: "Khởi động"
# friends_playing: "Friends Playing" # friends_playing: "Friends Playing"
# log_in_for_friends: "Log in to play with your friends!" log_in_for_friends: "Đăng nhập để chơi với bàn bè!"
# social_connect_blurb: "Connect and play against your friends!" social_connect_blurb: "Kết nối và chơi với bạn bè!"
# invite_friends_to_battle: "Invite your friends to join you in battle!" invite_friends_to_battle: "Mời bạn bè tham gia thi đấu tranh tài!"
# fight: "Fight!" fight: "Chiến!"
# watch_victory: "Watch your victory" # watch_victory: "Watch your victory"
# defeat_the: "Defeat the" # defeat_the: "Defeat the"
# watch_battle: "Watch the battle" watch_battle: "Xem trận đấu"
# tournament_started: ", started" # tournament_started: ", started"
# tournament_ends: "Tournament ends" tournament_ends: "Giải đấu kết thúc"
# tournament_ended: "Tournament ended" tournament_ended: "Giải đấu đã kết thúc"
# tournament_rules: "Tournament Rules" tournament_rules: "Luật lệ giải đấu"
# tournament_blurb: "Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details" # tournament_blurb: "Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details"
# tournament_blurb_criss_cross: "Win bids, construct paths, outwit opponents, grab gems, and upgrade your career in our Criss-Cross tournament! Check out the details" # tournament_blurb_criss_cross: "Win bids, construct paths, outwit opponents, grab gems, and upgrade your career in our Criss-Cross tournament! Check out the details"
# tournament_blurb_zero_sum: "Unleash your coding creativity in both gold gathering and battle tactics in this alpine mirror match between red sorcerer and blue sorcerer. The tournament began on Friday, March 27 and will run until Monday, April 6 at 5PM PDT. Compete for fun and glory! Check out the details" # tournament_blurb_zero_sum: "Unleash your coding creativity in both gold gathering and battle tactics in this alpine mirror match between red sorcerer and blue sorcerer. The tournament began on Friday, March 27 and will run until Monday, April 6 at 5PM PDT. Compete for fun and glory! Check out the details"
# tournament_blurb_ace_of_coders: "Battle it out in the frozen glacier in this domination-style mirror match! The tournament began on Wednesday, September 16 and will run until Wednesday, October 14 at 5PM PDT. Check out the details" # tournament_blurb_ace_of_coders: "Battle it out in the frozen glacier in this domination-style mirror match! The tournament began on Wednesday, September 16 and will run until Wednesday, October 14 at 5PM PDT. Check out the details"
# tournament_blurb_blog: "on our blog" # tournament_blurb_blog: "on our blog"
rules: "Những điều lệ" rules: "Những điều lệ"
# winners: "Winners" winners: "Những người thắng cuộc"
# league: "League" # league: "League"
# red_ai: "Red CPU" # "Red AI Wins", at end of multiplayer match playback red_ai: "CPU Đỏ" # "Red AI Wins", at end of multiplayer match playback
# blue_ai: "Blue CPU" blue_ai: "CPU Xanh"
# wins: "Wins" # At end of multiplayer match playback wins: "Những chiến thắng" # At end of multiplayer match playback
# humans: "Red" # Ladder page display team name humans: "Đỏ" # Ladder page display team name
# ogres: "Blue" ogres: "Xanh"
user: user:
stats: "Chỉ số" stats: "Chỉ số"
@ -1660,7 +1660,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
payments: "Thanh Toán" payments: "Thanh Toán"
prepaid_codes: "Mã Trả Trước" prepaid_codes: "Mã Trả Trước"
purchased: "Đã Thanh Toán" purchased: "Đã Thanh Toán"
subscription: "Subscription" subscription: "Dịch vụ nâng cao"
invoices: "Hóa Đơn" invoices: "Hóa Đơn"
service_apple: "Apple" service_apple: "Apple"
service_web: "Web" service_web: "Web"
@ -1722,16 +1722,16 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
login_required: "Yêu cầu đăng nhập" login_required: "Yêu cầu đăng nhập"
login_required_desc: "Bạn cần đăng nhập để truy cập trang này." login_required_desc: "Bạn cần đăng nhập để truy cập trang này."
unauthorized: "Bạn cần đăng nhập. Bạn có vô hiệu hóa cookie không?" unauthorized: "Bạn cần đăng nhập. Bạn có vô hiệu hóa cookie không?"
# forbidden: "Forbidden" forbidden: "Cấm"
forbidden_desc: "Ôi không, không có gì ở đây cả! Hãy đảm bảo rằng bạn đăng nhập đúng tài khoản, hoặc truy cập trong số những đường link phía dưới để quay lại tiếp tục lập trình!" forbidden_desc: "Ôi không, không có gì ở đây cả! Hãy đảm bảo rằng bạn đăng nhập đúng tài khoản, hoặc truy cập trong số những đường link phía dưới để quay lại tiếp tục lập trình!"
not_found: "Không tìm thấy" not_found: "Không tìm thấy"
not_found_desc: "Hm, chả có gì ở đây cả. Truy cập một trong số những đường link phía dưới để quay lại tiếp tục lập trình!" not_found_desc: "Hm, chả có gì ở đây cả. Truy cập một trong số những đường link phía dưới để quay lại tiếp tục lập trình!"
# not_allowed: "Method not allowed." not_allowed: "Phương thức không được phép."
# timeout: "Server Timeout" timeout: "Máy chủ timeout"
conflict: "Xung đột tài nguyên." conflict: "Xung đột tài nguyên."
bad_input: "Lỗi đầu vào." bad_input: "Lỗi đầu vào."
server_error: "Lỗi server." server_error: "Lỗi server."
# unknown: "Unknown Error" unknown: "Lỗi không xác định"
error: "LỖI" error: "LỖI"
general_desc: "Có lỗi xảy ra, và có thể là lỗi do chúng tôi. Hãy cố đợi một lát và tải lại trang, hoặc truy cập một trong số những đường link phía dưới để quay lại tiếp tục lập trình!" general_desc: "Có lỗi xảy ra, và có thể là lỗi do chúng tôi. Hãy cố đợi một lát và tải lại trang, hoặc truy cập một trong số những đường link phía dưới để quay lại tiếp tục lập trình!"
@ -1753,7 +1753,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
arithmetic: "Toán tử" arithmetic: "Toán tử"
arrays: "Mảng" arrays: "Mảng"
basic_syntax: "Cú pháp cơ bản" basic_syntax: "Cú pháp cơ bản"
# boolean_logic: "Boolean Logic" boolean_logic: "Luận lý Boolean"
break_statements: "Câu lệnh Break" break_statements: "Câu lệnh Break"
classes: "Lớp" classes: "Lớp"
continue_statements: "Câu lệnh Continue" continue_statements: "Câu lệnh Continue"
@ -1766,7 +1766,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn
# object_literals: "Object Literals" # object_literals: "Object Literals"
parameters: "Tham số" parameters: "Tham số"
strings: "Chuỗi" strings: "Chuỗi"
variables: "Biến" variables: "Biến số"
vectors: "Các Vector" vectors: "Các Vector"
while_loops: "Vòng lặp While" while_loops: "Vòng lặp While"
recursion: "Đệ quy" recursion: "Đệ quy"

View file

@ -25,7 +25,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
im_a_student: "我是學生"#"I'm a Student" im_a_student: "我是學生"#"I'm a Student"
learn_more: "了解更多"#"Learn more" learn_more: "了解更多"#"Learn more"
classroom_in_a_box: "在一個盒子中的電腦科學教室。"#"A classroom in-a-box for teaching computer science." classroom_in_a_box: "在一個盒子中的電腦科學教室。"#"A classroom in-a-box for teaching computer science."
codecombat_is: "CodeCombat是一個<strong>學生</strong>透過進行遊戲來學習電腦科學的平台。" #"CodeCombat is a platform <strong>for students</strong> to learn computer science while playing through a real game." codecombat_is: "CodeCombat是一個<strong>學生</strong>透過進行遊戲來學習電腦科學的平台。" #"CodeCombat is a platform <strong>for students</strong> to learn computer science while playing through a real game."
our_courses: "我們的課程經過特別的遊戲測試來<strong>超越教室教學</strong>,甚至是超越一些先前沒有編程經驗的老師。" #"Our courses have been specifically playtested to <strong>excel in the classroom</strong>, even by teachers with little to no prior programming experience." our_courses: "我們的課程經過特別的遊戲測試來<strong>超越教室教學</strong>,甚至是超越一些先前沒有編程經驗的老師。" #"Our courses have been specifically playtested to <strong>excel in the classroom</strong>, even by teachers with little to no prior programming experience."
top_screenshots_hint: "學生們寫程式碼並且隨時觀看他們的更新。" #"Students write code and see their changes update in real-time" top_screenshots_hint: "學生們寫程式碼並且隨時觀看他們的更新。" #"Students write code and see their changes update in real-time"
designed_with: "在心中與老師們一起設計"#"Designed with teachers in mind" designed_with: "在心中與老師們一起設計"#"Designed with teachers in mind"
@ -37,31 +37,31 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
teaching_computer_science: "教電腦科學不需要一個昂貴的學位,因為我們提供了工具來支援各種背景的教學者。" #"Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds." teaching_computer_science: "教電腦科學不需要一個昂貴的學位,因為我們提供了工具來支援各種背景的教學者。" #"Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds."
accessible_to: "提供給" accessible_to: "提供給"
everyone: "每一個人" everyone: "每一個人"
democratizing: "使學習寫程式碼的過程民主化是我們的核心理念,每個人都應該有機會學習寫程式。" #"Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code." democratizing: "使學習寫程式碼的過程普及化是我們的核心理念,每個人都應該有機會學習寫程式。" #"Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code."
forgot_learning: "我認為他們甚至忘記了他們正在學東西。" #"I think they actually forgot that they were actually learning something." forgot_learning: "我認為他們甚至忘記了他們正在學東西。" #"I think they actually forgot that they were actually learning something."
wanted_to_do: "寫程式一直是我想要做的事情,而我從不認為我會在學校裡學到它們。" #" Coding is something I've always wanted to do, and I never thought I would be able to learn it in school." wanted_to_do: "寫程式一直是我想要做的事情,而我從不認為我會在學校裡學到它們。" #" Coding is something I've always wanted to do, and I never thought I would be able to learn it in school."
why_games: "為何從遊戲中學習是很重要的呢?" #"Why is learning through games important?" why_games: "為何從遊戲中學習是很重要的呢?" #"Why is learning through games important?"
games_reward: "遊戲鼓勵我們良性競爭。"#"Games reward the productive struggle." games_reward: "遊戲鼓勵我們良性競爭。"#"Games reward the productive struggle."
encourage: "遊戲是一個促進互動、發現及嘗試錯誤的媒介,一款好的遊戲可以試煉玩家並隨著時間精進技巧,這個過程與學生在學習中的經歷是同等重要的" #"Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn." encourage: "遊戲是一個促進互動、發現及嘗試錯誤的媒介,一款好的遊戲可以試煉玩家並隨著時間精進技巧,這個過程與學生在學校學習中所經歷到的一樣重要" #"Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn."
excel: "遊戲在獎勵方面勝出" #"Games excel at rewarding" excel: "遊戲在獎勵方面勝出" #"Games excel at rewarding"
struggle: "良性競爭"#"productive struggle" struggle: "良性競爭"#"productive struggle"
kind_of_struggle: "那種競爭使學習引人入勝,並且"#"the kind of struggle that results in learning thats engaging and" kind_of_struggle: "那種競爭使學習引人入勝,並且"#"the kind of struggle that results in learning thats engaging and"
motivating: "有動力"#"motivating" motivating: "有動力"#"motivating"
not_tedious: "不乏味"#"not tedious." not_tedious: "不乏味"#"not tedious."
gaming_is_good: "研究指出玩遊戲對小孩的大腦是有幫助的。(這是真的!)"#"Studies suggest gaming is good for childrens brains. (its true!)" gaming_is_good: "研究指出玩遊戲對小孩的大腦是有幫助的。(這是真的!)"#"Studies suggest gaming is good for childrens brains. (its true!)"
game_based: "在基於遊戲的學習系統中這是"#"When game-based learning systems are" game_based: "在基於遊戲的學習系統中這是"#"When game-based learning systems are"
compared: "相符的" compared: "相符的"
conventional: "相較於傳統評定方式的差異很明顯,「遊戲在保存知識和保持專注上是有幫助的」。" #"against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and" conventional: "與傳統評定方式比較有很明顯的不同,「遊戲在保存知識和保持專注上是有幫助的」。" #"against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and"
perform_at_higher_level: "現更高的成就水平"#"perform at a higher level of achievement" perform_at_higher_level: "現更高的成就水平"#"perform at a higher level of achievement"
feedback: "遊戲中也提供了及時回饋,讓學生去調整他們的解決途徑,並且更完整的了解學習內容,而不是被解答的正確或是不正確限制住。" #"Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers." feedback: "遊戲中也提供了及時回饋,讓學生去調整他們的解決途徑,並且更完整的了解學習內容,而不是被解答的正確或是不正確限制住。" #"Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers."
real_game: "這是一個真正的遊戲,利用真正寫入程式碼來遊玩。"#"A real game, played with real coding." real_game: "這是一個真正的遊戲,利用真正寫入程式碼來遊玩。"#"A real game, played with real coding."
great_game: "一個很棒的遊戲不只有徽章和成就--它更是玩家的旅程、設計優良的關卡,以及藉由幫助和信心完成挑戰的能力。" # "A great game is more than just badges and achievements - its about a players journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence." great_game: "一個很棒的遊戲不只有徽章和成就--它更是玩家的旅程、設計優良的關卡,以及藉由信心和幫助完成挑戰的能力。" # "A great game is more than just badges and achievements - its about a players journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence."
agency: "CodeCombat是一個能給予玩家協助以及信心的遊戲藉由我們強大的程式碼鍵入引擎可以幫助新手或是進階的學生寫出正確的、有效的程式碼。" #"CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code." agency: "CodeCombat是一個能給予玩家協助以及信心的遊戲藉由我們強大的程式碼鍵入引擎可以幫助新手或是進階的學生寫出正確的、有效的程式碼。" #"CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code."
request_demo_title: "讓您的學生從今天開始遊玩吧!"#"Get your students started today!" request_demo_title: "讓您的學生從今天開始遊玩吧!"#"Get your students started today!"
request_demo_subtitle: "要求一個demo示範版本並且讓您的學生在一個小時之內就能上手。"#"Request a demo and get your students started in less than an hour." request_demo_subtitle: "申請一個demo示範版本並且讓您的學生在一個小時之內就能上手。"#"Request a demo and get your students started in less than an hour."
get_started_title: "現在就設立您的班級。"#"Set up your class today" get_started_title: "現在就設立您的班級。"#"Set up your class today"
get_started_subtitle: "設立一個班級、加入您的學生,並在他們學習電腦科學的過程中掌握他們的進度。"#"Set up a class, add your students, and monitor their progress as they learn computer science." get_started_subtitle: "設立一個班級、加入您的學生,並在他們學習電腦科學的過程中掌握他們的進度。"#"Set up a class, add your students, and monitor their progress as they learn computer science."
request_demo: "要求一個demo示範版本"#"Request a Demo" request_demo: "申請一個demo示範版本"#"Request a Demo"
setup_a_class: "設立一個班級"#"Set Up a Class" setup_a_class: "設立一個班級"#"Set Up a Class"
have_an_account: "您是否擁有一個帳號?"#"Have an account?" have_an_account: "您是否擁有一個帳號?"#"Have an account?"
logged_in_as: "您現在登入的身分為"#"You are currently logged in as" logged_in_as: "您現在登入的身分為"#"You are currently logged in as"
@ -69,7 +69,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
computer_science: "全年齡向的電腦科學課程"#"Computer science courses for all ages" computer_science: "全年齡向的電腦科學課程"#"Computer science courses for all ages"
show_me_lesson_time: "顯示課程預估時間:"#"Show me lesson time estimates for:" show_me_lesson_time: "顯示課程預估時間:"#"Show me lesson time estimates for:"
curriculum: "課程總共時數:"#"Total curriculum hours:" curriculum: "課程總共時數:"#"Total curriculum hours:"
ffa: "對所有學生都是免費的"#"Free for all students" ffa: "學生免費"#"Free for all students"
lesson_time: "課程時間:"#"Lesson time:" lesson_time: "課程時間:"#"Lesson time:"
coming_soon: "敬請期待!"#"Coming soon!" coming_soon: "敬請期待!"#"Coming soon!"
courses_available_in: "包含JavaScript, Python,和Java的課程(敬請期待!)"#"Courses are available in JavaScript, Python, and Java (coming soon!)" courses_available_in: "包含JavaScript, Python,和Java的課程(敬請期待!)"#"Courses are available in JavaScript, Python, and Java (coming soon!)"
@ -107,7 +107,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
stats: "記錄" stats: "記錄"
code: "程式碼" code: "程式碼"
home: "首頁" home: "首頁"
contribute: "貢獻" contribute: "幫助我們"
legal: "版權聲明" legal: "版權聲明"
about: "關於" about: "關於"
contact: "聯繫我們" contact: "聯繫我們"
@ -124,9 +124,9 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
jobs: "工作" jobs: "工作"
schools: "學校" schools: "學校"
educator_wiki: "教育者 Wiki" educator_wiki: "教育者 Wiki"
get_involved: "參與其中" get_involved: "親身參與"
open_source: "開源 (GitHub)" open_source: "開源 (GitHub)"
support: "支援" support: "取得幫助"
faqs: "FAQs常見問題" faqs: "FAQs常見問題"
help_pref: "需要協助嗎? 寫封Email給我們" help_pref: "需要協助嗎? 寫封Email給我們"
help_suff: "然後我們會與您接觸!" help_suff: "然後我們會與您接觸!"
@ -139,7 +139,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
page_not_found: "找不到網頁" page_not_found: "找不到網頁"
diplomat_suggestion: diplomat_suggestion:
title: "我們翻譯CodeCombat" # This shows up when a player switches to a non-English language using the language selector. title: "我們翻譯CodeCombat" # This shows up when a player switches to a non-English language using the language selector.
sub_heading: "我們需要您的語言技能" sub_heading: "我們需要您的語言技能"
pitch_body: "我們開發了CodeCombat的英文版但是現在我們的玩家遍佈全球。很多人想玩中文版的卻不會說英文所以如果您中英文都會請考慮一下參加我們的翻譯工作幫忙把 CodeCombat 網站還有所有的關卡翻譯成中文(繁體)。" pitch_body: "我們開發了CodeCombat的英文版但是現在我們的玩家遍佈全球。很多人想玩中文版的卻不會說英文所以如果您中英文都會請考慮一下參加我們的翻譯工作幫忙把 CodeCombat 網站還有所有的關卡翻譯成中文(繁體)。"
missing_translations: "直至所有正體中文的翻譯完畢,當無法提供正體中文時還會以英文顯示。" missing_translations: "直至所有正體中文的翻譯完畢,當無法提供正體中文時還會以英文顯示。"
@ -168,7 +168,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
next: "下一步" # Go from choose hero to choose inventory before playing a level next: "下一步" # Go from choose hero to choose inventory before playing a level
change_hero: "更換英雄" # Go back from choose inventory to choose hero change_hero: "更換英雄" # Go back from choose inventory to choose hero
buy_gems: "購買寶石" buy_gems: "購買寶石"
subscription_required: "需要訂購" subscription_required: "訂閱限定"
anonymous: "匿名玩家" anonymous: "匿名玩家"
level_difficulty: "難度" level_difficulty: "難度"
play_classroom_version: "遊玩教室版本" # Choose a level in campaign version that you also can play in one of your courses play_classroom_version: "遊玩教室版本" # Choose a level in campaign version that you also can play in one of your courses
@ -179,7 +179,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
adjust_volume: "調整音量" adjust_volume: "調整音量"
campaign_multiplayer: "多人競技場" campaign_multiplayer: "多人競技場"
campaign_multiplayer_description: "…在這裡您可以和其他玩家進行對戰。" campaign_multiplayer_description: "…在這裡您可以和其他玩家進行對戰。"
campaign_old_multiplayer: "(過時的)舊多人競技場" campaign_old_multiplayer: "(過時的)舊多人競技場"
campaign_old_multiplayer_description: "多個文明時代的遺跡。已沒有模擬運行這些陳舊、英雄蕪絕的多人競技場。" campaign_old_multiplayer_description: "多個文明時代的遺跡。已沒有模擬運行這些陳舊、英雄蕪絕的多人競技場。"
code: code:
@ -387,7 +387,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
done: "完成" done: "完成"
next_level: "下一個關卡:" next_level: "下一個關卡:"
next_game: "下一個遊戲" next_game: "下一個遊戲"
show_menu: "顯示遊戲" show_menu: "顯示遊戲"
home: "首頁" # Not used any more, will be removed soon. home: "首頁" # Not used any more, will be removed soon.
level: "關卡" # Like "Level: Dungeons of Kithgard" level: "關卡" # Like "Level: Dungeons of Kithgard"
skip: "跳過" skip: "跳過"
@ -425,11 +425,11 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
victory_new_item: "新的物品" victory_new_item: "新的物品"
victory_viking_code_school: "太厲害了您剛完成了非常困難的關卡如果您想成為一個軟體開發人員您就應該去試一下Viking Code School。在這裡您可以把您的知識增長到另一個台階。只需要14個星期您就能成為一個專業的網頁開發人員。" victory_viking_code_school: "太厲害了您剛完成了非常困難的關卡如果您想成為一個軟體開發人員您就應該去試一下Viking Code School。在這裡您可以把您的知識增長到另一個台階。只需要14個星期您就能成為一個專業的網頁開發人員。"
victory_become_a_viking: "成為一個維京人。" victory_become_a_viking: "成為一個維京人。"
victory_no_progress_for_teachers: "老師不能保存進度,但是您可以將自己的帳號加入班級作為學生來保存進度。" #"Progress is not saved for teachers. But, you can add a student account to your classroom for yourself." victory_no_progress_for_teachers: "老師不能保存進度,但是您可以將自己的帳號加入班級作為學生來保存進度。" #"Progress is not saved for teachers. But, you can add a student account to your classroom for yourself."
guide_title: "指南" guide_title: "指南"
tome_cast_button_run: "" tome_cast_button_run: ""
tome_cast_button_running: "" tome_cast_button_running: ""
tome_cast_button_ran: "已運" tome_cast_button_ran: "已運"
tome_submit_button: "送出" tome_submit_button: "送出"
tome_reload_method: "重新載入該方法的原程式碼" # Title text for individual method reload button. tome_reload_method: "重新載入該方法的原程式碼" # Title text for individual method reload button.
tome_select_method: "選擇一個方法" tome_select_method: "選擇一個方法"
@ -522,7 +522,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
tip_mulan: "相信你可以做到,然後你就會做到。 - Mulan"#"Believe you can, then you will. - Mulan" tip_mulan: "相信你可以做到,然後你就會做到。 - Mulan"#"Believe you can, then you will. - Mulan"
game_menu: game_menu:
inventory_tab: "倉庫" inventory_tab: "道具欄"
save_load_tab: "保存/載入" save_load_tab: "保存/載入"
options_tab: "選項" options_tab: "選項"
guide_tab: "導引" guide_tab: "導引"
@ -546,16 +546,16 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
week: "這週" week: "這週"
all: "長期以來" all: "長期以來"
time: "時間" time: "時間"
damage_taken: "遭受的攻擊" damage_taken: "遭受的傷害"
damage_dealt: "造成的攻擊" damage_dealt: "造成的傷害"
difficulty: "困難度" difficulty: "困難度"
gold_collected: "收集的黃金" gold_collected: "收集的黃金"
inventory: inventory:
equipped_item: "已裝備" equipped_item: "已裝備"
required_purchase_title: "需要" required_purchase_title: "必要的"
available_item: "可使用" available_item: "可使用"
restricted_title: "被限制" restricted_title: "被限制"
should_equip: "連點物品兩下可裝備" should_equip: "連點物品兩下可裝備"
equipped: "(已裝備)" equipped: "(已裝備)"
locked: "(需解鎖)" locked: "(需解鎖)"
@ -578,12 +578,12 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
subscribe: subscribe:
comparison_blurb: "訂閱 CodeCombat 來磨練您的技巧!" comparison_blurb: "訂閱 CodeCombat 來磨練您的技巧!"
feature1: "__levelsCount__ 個以上的基本關卡分在__worldsCount__張地圖中" # {change} feature1: "__levelsCount__ 個以上的基本關卡分在__worldsCount__張地圖中" # {change}
feature2: "__heroesCount__ 個強力的<strong>新英雄</strong>並且每位都有不同技能!" # {change} feature2: "__heroesCount__ 個強力的<strong>新英雄</strong>並且每位都有不同技能!" # {change}
feature3: "__bonusLevelsCount__ 個以上的額外關卡" # {change} feature3: "__bonusLevelsCount__ 個以上的額外關卡" # {change}
feature4: "每個月<strong>{{gems}}顆額外寶石</strong>" feature4: "每個月<strong>{{gems}}顆額外寶石</strong>"
feature5: "影片教學" feature5: "影片教學"
feature6: "頂級信箱支援" feature6: "高級郵件幫助"
feature7: "私密<strong>部落</strong>" feature7: "私密<strong>部落</strong>"
feature8: "<strong>沒有廣告!</strong>" feature8: "<strong>沒有廣告!</strong>"
free: "免費" free: "免費"
@ -744,7 +744,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
scott_title: "共同創辦人" # {change} scott_title: "共同創辦人" # {change}
scott_blurb: "理性至上" scott_blurb: "理性至上"
# maka_title: "Customer Advocate" # maka_title: "Customer Advocate"
# maka_blurb: "Storyteller" maka_blurb: "說書人"
rob_title: "編譯工程師" # {change} rob_title: "編譯工程師" # {change}
rob_blurb: "編寫一些的程式碼" rob_blurb: "編寫一些的程式碼"
josh_c_title: "遊戲設計師" josh_c_title: "遊戲設計師"
@ -1184,9 +1184,9 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
# last_level: "Last Level" # last_level: "Last Level"
# welcome_to_hoc: "Adventurers, welcome to our Hour of Code!" # welcome_to_hoc: "Adventurers, welcome to our Hour of Code!"
# logged_in_as: "Logged in as:" # logged_in_as: "Logged in as:"
# not_you: "Not you?" not_you: "不是您嗎?"#"Not you?"
# welcome_back: "Hi adventurer, welcome back!" # welcome_back: "Hi adventurer, welcome back!"
# continue_playing: "Continue Playing" continue_playing: "繼續進行遊戲"#"Continue Playing"
# more_options: "More options:" # more_options: "More options:"
# option1_header: "Option 1: Invite students via email" # option1_header: "Option 1: Invite students via email"
# option1_body: "Students will automatically be sent an invitation to join this class, and will need to create an account with a username and password." # option1_body: "Students will automatically be sent an invitation to join this class, and will need to create an account with a username and password."
@ -1223,7 +1223,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
# class_code: "Class Code" # class_code: "Class Code"
# optional_ask: "optional - ask your teacher to give you one!" # optional_ask: "optional - ask your teacher to give you one!"
# optional_school: "optional - what school do you go to?" # optional_school: "optional - what school do you go to?"
# start_playing: "Start Playing" start_playing: "開始遊戲"#"Start Playing"
# skip_this: "Skip this, I'll create an account later!" # skip_this: "Skip this, I'll create an account later!"
# welcome: "Welcome" # welcome: "Welcome"
# getting_started: "Getting Started with Courses" # getting_started: "Getting Started with Courses"
@ -1398,7 +1398,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
ambassador_title: "使節" ambassador_title: "使節"
ambassador_title_description: "(Support)" ambassador_title_description: "(Support)"
ambassador_summary: "安撫我們論壇的用戶並且提供發問者適當的方向。我們的使節代表CodeCombat面對全世界。" ambassador_summary: "安撫我們論壇的用戶並且提供發問者適當的方向。我們的使節代表CodeCombat面對全世界。"
# teacher_title: "Teacher" teacher_title: "教師"#"Teacher"
editor: editor:
main_title: "CodeCombat編輯器" main_title: "CodeCombat編輯器"
@ -1407,7 +1407,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
level_title: "關卡編輯器" level_title: "關卡編輯器"
achievement_title: "目標編輯器" achievement_title: "目標編輯器"
poll_title: "投票編輯器" poll_title: "投票編輯器"
back: "後退" back: "返回"
revert: "還原" revert: "還原"
revert_models: "還原模式" revert_models: "還原模式"
pick_a_terrain: "選擇地形" pick_a_terrain: "選擇地形"
@ -1511,7 +1511,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
join_desc_3: ",或者找到我們在" join_desc_3: ",或者找到我們在"
join_desc_4: "讓我們從這開始!" join_desc_4: "讓我們從這開始!"
join_url_email: "發信給我們" join_url_email: "發信給我們"
# join_url_slack: "public Slack channel" join_url_slack: "公共休閒頻道"#"public Slack channel"
archmage_subscribe_desc: "取得郵件關於新的編程機會和公告。" archmage_subscribe_desc: "取得郵件關於新的編程機會和公告。"
artisan_introduction_pref: "我們必須建造更多的關卡!大家為了更多的內容在高聲吶喊,但只靠我們只能建造這麼多。現在您的工作場所就是一關;我們的關卡編輯器是勉強可用的,所以請小心。只要您有新的靈感,不論從簡單的 for-loops 到" artisan_introduction_pref: "我們必須建造更多的關卡!大家為了更多的內容在高聲吶喊,但只靠我們只能建造這麼多。現在您的工作場所就是一關;我們的關卡編輯器是勉強可用的,所以請小心。只要您有新的靈感,不論從簡單的 for-loops 到"
artisan_introduction_suf: ",那個這職業會適合您。" artisan_introduction_suf: ",那個這職業會適合您。"
@ -1555,7 +1555,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
ambassador_join_note_strong: "注意" ambassador_join_note_strong: "注意"
ambassador_join_note_desc: "其中一件我們優先要做的事情是建立多人連線,玩家將面臨難以獨自解決的關卡而且可以招喚更高等級的法師來幫助。這將對於使節是一個很棒的方式來完成自己的責任。我們會及時地向大家公佈!" ambassador_join_note_desc: "其中一件我們優先要做的事情是建立多人連線,玩家將面臨難以獨自解決的關卡而且可以招喚更高等級的法師來幫助。這將對於使節是一個很棒的方式來完成自己的責任。我們會及時地向大家公佈!"
ambassador_subscribe_desc: "取得更新和多人連線開發的郵件。" ambassador_subscribe_desc: "取得更新和多人連線開發的郵件。"
# teacher_subscribe_desc: "Get emails on updates and announcements for teachers." teacher_subscribe_desc: "取得給教師的更新以及消息。"#"Get emails on updates and announcements for teachers."
changes_auto_save: "當您勾選後,改變將自動儲存。" changes_auto_save: "當您勾選後,改變將自動儲存。"
diligent_scribes: "我們勤奮的文書:" diligent_scribes: "我們勤奮的文書:"
powerful_archmages: "我們強勁的大法師:" powerful_archmages: "我們強勁的大法師:"
@ -1565,11 +1565,11 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
helpful_ambassadors: "我們善於幫助的使節:" helpful_ambassadors: "我們善於幫助的使節:"
ladder: ladder:
please_login: "在參與對前請先登入。" please_login: "在參與對前請先登入。"
my_matches: "我的對手" my_matches: "我的對手"
simulate: "模擬" simulate: "模擬"
simulation_explanation: "通過模擬遊戲,您可以使您的遊戲更快得到評分!" simulation_explanation: "通過模擬遊戲,您可以使您的遊戲更快得到評分!"
simulation_explanation_leagues: "會主要給在你的部落或者課程的同伴幫忙模擬遊戲。" simulation_explanation_leagues: "主要會為你的部落或者課程的同伴幫忙模擬遊戲。"
simulate_games: "模擬遊戲!" simulate_games: "模擬遊戲!"
games_simulated_by: "您模擬過的次數:" games_simulated_by: "您模擬過的次數:"
games_simulated_for: "替您模擬的次數:" games_simulated_for: "替您模擬的次數:"
@ -1590,14 +1590,14 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
rank_failed: "評分失敗" rank_failed: "評分失敗"
rank_being_ranked: "已評分" rank_being_ranked: "已評分"
rank_last_submitted: "已上傳 " rank_last_submitted: "已上傳 "
help_simulate: "模擬遊戲需要幫助" help_simulate: "幫我們模擬遊戲?"
code_being_simulated: "您的新程式碼正在被其他人模擬評分中。分數將隨每次新的配對而更新。" code_being_simulated: "您的新程式碼正在被其他人模擬評分中。分數將隨每次新的配對而更新。"
no_ranked_matches_pre: "對這個隊伍尚未有評分過的配對!" no_ranked_matches_pre: "對這個隊伍尚未有評分過的配對!"
no_ranked_matches_post: " 在別人的戰場上扮演競爭者並且回到這使您的程式碼接受評分。" no_ranked_matches_post: " 在別人的戰場上扮演對手並且回到這使您的程式碼接受評分。"
choose_opponent: "選擇對手" choose_opponent: "選擇對手"
select_your_language: "選擇您的語言!" select_your_language: "選擇您的語言!"
tutorial_play: "教學" tutorial_play: "教學"
tutorial_recommended: "如果您尚未玩過,推薦先嘗試教學" tutorial_recommended: "如果您尚未玩過,建議先嘗試教學"
tutorial_skip: "略過教學" tutorial_skip: "略過教學"
tutorial_not_sure: "不確定發生啥事?" tutorial_not_sure: "不確定發生啥事?"
tutorial_play_first: "先嘗試教學" tutorial_play_first: "先嘗試教學"
@ -1650,11 +1650,11 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
amount_achieved: "數量" amount_achieved: "數量"
achievement: "成就" achievement: "成就"
current_xp_prefix: "當前總共" current_xp_prefix: "當前總共"
current_xp_postfix: "經驗" current_xp_postfix: "經驗"
new_xp_prefix: "獲得" new_xp_prefix: "獲得"
new_xp_postfix: "經驗" new_xp_postfix: "經驗"
left_xp_prefix: "還需要" left_xp_prefix: "還需要"
left_xp_infix: "經驗" left_xp_infix: "經驗"
left_xp_postfix: "到下一個等級" left_xp_postfix: "到下一個等級"
account: account:
@ -1694,13 +1694,13 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
# purchase_code1: "Subscription Codes can be redeemed to add premium subscription time to one or more CodeCombat accounts." # purchase_code1: "Subscription Codes can be redeemed to add premium subscription time to one or more CodeCombat accounts."
# purchase_code2: "Each CodeCombat account can only redeem a particular Subscription Code once." # purchase_code2: "Each CodeCombat account can only redeem a particular Subscription Code once."
# purchase_code3: "Subscription Code months will be added to the end of any existing subscription on the account." # purchase_code3: "Subscription Code months will be added to the end of any existing subscription on the account."
# users: "Users" users: "使用者"#"Users"
# months: "Months" months: "月數"#"Months"
purchase_total: "總共" purchase_total: "總共"
purchase_button: "提交購買" purchase_button: "提交購買"
your_codes: "你的訂閱碼:" # {change} your_codes: "你的訂閱碼:" # {change}
redeem_codes: "兌換訂閱碼" redeem_codes: "兌換訂閱碼"
# prepaid_code: "Prepaid Code" prepaid_code: "預付代碼"#"Prepaid Code"
# lookup_code: "Lookup prepaid code" # lookup_code: "Lookup prepaid code"
# apply_account: "Apply to your account" # apply_account: "Apply to your account"
# copy_link: "You can copy the code's link and send it to someone." # copy_link: "You can copy the code's link and send it to someone."
@ -1847,7 +1847,7 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
nutshell_description: "我們在關卡編輯器裡公開的任何資源,您都可以在製作關卡時隨意使用,但我們保留在 codecombat.com 之上創建的關卡本身傳播的權利,因為我們往後可能決定以它們收費。" nutshell_description: "我們在關卡編輯器裡公開的任何資源,您都可以在製作關卡時隨意使用,但我們保留在 codecombat.com 之上創建的關卡本身傳播的權利,因為我們往後可能決定以它們收費。"
canonical: "我們宣告這篇說明的英文版本是權威版本。如果各個翻譯版本之間有任何衝突,以英文版為準。" canonical: "我們宣告這篇說明的英文版本是權威版本。如果各個翻譯版本之間有任何衝突,以英文版為準。"
third_party_title: "第三方服務" third_party_title: "第三方服務"
# third_party_description: "CodeCombat uses the following third party services (among others):" third_party_description: "CodeCombat使用下列的第三方服務"#"CodeCombat uses the following third party services (among others):"
ladder_prizes: ladder_prizes:
title: "錦標賽獎項" # This section was for an old tournament and doesn't need new translations now. title: "錦標賽獎項" # This section was for an old tournament and doesn't need new translations now.
@ -1869,19 +1869,19 @@ module.exports = nativeDescription: "繁體中文", englishDescription: "Chinese
license: "許可證" license: "許可證"
oreilly: "您選擇的電子書" oreilly: "您選擇的電子書"
# calendar: calendar:
# year: "Year" year: ""#"Year"
# day: "Day" day: ""#"Day"
# month: "Month" month: ""#"Month"
# january: "January" january: "一月"#"January"
# february: "February" february: "二月"#"February"
# march: "March" march: "三月"#"March"
# april: "April" april: "四月"#"April"
# may: "May" may: "五月"#"May"
# june: "June" june: "六月"#"June"
# july: "July" july: "七月"#"July"
# august: "August" august: "八月"#"August"
# september: "September" september: "九月"#"September"
# october: "October" october: "十月"#"October"
# november: "November" november: "十一月"#"November"
# december: "December" december: "十二月"#"December"

View file

@ -1,6 +1,7 @@
CocoModel = require './CocoModel' CocoModel = require './CocoModel'
schema = require 'schemas/models/classroom.schema' schema = require 'schemas/models/classroom.schema'
utils = require 'core/utils' utils = require 'core/utils'
User = require 'models/User'
module.exports = class Classroom extends CocoModel module.exports = class Classroom extends CocoModel
@className: 'Classroom' @className: 'Classroom'
@ -10,6 +11,15 @@ module.exports = class Classroom extends CocoModel
initialize: () -> initialize: () ->
@listenTo @, 'change:aceConfig', @capitalizeLanguageName @listenTo @, 'change:aceConfig', @capitalizeLanguageName
super(arguments...) super(arguments...)
parse: (obj) ->
if obj._id
# It's just the classroom object
return obj
else
# It's a compound response with other stuff too
@owner = new User(obj.owner)
return obj.data
capitalizeLanguageName: -> capitalizeLanguageName: ->
language = @get('aceConfig')?.language language = @get('aceConfig')?.language
@ -17,9 +27,19 @@ module.exports = class Classroom extends CocoModel
joinWithCode: (code, opts) -> joinWithCode: (code, opts) ->
options = { options = {
url: _.result(@, 'url') + '/~/members' url: @urlRoot + '/~/members'
type: 'POST' type: 'POST'
data: { code: code } data: { code: code }
success: => @trigger 'join:success'
error: => @trigger 'join:error'
}
_.extend options, opts
@fetch(options)
fetchByCode: (code, opts) ->
options = {
url: _.result(@, 'url')
data: { code: code, "with-owner": true }
} }
_.extend options, opts _.extend options, opts
@fetch(options) @fetch(options)
@ -32,6 +52,16 @@ module.exports = class Classroom extends CocoModel
} }
_.extend options, opts _.extend options, opts
@fetch(options) @fetch(options)
setStudentPassword: (student, password, options) ->
classroomID = @.id
$.ajax {
url: "/db/classroom/#{classroomID}/members/#{student.id}/reset-password"
method: 'POST'
data: { password }
success: => @trigger 'save-password:success'
error: (response) => @trigger 'save-password:error', response.responseJSON
}
getLevels: (options={}) -> getLevels: (options={}) ->
# options: courseID, withoutLadderLevels # options: courseID, withoutLadderLevels

View file

@ -19,3 +19,24 @@ module.exports = class Prepaid extends CocoModel
maxRedeemers = @get('maxRedeemers') maxRedeemers = @get('maxRedeemers')
if _.isString(maxRedeemers) if _.isString(maxRedeemers)
@set 'maxRedeemers', parseInt(maxRedeemers) @set 'maxRedeemers', parseInt(maxRedeemers)
status: ->
endDate = @get('endDate')
if endDate and new Date(endDate) < new Date()
return 'expired'
startDate = @get('startDate')
if startDate and new Date(startDate) > new Date()
return 'pending'
if @openSpots() <= 0
return 'empty'
return 'available'
redeem: (user, options={}) ->
options.url = _.result(@, 'url')+'/redeemers'
options.type = 'POST'
options.data ?= {}
options.data.userID = user.id or user
@fetch(options)

View file

@ -0,0 +1,19 @@
CocoModel = require './CocoModel'
module.exports = class StripeCoupon extends CocoModel
@className: 'StripeCoupon'
@schema: {}
urlRoot: '/stripe/coupons'
idAttribute: 'id'
formatString: ->
bits = [@id]
if @get('percent_off')
bits.push "(#{@get('percent_off')}% off)"
else if @get('amount_off')
bits.push "($#{@get('amount_off')} off)"
if @get('duration')
bits.push "(duration: #{@get('duration')})"
if @redeem_by
bits.push "(redeem by: #{moment(@get('redeem_by')).format('lll')}"
return bits.join(' ')

View file

@ -23,8 +23,8 @@ module.exports = class User extends CocoModel
return name if name return name if name
name = _.filter([@get('firstName'), @get('lastName')]).join(' ') name = _.filter([@get('firstName'), @get('lastName')]).join(' ')
return name if name return name if name
email = @get('email') [emailName, emailDomain] = @get('email')?.split('@') or []
return email if email return emailName if emailName
return 'Anoner' return 'Anoner'
getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) -> getPhotoURL: (size=80, useJobProfilePhoto=false, useEmployerPageAvatar=false) ->
@ -35,6 +35,9 @@ module.exports = class User extends CocoModel
return "#{photoURL}#{prefix}s=#{size}" if photoURL.search('http') isnt -1 # legacy return "#{photoURL}#{prefix}s=#{size}" if photoURL.search('http') isnt -1 # legacy
return "/file/#{photoURL}#{prefix}s=#{size}" return "/file/#{photoURL}#{prefix}s=#{size}"
return "/db/user/#{@id}/avatar?s=#{size}&employerPageAvatar=#{useEmployerPageAvatar}" return "/db/user/#{@id}/avatar?s=#{size}&employerPageAvatar=#{useEmployerPageAvatar}"
getRequestVerificationEmailURL: ->
@url() + "/request-verify-email"
getSlugOrID: -> @get('slug') or @get('_id') getSlugOrID: -> @get('slug') or @get('_id')
@ -65,6 +68,11 @@ module.exports = class User extends CocoModel
isTeacher: -> isTeacher: ->
return @get('role') in ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent'] return @get('role') in ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent']
justPlaysCourses: ->
# This heuristic could be better, but currently we don't add to me.get('courseInstances') for single-player anonymous intro courses, so they have to beat a level without choosing a hero.
return true if me.get('role') is 'student'
return me.get('stats')?.gamesCompleted and not me.get('heroConfig')
isSessionless: -> isSessionless: ->
# TODO: Fix old users who got mis-tagged as teachers # TODO: Fix old users who got mis-tagged as teachers
@ -205,12 +213,27 @@ module.exports = class User extends CocoModel
return true if me.hasSubscription() return true if me.hasSubscription()
return false return false
isEnrolled: ->
Boolean(@get('coursePrepaidID'))
isOnPremiumServer: -> isOnPremiumServer: ->
me.get('country') in ['china', 'brazil'] me.get('country') in ['china', 'brazil']
sendVerificationCode: (code) ->
$.ajax({
method: 'POST'
url: "/db/user/#{@id}/verify/#{code}"
success: (attributes) =>
this.set attributes
@trigger 'email-verify-success'
error: =>
@trigger 'email-verify-error'
})
isEnrolled: -> @prepaidStatus() is 'enrolled'
prepaidStatus: -> # 'not-enrolled', 'enrolled', 'expired'
coursePrepaid = @get('coursePrepaid')
return 'not-enrolled' unless coursePrepaid
return 'enrolled' unless coursePrepaid.endDate
return if coursePrepaid.endDate > new Date().toISOString() then 'enrolled' else 'expired'
# Function meant for "me" # Function meant for "me"
@ -266,6 +289,12 @@ module.exports = class User extends CocoModel
options.data.facebookID = facebookID options.data.facebookID = facebookID
options.data.facebookAccessToken = application.facebookHandler.token() options.data.facebookAccessToken = application.facebookHandler.token()
@fetch(options) @fetch(options)
makeCoursePrepaid: ->
coursePrepaid = @get('coursePrepaid')
return null unless coursePrepaid
Prepaid = require 'models/Prepaid'
return new Prepaid(coursePrepaid)
becomeStudent: (options={}) -> becomeStudent: (options={}) ->
options.url = '/db/user/-/become-student' options.url = '/db/user/-/become-student'

View file

@ -20,7 +20,7 @@ defaultTasks = [
'Choose music file in Introduction script.' 'Choose music file in Introduction script.'
'Choose autoplay in Introduction script.' 'Choose autoplay in Introduction script.'
'Add Clojure/Lua/CoffeeScript.' 'Add Lua/CoffeeScript/Java.'
'Write the description.' 'Write the description.'
'Write the guide.' 'Write the guide.'

View file

@ -6,11 +6,13 @@ PrepaidSchema = c.object({title: 'Prepaid', required: ['creator', 'type']}, {
c.object {required: ['date', 'userID']}, c.object {required: ['date', 'userID']},
date: c.date {title: 'Redeemed date'} date: c.date {title: 'Redeemed date'}
userID: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ]) userID: c.objectId(links: [ {rel: 'extra', href: '/db/user/{($)}'} ])
maxRedeemers: { type: 'integer'} maxRedeemers: { type: 'integer' }
code: c.shortString(title: "Unique code to redeem") code: c.shortString(title: "Unique code to redeem")
type: { type: 'string' } type: { type: 'string' }
properties: {type: 'object'} properties: {type: 'object' }
exhausted: { type: 'boolean' } exhausted: { type: 'boolean' }
startDate: c.stringDate()
endDate: c.stringDate()
}) })
c.extendBasicProperties(PrepaidSchema, 'prepaid') c.extendBasicProperties(PrepaidSchema, 'prepaid')

View file

@ -50,13 +50,14 @@ visa = c.shortString
_.extend UserSchema.properties, _.extend UserSchema.properties,
email: c.shortString({title: 'Email', format: 'email'}) email: c.shortString({title: 'Email', format: 'email'})
emailVerified: { type: 'boolean' }
iosIdentifierForVendor: c.shortString({format: 'hidden'}) iosIdentifierForVendor: c.shortString({format: 'hidden'})
firstName: c.shortString({title: 'First Name'}) firstName: c.shortString({title: 'First Name'})
lastName: c.shortString({title: 'Last Name'}) lastName: c.shortString({title: 'Last Name'})
gender: {type: 'string'} # , 'enum': ['male', 'female', 'secret', 'trans', 'other'] gender: {type: 'string'} # , 'enum': ['male', 'female', 'secret', 'trans', 'other']
# NOTE: ageRange enum changed on 4/27/16 from ['0-13', '14-17', '18-24', '25-34', '35-44', '45-100'] # NOTE: ageRange enum changed on 4/27/16 from ['0-13', '14-17', '18-24', '25-34', '35-44', '45-100']
ageRange: {type: 'string'} # 'enum': ['13-15', '16-17', '18-24', '25-34', '35-44', '45-100'] ageRange: {type: 'string'} # 'enum': ['13-15', '16-17', '18-24', '25-34', '35-44', '45-100']
password: {type: 'string', maxLength: 256, minLength: 2, title: 'Password'} password: c.passwordString
passwordReset: {type: 'string'} passwordReset: {type: 'string'}
photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'} photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'}
@ -327,6 +328,16 @@ _.extend UserSchema.properties,
coursePrepaidID: c.objectId({ coursePrepaidID: c.objectId({
description: 'Prepaid which has paid for this user\'s course access' description: 'Prepaid which has paid for this user\'s course access'
}) })
coursePrepaid: {
type: 'object'
properties: {
_id: c.objectId()
startDate: c.stringDate()
endDate: c.stringDate()
}
}
enrollmentRequestSent: { type: 'boolean' }
schoolName: {type: 'string'} schoolName: {type: 'string'}
role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]} role: {type: 'string', enum: ["God", "advisor", "parent", "principal", "student", "superintendent", "teacher", "technology coordinator"]}
birthday: c.stringDate({title: "Birthday"}) birthday: c.stringDate({title: "Birthday"})

View file

@ -15,6 +15,7 @@ me.object = (ext, props) -> combine({type: 'object', additionalProperties: false
me.array = (ext, items) -> combine({type: 'array', items: items or {}}, ext) me.array = (ext, items) -> combine({type: 'array', items: items or {}}, ext)
me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext) me.shortString = (ext) -> combine({type: 'string', maxLength: 100}, ext)
me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext)
me.passwordString = {type: 'string', maxLength: 256, minLength: 2, title: 'Password'}
# Dates should usually be strings, ObjectIds should be strings: https://github.com/codecombat/codecombat/issues/1384 # Dates should usually be strings, ObjectIds should be strings: https://github.com/codecombat/codecombat/issues/1384
me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # old me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # old

View file

@ -0,0 +1,3 @@
#administer-user-modal
.modal-dialog
width: 90%

View file

@ -1,4 +1,28 @@
@import "app/styles/bootstrap/variables"
#enrollments-view #enrollments-view
@media (min-width: $screen-md-min)
#prepaids-col
padding-right: 40px
#actions-col
border-left: 1px solid gray // Will tend to be the longer one
padding-left: 40px
.prepaid-card
border-radius: 10px
p
color: white
h1
font-size: 68px
&.pending-prepaid-card
background: #6e939f
.how-to-enroll .how-to-enroll
padding: 10px padding: 10px
ol ol
@ -8,11 +32,20 @@
border-radius: 5px border-radius: 5px
#students-input #students-input
width: 100px width: 220px
height: 50px height: 80px
line-height: 30px font-size: 50px
font-size: 30px
&::-webkit-inner-spin-button, &::-webkit-outer-spin-button &::-webkit-inner-spin-button, &::-webkit-outer-spin-button
-webkit-appearance: none -webkit-appearance: none
margin: 0 margin: 0
#enrollment-stats-table
td, th
border: none
.classroom-name-td
padding-left: 20px
th
padding-bottom: 10px

View file

@ -1,11 +0,0 @@
#purchase-courses-view
.enrollment-count
font-size: 30px
width: 120px
.not-enrolled
line-height: 16px
.uppercase
text-transform: uppercase

View file

@ -157,8 +157,14 @@
.glyphicon .glyphicon
color: $gray-light color: $gray-light
.edit-student-link
color: black
.remove-student-link .remove-student-link
color: $burgandy color: $burgandy
.edit-student-link, .remove-student-link
display: inline-block
font-weight: bold font-weight: bold
text-decoration: underline text-decoration: underline
line-height: 16px line-height: 16px
@ -279,3 +285,28 @@
.export-student-progress-btn .export-student-progress-btn
margin-top: 10px margin-top: 10px
// Enrollment Status Tab
#search-form-group
position: relative
input
width: auto
.glyphicon
color: $gray
position: absolute
top: 8px
right: 5px
#enrollment-status-table
// These column widths are just to keep the cells from resizing on search
.checkbox-col
width: 75px
.student-info-col
width: 240px
.status-col
width: 300px
.enroll-col
width: 140px
td
vertical-align: middle

View file

@ -248,6 +248,11 @@ body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='
background-color: $burgandy background-color: $burgandy
color: white color: white
.btn-burgandy-alt
background-color: white
border: 1px solid $burgandy
color: $burgandy
.btn-lg .btn-lg
font-size: 18px font-size: 18px

View file

@ -0,0 +1,4 @@
#edit-student-modal
.new-password-input
width: 300px
text-align: center

View file

@ -0,0 +1,3 @@
#teachers-contact-modal
textarea
height: 200px

View file

@ -1,6 +1,5 @@
#test-view #test-view
background-color: #eee background-color: #eee
margin: 0 20px
padding: 0 padding: 0
#test-h2 #test-h2

View file

@ -0,0 +1,11 @@
#email-verified-view
.alert
display: flex
align-items: center
justify-content: center
.glyphicon
font-size: 20pt
.btn-lg
min-width: 246px

View file

@ -17,7 +17,15 @@ else
label.control-label(for="name", data-i18n="general.name") Name label.control-label(for="name", data-i18n="general.name") Name
input#name-input.form-control(name="name", type="text", value="#{name}") input#name-input.form-control(name="name", type="text", value="#{name}")
.form-group .form-group
label.control-label(for="email", data-i18n="general.email") Email label.control-label(for="email")
span(data-i18n="general.email") Email
unless me.get('emailVerified')
span.spl (
span.spr(data-i18n="account.not_yet_verified")
a.resend-verification-email
span.resend-text(data-i18n="account.resend_email")
span.sent-text.hide(data-i18n="account.email_sent")
span )
input#email.form-control(name="email", type="text", value="#{email}") input#email.form-control(name="email", type="text", value="#{email}")
if !isProduction if !isProduction
.form-group.checkbox .form-group.checkbox

View file

@ -5,7 +5,7 @@ block content
p p
span(data-i18n="account.unsubscribe") Unsubscribing for span(data-i18n="account.unsubscribe") Unsubscribing for
span span
strong= email strong= view.email
button.btn.btn-warning#unsubscribe-button(data-i18n="account.unsubscribe_button") Do it button.btn.btn-warning#unsubscribe-button(data-i18n="account.unsubscribe_button") Do it

View file

@ -1,54 +1,80 @@
extends /templates/core/modal-base-flat extends /templates/core/modal-base-flat
// DNT
block modal-header-content block modal-header-content
h3 Administer User h3 Administer User
h4 #{user.get('name') || 'Unnamed'} / #{user.get('email')} h4 #{view.user.get('name') || 'Unnamed'} / #{view.user.get('email')}
span= user.id span= view.user.id
block modal-body-content block modal-body-content
h3.m-t-3 Grant Student Licenses
#prepaid-form.form
if view.state === 'creating-prepaid'
.progress.progress-striped.active
.progress-bar(style="width: 100%")
else if view.state === 'made-prepaid'
.alert.alert-success Licenses created!
else
.form-group
label Number of Licenses
input#seats-input.form-control(type="number", name="maxRedeemers")
.form-group
label Start Date
input.form-control(type="date" name="startDate" value=moment().format('YYYY-MM-DD'))
.form-group
label End Date
input.form-control(type="date" name="endDate" value=moment().add(1, 'year').format('YYYY-MM-DD')))
.form-group
button#add-seats-btn.btn.btn-primary Add Licenses
if view.prepaids.size()
h3.m-t-3 Existing Prepaids
table.table.table-condensed
tr
th ID
th Type
th Start
th End
th Used
for prepaid in view.prepaids.models
tr
td= prepaid.id
td= prepaid.get('type')
td
if prepaid.get('startDate')
= moment(prepaid.get('startDate')).utc().format('lll')
td
if prepaid.get('endDate')
= moment(prepaid.get('endDate')).utc().format('lll')
td #{(prepaid.get('redeemers') || []).length} / #{prepaid.get('maxRedeemers') || 0}
h3 Stripe Benefit h3 Stripe Benefit
.form .form
.form-group .form-group
.radio .radio
label label
input(type="radio" name="stripe-benefit" value="" checked=none) input(type="radio" name="stripe-benefit" value="" checked=view.none)
| None | None
.radio .radio
label label
input(type="radio" name="stripe-benefit" value="free" checked=free) input(type="radio" name="stripe-benefit" value="free" checked=view.free)
| Free | Free
.radio .radio
label label
input(type="radio" name="stripe-benefit" value="free-until" checked=FreeUntil) input(type="radio" name="stripe-benefit" value="free-until" checked=view.freeUntil)
| Free Until | Free Until
input.form-control.spl(type="date" name="stripe-free-until" value=freeUntilDate)#free-until-date input.form-control.spl(type="date" name="stripe-free-until" value=view.freeUntilDate)#free-until-date
.radio .radio
label label
input(type="radio" name="stripe-benefit" value="coupon" checked=coupon) input(type="radio" name="stripe-benefit" value="coupon" checked=view.coupon)
| Coupon | Coupon
select.form-control#coupon-select select.form-control#coupon-select
for couponOption in coupons for coupon in view.coupons.models
option(value=couponOption.id selected=coupon===couponOption.id)= couponOption.format option(value=coupon.id selected=coupon.id===view.currentCouponID)= coupon.formatString()
button#save-changes.btn.btn-primary Save Changes button#save-changes.btn.btn-primary Save Changes
h3 Grant Prepaid for Courses
#prepaid-form.form
if view.state === 'creating-prepaid'
.progress.progress-striped.active
.progress-bar(style="width: 100%")
else if view.state === 'made-prepaid'
.alert.alert-success Prepaid created!
else
.form-group
label Seats
input#seats-input.form-control(type="number")
.form-group
button#add-seats-btn.btn.btn-primary Add Seats
block modal-footer-content
block modal-footer

View file

@ -9,25 +9,25 @@ block content
.row .row
.col-md-5.big-stat.active-classes .col-md-5.big-stat.active-classes
div.description Monthly Active Classes div.description Monthly Active Classes
if activeClasses.length > 0 if view.activeClasses.length > 0
div.count= activeClasses[0].groups[activeClasses[0].groups.length - 1] div.count= view.activeClasses[0].groups[view.activeClasses[0].groups.length - 1]
.col-md-5.big-stat.recurring-revenue .col-md-5.big-stat.recurring-revenue
div.description Monthly Recurring Revenue div.description Monthly Recurring Revenue
if revenue.length > 0 if view.revenue.length > 0
div.count $#{Math.round((revenue[0].groups[revenue[0].groups.length - 1]) / 100)} div.count $#{Math.round((view.revenue[0].groups[view.revenue[0].groups.length - 1]) / 100)}
.col-md-5.big-stat.classroom-active-users .col-md-5.big-stat.classroom-active-users
div.description Classroom Monthly Active Users div.description Classroom Monthly Active Users
if activeUsers.length > 0 if view.activeUsers.length > 0
- var classroomBigMAU = 0; - var classroomBigMAU = 0;
each count, event in activeUsers[0].events each count, event in view.activeUsers[0].events
if event.indexOf('MAU classroom') >= 0 if event.indexOf('MAU classroom') >= 0
- classroomBigMAU += count; - classroomBigMAU += count;
div.count= classroomBigMAU div.count= classroomBigMAU
.col-md-5.big-stat.campaign-active-users .col-md-5.big-stat.campaign-active-users
div.description Campaign Monthly Active Users div.description Campaign Monthly Active Users
if activeUsers.length > 0 if view.activeUsers.length > 0
- var campaignBigMAU = 0; - var campaignBigMAU = 0;
each count, event in activeUsers[0].events each count, event in view.activeUsers[0].events
if event.indexOf('MAU campaign') >= 0 if event.indexOf('MAU campaign') >= 0
- campaignBigMAU += count; - campaignBigMAU += count;
div.count= campaignBigMAU div.count= campaignBigMAU
@ -57,8 +57,8 @@ block content
.tab-pane#tab_active_classes .tab-pane#tab_active_classes
h3 Active Classes 90 days h3 Active Classes 90 days
.small Active class: 12+ students in a classroom, with 6+ who played in last 30 days. Played == 'Started Level' analytics event. .small Active class: 12+ students in a classroom, with 6+ who played in last 30 days. Played == 'Started Level' analytics event.
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set .small Paid student: user.coursePrepaid set and prepaid.properties.trialRequestID NOT set
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set .small Trial student: user.coursePrepaid set and prepaid.properties.trialRequestID set
.small Paid class: at least one paid student in the classroom .small Paid class: at least one paid student in the classroom
.small Trial class: not paid, at least one trial student in classroom .small Trial class: not paid, at least one trial student in classroom
.small Free class: not paid, not trial .small Free class: not paid, not trial
@ -71,9 +71,9 @@ block content
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
th Day th Day
for group in activeClassGroups for group in view.activeClassGroups
th= group.replace('Active classes', '') th= group.replace('Active classes', '')
each activeClass in activeClasses each activeClass in view.activeClasses
tr tr
td= activeClass.day td= activeClass.day
each val in activeClass.groups each val in activeClass.groups
@ -125,9 +125,9 @@ block content
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
th(style='min-width:85px;') Day th(style='min-width:85px;') Day
for group in revenueGroups for group in view.revenueGroups
th= group.replace('DRR ', 'Daily ').replace('MRR ', 'Monthly ') th= group.replace('DRR ', 'Daily ').replace('MRR ', 'Monthly ')
each entry in revenue each entry in view.revenue
tr tr
td= entry.day td= entry.day
each val in entry.groups each val in entry.groups
@ -135,8 +135,8 @@ block content
.tab-pane#tab_classroom .tab-pane#tab_classroom
h3 Classroom Daily Active Users 90 days h3 Classroom Daily Active Users 90 days
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set .small Paid student: user.coursePrepaid set and prepaid.properties.trialRequestID NOT set
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set .small Trial student: user.coursePrepaid set and prepaid.properties.trialRequestID set
.small Free student: not paid, not trial .small Free student: not paid, not trial
.classroom-daily-active-users-chart-90.line-chart-container .classroom-daily-active-users-chart-90.line-chart-container
@ -149,7 +149,7 @@ block content
h3 Classroom Monthly Active Users 365 days h3 Classroom Monthly Active Users 365 days
.classroom-monthly-active-users-chart-365.line-chart-container .classroom-monthly-active-users-chart-365.line-chart-container
h3 Enrollments Issued and Redeemed 90 days h3 Licenses Issued and Redeemed 90 days
.paid-courses-chart.line-chart-container .paid-courses-chart.line-chart-container
#furthest-course #furthest-course
@ -159,8 +159,8 @@ block content
.small Student: member of a course instance (assigned to course) .small Student: member of a course instance (assigned to course)
.small For course instances != Single Player, hourOfCode != true .small For course instances != Single Player, hourOfCode != true
.small Counts are not summed. I.e. a student or teacher only contributes to the count of one course .small Counts are not summed. I.e. a student or teacher only contributes to the count of one course
.small Paid student: user.coursePrepaidID set and prepaid.properties.trialRequestID NOT set .small Paid student: user.coursePrepaid set and prepaid.properties.trialRequestID NOT set
.small Trial student: user.coursePrepaidID set and prepaid.properties.trialRequestID set .small Trial student: user.coursePrepaid set and prepaid.properties.trialRequestID set
.small Free student: not paid, not trial .small Free student: not paid, not trial
.small Paid teacher: at least one paid student in course instance .small Paid teacher: at least one paid student in course instance
.small Trial teacher: at least one trial student in course instance, and no paid students .small Trial teacher: at least one trial student in course instance, and no paid students
@ -265,9 +265,9 @@ block content
div Loading ... div Loading ...
h1 Active Users h1 Active Users
if activeUsers.length > 0 if view.activeUsers.length > 0
- var eventNames = []; - var eventNames = [];
each count, event in activeUsers[0].events each count, event in view.activeUsers[0].events
if event.indexOf('classroom') >= 0 if event.indexOf('classroom') >= 0
- eventNames.push(event) - eventNames.push(event)
- eventNames.sort(function (a, b) {return a.localeCompare(b);}); - eventNames.sort(function (a, b) {return a.localeCompare(b);});
@ -276,28 +276,28 @@ block content
th(style='min-width:85px;') Day th(style='min-width:85px;') Day
each eventName in eventNames each eventName in eventNames
th= eventName th= eventName
each activeUser in activeUsers each activeUser in view.activeUsers
tr tr
td= activeUser.day td= activeUser.day
each eventName in eventNames each eventName in eventNames
td= activeUser.events[eventName] || 0 td= activeUser.events[eventName] || 0
h1#enrollments-table Enrollments h1#enrollments-table Licenses
table.table.table-striped.table-condensed table.table.table-striped.table-condensed
tr tr
th Day th Day
th Paid Enrollments Issued th Paid Licenses Issued
th Paid Enrollments Redeemed th Paid Licenses Redeemed
th Trial Enrollments Issued th Trial Licenses Issued
th Trial Enrollments Redeemed th Trial Licenses Redeemed
each day in enrollmentDays each day in view.enrollmentDays
tr tr
td= day td= day
if dayEnrollmentsMap[day] if view.dayEnrollmentsMap[day]
td= dayEnrollmentsMap[day].paidIssued || 0 td= view.dayEnrollmentsMap[day].paidIssued || 0
td= dayEnrollmentsMap[day].paidRedeemed || 0 td= view.dayEnrollmentsMap[day].paidRedeemed || 0
td= dayEnrollmentsMap[day].trialIssued || 0 td= view.dayEnrollmentsMap[day].trialIssued || 0
td= dayEnrollmentsMap[day].trialRedeemed || 0 td= view.dayEnrollmentsMap[day].trialRedeemed || 0
else else
td 0 td 0
td 0 td 0
@ -320,9 +320,9 @@ block content
.campaign-monthly-active-users-chart-365.line-chart-container .campaign-monthly-active-users-chart-365.line-chart-container
h1 Active Users h1 Active Users
if activeUsers.length > 0 if view.activeUsers.length > 0
- var eventNames = []; - var eventNames = [];
each count, event in activeUsers[0].events each count, event in view.activeUsers[0].events
if event.indexOf('campaign') >= 0 if event.indexOf('campaign') >= 0
- eventNames.push(event) - eventNames.push(event)
- eventNames.sort(function (a, b) {return a.localeCompare(b);}); - eventNames.sort(function (a, b) {return a.localeCompare(b);});
@ -331,7 +331,7 @@ block content
th(style='min-width:85px;') Day th(style='min-width:85px;') Day
each eventName in eventNames each eventName in eventNames
th= eventName th= eventName
each activeUser in activeUsers each activeUser in view.activeUsers
tr tr
td= activeUser.day td= activeUser.day
each eventName in eventNames each eventName in eventNames
@ -345,9 +345,9 @@ block content
.campaign-vs-classroom-monthly-active-users-chart.line-chart-container .campaign-vs-classroom-monthly-active-users-chart.line-chart-container
h1 Active Users h1 Active Users
if activeUsers.length > 0 if view.activeUsers.length > 0
- var eventNames = []; - var eventNames = [];
each count, event in activeUsers[0].events each count, event in view.activeUsers[0].events
- eventNames.push(event) - eventNames.push(event)
- eventNames.sort(function (a, b) { - eventNames.sort(function (a, b) {
- if (a.indexOf('campaign') == b.indexOf('campaign') || a.indexOf('classroom') == b.indexOf('classroom')) { - if (a.indexOf('campaign') == b.indexOf('campaign') || a.indexOf('classroom') == b.indexOf('classroom')) {
@ -365,7 +365,7 @@ block content
th(style='min-width:85px;') Day th(style='min-width:85px;') Day
each eventName in eventNames each eventName in eventNames
th= eventName th= eventName
each activeUser in activeUsers each activeUser in view.activeUsers
tr tr
td= activeUser.day td= activeUser.day
each eventName in eventNames each eventName in eventNames

View file

@ -32,8 +32,8 @@ block content
th(data-i18n="clans.chieftain") Chieftain th(data-i18n="clans.chieftain") Chieftain
th th
tbody tbody
if publicClans.length if view.publicClansArray.length
each clan in publicClans each clan in view.publicClansArray
tr tr
td td
if clan.get('ownerID') === me.id if clan.get('ownerID') === me.id
@ -42,12 +42,12 @@ block content
a(href="/clans/#{clan.id}")= clan.get('name') a(href="/clans/#{clan.id}")= clan.get('name')
td= clan.get('memberCount') td= clan.get('memberCount')
td td
if idNameMap && idNameMap[clan.get('ownerID')] if view.idNameMap && view.idNameMap[clan.get('ownerID')]
a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')] a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
else else
a(href="/user/#{clan.get('ownerID')}") Anoner a(href="/user/#{clan.get('ownerID')}") Anoner
td td
if myClanIDs.indexOf(clan.id) < 0 if view.myClanIDs.indexOf(clan.id) < 0
button.btn.btn-success.join-clan-btn(data-id="#{clan.id}", data-i18n="clans.join_clan") Join Clan button.btn.btn-success.join-clan-btn(data-id="#{clan.id}", data-i18n="clans.join_clan") Join Clan
else if clan.get('ownerID') !== me.id else if clan.get('ownerID') !== me.id
button.btn.btn-xs.btn-warning.leave-clan-btn(data-id="#{clan.id}", data-i18n="clans.leave_clan") Leave Clan button.btn.btn-xs.btn-warning.leave-clan-btn(data-id="#{clan.id}", data-i18n="clans.leave_clan") Leave Clan
@ -62,8 +62,8 @@ block content
th(data-i18n="clans.type") Type th(data-i18n="clans.type") Type
th th
tbody tbody
if myClans.length if view.myClansArray.length
each clan in myClans each clan in view.myClansArray
tr tr
td td
if clan.get('ownerID') === me.id if clan.get('ownerID') === me.id
@ -72,8 +72,8 @@ block content
a(href="/clans/#{clan.id}")= clan.get('name') a(href="/clans/#{clan.id}")= clan.get('name')
td= clan.get('memberCount') td= clan.get('memberCount')
td td
if idNameMap && idNameMap[clan.get('ownerID')] if view.idNameMap && view.idNameMap[clan.get('ownerID')]
a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')] a(href="/user/#{clan.get('ownerID')}")= view.idNameMap[clan.get('ownerID')]
else else
a(href="/user/#{clan.get('ownerID')}") Anoner a(href="/user/#{clan.get('ownerID')}") Anoner
td= clan.get('type') td= clan.get('type')

View file

@ -21,16 +21,16 @@ block modal-body-content
span.spr : span.spr :
select.classroom-select select.classroom-select
each classroom in view.classrooms.models each classroom in view.classrooms.models
option(selected=(classroom.id === view.classroom.id), value=classroom.id) option(selected=(view.classroom ? classroom.id === view.classroom.id : false), value=classroom.id)
= classroom.get('name') = classroom.get('name')
option(selected=(view.classroom.id === 'all-students'), value='all-students' data-i18n='teacher.all_students') option(selected=(!view.classroom), value='' data-i18n='teacher.all_students')
form.form.m-t-3 form.form.m-t-3
span(data-i18n="teacher.enroll_the_following_students") span(data-i18n="teacher.enroll_the_following_students")
span : span :
.well.form-group .well.form-group
- var enrolledUsers = view.users.filter(function(user){ return Boolean(user.get('coursePrepaidID')) }) - var enrolledUsers = view.users.filter(function(user){ return user.isEnrolled() })
- var unenrolledUsers = view.users.filter(function(user){ return !Boolean(user.get('coursePrepaidID')) }) - var unenrolledUsers = view.users.filter(function(user){ return !user.isEnrolled() })
for user in unenrolledUsers for user in unenrolledUsers
- var selected = Boolean(paid || state.get('selectedUsers').get(user.id)) - var selected = Boolean(paid || state.get('selectedUsers').get(user.id))
.checkbox .checkbox
@ -70,6 +70,6 @@ block modal-body-content
span.spl(data-i18n="courses.students1") span.spl(data-i18n="courses.students1")
p p
a#get-more-licenses-btn.btn.btn-lg.btn-primary-alt(href="/teachers/enrollments", data-i18n="courses.get_enrollments") a#get-more-licenses-btn.btn.btn-lg.btn-primary-alt(href="/teachers/licenses", data-i18n="courses.get_enrollments")
block modal-footer-content block modal-footer-content

View file

@ -83,7 +83,7 @@ block content
span.spr : span.spr :
span= playtime span= playtime
- var paidFor = user.get('coursePrepaidID'); - var paidFor = user.isEnrolled();
for courseInstance in view.courseInstances.models for courseInstance in view.courseInstances.models
- var inCourse = _.contains(courseInstance.get('members'), user.id); - var inCourse = _.contains(courseInstance.get('members'), user.id);
if !(inCourse || view.teacherMode) if !(inCourse || view.teacherMode)

View file

@ -31,79 +31,105 @@ block content
a.btn.btn-primary.btn-lg(href="/teachers/update-account") Upgrade to teacher account a.btn.btn-primary.btn-lg(href="/teachers/update-account") Upgrade to teacher account
.container.m-t-5 .container.m-t-5
.pull-right
span.glyphicon.glyphicon-question-sign
=' '
a#how-to-enroll-link(data-i18n="teacher.how_to_enroll")
h3(data-i18n='teacher.enrollments') h3(data-i18n='teacher.enrollments')
h4 h4#enrollments-blurb
span(data-i18n='teacher.enrollments_blurb_1') span(data-i18n='teacher.enrollments_blurb_1')
span 2&ndash;8 span 2&ndash;#{view.state.get('totalCourses')}
span(data-i18n='teacher.enrollments_blurb_2') span(data-i18n='teacher.enrollments_blurb_2')
- var available = view.state.get('prepaidGroups').available
- var pending = view.state.get('prepaidGroups').pending
- var anyPrepaids = available || pending
.row.m-t-3 .row.m-t-3
.col-xs-4 if anyPrepaids
+enrollmentStats #prepaids-col.col-md-9
.col-xs-4 if _.size(available) > 0
+addCredits h5.m-b-1(data-i18n="teacher.available_credits")
.col-xs-3.col-xs-offset-1 .row
+howToEnroll for prepaid in available
+quoteSection .col-sm-4.col-xs-6
+prepaidCard(prepaid)
if _.size(pending) > 0
h5.m-b-1.m-t-3(data-i18n="teacher.pending_credits")
.row
for prepaid in pending
.col-sm-4.col-xs-6
+prepaidCard(prepaid, 'pending-prepaid-card')
#actions-col.col-md-3
+addCredits
+enrollmentStats
else
// no prepaids
.col-sm-6.col-lg-4.col-lg-offset-2
+addCredits
.col-sm-6.col-lg-4
+enrollmentStats
mixin prepaidCard(prepaid, className)
.prepaid-card.bg-navy.text-center.m-b-2.p-a-2(class=className)
h1.m-t-2.m-b-0= prepaid.openSpots()
div(data-i18n="teacher.credits")
hr
em.small-details
.pull-left(data-i18n="teacher.start_date")
.pull-right= moment(prepaid.get('startDate')).utc().format('l')
.clearfix
.pull-left(data-i18n="teacher.end_date")
.pull-right= moment(prepaid.get('endDate')).utc().format('l')
.clearfix
mixin addCredits
.text-center.m-b-3.m-t-3
h5(data-i18n="courses.get_enrollments")
if me.get('enrollmentRequestSent')
#enrollment-request-sent-blurb.small
p(data-i18n="teacher.enroll_request_sent_blurb1")
p(data-i18n="teacher.enroll_request_sent_blurb2")
p(data-i18n="[html]teacher.enroll_request_sent_blurb3")
button#request-sent-btn.btn-lg.btn.btn-forest(disabled=true, data-i18n="teacher.request_sent")
else
p(data-i18n="teacher.num_enrollments_needed")
div.m-t-2
input#students-input.enrollment-count.text-center(value=view.state.get('numberOfStudents') type='number')
strong(data-i18n="teacher.credits")
p.m-y-2(data-i18n="teacher.get_enrollments_blurb")
button#contact-us-btn.btn-lg.btn.btn-forest(data-i18n="contribute.contact_us_url")
mixin enrollmentStats mixin enrollmentStats
h5 h5.text-center.m-t-3.m-b-2(data-i18n='teacher.enrollment_status')
span(data-i18n='teacher.credits_available') table#enrollment-stats-table.table-condensed.table.small-details
span.spr : tr
= view.prepaids.totalAvailable() td
.small-details span(data-i18n='teacher.total_unique_students')
span(data-i18n='teacher.total_unique_students') span.spr :
span.spr : td= view.state.get('totalEnrolled') + view.state.get('totalNotEnrolled')
= view.totalEnrolled + view.totalNotEnrolled tr
.small-details td
span(data-i18n='teacher.total_enrolled_students') span(data-i18n='teacher.total_enrolled_students')
span.spr : span.spr :
= view.totalEnrolled td= view.state.get('totalEnrolled')
tr
h5.small-details.m-t-3 th(data-i18n='teacher.unenrolled_students')
span(data-i18n='teacher.unenrolled_students') th= view.state.get('totalNotEnrolled')
each classroom in view.classrooms.models each classroom in view.classrooms.models
if classroom.get('members').length > 0 && view.classroomNotEnrolledMap && view.classroomNotEnrolledMap[classroom.id] > 0 if classroom.get('members').length > 0 && view.state.get('classroomNotEnrolledMap')[classroom.id] > 0
.small-details tr
span= classroom.get('name') td.classroom-name-td
span.spr : span= classroom.get('name')
span= view.classroomNotEnrolledMap[classroom.id] span.spr :
td= view.state.get('classroomNotEnrolledMap')[classroom.id]
.small-details
span(data-i18n='teacher.total_unenrolled')
span.spr :
= view.totalNotEnrolled
//- .enroll-students.btn.btn-lg.btn-navy
//- span(data-i18n='teacher.enroll_students')
mixin addCredits
.text-center .text-center
h5(data-i18n='teacher.add_enrollment_credits') button#enroll-students-btn.btn.btn-lg.btn-navy
div.m-t-1 span(data-i18n='teacher.enroll_students')
input#students-input.text-center.enrollment-count(value=view.numberOfStudents type='number')
div.m-t-1
if view.state === 'purchasing'
.purchase-now.btn.btn-lg.btn-forest.disabled
span(data-i18n='teacher.purchasing')
else if view.state === 'purchased'
.purchase-now.btn.btn-lg.btn-forest
span(data-i18n='teacher.purchased')
else
.purchase-now.btn.btn-lg.btn-forest
span(data-i18n='teacher.purchase_now')
mixin howToEnroll
.how-to-enroll.small-details
.text-center
b(data-i18n='teacher.how_to_enroll')
ol
li.m-t-1(data-i18n='teacher.how_to_enroll_blurb_1')
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_2')
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_3')
mixin quoteSection
.text-center.m-t-5
h4(data-i18n='teacher.bulk_pricing_blurb')
a.request-quote.btn.btn-lg.btn-navy.m-t-2(href='/teachers/demo')
span(data-i18n='teachers_quote.title')

View file

@ -0,0 +1,44 @@
extends /templates/core/modal-base-flat
block modal-header-content
if view.classroom.loaded
.text-center
h3.modal-title
span(data-i18n="courses.join")
span.spr
span= view.classroom.get('name')
b.small-details
span.spr(data-i18n="courses.instructor")
span= view.classroom.owner.get('name')
block modal-body-content
if view.classroom.loaded
p
span.spr(data-i18n="courses.youve_been_invited_1")
span= view.classroom.get('name')
span.spr(data-i18n="courses.youve_been_invited_2")
span= view.classroom.capitalLanguage
span.spl(data-i18n="courses.youve_been_invited_3")
p
span.spr(data-i18n="courses.by_joining_1")
span= view.classroom.get('name')
span.spr ,
span= view.classroom.owner.get('name')
span.spr
span(data-i18n="courses.by_joining_2")
unless me.get('emailVerified')
div.text-center.m-t-4
div
b.small-details
span(data-i18n="courses.sent_verification")
.small= me.get('email')
.small
span.spr(data-i18n="courses.you_can_edit")
a(href="/account/settings")
span(data-i18n="courses.account_settings")
block modal-footer-content
.text-center
button.join-class-btn.btn.btn-lg.btn-navy(data-i18n="courses.join_class_2")
button.btn.btn-lg(data-dismiss="modal", data-i18n="common.cancel")

View file

@ -1,98 +0,0 @@
extends /templates/base
block content
if view.state === 'purchasing'
p.text-center(data-i18n="buy_gems.purchasing")
.progress.progress-striped.active
.progress-bar(style="width: 100%")
else if view.state === 'purchased'
p
span.spr(data-i18n="courses.thank_you_pref")
span= view.numberOfStudents
span.spl(data-i18n="courses.thank_you_suff")
p.text-center
if view.fromClassroom
a(href="/courses/"+view.fromClassroom, data-i18n="courses.return_to_class")
else
a(href="/teachers/classes", data-i18n="courses.return_to_course_man")
else
h2.text-center(data-i18n="courses.purchase_enrollments")
br
if view.state === 'error'
.alert.alert-danger= view.stateMessage
- var usedEnrollments = view.prepaids.totalRedeemers();
- var totalEnrollments = view.prepaids.totalMaxRedeemers();
- var remainingEnrollments = totalEnrollments - usedEnrollments;
.row
.col-md-4
.col-md-3
strong.uppercase(data-i18n="courses.you_have2")
.col-md-1
strong= remainingEnrollments
br
.row
.col-md-4
.col-md-3
strong.uppercase(data-i18n="courses.students_not_enrolled")
.row
.col-md-4
.col-md-3
each classroom in view.classrooms.models
if classroom.get('members').length > 0
.not-enrolled= classroom.get('name')
.not-enrolled(data-i18n="courses.total_all_classes")
.col-md-1
- var totalNotEnrolled = 0
each classroom in view.classrooms.models
if classroom.get('members').length > 0 && view.classroomNotEnrolledMap
.not-enrolled
strong= view.classroomNotEnrolledMap[classroom.id]
.not-enrolled
strong= view.totalNotEnrolled
br
br
p.text-center
strong(data-i18n="courses.how_many_enrollments")
br
p.text-center
input#students-input.text-center.enrollment-count(
value= view.numberOfStudents
type='number'
)
br
.container-fluid
.row
.col-md-offset-3.col-md-6
span(data-i18n="courses.each_student_access")
br
p.text-center#price-form-group
if view.numberOfStudentsIsValid()
strong
span(data-i18n="account_prepaid.purchase_total")
span.spr : #{view.numberOfStudents}
span(data-i18n="courses.enrollments")
span.spl x $#{(view.pricePerStudent/100).toFixed(2)} = #{view.getPriceString()}
else
strong Invalid number of students
p.text-center
button#purchase-btn.btn.btn-lg.btn-success.uppercase(data-i18n="courses.purchase_now" disabled=me.isAnonymous())
if me.isAnonymous()
// DNT. Temporary redirect until teacher-dashboard is finished
.alert.alert-danger.text-center
h2 You must be signed up to purchase enrollments.
p
a.btn.btn-primary.btn-lg(href="/teachers/signup") Create a teacher account

View file

@ -1,24 +1,24 @@
extends /templates/core/modal-base extends /templates/core/modal-base-flat
block modal-header-content block modal-header-content
.text-center .text-center
h3.modal-title(data-i18n="courses.remove_student1") h1.modal-title(data-i18n="courses.remove_student1")
span.glyphicon.glyphicon-warning-sign.text-danger span.glyphicon.glyphicon-warning-sign.text-danger
h3(data-i18n="courses.are_you_sure") h2(data-i18n="courses.are_you_sure")
block modal-body-content block modal-body-content
p.text-center p.text-center
span(data-i18n="courses.remove_description1") span(data-i18n="courses.remove_description1")
if view.user.get('coursePrepaidID') if view.user.isEnrolled()
span(data-i18n="courses.remove_description2") span(data-i18n="courses.remove_description2")
block modal-footer-content block modal-footer-content
#remove-student-buttons.text-center #remove-student-buttons.text-center
p p
button.btn.btn-lg.btn-success.text-uppercase(data-dismiss="modal", data-i18n="courses.keep_student") button.btn.btn-lg.btn-forest.text-uppercase(data-dismiss="modal", data-i18n="courses.keep_student")
p - OR - p - OR -
p p
button#remove-student-btn.btn.btn-lg.btn-default.text-uppercase(data-i18n="courses.remove_student1") button#remove-student-btn.btn.btn-lg.btn-burgandy.text-uppercase(data-i18n="courses.remove_student1")
#remove-student-progress.text-center.hide #remove-student-progress.text-center.hide
.progress .progress

View file

@ -108,21 +108,28 @@ block content
+copyCodes +copyCodes
+addStudentsButton +addStudentsButton
ul#student-info-tabs.nav.nav-tabs.m-t-5(role='tablist') ul.nav.nav-tabs.m-t-5(role='tablist')
li(class=(state.get('activeTab')==="#students-tab" ? 'active' : '')) - var activeTab = state.get('activeTab');
li(class=(activeTab === "#students-tab" ? 'active' : ''))
a.students-tab-btn(href='#students-tab') a.students-tab-btn(href='#students-tab')
.small-details.text-center(data-i18n='teacher.students') .small-details.text-center(data-i18n='teacher.students')
.tab-spacer .tab-spacer
li(class=(state.get('activeTab')==="#course-progress-tab" ? 'active' : '')) li(class=(activeTab === "#course-progress-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#course-progress-tab') a.course-progress-tab-btn(href='#course-progress-tab')
.small-details.text-center(data-i18n='teacher.course_progress') .small-details.text-center(data-i18n='teacher.course_progress')
.tab-spacer
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#enrollment-status-tab')
.small-details.text-center(data-i18n='teacher.enrollment_status')
.tab-filler .tab-filler
.tab-content .tab-content
if state.get('activeTab')=='#students-tab' if activeTab === '#students-tab'
+studentsTab +studentsTab
else else if activeTab === '#course-progress-tab'
+courseProgressTab +courseProgressTab
else
+enrollmentStatusTab
mixin breadcrumbs mixin breadcrumbs
.breadcrumbs .breadcrumbs
@ -179,8 +186,8 @@ mixin sortButtons
.sort-buttons.small .sort-buttons.small
span(data-i18n='teacher.sort_by') span(data-i18n='teacher.sort_by')
span.spr : span.spr :
button.sort-button.sort-by-name(data-i18n='general.name') button.sort-button.sort-by-name(data-i18n='general.name', value='name')
button.sort-button.sort-by-progress(data-i18n='teacher.progress') button.sort-button.sort-by-progress(data-i18n='teacher.progress', value='progress')
mixin studentRow(student) mixin studentRow(student)
tr.student-row.alternating-background tr.student-row.alternating-background
@ -220,9 +227,13 @@ mixin studentRow(student)
//- td //- td
//- span.view-class-arrow.glyphicon.glyphicon-chevron-right //- span.view-class-arrow.glyphicon.glyphicon-chevron-right
td td
a.remove-student-link.small.center-block.text-center.pull-right.m-r-2(data-student-id=student.id) .pull-right
div.glyphicon.glyphicon-remove a.edit-student-link.small.center-block.text-center.m-r-2(data-student-id=student.id)
div(data-i18n='teacher.remove') div.glyphicon.glyphicon-edit
div(data-i18n='teacher.edit')
a.remove-student-link.small.center-block.text-center.m-r-2(data-student-id=student.id)
div.glyphicon.glyphicon-remove
div(data-i18n='teacher.remove')
mixin enrollStudentButton(student) mixin enrollStudentButton(student)
a.enroll-student-button.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id data-user-id=student.id) a.enroll-student-button.btn.btn-lg.btn-primary(data-classroom-id=view.classroom.id data-user-id=student.id)
@ -365,3 +376,46 @@ mixin bulkAssignControls
span(data-i18n='teacher.assign_to_selected_students') span(data-i18n='teacher.assign_to_selected_students')
button.btn.btn-primary-alt.enroll-selected-students button.btn.btn-primary-alt.enroll-selected-students
span(data-i18n='teacher.enroll_selected_students') span(data-i18n='teacher.enroll_selected_students')
mixin enrollmentStatusTab
// TODO: Have search input in all tabs
//form.form-inline.text-center.m-t-3
// #search-form-group.form-group
// label(for="student-search") Search for student:
// input#student-search.form-control.m-l-1(type="search")
// span.glyphicon.glyphicon-search.form-control-feedback
table.table#enrollment-status-table.table-condensed.m-t-3
thead
// Checkbox code works, but don't need it yet.
//th.checkbox-col.select-all
.checkbox-flat
input(type='checkbox' id='checkbox-all-students')
label.checkmark(for='checkbox-all-students')
th
.sort-buttons.small
span(data-i18n='teacher.sort_by')
span.spr :
button.sort-button.sort-by-name(data-i18n='general.name', value='name')
button.sort-button.sort-by-status(data-i18n='user.status', value='status')
tbody
- var searchTerm = view.state.get('searchTerm');
each student in state.get('students').search(searchTerm)
- var status = student.prepaidStatus()
tr.student-row.alternating-background
//td.checkbox-col.student-checkbox
.checkbox-flat
input(type='checkbox' id='checkbox-student-' + student.id, data-student-id=student.id)
label.checkmark(for='checkbox-student-' + student.id)
td.student-info-col
.student-info
div.student-name= student.broadName()
div.student-email.small-details= student.get('email')
td.status-col
span(data-i18n='user.status')
span.spr :
strong(class= status === 'expired' ? 'text-danger' : '')= view.studentStatusString(student)
td.enroll-col
if status !== 'enrolled'
button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id)

View file

@ -107,7 +107,7 @@ mixin progressDot(classroom, course, index)
- var started = 0; - var started = 0;
if courseInstance if courseInstance
- complete = courseInstance.numCompleted - complete = courseInstance.numCompleted
- started = courseInstance.numStarted - started = courseInstance.started
- dotClass = complete === total ? 'forest' : started ? 'gold' : ''; - dotClass = complete === total ? 'forest' : started ? 'gold' : '';
- var progressDotContext = {total: total, complete: complete}; - var progressDotContext = {total: total, complete: complete};
.progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext)) .progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext))

View file

@ -18,8 +18,8 @@
li(class= path.indexOf('/teachers/courses') === 0 ? 'active' : '') li(class= path.indexOf('/teachers/courses') === 0 ? 'active' : '')
a(href='/teachers/courses') a(href='/teachers/courses')
small.label(data-i18n='teacher.courses') small.label(data-i18n='teacher.courses')
li(class= path.indexOf('/teachers/enrollments') === 0 ? 'active' : '') li(class= path.indexOf('/teachers/licenses') === 0 ? 'active' : '')
a(href='/teachers/enrollments') a(href='/teachers/licenses')
small.label(data-i18n='teacher.enrollments') small.label(data-i18n='teacher.enrollments')
//- li(class= path.indexOf('TODO') === 0 ? 'active' : '') //- li(class= path.indexOf('TODO') === 0 ? 'active' : '')
//- a(href='TODO') //- a(href='TODO')

View file

@ -85,7 +85,7 @@ mixin box
h6(data-i18n="new_home.want_coco") h6(data-i18n="new_home.want_coco")
a.btn.btn-primary.btn-lg.btn-block(href=view.demoRequestURL, data-i18n="new_home.get_started") a.btn.btn-primary.btn-lg.btn-block(href=view.demoRequestURL, data-i18n="new_home.get_started")
else if view.justPlaysCourses() else if me.justPlaysCourses()
div div
a.btn.btn-forest.btn-lg.btn-block(href=view.playURL, data-i18n="courses.continue_playing") a.btn.btn-forest.btn-lg.btn-block(href=view.playURL, data-i18n="courses.continue_playing")
div div

View file

@ -0,0 +1,41 @@
extends /templates/core/modal-base-flat
block modal-header-content
h1
span.spr(data-i18n="teacher.edit_2")
span=view.user.broadName()
block modal-body-content
.text-center
if view.user.get('emailVerified')
p
span(data-i18n="teacher.send_email_to")
p.m-b-3
= view.user.get('email')
if state.get('emailSent')
.send-recovery-email-btn.btn.btn-lg.btn-primary.uppercase.disabled
span(data-i18n="teacher.email_sent")
else
.send-recovery-email-btn.btn.btn-lg.btn-primary.uppercase
span(data-i18n="teacher.send_recovery_email")
else
div.m-b-1
span(data-i18n="teacher.enter_new_password_below")
div.m-b-2.form-group(class=(state.get('errorMessage') ? 'has-error' : ''))
input.new-password-input(placeholder="type a new password here" value=state.get('newPassword'))
div.help-block.error-help-block.m-t-1.small
span=state.get('errorMessage')
if state.get('passwordChanged')
button.change-password-btn.btn.btn-lg.btn-primary.uppercase.disabled
span(data-i18n="teacher.changed")
else
button.change-password-btn.btn.btn-lg.btn-primary.uppercase
span(data-i18n="teacher.change_password")
block modal-footer-content
button.btn.btn-primary(type="button", data-dismiss="modal", aria-hidden="true")
if state.get('passwordChanged') || state.get('emailSent')
span(data-i18n="modal.close")
else
span(data-i18n="modal.cancel")

View file

@ -0,0 +1,12 @@
extends /templates/core/modal-base-flat
block modal-header-content
.text-center
h3(data-i18n='teacher.how_to_enroll')
block modal-body
ol
li.m-t-1(data-i18n='teacher.how_to_enroll_blurb_1')
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_2')
li.m-t-2(data-i18n='teacher.how_to_enroll_blurb_3')

View file

@ -0,0 +1,42 @@
extends /templates/core/modal-base-flat
block modal-header-content
.text-center
h3 Contact Our Classroom Team
block modal-body-content
p Send us a message and our classroom success team will be in touch to help find the best solution for your students' needs!
form
- var sending = view.state.get('sendingState') === 'sending'
- var sent = view.state.get('sendingState') === 'sent';
- var values = view.state.get('formValues');
- var errors = view.state.get('formErrors');
.form-group(class=errors.email ? 'has-error' : '')
label.control-label(for="email" data-i18n="general.email")
+formErrors(errors.email)
input.form-control(name="email", type="email", value=values.email || '', tabindex=1, disabled=sending || sent)
.form-group(class=errors.message ? 'has-error' : '')
label.control-label(for="message" data-i18n="general.message")
+formErrors(errors.message)
textarea.form-control(name="message", tabindex=1 disabled=sending || sent)= values.message
if view.state.get('sendingState') === 'error'
.alert.alert-danger Could not send message.
if sent
.alert.alert-success Message sent!
.text-right
button#submit-btn.btn.btn-navy.btn-lg(type='submit' disabled=sending || sent) Submit
block modal-footer
mixin formErrors(errors)
if _.isString(errors)
- errors = [errors]
if _.size(errors)
.help-block
for error in errors
div= error

View file

@ -1,3 +1,5 @@
#demo-area
h2#test-h2 Testing Page h2#test-h2 Testing Page
ol.breadcrumb ol.breadcrumb
@ -10,7 +12,6 @@ ol.breadcrumb
.row .row
.col-md-8 .col-md-8
#test-wrapper.well #test-wrapper.well
#demo-area
#testing-area #testing-area
.col-md-4.hidden-sm.hidden-xs .col-md-4.hidden-sm.hidden-xs
@ -31,4 +32,4 @@ ol.breadcrumb
span.spl= child.name span.spl= child.name
if child.type == 'folder' if child.type == 'folder'
strong (#{child.size}) strong (#{child.size})

View file

@ -0,0 +1,37 @@
extends /templates/base-flat
block content
.container.text-center
if state.get('verifyStatus') === "pending"
span(data-i18n="account.verifying_email")
| Verifying your email address...
else if state.get('verifyStatus') === "success"
.alert.alert-success.center-block
.glyphicon.glyphicon-ok-circle.m-r-1
span(data-i18n="account.successfully_verified")
| You've successfully verified your email address!
if view.userID !== me.id
a.login-button.btn.btn-navy.btn-lg(data-i18n="login.log_in")
else if view.user.isTeacher()
a.btn.btn-lg.btn-forest(href="/teachers/classes")
span(data-i18n="new_home.goto_classes")
else if me.justPlaysCourses()
div.m-b-1
a.btn.btn-forest.btn-lg(href="/courses", data-i18n="courses.continue_playing")
div
a.btn.btn-primary.btn-lg.play-btn(href="/courses", data-i18n="new_home.view_progress")
else
div.m-b-1
a.btn.btn-forest.btn-lg.play-btn(href="/play", data-i18n="courses.continue_playing")
div
a.btn.btn-primary.btn-lg(href="/user/#{me.getSlugOrID()}", data-i18n="new_home.view_profile")
else if state.get('verifyStatus') === "error"
.alert.alert-danger.center-block
.glyphicon.glyphicon-remove-circle.m-r-1
span(data-i18n="account.verify_error")
| Something went wrong when verifying your email :(
else
div
| This really shouldn't happen
div
= state.get('verifyStatus')

View file

@ -44,12 +44,12 @@ module.exports = class NewHomeView extends RootView
@supermodel.loadCollection(@trialRequests) @supermodel.loadCollection(@trialRequests)
isHourOfCodeWeek = false # Temporary: default to /hoc flow during the main event week isHourOfCodeWeek = false # Temporary: default to /hoc flow during the main event week
if isHourOfCodeWeek and (@isNewPlayer() or (@justPlaysCourses() and me.isAnonymous())) if isHourOfCodeWeek and (@isNewPlayer() or (me.justPlaysCourses() and me.isAnonymous()))
# Go/return straight to playing single-player HoC course on Play click # Go/return straight to playing single-player HoC course on Play click
@playURL = '/hoc?go=true' @playURL = '/hoc?go=true'
@alternatePlayURL = '/play' @alternatePlayURL = '/play'
@alternatePlayText = 'home.play_campaign_version' @alternatePlayText = 'home.play_campaign_version'
else if @justPlaysCourses() else if me.justPlaysCourses()
# Save players who might be in a classroom from getting into the campaign # Save players who might be in a classroom from getting into the campaign
@playURL = '/courses' @playURL = '/courses'
@alternatePlayURL = '/play' @alternatePlayURL = '/play'
@ -123,11 +123,6 @@ module.exports = class NewHomeView extends RootView
$(@).find('.course-duration .unit').text($.i18n.t(if duration is '1' then 'units.hour' else 'units.hours')) $(@).find('.course-duration .unit').text($.i18n.t(if duration is '1' then 'units.hour' else 'units.hours'))
@$el.find('#semester-duration').text levels[level].total @$el.find('#semester-duration').text levels[level].total
justPlaysCourses: ->
# This heuristic could be better, but currently we don't add to me.get('courseInstances') for single-player anonymous intro courses, so they have to beat a level without choosing a hero.
return true if me.get('role') is 'student'
return me.get('stats')?.gamesCompleted and not me.get('heroConfig')
isNewPlayer: -> isNewPlayer: ->
not me.get('stats')?.gamesCompleted and not me.get('heroConfig') not me.get('stats')?.gamesCompleted and not me.get('heroConfig')

View file

@ -60,14 +60,10 @@ module.exports = TestView = class TestView extends RootView
@specFiles = (f for f in @specFiles when _.string.startsWith f, prefix) @specFiles = (f for f in @specFiles when _.string.startsWith f, prefix)
@runTests: (specFiles, demosOn=false) -> @runTests: (specFiles, demosOn=false) ->
application.testing = true
specFiles ?= @getAllSpecFiles() specFiles ?= @getAllSpecFiles()
if demosOn if demosOn
jasmine.demoEl = ($el) -> jasmine.demoEl = ($el) ->
$el.css({
'border': '2px solid black'
'background': 'white'
'padding': '20px'
})
$('#demo-area').append($el) $('#demo-area').append($el)
jasmine.demoModal = _.once (modal) -> jasmine.demoModal = _.once (modal) ->
currentView.openModalView(modal) currentView.openModalView(modal)

View file

@ -1,6 +1,7 @@
RootView = require 'views/core/RootView' RootView = require 'views/core/RootView'
template = require 'templates/account/account-settings-root-view' template = require 'templates/account/account-settings-root-view'
AccountSettingsView = require './AccountSettingsView' AccountSettingsView = require './AccountSettingsView'
CreateAccountModal = require 'views/core/CreateAccountModal'
module.exports = class AccountSettingsRootView extends RootView module.exports = class AccountSettingsRootView extends RootView
id: "account-settings-root-view" id: "account-settings-root-view"
@ -21,6 +22,9 @@ module.exports = class AccountSettingsRootView extends RootView
@listenTo @accountSettingsView, 'save-user-success', @onUserSaveSuccess @listenTo @accountSettingsView, 'save-user-success', @onUserSaveSuccess
@listenTo @accountSettingsView, 'save-user-error', @onUserSaveError @listenTo @accountSettingsView, 'save-user-error', @onUserSaveError
afterInsert: ->
@openModalView new CreateAccountModal() if me.get('anonymous')
onInputChanged: -> onInputChanged: ->
@$el.find('#save-button') @$el.find('#save-button')
.text($.i18n.t('common.save', defaultValue: 'Save')) .text($.i18n.t('common.save', defaultValue: 'Save'))
@ -45,4 +49,3 @@ module.exports = class AccountSettingsRootView extends RootView
.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')) .text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving'))
.removeClass('btn-success') .removeClass('btn-success')
.addClass('btn-danger', 500) .addClass('btn-danger', 500)

View file

@ -3,7 +3,6 @@ template = require 'templates/account/account-settings-view'
{me} = require 'core/auth' {me} = require 'core/auth'
forms = require 'core/forms' forms = require 'core/forms'
User = require 'models/User' User = require 'models/User'
CreateAccountModal = require 'views/core/CreateAccountModal'
ConfirmModal = require 'views/editor/modal/ConfirmModal' ConfirmModal = require 'views/editor/modal/ConfirmModal'
{logoutUser, me} = require('core/auth') {logoutUser, me} = require('core/auth')
@ -19,16 +18,13 @@ module.exports = class AccountSettingsView extends CocoView
'click #profile-photo-panel-body': 'onClickProfilePhotoPanelBody' 'click #profile-photo-panel-body': 'onClickProfilePhotoPanelBody'
'click #delete-account-btn': 'onClickDeleteAccountButton' 'click #delete-account-btn': 'onClickDeleteAccountButton'
'click #reset-progress-btn': 'onClickResetProgressButton' 'click #reset-progress-btn': 'onClickResetProgressButton'
'click .resend-verification-email': 'onClickResendVerificationEmail'
constructor: (options) -> constructor: (options) ->
super options super options
require('core/services/filepicker')() unless window.application.isIPadApp # Initialize if needed require('core/services/filepicker')() unless window.application.isIPadApp # Initialize if needed
@uploadFilePath = "db/user/#{me.id}" @uploadFilePath = "db/user/#{me.id}"
afterInsert: ->
super()
@openModalView new CreateAccountModal() if me.get('anonymous')
getEmailSubsDict: -> getEmailSubsDict: ->
subs = {} subs = {}
return subs unless me return subs unless me
@ -83,6 +79,12 @@ module.exports = class AccountSettingsView extends CocoView
confirmModal.on 'confirm', @resetProgress confirmModal.on 'confirm', @resetProgress
@openModalView confirmModal @openModalView confirmModal
onClickResendVerificationEmail: (e) ->
$.post me.getRequestVerificationEmailURL(), ->
link = $(e.currentTarget)
link.find('.resend-text').addClass('hide')
link.find('.sent-text').removeClass('hide')
validateCredentialsForDestruction: ($form, onSuccess) -> validateCredentialsForDestruction: ($form, onSuccess) ->
forms.clearFormAlerts($form) forms.clearFormAlerts($form)
enteredEmail = $form.find('input[type="email"]').val() enteredEmail = $form.find('input[type="email"]').val()

View file

@ -6,14 +6,12 @@ module.exports = class UnsubscribeView extends RootView
id: 'unsubscribe-view' id: 'unsubscribe-view'
template: template template: template
initialize: ->
@email = @getQueryVariable 'email'
events: events:
'click #unsubscribe-button': 'onUnsubscribeButtonClicked' 'click #unsubscribe-button': 'onUnsubscribeButtonClicked'
getRenderData: ->
context = super()
context.email = @getQueryVariable 'email'
context
onUnsubscribeButtonClicked: -> onUnsubscribeButtonClicked: ->
@$el.find('#unsubscribe-button').hide() @$el.find('#unsubscribe-button').hide()
@$el.find('.progress').show() @$el.find('.progress').show()

View file

@ -2,47 +2,37 @@ ModalView = require 'views/core/ModalView'
template = require 'templates/admin/administer-user-modal' template = require 'templates/admin/administer-user-modal'
User = require 'models/User' User = require 'models/User'
Prepaid = require 'models/Prepaid' Prepaid = require 'models/Prepaid'
StripeCoupons = require 'collections/StripeCoupons'
forms = require 'core/forms'
Prepaids = require 'collections/Prepaids'
module.exports = class AdministerUserModal extends ModalView module.exports = class AdministerUserModal extends ModalView
id: "administer-user-modal" id: 'administer-user-modal'
template: template template: template
events: events:
'click #save-changes': 'onSaveChanges' 'click #save-changes': 'onClickSaveChanges'
'click #add-seats-btn': 'onClickAddSeatsButton' 'click #add-seats-btn': 'onClickAddSeatsButton'
constructor: (options, @userHandle) -> initialize: (options, @userHandle) ->
super(options) @user = new User({_id:@userHandle})
@user = @supermodel.loadModel(new User({_id:@userHandle}), {cache: false}).model @supermodel.trackRequest @user.fetch({cache: false})
options = {cache: false, url: '/stripe/coupons'} @coupons = new StripeCoupons()
options.success = (@coupons) => @supermodel.trackRequest @coupons.fetch({cache: false})
@couponsResource = @supermodel.addRequestResource('coupon', options) @prepaids = new Prepaids()
@couponsResource.load() @supermodel.trackRequest @prepaids.fetchByCreator(@userHandle)
getRenderData: -> onLoaded: ->
c = super() # TODO: Figure out a better way to expose this info, perhaps User methods?
stripe = @user.get('stripe') or {} stripe = @user.get('stripe') or {}
c.free = stripe.free is true @free = stripe.free is true
c.freeUntil = _.isString(stripe.free) @freeUntil = _.isString(stripe.free)
c.freeUntilDate = if c.freeUntil then stripe.free else new Date().toISOString()[...10] @freeUntilDate = if @freeUntil then stripe.free else new Date().toISOString()[...10]
c.coupon = stripe.couponID @currentCouponID = stripe.couponID
c.coupons = @coupons or [] @none = not (@free or @freeUntil or @coupon)
for coupon in c.coupons super()
bits = [coupon.id]
if coupon.percent_off onClickSaveChanges: ->
bits.push "(#{coupon.percent_off}% off)"
else if coupon.amount_off
bits.push "($#{coupon.amount_off} off)"
if coupon.duration
bits.push "(duration: #{coupon.duration})"
if coupon.redeem_by
bits.push "(redeem by: #{moment(coupon.redeem_by).format('lll')}"
coupon.format = bits.join(' ')
c.none = not (c.free or c.freeUntil or c.coupon)
c.user = @user
c
onSaveChanges: ->
stripe = _.clone(@user.get('stripe') or {}) stripe = _.clone(@user.get('stripe') or {})
delete stripe.free delete stripe.free
delete stripe.couponID delete stripe.couponID
@ -61,15 +51,20 @@ module.exports = class AdministerUserModal extends ModalView
@user.patch(options) @user.patch(options)
onClickAddSeatsButton: -> onClickAddSeatsButton: ->
maxRedeemers = parseInt(@$('#seats-input').val()) attrs = forms.formToObject(@$('#prepaid-form'))
return unless maxRedeemers and maxRedeemers > 0 attrs.maxRedeemers = parseInt(attrs.maxRedeemers)
prepaid = new Prepaid({ return unless _.all(_.values(attrs))
maxRedeemers: maxRedeemers return unless attrs.maxRedeemers > 0
return unless attrs.endDate and attrs.startDate and attrs.endDate > attrs.startDate
attrs.startDate = new Date(attrs.startDate).toISOString()
attrs.endDate = new Date(attrs.endDate).toISOString()
_.extend(attrs, {
type: 'course' type: 'course'
creator: @user.id creator: @user.id
properties: properties:
adminAdded: me.id adminAdded: me.id
}) })
prepaid = new Prepaid(attrs)
prepaid.save() prepaid.save()
@state = 'creating-prepaid' @state = 'creating-prepaid'
@renderSelectors('#prepaid-form') @renderSelectors('#prepaid-form')

View file

@ -16,21 +16,16 @@ module.exports = class AnalyticsView extends RootView
lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan'] lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
minSchoolCount: 20 minSchoolCount: 20
constructor: (options) -> initialize: ->
super options @activeClasses = []
@activeClassGroups = {}
@activeUsers = []
@revenue = []
@revenueGroups = {}
@dayEnrollmentsMap = {}
@enrollmentDays = []
@loadData() @loadData()
getRenderData: ->
context = super()
context.activeClasses = @activeClasses ? []
context.activeClassGroups = @activeClassGroups ? {}
context.activeUsers = @activeUsers ? []
context.revenue = @revenue ? []
context.revenueGroups = @revenueGroups ? {}
context.dayEnrollmentsMap = @dayEnrollmentsMap ? {}
context.enrollmentDays = @enrollmentDays ? []
context
afterRender: -> afterRender: ->
super() super()
@createLineCharts() @createLineCharts()
@ -295,7 +290,7 @@ module.exports = class AnalyticsView extends RootView
prepaidUserMap = {} prepaidUserMap = {}
for user in data.students for user in data.students
continue unless studentPaidStatusMap[user._id] continue unless studentPaidStatusMap[user._id]
if prepaidID = user.coursePrepaidID if prepaidID = user.coursePrepaidID or user.coursePrepaid?._id
studentPaidStatusMap[user._id] = 'paid' studentPaidStatusMap[user._id] = 'paid'
prepaidUserMap[prepaidID] ?= [] prepaidUserMap[prepaidID] ?= []
prepaidUserMap[prepaidID].push(user._id) prepaidUserMap[prepaidID].push(user._id)

View file

@ -13,6 +13,7 @@ SubscribeModal = require 'views/core/SubscribeModal'
module.exports = class ClansView extends RootView module.exports = class ClansView extends RootView
id: 'clans-view' id: 'clans-view'
template: template template: template
events: events:
'click .create-clan-btn': 'onClickCreateClan' 'click .create-clan-btn': 'onClickCreateClan'
@ -20,28 +21,25 @@ module.exports = class ClansView extends RootView
'click .leave-clan-btn': 'onLeaveClan' 'click .leave-clan-btn': 'onLeaveClan'
'click .private-clan-checkbox': 'onClickPrivateCheckbox' 'click .private-clan-checkbox': 'onClickPrivateCheckbox'
constructor: (options) -> initialize: ->
super options @publicClansArray = []
@initData() @myClansArray = []
@idNameMap = {}
@loadData()
destroy: -> destroy: ->
@stopListening?() @stopListening?()
getRenderData: ->
context = super()
context.idNameMap = @idNameMap
context.publicClans = _.filter(@publicClans.models, (clan) -> clan.get('type') is 'public')
context.myClans = @myClans.models
context.myClanIDs = me.get('clans') ? []
context
afterRender: -> afterRender: ->
super() super()
@setupPrivateInfoPopover() @setupPrivateInfoPopover()
initData: -> onLoaded: ->
@idNameMap = {} super()
@publicClansArray = _.filter(@publicClans.models, (clan) -> clan.get('type') is 'public')
@myClansArray = @myClans.models
loadData: ->
sortClanList = (a, b) -> sortClanList = (a, b) ->
if a.get('memberCount') isnt b.get('memberCount') if a.get('memberCount') isnt b.get('memberCount')
if a.get('memberCount') < b.get('memberCount') then 1 else -1 if a.get('memberCount') < b.get('memberCount') then 1 else -1
@ -52,12 +50,15 @@ module.exports = class ClansView extends RootView
@refreshNames @publicClans.models @refreshNames @publicClans.models
@render?() @render?()
@supermodel.loadCollection(@publicClans, 'public_clans', {cache: false}) @supermodel.loadCollection(@publicClans, 'public_clans', {cache: false})
@myClans = new CocoCollection([], { url: "/db/user/#{me.id}/clans", model: Clan, comparator: sortClanList }) @myClans = new CocoCollection([], { url: "/db/user/#{me.id}/clans", model: Clan, comparator: sortClanList })
@listenTo @myClans, 'sync', => @listenTo @myClans, 'sync', =>
@refreshNames @myClans.models @refreshNames @myClans.models
@render?() @render?()
@supermodel.loadCollection(@myClans, 'my_clans', {cache: false}) @supermodel.loadCollection(@myClans, 'my_clans', {cache: false})
@listenTo me, 'sync', => @render?() @listenTo me, 'sync', => @render?()
@myClanIDs = me.get('clans') ? []
refreshNames: (clans) -> refreshNames: (clans) ->
clanIDs = _.filter(clans, (clan) -> clan.get('type') is 'public') clanIDs = _.filter(clans, (clan) -> clan.get('type') is 'public')

View file

@ -61,7 +61,7 @@ module.exports = class CreateAccountModal extends ModalView
error = false error = false
birthday = new Date Date.UTC attrs.birthdayYear, attrs.birthdayMonth - 1, attrs.birthdayDay birthday = new Date Date.UTC attrs.birthdayYear, attrs.birthdayMonth - 1, attrs.birthdayDay
if @classCode if @classCode
#PASS attrs.role = 'student'
else if isNaN(birthday.getTime()) else if isNaN(birthday.getTime())
forms.setErrorToProperty @$el, 'birthdayDay', 'Required' forms.setErrorToProperty @$el, 'birthdayDay', 'Required'
error = true error = true

View file

@ -16,9 +16,11 @@ module.exports = class ActivateLicensesModal extends ModalView
'change input[type="checkbox"][name="user"]': 'updateSelectedStudents' 'change input[type="checkbox"][name="user"]': 'updateSelectedStudents'
'change select.classroom-select': 'replaceStudentList' 'change select.classroom-select': 'replaceStudentList'
'submit form': 'onSubmitForm' 'submit form': 'onSubmitForm'
'click #get-more-licenses-btn': 'onClickGetMoreLicensesButton'
getInitialState: (options) -> getInitialState: (options) ->
selectedUserModels = _.filter(options.selectedUsers.models, (user) -> not user.isEnrolled()) selectedUsers = options.selectedUsers or options.users
selectedUserModels = _.filter(selectedUsers.models, (user) -> not user.isEnrolled())
{ {
selectedUsers: new Users(selectedUserModels) selectedUsers: new Users(selectedUserModels)
visibleSelectedUsers: new Users(selectedUserModels) visibleSelectedUsers: new Users(selectedUserModels)
@ -31,11 +33,10 @@ module.exports = class ActivateLicensesModal extends ModalView
@users = options.users.clone() @users = options.users.clone()
@users.comparator = (user) -> user.broadName().toLowerCase() @users.comparator = (user) -> user.broadName().toLowerCase()
@prepaids = new Prepaids() @prepaids = new Prepaids()
@prepaids.comparator = '_id' @prepaids.comparator = 'endDate' # use prepaids in order of expiration
@prepaids.fetchByCreator(me.id) @supermodel.trackRequest @prepaids.fetchByCreator(me.id)
@supermodel.trackCollection(@prepaids)
@classrooms = new Classrooms() @classrooms = new Classrooms()
@classrooms.fetchMine({ @supermodel.trackRequest @classrooms.fetchMine({
data: {archived: false} data: {archived: false}
success: => success: =>
@classrooms.each (classroom) => @classrooms.each (classroom) =>
@ -43,7 +44,6 @@ module.exports = class ActivateLicensesModal extends ModalView
jqxhrs = classroom.users.fetchForClassroom(classroom, { removeDeleted: true }) jqxhrs = classroom.users.fetchForClassroom(classroom, { removeDeleted: true })
@supermodel.trackRequests(jqxhrs) @supermodel.trackRequests(jqxhrs)
}) })
@supermodel.trackCollection(@classrooms)
@listenTo @state, 'change', @render @listenTo @state, 'change', @render
@listenTo @state.get('selectedUsers'), 'change add remove reset', -> @listenTo @state.get('selectedUsers'), 'change add remove reset', ->
@ -56,6 +56,10 @@ module.exports = class ActivateLicensesModal extends ModalView
@state.set { @state.set {
unusedEnrollments: @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() unusedEnrollments: @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers()
} }
onLoaded: ->
@prepaids.reset(@prepaids.filter((prepaid) -> prepaid.status() is 'available'))
super()
afterRender: -> afterRender: ->
super() super()
@ -73,8 +77,7 @@ module.exports = class ActivateLicensesModal extends ModalView
replaceStudentList: (e) -> replaceStudentList: (e) ->
selectedClassroomID = $(e.currentTarget).val() selectedClassroomID = $(e.currentTarget).val()
@classroom = @classrooms.get(selectedClassroomID) @classroom = @classrooms.get(selectedClassroomID)
if selectedClassroomID is 'all-students' if not @classroom
@classroom = new Classroom({ _id: 'all-students', name: 'All Students' }) # TODO: This is a horrible hack so the select shows the right option!
users = _.uniq _.flatten @classrooms.map (classroom) -> classroom.users.models users = _.uniq _.flatten @classrooms.map (classroom) -> classroom.users.models
@users.reset(users) @users.reset(users)
@users.sort() @users.sort()
@ -96,27 +99,21 @@ module.exports = class ActivateLicensesModal extends ModalView
return return
user = usersToRedeem.first() user = usersToRedeem.first()
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots() > 0) prepaid = @prepaids.find((prepaid) -> prepaid.status() is 'available')
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots() > 0) unless prepaid prepaid.redeem(user, {
$.ajax({ success: (prepaid) =>
method: 'POST' user.set('coursePrepaid', prepaid.pick('_id', 'startDate', 'endDate'))
url: _.result(prepaid, 'url') + '/redeemers'
data: { userID: user.id }
context: @
success: (prepaid) ->
user.set('coursePrepaidID', prepaid._id)
usersToRedeem.remove(user) usersToRedeem.remove(user)
# pct = 100 * (usersToRedeem.originalSize - usersToRedeem.size() / usersToRedeem.originalSize) # pct = 100 * (usersToRedeem.originalSize - usersToRedeem.size() / usersToRedeem.originalSize)
# @$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%") # @$('#progress-area .progress-bar').css('width', "#{pct.toFixed(1)}%")
application.tracker?.trackEvent 'Enroll modal finished enroll student', category: 'Courses', userID: user.id application.tracker?.trackEvent 'Enroll modal finished enroll student', category: 'Courses', userID: user.id
@redeemUsers(usersToRedeem) @redeemUsers(usersToRedeem)
error: (jqxhr, textStatus, errorThrown) -> error: (prepaid, jqxhr) =>
if jqxhr.status is 402 @state.set { error: jqxhr.responseJSON.message }
message = arguments[2]
else
message = "#{jqxhr.status}: #{jqxhr.responseText}"
@state.set { error: message } # TODO: Test this! ("should" never happen. Only on server responding with an error.)
}) })
finishRedeemUsers: -> finishRedeemUsers: ->
@trigger 'redeem-users', @state.get('selectedUsers') @trigger 'redeem-users', @state.get('selectedUsers')
onClickGetMoreLicensesButton: ->
@hide?() # In case this is opened in /teachers/licenses itself, otherwise the button does nothing

View file

@ -118,8 +118,7 @@ module.exports = class ClassroomView extends RootView
userID = $(e.target).closest('.btn').data('user-id') userID = $(e.target).closest('.btn').data('user-id')
if @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() > 0 if @prepaids.totalMaxRedeemers() - @prepaids.totalRedeemers() > 0
# Have an unused enrollment, enroll student immediately instead of opening the enroll modal # Have an unused enrollment, enroll student immediately instead of opening the enroll modal
prepaid = @prepaids.find((prepaid) -> prepaid.get('properties')?.endDate? and prepaid.openSpots() > 0) prepaid = @prepaids.find((prepaid) -> prepaid.status() is 'available')
prepaid = @prepaids.find((prepaid) -> prepaid.openSpots() > 0) unless prepaid
$.ajax({ $.ajax({
method: 'POST' method: 'POST'
url: _.result(prepaid, 'url') + '/redeemers' url: _.result(prepaid, 'url') + '/redeemers'
@ -182,7 +181,7 @@ module.exports = class ClassroomView extends RootView
stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' # ' stats.averageLevelsComplete = if @users.size() then (_.size(completeSessions) / @users.size()).toFixed(1) else 'N/A' # '
stats.totalLevelsComplete = _.size(completeSessions) stats.totalLevelsComplete = _.size(completeSessions)
enrolledUsers = @users.filter (user) -> user.get('coursePrepaidID') enrolledUsers = @users.filter (user) -> user.isEnrolled()
stats.enrolledUsers = _.size(enrolledUsers) stats.enrolledUsers = _.size(enrolledUsers)
return stats return stats

View file

@ -5,6 +5,7 @@ AuthModal = require 'views/core/AuthModal'
CreateAccountModal = require 'views/core/CreateAccountModal' CreateAccountModal = require 'views/core/CreateAccountModal'
ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal' ChangeCourseLanguageModal = require 'views/courses/ChangeCourseLanguageModal'
ChooseLanguageModal = require 'views/courses/ChooseLanguageModal' ChooseLanguageModal = require 'views/courses/ChooseLanguageModal'
JoinClassModal = require 'views/courses/JoinClassModal'
CourseInstance = require 'models/CourseInstance' CourseInstance = require 'models/CourseInstance'
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course' Course = require 'models/Course'
@ -91,10 +92,19 @@ module.exports = class CoursesView extends RootView
@renderSelectors '#join-class-form' @renderSelectors '#join-class-form'
return return
@renderSelectors '#join-class-form' @renderSelectors '#join-class-form'
newClassroom = new Classroom() if me.get('emailVerified') or me.isStudent()
newClassroom.joinWithCode(@classCode) newClassroom = new Classroom()
newClassroom.on 'sync', @onJoinClassroomSuccess, @ jqxhr = newClassroom.joinWithCode(@classCode)
newClassroom.on 'error', @onJoinClassroomError, @ @listenTo newClassroom, 'join:success', -> @onJoinClassroomSuccess(newClassroom)
@listenTo newClassroom, 'join:error', -> @onJoinClassroomError(newClassroom, jqxhr)
else
modal = new JoinClassModal({ @classCode })
@openModalView modal
@listenTo modal, 'join:success', @onJoinClassroomSuccess
@listenTo modal, 'join:error', @onJoinClassroomError
@listenTo modal, 'hidden', ->
@state = null
@renderSelectors '#join-class-form'
onJoinClassroomError: (classroom, jqxhr, options) -> onJoinClassroomError: (classroom, jqxhr, options) ->
@state = null @state = null
@ -108,6 +118,7 @@ module.exports = class CoursesView extends RootView
@renderSelectors '#join-class-form' @renderSelectors '#join-class-form'
onJoinClassroomSuccess: (newClassroom, data, options) -> onJoinClassroomSuccess: (newClassroom, data, options) ->
@state = null
application.tracker?.trackEvent 'Joined classroom', { application.tracker?.trackEvent 'Joined classroom', {
category: 'Courses' category: 'Courses'
classCode: @classCode classCode: @classCode

View file

@ -1,90 +1,83 @@
app = require 'core/application'
CreateAccountModal = require 'views/core/CreateAccountModal'
Classroom = require 'models/Classroom'
Classrooms = require 'collections/Classrooms'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
Prepaids = require 'collections/Prepaids'
RootView = require 'views/core/RootView' RootView = require 'views/core/RootView'
stripeHandler = require 'core/services/stripe' Classrooms = require 'collections/Classrooms'
State = require 'models/State'
Prepaids = require 'collections/Prepaids'
template = require 'templates/courses/enrollments-view' template = require 'templates/courses/enrollments-view'
User = require 'models/User'
Users = require 'collections/Users' Users = require 'collections/Users'
utils = require 'core/utils' Courses = require 'collections/Courses'
Products = require 'collections/Products' HowToEnrollModal = require 'views/teachers/HowToEnrollModal'
TeachersContactModal = require 'views/teachers/TeachersContactModal'
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
module.exports = class EnrollmentsView extends RootView module.exports = class EnrollmentsView extends RootView
id: 'enrollments-view' id: 'enrollments-view'
template: template template: template
numberOfStudents: 15
pricePerStudent: 0
initialize: (options) ->
@ownedClassrooms = new Classrooms()
@ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackCollection(@ownedClassrooms)
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
@fromClassroom = utils.getQueryVariable('from-classroom')
@members = new Users()
# @listenTo @members, 'sync add remove', @calculateEnrollmentStats
@classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom })
@classrooms.comparator = '_id'
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
@prepaids = new Prepaids()
@prepaids.comparator = '_id'
@prepaids.fetchByCreator(me.id)
@supermodel.loadCollection(@prepaids, 'prepaids')
@products = new Products()
@supermodel.loadCollection(@products, 'products')
super(options)
events: events:
'input #students-input': 'onInputStudentsInput' 'input #students-input': 'onInputStudentsInput'
'click .purchase-now': 'onClickPurchaseButton' 'click #enroll-students-btn': 'onClickEnrollStudentsButton'
# 'click .enroll-students': 'onClickEnrollStudents' 'click #how-to-enroll-link': 'onClickHowToEnrollLink'
'click #contact-us-btn': 'onClickContactUsButton'
onLoaded: -> initialize: ->
@calculateEnrollmentStats() @state = new State({
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount') totalEnrolled: 0
super() totalNotEnrolled: 0
classroomNotEnrolledMap: {}
classroomEnrolledMap: {}
numberOfStudents: 15
totalCourses: 0
prepaidGroups: {
'available': []
'pending': []
}
})
getPriceString: -> '$' + (@getPrice()/100).toFixed(2) @courses = new Courses()
getPrice: -> @pricePerStudent * @numberOfStudents @supermodel.trackRequest @courses.fetch({data: { project: 'free' }})
@members = new Users()
@classrooms = new Classrooms()
@classrooms.comparator = '_id'
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
@supermodel.trackRequest @classrooms.fetchMine()
@prepaids = new Prepaids()
@prepaids.comparator = '_id'
@supermodel.trackRequest @prepaids.fetchByCreator(me.id)
@debouncedRender = _.debounce @render, 0
@listenTo @prepaids, 'sync', @updatePrepaidGroups
@listenTo(@state, 'all', @debouncedRender)
@listenTo(me, 'change:enrollmentRequestSent', @debouncedRender)
onceClassroomsSync: -> onceClassroomsSync: ->
for classroom in @classrooms.models for classroom in @classrooms.models
@supermodel.trackRequests @members.fetchForClassroom(classroom, {remove: false, removeDeleted: true}) @supermodel.trackRequests @members.fetchForClassroom(classroom, {remove: false, removeDeleted: true})
onLoaded: ->
@calculateEnrollmentStats()
@state.set('totalCourses', @courses.size())
super()
updatePrepaidGroups: ->
@state.set('prepaidGroups', @prepaids.groupBy((p) -> p.status()))
calculateEnrollmentStats: -> calculateEnrollmentStats: ->
@removeDeletedStudents() @removeDeletedStudents()
@memberEnrolledMap = {}
for user in @members.models
@memberEnrolledMap[user.id] = user.get('coursePrepaidID')?
@totalEnrolled = _.reduce @members.models, ((sum, user) ->
sum + (if user.get('coursePrepaidID') then 1 else 0)
), 0
@numberOfStudents = @totalNotEnrolled = _.reduce @members.models, ((sum, user) -> # sort users into enrolled, not enrolled
sum + (if not user.get('coursePrepaidID') then 1 else 0) groups = @members.groupBy (m) -> m.isEnrolled()
), 0 enrolledUsers = new Users(groups.true)
@notEnrolledUsers = new Users(groups.false)
map = {}
@classroomEnrolledMap = _.reduce @classrooms.models, ((map, classroom) => for classroom in @classrooms.models
enrolled = _.reduce classroom.get('members'), ((sum, userID) => map[classroom.id] = _.countBy(classroom.get('members'), (userID) -> enrolledUsers.get(userID)?).false
sum + (if @members.get(userID).get('coursePrepaidID') then 1 else 0)
), 0
map[classroom.id] = enrolled
map
), {}
@classroomNotEnrolledMap = _.reduce @classrooms.models, ((map, classroom) => @state.set({
enrolled = _.reduce classroom.get('members'), ((sum, userID) => totalEnrolled: enrolledUsers.size()
sum + (if not @members.get(userID).get('coursePrepaidID') then 1 else 0) totalNotEnrolled: @notEnrolledUsers.size()
), 0 classroomNotEnrolledMap: map
map[classroom.id] = enrolled })
map
), {}
true true
@ -95,70 +88,24 @@ module.exports = class EnrollmentsView extends RootView
) )
true true
onClickHowToEnrollLink: ->
@openModalView(new HowToEnrollModal())
onClickContactUsButton: ->
@openModalView(new TeachersContactModal({ enrollmentsNeeded: @state.get('numberOfStudents') }))
onInputStudentsInput: -> onInputStudentsInput: ->
input = @$('#students-input').val() input = @$('#students-input').val()
if input isnt "" and (parseFloat(input) isnt parseInt(input) or _.isNaN parseInt(input)) if input isnt "" and (parseFloat(input) isnt parseInt(input) or _.isNaN parseInt(input))
@$('#students-input').val(@numberOfStudents) @$('#students-input').val(@state.get('numberOfStudents'))
else else
@numberOfStudents = Math.max(parseInt(@$('#students-input').val()) or 0, 0) @state.set({'numberOfStudents': Math.max(parseInt(@$('#students-input').val()) or 0, 0)}, {silent: true}) # do not re-render
@updatePrice()
updatePrice: -> numberOfStudentsIsValid: -> 0 < @get('numberOfStudents') < 100000
@renderSelectors '#price-form-group'
numberOfStudentsIsValid: -> 0 < @numberOfStudents < 100000 onClickEnrollStudentsButton: ->
modal = new ActivateLicensesModal({ selectedUsers: @notEnrolledUsers, users: @members })
# onClickEnrollStudents: -> @openModalView(modal)
# TODO: Needs "All students" in modal dropdown modal.once 'hidden', =>
@prepaids.add(modal.prepaids.models, { merge: true })
onClickPurchaseButton: -> @debouncedRender() # Because one changed model does not a collection update make
return @openModalView new CreateAccountModal() if me.isAnonymous()
unless @numberOfStudentsIsValid()
alert("Please enter the maximum number of students needed for your class.")
return
@state = undefined
@stateMessage = undefined
@render()
# Show Stripe handler
application.tracker?.trackEvent 'Started course prepaid purchase', {
price: @pricePerStudent, students: @numberOfStudents}
stripeHandler.open
amount: @numberOfStudents * @pricePerStudent
description: "Full course access for #{@numberOfStudents} students"
bitcoin: true
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
onStripeReceivedToken: (e) ->
@state = 'purchasing'
@render?()
data =
maxRedeemers: @numberOfStudents
type: 'course'
stripe:
token: e.token.id
timestamp: new Date().getTime()
$.ajax({
url: '/db/prepaid/-/purchase',
data: data,
method: 'POST',
context: @
success: (prepaid) ->
application.tracker?.trackEvent 'Finished course prepaid purchase', {price: @pricePerStudent, seats: @numberOfStudents}
@state = 'purchased'
@prepaids.add(prepaid)
@render?()
error: (jqxhr, textStatus, errorThrown) ->
application.tracker?.trackEvent 'Failed course prepaid purchase', status: textStatus
if jqxhr.status is 402
@state = 'error'
@stateMessage = arguments[2]
else
@state = 'error'
@stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}"
@render?()
})

View file

@ -0,0 +1,28 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/courses/join-class-modal'
Classroom = require 'models/Classroom'
User = require 'models/User'
module.exports = class JoinClassModal extends ModalView
id: 'join-class-modal'
template: template
events:
'click .join-class-btn': 'onClickJoinClassButton'
initialize: ({ @classCode }) ->
@classroom = new Classroom()
@teacher = new User()
jqxhr = @supermodel.trackRequest @classroom.fetchByCode(@classCode)
unless me.get('emailVerified')
@supermodel.trackRequest $.post("/db/user/#{me.id}/request-verify-email")
@listenTo @classroom, 'sync', ->
@render
@listenTo @classroom, 'join:success', ->
@trigger('join:success', @classroom)
@listenTo @classroom, 'join:error', ->
@trigger('join:error', @classroom, jqxhr)
# @close()
onClickJoinClassButton: ->
@classroom.joinWithCode(@classCode)

View file

@ -1,126 +0,0 @@
app = require 'core/application'
Classroom = require 'models/Classroom'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
Prepaids = require 'collections/Prepaids'
RootView = require 'views/core/RootView'
stripeHandler = require 'core/services/stripe'
template = require 'templates/courses/purchase-courses-view'
User = require 'models/User'
utils = require 'core/utils'
Products = require 'collections/Products'
module.exports = class PurchaseCoursesView extends RootView
id: 'purchase-courses-view'
template: template
numberOfStudents: 30
pricePerStudent: 0
initialize: (options) ->
@listenTo stripeHandler, 'received-token', @onStripeReceivedToken
@fromClassroom = utils.getQueryVariable('from-classroom')
@members = new CocoCollection([], { model: User })
@listenTo @members, 'sync', @membersSync
@classrooms = new CocoCollection([], { url: "/db/classroom", model: Classroom })
@classrooms.comparator = '_id'
@listenToOnce @classrooms, 'sync', @onceClassroomsSync
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
@prepaids = new Prepaids()
@prepaids.comparator = '_id'
@prepaids.fetchByCreator(me.id)
@supermodel.loadCollection(@prepaids, 'prepaids')
@products = new Products()
@supermodel.loadCollection(@products, 'products')
super(options)
events:
'input #students-input': 'onInputStudentsInput'
'click #purchase-btn': 'onClickPurchaseButton'
onLoaded: ->
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
super()
getPriceString: -> '$' + (@getPrice()/100).toFixed(2)
getPrice: -> @pricePerStudent * @numberOfStudents
onceClassroomsSync: ->
for classroom in @classrooms.models
@members.fetch({
remove: false
url: "/db/classroom/#{classroom.id}/members"
})
membersSync: ->
@memberEnrolledMap = {}
for user in @members.models
@memberEnrolledMap[user.id] = user.get('coursePrepaidID')?
@classroomNotEnrolledMap = {}
@totalNotEnrolled = 0
for classroom in @classrooms.models
@classroomNotEnrolledMap[classroom.id] = 0
for memberID in classroom.get('members')
@classroomNotEnrolledMap[classroom.id]++ unless @memberEnrolledMap[memberID]
@totalNotEnrolled += @classroomNotEnrolledMap[classroom.id]
@numberOfStudents = @totalNotEnrolled
@render?()
onInputStudentsInput: ->
@numberOfStudents = Math.max(parseInt(@$('#students-input').val()) or 0, 0)
@updatePrice()
updatePrice: ->
@renderSelectors '#price-form-group'
numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000
onClickPurchaseButton: ->
return application.router.navigate('/teachers/signup', {trigger: true}) if me.isAnonymous()
unless @numberOfStudentsIsValid()
alert("Please enter the maximum number of students needed for your class.")
return
@state = undefined
@stateMessage = undefined
@render()
# Show Stripe handler
application.tracker?.trackEvent 'Started course prepaid purchase', {
price: @pricePerStudent, students: @numberOfStudents}
stripeHandler.open
amount: @numberOfStudents * @pricePerStudent
description: "Full course access for #{@numberOfStudents} students"
bitcoin: true
alipay: if me.get('country') is 'china' or (me.get('preferredLanguage') or 'en-US')[...2] is 'zh' then true else 'auto'
onStripeReceivedToken: (e) ->
@state = 'purchasing'
@render?()
data =
maxRedeemers: @numberOfStudents
type: 'course'
stripe:
token: e.token.id
timestamp: new Date().getTime()
$.ajax({
url: '/db/prepaid/-/purchase',
data: data,
method: 'POST',
context: @
success: ->
application.tracker?.trackEvent 'Finished course prepaid purchase', {price: @pricePerStudent, seats: @numberOfStudents}
@state = 'purchased'
@render?()
error: (jqxhr, textStatus, errorThrown) ->
application.tracker?.trackEvent 'Failed course prepaid purchase', status: textStatus
if jqxhr.status is 402
@state = 'error'
@stateMessage = arguments[2]
else
@state = 'error'
@stateMessage = "#{jqxhr.status}: #{jqxhr.responseText}"
@render?()
})

View file

@ -5,6 +5,7 @@ helper = require 'lib/coursesHelper'
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal' ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
InviteToClassroomModal = require 'views/courses/InviteToClassroomModal' InviteToClassroomModal = require 'views/courses/InviteToClassroomModal'
ActivateLicensesModal = require 'views/courses/ActivateLicensesModal' ActivateLicensesModal = require 'views/courses/ActivateLicensesModal'
EditStudentModal = require 'views/teachers/EditStudentModal'
RemoveStudentModal = require 'views/courses/RemoveStudentModal' RemoveStudentModal = require 'views/courses/RemoveStudentModal'
Classroom = require 'models/Classroom' Classroom = require 'models/Classroom'
@ -23,39 +24,29 @@ module.exports = class TeacherClassView extends RootView
template: template template: template
events: events:
'click .students-tab-btn': (e) -> 'click .nav-tabs a': 'onClickNavTabLink'
e.preventDefault()
@trigger 'open-students-tab'
'click .course-progress-tab-btn': (e) ->
e.preventDefault()
@trigger 'open-course-progress-tab'
'click .unarchive-btn': 'onClickUnarchive' 'click .unarchive-btn': 'onClickUnarchive'
'click .edit-classroom': 'onClickEditClassroom' 'click .edit-classroom': 'onClickEditClassroom'
'click .add-students-btn': 'onClickAddStudents' 'click .add-students-btn': 'onClickAddStudents'
'click .sort-by-name': 'sortByName' 'click .edit-student-link': 'onClickEditStudentLink'
'click .sort-by-progress': 'sortByProgress' 'click .sort-button': 'onClickSortButton'
'click #copy-url-btn': 'copyURL' 'click #copy-url-btn': 'onClickCopyURLButton'
'click #copy-code-btn': 'copyCode' 'click #copy-code-btn': 'onClickCopyCodeButton'
'click .remove-student-link': 'onClickRemoveStudentLink' 'click .remove-student-link': 'onClickRemoveStudentLink'
'click .assign-student-button': 'onClickAssign' 'click .assign-student-button': 'onClickAssignStudentButton'
'click .enroll-student-button': 'onClickEnroll' 'click .enroll-student-button': 'onClickEnrollStudentButton'
'click .assign-to-selected-students': 'onClickBulkAssign' 'click .assign-to-selected-students': 'onClickBulkAssign'
'click .enroll-selected-students': 'onClickBulkEnroll' 'click .enroll-selected-students': 'onClickBulkEnroll'
'click .export-student-progress-btn': 'onClickExportStudentProgress' 'click .export-student-progress-btn': 'onClickExportStudentProgress'
'click .select-all': 'onClickSelectAll' 'click .select-all': 'onClickSelectAll'
'click .student-checkbox': 'onClickStudentCheckbox' 'click .student-checkbox': 'onClickStudentCheckbox'
'change .course-select, .bulk-course-select': (e) -> 'keyup #student-search': 'onKeyPressStudentSearch'
@trigger 'course-select:change', { selectedCourse: @courses.get($(e.currentTarget).val()) }
getInitialState: -> getInitialState: ->
if Backbone.history.getHash() in ['students-tab', 'course-progress-tab']
activeTab = '#' + Backbone.history.getHash()
else
activeTab = '#students-tab'
{ {
sortAttribute: 'name' sortAttribute: 'name'
sortDirection: 1 sortDirection: 1
activeTab activeTab: '#' + (Backbone.history.getHash() or 'students-tab')
students: new Users() students: new Users()
classCode: "" classCode: ""
joinURL: "" joinURL: ""
@ -78,38 +69,59 @@ module.exports = class TeacherClassView extends RootView
@allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level' @allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level'
@state = new State(@getInitialState()) @state = new State(@getInitialState())
window.location.hash = @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab) @updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab)
@classroom = new Classroom({ _id: classroomID }) @classroom = new Classroom({ _id: classroomID })
@classroom.fetch() @supermodel.trackRequest @classroom.fetch()
@supermodel.trackModel(@classroom) @onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200)
@students = new Users() @students = new Users()
@listenTo @classroom, 'sync', -> @listenTo @classroom, 'sync', ->
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true) jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
if jqxhrs.length > 0 @supermodel.trackRequests jqxhrs
@supermodel.trackCollection(@students)
@classroom.sessions = new LevelSessions() @classroom.sessions = new LevelSessions()
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom) requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
@supermodel.trackRequests(requests) @supermodel.trackRequests(requests)
@courses = new Courses() @students.comparator = (student1, student2) =>
@courses.fetch() dir = @state.get('sortDirection')
@supermodel.trackCollection(@courses) value = @state.get('sortValue')
if value is 'name'
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
if value is 'progress'
# TODO: I would like for this to be in the Level model,
# but it doesn't know about its own courseNumber.
level1 = student1.latestCompleteLevel
level2 = student2.latestCompleteLevel
return -dir if not level1
return dir if not level2
return dir * (level1.courseNumber - level2.courseNumber or level1.levelNumber - level2.levelNumber)
if value is 'status'
statusMap = { expired: 0, 'not-enrolled': 1, enrolled: 2 }
diff = statusMap[student1.prepaidStatus()] - statusMap[student2.prepaidStatus()]
return dir * diff if diff
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
@courses = new Courses()
@supermodel.trackRequest @courses.fetch()
@courseInstances = new CourseInstances() @courseInstances = new CourseInstances()
@courseInstances.fetchForClassroom(classroomID) @supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@supermodel.trackCollection(@courseInstances)
@levels = new Levels() @levels = new Levels()
@levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}}) @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts'}})
@supermodel.trackCollection(@levels)
@attachMediatorEvents() @attachMediatorEvents()
attachMediatorEvents: () -> attachMediatorEvents: () ->
@listenTo @state, 'sync change', @render @listenTo @state, 'sync change', ->
if _.isEmpty(_.omit(@state.changed, 'searchTerm'))
@renderSelectors('#enrollment-status-table')
else
@render()
# Model/Collection events # Model/Collection events
@listenTo @classroom, 'sync change update', -> @listenTo @classroom, 'sync change update', ->
@removeDeletedStudents() @removeDeletedStudents()
@ -126,8 +138,6 @@ module.exports = class TeacherClassView extends RootView
@render() # TODO: use state @render() # TODO: use state
@listenTo @courseInstances, 'add-members', -> @listenTo @courseInstances, 'add-members', ->
noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000 noty text: $.i18n.t('teacher.assigned'), layout: 'center', type: 'information', killer: true, timeout: 5000
@listenToOnce @students, 'sync', # TODO: This seems like it's in the wrong place?
@sortByName
@listenTo @students, 'sync change update add remove reset', -> @listenTo @students, 'sync change update add remove reset', ->
# Set state/props of things that depend on students? # Set state/props of things that depend on students?
# Set specific parts of state based on the models, rather than just dumping the collection there? # Set specific parts of state based on the models, rather than just dumping the collection there?
@ -139,18 +149,6 @@ module.exports = class TeacherClassView extends RootView
@listenTo @students, 'sort', -> @listenTo @students, 'sort', ->
@state.set students: @students @state.set students: @students
@render() @render()
# DOM events
@listenTo @, 'open-students-tab', ->
if window.location.hash isnt '#students-tab'
window.location.hash = '#students-tab'
@state.set activeTab: '#students-tab'
@listenTo @, 'open-course-progress-tab', ->
if window.location.hash isnt '#course-progress-tab'
window.location.hash = '#course-progress-tab'
@state.set activeTab: '#course-progress-tab'
@listenTo @, 'course-select:change', ({ selectedCourse }) ->
@state.set selectedCourse: selectedCourse
setCourseMembers: => setCourseMembers: =>
for course in @courses.models for course in @courses.models
@ -194,12 +192,22 @@ module.exports = class TeacherClassView extends RootView
progressData progressData
classStats: @calculateClassStats() classStats: @calculateClassStats()
} }
copyCode: -> onClickNavTabLink: (e) ->
e.preventDefault()
hash = $(e.target).closest('a').attr('href')
@updateHash(hash)
@state.set activeTab: hash
updateHash: (hash) ->
return if application.testing
window.location.hash = hash
onClickCopyCodeButton: ->
@$('#join-code-input').val(@state.get('classCode')).select() @$('#join-code-input').val(@state.get('classCode')).select()
@tryCopy() @tryCopy()
copyURL: -> onClickCopyURLButton: ->
@$('#join-url-input').val(@state.get('joinURL')).select() @$('#join-url-input').val(@state.get('joinURL')).select()
@tryCopy() @tryCopy()
@ -220,6 +228,11 @@ module.exports = class TeacherClassView extends RootView
@openModalView(modal) @openModalView(modal)
@listenToOnce modal, 'hide', @render @listenToOnce modal, 'hide', @render
onClickEditStudentLink: (e) ->
user = @students.get($(e.currentTarget).data('student-id'))
modal = new EditStudentModal({ user, @classroom })
@openModalView(modal)
onClickRemoveStudentLink: (e) -> onClickRemoveStudentLink: (e) ->
user = @students.get($(e.currentTarget).data('student-id')) user = @students.get($(e.currentTarget).data('student-id'))
modal = new RemoveStudentModal({ modal = new RemoveStudentModal({
@ -246,35 +259,19 @@ module.exports = class TeacherClassView extends RootView
) )
true true
sortByName: (e) -> onClickSortButton: (e) ->
if @state.get('sortValue') is 'name' value = $(e.target).val()
if value is @state.get('sortValue')
@state.set('sortDirection', -@state.get('sortDirection')) @state.set('sortDirection', -@state.get('sortDirection'))
else else
@state.set('sortValue', 'name') @state.set({
@state.set('sortDirection', 1) sortValue: value
sortDirection: 1
dir = @state.get('sortDirection') })
@students.comparator = (student1, student2) ->
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
@students.sort() @students.sort()
sortByProgress: (e) -> onKeyPressStudentSearch: (e) ->
if @state.get('sortValue') is 'progress' @state.set('searchTerm', $(e.target).val())
@state.set('sortDirection', -@state.get('sortDirection'))
else
@state.set('sortValue', 'progress')
@state.set('sortDirection', 1)
dir = @state.get('sortDirection')
@students.comparator = (student) ->
#TODO: I would like for this to be in the Level model,
# but it doesn't know about its own courseNumber
level = student.latestCompleteLevel
if not level
return -dir
return dir * ((1000 * level.courseNumber) + level.levelNumber)
@students.sort()
getSelectedStudentIDs: -> getSelectedStudentIDs: ->
@$('.student-row .checkbox-flat input:checked').map (index, checkbox) -> @$('.student-row .checkbox-flat input:checked').map (index, checkbox) ->
@ -282,7 +279,7 @@ module.exports = class TeacherClassView extends RootView
ensureInstance: (courseID) -> ensureInstance: (courseID) ->
onClickEnroll: (e) -> onClickEnrollStudentButton: (e) ->
userID = $(e.currentTarget).data('user-id') userID = $(e.currentTarget).data('user-id')
user = @students.get(userID) user = @students.get(userID)
selectedUsers = new Users([user]) selectedUsers = new Users([user])
@ -330,7 +327,7 @@ module.exports = class TeacherClassView extends RootView
window.open(encodedUri) window.open(encodedUri)
onClickAssign: (e) -> onClickAssignStudentButton: (e) ->
userID = $(e.currentTarget).data('user-id') userID = $(e.currentTarget).data('user-id')
user = @students.get(userID) user = @students.get(userID)
members = [userID] members = [userID]
@ -373,7 +370,7 @@ module.exports = class TeacherClassView extends RootView
courseInstance.addMembers members courseInstance.addMembers members
} }
null null
onClickSelectAll: (e) -> onClickSelectAll: (e) ->
e.preventDefault() e.preventDefault()
checkboxes = @$('.student-checkbox input') checkboxes = @$('.student-checkbox input')
@ -412,7 +409,16 @@ module.exports = class TeacherClassView extends RootView
stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # ' stats.averageLevelsComplete = if @students.size() then (_.size(completeSessions) / @students.size()).toFixed(1) else 'N/A' # '
stats.totalLevelsComplete = _.size(completeSessions) stats.totalLevelsComplete = _.size(completeSessions)
enrolledUsers = @students.filter (user) -> user.get('coursePrepaidID') enrolledUsers = @students.filter (user) -> user.isEnrolled()
stats.enrolledUsers = _.size(enrolledUsers) stats.enrolledUsers = _.size(enrolledUsers)
return stats return stats
studentStatusString: (student) ->
status = student.prepaidStatus()
expires = student.get('coursePrepaid')?.endDate
string = switch status
when 'not-enrolled' then $.i18n.t('teacher.status_not_enrolled')
when 'enrolled' then (if expires then $.i18n.t('teacher.status_enrolled') else '-')
when 'expired' then $.i18n.t('teacher.status_expired')
return string.replace('{{date}}', moment(expires).utc().format('l'))

View file

@ -12,7 +12,6 @@ CourseInstance = require 'models/CourseInstance'
RootView = require 'views/core/RootView' RootView = require 'views/core/RootView'
template = require 'templates/courses/teacher-courses-view' template = require 'templates/courses/teacher-courses-view'
ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal' ClassroomSettingsModal = require 'views/courses/ClassroomSettingsModal'
Prepaids = require 'collections/Prepaids'
module.exports = class TeacherCoursesView extends RootView module.exports = class TeacherCoursesView extends RootView
id: 'teacher-courses-view' id: 'teacher-courses-view'
@ -52,17 +51,11 @@ module.exports = class TeacherCoursesView extends RootView
@listenToOnce @classrooms, 'sync', @onceClassroomsSync @listenToOnce @classrooms, 'sync', @onceClassroomsSync
@supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}}) @supermodel.loadCollection(@classrooms, 'classrooms', {data: {ownerID: me.id}})
@campaigns = new Campaigns() @campaigns = new Campaigns()
@campaigns.fetch() @supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels,levelsUpdated' } })
@supermodel.trackCollection(@campaigns)
@courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance }) @courseInstances = new CocoCollection([], { url: "/db/course_instance", model: CourseInstance })
@courseInstances.comparator = 'courseID' @courseInstances.comparator = 'courseID'
@courseInstances.sliceWithMembers = -> return @filter (courseInstance) -> _.size(courseInstance.get('members')) and courseInstance.get('classroomID') @courseInstances.sliceWithMembers = -> return @filter (courseInstance) -> _.size(courseInstance.get('members')) and courseInstance.get('classroomID')
@supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}}) @supermodel.loadCollection(@courseInstances, 'course_instances', {data: {ownerID: me.id}})
@prepaids = new Prepaids()
@prepaids.comparator = '_id'
if not me.isAnonymous()
@prepaids.fetchByCreator(me.id)
@supermodel.loadCollection(@prepaids, 'prepaids') # just registers
@members = new CocoCollection([], { model: User }) @members = new CocoCollection([], { model: User })
@listenTo @members, 'sync', @render @listenTo @members, 'sync', @render
@ @

View file

@ -36,8 +36,7 @@ require 'vendor/aether-javascript'
require 'vendor/aether-python' require 'vendor/aether-python'
require 'vendor/aether-coffeescript' require 'vendor/aether-coffeescript'
require 'vendor/aether-lua' require 'vendor/aether-lua'
require 'vendor/aether-clojure' require 'vendor/aether-java'
require 'vendor/aether-io'
module.exports = class LevelEditView extends RootView module.exports = class LevelEditView extends RootView
id: 'editor-level-view' id: 'editor-level-view'
@ -113,7 +112,7 @@ module.exports = class LevelEditView extends RootView
@insertSubView new ComponentsDocumentationView lazy: true # Don't give it the supermodel, it'll pollute it! @insertSubView new ComponentsDocumentationView lazy: true # Don't give it the supermodel, it'll pollute it!
@insertSubView new SystemsDocumentationView lazy: true # Don't give it the supermodel, it'll pollute it! @insertSubView new SystemsDocumentationView lazy: true # Don't give it the supermodel, it'll pollute it!
@insertSubView new LevelFeedbackView level: @level @insertSubView new LevelFeedbackView level: @level
Backbone.Mediator.publish 'editor:level-loaded', level: @level Backbone.Mediator.publish 'editor:level-loaded', level: @level
@showReadOnly() if me.get('anonymous') @showReadOnly() if me.get('anonymous')

View file

@ -21,8 +21,6 @@ module.exports = class SimulateTabView extends CocoView
require 'vendor/aether-coffeescript' require 'vendor/aether-coffeescript'
require 'vendor/aether-lua' require 'vendor/aether-lua'
require 'vendor/aether-java' require 'vendor/aether-java'
require 'vendor/aether-clojure'
require 'vendor/aether-io'
onLoaded: -> onLoaded: ->
super() super()

View file

@ -225,10 +225,7 @@ module.exports = class PlayLevelView extends RootView
opponentSpells = opponentSpells.concat spells opponentSpells = opponentSpells.concat spells
if (not @session.get('teamSpells')) and @otherSession?.get('teamSpells') if (not @session.get('teamSpells')) and @otherSession?.get('teamSpells')
@session.set('teamSpells', @otherSession.get('teamSpells')) @session.set('teamSpells', @otherSession.get('teamSpells'))
if @getQueryVariable 'esper' opponentCode = @otherSession?.get('code') or {}
opponentCode = @otherSession?.get('code') or {}
else
opponentCode = @otherSession?.get('transpiledCode') or {}
myCode = @session.get('code') or {} myCode = @session.get('code') or {}
for spell in opponentSpells for spell in opponentSpells
[thang, spell] = spell.split '/' [thang, spell] = spell.split '/'
@ -363,15 +360,6 @@ module.exports = class PlayLevelView extends RootView
onLevelStarted: -> onLevelStarted: ->
return unless @surface? return unless @surface?
#TODO: Remove this at some point
if @session.get('codeLanguage') in ['clojure', 'io']
problem =
aetherProblem:
message: "Sorry, support for #{@session.get('codeLanguage')} has been removed."
Backbone.Mediator.publish 'tome:show-problem-alert', problem: problem
@loadingView.showReady() @loadingView.showReady()
@trackLevelLoadEnd() @trackLevelLoadEnd()
if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal
@ -429,16 +417,14 @@ module.exports = class PlayLevelView extends RootView
perhapsStartSimulating: -> perhapsStartSimulating: ->
return unless @shouldSimulate() return unless @shouldSimulate()
return console.error "Should not auto-simulate until we fix how these languages are loaded"
# TODO: how can we not require these as part of /play bundle? # TODO: how can we not require these as part of /play bundle?
#require "vendor/aether-#{codeLanguage}" for codeLanguage in ['javascript', 'python', 'coffeescript', 'lua', 'clojure', 'io'] ##require "vendor/aether-#{codeLanguage}" for codeLanguage in ['javascript', 'python', 'coffeescript', 'lua', 'java']
require 'vendor/aether-javascript' #require 'vendor/aether-javascript'
require 'vendor/aether-python' #require 'vendor/aether-python'
require 'vendor/aether-coffeescript' #require 'vendor/aether-coffeescript'
require 'vendor/aether-lua' #require 'vendor/aether-lua'
require 'vendor/aether-java' #require 'vendor/aether-java'
require 'vendor/aether-clojure'
require 'vendor/aether-io'
require 'vendor/aether-java'
@simulateNextGame() @simulateNextGame()
simulateNextGame: -> simulateNextGame: ->

View file

@ -57,23 +57,18 @@ module.exports = class DocFormatter
else (if @options.useHero then 'hero' else 'this') else (if @options.useHero then 'hero' else 'this')
if @doc.type is 'function' if @doc.type is 'function'
[docName, args] = @getDocNameAndArguments() [docName, args] = @getDocNameAndArguments()
sep = {clojure: ' '}[@options.language] ? ', ' argNames = args.join ', '
argNames = args.join sep
argString = if argNames then '__ARGS__' else '' argString = if argNames then '__ARGS__' else ''
@doc.shortName = switch @options.language @doc.shortName = switch @options.language
when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{docName}#{if argString then ' ' + argString else '()'}" when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{docName}#{if argString then ' ' + argString else '()'}"
when 'python' then "#{ownerName}.#{docName}(#{argString})" when 'python' then "#{ownerName}.#{docName}(#{argString})"
when 'lua' then "#{ownerName}:#{docName}(#{argString})" when 'lua' then "#{ownerName}:#{docName}(#{argString})"
when 'clojure' then "(.#{docName} #{ownerName}#{if argNames then ' ' + argString else ''})"
when 'io' then "#{if ownerName is 'this' then '' else ownerName + ' '}#{docName}#{if argNames then '(' + argNames + ')' else ''}"
else "#{ownerName}.#{docName}(#{argString});" else "#{ownerName}.#{docName}(#{argString});"
else else
@doc.shortName = switch @options.language @doc.shortName = switch @options.language
when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{@doc.name}" when 'coffeescript' then "#{ownerName}#{if ownerName is '@' then '' else '.'}#{@doc.name}"
when 'python' then "#{ownerName}.#{@doc.name}" when 'python' then "#{ownerName}.#{@doc.name}"
when 'lua' then "#{ownerName}.#{@doc.name}" when 'lua' then "#{ownerName}.#{@doc.name}"
when 'clojure' then "(.#{@doc.name} #{ownerName})"
when 'io' then "#{if ownerName is 'this' then '' else ownerName + ' '}#{@doc.name}"
else "#{ownerName}.#{@doc.name};" else "#{ownerName}.#{@doc.name};"
@doc.shorterName = @doc.shortName @doc.shorterName = @doc.shortName
if @doc.type is 'function' and argString if @doc.type is 'function' and argString
@ -89,7 +84,7 @@ module.exports = class DocFormatter
translatedName = utils.i18n(@doc, 'name') translatedName = utils.i18n(@doc, 'name')
if translatedName isnt @doc.name if translatedName isnt @doc.name
@doc.translatedShortName = @doc.shortName.replace(@doc.name, translatedName) @doc.translatedShortName = @doc.shortName.replace(@doc.name, translatedName)
# Grab the language-specific documentation for some sub-properties, if we have it. # Grab the language-specific documentation for some sub-properties, if we have it.
toTranslate = [{obj: @doc, prop: 'description'}, {obj: @doc, prop: 'example'}] toTranslate = [{obj: @doc, prop: 'description'}, {obj: @doc, prop: 'example'}]
@ -153,16 +148,12 @@ module.exports = class DocFormatter
when 'coffeescript' then "loop" when 'coffeescript' then "loop"
when 'python' then "while True:" when 'python' then "while True:"
when 'lua' then "while true do" when 'lua' then "while true do"
when 'clojure' then "(while true)"
when 'io' then "while(true)"
else "while (true)" else "while (true)"
for field in ['example', 'description'] for field in ['example', 'description']
[simpleLoop, whileLoop] = switch @options.language [simpleLoop, whileLoop] = switch @options.language
when 'coffeescript' then [/loop/g, "loop"] when 'coffeescript' then [/loop/g, "loop"]
when 'python' then [/loop:/g, "while True:"] when 'python' then [/loop:/g, "while True:"]
when 'lua' then [/loop/g, "while true do"] when 'lua' then [/loop/g, "while true do"]
when 'clojure' then [/\(dotimes( \[n \d+\])?/g, "(while true"]
when 'io' then [/loop\(/g, "while(true,"]
else [/loop/g, "while (true)"] else [/loop/g, "while (true)"]
@doc[field] = @doc[field].replace simpleLoop, whileLoop @doc[field] = @doc[field].replace simpleLoop, whileLoop

View file

@ -31,14 +31,13 @@ module.exports = class Spell
@languages.javascript ?= p.source @languages.javascript ?= p.source
@name = p.name @name = p.name
@permissions = read: p.permissions?.read ? [], readwrite: p.permissions?.readwrite ? [] # teams @permissions = read: p.permissions?.read ? [], readwrite: p.permissions?.readwrite ? [] # teams
@team = @permissions.readwrite[0] ? 'common'
if @canWrite() if @canWrite()
@setLanguage options.language @setLanguage options.language
else if @isEnemySpell() else if @otherSession and @team is @otherSession.get 'team'
@setLanguage @otherSession?.get('submittedCodeLanguage') ? @spectateOpponentCodeLanguage @setLanguage @otherSession.get('submittedCodeLanguage') or @otherSession.get('codeLanguage')
else else
@setLanguage 'javascript' @setLanguage 'javascript'
@useTranspiledCode = @shouldUseTranspiledCode()
#console.log 'Spell', @spellKey, 'is using transpiled code (should only happen if it\'s an enemy/spectate writable method).' if @useTranspiledCode
@source = @originalSource @source = @originalSource
@parameters = p.parameters @parameters = p.parameters
@ -53,7 +52,6 @@ module.exports = class Spell
@view.render() # Get it ready and code loaded in advance @view.render() # Get it ready and code loaded in advance
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level @tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level
@tabView.render() @tabView.render()
@team = @permissions.readwrite[0] ? 'common'
Backbone.Mediator.publish 'tome:spell-created', spell: @ Backbone.Mediator.publish 'tome:spell-created', spell: @
destroy: -> destroy: ->
@ -88,10 +86,8 @@ module.exports = class Spell
@originalSource = switch @language @originalSource = switch @language
when 'python' then @originalSource.replace /loop:/, 'while True:' when 'python' then @originalSource.replace /loop:/, 'while True:'
when 'javascript' then @originalSource.replace /loop {/, 'while (true) {' when 'javascript' then @originalSource.replace /loop {/, 'while (true) {'
when 'clojure' then @originalSource.replace /dotimes \[n 1000\]/, '(while true'
when 'lua' then @originalSource.replace /loop\n/, 'while true then\n' when 'lua' then @originalSource.replace /loop\n/, 'while true then\n'
when 'coffeescript' then @originalSource when 'coffeescript' then @originalSource
when 'io' then @originalSource.replace /loop\n/, 'while true,\n'
else @originalSource else @originalSource
addPicoCTFProblem: -> addPicoCTFProblem: ->
@ -126,15 +122,10 @@ module.exports = class Spell
else else
source = @getSource() source = @getSource()
[pure, problems] = [null, null] [pure, problems] = [null, null]
if @useTranspiledCode
transpiledCode = @session.get('code')
for thangID, spellThang of @thangs for thangID, spellThang of @thangs
unless pure unless pure
if @useTranspiledCode and transpiledSpell = transpiledCode[@spellKey.split('/')[0]]?[@name] pure = spellThang.aether.transpile source
spellThang.aether.pure = transpiledSpell problems = spellThang.aether.problems
else
pure = spellThang.aether.transpile source
problems = spellThang.aether.problems
#console.log 'aether transpiled', source.length, 'to', spellThang.aether.pure.length, 'for', thangID, @spellKey #console.log 'aether transpiled', source.length, 'to', spellThang.aether.pure.length, 'for', thangID, @spellKey
else else
spellThang.aether.raw = source spellThang.aether.raw = source
@ -182,7 +173,7 @@ module.exports = class Spell
skipProtectAPI: skipProtectAPI skipProtectAPI: skipProtectAPI
includeFlow: includeFlow includeFlow: includeFlow
problemContext: problemContext problemContext: problemContext
useInterpreter: if @spectateView then true else undefined useInterpreter: true
aether = new Aether aetherOptions aether = new Aether aetherOptions
if @worker if @worker
workerMessage = workerMessage =
@ -207,22 +198,6 @@ module.exports = class Spell
toString: -> toString: ->
"<Spell: #{@spellKey}>" "<Spell: #{@spellKey}>"
isEnemySpell: ->
return false unless @permissions.readwrite.length
return false unless @otherSession or @spectateView
teamSpells = @session.get('teamSpells')
team = @session.get('team') ? 'humans'
teamSpells and not _.contains(teamSpells[team], @spellKey)
shouldUseTranspiledCode: ->
# Determine whether this code has already been transpiled, or whether it's raw source needing transpilation.
return false if @spectateView or utils.getQueryVariable 'esper' # Don't use transpiled code with interpreter
return true if @isEnemySpell() # Use transpiled for enemy spells.
# Players without permissions can't view the raw code.
return false if @observing and @levelType in ['hero', 'course']
return true if @session.get('creator') isnt me.id and not (me.isAdmin() or 'employer' in me.get('permissions', true))
false
createProblemContext: (thang) -> createProblemContext: (thang) ->
# Create problemContext Aether can use to craft better error messages # Create problemContext Aether can use to craft better error messages
# stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak) # stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak)

View file

@ -46,10 +46,6 @@ module.exports = class SpellListEntryView extends CocoView
paramString = parameters.join ', ' paramString = parameters.join ', '
name = @spell.name name = @spell.name
switch @spell.language switch @spell.language
when 'io'
"#{name} := method(#{paramString})"
when 'clojure'
"(defn #{name} [#{paramString}] ...)"
when 'python' when 'python'
"def #{name}(#{paramString}):" "def #{name}(#{paramString}):"
when 'lua' when 'lua'

View file

@ -230,7 +230,7 @@ module.exports = class SpellView extends CocoView
disableSpaces = @options.level.get('disableSpaces') or false disableSpaces = @options.level.get('disableSpaces') or false
aceConfig = me.get('aceConfig') ? {} aceConfig = me.get('aceConfig') ? {}
disableSpaces = false if aceConfig.keyBindings and aceConfig.keyBindings isnt 'default' # Not in vim/emacs mode disableSpaces = false if aceConfig.keyBindings and aceConfig.keyBindings isnt 'default' # Not in vim/emacs mode
disableSpaces = false if @spell.language in ['clojure', 'lua', 'java', 'coffeescript', 'io'] # Don't disable for more advanced/experimental languages disableSpaces = false if @spell.language in ['lua', 'java', 'coffeescript'] # Don't disable for more advanced/experimental languages
if not disableSpaces or (_.isNumber(disableSpaces) and disableSpaces < me.level()) if not disableSpaces or (_.isNumber(disableSpaces) and disableSpaces < me.level())
return @ace.execCommand 'insertstring', ' ' return @ace.execCommand 'insertstring', ' '
line = @aceDoc.getLine @ace.getCursorPosition().row line = @aceDoc.getLine @ace.getCursorPosition().row
@ -301,7 +301,10 @@ module.exports = class SpellView extends CocoView
for row in [0..@aceSession.getLength()] for row in [0..@aceSession.getLength()]
foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]? foldWidgets[row] = @aceSession.getFoldWidget(row) unless foldWidgets[row]?
continue unless foldWidgets? and foldWidgets[row] is "start" continue unless foldWidgets? and foldWidgets[row] is "start"
docRange = @aceSession.getFoldWidgetRange(row) try
docRange = @aceSession.getFoldWidgetRange(row)
catch error
console.warn "Couldn't find fold widget docRange for row #{row}:", error
if not docRange? if not docRange?
guess = startOfRow(row) guess = startOfRow(row)
docRange = new Range(row,guess,row,guess+4) docRange = new Range(row,guess,row,guess+4)
@ -520,10 +523,8 @@ module.exports = class SpellView extends CocoView
content = switch e.language content = switch e.language
when 'python' then content.replace /loop:/, 'while True:' when 'python' then content.replace /loop:/, 'while True:'
when 'javascript' then content.replace /loop/, 'while (true)' when 'javascript' then content.replace /loop/, 'while (true)'
when 'clojure' then content.replace /dotimes \[n 1000\]/, '(while true'
when 'lua' then content.replace /loop/, 'while true then' when 'lua' then content.replace /loop/, 'while true then'
when 'coffeescript' then content when 'coffeescript' then content
when 'io' then content.replace /loop/, 'while true,'
else content else content
name = switch e.language name = switch e.language
when 'python' then 'while True' when 'python' then 'while True'
@ -557,9 +558,8 @@ module.exports = class SpellView extends CocoView
if doc.userShouldCaptureReturn if doc.userShouldCaptureReturn
varName = doc.userShouldCaptureReturn.variableName ? 'result' varName = doc.userShouldCaptureReturn.variableName ? 'result'
entry.captureReturn = switch e.language entry.captureReturn = switch e.language
when 'io' then varName + ' := '
when 'javascript' then 'var ' + varName + ' = ' when 'javascript' then 'var ' + varName + ' = '
when 'clojure' then '(let [' + varName + ' ' #when 'lua' then 'local ' + varName + ' = ' # TODO: should we do this?
else varName + ' = ' else varName + ' = '
# TODO: Generalize this snippet replacement # TODO: Generalize this snippet replacement
@ -583,15 +583,8 @@ module.exports = class SpellView extends CocoView
translateFindNearest: -> translateFindNearest: ->
# If they have advanced glasses but are playing a level which assumes earlier glasses, we'll adjust the sample code to use the more advanced APIs instead. # If they have advanced glasses but are playing a level which assumes earlier glasses, we'll adjust the sample code to use the more advanced APIs instead.
oldSource = @getSource() oldSource = @getSource()
if @spell.language is 'clojure' newSource = oldSource.replace /(self:|self.|this.|@)findNearestEnemy\(\)/g, "$1findNearest($1findEnemies())"
newSource = oldSource.replace /\(.findNearestEnemy this\)/g, "(.findNearest this (.findEnemies this))" newSource = newSource.replace /(self:|self.|this.|@)findNearestItem\(\)/g, "$1findNearest($1findItems())"
newSource = newSource.replace /\(.findNearestItem this\)/g, "(.findNearest this (.findItems this))"
else if @spell.language is 'io'
newSource = oldSource.replace /findNearestEnemy/g, "findNearest(findEnemies)"
newSource = newSource.replace /findNearestItem/g, "findNearest(findItems)"
else
newSource = oldSource.replace /(self:|self.|this.|@)findNearestEnemy\(\)/g, "$1findNearest($1findEnemies())"
newSource = newSource.replace /(self:|self.|this.|@)findNearestItem\(\)/g, "$1findNearest($1findItems())"
return if oldSource is newSource return if oldSource is newSource
@spell.originalSource = newSource @spell.originalSource = newSource
@updateACEText newSource @updateACEText newSource
@ -640,7 +633,7 @@ module.exports = class SpellView extends CocoView
return if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] # We'll turn this on later, maybe, but not yet. return if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] # We'll turn this on later, maybe, but not yet.
@debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell @debugView = new SpellDebugView ace: @ace, thang: @thang, spell:@spell
@$el.append @debugView.render().$el.hide() @$el.append @debugView.render().$el.hide()
createTranslationView: -> createTranslationView: ->
@translationView = new SpellTranslationView { @ace, @supermodel } @translationView = new SpellTranslationView { @ace, @supermodel }
@$el.append @translationView.render().$el.hide() @$el.append @translationView.render().$el.hide()
@ -731,7 +724,7 @@ module.exports = class SpellView extends CocoView
# Uncomment the below line for a debug panel to display inside the level # Uncomment the below line for a debug panel to display inside the level
#@spade.debugPlay(spadeEvents) #@spade.debugPlay(spadeEvents)
condensedEvents = @spade.condense(spadeEvents) condensedEvents = @spade.condense(spadeEvents)
return unless condensedEvents.length return unless condensedEvents.length
compressedEvents = LZString.compressToUTF16(JSON.stringify(condensedEvents)) compressedEvents = LZString.compressToUTF16(JSON.stringify(condensedEvents))
@ -746,7 +739,7 @@ module.exports = class SpellView extends CocoView
}) })
codeLog.save() codeLog.save()
onShowVictory: (e) -> onShowVictory: (e) ->
if @saveSpadeTimeout? if @saveSpadeTimeout?
window.clearTimeout @saveSpadeTimeout window.clearTimeout @saveSpadeTimeout
@ -1067,12 +1060,8 @@ module.exports = class SpellView extends CocoView
return unless @ace.isFocused() and e.x? and e.y? return unless @ace.isFocused() and e.x? and e.y?
if @spell.language is 'python' if @spell.language is 'python'
@ace.insert "{\"x\": #{e.x}, \"y\": #{e.y}}" @ace.insert "{\"x\": #{e.x}, \"y\": #{e.y}}"
else if @spell.language is 'clojure'
@ace.insert "{:x #{e.x} :y #{e.y}}"
else if @spell.language is 'lua' else if @spell.language is 'lua'
@ace.insert "{x=#{e.x}, y=#{e.y}}" @ace.insert "{x=#{e.x}, y=#{e.y}}"
else if @spell.language is 'io'
return
else else
@ace.insert "{x: #{e.x}, y: #{e.y}}" @ace.insert "{x: #{e.x}, y: #{e.y}}"
@ -1355,6 +1344,5 @@ commentStarts =
javascript: '//' javascript: '//'
python: '#' python: '#'
coffeescript: '#' coffeescript: '#'
clojure: ';'
lua: '--' lua: '--'
io: '//' java: '//'

View file

@ -129,7 +129,5 @@ commentStarts =
javascript: '// ' javascript: '// '
python: '# ' python: '# '
coffeescript: '# ' coffeescript: '# '
clojure: '; '
lua: '-- ' lua: '-- '
io: '// '
java: '// ' java: '// '

View file

@ -0,0 +1,45 @@
ModalView = require 'views/core/ModalView'
State = require 'models/State'
template = require 'templates/teachers/edit-student-modal'
auth = require 'core/auth'
module.exports = class EditStudentModal extends ModalView
id: 'edit-student-modal'
template: template
events:
'click .send-recovery-email-btn:not(.disabled)': 'onClickSendRecoveryEmail'
'click .change-password-btn:not(.disabled)': 'onClickChangePassword'
'input .new-password-input': 'onChangeNewPasswordInput'
initialize: ({ @user, @classroom }) ->
@supermodel.trackRequest @user.fetch()
@utils = require 'core/utils'
@state = new State({
emailSent: false
passwordChanged: false
newPassword: ""
errorMessage: ""
})
@listenTo @state, 'change', @render
@listenTo @classroom, 'save-password:success', ->
@state.set { passwordChanged: true, errorMessage: "" }
@listenTo @classroom, 'save-password:error', (error) ->
@state.set({ errorMessage: error.message })
# TODO: Show an error. (password too short)
onClickSendRecoveryEmail: ->
email = @user.get('email')
auth.sendRecoveryEmail(email).then =>
@state.set { emailSent: true }
onClickChangePassword: ->
@classroom.setStudentPassword(@user, @state.get('newPassword'))
onChangeNewPasswordInput: (e) ->
@state.set {
newPassword: $(e.currentTarget).val()
emailSent: false
passwordChanged: false
}, { silent: true }
@renderSelectors('.change-password-btn')

View file

@ -0,0 +1,6 @@
ModalView = require 'views/core/ModalView'
module.exports = class HowToEnrollModal extends ModalView
id: 'how-to-enroll-modal'
template: require 'templates/teachers/how-to-enroll-modal'

View file

@ -0,0 +1,70 @@
ModalView = require 'views/core/ModalView'
State = require 'models/State'
TrialRequests = require 'collections/TrialRequests'
forms = require 'core/forms'
contact = require 'core/contact'
module.exports = class TeachersContactModal extends ModalView
id: 'teachers-contact-modal'
template: require 'templates/teachers/teachers-contact-modal'
events:
'submit form': 'onSubmitForm'
initialize: (options={}) ->
@state = new State({
formValues: {
email: ''
message: ''
}
formErrors: {}
sendingState: 'standby' # 'sending', 'sent', 'error'
})
@enrollmentsNeeded = options.enrollmentsNeeded or '-'
@trialRequests = new TrialRequests()
@supermodel.trackRequest @trialRequests.fetchOwn()
@state.on 'change', @render, @
onLoaded: ->
trialRequest = @trialRequests.first()
props = trialRequest?.get('properties') or {}
message = """
Name of School/District: #{props.organization or ''}
Your Name: #{props.name || ''}
Enrollments Needed: #{@enrollmentsNeeded}
Message: Hi CodeCombat! I want to learn more about the Classroom experience and get licenses so that my students can access Computer Science 2 and on.
"""
email = props.email or me.get('email') or ''
@state.set('formValues', { email, message })
super()
onSubmitForm: (e) ->
e.preventDefault()
return if @state.get('sendingState') is 'sending'
formValues = forms.formToObject @$el
@state.set('formValues', formValues)
formErrors = {}
if not forms.validateEmail(formValues.email)
formErrors.email = 'Invalid email.'
if not formValues.message
formErrors.message = 'Message required.'
@state.set({ formErrors, formValues, sendingState: 'standby' })
return unless _.isEmpty(formErrors)
@state.set('sendingState', 'sending')
data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues)
contact.send({
data
context: @
success: ->
@state.set({ sendingState: 'sent' })
me.set('enrollmentRequestSent', true)
setTimeout(=>
@hide?()
, 3000)
error: -> @state.set({ sendingState: 'error' })
})

View file

@ -0,0 +1,31 @@
RootView = require 'views/core/RootView'
State = require 'models/State'
template = require 'templates/user/email-verified-view'
User = require 'models/User'
module.exports = class EmailVerifiedView extends RootView
id: 'email-verified-view'
template: template
events:
'click .login-button': 'onClickLoginButton'
initialize: (options, @userID, @verificationCode) ->
super(options)
@state = new State(@getInitialState())
@user = new User({ _id: @userID })
@user.sendVerificationCode(@verificationCode)
@listenTo @state, 'change', @render
@listenTo @user, 'email-verify-success', ->
@state.set { verifyStatus: 'success' }
me.fetch()
@listenTo @user, 'email-verify-error', ->
@state.set { verifyStatus: 'error' }
getInitialState: ->
verifyStatus: 'pending'
onClickLoginButton: (e) ->
AuthModal = require 'views/core/AuthModal'
@openModalView(new AuthModal())

View file

@ -32,7 +32,7 @@
"firepad": "~0.1.2", "firepad": "~0.1.2",
"marked": "~0.3.0", "marked": "~0.3.0",
"moment": "~2.5.0", "moment": "~2.5.0",
"aether": "~0.4.5", "aether": "~0.5.0",
"underscore.string": "~2.3.3", "underscore.string": "~2.3.3",
"firebase": "~1.0.2", "firebase": "~1.0.2",
"d3": "~3.4.4", "d3": "~3.4.4",

View file

@ -110,9 +110,7 @@ exports.config =
'javascripts/lodash.js': regJoin('^bower_components/lodash/dist/lodash.js') 'javascripts/lodash.js': regJoin('^bower_components/lodash/dist/lodash.js')
'javascripts/aether.js': regJoin('^bower_components/aether/build/aether.js') 'javascripts/aether.js': regJoin('^bower_components/aether/build/aether.js')
'javascripts/esper.js': 'bower_components/esper.js/esper.js' 'javascripts/esper.js': 'bower_components/esper.js/esper.js'
'javascripts/app/vendor/aether-clojure.js': 'bower_components/aether/build/clojure.js'
'javascripts/app/vendor/aether-coffeescript.js': 'bower_components/aether/build/coffeescript.js' 'javascripts/app/vendor/aether-coffeescript.js': 'bower_components/aether/build/coffeescript.js'
'javascripts/app/vendor/aether-io.js': 'bower_components/aether/build/io.js'
'javascripts/app/vendor/aether-javascript.js': 'bower_components/aether/build/javascript.js' 'javascripts/app/vendor/aether-javascript.js': 'bower_components/aether/build/javascript.js'
'javascripts/app/vendor/aether-lua.js': 'bower_components/aether/build/lua.js' 'javascripts/app/vendor/aether-lua.js': 'bower_components/aether/build/lua.js'
'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js' 'javascripts/app/vendor/aether-java.js': 'bower_components/aether/build/java.js'

View file

@ -53,7 +53,7 @@
"dependencies": { "dependencies": {
"JQDeferred": "~2.1.0", "JQDeferred": "~2.1.0",
"ace-builds": "https://github.com/ajaxorg/ace-builds/archive/3fb55e8e374ab02ce47c1ae55ffb60a1835f3055.tar.gz", "ace-builds": "https://github.com/ajaxorg/ace-builds/archive/3fb55e8e374ab02ce47c1ae55ffb60a1835f3055.tar.gz",
"aether": "~0.4.5", "aether": "~0.5.0",
"async": "0.2.x", "async": "0.2.x",
"aws-sdk": "~2.0.0", "aws-sdk": "~2.0.0",
"bayesian-battle": "0.0.7", "bayesian-battle": "0.0.7",
@ -101,6 +101,8 @@
"commonjs-require-definition": "0.2.0", "commonjs-require-definition": "0.2.0",
"compressible": "~1.0.1", "compressible": "~1.0.1",
"css-brunch": ">=2.0.0", "css-brunch": ">=2.0.0",
"country-data": "0.0.24",
"country-list": "0.0.3",
"fs-extra": "^0.26.2", "fs-extra": "^0.26.2",
"http-proxy": "^1.13.2", "http-proxy": "^1.13.2",
"jade-brunch": "1.7.5", "jade-brunch": "1.7.5",
@ -112,7 +114,7 @@
"karma-coverage": "~0.5.1", "karma-coverage": "~0.5.1",
"karma-firefox-launcher": "~0.1.3", "karma-firefox-launcher": "~0.1.3",
"karma-html2js-preprocessor": "~0.1.0", "karma-html2js-preprocessor": "~0.1.0",
"karma-jasmine": "~0.2.0", "karma-jasmine": "^1.0.2",
"karma-phantomjs-launcher": "~0.1.1", "karma-phantomjs-launcher": "~0.1.1",
"karma-requirejs": "~0.2.1", "karma-requirejs": "~0.2.1",
"karma-script-launcher": "~0.1.0", "karma-script-launcher": "~0.1.0",

View file

@ -1,8 +1,8 @@
// Follow up on Close.io leads // Follow up on Close.io leads
'use strict'; 'use strict';
if (process.argv.length !== 6) { if (process.argv.length !== 7) {
log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <mongo connection Url>"); log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Close.io mail API key3> <mongo connection Url>");
process.exit(); process.exit();
} }
@ -20,8 +20,8 @@ const demoRequestEmailTemplatesAuto2 = ['tmpl_HJ5zebh1SqC1QydDto05VPUMu4F7i5M35L
const scriptStartTime = new Date(); const scriptStartTime = new Date();
const closeIoApiKey = process.argv[2]; const closeIoApiKey = process.argv[2];
const closeIoMailApiKeys = [process.argv[3], process.argv[4]]; // Automatic mails sent as API owners const closeIoMailApiKeys = [process.argv[3], process.argv[4], process.argv[5]]; // Automatic mails sent as API owners
const mongoConnUrl = process.argv[5]; const mongoConnUrl = process.argv[6];
const MongoClient = require('mongodb').MongoClient; const MongoClient = require('mongodb').MongoClient;
const async = require('async'); const async = require('async');
const request = require('request'); const request = require('request');
@ -44,11 +44,6 @@ async.series([
// ** Utilities // ** Utilities
function getRandomEmailApiKey() {
if (closeIoMailApiKeys.length < 0) return;
return closeIoMailApiKeys[Math.floor(Math.random() * closeIoMailApiKeys.length)];
}
function getRandomEmailTemplate(templates) { function getRandomEmailTemplate(templates) {
if (templates.length < 0) return ''; if (templates.length < 0) return '';
return templates[Math.floor(Math.random() * templates.length)]; return templates[Math.floor(Math.random() * templates.length)];

View file

@ -0,0 +1,38 @@
// Unset someone with a teacher role. Remove trial requests, set role to student or nothing
// depending on if they're in any classrooms.
// Usage
// ---------------
// In mongo shell
//
// > db.loadServerScripts();
// > deteacher('some@email.com');
var deteacher = function deteacher(email) {
var user = db.users.findOne({emailLower: email.toLowerCase()});
if (!user) {
print('User not found');
return;
}
print('Found user', user.name, user.email, user.role, user._id);
var trialRequests = db.trial.requests.find({applicant: user._id}).toArray();
for (var index in trialRequests) {
var trialRequest = trialRequests[index];
print('Delete trial request', JSON.stringify(trialRequest, null, ' '), db.trial.requests.remove({_id: trialRequest._id}, true));
}
var classroomCount = db.classrooms.count({members: user._id});
if (classroomCount > 0) {
print('Set to student', db.users.update({_id: user._id}, {$set: {role: 'student'}}));
}
else {
print('Unset role', db.users.update({_id: user._id}, {$unset: {role: ''}}));
}
}
db.system.js.save(
{
_id: 'deteacher',
value: deteacher
}
)

View file

@ -0,0 +1,46 @@
// Migrate users from coursePrepaidID to coursePrepaid
startDate = new Date(Date.UTC(2016,4,15)).toISOString(); // NOTE: Month is 0 indexed...
endDate = new Date(Date.UTC(2017,5,1)).toISOString();
cutoffDate = new Date(2015,11,11);
cutoffID = ObjectId(Math.floor(cutoffDate/1000).toString(16)+'0000000000000000');
print('Setting start/end', startDate, endDate, cutoffID);
var cursor = db.prepaids.find({type: 'course', _id: { $gt: cutoffID }})
cursor.forEach(function (prepaid) {
var properties = prepaid.properties || {};
if (!(prepaid.endDate && prepaid.startDate)) {
if (!prepaid.endDate) {
if(properties.endDate) {
print('Updating from existing end date', properties.endDate);
prepaid.endDate = properties.endDate.toISOString();
}
else {
prepaid.endDate = endDate;
}
}
if (!prepaid.startDate) {
prepaid.startDate = startDate;
}
print('updating prepaid', prepaid._id, 'creator', prepaid.creator, 'start/end', prepaid.startDate, prepaid.endDate);
print(' -', db.prepaids.save(prepaid));
}
var redeemers = prepaid.redeemers || [];
for (var index in redeemers) {
var redeemer = redeemers[index];
var user = db.users.findOne({ _id: redeemer.userID }, { coursePrepaid: 1, coursePrepaidID: 1, email:1, name:1, permissions: 1 });
if (user.coursePrepaidID && !user.coursePrepaid) {
var update = {
$set: { coursePrepaid: { _id: user.coursePrepaidID, startDate: prepaid.startDate, endDate: prepaid.endDate } },
$unset: { coursePrepaidID: '' }
}
print('\t updating user', user._id, user.name, user.email, user.permissions, JSON.stringify(update));
print('\t', db.users.update({_id: user._id}, update));
}
}
});

View file

@ -1,8 +1,8 @@
// Upsert new lead data into Close.io // Upsert new lead data into Close.io
'use strict'; 'use strict';
if (process.argv.length !== 7) { if (process.argv.length !== 9) {
log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Intercom 'App ID:API key'> <mongo connection Url>"); log("Usage: node <script> <Close.io general API key> <Close.io mail API key1> <Close.io mail API key2> <Close.io mail API key3> <Close.io EU mail API key> <Intercom 'App ID:API key'> <mongo connection Url>");
process.exit(); process.exit();
} }
@ -13,6 +13,7 @@ if (process.argv.length !== 7) {
// TODO: Use generators and promises // TODO: Use generators and promises
// TODO: Reduce response data via _fields param // TODO: Reduce response data via _fields param
// TODO: Assumes 1:1 contact:email relationship (Close.io supports multiple emails for a single contact) // TODO: Assumes 1:1 contact:email relationship (Close.io supports multiple emails for a single contact)
// TODO: Cleanup country/status lookup code
// Save as custom fields instead of user-specific lead notes (also saving nces_ props) // Save as custom fields instead of user-specific lead notes (also saving nces_ props)
const commonTrialProperties = ['organization', 'city', 'state', 'country']; const commonTrialProperties = ['organization', 'city', 'state', 'country'];
@ -30,6 +31,10 @@ const leadsToSkip = ['6 sınıflar', 'fdsafd', 'ashtasht', 'matt+20160404teacher
const createTeacherEmailTemplatesAuto1 = ['tmpl_i5bQ2dOlMdZTvZil21bhTx44JYoojPbFkciJ0F560mn', 'tmpl_CEZ9PuE1y4PRvlYiKB5kRbZAQcTIucxDvSeqvtQW57G']; const createTeacherEmailTemplatesAuto1 = ['tmpl_i5bQ2dOlMdZTvZil21bhTx44JYoojPbFkciJ0F560mn', 'tmpl_CEZ9PuE1y4PRvlYiKB5kRbZAQcTIucxDvSeqvtQW57G'];
const demoRequestEmailTemplatesAuto1 = ['tmpl_s7BZiydyCHOMMeXAcqRZzqn0fOtk0yOFlXSZ412MSGm', 'tmpl_cGb6m4ssDvqjvYd8UaG6cacvtSXkZY3vj9b9lSmdQrf']; const demoRequestEmailTemplatesAuto1 = ['tmpl_s7BZiydyCHOMMeXAcqRZzqn0fOtk0yOFlXSZ412MSGm', 'tmpl_cGb6m4ssDvqjvYd8UaG6cacvtSXkZY3vj9b9lSmdQrf'];
const createTeacherInternationalEmailTemplateAuto1 = 'tmpl_8vsXwcr6dWefMnAEfPEcdHaxqSfUKUY8UKq6WfReGqG';
const demoRequestInternationalEmailTemplateAuto1 = 'tmpl_nnH1p3II7G7NJYiPOIHphuj4XUaDptrZk1mGQb2d9Xa';
const createTeacherNlEmailTemplatesAuto1 = ['tmpl_yf9tAPasz8KV7L414GhWWIclU8ewclh3Z8lCx2mCoIU', 'tmpl_OgPCV2p59uq0daVuUPF6r1rcQkxJbViyZ1ZMtW45jY8'];
const demoRequestNlEmailTemplatesAuto1 = ['tmpl_XGKyZm6gcbqZ5jirt7A54Vu8p68cLxAsKZtb9QBABUE', 'tmpl_xcfgQjUHPa6LLsbPWuPvEUElFXHmIpLa4IZEybJ0b0u'];
// Prioritized Close.io lead status match list // Prioritized Close.io lead status match list
const closeIoInitialLeadStatuses = [ const closeIoInitialLeadStatuses = [
@ -41,18 +46,27 @@ const closeIoInitialLeadStatuses = [
{status: 'Inbound International Auto Attempt 1', regex: /^[A-Za-z]{2}$|\.[A-Za-z]{2}$/}, {status: 'Inbound International Auto Attempt 1', regex: /^[A-Za-z]{2}$|\.[A-Za-z]{2}$/},
{status: 'Auto Attempt 1', regex: /^[A-Za-z]*$/} {status: 'Auto Attempt 1', regex: /^[A-Za-z]*$/}
]; ];
const defaultLeadStatus = 'Auto Attempt 1';
const defaultInternationalLeadStatus = 'Inbound International Auto Attempt 1';
const defaultEuLeadStatus = 'Inbound EU Auto Attempt 1';
const usSchoolStatuses = ['Auto Attempt 1', 'New US Schools Auto Attempt 1', 'New US Schools Auto Attempt 1 Low'];
const emailDelayMinutes = 27; const emailDelayMinutes = 27;
const scriptStartTime = new Date(); const scriptStartTime = new Date();
const closeIoApiKey = process.argv[2]; const closeIoApiKey = process.argv[2];
const closeIoMailApiKeys = [process.argv[3], process.argv[4]]; // Automatic mails sent as API owners // Automatic mails sent as API owners, first key assumed to be primary and gets 50% of the leads
const intercomAppIdApiKey = process.argv[5]; const closeIoMailApiKeys = [process.argv[3], process.argv[3], process.argv[4], process.argv[5]];
const closeIoEuMailApiKey = process.argv[6];
const intercomAppIdApiKey = process.argv[7];
const intercomAppId = intercomAppIdApiKey.split(':')[0]; const intercomAppId = intercomAppIdApiKey.split(':')[0];
const intercomApiKey = intercomAppIdApiKey.split(':')[1]; const intercomApiKey = intercomAppIdApiKey.split(':')[1];
const mongoConnUrl = process.argv[6]; const mongoConnUrl = process.argv[8];
const MongoClient = require('mongodb').MongoClient; const MongoClient = require('mongodb').MongoClient;
const async = require('async'); const async = require('async');
const countryData = require('country-data');
const countryList = require('country-list')();
const parseDomain = require('parse-domain'); const parseDomain = require('parse-domain');
const request = require('request'); const request = require('request');
@ -90,14 +104,60 @@ function upsertLeads(done) {
// ** Utilities // ** Utilities
function getInitialLeadStatusViaCountry(country, trialRequests) { function getCountryCode(country, emails) {
if (/usa|america|united states/ig.test(country)) { // console.log(`DEBUG: getCountryCode ${country} ${emails.length}`);
const status = 'New US Schools Auto Attempt 1' if (country) {
return isLowValueLead(status, trialRequests) ? `${status} Low` : status; let countryCode = countryList.getCode(country);
if (countryCode) return countryCode;
}
for (const email of emails) {
const tld = parseDomain(email).tld;
if (tld) {
const matches = /^[A-Za-z]*\.?([A-Za-z]{2})$/ig.exec(tld);
if (matches && matches.length === 2) {
return matches[1].toUpperCase();
}
}
} }
} }
function getInitialLeadStatusViaCountry(country, trialRequests) {
// console.log(`DEBUG: getInitialLeadStatusViaCountry ${country} ${trialRequests.length}`);
if (/^u\.s\.?(\.a)?\.?$|^us$|usa|america|united states/ig.test(country)) {
const status = 'New US Schools Auto Attempt 1'
return isLowValueUsLead(status, trialRequests) ? `${status} Low` : status;
}
const highValueLead = isHighValueLead(trialRequests);
if (/^england$|^uk$|^united kingdom$/ig.test(country) && highValueLead) {
return 'Inbound UK Auto Attempt 1';
}
if (/^ca$|^canada$/ig.test(country)) {
return 'Inbound Canada Auto Attempt 1';
}
if (/^au$|^australia$/ig.test(country)) {
return 'Inbound AU Auto Attempt 1';
}
if (/^nz$|^new zealand$/ig.test(country)) {
return 'Inbound AU Auto Attempt 1';
}
if (/bolivia|iran|korea|macedonia|taiwan|tanzania|^venezuela$/ig.test(country)) {
return defaultInternationalLeadStatus;
}
const countryCode = countryList.getCode(country);
if (countryCode) {
if (countryCode === 'NL' || countryCode === 'BE') {
return defaultEuLeadStatus;
}
if (isEuCountryCode(countryCode)) {
return highValueLead ? 'Inbound EU Auto Attempt 1 High' : defaultEuLeadStatus;
}
return defaultInternationalLeadStatus;
}
return null;
}
function getInitialLeadStatusViaEmails(emails, trialRequests) { function getInitialLeadStatusViaEmails(emails, trialRequests) {
// console.log(`DEBUG: getInitialLeadStatusViaEmails ${emails.length} ${trialRequests.length}`);
let currentStatus = null; let currentStatus = null;
let currentRank = closeIoInitialLeadStatuses.length; let currentRank = closeIoInitialLeadStatuses.length;
for (const email of emails) { for (const email of emails) {
@ -110,16 +170,45 @@ function getInitialLeadStatusViaEmails(emails, trialRequests) {
} }
} }
} }
currentStatus = currentStatus ? currentStatus : closeIoInitialLeadStatuses[closeIoInitialLeadStatuses.length - 1].status; if (!currentStatus || [defaultLeadStatus, defaultInternationalLeadStatus].indexOf(currentStatus) >= 0) {
return isLowValueLead(currentStatus, trialRequests) ? `${currentStatus} Low` : currentStatus; // Look for a better EU match
const countryCode = getCountryCode(null, emails);
if (countryCode === 'NL' || countryCode === 'BE') {
return defaultEuLeadStatus;
}
if (isEuCountryCode(countryCode)) {
return isHighValueLead(trialRequests) ? 'Inbound EU Auto Attempt 1 High' : defaultEuLeadStatus;
}
}
currentStatus = currentStatus ? currentStatus : defaultLeadStatus;
return isLowValueUsLead(currentStatus, trialRequests) ? `${currentStatus} Low` : currentStatus;
} }
function isLowValueLead(status, trialRequests) { function isEuCountryCode(countryCode) {
if (['Auto Attempt 1', 'New US Schools Auto Attempt 1'].indexOf(status) >= 0) { if (countryData.regions.northernEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
if (countryData.regions.southernEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
if (countryData.regions.easternEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
if (countryData.regions.westernEurope.countries.indexOf(countryCode) >= 0) {
return true;
}
return false;
}
function isLowValueUsLead(status, trialRequests) {
if (isUSSchoolStatus(status)) {
for (const trialRequest of trialRequests) { for (const trialRequest of trialRequests) {
if (parseInt(trialRequest.properties.nces_district_students) < 5000) { if (parseInt(trialRequest.properties.nces_district_students) < 5000) {
return true; return true;
} }
else if (parseInt(trialRequest.properties.nces_district_students) >= 5000) {
return false;
}
} }
for (const trialRequest of trialRequests) { for (const trialRequest of trialRequests) {
// Must match these values: https://github.com/codecombat/codecombat/blob/master/app/templates/teachers/request-quote-view.jade#L159 // Must match these values: https://github.com/codecombat/codecombat/blob/master/app/templates/teachers/request-quote-view.jade#L159
@ -131,7 +220,22 @@ function isLowValueLead(status, trialRequests) {
return false; return false;
} }
function getRandomEmailApiKey() { function isHighValueLead(trialRequests) {
for (const trialRequest of trialRequests) {
// Must match these values: https://github.com/codecombat/codecombat/blob/master/app/templates/teachers/request-quote-view.jade#L159
if (['5,000-10,000', '10,000+'].indexOf(trialRequest.properties.numStudentsTotal) >= 0) {
return true;
}
}
return false;
}
function isUSSchoolStatus(status) {
return usSchoolStatuses.indexOf(status) >= 0;
}
function getEmailApiKey(leadStatus) {
if (leadStatus === defaultEuLeadStatus) return closeIoEuMailApiKey;
if (closeIoMailApiKeys.length < 0) return; if (closeIoMailApiKeys.length < 0) return;
return closeIoMailApiKeys[Math.floor(Math.random() * closeIoMailApiKeys.length)]; return closeIoMailApiKeys[Math.floor(Math.random() * closeIoMailApiKeys.length)];
} }
@ -141,7 +245,30 @@ function getRandomEmailTemplate(templates) {
return templates[Math.floor(Math.random() * templates.length)]; return templates[Math.floor(Math.random() * templates.length)];
} }
function getEmailTemplate(siteOrigin, leadStatus, countryCode) {
// console.log(`DEBUG: getEmailTemplate ${siteOrigin} ${leadStatus} ${countryCode}`);
if (isUSSchoolStatus(leadStatus)) {
if (['create teacher', 'convert teacher'].indexOf(siteOrigin) >= 0) {
return getRandomEmailTemplate(createTeacherEmailTemplatesAuto1);
}
return getRandomEmailTemplate(demoRequestEmailTemplatesAuto1);
}
if (leadStatus === defaultEuLeadStatus && (countryCode === 'NL' || countryCode === 'BE')) {
if (['create teacher', 'convert teacher'].indexOf(siteOrigin) >= 0) {
return getRandomEmailTemplate(createTeacherNlEmailTemplatesAuto1);
}
return getRandomEmailTemplate(demoRequestNlEmailTemplatesAuto1);
}
if (['create teacher', 'convert teacher'].indexOf(siteOrigin) >= 0) {
return createTeacherInternationalEmailTemplateAuto1;
}
return demoRequestInternationalEmailTemplateAuto1;
}
function isSameEmailTemplateType(template1, template2) { function isSameEmailTemplateType(template1, template2) {
if (template1 == template2) {
return true;
}
if (createTeacherEmailTemplatesAuto1.indexOf(template1) >= 0 && createTeacherEmailTemplatesAuto1.indexOf(template2) >= 0) { if (createTeacherEmailTemplatesAuto1.indexOf(template1) >= 0 && createTeacherEmailTemplatesAuto1.indexOf(template2) >= 0) {
return true; return true;
} }
@ -348,8 +475,7 @@ class CocoLead {
} }
for (const prop in props) { for (const prop in props) {
// Always overwrite common props if we have NCES data, because other fields more likely to be accurate // Always overwrite common props if we have NCES data, because other fields more likely to be accurate
if (commonTrialProperties.indexOf(prop) >= 0 if (commonTrialProperties.indexOf(prop) >= 0 && (haveNcesData || !currentCustom[`demo_${prop}`] || currentCustom[`demo_${prop}`] !== props[prop] && currentCustom[`demo_${prop}`].indexOf(props[prop]) < 0)) {
&& (haveNcesData || currentCustom[`demo_${prop}`] !== props[prop] && currentCustom[`demo_${prop}`].indexOf(props[prop]) < 0)) {
putData[`custom.demo_${prop}`] = props[prop]; putData[`custom.demo_${prop}`] = props[prop];
} }
} }
@ -552,12 +678,9 @@ function saveNewLead(lead, done) {
const tasks = []; const tasks = [];
for (const contact of existingLead.contacts) { for (const contact of existingLead.contacts) {
for (const email of contact.emails) { for (const email of contact.emails) {
if (['create teacher', 'convert teacher'].indexOf(lead.contacts[email.email].trial.properties.siteOrigin) >= 0) { const countryCode = getCountryCode(lead.contacts[email.email].trial.properties.country, [email.email]);
tasks.push(createSendEmailFn(email.email, existingLead.id, contact.id, getRandomEmailTemplate(createTeacherEmailTemplatesAuto1))); const emailTemplate = getEmailTemplate(lead.contacts[email.email].trial.properties.siteOrigin, postData.status, countryCode);
} tasks.push(createSendEmailFn(email.email, existingLead.id, contact.id, emailTemplate, postData.status));
else {
tasks.push(createSendEmailFn(email.email, existingLead.id, contact.id, getRandomEmailTemplate(demoRequestEmailTemplatesAuto1)));
}
} }
} }
async.parallel(tasks, (err, results) => { async.parallel(tasks, (err, results) => {
@ -620,7 +743,7 @@ function createUpdateLeadFn(lead, existingLeads) {
return updateExistingLead(lead, data.data[0], done); return updateExistingLead(lead, data.data[0], done);
} catch (error) { } catch (error) {
// console.log(url); // console.log(url);
// console.log(error); console.log(`ERROR: updateLead ${error}`);
// console.log(body); // console.log(body);
return done(); return done();
} }
@ -646,12 +769,9 @@ function createAddContactFn(postData, internalLead, externalLead) {
// Send emails to new contact // Send emails to new contact
const email = postData.emails[0].email; const email = postData.emails[0].email;
if (['create teacher', 'convert teacher'].indexOf(internalLead.contacts[email].trial.properties.siteOrigin) >= 0) { const countryCode = getCountryCode(internalLead.contacts[email].trial.properties.country, [email]);
return sendMail(email, externalLead.id, newContact.id, getRandomEmailTemplate(createTeacherEmailTemplatesAuto1), getRandomEmailApiKey(), emailDelayMinutes, done); const emailTemplate = getEmailTemplate(internalLead.contacts[email].trial.properties.siteOrigin, externalLead.status_label);
} sendMail(email, externalLead.id, newContact.id, emailTemplate, getEmailApiKey(externalLead.status_label), emailDelayMinutes, done);
else {
return sendMail(email, externalLead.id, newContact.id, getRandomEmailTemplate(demoRequestEmailTemplatesAuto1), getRandomEmailApiKey(), emailDelayMinutes, done);
}
}); });
}; };
} }
@ -680,9 +800,9 @@ function createAddNoteFn(leadId, newNote) {
}; };
} }
function createSendEmailFn(email, leadId, contactId, template) { function createSendEmailFn(email, leadId, contactId, template, leadStatus) {
return (done) => { return (done) => {
return sendMail(email, leadId, contactId, template, getRandomEmailApiKey(), emailDelayMinutes, done); return sendMail(email, leadId, contactId, template, getEmailApiKey(leadStatus), emailDelayMinutes, done);
}; };
} }

View file

@ -38,7 +38,6 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members' return @removeMember(req, res, args[0]) if req.method is 'DELETE' and args[1] is 'members'
return @getMembersAPI(req, res, args[0]) if args[1] is 'members' return @getMembersAPI(req, res, args[0]) if args[1] is 'members'
return @inviteStudents(req, res, args[0]) if relationship is 'invite_students' return @inviteStudents(req, res, args[0]) if relationship is 'invite_students'
return @getRecentAPI(req, res) if relationship is 'recent'
return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid' return @redeemPrepaidCodeAPI(req, res) if args[1] is 'redeem_prepaid'
return @getMyCourseLevelSessionsAPI(req, res, args[0]) if args[1] is 'my-course-level-sessions' return @getMyCourseLevelSessionsAPI(req, res, args[0]) if args[1] is 'my-course-level-sessions'
return @findByLevel(req, res, args[2]) if args[1] is 'find_by_level' return @findByLevel(req, res, args[2]) if args[1] is 'find_by_level'
@ -169,33 +168,6 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
cleandocs = (UserHandler.formatEntity(req, doc) for doc in users) cleandocs = (UserHandler.formatEntity(req, doc) for doc in users)
@sendSuccess(res, cleandocs) @sendSuccess(res, cleandocs)
getRecentAPI: (req, res) ->
return @sendUnauthorizedError(res) unless req.user?.isAdmin()
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
query["$and"].push(_id: {$gte: objectIdFromTimestamp(req.body.startDay + "T00:00:00.000Z")}) if req.body.startDay?
query["$and"].push(_id: {$lt: objectIdFromTimestamp(req.body.endDay + "T00:00:00.000Z")}) if req.body.endDay?
CourseInstance.find query, {courseID: 1, members: 1, ownerID: 1}, (err, courseInstances) =>
return @sendDatabaseError(res, err) if err
userIDs = []
for courseInstance in courseInstances
if members = courseInstance.get('members')
userIDs.push(userID) for userID in members
User.find {_id: {$in: userIDs}}, {coursePrepaidID: 1}, (err, users) =>
return @sendDatabaseError(res, err) if err
prepaidIDs = []
for user in users
if prepaidID = user.get('coursePrepaidID')
prepaidIDs.push(prepaidID)
Prepaid.find {_id: {$in: prepaidIDs}}, {properties: 1}, (err, prepaids) =>
return @sendDatabaseError(res, err) if err
data =
courseInstances: (@formatEntity(req, courseInstance) for courseInstance in courseInstances)
students: (@formatEntity(req, user) for user in users)
prepaids: (@formatEntity(req, prepaid) for prepaid in prepaids)
@sendSuccess(res, data)
inviteStudents: (req, res, courseInstanceID) -> inviteStudents: (req, res, courseInstanceID) ->
return @sendUnauthorizedError(res) if not req.user? return @sendUnauthorizedError(res) if not req.user?
if not req.body.emails if not req.body.emails

View file

@ -16,7 +16,7 @@ class LevelSessionHandler extends Handler
formatEntity: (req, document) -> formatEntity: (req, document) ->
document = super(req, document) document = super(req, document)
submittedCode = document.submittedCode submittedCode = document.submittedCode ? {}
unless req.user?.isAdmin() or unless req.user?.isAdmin() or
req.user?.id is document.creator or req.user?.id is document.creator or
('employer' in (req.user?.get('permissions') ? [])) or ('employer' in (req.user?.get('permissions') ? [])) or

View file

@ -30,7 +30,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
return @getPrepaidAPI(req, res, args[2]) if relationship is 'code' return @getPrepaidAPI(req, res, args[2]) if relationship is 'code'
return @createPrepaidAPI(req, res) if relationship is 'create' return @createPrepaidAPI(req, res) if relationship is 'create'
return @purchasePrepaidAPI(req, res) if relationship is 'purchase' return @purchasePrepaidAPI(req, res) if relationship is 'purchase'
return @postRedeemerAPI(req, res, args[0]) if relationship is 'redeemers'
super arguments... super arguments...
getCoursePrepaidsAPI: (req, res, code) -> getCoursePrepaidsAPI: (req, res, code) ->
@ -78,45 +77,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
@sendSuccess(res, prepaid.toObject()) @sendSuccess(res, prepaid.toObject())
postRedeemerAPI: (req, res, prepaidID) ->
return @sendForbiddenError(res) if prepaidID.toString() < cutoffID.toString()
return @sendMethodNotAllowed(res, 'You may only POST redeemers.') if req.method isnt 'POST'
return @sendBadInputError(res, 'Need an object with a userID') unless req.body?.userID
Prepaid.findById(prepaidID).exec (err, prepaid) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) if not prepaid
return @sendForbiddenError(res) if prepaid.get('creator').toString() isnt req.user.id
return @sendForbiddenError(res) if prepaid.get('redeemers')? and _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
return @sendForbiddenError(res) unless prepaid.get('type') is 'course'
return @sendForbiddenError(res) if prepaid.get('properties')?.endDate < new Date()
User.findById(req.body.userID).exec (err, user) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res, 'User for given ID not found') if not user
return @sendSuccess(res, @formatEntity(req, prepaid)) if user.get('coursePrepaidID')
return @sendForbiddenError(res, 'Teachers may not be enrolled') if user.isTeacher()
userID = user.get('_id')
query =
_id: prepaid.get('_id')
'redeemers.userID': { $ne: user.get('_id') }
$where: "this.maxRedeemers > 0 && (!this.redeemers || this.redeemers.length < #{prepaid.get('maxRedeemers')})"
update = { $push: { redeemers : { date: new Date(), userID: userID } }}
Prepaid.update query, update, (err, result) =>
return @sendDatabaseError(res, err) if err
if result.nModified is 0
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
return @sendForbiddenError(res)
user.set('coursePrepaidID', prepaid.get('_id'))
user.set('role', 'student') if not user.get('role')
user.save (err, user) =>
return @sendDatabaseError(res, err) if err
# return prepaid with new redeemer added locally
redeemers = _.clone(prepaid.get('redeemers') or [])
redeemers.push({ date: new Date(), userID: userID })
prepaid.set('redeemers', redeemers)
@sendSuccess(res, @formatEntity(req, prepaid))
createPrepaid: (user, type, maxRedeemers, properties, done) -> createPrepaid: (user, type, maxRedeemers, properties, done) ->
Prepaid.generateNewCode (code) => Prepaid.generateNewCode (code) =>
return done('Database error.') unless code return done('Database error.') unless code
@ -254,23 +214,6 @@ PrepaidHandler = class PrepaidHandler extends Handler
slack.sendSlackMessage msg, ['tower'] slack.sendSlackMessage msg, ['tower']
done(null, prepaid) done(null, prepaid)
get: (req, res) ->
if creator = req.query.creator
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or creator is req.user.id)
return @sendBadInputError(res, 'Bad creator') unless utils.isID creator
q = {
_id: {$gt: cutoffID}
creator: mongoose.Types.ObjectId(creator)
type: 'course'
}
Prepaid.find q, (err, prepaids) =>
return @sendDatabaseError(res, err) if err
documents = []
for prepaid in prepaids
documents.push(@formatEntity(req, prepaid)) unless prepaid.get('properties')?.endDate < new Date()
return @sendSuccess(res, documents)
else
super(arguments...)
makeNewInstance: (req) -> makeNewInstance: (req) ->
prepaid = super(req) prepaid = super(req)

View file

@ -43,8 +43,8 @@ UserHandler = class UserHandler extends Handler
props.push 'jobProfileApproved', 'jobProfileNotes','jobProfileApprovedDate' if req.user.isAdmin() # Admins naturally edit these props.push 'jobProfileApproved', 'jobProfileNotes','jobProfileApprovedDate' if req.user.isAdmin() # Admins naturally edit these
props.push @privateProperties... if req.user.isAdmin() # Admins are mad with power props.push @privateProperties... if req.user.isAdmin() # Admins are mad with power
if not req.user.isAdmin() if not req.user.isAdmin()
if document.isTeacher() and req.body.role not in User.teacherRoles if document.isTeacher() and req.body.role not in User.teacherRoles
props = _.without props, 'role' props = _.without props, 'role'
props props
formatEntity: (req, document, publicOnly=false) => formatEntity: (req, document, publicOnly=false) =>

View file

@ -48,3 +48,24 @@ module.exports =
else if leads.data?.length > 1 else if leads.data?.length > 1
return done('ERROR: multiple leads returned for ' + email + ' ' + leads.data.length) return done('ERROR: multiple leads returned for ' + email + ' ' + leads.data.length)
return done() return done()
getSalesContactEmail: (email, done) ->
try
# NOTE: does not work on + email addresses due to Close.io API bug
uri = "https://#{apiKey}:X@app.close.io/api/v1/lead/?query=email_address:#{email}"
request.get uri, (error, response, body) =>
return done(error) if error
leads = JSON.parse(body)
return done("Unexpected leads format: " + body) unless leads.data?
return done(null, config.mail.supportSchools) unless leads.data?.length > 0
lead = leads.data[0]
uri = "https://#{apiKey}:X@app.close.io/api/v1/activity/?lead_id=#{lead.id}"
request.get uri, (error, response, body) =>
return done(error) if error
activities = JSON.parse(body)
return done("Unexpected activities format: " + body) unless activities.data?
for activity in activities.data when activity._type is 'Email'
return done(null, activity.sender) if /@codecombat\.com/ig.test(activity.sender)
return done(null, config.mail.supportSchools)
catch error
return done(error, config.mail.supportSchools)

View file

@ -98,12 +98,7 @@ module.exports =
user = req.body.user user = req.body.user
throw new errors.UnprocessableEntity('Specify an id, username or email to espionage.') unless user throw new errors.UnprocessableEntity('Specify an id, username or email to espionage.') unless user
if utils.isID(user) user = yield User.search(user)
query = {_id: mongoose.Types.ObjectId(user)}
else
user = user.toLowerCase()
query = $or: [{nameLower: user}, {emailLower: user}]
user = yield User.findOne(query)
amActually = req.user amActually = req.user
throw new errors.NotFound() unless user throw new errors.NotFound() unless user
req.loginAsync = Promise.promisify(req.login) req.loginAsync = Promise.promisify(req.login)
@ -210,4 +205,4 @@ module.exports =
if originalName is name if originalName is name
res.send 200, response res.send 200, response
else else
throw new errors.Conflict('Name is taken', response) throw new errors.Conflict('Name is taken', response)

View file

@ -1,6 +1,7 @@
_ = require 'lodash' _ = require 'lodash'
utils = require '../lib/utils' utils = require '../lib/utils'
errors = require '../commons/errors' errors = require '../commons/errors'
schemas = require '../../app/schemas/schemas'
wrap = require 'co-express' wrap = require 'co-express'
Promise = require 'bluebird' Promise = require 'bluebird'
database = require '../commons/database' database = require '../commons/database'
@ -15,6 +16,17 @@ User = require '../models/User'
CourseInstance = require '../models/CourseInstance' CourseInstance = require '../models/CourseInstance'
module.exports = module.exports =
fetchByCode: wrap (req, res, next) ->
code = req.query.code
return next() unless code
classroom = yield Classroom.findOne({ code: code.toLowerCase() }).select('name ownerID aceConfig')
if not classroom
throw new errors.NotFound('Classroom not found.')
classroom = classroom.toObject()
# Tack on the teacher's name for display to the user
owner = (yield User.findOne({ _id: mongoose.Types.ObjectId(classroom.ownerID) }).select('name')).toObject()
res.status(200).send({ data: classroom, owner } )
getByOwner: wrap (req, res, next) -> getByOwner: wrap (req, res, next) ->
options = req.query options = req.query
ownerID = options.ownerID ownerID = options.ownerID
@ -151,7 +163,7 @@ module.exports =
code = req.body.code.toLowerCase() code = req.body.code.toLowerCase()
classroom = yield Classroom.findOne({code: code}) classroom = yield Classroom.findOne({code: code})
if not classroom if not classroom
throw new errors.NotFound(res) throw new errors.NotFound('Classroom not found.')
members = _.clone(classroom.get('members')) members = _.clone(classroom.get('members'))
if _.any(members, (memberID) -> memberID.equals(req.user._id)) if _.any(members, (memberID) -> memberID.equals(req.user._id))
return res.send(classroom.toObject({req: req})) return res.send(classroom.toObject({req: req}))
@ -174,3 +186,22 @@ module.exports =
yield CourseInstance.update({_id: {$in: freeCourseInstanceIDs}}, { $addToSet: { members: req.user._id }}) yield CourseInstance.update({_id: {$in: freeCourseInstanceIDs}}, { $addToSet: { members: req.user._id }})
yield User.update({ _id: req.user._id }, { $addToSet: { courseInstances: { $each: freeCourseInstanceIDs } } }) yield User.update({ _id: req.user._id }, { $addToSet: { courseInstances: { $each: freeCourseInstanceIDs } } })
res.send(classroom.toObject({req: req})) res.send(classroom.toObject({req: req}))
setStudentPassword: wrap (req, res, next) ->
newPassword = req.body.password
{ classroomID, memberID } = req.params
teacherID = req.user.id
return next() if teacherID is memberID or not newPassword
ownedClassrooms = yield Classroom.find({ ownerID: mongoose.Types.ObjectId(teacherID) })
ownedStudentIDs = _.flatten ownedClassrooms.map (c) ->
c.get('members').map (id) ->
id.toString()
return next() unless memberID in ownedStudentIDs
student = yield User.findById(memberID)
if student.get('emailVerified')
return next new errors.Forbidden("Can't reset password for a student that has verified their email address.")
{ valid, error } = tv4.validateResult(newPassword, schemas.passwordString)
unless valid
throw new errors.UnprocessableEntity(error.message)
yield student.update({ $set: { passwordHash: User.hashPassword(newPassword) } })
res.status(200).send({})

View file

@ -10,6 +10,8 @@ Course = require '../models/Course'
User = require '../models/User' User = require '../models/User'
Level = require '../models/Level' Level = require '../models/Level'
parse = require '../commons/parse' parse = require '../commons/parse'
{objectIdFromTimestamp} = require '../lib/utils'
Prepaid = require '../models/Prepaid'
module.exports = module.exports =
addMembers: wrap (req, res) -> addMembers: wrap (req, res) ->
@ -42,13 +44,13 @@ module.exports =
throw new errors.Forbidden('You must own the classroom to add members') throw new errors.Forbidden('You must own the classroom to add members')
# Only the enrolled users # Only the enrolled users
users = yield User.find({ _id: { $in: userIDs }}).select('coursePrepaidID') users = yield User.find({ _id: { $in: userIDs }}).select('coursePrepaid coursePrepaidID') # TODO: remove coursePrepaidID once migrated
usersArePrepaid = _.all((user.get('coursePrepaidID') for user in users)) usersAreEnrolled = _.all((user.isEnrolled() for user in users))
course = yield Course.findById courseInstance.get('courseID') course = yield Course.findById courseInstance.get('courseID')
throw new errors.NotFound('Course referenced by course instance not found') unless course throw new errors.NotFound('Course referenced by course instance not found') unless course
if not (course.get('free') or usersArePrepaid) if not (course.get('free') or usersAreEnrolled)
throw new errors.PaymentRequired('Cannot add users to a course instance until they are added to a prepaid') throw new errors.PaymentRequired('Cannot add users to a course instance until they are added to a prepaid')
userObjectIDs = (mongoose.Types.ObjectId(userID) for userID in userIDs) userObjectIDs = (mongoose.Types.ObjectId(userID) for userID in userIDs)
@ -96,7 +98,7 @@ module.exports =
throw new errors.NotFound('Level original ObjectId not found in Classroom courses') throw new errors.NotFound('Level original ObjectId not found in Classroom courses')
if not nextLevelOriginal if not nextLevelOriginal
res.status(200).send({}) return res.status(200).send({})
dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)}) dbq = Level.findOne({original: mongoose.Types.ObjectId(nextLevelOriginal)})
dbq.sort({ 'version.major': -1, 'version.minor': -1 }) dbq.sort({ 'version.major': -1, 'version.minor': -1 })
@ -123,3 +125,28 @@ module.exports =
classroom = classroom.toObject({req: req}) classroom = classroom.toObject({req: req})
res.status(200).send(classroom) res.status(200).send(classroom)
fetchRecent: wrap (req, res) ->
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
query["$and"].push(_id: {$gte: objectIdFromTimestamp(req.body.startDay + "T00:00:00.000Z")}) if req.body.startDay?
query["$and"].push(_id: {$lt: objectIdFromTimestamp(req.body.endDay + "T00:00:00.000Z")}) if req.body.endDay?
courseInstances = yield CourseInstance.find(query, {courseID: 1, members: 1, ownerID: 1})
userIDs = []
for courseInstance in courseInstances
if members = courseInstance.get('members')
userIDs.push(userID) for userID in members
users = yield User.find({_id: {$in: userIDs}}, {coursePrepaid: 1, coursePrepaidID: 1})
prepaidIDs = []
for user in users
if prepaidID = user.get('coursePrepaid')
prepaidIDs.push(prepaidID._id)
prepaids = yield Prepaid.find({_id: {$in: prepaidIDs}}, {properties: 1})
res.send({
courseInstances: (courseInstance.toObject({req: req}) for courseInstance in courseInstances)
students: (user.toObject({req: req}) for user in users)
prepaids: (prepaid.toObject({req: req}) for prepaid in prepaids)
})

View file

@ -8,8 +8,10 @@ module.exports =
courses: require './courses' courses: require './courses'
files: require './files' files: require './files'
healthcheck: require './healthcheck' healthcheck: require './healthcheck'
levels: require './levels'
named: require './named' named: require './named'
patchable: require './patchable' patchable: require './patchable'
prepaids: require './prepaids'
rest: require './rest' rest: require './rest'
trialRequests: require './trial-requests' trialRequests: require './trial-requests'
users: require './users' users: require './users'

View file

@ -0,0 +1,84 @@
wrap = require 'co-express'
errors = require '../commons/errors'
Level = require '../models/Level'
LevelSession = require '../models/LevelSession'
CourseInstance = require '../models/CourseInstance'
Classroom = require '../models/Classroom'
Course = require '../models/Course'
database = require '../commons/database'
module.exports =
upsertSession: wrap (req, res) ->
level = yield database.getDocFromHandle(req, Level)
if not level
throw new errors.NotFound('Level not found.')
sessionQuery =
level:
original: level.get('original').toString()
majorVersion: level.get('version').major
creator: req.user.id
if req.query.team?
sessionQuery.team = req.query.team
session = yield LevelSession.findOne(sessionQuery)
if session
return res.send(session.toObject({req: req}))
attrs = sessionQuery
_.extend(attrs, {
state:
complete: false
scripts:
currentScript: null # will not save empty objects
permissions: [
{target: req.user.id, access: 'owner'}
{target: 'public', access: 'write'}
]
codeLanguage: req.user.get('aceConfig')?.language ? 'python'
})
if level.get('type') in ['course', 'course-ladder'] or req.query.course?
# Find the course and classroom that has assigned this level, verify access
courseInstances = yield CourseInstance.find({members: req.user._id})
classroomIDs = (courseInstance.get('classroomID') for courseInstance in courseInstances)
classroomIDs = _.filter _.uniq classroomIDs, false, (objectID='') -> objectID.toString()
classrooms = yield Classroom.find({ _id: { $in: classroomIDs }})
classroomWithLevel = null
courseID = null
classroomMap = {}
classroomMap[classroom.id] = classroom for classroom in classrooms
levelOriginal = level.get('original')
for courseInstance in courseInstances
classroomID = courseInstance.get('classroomID')
continue unless classroomID
classroom = classroomMap[classroomID.toString()]
courseID = courseInstance.get('courseID')
classroomCourse = _.find(classroom.get('courses'), (c) -> c._id.equals(courseID))
for courseLevel in classroomCourse.levels
if courseLevel.original.equals(levelOriginal)
classroomWithLevel = classroom
break
break if classroomWithLevel
unless classroomWithLevel
throw new errors.PaymentRequired('You must be in a course which includes this level to play it')
course = yield Course.findById(courseID).select('free')
unless course.get('free') or req.user.isEnrolled()
throw new errors.PaymentRequired('You must be enrolled to access this content')
lang = classroomWithLevel.get('aceConfig')?.language
attrs.codeLanguage = lang if lang
else
requiresSubscription = level.get('requiresSubscription') or (req.user.isOnPremiumServer() and level.get('campaign') and not (level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'forgetful-gemsmith', 'signs-and-portents', 'true-names']))
canPlayAnyway = req.user.isPremium() or level.get 'adventurer'
if requiresSubscription and not canPlayAnyway
throw new errors.PaymentRequired('This level requires a subscription to play')
session = new LevelSession(attrs)
yield session.save()
res.send(session.toObject({req: req}))

View file

@ -0,0 +1,110 @@
wrap = require 'co-express'
errors = require '../commons/errors'
database = require '../commons/database'
Prepaid = require '../models/Prepaid'
User = require '../models/User'
mongoose = require 'mongoose'
cutoffDate = new Date(2015,11,11)
cutoffID = mongoose.Types.ObjectId(Math.floor(cutoffDate/1000).toString(16)+'0000000000000000')
module.exports =
logError: (user, msg) ->
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
post: wrap (req, res) ->
validTypes = ['course']
unless req.body.type in validTypes
throw new errors.UnprocessableEntity("type must be on of: #{validTypes}.")
# TODO: deprecate or refactor other prepaid types
if req.body.creator
user = yield User.search(req.body.creator)
if not user
throw new errors.NotFound('User not found')
req.body.creator = user.id
prepaid = database.initDoc(req, Prepaid)
database.assignBody(req, prepaid)
prepaid.set('code', yield Prepaid.generateNewCodeAsync())
prepaid.set('redeemers', [])
database.validateDoc(prepaid)
yield prepaid.save()
res.status(201).send(prepaid.toObject())
redeem: wrap (req, res) ->
if not req.user?.isTeacher()
throw new errors.Forbidden('Must be a teacher to use licenses')
prepaid = yield database.getDocFromHandle(req, Prepaid)
if not prepaid
throw new errors.NotFound('Prepaid not found.')
if prepaid._id.getTimestamp().getTime() < cutoffDate.getTime()
throw new errors.Forbidden('Cannot redeem from prepaids older than November 11, 2015')
unless prepaid.get('creator').equals(req.user._id)
throw new errors.Forbidden('You may not redeem licenses from this prepaid')
if prepaid.get('redeemers')? and _.size(prepaid.get('redeemers')) >= prepaid.get('maxRedeemers')
throw new errors.Forbidden('This prepaid is exhausted')
unless prepaid.get('type') is 'course'
throw new errors.Forbidden('This prepaid is not of type "course"')
if prepaid.get('endDate') and new Date(prepaid.get('endDate')) < new Date()
throw new errors.Forbidden('This prepaid is expired')
user = yield User.findById(req.body?.userID)
if not user
throw new errors.NotFound('User not found.')
if user.isEnrolled()
return res.status(200).send(prepaid.toObject({req: req}))
if user.isTeacher()
throw new errors.Forbidden('Teachers may not be enrolled')
query =
_id: prepaid._id
'redeemers.userID': { $ne: user._id }
$where: "this.maxRedeemers > 0 && (!this.redeemers || this.redeemers.length < #{prepaid.get('maxRedeemers')})"
update = { $push: { redeemers : { date: new Date(), userID: user._id } }}
result = yield Prepaid.update(query, update)
if result.nModified is 0
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
throw new errors.Forbidden('This prepaid is exhausted')
update = {
$set: {
coursePrepaid: {
_id: prepaid._id
startDate: prepaid.get('startDate')
endDate: prepaid.get('endDate')
}
}
}
if not user.get('role')
update.$set.role = 'student'
yield user.update(update)
# return prepaid with new redeemer added locally
redeemers = _.clone(prepaid.get('redeemers') or [])
redeemers.push({ date: new Date(), userID: user._id })
prepaid.set('redeemers', redeemers)
res.status(201).send(prepaid.toObject({req: req}))
fetchByCreator: wrap (req, res, next) ->
creator = req.query.creator
return next() if not creator
unless req.user.isAdmin() or creator is req.user.id
throw new errors.Forbidden('Must be logged in as given creator')
unless database.isID(creator)
throw new errors.UnprocessableEntity('Invalid creator')
q = {
_id: { $gt: cutoffID }
creator: mongoose.Types.ObjectId(creator)
type: 'course'
}
prepaids = yield Prepaid.find(q)
res.send((prepaid.toObject({req: req}) for prepaid in prepaids))

Some files were not shown because too many files have changed in this diff Show more