Ripped out Gravatar profiles in favor of our own photo uploads, with Gravatar and Wizard portrait fallbacks.

This commit is contained in:
Nick Winter 2014-04-09 16:46:44 -07:00
parent f17775cc10
commit dd46f9d039
14 changed files with 110 additions and 212 deletions

View file

@ -11,7 +11,6 @@ init = ->
me.set 'testGroupNumber', Math.floor(Math.random() * 256)
me.save()
me.loadGravatarProfile() if me.get('email')
Backbone.listenTo(me, 'sync', Backbone.Mediator.publish('me:synced', {me:me}))
module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null) ->
@ -52,4 +51,3 @@ trackFirstArrival = ->
storage.save(BEEN_HERE_BEFORE_KEY, true)
init()

View file

@ -142,9 +142,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
password_tab: "Password"
emails_tab: "Emails"
admin: "Admin"
gravatar_select: "Select which Gravatar photo to use"
gravatar_add_photos: "Add thumbnails and photos to a Gravatar account for your email to choose an image."
gravatar_add_more_photos: "Add more photos to your Gravatar account to access them here."
wizard_color: "Wizard Clothes Color"
new_password: "New Password"
new_password_verify: "Verify"
@ -166,17 +163,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
edit_settings: "Edit Settings"
profile_for_prefix: "Profile for "
profile_for_suffix: ""
profile: "Profile"
user_not_found: "No user found. Check the URL?"
gravatar_not_found_mine: "We couldn't find your profile associated with:"
gravatar_not_found_email_suffix: "."
gravatar_signup_prefix: "Sign up at "
gravatar_signup_suffix: " to get set up!"
gravatar_not_found_other: "Alas, there's no profile associated with this person's email address."
gravatar_contact: "Contact"
gravatar_websites: "Websites"
gravatar_accounts: "As Seen On"
gravatar_profile_link: "Full Gravatar Profile"
play_level:
level_load_error: "Level could not be loaded: "
@ -628,3 +614,4 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr
gplus_friend_sessions: "G+ Friend Sessions"
leaderboard: "Leaderboard"
user_schema: "User Schema"
user_profile: "User Profile"

View file

@ -8,53 +8,23 @@ module.exports = class User extends CocoModel
initialize: ->
super()
@on 'change:emailHash', ->
@gravatarProfile = null
@loadGravatarProfile()
isAdmin: ->
permissions = @attributes['permissions'] or []
return 'admin' in permissions
gravatarAvatarURL: ->
avatar_url = GRAVATAR_URL + 'avatar/'
return avatar_url if not @emailHash
return avatar_url + @emailHash
loadGravatarProfile: ->
emailHash = @get('emailHash')
return if not emailHash
functionName = 'gotProfile'+emailHash
profileUrl = "#{GRAVATAR_URL}#{emailHash}.json?callback=#{functionName}"
script = $("<script src='#{profileUrl}' type='text/javascript'></script>")
$('head').append(script)
window[functionName] = (profile) =>
@gravatarProfile = profile
@trigger('change', @)
func = => @gravatarProfile = null unless @gravatarProfile
setTimeout(func, 1000)
displayName: ->
@get('name') or @gravatarName() or "Anoner"
@get('name') or "Anoner"
lang: ->
@get('preferredLanguage') or "en-US"
gravatarName: ->
@gravatarProfile?.entry[0]?.name?.formatted or ''
gravatarPhotoURLs: ->
photos = @gravatarProfile?.entry[0]?.photos
return if not photos
(photo.value for photo in photos)
getPhotoURL: ->
photoURL = @get('photoURL')
validURLs = @gravatarPhotoURLs()
return @gravatarAvatarURL() unless validURLs and validURLs.length
return validURLs[0] unless photoURL in validURLs
return photoURL
getPhotoURL: (size=80) ->
if photoURL = @get('photoURL')
prefix = if photoURL.search(/\?/) is -1 then "?" else "&"
return "#{photoURL}#{prefix}s=#{size}" if photoURL.search('http') isnt -1 # legacy
return "/file/#{photoURL}#{prefix}s=#{size}"
return "/db/user/#{@id}/avatar?s=#{size}"
@getByID = (id, properties, force) ->
{me} = require('lib/auth')
@ -66,7 +36,8 @@ module.exports = class User extends CocoModel
success: ->
user.loading = false
Backbone.Mediator.publish('user:fetched')
user.loadGravatarProfile()
console.log 'triggering sync'
user.trigger 'sync'
)
cache[id] = user
user

