Merge branch 'master' into production

This commit is contained in:
Matt Lott 2015-10-08 09:44:14 -07:00
commit bb3d9fa05a
21 changed files with 200 additions and 119 deletions

View file

@ -14,7 +14,7 @@ We've made it easy to fork the project, run a simple script that'll install all
### [Getting In Touch](https://github.com/codecombat/codecombat/wiki/Developer-organization)
Whether you're novice or pro, the CodeCombat team is ready to help you implement your ideas. Reach out on our [forum](http://discourse.codecombat.com), our [issue tracker](https://github.com/codecombat/codecombat/issues), or our [developer chat room](https://www.hipchat.com/g3plnOKqa), or see the docs for [more on how to contribute](https://github.com/codecombat/codecombat/wiki/Developer-organization).
Whether you're novice or pro, the CodeCombat team is ready to help you implement your ideas. Reach out on our [forum](http://discourse.codecombat.com), our [issue tracker](https://github.com/codecombat/codecombat/issues), or our [developer chat room](https://www.hipchat.com/gkaufqwnj), or see the docs for [more on how to contribute](https://github.com/codecombat/codecombat/wiki/Developer-organization).
### [License](https://github.com/codecombat/codecombat/blob/master/LICENSE)

View file

@ -65,6 +65,8 @@ module.exports = class CocoRouter extends Backbone.Router
'courses/mock1/enroll/:courseID': go('courses/mock1/CourseEnrollView')
'courses/mock1/:courseID': go('courses/mock1/CourseDetailsView')
'courses': go('courses/CoursesView')
'courses/students': go('courses/CoursesView')
'courses/teachers': go('courses/CoursesView')
'courses/enroll(/:courseID)': go('courses/CourseEnrollView')
'courses/:courseID(/:courseInstanceID)': go('courses/CourseDetailsView')

View file

