Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-06-17 13:04:38 -07:00
commit 52d08d7d05
14 changed files with 190 additions and 10 deletions

View file

@ -282,6 +282,7 @@
education_description: "Description" education_description: "Description"
education_description_help: "Highlight anything about this educational experience. (140 chars; optional)" education_description_help: "Highlight anything about this educational experience. (140 chars; optional)"
our_notes: "Our Notes" our_notes: "Our Notes"
remarks: "Remarks"
projects: "Projects" projects: "Projects"
projects_header: "Add 3 projects" projects_header: "Add 3 projects"
projects_header_2: "Projects (Top 3)" projects_header_2: "Projects (Top 3)"
@ -320,6 +321,7 @@
candidate_top_skills: "Top Skills" candidate_top_skills: "Top Skills"
candidate_years_experience: "Yrs Exp" candidate_years_experience: "Yrs Exp"
candidate_last_updated: "Last Updated" candidate_last_updated: "Last Updated"
candidate_who: "Who"
featured_developers: "Featured Developers" featured_developers: "Featured Developers"
other_developers: "Other Developers" other_developers: "Other Developers"
inactive_developers: "Inactive Developers" inactive_developers: "Inactive Developers"
@ -882,6 +884,7 @@
document: "Document" document: "Document"
sprite_sheet: "Sprite Sheet" sprite_sheet: "Sprite Sheet"
candidate_sessions: "Candidate Sessions" candidate_sessions: "Candidate Sessions"
user_remark: "User Remark"
delta: delta:
added: "Added" added: "Added"

View file

@ -0,0 +1,6 @@
CocoModel = require('./CocoModel')
module.exports = class UserRemark extends CocoModel
@className: "UserRemark"
@schema: require 'schemas/models/user_remark'
urlRoot: "/db/user.remark"

View file

@ -0,0 +1,24 @@
c = require './../schemas'
UserRemarkSchema = c.object {
title: "Remark"
description: "Remarks on a user, point of contact, tasks."
}
_.extend UserRemarkSchema.properties,
user: c.objectId links: [{rel: 'extra', href: "/db/user/{($)}"}]
contact: c.objectId links: [{rel: 'extra', href: "/db/user/{($)}"}]
created: c.date title: 'Created', readOnly: true
history: c.array {title: 'History', description: 'Records of our interactions with the user.'},
c.object {title: 'Record'}, {date: c.date(title: 'Date'), content: {title: 'Content', type: 'string', format: 'markdown'}}
tasks: c.array {title: 'Tasks', description: 'Task entries: when to email the contact about something.'},
c.object {title: 'Task'}, {date: c.date(title: 'Date'), action: {title: 'Action', type: 'string'}}
# denormalization
userName: { title: "Player Name", type: 'string' }
contactName: { title: "Contact Name", type: 'string' } # Not actually our usernames
c.extendBasicProperties UserRemarkSchema, 'user.remark'
module.exports = UserRemarkSchema

View file

@ -193,6 +193,11 @@
width: 100% width: 100%
height: 100px height: 100px
#remark-treema
background-color: white
border: 0
padding-top: 0
.right-column .right-column
width: $side-width width: $side-width
background-color: $sideBackground background-color: $sideBackground

View file

