diff --git a/app/assets/images/pages/account/profile/education.png b/app/assets/images/pages/account/profile/education.png new file mode 100644 index 000000000..dad4914c6 Binary files /dev/null and b/app/assets/images/pages/account/profile/education.png differ diff --git a/app/assets/images/pages/account/profile/icon_facebook.png b/app/assets/images/pages/account/profile/icon_facebook.png new file mode 100644 index 000000000..b775c18fa Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_facebook.png differ diff --git a/app/assets/images/pages/account/profile/icon_github.png b/app/assets/images/pages/account/profile/icon_github.png new file mode 100644 index 000000000..fc1801abc Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_github.png differ diff --git a/app/assets/images/pages/account/profile/icon_gplus.png b/app/assets/images/pages/account/profile/icon_gplus.png new file mode 100644 index 000000000..c2343eb50 Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_gplus.png differ diff --git a/app/assets/images/pages/account/profile/icon_linkedin.png b/app/assets/images/pages/account/profile/icon_linkedin.png new file mode 100644 index 000000000..cdd0ff6c2 Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_linkedin.png differ diff --git a/app/assets/images/pages/account/profile/icon_twitter.png b/app/assets/images/pages/account/profile/icon_twitter.png new file mode 100644 index 000000000..1280ad6df Binary files /dev/null and b/app/assets/images/pages/account/profile/icon_twitter.png differ diff --git a/app/assets/images/pages/account/profile/work.png b/app/assets/images/pages/account/profile/work.png new file mode 100644 index 000000000..72e659071 Binary files /dev/null and b/app/assets/images/pages/account/profile/work.png differ diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee index 8b270808b..f4b24bc47 100644 --- a/app/lib/LevelBus.coffee +++ b/app/lib/LevelBus.coffee @@ -112,7 +112,7 @@ module.exports = class LevelBus extends Bus @changedSessionProperties.teamSpells = true @session.set({'teamSpells': @teamSpellMap}) @saveSession() - if spellTeam is me.team + if spellTeam is me.team or spellTeam is "common" @onSpellChanged e # Save the new spell to the session, too. onScriptStateChanged: (e) -> 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/lib/contact.coffee b/app/lib/contact.coffee index fa38fbf69..ade94f2e9 100644 --- a/app/lib/contact.coffee +++ b/app/lib/contact.coffee @@ -1,16 +1,9 @@ - - module.exports.sendContactMessage = (contactMessageObject, modal) -> modal.find('.sending-indicator').show() - jqxhr = $.post '/contact', - email: contactMessageObject.email - message: contactMessageObject.message - , - (response) -> - console.log "Got contact response:", response - modal.find('.sending-indicator').hide() - modal.find('#contact-message').val("Thanks!") - _.delay -> - modal.find('#contact-message').val("") - modal.modal 'hide' - , 1000 + jqxhr = $.post '/contact', contactMessageObject, (response) -> + modal.find('.sending-indicator').hide() + modal.find('#contact-message').val("Thanks!") + _.delay -> + modal.find('#contact-message').val("") + modal.modal 'hide' + , 1000 diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 7d35d42aa..36bbfd9e8 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -3,6 +3,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr loading: "Loading..." saving: "Saving..." sending: "Sending..." + send: "Send" cancel: "Cancel" save: "Save" create: "Create" @@ -111,6 +112,8 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr forum_page: "our forum" forum_suffix: " instead." send: "Send Feedback" + contact_candidate: "Contact Candidate" + recruitment_reminder: "Use this form to get in touch with candidates you are interested in interviewing. Remember that CodeCombat charges 18% of first-year salary for any full-time candidate you hire who stays 90 days, but that part-timers, remote employees, contractors, and interns are free." diplomat_suggestion: title: "Help translate CodeCombat!" @@ -142,9 +145,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 +166,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: " @@ -345,6 +334,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr results: "Results" description: "Description" or: "or" + subject: "Subject" email: "Email" password: "Password" message: "Message" @@ -616,7 +606,7 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr bad_input: "Bad input." server_error: "Server error." unknown: "Unknown error." - + resources: your_sessions: "Your Sessions" level: "Level" @@ -626,4 +616,6 @@ module.exports = nativeDescription: "English", englishDescription: "English", tr facebook_friend_sessions: "Facebook Friend Sessions" gplus_friends: "G+ Friends" gplus_friend_sessions: "G+ Friend Sessions" - leaderboard: 'leaderboard' \ No newline at end of file + leaderboard: "Leaderboard" + user_schema: "User Schema" + user_profile: "User Profile" diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index 45c920faa..5e51f04be 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -102,7 +102,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi spectate: "Наблюдать" contact: - contact_us: "Связаться с CodeCombat" + contact_us: "Связаться с Нами" welcome: "Мы рады вашему сообщению! Используйте эту форму, чтобы отправить нам email. " contribute_prefix: "Если вы хотите внести свой вклад в проект, зайдите на нашу " contribute_page: "страницу сотрудничества" @@ -604,21 +604,21 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi so_ready: "Я полностью готов(а) для этого" # loading_error: -# could_not_load: "Error loading from server" -# connection_failure: "Connection failed." +# could_not_load: "Ошибка соединения с сервером" +# connection_failure: "Соединение потеряно." # unauthorized: "You need to be signed in. Do you have cookies disabled?" # forbidden: "You do not have the permissions." -# not_found: "Not found." +# not_found: "Не наидено." # not_allowed: "Method not allowed." # timeout: "Server timeout." # conflict: "Resource conflict." # bad_input: "Bad input." -# server_error: "Server error." -# unknown: "Unknown error." +# server_error: "Ошибка сервера." +# unknown: "Неизвестная ошибка." # resources: # your_sessions: "Your Sessions" -# level: "Level" +# level: "Уровень" # social_network_apis: "Social Network APIs" # facebook_status: "Facebook Status" # facebook_friends: "Facebook Friends" diff --git a/app/locale/tr.coffee b/app/locale/tr.coffee index d99ed8dd3..6c0e9dcaa 100644 --- a/app/locale/tr.coffee +++ b/app/locale/tr.coffee @@ -5,7 +5,7 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t sending: "Gönderiliyor..." cancel: "İptal" save: "Kaydet" -# create: "Create" + create: "Oluştur" delay_1_sec: "1 saniye" delay_3_sec: "3 saniye" delay_5_sec: "5 saniye" @@ -16,11 +16,11 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t units: second: "saniye" - seconds: "saniyeler" + seconds: "saniye" minute: "dakika" - minutes: "dakikalar" + minutes: "dakika" hour: "saat" - hours: "saatler" + hours: "saat" modal: close: "Kapat" @@ -224,13 +224,13 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t tome_available_spells: "Kullanılabilir Büyüler" hud_continue: "Devam (ÜstKarakter+Boşluk)" spell_saved: "Büyü Kaydedildi" -# skip_tutorial: "Skip (esc)" + skip_tutorial: "Atla (esc)" # editor_config: "Editor Config" # editor_config_title: "Editor Configuration" # editor_config_language_label: "Programming Language" # editor_config_language_description: "Define the programming language you want to code in." # editor_config_keybindings_label: "Key Bindings" -# editor_config_keybindings_default: "Default (Ace)" + editor_config_keybindings_default: "Varsayılan (Ace)" # editor_config_keybindings_description: "Adds additional shortcuts known from the common editors." # editor_config_invisibles_label: "Show Invisibles" # editor_config_invisibles_description: "Displays invisibles such as spaces or tabs." @@ -238,7 +238,7 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t # editor_config_indentguides_description: "Displays vertical lines to see indentation better." # editor_config_behaviors_label: "Smart Behaviors" # editor_config_behaviors_description: "Autocompletes brackets, braces, and quotes." -# loading_ready: "Ready!" + loading_ready: "Hazır!" # tip_insert_positions: "Shift+Click a point on the map to insert it into the spell editor." # tip_toggle_play: "Toggle play/paused with Ctrl+P." # tip_scrub_shortcut: "Ctrl+[ and Ctrl+] rewind and fast-forward." @@ -267,9 +267,9 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t # tip_impossible: "It always seems impossible until it's done. - Nelson Mandela" # tip_talk_is_cheap: "Talk is cheap. Show me the code. - Linus Torvalds" # tip_first_language: "The most disastrous thing that you can ever learn is your first programming language. - Alan Kay" -# time_current: "Now:" -# time_total: "Max:" -# time_goto: "Go to:" + time_current: "Şimdi:" + time_total: "Max:" + time_goto: "Git:" admin: av_title: "Yönetici Görünümleri" diff --git a/app/models/User.coffee b/app/models/User.coffee index c3bf71146..9f688f46b 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -8,53 +8,25 @@ 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 = $("") - $('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, useJobProfilePhoto=false) -> + photoURL = if useJobProfilePhoto then @get('jobProfile')?.photoURL else null + photoURL ||= @get('photoURL') + if 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 +38,7 @@ module.exports = class User extends CocoModel success: -> user.loading = false Backbone.Mediator.publish('user:fetched') - user.loadGravatarProfile() + #user.trigger 'sync' # needed? ) cache[id] = user user diff --git a/app/styles/account/profile.sass b/app/styles/account/profile.sass index 2edec8f24..0d0f4e450 100644 --- a/app/styles/account/profile.sass +++ b/app/styles/account/profile.sass @@ -1,15 +1,195 @@ #profile-view - button - float: right - i - margin-right: 5px - - img.img-thumbnail - margin: 20px 0 + .profile-control-bar + background-color: rgb(78, 78, 78) + width: 100% + text-align: center + + button.edit-settings-button + margin: 2px + i + margin-right: 5px - li - list-style: none - - ul - margin: 0 + .approved, .not-approved + display: none + + .main-content-area padding: 0 + + .flat-button + width: 100% + margin-bottom: 10px + background: rgb(78, 78, 78) + border: 0 + 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% + padding: 0 + display: table + + h1, h2, h3, h4, h5, h6 + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif + color: #555 + + ul.links, ul.projects + margin: 0 + padding: 0 + + li + list-style: none + + .job-profile-row + height: 100% + display: table-row + + .full-height-column + height: 100% + padding: 5px + display: table-cell + vertical-align: top + + h3:first-child + margin: 5px 0 5px 0 + + .left-column + width: 250px + padding: 5px + background-color: rgb(220, 220, 220) + + .profile-photo-container + position: relative + margin-bottom: 10px + + img.profile-photo + width: 240px + border-radius: 6px + + .profile-caption + background-color: rgba(0, 0, 0, 0.5) + color: white + border-bottom-right-radius: 6px + border-bottom-left-radius: 6px + position: absolute + width: 100% + bottom: 0px + text-align: center + + ul.links + li.has-icon + display: inline-block + img + margin: 0 0 10px 0 + li.has-icon:not(:nth-child(5)) + img + margin: 0 10px 10px 0 + + #contact-candidate + margin-top: 20px + background-color: rgb(177, 55, 25) + padding: 15px + font-size: 20px + + .middle-column + width: 524px + background-color: white + padding-left: 20px + padding-right: 20px + + &.double-column + width: 524px + 250px + padding-left: 30px + padding-right: 30px + + code + background-color: rgb(220, 220, 220) + color: #555 + margin: 2px 0 + display: inline-block + text-transform: lowercase + + .long-description + margin-top: 10px + img + max-width: 524px - 60px + max-height: 200px + + .experience-header + margin-top: 25px + + .header-icon + margin-right: 10px + width: 32px + height: 32px + + .duration + margin-left: 10px + margin-bottom: 10px + + #job-profile-notes + width: 100% + height: 100px + + .right-column + width: 250px + background-color: rgb(220, 220, 220) + + > h3:first-child + background-color: white + padding: 5px 5px + margin: 5px 2px 5px 2px + + ul.projects + li + margin-bottom: 10px + padding: 5px 5px + border: 2px solid rgb(220, 220, 220) + transition: .5s ease-in-out + position: relative + background-color: white + + &:hover + border-color: rgb(100, 130, 255) + + a + position: relative + z-index: 2 + + > a + position: absolute + width: 100% + height: 100% + top: 0 + left: 0 + z-index: 1 + + .project-image + width: 230px + height: 115px + background-size: cover + background-repeat: no-repeat + background-position: center + + -webkit-filter: grayscale(100%) + -webkit-transition: .5s ease-in-out + -moz-filter: grayscale(100%) + -moz-transition: .5s ease-in-out + -o-filter: grayscale(100%) + -o-transition: .5s ease-in-out + filter: grayscale(100%) + transition: .5s ease-in-out + + li:hover + .project-image + -webkit-filter: grayscale(0%) + -moz-filter: grayscale(0%) + -o-filter: grayscale(0%) + filter: grayscale(0%) diff --git a/app/styles/account/settings.sass b/app/styles/account/settings.sass index 8751e59ef..b768d69a2 100644 --- a/app/styles/account/settings.sass +++ b/app/styles/account/settings.sass @@ -8,15 +8,20 @@ background: #eee border-radius: 5px - #save-button - float: right + #save-button-container + position: fixed + top: 100px + width: 1000px + z-index: 10 - .thumbnails - text-align: center - .thumbnail - margin-bottom: 30px - margin-right: 20px - float: left + #save-button + float: right + + &.btn-info, &.btn-danger + opacity: 1.0 + + .gravatar-fallback + margin-top: 10px input.range position: relative @@ -37,4 +42,15 @@ font-size: 12px .form - max-width: 600px \ No newline at end of file + max-width: 600px + + #job-profile-treema + background-color: white + + input + width: 790px + + .treema-description + font-size: 14px + line-height: 22px + opacity: 1 diff --git a/app/styles/employers.sass b/app/styles/employers.sass new file mode 100644 index 000000000..2d61d81fa --- /dev/null +++ b/app/styles/employers.sass @@ -0,0 +1,18 @@ +#employers-view + .tablesorter + //img + // display: none + + .tablesorter-header + cursor: pointer + &:hover + color: black + + .tablesorter-headerAsc + background-color: #cfc + + .tablesorter-headerDesc + background-color: #ccf + + tr + cursor: pointer diff --git a/app/styles/play/level/loading.sass b/app/styles/play/level/loading.sass index 177334f72..9570497f3 100644 --- a/app/styles/play/level/loading.sass +++ b/app/styles/play/level/loading.sass @@ -22,7 +22,9 @@ position: absolute z-index: 20 $UNVEIL_TIME: 1.2s - pointer-events: none + + &.unveiled + pointer-events: none .loading-details position: absolute diff --git a/app/templates/account/job_profile.jade b/app/templates/account/job_profile.jade new file mode 100644 index 000000000..a5eff46d3 --- /dev/null +++ b/app/templates/account/job_profile.jade @@ -0,0 +1,8 @@ +h3(data-i18n="account_settings.job_profile") Job Profile + +if me.get('jobProfileApproved') + p.lead(data-i18n="account_settings.job_profile_approved") Your job profile has been approved by CodeCombat. Hungry employers will see it until you mark it inactive or it is stale for two months. +else + p.lead(data-i18n="account_settings.job_profile_explanation") Hi! Fill this out, and if we think we can find you a software developer job, we will get in touch to approve your profile. + +#job-profile-treema \ No newline at end of file diff --git a/app/templates/account/profile.jade b/app/templates/account/profile.jade index 65dc9786b..5ef74256e 100644 --- a/app/templates/account/profile.jade +++ b/app/templates/account/profile.jade @@ -1,72 +1,100 @@ extends /templates/base block content + + if myProfile || (me.isAdmin() && user.get('jobProfile')) + .profile-control-bar + if myProfile + a(href="/account/settings") + button.btn.edit-settings-button + i.icon-cog + span(data-i18n="account_profile.edit_settings") Edit Settings + if me.isAdmin() && user.get('jobProfile') + button.btn.edit-settings-button#toggle-job-profile-approved + i.icon-cog + span(data-i18n='account_profile.approved').approved Approved + span(data-i18n='account_profile.approved').not-approved Not Approved - if myProfile - a(href="/account/settings") - button.btn - i.icon-cog - span(data-i18n="account_profile.edit_settings") Edit Settings - - h2 - if grav && grav.name && grav.name.formatted - span(data-i18n="account_profile.profile_for_prefix") Profile for - span= grav.name.formatted - span(data-i18n="account_profile.profile_for_suffix") - else - span(data-i18n="account_profile.profile") Profile + if user.get('jobProfile') + - var profile = user.get('jobProfile'); + .job-profile-container + .job-profile-row + .left-column.full-height-column + .profile-photo-container + img.profile-photo(src=user.getPhotoURL(240, true)) + .profile-caption= profile.jobTitle || 'Software Developer' - if loadingProfile - p(data-i18n="common.loading") Loading... + if profileLinks.length + ul.links + each link in profileLinks + li(title=profile.name + " on " + link.name, class=link.icon ? "has-icon" : "") + a(href=link.link) + if link.icon + img(src=link.icon.url, alt=link.icon.name) + else + button.btn.btn-large.btn-inverse.flat-button= link.name - else if !user.get('emailHash') - p(data-i18n="account_profile.user_not_found") No user found. Check the URL? + div= profile.city + ', ' + profile.country + div= profile.visa + div Looking for: #{profile.lookingFor} + div Last updated #{moment(profile.updated).fromNow()} - 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. + button#contact-candidate.btn.btn-large.btn-inverse.flat-button Contact #{profile.name.split(' ')[0]} + + .middle-column.full-height-column + h3= profile.name + p= profile.shortDescription + + each skill in profile.skills + code= skill + span + div.long-description!= marked(profile.longDescription) + + if profile.work.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/work.png", alt="") + | Work Experience + each job in profile.work + div.duration.pull-right= job.duration + | #{job.role} at #{job.employer} + .clearfix + + if profile.education.length + h3.experience-header + img.header-icon(src="/images/pages/account/profile/education.png", alt="") + | Education + each school in profile.education + div.duration.pull-right= school.duration + | #{school.degree} at #{school.school} + .clearfix + + if user.get('jobProfileNotes') || me.isAdmin() + h3.experience-header Our Notes + - var notes = user.get('jobProfileNotes') || ''; + if me.isAdmin() + textarea#job-profile-notes!= notes + else + div!= marked(notes) + + .right-column.full-height-column + if profile.projects.length + h3 Projects + ul.projects + each project in profile.projects + li + a(href=project.link) + .project-image(style="background-image: url(/file/" + project.picture + ")") + p= project.name + div!= marked(project.description) else - .container - div.row - div.col-xs-3 - img(src=photoURL).img-thumbnail - - p.about-me #{grav.aboutMe} + .public-profile-container + h2 + span(data-i18n="account_profile.profile_for_prefix") Profile for + span= user.get('name') + span(data-i18n="account_profile.profile_for_suffix") - 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 91b533b1b..a1e829b7e 100644 --- a/app/templates/account/settings.jade +++ b/app/templates/account/settings.jade @@ -8,7 +8,8 @@ block content p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings. else - button.btn#save-button.disabled.secret(data-i18n="account_settings.autosave") Changes Save Automatically + #save-button-container + button.btn#save-button.disabled.secret(data-i18n="account_settings.autosave") Changes Save Automatically ul.nav.nav-pills#settings-tabs li @@ -21,6 +22,9 @@ block content a(href="#password-pane", data-toggle="tab", data-i18n="account_settings.password_tab") Password li a(href="#email-pane", data-toggle="tab", data-i18n="account_settings.emails_tab") Emails + if showsJobProfileTab + li + a(href="#job-profile-pane", data-toggle="tab", data-i18n="account_settings.job_profile_tab") Job Profile .tab-content#settings-panes #general-pane.tab-pane @@ -28,7 +32,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')}") @@ -39,23 +43,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 @@ -153,3 +145,6 @@ block content span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments. button.btn#toggle-all-button(data-i18n="account_settings.email_toggle") Toggle All + + #job-profile-pane.tab-pane + #job-profile-view diff --git a/app/templates/base.jade b/app/templates/base.jade index 8212882af..f31bc2db3 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 485d21622..d242a7d77 100644 --- a/app/templates/employers.jade +++ b/app/templates/employers.jade @@ -32,3 +32,54 @@ block content h4 Skill: from interns and entry level to senior developers and management h4 Technologies: just about everything h4 Countries: USA, Canada, Australia, and many more + + if candidates.length + table.table.table-condensed.table-hover.table-responsive.tablesorter + thead + tr + th Name + th Location + th Looking For + th Top 5 Skills + th Yrs Exp + th Last Updated + th Current Job + if me.isAdmin() + th ✓? + + tbody + for candidate, index in candidates + - var profile = candidate.get('jobProfile'); + - var authorized = candidate.id; // If we have the id, then we are authorized. + tr(data-candidate-id=candidate.id) + td + if authorized + 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) + p Developer ##{index + 1} + if profile.country == 'USA' + td= profile.city + else + td= profile.country + td= profile.lookingFor + td + each skill in profile.skills.slice(0, 5) + code= skill + span + td= profile.experience + td= moment(profile.updated).fromNow() + if authorized + if profile.work.length + td= profile.work[0].role + ' at ' + profile.work[0].employer + else + td + else + td + em Employer sign-up required. + if me.isAdmin() + if candidate.get('jobProfileApproved') + td ✓ + else + td ✗ \ No newline at end of file diff --git a/app/templates/modal/diplomat_suggestion.jade b/app/templates/modal/diplomat_suggestion.jade index b3d29cb6b..56d98f33a 100644 --- a/app/templates/modal/diplomat_suggestion.jade +++ b/app/templates/modal/diplomat_suggestion.jade @@ -1,7 +1,7 @@ extends /templates/modal/modal_base block modal-header-content - h3(data-i18n="diplomat_suggestion.title") + h3(data-i18n="diplomat_suggestion.title") Help translate CodeCombat! block modal-body-content h4(data-i18n="diplomat_suggestion.sub_heading") We need your language skills. diff --git a/app/templates/modal/employer_signup_modal.jade b/app/templates/modal/employer_signup_modal.jade new file mode 100644 index 000000000..809b26452 --- /dev/null +++ b/app/templates/modal/employer_signup_modal.jade @@ -0,0 +1,9 @@ +extends /templates/modal/modal_base + +block modal-header-content + h3(data-i18n="employer_signup.title") Hire CodeCombat Players + +block modal-body-content + h4(data-i18n="employer_signup.sub_heading") Let us find your next brilliant developers. + + p(data-i18n="employer_signup.pitch_body") When you hire one of our players, you will pay CodeCombat 18% of her first-year salary, payable within 30 days of when she starts working. We will fully refund our placement fee if she leaves or is fired within 90 days. Cool? Email george@codecombat.com to get set up with employer permissions to see our candidates. diff --git a/app/templates/modal/job_profile_contact.jade b/app/templates/modal/job_profile_contact.jade new file mode 100644 index 000000000..87120f033 --- /dev/null +++ b/app/templates/modal/job_profile_contact.jade @@ -0,0 +1,22 @@ +extends /templates/modal/contact + +block modal-header-content + h3(data-i18n="contact.contact_candidate") Contact Candidate + +block modal-body-content + p(data-i18n="contact.recruitment_reminder") Use this form to get in touch with candidates you are interested in interviewing. Remember that CodeCombat charges 18% of first-year salary for any full-time candidate you hire who stays 90 days, but that part-timers, remote employees, contractors, and interns are free. + .form + .form-group + label.control-label(for="contact-email", data-i18n="general.email") Email + input#contact-email.form-control(name="email", type="email", value=me.get('email'), placeholder="Where should the candidate reply?") + .form-group + label.control-label(for="contact-subject", data-i18n="general.subject") Subject + input#contact-subject.form-control(name="subject", type="text", value="Job interest", placeholder="Subject of the email the candidate will receive.") + .form-group + label.control-label(for="contact-message", data-i18n="general.message") Message + textarea#contact-message.form-control(name="message", rows=8) + +block modal-footer-content + span.sending-indicator.pull-left.secret(data-i18n="common.sending") Sending... + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="common.cancel").btn Cancel + button.btn.btn-primary#contact-submit-button(data-i18n="common.send") Send diff --git a/app/views/account/job_profile_view.coffee b/app/views/account/job_profile_view.coffee new file mode 100644 index 000000000..a39fb6b16 --- /dev/null +++ b/app/views/account/job_profile_view.coffee @@ -0,0 +1,94 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/account/job_profile' +{me} = require('lib/auth') + +module.exports = class JobProfileView extends CocoView + id: 'job-profile-view' + template: template + + editableSettings: [ + 'lookingFor', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', + 'work', 'education', 'visa', 'projects', 'links', 'jobTitle', 'photoURL' + ] + readOnlySettings: [ + 'updated' + ] + + constructor: (options) -> + super options + unless me.schema().loaded + @addSomethingToLoad("user_schema") + @listenToOnce me, 'schema-loaded', => @somethingLoaded 'user_schema' + + afterRender: -> + super() + return if @loading() + @buildJobProfileTreema() + + buildJobProfileTreema: -> + visibleSettings = @editableSettings.concat @readOnlySettings + 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().get('properties').jobProfile + 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 + treemaOptions = + filePath: "db/user/#{me.id}" + schema: schema + data: data + aceUseWrapMode: true + callbacks: {change: @onJobProfileChanged} + nodeClasses: + 'skill': SkillTagNode + 'link-name': LinkNameNode + 'city': CityNode + 'country': CountryNode + + @jobProfileTreema = @$el.find('#job-profile-treema').treema treemaOptions + @jobProfileTreema.build() + @jobProfileTreema.open() + + onJobProfileChanged: (e) => + @hasEditedProfile = true + @trigger 'change' + + getData: -> + return {} unless me.get('jobProfile') or @hasEditedProfile + _.pick @jobProfileTreema.data, (value, key) => key in @editableSettings + + +commonSkills = ['c#', 'java', 'javascript', 'php', 'android', 'jquery', 'python', 'c++', 'html', 'mysql', 'ios', 'asp.net', 'css', 'sql', 'iphone', '.net', 'objective-c', 'ruby-on-rails', 'c', 'ruby', 'sql-server', 'ajax', 'wpf', 'linux', 'database', 'django', 'vb.net', 'windows', 'facebook', 'r', 'html5', 'multithreading', 'ruby-on-rails-3', 'wordpress', 'winforms', 'node.js', 'spring', 'osx', 'performance', 'visual-studio-2010', 'oracle', 'swing', 'algorithm', 'git', 'linq', 'apache', 'web-services', 'perl', 'wcf', 'entity-framework', 'bash', 'visual-studio', 'sql-server-2008', 'hibernate', 'actionscript-3', 'angularjs', 'matlab', 'qt', 'ipad', 'sqlite', 'cocoa-touch', 'cocoa', 'flash', 'mongodb', 'codeigniter', 'jquery-ui', 'css3', 'tsql', 'google-maps', 'silverlight', 'security', 'delphi', 'vba', 'postgresql', 'jsp', 'shell', 'internet-explorer', 'google-app-engine', 'sockets', 'validation', 'scala', 'oop', 'unit-testing', 'xaml', 'parsing', 'twitter-bootstrap', 'google-chrome', 'http', 'magento', 'email', 'android-layout', 'flex', 'rest', 'maven', 'jsf', 'listview', 'date', 'winapi', 'windows-phone-7', 'facebook-graph-api', 'unix', 'url', 'c#-4.0', 'jquery-ajax', 'svn', 'symfony2', 'table', 'cakephp', 'firefox', 'ms-access', 'java-ee', 'jquery-mobile', 'python-2.7', 'tomcat', 'zend-framework', 'opencv', 'visual-c++', 'opengl', 'spring-mvc', 'sql-server-2005', 'authentication', 'search', 'xslt', 'servlets', 'pdf', 'animation', 'math', 'batch-file', 'excel-vba', 'iis', 'mod-rewrite', 'sharepoint', 'gwt', 'powershell', 'visual-studio-2012', 'haskell', 'grails', 'ubuntu', 'networking', 'nhibernate', 'design-patterns', 'testing', 'jpa', 'visual-studio-2008', 'core-data', 'user-interface', 'audio', 'backbone.js', 'gcc', 'mobile', 'design', 'activerecord', 'extjs', 'video', 'stored-procedures', 'optimization', 'drupal', 'image-processing', 'android-intent', 'logging', 'web-applications', 'razor', 'database-design', 'azure', 'vim', 'memory-management', 'model-view-controller', 'cordova', 'c++11', 'selenium', 'ssl', 'assembly', 'soap', 'boost', 'canvas', 'google-maps-api-3', 'netbeans', 'heroku', 'jsf-2', 'encryption', 'hadoop', 'linq-to-sql', 'dll', 'xpath', 'data-binding', 'windows-phone-8', 'phonegap', 'jdbc', 'python-3.x', 'twitter', 'mvvm', 'gui', 'web', 'jquery-plugins', 'numpy', 'deployment', 'ios7', 'emacs', 'knockout.js', 'graphics', 'joomla', 'unicode', 'windows-8', 'android-fragments', 'ant', 'command-line', 'version-control', 'yii', 'github', 'amazon-web-services', 'macros', 'ember.js', 'svg', 'opengl-es', 'django-models', 'solr', 'orm', 'blackberry', 'windows-7', 'ruby-on-rails-4', 'compiler', 'tcp', 'pdo', 'architecture', 'groovy', 'nginx', 'concurrency', 'paypal', 'iis-7', 'express', 'vbscript', 'google-chrome-extension', 'memory-leaks', 'rspec', 'actionscript', 'interface', 'fonts', 'oauth', 'ssh', 'tfs', 'junit', 'struts2', 'd3.js', 'coldfusion', '.net-4.0', 'jqgrid', 'asp-classic', 'https', 'plsql', 'stl', 'sharepoint-2010', 'asp.net-web-api', 'mysqli', 'sed', 'awk', 'internet-explorer-8', 'jboss', 'charts', 'scripting', 'matplotlib', 'laravel', 'clojure', 'entity-framework-4', 'intellij-idea', 'xml-parsing', 'sqlite3', '3d', 'io', 'mfc', 'devise', 'playframework', 'youtube', 'amazon-ec2', 'localization', 'cuda', 'jenkins', 'ssis', 'safari', 'doctrine2', 'vb6', 'amazon-s3', 'dojo', 'air', 'eclipse-plugin', 'android-asynctask', 'crystal-reports', 'cocos2d-iphone', 'dns', 'highcharts', 'ruby-on-rails-3.2', 'ado.net', 'sql-server-2008-r2', 'android-emulator', 'spring-security', 'cross-browser', 'oracle11g', 'bluetooth', 'f#', 'msbuild', 'drupal-7', 'google-apps-script', 'mercurial', 'xna', 'google-analytics', 'lua', 'parallel-processing', 'internationalization', 'java-me', 'mono', 'monotouch', 'android-ndk', 'lucene', 'kendo-ui', 'linux-kernel', 'terminal', 'phpmyadmin', 'makefile', 'ffmpeg', 'applet', 'active-directory', 'coffeescript', 'pandas', 'responsive-design', 'xhtml', 'silverlight-4.0', '.net-3.5', 'jaxb', 'ruby-on-rails-3.1', 'gps', 'geolocation', 'network-programming', 'windows-services', 'laravel-4', 'ggplot2', 'rss', 'webkit', 'functional-programming', 'wsdl', 'telerik', 'maven-2', 'cron', 'mapreduce', 'websocket', 'automation', 'windows-runtime', 'django-forms', 'tkinter', 'android-widget', 'android-activity', 'rubygems', 'content-management-system', 'doctrine', 'django-templates', 'gem', 'fluent-nhibernate', 'seo', 'meteor', 'serial-port', 'glassfish', 'documentation', 'cryptography', 'ef-code-first', 'extjs4', 'x86', 'wordpress-plugin', 'go', 'wix', 'linq-to-entities', 'oracle10g', 'cocos2d', 'selenium-webdriver', 'open-source', 'jtable', 'qt4', 'smtp', 'redis', 'jvm', 'openssl', 'timezone', 'nosql', 'erlang', 'playframework-2.0', 'machine-learning', 'mocking', 'unity3d', 'thread-safety', 'android-actionbar', 'jni', 'udp', 'jasper-reports', 'zend-framework2', 'apache2', 'internet-explorer-7', 'sqlalchemy', 'neo4j', 'ldap', 'jframe', 'youtube-api', 'filesystems', 'make', 'flask', 'gdb', 'cassandra', 'sms', 'g++', 'django-admin', 'push-notification', 'statistics', 'tinymce', 'locking', 'javafx', 'firefox-addon', 'fancybox', 'windows-phone', 'log4j', 'uikit', 'prolog', 'socket.io', 'icons', 'oauth-2.0', 'refactoring', 'sencha-touch', 'elasticsearch', 'symfony1', 'google-api', 'webserver', 'wpf-controls', 'microsoft-metro', 'gtk', 'flex4', 'three.js', 'gradle', 'centos', 'angularjs-directive', 'internet-explorer-9', 'sass', 'html5-canvas', 'interface-builder', 'programming-languages', 'gmail', 'jersey', 'twitter-bootstrap-3', 'arduino', 'requirejs', 'cmake', 'web-development', 'software-engineering', 'startups', 'entrepreneurship', 'social-media-marketing', 'writing', 'marketing', 'web-design', 'graphic-design', 'game-development', 'game-design', 'photoshop', 'illustrator', 'robotics', 'aws', 'devops', 'mathematica', 'bioinformatics', 'data-vis', 'ui', 'embedded-systems', 'codecombat'] + +commonLinkNames = ['GitHub', 'Facebook', 'Twitter', 'G+', 'LinkedIn', 'Personal Website', 'Blog'] + +countries = ['Afghanistan', 'Albania', 'Algeria', 'American Samoa', 'Andorra', 'Angola', 'Anguilla', 'Antarctica', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Aruba', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bermuda', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei Darussalam', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cambodia', 'Cameroon', 'Canada', 'Cape Verde', 'Cayman Islands', 'Central African Republic', 'Chad', 'Chile', 'China', 'Christmas Island', 'Cocos (Keeling) Islands', 'Colombia', 'Comoros', 'Democratic Republic of the Congo (Kinshasa)', 'Congo, Republic of (Brazzaville)', 'Cook Islands', 'Costa Rica', 'Ivory Coast', 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'East Timor', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Ethiopia', 'Falkland Islands', 'Faroe Islands', 'Fiji', 'Finland', 'France', 'French Guiana', 'French Polynesia', 'French Southern Territories', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Gibraltar', 'Great Britain', 'Greece', 'Greenland', 'Grenada', 'Guadeloupe', 'Guam', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Holy See', 'Honduras', 'Hong Kong', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'North Korea', 'South Korea', 'Kosovo', 'Kuwait', 'Kyrgyzstan', 'Lao, People\'s Democratic Republic', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Macau', 'Macedonia, Rep. of', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Martinique', 'Mauritania', 'Mauritius', 'Mayotte', 'Mexico', 'Micronesia, Federal States of', 'Moldova, Republic of', 'Monaco', 'Mongolia', 'Montenegro', 'Montserrat', 'Morocco', 'Mozambique', 'Myanmar, Burma', 'Namibia', 'Nauru', 'Nepal', 'Netherlands', 'Netherlands Antilles', 'New Caledonia', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'Niue', 'Northern Mariana Islands', 'Norway', 'Oman', 'Pakistan', 'Palau', 'Palestinian territories', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Pitcairn Island', 'Poland', 'Portugal', 'Puerto Rico', 'Qatar', 'Reunion Island', 'Romania', 'Russian Federation', 'Rwanda', 'Saint Kitts and Nevis', 'Saint Lucia', 'Saint Vincent and the Grenadines', 'Samoa', 'San Marino', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Sudan', 'Spain', 'Sri Lanka', 'Sudan', 'Suriname', 'Swaziland', 'Sweden', 'Switzerland', 'Syria, Syrian Arab Republic', 'Taiwan', 'Tajikistan', 'Tanzania; officially the United Republic of Tanzania', 'Thailand', 'Tibet', 'Timor-Leste', 'Togo', 'Tokelau', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Turks and Caicos Islands', 'Tuvalu', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'USA', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Vatican City State', 'Venezuela', 'Vietnam', 'Virgin Islands (British)', 'Virgin Islands (U.S.)', 'Wallis and Futuna Islands', 'Western Sahara', 'Yemen', 'Zambia', 'Zimbabwe'] + +commonCities = ['Tokyo', 'Jakarta', 'Seoul', 'Delhi', 'Shanghai', 'Manila', 'Karachi', 'New York', 'Sao Paulo', 'Mexico City', 'Cairo', 'Beijing', 'Osaka', 'Mumbai (Bombay)', 'Guangzhou', 'Moscow', 'Los Angeles', 'Calcutta', 'Dhaka', 'Buenos Aires', 'Istanbul', 'Rio de Janeiro', 'Shenzhen', 'Lagos', 'Paris', 'Nagoya', 'Lima', 'Chicago', 'Kinshasa', 'Tianjin', 'Chennai', 'Bogota', 'Bengaluru', 'London', 'Taipei', 'Ho Chi Minh City (Saigon)', 'Dongguan', 'Hyderabad', 'Chengdu', 'Lahore', 'Johannesburg', 'Tehran', 'Essen', 'Bangkok', 'Hong Kong', 'Wuhan', 'Ahmedabad', 'Chongqung', 'Baghdad', 'Hangzhou', 'Toronto', 'Kuala Lumpur', 'Santiago', 'Dallas-Fort Worth', 'Quanzhou', 'Miami', 'Shenyang', 'Belo Horizonte', 'Philadelphia', 'Nanjing', 'Madrid', 'Houston', 'Xi\'an-Xianyang', 'Milan', 'Luanda', 'Pune', 'Singapore', 'Riyadh', 'Khartoum', 'Saint Petersburg', 'Atlanta', 'Surat', 'Washington', 'Bandung', 'Surabaya', 'Yangoon', 'Alexandria', 'Guadalajara', 'Harbin', 'Boston', 'Zhengzhou', 'Qingdao', 'Abidjan', 'Barcelona', 'Monterrey', 'Ankara', 'Suzhou', 'Phoenix-Mesa', 'Salvador', 'Porto Alegre', 'Rome', 'Accra', 'Sydney', 'Recife', 'Naples', 'Detroit', 'Dalian', 'Fuzhou', 'Medellin', 'San Francisco', 'Silicon Valley', 'Portland', 'Seattle', 'Austin', 'Denver', 'Boulder'] + +autoFocus = true # Not working right now, possibly a Treema bower thing. + +class SkillTagNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonSkills, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl + +class LinkNameNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonLinkNames, minLength: 0, delay: 0, autoFocus: autoFocus) + valEl + +class CityNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: commonCities, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl + +class CountryNode extends TreemaNode.nodeMap.string + buildValueForEditing: (valEl) -> + super(valEl) + valEl.find('input').autocomplete(source: countries, minLength: 1, delay: 0, autoFocus: autoFocus) + valEl diff --git a/app/views/account/profile_view.coffee b/app/views/account/profile_view.coffee index f61f7f1e2..854f9e985 100644 --- a/app/views/account/profile_view.coffee +++ b/app/views/account/profile_view.coffee @@ -1,36 +1,75 @@ View = require 'views/kinds/RootView' template = require 'templates/account/profile' User = require 'models/User' +JobProfileContactView = require 'views/modal/job_profile_contact_modal' module.exports = class ProfileView extends View id: "profile-view" template: template - loadingProfile: true + + events: + 'click #toggle-job-profile-approved': 'toggleJobProfileApproved' + 'keyup #job-profile-notes': 'onJobProfileNotesChanged' + 'click #contact-candidate': 'onContactCandidate' 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 + if links = @user.get('jobProfile')?.links + links = ($.extend(true, {}, link) for link in links) + link.icon = @iconForLink link for link in links + context.profileLinks = _.sortBy links, (link) -> not link.icon # icons first context + + afterRender: -> + super() + @updateProfileApproval() if me.isAdmin() + unless @user.get('jobProfile')?.projects?.length + @$el.find('.right-column').hide() + @$el.find('.middle-column').addClass('double-column') + + updateProfileApproval: -> + approved = @user.get 'jobProfileApproved' + @$el.find('.approved').toggle Boolean(approved) + @$el.find('.not-approved').toggle not approved + + toggleJobProfileApproved: -> + approved = not @user.get 'jobProfileApproved' + @user.set 'jobProfileApproved', approved + @user.save() + @updateProfileApproval() + + onJobProfileNotesChanged: (e) => + notes = @$el.find("#job-profile-notes").val() + @user.set 'jobProfileNotes', notes + @user.save() + + iconForLink: (link) -> + icons = [ + {icon: 'facebook', name: 'Facebook', domain: 'facebook.com', match: /facebook/i} + {icon: 'twitter', name: 'Twitter', domain: 'twitter.com', match: /twitter/i} + {icon: 'github', name: 'GitHub', domain: 'github.com', match: /github/i} + {icon: 'gplus', name: 'Google Plus', domain: 'plus.google.com', match: /(google|^g).?(\+|plus)/i} + {icon: 'linkedin', name: 'LinkedIn', domain: 'linkedin.com', match: /(google|^g).?(\+|plus)/i} + ] + for icon in icons + if (link.name.search(icon.match) isnt -1) or (link.link.search(icon.domain) isnt -1) + icon.url = "/images/pages/account/profile/icon_#{icon.icon}.png" + return icon + null + + onContactCandidate: (e) -> + @openModalView new JobProfileContactView recipientID: @user.id diff --git a/app/views/account/settings_view.coffee b/app/views/account/settings_view.coffee index df815b3cb..be4a79c59 100644 --- a/app/views/account/settings_view.coffee +++ b/app/views/account/settings_view.coffee @@ -5,6 +5,7 @@ forms = require('lib/forms') User = require('models/User') WizardSettingsView = require './wizard_settings_view' +JobProfileView = require './job_profile_view' module.exports = class SettingsView extends View id: 'account-settings-view' @@ -19,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() @@ -45,9 +35,19 @@ module.exports = class SettingsView extends View ) @chooseTab(location.hash.replace('#','')) - WizardSettingsView = new WizardSettingsView() - @listenTo(WizardSettingsView, 'change', @save) - @insertSubView WizardSettingsView + + wizardSettingsView = new WizardSettingsView() + @listenTo wizardSettingsView, 'change', @save + @insertSubView wizardSettingsView + + @jobProfileView = new JobProfileView() + @listenTo @jobProfileView, 'change', @save + @insertSubView @jobProfileView + + if me.schema().loaded + @buildPictureTreema() + else + @listenToOnce me, 'schema-loaded', @buildPictureTreema chooseTab: (category) -> id = "##{category}-pane" @@ -62,11 +62,9 @@ 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 c getSubscriptions: -> @@ -81,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() @@ -94,14 +116,14 @@ module.exports = class SettingsView extends View res = me.save() return unless res save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...')) - .addClass('btn-info').show().removeClass('btn-danger') + .removeClass('btn-danger').addClass('btn-success').show() res.error -> errors = JSON.parse(res.responseText) forms.applyErrorsToForm(@$el, errors) - save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-info').addClass('btn-danger') + save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-success').addClass('btn-danger', 500) res.success (model, response, options) -> - save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-info') + save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-success', 500) grabData: -> @grabPasswordData() @@ -120,12 +142,22 @@ 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 permissions = [] permissions.push 'admin' if adminCheckbox.prop('checked') me.set('permissions', permissions) + + jobProfile = me.get('jobProfile') ? {} + updated = false + for key, val of @jobProfileView.getData() + updated = updated or jobProfile[key] isnt val + jobProfile[key] = val + if updated + jobProfile.updated = (new Date()).toISOString() + me.set 'jobProfile', jobProfile diff --git a/app/views/admin/level_sessions_view.coffee b/app/views/admin/level_sessions_view.coffee index e66fefc2f..c00fcc2fe 100644 --- a/app/views/admin/level_sessions_view.coffee +++ b/app/views/admin/level_sessions_view.coffee @@ -16,12 +16,12 @@ module.exports = class LevelSessionsView extends View @getLevelSessions() getLevelSessions: -> - @sessions = new LevelSessionCollection + @sessions = new LevelSessionCollection() @sessions.fetch() - @listenTo(@sessions, 'all', @render) + @listenToOnce @sessions, 'all', @render getRenderData: => c = super() c.sessions = @sessions.models c.moment = moment - c \ No newline at end of file + c diff --git a/app/views/editor/level/home.coffee b/app/views/editor/level/home.coffee index ffb1a5ac9..247c9d3ff 100644 --- a/app/views/editor/level/home.coffee +++ b/app/views/editor/level/home.coffee @@ -1,8 +1,8 @@ SearchView = require 'views/kinds/SearchView' -module.exports = class ThangTypeHomeView extends SearchView +module.exports = class EditorSearchView extends SearchView id: "editor-level-home-view" modelLabel: 'Level' model: require 'models/Level' modelURL: '/db/level' - tableTemplate: require 'templates/editor/level/table' \ No newline at end of file + tableTemplate: require 'templates/editor/level/table' diff --git a/app/views/employers_view.coffee b/app/views/employers_view.coffee index d5e2eeb2a..a43bbf70a 100644 --- a/app/views/employers_view.coffee +++ b/app/views/employers_view.coffee @@ -1,6 +1,90 @@ View = require 'views/kinds/RootView' template = require 'templates/employers' +app = require 'application' +User = require 'models/User' +CocoCollection = require 'models/CocoCollection' +employerSignupTemplate = require 'templates/modal/employer_signup_modal' +ModalView = require 'views/kinds/ModalView' + +class CandidatesCollection extends CocoCollection + url: '/db/user/x/candidates' + model: User module.exports = class EmployersView extends View id: "employers-view" template: template + + events: + 'click tbody tr': 'onCandidateClicked' + + constructor: (options) -> + super options + @getCandidates() + + afterRender: -> + super() + @sortTable() if @candidates.models.length + + getRenderData: -> + c = super() + c.candidates = @candidates.models + c.moment = moment + c + + getCandidates: -> + @candidates = new CandidatesCollection() + @candidates.fetch() + # Re-render when we have fetched them, but don't wait and show a progress bar while loading. + @listenToOnce @candidates, 'all', @render + + 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 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 + + # call the tablesorter plugin and apply the uitheme widget + @$el.find(".tablesorter").tablesorter( + theme: "bootstrap" + widthFixed: true + headerTemplate: "{content} {icon}" + # 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" + ] + 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" + ] + # reset filters button + filter_reset: ".reset" + ) + + onCandidateClicked: (e) -> + id = $(e.target).closest('tr').data('candidate-id') + if id + url = "/account/profile/#{id}" + app.router.navigate url, {trigger: true} + else + employerSignupModal = new ModalView() + employerSignupModal.template = employerSignupTemplate + @openModalView employerSignupModal diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index 3ea8204a3..360913182 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/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index f49eb4994..5f93924c3 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -8,7 +8,7 @@ class SearchCollection extends Backbone.Collection @url = "#{modelURL}/search?project=true" @url += "&term=#{term}" if @term -module.exports = class ThangTypeHomeView extends View +module.exports = class SearchView extends View template: template className: 'search-view' diff --git a/app/views/modal/contact_modal.coffee b/app/views/modal/contact_modal.coffee index dd3f0c40e..2e313d44a 100644 --- a/app/views/modal/contact_modal.coffee +++ b/app/views/modal/contact_modal.coffee @@ -6,16 +6,15 @@ forms = require 'lib/forms' contactSchema = additionalProperties: false + required: ['email', 'message'] properties: email: - required: true type: 'string' maxLength: 100 minLength: 1 format: 'email' message: - required: true type: 'string' minLength: 1 diff --git a/app/views/modal/job_profile_contact_modal.coffee b/app/views/modal/job_profile_contact_modal.coffee new file mode 100644 index 000000000..236301454 --- /dev/null +++ b/app/views/modal/job_profile_contact_modal.coffee @@ -0,0 +1,41 @@ +ContactView = require 'views/modal/contact_modal' +template = require 'templates/modal/job_profile_contact' + +forms = require 'lib/forms' +{sendContactMessage} = require 'lib/contact' + +contactSchema = + additionalProperties: false + required: ['email', 'message'] + properties: + email: + type: 'string' + maxLength: 100 + minLength: 1 + format: 'email' + + subject: + type: 'string' + minLength: 1 + + message: + type: 'string' + minLength: 1 + + recipientID: + type: 'string' + minLength: 1 + +module.exports = class JobProfileContactView extends ContactView + id: "job-profile-contact-modal" + template: template + + contact: -> + forms.clearFormAlerts @$el + contactMessage = forms.formToObject @$el + contactMessage.recipientID = @options.recipientID + res = tv4.validateMultiple contactMessage, contactSchema + return forms.applyErrorsToForm @$el, res.errors unless res.valid + contactMessage.message += '\n\n\n\n[CodeCombat says: please let us know if you end up accepting this job. Thanks!]' + window.tracker?.trackEvent 'Sent Job Profile Message', message: contactMessage + sendContactMessage contactMessage, @$el diff --git a/app/views/play/ladder/play_modal.coffee b/app/views/play/ladder/play_modal.coffee index 2272be191..43286b1d0 100644 --- a/app/views/play/ladder/play_modal.coffee +++ b/app/views/play/ladder/play_modal.coffee @@ -11,6 +11,7 @@ module.exports = class LadderPlayModal extends View closeButton: true startsLoading: true @shownTutorialButton: false + tutorialLevelExists: null events: 'click #skip-tutorial-button': 'hideTutorialButtons' @@ -21,7 +22,7 @@ module.exports = class LadderPlayModal extends View @otherTeam = if team is 'ogres' then 'humans' else 'ogres' @startLoadingChallengersMaybe() @wizardType = ThangType.loadUniversalWizard() - + # PART 1: Load challengers from the db unless some are in the matches startLoadingChallengersMaybe: -> @@ -58,9 +59,11 @@ module.exports = class LadderPlayModal extends View # PART 4: Render finishRendering: -> - @startsLoading = false - @render() - @maybeShowTutorialButtons() + @checkTutorialLevelExists (exists) => + @tutorialLevelExists = exists + @startsLoading = false + @render() + @maybeShowTutorialButtons() getRenderData: -> ctx = super() @@ -94,7 +97,7 @@ module.exports = class LadderPlayModal extends View ctx maybeShowTutorialButtons: -> - return if @session or LadderPlayModal.shownTutorialButton + return if @session or LadderPlayModal.shownTutorialButton or not @tutorialLevelExists @$el.find('#normal-view').addClass('secret') @$el.find('.modal-header').addClass('secret') @$el.find('#noob-view').removeClass('secret') @@ -105,6 +108,17 @@ module.exports = class LadderPlayModal extends View @$el.find('.modal-header').removeClass('secret') @$el.find('#noob-view').addClass('secret') + checkTutorialLevelExists: (cb) -> + levelID = @level.get('slug') or @level.id + tutorialLevelID = "#{levelID}-tutorial" + success = => cb true + failure = => cb false + $.ajax + type: "GET" + url: "/db/level/#{tutorialLevelID}/exists" + success: success + error: failure + # Choosing challengers getChallengers: -> diff --git a/app/views/play/level/level_loading_view.coffee b/app/views/play/level/level_loading_view.coffee index 32da3377e..8035c5d14 100644 --- a/app/views/play/level/level_loading_view.coffee +++ b/app/views/play/level/level_loading_view.coffee @@ -8,7 +8,7 @@ module.exports = class LevelLoadingView extends View subscriptions: 'level-loader:progress-changed': 'onLevelLoaderProgressChanged' - + afterRender: -> @$el.find('.tip.rare').remove() if _.random(1, 10) < 9 tips = @$el.find('.tip').addClass('to-remove') @@ -34,6 +34,7 @@ module.exports = class LevelLoadingView extends View reallyUnveil: => return if @destroyed + @$el.addClass 'unveiled' loadingDetails = @$el.find('.loading-details') duration = parseFloat loadingDetails.css 'transition-duration' loadingDetails.css 'top', -loadingDetails.outerHeight(true) diff --git a/app/views/play/level/tome/spell.coffee b/app/views/play/level/tome/spell.coffee index a0cb680cb..832d59623 100644 --- a/app/views/play/level/tome/spell.coffee +++ b/app/views/play/level/tome/spell.coffee @@ -90,6 +90,8 @@ module.exports = class Spell problems: jshint_W040: {level: "ignore"} jshint_W030: {level: "ignore"} # aether_NoEffect instead + jshint_W038: {level: "ignore"} #eliminates hoisting problems + jshint_W091: {level: "ignore"} #eliminates more hoisting problems aether_MissingThis: {level: (if thang.requiresThis then 'error' else 'warning')} language: aceConfig.language ? 'javascript' functionName: @name diff --git a/bower.json b/bower.json index 84e10d876..5995b14c3 100644 --- a/bower.json +++ b/bower.json @@ -24,7 +24,7 @@ "test" ], "dependencies": { - "jquery": "~2.0.3", + "jquery": "~2.1.0", "lodash": "~2.4.1", "backbone": "1.1.0", "jquery-mousewheel": "~3.1.9", @@ -37,8 +37,15 @@ "firebase": "~1.0.2", "catiline": "~2.9.3", "d3": "~3.4.4", +<<<<<<< HEAD "jsondiffpatch": "~0.1.5", "nanoscroller": "~0.8.0" +======= + "nanoscroller": "~0.8.0", + "jquery.tablesorter": "~2.15.13", + "treema": "~0.0.1", + "bootstrap": "~3.1.1" +>>>>>>> master }, "overrides": { "backbone": { @@ -53,8 +60,26 @@ "underscore.string": { "main": "lib/underscore.string.js" }, +<<<<<<< HEAD "jsondiffpatch": { "main": ["build/bundle-full.js", "build/formatters.js", "src/formatters/html.css"] +======= + "jquery.tablesorter": { + "main": [ + "js/jquery.tablesorter.js", + "js/jquery.tablesorter.widgets.js", + "css/theme.bootstrap.css" + ] + }, + "bootstrap": { + "main": [ + "./dist/js/bootstrap.js", + "./dist/fonts/glyphicons-halflings-regular.eot", + "./dist/fonts/glyphicons-halflings-regular.svg", + "./dist/fonts/glyphicons-halflings-regular.ttf", + "./dist/fonts/glyphicons-halflings-regular.woff" + ] +>>>>>>> master } } } diff --git a/config.coffee b/config.coffee index 10daa6b93..176701755 100644 --- a/config.coffee +++ b/config.coffee @@ -41,21 +41,11 @@ exports.config = 'test/javascripts/test-vendor.js': /^test[\/\\](?=vendor)/ order: before: [ - 'bower_components/jquery/jquery.js' + 'bower_components/jquery/dist/jquery.js' 'bower_components/lodash/dist/lodash.js' 'bower_components/backbone/backbone.js' # Twitter Bootstrap jquery plugins - 'vendor/scripts/bootstrap/transition.js' - 'vendor/scripts/bootstrap/affix.js' - 'vendor/scripts/bootstrap/alert.js' - 'vendor/scripts/bootstrap/button.js' - 'vendor/scripts/bootstrap/carousel.js' - 'vendor/scripts/bootstrap/collapse.js' - 'vendor/scripts/bootstrap/dropdown.js' - 'vendor/scripts/bootstrap/modal.js' - 'vendor/scripts/bootstrap/scrollspy.js' - 'vendor/scripts/bootstrap/tab.js' - 'vendor/scripts/bootstrap/tooltip.js' + 'bower_components/bootstrap/dist/bootstrap.js' # CreateJS dependencies 'vendor/scripts/easeljs-NEXT.combined.js' 'vendor/scripts/preloadjs-NEXT.combined.js' diff --git a/package.json b/package.json index 3e2552f1c..ee0dcc201 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "css-brunch": "> 1.0 < 1.8", "jade-brunch": "> 1.0 < 1.8", "uglify-js-brunch": "~1.7.4", - "clean-css-brunch": "> 1.0 < 1.8", "auto-reload-brunch": "> 1.0 < 1.8", "brunch": "~1.7.4", "jasmine-node": "1.13.x", diff --git a/scripts/windows/coco-dev-setup/batch/config/config.coco b/scripts/windows/coco-dev-setup/batch/config/config.coco index b0bf33e15..e2d7570f5 100755 --- a/scripts/windows/coco-dev-setup/batch/config/config.coco +++ b/scripts/windows/coco-dev-setup/batch/config/config.coco @@ -1,6 +1,6 @@ - 2.1 + 3.3 GlenDC CodeCombat.com 2013-2014 https://github.com/codecombat/codecombat.git diff --git a/scripts/windows/coco-dev-setup/batch/config/downloads.coco b/scripts/windows/coco-dev-setup/batch/config/downloads.coco index e65d0f7ee..1d57fbb71 100755 --- a/scripts/windows/coco-dev-setup/batch/config/downloads.coco +++ b/scripts/windows/coco-dev-setup/batch/config/downloads.coco @@ -20,18 +20,18 @@ - http://fastdl.mongodb.org/win32/mongodb-win32-i386-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.0.zip - http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.6.0.zip - http://fastdl.mongodb.org/win32/mongodb-win32-i386-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.0.zip - http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2.5.4.zip + https://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2.6.0.zip - \ No newline at end of file + diff --git a/scripts/windows/coco-dev-setup/batch/configuration.exe b/scripts/windows/coco-dev-setup/batch/configuration.exe new file mode 100755 index 000000000..28177aeab Binary files /dev/null and b/scripts/windows/coco-dev-setup/batch/configuration.exe differ diff --git a/scripts/windows/coco-dev-setup/batch/localisation/de.coco b/scripts/windows/coco-dev-setup/batch/localisation/de.coco index 075c10991..e8af90621 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/de.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/de.coco @@ -53,6 +53,11 @@ Willst du das Repository via SSH auschecken? + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + Installing bower, brunch, nodemon and sendwithus... Installing bower packages... diff --git a/scripts/windows/coco-dev-setup/batch/localisation/en.coco b/scripts/windows/coco-dev-setup/batch/localisation/en.coco index 081f6672a..46464bb55 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/en.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/en.coco @@ -60,6 +60,11 @@ Thank you... Configuring your local repistory right now... + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + Installing bower, brunch, nodemon and sendwithus... Installing bower packages... diff --git a/scripts/windows/coco-dev-setup/batch/localisation/fr.coco b/scripts/windows/coco-dev-setup/batch/localisation/fr.coco index 6c3902376..d2a0d67ae 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/fr.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/fr.coco @@ -53,6 +53,11 @@ Do you want to checkout the repository via ssh? + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + Installing bower, brunch, nodemon and sendwithus... Installing bower packages... diff --git a/scripts/windows/coco-dev-setup/batch/localisation/languages.coco b/scripts/windows/coco-dev-setup/batch/localisation/languages.coco index a267d65d0..a98092066 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/languages.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/languages.coco @@ -2,6 +2,5 @@ en nl de fr -zh zh-HANT zh-HANS \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/localisation/nl.coco b/scripts/windows/coco-dev-setup/batch/localisation/nl.coco index 294b1ae89..39b95ef9b 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/nl.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/nl.coco @@ -53,6 +53,11 @@ Wil je het git project downloaden via ssh? + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + Installing bower, brunch, nodemon and sendwithus... Installing bower packages... diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco index 86711ea66..410d032f7 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANS.coco @@ -53,6 +53,11 @@ 你是否想使用 ssh 来检出(checkout)库(repository)? + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + 正在安装 bower, brunch, nodemon 和 sendwithus... 正在用 bower 安装依赖包... diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco index 61e7ff3d6..8c242effa 100755 --- a/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco +++ b/scripts/windows/coco-dev-setup/batch/localisation/zh-HANT.coco @@ -53,6 +53,11 @@ Do you want to checkout the repository via ssh? + + The installation of your local environment was succesfull! + You can now close this setup. + After that, you should open the configuration setup to automaticly configure your environment... + Installing bower, brunch, nodemon and sendwithus... Installing bower packages... diff --git a/scripts/windows/coco-dev-setup/batch/localisation/zh.coco b/scripts/windows/coco-dev-setup/batch/localisation/zh.coco deleted file mode 100755 index 6c7c8dd2f..000000000 --- a/scripts/windows/coco-dev-setup/batch/localisation/zh.coco +++ /dev/null @@ -1,84 +0,0 @@ - - - - 中文 - Chinese - From now on we'll send our feedback in English! - - - - -bit computer detected. - The operating system - was detected. - We don't support Windows XP, installation cancelled. - - - Have you already installed all the software needed for CodeCombat? - We recommand that you reply negative in case you're not sure. - Skipping the installation of the software... - CodeCombat couldn't be developed without third-party software. - That's why you'll need to install this software, - in order to start contributing to our community. - Cancel the installation if you already have the application. - Make sure to select the option that adds the application to your Windows Path, if the option is available. - Do you already have the latest version of - installed? - is downloading... - is installing... - is unzipping... - is cleaning... - Please define the full path where mongodb should be installed - - - - - CodeCombat is opensource, like you already know. - All our sourcecode can be found online at Github. - You can choose to do the entire Git setup yourself. - However we recommend that you instead let us handle it instead. - - - Do you want to do the Local Git setup manually yourself? - Make sure you have correctly setup your repository before processing. - Do not close this window please. - When you're ready, press any key to continue... - - - Please give the full path of your CodeCombat git repository: - Please enter the full path where you want to install your CodeCombat environment - This installation requires Git Bash. - Git bash is by default installed at 'C:\Program Files (x86)\Git'. - Git bash is by default installed at 'C:\Program Files\Git'. - Please enter the full path where git bash is installed or just press enter if it's in the default location - Do you want to checkout the repository via ssh? - - - - Installing bower, brunch, nodemon and sendwithus... - Installing bower packages... - Installing sass... - Installing npm... - Starting brunch.... - Setting up a MongoDB database for you... - Downloading the last version of the CodeCombat database... - - Don't close! - - - That path already exists, are you sure you want to overwrite it? - That path doesn't exist. Please try again... - - - The setup of the CodeCombat Dev. Environment was succesfull. - Thank you already for your contribution and see you soon. - Do you want to read the README for more information? - - - From now on you can start the dev. environment at - the touch of a single mouse click. - 1) Just double click - and let the environment start up. - 2) Now just open 'localhost:3000' in your prefered browser. - That's it, you're now ready to start working on CodeCombat! - - \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/setup_p3.bat b/scripts/windows/coco-dev-setup/batch/scripts/configuration.bat similarity index 93% rename from scripts/windows/coco-dev-setup/batch/scripts/setup_p3.bat rename to scripts/windows/coco-dev-setup/batch/scripts/configuration.bat index 6eee20e91..f70748a7a 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/setup_p3.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/configuration.bat @@ -1,6 +1,8 @@ @echo off setlocal EnableDelayedExpansion +call read_cache + call configuration_cmd call npm_and_brunch_setup diff --git a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat index 5fbca9f4e..3408a13e6 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/download_and_install_app.bat @@ -21,7 +21,7 @@ call get_extension %2 download_extension call get_local_text install_process_downloading install process downloading echo %1 !install_process_downloading! set "install_file=!temp_directory!%1.!download_extension!" -%curl_app% -k %2 -o !install_file! +start /wait cmd.exe /c "TITLE %1 !install_process_downloading! && %curl_app% -k -m 10800 --retry 100 -o !install_file! %2" if "%download_extension%"=="zip" ( set "package_path=!temp_directory!%1\" @@ -49,9 +49,9 @@ if "%download_extension%"=="zip" ( md %mongodb_path% %systemroot%\System32\xcopy !package_path!!mongodb_original_directory! !mongodb_path! /r /h /s /e /y - - setx path ";!mongodb_path!\bin" - + + call set_environment_var "!mongodb_path!\bin" + goto:clean_up ) diff --git a/scripts/windows/coco-dev-setup/batch/scripts/get_cache_var.bat b/scripts/windows/coco-dev-setup/batch/scripts/get_cache_var.bat new file mode 100755 index 000000000..d7eebbcfe --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/get_cache_var.bat @@ -0,0 +1,3 @@ +for /F "delims=" %%F in ('call run_script .\\get_var.ps1 ..\\config\\cache.coco %1') do ( + set "%1=%%F" +) \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat b/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat index ebe97a08b..6436aacb0 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/github_setup.bat @@ -136,8 +136,8 @@ goto:eof set cur_dir=%CD% cd !repository_path!\coco - git remote rm origin - git remote add origin https://!git_username!:!git_password!@github.com/!git_username!/codecombat.git + "%git_app_path%" remote rm origin + "%git_app_path%" remote add origin https://!git_username!:!git_password!@github.com/!git_username!/codecombat.git cd !cur_dir! diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_automatic_script.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_automatic_script.bat index b548cb6b8..7048f0180 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/nab_automatic_script.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_automatic_script.bat @@ -2,6 +2,6 @@ call print_dashed_seperator call get_local_text npm_script npm script echo %npm_script% -echo start cmd.exe cmd /c "TITLE CodeCombat.com - nodemon server & call nodemon -w server -w server_config.js">%~1\SCOCODE.bat -echo start cmd.exe cmd /c "TITLE CodeCombat.com - brunch - live compiler & call brunch w">>%~1\SCOCODE.bat -echo start cmd.exe cmd /c "TITLE CodeCombat.com - mongodb database & mongod --setParameter textSearchEnabled=true --dbpath %~2">>%~1\SCOCODE.bat \ No newline at end of file +echo start cmd.exe cmd /c "TITLE CodeCombat.com - mongodb database & mongod --setParameter textSearchEnabled=true --dbpath %~2">%~1\SCOCODE.bat +echo start cmd.exe cmd /c "TITLE CodeCombat.com - nodemon server & nodemon index.js">>%~1\SCOCODE.bat +echo start cmd.exe cmd /c "TITLE CodeCombat.com - brunch - live compiler & brunch w">>%~1\SCOCODE.bat \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/nab_install_mongodb.bat b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_mongodb.bat index 8f22c4dee..84d9a3691 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/nab_install_mongodb.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/nab_install_mongodb.bat @@ -20,7 +20,7 @@ cd /D %~1 start cmd /c "TITLE MongoDB - %npm_close% & mongod --setParameter textSearchEnabled=true --dbpath %~1" -%work_directory%\%curl_app% -k %database_backup% -o dump.tar.gz +start /wait cmd.exe /c "TITLE downloading database backup... && %work_directory%\%curl_app% -k -m 10800 --retry 100 -o dump.tar.gz %database_backup%" start /wait cmd /c "%work_directory%\%zu_app% e dump.tar.gz && del dump.tar.gz && %work_directory%\%zu_app% x dump.tar && del dump.tar" @@ -32,4 +32,6 @@ call %work_directory%\print_dashed_seperator taskkill /F /fi "IMAGENAME eq mongod.exe" +del /F %~1\mongod.lock + cd /D %work_directory% \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/read_cache.bat b/scripts/windows/coco-dev-setup/batch/scripts/read_cache.bat new file mode 100755 index 000000000..13f96330d --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/read_cache.bat @@ -0,0 +1,2 @@ +call get_cache_var language_id +call get_cache_var repository_path \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/set_environment_var.bat b/scripts/windows/coco-dev-setup/batch/scripts/set_environment_var.bat new file mode 100755 index 000000000..05d5362a5 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/set_environment_var.bat @@ -0,0 +1 @@ +setx path ";%~1" \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/setup_p2.bat b/scripts/windows/coco-dev-setup/batch/scripts/setup_p2.bat index c79fb2e61..f6e0d3b21 100755 --- a/scripts/windows/coco-dev-setup/batch/scripts/setup_p2.bat +++ b/scripts/windows/coco-dev-setup/batch/scripts/setup_p2.bat @@ -5,6 +5,16 @@ call configuration_cmd call github_setup -start cmd /c "setup_p3.bat" +call write_cache + +call get_local_text switch_install switch install +call get_local_text switch_close switch close +call get_local_text switch_open switch open + +echo %switch_install% +echo %switch_close% +echo. + +set /p "dummy=%switch_open%" endlocal \ No newline at end of file diff --git a/scripts/windows/coco-dev-setup/batch/scripts/write_cache.bat b/scripts/windows/coco-dev-setup/batch/scripts/write_cache.bat new file mode 100755 index 000000000..2e46ca4c3 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/scripts/write_cache.bat @@ -0,0 +1,10 @@ +set "cache=..\\config\\cache.coco" + +echo ^>%cache% + +echo ^>>%cache% + +echo ^%language_id%^>>%cache% +echo ^%repository_path%^>>%cache% + +echo ^>>%cache% \ No newline at end of file diff --git a/server/commons/schemas.coffee b/server/commons/schemas.coffee index e81f49587..a98c4a9b5 100644 --- a/server/commons/schemas.coffee +++ b/server/commons/schemas.coffee @@ -8,6 +8,8 @@ combine = (base, ext) -> return base unless ext? return _.extend(base, ext) +urlPattern = '^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-‌​\.\?\,\'\/\\\+&%\$#_=]*)?$' + # Common schema properties me.object = (ext, props) -> combine {type: 'object', additionalProperties: false, properties: props or {}}, ext me.array = (ext, items) -> combine {type: 'array', items: items or {}}, ext @@ -16,6 +18,7 @@ me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext) me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient me.objectId = (ext) -> schema = combine({type: ['object', 'string'] }, ext) +me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) PointSchema = me.object {title: "Point", description: "An {x, y} coordinate point.", format: "point2d", required: ["x", "y"]}, x: {title: "x", description: "The x coordinate.", type: "number", "default": 15} diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index af9392bf2..15c75b521 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -38,7 +38,12 @@ LevelHandler = class LevelHandler extends Handler return @getLeaderboardFacebookFriends(req, res, args[0]) if args[1] is 'leaderboard_facebook_friends' return @getLeaderboardGPlusFriends(req, res, args[0]) if args[1] is 'leaderboard_gplus_friends' return @getHistogramData(req, res, args[0]) if args[1] is 'histogram_data' +<<<<<<< HEAD super(arguments...) +======= + return @checkExistence(req, res, args[0]) if args[1] is 'exists' + return @sendNotFoundError(res) +>>>>>>> master fetchLevelByIDAndHandleErrors: (id, req, res, callback) -> @getDocumentForIdOrSlug id, (err, level) => @@ -130,7 +135,23 @@ LevelHandler = class LevelHandler extends Handler if err? then return @sendDatabaseError res, err valueArray = _.pluck data, "totalScore" @sendSuccess res, valueArray - + + checkExistence: (req, res, slugOrID) -> + findParameters = {} + if Handler.isID slugOrID + findParameters["_id"] = slugOrID + else + findParameters["slug"] = slugOrID + selectString = 'original version.major permissions' + query = Level.findOne(findParameters) + .select(selectString) + .lean() + + query.exec (err, level) => + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless level? + res.send({"exists":true}) + res.end() getLeaderboard: (req, res, id) -> sessionsQueryParameters = @makeLeaderboardQueryParameters(req, id) diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee index 51aaa78fc..1ceb8d196 100644 --- a/server/routes/contact.coffee +++ b/server/routes/contact.coffee @@ -1,26 +1,38 @@ config = require '../../server_config' log = require 'winston' mail = require '../commons/mail' +User = require '../users/User' module.exports.setup = (app) -> app.post '/contact', (req, res) -> return res.end() unless req.user log.info "Sending mail from #{req.body.email} saying #{req.body.message}" if config.isProduction - options = createMailOptions req.body.email, req.body.message, req.user - mail.transport.sendMail options, (error, response) -> - if error - log.error "Error sending mail: #{error.message or error}" - else - log.info "Mail sent successfully. Response: #{response.message}" + createMailOptions req.body.email, req.body.message, req.user, req.body.recipientID, req.body.subject, (options) -> + mail.transport.sendMail options, (error, response) -> + if error + log.error "Error sending mail: #{error.message or error}" + else + log.info "Mail sent successfully. Response: #{response.message}" return res.end() -createMailOptions = (sender, message, user) -> +createMailOptions = (sender, message, user, recipientID, subject, done) -> # TODO: use email templates here options = from: config.mail.username to: config.mail.username replyTo: sender - subject: "[CodeCombat] Feedback - #{sender}" + subject: "[CodeCombat] #{subject ? ('Feedback - ' + sender)}" text: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}" - #html: message.replace '\n', '
\n' \ No newline at end of file + #html: message.replace '\n', '
\n' + + if recipientID and (user.isAdmin() or ('employer' in (user.permissions ? []))) + User.findById(recipientID, 'email').exec (err, document) -> + if err + log.error "Error looking up recipient to email from #{recipientID}: #{err}" if err + else + options.bcc = options.to + options.to = document.get('email') + done options + else + done options diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index b1a4b6ba3..9023209d2 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -37,7 +37,7 @@ getTimeFromDaysAgo = (now, daysAgo) -> t = now - 86400 * 1000 * daysAgo - LADDER_PREGAME_INTERVAL isRequestFromDesignatedCronHandler = (req, res) -> - requestIP = req.headers['x-forwarded-for'][0] + requestIP = req.headers['x-forwarded-for']?.replace(" ","").split(",")[0] if requestIP isnt config.mail.cronHandlerPublicIP and requestIP isnt config.mail.cronHandlerPrivateIP console.log "RECEIVED REQUEST FROM IP #{requestIP}(headers indicate #{req.headers['x-forwarded-for']}" console.log "UNAUTHORIZED ATTEMPT TO SEND TRANSACTIONAL LADDER EMAIL THROUGH CRON MAIL HANDLER" diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index b43f3ed6d..0ef407690 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -9,12 +9,16 @@ 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 = [ 'permissions', 'email', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'music', 'volume', 'aceConfig' ] +candidateProperties = [ + 'jobProfile', 'jobProfileApproved', 'jobProfileNotes' +] UserHandler = class UserHandler extends Handler modelClass: User @@ -23,7 +27,7 @@ UserHandler = class UserHandler extends Handler 'name', 'photoURL', 'password', 'anonymous', 'wizardColor1', 'volume', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'emailSubscriptions', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', - 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel' + 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile' ] jsonSchema: schema @@ -32,21 +36,19 @@ UserHandler = class UserHandler extends Handler super(arguments...) @editableProperties.push('permissions') unless config.isProduction + getEditableProperties: (req, document) -> + props = super req, document + props.push 'jobProfileApproved', 'jobProfileNotes' if req.user.isAdmin() + props + formatEntity: (req, document) -> return null unless document? obj = document.toObject() delete obj[prop] for prop in serverProperties - includePrivates = req.user and (req.user?.isAdmin() or req.user?._id.equals(document._id)) + includePrivates = req.user and (req.user.isAdmin() or req.user._id.equals(document._id)) delete obj[prop] for prop in privateProperties unless includePrivates - - # emailHash is used by gravatar - hash = crypto.createHash('md5') - if document.get('email') - hash.update(_.trim(document.get('email')).toLowerCase()) - else - hash.update(@_id+'') - obj.emailHash = hash.digest('hex') - + includeCandidate = includePrivates or (obj.jobProfileApproved and req.user and ('employer' in (req.user.permissions ? []))) + delete obj[prop] for prop in candidateProperties unless includeCandidate return obj waterfallFunctions: [ @@ -115,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) -> @@ -171,7 +173,12 @@ UserHandler = class UserHandler extends Handler return @getNamesByIds(req, res) if args[1] is 'names' return @nameToID(req, res, args[0]) if args[1] is 'nameToID' return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions' +<<<<<<< HEAD super(arguments...) +======= + return @getCandidates(req, res) if args[1] is 'candidates' + return @sendNotFoundError(res) +>>>>>>> master agreeToCLA: (req, res) -> return @sendUnauthorizedError(res) unless req.user @@ -191,9 +198,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) -> @@ -205,8 +214,46 @@ 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) -> + authorized = req.user.isAdmin() or ('employer' in req.user.get('permissions')) + since = (new Date((new Date()) - 2 * 30.4 * 86400 * 1000)).toISOString() + #query = {'jobProfileApproved': true, 'jobProfile.active': true, 'jobProfile.updated': {$gt: since}} + query = {'jobProfile.active': true, 'jobProfile.updated': {$gt: since}} # testing + query.jobProfileApproved = true unless req.user.isAdmin() + selection = 'jobProfile' + selection += ' email' if authorized + selection += ' jobProfileApproved' if req.user.isAdmin() + User.find(query).select(selection).exec (err, documents) => + return @sendDatabaseError(res, err) if err + candidates = (@formatCandidate(authorized, doc) for doc in documents) + @sendSuccess(res, candidates) + + formatCandidate: (authorized, document) -> + fields = if authorized then ['jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['jobProfile'] + obj = _.pick document.toObject(), fields + obj.photoURL ||= obj.jobProfile.photoURL if authorized + 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 18d526de5..c7de194e3 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 @@ -56,6 +55,44 @@ UserSchema = c.object {}, simulatedBy: {type: 'integer', minimum: 0, default: 0} simulatedFor: {type: 'integer', minimum: 0, default: 0} + jobProfile: c.object {title: 'Job Profile', required: ['lookingFor', 'jobTitle', 'active', 'name', 'city', 'country', 'skills', 'experience', 'shortDescription', 'longDescription', 'visa', 'work', 'education', 'projects', 'links']}, + lookingFor: {title: 'Looking For', type: 'string', enum: ['Full-time', 'Part-time', 'Remote', 'Contracting', 'Internship'], default: 'Full-time', description: 'What kind of developer position do you want?'} + jobTitle: {type: 'string', maxLength: 50, title: 'Desired Job Title', description: 'What role are you looking for? Ex.: "Full Stack Engineer", "Front-End Developer", "iOS Developer"', default: 'Software Developer'} + active: {title: 'Active', type: 'boolean', description: 'Want interview offers right now?'} + updated: c.date {title: 'Last Updated', description: 'How fresh your profile appears to employers. The fresher, the better. Profiles go inactive after 30 days.'} + name: c.shortString {title: 'Name', description: 'Name you want employers to see, like "Nick Winter".'} + city: c.shortString {title: 'City', description: 'City you want to work in (or live in now), like "San Francisco" or "Lubbock, TX".', default: 'Defaultsville, CA', format: 'city'} + country: c.shortString {title: 'Country', description: 'Country you want to work in (or live in now), like "USA" or "France".', default: 'USA', format: 'country'} + skills: c.array {title: 'Skills', description: 'Tag relevant developer skills in order of proficiency. Employers will see the first five at a glance.', default: ['javascript'], minItems: 1, maxItems: 30, uniqueItems: true}, + {type: 'string', minLength: 1, maxLength: 20, description: 'Ex.: "objective-c", "mongodb", "rails", "android", "javascript"', format: 'skill'} + experience: {type: 'integer', title: 'Years of Experience', minimum: 0, description: 'How many years of professional experience (getting paid) developing software do you have?'} + shortDescription: {type: 'string', maxLength: 140, title: 'Short Description', description: 'Who are you, and what are you looking for? 140 characters max.', default: 'Programmer seeking to build great software.'} + longDescription: {type: 'string', maxLength: 600, title: 'Description', description: 'Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max.', format: 'markdown', default: '* I write great code.\n* You need great code?\n* Great!'} + visa: c.shortString {title: 'US Work Status', description: 'Are you authorized to work in the US, or do you need visa sponsorship?', enum: ['Authorized to work in the US', 'Need visa sponsorship'], default: 'Authorized to work in the US'} + work: c.array {title: 'Work Experience', description: 'List your relevant work experience, most recent first.'}, + c.object {title: 'Job', description: 'Some work experience you had.', required: ['employer', 'role', 'duration']}, + employer: c.shortString {title: 'Employer', description: 'Name of your employer.'} + role: c.shortString {title: 'Job Title', description: 'What was your job title or role?'} + duration: c.shortString {title: 'Duration', description: 'When did you hold this gig? Ex.: "Feb 2013 - present".'} + education: c.array {title: 'Education', description: 'List your academic ordeals.'}, + c.object {title: 'Ordeal', description: 'Some education that befell you.', required: ['school', 'degree', 'duration']}, + school: c.shortString {title: 'School', description: 'Name of your school.'} + degree: c.shortString {title: 'Degree', description: 'What was your degree and field of study? Ex. Ph.D. Human-Computer Interaction (incomplete)'} + duration: c.shortString {title: 'Dates', description: 'When? Ex.: "Aug 2004 - May 2008".'} + projects: c.array {title: 'Projects', description: 'Highlight your projects to amaze employers.'}, + c.object {title: 'Project', description: 'A project you created.', required: ['name', 'description', 'picture'], default: {name: 'My Project', description: 'A project I worked on.', link: 'http://example.com', picture: ''}}, + name: c.shortString {title: 'Project Name', description: 'What was the project called?', default: 'My Project'} + description: {type: 'string', title: 'Description', description: 'Briefly describe the project.', maxLength: 400, default: 'A project I worked on.', format: 'markdown'} + picture: {type: 'string', title: 'Picture', format: 'image-file', description: 'Upload a 230x115px or larger image showing off the project.'} + link: c.url {title: 'Link', description: 'Link to the project.', default: 'http://example.com'} + links: c.array {title: 'Personal and Social Links', description: 'Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog.'}, + c.object {title: 'Link', description: 'A link to another site you want to highlight, like your GitHub, your LinkedIn, or your blog.', required: ['name', 'link']}, + name: {type: 'string', maxLength: 30, title: 'Link Name', description: 'What are you linking to? Ex: "Personal Website", "Twitter"', format: 'link-name'} + link: c.url {title: 'Link', description: 'The URL.', default: 'http://example.com'} + photoURL: {type: 'string', format: 'image-file', title: 'Profile Picture', description: 'Upload a 256x256px or larger image if you want to show a different profile picture to employers than your normal avatar.'} + + jobProfileApproved: {title: 'Job Profile Approved', type: 'boolean', description: 'Whether your profile has been approved by CodeCombat.'} + jobProfileNotes: {type: 'string', maxLength: 1000, title: 'Our Notes', description: "CodeCombat's notes on the candidate.", format: 'markdown', default: ''} c.extendBasicProperties UserSchema, 'user' module.exports = UserSchema diff --git a/server_setup.coffee b/server_setup.coffee index c06482a85..d8dcd1c03 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -3,6 +3,7 @@ path = require 'path' authentication = require 'passport' useragent = require 'express-useragent' fs = require 'graceful-fs' +log = require('winston') database = require './server/commons/database' baseRoute = require './server/routes/base' @@ -96,7 +97,8 @@ setupFallbackRouteToIndex = (app) -> auth.loginUser(req, res, user, false, next) sendMain = (req, res) -> - fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err,data) -> + fs.readFile path.join(__dirname, 'public', 'main.html'), 'utf8', (err, data) -> + log.error "Error modifying main.html: #{err}" if err # insert the user object directly into the html so the application can have it immediately data = data.replace('"userObjectTag"', JSON.stringify(UserHandler.formatEntity(req, req.user))) res.send data diff --git a/vendor/scripts/bootstrap/affix.js b/vendor/scripts/bootstrap/affix.js deleted file mode 100644 index 552bffa3f..000000000 --- a/vendor/scripts/bootstrap/affix.js +++ /dev/null @@ -1,126 +0,0 @@ -/* ======================================================================== - * Bootstrap: affix.js v3.0.3 - * http://getbootstrap.com/javascript/#affix - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // AFFIX CLASS DEFINITION - // ====================== - - var Affix = function (element, options) { - this.options = $.extend({}, Affix.DEFAULTS, options) - this.$window = $(window) - .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) - - this.$element = $(element) - this.affixed = - this.unpin = null - - this.checkPosition() - } - - Affix.RESET = 'affix affix-top affix-bottom' - - Affix.DEFAULTS = { - offset: 0 - } - - Affix.prototype.checkPositionWithEventLoop = function () { - setTimeout($.proxy(this.checkPosition, this), 1) - } - - Affix.prototype.checkPosition = function () { - if (!this.$element.is(':visible')) return - - var scrollHeight = $(document).height() - var scrollTop = this.$window.scrollTop() - var position = this.$element.offset() - var offset = this.options.offset - var offsetTop = offset.top - var offsetBottom = offset.bottom - - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top() - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() - - var affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? false : - offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? 'bottom' : - offsetTop != null && (scrollTop <= offsetTop) ? 'top' : false - - if (this.affixed === affix) return - if (this.unpin) this.$element.css('top', '') - - this.affixed = affix - this.unpin = affix == 'bottom' ? position.top - scrollTop : null - - this.$element.removeClass(Affix.RESET).addClass('affix' + (affix ? '-' + affix : '')) - - if (affix == 'bottom') { - this.$element.offset({ top: document.body.offsetHeight - offsetBottom - this.$element.height() }) - } - } - - - // AFFIX PLUGIN DEFINITION - // ======================= - - var old = $.fn.affix - - $.fn.affix = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.affix') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.affix', (data = new Affix(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.affix.Constructor = Affix - - - // AFFIX NO CONFLICT - // ================= - - $.fn.affix.noConflict = function () { - $.fn.affix = old - return this - } - - - // AFFIX DATA-API - // ============== - - $(window).on('load', function () { - $('[data-spy="affix"]').each(function () { - var $spy = $(this) - var data = $spy.data() - - data.offset = data.offset || {} - - if (data.offsetBottom) data.offset.bottom = data.offsetBottom - if (data.offsetTop) data.offset.top = data.offsetTop - - $spy.affix(data) - }) - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/alert.js b/vendor/scripts/bootstrap/alert.js deleted file mode 100644 index 695ad74d0..000000000 --- a/vendor/scripts/bootstrap/alert.js +++ /dev/null @@ -1,98 +0,0 @@ -/* ======================================================================== - * Bootstrap: alert.js v3.0.3 - * http://getbootstrap.com/javascript/#alerts - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // ALERT CLASS DEFINITION - // ====================== - - var dismiss = '[data-dismiss="alert"]' - var Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype.close = function (e) { - var $this = $(this) - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = $(selector) - - if (e) e.preventDefault() - - if (!$parent.length) { - $parent = $this.hasClass('alert') ? $this : $this.parent() - } - - $parent.trigger(e = $.Event('close.bs.alert')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - $parent.trigger('closed.bs.alert').remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent - .one($.support.transition.end, removeElement) - .emulateTransitionEnd(150) : - removeElement() - } - - - // ALERT PLUGIN DEFINITION - // ======================= - - var old = $.fn.alert - - $.fn.alert = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.alert') - - if (!data) $this.data('bs.alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - // ALERT NO CONFLICT - // ================= - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - // ALERT DATA-API - // ============== - - $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/bootstrap.js b/vendor/scripts/bootstrap/bootstrap.js deleted file mode 100644 index fee1bda9d..000000000 --- a/vendor/scripts/bootstrap/bootstrap.js +++ /dev/null @@ -1,12 +0,0 @@ -//= require affix -//= require alert -//= require button -//= require carousel -//= require collapse -//= require dropdown -//= require tab -//= require transition -//= require scrollspy -//= require modal -//= require tooltip -//= require popover diff --git a/vendor/scripts/bootstrap/button.js b/vendor/scripts/bootstrap/button.js deleted file mode 100644 index c9fdde5e4..000000000 --- a/vendor/scripts/bootstrap/button.js +++ /dev/null @@ -1,115 +0,0 @@ -/* ======================================================================== - * Bootstrap: button.js v3.0.3 - * http://getbootstrap.com/javascript/#buttons - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // BUTTON PUBLIC CLASS DEFINITION - // ============================== - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Button.DEFAULTS, options) - } - - Button.DEFAULTS = { - loadingText: 'loading...' - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - var $el = this.$element - var val = $el.is('input') ? 'val' : 'html' - var data = $el.data() - - state = state + 'Text' - - if (!data.resetText) $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d); - }, 0) - } - - Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons"]') - var changed = true - - if ($parent.length) { - var $input = this.$element.find('input') - if ($input.prop('type') === 'radio') { - // see if clicking on current one - if ($input.prop('checked') && this.$element.hasClass('active')) - changed = false - else - $parent.find('.active').removeClass('active') - } - if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change') - } - - if (changed) this.$element.toggleClass('active') - } - - - // BUTTON PLUGIN DEFINITION - // ======================== - - var old = $.fn.button - - $.fn.button = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.button') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.button', (data = new Button(this, options))) - - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.Constructor = Button - - - // BUTTON NO CONFLICT - // ================== - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - // BUTTON DATA-API - // =============== - - $(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - e.preventDefault() - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/carousel.js b/vendor/scripts/bootstrap/carousel.js deleted file mode 100644 index 6391a36df..000000000 --- a/vendor/scripts/bootstrap/carousel.js +++ /dev/null @@ -1,217 +0,0 @@ -/* ======================================================================== - * Bootstrap: carousel.js v3.0.3 - * http://getbootstrap.com/javascript/#carousel - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // CAROUSEL CLASS DEFINITION - // ========================= - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = - this.sliding = - this.interval = - this.$active = - this.$items = null - - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.DEFAULTS = { - interval: 5000 - , pause: 'hover' - , wrap: true - } - - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) - - this.interval && clearInterval(this.interval) - - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - - return this - } - - Carousel.prototype.getActiveIndex = function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - - return this.$items.index(this.$active) - } - - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getActiveIndex() - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) - if (activeIndex == pos) return this.pause().cycle() - - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - - this.interval = clearInterval(this.interval) - - return this - } - - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') - } - - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') - } - - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.item.active') - var $next = next || $active[type]() - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var fallback = type == 'next' ? 'first' : 'last' - var that = this - - if (!$next.length) { - if (!this.options.wrap) return - $next = this.$element.find('.item')[fallback]() - } - - this.sliding = true - - isCycling && this.pause() - - var e = $.Event('slide.bs.carousel', { relatedTarget: $next[0], direction: direction }) - - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid.bs.carousel', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid.bs.carousel') }, 0) - }) - .emulateTransitionEnd(600) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid.bs.carousel') - } - - isCycling && this.cycle() - - return this - } - - - // CAROUSEL PLUGIN DEFINITION - // ========================== - - var old = $.fn.carousel - - $.fn.carousel = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - $.fn.carousel.Constructor = Carousel - - - // CAROUSEL NO CONFLICT - // ==================== - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - - // CAROUSEL DATA-API - // ================= - - $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false - - $target.carousel(options) - - if (slideIndex = $this.attr('data-slide-to')) { - $target.data('bs.carousel').to(slideIndex) - } - - e.preventDefault() - }) - - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - $carousel.carousel($carousel.data()) - }) - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/collapse.js b/vendor/scripts/bootstrap/collapse.js deleted file mode 100644 index 1a079938e..000000000 --- a/vendor/scripts/bootstrap/collapse.js +++ /dev/null @@ -1,179 +0,0 @@ -/* ======================================================================== - * Bootstrap: collapse.js v3.0.3 - * http://getbootstrap.com/javascript/#collapse - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // COLLAPSE PUBLIC CLASS DEFINITION - // ================================ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Collapse.DEFAULTS, options) - this.transitioning = null - - if (this.options.parent) this.$parent = $(this.options.parent) - if (this.options.toggle) this.toggle() - } - - Collapse.DEFAULTS = { - toggle: true - } - - Collapse.prototype.dimension = function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - Collapse.prototype.show = function () { - if (this.transitioning || this.$element.hasClass('in')) return - - var startEvent = $.Event('show.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var actives = this.$parent && this.$parent.find('> .panel > .in') - - if (actives && actives.length) { - var hasData = actives.data('bs.collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('bs.collapse', null) - } - - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - .addClass('collapsing') - [dimension](0) - - this.transitioning = 1 - - var complete = function () { - this.$element - .removeClass('collapsing') - .addClass('in') - [dimension]('auto') - this.transitioning = 0 - this.$element.trigger('shown.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - var scrollSize = $.camelCase(['scroll', dimension].join('-')) - - this.$element - .one($.support.transition.end, $.proxy(complete, this)) - .emulateTransitionEnd(350) - [dimension](this.$element[0][scrollSize]) - } - - Collapse.prototype.hide = function () { - if (this.transitioning || !this.$element.hasClass('in')) return - - var startEvent = $.Event('hide.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var dimension = this.dimension() - - this.$element - [dimension](this.$element[dimension]()) - [0].offsetHeight - - this.$element - .addClass('collapsing') - .removeClass('collapse') - .removeClass('in') - - this.transitioning = 1 - - var complete = function () { - this.transitioning = 0 - this.$element - .trigger('hidden.bs.collapse') - .removeClass('collapsing') - .addClass('collapse') - } - - if (!$.support.transition) return complete.call(this) - - this.$element - [dimension](0) - .one($.support.transition.end, $.proxy(complete, this)) - .emulateTransitionEnd(350) - } - - Collapse.prototype.toggle = function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - - // COLLAPSE PLUGIN DEFINITION - // ========================== - - var old = $.fn.collapse - - $.fn.collapse = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.collapse') - var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.Constructor = Collapse - - - // COLLAPSE NO CONFLICT - // ==================== - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - // COLLAPSE DATA-API - // ================= - - $(document).on('click.bs.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - var target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - var $target = $(target) - var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $this.data() - var parent = $this.attr('data-parent') - var $parent = parent && $(parent) - - if (!data || !data.transitioning) { - if ($parent) $parent.find('[data-toggle=collapse][data-parent="' + parent + '"]').not($this).addClass('collapsed') - $this[$target.hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - } - - $target.collapse(option) - }) - -}(jQuery); diff --git a/vendor/scripts/bootstrap/dropdown.js b/vendor/scripts/bootstrap/dropdown.js deleted file mode 100644 index 13352ef7c..000000000 --- a/vendor/scripts/bootstrap/dropdown.js +++ /dev/null @@ -1,154 +0,0 @@ -/* ======================================================================== - * Bootstrap: dropdown.js v3.0.3 - * http://getbootstrap.com/javascript/#dropdowns - * ======================================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== */ - - -+function ($) { "use strict"; - - // DROPDOWN CLASS DEFINITION - // ========================= - - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle=dropdown]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) - } - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { - // if mobile we use a backdrop because click events don't delegate - $('