View file

@ -8,10 +8,6 @@
margin: 2px
i
margin-right: 5px
img.img-thumbnail
margin: 5px 20px 20px 20px
.approved, .not-approved
display: none
@ -27,6 +23,13 @@
border-radius: 0
padding: 10px
.public-profile-container
padding: 20px
img.profile-photo
width: 256px
border-radius: 6px
.job-profile-container
width: 100%
height: 100%
@ -112,6 +115,7 @@
margin-top: 10px
img
max-width: 524px - 60px
max-height: 200px
.header-icon
margin-right: 10px

View file

@ -11,12 +11,8 @@
#save-button
float: right
.thumbnails
text-align: center
.thumbnail
margin-bottom: 30px
margin-right: 20px
float: left
.gravatar-fallback
margin-top: 10px
input.range
position: relative

View file

@ -21,7 +21,7 @@ block content
.job-profile-row
.left-column.full-height-column
.profile-photo-container
img.profile-photo(src=photoURL)
img.profile-photo(src=user.getPhotoURL(240))
.profile-caption= profile.jobTitle || 'Software Developer'
if profileLinks.length
@ -89,65 +89,13 @@ block content
a(href=project.link).btn.btn-large.btn-inverse.flat-button Check it out
else
h2
if grav && grav.name && grav.name.formatted
.public-profile-container
h2
span(data-i18n="account_profile.profile_for_prefix") Profile for
span= grav.name.formatted
span= user.get('name')
span(data-i18n="account_profile.profile_for_suffix")
else
span(data-i18n="account_profile.profile") Profile
if loadingProfile
p(data-i18n="common.loading") Loading...
else if !user.get('emailHash')
p(data-i18n="account_profile.user_not_found") No user found. Check the URL?
else if !user.gravatarProfile
if myProfile
p
span(data-i18n="account_profile.gravatar_not_found_mine") We couldn't find your profile associated with:
strong "#{me.get('email')}"
span(data-i18n="account_profile.gravatar_not_found_email_suffix") .
span
span(data-i18n="account_profile.gravatar_signup_prefix") Sign up at
a(href="http://en.gravatar.com/") Gravatar
span(data-i18n="account_profile.gravatar_signup_suffix") to get set up!
else
p(data-i18n="account_profile.gravatar_not_found_other")
| Alas, there's no profile associated with this person's email address.
else
.container
div.row
div.col-xs-3
img(src=photoURL).img-thumbnail
p.about-me #{grav.aboutMe}
if grav.emails
div.col-xs-3
h3(data-i18n="account_profile.gravatar_contact") Contact
ul
each email in grav.emails
li #{email.value}
if grav.urls && grav.urls.length
div.col-xs-3
h3(data-i18n="account_profile.gravatar_websites") Websites
ul
each url in grav.urls
li
a(href="#{url.value}") #{url.title}
if grav.accounts
div.col-xs-3
h3(data-i18n="account_profile.gravatar_accounts") As Seen On
ul
each account in grav.accounts
li
a(href="#{account.url}") #{account.domain}
hr
p
a(href="#{grav.profileUrl}", data-i18n="account_profile.gravatar_profile_link") Full Gravatar Profile
img.profile-photo(src=user.getPhotoURL(256))
h2 TODO
p Public user profiles are not ready yet.

View file

