2011-02-01 09:57:29 +08:00
#
2012-03-16 13:40:42 +08:00
# Copyright (C) 2011 - 2012 Instructure, Inc.
2011-02-01 09:57:29 +08:00
#
# 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 User < ActiveRecord :: Base
2012-02-02 07:35:28 +08:00
# this has to be before include Context to prevent a circular dependency in Course
2011-10-27 05:09:09 +08:00
def self . sortable_name_order_by_clause ( table = nil )
col = table ? " #{ table } .sortable_name " : 'sortable_name'
2012-05-03 03:43:00 +08:00
best_unicode_collation_key ( col )
2011-10-27 05:09:09 +08:00
end
2011-02-01 09:57:29 +08:00
include Context
2012-06-07 04:16:55 +08:00
include UserFollow :: FollowedItem
2011-02-01 09:57:29 +08:00
2012-08-11 06:48:00 +08:00
attr_accessible :name , :short_name , :sortable_name , :time_zone , :show_user_services , :gender , :visible_inbox_types , :avatar_image , :subscribe_to_emails , :locale , :bio , :birthdate , :terms_of_use , :self_enrollment_code , :initial_enrollment_type
2011-09-27 03:08:41 +08:00
attr_accessor :original_id , :menu_data
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
before_save :infer_defaults
serialize :preferences
include Workflow
2012-05-23 03:55:23 +08:00
# Internal: SQL fragments used to return enrollments in their respective workflow
# states. Where needed, these consider the state of the course to ensure that
# students do not see their enrollments on unpublished courses.
2012-08-04 17:41:44 +08:00
#
# strict_course_state can be used to bypass the course state checks. This is
# useful in places like the course settings UI, where we use these conditions
# to search users in the course (rather than as an association on a
# particular user)
def self . enrollment_conditions ( state , strict_course_state = true )
#strict_course_state = true
case state
when :active
if strict_course_state
" ( enrollments.workflow_state = 'active' and ((courses.workflow_state = 'claimed' and enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment')) or (courses.workflow_state = 'available')) ) "
else
" ( enrollments.workflow_state = 'active' and courses.workflow_state != 'deleted' ) "
end
when :invited
if strict_course_state
" ( enrollments.workflow_state = 'invited' and ((courses.workflow_state = 'available' and (enrollments.type = 'StudentEnrollment' or enrollments.type = 'ObserverEnrollment')) or (courses.workflow_state != 'deleted' and (enrollments.type IN ('TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment', 'StudentViewEnrollment'))))) "
else
" ( enrollments.workflow_state IN ('invited', 'creation_pending') and courses.workflow_state != 'deleted' ) "
end
when :deleted ; " enrollments.workflow_state = 'deleted' "
when :rejected ; " enrollments.workflow_state = 'rejected' "
when :completed ; " enrollments.workflow_state = 'completed' "
when :creation_pending ; " enrollments.workflow_state = 'creation_pending' "
when :inactive ; " enrollments.workflow_state = 'inactive' "
when :current_and_invited
enrollment_conditions ( :active , strict_course_state ) +
" OR " +
enrollment_conditions ( :invited , strict_course_state )
end
end
2012-05-23 03:55:23 +08:00
2012-07-19 07:18:29 +08:00
has_many :communication_channels , :order = > 'communication_channels.position ASC' , :dependent = > :destroy
2011-02-01 09:57:29 +08:00
has_one :communication_channel , :order = > 'position'
has_many :enrollments , :dependent = > :destroy
2012-01-04 04:30:49 +08:00
2012-08-04 17:41:44 +08:00
has_many :current_enrollments , :class_name = > 'Enrollment' , :include = > [ :course , :course_section ] , :conditions = > enrollment_conditions ( :active ) , :order = > 'enrollments.created_at'
has_many :invited_enrollments , :class_name = > 'Enrollment' , :include = > [ :course , :course_section ] , :conditions = > enrollment_conditions ( :invited ) , :order = > 'enrollments.created_at'
2012-01-04 04:30:49 +08:00
has_many :current_and_invited_enrollments , :class_name = > 'Enrollment' , :include = > [ :course ] , :order = > 'enrollments.created_at' ,
2012-08-04 17:41:44 +08:00
:conditions = > enrollment_conditions ( :current_and_invited )
2012-08-03 05:56:20 +08:00
has_many :not_ended_enrollments , :class_name = > 'Enrollment' , :conditions = > " enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted') " , :order = > 'enrollments.created_at'
2012-08-04 17:41:44 +08:00
has_many :concluded_enrollments , :class_name = > 'Enrollment' , :include = > [ :course , :course_section ] , :conditions = > enrollment_conditions ( :completed ) , :order = > 'enrollments.created_at'
2012-05-17 01:22:27 +08:00
has_many :observer_enrollments
has_many :observee_enrollments , :foreign_key = > :associated_user_id , :class_name = > 'ObserverEnrollment'
has_many :user_observers , :dependent = > :delete_all
has_many :observers , :through = > :user_observers , :class_name = > 'User'
has_many :user_observees , :class_name = > 'UserObserver' , :foreign_key = > :observer_id , :dependent = > :delete_all
has_many :observed_users , :through = > :user_observees , :source = > :user
2012-02-04 01:31:36 +08:00
has_many :courses , :through = > :current_enrollments , :uniq = > true
2011-11-01 03:42:10 +08:00
has_many :current_and_invited_courses , :source = > :course , :through = > :current_and_invited_enrollments
2012-04-26 05:57:43 +08:00
has_many :concluded_courses , :source = > :course , :through = > :concluded_enrollments , :uniq = > true
2012-01-04 04:30:49 +08:00
has_many :all_courses , :source = > :course , :through = > :enrollments
2012-06-30 04:37:05 +08:00
has_many :current_and_concluded_enrollments , :class_name = > 'Enrollment' , :include = > [ :course , :course_section ] ,
2012-08-04 17:41:44 +08:00
:conditions = > [ enrollment_conditions ( :active ) , enrollment_conditions ( :completed ) ] . join ( ' OR ' ) , :order = > 'enrollments.created_at'
2012-06-30 04:37:05 +08:00
has_many :current_and_concluded_courses , :source = > :course , :through = > :current_and_concluded_enrollments , :uniq = > true
2011-02-01 09:57:29 +08:00
has_many :group_memberships , :include = > :group , :dependent = > :destroy
has_many :groups , :through = > :group_memberships
2012-02-16 05:40:13 +08:00
has_many :current_group_memberships , :include = > :group , :class_name = > 'GroupMembership' , :conditions = > " group_memberships.workflow_state = 'accepted' AND groups.workflow_state <> 'deleted' "
has_many :current_groups , :through = > :current_group_memberships , :source = > :group
2011-02-01 09:57:29 +08:00
has_many :user_account_associations
has_many :associated_accounts , :source = > :account , :through = > :user_account_associations , :order = > 'user_account_associations.depth'
has_many :associated_root_accounts , :source = > :account , :through = > :user_account_associations , :order = > 'user_account_associations.depth' , :conditions = > 'accounts.parent_account_id IS NULL'
2011-05-27 07:41:43 +08:00
has_many :developer_keys
has_many :access_tokens , :include = > :developer_key
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
has_many :student_enrollments
has_many :ta_enrollments
has_many :teacher_enrollments
has_many :submissions , :include = > [ :assignment , :submission_comments ] , :order = > 'submissions.updated_at DESC' , :dependent = > :destroy
has_many :pseudonyms_with_channels , :class_name = > 'Pseudonym' , :order = > 'position' , :include = > :communication_channels
has_many :pseudonyms , :order = > 'position' , :dependent = > :destroy
has_many :pseudonym_accounts , :source = > :account , :through = > :pseudonyms
2011-04-15 05:01:40 +08:00
has_one :pseudonym , :conditions = > [ 'pseudonyms.workflow_state != ?' , 'deleted' ] , :order = > 'position'
2011-02-01 09:57:29 +08:00
has_many :attachments , :as = > 'context' , :dependent = > :destroy
2011-07-12 02:25:54 +08:00
has_many :active_images , :as = > :context , :class_name = > 'Attachment' , :conditions = > [ " attachments.file_state != ? AND attachments.content_type LIKE 'image%' " , 'deleted' ] , :order = > 'attachments.display_name' , :include = > :thumbnail
2011-02-01 09:57:29 +08:00
has_many :active_assignments , :as = > :context , :class_name = > 'Assignment' , :conditions = > [ 'assignments.workflow_state != ?' , 'deleted' ]
has_many :all_attachments , :as = > 'context' , :class_name = > 'Attachment'
has_many :folders , :as = > 'context' , :order = > 'folders.name'
has_many :active_folders , :class_name = > 'Folder' , :as = > :context , :conditions = > [ 'folders.workflow_state != ?' , 'deleted' ] , :order = > 'folders.name'
2011-03-02 03:17:42 +08:00
has_many :active_folders_with_sub_folders , :class_name = > 'Folder' , :as = > :context , :include = > [ :active_sub_folders ] , :conditions = > [ 'folders.workflow_state != ?' , 'deleted' ] , :order = > 'folders.name'
2011-02-01 09:57:29 +08:00
has_many :active_folders_detailed , :class_name = > 'Folder' , :as = > :context , :include = > [ :active_sub_folders , :active_file_attachments ] , :conditions = > [ 'folders.workflow_state != ?' , 'deleted' ] , :order = > 'folders.name'
2012-01-04 04:30:49 +08:00
has_many :calendar_events , :as = > 'context' , :dependent = > :destroy , :include = > [ :parent_event ]
2011-02-01 09:57:29 +08:00
has_many :eportfolios , :dependent = > :destroy
has_many :quiz_submissions , :dependent = > :destroy
has_many :dashboard_messages , :class_name = > 'Message' , :conditions = > { :to = > " dashboard " , :workflow_state = > 'dashboard' } , :order = > 'created_at DESC' , :dependent = > :destroy
has_many :collaborations , :order = > 'created_at DESC'
has_many :user_services , :order = > 'created_at' , :dependent = > :destroy
has_one :scribd_account , :as = > :scribdable
has_many :rubric_associations , :as = > :context , :include = > :rubric , :order = > 'rubric_associations.created_at DESC'
has_many :rubrics
has_many :context_rubrics , :as = > :context , :class_name = > 'Rubric'
has_many :grading_standards
has_many :context_module_progressions
has_many :assignment_reminders
has_many :assessment_question_bank_users
has_many :assessment_question_banks , :through = > :assessment_question_bank_users
has_many :learning_outcome_results
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
has_many :inbox_items , :order = > 'created_at DESC'
has_many :submission_comment_participants
has_many :submission_comments , :through = > :submission_comment_participants , :include = > { :submission = > { :assignment = > { } , :user = > { } } }
has_many :collaborators
has_many :collaborations , :through = > :collaborators , :include = > [ :user , :collaborators ]
has_many :assigned_submission_assessments , :class_name = > 'AssessmentRequest' , :foreign_key = > 'assessor_id' , :include = > { :user = > { } , :submission = > :assignment }
has_many :assigned_assessments , :class_name = > 'AssessmentRequest' , :foreign_key = > 'assessor_id'
has_many :web_conference_participants
has_many :web_conferences , :through = > :web_conference_participants
has_many :account_users
has_many :media_objects , :as = > :context
has_many :user_generated_media_objects , :class_name = > 'MediaObject'
has_many :user_notes
has_many :account_reports
has_many :stream_item_instances , :dependent = > :delete_all
2012-02-01 09:31:23 +08:00
has_many :all_conversations , :class_name = > 'ConversationParticipant' , :include = > :conversation
2012-09-08 07:57:12 +08:00
has_many :conversation_batches , :include = > :root_conversation_message
2011-09-28 04:06:52 +08:00
has_many :favorites
2011-11-01 03:42:10 +08:00
has_many :favorite_courses , :source = > :course , :through = > :current_and_invited_enrollments , :conditions = > " EXISTS (SELECT 1 FROM favorites WHERE context_type = 'Course' AND context_id = enrollments.course_id AND user_id = enrollments.user_id) "
2011-12-10 06:37:12 +08:00
has_many :zip_file_imports , :as = > :context
2012-03-10 06:52:44 +08:00
has_many :messages
2011-09-28 04:06:52 +08:00
2012-05-26 05:26:17 +08:00
has_many :following_user_follows , :class_name = > 'UserFollow' , :as = > :followed_item
has_many :user_follows , :foreign_key = > 'following_user_id'
2012-05-05 04:16:03 +08:00
has_many :collections , :as = > :context
2012-06-02 06:45:39 +08:00
has_many :collection_items , :through = > :collections
2012-05-05 04:16:03 +08:00
2012-07-12 07:20:39 +08:00
has_one :profile , :class_name = > 'UserProfile'
alias :orig_profile :profile
multi-factor authentication closes #9532
test plan:
* enable optional MFA, and check the following:
* normal log in should not be affected
* you can enroll in MFA from your profile page
* you can re-enroll in MFA from your profile page
* you can disable MFA from your profile page
* MFA can be reset by an admin on your user page
* when enrolled, you are asked for verification code after
username/password when logging in
* you can't access any other part of the site directly until
until entering your verification code
* enable required MFA, and check the following
* when not enrolled in MFA, and you log in, you are forced to
enroll
* you cannot disable MFA from your profile page
* you can re-enroll in MFA from your profile page
* an admin (other than himself) can reset MFA from the user page
* for enrolling in MFA
* use Google Authenticator and scan the QR code; you should have
30-seconds or so of extra leeway to enter your code
* having no SMS communication channels on your profile, the
enrollment page should just have a form to add a new phone
* having one or more SMS communication channels on your profile,
the enrollment page should list them, or allow you to create
a new one (and switch back)
* having more than one SMS communication channel on your profile,
the enrollment page should remember which one you have selected
after you click "send"
* an unconfirmed SMS channel should go to confirmed when it's used
to enroll in MFA
* you should not be able to go directly to /login/otp to enroll
if you used "Remember me" token to log in
* MFA login flow
* if configured with SMS, it should send you an SMS after you
put in your username/password; you should have about 5 minutes
of leeway to put it in
* if you don't check "remember computer" checkbox, you should have
to enter a verification code each time you log in
* if you do check it, you shouldn't have to enter your code
anymore (for three days). it also shouldn't SMS you a
verification code each time you log in
* setting MFA to required for admins should make it required for
admins, optional for other users
* with MFA enabled, directly go to /login/otp after entering
username/password but before entering a verification code; it
should send you back to the main login page
* if you enrolled via SMS, you should not be able to remove that
SMS from your profile
* there should not be a reset MFA link on a user page if they
haven't enrolled
* test a login or required enrollment sequence with CAS and/or SAML
Change-Id: I692de7405bf7ca023183e717930ee940ccf0d5e6
Reviewed-on: https://gerrit.instructure.com/12700
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
2012-08-03 05:17:50 +08:00
belongs_to :otp_communication_channel , :class_name = > 'CommunicationChannel'
2011-09-22 01:36:45 +08:00
include StickySisFields
2011-10-27 05:09:09 +08:00
are_sis_sticky :name , :sortable_name , :short_name
2011-09-22 01:36:45 +08:00
2012-02-17 04:14:28 +08:00
def conversations
# i.e. exclude any where the user has deleted all the messages
all_conversations . visible . scoped ( :order = > " last_message_at DESC, conversation_id DESC " )
2011-08-19 14:15:43 +08:00
end
2011-02-01 09:57:29 +08:00
2012-09-25 04:05:43 +08:00
def page_views
PageView . for_user ( self )
end
2011-02-01 09:57:29 +08:00
named_scope :of_account , lambda { | account |
{
:joins = > :user_account_associations ,
:conditions = > [ 'user_account_associations.account_id = ?' , account . id ]
}
}
named_scope :recently_logged_in , lambda {
{
:include = > :pseudonyms ,
:conditions = > [ 'pseudonyms.current_login_at > ?' , 1 . month . ago ] ,
:order = > 'pseudonyms.current_login_at DESC' ,
:limit = > 25
}
}
named_scope :include_pseudonym , lambda {
{ :include = > :pseudonym }
}
2012-02-18 10:49:16 +08:00
named_scope :restrict_to_sections , lambda { | sections |
2011-02-01 09:57:29 +08:00
section_ids = Array ( sections ) . map { | s | s . is_a? ( Fixnum ) ? s : s . id }
2012-02-18 10:49:16 +08:00
if section_ids . empty?
{ :conditions = > { } }
else
{ :conditions = > [ " enrollments.limit_privileges_to_course_section IS NULL OR enrollments.limit_privileges_to_course_section != ? OR enrollments.course_section_id IN (?) " , true , section_ids ] }
end
2011-02-01 09:57:29 +08:00
}
named_scope :name_like , lambda { | name |
2011-07-08 05:53:21 +08:00
{ :conditions = > [ " ( " , wildcard ( 'users.name' , 'users.short_name' , name ) , " OR exists (select 1 from pseudonyms where " , wildcard ( 'pseudonyms.sis_user_id' , 'pseudonyms.unique_id' , name ) , " and pseudonyms.user_id = users.id and ( " , User . send ( :sanitize_sql_array , Pseudonym . active . proxy_options [ :conditions ] ) , " ))) " ] . join }
2011-02-01 09:57:29 +08:00
}
named_scope :active , lambda {
{ :conditions = > [ " users.workflow_state != ? " , 'deleted' ] }
}
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
named_scope :has_current_student_enrollments , :conditions = > " EXISTS (SELECT * FROM enrollments JOIN courses ON courses.id = enrollments.course_id AND courses.workflow_state = 'available' WHERE enrollments.user_id = users.id AND enrollments.workflow_state IN ('active','invited') AND enrollments.type = 'StudentEnrollment') "
2012-01-04 04:30:49 +08:00
2012-08-24 05:10:00 +08:00
# NOTE: if :order is passed in, sortable name will be tacked onto the end
# rather than prepending or replacing it
def self . order_by_sortable_name ( options = { } )
add_sort_key! ( options , sortable_name_order_by_clause )
uber_scope ( options )
end
def self . by_top_enrollment ( options = { } )
options [ :select ] || = " users.* "
options [ :select ] << " , MIN( #{ Enrollment . type_rank_sql ( :student ) } ) AS enrollment_rank "
options [ :group ] = User . connection . group_by ( User )
options [ :order ] = " enrollment_rank "
order_by_sortable_name ( options )
2012-05-03 03:43:00 +08:00
end
2012-01-04 04:30:49 +08:00
named_scope :enrolled_in_course_between , lambda { | course_ids , start_at , end_at |
2011-02-01 09:57:29 +08:00
ids_string = course_ids . join ( " , " )
{
:joins = > :enrollments ,
:conditions = > [ " enrollments.course_id in ( #{ ids_string } ) AND enrollments.created_at > ? AND enrollments.created_at < ? " , start_at , end_at ]
}
}
2012-01-04 04:30:49 +08:00
2012-02-15 04:26:39 +08:00
named_scope :for_course_with_last_login , lambda { | course , root_account_id , enrollment_type |
course_id = course . is_a? ( Course ) ? course . id : course
enrollment_conditions = sanitize_sql ( [ 'enrollments.course_id = ? AND enrollments.workflow_state != ?' , course_id , 'deleted' ] )
enrollment_conditions += sanitize_sql ( [ 'AND enrollments.type = ?' , enrollment_type ] ) if enrollment_type
{
# add a field to each user that is the aggregated max from current_login_at and last_login_at from their pseudonyms
:select = > 'users.*, MAX(current_login_at) as last_login, MAX(current_login_at) IS NULL as login_info_exists' ,
# left outer join ensures we get the user even if they don't have a pseudonym
:joins = > sanitize_sql ( [ <<-SQL, root_account_id]),
2012-09-12 01:30:23 +08:00
LEFT OUTER JOIN pseudonyms ON pseudonyms . user_id = users . id AND pseudonyms . account_id = ?
INNER JOIN enrollments ON enrollments . user_id = users . id
2012-02-15 04:26:39 +08:00
SQL
:conditions = > enrollment_conditions ,
# the trick to get unique users
:group = > 'users.id'
}
}
2011-02-01 09:57:29 +08:00
has_a_broadcast_policy
2012-05-30 07:35:00 +08:00
attr_accessor :require_acceptance_of_terms , :require_presence_of_name ,
2012-06-29 04:43:57 +08:00
:require_self_enrollment_code , :require_birthdate , :self_enrollment_code ,
2012-06-28 13:08:49 +08:00
:self_enrollment_course , :validation_root_account
2012-05-30 07:35:00 +08:00
2012-06-29 04:43:57 +08:00
# users younger than this age can't sign up without a course join code
def self . self_enrollment_min_age
13
end
2011-04-02 00:59:20 +08:00
validates_length_of :name , :maximum = > maximum_string_length , :allow_nil = > true
2012-09-18 03:46:35 +08:00
validates_length_of :short_name , :maximum = > maximum_string_length , :allow_nil = > true
validates_length_of :sortable_name , :maximum = > maximum_string_length , :allow_nil = > true
2012-05-30 07:35:00 +08:00
validates_presence_of :name , :if = > :require_presence_of_name
2011-07-13 04:31:40 +08:00
validates_locale :locale , :browser_locale , :allow_nil = > true
2012-05-30 07:35:00 +08:00
validates_acceptance_of :terms_of_use , :if = > :require_acceptance_of_terms , :allow_nil = > false
validates_each :birthdate do | record , attr , value |
2012-06-29 04:43:57 +08:00
next unless record . require_birthdate
2012-05-30 07:35:00 +08:00
if value
2012-06-29 04:43:57 +08:00
record . errors . add ( attr , " too_young " ) if ! record . require_self_enrollment_code && value > self_enrollment_min_age . years . ago
2012-05-30 07:35:00 +08:00
else
record . errors . add ( attr , " blank " )
end
end
validates_each :self_enrollment_code do | record , attr , value |
next unless record . require_self_enrollment_code
if value . blank?
record . errors . add ( attr , " blank " )
2012-06-28 13:08:49 +08:00
elsif record . validation_root_account
record . self_enrollment_course = record . validation_root_account . all_courses . find_by_self_enrollment_code ( value )
2012-05-30 07:35:00 +08:00
record . errors . add ( attr , " invalid " ) unless record . self_enrollment_course
else
record . errors . add ( attr , " account_required " )
end
end
2011-04-02 00:59:20 +08:00
2011-02-01 09:57:29 +08:00
before_save :assign_uuid
before_save :update_avatar_image
after_save :generate_reminders_if_changed
2012-02-04 05:46:14 +08:00
after_save :update_account_associations_if_necessary
2012-05-30 07:35:00 +08:00
after_save :self_enroll_if_necessary
2012-01-04 04:30:49 +08:00
2011-08-18 00:32:30 +08:00
def self . skip_updating_account_associations ( & block )
@skip_updating_account_associations = true
2011-05-04 03:21:18 +08:00
block . call
ensure
2011-08-18 00:32:30 +08:00
@skip_updating_account_associations = false
2011-05-04 03:21:18 +08:00
end
2011-08-18 00:32:30 +08:00
def self . skip_updating_account_associations?
! ! @skip_updating_account_associations
2011-05-04 03:21:18 +08:00
end
2012-01-04 04:30:49 +08:00
2011-05-04 03:21:18 +08:00
def update_account_associations_later
2011-08-18 00:32:30 +08:00
self . send_later_if_production ( :update_account_associations ) unless self . class . skip_updating_account_associations?
2011-05-04 03:21:18 +08:00
end
2012-01-04 04:30:49 +08:00
2012-02-04 05:46:14 +08:00
def update_account_associations_if_necessary
update_account_associations if ! self . class . skip_updating_account_associations? && self . workflow_state_changed?
end
2011-08-24 00:39:52 +08:00
def update_account_associations ( opts = { } )
User . update_account_associations ( [ self ] , opts )
2011-08-18 03:33:10 +08:00
end
def self . add_to_account_chain_cache ( account_id , account_chain_cache )
if account_id . is_a? Account
account = account_id
account_id = account . id
2011-02-01 09:57:29 +08:00
end
2011-08-18 03:33:10 +08:00
return account_chain_cache [ account_id ] if account_chain_cache . has_key? ( account_id )
account || = Account . find ( account_id )
return account_chain_cache [ account . id ] = [ account . id ] if account . root_account?
account_chain_cache [ account . id ] = [ account . id ] + add_to_account_chain_cache ( account . parent_account_id , account_chain_cache )
2011-02-01 09:57:29 +08:00
end
2011-08-18 03:33:10 +08:00
2011-08-24 00:39:52 +08:00
def self . calculate_account_associations_from_accounts ( starting_account_ids , account_chain_cache = { } )
2011-08-18 03:33:10 +08:00
results = { }
remaining_ids = [ ]
starting_account_ids . each do | account_id |
unless account_chain_cache . has_key? account_id
remaining_ids << account_id
next
end
account_chain = account_chain_cache [ account_id ]
account_chain . each_with_index do | account_id , idx |
results [ account_id ] || = idx
results [ account_id ] = idx if idx < results [ account_id ]
2011-02-01 09:57:29 +08:00
end
end
2011-08-18 03:33:10 +08:00
unless remaining_ids . empty?
accounts = Account . find_all_by_id ( remaining_ids )
accounts . each do | account |
account_chain = add_to_account_chain_cache ( account , account_chain_cache )
account_chain . each_with_index do | account_id , idx |
results [ account_id ] || = idx
results [ account_id ] = idx if idx < results [ account_id ]
end
end
2011-02-01 09:57:29 +08:00
end
2011-08-18 03:33:10 +08:00
results
end
# Users are tied to accounts a couple ways:
# Through enrollments:
# User -> Enrollment -> Section -> Course -> Account
# User -> Enrollment -> Section -> Non-Xlisted Course -> Account
# Through pseudonyms:
# User -> Pseudonym -> Account
# Through account_users
# User -> AccountUser -> Account
def calculate_account_associations ( account_chain_cache = { } )
2012-02-04 05:46:14 +08:00
return [ ] if %w{ creation_pending deleted } . include? ( self . workflow_state )
2011-08-18 03:33:10 +08:00
# Hopefully these have all been pre-loaded
starting_account_ids = self . enrollments . map { | e | e . workflow_state != 'deleted' ? [ e . course_section . course . account_id , e . course_section . nonxlist_course . try ( :account_id ) ] : nil } . flatten . compact
starting_account_ids += self . pseudonyms . map { | p | p . active? ? p . account_id : nil } . compact
starting_account_ids += self . account_users . map ( & :account_id )
starting_account_ids . uniq!
result = User . calculate_account_associations_from_accounts ( starting_account_ids , account_chain_cache )
result
end
def self . update_account_associations ( users_or_user_ids , opts = { } )
return if users_or_user_ids . empty?
opts . reverse_merge! :account_chain_cache = > { }
account_chain_cache = opts [ :account_chain_cache ]
# Split it up into manageable chunks
if users_or_user_ids . length > 500
users_or_user_ids . uniq . compact . each_slice ( 500 ) do | users_or_user_ids_slice |
update_account_associations ( users_or_user_ids_slice , opts )
end
return
end
incremental = opts [ :incremental ]
precalculated_associations = opts [ :precalculated_associations ]
user_ids = users_or_user_ids
user_ids = user_ids . map ( & :id ) if user_ids . first . is_a? ( User )
users_or_user_ids = User . find ( :all , :conditions = > { :id = > user_ids } , :include = > [ :pseudonyms , :account_users , { :enrollments = > { :course_section = > [ :course , :nonxlist_course ] } } ] ) if ! user_ids . first . is_a? ( User ) && ! precalculated_associations
UserAccountAssociation . transaction do
current_associations = { }
to_delete = [ ]
UserAccountAssociation . find ( :all , :conditions = > { :user_id = > user_ids } ) . each do | aa |
key = [ aa . user_id , aa . account_id ]
# duplicates
if current_associations . has_key? ( key )
to_delete << aa . id
next
end
current_associations [ key ] = [ aa . id , aa . depth ]
2011-02-01 09:57:29 +08:00
end
2011-08-18 03:33:10 +08:00
users_or_user_ids . each do | user_id |
if user_id . is_a? User
user = user_id
user_id = user_id . id
end
account_ids_with_depth = precalculated_associations
if account_ids_with_depth . nil?
user || = User . find ( user_id )
account_ids_with_depth = user . calculate_account_associations ( account_chain_cache )
end
2012-03-14 04:08:19 +08:00
# we don't want student view students to have account associations.
next if user && user . fake_student?
2011-08-18 03:33:10 +08:00
account_ids_with_depth . each do | account_id , depth |
key = [ user_id , account_id ]
association = current_associations [ key ]
if association . nil?
# new association, create it
UserAccountAssociation . create! do | aa |
aa . user_id = user_id
aa . account_id = account_id
aa . depth = depth
end
else
# for incremental, only update the old association if it is deeper than the new one
# for non-incremental, update it if it changed
if incremental && association [ 1 ] > depth || ! incremental && association [ 1 ] != depth
UserAccountAssociation . update_all ( " depth= #{ depth } " , :id = > association [ 0 ] )
end
# remove from list of existing for non-incremental
current_associations . delete ( key ) unless incremental
end
2011-02-01 09:57:29 +08:00
end
end
2011-08-18 03:33:10 +08:00
to_delete += current_associations . map { | k , v | v [ 0 ] }
UserAccountAssociation . delete_all ( :id = > to_delete ) unless incremental || to_delete . empty?
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# These two methods can be overridden by a plugin if you want to have an approval process for new teachers
def registration_approval_required? ; false ; end
2011-02-03 03:31:33 +08:00
def new_teacher_registration ( form_params = { } ) ; end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
set_broadcast_policy do | p |
p . dispatch :new_teacher_registration
p . to { Account . site_admin . users }
p . whenever { | record |
2012-01-04 04:30:49 +08:00
record . just_created && record . school_name && record . school_position
2011-02-01 09:57:29 +08:00
}
end
def assign_uuid
2011-05-06 04:43:15 +08:00
# DON'T use ||=, because that will cause an immediate save to the db if it
# doesn't already exist
self . uuid = AutoHandle . generate_securish_uuid if ! read_attribute ( :uuid )
2011-02-01 09:57:29 +08:00
end
protected :assign_uuid
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
named_scope :with_service , lambda { | service |
if service . is_a? ( UserService )
{ :include = > :user_services , :conditions = > [ 'user_services.service = ?' , service . service ] }
else
{ :include = > :user_services , :conditions = > [ 'user_services.service = ?' , service . to_s ] }
end
}
named_scope :enrolled_before , lambda { | date |
{ :conditions = > [ 'enrollments.created_at < ?' , date ] }
}
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def group_memberships_for ( context )
return [ ] unless context
self . group_memberships . select do | m |
m . group &&
m . group . context_id == context . id &&
m . group . context_type == context . class . to_s &&
! m . group . deleted? &&
2012-02-16 05:40:13 +08:00
m . accepted?
2011-02-01 09:57:29 +08:00
end . map ( & :group )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def <=> ( other )
self . name < = > other . name
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def default_pseudonym_id
self . pseudonyms . active . first . id
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def available?
true
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def participants
[ ]
end
2011-10-27 05:09:09 +08:00
# compatibility only - this isn't really last_name_first
2011-02-01 09:57:29 +08:00
def last_name_first
2011-10-27 05:09:09 +08:00
self . sortable_name
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def last_name_first_or_unnamed
res = last_name_first
res = " No Name " if res . strip . empty?
res
end
2011-10-27 05:09:09 +08:00
2011-02-01 09:57:29 +08:00
def first_name
2011-10-27 05:09:09 +08:00
User . name_parts ( self . sortable_name ) [ 0 ] || ''
2011-02-01 09:57:29 +08:00
end
2011-10-27 05:09:09 +08:00
2011-02-01 09:57:29 +08:00
def last_name
2011-10-27 05:09:09 +08:00
User . name_parts ( self . sortable_name ) [ 1 ] || ''
2011-02-01 09:57:29 +08:00
end
2011-10-27 05:09:09 +08:00
# Feel free to add, but the "authoritative" list (http://en.wikipedia.org/wiki/Title_(name)) is quite large
SUFFIXES = / ^(Sn?r \ .?|Senior|Jn?r \ .?|Junior|II|III|IV|V|VI|Esq \ .?|Esquire)$ /i
# see also user_sortable_name.js
def self . name_parts ( name , prior_surname = nil )
return [ nil , nil , nil ] unless name
surname , given , suffix = name . strip . split ( / \ s*, \ s* / , 3 )
# Doe, John, Sr.
# Otherwise change Ho, Chi, Min to Ho, Chi Min
if suffix && ! ( suffix =~ SUFFIXES )
given = " #{ given } #{ suffix } "
suffix = nil
end
if given
# John Doe, Sr.
if ! suffix && given =~ SUFFIXES
suffix = given
given = surname
surname = nil
2011-02-01 09:57:29 +08:00
end
2011-10-27 05:09:09 +08:00
else
# John Doe
given = name . strip
surname = nil
end
given_parts = given . split
# John Doe Sr.
if ! suffix && given_parts . length > 1 && given_parts . last =~ SUFFIXES
suffix = given_parts . pop
2011-02-01 09:57:29 +08:00
end
2011-10-27 05:09:09 +08:00
# Use prior information on the last name to try and reconstruct it
prior_surname_parts = nil
surname = given_parts . pop ( prior_surname_parts . length ) . join ( ' ' ) if ! surname && prior_surname . present? && ( prior_surname_parts = prior_surname . split ) && ! prior_surname_parts . empty? && given_parts . length > = prior_surname_parts . length && given_parts [ - prior_surname_parts . length .. - 1 ] == prior_surname_parts
# Last resort; last name is just the last word given
surname = given_parts . pop if ! surname && given_parts . length > 1
[ given_parts . empty? ? nil : given_parts . join ( ' ' ) , surname , suffix ]
2011-02-01 09:57:29 +08:00
end
2011-10-27 05:09:09 +08:00
def self . last_name_first ( name , name_was = nil )
given , surname , suffix = name_parts ( name , name_parts ( name_was ) [ 1 ] )
given = [ given , suffix ] . compact . join ( ' ' )
surname ? " #{ surname } , #{ given } " . strip : given
end
2011-02-01 09:57:29 +08:00
def self . user_lookup_cache_key ( id )
2011-09-28 04:36:33 +08:00
[ '_user_lookup2' , id ] . cache_key
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . invalidate_cache ( id )
Rails . cache . delete ( user_lookup_cache_key ( id ) ) if id
2012-01-04 04:30:49 +08:00
rescue
2011-02-01 09:57:29 +08:00
nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def infer_defaults
self . name = nil if self . name == " User "
2011-10-27 02:55:27 +08:00
self . name || = self . email || t ( :default_user_name , " User " )
self . short_name = nil if self . short_name == " "
2011-02-01 09:57:29 +08:00
self . short_name || = self . name
2011-10-27 05:09:09 +08:00
self . sortable_name = nil if self . sortable_name == " "
# recalculate the sortable name if the name changed, but the sortable name didn't, and the sortable_name matches the old name
self . sortable_name = nil if ! self . sortable_name_changed? && self . name_changed? && User . name_parts ( self . sortable_name ) . compact . join ( ' ' ) == self . name_was
self . sortable_name = User . last_name_first ( self . name , self . sortable_name_was ) unless read_attribute ( :sortable_name )
2011-02-01 09:57:29 +08:00
self . reminder_time_for_due_dates || = 48 . hours . to_i
self . reminder_time_for_grading || = 0
2012-08-11 06:48:00 +08:00
self . initial_enrollment_type = nil unless [ 'student' , 'teacher' , 'ta' , 'observer' ] . include? ( initial_enrollment_type )
2011-02-01 09:57:29 +08:00
User . invalidate_cache ( self . id ) if self . id
@reminder_times_changed = self . reminder_time_for_due_dates_changed? || self . reminder_time_for_grading_changed?
true
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def sortable_name
2011-10-27 05:09:09 +08:00
self . sortable_name = read_attribute ( :sortable_name ) || User . last_name_first ( self . name )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def primary_pseudonym
self . pseudonyms . active . first
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def primary_pseudonym = ( p )
p = Pseudonym . find ( p )
p . move_to_top
self . reload
p
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def email_channel
# It's already ordered, so find the first one, if there's one.
2011-12-03 03:29:50 +08:00
communication_channels . to_a . find { | cc | cc . path_type == 'email' && cc . workflow_state != 'retired' }
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def email
2012-08-21 00:41:17 +08:00
# if you change this cache_key, change it in email_cached? as well
value = Rails . cache . fetch ( [ 'user_email' , self ] . cache_key ) do
email_channel . try ( :path ) || :none
2011-02-01 09:57:29 +08:00
end
2012-08-21 00:41:17 +08:00
# this sillyness is because rails equates falsey as not in the cache
value == :none ? nil : value
end
def email_cached?
Rails . cache . exist? ( [ 'user_email' , self ] . cache_key )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . cached_name ( id )
key = user_lookup_cache_key ( id )
2011-09-28 04:36:33 +08:00
user = Rails . cache . fetch ( key ) do
2011-02-01 09:57:29 +08:00
User . find_by_id ( id )
end
user && user . name
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def gmail_channel
google_services = self . user_services . find_all_by_service_domain ( " google.com " )
addr = google_services . find { | s | s . service_user_id } . service_user_id rescue nil
2011-12-31 07:10:04 +08:00
self . communication_channels . email . by_path ( addr ) . find ( :first )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def gmail
res = gmail_channel . path rescue nil
res || = self . user_services . find_all_by_service_domain ( " google.com " ) . map ( & :service_user_id ) . compact . first
res || = email
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def google_docs_address
service = self . user_services . find_by_service ( 'google_docs' )
service && service . service_user_id
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def email = ( e )
if e . is_a? ( CommunicationChannel ) and e . user_id == self . id
cc = e
else
2011-12-17 06:16:37 +08:00
cc = self . communication_channels . find_or_create_by_path_and_path_type ( e , 'email' )
cc . user = self
2011-02-01 09:57:29 +08:00
end
cc . move_to_top
cc . save!
self . reload
cc . path
end
def sms_channel
# It's already ordered, so find the first one, if there's one.
communication_channels . find ( :first , :conditions = > { :path_type = > 'sms' } )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def sms
sms_channel . path if sms_channel
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def sms = ( s )
if s . is_a? ( CommunicationChannel ) and s . user_id == self . id
cc = s
else
cc = CommunicationChannel . find_or_create_by_path_and_user_id ( s , self . id )
end
cc . move_to_top
cc . save!
self . reload
cc . path
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def short_name
read_attribute ( :short_name ) || name
end
def unread_inbox_items_count
count = read_attribute ( :unread_inbox_items_count )
if count . nil?
self . unread_inbox_items_count = count = self . inbox_items . unread . count rescue 0
self . save
end
count
end
workflow do
state :pre_registered do
event :register , :transitions_to = > :registered
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# Not listing this first so it is not the default.
state :pending_approval do
event :approve , :transitions_to = > :pre_registered
event :reject , :transitions_to = > :deleted
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
state :creation_pending do
event :create_user , :transitions_to = > :pre_registered
event :register , :transitions_to = > :registered
end
2012-01-04 04:30:49 +08:00
2011-10-27 00:56:51 +08:00
state :registered
2011-02-01 09:57:29 +08:00
state :deleted
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def unavailable?
deleted?
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
alias_method :destroy! , :destroy
2011-12-06 04:48:11 +08:00
def destroy ( even_if_managed_passwords = false )
ActiveRecord :: Base . transaction do
self . workflow_state = 'deleted'
self . save
self . pseudonyms . each { | p | p . destroy ( even_if_managed_passwords ) }
self . communication_channels . each { | cc | cc . destroy }
self . enrollments . each { | e | e . destroy }
end
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def remove_from_root_account ( account )
self . enrollments . find_all_by_root_account_id ( account . id ) . each ( & :destroy )
2011-06-21 03:51:37 +08:00
self . pseudonyms . active . find_all_by_account_id ( account . id ) . each { | p | p . destroy ( true ) }
2011-05-05 07:09:33 +08:00
self . account_users . find_all_by_account_id ( account . id ) . each ( & :destroy )
self . save
self . update_account_associations
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-10-05 05:47:38 +08:00
def associate_with_shard ( shard )
end
def self . clone_communication_channel ( cc , new_user , max_position )
new_user . shard . activate do
new_cc = cc . clone
new_cc . position += max_position
new_cc . user = new_user
new_cc . save!
cc . notification_policies . each do | np |
new_np = np . clone
new_np . communication_channel = new_cc
new_np . save!
end
end
end
2011-02-01 09:57:29 +08:00
def move_to_user ( new_user )
return unless new_user
return if new_user == self
new_user . save
2012-10-05 05:47:38 +08:00
new_user . associate_with_shard ( self . shard )
2011-10-21 01:24:55 +08:00
2012-10-05 05:47:38 +08:00
max_position = new_user . communication_channels . last . try ( :position ) || 0
2011-10-21 01:24:55 +08:00
to_retire_ids = [ ]
2011-02-01 09:57:29 +08:00
self . communication_channels . each do | cc |
2011-10-21 01:24:55 +08:00
source_cc = cc
# have to find conflicting CCs, and make sure we don't have conflicts
# To avoid the case where a user has duplicate CCs and one of them is retired, don't look for retired ccs
# it's okay to do that even if the only matching CC is a retired CC, because it would end up on the no-op
# case below anyway.
# Behavior is undefined if a user has both an active and an unconfirmed CC; it's not allowed with current
# validations, but could be there due to older code that didn't enforce the uniqueness. The results would
# simply be that they'll continue to have duplicate unretired CCs
target_cc = new_user . communication_channels . detect { | cc | cc . path . downcase == source_cc . path . downcase && cc . path_type == source_cc . path_type && ! cc . retired? }
2012-10-05 05:47:38 +08:00
if ! target_cc && self . shard != new_user . shard
User . clone_communication_channel ( source_cc , new_user , max_position )
end
2011-10-21 01:24:55 +08:00
next unless target_cc
# we prefer keeping the "most" active one, preferring the target user if they're equal
# the comments inline show all the different cases, with the source cc on the left,
# target cc on the right. The * indicates the CC that will be retired in order
# to resolve the conflict
if target_cc . active?
# retired, active
# unconfirmed*, active
# active*, active
to_retire = source_cc
elsif source_cc . active?
# active, unconfirmed*
# active, retired
to_retire = target_cc
2012-10-05 05:47:38 +08:00
if self . shard != new_user . shard
target_cc . retire unless target_cc . retired?
User . clone_communication_channel ( source_cc , new_user , max_position )
end
2011-10-21 01:24:55 +08:00
elsif target_cc . unconfirmed?
# unconfirmed*, unconfirmed
# retired, unconfirmed
to_retire = source_cc
2012-10-05 05:47:38 +08:00
elsif source_cc . unconfirmed? && self . shard != new_user . shard
# unconfirmed, retired
User . clone_communication_channel ( source_cc , new_user , max_position )
2011-10-21 01:24:55 +08:00
end
#elsif
2012-10-05 05:47:38 +08:00
# retired, retired
2011-10-21 01:24:55 +08:00
#end
2012-10-05 05:47:38 +08:00
if to_retire && ! to_retire . retired?
to_retire_ids << to_retire . id
end
2011-02-01 09:57:29 +08:00
end
2011-10-21 01:24:55 +08:00
2012-10-05 05:47:38 +08:00
if self . shard != new_user . shard
self . communication_channels . update_all ( :workflow_state = > 'retired' ) unless self . communication_channels . empty?
2011-10-22 00:00:13 +08:00
2012-10-05 05:47:38 +08:00
self . user_services . each do | us |
new_user . shard . activate do
new_us = us . clone
new_us . user = new_user
new_us . save!
end
2011-10-22 00:00:13 +08:00
end
2012-10-05 05:47:38 +08:00
self . user_services . delete_all
else
self . shard . activate do
CommunicationChannel . update_all ( { :workflow_state = > 'retired' } , :id = > to_retire_ids ) unless to_retire_ids . empty?
2011-02-01 09:57:29 +08:00
end
2012-10-05 05:47:38 +08:00
self . communication_channels . update_all ( " user_id= #{ new_user . id } , position=position+ #{ max_position } " ) unless self . communication_channels . empty?
2011-02-01 09:57:29 +08:00
end
2012-10-05 05:47:38 +08:00
Shard . with_each_shard ( self . associated_shards ) do
max_position = Pseudonym . find ( :last , :conditions = > { :user_id = > new_user . id } , :order = > 'position' ) . try ( :position ) || 0
Pseudonym . update_all ( " position=position+ #{ max_position } , user_id= #{ new_user . id } " , :user_id = > self . id )
to_delete_ids = [ ]
new_user_enrollments = Enrollment . find ( :all , :conditions = > { :user_id = > new_user . id } )
Enrollment . scoped ( :conditions = > { :user_id = > self . id } ) . each do | enrollment |
source_enrollment = enrollment
# non-deleted enrollments should be unique per [course_section, type]
target_enrollment = new_user_enrollments . detect { | enrollment | enrollment . course_section_id == source_enrollment . course_section_id && enrollment . type == source_enrollment . type && ! [ 'deleted' , 'inactive' , 'rejected' ] . include? ( enrollment . workflow_state ) }
next unless target_enrollment
# we prefer keeping the "most" active one, preferring the target user if they're equal
# the comments inline show all the different cases, with the source enrollment on the left,
# target enrollment on the right. The * indicates the enrollment that will be deleted in order
# to resolve the conflict.
if target_enrollment . active?
# deleted, active
# inactive, active
# rejected, active
# invited*, active
# creation_pending*, active
# active*, active
# completed*, active
to_delete = source_enrollment
elsif source_enrollment . active?
# active, deleted
# active, inactive
# active, rejected
# active, invited*
# active, creation_pending*
# active, completed*
to_delete = target_enrollment
elsif target_enrollment . completed?
# deleted, completed
# inactive, completed
# rejected, completed
# invited*, completed
# creation_pending*, completed
# completed*, completed
to_delete = source_enrollment
elsif source_enrollment . completed?
# completed, deleted
# completed, inactive
# completed, rejected
# completed, invited*
# completed, creation_pending*
to_delete = target_enrollment
elsif target_enrollment . invited?
# deleted, invited
# inactive, invited
# rejected, invited
# creation_pending*, invited
# invited*, invited
to_delete = source_enrollment
elsif source_enrollment . invited?
# invited, deleted
# invited, inactive
# invited, rejected
# invited, creation_pending*
to_delete = target_enrollment
elsif target_enrollment . creation_pending?
# deleted, creation_pending
# inactive, creation_pending
# rejected, creation_pending
# creation_pending*, creation_pending
to_delete = source_enrollment
end
#elsif
# creation_pending, deleted
# creation_pending, inactive
# creation_pending, rejected
# deleted, rejected
# inactive, rejected
# rejected, rejected
# rejected, deleted
# rejected, inactive
# deleted, inactive
# inactive, inactive
# inactive, deleted
# deleted, deleted
#end
to_delete_ids << to_delete . id if to_delete && ! [ 'deleted' , 'inactive' , 'rejected' ] . include? ( to_delete . workflow_state )
end
Enrollment . update_all ( { :workflow_state = > 'deleted' } , :id = > to_delete_ids ) unless to_delete_ids . empty?
[
[ :quiz_id , :quiz_submissions ] ,
[ :assignment_id , :submissions ]
] . each do | unique_id , table |
begin
# Submissions are a special case since there's a unique index
# on the table, and if both the old user and the new user
# have a submission for the same assignment there will be
# a conflict.
already_there_ids = table . to_s . classify . constantize . find_all_by_user_id ( new_user . id ) . map ( & unique_id )
already_there_ids = [ 0 ] if already_there_ids . empty?
table . to_s . classify . constantize . update_all ( { :user_id = > new_user . id } , " user_id= #{ self . id } AND #{ unique_id } NOT IN ( #{ already_there_ids . join ( ',' ) } ) " )
rescue = > e
logger . error " migrating #{ table } column user_id failed: #{ e . to_s } "
2011-02-01 09:57:29 +08:00
end
end
2012-10-05 05:47:38 +08:00
all_conversations . find_each { | c | c . move_to_user ( new_user ) } unless Shard . current != new_user . shard
updates = { }
[ 'account_users' , 'asset_user_accesses' ,
'assignment_reminders' , 'attachments' ,
'calendar_events' , 'collaborations' ,
'context_module_progressions' , 'discussion_entries' , 'discussion_topics' ,
'enrollments' , 'group_memberships' , 'page_comments' ,
'rubric_assessments' ,
'submission_comment_participants' , 'user_services' , 'web_conferences' ,
'web_conference_participants' , 'wiki_pages' ] . each do | key |
updates [ key ] = " user_id "
end
updates [ 'submission_comments' ] = 'author_id'
updates [ 'conversation_messages' ] = 'author_id'
updates = updates . to_a
updates << [ 'enrollments' , 'associated_user_id' ]
updates . each do | table , column |
begin
klass = table . classify . constantize
if klass . new . respond_to? ( " #{ column } = " . to_sym )
klass . connection . execute ( " UPDATE #{ table } SET #{ column } = #{ new_user . id } WHERE #{ column } = #{ self . id } " )
end
rescue = > e
logger . error " migrating #{ table } column #{ column } failed: #{ e . to_s } "
end
end
unless Shard . current != new_user . shard
# delete duplicate enrollments where this user is the observee
new_user . observee_enrollments . remove_duplicates!
# delete duplicate observers/observees, move the rest
user_observees . where ( :user_id = > new_user . user_observees . map ( & :user_id ) ) . delete_all
user_observees . update_all ( :observer_id = > new_user . id )
xor_observer_ids = ( Set . new ( user_observers . map ( & :observer_id ) ) ^ new_user . user_observers . map ( & :observer_id ) ) . to_a
user_observers . where ( :observer_id = > new_user . user_observers . map ( & :observer_id ) ) . delete_all
user_observers . update_all ( :user_id = > new_user . id )
# for any observers not already watching both users, make sure they have
# any missing observer enrollments added
new_user . user_observers . where ( :observer_id = > xor_observer_ids ) . each ( & :create_linked_enrollments )
end
Enrollment . send_later ( :recompute_final_scores , new_user . id )
new_user . update_account_associations
2011-02-01 09:57:29 +08:00
end
2012-05-17 01:22:27 +08:00
2011-02-01 09:57:29 +08:00
self . reload
2011-11-04 06:03:12 +08:00
new_user . touch
2011-02-01 09:57:29 +08:00
self . destroy
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# Overwrites the old user name, if there was one. Fills in the new one otherwise.
def assert_name ( name = nil )
if name && ( self . pre_registered? || self . creation_pending? ) && name != email
self . name = name
save!
end
self
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def to_atom
Atom :: Entry . new do | entry |
entry . title = self . name
entry . updated = self . updated_at
entry . published = self . created_at
2012-01-04 04:30:49 +08:00
entry . links << Atom :: Link . new ( :rel = > 'alternate' ,
2011-02-01 09:57:29 +08:00
:href = > " /users/ #{ self . id } " )
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def admins
[ self ]
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def students
[ self ]
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def latest_pseudonym
Pseudonym . scoped ( :order = > 'created_at DESC' , :conditions = > { :user_id = > id } ) . active . first
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def used_feature ( feature )
self . update_attribute ( :features_used , ( ( self . features_used || " " ) . split ( / , / ) . map ( & :to_s ) + [ feature . to_s ] ) . uniq . join ( ',' ) )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def used_feature? ( feature )
self . features_used && self . features_used . split ( / , / ) . include? ( feature . to_s )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def available_courses
2011-12-09 08:02:47 +08:00
# this list should be longer if the person has admin privileges...
2011-02-01 09:57:29 +08:00
self . courses
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def courses_with_grades
self . available_courses . select { | c | c . grants_right? ( self , nil , :participate_as_student ) }
end
memoize :courses_with_grades
2012-01-04 04:30:49 +08:00
2011-11-30 05:59:40 +08:00
def sis_pseudonym_for ( context )
2011-12-28 05:57:56 +08:00
root_account = context . root_account
2011-11-30 05:59:40 +08:00
raise " could not resolve root account " unless root_account . is_a? ( Account )
2011-09-14 23:49:21 +08:00
if self . pseudonyms . loaded?
self . pseudonyms . detect { | p | p . active? && p . sis_user_id && p . account_id == root_account . id }
else
self . pseudonyms . active . find_by_account_id ( root_account . id , :conditions = > [ " sis_user_id IS NOT NULL " ] )
end
2011-11-30 05:59:40 +08:00
end
2011-02-01 09:57:29 +08:00
set_policy do
given { | user | user == self }
2012-03-28 06:53:55 +08:00
can :read and can :manage and can :manage_content and can :manage_files and can :manage_calendar and can :send_messages and can :update_avatar
2012-05-26 05:26:17 +08:00
given { | user | user . present? && self . public? }
can :follow
2012-03-28 06:53:55 +08:00
given { | user | user == self && user . user_can_edit_name? }
can :rename
2011-02-01 09:57:29 +08:00
2012-10-20 04:04:42 +08:00
given { | user | self . courses . any? { | c | c . user_is_instructor? ( user ) } }
2011-07-14 00:24:17 +08:00
can :rename and can :create_user_notes and can :read_user_notes
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
given do | user |
user && (
# this means that the user we are given is an administrator of an account of one of the courses that this user is enrolled in
self . all_courses . any? { | c | c . grants_right? ( user , nil , :read_reports ) }
)
end
2011-08-12 04:50:02 +08:00
can :rename and can :remove_avatar and can :view_statistics
given do | user |
user && self . all_courses . any? { | c | c . grants_right? ( user , nil , :manage_user_notes ) }
end
can :create_user_notes and can :read_user_notes
2011-08-15 23:53:55 +08:00
given { | user | user && self . all_courses . any? { | c | c . grants_right? ( user , nil , :read_user_notes ) } }
can :read_user_notes
2011-08-12 04:50:02 +08:00
given do | user |
user && (
self . associated_accounts . any? { | a | a . grants_right? ( user , nil , :manage_user_notes ) }
)
end
can :create_user_notes and can :read_user_notes and can :delete_user_notes
2011-02-01 09:57:29 +08:00
given do | user |
user && (
# or, if the user we are given is an admin in one of this user's accounts
2012-04-11 03:54:42 +08:00
Account . site_admin . grants_right? ( user , :manage_students ) ||
self . associated_accounts . any? { | a | a . grants_right? ( user , nil , :manage_students ) }
2011-02-01 09:57:29 +08:00
)
end
2012-03-28 06:53:55 +08:00
can :manage_user_details and can :update_avatar and can :remove_avatar and can :rename and can :view_statistics and can :read
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
given do | user |
user && (
# or, if the user we are given is an admin in one of this user's accounts
2012-04-11 03:54:42 +08:00
Account . site_admin . grants_right? ( user , :manage_user_logins ) ||
self . associated_accounts . any? { | a | a . grants_right? ( user , nil , :manage_user_logins ) }
2011-02-01 09:57:29 +08:00
)
end
2012-01-12 08:26:43 +08:00
can :manage_user_details and can :manage_logins and can :rename and can :view_statistics and can :read
2012-02-21 06:57:32 +08:00
end
2011-07-09 02:59:34 +08:00
2012-02-21 06:57:32 +08:00
def can_masquerade? ( masquerader , account )
return true if self == masquerader
2012-03-14 04:08:19 +08:00
# student view should only ever have enrollments in a single course
return true if self . fake_student? && self . courses . any? { | c | c . grants_right? ( masquerader , nil , :use_student_view ) }
2012-02-21 06:57:32 +08:00
return false unless
account . grants_right? ( masquerader , nil , :become_user ) && self . find_pseudonym_for_account ( account , true )
account_users = account . all_account_users_for ( self )
return true if account_users . empty?
account_users . map ( & :account ) . uniq . all? do | account |
needed_rights = account . check_policy ( self )
account . grants_rights? ( masquerader , nil , * needed_rights ) . values . all?
end
2011-02-01 09:57:29 +08:00
end
def self . infer_id ( obj )
case obj
when User
obj . id
when Numeric
obj
when CommunicationChannel
obj . user_id
when Pseudonym
obj . user_id
2012-01-04 04:30:49 +08:00
when AccountUser
2011-02-01 09:57:29 +08:00
obj . user_id
when OpenObject
obj . id
when String
obj . to_i
2012-01-04 04:30:49 +08:00
else
2011-02-01 09:57:29 +08:00
raise ArgumentError , " Cannot infer a user_id from #{ obj . inspect } "
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def management_contexts
contexts = [ self ] + self . courses + self . groups . active + self . all_courses
contexts . uniq
end
def file_management_contexts
contexts = [ self ] + self . courses + self . groups . active + self . all_courses
contexts . uniq . select { | c | c . grants_right? ( self , nil , :manage_files ) }
end
def facebook
self . user_services . for_service ( 'facebook' ) . first rescue nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def visible_inbox_types = ( val )
types = ( val || " " ) . split ( " , " )
write_attribute ( :visible_inbox_types , types . map { | t | t . classify } . join ( " , " ) )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def show_in_inbox? ( type )
if self . respond_to? ( :visible_inbox_types ) && self . visible_inbox_types
types = self . visible_inbox_types . split ( " , " )
types . include? ( type )
else
true
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def submitted_submission_for ( assignment_id )
@submissions || = self . submissions . having_submission . to_a
@submissions . detect { | s | s . assignment_id == assignment_id }
end
2012-01-04 04:30:49 +08:00
2011-01-06 14:09:46 +08:00
def attempted_quiz_submission_for ( quiz_id )
@quiz_submissions || = self . quiz_submissions . select { | s | ! s . settings_only? }
2011-02-01 09:57:29 +08:00
@quiz_submissions . detect { | qs | qs . quiz_id == quiz_id }
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def module_progression_for ( module_id )
@module_progressions || = self . context_module_progressions . to_a
@module_progressions . detect { | p | p . context_module_id == module_id }
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def clear_cached_lookups
@module_progressions = nil
@quiz_submissions = nil
@submissions = nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def update_avatar_image ( force_reload = false )
if ! self . avatar_image_url || force_reload
if self . avatar_image_source == 'facebook'
2011-05-15 12:40:44 +08:00
# TODO: support this
2011-02-01 09:57:29 +08:00
elsif self . avatar_image_source == 'twitter'
twitter = self . user_services . for_service ( 'twitter' ) . first rescue nil
if twitter
url = URI . parse ( " http://twitter.com/users/show.json?user_id= #{ twitter . service_user_id } " )
data = JSON . parse ( Net :: HTTP . get ( url ) ) rescue nil
if data
2011-10-07 06:02:51 +08:00
self . avatar_image_url = data [ 'profile_image_url_https' ] || self . avatar_image_url
2011-02-01 09:57:29 +08:00
self . avatar_image_updated_at = Time . now
end
end
end
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . max_messages_per_day
2012-02-25 07:37:50 +08:00
Setting . get ( 'max_messages_per_day_per_user' , 500 ) . to_i
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def max_messages_per_day
User . max_messages_per_day
end
2012-01-04 04:30:49 +08:00
2012-02-24 03:21:12 +08:00
def gravatar_url ( size = 50 , fallback = nil , request = nil )
2012-03-02 14:54:17 +08:00
fallback = self . class . avatar_fallback_url ( fallback , request )
2011-03-09 02:19:33 +08:00
" https://secure.gravatar.com/avatar/ #{ Digest :: MD5 . hexdigest ( self . email ) rescue '000' } ?s= #{ size } &d= #{ CGI :: escape ( fallback ) } "
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-03-29 03:57:05 +08:00
# Public: Set a user's avatar image. This is a convenience method that sets
# the avatar_image_source, avatar_image_url, avatar_updated_at, and
# avatar_state on the user model.
#
# val - A hash of options used to configure the avatar.
# :type - The type of avatar. Should be 'facebook,' 'gravatar,'
2012-08-18 00:03:22 +08:00
# 'external,' or 'attachment.'
2012-03-29 03:57:05 +08:00
# :url - The URL of the gravatar. Used for types 'external' and
# 'attachment.'
#
# Returns nothing if avatar is set; false if avatar is locked.
2011-02-01 09:57:29 +08:00
def avatar_image = ( val )
return false if avatar_state == :locked
2011-10-12 03:19:23 +08:00
2012-03-29 03:57:05 +08:00
# Clear out the old avatar first, in case of failure to get new avatar.
# The order of these attributes is standard throughout the method.
2011-10-12 03:19:23 +08:00
self . avatar_image_source = 'no_pic'
2012-03-29 03:57:05 +08:00
self . avatar_image_url = nil
self . avatar_image_updated_at = Time . zone . now
2011-10-12 03:19:23 +08:00
self . avatar_state = 'approved'
2012-03-29 03:57:05 +08:00
# Return here if we're passed a nil val or any non-hash val (both of which
# will just nil the user's avatar).
return unless val . is_a? ( Hash )
2011-02-01 09:57:29 +08:00
if val [ 'type' ] == 'facebook'
2011-05-15 12:40:44 +08:00
# TODO: support this
2011-02-01 09:57:29 +08:00
elsif val [ 'type' ] == 'gravatar'
self . avatar_image_source = 'gravatar'
self . avatar_image_url = nil
self . avatar_state = 'submitted'
2012-03-29 03:57:05 +08:00
elsif val [ 'type' ] == 'external'
self . avatar_image_source = 'external'
self . avatar_image_url = val [ 'url' ]
self . avatar_state = 'submitted'
2012-03-28 06:53:55 +08:00
elsif val [ 'type' ] == 'attachment' && val [ 'url' ]
2011-02-01 09:57:29 +08:00
self . avatar_image_source = 'attachment'
2012-03-28 06:53:55 +08:00
self . avatar_image_url = val [ 'url' ]
2011-02-01 09:57:29 +08:00
self . avatar_state = 'submitted'
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def report_avatar_image! ( associated_context = nil )
if avatar_state == :approved || avatar_state == :locked
avatar_state = 're_reported'
else
avatar_state = 'reported'
end
save!
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def avatar_state
if [ 'none' , 'submitted' , 'approved' , 'locked' , 'reported' , 're_reported' ] . include? ( read_attribute ( :avatar_state ) )
read_attribute ( :avatar_state ) . to_sym
else
:none
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def avatar_state = ( val )
if [ 'none' , 'submitted' , 'approved' , 'locked' , 'reported' , 're_reported' ] . include? ( val . to_s )
if val == 'none'
self . avatar_image_url = nil
self . avatar_image_source = 'no_pic'
self . avatar_image_updated_at = Time . now
end
2012-03-28 06:53:55 +08:00
write_attribute ( :avatar_state , val . to_s )
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def avatar_reportable?
[ :submitted , :approved , :reported , :re_reported ] . include? ( avatar_state )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def avatar_approvable?
[ :submitted , :reported , :re_reported ] . include? ( avatar_state )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def avatar_approved?
[ :approved , :locked , :re_reported ] . include? ( avatar_state )
end
2012-06-30 04:37:05 +08:00
2012-02-04 14:05:19 +08:00
def self . avatar_key ( user_id )
user_id = user_id . to_s
if ! user_id . blank? && user_id != '0'
" #{ user_id } - #{ Canvas :: Security . hmac_sha1 ( user_id ) [ 0 , 10 ] } "
else
" 0 "
end
end
2012-06-30 04:37:05 +08:00
2012-02-04 14:05:19 +08:00
def self . user_id_from_avatar_key ( key )
user_id , sig = key . to_s . split ( / - / , 2 )
( Canvas :: Security . hmac_sha1 ( user_id . to_s ) [ 0 , 10 ] == sig ) ? user_id : nil
end
2012-01-04 04:30:49 +08:00
2012-01-13 07:57:58 +08:00
# Returns the LTI membership based on the LTI specs here: http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649701
2011-09-08 13:20:55 +08:00
def lti_role_types ( context = nil )
memberships = [ ]
if context . is_a? ( Course )
memberships += current_enrollments . find_all_by_course_id ( context . id ) . uniq
end
if context . respond_to? ( :account_chain ) && ! context . account_chain_ids . empty?
memberships += account_users . find_all_by_membership_type_and_account_id ( 'AccountAdmin' , context . account_chain_ids ) . uniq
end
basic lti navigation links
By properly configuring external tools (see
/spec/models/course_spec/rb:898 for examples) they can
be added as left-side navigation links to a course,
an account, or to the user profile section of Canvas.
testing notes:
- you have to manually set options on the external tool:
- for user navigation the tool needs to be created on the root account
with the following settings:
{:user_navigation => {:url => <url>, :text => <tab label>} }
(there are also some optional language options you can set using
the :labels attribute)
- for account navigation it's the same
- for course navigation it's the same, except with :course_navigation
there's also some additional options:
:visibility => <value> // public, members, admins
:default => <value> // disabled, enabled
test plan:
- configure a user navigation tool at the root account level,
make sure it shows up in the user's profile section
- configure a course navigation tool at the account level,
make sure it shows up in the course's navigation
- configure a course navigation tool at the course level,
make sure it shows up in the course's navigation
- make sure :default => 'disabled' course navigation tools don't
appear by default in the navigation, but can be enabled on
the course settings page
- make sure :visibility => 'members' only shows up for course members
- make sure :visibility => 'admins' only shows up for course admins
- configure an account navigation tool at the account level,
make sure it shows up in the account's navigation, and
any sub-account's navigation
Change-Id: I977da3c6b89a9e32b4cff4c2b6b221f8162782ff
Reviewed-on: https://gerrit.instructure.com/5427
Reviewed-by: Brian Whitmer <brian@instructure.com>
Tested-by: Hudson <hudson@instructure.com>
2011-08-18 13:49:01 +08:00
return [ " urn:lti:sysrole:ims/lis/None " ] if memberships . empty?
2011-03-10 00:11:22 +08:00
memberships . map { | membership |
case membership
2012-03-14 04:08:19 +08:00
when StudentEnrollment , StudentViewEnrollment
2011-09-08 13:20:55 +08:00
'Learner'
2011-03-10 00:11:22 +08:00
when TeacherEnrollment
'Instructor'
when TaEnrollment
'Instructor'
2012-01-13 07:57:58 +08:00
when DesignerEnrollment
'ContentDeveloper'
2011-03-10 00:11:22 +08:00
when ObserverEnrollment
2011-09-08 13:20:55 +08:00
'urn:lti:instrole:ims/lis/Observer'
2011-03-10 00:11:22 +08:00
when AccountUser
2011-09-08 13:20:55 +08:00
'urn:lti:instrole:ims/lis/Administrator'
2011-03-10 00:11:22 +08:00
else
2011-09-08 13:20:55 +08:00
'urn:lti:instrole:ims/lis/Observer'
2011-03-10 00:11:22 +08:00
end
} . uniq
end
2012-01-04 04:30:49 +08:00
2012-03-02 14:54:17 +08:00
AVATAR_SETTINGS = [ 'enabled' , 'enabled_pending' , 'sis_only' , 'disabled' ]
def avatar_url ( size = nil , avatar_setting = nil , fallback = nil , request = nil )
2012-08-21 22:32:47 +08:00
return fallback if avatar_setting == 'disabled'
2011-02-01 09:57:29 +08:00
size || = 50
avatar_setting || = 'enabled'
2012-03-02 14:54:17 +08:00
fallback = self . class . avatar_fallback_url ( fallback , request )
2011-02-01 09:57:29 +08:00
if avatar_setting == 'enabled' || ( avatar_setting == 'enabled_pending' && avatar_approved? ) || ( avatar_setting == 'sis_only' )
2012-01-04 04:30:49 +08:00
@avatar_url || = self . avatar_image_url
2011-02-01 09:57:29 +08:00
end
2011-10-11 06:17:22 +08:00
@avatar_url || = fallback if self . avatar_image_source == 'no_pic'
2012-02-24 03:21:12 +08:00
@avatar_url || = gravatar_url ( size , fallback , request ) if avatar_setting == 'enabled'
2011-10-11 06:17:22 +08:00
@avatar_url || = fallback
2011-02-01 09:57:29 +08:00
end
2012-06-30 04:37:05 +08:00
2012-02-04 14:05:19 +08:00
def avatar_path
" /images/users/ #{ User . avatar_key ( self . id ) } "
end
2012-06-30 04:37:05 +08:00
2012-03-02 14:54:17 +08:00
def self . default_avatar_fallback
2012-02-04 14:44:45 +08:00
" /images/messages/avatar-50.png "
2012-03-02 14:54:17 +08:00
end
def self . avatar_fallback_url ( fallback = nil , request = nil )
return fallback if fallback == '%{fallback}'
if fallback and uri = URI . parse ( fallback ) rescue nil
2012-08-11 05:25:51 +08:00
uri . scheme || = request ? request . protocol [ 0 .. - 4 ] : " https " # -4 to chop off the ://
2012-10-09 07:51:32 +08:00
if HostUrl . cdn_host
uri . host = HostUrl . cdn_host
elsif request && ! uri . host
2012-02-04 14:44:45 +08:00
uri . host = request . host
uri . port = request . port if ! [ 80 , 443 ] . include? ( request . port )
elsif ! uri . host
2012-10-09 07:51:32 +08:00
uri . host , uri . port = HostUrl . default_host . split ( / : / )
2012-02-04 14:44:45 +08:00
end
2012-03-02 14:54:17 +08:00
uri . to_s
else
avatar_fallback_url ( default_avatar_fallback , request )
end
end
2012-05-10 06:47:48 +08:00
# Clear the avatar_image_url attribute and save it if the URL contains the given uuid.
#
# ==== Arguments
# * <tt>uuid</tt> - The Attachment#uuid value for the file. Used as part of the url identifier.
def clear_avatar_image_url_with_uuid ( uuid )
raise ArgumentError , " 'uuid' is required and cannot be blank " if uuid . blank?
if self . avatar_image_url . to_s . match ( / #{ uuid } / )
self . avatar_image_url = nil
self . save
end
end
2012-06-30 04:37:05 +08:00
2011-02-01 09:57:29 +08:00
named_scope :with_avatar_state , lambda { | state |
if state == 'any'
{
2011-08-10 13:46:52 +08:00
:conditions = > [ 'avatar_image_url IS NOT NULL AND avatar_state IS NOT NULL AND avatar_state != ?' , 'none' ] ,
2011-02-01 09:57:29 +08:00
:order = > 'avatar_image_updated_at DESC'
}
else
{
2011-08-10 13:46:52 +08:00
:conditions = > [ 'avatar_image_url IS NOT NULL AND avatar_state = ?' , state ] ,
2011-02-01 09:57:29 +08:00
:order = > 'avatar_image_updated_at DESC'
}
end
}
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def sorted_rubrics
context_codes = ( [ self ] + self . management_contexts ) . uniq . map ( & :asset_string )
rubrics = self . context_rubrics . active
rubrics += Rubric . active . find_all_by_context_code ( context_codes )
rubrics . uniq . sort_by { | r | [ ( r . association_count || 0 ) > 3 ? 'a' : 'b' , ( r . title . downcase rescue 'zzzzz' ) ] }
end
def assignments_recently_graded ( opts = { } )
opts = { :start_at = > 1 . week . ago , :limit = > 10 } . merge ( opts )
Submission . recently_graded_assignments ( id , opts [ :start_at ] , opts [ :limit ] )
end
memoize :assignments_recently_graded
def assignments_recently_graded_total_count ( opts = { } )
assignments_recently_graded ( opts . merge ( { :limit = > nil } ) ) . size
end
memoize :assignments_recently_graded_total_count
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def preferences
read_attribute ( :preferences ) || write_attribute ( :preferences , { } )
end
2012-01-04 04:30:49 +08:00
2011-08-19 17:12:45 +08:00
def watched_conversations_intro?
preferences [ :watched_conversations_intro ] == true
end
def watched_conversations_intro ( value = true )
preferences [ :watched_conversations_intro ] = value
end
2011-05-04 11:16:50 +08:00
def send_scores_in_emails?
preferences [ :send_scores_in_emails ] == true
end
2012-01-04 04:30:49 +08:00
2012-02-24 00:13:20 +08:00
def close_announcement ( announcement )
2011-02-15 15:07:14 +08:00
preferences [ :closed_notifications ] || = [ ]
2012-02-24 00:13:20 +08:00
# serialize ids relative to the user
self . shard . activate do
preferences [ :closed_notifications ] << announcement . id
end
2011-02-15 15:07:14 +08:00
preferences [ :closed_notifications ] . uniq!
save
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def ignore_item! ( asset_string , purpose , permanent = nil )
permanent || = false
asset_string = asset_string . gsub ( / ![0-9a-z_] / , '' )
preferences [ :ignore ] || = { }
preferences [ :ignore ] [ purpose . to_sym ] || = { }
preferences [ :ignore ] [ purpose . to_sym ] . each do | key , item |
preferences [ :ignore ] [ purpose . to_sym ] . delete ( key ) if item && ( ! item [ :set ] || item [ :set ] < 6 . months . ago . utc . iso8601 )
end
preferences [ :ignore ] [ purpose . to_sym ] [ asset_string ] = { :permanent = > permanent , :set = > Time . now . utc . iso8601 }
2011-03-19 04:37:33 +08:00
self . updated_at = Time . now
2011-02-01 09:57:29 +08:00
save!
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def ignored_item_changed! ( asset_string , purpose )
preferences [ :ignore ] || = { }
preferences [ :ignore ] [ purpose . to_sym ] || = { }
if preferences [ :ignore ] [ purpose . to_sym ] [ asset_string ]
preferences [ :ignore ] [ purpose . to_sym ] . delete ( asset_string ) if ! preferences [ :ignore ] [ purpose . to_sym ] [ asset_string ] [ :permanent ]
end
2011-03-19 04:37:33 +08:00
self . updated_at = Time . now
2011-02-01 09:57:29 +08:00
save!
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def ignored_items ( purpose )
( preferences [ :ignore ] || { } ) [ purpose . to_sym ] || { }
end
def assignments_needing_submitting ( opts = { } )
course_codes = opts [ :contexts ] ? ( Array ( opts [ :contexts ] ) . map ( & :asset_string ) & current_student_enrollment_course_codes ) : current_student_enrollment_course_codes
ignored_ids = ignored_items ( :submitting ) . select { | key , val | key . match ( / \ Aassignment_ / ) } . map { | key , val | key . sub ( / \ Aassignment_ / , " " ) }
2012-05-09 01:15:24 +08:00
Assignment . for_context_codes ( course_codes ) . active . due_before ( 1 . week . from_now ) .
expecting_submission . due_after ( opts [ :due_after ] || 4 . weeks . ago ) .
need_submitting_info ( id , opts [ :limit ] || 15 , ignored_ids ) .
not_locked
2011-02-01 09:57:29 +08:00
end
memoize :assignments_needing_submitting
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def assignments_needing_submitting_total_count ( opts = { } )
course_codes = opts [ :contexts ] ? ( Array ( opts [ :contexts ] ) . map ( & :asset_string ) & current_student_enrollment_course_codes ) : current_student_enrollment_course_codes
ignored_ids = ignored_items ( :submitting ) . select { | key , val | key . match ( / \ Aassignment_ / ) } . map { | key , val | key . sub ( / \ Aassignment_ / , " " ) }
Assignment . for_context_codes ( course_codes ) . active . due_before ( 1 . week . from_now ) . expecting_submission . due_after ( 4 . weeks . ago ) . need_submitting_info ( id , nil , ignored_ids ) . size
end
memoize :assignments_needing_submitting_total_count
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def assignments_needing_grading ( opts = { } )
course_codes = opts [ :contexts ] ? ( Array ( opts [ :contexts ] ) . map ( & :asset_string ) & current_admin_enrollment_course_codes ) : current_admin_enrollment_course_codes
ignored_ids = ignored_items ( :grading ) . select { | key , val | key . match ( / \ Aassignment_ / ) } . map { | key , val | key . sub ( / \ Aassignment_ / , " " ) }
2012-09-29 01:34:02 +08:00
Assignment . for_context_codes ( course_codes ) . active . expecting_submission . need_grading_info ( opts [ :limit ] || 15 , ignored_ids ) . reject { | a | a . needs_grading_count_for_user ( self ) == 0 }
2011-02-01 09:57:29 +08:00
end
2012-10-12 06:40:52 +08:00
memoize :assignments_needing_grading
2012-01-04 04:30:49 +08:00
2012-10-11 03:00:01 +08:00
def assignments_needing_grading_total_count ( opts = { } )
course_codes = opts [ :contexts ] ? ( Array ( opts [ :contexts ] ) . map ( & :asset_string ) & current_admin_enrollment_course_codes ) : current_admin_enrollment_course_codes
ignored_ids = ignored_items ( :grading ) . select { | key , val | key . match ( / \ Aassignment_ / ) } . map { | key , val | key . sub ( / \ Aassignment_ / , " " ) }
Assignment . for_context_codes ( course_codes ) . active . expecting_submission . need_grading_info ( nil , ignored_ids ) . reject { | a | a . needs_grading_count_for_user ( self ) == 0 } . size
end
2012-10-12 06:40:52 +08:00
memoize :assignments_needing_grading_total_count
2012-10-11 03:00:01 +08:00
2011-02-01 09:57:29 +08:00
def generate_access_verifier ( ts )
require 'openssl'
digest = OpenSSL :: Digest :: MD5 . new
OpenSSL :: HMAC . hexdigest ( digest , uuid , ts . to_s )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
private :generate_access_verifier
def access_verifier
ts = Time . now . utc . to_i
[ ts , generate_access_verifier ( ts ) ]
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def valid_access_verifier? ( ts , sig )
ts . to_i > 5 . minutes . ago . to_i && ts . to_i < 1 . minute . from_now . to_i && sig == generate_access_verifier ( ts . to_i )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def uuid
if ! read_attribute ( :uuid )
2011-04-15 06:09:37 +08:00
self . update_attribute ( :uuid , AutoHandle . generate_securish_uuid )
2011-02-01 09:57:29 +08:00
end
read_attribute ( :uuid )
end
2012-01-04 04:30:49 +08:00
2012-08-25 05:25:28 +08:00
def self . serialization_excludes ; [ :uuid , :phone , :features_used , :otp_communication_channel_id , :otp_secret_key_enc , :otp_secret_key_salt ] ; end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def migrate_content_links ( html , from_course )
Course . migrate_content_links ( html , from_course , self )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
attr_accessor :merge_mappings
attr_accessor :merge_results
def merge_mapped_id ( * args )
nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def map_merge ( * args )
end
def log_merge_result ( text )
@merge_results || = [ ]
@merge_results << text
end
def warn_merge_result ( text )
record_merge_result ( text )
end
def file_structure_for ( user )
User . file_structure_for ( self , user )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def secondary_identifier
self . email || self . id
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . file_structure_for ( context , user )
2012-08-28 05:42:45 +08:00
results = {
2011-02-01 09:57:29 +08:00
:contexts = > [ context ] ,
:collaborations = > [ ] ,
:folders = > [ ] ,
:folders_with_subcontent = > [ ] ,
:files = > [ ]
}
2012-08-28 05:42:45 +08:00
context_codes = results [ :contexts ] . map { | c | c . asset_string }
2011-02-01 09:57:29 +08:00
if ! context . is_a? ( User ) && user
2012-08-28 05:42:45 +08:00
results [ :collaborations ] = user . collaborations . active . find ( :all , :include = > [ :user , :users ] ) . select { | c | c . context_id && c . context_type && context_codes . include? ( " #{ c . context_type . underscore } _ #{ c . context_id } " ) }
results [ :collaborations ] = results [ :collaborations ] . sort_by { | c | c . created_at } . reverse
2011-02-01 09:57:29 +08:00
end
2012-08-28 05:42:45 +08:00
results [ :contexts ] . each do | context |
results [ :folders ] += context . active_folders_with_sub_folders
2011-02-01 09:57:29 +08:00
end
2012-08-28 05:42:45 +08:00
results [ :folders ] = results [ :folders ] . sort_by { | f | [ f . parent_folder_id || 0 , f . position || 0 , f . name || " " , f . created_at ] }
results
2011-02-01 09:57:29 +08:00
end
refactor user creation/invitations closes #5833
fixes #5573, #5572, #5753
* communication channels are now only unique within a single user
* UserList changes
* Always resolve pseudonym#unique_ids
* Support looking up by SMS CCs
* Option to either require e-mails match an existing CC,
or e-mails that don't match a Pseudonym will always be
returned unattached (relying on better merging behavior
to not have a gazillion accounts created)
* Method to return users, creating new ones (*without* a
Pseudonym) if necessary. (can't create with a pseudonym,
since Pseudonym#unique_id is still unique, I can't have
multiple outstanding users with the same unique_id)
* EnrollmentsFromUserList is mostly gutted, now using UserList's
functionality directy.
* Use UserList for adding account admins, removing the now
unused Account#add_admin => User#find_by_email/User#assert_by_email
codepath
* Update UsersController#create to not worry about duplicate
communication channels
* Remove AccountsController#add_user, and just use
UsersController#create
* Change SIS::UserImporter to send out a merge opportunity
e-mail if a conflicting CC is found (but still create the CC)
* In /profile, don't worry about conflicting CCs (the CC confirmation
process will now allow merging)
* Remove CommunicationChannelsController#try_merge and #merge
* For the non-simple case of CoursesController#enrollment_invitation
redirect to /register (CommunicationsChannelController#confirm)
* Remove CoursesController#transfer_enrollment
* Move PseudonymsController#registration_confirmation to
CommunicationChannelsController#confirm (have to be able to
register an account without a Pseudonym yet)
* Fold the old direct confirm functionality in, if there are
no available merge opportunities
* Allow merging the new account with the currently logged in user
* Allow changing the Pseudonym#unique_id when registering a new
account (since there might be conflicts)
* Display a list of merge opportunities based on conflicting
communication channels
* Provide link(s) to log in as the other user,
redirecting back to the registration page after login is
complete (to complete the merge as the current user)
* Remove several assert_* methods that are no longer needed
* Update PseudonymSessionsController a bit to deal with the new
way of dealing with conflicting CCs (especially CCs from LDAP),
and to redirect back to the registration/confirmation page when
attempting to do a merge
* Expose the open_registration setting; use it to control if
inviting users to a course is able to create new users
Change-Id: If2f38818a71af656854d3bf8431ddbf5dcb84691
Reviewed-on: https://gerrit.instructure.com/6149
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Jacob Fugal <jacob@instructure.com>
2011-10-13 04:30:48 +08:00
2011-02-01 09:57:29 +08:00
def generate_reminders_if_changed
send_later ( :generate_reminders! ) if @reminder_times_changed
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def generate_reminders!
enrollments = self . current_enrollments
2012-01-13 07:57:58 +08:00
mgmt_course_ids = enrollments . select { | e | e . instructor? } . map ( & :course_id ) . uniq
2011-02-01 09:57:29 +08:00
student_course_ids = enrollments . select { | e | ! e . admin? } . map ( & :course_id ) . uniq
assignments = Assignment . for_courses ( mgmt_course_ids + student_course_ids ) . active . due_after ( Time . now )
student_assignments = assignments . select { | a | student_course_ids . include? ( a . context_id ) }
mgmt_assignments = assignments - student_assignments
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
due_assignment_ids = [ ]
grading_assignment_ids = [ ]
assignment_reminders . each do | r |
res = r . update_for ( self )
if r . reminder_type == 'grading' && res
grading_assignment_ids << r . assignment_id
elsif r . reminder_type == 'due_at' && res
due_assignment_ids << r . assignment_id
end
end
needed_ids = student_assignments . map ( & :id ) - due_assignment_ids
student_assignments . select { | a | needed_ids . include? ( a . id ) } . each do | assignment |
r = assignment_reminders . build ( :user = > self , :assignment = > assignment , :reminder_type = > 'due_at' )
r . update_for ( assignment )
end
needed_ids = mgmt_assignments . map ( & :id ) - grading_assignment_ids
mgmt_assignments . select { | a | needed_ids . include? ( a . id ) } . each do | assignment |
r = assignment_reminders . build ( :user = > self , :assignment = > assignment , :reminder_type = > 'grading' )
r . update_for ( assignment )
end
save
end
2012-05-30 07:35:00 +08:00
def self_enroll_if_necessary
return unless @self_enrollment_course
@self_enrollment_course . self_enroll_student ( self , :skip_pseudonym = > @just_created , :skip_touch_user = > true )
end
2011-02-01 09:57:29 +08:00
def time_difference_from_date ( hash )
n = hash [ :number ] . to_i
n = nil if n == 0
if hash [ :metric ] == " weeks "
( n || 1 ) . weeks . to_i
elsif hash [ :metric ] == " days "
( n || 1 ) . days . to_i
elsif hash [ :metric ] == " hours "
( n || 1 ) . hours . to_i
elsif hash [ :metric ] == " never "
0
else
nil
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def remind_for_due_dates = ( hash )
self . reminder_time_for_due_dates = time_difference_from_date ( hash )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def remind_for_grading = ( hash )
self . reminder_time_for_grading = time_difference_from_date ( hash )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def is_a_context?
true
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def account
self . pseudonym . account rescue Account . default
end
memoize :account
2012-01-04 04:30:49 +08:00
sub-account branding; closes #9368
allow sub accounts to include their own global scripts and stylesheets. if
global includes are enabled on the root account, root account administrators
will have an option to enable them for immediate child accounts. those child
accounts can then choose to enable them for their sub-accounts, and so on down
the chain.
these includes are added to the page in order from highest to lowest account,
so sub-accounts are able to override styles added by their parents.
the logic for which styles to display on which pages is as follows:
- on account pages, include all styles in the chain from this account up to the
root account. this ensures that you can always see styles for account
X without any sub-account overrides on account X's page
- on course/group pages, include all styles in the chain from the account which
contains that course/group up to the root
- on the dashboard, calendar, user pages, and other pages that don't fall into
one of the above categories, we find the lowest account that contains all of
the current user's active classes + groups, and include styles from that
account up to the root
test plan:
- in a root account, create two sub-accounts, create courses in each of them,
and create 3 users, one enrolled only in the first course, one only in the
second course, and one enrolled in both courses.
- enable global includes on the root account (no sub-accounts yet) add files,
and make sure all three students see them.
- now enable sub-account includes, and add include files to each sub-account
- make sure both users in course 1 see include for sub-account 1
- make sure user 1 sees include for sub-account 1 on her dashboard, but user
3 does not.
Change-Id: I3d07d4bced39593f3084d5eac6ea3137666e319b
Reviewed-on: https://gerrit.instructure.com/12248
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-07-10 05:30:16 +08:00
# this finds the reverse account chain starting at in_root_account and ending
# at the lowest account such that all of the accounts to which the user is
# associated which descend from in_root_account, descend from one of the
# accounts in the chain. In other words, if the users associated accounts
# made a tree, it would be the chain between the root and the first branching
# point.
def common_account_chain ( in_root_account )
2012-08-19 02:37:31 +08:00
rid = in_root_account . id
accts = self . associated_accounts . scoped ( :conditions = > [ " accounts.id = ? OR accounts.root_account_id = ? " , rid , rid ] )
2012-08-19 08:51:52 +08:00
return [ ] if accts . blank?
2012-08-19 02:37:31 +08:00
children = accts . inject ( { } ) do | hash , acct |
pid = acct . parent_account_id
if pid . present?
hash [ pid ] || = [ ]
hash [ pid ] << acct
sub-account branding; closes #9368
allow sub accounts to include their own global scripts and stylesheets. if
global includes are enabled on the root account, root account administrators
will have an option to enable them for immediate child accounts. those child
accounts can then choose to enable them for their sub-accounts, and so on down
the chain.
these includes are added to the page in order from highest to lowest account,
so sub-accounts are able to override styles added by their parents.
the logic for which styles to display on which pages is as follows:
- on account pages, include all styles in the chain from this account up to the
root account. this ensures that you can always see styles for account
X without any sub-account overrides on account X's page
- on course/group pages, include all styles in the chain from the account which
contains that course/group up to the root
- on the dashboard, calendar, user pages, and other pages that don't fall into
one of the above categories, we find the lowest account that contains all of
the current user's active classes + groups, and include styles from that
account up to the root
test plan:
- in a root account, create two sub-accounts, create courses in each of them,
and create 3 users, one enrolled only in the first course, one only in the
second course, and one enrolled in both courses.
- enable global includes on the root account (no sub-accounts yet) add files,
and make sure all three students see them.
- now enable sub-account includes, and add include files to each sub-account
- make sure both users in course 1 see include for sub-account 1
- make sure user 1 sees include for sub-account 1 on her dashboard, but user
3 does not.
Change-Id: I3d07d4bced39593f3084d5eac6ea3137666e319b
Reviewed-on: https://gerrit.instructure.com/12248
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-07-10 05:30:16 +08:00
end
2012-08-19 02:37:31 +08:00
hash
end
sub-account branding; closes #9368
allow sub accounts to include their own global scripts and stylesheets. if
global includes are enabled on the root account, root account administrators
will have an option to enable them for immediate child accounts. those child
accounts can then choose to enable them for their sub-accounts, and so on down
the chain.
these includes are added to the page in order from highest to lowest account,
so sub-accounts are able to override styles added by their parents.
the logic for which styles to display on which pages is as follows:
- on account pages, include all styles in the chain from this account up to the
root account. this ensures that you can always see styles for account
X without any sub-account overrides on account X's page
- on course/group pages, include all styles in the chain from the account which
contains that course/group up to the root
- on the dashboard, calendar, user pages, and other pages that don't fall into
one of the above categories, we find the lowest account that contains all of
the current user's active classes + groups, and include styles from that
account up to the root
test plan:
- in a root account, create two sub-accounts, create courses in each of them,
and create 3 users, one enrolled only in the first course, one only in the
second course, and one enrolled in both courses.
- enable global includes on the root account (no sub-accounts yet) add files,
and make sure all three students see them.
- now enable sub-account includes, and add include files to each sub-account
- make sure both users in course 1 see include for sub-account 1
- make sure user 1 sees include for sub-account 1 on her dashboard, but user
3 does not.
Change-Id: I3d07d4bced39593f3084d5eac6ea3137666e319b
Reviewed-on: https://gerrit.instructure.com/12248
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-07-10 05:30:16 +08:00
2012-08-19 02:37:31 +08:00
longest_chain = [ in_root_account ]
while true
next_children = children [ longest_chain . last . id ]
break unless next_children . present? && next_children . count == 1
longest_chain << next_children . first
sub-account branding; closes #9368
allow sub accounts to include their own global scripts and stylesheets. if
global includes are enabled on the root account, root account administrators
will have an option to enable them for immediate child accounts. those child
accounts can then choose to enable them for their sub-accounts, and so on down
the chain.
these includes are added to the page in order from highest to lowest account,
so sub-accounts are able to override styles added by their parents.
the logic for which styles to display on which pages is as follows:
- on account pages, include all styles in the chain from this account up to the
root account. this ensures that you can always see styles for account
X without any sub-account overrides on account X's page
- on course/group pages, include all styles in the chain from the account which
contains that course/group up to the root
- on the dashboard, calendar, user pages, and other pages that don't fall into
one of the above categories, we find the lowest account that contains all of
the current user's active classes + groups, and include styles from that
account up to the root
test plan:
- in a root account, create two sub-accounts, create courses in each of them,
and create 3 users, one enrolled only in the first course, one only in the
second course, and one enrolled in both courses.
- enable global includes on the root account (no sub-accounts yet) add files,
and make sure all three students see them.
- now enable sub-account includes, and add include files to each sub-account
- make sure both users in course 1 see include for sub-account 1
- make sure user 1 sees include for sub-account 1 on her dashboard, but user
3 does not.
Change-Id: I3d07d4bced39593f3084d5eac6ea3137666e319b
Reviewed-on: https://gerrit.instructure.com/12248
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-07-10 05:30:16 +08:00
end
2012-08-19 02:37:31 +08:00
longest_chain
sub-account branding; closes #9368
allow sub accounts to include their own global scripts and stylesheets. if
global includes are enabled on the root account, root account administrators
will have an option to enable them for immediate child accounts. those child
accounts can then choose to enable them for their sub-accounts, and so on down
the chain.
these includes are added to the page in order from highest to lowest account,
so sub-accounts are able to override styles added by their parents.
the logic for which styles to display on which pages is as follows:
- on account pages, include all styles in the chain from this account up to the
root account. this ensures that you can always see styles for account
X without any sub-account overrides on account X's page
- on course/group pages, include all styles in the chain from the account which
contains that course/group up to the root
- on the dashboard, calendar, user pages, and other pages that don't fall into
one of the above categories, we find the lowest account that contains all of
the current user's active classes + groups, and include styles from that
account up to the root
test plan:
- in a root account, create two sub-accounts, create courses in each of them,
and create 3 users, one enrolled only in the first course, one only in the
second course, and one enrolled in both courses.
- enable global includes on the root account (no sub-accounts yet) add files,
and make sure all three students see them.
- now enable sub-account includes, and add include files to each sub-account
- make sure both users in course 1 see include for sub-account 1
- make sure user 1 sees include for sub-account 1 on her dashboard, but user
3 does not.
Change-Id: I3d07d4bced39593f3084d5eac6ea3137666e319b
Reviewed-on: https://gerrit.instructure.com/12248
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-07-10 05:30:16 +08:00
end
2012-06-30 04:37:05 +08:00
def courses_with_primary_enrollment ( association = :current_and_invited_courses , enrollment_uuid = nil , options = { } )
res = Rails . cache . fetch ( [ self , 'courses_with_primary_enrollment' , association , options ] . cache_key , :expires_in = > 15 . minutes ) do
2012-03-16 13:40:42 +08:00
courses = send ( association ) . distinct_on ( [ " courses.id " ] ,
:select = > " courses.*, enrollments.id AS primary_enrollment_id, enrollments.type AS primary_enrollment, #{ Enrollment . type_rank_sql } AS primary_enrollment_rank, enrollments.workflow_state AS primary_enrollment_state " ,
:order = > " courses.id, #{ Enrollment . type_rank_sql } , #{ Enrollment . state_rank_sql } " )
2012-06-30 04:37:05 +08:00
unless options [ :include_completed_courses ]
date_restricted = Enrollment . find ( :all , :conditions = > { :id = > courses . map ( & :primary_enrollment_id ) } ) . select { | e | e . completed? || e . inactive? } . map ( & :id )
courses . reject! { | course | date_restricted . include? ( course . primary_enrollment_id . to_i ) }
end
2012-03-16 13:40:42 +08:00
courses
2011-11-08 03:10:20 +08:00
end . dup
if association == :current_and_invited_courses
if enrollment_uuid && pending_course = Course . find ( :first ,
2012-03-16 13:40:42 +08:00
:select = > " courses.*, enrollments.type AS primary_enrollment, #{ Enrollment . type_rank_sql } AS primary_enrollment_rank, enrollments.workflow_state AS primary_enrollment_state " ,
:joins = > :enrollments , :conditions = > [ " enrollments.uuid=? AND enrollments.workflow_state='invited' " , enrollment_uuid ] )
2011-11-08 03:10:20 +08:00
res << pending_course
res . uniq!
end
pending_enrollments = temporary_invitations
unless pending_enrollments . empty?
Enrollment . send ( :preload_associations , pending_enrollments , :course )
res . concat ( pending_enrollments . map { | e | c = e . course ; c . write_attribute ( :primary_enrollment , e . type ) ; c . write_attribute ( :primary_enrollment_rank , e . rank_sortable . to_s ) ; c . write_attribute ( :primary_enrollment_state , e . workflow_state ) ; c . write_attribute ( :invitation , e . uuid ) ; c } )
res . uniq!
end
2011-10-07 07:38:54 +08:00
end
2011-11-08 03:10:20 +08:00
res . sort_by { | c | [ c . primary_enrollment_rank , c . name . downcase ] }
2011-10-07 07:38:54 +08:00
end
memoize :courses_with_primary_enrollment
2011-11-08 03:10:20 +08:00
def cached_active_emails
Rails . cache . fetch ( [ self , 'active_emails' ] . cache_key ) do
self . communication_channels . active . email . map ( & :path )
end
end
def temporary_invitations
cached_active_emails . map { | email | Enrollment . cached_temporary_invitations ( email ) . dup . reject { | e | e . user_id == self . id } } . flatten
end
2011-02-01 09:57:29 +08:00
# activesupport/lib/active_support/memoizable.rb from rails and
# http://github.com/seamusabshere/cacheable/blob/master/lib/cacheable.rb from the cacheable gem
# to get a head start
2012-01-04 04:30:49 +08:00
# this method takes an optional {:include_enrollment_uuid => uuid} so that you can pass it the session[:enrollment_uuid] and it will include it.
2011-02-01 09:57:29 +08:00
def cached_current_enrollments ( opts = { } )
2011-11-08 03:10:20 +08:00
res = Rails . cache . fetch ( [ self , 'current_enrollments' , opts [ :include_enrollment_uuid ] ] . cache_key ) do
2012-01-13 23:31:20 +08:00
res = self . current_and_invited_enrollments ( true ) . to_a . dup
2011-02-01 09:57:29 +08:00
if opts [ :include_enrollment_uuid ] && pending_enrollment = Enrollment . find_by_uuid_and_workflow_state ( opts [ :include_enrollment_uuid ] , " invited " )
res << pending_enrollment
res . uniq!
end
res
2011-11-08 03:10:20 +08:00
end + temporary_invitations
2011-02-01 09:57:29 +08:00
end
memoize :cached_current_enrollments
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def cached_not_ended_enrollments
@cached_all_enrollments = Rails . cache . fetch ( [ self , 'not_ended_enrollments' ] . cache_key ) do
self . not_ended_enrollments . to_a
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def cached_current_group_memberships
2012-02-16 05:40:13 +08:00
@cached_current_group_memberships = Rails . cache . fetch ( [ self , 'current_group_memberships' ] . cache_key ) do
self . current_group_memberships . to_a
end
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def current_student_enrollment_course_codes
@current_student_enrollment_course_codes || = Rails . cache . fetch ( [ self , 'current_student_enrollment_course_codes' ] . cache_key ) do
self . enrollments . student . scoped ( :select = > " course_id " ) . map { | e | " course_ #{ e . course_id } " }
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def current_admin_enrollment_course_codes
@current_admin_enrollment_course_codes || = Rails . cache . fetch ( [ self , 'current_admin_enrollment_course_codes' ] . cache_key ) do
self . enrollments . admin . scoped ( :select = > " course_id " ) . map { | e | " course_ #{ e . course_id } " }
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# TODO: this smells, I really don't get it (anymore... I wrote it :-( )
def self . module_progression_job_queued ( user_id , time_string = nil )
time_string || = Time . now . utc . iso8601
@@user_jobs || = { }
@@user_jobs [ user_id ] || = time_string
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def self . module_progression_jobs_queued? ( user_id )
recent = 1 . minute . ago . utc . iso8601
@@user_jobs || = { }
! ! ( @@user_jobs && @@user_jobs [ user_id ] && @@user_jobs [ user_id ] > recent )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def submissions_for_context_codes ( context_codes , opts = { } )
return [ ] if ( ! context_codes || context_codes . empty? )
opts [ :start_at ] || = 2 . weeks . ago
opts [ :limit ] || = 20
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
submissions = [ ]
2012-01-04 04:30:49 +08:00
submissions += self . submissions . after ( opts [ :start_at ] ) . for_context_codes ( context_codes ) . find (
:all ,
2011-09-15 06:41:56 +08:00
:conditions = > [ " submissions.score IS NOT NULL AND assignments.workflow_state != ? AND assignments.muted = ? " , 'deleted' , false ] ,
2011-02-01 09:57:29 +08:00
:include = > [ :assignment , :user , :submission_comments ] ,
:order = > 'submissions.created_at DESC' ,
:limit = > opts [ :limit ]
)
2011-09-15 06:41:56 +08:00
2011-02-01 09:57:29 +08:00
# THIS IS SLOW, it takes ~230ms for mike
submissions += Submission . for_context_codes ( context_codes ) . find (
:all ,
2011-03-01 08:37:39 +08:00
:select = > " submissions.*, last_updated_at_from_db " ,
2012-01-04 04:30:49 +08:00
:joins = > self . class . send ( :sanitize_sql_array , [ <<-SQL, opts[:start_at], self.id, self.id]),
2011-03-01 08:37:39 +08:00
INNER JOIN (
SELECT MAX ( submission_comments . created_at ) AS last_updated_at_from_db , submission_id
FROM submission_comments , submission_comment_participants
WHERE submission_comments . id = submission_comment_id
AND ( submission_comments . created_at > ?)
AND ( submission_comment_participants . user_id = ?)
AND ( submission_comments . author_id < > ?)
GROUP BY submission_id
) AS relevant_submission_comments ON submissions . id = submission_id
2011-03-23 06:23:04 +08:00
INNER JOIN assignments ON assignments . id = submissions . assignment_id AND assignments . workflow_state < > 'deleted'
2011-03-01 08:37:39 +08:00
SQL
:order = > 'last_updated_at_from_db DESC' ,
2011-09-15 06:41:56 +08:00
:limit = > opts [ :limit ] ,
:conditions = > { " assignments.muted " = > false }
2011-02-01 09:57:29 +08:00
)
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
submissions = submissions . sort_by { | t | ( t . last_updated_at_from_db . to_datetime . in_time_zone rescue nil ) || t . created_at } . reverse
submissions = submissions . uniq
submissions . first ( opts [ :limit ] )
submissions
end
memoize :submissions_for_context_codes
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# This is only feedback for student contexts (unless specific contexts are passed in)
def recent_feedback ( opts = { } )
context_codes = opts [ :context_codes ] || ( opts [ :contexts ] ? setup_context_lookups ( opts [ :contexts ] ) : self . current_student_enrollment_course_codes )
submissions_for_context_codes ( context_codes , opts )
end
memoize :recent_feedback
2012-01-04 04:30:49 +08:00
2012-06-02 05:06:29 +08:00
def visible_stream_item_instances ( opts = { } )
instances = stream_item_instances . scoped ( :conditions = > { 'stream_item_instances.hidden' = > false } , :order = > 'stream_item_instances.id desc' , :include = > :stream_item )
2012-01-04 04:30:49 +08:00
2012-06-02 05:06:29 +08:00
# dont make the query do an stream_item_instances.context_code IN
2011-05-12 06:02:16 +08:00
# ('course_20033','course_20237','course_20247' ...) if they dont pass any
# contexts, just assume it wants any context code.
2011-02-01 09:57:29 +08:00
if opts [ :contexts ]
# still need to optimize the query to use a root_context_code. that way a
# users course dashboard even if they have groups does a query with
# "context_code=..." instead of "context_code IN ..."
2012-06-02 05:06:29 +08:00
instances = instances . scoped ( :conditions = > [ 'stream_item_instances.context_code in (?)' , setup_context_lookups ( opts [ :contexts ] ) ] )
2011-02-01 09:57:29 +08:00
end
2012-06-02 05:06:29 +08:00
instances
2012-05-11 03:59:21 +08:00
end
2011-02-01 09:57:29 +08:00
2012-05-11 03:59:21 +08:00
def recent_stream_items ( opts = { } )
2012-09-05 01:42:51 +08:00
# cross-shard stream items need a *lot* of work; just disable them for now
return [ ] if self . shard != Shard . current
2012-08-21 01:12:08 +08:00
ActiveRecord :: Base :: ConnectionSpecification . with_environment ( :slave ) do
visible_stream_item_instances ( opts ) . scoped ( :include = > :stream_item , :limit = > 21 ) . map ( & :stream_item ) . compact
end
2011-02-01 09:57:29 +08:00
end
2012-05-11 03:59:21 +08:00
memoize :recent_stream_items
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def calendar_events_for_calendar ( opts = { } )
opts = opts . dup
context_codes = opts [ :context_codes ] || ( opts [ :contexts ] ? setup_context_lookups ( opts [ :contexts ] ) : self . cached_context_codes )
return [ ] if ( ! context_codes || context_codes . empty? )
opts [ :start_at ] || = 2 . weeks . ago
opts [ :end_at ] || = 1 . weeks . from_now
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
events = [ ]
ev = CalendarEvent
ev = CalendarEvent . active if ! opts [ :include_deleted_events ]
2012-01-04 04:30:49 +08:00
event_codes = context_codes + AppointmentGroup . manageable_by ( self , context_codes ) . intersecting ( opts [ :start_at ] , opts [ :end_at ] ) . map ( & :asset_string )
2012-04-18 06:38:45 +08:00
events += ev . for_user_and_context_codes ( self , event_codes , [ ] ) . between ( opts [ :start_at ] , opts [ :end_at ] ) . updated_after ( opts [ :updated_at ] )
2012-01-04 04:30:49 +08:00
events += Assignment . active . for_context_codes ( context_codes ) . due_between ( opts [ :start_at ] , opts [ :end_at ] ) . updated_after ( opts [ :updated_at ] ) . with_just_calendar_attributes
2011-02-01 09:57:29 +08:00
events . sort_by { | e | [ e . start_at , e . title || " " ] } . uniq
end
def upcoming_events ( opts = { } )
context_codes = opts [ :context_codes ] || ( opts [ :contexts ] ? setup_context_lookups ( opts [ :contexts ] ) : self . cached_context_codes )
return [ ] if ( ! context_codes || context_codes . empty? )
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
opts [ :end_at ] || = 1 . weeks . from_now
opts [ :limit ] || = 20
2012-01-04 04:30:49 +08:00
2012-04-18 06:38:45 +08:00
events = CalendarEvent . active . for_user_and_context_codes ( self , context_codes ) . between ( Time . now . utc , opts [ :end_at ] ) . scoped ( :limit = > opts [ :limit ] ) . reject ( & :hidden? )
2012-01-04 04:30:49 +08:00
events += Assignment . active . for_context_codes ( context_codes ) . due_between ( Time . now . utc , opts [ :end_at ] ) . scoped ( :limit = > opts [ :limit ] ) . include_submitted_count
2012-04-19 07:47:03 +08:00
appointment_groups = AppointmentGroup . manageable_by ( self , context_codes ) . intersecting ( Time . now . utc , opts [ :end_at ] ) . scoped ( :limit = > opts [ :limit ] )
appointment_groups . each { | ag | ag . context = ag . contexts_for_user ( self ) . first }
events += appointment_groups
2011-02-01 09:57:29 +08:00
events . sort_by { | e | [ e . start_at , e . title ] } . uniq . first ( opts [ :limit ] )
end
def undated_events ( opts = { } )
opts = opts . dup
context_codes = opts [ :context_codes ] || ( opts [ :contexts ] ? setup_context_lookups ( opts [ :contexts ] ) : self . cached_context_codes )
return [ ] if ( ! context_codes || context_codes . empty? )
undated_events = [ ]
2012-04-18 06:38:45 +08:00
undated_events += CalendarEvent . active . for_user_and_context_codes ( self , context_codes , [ ] ) . undated . updated_after ( opts [ :updated_at ] )
2011-02-01 09:57:29 +08:00
undated_events += Assignment . active . for_context_codes ( context_codes ) . undated . updated_after ( opts [ :updated_at ] ) . with_just_calendar_attributes
undated_events . sort_by { | e | e . title }
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def setup_context_lookups ( contexts = nil )
# TODO: All the event methods use this and it's really slow.
Array ( contexts || cached_contexts ) . map ( & :asset_string )
end
memoize :setup_context_lookups
# TODO: doesn't actually cache, needs to be optimized
def cached_contexts
@cached_contexts || = begin
context_groups = [ ]
2011-03-02 05:28:15 +08:00
# according to the set_policy block in group.rb, user u can manage group
# g if either:
# (a) g.context.grants_right?(u, :manage_groups)
2012-05-30 06:55:40 +08:00
# (b) g.has_member?(u)
2011-03-02 05:28:15 +08:00
# this is a very performance sensitive method, so we're bypassing the
# normal policy checking and somewhat duplicating auth logic here. which
# is a shame. it'd be really nice to add support to our policy framework
# for understanding how to load associations based on policies.
self . courses . all ( :include = > :active_groups ) . select { | c | c . grants_right? ( self , :manage_groups ) } . each { | c | context_groups += c . active_groups }
self . courses + ( self . groups . active + context_groups ) . uniq
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
# TODO: doesn't actually cache, needs to be optimized
def cached_context_codes
Array ( self . cached_contexts ) . map ( & :asset_string )
end
2012-01-04 04:30:49 +08:00
# context codes of things that might have a schedulable appointment for the
# given user, i.e. courses and sections
def appointment_context_codes
ret = { :primary = > [ ] , :secondary = > [ ] }
cached_current_enrollments . each do | e |
2012-03-14 04:08:19 +08:00
next unless e . student? && e . active?
2012-01-04 04:30:49 +08:00
ret [ :primary ] << " course_ #{ e . course_id } "
ret [ :secondary ] << " course_section_ #{ e . course_section_id } "
end
ret [ :secondary ] . concat groups . map { | g | " group_category_ #{ g . group_category_id } " }
ret
end
memoize :appointment_context_codes
def manageable_appointment_context_codes
ret = { :full = > [ ] , :limited = > [ ] , :secondary = > [ ] }
cached_current_enrollments . each do | e |
next unless e . course . grants_right? ( self , nil , :manage_calendar )
if e . course . visibility_limited_to_course_sections? ( self )
ret [ :limited ] << " course_ #{ e . course_id } "
ret [ :secondary ] << " course_section_ #{ e . course_section_id } "
else
ret [ :full ] << " course_ #{ e . course_id } "
end
end
ret
end
memoize :manageable_appointment_context_codes
conversation course filters, fixes #6827
implemented generic tags for conversations (and messages), but currently those
only track course/group asset strings. conversations are automatically tagged
with the most relevant courses/groups, i.e.
1. if a specific course/group is a recipient, it will be added as a tag
2. if a course can be inferred, it will be added as a tag. this can happen
several ways:
* if a user is added by browsing/searching under a course
* if a synthetic context or section is a recipient (e.g. course_1_students)
* if a group is a recipient
3. if there are no explicit or inferred tags, we do the old behavior (tag
courses/groups that are shared by > 50% of the audience)
some notes:
1. as was the previous behavior, you can only see tags (courses/groups) that
you belong to. for example, a teacher could send a message to a student
group, but the teacher would not see the group as a tag (though the
students would)
2. tags can be added as recipients are added (though we still just show <= 2
in the UI). a private conversation may get new tags if a bare PM is sent
and a different context can be inferred (see below)
3. private conversations may have tags removed, since we also track them at
a message level. the scenario is if you have a bunch of messages from your
Spanish 101 teacher. the course ends and you then take Spanish 102 from the
same teacher, and the teacher sends the class a bunch of PMs. if you delete
the old messages, your view of the conversation will lose the Spanish 101
tags.
added a course filter dropdown in the ui. currently this is limited to active
enrollments, and only returns courses (no groups)
test plan:
1. test automatic tagging for new conversations
* ensure explicit tags work (course as recipient)
* ensure inferred tags work (e.g. user under a course)
* ensure default tags work (e.g. send a PM to someone you have a class with)
2. test automatic tagging for adding recipients (same as above)
3. test automatic tagging for a new message on a private conversation, i.e.
1. find a user you share 2+ contexts with
2. start a private conversation w/ a single inferred tag, ensure that only
that tag is on the conversation
3. start another private conversation w/ a different inferred tag (this will
reuse the existing conversation), ensure it has both tags
4. test private conversation tag recalculation, i.e. perform test 3, remove a
message, and ensure there is just one tag again
5. test conversation filtering in the UI
migration:
there is no traditional migration per se, rather we will run something like:
Conversation.find_each{ |c| c.migrate_context_tags! }
Change-Id: If467c8739ef39a655ef5a528b0da77213130e825
Reviewed-on: https://gerrit.instructure.com/8225
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Jacob Fugal <jacob@instructure.com>
2012-01-24 02:26:47 +08:00
def conversation_context_codes
2012-04-26 05:57:43 +08:00
Rails . cache . fetch ( [ self , 'conversation_context_codes2' ] . cache_key , :expires_in = > 1 . day ) do
( courses . map { | c | " course_ #{ c . id } " } +
concluded_courses . map { | c | " course_ #{ c . id } " } +
current_groups . map { | g | " group_ #{ g . id } " }
) . uniq
2012-03-03 06:39:56 +08:00
end
conversation course filters, fixes #6827
implemented generic tags for conversations (and messages), but currently those
only track course/group asset strings. conversations are automatically tagged
with the most relevant courses/groups, i.e.
1. if a specific course/group is a recipient, it will be added as a tag
2. if a course can be inferred, it will be added as a tag. this can happen
several ways:
* if a user is added by browsing/searching under a course
* if a synthetic context or section is a recipient (e.g. course_1_students)
* if a group is a recipient
3. if there are no explicit or inferred tags, we do the old behavior (tag
courses/groups that are shared by > 50% of the audience)
some notes:
1. as was the previous behavior, you can only see tags (courses/groups) that
you belong to. for example, a teacher could send a message to a student
group, but the teacher would not see the group as a tag (though the
students would)
2. tags can be added as recipients are added (though we still just show <= 2
in the UI). a private conversation may get new tags if a bare PM is sent
and a different context can be inferred (see below)
3. private conversations may have tags removed, since we also track them at
a message level. the scenario is if you have a bunch of messages from your
Spanish 101 teacher. the course ends and you then take Spanish 102 from the
same teacher, and the teacher sends the class a bunch of PMs. if you delete
the old messages, your view of the conversation will lose the Spanish 101
tags.
added a course filter dropdown in the ui. currently this is limited to active
enrollments, and only returns courses (no groups)
test plan:
1. test automatic tagging for new conversations
* ensure explicit tags work (course as recipient)
* ensure inferred tags work (e.g. user under a course)
* ensure default tags work (e.g. send a PM to someone you have a class with)
2. test automatic tagging for adding recipients (same as above)
3. test automatic tagging for a new message on a private conversation, i.e.
1. find a user you share 2+ contexts with
2. start a private conversation w/ a single inferred tag, ensure that only
that tag is on the conversation
3. start another private conversation w/ a different inferred tag (this will
reuse the existing conversation), ensure it has both tags
4. test private conversation tag recalculation, i.e. perform test 3, remove a
message, and ensure there is just one tag again
5. test conversation filtering in the UI
migration:
there is no traditional migration per se, rather we will run something like:
Conversation.find_each{ |c| c.migrate_context_tags! }
Change-Id: If467c8739ef39a655ef5a528b0da77213130e825
Reviewed-on: https://gerrit.instructure.com/8225
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Jacob Fugal <jacob@instructure.com>
2012-01-24 02:26:47 +08:00
end
memoize :conversation_context_codes
2012-04-18 06:38:45 +08:00
def section_context_codes ( context_codes )
course_ids = context_codes . grep ( / \ Acourse_ \ d+ \ z / ) . map { | s | s . sub ( / \ Acourse_ / , '' ) . to_i }
return [ ] unless course_ids . present?
Course . find_all_by_id ( course_ids ) . inject ( [ ] ) do | ary , course |
ary . concat course . sections_visible_to ( self ) . map ( & :asset_string )
end
end
2012-02-21 08:23:23 +08:00
def manageable_courses ( include_concluded = false )
Course . manageable_by_user ( self . id , include_concluded ) . not_deleted
2011-02-01 09:57:29 +08:00
end
2011-06-09 03:56:27 +08:00
2012-02-21 08:23:23 +08:00
def manageable_courses_name_like ( query = '' , include_concluded = false )
self . manageable_courses ( include_concluded ) . not_deleted . name_like ( query ) . limit ( 50 )
2011-06-09 03:56:27 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def last_completed_module
self . context_module_progressions . select { | p | p . completed? } . sort_by { | p | p . completed_at || p . created_at } . last . context_module rescue nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def last_completed_course
self . enrollments . select { | e | e . completed? } . sort_by { | e | e . completed_at || e . created_at } . last . course rescue nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def last_mastered_assignment
self . learning_outcome_results . sort_by { | r | r . assessed_at || r . created_at } . select { | r | r . mastery? } . map { | r | r . assignment } . last
end
2012-01-04 04:30:49 +08:00
2011-08-05 02:35:32 +08:00
def profile_pics_folder
2012-02-28 07:54:00 +08:00
initialize_default_folder ( Folder :: PROFILE_PICS_FOLDER_NAME )
end
def conversation_attachments_folder
initialize_default_folder ( Folder :: CONVERSATION_ATTACHMENTS_FOLDER_NAME )
end
def initialize_default_folder ( name )
folder = self . active_folders . find_by_name ( name )
2011-08-05 02:35:32 +08:00
unless folder
2012-02-28 07:54:00 +08:00
folder = self . folders . create! ( :name = > name ,
2011-08-05 02:35:32 +08:00
:parent_folder = > Folder . root_folders ( self ) . find { | f | f . name == Folder :: MY_FILES_FOLDER_NAME } )
end
folder
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def quota
2012-07-03 03:16:08 +08:00
return read_attribute ( :storage_quota ) if read_attribute ( :storage_quota )
accounts = associated_root_accounts . reject ( & :site_admin? )
accounts . empty? ?
self . class . default_storage_quota :
accounts . sum ( & :default_user_storage_quota )
end
def self . default_storage_quota
Setting . get_cached ( 'user_default_quota' , 50 . megabytes . to_s ) . to_i
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def update_last_user_note
note = user_notes . active . scoped ( :order = > 'user_notes.created_at DESC' , :limit = > 1 ) . first
self . last_user_note = note ? note . created_at : nil
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
TAB_PROFILE = 0
TAB_COMMUNICATION_PREFERENCES = 1
TAB_FILES = 2
TAB_EPORTFOLIOS = 3
TAB_HOME = 4
2011-05-13 05:48:18 +08:00
def sis_user_id
pseudonym . try ( :sis_user_id )
end
2012-01-04 04:30:49 +08:00
2011-08-27 14:18:36 +08:00
def highest_role
return 'admin' unless self . accounts . empty?
return 'teacher' if self . cached_current_enrollments . any? ( & :admin? )
return 'student' if self . cached_current_enrollments . any? ( & :student? )
return 'user'
end
memoize :highest_role
2011-06-28 03:43:06 +08:00
2011-11-11 10:43:36 +08:00
def roles
res = [ 'user' ]
res << 'student' if self . cached_current_enrollments . any? ( & :student? )
res << 'teacher' if self . cached_current_enrollments . any? ( & :admin? )
res << 'admin' unless self . accounts . empty?
res
end
memoize :roles
2011-08-24 03:44:01 +08:00
def eportfolios_enabled?
accounts = associated_root_accounts . reject ( & :site_admin? )
accounts . size == 0 || accounts . any? { | a | a . settings [ :enable_eportfolios ] != false }
end
2011-05-20 00:33:20 +08:00
def initiate_conversation ( user_ids , private = nil )
2011-08-17 03:32:39 +08:00
user_ids = ( [ self . id ] + user_ids ) . uniq
2011-08-18 05:37:49 +08:00
private = user_ids . size < = 2 if private . nil?
2011-08-17 03:32:39 +08:00
Conversation . initiate ( user_ids , private ) . conversation_participants . find_by_user_id ( self . id )
2011-05-20 00:33:20 +08:00
end
2012-03-01 03:57:53 +08:00
def messageable_user_clause
" users.workflow_state IN ('registered', 'pre_registered') "
end
def messageable_enrollment_user_clause
" EXISTS (SELECT 1 FROM users WHERE id = enrollments.user_id AND #{ messageable_user_clause } ) "
end
2012-08-04 17:41:44 +08:00
def messageable_enrollment_clause ( options = { } )
options = { :strict_course_state = > true } . merge ( options )
2012-03-16 00:34:31 +08:00
<<-SQL
(
2012-08-04 17:41:44 +08:00
#{self.class.enrollment_conditions(:current_and_invited, options[:strict_course_state])}
2012-03-16 00:34:31 +08:00
OR
2012-08-04 17:41:44 +08:00
#{self.class.enrollment_conditions(:completed, options[:strict_course_state])}
#{options[:include_concluded_students] ? "" : "AND enrollments.type IN ('TeacherEnrollment', 'TaEnrollment')"}
2012-03-16 00:34:31 +08:00
)
SQL
end
2011-08-19 06:03:33 +08:00
def enrollment_visibility
2012-07-21 22:40:42 +08:00
Rails . cache . fetch ( [ self , 'enrollment_visibility_with_sections_2' ] . cache_key , :expires_in = > 1 . day ) do
2011-08-19 06:03:33 +08:00
full_course_ids = [ ]
section_id_hash = { }
restricted_course_hash = { }
2011-08-27 02:58:34 +08:00
user_counts = { }
2011-10-01 07:07:35 +08:00
section_user_counts = { }
2012-06-30 04:37:05 +08:00
student_in_course_ids = [ ]
linked_observer_ids = observee_enrollments . collect { | e | e . user_id } . uniq
courses_with_primary_enrollment ( :current_and_concluded_courses , nil , :include_completed_courses = > true ) . each do | course |
2011-08-19 06:03:33 +08:00
section_visibilities = course . section_visibilities_for ( self )
2011-08-27 02:58:34 +08:00
conditions = nil
2011-08-19 06:03:33 +08:00
case course . enrollment_visibility_level_for ( self , section_visibilities )
2011-08-27 02:58:34 +08:00
when :full
full_course_ids << course . id
when :sections
section_id_hash [ course . id ] = section_visibilities . map { | s | s [ :course_section_id ] }
conditions = { :course_section_id = > section_id_hash [ course . id ] }
2011-08-19 06:03:33 +08:00
when :restricted
section_visibilities . each do | s |
2011-09-01 04:13:50 +08:00
restricted_course_hash [ course . id ] || = [ ]
restricted_course_hash [ course . id ] << s [ :associated_user_id ] if s [ :associated_user_id ]
2011-08-19 06:03:33 +08:00
end
2011-08-27 02:58:34 +08:00
conditions = " enrollments.type = 'TeacherEnrollment' OR enrollments.type = 'TaEnrollment' OR enrollments.user_id IN ( #{ ( [ self . id ] + restricted_course_hash [ course . id ] . uniq ) . join ( ',' ) } ) "
2011-08-19 06:03:33 +08:00
end
2012-03-16 00:34:31 +08:00
base_conditions = messageable_enrollment_clause
2012-03-01 03:57:53 +08:00
base_conditions << " AND " << messageable_enrollment_user_clause
2012-06-30 04:37:05 +08:00
if course . primary_enrollment == 'StudentEnrollment'
student_in_course_ids << course . id
base_conditions << " AND (enrollments.type != 'ObserverEnrollment' "
base_conditions << " OR enrollments.user_id IN ( #{ linked_observer_ids . join ( ',' ) } ) " if linked_observer_ids . any?
base_conditions << " ) "
end
2012-03-14 04:08:19 +08:00
user_counts [ course . id ] = course . enrollments . scoped ( :conditions = > base_conditions ) . scoped ( :conditions = > conditions ) . scoped ( :conditions = > " enrollments.type != 'StudentViewEnrollment' " ) . count ( " DISTINCT user_id " )
2011-10-01 07:07:35 +08:00
sections = course . sections_visible_to ( self )
if sections . size > 1
sections . each { | section | section_user_counts [ section . id ] = 0 }
2012-03-14 04:08:19 +08:00
connection . select_all ( " SELECT course_section_id, COUNT(DISTINCT user_id) AS user_count FROM courses, enrollments WHERE ( #{ base_conditions } ) AND course_section_id IN ( #{ sections . map ( & :id ) . join ( ', ' ) } ) AND courses.id = #{ course . id } AND enrollments.type != 'StudentViewEnrollment' GROUP BY course_section_id " ) . each do | row |
2011-10-01 07:07:35 +08:00
section_user_counts [ row [ " course_section_id " ] . to_i ] = row [ " user_count " ] . to_i
end
end
2011-08-19 06:03:33 +08:00
end
2011-08-27 02:58:34 +08:00
{ :full_course_ids = > full_course_ids ,
:section_id_hash = > section_id_hash ,
:restricted_course_hash = > restricted_course_hash ,
2011-10-01 07:07:35 +08:00
:user_counts = > user_counts ,
2012-06-30 04:37:05 +08:00
:section_user_counts = > section_user_counts ,
:student_in_course_ids = > student_in_course_ids ,
:linked_observer_ids = > linked_observer_ids
2011-08-27 02:58:34 +08:00
}
2011-08-19 06:03:33 +08:00
end
end
2011-09-30 11:29:41 +08:00
memoize :enrollment_visibility
2011-08-19 06:03:33 +08:00
2011-09-10 04:44:22 +08:00
def messageable_groups
group_visibility = group_membership_visibility
2011-10-01 07:07:35 +08:00
Group . scoped ( :conditions = > { :id = > visible_group_ids . reject { | id | group_visibility [ :user_counts ] [ id ] == 0 } + [ 0 ] } )
2011-09-10 04:44:22 +08:00
end
def visible_group_ids
2012-03-03 06:39:56 +08:00
Rails . cache . fetch ( [ self , 'messageable_groups' ] . cache_key , :expires_in = > 1 . day ) do
2011-09-10 04:44:22 +08:00
( courses + concluded_courses . recently_ended ) . inject ( self . current_groups ) { | groups , course |
groups | course . groups . active
} . map ( & :id )
end
end
2011-09-30 11:29:41 +08:00
memoize :visible_group_ids
2011-09-10 04:44:22 +08:00
def group_membership_visibility
2012-03-03 06:39:56 +08:00
Rails . cache . fetch ( [ self , 'group_membership_visibility' ] . cache_key , :expires_in = > 1 . day ) do
2011-09-10 04:44:22 +08:00
course_visibility = enrollment_visibility
own_group_ids = current_groups . map ( & :id )
full_group_ids = [ ]
section_id_hash = { }
user_counts = { }
if visible_group_ids . present?
Group . find_all_by_id ( visible_group_ids ) . each do | group |
if own_group_ids . include? ( group . id ) || group . context_type == 'Course' && course_visibility [ :full_course_ids ] . include? ( group . context_id )
full_group_ids << group . id
user_counts [ group . id ] = group . users . size
elsif group . context_type == 'Course' && sections = course_visibility [ :section_id_hash ] [ group . context_id ]
section_id_hash [ group . id ] = sections
2012-03-01 03:57:53 +08:00
user_counts [ group . id ] = group . context . enrollments . scoped ( :conditions = > [
2012-08-04 17:41:44 +08:00
" user_id IN (?) AND course_section_id IN (?) AND #{ messageable_enrollment_user_clause } AND #{ messageable_enrollment_clause ( :include_concluded_students = > true ) } " ,
2012-03-01 03:57:53 +08:00
group . group_memberships . map ( & :user_id ) ,
sections
] ) . size
2011-09-10 04:44:22 +08:00
end
end
end
{ :full_group_ids = > full_group_ids ,
:section_id_hash = > section_id_hash ,
:user_counts = > user_counts
}
end
end
2011-09-30 11:29:41 +08:00
memoize :group_membership_visibility
2011-09-10 04:44:22 +08:00
2011-10-27 02:55:27 +08:00
MESSAGEABLE_USER_COLUMNS = [ 'id' , 'short_name' , 'name' , 'avatar_image_url' , 'avatar_image_source' ] . map { | col | " users. #{ col } " }
2011-08-21 08:16:40 +08:00
MESSAGEABLE_USER_COLUMN_SQL = MESSAGEABLE_USER_COLUMNS . join ( " , " )
2011-10-01 07:07:35 +08:00
MESSAGEABLE_USER_CONTEXT_REGEX = / \ A(course|section|group)_( \ d+)(_([a-z]+))? \ z /
2011-05-20 00:33:20 +08:00
def messageable_users ( options = { } )
2012-03-03 06:39:56 +08:00
# if :ids is specified but empty (different than just not specified), don't
# bother doing a query that's guaranteed to return no results.
return [ ] if options [ :ids ] && options [ :ids ] . empty?
2012-08-04 15:39:04 +08:00
# provides a mechanism for admins to search within a context, even if not
# enrolled in it
admin_context = options [ :admin_context ]
2011-09-10 04:44:22 +08:00
course_hash = enrollment_visibility
2012-08-04 15:39:04 +08:00
course_hash [ :full_course_ids ] << admin_context . id if admin_context . is_a? ( Course )
course_hash [ :full_course_ids ] << admin_context . course_id if admin_context . is_a? ( CourseSection )
2011-09-10 04:44:22 +08:00
full_course_ids = course_hash [ :full_course_ids ]
restricted_course_hash = course_hash [ :restricted_course_hash ]
group_hash = group_membership_visibility
2012-08-04 15:39:04 +08:00
group_hash [ :full_group_ids ] << admin_context . id if admin_context . is_a? ( Group )
2011-09-10 04:44:22 +08:00
full_group_ids = group_hash [ :full_group_ids ]
group_section_ids = [ ]
2012-06-30 04:37:05 +08:00
student_in_course_ids = course_hash [ :student_in_course_ids ]
linked_observer_ids = course_hash [ :linked_observer_ids ]
2011-08-01 08:35:03 +08:00
account_ids = [ ]
2011-05-20 00:33:20 +08:00
2011-10-01 07:07:35 +08:00
limited_id = { }
2012-03-14 04:08:19 +08:00
enrollment_type_sql = " AND enrollments.type != 'StudentViewEnrollment' "
2012-07-21 22:40:42 +08:00
if student_in_course_ids . present?
2012-06-30 04:37:05 +08:00
enrollment_type_sql += " AND (enrollments.type != 'ObserverEnrollment' OR course_id NOT IN ( #{ student_in_course_ids . join ( ',' ) } ) "
2012-07-21 22:40:42 +08:00
enrollment_type_sql += " OR user_id IN ( #{ linked_observer_ids . join ( ',' ) } ) " if linked_observer_ids . present?
2012-06-30 04:37:05 +08:00
enrollment_type_sql += " ) "
end
2012-03-16 00:34:31 +08:00
include_concluded_students = true
2011-10-01 07:07:35 +08:00
2011-07-28 00:33:04 +08:00
if options [ :context ]
2011-10-01 07:07:35 +08:00
if options [ :context ] . sub ( / _all \ z / , '' ) =~ MESSAGEABLE_USER_CONTEXT_REGEX
type = $1
2012-03-16 00:34:31 +08:00
include_concluded_students = false unless type == 'group'
2011-10-01 07:07:35 +08:00
limited_id [ type ] = $2 . to_i
enrollment_type = $4
if enrollment_type && type != 'group' # course and section only, since the only group "enrollment type" is member
2011-11-11 10:43:36 +08:00
if enrollment_type == 'admins'
2012-03-14 04:08:19 +08:00
enrollment_type_sql += " AND enrollments.type IN ('TeacherEnrollment','TaEnrollment') "
2011-11-11 10:43:36 +08:00
else
2012-03-14 04:08:19 +08:00
enrollment_type_sql += " AND enrollments.type = ' #{ enrollment_type . capitalize . singularize } Enrollment' "
2011-11-11 10:43:36 +08:00
end
2011-07-28 00:33:04 +08:00
end
end
2011-10-01 07:07:35 +08:00
full_course_ids & = [ limited_id [ 'course' ] ]
full_group_ids & = [ limited_id [ 'group' ] ]
restricted_course_hash . delete_if { | course_id , ids | course_id != limited_id [ 'course' ] }
if limited_id [ 'section' ] && section = CourseSection . find_by_id ( limited_id [ 'section' ] )
course_section_ids = course_hash [ :full_course_ids ] . include? ( section . course_id ) ?
[ limited_id [ 'section' ] ] :
( course_hash [ :section_id_hash ] [ section . course_id ] || [ ] ) & [ limited_id [ 'section' ] ]
else
course_section_ids = course_hash [ :section_id_hash ] . values_at ( limited_id [ 'course' ] ) . flatten . compact
group_section_ids = group_hash [ :section_id_hash ] . values_at ( limited_id [ 'group' ] ) . flatten . compact
end
2011-08-01 08:35:03 +08:00
else
2011-09-10 04:44:22 +08:00
course_section_ids = course_hash [ :section_id_hash ] . values . flatten
2011-10-01 07:07:35 +08:00
# if we're not searching with a context in mind, include any users we
2011-08-01 08:35:03 +08:00
# have admin access to know about
account_ids = associated_accounts . select { | a | a . grants_right? ( self , nil , :read_roster ) } . map ( & :id )
account_ids & = options [ :account_ids ] if options [ :account_ids ]
2011-05-20 00:33:20 +08:00
end
2011-08-01 08:35:03 +08:00
2012-03-07 00:35:25 +08:00
user_conditions = [ ]
2012-08-03 05:56:20 +08:00
if options [ :skip_visibility_checks ]
user_conditions << " users.workflow_state != 'deleted' " if options [ :ids ] . blank?
else
user_conditions << messageable_user_clause
end
2011-10-01 01:14:52 +08:00
user_conditions << " users.id IN ( #{ options [ :ids ] . map ( & :to_i ) . join ( ', ' ) } ) " if options [ :ids ] . present?
user_conditions << " users.id NOT IN ( #{ options [ :exclude_ids ] . map ( & :to_i ) . join ( ', ' ) } ) " if options [ :exclude_ids ] . present?
2011-07-28 00:33:04 +08:00
if options [ :search ] && ( parts = options [ :search ] . strip . split ( / \ s+ / ) ) . present?
parts . each do | part |
2011-10-01 01:14:52 +08:00
user_conditions << " ( #{ wildcard ( 'users.name' , 'users.short_name' , part ) } ) "
2011-07-28 00:33:04 +08:00
end
end
2011-10-01 01:14:52 +08:00
user_condition_sql = user_conditions . present? ? " AND " + user_conditions . join ( " AND " ) : " "
2011-05-20 00:33:20 +08:00
user_sql = [ ]
2011-12-13 08:46:58 +08:00
# this is redundant (and potentially less restrictive than course_sql),
# but it allows the planner to initially limit enrollments to relevant
# courses much more efficiently than the OR'ed course_sql does
all_course_ids = ( course_hash [ :full_course_ids ] + course_hash [ :section_id_hash ] . keys + restricted_course_hash . keys ) . compact
2011-08-19 06:03:33 +08:00
course_sql = [ ]
course_sql << " (course_id IN ( #{ full_course_ids . join ( ',' ) } )) " if full_course_ids . present?
2011-09-10 04:44:22 +08:00
course_sql << " (course_section_id IN ( #{ course_section_ids . join ( ',' ) } )) " if course_section_ids . present?
2011-10-01 07:07:35 +08:00
course_sql << " (course_section_id IN ( #{ group_section_ids . join ( ',' ) } ) AND EXISTS(SELECT 1 FROM group_memberships WHERE user_id = users.id AND group_id = #{ limited_id [ 'group' ] } ) ) " if limited_id [ 'group' ] && group_section_ids . present?
2011-08-19 06:03:33 +08:00
course_sql << " (course_id IN ( #{ restricted_course_hash . keys . join ( ',' ) } ) AND (enrollments.type = 'TeacherEnrollment' OR enrollments.type = 'TaEnrollment' OR enrollments.user_id IN ( #{ ( [ self . id ] + restricted_course_hash . values . flatten . uniq ) . join ( ',' ) } ))) " if restricted_course_hash . present?
user_sql << <<-SQL if course_sql.present?
2011-08-21 08:16:40 +08:00
SELECT #{MESSAGEABLE_USER_COLUMN_SQL}, course_id, NULL AS group_id, #{connection.func(:group_concat, :'enrollments.type', ':')} AS roles
2011-05-20 00:33:20 +08:00
FROM users , enrollments , courses
2011-12-13 08:46:58 +08:00
WHERE course_id IN ( #{all_course_ids.join(', ')})
AND ( #{course_sql.join(' OR ')}) AND users.id = user_id AND courses.id = course_id
2012-08-04 17:41:44 +08:00
AND #{messageable_enrollment_clause(:include_concluded_students => include_concluded_students, :strict_course_state => !options[:skip_visibility_checks])}
2011-10-01 07:07:35 +08:00
#{enrollment_type_sql}
2011-10-01 01:14:52 +08:00
#{user_condition_sql}
2011-08-23 09:45:43 +08:00
GROUP BY #{connection.group_by(['users.id', 'course_id'], *(MESSAGEABLE_USER_COLUMNS[1, MESSAGEABLE_USER_COLUMNS.size]))}
2011-05-20 00:33:20 +08:00
SQL
2011-09-10 04:44:22 +08:00
user_sql << <<-SQL if full_group_ids.present?
2011-08-21 08:16:40 +08:00
SELECT #{MESSAGEABLE_USER_COLUMN_SQL}, NULL AS course_id, group_id, NULL AS roles
2011-05-20 00:33:20 +08:00
FROM users , group_memberships
2011-09-10 04:44:22 +08:00
WHERE group_id IN ( #{full_group_ids.join(',')}) AND users.id = user_id
AND group_memberships . workflow_state = 'accepted'
2011-10-01 01:14:52 +08:00
#{user_condition_sql}
2011-05-20 00:33:20 +08:00
SQL
2011-08-21 08:16:40 +08:00
# if this is an account admin who doesn't have any courses/groups in common
# with the user, we want to know the user's highest current enrollment type
highest_enrollment_sql = <<-SQL
SELECT type
FROM enrollments , courses
WHERE
user_id = users . id AND courses . id = course_id
2012-08-04 17:41:44 +08:00
AND ( #{self.class.enrollment_conditions(:current_and_invited)})
2012-03-03 02:21:53 +08:00
ORDER BY #{Enrollment.type_rank_sql}
2011-08-21 08:16:40 +08:00
LIMIT 1
SQL
2011-08-01 08:35:03 +08:00
user_sql << <<-SQL if account_ids.present?
2011-08-21 08:16:40 +08:00
SELECT #{MESSAGEABLE_USER_COLUMN_SQL}, 0 AS course_id, NULL AS group_id, (#{highest_enrollment_sql}) AS roles
2011-08-01 08:35:03 +08:00
FROM users , user_account_associations
WHERE user_account_associations . account_id IN ( #{account_ids.join(',')})
AND user_account_associations . user_id = users . id
2011-10-01 01:14:52 +08:00
#{user_condition_sql}
2011-08-01 08:35:03 +08:00
SQL
2012-08-09 05:46:06 +08:00
user_sql << <<-SQL unless options[:context]
SELECT #{MESSAGEABLE_USER_COLUMN_SQL}, NULL AS course_id, NULL AS group_id, NULL AS roles
FROM users
WHERE id = #{self.id}
#{user_condition_sql}
SQL
2011-08-01 08:35:03 +08:00
2011-07-28 00:33:04 +08:00
if options [ :ids ]
# provides a way for this user to start a conversation with someone
# that isn't normally messageable (requires that they already be in a
# conversation with that user)
if options [ :conversation_id ] . present?
user_sql << <<-SQL
2011-08-21 08:16:40 +08:00
SELECT #{MESSAGEABLE_USER_COLUMN_SQL}, NULL AS course_id, NULL AS group_id, NULL AS roles
2011-07-28 00:33:04 +08:00
FROM users , conversation_participants
2011-10-01 01:14:52 +08:00
WHERE conversation_participants . user_id = users . id
2011-07-28 00:33:04 +08:00
AND conversation_participants . conversation_id = #{options[:conversation_id].to_i}
2011-10-01 01:14:52 +08:00
#{user_condition_sql}
2011-07-28 00:33:04 +08:00
SQL
2012-03-07 00:35:25 +08:00
elsif options [ :skip_visibility_checks ] # we don't care about the contexts, we've passed in ids
2011-07-28 00:33:04 +08:00
user_sql << <<-SQL
2011-08-21 08:16:40 +08:00
SELECT #{MESSAGEABLE_USER_COLUMN_SQL}, NULL AS course_id, NULL AS group_id, NULL AS roles
2011-07-28 00:33:04 +08:00
FROM users
2011-10-01 01:14:52 +08:00
#{user_condition_sql.sub(/\AAND/, "WHERE")}
2011-07-28 00:33:04 +08:00
SQL
end
end
2011-06-08 08:00:17 +08:00
2011-08-01 08:35:03 +08:00
# if none of our potential sources was included, we're done
return [ ] if user_sql . empty?
2011-08-21 08:16:40 +08:00
concat_sql = connection . adapter_name =~ / postgres /i ? :" course_id::text || ':' || roles::text " : :" course_id || ':' || roles "
2011-06-08 08:00:17 +08:00
users = User . find_by_sql ( <<-SQL)
SELECT #{MESSAGEABLE_USER_COLUMN_SQL},
2011-08-21 08:16:40 +08:00
#{connection.func(:group_concat, concat_sql)} AS common_courses,
#{connection.func(:group_concat, :group_id)} AS common_groups
2011-05-20 00:33:20 +08:00
FROM (
#{user_sql.join(' UNION ')}
) users
2011-06-08 08:00:17 +08:00
GROUP BY #{connection.group_by(*MESSAGEABLE_USER_COLUMNS)}
2012-02-07 08:30:03 +08:00
ORDER BY #{options[:rank_results] ? "(COUNT(course_id) + COUNT(group_id)) DESC," : ""}
LOWER ( COALESCE ( short_name , name ) ) ,
id
2011-09-02 23:34:12 +08:00
#{options[:limit] && options[:limit] > 0 ? "LIMIT #{options[:limit].to_i}" : ""}
#{options[:offset] && options[:offset] > 0 ? "OFFSET #{options[:offset].to_i}" : ""}
2011-05-20 00:33:20 +08:00
SQL
2011-06-08 08:00:17 +08:00
users . each do | user |
2011-08-21 08:16:40 +08:00
user . common_courses = user . common_courses . to_s . split ( " , " ) . inject ( { } ) { | hash , info |
roles = info . split ( / : / )
hash [ roles . shift . to_i ] = roles
hash
}
user . common_groups = user . common_groups . to_s . split ( " , " ) . inject ( { } ) { | hash , info |
roles = info . split ( / : / )
hash [ roles . shift . to_i ] = [ 'Member' ]
hash
}
2011-06-08 08:00:17 +08:00
end
2011-05-20 00:33:20 +08:00
end
2011-07-21 00:05:17 +08:00
2011-10-21 07:19:13 +08:00
def short_name_with_shared_contexts ( user )
if ( contexts = shared_contexts ( user ) ) . present?
" #{ short_name } ( #{ contexts [ 0 , 2 ] . to_sentence } ) "
else
short_name
end
end
def shared_contexts ( user )
contexts = [ ]
if info = messageable_users ( :ids = > [ user . id ] ) . first
contexts += Course . find ( :all , :conditions = > { :id = > info . common_courses . keys } ) if info . common_courses . present?
contexts += Group . find ( :all , :conditions = > { :id = > info . common_groups . keys } ) if info . common_groups . present?
end
contexts . map ( & :name ) . sort_by { | c | c . downcase }
end
2011-08-01 18:01:38 +08:00
def mark_all_conversations_as_read!
conversations . unread . update_all ( :workflow_state = > 'read' )
User . update_all 'unread_conversations_count = 0' , :id = > id
end
2011-09-14 01:00:19 +08:00
def conversation_participant ( conversation_id )
all_conversations . find_by_conversation_id ( conversation_id )
end
2011-06-28 03:43:06 +08:00
# association with dynamic, filtered join condition for submissions.
# This is messy, but in ActiveRecord 2 this is the only way to do an eager
# loading :include condition that has dynamic join conditions. It looks like
# there's better solutions in AR 3.
# See also e.g., http://makandra.com/notes/983-dynamic-conditions-for-belongs_to-has_many-and-has_one-associations
has_many :submissions_for_given_assignments , :include = > [ :assignment , :submission_comments ] , :conditions = > 'submissions.assignment_id IN (#{Api.assignment_ids_for_students_api.join(",")})' , :class_name = > 'Submission'
2011-09-27 03:08:41 +08:00
def set_menu_data ( enrollment_uuid )
2011-09-28 04:06:52 +08:00
return @menu_data if @menu_data
2011-09-27 03:08:41 +08:00
coalesced_enrollments = [ ]
2011-09-28 04:06:52 +08:00
2011-09-27 03:08:41 +08:00
cached_enrollments = self . cached_current_enrollments ( :include_enrollment_uuid = > enrollment_uuid )
cached_enrollments . each do | e |
next if e . state_based_on_date == :inactive
if e . state_based_on_date == :completed
has_completed_enrollment = true
next
end
if ! e . course
coalesced_enrollments << {
:enrollment = > e ,
:sortable = > [ e . rank_sortable , e . state_sortable , e . long_name ] ,
:types = > [ e . readable_type ]
}
end
existing_enrollment_info = coalesced_enrollments . find { | en |
# coalesce together enrollments for the same course and the same state
! e . course . nil? && en [ :enrollment ] . course == e . course && en [ :enrollment ] . workflow_state == e . workflow_state
}
if existing_enrollment_info
existing_enrollment_info [ :types ] << e . readable_type
existing_enrollment_info [ :sortable ] = [ existing_enrollment_info [ :sortable ] || [ 999 , 999 , 999 ] , [ e . rank_sortable , e . state_sortable , 0 - e . id ] ] . min
else
coalesced_enrollments << { :enrollment = > e , :sortable = > [ e . rank_sortable , e . state_sortable , 0 - e . id ] , :types = > [ e . readable_type ] }
end
end
coalesced_enrollments = coalesced_enrollments . sort_by { | e | e [ :sortable ] || [ 999 , 999 , 999 ] }
2011-09-29 06:27:13 +08:00
active_enrollments = coalesced_enrollments . map { | e | e [ :enrollment ] }
2012-02-16 05:40:13 +08:00
2011-09-29 06:27:13 +08:00
cached_group_memberships = self . cached_current_group_memberships
coalesced_group_memberships = cached_group_memberships .
select { | gm | gm . active_given_enrollments? ( active_enrollments ) } .
sort_by { | gm | gm . group . name }
2011-09-27 03:08:41 +08:00
@menu_data = {
2011-09-29 06:27:13 +08:00
:group_memberships = > coalesced_group_memberships ,
:group_memberships_count = > cached_group_memberships . length ,
:accounts = > self . accounts ,
:accounts_count = > self . accounts . length ,
2011-09-27 03:08:41 +08:00
}
end
2011-11-08 03:10:20 +08:00
def menu_courses ( enrollment_uuid = nil )
return @menu_courses if @menu_courses
favorites = self . courses_with_primary_enrollment ( :favorite_courses , enrollment_uuid )
return ( @menu_courses = favorites ) if favorites . length > 0
2012-03-16 13:40:42 +08:00
@menu_courses = self . courses_with_primary_enrollment ( :current_and_invited_courses , enrollment_uuid ) . first ( 12 )
2011-09-27 03:08:41 +08:00
end
2011-10-06 23:54:05 +08:00
def user_can_edit_name?
associated_root_accounts . any? { | a | a . settings [ :users_can_edit_name ] != false } || associated_root_accounts . empty?
end
2012-02-02 04:14:56 +08:00
def sections_for_course ( course )
course . student_enrollments . active . for_user ( self ) . map { | e | e . course_section }
2011-10-08 07:19:20 +08:00
end
2011-10-29 07:19:11 +08:00
2011-12-08 04:47:19 +08:00
def can_create_enrollment_for? ( course , session , type )
can_add = %w{ StudentEnrollment ObserverEnrollment } . include? ( type ) && course . grants_right? ( self , session , :manage_students )
can_add || = type == 'TeacherEnrollment' && course . teacherless? && course . grants_right? ( self , session , :manage_students )
can_add || = course . grants_right? ( self , session , :manage_admin_users )
can_add
end
2011-10-29 07:19:11 +08:00
def group_member_json ( context )
h = { :user_id = > self . id , :name = > self . last_name_first , :display_name = > self . short_name }
2012-02-02 04:14:56 +08:00
if context && context . is_a? ( Course )
self . sections_for_course ( context ) . each do | section |
h [ :sections ] || = [ ]
h [ :sections ] << { :section_id = > section . id , :section_code = > section . section_code }
end
2011-10-29 07:19:11 +08:00
end
h
end
2011-11-19 06:20:09 +08:00
2012-01-14 06:58:17 +08:00
def find_pseudonym_for_account ( account , allow_implicit = false )
self . pseudonyms . detect { | p | p . active? && p . works_for_account? ( account , allow_implicit ) }
2011-11-16 02:13:23 +08:00
end
2011-11-19 06:20:09 +08:00
# account = the account that you want a pseudonym for
2011-11-16 02:13:23 +08:00
# preferred_template_account = pass in an actual account if you have a preference for which account the new pseudonym gets copied from
2011-11-19 06:20:09 +08:00
# this may not be able to find a suitable pseudonym to copy, so would still return nil
# if a pseudonym is created, it is *not* saved, and *not* added to the pseudonyms collection
2011-11-16 02:13:23 +08:00
def find_or_initialize_pseudonym_for_account ( account , preferred_template_account = nil )
pseudonym = find_pseudonym_for_account ( account )
if ! pseudonym
2011-11-19 06:20:09 +08:00
# list of copyable pseudonyms
active_pseudonyms = self . pseudonyms . select { | p | p . active? && ! p . password_auto_generated? && ! p . account . delegated_authentication? }
templates = [ ]
# re-arrange in the order we prefer
2011-11-16 02:13:23 +08:00
templates . concat active_pseudonyms . select { | p | p . account_id == preferred_template_account . id } if preferred_template_account
2011-11-19 06:20:09 +08:00
templates . concat active_pseudonyms . select { | p | p . account_id == Account . site_admin . id }
templates . concat active_pseudonyms . select { | p | p . account_id == Account . default . id }
templates . concat active_pseudonyms
templates . uniq!
2011-12-29 05:55:01 +08:00
template = templates . detect { | template | ! account . pseudonyms . custom_find_by_unique_id ( template . unique_id ) }
2011-11-19 06:20:09 +08:00
if template
# creating this not attached to the user's pseudonyms is intentional
pseudonym = account . pseudonyms . build
pseudonym . user = self
pseudonym . unique_id = template . unique_id
pseudonym . password_salt = template . password_salt
pseudonym . crypted_password = template . crypted_password
end
end
pseudonym
end
2011-12-13 05:19:43 +08:00
2012-07-03 05:02:14 +08:00
# Public: Add this user as an admin in the given account.
#
# account - The account model to create the admin in.
# role - String name of the role to add the user to. If nil,
# 'AccountAdmin' will be used (default: nil).
# send_notification - If set to false, do not send any email
# notifications (default: true).
#
# Returns an AccountUser model object.
def flag_as_admin ( account , role = nil , send_notification = true )
2011-12-17 05:27:59 +08:00
admin = account . add_user ( self , role )
2012-07-03 05:02:14 +08:00
return admin unless send_notification
2011-12-13 05:19:43 +08:00
if self . registered?
admin . account_user_notification!
else
admin . account_user_registration!
end
admin
end
2012-03-14 04:08:19 +08:00
def fake_student?
self . preferences [ :fake_student ] && ! ! self . enrollments . find ( :first , :conditions = > { :type = > " StudentViewEnrollment " } )
end
2012-05-24 07:06:00 +08:00
def private?
not public ?
end
2012-05-30 04:56:58 +08:00
def default_collection_name
t :default_collection_name , " %{user_name}'s Collection " , :user_name = > self . short_name
end
2012-07-12 07:20:39 +08:00
def profile ( force_reload = false )
orig_profile ( force_reload ) || build_profile
end
multi-factor authentication closes #9532
test plan:
* enable optional MFA, and check the following:
* normal log in should not be affected
* you can enroll in MFA from your profile page
* you can re-enroll in MFA from your profile page
* you can disable MFA from your profile page
* MFA can be reset by an admin on your user page
* when enrolled, you are asked for verification code after
username/password when logging in
* you can't access any other part of the site directly until
until entering your verification code
* enable required MFA, and check the following
* when not enrolled in MFA, and you log in, you are forced to
enroll
* you cannot disable MFA from your profile page
* you can re-enroll in MFA from your profile page
* an admin (other than himself) can reset MFA from the user page
* for enrolling in MFA
* use Google Authenticator and scan the QR code; you should have
30-seconds or so of extra leeway to enter your code
* having no SMS communication channels on your profile, the
enrollment page should just have a form to add a new phone
* having one or more SMS communication channels on your profile,
the enrollment page should list them, or allow you to create
a new one (and switch back)
* having more than one SMS communication channel on your profile,
the enrollment page should remember which one you have selected
after you click "send"
* an unconfirmed SMS channel should go to confirmed when it's used
to enroll in MFA
* you should not be able to go directly to /login/otp to enroll
if you used "Remember me" token to log in
* MFA login flow
* if configured with SMS, it should send you an SMS after you
put in your username/password; you should have about 5 minutes
of leeway to put it in
* if you don't check "remember computer" checkbox, you should have
to enter a verification code each time you log in
* if you do check it, you shouldn't have to enter your code
anymore (for three days). it also shouldn't SMS you a
verification code each time you log in
* setting MFA to required for admins should make it required for
admins, optional for other users
* with MFA enabled, directly go to /login/otp after entering
username/password but before entering a verification code; it
should send you back to the main login page
* if you enrolled via SMS, you should not be able to remove that
SMS from your profile
* there should not be a reset MFA link on a user page if they
haven't enrolled
* test a login or required enrollment sequence with CAS and/or SAML
Change-Id: I692de7405bf7ca023183e717930ee940ccf0d5e6
Reviewed-on: https://gerrit.instructure.com/12700
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
2012-08-03 05:17:50 +08:00
def otp_secret_key_remember_me_cookie ( time )
" #{ time . to_i } . #{ Canvas :: Security . hmac_sha1 ( " #{ time . to_i } . #{ self . otp_secret_key } " ) } "
end
def validate_otp_secret_key_remember_me_cookie ( value )
value =~ / ^( \ d+) \ .[0-9a-f]+ / &&
$1 . to_i > = ( Time . now . utc - 30 . days ) . to_i &&
value == otp_secret_key_remember_me_cookie ( $1 )
end
def otp_secret_key
return nil unless otp_secret_key_enc
Canvas :: Security :: decrypt_password ( otp_secret_key_enc , otp_secret_key_salt , 'otp_secret_key' , self . shard . settings [ :encryption_key ] ) if otp_secret_key_enc
end
def otp_secret_key = ( key )
if key
self . otp_secret_key_enc , self . otp_secret_key_salt = Canvas :: Security :: encrypt_password ( key , 'otp_secret_key' , self . shard . settings [ :encryption_key ] )
else
self . otp_secret_key_enc = self . otp_secret_key_salt = nil
end
key
end
2012-08-21 07:41:42 +08:00
def crocodoc_id!
cid = read_attribute ( :crocodoc_id )
return cid if cid
Setting . transaction do
s = Setting . find_by_name ( 'crocodoc_counter' , :lock = > true )
cid = s . value = s . value . to_i + 1
s . save!
end
update_attribute ( :crocodoc_id , cid )
cid
end
def crocodoc_user
2012-08-22 03:32:43 +08:00
" #{ crocodoc_id! } , #{ short_name . gsub ( " , " , " " ) } "
2012-08-21 07:41:42 +08:00
end
multi-factor authentication closes #9532
test plan:
* enable optional MFA, and check the following:
* normal log in should not be affected
* you can enroll in MFA from your profile page
* you can re-enroll in MFA from your profile page
* you can disable MFA from your profile page
* MFA can be reset by an admin on your user page
* when enrolled, you are asked for verification code after
username/password when logging in
* you can't access any other part of the site directly until
until entering your verification code
* enable required MFA, and check the following
* when not enrolled in MFA, and you log in, you are forced to
enroll
* you cannot disable MFA from your profile page
* you can re-enroll in MFA from your profile page
* an admin (other than himself) can reset MFA from the user page
* for enrolling in MFA
* use Google Authenticator and scan the QR code; you should have
30-seconds or so of extra leeway to enter your code
* having no SMS communication channels on your profile, the
enrollment page should just have a form to add a new phone
* having one or more SMS communication channels on your profile,
the enrollment page should list them, or allow you to create
a new one (and switch back)
* having more than one SMS communication channel on your profile,
the enrollment page should remember which one you have selected
after you click "send"
* an unconfirmed SMS channel should go to confirmed when it's used
to enroll in MFA
* you should not be able to go directly to /login/otp to enroll
if you used "Remember me" token to log in
* MFA login flow
* if configured with SMS, it should send you an SMS after you
put in your username/password; you should have about 5 minutes
of leeway to put it in
* if you don't check "remember computer" checkbox, you should have
to enter a verification code each time you log in
* if you do check it, you shouldn't have to enter your code
anymore (for three days). it also shouldn't SMS you a
verification code each time you log in
* setting MFA to required for admins should make it required for
admins, optional for other users
* with MFA enabled, directly go to /login/otp after entering
username/password but before entering a verification code; it
should send you back to the main login page
* if you enrolled via SMS, you should not be able to remove that
SMS from your profile
* there should not be a reset MFA link on a user page if they
haven't enrolled
* test a login or required enrollment sequence with CAS and/or SAML
Change-Id: I692de7405bf7ca023183e717930ee940ccf0d5e6
Reviewed-on: https://gerrit.instructure.com/12700
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
2012-08-03 05:17:50 +08:00
# mfa settings for a user are the most restrictive of any pseudonyms the user has
# a login for
def mfa_settings
result = self . pseudonyms ( :include = > :account ) . map ( & :account ) . uniq . map do | account |
case account . mfa_settings
when :disabled
0
when :optional
1
when :required_for_admins
if account . all_account_users_for ( self ) . empty?
1
else
# short circuit the entire method
return :required
end
when :required
# short circuit the entire method
return :required
end
end . max
return :disabled if result . nil?
[ :disabled , :optional ] [ result ]
end
2012-09-27 04:19:05 +08:00
def weekly_notification_bucket
# place in the next 24 hours after saturday morning midnight is
# determined by account and user. messages for any user in the same
# account (on the same shard) map into the same 6-hour window, and then
# are spread within that window by user. this is specifically 24 real
# hours, not 1 day, because DST sucks. so it'll go to 1am sunday
# morning and 11pm saturday night on the DST transition days, but
# midnight sunday morning the rest of the time.
account_bucket = ( shard . id . to_i + pseudonym . try ( :account_id ) . to_i ) % DelayedMessage :: WEEKLY_ACCOUNT_BUCKETS
user_bucket = self . id % DelayedMessage :: MINUTES_PER_WEEKLY_ACCOUNT_BUCKET
account_bucket * DelayedMessage :: MINUTES_PER_WEEKLY_ACCOUNT_BUCKET + user_bucket
end
def weekly_notification_time
# weekly notification scheduling happens in Eastern-time
time_zone = ActiveSupport :: TimeZone . us_zones . find { | zone | zone . name == 'Eastern Time (US & Canada)' }
# start at midnight saturday morning before next monday
target = time_zone . now . next_week - 2 . days
minutes = weekly_notification_bucket . minutes
# if we're already past that (e.g. it's sunday or late saturday),
# advance by a week
target += 1 . week if target + minutes < time_zone . now
# move into the 24 hours after midnight saturday morning and return
target + minutes
end
def weekly_notification_range
# weekly notification scheduling happens in Eastern-time
time_zone = ActiveSupport :: TimeZone . us_zones . find { | zone | zone . name == 'Eastern Time (US & Canada)' }
# start on January first instead of "today" to avoid DST, but still move to
# a saturday from there so we get the right day-of-week on start_hour
target = time_zone . now . change ( :month = > 1 , :day = > 1 ) . next_week - 2 . days + weekly_notification_bucket . minutes
# 2 hour on-the-hour span around the target such that distance from the
# start hour is at least 30 minutes.
start_hour = target - 30 . minutes
start_hour = start_hour . change ( :hour = > start_hour . hour )
end_hour = start_hour + 2 . hours
[ start_hour , end_hour ]
end
2012-10-02 06:13:46 +08:00
2012-10-03 05:24:46 +08:00
# Given a text string, return a value suitable for the user's initial_enrollment_type.
# It supports strings formatted as enrollment types like "StudentEnrollment" and
# it also supports text like "student", "teacher", "observer" and "ta".
#
# Any unsupported types have +nil+ returned.
def self . initial_enrollment_type_from_text ( type )
# Convert the string "StudentEnrollment" to "student".
# Return only valid matching types. Otherwise, nil.
type = type . to_s . downcase . sub ( / (view)?enrollment / , '' )
%w{ student teacher ta observer } . include? ( type ) ? type : nil
end
2012-10-02 06:13:46 +08:00
def associated_shards
[ Shard . default ]
end
def accounts
Shard . with_each_shard ( self . associated_shards ) do
AccountUser . find ( :all , :conditions = > { :user_id = > self . id } , :include = > :account ) . map ( & :account ) . uniq
end
end
memoize :accounts
2012-10-10 04:34:00 +08:00
def all_pseudonyms ( options = { } )
Shard . with_each_shard ( self . associated_shards ) do
Pseudonym . scoped ( options ) . find ( :all , :conditions = > { :user_id = > self . id } )
end
end
memoize :all_pseudonyms
2011-02-01 09:57:29 +08:00
end