Merge pull request #1162 from sharelatex/ja-show-group-management

Add group and institution membership and management info to subscription dashboard

GitOrigin-RevId: 1aba5d5a20cd00ff5090811d0f66dc9c4944dd60
This commit is contained in:
James Allen 2018-11-20 11:48:21 +01:00 committed by sharelatex
parent 392be7cb44
commit 49e19cad64
18 changed files with 288 additions and 49 deletions

View file

@ -1,4 +1,6 @@
UserGetter = require '../User/UserGetter'
UserMembershipHandler = require "../UserMembership/UserMembershipHandler"
UserMembershipEntityConfigs = require "../UserMembership/UserMembershipEntityConfigs"
logger = require 'logger-sharelatex'
module.exports = InstitutionsGetter =
@ -12,3 +14,6 @@ module.exports = InstitutionsGetter =
emailData.affiliation?.institution
callback(null, confirmedInstitutions)
getManagedInstitutions: (user_id, callback = (error, managedInstitutions) ->) ->
UserMembershipHandler.getEntitiesByUser UserMembershipEntityConfigs.institution, user_id, callback

View file

@ -91,16 +91,35 @@ module.exports = SubscriptionController =
user = AuthenticationController.getSessionUser(req)
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel user, (error, results) ->
return next(error) if error?
{ personalSubscription, groupSubscriptions, v1Subscriptions } = results
logger.log {user, personalSubscription, groupSubscriptions, v1Subscriptions}, "showing subscription dashboard"
{
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
v1Subscriptions
} = results
logger.log {
user,
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
v1Subscriptions
}, "showing subscription dashboard"
plans = SubscriptionViewModelBuilder.buildViewModel()
data =
data = {
title: "your_subscription"
plans: plans
user: user
personalSubscription: personalSubscription
groupSubscriptions: groupSubscriptions
v1Subscriptions: v1Subscriptions
plans,
user,
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
v1Subscriptions
}
res.render "subscriptions/dashboard", data
createSubscription: (req, res, next)->

View file

@ -5,10 +5,7 @@ ObjectId = require('mongoose').Types.ObjectId
module.exports = SubscriptionLocator =
getUsersSubscription: (user_or_id, callback)->
if user_or_id? and user_or_id._id?
user_id = user_or_id._id
else if user_or_id?
user_id = user_or_id
user_id = @_getUserId(user_or_id)
logger.log user_id:user_id, "getting users subscription"
Subscription.findOne admin_id:user_id, (err, subscription)->
logger.log user_id:user_id, "got users subscription"
@ -18,11 +15,15 @@ module.exports = SubscriptionLocator =
logger.log managerId: managerId, "finding managed subscription"
Subscription.findOne manager_ids: managerId, callback
getManagedGroupSubscriptions: (user_or_id, callback = (error, managedSubscriptions) ->) ->
user_id = @_getUserId(user_or_id)
Subscription.find({
manager_ids: user_or_id,
groupPlan: true
}).populate("admin_id").exec callback
getMemberSubscriptions: (user_or_id, callback) ->
if user_or_id? and user_or_id._id?
user_id = user_or_id._id
else if user_or_id?
user_id = user_or_id
user_id = @_getUserId(user_or_id)
logger.log user_id: user_id, "getting users group subscriptions"
Subscription.find(member_ids: user_id).populate("admin_id").exec callback
@ -40,3 +41,9 @@ module.exports = SubscriptionLocator =
getGroupWithV1Id: (v1TeamId, callback) ->
Subscription.findOne { "overleaf.id": v1TeamId }, callback
_getUserId: (user_or_id) ->
if user_or_id? and user_or_id._id?
return user_or_id._id
else if user_or_id?
return user_or_id

View file

