###1Functions for determining various things about applying upgrades to a project.23WARNING: Pure Javascript with no crazy dependencies for easy unit testing.4###56misc = require('smc-util/misc')7{defaults, required, types} = misc89exports.available_upgrades = (opts) ->10types opts,11account_id : types.string.isRequired # id of a user12purchased_upgrades : types.object.isRequired # map of the total upgrades purchased by account_id13project_map : types.immutable.Map.isRequired # immutable.js map of data about projects14student_project_ids : types.object.isRequired # map project_id:true with keys *all* student15# projects in course, including deleted16###17Return the total upgrades that the user with given account_id has to apply18toward this course. This is all upgrades they have purchased minus19upgrades they have applied to projects that aren't student projects in20this course. Thus this is what they have available to distribute to21their students in this course.2223This is a map {quota0:x, quota1:y, ...}24###25available = misc.copy(opts.purchased_upgrades)26opts.project_map.forEach (project, project_id) ->27if opts.student_project_ids[project_id] # do not count projects in course28return29upgrades = project.getIn(['users', opts.account_id, 'upgrades'])?.toJS()30if upgrades?31available = misc.map_diff(available, upgrades)32return33return available343536exports.current_student_project_upgrades = (opts) ->37types opts,38account_id : types.string.isRequired # id of a user39project_map : types.immutable.Map.isRequired # immutable.js map of data about projects40student_project_ids : types.object.isRequired # map project_id:true with keys *all* student41###42Return the total upgrades currently applied to each student project from43everybody else except the user with given account_id.4445This output is a map {project_id:{quota0:x, quota1:y, ...}, ...}; only projects with46actual upgrades are included.47###48other = {}49for project_id of opts.student_project_ids50users = opts.project_map.getIn([project_id, 'users'])51if not users?52continue53x = undefined54users.forEach (info, user_id) ->55if user_id == opts.account_id56return57upgrades = info.get('upgrades')?.toJS()58if not upgrades?59return60x = misc.map_sum(upgrades, x ? {})61return62if x?63other[project_id] = x64return other6566exports.upgrade_plan = (opts) ->67types opts,68account_id : types.string.isRequired # id of a user69purchased_upgrades : types.object.isRequired # map of the total upgrades purchased by account_id70project_map : types.immutable.Map.isRequired # immutable.js map of data about projects71student_project_ids : types.object.isRequired # map project_id:true with keys *all* student72# projects in course, including deleted73deleted_project_ids : types.object.isRequired # map project_id:true just for projects where74# student is considered deleted from class75upgrade_goal : types.object.isRequired # [quota0:x, quota1:y]76###77Determine what upgrades should be applied by this user to get78the student projects to the given upgrade goal. Preference79is by project_id in order (arbitrary, but stable).8081The output is a map {student_project_id:{quota0:x, quota1:y, ...}, ...}, where the quota0:x means82that account_id will apply x amount of quota0 total. Thus to actually *do* the upgrading,83this user (account_id) would go through the project map and set their upgrade contribution84for the student projects in this course to exactly what is specified by this function.85Note that no upgrade quota will be deducted from projects outside this course to satisfy86the upgrade_goal.8788If a student_project_id is missing from the output the contribution is 0; if a quota is89missing, the contribution is 0.9091The keys of the output map are **exactly** the ids of the projects where the current92allocation should be *changed*. That said, we only consider quotas explicitly given93in the upgrade_goal map.94###95# upgrades, etc., that student projects already have (which account_id did not provide)96cur = exports.current_student_project_upgrades97account_id : opts.account_id98project_map : opts.project_map99student_project_ids : opts.student_project_ids100101# upgrades we have that have not been allocated to our course102available = exports.available_upgrades103account_id : opts.account_id104purchased_upgrades : opts.purchased_upgrades105project_map : opts.project_map106student_project_ids : opts.student_project_ids107108ids = misc.keys(opts.student_project_ids); ids.sort()109plan = {}110for project_id in ids111if opts.deleted_project_ids[project_id]112# give this project NOTHING113continue114plan[project_id] = {}115# we only care about quotas in the upgrade_goal116for quota, val of opts.upgrade_goal117need = val - (cur[project_id]?[quota] ? 0)118if need > 0119have = Math.min(need, available[quota])120plan[project_id][quota] = have121available[quota] -= have122# is there an actual allocation change? if not, we do not include this key.123alloc = opts.project_map.getIn([project_id, 'users', opts.account_id, 'upgrades'])?.toJS() ? {}124change = false125for quota, _ of opts.upgrade_goal126if (alloc[quota] ? 0) != (plan[project_id][quota] ? 0)127change = true128break129if not change130delete plan[project_id]131return plan132133134135136137138139