mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-31 07:12:49 -04:00
Merge branch 'master' of https://github.com/codecombat/codecombat
This commit is contained in:
commit
8813a7f545
5 changed files with 394 additions and 1 deletions
app
server/routes
74
app/styles/admin/candidates.sass
Normal file
74
app/styles/admin/candidates.sass
Normal file
|
@ -0,0 +1,74 @@
|
|||
#admin-candidates-view
|
||||
|
||||
h1, h2, h3
|
||||
font: Arial
|
||||
|
||||
.see-candidates-header
|
||||
margin: 30px
|
||||
text-align: center
|
||||
|
||||
#see-candidates
|
||||
cursor: pointer
|
||||
|
||||
.employer_icon
|
||||
width: 125px
|
||||
float: left
|
||||
margin: 0px 15px 15px 0px
|
||||
|
||||
.information_row
|
||||
height: 150px
|
||||
padding-right: 15px
|
||||
|
||||
#leftside
|
||||
width: 500px
|
||||
float: left
|
||||
|
||||
#rightside
|
||||
width: 500px
|
||||
float: left
|
||||
|
||||
.tablesorter
|
||||
//img
|
||||
// display: none
|
||||
|
||||
.tablesorter-header
|
||||
cursor: pointer
|
||||
&:hover
|
||||
color: black
|
||||
|
||||
&:first-child
|
||||
// Make sure that "Developer #56" doesn't wrap onto second row
|
||||
min-width: 110px
|
||||
|
||||
.tablesorter-headerAsc
|
||||
background-color: #cfc
|
||||
|
||||
.tablesorter-headerDesc
|
||||
background-color: #ccf
|
||||
|
||||
tr
|
||||
cursor: pointer
|
||||
|
||||
tr.expired
|
||||
opacity: 0.5
|
||||
|
||||
code
|
||||
background-color: rgb(220, 220, 220)
|
||||
color: #555
|
||||
margin: 2px 0
|
||||
display: inline-block
|
||||
text-transform: lowercase
|
||||
|
||||
td:nth-child(3) select
|
||||
min-width: 100px
|
||||
td:nth-child(6) select
|
||||
min-width: 50px
|
||||
td:nth-child(7) select
|
||||
min-width: 100px
|
||||
|
||||
#employers-view, #profile-view.viewed-by-employer
|
||||
#outer-content-wrapper, #intermediate-content-wrapper, #inner-content-wrapper
|
||||
background: #949494
|
||||
|
||||
.main-content-area
|
||||
background-color: #EAEAEA
|
|
@ -25,6 +25,8 @@ block content
|
|||
a(href="/admin/level_sessions", data-i18n="admin.av_entities_active_instances_url") Active Instances
|
||||
li
|
||||
a(href="/admin/employer_list", data-i18n="admin.av_entities_employer_list_url") Employer List
|
||||
li
|
||||
a(href="/admin/candidates") Candidate List
|
||||
|
||||
h4(data-i18n="admin.av_other_sub_title") Other
|
||||
|
||||
|
|
110
app/templates/admin/candidates.jade
Normal file
110
app/templates/admin/candidates.jade
Normal file
|
@ -0,0 +1,110 @@
|
|||
extends /templates/base
|
||||
block content
|
||||
if !me.isAdmin()
|
||||
h1 Admins Only
|
||||
if me.isAdmin()
|
||||
h1(data-i18n="employers.want_to_hire_our_players") Hire CodeCombat Players
|
||||
if !isEmployer && !me.isAdmin()
|
||||
div#info_wrapper
|
||||
div#leftside
|
||||
div.information_row
|
||||
img(class="employer_icon" src="/images/pages/employer/employer_icon1.png")
|
||||
h2(data-i18n="employers.what") What is CodeCombat?
|
||||
p(data-i18n="employers.what_blurb") CodeCombat is a multiplayer browser programming game. Players write code to control their forces in battle against other developers. We support JavaScript, Python, Lua, Clojure, CoffeeScript, and Io.
|
||||
div.information_row
|
||||
img(class="employer_icon" src="/images/pages/employer/employer_icon3.png")
|
||||
h2(data-i18n="employers.who") Who Are the Players?
|
||||
p(data-i18n="employers.who_blurb") CodeCombateers are CTOs, VPs of Engineering, and graduates of top 20 engineering schools. No junior developers here. Our players enjoy playing with code and solving problems.
|
||||
div.information_row
|
||||
img(class="employer_icon" src="/images/pages/employer/employer_icon5.png")
|
||||
h2(data-i18n="employers.cost") Who Are the Players?
|
||||
p(data-i18n="employers.cost_blurb") CodeCombateers are CTOs, VPs of Engineering, and graduates of top 20 engineering schools. No junior developers here. Our players enjoy playing with code and solving problems.
|
||||
div#rightside
|
||||
div.information_row
|
||||
img(class="employer_icon" src="/images/pages/employer/employer_icon2.png")
|
||||
h2(data-i18n="employers.how") How Much Do we Charge?
|
||||
p(data-i18n="employers.how_blurb") We charge 15% of first year's salary and offer a 100% money back guarantee for 90 days. We don't charge for candidates who are already actively being interviewed at your company.
|
||||
div.information_row
|
||||
img(class="employer_icon" src="/images/pages/employer/employer_icon4.png")
|
||||
h2(data-i18n="employers.why") Why Hire Through Us?
|
||||
p
|
||||
span(data-i18n="employers.why_blurb_1") We will save you time. Every CodeCombateer we feaure is
|
||||
strong(data-i18n="employers.why_blurb_2") looking for work
|
||||
span(data-i18n="employers.why_blurb_3") , has
|
||||
strong(data-i18n="employers.why_blurb_4") demonstrated top notch technical skills
|
||||
span(data-i18n="employers.why_blurb_5") , and has been
|
||||
strong(data-i18n="employers.why_blurb_6") personally screened by us
|
||||
span(data-i18n="employers.why_blurb_7") . Stop screening and start hiring.
|
||||
div.information_row
|
||||
img(class="employer_icon" src="/images/pages/employer/employer_icon6.png")
|
||||
h2(data-i18n="employers.response") What's the Response Rate?
|
||||
p(data-i18n="employers.response_blurb") Almost every developer you contact on CodeCombat will respond to inquires whether or not they want to interivew. If you would like help finding a candidate for your role, we can make recommendations.
|
||||
if candidates.length
|
||||
ul.nav.nav-pills
|
||||
li.active
|
||||
a(href="#featured-candidates", data-toggle="tab")
|
||||
span(data-i18n="employers.featured_developers") Featured Developers
|
||||
| (#{featuredCandidates.length})
|
||||
if otherCandidates.length
|
||||
li
|
||||
a(href="#other-candidates", data-toggle="tab")
|
||||
span(data-i18n="employers.other_developers") Other Developers
|
||||
| (#{otherCandidates.length})
|
||||
if me.isAdmin() && inactiveCandidates.length
|
||||
li
|
||||
a(href="#inactive-candidates", data-toggle="tab")
|
||||
span(data-i18n="employers.inactive_developers") Inactive Developers
|
||||
| (#{inactiveCandidates.length})
|
||||
div.tab-content
|
||||
for area, tabIndex in [{id: "featured-candidates", candidates: featuredCandidates}, {id: "other-candidates", candidates: otherCandidates}, {id: "inactive-candidates", candidates: inactiveCandidates}]
|
||||
div(class="tab-pane well" + (tabIndex ? "" : " active"), id=area.id)
|
||||
table.table.table-condensed.table-hover.table-responsive.tablesorter
|
||||
thead
|
||||
tr
|
||||
th(data-i18n="general.name") Name
|
||||
th(data-i18n="employers.candidate_location") Location
|
||||
th(data-i18n="employers.candidate_looking_for") Looking For
|
||||
th(data-i18n="employers.candidate_role") Role
|
||||
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 ✓?
|
||||
tbody
|
||||
for candidate, index in area.candidates
|
||||
- var profile = candidate.get('jobProfile');
|
||||
- var authorized = candidate.id; // If we have the id, then we are authorized.
|
||||
- var profileAge = (new Date() - new Date(profile.updated)) / 86400 / 1000;
|
||||
- var expired = profileAge > 2 * 30.4;
|
||||
tr(data-candidate-id=candidate.id, id=candidate.id, class=expired ? "expired" : "")
|
||||
td
|
||||
if authorized
|
||||
img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, height=50)
|
||||
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)}
|
||||
if profile.country == 'USA'
|
||||
td= profile.city
|
||||
else
|
||||
td= profile.country
|
||||
td= profile.lookingFor
|
||||
td= profile.jobTitle
|
||||
td
|
||||
each skill in profile.skills
|
||||
code= skill
|
||||
span
|
||||
td= profile.experience
|
||||
td(data-profile-age=profileAge)= 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 ✓
|
||||
else
|
||||
td ✗
|
207
app/views/admin/candidates_view.coffee
Normal file
207
app/views/admin/candidates_view.coffee
Normal file
|
@ -0,0 +1,207 @@
|
|||
View = require 'views/kinds/RootView'
|
||||
template = require 'templates/admin/candidates'
|
||||
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'
|
||||
|
||||
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: "admin-candidates-view"
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click tbody tr': 'onCandidateClicked'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@getCandidates()
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@sortTable() if @candidates.models.length
|
||||
|
||||
afterInsert: ->
|
||||
super()
|
||||
_.delay @checkForEmployerSignupHash, 500
|
||||
|
||||
getRenderData: ->
|
||||
ctx = super()
|
||||
ctx.isEmployer = @isEmployer()
|
||||
ctx.candidates = _.sortBy @candidates.models, (c) -> c.get('jobProfile').updated
|
||||
ctx.activeCandidates = _.filter 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.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
|
||||
|
||||
isEmployer: ->
|
||||
userPermissions = me.get('permissions') ? []
|
||||
_.contains userPermissions, "employer"
|
||||
|
||||
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
|
||||
$(".nano").nanoScroller({scrollTo:$("#" + window.history.state.lastViewedCandidateID)})
|
||||
else if window.location.hash.length is 25
|
||||
$(".nano").nanoScroller({scrollTo:$(window.location.hash)})
|
||||
|
||||
checkForEmployerSignupHash: =>
|
||||
if window.location.hash is "#employerSignupLoggingIn" and not ("employer" in me.get("permissions"))
|
||||
@openModalView application.router.getView("modal/employer_signup","_modal")
|
||||
window.location.hash = ""
|
||||
|
||||
sortTable: ->
|
||||
# http://mottie.github.io/tablesorter/docs/example-widget-bootstrap-theme.html
|
||||
$.extend $.tablesorter.themes.bootstrap,
|
||||
# these classes are added to the table. To see other table classes available,
|
||||
# look here: http://twitter.github.com/bootstrap/base-css.html#tables
|
||||
table: "table table-bordered"
|
||||
caption: "caption"
|
||||
header: "bootstrap-header" # give the header a gradient background
|
||||
footerRow: ""
|
||||
footerCells: ""
|
||||
icons: "" # add "icon-white" to make them white; this icon class is added to the <i> in the header
|
||||
sortNone: "bootstrap-icon-unsorted"
|
||||
sortAsc: "icon-chevron-up" # glyphicon glyphicon-chevron-up" # we are still using v2 icons
|
||||
sortDesc: "icon-chevron-down" # glyphicon-chevron-down" # we are still using v2 icons
|
||||
active: "" # applied when column is sorted
|
||||
hover: "" # use custom css here - bootstrap class may not override it
|
||||
filterRow: "" # filter row class
|
||||
even: "" # odd row zebra striping
|
||||
odd: "" # even row zebra striping
|
||||
|
||||
|
||||
# e = exact text from cell
|
||||
# n = normalized value returned by the column parser
|
||||
# f = search filter input value
|
||||
# i = column index
|
||||
# $r = ???
|
||||
filterSelectExactMatch = (e, n, f, i, $r) -> e is f
|
||||
|
||||
# call the tablesorter plugin and apply the uitheme widget
|
||||
@$el.find(".tablesorter").tablesorter
|
||||
theme: "bootstrap"
|
||||
widthFixed: true
|
||||
headerTemplate: "{content} {icon}"
|
||||
textSorter:
|
||||
6: (a, b, direction, column, table) ->
|
||||
days = []
|
||||
for s in [a, b]
|
||||
n = parseInt s
|
||||
n = 0 unless _.isNumber n
|
||||
n = 1 if /^a/.test s
|
||||
for [duration, factor] in [
|
||||
[/second/i, 1 / (86400 * 1000)]
|
||||
[/minute/i, 1 / 1440]
|
||||
[/hour/i, 1 / 24]
|
||||
[/week/i, 7]
|
||||
[/month/i, 30.42]
|
||||
[/year/i, 365.2425]
|
||||
]
|
||||
if duration.test s
|
||||
n *= factor
|
||||
break
|
||||
if /^in /i.test s
|
||||
n *= -1
|
||||
days.push n
|
||||
days[0] - days[1]
|
||||
sortList: if @isEmployer() or me.isAdmin() then [[6, 0]] else [[0, 1]]
|
||||
# widget code contained in the jquery.tablesorter.widgets.js file
|
||||
# use the zebra stripe widget if you plan on hiding any rows (filter widget)
|
||||
widgets: ["uitheme", "zebra", "filter"]
|
||||
widgetOptions:
|
||||
# using the default zebra striping class name, so it actually isn't included in the theme variable above
|
||||
# this is ONLY needed for bootstrap theming if you are using the filter widget, because rows are hidden
|
||||
zebra: ["even", "odd"]
|
||||
|
||||
# extra css class applied to the table row containing the filters & the inputs within that row
|
||||
filter_cssFilter: ""
|
||||
|
||||
# If there are child rows in the table (rows with class name from "cssChildRow" option)
|
||||
# and this option is true and a match is found anywhere in the child row, then it will make that row
|
||||
# visible; default is false
|
||||
filter_childRows: false
|
||||
|
||||
# if true, filters are collapsed initially, but can be revealed by hovering over the grey bar immediately
|
||||
# below the header row. Additionally, tabbing through the document will open the filter row when an input gets focus
|
||||
filter_hideFilters: false
|
||||
|
||||
# Set this option to false to make the searches case sensitive
|
||||
filter_ignoreCase: true
|
||||
|
||||
# jQuery selector string of an element used to reset the filters
|
||||
filter_reset: ".reset"
|
||||
|
||||
# Use the $.tablesorter.storage utility to save the most recent filters
|
||||
filter_saveFilters: true
|
||||
|
||||
# Delay in milliseconds before the filter widget starts searching; This option prevents searching for
|
||||
# every character while typing and should make searching large tables faster.
|
||||
filter_searchDelay: 150
|
||||
|
||||
# Set this option to true to use the filter to find text from the start of the column
|
||||
# So typing in "a" will find "albert" but not "frank", both have a's; default is false
|
||||
filter_startsWith: false
|
||||
|
||||
filter_functions:
|
||||
2:
|
||||
"Full-time": filterSelectExactMatch
|
||||
"Part-time": filterSelectExactMatch
|
||||
"Contracting": filterSelectExactMatch
|
||||
"Remote": filterSelectExactMatch
|
||||
"Internship": filterSelectExactMatch
|
||||
5:
|
||||
"0-1": (e, n, f, i, $r) -> n <= 1
|
||||
"2-5": (e, n, f, i, $r) -> 2 <= n <= 5
|
||||
"6+": (e, n, f, i, $r) -> 6 <= n
|
||||
6:
|
||||
"Last day": (e, n, f, i, $r) ->
|
||||
days = parseFloat $($r.find('td')[i]).data('profile-age')
|
||||
days <= 1
|
||||
"Last week": (e, n, f, i, $r) ->
|
||||
days = parseFloat $($r.find('td')[i]).data('profile-age')
|
||||
days <= 7
|
||||
"Last 4 weeks": (e, n, f, i, $r) ->
|
||||
days = parseFloat $($r.find('td')[i]).data('profile-age')
|
||||
days <= 28
|
||||
8:
|
||||
"✓": filterSelectExactMatch
|
||||
"✗": filterSelectExactMatch
|
||||
|
||||
onCandidateClicked: (e) ->
|
||||
id = $(e.target).closest('tr').data('candidate-id')
|
||||
if id
|
||||
if window.history
|
||||
oldState = _.cloneDeep window.history.state ? {}
|
||||
oldState["lastViewedCandidateID"] = id
|
||||
window.history.replaceState(oldState,"")
|
||||
else
|
||||
window.location.hash = id
|
||||
url = "/account/profile/#{id}"
|
||||
window.open url,"_blank"
|
||||
else
|
||||
@openModalView new EmployerSignupView
|
|
@ -327,7 +327,7 @@ employerMatchingCandidateNotificationTask = ->
|
|||
###
|
||||
### End Employer Matching Candidate Notification Email ###
|
||||
### Ladder Update Email ###
|
||||
|
||||
### Employer ignore ###
|
||||
DEBUGGING = false
|
||||
LADDER_PREGAME_INTERVAL = 2 * 3600 * 1000 # Send emails two hours before players last submitted.
|
||||
getTimeFromDaysAgo = (now, daysAgo) ->
|
||||
|
|
Loading…
Add table
Reference in a new issue