@ -5,6 +5,7 @@ SubscriptionFormatters = require("./SubscriptionFormatters")
LimitationsManager = require("./LimitationsManager")
SubscriptionLocator = require("./SubscriptionLocator")
V1SubscriptionManager = require("./V1SubscriptionManager")
InstitutionsGetter = require("../Institutions/InstitutionsGetter")
logger = require('logger-sharelatex')
_ = require("underscore")
async = require('async')
@ -37,8 +38,14 @@ module.exports =
return cb(new Error("No plan found for planCode '#{personalSubscription.planCode}'")) if !plan?
cb(null, plan)
]
groupSubscriptions: (cb) ->
memberGroupSubscriptions: (cb) ->
SubscriptionLocator.getMemberSubscriptions user, cb
managedGroupSubscriptions: (cb) ->
SubscriptionLocator.getManagedGroupSubscriptions user, cb
confirmedMemberInstitutions: (cb) ->
InstitutionsGetter.getConfirmedInstitutions user._id, cb
managedInstitutions: (cb) ->
InstitutionsGetter.getManagedInstitutions user._id, cb
v1Subscriptions: (cb) ->
V1SubscriptionManager.getSubscriptionsFromV1 user._id, (error, subscriptions, v1Id) ->
return cb(error) if error?
@ -46,10 +53,23 @@ module.exports =
cb(null, subscriptions)
}, (err, results) ->
return callback(err) if err?
{personalSubscription, groupSubscriptions, v1Subscriptions, recurlySubscription, plan} = results
groupSubscriptions ?= []
{
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
v1Subscriptions,
recurlySubscription,
plan
} = results
memberGroupSubscriptions ?= []
managedGroupSubscriptions ?= []
confirmedMemberInstitutions ?= []
managedInstitutions ?= []
v1Subscriptions ?= {}
if personalSubscription?.toObject?
# Downgrade from Mongoose object, so we can add a recurly and plan attribute
personalSubscription = personalSubscription.toObject()
@ -72,7 +92,12 @@ module.exports =
}
callback null, {
personalSubscription, groupSubscriptions, v1Subscriptions
personalSubscription,
managedGroupSubscriptions,
memberGroupSubscriptions,
confirmedMemberInstitutions,
managedInstitutions,
v1Subscriptions
}
buildViewModel : ->

View file

@ -7,6 +7,7 @@ EntityModels =
UserMembershipViewModel = require('./UserMembershipViewModel')
UserGetter = require('../User/UserGetter')
logger = require('logger-sharelatex')
UserMembershipEntityConfigs = require "./UserMembershipEntityConfigs"
module.exports =
getEntity: (entityId, entityConfig, loggedInUser, callback = (error, entity) ->) ->
@ -39,6 +40,15 @@ module.exports =
return callback(isAdmin: true)
removeUserFromEntity entity, attribute, userId, callback
getEntitiesByUser: (entityConfig, userId, callback = (error, entities) ->) ->
query = Object.assign({}, entityConfig.baseQuery)
query[entityConfig.fields.access] = userId
EntityModels[entityConfig.modelName].find query, (error, entities = []) ->
return callback(error) if error?
async.mapSeries entities,
(entity, cb) -> entity.fetchV1Data(cb),
callback
getPopulatedListOfMembers = (entity, attributes, callback = (error, users)->)->
userObjects = []

View file

@ -19,6 +19,11 @@ InstitutionSchema.method 'fetchV1Data', (callback = (error, institution)->) ->
this.portalSlug = parsedBody?.portal_slug
callback(null, this)
mongoose.model 'Institution', InstitutionSchema
exports.Institution = mongoose.model 'Institution'
conn = mongoose.createConnection(settings.mongo.url, {
server: {poolSize: settings.mongo.poolSize || 10},
config: {autoIndex: false}
})
Institution = conn.model 'Institution', InstitutionSchema
exports.Institution = Institution
exports.InstitutionSchema = InstitutionSchema

View file

