diff --git a/app/lib/auth.coffee b/app/lib/auth.coffee index d46733089..4fa94e61f 100644 --- a/app/lib/auth.coffee +++ b/app/lib/auth.coffee @@ -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() - diff --git a/app/locale/en.coffee b/app/locale/en.coffee index cd6367464..b963b86e3 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -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" diff --git a/app/models/User.coffee b/app/models/User.coffee index c3bf71146..d81eae86e 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -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 diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass index d54e0e834..7ca283ea6 100644 --- a/app/styles/account/profile.sass +++ b/app/styles/account/profile.sass @@ -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 diff --git a/app/styles/account/settings.sass b/app/styles/account/settings.sass index 7c41cf895..6d033d98a 100644 --- a/app/styles/account/settings.sass +++ b/app/styles/account/settings.sass @@ -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 diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index 3d63858ba..837aa703e 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -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. \ No newline at end of file diff --git a/app/templates/account/settings.jade b/app/templates/account/settings.jade index 044ba6fda..770ee6fb4 100644 --- a/app/templates/account/settings.jade +++ b/app/templates/account/settings.jade @@ -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 diff --git a/app/templates/base.jade b/app/templates/base.jade index 3aec9624e..b568ccbde 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -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 diff --git a/app/templates/employers.jade b/app/templates/employers.jade index 0acbdc34e..d242a7d77 100644 --- a/app/templates/employers.jade +++ b/app/templates/employers.jade @@ -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 diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index 8e6302707..fea7a7c49 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -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 diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index b042b34ef..9a151f6d4 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -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 diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index 768072a26..d064f9ab6 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -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]?() diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 4fb0f1515..11038b9fa 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -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() diff --git a/server/users/user_schema.coffee b/server/users/user_schema.coffee index f4517db99..5b9605f44 100644 --- a/server/users/user_schema.coffee +++ b/server/users/user_schema.coffee @@ -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