mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-02 03:47:09 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
52d08d7d05
14 changed files with 190 additions and 10 deletions
|
@ -282,6 +282,7 @@
|
|||
education_description: "Description"
|
||||
education_description_help: "Highlight anything about this educational experience. (140 chars; optional)"
|
||||
our_notes: "Our Notes"
|
||||
remarks: "Remarks"
|
||||
projects: "Projects"
|
||||
projects_header: "Add 3 projects"
|
||||
projects_header_2: "Projects (Top 3)"
|
||||
|
@ -320,6 +321,7 @@
|
|||
candidate_top_skills: "Top Skills"
|
||||
candidate_years_experience: "Yrs Exp"
|
||||
candidate_last_updated: "Last Updated"
|
||||
candidate_who: "Who"
|
||||
featured_developers: "Featured Developers"
|
||||
other_developers: "Other Developers"
|
||||
inactive_developers: "Inactive Developers"
|
||||
|
@ -882,6 +884,7 @@
|
|||
document: "Document"
|
||||
sprite_sheet: "Sprite Sheet"
|
||||
candidate_sessions: "Candidate Sessions"
|
||||
user_remark: "User Remark"
|
||||
|
||||
delta:
|
||||
added: "Added"
|
||||
|
|
6
app/models/UserRemark.coffee
Normal file
6
app/models/UserRemark.coffee
Normal 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"
|
24
app/schemas/models/user_remark.coffee
Normal file
24
app/schemas/models/user_remark.coffee
Normal 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
|
|
@ -193,6 +193,11 @@
|
|||
width: 100%
|
||||
height: 100px
|
||||
|
||||
#remark-treema
|
||||
background-color: white
|
||||
border: 0
|
||||
padding-top: 0
|
||||
|
||||
.right-column
|
||||
width: $side-width
|
||||
background-color: $sideBackground
|
||||
|
|
|
@ -169,6 +169,10 @@ block content
|
|||
button#contact-candidate.btn.btn-large.btn-inverse.flat-button
|
||||
span(data-i18n="account_profile.contact") Contact
|
||||
| #{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
|
||||
h3(data-i18n="account_profile.player_code") Player Code
|
||||
|
@ -191,9 +195,12 @@ block content
|
|||
if editing && !profile.name
|
||||
h3.edit-label(data-i18n="account_profile.name_header") Fill in your name
|
||||
else if profile.name
|
||||
h3= profile.name
|
||||
h3= profile.name + (me.isAdmin() ? ' (' + user.get('name') + ')' : '')
|
||||
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
|
||||
.editable-icon.glyphicon.glyphicon-remove
|
||||
|
@ -396,6 +403,10 @@ block content
|
|||
else
|
||||
div!= marked(notes)
|
||||
|
||||
if me.isAdmin()
|
||||
h3(data-i18n="account_profile.remarks") Remarks
|
||||
#remark-treema
|
||||
|
||||
.right-column.full-height-column
|
||||
.sub-column
|
||||
#projects-container.editable-section
|
||||
|
|
|
@ -84,6 +84,8 @@ block content
|
|||
th(data-i18n="employers.candidate_top_skills") Top Skills
|
||||
th(data-i18n="employers.candidate_years_experience") Yrs Exp
|
||||
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'
|
||||
th ✓?
|
||||
|
||||
|
@ -95,7 +97,10 @@ block content
|
|||
td
|
||||
if authorized
|
||||
img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, height=50)
|
||||
p= profile.name
|
||||
if profile.name
|
||||
p= profile.name
|
||||
else if me.isAdmin()
|
||||
p (#{candidate.get('name')})
|
||||
else
|
||||
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)}
|
||||
|
@ -111,6 +116,8 @@ block content
|
|||
span
|
||||
td= profile.experience
|
||||
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 candidate.get('jobProfileApproved')
|
||||
td ✓
|
||||
|
|
|
@ -6,6 +6,8 @@ locale = require 'locale/locale'
|
|||
class DateTimeTreema extends TreemaNode.nodeMap.string
|
||||
valueClass: 'treema-date-time'
|
||||
buildValueForDisplay: (el) -> el.text(moment(@data).format('llll'))
|
||||
buildValueForEditing: (valEl) ->
|
||||
@buildValueForEditingSimply valEl, null, 'date'
|
||||
|
||||
class VersionTreema extends TreemaNode
|
||||
valueClass: 'treema-version'
|
||||
|
|
|
@ -19,10 +19,10 @@ module.exports = class JobProfileView extends CocoView
|
|||
|
||||
buildJobProfileTreema: ->
|
||||
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')
|
||||
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
|
||||
for prop in @readOnlySettings
|
||||
schema.properties[prop].readOnly = true
|
||||
|
|
|
@ -6,6 +6,7 @@ CocoCollection = require 'collections/CocoCollection'
|
|||
{me} = require 'lib/auth'
|
||||
JobProfileContactView = require 'views/modal/job_profile_contact_modal'
|
||||
JobProfileView = require 'views/account/job_profile_view'
|
||||
UserRemark = require 'models/UserRemark'
|
||||
forms = require 'lib/forms'
|
||||
|
||||
class LevelSessionsCollection extends CocoCollection
|
||||
|
@ -14,6 +15,14 @@ class LevelSessionsCollection extends CocoCollection
|
|||
constructor: (@userID) ->
|
||||
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
|
||||
id: "profile-view"
|
||||
template: template
|
||||
|
@ -36,12 +45,14 @@ module.exports = class ProfileView extends View
|
|||
'change .editable-profile .editable-array input': 'onEditArray'
|
||||
'keyup .editable-profile .editable-array input': 'onEditArray'
|
||||
'click .editable-profile a': 'onClickLinkWhileEditing'
|
||||
'change #admin-contact': 'onAdminContactChanged'
|
||||
|
||||
constructor: (options, @userID) ->
|
||||
@userID ?= me.id
|
||||
@onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000
|
||||
@onRemarkChanged = _.debounce @onRemarkChanged, 1000
|
||||
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
|
||||
@linkedInLoaded = Boolean(IN.parse)
|
||||
@linkedInLoaded = Boolean(IN?.parse)
|
||||
@waitingForLinkedIn = false
|
||||
window.contractCallback = =>
|
||||
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
|
||||
|
@ -70,6 +81,22 @@ module.exports = class ProfileView extends View
|
|||
else
|
||||
@user = User.getByID(@userID)
|
||||
@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: =>
|
||||
@linkedinLoaded = true
|
||||
|
@ -229,6 +256,8 @@ module.exports = class ProfileView extends View
|
|||
context.sessions.sort (a, b) -> (b.playtime ? 0) - (a.playtime ? 0)
|
||||
else
|
||||
context.sessions = []
|
||||
context.adminContacts = adminContacts
|
||||
context.remark = @remark
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
@ -249,6 +278,31 @@ module.exports = class ProfileView extends View
|
|||
_.delay ->
|
||||
justSavedSection.removeClass "just-saved", duration: 1500, easing: 'easeOutQuad'
|
||||
, 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) ->
|
||||
(container ? @$el).find('input[data-autocomplete]').each ->
|
||||
|
@ -455,6 +509,26 @@ module.exports = class ProfileView extends View
|
|||
onClickLinkWhileEditing: (e) ->
|
||||
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) ->
|
||||
return unless @user
|
||||
completed = 0
|
||||
|
|
|
@ -2,6 +2,7 @@ View = require 'views/kinds/RootView'
|
|||
template = require 'templates/employers'
|
||||
app = require 'application'
|
||||
User = require 'models/User'
|
||||
UserRemark = require 'models/UserRemark'
|
||||
{me} = require 'lib/auth'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
EmployerSignupView = require 'views/modal/employer_signup_modal'
|
||||
|
@ -10,6 +11,10 @@ class CandidatesCollection extends CocoCollection
|
|||
url: '/db/user/x/candidates'
|
||||
model: User
|
||||
|
||||
class UserRemarksCollection extends CocoCollection
|
||||
url: '/db/user.remark?project=contact,contactName,user'
|
||||
model: UserRemark
|
||||
|
||||
module.exports = class EmployersView extends View
|
||||
id: "employers-view"
|
||||
template: template
|
||||
|
@ -37,6 +42,8 @@ module.exports = class EmployersView extends View
|
|||
ctx.inactiveCandidates = _.reject ctx.candidates, (c) -> c.get('jobProfile').active
|
||||
ctx.featuredCandidates = _.filter 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._ = _
|
||||
ctx
|
||||
|
@ -48,11 +55,13 @@ module.exports = class EmployersView extends View
|
|||
getCandidates: ->
|
||||
@candidates = new CandidatesCollection()
|
||||
@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.
|
||||
@listenToOnce @candidates, 'all', @renderCandidatesAndSetupScrolling
|
||||
@listenToOnce @remarks, 'all', @renderCandidatesAndSetupScrolling
|
||||
|
||||
renderCandidatesAndSetupScrolling: =>
|
||||
|
||||
@render()
|
||||
$(".nano").nanoScroller()
|
||||
if window.history?.state?.lastViewedCandidateID
|
||||
|
@ -179,7 +188,7 @@ module.exports = class EmployersView extends View
|
|||
"Last 4 weeks": (e, n, f, i, $r) ->
|
||||
days = parseFloat $($r.find('td')[i]).data('profile-age')
|
||||
days <= 28
|
||||
7:
|
||||
8:
|
||||
"✓": filterSelectExactMatch
|
||||
"✗": filterSelectExactMatch
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ module.exports.handlers =
|
|||
'patch': 'patches/patch_handler'
|
||||
'thang_type': 'levels/thangs/thang_type_handler'
|
||||
'user': 'users/user_handler'
|
||||
'user_remark': 'users/remarks/user_remark_handler'
|
||||
'achievement': 'achievements/achievement_handler'
|
||||
'earned_achievement': 'achievements/earned_achievement_handler'
|
||||
|
||||
|
|
11
server/users/remarks/UserRemark.coffee
Normal file
11
server/users/remarks/UserRemark.coffee
Normal 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)
|
12
server/users/remarks/user_remark_handler.coffee
Normal file
12
server/users/remarks/user_remark_handler.coffee
Normal 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()
|
|
@ -11,6 +11,7 @@ log = require 'winston'
|
|||
LevelSession = require('../levels/sessions/LevelSession')
|
||||
LevelSessionHandler = require '../levels/sessions/level_session_handler'
|
||||
EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||
UserRemark = require './remarks/UserRemark'
|
||||
|
||||
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset']
|
||||
privateProperties = [
|
||||
|
@ -197,6 +198,7 @@ UserHandler = class UserHandler extends Handler
|
|||
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 @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)
|
||||
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['jobProfile.active'] = true unless req.user.isAdmin()
|
||||
selection = 'jobProfile jobProfileApproved photoURL'
|
||||
selection += ' email' if authorized
|
||||
selection += ' email name' if authorized
|
||||
User.find(query).select(selection).exec (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
candidates = (candidate for candidate in documents when @employerCanViewCandidate req.user, candidate.toObject())
|
||||
|
@ -321,7 +323,7 @@ UserHandler = class UserHandler extends Handler
|
|||
@sendSuccess(res, candidates)
|
||||
|
||||
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.photoURL ||= obj.jobProfile.photoURL if authorized
|
||||
subfields = ['country', 'city', 'lookingFor', 'jobTitle', 'skills', 'experience', 'updated', 'active']
|
||||
|
@ -363,4 +365,17 @@ UserHandler = class UserHandler extends Handler
|
|||
hash.update(user.get('_id') + '')
|
||||
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()
|
||||
|
|
Loading…
Reference in a new issue