@ -31,7 +31,7 @@ block content
.form
.form-group
label.control-label(for="name", data-i18n="general.name") Name
input#name.form-control(name="name", type="text", value="#{me.get('name')||''}", placeholder="#{gravatarName}")
input#name.form-control(name="name", type="text", value="#{me.get('name') || ''}")
.form-group
label.control-label(for="email", data-i18n="general.email") Email
input#email.form-control(name="email", type="text", value="#{me.get('email')}")
@ -42,23 +42,11 @@ block content
#picture-pane.tab-pane
h3(data-i18n="account_settings.gravatar_select") Select which Gravatar photo to use
p
if !photos
span(data-i18n="account_settings.gravatar_add_photos") Add thumbnails and photos to a Gravatar account for your email to choose an image.
else
.thumbnails
each photo, i in photos
.thumbnail
label(for="photo-#{i}")
img(src=photo)
br
input(type="radio", name="photoURL", value="#{photo}", id="photo-#{i}", checked=photo==chosenPhoto)
.clearfix
p
a(href="http://en.gravatar.com/profiles/edit/?noclose#your-images", target="_blank", data-i18n="account_settings.gravatar_add_more_photos") Add more photos to your Gravatar account to access them here.
h3(data-i18n="account_settings.upload_picture") Upload a picture
#picture-treema
.gravatar-fallback
img(src=me.getPhotoURL(256), alt="Gravatar", title="Gravatar fallback image")
#wizard-pane.tab-pane
#wizard-settings-view

View file

@ -33,7 +33,7 @@ body
if me.get('anonymous') === false
button.btn.btn-primary.navbuttontext.header-font#logout-button(data-i18n="login.log_out") Log Out
a.btn.btn-primary.navbuttontext.header-font(href="/account/profile/#{me.id}")
a.btn.btn-primary.navbuttontext.header-font(href=me.get('jobProfile') ? "/account/profile/#{me.id}" : "/account/settings")
div.navbuttontext-user-name
| #{me.displayName()}
i.icon-cog.icon-white.big

View file

@ -54,11 +54,10 @@ block content
tr(data-candidate-id=candidate.id)
td
if authorized
// Want image, but it doesn't work without loading every Gravatar profile
//img(src=candidate.getPhotoURL(), alt=profile.name, title=profile.name, width=50)
img(src=candidate.getPhotoURL(50), alt=profile.name, title=profile.name, width=50)
p= profile.name
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}
if profile.country == 'USA'
td= profile.city

View file

@ -5,7 +5,6 @@ User = require 'models/User'
module.exports = class ProfileView extends View
id: "profile-view"
template: template
loadingProfile: true
events:
'click #toggle-job-profile-approved': 'toggleJobProfileApproved'
@ -14,30 +13,16 @@ module.exports = class ProfileView extends View
constructor: (options, @userID) ->
@onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000
super options
@user = User.getByID(@userID)
@loadingProfile = false if 'gravatarProfile' of @user
@listenTo(@user, 'change', @userChanged)
@listenTo(@user, 'error', @userError)
userChanged: (user) ->
@loadingProfile = false if 'gravatarProfile' of user
@render()
userError: (user) ->
@loadingProfile = false
@render()
if @userID is me.id
@user = me
else
@user = User.getByID(@userID)
@addResourceToLoad @user, 'user_profile'
getRenderData: ->
context = super()
grav = @user.gravatarProfile
grav = grav.entry[0] if grav
addedContext =
user: @user
loadingProfile: @loadingProfile
myProfile: @user.id is context.me.id
grav: grav
photoURL: @user.getPhotoURL()
context[key] = addedContext[key] for key of addedContext
context.user = @user
context.myProfile = @user.id is context.me.id
context.marked = marked
context.moment = moment
context.iconForLink = @iconForLink

View file