@ -169,6 +169,10 @@ block content
button#contact-candidate.btn.btn-large.btn-inverse.flat-button button#contact-candidate.btn.btn-large.btn-inverse.flat-button
span(data-i18n="account_profile.contact") Contact span(data-i18n="account_profile.contact") Contact
| #{profile.name.split(' ')[0]} | #{profile.name.split(' ')[0]}
if me.isAdmin()
select#admin-contact.form-control
for contact in adminContacts
option(value=contact.id, selected=remark && remark.get('contact') == contact.id)= contact.name
if !editing && sessions.length if !editing && sessions.length
h3(data-i18n="account_profile.player_code") Player Code h3(data-i18n="account_profile.player_code") Player Code
@ -191,9 +195,12 @@ block content
if editing && !profile.name if editing && !profile.name
h3.edit-label(data-i18n="account_profile.name_header") Fill in your name h3.edit-label(data-i18n="account_profile.name_header") Fill in your name
else if profile.name else if profile.name
h3= profile.name h3= profile.name + (me.isAdmin() ? ' (' + user.get('name') + ')' : '')
else else
h3(data-i18n="account_profile.name_anonymous") Anonymous Developer h3
span(data-i18n="account_profile.name_anonymous") Anonymous Developer
if me.isAdmin()
span (#{user.get('name')})
form.editable-form form.editable-form
.editable-icon.glyphicon.glyphicon-remove .editable-icon.glyphicon.glyphicon-remove
@ -396,6 +403,10 @@ block content
else else
div!= marked(notes) div!= marked(notes)
if me.isAdmin()
h3(data-i18n="account_profile.remarks") Remarks
#remark-treema
.right-column.full-height-column .right-column.full-height-column
.sub-column .sub-column
#projects-container.editable-section #projects-container.editable-section

View file

@ -84,6 +84,8 @@ block content
th(data-i18n="employers.candidate_top_skills") Top Skills th(data-i18n="employers.candidate_top_skills") Top Skills
th(data-i18n="employers.candidate_years_experience") Yrs Exp th(data-i18n="employers.candidate_years_experience") Yrs Exp
th(data-i18n="employers.candidate_last_updated") Last Updated th(data-i18n="employers.candidate_last_updated") Last Updated
if me.isAdmin()
th(data-i18n="employers.candidate_who") Who
if me.isAdmin() && area.id == 'inactive-candidates' if me.isAdmin() && area.id == 'inactive-candidates'
th ✓? th ✓?
@ -95,7 +97,10 @@ block content
td td
if authorized if authorized
img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, height=50) img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, height=50)
if profile.name
p= profile.name p= profile.name
else if me.isAdmin()
p (#{candidate.get('name')})
else else
img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50) img(src="/images/pages/contribute/archmage.png", alt="", title="Sign up as an employer to see our candidates", width=50)
p Developer ##{index + 1 + (area.id == 'featured-candidates' ? 0 : featuredCandidates.length)} p Developer ##{index + 1 + (area.id == 'featured-candidates' ? 0 : featuredCandidates.length)}
@ -111,6 +116,8 @@ block content
span span
td= profile.experience td= profile.experience
td(data-profile-age=(new Date() - new Date(profile.updated)) / 86400 / 1000)= moment(profile.updated).fromNow() td(data-profile-age=(new Date() - new Date(profile.updated)) / 86400 / 1000)= moment(profile.updated).fromNow()
if me.isAdmin()
td= remarks[candidate.id] ? remarks[candidate.id].get('contactName') : ''
if me.isAdmin() && area.id == 'inactive-candidates' if me.isAdmin() && area.id == 'inactive-candidates'
if candidate.get('jobProfileApproved') if candidate.get('jobProfileApproved')
td ✓ td ✓

View file

@ -6,6 +6,8 @@ locale = require 'locale/locale'
class DateTimeTreema extends TreemaNode.nodeMap.string class DateTimeTreema extends TreemaNode.nodeMap.string
valueClass: 'treema-date-time' valueClass: 'treema-date-time'
buildValueForDisplay: (el) -> el.text(moment(@data).format('llll')) buildValueForDisplay: (el) -> el.text(moment(@data).format('llll'))
buildValueForEditing: (valEl) ->
@buildValueForEditingSimply valEl, null, 'date'
class VersionTreema extends TreemaNode class VersionTreema extends TreemaNode
valueClass: 'treema-version' valueClass: 'treema-version'

View file

