School active licenses admin page

This commit is contained in:
Matt Lott 2016-07-18 09:41:42 -07:00
parent 0922eec2cc
commit 607c129c7f
7 changed files with 192 additions and 14 deletions

View file

@ -34,9 +34,10 @@ module.exports = class CocoRouter extends Backbone.Router
'admin/design-elements': go('admin/DesignElementsView')
'admin/files': go('admin/FilesView')
'admin/analytics': go('admin/AnalyticsView')
'admin/school-counts': go('admin/SchoolCountsView')
'admin/analytics/subscriptions': go('admin/AnalyticsSubscriptionsView')
'admin/level-sessions': go('admin/LevelSessionsView')
'admin/school-counts': go('admin/SchoolCountsView')
'admin/school-licenses': go('admin/SchoolLicensesView')
'admin/users': go('admin/UsersView')
'admin/base': go('admin/BaseView')
'admin/demo-requests': go('admin/DemoRequestsView')

View file

@ -0,0 +1,21 @@
#admin-school-licenses-view
table
td, th
padding: 0px
.range-container
position: relative
width: 100%
.range-background
position: absolute
height: 100%
left: 0px
top: 0px
background-color: green
opacity: 0.25
.range-dates
position: absolute
height: 100%
left: 0px
top: 0px

View file

@ -47,6 +47,8 @@ block content
input.classroom-progress-class-code(type=text value="<class code>")
li
a(href="/admin/analytics") Dashboard
li
a(href="/admin/school-licenses") School Active Licenses
li
a(href="/admin/school-counts") School Counts
li

View file

@ -0,0 +1,39 @@
extends /templates/base-flat
//- DO NOT TRANSLATE
block content
if !me.isAdmin()
div You must be logged in as an admin to view this page.
else if !view.schools
h3 Loading...
else
h3 School Active Licenses
.small Max: total licenses
.small Used: licenses redeemed
.small Activity: level sessions created in last 30 days
table.table.table-condensed
thead
th School
th Max
th Used&nbsp;
th Activity
tr
td(style="height:26px;").range-container
each rangeKey in view.rangeKeys
span.range-background(style="left:#{rangeKey.startScale}%;width:#{rangeKey.width}%;background-color:#{rangeKey.color}")
span.range-dates(style="left:#{rangeKey.startScale}%;width:#{rangeKey.width}%;") #{rangeKey.name}
td(colspan=2)
each school in view.schools
each prepaid in school.prepaids
tr
td.range-container
span.range-background(style="left:#{prepaid.startScale}%;width:#{prepaid.rangeScale}%;")
span.range-dates(style="left:#{prepaid.startScale}%;")
span.spr #{prepaid.startDate.substring(0, 10)}
strong.spr #{school.name}
span #{prepaid.endDate.substring(0, 10)}
td #{prepaid.max}&nbsp;
td #{prepaid.used}
td= school.activity

View file

