2016-06-21 12:29:41 -04:00
|
|
|
RootView = require 'views/core/RootView'
|
|
|
|
CocoCollection = require 'collections/CocoCollection'
|
|
|
|
Classroom = require 'models/Classroom'
|
2016-06-28 19:41:33 -04:00
|
|
|
CourseInstance = require 'models/CourseInstance'
|
2016-06-21 12:29:41 -04:00
|
|
|
TrialRequest = require 'models/TrialRequest'
|
|
|
|
User = require 'models/User'
|
2016-06-28 19:41:33 -04:00
|
|
|
utils = require 'core/utils'
|
2016-06-21 12:29:41 -04:00
|
|
|
|
|
|
|
# TODO: match anonymous trial requests with real users via email
|
2016-06-29 18:01:01 -04:00
|
|
|
# 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
|
2016-06-21 12:29:41 -04:00
|
|
|
|
|
|
|
module.exports = class SchoolCountsView extends RootView
|
|
|
|
id: 'admin-school-counts-view'
|
|
|
|
template: require 'templates/admin/school-counts'
|
|
|
|
|
|
|
|
initialize: ->
|
|
|
|
return super() unless me.isAdmin()
|
|
|
|
@classrooms = new CocoCollection([], { url: "/db/classroom/-/users", model: Classroom })
|
|
|
|
@supermodel.loadCollection(@classrooms, 'classrooms', {cache: false})
|
2016-06-28 19:41:33 -04:00
|
|
|
@courseInstances = new CocoCollection([], { url: "/db/course_instance/-/non-hoc", model: CourseInstance})
|
|
|
|
@supermodel.loadCollection(@courseInstances, 'course-instances', {cache: false})
|
2016-06-21 12:29:41 -04:00
|
|
|
@students = new CocoCollection([], { url: "/db/user/-/students", model: User })
|
|
|
|
@supermodel.loadCollection(@students, 'students', {cache: false})
|
|
|
|
@teachers = new CocoCollection([], { url: "/db/user/-/teachers", model: User })
|
|
|
|
@supermodel.loadCollection(@teachers, 'teachers', {cache: false})
|
|
|
|
@trialRequests = new CocoCollection([], { url: "/db/trial.request/-/users", model: TrialRequest })
|
|
|
|
@supermodel.loadCollection(@trialRequests, 'trial-requests', {cache: false})
|
|
|
|
super()
|
|
|
|
|
|
|
|
onLoaded: ->
|
|
|
|
return super() unless me.isAdmin()
|
|
|
|
|
|
|
|
console.log(new Date().toISOString(), 'onLoaded')
|
|
|
|
|
|
|
|
teacherMap = {} # Used to make sure teachers and students only counted once
|
|
|
|
studentMap = {} # Used to make sure teachers and students only counted once
|
2016-06-28 19:41:33 -04:00
|
|
|
studentNonHocMap = {} # Used to exclude HoC users
|
2016-06-21 12:29:41 -04:00
|
|
|
teacherStudentMap = {} # Used to link students to their teacher locations
|
2016-06-29 18:01:01 -04:00
|
|
|
unknownSchoolCount = 1 # Used to separate unique but unknown schools
|
2016-06-21 12:29:41 -04:00
|
|
|
|
2016-06-28 19:41:33 -04:00
|
|
|
console.log(new Date().toISOString(), "Processing #{@courseInstances.models.length} course instances...")
|
|
|
|
for courseInstance in @courseInstances.models
|
|
|
|
studentNonHocMap[courseInstance.get('ownerID')] = true
|
|
|
|
studentNonHocMap[studentID] = true for studentID in courseInstance.get('members') ? []
|
|
|
|
|
|
|
|
console.log(new Date().toISOString(), "Processing #{@classrooms.models.length} classrooms...")
|
2016-06-21 12:29:41 -04:00
|
|
|
for classroom in @classrooms.models
|
|
|
|
teacherID = classroom.get('ownerID')
|
|
|
|
teacherMap[teacherID] ?= {}
|
|
|
|
teacherStudentMap[teacherID] ?= {}
|
|
|
|
for studentID in classroom.get('members')
|
2016-06-29 18:01:01 -04:00
|
|
|
continue if teacherMap[studentID]
|
2016-06-28 19:41:33 -04:00
|
|
|
continue unless studentNonHocMap[studentID]
|
2016-06-29 18:01:01 -04:00
|
|
|
studentMap[studentID] = {}
|
2016-06-21 12:29:41 -04:00
|
|
|
teacherStudentMap[teacherID][studentID] = true
|
|
|
|
|
2016-06-28 19:41:33 -04:00
|
|
|
console.log(new Date().toISOString(), "Processing #{@teachers.models.length} teachers...")
|
2016-06-21 12:29:41 -04:00
|
|
|
for teacher in @teachers.models
|
2016-06-29 18:01:01 -04:00
|
|
|
teacherMap[teacher.id] = teacher.get('geo') ? {}
|
2016-06-21 12:29:41 -04:00
|
|
|
delete studentMap[teacher.id]
|
|
|
|
|
2016-06-28 19:41:33 -04:00
|
|
|
console.log(new Date().toISOString(), "Processing #{@students.models.length} students...")
|
2016-06-29 18:01:01 -04:00
|
|
|
for student in @students.models
|
2016-06-28 19:41:33 -04:00
|
|
|
continue unless studentNonHocMap[student.id]
|
2016-06-29 18:01:01 -04:00
|
|
|
continue if teacherMap[student.id]
|
|
|
|
studentMap[student.id] = {geo: student.get('geo')}
|
2016-06-21 12:29:41 -04:00
|
|
|
|
2016-06-29 18:01:01 -04:00
|
|
|
orphanStudentMap = _.cloneDeep(studentMap)
|
|
|
|
orphanTeacherMap = _.cloneDeep(teacherMap)
|
|
|
|
|
|
|
|
console.log(new Date().toISOString(), "Processing #{@trialRequests.models.length} trial requests...")
|
|
|
|
countryStateDistrictSchoolCountsMap = {}
|
2016-06-21 12:29:41 -04:00
|
|
|
for trialRequest in @trialRequests.models
|
|
|
|
teacherID = trialRequest.get('applicant')
|
|
|
|
unless teacherMap[teacherID]
|
2016-06-28 19:41:33 -04:00
|
|
|
# E.g. parents
|
2016-06-21 12:29:41 -04:00
|
|
|
# console.log("Skipping non-teacher #{teacherID} trial request #{trialRequest.id}")
|
2016-06-28 19:41:33 -04:00
|
|
|
continue
|
2016-06-21 12:29:41 -04:00
|
|
|
props = trialRequest.get('properties')
|
|
|
|
if props.nces_id and props.country and props.state
|
|
|
|
country = props.country
|
|
|
|
state = props.state
|
|
|
|
district = props.nces_district
|
|
|
|
school = props.nces_name
|
|
|
|
countryStateDistrictSchoolCountsMap[country] ?= {}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state] ?= {}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district] ?= {}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true
|
2016-06-29 18:01:01 -04:00
|
|
|
for studentID, val of teacherStudentMap[teacherID] when orphanStudentMap[studentID]
|
2016-06-21 12:29:41 -04:00
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true
|
2016-06-29 18:01:01 -04:00
|
|
|
delete orphanStudentMap[studentID]
|
|
|
|
delete orphanTeacherMap[teacherID]
|
2016-06-28 19:41:33 -04:00
|
|
|
else if not _.isEmpty(props.country)
|
2016-06-28 20:18:04 -04:00
|
|
|
country = props.country?.trim()
|
2016-06-28 19:41:33 -04:00
|
|
|
country = country[0].toUpperCase() + country.substring(1).toLowerCase()
|
2016-06-28 20:18:04 -04:00
|
|
|
country = 'Taiwan' if /台灣/ig.test(country)
|
|
|
|
country = 'UK' if /^uk$|united kingdom|england/ig.test(country)
|
|
|
|
country = 'USA' if /^u\.s\.?(\.a)?\.?$|^us$|america|united states|usa/ig.test(country)
|
2016-06-28 19:41:33 -04:00
|
|
|
state = props.state ? 'unknown'
|
|
|
|
if country is 'USA'
|
|
|
|
stateName = utils.usStateCodes.sanitizeStateName(state)
|
|
|
|
state = utils.usStateCodes.getStateCodeByStateName(stateName) if stateName
|
|
|
|
state = utils.usStateCodes.sanitizeStateCode(state) ? state
|
|
|
|
district = 'unknown'
|
|
|
|
school = props.organiziation ? 'unknown'
|
|
|
|
countryStateDistrictSchoolCountsMap[country] ?= {}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state] ?= {}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district] ?= {}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district][school] ?= {students: {}, teachers: {}}
|
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district][school].teachers[teacherID] = true
|
2016-06-29 18:01:01 -04:00
|
|
|
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]
|
2016-06-28 19:41:33 -04:00
|
|
|
countryStateDistrictSchoolCountsMap[country][state][district][school].students[studentID] = true
|
2016-06-29 18:01:01 -04:00
|
|
|
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]
|
2016-06-21 12:29:41 -04:00
|
|
|
|
2016-06-28 19:41:33 -04:00
|
|
|
console.log(new Date().toISOString(), 'Building country graphs...')
|
|
|
|
@countryGraphs = {}
|
|
|
|
@countryCounts = []
|
|
|
|
totalStudents = 0
|
|
|
|
totalTeachers = 0
|
2016-06-21 12:29:41 -04:00
|
|
|
for country, stateDistrictSchoolCountsMap of countryStateDistrictSchoolCountsMap
|
2016-06-28 19:41:33 -04:00
|
|
|
@countryGraphs[country] =
|
|
|
|
districtCounts: []
|
|
|
|
stateCounts: []
|
|
|
|
stateCountsMap: {}
|
|
|
|
totalSchools: 0
|
|
|
|
totalStates: 0
|
|
|
|
totalStudents: 0
|
|
|
|
totalTeachers: 0
|
2016-06-21 12:29:41 -04:00
|
|
|
for state, districtSchoolCountsMap of stateDistrictSchoolCountsMap
|
2016-06-28 19:41:33 -04:00
|
|
|
if utils.usStateCodes.sanitizeStateCode(state)? or ['GU', 'PR'].indexOf(state) >= 0
|
|
|
|
@countryGraphs[country].totalStates++
|
2016-06-21 12:29:41 -04:00
|
|
|
stateData = {state: state, districts: 0, schools: 0, students: 0, teachers: 0}
|
|
|
|
for district, schoolCountsMap of districtSchoolCountsMap
|
|
|
|
stateData.districts++
|
|
|
|
districtData = {state: state, district: district, schools: 0, students: 0, teachers: 0}
|
|
|
|
for school, counts of schoolCountsMap
|
|
|
|
studentCount = Object.keys(counts.students).length
|
|
|
|
teacherCount = Object.keys(counts.teachers).length
|
2016-06-28 19:41:33 -04:00
|
|
|
@countryGraphs[country].totalSchools++
|
|
|
|
@countryGraphs[country].totalStudents += studentCount
|
|
|
|
@countryGraphs[country].totalTeachers += teacherCount
|
2016-06-21 12:29:41 -04:00
|
|
|
stateData.schools++
|
|
|
|
stateData.students += studentCount
|
|
|
|
stateData.teachers += teacherCount
|
|
|
|
districtData.schools++
|
|
|
|
districtData.students += studentCount
|
|
|
|
districtData.teachers += teacherCount
|
2016-06-28 19:41:33 -04:00
|
|
|
@countryGraphs[country].districtCounts.push(districtData)
|
|
|
|
@countryGraphs[country].stateCounts.push(stateData)
|
|
|
|
@countryGraphs[country].stateCountsMap[state] = stateData
|
|
|
|
@countryCounts.push
|
|
|
|
country: country
|
|
|
|
schools: @countryGraphs[country].totalSchools
|
|
|
|
students: @countryGraphs[country].totalStudents
|
|
|
|
teachers: @countryGraphs[country].totalTeachers
|
2016-06-29 18:01:01 -04:00
|
|
|
totalStudents += @countryGraphs[country].totalStudents
|
2016-06-28 19:41:33 -04:00
|
|
|
totalTeachers += @countryGraphs[country].totalTeachers
|
2016-06-29 18:01:01 -04:00
|
|
|
|
|
|
|
# Compare against orphanStudentMap and orphanTeacherMap to catch bugs
|
2016-06-28 19:41:33 -04:00
|
|
|
@untriagedStudents = Object.keys(studentMap).length - totalStudents
|
|
|
|
@untriagedTeachers = Object.keys(teacherMap).length - totalTeachers
|
2016-06-21 12:29:41 -04:00
|
|
|
|
2016-06-29 18:01:01 -04:00
|
|
|
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}")
|
|
|
|
|
2016-06-28 19:41:33 -04:00
|
|
|
for country, graph of @countryGraphs
|
|
|
|
graph.stateCounts.sort (a, b) ->
|
2016-06-28 20:18:04 -04:00
|
|
|
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)
|
2016-06-28 19:41:33 -04:00
|
|
|
graph.districtCounts.sort (a, b) ->
|
|
|
|
if a.state isnt b.state
|
2016-06-28 20:18:04 -04:00
|
|
|
stateCountsA = graph.stateCountsMap[a.state]
|
|
|
|
stateCountsB = graph.stateCountsMap[b.state]
|
|
|
|
stateCountsB.students - stateCountsA.students or stateCountsB.teachers - stateCountsA.teachers or stateCountsB.schools - stateCountsA.schools or stateCountsB.districts - stateCountsA.districts or a.state.localeCompare(b.state)
|
2016-06-28 19:41:33 -04:00
|
|
|
else
|
2016-06-28 20:18:04 -04:00
|
|
|
b.students - a.students or b.teachers - a.teachers or b.schools - a.schools or b.district.localeCompare(a.district)
|
2016-06-28 19:41:33 -04:00
|
|
|
@countryCounts.sort (a, b) ->
|
2016-06-28 20:18:04 -04:00
|
|
|
b.students - a.students or b.teachers - a.teachers or b.schools - a.schools or b.country.localeCompare(a.country)
|
2016-06-28 19:41:33 -04:00
|
|
|
|
|
|
|
console.log(new Date().toISOString(), 'Done...')
|
2016-06-21 12:29:41 -04:00
|
|
|
super()
|