@ -19,10 +19,10 @@ module.exports = class JobProfileView extends CocoView
buildJobProfileTreema: -> buildJobProfileTreema: ->
visibleSettings = @editableSettings.concat @readOnlySettings visibleSettings = @editableSettings.concat @readOnlySettings
data = _.pick (me.get('jobProfile') ? {}), (value, key) => key in visibleSettings data = _.pick (me.get('jobProfile') ? {}), (value, key) -> key in visibleSettings
data.name ?= (me.get('firstName') + ' ' + me.get('lastName')).trim() if me.get('firstName') data.name ?= (me.get('firstName') + ' ' + me.get('lastName')).trim() if me.get('firstName')
schema = _.cloneDeep me.schema().properties.jobProfile schema = _.cloneDeep me.schema().properties.jobProfile
schema.properties = _.pick schema.properties, (value, key) => key in visibleSettings schema.properties = _.pick schema.properties, (value, key) -> key in visibleSettings
schema.required = _.intersection schema.required, visibleSettings schema.required = _.intersection schema.required, visibleSettings
for prop in @readOnlySettings for prop in @readOnlySettings
schema.properties[prop].readOnly = true schema.properties[prop].readOnly = true

View file

@ -6,6 +6,7 @@ CocoCollection = require 'collections/CocoCollection'
{me} = require 'lib/auth' {me} = require 'lib/auth'
JobProfileContactView = require 'views/modal/job_profile_contact_modal' JobProfileContactView = require 'views/modal/job_profile_contact_modal'
JobProfileView = require 'views/account/job_profile_view' JobProfileView = require 'views/account/job_profile_view'
UserRemark = require 'models/UserRemark'
forms = require 'lib/forms' forms = require 'lib/forms'
class LevelSessionsCollection extends CocoCollection class LevelSessionsCollection extends CocoCollection
@ -14,6 +15,14 @@ class LevelSessionsCollection extends CocoCollection
constructor: (@userID) -> constructor: (@userID) ->
super() super()
adminContacts = [
{id: "", name: "Assign a Contact"}
{id: "512ef4805a67a8c507000001", name: "Nick"}
{id: "5162fab9c92b4c751e000274", name: "Scott"}
{id: "51eb2714fa058cb20d0006ef", name: "Michael"}
{id: "51538fdb812dd9af02000001", name: "George"}
]
module.exports = class ProfileView extends View module.exports = class ProfileView extends View
id: "profile-view" id: "profile-view"
template: template template: template
@ -36,12 +45,14 @@ module.exports = class ProfileView extends View
'change .editable-profile .editable-array input': 'onEditArray' 'change .editable-profile .editable-array input': 'onEditArray'
'keyup .editable-profile .editable-array input': 'onEditArray' 'keyup .editable-profile .editable-array input': 'onEditArray'
'click .editable-profile a': 'onClickLinkWhileEditing' 'click .editable-profile a': 'onClickLinkWhileEditing'
'change #admin-contact': 'onAdminContactChanged'
constructor: (options, @userID) -> constructor: (options, @userID) ->
@userID ?= me.id @userID ?= me.id
@onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000 @onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000
@onRemarkChanged = _.debounce @onRemarkChanged, 1000
@authorizedWithLinkedIn = IN?.User?.isAuthorized() @authorizedWithLinkedIn = IN?.User?.isAuthorized()
@linkedInLoaded = Boolean(IN.parse) @linkedInLoaded = Boolean(IN?.parse)
@waitingForLinkedIn = false @waitingForLinkedIn = false
window.contractCallback = => window.contractCallback = =>
@authorizedWithLinkedIn = IN?.User?.isAuthorized() @authorizedWithLinkedIn = IN?.User?.isAuthorized()
@ -70,6 +81,22 @@ module.exports = class ProfileView extends View
else else
@user = User.getByID(@userID) @user = User.getByID(@userID)
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@userID), 'candidate_sessions').model @sessions = @supermodel.loadCollection(new LevelSessionsCollection(@userID), 'candidate_sessions').model
if me.isAdmin()
# Mimicking how the VictoryModal fetches LevelFeedback
@remark = new UserRemark()
@remark.setURL "/db/user/#{@userID}/remark"
@remark.fetch()
@listenToOnce @remark, 'sync', @onRemarkLoaded
@listenToOnce @remark, 'error', @onRemarkNotFound
onRemarkLoaded: ->
@remark.setURL "/db/user.remark/#{@remark.id}"
@render()
onRemarkNotFound: ->
@remark = new UserRemark() # hmm, why do we create a new one here?
@remark.set 'user', @userID
@remark.set 'userName', name if name = @user.get('name')
onLinkedInLoaded: => onLinkedInLoaded: =>
@linkedinLoaded = true @linkedinLoaded = true
@ -229,6 +256,8 @@ module.exports = class ProfileView extends View
context.sessions.sort (a, b) -> (b.playtime ? 0) - (a.playtime ? 0) context.sessions.sort (a, b) -> (b.playtime ? 0) - (a.playtime ? 0)
else else
context.sessions = [] context.sessions = []
context.adminContacts = adminContacts
context.remark = @remark
context context
afterRender: -> afterRender: ->
@ -249,6 +278,31 @@ module.exports = class ProfileView extends View
_.delay -> _.delay ->
justSavedSection.removeClass "just-saved", duration: 1500, easing: 'easeOutQuad' justSavedSection.removeClass "just-saved", duration: 1500, easing: 'easeOutQuad'
, 500 , 500
if me.isAdmin()
visibleSettings = ['history', 'tasks']
data = _.pick (@remark.attributes), (value, key) -> key in visibleSettings
data.history ?= []
data.tasks ?= []
schema = _.cloneDeep @remark.schema()
schema.properties = _.pick schema.properties, (value, key) => key in visibleSettings
schema.required = _.intersection (schema.required ? []), visibleSettings
treemaOptions =
filePath: "db/user/#{@userID}"
schema: schema
data: data
aceUseWrapMode: true
callbacks: {change: @onRemarkChanged}
@remarkTreema = @$el.find('#remark-treema').treema treemaOptions
@remarkTreema.build()
@remarkTreema.open(3)
onRemarkChanged: (e) =>
return unless @remarkTreema.isValid()
for key in ['history', 'tasks']
val = _.filter(@remarkTreema.get(key), (entry) -> entry?.content or entry?.action)
entry.date ?= (new Date()).toISOString() for entry in val if key is 'history'
@remark.set key, val
@saveRemark()
initializeAutocomplete: (container) -> initializeAutocomplete: (container) ->
(container ? @$el).find('input[data-autocomplete]').each -> (container ? @$el).find('input[data-autocomplete]').each ->
@ -455,6 +509,26 @@ module.exports = class ProfileView extends View
onClickLinkWhileEditing: (e) -> onClickLinkWhileEditing: (e) ->
e.preventDefault() e.preventDefault()
onAdminContactChanged: (e) ->
newContact = @$el.find('#admin-contact').val()
newContactName = if newContact then _.find(adminContacts, id: newContact).name else ''
@remark.set 'contact', newContact
@remark.set 'contactName', newContactName
@saveRemark()
saveRemark: ->
@remark.set 'user', @user.id
@remark.set 'userName', @user.get('name')
if errors = @remark.validate()
return console.error "UserRemark", @remark, "failed validation with errors:", errors
res = @remark.save()
res.error =>
return if @destroyed
console.error "UserRemark", @remark, "failed to save with error:", res.responseText
res.success (model, response, options) =>
return if @destroyed
console.log "Saved UserRemark", @remark, "with response", response
updateProgress: (highlightNext) -> updateProgress: (highlightNext) ->
return unless @user return unless @user
completed = 0 completed = 0

