mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 08:50:58 -05:00
School active licenses admin page
This commit is contained in:
parent
0922eec2cc
commit
607c129c7f
7 changed files with 192 additions and 14 deletions
|
@ -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')
|
||||
|
|
21
app/styles/admin/admin-school-licenses.sass
Normal file
21
app/styles/admin/admin-school-licenses.sass
Normal 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
|
|
@ -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
|
||||
|
|
39
app/templates/admin/school-licenses.jade
Normal file
39
app/templates/admin/school-licenses.jade
Normal 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
|
||||
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}
|
||||
td #{prepaid.used}
|
||||
td= school.activity
|
72
app/views/admin/SchoolLicensesView.coffee
Normal file
72
app/views/admin/SchoolLicensesView.coffee
Normal 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()
|
|
@ -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})
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue