diff --git a/app/core/Router.coffee b/app/core/Router.coffee index bd7641f25..bec320c7b 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -30,6 +30,7 @@ module.exports = class CocoRouter extends Backbone.Router 'admin': go('admin/MainAdminView') 'admin/clas': go('admin/CLAsView') + 'admin/classroom-levels': go('admin/AdminClassroomLevelsView') 'admin/design-elements': go('admin/DesignElementsView') 'admin/files': go('admin/FilesView') 'admin/analytics': go('admin/AnalyticsView') diff --git a/app/core/utils.coffee b/app/core/utils.coffee index e8c29bd88..07e90f4f5 100644 --- a/app/core/utils.coffee +++ b/app/core/utils.coffee @@ -342,31 +342,31 @@ module.exports.createLevelNumberMap = (levels) -> levelNumberMap module.exports.findNextLevel = (levels, currentIndex, needsPractice) -> - # levels = [{practice: true/false, complete: true/false}] - index = currentIndex - index++ - if needsPractice - if levels[currentIndex].practice or index < levels.length and levels[index].practice - # Needs practice, on practice or next practice, choose next incomplete level - # May leave earlier practice levels incomplete and reach end of course - index++ while index < levels.length and levels[index].complete - else - # Needs practice, on required, next required, choose first incomplete level of previous practice chain - index-- - index-- while index >= 0 and not levels[index].practice - if index >= 0 - index-- while index >= 0 and levels[index].practice - if index >= 0 - index++ - index++ while index < levels.length and levels[index].practice and levels[index].complete - if levels[index].practice and not levels[index].complete - return index - index = currentIndex + 1 - index++ while index < levels.length and levels[index].complete + # levels = [{practice: true/false, complete: true/false}] + index = currentIndex + index++ + if needsPractice + if levels[currentIndex].practice or index < levels.length and levels[index].practice + # Needs practice, on practice or next practice, choose next incomplete level + # May leave earlier practice levels incomplete and reach end of course + index++ while index < levels.length and levels[index].complete else - # No practice needed, next required incomplete level - index++ while index < levels.length and (levels[index].practice or levels[index].complete) - index + # Needs practice, on required, next required, choose first incomplete level of previous practice chain + index-- + index-- while index >= 0 and not levels[index].practice + if index >= 0 + index-- while index >= 0 and levels[index].practice + if index >= 0 + index++ + index++ while index < levels.length and levels[index].practice and levels[index].complete + if levels[index].practice and not levels[index].complete + return index + index = currentIndex + 1 + index++ while index < levels.length and levels[index].complete + else + # No practice needed, next required incomplete level + index++ while index < levels.length and (levels[index].practice or levels[index].complete) + index module.exports.needsPractice = (playtime=0, threshold=2) -> playtime / 60 > threshold diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index 95e5d6f1d..24bb9a6b9 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -15,7 +15,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi or_ipad: "Или скачайте на iPad" new_home: - slogan: "Самая захватывающая игра для обучения программированию" + slogan: "Самая захватывающая игра для обучения программированию." classroom_edition: "Классная комната:" learn_to_code: "Учись программировать:" teacher: "Учитель" @@ -24,44 +24,44 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi im_a_teacher: "Я учитель" im_a_student: "Я ученик" learn_more: "Узнать больше" -# classroom_in_a_box: "A classroom in-a-box for teaching computer science." - codecombat_is: "CodeCombat это платформа для студентов чтобы изучать компьтерные науки во время игры." -# our_courses: "Our courses have been specifically playtested to excel in the classroom, even by teachers with little to no prior programming experience." + classroom_in_a_box: "Готовая учебный кабинет из коробки для обучения информатике." + codecombat_is: "CodeCombat это платформа для студентов чтобы изучать информатику во время игры." + our_courses: "Наши курсы были тщательно проработаны чтобы качественно обучать, даже если учителя не имееют особого опыта в программировании." top_screenshots_hint: "Студенты пишут код и видят как их изменения обновляются в реальном времени" - designed_with: "Разработанный при поддержке учителей" + designed_with: "Разработано при поддержке учителей" real_code: "По настоящему писать код" from_the_first_level: "с первого уровня" - getting_students: "Студенты получают возможность писать код как можно скорее это важно для обучения синтаксиса программирования и правильной структуры." - educator_resources: "Ресурсы педагога" + getting_students: "Студенты получают возможность писать код как можно скорее. Это важно для обучения синтаксиса языков программирования и правильной структуры." + educator_resources: "Материалы для педагогов" course_guides: "и руководство по курсам" - teaching_computer_science: "Обучение компьютерным наукам не требует высокой степени, потому что мы предоставляем инстументы для поддержки педагогов с любым уровнем подготовки." + teaching_computer_science: "Обучение информатике не требует высокой степени, так как мы предоставляем инструменты для поддержки педагогов с любым уровнем подготовки." accessible_to: "Доступно для" everyone: "каждого" democratizing: "Демократизация процесса обучения программированию лежит в основе нашей философии. Каждый должен иметь возможность обучаться программированию." forgot_learning: "Я думаю, что они забудут, что они действительно что-то изучают." -# 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?" -# 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." -# excel: "Games excel at rewarding" -# struggle: "productive struggle" -# kind_of_struggle: "the kind of struggle that results in learning that’s engaging and" -# motivating: "motivating" -# not_tedious: "not tedious." -# gaming_is_good: "Studies suggest gaming is good for children’s brains. (it’s true!)" -# 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" -# 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." -# real_game: "A real game, played with real coding." -# great_game: "A great game is more than just badges and achievements - it’s about a player’s journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence." -# agency: "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_subtitle: "Request a demo and get your students started in less than an hour." -# 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." - request_demo: "Запрос Демонстрации" + wanted_to_do: " Программирование - это то, чем я всегда хотел заниматься и никогда не подумал бы, что смогу изучать это в школе." + why_games: "Почему обучение с помощью игры важно?" + games_reward: "Игры поощряют эффективное упорство." + encourage: "Игра - это процесс, который поощряет взаимодействие, исследования и метод проб и ошибок. Хорошие испытания в игре заставляют игрока постоянно улучшать свои навыки, что также очень важно для в процессе обучения." + excel: "Игры особенно хороши в поощрении" + struggle: "эффективного упорства" + kind_of_struggle: "- это то упорство, достигаемое обучением, которое увлекательное и " + motivating: "мотивирующее" + not_tedious: "а не скучное." + gaming_is_good: "Исследования считают, что игры полезны для детских умов. (и это так и есть!)" + game_based: "Когда игровая система обучения" + compared: "сравнивается" + conventional: "с традиционными оценочными методами, то разница очевидна: игры лучше помогают ученикам получать знания, концетрироваться и" + perform_at_higher_level: "достигать высоких результатов." + feedback: "Также игры дают обратную связь в реальном времени, что позволяет ученикам быть гибкими в поисках решений и понимать концепции более глубоко, а не быть ограничеными результами “правильно” или “неправильно”." + real_game: "Настоящая игра в которую играешь с помощью настоящего программирования." + great_game: "Отличная игра - это больше чем бейджики и достижения. Это приключение для игрока, качественно сделаные головоломки и возможность преодолевать проблемы с уверенностью." + agency: "CodeCombat - это игра дающая с помощью надежной программной платформы, игрокам уверенность в результате, Что помогает как начинающим, так и опытным ученикам писать надежные, качественные программы." + request_demo_title: "Начните учить ваших учеников уже сегодня!" + request_demo_subtitle: "Запросите демо версию и начинайте обучение меньше чем через час." + get_started_title: "Подготовьте ваш класс уже сегодня" + get_started_subtitle: "Подготовьте класс, добавьте учеников, и наблюдайте за их прогрессом в обучении информатики." + request_demo: "Запросить демо версию" setup_a_class: "Настройка класса" have_an_account: "Уже есть аккаунт?" logged_in_as: "Вы вошли как" @@ -72,13 +72,13 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi ffa: "Бесплатно для всех студентов" lesson_time: "Время урока:" coming_soon: "Скоро!" -# courses_available_in: "Courses are available in JavaScript, Python, and Java (coming soon!)" -# boast: "Boasts riddles that are complex enough to fascinate gamers and coders alike." -# winning: "A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable." -# run_class: "Everything you need to run a computer science class in your school today, no CS background required." + courses_available_in: "Курсы доступны на JavaScript, Python, и Java (скоро!)" + boast: "Похвальные задачи, которые сложны в меру, чтобы очаровать как игроков, так и программистов." + winning: "Выигрышная комбинация ролевой игры и домашнего задания по программированию у которой получилось сделать обучение правильным, захватывающим и дружественным к детям." + run_class: "Все что вам нужно, чтобы начать класс информатики в вашей школе уже сегодня без подготовки по курсу информатики." teachers: "Учителя!" teachers_and_educators: "Учителя & Педагоги" - class_in_box: "Узнайте, как наш класс в коробке вписывается в ваш учебный план." + class_in_box: "Узнайте, как наш концепция готового класса из коробки вписывается в ваш учебный план." get_started: "Начать" students: "Студенты:" join_class: "Присоединиться к классу" @@ -89,9 +89,9 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi goto_classes: "Перейти в Мои классы" view_profile: "Посмотреть Мой профиль" view_progress: "Посмотреть прогресс" - check_out_wiki: "Посетите наш новый Wiki педагога" + check_out_wiki: "Посетите наш новый Wiki для педагогов" want_coco: "Хотите CodeCombat в вашей школе?" - form_select_role: "Выберите главную роль" + form_select_role: "Выберите основную роль" form_select_range: "Выберите размер класса" nav: diff --git a/app/styles/admin/admin-classroom-levels.sass b/app/styles/admin/admin-classroom-levels.sass new file mode 100644 index 000000000..0c16d7582 --- /dev/null +++ b/app/styles/admin/admin-classroom-levels.sass @@ -0,0 +1,5 @@ +#admin-classroom-levels-view + + table + td, th + padding: 0px diff --git a/app/styles/editor/verifier/verifier-view.sass b/app/styles/editor/verifier/verifier-view.sass index ecc9c5660..419c20cc8 100644 --- a/app/styles/editor/verifier/verifier-view.sass +++ b/app/styles/editor/verifier/verifier-view.sass @@ -20,5 +20,9 @@ .test-failed color: red + .solution + max-height: 200px + overflow: auto + .lineUnder border-bottom: 1px solid #ccc \ No newline at end of file diff --git a/app/templates/admin.jade b/app/templates/admin.jade index f154dd99a..fecf56dff 100644 --- a/app/templates/admin.jade +++ b/app/templates/admin.jade @@ -42,6 +42,8 @@ block content if me.isAdmin() h4 Analytics ul + li + a(href="/admin/classroom-levels") Classroom Levels li a(href="/admin/analytics") Dashboard li diff --git a/app/templates/admin/admin-classroom-levels.jade b/app/templates/admin/admin-classroom-levels.jade new file mode 100644 index 000000000..d4839f596 --- /dev/null +++ b/app/templates/admin/admin-classroom-levels.jade @@ -0,0 +1,39 @@ +extends /templates/base + +//- DO NOT TRANSLATE + +block content + + if !me.isAdmin() + div You must be logged in as an admin to view this page. + else + p + - var levelsTotal = 0; + each course in view.courses.models + - var campaign = view.campaigns.get(course.get('campaignID')); + - var levels = campaign.getLevels().models; + - levelsTotal += levels.length; + div #{levels.length} #{course.get('name')} + div #{levelsTotal} levels total + each course in view.courses.models + - var campaign = view.campaigns.get(course.get('campaignID')); + - var levels = campaign.getLevels().models; + - levelCounts = levels.length; + strong #{course.get('name')} + .small= course.get('description') + .small Levels last updated #{campaign.get('levelsUpdated')} + table.table.table-striped.table-condensed + tr + th= levels.length + th Slug + th Type + th Practice + th Practice Threshold (m) + each level, levelIndex in levels + - var levelNumber = campaign.getLevelNumber(level.get('original'), levelIndex + 1) + tr + td= levelNumber + td= level.get('slug') + td= level.get('type') + td= level.get('practice') + td= level.get('practiceThresholdMinutes') diff --git a/app/templates/admin/school-counts.jade b/app/templates/admin/school-counts.jade index fad5b8edb..62b7d1b34 100644 --- a/app/templates/admin/school-counts.jade +++ b/app/templates/admin/school-counts.jade @@ -14,8 +14,9 @@ block content p div Untriaged students: #{view.untriagedStudents} div Untriaged teachers: #{view.untriagedTeachers} - .small Teacher: owns a classroom or has a teacher role - .small Student: member of a classroom or has schoolName set, not in HoC course instance + .small Teacher: teacherish role or owns a classroom + .small Student: student role or member of a classroom or has schoolName set, not in HoC course instance + .small School: trial request data or teacher with 10+ students .small +3 USA states are GU, PR, DC p diff --git a/app/templates/editor/verifier/verifier-view.jade b/app/templates/editor/verifier/verifier-view.jade index 02db75e34..f6a76056a 100644 --- a/app/templates/editor/verifier/verifier-view.jade +++ b/app/templates/editor/verifier/verifier-view.jade @@ -77,7 +77,7 @@ block content div.row(class=(test.isSuccessful() && id > 1 ? 'collapse' : 'collapse in'), id='verifier-test-' + id) div.col-xs-8 if test.solution - pre #{test.solution.source} + pre.solution #{test.solution.source} else h4 Error Loading Test pre #{test.error} diff --git a/app/views/admin/AdminClassroomLevelsView.coffee b/app/views/admin/AdminClassroomLevelsView.coffee new file mode 100644 index 000000000..906e0b382 --- /dev/null +++ b/app/views/admin/AdminClassroomLevelsView.coffee @@ -0,0 +1,16 @@ +RootView = require 'views/core/RootView' +CocoCollection = require 'collections/CocoCollection' +Campaigns = require 'collections/Campaigns' +Course = require 'models/Course' + +module.exports = class AdminClassroomLevelsView extends RootView + id: 'admin-classroom-levels-view' + template: require 'templates/admin/admin-classroom-levels' + + initialize: -> + return super() unless me.isAdmin() + @campaigns = new Campaigns() + @supermodel.trackRequest @campaigns.fetchByType('course', { data: { project: 'levels,levelsUpdated' } }) + @courses = new CocoCollection([], { url: "/db/course", model: Course}) + @supermodel.loadCollection(@courses, 'courses') + super() diff --git a/app/views/admin/SchoolCountsView.coffee b/app/views/admin/SchoolCountsView.coffee index 28e7e7b5a..250d97f13 100644 --- a/app/views/admin/SchoolCountsView.coffee +++ b/app/views/admin/SchoolCountsView.coffee @@ -7,6 +7,10 @@ User = require 'models/User' utils = require 'core/utils' # TODO: match anonymous trial requests with real users via email +# TODO: sanitize and use student.schoolName, can't use it directly +# TODO: example untriaged student: no geo IP, not attached to teacher with school +# TODO: example untriaged teacher: deleted but owner of a classroom +# TODO: use student geoip on their teacher module.exports = class SchoolCountsView extends RootView id: 'admin-school-counts-view' @@ -35,7 +39,7 @@ module.exports = class SchoolCountsView extends RootView studentMap = {} # Used to make sure teachers and students only counted once studentNonHocMap = {} # Used to exclude HoC users teacherStudentMap = {} # Used to link students to their teacher locations - countryStateDistrictSchoolCountsMap = {} # Data graph + unknownSchoolCount = 1 # Used to separate unique but unknown schools console.log(new Date().toISOString(), "Processing #{@courseInstances.models.length} course instances...") for courseInstance in @courseInstances.models @@ -46,25 +50,29 @@ module.exports = class SchoolCountsView extends RootView for classroom in @classrooms.models teacherID = classroom.get('ownerID') teacherMap[teacherID] ?= {} - teacherMap[teacherID] = true teacherStudentMap[teacherID] ?= {} for studentID in classroom.get('members') + continue if teacherMap[studentID] continue unless studentNonHocMap[studentID] - studentMap[studentID] = true + studentMap[studentID] = {} teacherStudentMap[teacherID][studentID] = true console.log(new Date().toISOString(), "Processing #{@teachers.models.length} teachers...") for teacher in @teachers.models - teacherMap[teacher.id] ?= {} + teacherMap[teacher.id] = teacher.get('geo') ? {} delete studentMap[teacher.id] console.log(new Date().toISOString(), "Processing #{@students.models.length} students...") - for student in @students.models when not teacherMap[student.id] + for student in @students.models continue unless studentNonHocMap[student.id] - schoolName = student.get('schoolName') - studentMap[student.id] = true + continue if teacherMap[student.id] + studentMap[student.id] = {geo: student.get('geo')} - console.log(new Date().toISOString(), "Processing trial #{@trialRequests.models.length} requests...") + orphanStudentMap = _.cloneDeep(studentMap) + orphanTeacherMap = _.cloneDeep(teacherMap) + + console.log(new Date().toISOString(), "Processing #{@trialRequests.models.length} trial requests...") + countryStateDistrictSchoolCountsMap = {} for trialRequest in @trialRequests.models teacherID = trialRequest.get('applicant') unless teacherMap[teacherID] @@ -82,8 +90,10 @@ module.exports = class SchoolCountsView extends RootView countryStateDistrictSchoolCountsMap[country][state][district] ?= {} countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}} countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true - for studentID, val of teacherStudentMap[teacherID] + for studentID, val of teacherStudentMap[teacherID] when orphanStudentMap[studentID] countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true + delete orphanStudentMap[studentID] + delete orphanTeacherMap[teacherID] else if not _.isEmpty(props.country) country = props.country?.trim() country = country[0].toUpperCase() + country.substring(1).toLowerCase() @@ -102,8 +112,65 @@ module.exports = class SchoolCountsView extends RootView countryStateDistrictSchoolCountsMap[country][state][district] ?= {} countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}} countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true - for studentID, val of teacherStudentMap[teacherID] + for studentID, val of teacherStudentMap[teacherID] when orphanStudentMap[studentID] countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true + delete orphanStudentMap[studentID] + delete orphanTeacherMap[teacherID] + + console.log(new Date().toISOString(), "Processing #{Object.keys(orphanTeacherMap).length} orphaned teachers with geo IPs...") + for teacherID, val of orphanTeacherMap + continue unless teacherMap[teacherID].country + country = teacherMap[teacherID].countryName or teacherMap[teacherID].country + country = 'UK' if country is 'GB' or country is 'United Kingdom' + country = 'USA' if country is 'US' or country is 'United States' + state = teacherMap[teacherID].region or 'unknown' + district = 'unknown' + school = 'unknown' + if teacherStudentMap[teacherID] and Object.keys(teacherStudentMap[teacherID]).length >= 10 + school += unknownSchoolCount++ + countryStateDistrictSchoolCountsMap[country] ?= {} + countryStateDistrictSchoolCountsMap[country][state] ?= {} + countryStateDistrictSchoolCountsMap[country][state][district] ?= {} + countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}} + countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true + if teacherStudentMap[teacherID] and Object.keys(teacherStudentMap[teacherID]).length >= 10 + for studentID, val of teacherStudentMap[teacherID] when orphanStudentMap[studentID] + countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true + delete orphanStudentMap[studentID] + delete orphanTeacherMap[teacherID] + + console.log(new Date().toISOString(), "Processing #{Object.keys(orphanTeacherMap).length} orphaned teachers with 10+ students...") + for teacherID, val of orphanTeacherMap + continue unless teacherStudentMap[teacherID] and Object.keys(teacherStudentMap[teacherID]).length >= 10 + country = 'unknown' + state = 'unknown' + district = 'unknown' + school = "unknown#{unknownSchoolCount++}" + countryStateDistrictSchoolCountsMap[country] ?= {} + countryStateDistrictSchoolCountsMap[country][state] ?= {} + countryStateDistrictSchoolCountsMap[country][state][district] ?= {} + countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}} + countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true + for studentID, val of teacherStudentMap[teacherID] when orphanStudentMap[studentID] + countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true + delete orphanStudentMap[studentID] + delete orphanTeacherMap[teacherID] + + console.log(new Date().toISOString(), "Processing #{Object.keys(orphanStudentMap).length} orphaned students with geo IPs...") + for studentID, val of orphanStudentMap + continue unless studentMap[studentID].geo?.country + country = studentMap[studentID].geo.countryName or studentMap[studentID].geo.country + country = 'UK' if country is 'GB' or country is 'United Kingdom' + country = 'USA' if country is 'US' or country is 'United States' + state = studentMap[studentID].geo.region or 'unknown' + district = 'unknown' + school = 'unknown' + countryStateDistrictSchoolCountsMap[country] ?= {} + countryStateDistrictSchoolCountsMap[country][state] ?= {} + countryStateDistrictSchoolCountsMap[country][state][district] ?= {} + countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}} + countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true + delete orphanStudentMap[studentID] console.log(new Date().toISOString(), 'Building country graphs...') @countryGraphs = {} @@ -146,11 +213,16 @@ module.exports = class SchoolCountsView extends RootView schools: @countryGraphs[country].totalSchools students: @countryGraphs[country].totalStudents teachers: @countryGraphs[country].totalTeachers - totalStudents += @countryGraphs[country].totalSchools + totalStudents += @countryGraphs[country].totalStudents totalTeachers += @countryGraphs[country].totalTeachers + + # Compare against orphanStudentMap and orphanTeacherMap to catch bugs @untriagedStudents = Object.keys(studentMap).length - totalStudents @untriagedTeachers = Object.keys(teacherMap).length - totalTeachers + console.log(new Date().toISOString(), "teacherMap #{Object.keys(teacherMap).length} totalTeachers #{totalTeachers} orphanTeacherMap #{Object.keys(orphanTeacherMap).length} @untriagedTeachers #{@untriagedTeachers}") + console.log(new Date().toISOString(), "studentMap #{Object.keys(studentMap).length} totalStudents #{totalStudents} orphanStudentMap #{Object.keys(orphanStudentMap).length} @untriagedStudents #{@untriagedStudents}") + for country, graph of @countryGraphs graph.stateCounts.sort (a, b) -> b.students - a.students or b.teachers - a.teachers or b.schools - a.schools or b.districts - a.districts or b.state.localeCompare(a.state) diff --git a/app/views/editor/verifier/VerifierView.coffee b/app/views/editor/verifier/VerifierView.coffee index b03c9bb30..32c8e22e2 100644 --- a/app/views/editor/verifier/VerifierView.coffee +++ b/app/views/editor/verifier/VerifierView.coffee @@ -30,6 +30,7 @@ module.exports = class VerifierView extends RootView if @levelID @levelIDs = [@levelID] @testLanguages = ['python', 'javascript', 'java', 'lua', 'coffeescript'] + @cores = 1 @startTestingLevels() else @campaigns = new Campaigns() diff --git a/package.json b/package.json index 80047b905..85f417574 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "co-express": "^1.2.1", "coffee-script": "1.9.x", "connect": "2.7.x", + "country-list": "0.0.3", "express": "~3.0.6", "express-useragent": "~0.0.9", "geoip-lite": "^1.1.6", @@ -101,7 +102,6 @@ "commonjs-require-definition": "0.2.0", "compressible": "~1.0.1", "country-data": "0.0.24", - "country-list": "0.0.3", "css-brunch": "^1.7.0", "fs-extra": "^0.26.2", "http-proxy": "^1.13.2", diff --git a/server/handlers/user_handler.coffee b/server/handlers/user_handler.coffee index 73be9b4ad..db3bc6d4e 100644 --- a/server/handlers/user_handler.coffee +++ b/server/handlers/user_handler.coffee @@ -408,7 +408,7 @@ UserHandler = class UserHandler extends Handler getSubSponsors: (req, res) -> return @sendForbiddenError(res) unless req.user?.isAdmin() - Payment.find {$where: 'this.purchaser.valueOf() != this.recipient.valueOf()'}, (err, payments) => + Payment.find {$where: 'this.purchaser && this.recipient && this.purchaser.valueOf() != this.recipient.valueOf()'}, (err, payments) => return @sendDatabaseError(res, err) if err sponsorIDs = (payment.get('purchaser') for payment in payments) User.find {$and: [{_id: {$in: sponsorIDs}}, {"stripe.sponsorSubscriptionID": {$exists: true}}]}, (err, users) => diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index 4f974f458..997cd87f0 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -1,6 +1,8 @@ _ = require 'lodash' co = require 'co' +countryList = require('country-list')() errors = require '../commons/errors' +geoip = require 'geoip-lite' wrap = require 'co-express' Promise = require 'bluebird' parse = require '../commons/parse' @@ -10,7 +12,6 @@ sendwithus = require '../sendwithus' User = require '../models/User' Classroom = require '../models/Classroom' - module.exports = fetchByGPlusID: wrap (req, res, next) -> gpID = req.query.gplusID @@ -97,11 +98,22 @@ module.exports = getStudents: wrap (req, res, next) -> throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin() - students = yield User.find({$and: [{schoolName: {$exists: true}}, {schoolName: {$ne: ''}}, {anonymous: false}]}).select('schoolName').lean() - res.status(200).send(students) + query = $or: [{role: 'student'}, {$and: [{schoolName: {$exists: true}}, {schoolName: {$ne: ''}}, {anonymous: false}]}] + users = yield User.find(query).select('lastIP schoolName').lean() + for user in users + if ip = user.lastIP + user.geo = geoip.lookup(ip) + if country = user.geo?.country + user.geo.countryName = countryList.getName(country) + res.status(200).send(users) getTeachers: wrap (req, res, next) -> throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin() teacherRoles = ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent', 'parent'] - teachers = yield User.find(anonymous: false, role: {$in: teacherRoles}).select('').lean() - res.status(200).send(teachers) + users = yield User.find(anonymous: false, role: {$in: teacherRoles}).select('lastIP').lean() + for user in users + if ip = user.lastIP + user.geo = geoip.lookup(ip) + if country = user.geo?.country + user.geo.countryName = countryList.getName(country) + res.status(200).send(users)