@ -15,6 +15,7 @@ module.exports.thangNames = thangNames =
'Shmeal'
'Upfish'
'Yugark'
'Shema'
]
'Ogre Munchkin M': [
# Male
@ -50,6 +51,7 @@ module.exports.thangNames = thangNames =
'Weeb'
'Yart'
'Zozo'
'Zock'
]
'Ogre Thrower': [
# Female
@ -90,6 +92,7 @@ module.exports.thangNames = thangNames =
'Taric'
'Vaelia'
'Antary'
'Femae'
]
'Ogre Witch': [
# Female
@ -121,12 +124,14 @@ module.exports.thangNames = thangNames =
'Ganju'
'Hopper'
'Ralthora'
'Yugorota'
]
'Burl': [
# Animal
'Borlit'
'Burlosh'
'Dorf'
'Teemer'
]
'Sand Yak': [
# Animal
@ -468,6 +473,7 @@ module.exports.thangNames = thangNames =
'Warshall'
'Yue Fei'
'Zhou Tong'
'Archy'
]
'Peasant M': [
# Male
@ -497,6 +503,7 @@ module.exports.thangNames = thangNames =
'Winkler'
'Yorik'
'Yusef'
'Yoltovic'
]
'Peasant F': [
# Female
@ -522,6 +529,7 @@ module.exports.thangNames = thangNames =
'Ruth'
'Tabitha'
'Thea'
'Lea'
]
'Soldier M': [
# Male
@ -648,6 +656,7 @@ module.exports.thangNames = thangNames =
'Randy'
'Raymond'
'Remy'
'Rex'
'Ricardo'
'Richard'
'Robert'

View file

@ -294,7 +294,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
infinite_loop_reset_level: "Reiniciar Nivel"
infinite_loop_comment_out: "Comente Mi Código"
tip_toggle_play: "Activa jugar/pausa con Ctrl+P."
tip_scrub_shortcut: "Ctrl+[ y Ctrl+] para rebobinar y avanzar rápido."
tip_scrub_shortcut: "Ctrl+[ y Ctrl+] para rebobinar y avanzar rápido."
tip_guide_exists: "Haga click en la guía en la parte superior de la página para obtener información útil"
tip_open_source: "¡CodeCombat es 100% código abierto!"
tip_tell_friends: "¿Disfrutando de CodeCombat? ¡Cuéntale a tus amigos acerca de nosotros!"
@ -658,17 +658,17 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
sys_requirements_2: "CodeCombat no está soportado en iPad aún."
teachers_survey:
title: "Encuesta para profesores"
must_be_logged: "Debe iniciar sesión primero. Por favor cree una cuenta o inicie sesión desde el menú en la parte superior."
title: "Encuesta de Maestros"
must_be_logged: "Debes ingresar primero. Por favor, crea una cuenta o ingresa desde el menú de arriba."
retrieving: "Obteniendo información..."
being_reviewed_1: "Su solicitud para una prueba gratuita de subscripción está siendo"
being_reviewed_1: "Su aplicación a una suscripción gratuita está siendo"
being_reviewed_2: "revisada."
approved_1: "Su solicitud para una prueba gratuita de subscripción fue"
approved_2: "Aprobada."
approved_3: "Instruccciones posteriores han sido enviadas a"
denied_1: "Su solicitud para una prueba gratuita de subscripción fue"
approved_1: "Su aplicación a una suscripción gratuita fue"
approved_2: "aprobada."
approved_3: "Más instrucciones han sido enviadas a"
denied_1: "Su aplicación a una suscripción gratuita ha sido"
denied_2: "denegada."
contact_1: "Por favor contáctenos"
contact_1: "Por favor contactarse"
contact_2: "si tiene más preguntas."
description_1: "Ofrecemos suscripciones gratuitas a maestros con propósitos de evaluación. Puede hallar más información en nuestra"
description_2: "página"
@ -677,13 +677,13 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
email: "Dirección de email"
school: "Nombre del colegio"
location: "Nombre de la ciudad"
age_students: "¿Qué edad tienen sus estudiantes?"
under: "Menor"
other: "Otro:"
amount_students: "¿A cuantos alumnos les enseña?"
hear_about: "¿Donde escuchó sobre CodeCombat?"
fill_fields: "Porfavor llene todos los campos."
thanks: "Gracias! Vamos a mandarle instrucciónes para iniciar proximamente."
age_students: "¿Qué edad tienen tus estudiantes?"
under: "Bajo"
other: "Otros:"
amount_students: "¿A cuantos alumnos le enseña?"
hear_about: "¿Donde escuchaste acerca de CodeCombat?"
fill_fields: "Por favor llene todos los campos."
thanks: "¡Gracias! Pronto te enviaremos instrucciones para iniciar."
versions:
save_version_title: "Guardar nueva versión"
@ -693,7 +693,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
cla_url: "CLA"
cla_suffix: "."
cla_agree: "ACEPTO"
owner_approve: "Un dueño necesitará aprobarlo antes de que tus cambios puedan ser visibles."
owner_approve: "Necesita la aprobación de un propietario para que los cambios sean visibles."
contact:
contact_us: "Contacta a CodeCombat"
@ -730,8 +730,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
admin: "Admin"
new_password: "Nueva Contraseña"
new_password_verify: "Verificar"
type_in_email: "Ingrese su correo electrónico para confirmar la eliminación" # {change}
type_in_password: "También, escribe tu password."
type_in_email: "Ingrese su correo electrónico para confirmar la eliminación de su cuenta."
type_in_password: "Asimismo, ingrese su contraseña."
email_subscriptions: "Suscripciones de Email"
email_subscriptions_none: "No tienes suscripciones."
email_announcements: "Noticias"
@ -789,9 +789,9 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
article_editor_prefix: "¿Ves algún error en nuestros documentos? ¿Quieres hacer algunas instrucciones para tus propias creaciones? Revisa el"
article_editor_suffix: "y ayuda a los jugadores de CodeCombat conseguir lo más posible de su tiempo jugando."
find_us: "Encuentranos en etsos sitios"
social_github: "Checa todo nuestro código en el GitHub"
social_github: "Revisa todo nuestro código en GitHub"
social_blog: "Lee el blog de CodeCombat en Sett"
social_discource: "Unite a la discusión en nuestro foro"
social_discource: "Únete a la discusión en nuestro foro"
social_facebook: "Me Gusta CodeCombat en Facebook"
social_twitter: "Sigue a CodeCombat en Twitter"
social_gplus: "Únete a CodeCombat con Google+"
@ -806,12 +806,12 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
make_private: "Hacer clan privado"
subs_only: "solo suscriptores"
create_clan: "Crear nuevo clan"
private_preview: "Previsualizar"
private_preview: "Vista previa"
public_clans: "Clanes publicos"
my_clans: "Mis Clanes"
clan_name: "Nombre del clan"
name: "Nombre"
chieftain: "Cacique/Líder"
chieftain: "Líder del Clan"
type: "Tipo"
edit_clan_name: "Editar el nombre del Clan"
edit_clan_description: "Editar descripción del clan"
@ -962,7 +962,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
indoor: "Interior"
desert: "Desierto"
grassy: "Herboso"
mountain: "Mountaña"
mountain: "Montaña"
glacier: "Glaciar"
small: "Pequeño"
large: "Grande"
@ -985,8 +985,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
level_tab_thangs_title: "Tiliches Actuales"
level_tab_thangs_all: "Todo"
level_tab_thangs_conditions: "Condiciones Iniciales"
level_tab_thangs_add: "Agregar Tiliches"
level_tab_thangs_search: "Buscar thangs"
level_tab_thangs_add: "Agregar Thangs"
level_tab_thangs_search: "Buscar Thangs"
add_components: "Agregar Componentes"
component_configs: "Configuraciones del Componente"
config_thang: "Doble clic para configurar un Tiliche"
@ -1032,8 +1032,8 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
pop_i18n: "Poblar I18N"
tasks: "Tareas"
clear_storage: "Borrar tus cambios locales"
add_system_title: "Agregar Sistemas al Level"
done_adding: "Se terminó de agregar"
add_system_title: "Agregar Sistemas al Nivel"
done_adding: "Finalizar"
article:
edit_btn_preview: "Vista previa"
@ -1317,7 +1317,7 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
user_polls_record: "Historia de Visitas de Encuestas"
concepts:
advanced_strings: "Manipulación Avanzada de Cadenas"
advanced_strings: "Cadenas - Avanzado"
algorithms: "Algoritmos"
arguments: "Argumentos"
arithmetic: "Aritmética"
@ -1327,18 +1327,17 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip
break_statements: "Sentencias Break"
classes: "Clases"
continue_statements: "Sentencias Continue"
for_loops: "Ciclos For"
for_loops: "Bucle For"
functions: "Funciones"
graphics: "Gráficos"
if_statements: "Sentencias If"
input_handling: "Manejo de Entrada"
input_handling: "Manejo de Entradas"
math_operations: "Operaciones Matemáticas"
object_literals: "Literales de Objeto"
parameters: "Parámetros"
object_literals: "Objetos Literales"
strings: "Cadenas"
variables: "Variables"
vectors: "Vectores"
while_loops: "Ciclos While"
while_loops: "Bucles"
recursion: "Recursividad"
delta:

View file

@ -26,10 +26,10 @@
padding: 2px
.progress-concept-cell-complete
background-color: lightgray
background-color: lightgreen
.progress-concept-cell-started
background-color: lightgreen
background-color: lightyellow
.progress-concept-completion-container
font-size: 10pt
@ -76,10 +76,10 @@
padding: 2px
.progress-key-complete
background-color: lightgray
background-color: lightgreen
.progress-key-started
background-color: lightgreen
background-color: lightyellow
.progress-expand-checkbox
margin-left: 14px
@ -99,11 +99,11 @@
.progress-level-cell-complete
cursor: pointer
background-color: lightgray
background-color: lightgreen
.progress-level-cell-started
cursor: pointer
background-color: lightgreen
background-color: lightyellow
.progess-levels-label
color: #317EAC

View file

@ -65,7 +65,7 @@ block content
a(href="https://plus.google.com/115285980638641924488/posts")
img(src="/images/pages/community/logo_g+.png", data-i18n="[data-content]community.social_gplus" data-content="Join CodeCombat on Google+")
a(href="http://www.hipchat.com/g3plnOKqa")
a(href="http://www.hipchat.com/gkaufqwnj")
img(src="/images/pages/community/logo_hipchat.png", data-i18n="[data-content]community.social_hipchat" data-content="Chat with us in the public CodeCombat HipChat room")
.half-width

View file

@ -51,7 +51,7 @@ block content
| Email us
span(data-i18n="contribute.join_desc_3")
| , or find us in our
a(href="http://www.hipchat.com/g3plnOKqa", data-i18n="contribute.join_url_hipchat") public HipChat room
a(href="http://www.hipchat.com/gkaufqwnj", data-i18n="contribute.join_url_hipchat") public HipChat room
span
span(data-i18n="contribute.join_desc_4")
| and we'll go from there!

View file

@ -49,7 +49,7 @@ block content
li
a(href="/editor/level", data-i18n="contribute.artisan_join_step2") Create a new level and explore existing levels.
li
a(href="http://www.hipchat.com/g3plnOKqa", data-i18n="contribute.artisan_join_step3") Find us in our public HipChat room for help.
a(href="http://www.hipchat.com/gkaufqwnj", data-i18n="contribute.artisan_join_step3") Find us in our public HipChat room for help.
li
a(href="http://discourse.codecombat.com", data-i18n="contribute.artisan_join_step4") Post your levels on the forum for feedback.

View file

@ -50,21 +50,31 @@ block content
div.well.well-sm(role='tabpanel')
ul.nav.nav-pills(role='tablist')
li.active(role='presentation')
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
if adminMode
li.active(role='presentation')
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
li(role='presentation')
a(href='#invite', aria-controls='invite', role='tab', data-toggle='tab', data-i18n="courses.add_students")
li(role='presentation')
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
li(role='presentation')
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
else
li.active(role='presentation')
a(href='#levels', aria-controls='levels', role='tab', data-toggle='tab', data-i18n="nav.play")
li(role='presentation')
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
.tab-content
.tab-pane.active#progress(role='tabpanel')
+progress-tab
if adminMode
.tab-pane.active#progress(role='tabpanel')
+progress-tab
.tab-pane#invite(role='tabpanel')
+invite-tab
.tab-pane#levels(role='tabpanel')
+levels-tab
.tab-pane#levels(role='tabpanel')
+levels-tab
else
.tab-pane.active#levels(role='tabpanel')
+levels-tab
.tab-pane#progress(role='tabpanel')
+progress-tab
mixin progress-tab
.container-fluid.progress-summary-container
@ -141,7 +151,8 @@ mixin progress-members
span(style='padding-left:16px;')
span.progress-key.progress-key-complete(data-i18n="clans.complete_1")
span.progress-key.progress-key-started(data-i18n="clans.started_1")
span.progress-key(data-i18n="clans.not_started_1")
if showExpandedProgress
span.progress-key(data-i18n="clans.not_started_1")
input.progress-expand-checkbox(type='checkbox')
span.spl.progress-expand-label(data-i18n="courses.expand_details")
tbody
@ -250,19 +261,20 @@ mixin invite-tab
h3(data-i18n="courses.invite_link_header")
p(data-i18n="courses.invite_link_p_1")
.alert.alert-info
strong= document.location.origin + "/courses?_ppc=" + view.prepaid.get('code')
strong= document.location.origin + "/courses/students?_ppc=" + view.prepaid.get('code')
p(data-i18n="courses.invite_link_p_2")
.form
.form-group
textarea#invite-emails-textarea.form-control(
rows=3, data-i18n="[placeholder]courses.enter_emails", placeholder="Enter student emails to invite, one per line")
rows=3, data-i18n="[placeholder]courses.enter_emails")
.help-block(data-i18n="courses.enter_emails")
.form-group
button#invite-btn.btn.btn-success(data-i18n="courses.send_invites")
#invite-emails-sending-alert.alert.alert-info.hide(data-i18n="common.sending")
#invite-emails-success-alert.alert.alert-success.hide(data-i18n="play_level.done")
h3 Class Capacity
if view.prepaid.loaded
if view.prepaid.loaded && pricePerSeat > 0
h3 Class Capacity
p
span.spr(data-i18n="courses.capacity_used")
span #{view.prepaid.get('redeemers').length} / #{view.prepaid.get('maxRedeemers')}.
@ -277,13 +289,18 @@ mixin levels-tab
th(data-i18n="courses.concepts")
tbody
if campaign
- var lastLevelCompleted = true;
each level, levelID in campaign.get('levels')
tr
td
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n="home.play")
if lastLevelCompleted || adminMode
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n="home.play")
td
if userLevelStateMap[me.id]
div= userLevelStateMap[me.id][levelID]
- lastLevelCompleted = userLevelStateMap[me.id][levelID] === 'complete'
else
- lastLevelCompleted = false
td= level.name.replace('Course: ', '')
td
if levelConceptMap[levelID]
@ -307,14 +324,5 @@ mixin settings-dialog
strong(data-i18n="courses.description")
p
textarea.settings-description-input(rows=2)= courseInstance.get('description')
p(data-i18n="courses.languages_available")
p
select.form-control.settings-language-select
option(value="Python") Python
option(value="JavaScript") JavaScript
option(value="All Languages", data-i18n="courses.all_lang")
p
input.settings-public-progress(type='checkbox', checked)
span.spl(data-i18n="courses.show_progress")
.modal-footer
button.btn.btn-save-settings(data-i18n="common.save_changes")