@ -0,0 +1,72 @@
RootView = require 'views/core/RootView'
CocoCollection = require 'collections/CocoCollection'
Prepaid = require 'models/Prepaid'
TrialRequests = require 'collections/TrialRequests'
# TODO: year ranges hard-coded
module.exports = class SchoolLicensesView extends RootView
id: 'admin-school-licenses-view'
template: require 'templates/admin/school-licenses'
initialize: ->
return super() unless me.isAdmin()
@startDateRange = new Date()
@endDateRange = new Date()
@endDateRange.setUTCFullYear(@endDateRange.getUTCFullYear() + 2)
@supermodel.addRequestResource({
url: '/db/prepaid/-/active-schools'
method: 'GET'
success: ({prepaidActivityMap, schoolPrepaidsMap}) =>
@updateSchools(prepaidActivityMap, schoolPrepaidsMap)
}, 0).load()
super()
updateSchools: (prepaidActivityMap, schoolPrepaidsMap) ->
timeStart = @startDateRange.getTime()
time2017 = new Date('2017').getTime()
time2018 = new Date('2018').getTime()
timeEnd = @endDateRange.getTime()
rangeMilliseconds = timeEnd - timeStart
@rangeKeys = [
{name :'Today', color: 'blue', startScale: 0, width: Math.round((time2017 - timeStart) / rangeMilliseconds * 100)}
{name: '2017', color: 'red', startScale: Math.round((time2017 - timeStart) / rangeMilliseconds * 100), width: Math.round((time2018 - time2017) / rangeMilliseconds * 100)}
{name: '2018', color: 'yellow', startScale: Math.round((time2018 - timeStart) / rangeMilliseconds * 100), width: Math.round((timeEnd - time2018) / rangeMilliseconds * 100)}
]
@schools = []
for school, prepaids of schoolPrepaidsMap
activity = 0
schoolMax = 0
schoolUsed = 0
collapsedPrepaids = []
for prepaid in prepaids
activity += prepaidActivityMap[prepaid._id] ? 0
startDate = prepaid.startDate
endDate = prepaid.endDate
max = parseInt(prepaid.maxRedeemers)
used = parseInt(prepaid.redeemers?.length ? 0)
schoolMax += max
schoolUsed += used
foundIdenticalDates = false
for collapsedPrepaid in collapsedPrepaids
if collapsedPrepaid.startDate.substring(0, 10) is startDate.substring(0, 10) and collapsedPrepaid.endDate.substring(0, 10) is endDate.substring(0, 10)
collapsedPrepaid.max += parseInt(prepaid.maxRedeemers)
collapsedPrepaid.used += parseInt(prepaid.redeemers?.length ? 0)
foundIdenticalDates = true
break
unless foundIdenticalDates
collapsedPrepaids.push({startDate, endDate, max, used})
for collapsedPrepaid in collapsedPrepaids
collapsedPrepaid.startScale = Math.round((new Date(collapsedPrepaid.startDate).getTime() - @startDateRange.getTime()) / rangeMilliseconds * 100)
collapsedPrepaid.startScale = 0 if collapsedPrepaid.startScale < 0
collapsedPrepaid.rangeScale = Math.round((new Date(collapsedPrepaid.endDate).getTime() - new Date(collapsedPrepaid.startDate).getTime()) / rangeMilliseconds * 100)
collapsedPrepaid.rangeScale = 100 - collapsedPrepaid.startScale if collapsedPrepaid.rangeScale + collapsedPrepaid.startScale > 100
@schools.push {name: school, activity, max: schoolMax, used: schoolUsed, prepaids: collapsedPrepaids, startDate: collapsedPrepaids[0].startDate, endDate: collapsedPrepaids[0].endDate}
@schools.sort (a, b) ->
b.activity - a.activity or new Date(a.endDate).getTime() - new Date(b.endDate).getTime() or b.max - a.max or b.used - a.used or b.prepaids.length - a.prepaids.length or b.name.localeCompare(a.name)
# console.log @schools
@render()

View file