View file

@ -2,6 +2,7 @@ View = require 'views/kinds/RootView'
template = require 'templates/employers' template = require 'templates/employers'
app = require 'application' app = require 'application'
User = require 'models/User' User = require 'models/User'
UserRemark = require 'models/UserRemark'
{me} = require 'lib/auth' {me} = require 'lib/auth'
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
EmployerSignupView = require 'views/modal/employer_signup_modal' EmployerSignupView = require 'views/modal/employer_signup_modal'
@ -10,6 +11,10 @@ class CandidatesCollection extends CocoCollection
url: '/db/user/x/candidates' url: '/db/user/x/candidates'
model: User model: User
class UserRemarksCollection extends CocoCollection
url: '/db/user.remark?project=contact,contactName,user'
model: UserRemark
module.exports = class EmployersView extends View module.exports = class EmployersView extends View
id: "employers-view" id: "employers-view"
template: template template: template
@ -37,6 +42,8 @@ module.exports = class EmployersView extends View
ctx.inactiveCandidates = _.reject ctx.candidates, (c) -> c.get('jobProfile').active ctx.inactiveCandidates = _.reject ctx.candidates, (c) -> c.get('jobProfile').active
ctx.featuredCandidates = _.filter ctx.activeCandidates, (c) -> c.get('jobProfileApproved') ctx.featuredCandidates = _.filter ctx.activeCandidates, (c) -> c.get('jobProfileApproved')
ctx.otherCandidates = _.reject ctx.activeCandidates, (c) -> c.get('jobProfileApproved') ctx.otherCandidates = _.reject ctx.activeCandidates, (c) -> c.get('jobProfileApproved')
ctx.remarks = {}
ctx.remarks[remark.get('user')] = remark for remark in @remarks.models
ctx.moment = moment ctx.moment = moment
ctx._ = _ ctx._ = _
ctx ctx
@ -48,11 +55,13 @@ module.exports = class EmployersView extends View
getCandidates: -> getCandidates: ->
@candidates = new CandidatesCollection() @candidates = new CandidatesCollection()
@candidates.fetch() @candidates.fetch()
@remarks = new UserRemarksCollection()
@remarks.fetch()
# Re-render when we have fetched them, but don't wait and show a progress bar while loading. # Re-render when we have fetched them, but don't wait and show a progress bar while loading.
@listenToOnce @candidates, 'all', @renderCandidatesAndSetupScrolling @listenToOnce @candidates, 'all', @renderCandidatesAndSetupScrolling
@listenToOnce @remarks, 'all', @renderCandidatesAndSetupScrolling
renderCandidatesAndSetupScrolling: => renderCandidatesAndSetupScrolling: =>
@render() @render()
$(".nano").nanoScroller() $(".nano").nanoScroller()
if window.history?.state?.lastViewedCandidateID if window.history?.state?.lastViewedCandidateID
@ -179,7 +188,7 @@ module.exports = class EmployersView extends View
"Last 4 weeks": (e, n, f, i, $r) -> "Last 4 weeks": (e, n, f, i, $r) ->
days = parseFloat $($r.find('td')[i]).data('profile-age') days = parseFloat $($r.find('td')[i]).data('profile-age')
days <= 28 days <= 28
7: 8:
"": filterSelectExactMatch "": filterSelectExactMatch
"": filterSelectExactMatch "": filterSelectExactMatch