@ -1,5 +1,7 @@
extends ../layout
include ./dashboard/_team_name_mixin
block content
.content.content-alt(ng-cloak)
.container
@ -14,10 +16,22 @@ block content
-hasAnySubscription = true
include ./dashboard/_personal_subscription
-if (groupSubscriptions && groupSubscriptions.length > 0)
-if (managedGroupSubscriptions && managedGroupSubscriptions.length > 0)
-hasAnySubscription = true
include ./dashboard/_managed_groups
-if (managedInstitutions && managedInstitutions.length > 0)
-hasAnySubscription = true
include ./dashboard/_managed_institutions
-if (memberGroupSubscriptions && memberGroupSubscriptions.length > 0)
-hasAnySubscription = true
include ./dashboard/_group_memberships
-if (confirmedMemberInstitutions && confirmedMemberInstitutions.length > 0)
-hasAnySubscription = true
include ./dashboard/_institution_memberships
-if (settings.overleaf && v1Subscriptions)
include ./dashboard/_v1_subscriptions

View file

@ -1,8 +1,14 @@
div(ng-controller="GroupMembershipController")
each groupSubscription in groupSubscriptions
each groupSubscription in memberGroupSubscriptions
- if (user._id+'' != groupSubscription.admin_id._id+'')
div
p !{translate("member_of_group_subscription", {admin_email: "<strong>" + groupSubscription.admin_id.email + "</strong>"})}
p
| You are a member of
|
+teamName(groupSubscription)
- if (groupSubscription.teamNotice && groupSubscription.teamNotice != '')
p
em= groupSubscription.teamNotice
span
button.btn.btn-danger.text-capitalise(ng-click="removeSelfFromGroup('"+groupSubscription.admin_id._id+"')") #{translate("leave_group")}
hr

View file

@ -0,0 +1,12 @@
each institution in confirmedMemberInstitutions
if (institution.licence != 'free')
p
| You have a
|
strong Professional
|
| #{settings.appName} account as a confirmed member of
|
strong= institution.name
hr

View file

@ -0,0 +1,13 @@
each managedGroupSubscription in managedGroupSubscriptions
p
| You are a manager of
|
+teamName(managedGroupSubscription)
p
a.btn.btn-primary(href="/manage/groups/" + managedGroupSubscription._id + "/members")
| Manage members
| &nbsp;
a.btn.btn-primary(href="/manage/groups/" + managedGroupSubscription._id + "/managers")
| Manage group managers
hr

View file

@ -0,0 +1,9 @@
each institution in managedInstitutions
p
| You are a manager of
|
strong= institution.name
p
a.btn.btn-primary(href="/manage/institutions/" + institution.v1Id + "/managers")
| Manage institution managers
hr

View file

@ -3,11 +3,4 @@ if (personalSubscription.recurly)
else
include ./_personal_subscription_custom
if(personalSubscription.groupPlan)
hr
p
| You are the manager of a group subscription
p
a(href="/subscription/group").btn.btn-success !{translate("manage_group")}
hr

View file

@ -0,0 +1,9 @@
mixin teamName(subscription)
- if (subscription.teamName && subscription.teamName != '')
strong= subscription.teamName
- else if (subscription.admin_id._id == user._id)
| a group account
- else
| the group account owned by
|
strong= subscription.admin_id.email

View file

@ -24,8 +24,8 @@ describe "ProxyUrls", ->
it 'proxy dynamic URLs', (done) ->
async.series [
(cb) -> assertResponse '/institutions/list/123', 200, { id: 123 }, cb
(cb) -> assertResponse '/institutions/list/456', 200, { id: 456 }, cb
(cb) -> assertResponse '/institutions/list/123', 200, { id: 123, name: "Institution 123" }, cb
(cb) -> assertResponse '/institutions/list/456', 200, { id: 456, name: "Institution 456" }, cb
],
done

View file

