diff --git a/README.md b/README.md index 548b2a3bf..ba81262ad 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/app/core/Router.coffee b/app/core/Router.coffee index a958417bc..29ce8a800 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -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') diff --git a/app/lib/world/names.coffee b/app/lib/world/names.coffee index d17ab48f7..48cdc51d2 100644 --- a/app/lib/world/names.coffee +++ b/app/lib/world/names.coffee @@ -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' diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 967f56134..aa71c96eb 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -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: diff --git a/app/styles/courses/course-details.sass b/app/styles/courses/course-details.sass index 3cace8e09..aa895e2d1 100644 --- a/app/styles/courses/course-details.sass +++ b/app/styles/courses/course-details.sass @@ -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 diff --git a/app/templates/community-view.jade b/app/templates/community-view.jade index b48e1514f..e4cd73006 100644 --- a/app/templates/community-view.jade +++ b/app/templates/community-view.jade @@ -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 diff --git a/app/templates/contribute/archmage.jade b/app/templates/contribute/archmage.jade index 200a2e454..0a7fac0b9 100644 --- a/app/templates/contribute/archmage.jade +++ b/app/templates/contribute/archmage.jade @@ -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! diff --git a/app/templates/contribute/artisan.jade b/app/templates/contribute/artisan.jade index 47a9438be..08a033ad7 100644 --- a/app/templates/contribute/artisan.jade +++ b/app/templates/contribute/artisan.jade @@ -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. diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade index 1e9879484..ed424f25c 100644 --- a/app/templates/courses/course-details.jade +++ b/app/templates/courses/course-details.jade @@ -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") diff --git a/app/templates/courses/course-enroll.jade b/app/templates/courses/course-enroll.jade index c18d47006..056309c2e 100644 --- a/app/templates/courses/course-enroll.jade +++ b/app/templates/courses/course-enroll.jade @@ -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 diff --git a/app/templates/courses/courses.jade b/app/templates/courses/courses.jade index 997c9f5ba..4d740ada5 100644 --- a/app/templates/courses/courses.jade +++ b/app/templates/courses/courses.jade @@ -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) diff --git a/app/templates/editor/level/edit.jade b/app/templates/editor/level/edit.jade index f12e3df08..d551f48f6 100644 --- a/app/templates/editor/level/edit.jade +++ b/app/templates/editor/level/edit.jade @@ -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 diff --git a/app/templates/editor/thang/thang-type-edit-view.jade b/app/templates/editor/thang/thang-type-edit-view.jade index 8da04e152..2d9160f2f 100644 --- a/app/templates/editor/thang/thang-type-edit-view.jade +++ b/app/templates/editor/thang/thang-type-edit-view.jade @@ -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 diff --git a/app/templates/i18n/i18n-edit-model-view.jade b/app/templates/i18n/i18n-edit-model-view.jade index efa2f4a9b..8898079cb 100644 --- a/app/templates/i18n/i18n-edit-model-view.jade +++ b/app/templates/i18n/i18n-edit-model-view.jade @@ -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 diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee index f8bde2ab9..cc6d60696 100644 --- a/app/views/courses/CourseDetailsView.coffee +++ b/app/views/courses/CourseDetailsView.coffee @@ -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' diff --git a/app/views/courses/CourseEnrollView.coffee b/app/views/courses/CourseEnrollView.coffee index b34850e25..43e6b5386 100644 --- a/app/views/courses/CourseEnrollView.coffee +++ b/app/views/courses/CourseEnrollView.coffee @@ -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() diff --git a/app/views/courses/CoursesView.coffee b/app/views/courses/CoursesView.coffee index ff42d9541..dfb496cf6 100644 --- a/app/views/courses/CoursesView.coffee +++ b/app/views/courses/CoursesView.coffee @@ -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) -> diff --git a/scripts/mongodb/createBulkPrepaids.js b/scripts/mongodb/createBulkPrepaids.js index 04d20efc9..0b5978ca1 100644 --- a/scripts/mongodb/createBulkPrepaids.js +++ b/scripts/mongodb/createBulkPrepaids.js @@ -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)); diff --git a/server/courses/course_instance_handler.coffee b/server/courses/course_instance_handler.coffee index 25abed4c8..7ae5dcf52 100644 --- a/server/courses/course_instance_handler.coffee +++ b/server/courses/course_instance_handler.coffee @@ -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') diff --git a/server/prepaids/Prepaid.coffee b/server/prepaids/Prepaid.coffee index 3461dc32c..c56a60839 100644 --- a/server/prepaids/Prepaid.coffee +++ b/server/prepaids/Prepaid.coffee @@ -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 diff --git a/test/server/functional/course_instance.spec.coffee b/test/server/functional/course_instance.spec.coffee index 5084f48c5..ff6506a01 100644 --- a/test/server/functional/course_instance.spec.coffee +++ b/test/server/functional/course_instance.spec.coffee @@ -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()