View file

@ -37,14 +37,18 @@ block content
if courses.length > 1
option(value="All Courses", data-i18n="courses.all_courses")
h3
span 2.
span.spl(data-i18n="courses.number_students")
p(data-i18n="courses.enter_number_students")
input.input-seats(type='text', value="#{seats}")
if price > 0
h3
span 2.
span.spl(data-i18n="courses.number_students")
p(data-i18n="courses.enter_number_students")
input.input-seats(type='text', value="#{seats}")
h3
span 3.
if price > 0
span 3.
else
span 2.
span.spl(data-i18n="courses.name_class")
p(data-i18n="courses.displayed_course_page")
input.class-name(type='text', placeholder="Mrs. Smith's 4th Period", value="#{className ? className : ''}")
@ -55,7 +59,7 @@ block content
span.spl(data-i18n="courses.buy") Buy
else
h3
span 4.
span 3.
span.spl(data-i18n="courses.create_class")
p
if price > 0
@ -77,11 +81,11 @@ block content
+trial-and-questions
mixin trial-and-questions
h3(data-i18n="courses.free_trial")
p
span.spr(data-i18n="teachers.teacher_subs_1")
a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
span.spl(data-i18n="courses.get_access")
//- h3(data-i18n="courses.free_trial")
//- p
//- span.spr(data-i18n="teachers.teacher_subs_1")
//- a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
//- span.spl(data-i18n="courses.get_access")
h3(data-i18n="courses.questions")
p

