self enrollment refactor to facilitate CN integration

fixes #CNVS-1119, potentially supersedes
https://gerrit.instructure.com/14501 with a little work.

simpler flow that is more consistent with FFT signup. whether you click
the "join course" button (popup) or go to the join url, the workflow is
the same:

1. if you are authenticated, you just click the enroll button.
2. if you are not authenticated, you can either:
   1. enter your (canvas/ldap) credentials and submit to join the course.
   2. register and join the course (single form). you will then be
      dropped on the course dashboard in the pre_registered state just
      like a /register signup (you have to follow the link in your email
      to set a password).

note that if open registration is turned off, option 2.2 is not available.

other items of interest:

* fix CSRF vulnerabilities where you can enroll authenticated users in
  open courses, or un-enroll them if you know their enrollment's UUID
* move to shorter course-id-less route (w/ join code)
* reuse UserController#create
* handy openAsDialog behavior and embedded view mode
* better json support in PseudonymSessionsController#create
* extract markdown helper from mt
* show "you need to confirm your email" popup when you land on the course
  page the first time (already showed on dashboard)

test plan:
1. test the authenticated/unauthenticated scenarios above, for both the
   popup and join pages
2. regression test of /registration forms

Change-Id: I0d8351695356d437bdbba72cb66c23ed268b0d1a
Reviewed-on: https://gerrit.instructure.com/15902
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Joe Tanner <joe@instructure.com>
QA-Review: Jon Jensen <jon@instructure.com>
This commit is contained in:
Jon Jensen 2012-12-06 23:28:37 -07:00
parent 8ff2592a3e
commit f74ebd096b
46 changed files with 923 additions and 294 deletions

View File

@ -0,0 +1,29 @@
define [
'jquery'
'compiled/fn/preventDefault'
'jqueryui/dialog',
], ($, preventDefault) ->
$.fn.openAsDialog = (options = {}) ->
@click preventDefault (e) ->
$link = $(e.target)
options.width ?= 550
options.height ?= 450
options.title ?= $link.attr('title')
options.resizable ?= false
$dialog = $("<div>")
$iframe = $('<iframe>', style: "position:absolute;top:0;left:0;border:none", src: $link.attr('href') + '?embedded=1')
$dialog.append $iframe
$dialog.on "dialogopen", ->
$container = $dialog.closest('.ui-dialog-content')
$iframe.height $container.outerHeight()
$iframe.width $container.outerWidth()
$dialog.dialog options
$ ->
$('a[data-open-as-dialog]').openAsDialog()
$

View File

@ -31,6 +31,7 @@ require [
'jqueryui/effects/drop'
'jqueryui/progressbar'
'jqueryui/tabs'
'compiled/registration/incompleteRegistrationWarning'
# random modules required by the js_blocks, put them all in here
# so RequireJS doesn't try to load them before common is loaded

View File

@ -3,11 +3,7 @@ require [
'Backbone',
'jquery',
'i18n!dashboard'
'compiled/registration/incompleteRegistrationWarning'
], (_, {View}, $, I18n, incompleteRegistrationWarning) ->
if ENV.INCOMPLETE_REGISTRATION
incompleteRegistrationWarning(ENV.USER_EMAIL)
], (_, {View}, $, I18n) ->
class DashboardView extends View

View File

@ -0,0 +1,6 @@
require [
'jquery'
'compiled/views/registration/SelfEnrollmentForm'
], ($, SelfEnrollmentForm) ->
new SelfEnrollmentForm el: '#enroll_form'

View File