View file

@ -8,6 +8,7 @@ module.exports.handlers =
'patch': 'patches/patch_handler' 'patch': 'patches/patch_handler'
'thang_type': 'levels/thangs/thang_type_handler' 'thang_type': 'levels/thangs/thang_type_handler'
'user': 'users/user_handler' 'user': 'users/user_handler'
'user_remark': 'users/remarks/user_remark_handler'
'achievement': 'achievements/achievement_handler' 'achievement': 'achievements/achievement_handler'
'earned_achievement': 'achievements/earned_achievement_handler' 'earned_achievement': 'achievements/earned_achievement_handler'

View file

@ -0,0 +1,11 @@
mongoose = require('mongoose')
plugins = require('../../plugins/plugins')
jsonschema = require('../../../app/schemas/models/user_remark')
UserRemarkSchema = new mongoose.Schema({
created:
type: Date
'default': Date.now
}, {strict: false})
module.exports = UserRemark = mongoose.model('user.remark', UserRemarkSchema)

View file

@ -0,0 +1,12 @@
UserRemark = require('./UserRemark')
Handler = require('../../commons/Handler')
class UserRemarkHandler extends Handler
modelClass: UserRemark
editableProperties: ['user', 'contact', 'history', 'tasks', 'userName', 'contactName']
jsonSchema: require '../../../app/schemas/models/user_remark'
hasAccess: (req) ->
req.user?.isAdmin()
module.exports = new UserRemarkHandler()