View file

@ -9,6 +9,8 @@ block content
br
if state === 'enrolling'
.alert.alert-info Enrolling in course..
else if state === 'ppc_logged_out'
.alert.alert-success Log in or create an account to join this course.
else
if state === 'unknown_error'
.alert.alert-danger.alert-dismissible= stateMessage
@ -55,6 +57,14 @@ mixin teacher-main
.well.well-sm
div.praise-quote "#{praise.quote}"
div.praise-caption - #{praise.source}
//- h1.center(data-i18n="courses.free_trial")
//- .info-container
//- p
//- span.spr(data-i18n="teachers.teacher_subs_1")
//- a(href='/teachers/freetrial', data-i18n="teachers.teacher_subs_2")
//- span.spl(data-i18n="courses.get_access")
h2.center(data-i18n="courses.choose_course")
mixin student-dialog(course)

View file

@ -106,7 +106,7 @@ block header
li
a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home', data-i18n="editor.wiki", target="_blank") Wiki
li
a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat", target="_blank") Live Chat
a(href='http://www.hipchat.com/gkaufqwnj', data-i18n="editor.live_chat", target="_blank") Live Chat
li
a(href='http://discourse.codecombat.com/category/artisan', data-i18n="nav.forum", target="_blank") Forum
li

View file