@ -12,6 +12,7 @@ define [
invalid: I18n.t("errors.invalid", "May only contain letters, numbers, or the following: %{characters}", {characters: ". + - _ @ ="})
taken: I18n.t("errors.taken", "Email already in use")
bad_credentials: I18n.t("errors.bad_credentials", "Invalid username or password")
not_email: I18n.t("errors.not_email", "Not a valid email address")
password:
too_short: I18n.t("errors.too_short", "Must be at least %{min} characters", {min: 6})
confirmation: I18n.t("errors.mismatch", "Doesn't match")

View File

@ -16,6 +16,7 @@ define [
self_enrollment_code:
blank: I18n.t("errors.required", "Required")
invalid: I18n.t("errors.invalid_code", "Invalid code")
already_enrolled: I18n.t("errors.already_enrolled", "You are already enrolled in this course")
terms_of_use:
accepted: I18n.t("errors.terms", "You must agree to the terms")

View File

@ -3,8 +3,8 @@ define [
'i18n!registration'
'jst/registration/incompleteRegistrationWarning'
], ($, I18n, template) ->
(email) ->
$(template(email: email)).
if ENV.INCOMPLETE_REGISTRATION
$(template(email: ENV.USER_EMAIL)).
appendTo($('body')).
dialog
title: I18n.t('welcome_to_canvas', 'Welcome to Canvas!')

View File

@ -0,0 +1,17 @@
define [
'compiled/models/User'
'compiled/models/Pseudonym'
'compiled/object/flatten'
], (User, Pseudonym, flatten) ->
# normalize errors we get from POST /user (user creation API)
registrationErrors = (errors) ->
errors = flatten
user: User::normalizeErrors(errors.user)
pseudonym: Pseudonym::normalizeErrors(errors.pseudonym)
observee: Pseudonym::normalizeErrors(errors.observee)
, arrays: false
if errors['user[birthdate]']
errors['user[birthdate(1i)]'] = errors['user[birthdate]']
delete errors['user[birthdate]']
errors

View File

@ -2,16 +2,14 @@ define [
'underscore'
'i18n!registration'
'compiled/fn/preventDefault'
'compiled/models/User'
'compiled/models/Pseudonym'
'compiled/registration/registrationErrors'
'jst/registration/teacherDialog'
'jst/registration/studentDialog'
'jst/registration/studentHigherEdDialog'
'jst/registration/parentDialog'
'compiled/object/flatten'
'jquery.instructure_forms'
'jquery.instructure_date_and_time'
], (_, I18n, preventDefault, User, Pseudonym, teacherDialog, studentDialog, studentHigherEdDialog, parentDialog, flatten) ->
], (_, I18n, preventDefault, registrationErrors, teacherDialog, studentDialog, studentHigherEdDialog, parentDialog) ->
$nodes = {}
templates = {teacherDialog, studentDialog, studentHigherEdDialog, parentDialog}
@ -36,27 +34,22 @@ define [
$form.disableWhileLoading(promise)
success: (data) =>
# they should now be authenticated (either registered or pre_registered)
window.location = "/?login_success=1&registration_success=1"
if data.course
window.location = "/courses/#{data.course.course.id}?registration_success=1"
else
window.location = "/?registration_success=1"
formErrors: false
error: (errors) ->
promise.reject()
if _.any(errors.user.birthdate ? [], (e) -> e.type is 'too_young')
$node.find('.registration-dialog').html I18n.t('too_young_error', 'You must be at least %{min_years} years of age to use Canvas without a course join code.', min_years: ENV.USER.MIN_AGE)
$node.find('.registration-dialog').text I18n.t('too_young_error_join_code', 'You must be at least %{min_years} years of age to use Canvas without a course join code.', min_years: ENV.USER.MIN_AGE)
$node.dialog buttons: [
text: I18n.t('ok', "OK")
click: -> $node.dialog('close')
class: 'btn-primary'
]
return
errors = flatten
user: User::normalizeErrors(errors.user)
pseudonym: Pseudonym::normalizeErrors(errors.pseudonym)
observee: Pseudonym::normalizeErrors(errors.observee)
, arrays: false
if errors['user[birthdate]']
errors['user[birthdate(1)]'] = errors['user[birthdate]']
delete errors['user[birthdate]']
$form.formErrors errors
$form.formErrors registrationErrors(errors)
$node.dialog
resizable: false

View File

@ -34,6 +34,8 @@ define [
dateSelect = (name, options, htmlOptions = _.clone(options)) ->
validOptions = ['type', 'startYear', 'endYear', 'includeBlank', 'order']
delete htmlOptions[opt] for opt in validOptions
htmlOptions['class'] ?= ''
htmlOptions['class'] += ' date-select'
year = (new Date()).getFullYear()
position = {year: 1, month: 2, day: 3}
@ -53,7 +55,7 @@ define [
$result = $('<span>')
for i in [0...options.order.length]
type = options.order[i]
tName = name.replace(/(\]?)$/, "(" + position[type] + ")$1")
tName = name.replace(/(\]?)$/, "(" + position[type] + "i)$1")
$result.append(
builders[type](
options,

View File

@ -0,0 +1,124 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
define [
'jquery'
'underscore'
'Backbone'
'i18n!registration'
'compiled/registration/registrationErrors'
'jquery.instructure_forms'
'jquery.ajaxJSON'
], ($, _, Backbone, I18n, registrationErrors) ->
class SelfEnrollmentForm extends Backbone.View
events:
'change input[name=user_type]': 'changeType'
'click #logout_link': 'logOutAndRefresh'
initialize: ->
@enrollAction = @$el.attr('action')
@userType = @$el.find('input[type=hidden][name=user_type]').val()
@$el.formSubmit
beforeSubmit: @beforeSubmit
onSubmit: @onSubmit
success: @enrollSuccess
error: @enrollError
formErrors: false
changeType: (e) =>
@userType = $(e.target).val()
@$el.find('.user_info').hide()
@$el.find("##{@userType}_user_info").show()
@$el.find("#submit_button").css(visibility: 'visible')
beforeSubmit: =>
return false unless @userType
unless @promise?
@promise = $.Deferred()
@$el.disableWhileLoading(@promise)
switch @userType
when 'new'
# create user and self-enroll in course(s)
@$el.attr('action', '/users')
when 'existing'
@logIn =>
# yay, now enroll the user
@userType = 'authenticated'
@enrollErrorOnce = (errors) =>
if @hasError(errors.user?.self_enrollment_code, 'already_enrolled')
# we don't reload the form, so we want a subsequent login
# or signup attempt to work
@userType = 'existing'
@logOut()
@$el.submit()
return false
when 'authenticated'
@$el.attr('action', @enrollAction)
onSubmit: (deferred) ->
$.when(deferred).done => @enrollErrorOnce = null
error: (errors) =>
@promise.reject()
# move the "already enrolled" error to the username, since that's visible
if errors['user[self_enrollment_code]']
errors['pseudonym[unique_id]'] ?= []
errors['pseudonym[unique_id]'].push errors['user[self_enrollment_code]'][0]
delete errors['user[self_enrollment_code]']
@$el.formErrors errors
@promise = null
enrollError: (errors) =>
@enrollErrorOnce?(errors)
if @hasError(errors.user?.birthdate, 'too_young')
errors = []
@$el.text I18n.t('too_young_error', 'You must be at least %{min_years} years of age to enroll in this course.', min_years: ENV.USER.MIN_AGE)
@error registrationErrors(errors)
enrollSuccess: (data) =>
# they should now be authenticated (either registered or pre_registered)
q = window.location.search
q = (if q then "#{q}&" else "?")
q += "enrolled=1"
q += '&just_created=1' if @userType is 'new'
window.location.search = q
logIn: (successCb) ->
data = pseudonym_session:
unique_id: @$el.find('#student_email').val()
password: @$el.find('#student_password').val()
$.ajaxJSON '/login', 'POST', data, successCb, (errors, xhr) =>
baseErrors = errors.errors.base
error = baseErrors[baseErrors.length - 1].message
@error 'pseudonym[password]': error
logOut: (refresh = false) =>
$.ajaxJSON '/logout', 'POST', {}, ->
location.reload true if refresh
logOutAndRefresh: (e) =>
e.preventDefault()
@logOut(true)
hasError: (errors, type) ->
return false unless errors
return true for e in errors when e.type is type
false

View File

@ -46,7 +46,7 @@ class ApplicationController < ActionController::Base
after_filter :set_user_id_header
before_filter :fix_xhr_requests
before_filter :init_body_classes
before_filter :set_response_headers
after_filter :set_response_headers
add_crumb(proc { %Q{<i title="#{I18n.t('links.dashboard', "My Dashboard")}" class="icon-home standalone-icon"></i>}.html_safe }, :root_path, :class => "home")
@ -153,7 +153,7 @@ class ApplicationController < ActionController::Base
# we can't block frames on the files domain, since files domain requests
# are typically embedded in an iframe in canvas, but the hostname is
# different
if !files_domain? && Setting.get_cached('block_html_frames', 'true') == 'true'
if !files_domain? && Setting.get_cached('block_html_frames', 'true') == 'true' && !@embeddable
headers['X-Frame-Options'] = 'SAMEORIGIN'
end
true
@ -1177,6 +1177,12 @@ class ApplicationController < ActionController::Base
end
end
def check_incomplete_registration
if @current_user
js_env :INCOMPLETE_REGISTRATION => params[:registration_success] && @current_user.pre_registered?, :USER_EMAIL => @current_user.email
end
end
def page_views_enabled?
PageView.page_views_enabled?
end
@ -1237,7 +1243,21 @@ class ApplicationController < ActionController::Base
(params[:format].to_s != 'json' || in_app?)
end
def reset_session
# when doing login/logout via ajax, we need to have the new csrf token
# for subsequent requests.
@resend_csrf_token_if_json = true
super
end
def set_layout_options
@embedded_view = params[:embedded]
@headers = false if params[:no_headers] || @embedded_view
(@body_classes ||= []) << 'embedded' if @embedded_view
end
def render(options = nil, extra_options = {}, &block)
set_layout_options
if options && options.key?(:json)
json = options.delete(:json)
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
@ -1247,6 +1267,10 @@ class ApplicationController < ActionController::Base
json = "while(1);#{json}"
end
if @resend_csrf_token_if_json
response.headers['X-CSRF-Token'] = form_authenticity_token
end
# fix for some browsers not properly handling json responses to multipart
# file upload forms and s3 upload success redirects -- we'll respond with text instead.
if options[:as_text] || json_as_text?

View File

@ -778,46 +778,21 @@ class CoursesController < ApplicationController
def self_unenrollment
get_context
unless @context_enrollment && params[:self_unenrollment] && params[:self_unenrollment] == @context_enrollment.uuid && @context_enrollment.self_enrolled?
redirect_to course_url(@context)
return
if @context_enrollment && params[:self_unenrollment] && params[:self_unenrollment] == @context_enrollment.uuid && @context_enrollment.self_enrolled?
@context_enrollment.conclude
render :json => ""
else
render :json => "", :status => :bad_request
end
@context_enrollment.complete
redirect_to course_url(@context)
end
# DEPRECATED
def self_enrollment
get_context
unless @context.self_enrollment && params[:self_enrollment] && @context.self_enrollment_codes.include?(params[:self_enrollment])
return redirect_to course_url(@context)
end
unless @current_user || @context.root_account.open_registration?
store_location
flash[:notice] = t('notices.login_required', "Please log in to join this course.")
return redirect_to login_url
end
if @current_user
@enrollment = @context.self_enroll_student(@current_user)
flash[:notice] = t('notices.enrolled', "You are now enrolled in this course.")
return redirect_to course_url(@context)
end
if params[:email]
begin
address = TMail::Address::parse(params[:email])
rescue
flash[:error] = t('errors.invalid_email', "Invalid e-mail address, please try again.")
render :action => 'open_enrollment'
return
end
user = User.new(:name => address.name || address.address)
user.communication_channels.build(:path => address.address)
user.workflow_state = 'creation_pending'
user.save!
@enrollment = @context.enroll_student(user)
@enrollment.update_attribute(:self_enrolled, true)
return render :action => 'open_enrollment_confirmed'
end
render :action => 'open_enrollment'
redirect_to enroll_url(@context.self_enrollment_code)
end
def check_pending_teacher
@ -900,6 +875,8 @@ class CoursesController < ApplicationController
@context_enrollment ||= @pending_enrollment
if is_authorized_action?(@context, @current_user, :read)
check_incomplete_registration
if @current_user && @context.grants_right?(@current_user, session, :manage_grades)
@assignments_needing_publishing = @context.assignments.active.need_publishing || []
end

View File

@ -163,35 +163,25 @@ class PseudonymSessionsController < ApplicationController
end
if pseudonym == :too_many_attempts || @pseudonym_session.too_many_attempts?
flash[:error] = t 'errors.max_attempts', "Too many failed login attempts. Please try again later or contact your system administrator."
redirect_to login_url
unsuccessful_login t('errors.max_attempts', "Too many failed login attempts. Please try again later or contact your system administrator."), true
return
end
@pseudonym = @pseudonym_session && @pseudonym_session.record
# If the user's account has been deleted, feel free to share that information
if @pseudonym && (!@pseudonym.user || @pseudonym.user.unavailable?)
flash[:error] = t 'errors.user_deleted', "That user account has been deleted. Please contact your system administrator to have your account re-activated."
redirect_to login_url
unsuccessful_login t('errors.user_deleted', "That user account has been deleted. Please contact your system administrator to have your account re-activated."), true
return
end
# Call for some cleanups that should be run when a user logs in
@user = @pseudonym.login_assertions_for_user if found
# If the user is registered and logged in, redirect them to their dashboard page
if found
# Call for some cleanups that should be run when a user logs in
@user = @pseudonym.login_assertions_for_user
successful_login(@user, @pseudonym)
# Otherwise re-render the login page to show the error
else
respond_to do |format|
flash[:error] = t 'errors.invalid_credentials', "Incorrect username and/or password"
@errored = true
@pre_registered = @user if @user && !@user.registered?
@headers = false
format.html { maybe_render_mobile_login :bad_request }
format.json { render :json => @pseudonym_session.errors.to_json, :status => :bad_request }
end
unsuccessful_login t('errors.invalid_credentials', "Incorrect username and/or password")
end
end
@ -553,6 +543,26 @@ class PseudonymSessionsController < ApplicationController
end
end
def unsuccessful_login(message, refresh = false)
respond_to do |format|
flash[:error] = message
format.html do
if refresh
redirect_to login_url
else
@errored = true
@pre_registered = @user if @user && !@user.registered?
@headers = false
maybe_render_mobile_login :bad_request
end
end
format.json do
@pseudonym_session.errors.add('base', message)
render :json => @pseudonym_session.errors.to_json, :status => :bad_request
end
end
end
OAUTH2_OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'
def oauth2_auth

View File

@ -0,0 +1,49 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class SelfEnrollmentsController < ApplicationController
before_filter :infer_signup_info, :only => [:new, :create]
before_filter :require_user, :only => :create
include Api::V1::Course
def new
js_env :USER => {:MIN_AGE => @course.self_enrollment_min_age || User.self_enrollment_min_age}
end
def create
@current_user.validation_root_account = @domain_root_account
@current_user.require_self_enrollment_code = true
@current_user.self_enrollment_code = params[:self_enrollment_code]
if @current_user.save
render :json => course_json(@current_user.self_enrollment_course, @current_user, session, [], nil)
else
render :json => {:user => @current_user.errors.as_json[:errors]}, :status => :bad_request
end
end
private
def infer_signup_info
@embeddable = true
@course = @domain_root_account.self_enrollment_course_for(params[:self_enrollment_code])
# TODO: have a join code field in new.html.erb if none is provided in the url
raise ActiveRecord::RecordNotFound unless @course
end
end

View File

@ -284,10 +284,7 @@ class UsersController < ApplicationController
end
def user_dashboard
if @current_user && @current_user.registered? && params[:registration_success]
# user just joined with a course code
return redirect_to @current_user.courses.first if @current_user.courses.size == 1
end
check_incomplete_registration
get_context
# dont show crubms on dashboard because it does not make sense to have a breadcrumb
@ -305,9 +302,6 @@ class UsersController < ApplicationController
@announcements = AccountNotification.for_user_and_account(@current_user, @domain_root_account)
@pending_invitations = @current_user.cached_current_enrollments(:include_enrollment_uuid => session[:enrollment_uuid]).select { |e| e.invited? }
@stream_items = @current_user.try(:cached_recent_stream_items) || []
incomplete_registration = @current_user && @current_user.pre_registered? && params[:registration_success]
js_env({:INCOMPLETE_REGISTRATION => incomplete_registration, :USER_EMAIL => @current_user.email})
end
def toggle_dashboard
@ -649,6 +643,14 @@ class UsersController < ApplicationController
def new
return redirect_to(root_url) if @current_user
unless @context == Account.default && @context.no_enrollments_can_create_courses?
# TODO: generic/brandable page, so we can up it up to non-default accounts
# also more control so we can conditionally enable features (e.g. if
# no_enrollments_can_create_courses==false, but open reg is on, students
# should still be able to sign up with join codes, etc. ... we should just
# not have the teacher button/form)
return redirect_to(root_url)
end
render :layout => 'bare'
end
@ -678,7 +680,10 @@ class UsersController < ApplicationController
manage_user_logins = @context.grants_right?(@current_user, session, :manage_user_logins)
self_enrollment = params[:self_enrollment].present?
allow_password = self_enrollment || manage_user_logins
allow_non_email_pseudonyms = manage_user_logins || self_enrollment && params[:pseudonym_type] == 'username'
@domain_root_account.email_pseudonyms = !allow_non_email_pseudonyms
require_password = self_enrollment && allow_non_email_pseudonyms
allow_password = require_password || manage_user_logins
notify = params[:pseudonym].delete(:send_confirmation) == '1'
notify = :self_registration unless manage_user_logins
@ -695,7 +700,7 @@ class UsersController < ApplicationController
end
@user.name ||= params[:pseudonym][:unique_id]
unless @user.registered?
@user.workflow_state = if self_enrollment
@user.workflow_state = if require_password
# no email confirmation required (self_enrollment_code and password
# validations will ensure everything is legit)
'registered'
@ -727,7 +732,7 @@ class UsersController < ApplicationController
end
@pseudonym ||= @user.pseudonyms.build(:account => @context)
@pseudonym.require_password = self_enrollment
@pseudonym.require_password = require_password
# pre-populate the reverse association
@pseudonym.user = @user
# don't require password_confirmation on api calls
@ -754,7 +759,11 @@ class UsersController < ApplicationController
# unless the user is registered/pre_registered (if the latter, he still
# needs to confirm his email and set a password, otherwise he can't get
# back in once his session expires)
@pseudonym.send(:skip_session_maintenance=, true) unless @user.registered? || @user.pre_registered? # automagically logged in
if @user.registered? || @user.pre_registered? # automagically logged in
PseudonymSession.new(@pseudonym).save unless @pseudonym.new_record?
else
@pseudonym.send(:skip_session_maintenance=, true)
end
@user.save!
message_sent = false
if notify == :self_registration
@ -762,7 +771,7 @@ class UsersController < ApplicationController
message_sent = true
@pseudonym.send_confirmation!
end
@user.new_teacher_registration((params[:user] || {}).merge({:remote_ip => request.remote_ip}))
@user.new_registration((params[:user] || {}).merge({:remote_ip => request.remote_ip}))
elsif notify && !@user.registered?
message_sent = true
@pseudonym.send_registration_notification!
@ -770,7 +779,7 @@ class UsersController < ApplicationController
@cc.send_merge_notification! if @cc.merge_candidates.length != 0
end
data = { :user => @user, :pseudonym => @pseudonym, :channel => @cc, :observee => @observee, :message_sent => message_sent }
data = { :user => @user, :pseudonym => @pseudonym, :channel => @cc, :observee => @observee, :message_sent => message_sent, :course => @user.self_enrollment_course }
if api_request?
render(:json => user_json(@user, @current_user, session, %w{locale}))
else
@ -1133,7 +1142,7 @@ class UsersController < ApplicationController
get_context
@context = @domain_root_account || Account.default unless @context.is_a?(Account)
@context = @context.root_account
if !@context.grants_right?(@current_user, session, :manage_user_logins) && (!@context.open_registration? || !@context.no_enrollments_can_create_courses? || @context != Account.default)
unless @context.grants_right?(@current_user, session, :manage_user_logins) || @context.open_registration?
flash[:error] = t('no_open_registration', "Open registration has not been enabled for this account")
respond_to do |format|
format.html { redirect_to root_url }

View File

@ -43,6 +43,23 @@ module DashboardHelper
@current_user.cached_current_enrollments(:include_enrollment_uuid => session[:enrollment_uuid]).empty?
end
def welcome_message
if @current_user.cached_current_enrollments(:include_future => true).present?
t('#users.welcome.unpublished_courses_message', <<-BODY)
You've enrolled in one or more courses that have not started yet. Once
those courses are available, you will see information about them here
and in the top navigation. In the meantime, feel free to sign up for
more courses or set up your profile.
BODY
else
t('#users.welcome.no_courses_message', <<-BODY)
You don't have any courses, so this page won't be very exciting for now.
Once you've created or signed up for courses, you'll start to see
conversations from all of your classes.
BODY
end
end
def activity_category_links(category, items)
max_contexts = 4
contexts = items.map{ |i| [i.context.name, i.context.linked_to] }.uniq

View File

@ -0,0 +1,26 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module SelfEnrollmentsHelper
def registration_summary
# allow plugins to display additional content
if @registration_summary
markdown(@registration_summary, :never) rescue nil
end
end
end

View File

@ -377,6 +377,12 @@ class Account < ActiveRecord::Base
@cached_courses_name_like[[query, opts]] ||= self.fast_course_base(opts) {|q| q.name_like(query)}
end
def self_enrollment_course_for(code)
all_courses.
where(:self_enrollment => true, :self_enrollment_code => code).
first
end
def file_namespace
Shard.default.activate { "account_#{self.root_account.id}" }
end
@ -684,9 +690,8 @@ class Account < ActiveRecord::Base
name
end
def email_pseudonyms
false
end
# can be set/overridden by plugin to enforce email pseudonyms
attr_accessor :email_pseudonyms
def password_authentication?
!!(!self.account_authorization_config || self.account_authorization_config.password_authentication?)

View File

@ -550,10 +550,12 @@ class Course < ActiveRecord::Base
end
memoize :user_is_instructor?
def user_is_student?(user)
def user_is_student?(user, opts = {})
return unless user
Rails.cache.fetch([self, user, "course_user_is_student"].cache_key) do
user.cached_current_enrollments.any? { |e| e.course_id == self.id && e.participating_student? }
Rails.cache.fetch([self, user, "course_user_is_student", opts[:include_future]].cache_key) do
user.cached_current_enrollments(:include_future => opts[:include_future]).any? { |e|
e.course_id == self.id && (opts[:include_future] ? e.student? : e.participating_student?)
}
end
end
memoize :user_is_student?
@ -742,6 +744,10 @@ class Course < ActiveRecord::Base
code
end
# can be overridden via plugin
def self_enrollment_min_age
end
def long_self_enrollment_code
Digest::MD5.hexdigest("#{uuid}_for_#{id}")
end
@ -1511,7 +1517,7 @@ class Course < ActiveRecord::Base
def self_enroll_student(user, opts = {})
enrollment = enroll_student(user, opts.merge(:no_notify => true))
enrollment.self_enrolled = true
enrollment.accept
enrollment.accept(:force)
unless opts[:skip_pseudonym]
new_pseudonym = user.find_or_initialize_pseudonym_for_account(root_account)
new_pseudonym.save if new_pseudonym && new_pseudonym.changed?

View File

@ -496,13 +496,13 @@ class Enrollment < ActiveRecord::Base
res
end
def accept
return false unless invited?
def accept(force = false)
return false unless force || invited?
ids = nil
ids = self.user.dashboard_messages.find_all_by_context_id_and_context_type(self.id, 'Enrollment', :select => "id").map(&:id) if self.user
Message.delete_all({:id => ids}) if ids && !ids.empty?
update_attribute(:workflow_state, 'active')
user.touch
touch_user
end
workflow do

View File

@ -192,7 +192,7 @@ class Pseudonym < ActiveRecord::Base
def validate_unique_id
if (!self.account || self.account.email_pseudonyms) && !self.deleted?
unless self.unique_id.match(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i)
self.errors.add(:unique_id, t('errors.invalid_email_address', "\"%{email}\" is not a valid email address", :email => self.unique_id))
self.errors.add(:unique_id, "not_email")
return false
end
end

View File

@ -76,6 +76,8 @@ class User < ActiveRecord::Base
has_many :invited_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => enrollment_conditions(:invited), :order => 'enrollments.created_at'
has_many :current_and_invited_enrollments, :class_name => 'Enrollment', :include => [:course], :order => 'enrollments.created_at',
:conditions => enrollment_conditions(:current_and_invited)
has_many :current_and_future_enrollments, :class_name => 'Enrollment', :include => [:course], :order => 'enrollments.created_at',
:conditions => enrollment_conditions(:current_and_invited, false)
has_many :not_ended_enrollments, :class_name => 'Enrollment', :conditions => "enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted')", :order => 'enrollments.created_at'
has_many :concluded_enrollments, :class_name => 'Enrollment', :include => [:course, :course_section], :conditions => enrollment_conditions(:completed), :order => 'enrollments.created_at'
has_many :observer_enrollments
@ -288,8 +290,11 @@ class User < ActiveRecord::Base
if value.blank?
record.errors.add(attr, "blank")
elsif record.validation_root_account
record.self_enrollment_course = record.validation_root_account.all_courses.find_by_self_enrollment_code(value)
record.errors.add(attr, "invalid") unless record.self_enrollment_course
course = record.validation_root_account.self_enrollment_course_for(value)
record.self_enrollment_course = course
record.errors.add(attr, "invalid") unless course
record.errors.add(attr, "already_enrolled") if course && course.user_is_student?(record, :include_future => true)
record.errors.add(:birthdate, "too_young") if course && course.self_enrollment_min_age && record.birthdate && record.birthdate > course.self_enrollment_min_age.years.ago
else
record.errors.add(attr, "account_required")
end
@ -457,9 +462,12 @@ class User < ActiveRecord::Base
end
end
# These two methods can be overridden by a plugin if you want to have an approval process for new teachers
# These methods can be overridden by a plugin if you want to have an approval
# process or implement additional tracking for new users
def registration_approval_required?; false; end
def new_teacher_registration(form_params = {}); end
def new_registration(form_params = {}); end
# DEPRECATED, override new_registration instead
def new_teacher_registration(form_params = {}); new_registration(form_params); end
set_broadcast_policy do |p|
p.dispatch :new_teacher_registration
@ -1737,8 +1745,8 @@ class User < ActiveRecord::Base
# this method takes an optional {:include_enrollment_uuid => uuid} so that you can pass it the session[:enrollment_uuid] and it will include it.
def cached_current_enrollments(opts={})
self.shard.activate do
res = Rails.cache.fetch([self, 'current_enrollments', opts[:include_enrollment_uuid] ].cache_key) do
res = self.current_and_invited_enrollments.with_each_shard
res = Rails.cache.fetch([self, 'current_enrollments', opts[:include_enrollment_uuid], opts[:include_future] ].cache_key) do
res = (opts[:include_future] ? current_and_future_enrollments : current_and_invited_enrollments).with_each_shard
if opts[:include_enrollment_uuid] && pending_enrollment = Enrollment.find_by_uuid_and_workflow_state(opts[:include_enrollment_uuid], "invited")
res << pending_enrollment
res.uniq!

View File

@ -19,6 +19,28 @@ body
box-shadow: none
#content
padding-top: 0
&.embedded
#wrapper-container, #wrapper, #main
height: 100%
#main
min-height: 0
#content
padding: 1em
.embedded-footer
position: absolute
bottom: 0
width: 100%
padding: 14px 0 15px
margin: 0 -1em !important
background-color: #EFEFEF
border-top: 1px solid #DDD
box-shadow: inset 0 1px 0 white
text-align: right
.controls
margin-left: 15px
margin-right: 15px
background: #fff
// so we don't get the non-interactionable content
.scripts-not-loaded

View File

@ -134,6 +134,11 @@ input[type="url"], input[type="search"], input[type="tel"], input[type="color"],
width: 220px; // default input width + 10px of padding that doesn't get applied
border: 1px solid #bbb;
}
select.date-select {
width: auto;
float: left;
margin: 0 3px 0 0;
}
// Make multiple select elements height not fixed
select[multiple], select[size] {

View File

@ -17,9 +17,6 @@ a {
.registration-dialog .spinner {
width: 100px;
}
.select-birthdate {
width: auto;
}
}
.help-block-small {

View File

@ -1,27 +0,0 @@
<%
jammit_css :login
@headers = false
@body_classes << "modal"
content_for :page_title, t('titles.invitation', 'Invitation to %{course}', :course => @context.name)
%>
<% content_for :page_header do %>
<h1><%= t('headings.invitation', %{Course Invitation}) %></h1>
<% end %>
<div id="modal-box">
<h2><%= t('headings.accept_invitation', %{Accept Enrollment Invitation}) %></h2>
<p>
<%= t 'details', %{You've been invited to participate in this course, %{course}.
To continue the enrollment process, please provide us with your current
email address.}, :course => content_tag('b', @context.name) %>
</p>
<% form_tag course_self_enrollment_path(@context, @context.self_enrollment_code), :class => 'bootstrap-form', :style => 'overflow:hidden' do %>
<div style="float:left;padding-top:8px">
<label for="enrollment_email" style="display:inline"><%= before_label('email', %{Email Address}) %></label>
<input type="text" name="email" id="enrollment_email"/>
</div>
<button type="submit" class="btn btn-primary"><%= t('buttons.enroll', %{Enroll in Course}) %></button>
<% end %>
</div>
</div>

View File

@ -1,17 +0,0 @@
<%
jammit_css :login
@headers = false
@body_classes << "modal"
content_for :page_title, t('titles.invitation', 'Invitation to %{course}', :course => @context.name)
%>
<% @show_left_side = false %>
<% content_for :page_header do %>
<h1><%= t('headings.invitation', %{Course Invitation}) %></h1>
<% end %>
<div id="modal-box">
<h2><%= t('headings.received', %{Invitation Received!}) %></h2>
<%= t 'details', %{You should get an email at %{email} in the next few minutes. That email will have a link that you can use to finish the registration process. Once you've set up your account you'll be able to access content from %{course}. Welcome to Canvas!}, :email => content_tag('b', params[:email]), :course => content_tag('b', @context.name) %>
</div>

View File

@ -19,15 +19,7 @@
<% end %>
<% if @context.available? && @context.self_enrollment && @context.open_enrollment && (!@context_enrollment || !@context_enrollment.active?) && !session["role_course_#{@context.id}"] %>
<div class="rs-margin-lr rs-margin-top">
<a href="#" class="btn button-sidebar-wide self_enrollment_link"><i class="icon-user-add"></i> <%= t('links.join_course', %{Join this Course}) %></a>
</div>
<div id="self_enrollment_dialog" style="display: none;">
<h2><%= image_tag "group.png", :style => "vertical-align: middle;" %> <%= t('headings.joining_a_course', %{Joining a Course}) %></h2>
<%= t('details.joining_a_course', %{Once you join a course you will see it show up in your course list. You'll be able to participate in graded portions of the course and communicate directly with the teachers and other students. You'll also see conversations and events from this course show up in your stream and as notifications.}) %>
<div class="button-container">
<a href="<%= course_self_enrollment_path(@context, @context.self_enrollment_code) %>" class="btn action"><i class="icon-user-add"></i> <span><%= t('links.join_course', %{Join this Course}) %></span></a>
<a href="#" class="btn button-secondary cancel_button"><%= t('#buttons.cancel', %{Cancel}) %></a>
</div>
<a href="<%= enroll_url(@context.self_enrollment_code) %>" title="<%= t('links.join_course', %{Join this Course}) %>" class="btn button-sidebar-wide self_enrollment_link" data-open-as-dialog><i class="icon-user-add"></i> <%= t('links.join_course', %{Join this Course}) %></a>
</div>
<% elsif @context_enrollment && @context_enrollment.self_enrolled && @context_enrollment.active? && (!session["role_course_#{@context.id}"]) %>
<div class="rs-margin-lr rs-margin-top">
@ -37,8 +29,8 @@
<h2><i class="icon-warning"></i> <%= t('headings.confirm_unenroll', %{Confirm Unenrollment}) %></h2>
<%= t('details.confirm_unenroll', %{Are you sure you want to unenroll in this course? You will no longer be able to see the course roster or communicate directly with the teachers, and you will no longer see course events in your stream and as notifications.}) %>
<div class="button-container">
<a href="<%= course_self_unenrollment_path(@context, @context_enrollment.uuid) %>" class="btn action"><i class="icon-end"></i> <span><%= t('links.drop_course', %{Drop this Course}) %></span></a>
<a href="#" class="btn button-secondary cancel_button"><%= t('#buttons.cancel', %{Cancel}) %></a>
<a href="<%= course_self_unenrollment_path(@context, @context_enrollment.uuid) %>" class="btn btn-primary action"><i class="icon-end"></i> <span><%= t('links.drop_course', %{Drop this Course}) %></span></a>
<a href="#" class="btn dialog_closer"><%= t('#buttons.cancel', %{Cancel}) %></a>
</div>
</div>
<% elsif temp_type = session["role_course_#{@context.id}"] %>
@ -101,34 +93,23 @@ require([
'jqueryui/dialog',
'compiled/jquery/fixDialogButtons' /* fix dialog formatting */,
'jquery.loadingImg' /* loadingImg, loadingImage */,
'vendor/jquery.scrollTo' /* /\.scrollTo/ */
'vendor/jquery.scrollTo' /* /\.scrollTo/ */,
'compiled/behaviors/openAsDialog'
], function($) {
$(document).ready(function() {
var $selfUnenrollmentDialog = $("#self_unenrollment_dialog");
$(".self_unenrollment_link").click(function(event) {
$("#self_unenrollment_dialog").dialog({
title: <%= raw t('titles.drop_course', "Drop this Course").to_json %>
$selfUnenrollmentDialog.dialog({
title: <%= raw t('titles.drop_course', "Drop this Course").to_json %>,
}).fixDialogButtons();
});
$("#self_unenrollment_dialog .action").click(function() {
$("#self_unenrollment_dialog a.btn").attr('disabled', true);
$(this).find("span").text(<%= raw t('dropping_course', "Dropping Course...").to_json %>);
});
$("#self_unenrollment_dialog .cancel_button").click(function() {
$("#self_enrollment_dialog").dialog('close');
});
$(".self_enrollment_link").click(function(event) {
$("#self_enrollment_dialog").dialog({
title: <%= raw t('titles.join_course', "Join this Course").to_json %>
$selfUnenrollmentDialog.on('click', '.action', function() {
$selfUnenrollmentDialog.disableWhileLoading($.Deferred());
$.ajaxJSON($(this).attr('href'), 'POST', {}, function() {
window.location.reload();
});
});
$("#self_enrollment_dialog .action").click(function() {
$("#self_enrollment_dialog a.btn").attr('disabled', true);
$(this).find("span").text(<%= raw t('joining_course', "Joining Course...").to_json %>);
});
$("#self_enrollment_dialog .cancel_button").click(function() {
$("#self_enrollment_dialog").dialog('close');
});
$(".re_send_confirmation_link").click(function(event) {
event.preventDefault();
var $link = $(this);

View File

@ -50,8 +50,9 @@
<div class="controls">
<input type="hidden" name="user[initial_enrollment_type]" value="student">
<input type="hidden" name="self_enrollment" value="1">
<input type="hidden" name="pseudonym_type" value="username">
<button class="btn btn-primary" type="submit">{{#t "buttons.start_learning"}}Start Learning{{/t}}</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,8 @@
<div class="form-horizontal bootstrap-form" id="enroll_form">
<p><%= mt :already_enrolled, "You are already enrolled in **%{course}**.", :course => @course.name %></p>
<% if params[:login_warning] %>
<p><%= t :switch_users, "You are currently signed in as *%{user}*. **Sign in as another user**.", :user => @current_user.name, :wrapper => {'*' => '<b>\1</b>', '**' => link_to('\1', "/logout", :id => 'logout_link')} %></p>
<% end %>
</div>

View File

@ -0,0 +1,22 @@
<form action="<%= url_for(request.query_parameters) %>" method="post" id="enroll_form" class="form-horizontal bootstrap-form">
<%= registration_summary || content_tag(:p, mt(:getting_started, "You are enrolling in **%{course}**.", :course => @course.name)) %>
<p><%= mt :log_in, "Please enter your email and password:" %></p>
<input type="hidden" name="user_type" value="existing">
<div class="control-group">
<label class="control-label" for="student_email"><%= t "labels.email", "Email" %></label>
<div class="controls">
<input type="text" id="student_email" name="pseudonym[unique_id]">
</div>
</div>
<div class="control-group">
<label class="control-label" for="student_password"><%= t "labels.password", "Password" %></label>
<div class="controls">
<input type="password" id="student_password" name="pseudonym[password]">
</div>
</div>
<div class="control-group embedded-footer">
<div class="controls">
<button class="btn btn-primary" type="submit"><%= t "buttons.enroll_in_course", "Enroll in Course" %></button>
</div>
</div>
</form>

View File

@ -0,0 +1,61 @@
<form action="<%= url_for(request.query_parameters) %>" method="post" id="enroll_form" class="form-horizontal bootstrap-form">
<%= registration_summary || content_tag(:p, mt(:getting_started, "You are enrolling in **%{course}**.", :course => @course.name)) %>
<p><%= t(:enter_email, "Please enter your email address:") %></p>
<input type="hidden" name="user[initial_enrollment_type]" value="student">
<input type="hidden" name="self_enrollment" value="1">
<input type="hidden" name="self_enrollment_mode" value="email">
<input type="hidden" name="user[self_enrollment_code]" value="<%= params[:self_enrollment_code] %>">
<div class="control-group" id="email_info">
<label class="control-label" for="student_email"><%= t "labels.email", "Email" %></label>
<div class="controls">
<input type="text" id="student_email" name="pseudonym[unique_id]">
</div>
</div>
<div class="control-group" id="user_type">
<div class="controls">
<label class="radio">
<input type="radio" name="user_type" value="new" id="user_type_new">
<%= t "new_user", "I am a new user" %>
</label>
<label class="radio">
<input type="radio" name="user_type" value="existing" id="user_type_existing">
<%= t "existing_user", "I already have a %{institution_name} login", :institution_name => @domain_root_account.short_name %>
</label>
</div>
</div>
<div class="user_info" id="existing_user_info" style="<%= hidden %>">
<div class="control-group">
<label class="control-label" for="student_password"><%= t "labels.password", "Password" %></label>
<div class="controls">
<input type="password" id="student_password" name="pseudonym[password]">
</div>
</div>
</div>
<div class="user_info" id="new_user_info" style="<%= hidden %>">
<div class="control-group">
<label class="control-label" for="student_name"><%= t "labels.name", "Full Name" %></label>
<div class="controls">
<input type="text" id="student_name" name="user[name]">
</div>
</div>
<div class="control-group">
<label class="control-label" for="student_birthdate"><%= t "labels.birthdate", "Birth Date" %></label>
<div class="controls">
<%= date_select 'user', 'birthdate', {:start_year => Time.now.year - 1, :end_year => Time.now.year - 125, :include_blank => true}, :class => 'date-select', :id => 'student_birthdate' %>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="user[terms_of_use]" value="1">
<%= t "agree_to_terms", "You agree to the *terms of use*.", :wrapper => link_to('\1', "http://www.instructure.com/terms-of-use", :target => "_new") %>
</label>
</div>
</div>
</div>
<div class="control-group embedded-footer">
<div class="controls">
<button class="btn btn-primary" style="visibility: hidden" id="submit_button" type="submit"><%= t "buttons.enroll_in_course", "Enroll in Course" %></button>
</div>
</div>
</form>

View File

@ -0,0 +1,12 @@
<form action="<%= url_for(request.query_parameters) %>" method="post" id="enroll_form" class="form-horizontal bootstrap-form">
<input type="hidden" name="user_type" value="authenticated">
<%= registration_summary || content_tag(:p, mt(:getting_started, "You are enrolling in **%{course}**", :course => @course.name)) %>
<% if params[:login_warning] %>
<p><%= t :switch_users, "You are currently signed in as *%{user}*. **Sign in as another user**.", :user => @current_user.name, :wrapper => {'*' => '<b>\1</b>', '**' => link_to('\1', "/logout", :id => 'logout_link')} %></p>
<% end %>
<div class="control-group embedded-footer">
<div class="controls">
<button class="btn btn-primary" type="submit"><%= t "buttons.enroll_in_course", "Enroll in Course" %></button>
</div>
</div>
</form>

View File

@ -0,0 +1,22 @@
<div class="form-horizontal bootstrap-form" id="enroll_form">
<p><%= mt :already_enrolled, "You have successfully enrolled in **%{course}**.", :course => @course.name %></p>
<% if @course.available? %>
<%= registration_summary %>
<div class="control-group embedded-footer">
<div class="controls">
<%= @extra_actions %>
<a class="btn" href="<%= dashboard_url(:registration_success => params[:just_created]) %>" target="_top"><%= t "buttons.go_to_dashboard", "Go to your Dashboard" %></a>
<a class="btn btn-primary" href="<%= course_url(@course, :registration_success => params[:just_created]) %>" target="_top"><%= t "buttons.go_to_course", "Go to the Course" %></a>
</div>
</div>
<% else %>
<%= registration_summary || content_tag(:p, t(:not_available_yet, "We'll send you an email shortly before the course begins.")) %>
<div class="control-group embedded-footer">
<div class="controls">
<%= @extra_actions %>
<a class="btn btn-primary" href="<%= dashboard_url(:registration_success => params[:just_created]) %>" target="_top"><%= t "buttons.go_to_dashboard", "Go to your Dashboard" %></a>
</div>
</div>
<% end %>
</div>

View File

@ -0,0 +1,34 @@
<%
content_for :page_title, t('titles.course_enrollment', 'Enroll in %{course}', :course => @course.name)
js_bundle :self_enrollment
if !@current_user && !@embedded_view
jammit_css :login
@headers = false
@body_classes << "modal"
end
%>
<div id="modal-box">
<% if !@embedded_view %>
<h2><%= t :course_enrollment, "Course Enrollment" %></h2>
<% end %>
<% if @current_user %>
<% if @course.user_is_student?(@current_user, :include_future => true) %>
<% if params[:enrolled] %>
<%= render :partial => 'successfully_enrolled' %>
<% else %>
<%= render :partial => 'already_enrolled' %>
<% end %>
<% else %>
<%= render :partial => 'confirm_enrollments' %>
<% end %>
<% else %>
<% if @domain_root_account.open_registration? %>
<%= render :partial => 'authenticate_or_register' %>
<% else %>
<%= render :partial => 'authenticate' %>
<% end %>
<% end %>
</div>

View File

@ -11,12 +11,7 @@
<%= t('welcome_to_happiness', 'Welcome to Canvas!') %>
</div>
<div class="message">
<p><%= t 'no_courses_message', <<-BODY
You don't have any courses, so this page won't be very exciting for now.
Once you've created or signed up for courses, you'll start to see
conversations from all of your classes.
BODY
%></p>
<p><%= welcome_message %></p>
</div>
</div>
</div>

View File

@ -146,8 +146,8 @@ ActionController::Routing::Routes.draw do |map|
# and the application_helper method :context_url to make retrieving
# these contexts, and also generating context-specific urls, easier.
map.resources :courses do |course|
course.self_enrollment 'self_enrollment/:self_enrollment', :controller => 'courses', :action => 'self_enrollment'
course.self_unenrollment 'self_unenrollment/:self_unenrollment', :controller => 'courses', :action => 'self_unenrollment'
course.self_enrollment 'self_enrollment/:self_enrollment', :controller => 'courses', :action => 'self_enrollment', :conditions => {:method => :get}
course.self_unenrollment 'self_unenrollment/:self_unenrollment', :controller => 'courses', :action => 'self_unenrollment', :conditions => {:method => :post}
course.restore 'restore', :controller => 'courses', :action => 'restore'
course.backup 'backup', :controller => 'courses', :action => 'backup'
course.unconclude 'unconclude', :controller => 'courses', :action => 'unconclude'
@ -503,6 +503,8 @@ ActionController::Routing::Routes.draw do |map|
map.register "register", :controller => "users", :action => "new"
map.register_from_website "register_from_website", :controller => "users", :action => "new"
map.registered "registered", :controller => "users", :action => "registered"
map.enroll 'enroll/:self_enrollment_code', :controller => 'self_enrollments', :action => 'new', :conditions => {:method => :get}
map.enroll_frd 'enroll/:self_enrollment_code', :controller => 'self_enrollments', :action => 'create', :conditions => {:method => :post}
map.services 'services', :controller => 'users', :action => 'services'
map.bookmark_search 'search/bookmarks', :controller => 'users', :action => 'bookmark_search'
map.search_rubrics 'search/rubrics', :controller => "search", :action => "rubrics"

View File

@ -420,8 +420,12 @@ def self.date_component(start_date, style=:normal)
end
end
translated = t(*args)
translated = ERB::Util.h(translated) unless translated.html_safe?
result = RDiscount.new(translated).to_html.strip
markdown(translated, inlinify)
end
def markdown(string, inlinify = :auto)
string = ERB::Util.h(string) unless string.html_safe?
result = RDiscount.new(string).to_html.strip
# Strip wrapping <p></p> if inlinify == :auto && they completely wrap the result && there are not multiple <p>'s
result.gsub!(/<\/?p>/, '') if inlinify == :auto && result =~ /\A<p>.*<\/p>\z/m && !(result =~ /.*<p>.*<p>.*/m)
result.html_safe.strip

View File

@ -69,7 +69,8 @@ define([
url: url,
dataType: "json",
type: submit_type,
success: function(data) {
success: function(data, textStatus, xhr) {
updateCSRFToken(xhr);
data = data || {};
var page_view_id = null;
if(xhr && xhr.getResponseHeader && (page_view_id = xhr.getResponseHeader("X-Canvas-Page-View-Id"))) {
@ -88,9 +89,12 @@ define([
success(data, xhr);
}
},
error: function() {
error: function(xhr) {
updateCSRFToken(xhr);
ajaxError.apply(this, arguments);
},
complete: function(xhr) {
},
data: data
};
if(options && options.timeout) {
@ -121,6 +125,17 @@ define([
return null;
};
function updateCSRFToken(xhr) {
// in case the server has generated a new one, e.g. session reset on
// login actions
var token = xhr.getResponseHeader('X-CSRF-Token');
if (token) {
ENV.AUTHENTICITY_TOKEN = token;
// TODO: stop using me
$("#ajax_authenticity_token").text(token);
}
}
// Defines a default error for all ajax requests. Will always be called
// in the development environment, and as a last-ditch error catching
// otherwise. See "ajax_errors.js"

View File

@ -685,59 +685,21 @@ describe CoursesController do
Account.default.update_attribute(:settings, :self_enrollment => 'any', :open_registration => true)
end
it "should enroll the currently logged in user" do
it "should redirect to the new self enrollment form" do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
user
user_session(@user, @pseudonym)
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.self_enrollment_code.dup
response.should redirect_to(course_url(@course))
flash[:notice].should_not be_empty
@user.enrollments.length.should == 1
@enrollment = @user.enrollments.first
@enrollment.course.should == @course
@enrollment.workflow_state.should == 'active'
@enrollment.should be_self_enrolled
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.self_enrollment_code
response.should redirect_to(enroll_url(@course.self_enrollment_code))
end
it "should enroll the currently logged in user using the long code" do
it "should redirect to the new self enrollment form if using a long code" do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
user
user_session(@user, @pseudonym)
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.long_self_enrollment_code.dup
response.should redirect_to(course_url(@course))
flash[:notice].should_not be_empty
@user.enrollments.length.should == 1
@enrollment = @user.enrollments.first
@enrollment.course.should == @course
@enrollment.workflow_state.should == 'active'
@enrollment.should be_self_enrolled
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.long_self_enrollment_code
response.should redirect_to(enroll_url(@course.self_enrollment_code))
end
it "should create a compatible pseudonym" do
@account2 = Account.create!
course(:active_all => true, :account => @account2)
@course.update_attribute(:self_enrollment, true)
user_with_pseudonym(:active_all => 1, :username => 'jt@instructure.com')
user_session(@user, @pseudonym)
@new_pseudonym = Pseudonym.new(:account => @account2, :unique_id => 'jt@instructure.com', :user => @user)
User.any_instance.stubs(:find_or_initialize_pseudonym_for_account).with(@account2).once.returns(@new_pseudonym)
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.self_enrollment_code.dup
response.should redirect_to(course_url(@course))
flash[:notice].should_not be_empty
@user.enrollments.length.should == 1
@enrollment = @user.enrollments.first
@enrollment.course.should == @course
@enrollment.workflow_state.should == 'active'
@enrollment.should be_self_enrolled
@user.reload.pseudonyms.length.should == 2
end
it "should not enroll for incorrect code" do
it "should return to the course page for an incorrect code" do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
user
@ -748,59 +710,24 @@ describe CoursesController do
@user.enrollments.length.should == 0
end
it "should not enroll if self_enrollment is disabled" do
it "should return to the course page if self_enrollment is disabled" do
course(:active_all => true)
user
user_session(@user)
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.long_self_enrollment_code.dup
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.long_self_enrollment_code
response.should redirect_to(course_url(@course))
@user.enrollments.length.should == 0
end
it "should redirect to login without open registration" do
Account.default.update_attribute(:settings, :open_registration => false)
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.self_enrollment_code.dup
response.should redirect_to(login_url)
end
it "should render for non-logged-in user" do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
get 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.self_enrollment_code.dup
response.should be_success
response.should render_template('open_enrollment')
end
it "should create a creation_pending user" do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
post 'self_enrollment', :course_id => @course.id, :self_enrollment => @course.self_enrollment_code.dup, :email => 'bracken@instructure.com'
response.should be_success
response.should render_template('open_enrollment_confirmed')
@course.student_enrollments.length.should == 1
@enrollment = @course.student_enrollments.first
@enrollment.should be_self_enrolled
@enrollment.should be_invited
@enrollment.user.should be_creation_pending
@enrollment.user.email_channel.path.should == 'bracken@instructure.com'
@enrollment.user.email_channel.should be_unconfirmed
@enrollment.user.pseudonyms.should be_empty
end
end
describe "GET 'self_unenrollment'" do
describe "POST 'self_unenrollment'" do
it "should unenroll" do
course_with_student_logged_in(:active_all => true)
@enrollment.update_attribute(:self_enrolled, true)
get 'self_unenrollment', :course_id => @course.id, :self_unenrollment => @enrollment.uuid
response.should redirect_to(course_url(@course))
post 'self_unenrollment', :course_id => @course.id, :self_unenrollment => @enrollment.uuid
response.should be_success
@enrollment.reload
@enrollment.should be_completed
end
@ -809,8 +736,8 @@ describe CoursesController do
course_with_student_logged_in(:active_all => true)
@enrollment.update_attribute(:self_enrolled, true)
get 'self_unenrollment', :course_id => @course.id, :self_unenrollment => 'abc'
response.should redirect_to(course_url(@course))
post 'self_unenrollment', :course_id => @course.id, :self_unenrollment => 'abc'
response.status.should =~ /400 Bad Request/
@enrollment.reload
@enrollment.should be_active
end
@ -818,8 +745,8 @@ describe CoursesController do
it "should not unenroll a non-self-enrollment" do
course_with_student_logged_in(:active_all => true)
get 'self_unenrollment', :course_id => @course.id, :self_unenrollment => @enrollment.uuid
response.should redirect_to(course_url(@course))
post 'self_unenrollment', :course_id => @course.id, :self_unenrollment => @enrollment.uuid
response.status.should =~ /400 Bad Request/
@enrollment.reload
@enrollment.should be_active
end

View File

@ -0,0 +1,96 @@
#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe SelfEnrollmentsController do
describe "GET 'new'" do
before do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
end
it "should render if the course is open for enrollment" do
get 'new', :self_enrollment_code => @course.self_enrollment_code
response.should be_success
end
it "should not render for an incorrect code" do
lambda {
get 'new', :self_enrollment_code => 'abc'
}.should raise_exception(ActiveRecord::RecordNotFound)
end
it "should not render if self_enrollment is disabled" do
code = @course.self_enrollment_code
@course.update_attribute(:self_enrollment, false)
lambda {
get 'new', :self_enrollment_code => code
}.should raise_exception(ActiveRecord::RecordNotFound)
end
end
describe "POST 'create'" do
before do
Account.default.update_attribute(:settings, :self_enrollment => 'any', :open_registration => true)
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
end
it "should enroll the currently logged in user" do
user
user_session(@user, @pseudonym)
post 'create', :self_enrollment_code => @course.self_enrollment_code
response.should be_success
@user.enrollments.length.should == 1
@enrollment = @user.enrollments.first
@enrollment.course.should == @course
@enrollment.workflow_state.should == 'active'
@enrollment.should be_self_enrolled
end
it "should not enroll an unauthenticated user" do
post 'create', :self_enrollment_code => @course.self_enrollment_code
response.should redirect_to(login_url)
end
it "should not enroll for an incorrect code" do
user
user_session(@user)
lambda {
post 'create', :self_enrollment_code => 'abc'
}.should raise_exception(ActiveRecord::RecordNotFound)
@user.enrollments.length.should == 0
end
it "should not enroll if self_enrollment is disabled" do
code = @course.self_enrollment_code
@course.update_attribute(:self_enrollment, false)
user
user_session(@user)
lambda {
post 'create', :self_enrollment_code => code
}.should raise_exception(ActiveRecord::RecordNotFound)
@user.enrollments.length.should == 0
end
end
end

View File

@ -287,6 +287,20 @@ describe UsersController do
response.should be_success
end
it "should require email pseudonyms by default" do
post 'create', :pseudonym => { :unique_id => 'jacob' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1' }
response.status.should =~ /400 Bad Request/
json = JSON.parse(response.body)
json["errors"]["pseudonym"]["unique_id"].should be_present
end
it "should require email pseudonyms if not self enrolling" do
post 'create', :pseudonym => { :unique_id => 'jacob' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1' }, :pseudonym_type => 'username'
response.status.should =~ /400 Bad Request/
json = JSON.parse(response.body)
json["errors"]["pseudonym"]["unique_id"].should be_present
end
it "should validate the self enrollment code" do
post 'create', :pseudonym => { :unique_id => 'jacob@instructure.com', :password => 'asdfasdf', :password_confirmation => 'asdfasdf' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1', :birthdate => 20.years.ago.strftime('%Y-%m-%d'), :self_enrollment_code => 'omg ... not valid', :initial_enrollment_type => 'student' }, :self_enrollment => '1'
response.status.should =~ /400 Bad Request/
@ -302,11 +316,22 @@ describe UsersController do
u.pseudonym.should be_password_auto_generated
end
it "should require a password if self enrolling" do
it "should ignore the password if self enrolling with an email pseudonym" do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
post 'create', :pseudonym => { :unique_id => 'jacob@instructure.com' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1', :birthdate => 20.years.ago.strftime('%Y-%m-%d'), :self_enrollment_code => @course.self_enrollment_code, :initial_enrollment_type => 'student' }, :self_enrollment => '1'
post 'create', :pseudonym => { :unique_id => 'jacob@instructure.com', :password => 'asdfasdf', :password_confirmation => 'asdfasdf' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1', :birthdate => 20.years.ago.strftime('%Y-%m-%d'), :self_enrollment_code => @course.self_enrollment_code, :initial_enrollment_type => 'student' }, :pseudonym_type => 'email', :self_enrollment => '1'
response.should be_success
u = User.find_by_name 'Jacob Fugal'
u.should be_pre_registered
u.pseudonym.should be_password_auto_generated
end
it "should require a password if self enrolling with a non-email pseudonym" do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
post 'create', :pseudonym => { :unique_id => 'jacob' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1', :birthdate => 20.years.ago.strftime('%Y-%m-%d'), :self_enrollment_code => @course.self_enrollment_code, :initial_enrollment_type => 'student' }, :pseudonym_type => 'username', :self_enrollment => '1'
response.status.should =~ /400 Bad Request/
json = JSON.parse(response.body)
json["errors"]["pseudonym"]["password"].should be_present
@ -317,7 +342,7 @@ describe UsersController do
course(:active_all => true)
@course.update_attribute(:self_enrollment, true)
post 'create', :pseudonym => { :unique_id => 'jacob@instructure.com', :password => 'asdfasdf', :password_confirmation => 'asdfasdf' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1', :birthdate => 20.years.ago.strftime('%Y-%m-%d'), :self_enrollment_code => @course.self_enrollment_code, :initial_enrollment_type => 'student' }, :self_enrollment => '1'
post 'create', :pseudonym => { :unique_id => 'jacob', :password => 'asdfasdf', :password_confirmation => 'asdfasdf' }, :user => { :name => 'Jacob Fugal', :terms_of_use => '1', :birthdate => 20.years.ago.strftime('%Y-%m-%d'), :self_enrollment_code => @course.self_enrollment_code, :initial_enrollment_type => 'student' }, :pseudonym_type => 'username', :self_enrollment => '1'
response.should be_success
u = User.find_by_name 'Jacob Fugal'
@course.students.should include(u)
@ -361,6 +386,20 @@ describe UsersController do
p.user.should be_pre_registered
end
it "should create users with non-email pseudonyms" do
account = Account.create!
user_with_pseudonym(:account => account)
account.add_user(@user)
user_session(@user, @pseudonym)
post 'create', :format => 'json', :account_id => account.id, :pseudonym => { :unique_id => 'jacob', :sis_user_id => 'testsisid' }, :user => { :name => 'Jacob Fugal' }
response.should be_success
p = Pseudonym.find_by_unique_id('jacob')
p.account_id.should == account.id
p.should be_active
p.sis_user_id.should == 'testsisid'
p.user.should be_pre_registered
end
it "should not allow an admin to set the sis id when creating a user if they don't have privileges to manage sis" do
account = Account.create!
admin = account_admin_user_with_role_changes(:account => account, :role_changes => {'manage_sis' => false})

View File

@ -37,7 +37,7 @@ describe "site-wide" do
end
it "should set the x-ua-compatible http header" do
get "/"
get "/login"
response['x-ua-compatible'].should == "IE=edge,chrome=1"
end
@ -60,8 +60,10 @@ describe "site-wide" do
end
it "should not set x-frame-options when on a files domain" do
user_session user(:active_all => true)
attachment_model(:context => @user)
FilesController.any_instance.expects(:files_domain?).returns(true)
get "http://files-test.host/files/1/download"
get "http://files-test.host/files/#{@attachment.id}/download"
response['x-frame-options'].should be_nil
end

View File

@ -0,0 +1,127 @@
require File.expand_path(File.dirname(__FILE__) + '/common')
describe "self enrollment" do
it_should_behave_like "in-process server selenium tests"
shared_examples_for "open registration" do
before do
Account.default.update_attribute(:settings, :self_enrollment => 'any', :open_registration => true)
course(:active_all => active_course)
@course.update_attribute(:self_enrollment, true)
end
it "should register a new user" do
get "/enroll/#{@course.self_enrollment_code}"
f("#student_email").send_keys('new@example.com')
f('#user_type_new').click
f("#student_name").send_keys('new guy')
f("#enroll_form select[name='user[birthdate(1i)]'] option[value='#{Time.now.year - 20}']").click
f("#enroll_form select[name='user[birthdate(2i)]'] option[value='1']").click
f("#enroll_form select[name='user[birthdate(3i)]'] option[value='1']").click
f('#enroll_form input[name="user[terms_of_use]"]').click
expect_new_page_load {
submit_form("#enroll_form")
}
f('.btn-primary').text.should eql primary_action
get "/"
assert_valid_dashboard
end
it "should authenticate and register an existing user" do
user_with_pseudonym(:active_all => true, :username => "existing@example.com", :password => "asdfasdf")
get "/enroll/#{@course.self_enrollment_code}"
f("#student_email").send_keys("existing@example.com")
f('#user_type_existing').click
f("#student_password").send_keys("asdfasdf")
expect_new_page_load {
submit_form("#enroll_form")
}
f('.btn-primary').text.should eql primary_action
get "/"
assert_valid_dashboard
end
it "should register an authenticated user" do
user_logged_in
get "/enroll/#{@course.self_enrollment_code}"
# no option to log in/register, since already authenticated
f("input[name='pseudonym[unique_id]']").should be_nil
expect_new_page_load {
submit_form("#enroll_form")
}
f('.btn-primary').text.should eql primary_action
get "/"
assert_valid_dashboard
end
end
shared_examples_for "closed registration" do
before do
course(:active_all => active_course)
@course.update_attribute(:self_enrollment, true)
end
it "should not register a new user" do
get "/enroll/#{@course.self_enrollment_code}"
f("input[type=radio][name=user_type]").should be_nil
f("input[name='user[name]']").should be_nil
end
it "should authenticate and register an existing user" do
user_with_pseudonym(:active_all => true, :username => "existing@example.com", :password => "asdfasdf")
get "/enroll/#{@course.self_enrollment_code}"
f("#student_email").send_keys("existing@example.com")
f("#student_password").send_keys("asdfasdf")
expect_new_page_load {
submit_form("#enroll_form")
}
f('.btn-primary').text.should eql primary_action
get "/"
assert_valid_dashboard
end
it "should register an authenticated user" do
user_logged_in
get "/enroll/#{@course.self_enrollment_code}"
# no option to log in/register, since already authenticated
f("input[name='pseudonym[unique_id]']").should be_nil
expect_new_page_load {
submit_form("#enroll_form")
}
f('.btn-primary').text.should eql primary_action
get "/"
assert_valid_dashboard
end
end
context "in a published course" do
let(:active_course){ true }
let(:primary_action){ "Go to the Course" }
let(:assert_valid_dashboard) {
f('#courses_menu_item').should include_text("Courses")
}
context "with open registration" do
it_should_behave_like "open registration"
end
context "without open registration" do
it_should_behave_like "closed registration"
end
end
context "in an unpublished course" do
let(:active_course){ false }
let(:primary_action){ "Go to your Dashboard" }
let(:assert_valid_dashboard) {
f('#courses_menu_item').should include_text("Home")
f('#dashboard').should include_text("You've enrolled in one or more courses that have not started yet")
}
context "with open registration" do
it_should_behave_like "open registration"
end
context "without open registration" do
it_should_behave_like "closed registration"
end
end
end

View File

@ -202,9 +202,9 @@ describe "users" do
form = fj('.ui-dialog:visible form')
f('#student_join_code').send_keys(@course.self_enrollment_code)
f('#student_name').send_keys('student!')
form.find_element(:css, "select[name='user[birthdate(1)]'] option[value='#{Time.now.year - 20}']").click
form.find_element(:css, "select[name='user[birthdate(2)]'] option[value='1']").click
form.find_element(:css, "select[name='user[birthdate(3)]'] option[value='1']").click
form.find_element(:css, "select[name='user[birthdate(1i)]'] option[value='#{Time.now.year - 20}']").click
form.find_element(:css, "select[name='user[birthdate(2i)]'] option[value='1']").click
form.find_element(:css, "select[name='user[birthdate(3i)]'] option[value='1']").click
f('#student_username').send_keys('student')
f('#student_password').send_keys('asdfasdf')
f('#student_password_confirmation').send_keys('asdfasdf')
@ -225,9 +225,9 @@ describe "users" do
form = fj('.ui-dialog:visible form')
f('#student_higher_ed_name').send_keys('student!')
f('#student_higher_ed_email').send_keys('student@example.com')
form.find_element(:css, "select[name='user[birthdate(1)]'] option[value='#{Time.now.year - 20}']").click
form.find_element(:css, "select[name='user[birthdate(2)]'] option[value='1']").click
form.find_element(:css, "select[name='user[birthdate(3)]'] option[value='1']").click
form.find_element(:css, "select[name='user[birthdate(1i)]'] option[value='#{Time.now.year - 20}']").click
form.find_element(:css, "select[name='user[birthdate(2i)]'] option[value='1']").click
form.find_element(:css, "select[name='user[birthdate(3i)]'] option[value='1']").click
form.find_element(:css, 'input[name="user[terms_of_use]"]').click
expect_new_page_load { form.submit }