codecombat/app/core/Router.coffee

281 lines
11 KiB
CoffeeScript
Raw Normal View History

2016-03-30 16:57:19 -04:00
go = (path, options) -> -> @routeDirectly path, arguments, options
redirect = (path) -> -> @navigate(path, { trigger: true, replace: true })
2016-04-13 12:54:24 -04:00
utils = require './utils'
2014-01-03 13:32:13 -05:00
module.exports = class CocoRouter extends Backbone.Router
initialize: ->
# http://nerds.airbnb.com/how-to-add-google-analytics-page-tracking-to-57536
@bind 'route', @_trackPageView
2014-01-03 13:32:13 -05:00
Backbone.Mediator.subscribe 'router:navigate', @onNavigate, @
@initializeSocialMediaServices = _.once @initializeSocialMediaServices
2014-01-03 13:32:13 -05:00
routes:
2016-01-26 19:28:29 -05:00
'': ->
if window.serverConfig.picoCTF
return @routeDirectly 'play/CampaignView', ['picoctf'], {}
2016-04-13 12:54:24 -04:00
if utils.getQueryVariable 'hour_of_code'
return @navigate "/play", {trigger: true, replace: true}
return @routeDirectly('NewHomeView', [])
'about': go('AboutView')
2014-07-29 11:28:13 -04:00
'account': go('account/MainAccountView')
'account/settings': go('account/AccountSettingsRootView')
'account/unsubscribe': go('account/UnsubscribeView')
2014-11-25 14:09:29 -05:00
'account/payments': go('account/PaymentsView')
'account/subscription': go('account/SubscriptionView')
'account/invoices': go('account/InvoicesView')
2015-09-25 13:03:44 -04:00
'account/prepaid': go('account/PrepaidView')
'admin': go('admin/MainAdminView')
'admin/clas': go('admin/CLAsView')
2016-06-30 11:29:27 -04:00
'admin/classroom-levels': go('admin/AdminClassroomLevelsView')
'admin/design-elements': go('admin/DesignElementsView')
'admin/files': go('admin/FilesView')
'admin/analytics': go('admin/AnalyticsView')
'admin/analytics/subscriptions': go('admin/AnalyticsSubscriptionsView')
'admin/level-sessions': go('admin/LevelSessionsView')
2016-07-18 12:41:42 -04:00
'admin/school-counts': go('admin/SchoolCountsView')
'admin/school-licenses': go('admin/SchoolLicensesView')
'admin/users': go('admin/UsersView')
2014-08-30 23:27:58 -04:00
'admin/base': go('admin/BaseView')
'admin/demo-requests': go('admin/DemoRequestsView')
'admin/trial-requests': go('admin/TrialRequestsView')
'admin/user-code-problems': go('admin/UserCodeProblemsView')
'admin/pending-patches': go('admin/PendingPatchesView')
'admin/codelogs': go('admin/CodeLogsView')
'artisans': go('artisans/ArtisansView')
'artisans/level-tasks': go('artisans/LevelTasksView')
'artisans/solution-problems': go('artisans/SolutionProblemsView')
'artisans/thang-tasks': go('artisans/ThangTasksView')
2016-07-19 20:58:10 -04:00
'artisans/level-concepts': go('artisans/LevelConceptMap')
'beta': go('HomeView')
2015-09-25 18:49:00 -04:00
'careers': => window.location.href = 'https://jobs.lever.co/codecombat'
2015-10-26 23:39:36 -04:00
'Careers': => window.location.href = 'https://jobs.lever.co/codecombat'
2015-09-14 20:38:18 -04:00
'cla': go('CLAView')
'clans': go('clans/ClansView')
'clans/:clanID': go('clans/ClanDetailsView')
'community': go('CommunityView')
'contribute': go('contribute/MainContributeView')
'contribute/adventurer': go('contribute/AdventurerView')
'contribute/ambassador': go('contribute/AmbassadorView')
'contribute/archmage': go('contribute/ArchmageView')
'contribute/artisan': go('contribute/ArtisanView')
'contribute/diplomat': go('contribute/DiplomatView')
'contribute/scribe': go('contribute/ScribeView')
2016-06-14 20:15:48 -04:00
'courses': go('courses/CoursesView')
'Courses': go('courses/CoursesView')
2016-03-30 16:57:19 -04:00
'courses/students': redirect('/courses')
'courses/teachers': redirect('/teachers/classes')
'courses/purchase': redirect('/teachers/licenses')
'courses/enroll(/:courseID)': redirect('/teachers/licenses')
'courses/update-account': go('courses/CoursesUpdateAccountView')
2016-05-26 17:25:34 -04:00
'courses/:classroomID': go('courses/ClassroomView', { studentsOnly: true })
'courses/:courseID/:courseInstanceID': go('courses/CourseDetailsView', { studentsOnly: true })
'db/*path': 'routeToServer'
'demo(/*subpath)': go('DemoView')
'docs/components': go('docs/ComponentsDocumentationView')
'docs/systems': go('docs/SystemsDocumentationView')
2014-08-26 20:34:00 -04:00
'editor': go('CommunityView')
'editor/achievement': go('editor/achievement/AchievementSearchView')
'editor/achievement/:articleID': go('editor/achievement/AchievementEditView')
'editor/article': go('editor/article/ArticleSearchView')
'editor/article/preview': go('editor/article/ArticlePreviewView')
'editor/article/:articleID': go('editor/article/ArticleEditView')
'editor/level': go('editor/level/LevelSearchView')
'editor/level/:levelID': go('editor/level/LevelEditView')
'editor/thang': go('editor/thang/ThangTypeSearchView')
'editor/thang/:thangID': go('editor/thang/ThangTypeEditView')
'editor/campaign/:campaignID': go('editor/campaign/CampaignEditorView')
2015-03-07 19:30:25 -05:00
'editor/poll': go('editor/poll/PollSearchView')
'editor/poll/:articleID': go('editor/poll/PollEditView')
2015-12-21 15:44:22 -05:00
'editor/thang-tasks': go('editor/ThangTasksView')
'editor/verifier': go('editor/verifier/VerifierView')
'editor/verifier/:levelID': go('editor/verifier/VerifierView')
'file/*path': 'routeToServer'
2014-08-15 10:20:45 -04:00
'github/*path': 'routeToServer'
'hoc': ->
# Matching /?hour_of_code=true behavior
@navigate "/play", {trigger: true, replace: true}
2016-01-26 19:28:29 -05:00
'home': go('NewHomeView')
'i18n': go('i18n/I18NHomeView')
'i18n/thang/:handle': go('i18n/I18NEditThangTypeView')
'i18n/component/:handle': go('i18n/I18NEditComponentView')
'i18n/level/:handle': go('i18n/I18NEditLevelView')
'i18n/achievement/:handle': go('i18n/I18NEditAchievementView')
'i18n/campaign/:handle': go('i18n/I18NEditCampaignView')
2015-03-07 19:30:25 -05:00
'i18n/poll/:handle': go('i18n/I18NEditPollView')
2015-04-30 16:35:21 -04:00
'identify': go('user/IdentifyView')
'legal': go('LegalView')
'play(/)': go('play/CampaignView') # extra slash is to get Facebook app to work
'play/ladder/:levelID/:leagueType/:leagueID': go('ladder/LadderView')
'play/ladder/:levelID': go('ladder/LadderView')
'play/ladder': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView')
'play/game-dev-level/:levelID/:sessionID': go('play/level/PlayGameDevLevelView')
'play/web-dev-level/:levelID/:sessionID': go('play/level/PlayWebDevLevelView')
'play/spectate/:levelID': go('play/SpectateView')
'play/:map': go('play/CampaignView')
'preview': go('HomeView')
2016-02-01 11:39:00 -05:00
'privacy': go('PrivacyView')
'schools': go('NewHomeView')
2016-05-17 13:50:25 -04:00
'seen': go('NewHomeView')
'SEEN': go('NewHomeView')
2015-12-14 23:43:34 -05:00
2016-04-05 12:34:45 -04:00
'teachers': redirect('/teachers/classes')
2016-05-26 17:25:34 -04:00
'teachers/classes': go('courses/TeacherClassesView', { teachersOnly: true })
'teachers/classes/:classroomID': go('courses/TeacherClassView', { teachersOnly: true })
2016-03-30 16:57:19 -04:00
'teachers/courses': go('courses/TeacherCoursesView')
2016-03-09 17:40:52 -05:00
'teachers/demo': go('teachers/RequestQuoteView')
'teachers/enrollments': redirect('/teachers/licenses')
2016-05-26 17:25:34 -04:00
'teachers/licenses': go('courses/EnrollmentsView', { teachersOnly: true })
2016-03-09 17:40:52 -05:00
'teachers/freetrial': go('teachers/RequestQuoteView')
2016-05-26 17:25:34 -04:00
'teachers/quote': redirect('/teachers/demo')
2016-03-09 17:40:52 -05:00
'teachers/signup': ->
return @routeDirectly('teachers/CreateTeacherAccountView', []) if me.isAnonymous()
@navigate('/teachers/update-account', {trigger: true, replace: true})
'teachers/update-account': ->
2016-03-09 17:40:52 -05:00
return @navigate('/teachers/signup', {trigger: true, replace: true}) if me.isAnonymous()
@routeDirectly('teachers/ConvertToTeacherAccountView', [])
'test(/*subpath)': go('TestView')
'user/:slugOrID': go('user/MainUserView')
Improve student account recovery This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
2016-05-11 17:39:26 -04:00
'user/:userID/verify/:verificationCode': go('user/EmailVerifiedView')
'*name/': 'removeTrailingSlash'
2015-06-24 17:46:59 -04:00
'*name': go('NotFoundView')
routeToServer: (e) ->
window.location.href = window.location.href
removeTrailingSlash: (e) ->
@navigate e, {trigger: true}
2016-03-30 16:57:19 -04:00
routeDirectly: (path, args=[], options={}) ->
if options.teachersOnly and not (me.isTeacher() or me.isAdmin())
2016-03-30 16:57:19 -04:00
return @routeDirectly('teachers/RestrictedToTeachersView')
if options.studentsOnly and not (me.isStudent() or me.isAdmin())
2016-03-30 16:57:19 -04:00
return @routeDirectly('courses/RestrictedToStudentsView')
leavingMessage = _.result(window.currentView, 'onLeaveMessage')
if leavingMessage
if not confirm(leavingMessage)
return @navigate(this.path, {replace: true})
else
window.currentView.onLeaveMessage = _.noop # to stop repeat confirm calls
path = 'play/CampaignView' if window.serverConfig.picoCTF and not /^(views)?\/?play/.test(path)
path = "views/#{path}" if not _.string.startsWith(path, 'views/')
ViewClass = @tryToLoadModule path
if not ViewClass and application.moduleLoader.load(path)
@listenToOnce application.moduleLoader, 'load-complete', ->
2016-01-26 19:28:29 -05:00
@routeDirectly(path, args, options)
return
return go('NotFoundView') if not ViewClass
2016-01-26 19:28:29 -05:00
view = new ViewClass(options, args...) # options, then any path fragment args
view.render()
@openView(view)
tryToLoadModule: (path) ->
try
return require(path)
catch error
if error.toString().search('Cannot find module "' + path + '" from') is -1
throw error
2014-01-03 13:32:13 -05:00
openView: (view) ->
@closeCurrentView()
$('#page-container').empty().append view.el
window.currentView = view
@activateTab()
view.afterInsert()
view.didReappear()
@path = document.location.pathname + document.location.search
closeCurrentView: ->
if window.currentView?.reloadOnClose
return document.location.reload()
window.currentModal?.hide?()
return unless window.currentView?
window.currentView.destroy()
$('.popover').popover 'hide'
$('#flying-focus').css({top: 0, left: 0}) # otherwise it might make the page unnecessarily tall
2016-03-30 16:57:19 -04:00
_.delay (->
$('html')[0].scrollTop = 0
$('body')[0].scrollTop = 0
), 10
2014-01-03 13:32:13 -05:00
initializeSocialMediaServices: ->
return if application.testing or application.demoing
application.facebookHandler.loadAPI()
application.gplusHandler.loadAPI()
require('core/services/twitter')()
2014-01-03 13:32:13 -05:00
renderSocialButtons: =>
# TODO: Refactor remaining services to Handlers, use loadAPI success callback
@initializeSocialMediaServices()
$('.share-buttons, .partner-badges').addClass('fade-in').delay(10000).removeClass('fade-in', 5000)
application.facebookHandler.renderButtons()
application.gplusHandler.renderButtons()
twttr?.widgets?.load?()
2014-01-03 13:32:13 -05:00
activateTab: ->
base = _.string.words(document.location.pathname[1..], '/')[0]
$("ul.nav li.#{base}").addClass('active')
_trackPageView: ->
window.tracker?.trackPageView()
2014-01-03 13:32:13 -05:00
onNavigate: (e) ->
if _.isString e.viewClass
ViewClass = @tryToLoadModule e.viewClass
if not ViewClass and application.moduleLoader.load(e.viewClass)
@listenToOnce application.moduleLoader, 'load-complete', ->
@onNavigate(e)
return
e.viewClass = ViewClass
2014-01-03 13:32:13 -05:00
manualView = e.view or e.viewClass
if (e.route is document.location.pathname) and not manualView
return document.location.reload()
@navigate e.route, {trigger: not manualView}
@_trackPageView()
2014-01-03 13:32:13 -05:00
return unless manualView
if e.viewClass
args = e.viewArgs or []
view = new e.viewClass(args...)
view.render()
@openView view
else
@openView e.view
navigate: (fragment, options) ->
super fragment, options
Backbone.Mediator.publish 'router:navigated', route: fragment
2016-03-09 17:40:52 -05:00
reload: ->
document.location.reload()