@ -67,7 +67,7 @@ block header
li
a(href='https://github.com/codecombat/codecombat/wiki/Artisan-Home', data-i18n="editor.wiki", target="_blank") Wiki
li
a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat", target="_blank") Live Chat
a(href='http://www.hipchat.com/gkaufqwnj', data-i18n="editor.live_chat", target="_blank") Live Chat
li
a(href='http://discourse.codecombat.com/category/artisan', data-i18n="nav.forum", target="_blank") Forum
li

View file

@ -43,7 +43,7 @@ block header
li
a(href='https://github.com/codecombat/codecombat/wiki', data-i18n="editor.wiki", target="_blank") Wiki
li
a(href='http://www.hipchat.com/g3plnOKqa', data-i18n="editor.live_chat", target="_blank") Live Chat
a(href='http://www.hipchat.com/gkaufqwnj', data-i18n="editor.live_chat", target="_blank") Live Chat
li
a(href='http://discourse.codecombat.com/category/diplomat', data-i18n="nav.forum", target="_blank") Forum
li

View file

@ -54,6 +54,7 @@ module.exports = class CourseDetailsView extends RootView
context.memberUserMap = @memberUserMap ? {}
context.noCourseInstance = @noCourseInstance
context.noCourseInstanceSelected = @noCourseInstanceSelected
context.pricePerSeat = @course.get('pricePerSeat')
context.showExpandedProgress = @showExpandedProgress
context.sortedMembers = @sortedMembers ? []
context.userConceptStateMap = @userConceptStateMap ? {}
@ -150,11 +151,11 @@ module.exports = class CourseDetailsView extends RootView
levelStateMap[levelID] = state
@instanceStats.totalLevelsCompleted++ if state is 'complete'
@instanceStats.totalPlayTime += levelSession.get('playtime')
@instanceStats.totalPlayTime += parseInt(levelSession.get('playtime') ? 0)
@memberStats[userID] ?= totalLevelsCompleted: 0, totalPlayTime: 0
@memberStats[userID].totalLevelsCompleted++ if state is 'complete'
@memberStats[userID].totalPlayTime += levelSession.get('playtime')
@memberStats[userID].totalPlayTime += parseInt(levelSession.get('playtime') ? 0)
@userConceptStateMap[userID] ?= {}
for concept of @levelConceptMap[levelID]
@ -168,6 +169,7 @@ module.exports = class CourseDetailsView extends RootView
if @courseInstance.get('members').length > 0
@instanceStats.averageLevelsCompleted = @instanceStats.totalLevelsCompleted / @courseInstance.get('members').length
@instanceStats.averageLevelPlaytime = @instanceStats.totalPlayTime / @courseInstance.get('members').length
for levelID, level of @campaign.get('levels')
@instanceStats.furthestLevelCompleted = level.name if levelStateMap[levelID] is 'complete'