@ -1,9 +1,11 @@
wrap = require 'co-express'
errors = require '../commons/errors'
database = require '../commons/database'
Prepaid = require '../models/Prepaid'
User = require '../models/User'
mongoose = require 'mongoose'
LevelSession = require '../models/LevelSession'
Prepaid = require '../models/Prepaid'
TrialRequest = require '../models/TrialRequest'
User = require '../models/User'
cutoffDate = new Date(2015,11,11)
cutoffID = mongoose.Types.ObjectId(Math.floor(cutoffDate/1000).toString(16)+'0000000000000000')
@ -11,14 +13,14 @@ cutoffID = mongoose.Types.ObjectId(Math.floor(cutoffDate/1000).toString(16)+'000
module.exports =
logError: (user, msg) ->
console.warn "Prepaid Error: [#{user.get('slug')} (#{user._id})] '#{msg}'"
post: wrap (req, res) ->
validTypes = ['course']
unless req.body.type in validTypes
throw new errors.UnprocessableEntity("type must be on of: #{validTypes}.")
# TODO: deprecate or refactor other prepaid types
if req.body.creator
user = yield User.search(req.body.creator)
if not user
@ -32,16 +34,16 @@ module.exports =
database.validateDoc(prepaid)
yield prepaid.save()
res.status(201).send(prepaid.toObject())
redeem: wrap (req, res) ->
if not req.user?.isTeacher()
throw new errors.Forbidden('Must be a teacher to use licenses')
prepaid = yield database.getDocFromHandle(req, Prepaid)
if not prepaid
throw new errors.NotFound('Prepaid not found.')
if prepaid._id.getTimestamp().getTime() < cutoffDate.getTime()
throw new errors.Forbidden('Cannot redeem from prepaids older than November 11, 2015')
unless prepaid.get('creator').equals(req.user._id)
@ -61,7 +63,7 @@ module.exports =
return res.status(200).send(prepaid.toObject({req: req}))
if user.isTeacher()
throw new errors.Forbidden('Teachers may not be enrolled')
query =
_id: prepaid._id
'redeemers.userID': { $ne: user._id }
@ -71,7 +73,7 @@ module.exports =
if result.nModified is 0
@logError(req.user, "POST prepaid redeemer lost race on maxRedeemers")
throw new errors.Forbidden('This prepaid is exhausted')
update = {
$set: {
coursePrepaid: {
@ -84,7 +86,7 @@ module.exports =
if not user.get('role')
update.$set.role = 'student'
yield user.update(update)
# return prepaid with new redeemer added locally
redeemers = _.clone(prepaid.get('redeemers') or [])
redeemers.push({ date: new Date(), userID: user._id })
@ -94,12 +96,12 @@ module.exports =
fetchByCreator: wrap (req, res, next) ->
creator = req.query.creator
return next() if not creator
unless req.user.isAdmin() or creator is req.user.id
throw new errors.Forbidden('Must be logged in as given creator')
unless database.isID(creator)
throw new errors.UnprocessableEntity('Invalid creator')
q = {
_id: { $gt: cutoffID }
creator: mongoose.Types.ObjectId(creator)
@ -108,3 +110,43 @@ module.exports =
prepaids = yield Prepaid.find(q)
res.send((prepaid.toObject({req: req}) for prepaid in prepaids))
fetchActiveSchools: wrap (req, res) ->
unless req.user.isAdmin() or creator is req.user.id
throw new errors.Forbidden('Must be logged in as given creator')
prepaids = yield Prepaid.find({type: 'course'}, {creator: 1, properties: 1, startDate: 1, endDate: 1, maxRedeemers: 1, redeemers: 1}).lean()
userPrepaidsMap = {}
today = new Date()
userIDs = []
redeemerIDs = []
redeemerPrepaidMap = {}
for prepaid in prepaids
continue if new Date(prepaid.endDate ? prepaid.properties?.endDate ? '2000') < today
continue if new Date(prepaid.endDate) < new Date(prepaid.startDate)
userPrepaidsMap[prepaid.creator.valueOf()] ?= []
userPrepaidsMap[prepaid.creator.valueOf()].push(prepaid)
userIDs.push prepaid.creator
for redeemer in prepaid.redeemers ? []
redeemerIDs.push redeemer.userID + ""
redeemerPrepaidMap[redeemer.userID + ""] = prepaid._id.valueOf()
# Find recently created level sessions for redeemers
lastMonth = new Date()
lastMonth.setUTCDate(lastMonth.getUTCDate() - 30)
levelSessions = yield LevelSession.find({$and: [{created: {$gte: lastMonth}}, {creator: {$in: redeemerIDs}}]}, {creator: 1}).lean()
prepaidActivityMap = {}
for levelSession in levelSessions
prepaidActivityMap[redeemerPrepaidMap[levelSession.creator.valueOf()]] ?= 0
prepaidActivityMap[redeemerPrepaidMap[levelSession.creator.valueOf()]]++
trialRequests = yield TrialRequest.find({$and: [{type: 'course'}, {applicant: {$in: userIDs}}]}, {applicant: 1, properties: 1}).lean()
schoolPrepaidsMap = {}
for trialRequest in trialRequests
school = trialRequest.properties?.organization ? trialRequest.properties?.school
continue unless school
if userPrepaidsMap[trialRequest.applicant.valueOf()]?.length > 0
schoolPrepaidsMap[school] ?= []
for prepaid in userPrepaidsMap[trialRequest.applicant.valueOf()]
schoolPrepaidsMap[school].push prepaid
res.send({prepaidActivityMap, schoolPrepaidsMap})

View file

@ -104,6 +104,7 @@ module.exports.setup = (app) ->
app.post('/db/user/:handle/signup-with-password', mw.users.signupWithPassword)
app.get('/db/prepaid', mw.auth.checkLoggedIn(), mw.prepaids.fetchByCreator)
app.get('/db/prepaid/-/active-schools', mw.auth.checkHasPermission(['admin']), mw.prepaids.fetchActiveSchools)
app.post('/db/prepaid', mw.auth.checkHasPermission(['admin']), mw.prepaids.post)
app.post('/db/prepaid/:handle/redeemers', mw.prepaids.redeem)