View file

@ -11,6 +11,7 @@ log = require 'winston'
LevelSession = require('../levels/sessions/LevelSession') LevelSession = require('../levels/sessions/LevelSession')
LevelSessionHandler = require '../levels/sessions/level_session_handler' LevelSessionHandler = require '../levels/sessions/level_session_handler'
EarnedAchievement = require '../achievements/EarnedAchievement' EarnedAchievement = require '../achievements/EarnedAchievement'
UserRemark = require './remarks/UserRemark'
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset'] serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset']
privateProperties = [ privateProperties = [
@ -197,6 +198,7 @@ UserHandler = class UserHandler extends Handler
return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank' return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank'
return @getEarnedAchievements(req, res, args[0]) if args[1] is 'achievements' return @getEarnedAchievements(req, res, args[0]) if args[1] is 'achievements'
return @trackActivity(req, res, args[0], args[2], args[3]) if args[1] is 'track' and args[2] return @trackActivity(req, res, args[0], args[2], args[3]) if args[1] is 'track' and args[2]
return @getRemark(req, res, args[0]) if args[1] is 'remark'
return @sendNotFoundError(res) return @sendNotFoundError(res)
super(arguments...) super(arguments...)
@ -313,7 +315,7 @@ UserHandler = class UserHandler extends Handler
#query.jobProfileApproved = true unless req.user.isAdmin() # We split into featured and other now. #query.jobProfileApproved = true unless req.user.isAdmin() # We split into featured and other now.
query['jobProfile.active'] = true unless req.user.isAdmin() query['jobProfile.active'] = true unless req.user.isAdmin()
selection = 'jobProfile jobProfileApproved photoURL' selection = 'jobProfile jobProfileApproved photoURL'
selection += ' email' if authorized selection += ' email name' if authorized
User.find(query).select(selection).exec (err, documents) => User.find(query).select(selection).exec (err, documents) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
candidates = (candidate for candidate in documents when @employerCanViewCandidate req.user, candidate.toObject()) candidates = (candidate for candidate in documents when @employerCanViewCandidate req.user, candidate.toObject())
@ -321,7 +323,7 @@ UserHandler = class UserHandler extends Handler
@sendSuccess(res, candidates) @sendSuccess(res, candidates)
formatCandidate: (authorized, document) -> formatCandidate: (authorized, document) ->
fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile', 'jobProfileApproved'] fields = if authorized then ['name', 'jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile', 'jobProfileApproved']
obj = _.pick document.toObject(), fields obj = _.pick document.toObject(), fields
obj.photoURL ||= obj.jobProfile.photoURL if authorized obj.photoURL ||= obj.jobProfile.photoURL if authorized
subfields = ['country', 'city', 'lookingFor', 'jobTitle', 'skills', 'experience', 'updated', 'active'] subfields = ['country', 'city', 'lookingFor', 'jobTitle', 'skills', 'experience', 'updated', 'active']
@ -363,4 +365,17 @@ UserHandler = class UserHandler extends Handler
hash.update(user.get('_id') + '') hash.update(user.get('_id') + '')
hash.digest('hex') hash.digest('hex')
getRemark: (req, res, userID) ->
return @sendUnauthorizedError(res) unless req.user.isAdmin()
query = user: userID
projection = null
if req.query.project
projection = {}
projection[field] = 1 for field in req.query.project.split(',')
UserRemark.findOne(query).select(projection).exec (err, remark) =>
return @sendDatabaseError res, err if err
return @sendNotFoundError res unless remark?
@sendSuccess res, remark
module.exports = new UserHandler() module.exports = new UserHandler()