@ -20,18 +20,7 @@ module.exports = class SettingsView extends View
@save = _.debounce(@save, 200)
super options
return unless me
@listenTo(me, 'change', @refreshPicturePane) # depends on gravatar load
@listenTo(me, 'invalid', (errors) -> forms.applyErrorsToForm(@$el, me.validationError))
window.f = @getSubscriptions
refreshPicturePane: ->
h = $(@template(@getRenderData()))
newPane = $('#picture-pane', h)
oldPane = $('#picture-pane')
active = oldPane.hasClass('active')
oldPane.replaceWith(newPane)
newPane.i18n()
newPane.addClass('active') if active
afterRender: ->
super()
@ -55,6 +44,11 @@ module.exports = class SettingsView extends View
@listenTo @jobProfileView, 'change', @save
@insertSubView @jobProfileView
if me.schema().loaded
@buildPictureTreema()
else
@listenToOnce me, 'schema-loaded', @buildPictureTreema
chooseTab: (category) ->
id = "##{category}-pane"
pane = $(id, @$el)
@ -68,9 +62,6 @@ module.exports = class SettingsView extends View
getRenderData: ->
c = super()
return c unless me
c.gravatarName = c.me?.gravatarName()
c.photos = me.gravatarPhotoURLs()
c.chosenPhoto = me.getPhotoURL()
c.subs = {}
c.subs[sub] = 1 for sub in c.me.get('emailSubscriptions') or ['announcement', 'notification', 'tester', 'level_creator', 'developer']
c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1
@ -88,6 +79,30 @@ module.exports = class SettingsView extends View
$('#email-pane input[type="checkbox"]', @$el).prop('checked', not Boolean(subs.length))
@save()
buildPictureTreema: ->
data = photoURL: me.get('photoURL')
if data.photoURL?.search('gravatar') isnt -1
# Old style
data.photoURL = null
schema = _.cloneDeep me.schema().attributes
schema.properties = _.pick me.schema().get('properties'), 'photoURL'
schema.required = ['photoURL']
console.log 'schema is', schema
treemaOptions =
filePath: "db/user/#{me.id}"
schema: schema
data: data
callbacks: {change: @onPictureChanged}
@pictureTreema = @$el.find('#picture-treema').treema treemaOptions
@pictureTreema.build()
@pictureTreema.open()
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
onPictureChanged: (e) =>
@trigger 'change'
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
save: ->
forms.clearFormAlerts(@$el)
@grabData()
@ -127,9 +142,10 @@ module.exports = class SettingsView extends View
me.set('password', password1)
grabOtherData: ->
me.set('name', $('#name', @$el).val())
me.set('email', $('#email', @$el).val())
me.set('emailSubscriptions', @getSubscriptions())
me.set 'name', $('#name', @$el).val()
me.set 'email', $('#email', @$el).val()
me.set 'emailSubscriptions', @getSubscriptions()
me.set 'photoURL', @pictureTreema.get('/photoURL')
adminCheckbox = @$el.find('#admin')
if adminCheckbox.length

View file

@ -104,15 +104,15 @@ module.exports = class CocoView extends Backbone.View
context
afterRender: ->
# Resource and request loading management for any given view
addResourceToLoad: (modelOrCollection, name, value=1) ->
@loadProgress.resources.push {resource:modelOrCollection, value:value, name:name}
@listenToOnce modelOrCollection, 'sync', @updateProgress
@listenTo modelOrCollection, 'error', @onResourceLoadFailed
@updateProgress()
addRequestToLoad: (jqxhr, name, retryFunc, value=1) ->
@loadProgress.requests.push {request:jqxhr, value:value, name: name, retryFunc: retryFunc}
jqxhr.done @updateProgress
@ -152,7 +152,7 @@ module.exports = class CocoView extends Backbone.View
num += r.value for r in @loadProgress.requests when r.request.status
num += r.value for r in @loadProgress.somethings when r.loaded
#console.log 'update progress', @, num, denom, arguments
progress = if denom then num / denom else 0
# sometimes the denominator isn't known from the outset, so make sure the overall progress only goes up
@loadProgress.progress = progress if progress > @loadProgress.progress
@ -160,7 +160,7 @@ module.exports = class CocoView extends Backbone.View
if num is denom and not @loaded
@loaded = true
@onLoaded()
updateProgressBar: =>
prog = "#{parseInt(@loadProgress.progress*100)}%"
@$el.find('.loading-screen .progress-bar').css('width', prog)
@ -169,7 +169,7 @@ module.exports = class CocoView extends Backbone.View
@render()
# Error handling for loading
onResourceLoadFailed: (resource, jqxhr) ->
for r, index in @loadProgress.resources
break if r.resource is resource
@ -179,12 +179,12 @@ module.exports = class CocoView extends Backbone.View
resourceIndex: index,
responseText: jqxhr.responseText
})).i18n()
onRetryResource: (e) ->
r = @loadProgress.resources[$(e.target).data('resource-index')]
r.resource.fetch()
$(e.target).closest('.loading-error-alert').remove()
onRequestLoadFailed: (jqxhr) =>
for r, index in @loadProgress.requests
break if r.request is jqxhr
@ -194,7 +194,7 @@ module.exports = class CocoView extends Backbone.View
requestIndex: index,
responseText: jqxhr.responseText
}))
onRetryRequest: (e) ->
r = @loadProgress.requests[$(e.target).data('request-index')]
@[r.retryFunc]?()