@ -2,6 +2,7 @@ expect = require('chai').expect
async = require("async")
User = require "./helpers/User"
{Subscription} = require "../../../app/js/models/Subscription"
{Institution} = require "../../../app/js/models/Institution"
SubscriptionViewModelBuilder = require "../../../app/js/Features/Subscription/SubscriptionViewModelBuilder"
MockRecurlyApi = require "./helpers/MockRecurlyApi"
@ -22,8 +23,8 @@ describe 'Subscriptions', ->
it 'should return no personalSubscription', ->
expect(@data.personalSubscription).to.equal null
it 'should return no groupSubscriptions', ->
expect(@data.groupSubscriptions).to.deep.equal []
it 'should return no memberGroupSubscriptions', ->
expect(@data.memberGroupSubscriptions).to.deep.equal []
it 'should return no v1Subscriptions', ->
expect(@data.v1Subscriptions).to.deep.equal {}
@ -81,8 +82,8 @@ describe 'Subscriptions', ->
"trialEndsAtFormatted": "7th July 2018"
}
it 'should return no groupSubscriptions', ->
expect(@data.groupSubscriptions).to.deep.equal []
it 'should return no memberGroupSubscriptions', ->
expect(@data.memberGroupSubscriptions).to.deep.equal []
describe 'when the user has a subscription without recurly', ->
before (done) ->
@ -109,8 +110,8 @@ describe 'Subscriptions', ->
expect(subscription.planCode).to.equal 'collaborator'
expect(subscription.recurly).to.not.exist
it 'should return no groupSubscriptions', ->
expect(@data.groupSubscriptions).to.deep.equal []
it 'should return no memberGroupSubscriptions', ->
expect(@data.memberGroupSubscriptions).to.deep.equal []
describe 'when the user is a member of a group subscription', ->
before (done) ->
@ -153,16 +154,122 @@ describe 'Subscriptions', ->
it 'should return no personalSubscription', ->
expect(@data.personalSubscription).to.equal null
it 'should return the two groupSubscriptions', ->
expect(@data.groupSubscriptions.length).to.equal 2
it 'should return the two memberGroupSubscriptions', ->
expect(@data.memberGroupSubscriptions.length).to.equal 2
expect(
# Mongoose populates the admin_id with the user
@data.groupSubscriptions[0].admin_id._id.toString()
@data.memberGroupSubscriptions[0].admin_id._id.toString()
).to.equal @owner1._id
expect(
@data.groupSubscriptions[1].admin_id._id.toString()
@data.memberGroupSubscriptions[1].admin_id._id.toString()
).to.equal @owner2._id
describe 'when the user is a manager of a group subscription', ->
before (done) ->
@owner1 = new User()
@owner2 = new User()
async.series [
(cb) => @owner1.ensureUserExists cb
(cb) => @owner2.ensureUserExists cb
(cb) => Subscription.create {
admin_id: @owner1._id,
manager_ids: [@owner1._id, @user._id],
planCode: 'collaborator',
groupPlan: true
}, cb
], (error) =>
return done(error) if error?
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
return done(error) if error?
done()
return
after (done) ->
Subscription.remove {
admin_id: @owner1._id
}, done
return
it 'should return no personalSubscription', ->
expect(@data.personalSubscription).to.equal null
it 'should return the managedGroupSubscriptions', ->
expect(@data.managedGroupSubscriptions.length).to.equal 1
subscription = @data.managedGroupSubscriptions[0]
expect(
# Mongoose populates the admin_id with the user
subscription.admin_id._id.toString()
).to.equal @owner1._id
expect(subscription.groupPlan).to.equal true
describe 'when the user is a manager of an institution', ->
before (done) ->
@v1Id = MockV1Api.nextV1Id()
async.series [
(cb) =>
Institution.create({
v1Id: @v1Id,
managerIds: [@user._id]
}, cb)
], (error) =>
return done(error) if error?
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
return done(error) if error?
done()
return
after (done) ->
Institution.remove {
v1Id: @v1Id
}, done
return
it 'should return the managedInstitutions', ->
expect(@data.managedInstitutions.length).to.equal 1
institution = @data.managedInstitutions[0]
expect(institution.v1Id).to.equal @v1Id
expect(institution.name).to.equal "Institution #{@v1Id}"
describe 'when the user is a member of an affiliation', ->
before (done) ->
v1Id = MockV1Api.nextV1Id()
MockV1Api.setUser v1Id, {
subscription: {}
}
MockV1Api.setAffiliations [{
email: 'confirmed-affiliation-email@stanford.example.edu'
institution: { name: 'Stanford', licence: 'pro_plus', confirmed: true }
}, {
email: 'unconfirmed-affiliation-email@harvard.example.edu'
institution: { name: 'Harvard', licence: 'pro_plus', confirmed: true }
}, {
email: 'confirmed-affiliation-email@mit.example.edu'
institution: { name: 'MIT', licence: 'pro_plus', confirmed: false }
}]
async.series [
(cb) =>
@user.setV1Id v1Id, cb
(cb) =>
@user.addEmail 'unconfirmed-affiliation-email@harvard.example.edu', cb
(cb) =>
@user.addEmail 'confirmed-affiliation-email@stanford.example.edu', cb
(cb) =>
@user.confirmEmail 'confirmed-affiliation-email@stanford.example.edu', cb
(cb) =>
@user.addEmail 'confirmed-affiliation-email@mit.example.edu', cb
(cb) =>
@user.confirmEmail 'confirmed-affiliation-email@mit.example.edu', cb
], (error) =>
return done(error) if error?
SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel @user, (error, @data) =>
return done(error) if error?
done()
it 'should return only the affilations with confirmed institutions, and confirmed emails', ->
expect(@data.confirmedMemberInstitutions).to.deep.equal [
{ name: 'Stanford', licence: 'pro_plus', confirmed: true }
]
describe 'when the user has a v1 subscription', ->
before (done) ->
MockV1Api.setUser v1Id = MockV1Api.nextV1Id(), {
@ -184,8 +291,8 @@ describe 'Subscriptions', ->
it 'should return no personalSubscription', ->
expect(@data.personalSubscription).to.equal null
it 'should return no groupSubscriptions', ->
expect(@data.groupSubscriptions).to.deep.equal []
it 'should return no memberGroupSubscriptions', ->
expect(@data.memberGroupSubscriptions).to.deep.equal []
it 'should return a v1Subscriptions', ->
expect(@data.v1Subscriptions).to.deep.equal @subscription

View file

@ -75,7 +75,10 @@ module.exports = MockV1Api =
res.json []
app.get '/universities/list/:id', (req, res, next) ->
res.json id: parseInt(req.params.id)
res.json {
id: parseInt(req.params.id)
name: "Institution #{req.params.id}"
}
app.get '/university/domains', (req, res, next) ->
res.json []

View file

@ -9,6 +9,8 @@ describe 'InstitutionsGetter', ->
@UserGetter = getUserFullEmails: sinon.stub()
@InstitutionsGetter = SandboxedModule.require modulePath, requires:
'../User/UserGetter': @UserGetter
"../UserMembership/UserMembershipHandler": @UserMembershipHandler = {}
"../UserMembership/UserMembershipEntityConfigs": @UserMembershipEntityConfigs = {}
'logger-sharelatex':
log:-> console.log(arguments)
err:->

View file

@ -230,7 +230,7 @@ describe "SubscriptionController", ->
beforeEach (done) ->
@SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel.callsArgWith(1, null, {
personalSubscription: @personalSubscription = { 'personal-subscription': 'mock' }
groupSubscriptions: @groupSubscriptions = { 'personal-subscriptions': 'mock' }
memberGroupSubscriptions: @memberGroupSubscriptions = { 'group-subscriptions': 'mock' }
v1Subscriptions: @v1Subscriptions = { 'v1-subscriptions': 'mock' }
})
@SubscriptionViewModelBuilder.buildViewModel.returns(@plans = {'plans': 'mock'})
@ -241,7 +241,7 @@ describe "SubscriptionController", ->
it "should load the personal, groups and v1 subscriptions", ->
expect(@data.personalSubscription).to.deep.equal @personalSubscription
expect(@data.groupSubscriptions).to.deep.equal @groupSubscriptions
expect(@data.memberGroupSubscriptions).to.deep.equal @memberGroupSubscriptions
expect(@data.v1Subscriptions).to.deep.equal @v1Subscriptions
it "should load the user", ->