View file

@ -58,15 +58,16 @@ module.exports = class CourseEnrollView extends RootView
onClickBuy: (e) ->
return @openModalView new AuthModal() if me.isAnonymous()
if @seats < 1 or not _.isFinite(@seats)
alert("Please enter the maximum number of students needed for your class.")
return
if @price is 0
@seats = 9999
@state = 'creating'
@createClass()
return
if @seats < 1 or not _.isFinite(@seats)
alert("Please enter the maximum number of students needed for your class.")
return
@state = undefined
@stateMessage = undefined
@render()

View file

@ -20,13 +20,18 @@ module.exports = class CoursesView extends RootView
constructor: (options) ->
super(options)
@praise = utils.getCoursePraise()
@studentMode = utils.getQueryVariable('student', false) or options.studentMode
@studentMode = Backbone.history.getFragment()?.indexOf('courses/students') >= 0
@courses = new CocoCollection([], { url: "/db/course", model: Course})
@supermodel.loadCollection(@courses, 'courses')
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
@listenToOnce @courseInstances, 'sync', @onCourseInstancesLoaded
@supermodel.loadCollection(@courseInstances, 'course_instances')
@courseEnroll(prepaidCode) if prepaidCode = utils.getQueryVariable('_ppc', false)
if prepaidCode = utils.getQueryVariable('_ppc', false)
if me.isAnonymous()
@state = 'ppc_logged_out'
else
@studentMode = true
@courseEnroll(prepaidCode)
getRenderData: ->
context = super()
@ -75,7 +80,7 @@ module.exports = class CoursesView extends RootView
onClickEnroll: (e) ->
$('.continue-dialog').modal('hide')
courseID = $(e.target).data('course-id')
prepaidCode = $(".code-input[data-course-id=#{courseID}]").val()
prepaidCode = ($(".code-input[data-course-id=#{courseID}]").val() ? '').trim()
@courseEnroll(prepaidCode)
onClickEnter: (e) ->
@ -89,17 +94,15 @@ module.exports = class CoursesView extends RootView
Backbone.Mediator.publish 'router:navigate', navigationEvent
onClickStudent: (e) ->
route = "/courses?student=true"
route = "/courses/students"
viewClass = require 'views/courses/CoursesView'
viewArgs = [studentMode: true]
navigationEvent = route: route, viewClass: viewClass, viewArgs: viewArgs
navigationEvent = route: route, viewClass: viewClass, viewArgs: []
Backbone.Mediator.publish 'router:navigate', navigationEvent
onClickTeacher: (e) ->
route = "/courses?student=false"
route = "/courses/teachers"
viewClass = require 'views/courses/CoursesView'
viewArgs = [studentMode: false]
navigationEvent = route: route, viewClass: viewClass, viewArgs: viewArgs
navigationEvent = route: route, viewClass: viewClass, viewArgs: []
Backbone.Mediator.publish 'router:navigate', navigationEvent
courseEnroll: (prepaidCode) ->