View file

@ -9,6 +9,7 @@ errors = require '../commons/errors'
async = require 'async'
log = require 'winston'
LevelSession = require('../levels/sessions/LevelSession')
LevelSessionHandler = require '../levels/sessions/level_session_handler'
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset']
privateProperties = [
@ -48,18 +49,8 @@ UserHandler = class UserHandler extends Handler
delete obj[prop] for prop in privateProperties unless includePrivates
includeCandidate = includePrivates or (obj.jobProfileApproved and req.user and ('employer' in (req.user.permissions ? [])))
delete obj[prop] for prop in candidateProperties unless includeCandidate
obj.emailHash = @buildEmailHash document
return obj
buildEmailHash: (user) ->
# emailHash is used by gravatar
hash = crypto.createHash('md5')
if user.get('email')
hash.update(_.trim(user.get('email')).toLowerCase())
else
hash.update(user.get('_id') + '')
hash.digest('hex')
waterfallFunctions: [
# FB access token checking
# Check the email is the same as FB reports
@ -126,7 +117,7 @@ UserHandler = class UserHandler extends Handler
getById: (req, res, id) ->
if req.user?._id.equals(id)
return @sendSuccess(res, @formatEntity(req, req.user))
return @sendSuccess(res, @formatEntity(req, req.user, 256))
super(req, res, id)
getNamesByIds: (req, res) ->
@ -203,9 +194,11 @@ UserHandler = class UserHandler extends Handler
@sendSuccess(res, {result:'success'})
avatar: (req, res, id) ->
@modelClass.findById(id).exec (err, document) ->
@modelClass.findById(id).exec (err, document) =>
return @sendDatabaseError(res, err) if err
res.redirect(document?.get('photoURL') or '/images/generic-wizard-icon.png')
photoURL = document?.get('photoURL')
photoURL ||= @buildGravatarURL document
res.redirect photoURL
res.end()
getLevelSessions: (req, res, userID) ->
@ -217,7 +210,7 @@ UserHandler = class UserHandler extends Handler
projection[field] = 1 for field in req.query.project.split(',')
LevelSession.find(query).select(projection).exec (err, documents) =>
return @sendDatabaseError(res, err) if err
documents = (@formatEntity(req, doc) for doc in documents)
documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
@sendSuccess(res, documents)
getCandidates: (req, res) ->
@ -235,13 +228,27 @@ UserHandler = class UserHandler extends Handler
@sendSuccess(res, candidates)
formatCandidate: (authorized, document) ->
fields = if authorized then ['jobProfile', 'jobProfileApproved', '_id'] else ['jobProfile']
fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile']
obj = _.pick document.toObject(), fields
obj.emailHash = @buildEmailHash document
obj.photoURL ||= @buildGravatarURL document if authorized
subfields = ['country', 'city', 'lookingFor', 'skills', 'experience', 'updated']
if authorized
subfields = subfields.concat ['name', 'work']
obj.jobProfile = _.pick obj.jobProfile, subfields
obj
buildGravatarURL: (user) ->
emailHash = @buildEmailHash user
defaultAvatar = "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png"
"https://www.gravatar.com/avatar/#{emailHash}?default=#{defaultAvatar}"
buildEmailHash: (user) ->
# emailHash is used by gravatar
hash = crypto.createHash('md5')
if user.get('email')
hash.update(_.trim(user.get('email')).toLowerCase())
else
hash.update(user.get('_id') + '')
hash.digest('hex')
module.exports = new UserHandler()

View file

@ -9,7 +9,7 @@ UserSchema = c.object {},
gender: {type: 'string', 'enum': ['male', 'female']}
password: {type: 'string', maxLength: 256, minLength: 2, title:'Password'}
passwordReset: {type: 'string'}
photoURL: {type: 'string', format: 'url', required: false}
photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image to serve as your profile picture.'}
facebookID: c.shortString({title: 'Facebook ID'})
gplusID: c.shortString({title: 'G+ ID'})
@ -36,7 +36,6 @@ UserSchema = c.object {},
passwordHash: {type: 'string', maxLength: 256}
# client side
#gravatarProfile: {} (should only ever be kept locally)
emailHash: {type: 'string'}
#Internationalization stuff