View file

@ -51,7 +51,7 @@ function generateNewCode(done)
function createCode(length)
{
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var possible = "abcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < length; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));

View file

@ -1,5 +1,6 @@
async = require 'async'
Handler = require '../commons/Handler'
Campaign = require '../campaigns/Campaign'
Course = require './Course'
CourseInstance = require './CourseInstance'
LevelSession = require '../levels/sessions/LevelSession'
@ -88,11 +89,19 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless courseInstance
memberIDs = _.map courseInstance.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID
LevelSession.find {creator: {$in: memberIDs}}, (err, documents) =>
return @sendDatabaseError(res, err) if err?
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, cleandocs)
Course.findById courseInstance.get('courseID'), (err, course) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless course
Campaign.findById course.get('campaignID'), (err, campaign) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless campaign
levelIDs = (levelID for levelID of campaign.get('levels'))
memberIDs = _.map courseInstance.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID
query = {$and: [{creator: {$in: memberIDs}}, {'level.original': {$in: levelIDs}}]}
LevelSession.find query, (err, documents) =>
return @sendDatabaseError(res, err) if err?
cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, cleandocs)
getMembersAPI: (req, res, courseInstanceID) ->
CourseInstance.findById courseInstanceID, (err, courseInstance) =>
@ -112,20 +121,25 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @sendNotFoundError(res) unless courseInstance
return @sendForbiddenError(res) unless @hasAccessToDocument(req, courseInstance)
Prepaid.findById courseInstance.get('prepaidID'), (err, prepaid) =>
Course.findById courseInstance.get('courseID'), (err, course) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless prepaid
return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length
for email in req.body.emails
context =
email_id: sendwithus.templates.course_invite_email
recipient:
address: email
email_data:
class_name: courseInstance.get('name')
join_link: "https://codecombat.com/courses?_ppc=" + prepaid.get('code')
sendwithus.api.send context, _.noop
return @sendSuccess(res, {})
return @sendNotFoundError(res) unless course
Prepaid.findById courseInstance.get('prepaidID'), (err, prepaid) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless prepaid
return @sendForbiddenError(res) unless prepaid.get('maxRedeemers') > prepaid.get('redeemers').length
for email in req.body.emails
context =
email_id: sendwithus.templates.course_invite_email
recipient:
address: email
subject: course.get('name')
email_data:
class_name: course.get('name')
join_link: "https://codecombat.com/courses/students?_ppc=" + prepaid.get('code')
sendwithus.api.send context, _.noop
return @sendSuccess(res, {})
redeemPrepaidCodeAPI: (req, res) ->
return @sendUnauthorizedError(res) if not req.user? or req.user?.isAnonymous()
@ -142,6 +156,9 @@ CourseInstanceHandler = class CourseInstanceHandler extends Handler
return @sendDatabaseError(res, err) if err
return @sendForbiddenError(res) if prepaid.get('redeemers')?.length >= prepaid.get('maxRedeemers')
if _.find((prepaid.get('redeemers') ? []), (a) -> a.userID.equals(req.user.id))
return @sendSuccess(res, courseInstances)
# Add to prepaid redeemers
query =
_id: prepaid.get('_id')

View file

@ -6,7 +6,7 @@ PrepaidSchema.index({code: 1}, { unique: true })
PrepaidSchema.statics.generateNewCode = (done) ->
tryCode = ->
code = _.sample("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 8).join('')
code = _.sample("abcdefghijklmnopqrstuvwxyz0123456789", 8).join('')
Prepaid.findOne code: code, (err, prepaid) ->
return done() if err
return done(code) unless prepaid

View file

@ -222,7 +222,7 @@ describe 'CourseInstance', ->
describe 'Redeem prepaid code', ->
it 'Redeem prepaid code an instance of max 2', (done) ->
it 'Redeem prepaid code for an instance of max 2', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
@ -264,7 +264,7 @@ describe 'CourseInstance', ->
expect(usersFound).toEqual(2)
done()
it 'Redeem full prepaid code on instance of max 1', (done) ->
it 'Redeem full prepaid code for on instance of max 1', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
@ -326,3 +326,29 @@ describe 'CourseInstance', ->
return done(err) if err
expect(prepaid.get('redeemers')?.length).toEqual(prepaid.get('maxRedeemers'))
done()
it 'Redeem prepaid code twice', (done) ->
stripe.tokens.create {
card: { number: '4242424242424242', exp_month: 12, exp_year: 2020, cvc: '123' }
}, (err, token) ->
loginNewUser (user1) ->
createCourse 0, (err, course) ->
expect(err).toBeNull()
return done(err) if err
createCourseInstances user1, course.get('_id'), 2, token.id, (err, courseInstances) ->
expect(err).toBeNull()
return done(err) if err
expect(courseInstances.length).toEqual(1)
Prepaid.findById courseInstances[0].get('prepaidID'), (err, prepaid) ->
expect(err).toBeNull()
return done(err) if err
loginNewUser (user2) ->
# Redeem once
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
# Redeem twice
request.post {uri: courseInstanceRedeemURL, json: {prepaidCode: prepaid.get('code')} }, (err, res) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
done()