2011-02-01 09:57:29 +08:00
#
2013-02-06 07:31:21 +08:00
# Copyright (C) 2011 - 2013 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
2013-07-23 05:10:01 +08:00
include TimeZoneHelper
time_zone_attribute :time_zone
2011-02-01 09:57:29 +08:00
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-12-12 21:50:15 +08:00
#
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)
2013-09-07 06:29:48 +08:00
#
# the course_workflow_state parameter can be used to simplify the query when
# the enrollments are all known to come from one course whose workflow state
# is already known. when provided, the method may return nil, in which case
# the condition should be treated as 'always false'.
def self . enrollment_conditions ( state , strict_course_state = true , course_workflow_state = nil )
2012-08-04 17:41:44 +08:00
#strict_course_state = true
case state
when :active
if strict_course_state
2013-09-07 06:29:48 +08:00
case course_workflow_state
when 'available'
# all active enrollments in a published and active course count
" enrollments.workflow_state='active' "
when 'claimed'
# student and observer enrollments don't count as active if the
# course is unpublished
" enrollments.workflow_state='active' AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment') "
when nil
# combine the other branches dynamically based on joined course's
# workflow_state
" enrollments.workflow_state='active' AND (courses.workflow_state='available' OR courses.workflow_state='claimed' AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment')) "
else
# never include enrollments from unclaimed/completed/deleted
# courses
nil
end
2012-08-04 17:41:44 +08:00
else
2013-09-07 06:29:48 +08:00
case course_workflow_state
when 'deleted'
# never include enrollments from deleted courses, even without
# strict checks
nil
when nil
# combine the other branches dynamically based on joined course's
# workflow_state
" enrollments.workflow_state='active' AND courses.workflow_state<>'deleted' "
else
# all active enrollments in a non-deleted course count
" enrollments.workflow_state='active' "
end
2012-08-04 17:41:44 +08:00
end
when :invited
if strict_course_state
2013-09-07 06:29:48 +08:00
case course_workflow_state
when 'available'
# all invited enrollments in a published and active course count
" enrollments.workflow_state='invited' "
when 'deleted'
# never include enrollments from deleted courses
nil
when nil
# combine the other branches dynamically based on joined course's
# workflow_state
" enrollments.workflow_state='invited' AND (courses.workflow_state='available' OR courses.workflow_state<>'deleted' AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment')) "
else
# student and observer enrollments don't count as invited if
# the course is unclaimed/unpublished/completed
" enrollments.workflow_state='invited' AND enrollments.type IN ('TeacherEnrollment','TaEnrollment','DesignerEnrollment','StudentViewEnrollment') "
end
2012-08-04 17:41:44 +08:00
else
2013-09-07 06:29:48 +08:00
case course_workflow_state
when 'deleted'
# never include enrollments from deleted courses
nil
when nil
# combine the other branches dynamically based on joined course's
# workflow_state
" enrollments.workflow_state IN ('invited','creation_pending') AND courses.workflow_state<>'deleted' "
else
# all invited and creation_pending enrollments in a non-deleted
# course count
" enrollments.workflow_state IN ('invited','creation_pending') "
end
2012-08-04 17:41:44 +08:00
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
2013-09-07 06:29:48 +08:00
enrollment_conditions ( :active , strict_course_state , course_workflow_state ) +
2012-08-04 17:41:44 +08:00
" OR " +
2013-09-07 06:29:48 +08:00
enrollment_conditions ( :invited , strict_course_state , course_workflow_state )
2012-08-04 17:41:44 +08:00
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-12-07 14:28:37 +08:00
has_many :current_and_future_enrollments , :class_name = > 'Enrollment' , :include = > [ :course ] , :order = > 'enrollments.created_at' ,
:conditions = > enrollment_conditions ( :current_and_invited , false )
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
2013-01-30 02:52:36 +08:00
has_many :context_external_tools , :as = > :context , :dependent = > :destroy , :order = > 'name'
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
has_many :student_enrollments
has_many :ta_enrollments
2012-11-08 00:58:15 +08:00
has_many :teacher_enrollments , :class_name = > 'TeacherEnrollment' , :conditions = > [ " enrollments.type = 'TeacherEnrollment' " ]
2011-02-01 09:57:29 +08:00
has_many :submissions , :include = > [ :assignment , :submission_comments ] , :order = > 'submissions.updated_at DESC' , :dependent = > :destroy
has_many :pseudonyms , :order = > 'position' , :dependent = > :destroy
2012-10-27 06:30:10 +08:00
has_many :active_pseudonyms , :class_name = > 'Pseudonym' , :conditions = > [ 'pseudonyms.workflow_state != ?' , 'deleted' ]
2011-02-01 09:57:29 +08:00
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'
2012-12-15 03:49:40 +08:00
has_many :grading_standards , :conditions = > [ 'workflow_state != ?' , 'deleted' ]
2011-02-01 09:57:29 +08:00
has_many :context_module_progressions
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-10-24 05:57:37 +08:00
has_many :collection_item_upvotes
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
2013-02-06 07:31:21 +08:00
has_many :progresses , :as = > :context
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
2013-03-19 03:07:47 +08:00
all_conversations . visible . 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
2013-07-19 05:41:53 +08:00
def page_views ( options = { } )
PageView . for_user ( self , options )
2012-09-25 04:05:43 +08:00
end
2013-06-14 06:14:22 +08:00
scope :of_account , lambda { | account | where ( " EXISTS ( #{ account . user_account_associations . select ( " 1 " ) . where ( " user_account_associations.user_id=users.id " ) . to_sql } ) " ) }
2013-03-21 03:38:19 +08:00
scope :recently_logged_in , lambda {
includes ( :pseudonyms ) .
where ( " pseudonyms.current_login_at>? " , 1 . month . ago ) .
order ( " pseudonyms.current_login_at DESC " ) .
limit ( 25 )
2011-02-01 09:57:29 +08:00
}
2013-03-21 03:38:19 +08:00
scope :include_pseudonym , includes ( :pseudonym )
scope :restrict_to_sections , lambda { | sections |
if sections . empty?
scoped
2012-02-18 10:49:16 +08:00
else
2013-03-21 03:38:19 +08:00
where ( " enrollments.limit_privileges_to_course_section IS NULL OR enrollments.limit_privileges_to_course_section<>? OR enrollments.course_section_id IN (?) " , true , sections )
2012-02-18 10:49:16 +08:00
end
2011-02-01 09:57:29 +08:00
}
2013-03-21 03:38:19 +08:00
scope :name_like , lambda { | name |
where ( " #{ wildcard ( 'users.name' , 'users.short_name' , name ) } OR EXISTS ( #{ Pseudonym . select ( " 1 " ) . where ( wildcard ( 'pseudonyms.sis_user_id' , 'pseudonyms.unique_id' , name ) ) . where ( " pseudonyms.user_id=users.id " ) . active . to_sql } ) " )
2011-02-01 09:57:29 +08:00
}
2013-03-21 03:38:19 +08:00
scope :active , where ( " users.workflow_state<>'deleted' " )
2012-01-04 04:30:49 +08:00
2013-03-21 03:38:19 +08:00
scope :has_current_student_enrollments , where ( " 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
def self . order_by_sortable_name ( options = { } )
2013-04-22 23:36:15 +08:00
order_clause = clause = sortable_name_order_by_clause
order_clause = " #{ clause } DESC " if options [ :direction ] == :descending
scope = self . order ( order_clause )
2013-03-08 07:23:32 +08:00
if ( scope . scope ( :find , :select ) )
scope = scope . select ( clause )
end
if scope . scope ( :find , :group )
scope = scope . group ( clause )
end
scope
2012-08-24 05:10:00 +08:00
end
2013-03-08 07:23:32 +08:00
def self . by_top_enrollment
scope = self
if ( ! scope . scope ( :find , :select ) )
scope = scope . select ( " users.* " )
end
scope . select ( " MIN( #{ Enrollment . type_rank_sql ( :student ) } ) AS enrollment_rank " ) .
group ( User . connection . group_by ( User ) ) .
order ( " enrollment_rank " ) .
order_by_sortable_name
2012-05-03 03:43:00 +08:00
end
2012-01-04 04:30:49 +08:00
2013-03-21 03:38:19 +08:00
scope :enrolled_in_course_between , lambda { | course_ids , start_at , end_at | joins ( :enrollments ) . where ( :enrollments = > { :course_id = > course_ids , :created_at = > start_at .. end_at } ) }
2012-01-04 04:30:49 +08:00
2013-03-21 03:38:19 +08:00
scope :for_course_with_last_login , lambda { | course , root_account_id , enrollment_type |
# add a field to each user that is the aggregated max from current_login_at and last_login_at from their pseudonyms
scope = select ( " users.*, MAX(current_login_at) as last_login, MAX(current_login_at) IS NULL as login_info_exists " ) .
2012-02-15 04:26:39 +08:00
# left outer join ensures we get the user even if they don't have a pseudonym
2013-03-21 03:38:19 +08:00
joins ( sanitize_sql ( [ <<-SQL, root_account_id])).where(:enrollments => { :course_id => course })
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
2013-03-21 03:38:19 +08:00
scope = scope . where ( " enrollments.workflow_state<>'deleted' " )
scope = scope . where ( :enrollments = > { :type = > enrollment_type } ) if enrollment_type
# the trick to get unique users
scope . group ( " users.id " )
2012-02-15 04:26:39 +08:00
}
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 ,
2013-02-28 06:56:30 +08:00
:require_self_enrollment_code , :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
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 :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
2012-12-07 14:28:37 +08:00
course = record . validation_root_account . self_enrollment_course_for ( value )
record . self_enrollment_course = course
2013-01-29 08:36:31 +08:00
if course && course . self_enrollment?
2012-12-12 13:22:25 +08:00
record . errors . add ( attr , " full " ) if course . self_enrollment_limit_met?
record . errors . add ( attr , " already_enrolled " ) if course . user_is_student? ( record , :include_future = > true )
else
record . errors . add ( attr , " invalid " )
end
2012-05-30 07:35:00 +08:00
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
2013-07-30 05:28:02 +08:00
before_save :record_acceptance_of_terms
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
2013-01-25 12:01:28 +08:00
update_account_associations if ! self . class . skip_updating_account_associations? && self . workflow_state_changed? && self . id_was
2012-02-04 05:46:14 +08:00
end
2013-01-26 01:32:08 +08:00
def update_account_associations ( opts = nil )
opts || = { :all_shards = > true }
2013-06-26 23:36:29 +08:00
# incremental is only for the current shard
return User . update_account_associations ( [ self ] , opts ) if opts [ :incremental ]
2013-01-26 01:32:08 +08:00
self . shard . activate do
User . update_account_associations ( [ self ] , opts )
end
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
2013-01-25 12:01:28 +08:00
def self . calculate_account_associations ( user , data , account_chain_cache )
return [ ] if %w{ creation_pending deleted } . include? ( user . workflow_state ) || user . fake_student?
enrollments = data [ :enrollments ] [ user . id ] || [ ]
sections = enrollments . map { | e | data [ :sections ] [ e . course_section_id ] }
courses = sections . map { | s | data [ :courses ] [ s . course_id ] }
courses += sections . select ( & :nonxlist_course_id ) . map { | s | data [ :courses ] [ s . nonxlist_course_id ] }
starting_account_ids = courses . map ( & :account_id )
starting_account_ids += ( data [ :pseudonyms ] [ user . id ] || [ ] ) . map ( & :account_id )
starting_account_ids += ( data [ :account_users ] [ user . id ] || [ ] ) . map ( & :account_id )
2011-08-18 03:33:10 +08:00
starting_account_ids . uniq!
2013-01-25 12:01:28 +08:00
result = calculate_account_associations_from_accounts ( starting_account_ids , account_chain_cache )
2011-08-18 03:33:10 +08:00
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 )
2013-01-26 01:32:08 +08:00
shards = [ Shard . current ]
2013-01-25 12:01:28 +08:00
if ! precalculated_associations
if ! users_or_user_ids . first . is_a? ( User )
2013-03-19 03:07:47 +08:00
users = users_or_user_ids = User . select ( [ :id , :preferences , :workflow_state ] ) . where ( :id = > user_ids ) . all
2013-01-26 01:32:08 +08:00
else
users = users_or_user_ids
end
if opts [ :all_shards ]
shards = Set . new
users . each { | u | shards += u . associated_shards }
shards = shards . to_a
2013-01-25 12:01:28 +08:00
end
# basically we're going to do a huge preload here, but custom sql to only load the columns we need
2013-01-26 01:32:08 +08:00
data = { :enrollments = > [ ] , :sections = > [ ] , :courses = > [ ] , :pseudonyms = > [ ] , :account_users = > [ ] }
Shard . with_each_shard ( shards ) do
shard_user_ids = users . map ( & :id )
data [ :enrollments ] += shard_enrollments =
2013-03-19 03:07:47 +08:00
Enrollment . where ( " workflow_state<>'deleted' AND type<>'StudentViewEnrollment' " ) .
where ( :user_id = > shard_user_ids ) .
select ( [ :user_id , :course_id , :course_section_id ] ) .
uniq .
all
2013-01-26 01:32:08 +08:00
# probably a lot of dups, so more efficient to use a set than uniq an array
course_section_ids = Set . new
shard_enrollments . each { | e | course_section_ids << e . course_section_id }
2013-03-19 03:07:47 +08:00
data [ :sections ] += shard_sections = CourseSection . select ( [ :id , :course_id , :nonxlist_course_id ] ) .
where ( :id = > course_section_ids . to_a ) . all unless course_section_ids . empty?
2013-01-26 01:32:08 +08:00
shard_sections || = [ ]
course_ids = Set . new
shard_sections . each do | s |
course_ids << s . course_id
course_ids << s . nonxlist_course_id if s . nonxlist_course_id
end
2013-03-19 03:07:47 +08:00
data [ :courses ] += Course . select ( [ :id , :account_id ] ) . where ( :id = > course_ids . to_a ) . all unless course_ids . empty?
2013-01-26 01:32:08 +08:00
2013-03-19 03:07:47 +08:00
data [ :pseudonyms ] += Pseudonym . active . select ( [ :user_id , :account_id ] ) . uniq . where ( :user_id = > shard_user_ids ) . all
data [ :account_users ] += AccountUser . select ( [ :user_id , :account_id ] ) . uniq . where ( :user_id = > shard_user_ids ) . all
2013-01-25 12:01:28 +08:00
end
2013-01-26 01:32:08 +08:00
# now make it easy to get the data by user id
data [ :enrollments ] = data [ :enrollments ] . group_by ( & :user_id )
2013-01-25 12:01:28 +08:00
data [ :sections ] = data [ :sections ] . index_by ( & :id )
2013-01-26 01:32:08 +08:00
data [ :courses ] = data [ :courses ] . index_by ( & :id )
data [ :pseudonyms ] = data [ :pseudonyms ] . group_by ( & :user_id )
data [ :account_users ] = data [ :account_users ] . group_by ( & :user_id )
2013-01-25 12:01:28 +08:00
end
2013-01-26 01:32:08 +08:00
# TODO: transaction on each shard?
2011-08-18 03:33:10 +08:00
UserAccountAssociation . transaction do
current_associations = { }
to_delete = [ ]
2013-01-26 01:32:08 +08:00
Shard . with_each_shard ( shards ) do
# if shards is more than just the current shard, users will be set; otherwise
# we never loaded users, but it doesn't matter, cause it's all the current shard
shard_user_ids = users ? users . map ( & :id ) : user_ids
2013-03-19 03:07:47 +08:00
UserAccountAssociation . where ( :user_id = > shard_user_ids ) . all
2013-01-26 01:32:08 +08:00
end . each do | aa |
2011-08-18 03:33:10 +08:00
key = [ aa . user_id , aa . account_id ]
2013-01-25 05:16:42 +08:00
# duplicates. the unique index prevents these now, but this code
# needs to hang around for the migration itself
2011-08-18 03:33:10 +08:00
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
2013-01-25 12:01:28 +08:00
user_id = user . id
2011-08-18 03:33:10 +08:00
end
account_ids_with_depth = precalculated_associations
if account_ids_with_depth . nil?
user || = User . find ( user_id )
2013-01-25 12:01:28 +08:00
account_ids_with_depth = calculate_account_associations ( user , data , account_chain_cache )
2011-08-18 03:33:10 +08:00
end
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
2013-02-18 23:14:18 +08:00
aa = UserAccountAssociation . new
aa . user_id = user_id
aa . account_id = account_id
aa . depth = depth
aa . shard = Shard . shard_for ( account_id )
aa . shard . activate do
begin
UserAccountAssociation . transaction ( :requires_new = > true ) do
aa . save!
end
rescue ActiveRecord :: Base :: UniqueConstraintViolation
# race condition - someone else created the UAA after we queried for existing ones
old_aa = UserAccountAssociation . find_by_user_id_and_account_id ( aa . user_id , aa . account_id )
raise unless old_aa # wtf!
# make sure we don't need to change the depth
if depth < old_aa . depth
old_aa . depth = depth
old_aa . save!
end
end
2011-08-18 03:33:10 +08:00
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
2013-03-19 03:07:47 +08:00
if Rails . version < '3.0'
UserAccountAssociation . update_all ( { :depth = > depth } , :id = > association [ 0 ] )
else
UserAccountAssociation . where ( :id = > association [ 0 ] ) . update_all ( :depth = > depth )
end
2011-08-18 03:33:10 +08:00
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 ] }
2013-03-19 03:07:47 +08:00
if Rails . version < '3.0'
UserAccountAssociation . delete_all ( :id = > to_delete ) unless incremental || to_delete . empty?
else
UserAccountAssociation . where ( :id = > to_delete ) . delete_all unless incremental || to_delete . empty?
end
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2012-12-07 14:28:37 +08:00
# These methods can be overridden by a plugin if you want to have an approval
# process or implement additional tracking for new users
2011-02-01 09:57:29 +08:00
def registration_approval_required? ; false ; end
2012-12-07 14:28:37 +08:00
def new_registration ( form_params = { } ) ; end
# DEPRECATED, override new_registration instead
def new_teacher_registration ( form_params = { } ) ; new_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
2013-03-21 03:38:19 +08:00
scope :with_service , lambda { | service |
service = service . service if service . is_a? ( UserService )
includes ( :user_services ) . where ( :user_services = > { :service = > service . to_s } )
2011-02-01 09:57:29 +08:00
}
2013-03-21 03:38:19 +08:00
scope :enrolled_before , lambda { | date | where ( " 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 )
2013-03-19 03:07:47 +08:00
groups . where ( 'groups.context_id' = > context ,
2012-12-08 04:37:22 +08:00
'groups.context_type' = > context . class . to_s ,
2013-03-19 03:07:47 +08:00
'group_memberships.workflow_state' = > 'accepted' ) .
where ( " groups.workflow_state <> 'deleted' " )
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 <=> ( 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 "
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
self . name || = self . email || t ( '#user.default_user_name' , " User " )
2011-10-27 02:55:27 +08:00
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
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
2013-03-19 03:07:47 +08:00
self . communication_channels . email . by_path ( addr ) . 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.
2013-03-19 03:07:47 +08:00
communication_channels . sms . 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 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 )
2012-10-25 05:29:06 +08:00
new_cc = cc . clone
new_cc . shard = new_user . shard
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 . shard = new_user . shard
new_np . communication_channel = new_cc
new_np . save!
2012-10-05 05:47:38 +08:00
end
end
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
2013-03-19 03:07:47 +08:00
Pseudonym . order ( :created_at ) . where ( :user_id = > id ) . active . last
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 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
2013-02-13 06:29:03 +08:00
self . available_courses . with_each_shard . select { | c | c . grants_right? ( self , nil , :participate_as_student ) }
2011-02-01 09:57:29 +08:00
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 )
2012-10-31 23:40:09 +08:00
if self . pseudonyms . loaded? && self . shard == root_account . shard
2011-09-14 23:49:21 +08:00
self . pseudonyms . detect { | p | p . active? && p . sis_user_id && p . account_id == root_account . id }
else
2012-10-31 23:40:09 +08:00
root_account . shard . activate do
root_account . pseudonyms . active . find_by_user_id ( self . id , :conditions = > " sis_user_id IS NOT NULL " )
end
2011-09-14 23:49:21 +08:00
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 && (
2013-03-10 05:27:44 +08:00
# by default 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, or
# an admin (teacher/ta/designer) in the course
2011-02-01 09:57:29 +08:00
self . all_courses . any? { | c | c . grants_right? ( user , nil , :read_reports ) }
)
end
2013-03-10 05:27:44 +08:00
can :rename and can :remove_avatar and can :read_reports
2011-08-12 04:50:02 +08:00
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
2013-03-13 03:47:21 +08:00
given do | user |
user && (
Account . site_admin . grants_right? ( user , :view_statistics ) ||
self . associated_accounts . any? { | a | a . grants_right? ( user , nil , :view_statistics ) }
)
end
can :view_statistics
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
2013-03-10 05:27:44 +08:00
can :manage_user_details and can :update_avatar and can :remove_avatar and can :rename and can :view_statistics and can :read and can :read_reports
2012-01-04 04:30:49 +08:00
2013-01-09 00:47:47 +08:00
given do | user |
user && (
Account . site_admin . grants_right? ( user , :manage_user_logins ) ||
self . associated_accounts . any? { | a | a . grants_right? ( user , nil , :manage_user_logins ) }
)
end
2013-03-10 05:27:44 +08:00
can :view_statistics and can :read and can :read_reports
2013-01-09 00:47:47 +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 ) ||
2013-01-09 00:47:47 +08:00
( self . associated_accounts . any? { | a | a . grants_right? ( user , nil , :manage_user_logins ) } &&
2013-01-30 03:21:46 +08:00
self . accounts . select ( & :root_account? ) . all? { | a | has_subset_of_account_permissions? ( user , a ) } )
2011-02-01 09:57:29 +08:00
)
end
2013-01-09 00:47:47 +08:00
can :manage_user_details and can :manage_logins and can :rename
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 )
2013-01-09 00:47:47 +08:00
has_subset_of_account_permissions? ( masquerader , account )
end
def has_subset_of_account_permissions? ( user , account )
return true if user == self
2013-01-30 03:21:46 +08:00
return false unless account . root_account?
2012-02-21 06:57:32 +08:00
account_users = account . all_account_users_for ( self )
return true if account_users . empty?
2012-12-07 07:15:53 +08:00
account_users . all? do | account_user |
2013-01-09 00:47:47 +08:00
account_user . is_subset_of? ( user )
2012-02-21 06:57:32 +08:00
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
2013-05-11 19:32:06 +08:00
# only used by ContextModuleProgression#deep_evaluate
2011-02-01 09:57:29 +08:00
def submitted_submission_for ( assignment_id )
2013-05-11 19:32:06 +08:00
@submissions || = self . submissions . having_submission . except ( :includes ) . select ( [ :id , :score , :assignment_id ] ) . all
2011-02-01 09:57:29 +08:00
@submissions . detect { | s | s . assignment_id == assignment_id }
end
2012-01-04 04:30:49 +08:00
2013-05-11 19:32:06 +08:00
# only used by ContextModuleProgression#deep_evaluate
2011-01-06 14:09:46 +08:00
def attempted_quiz_submission_for ( quiz_id )
2013-05-11 19:32:06 +08:00
@quiz_submissions || = self . quiz_submissions . select ( [ :id , :kept_score , :quiz_id , :workflow_state ] ) . 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
2013-07-30 05:28:02 +08:00
def record_acceptance_of_terms
2013-08-05 23:40:33 +08:00
accept_terms if @require_acceptance_of_terms && @terms_of_use
end
def accept_terms
preferences [ :accepted_terms ] = Time . now . utc
2013-07-30 05:28:02 +08:00
end
2011-02-01 09:57:29 +08:00
def self . max_messages_per_day
2013-08-27 01:21:57 +08:00
Setting . get_cached ( '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-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
2013-03-21 03:38:19 +08:00
scope :with_avatar_state , lambda { | state |
scope = where ( " avatar_image_url IS NOT NULL " ) . order ( " avatar_image_updated_at DESC " )
2011-02-01 09:57:29 +08:00
if state == 'any'
2013-03-21 03:38:19 +08:00
scope . where ( " avatar_state IS NOT NULL AND avatar_state<>'none' " )
2011-02-01 09:57:29 +08:00
else
2013-03-21 03:38:19 +08:00
scope . where ( :avatar_state = > state )
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 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 )
2013-09-27 05:15:55 +08:00
rubrics . uniq . sort_by { | r | [ ( r . association_count || 0 ) > 3 ? 'a' : 'b' , Canvas :: ICU . collation_key ( r . title || 'zzzzz' ) ] }
2011-02-01 09:57:29 +08:00
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
2013-05-07 06:18:56 +08:00
def manual_mark_as_read?
! ! preferences [ :manual_mark_as_read ]
end
2013-08-06 07:13:54 +08:00
def use_new_conversations?
preferences [ :use_new_conversations ] == true
end
2013-01-31 03:39:40 +08:00
def ignore_item! ( asset , purpose , permanent = false )
begin
# more likely this doesn't exist, so try the create first
asset . ignores . create! ( :user = > self , :purpose = > purpose , :permanent = > permanent )
rescue ActiveRecord :: Base :: UniqueConstraintViolation
asset . shard . activate do
ignore = asset . ignores . find_by_user_id_and_purpose ( self . id , purpose )
ignore . permanent = permanent
ignore . save!
end
2011-02-01 09:57:29 +08:00
end
2013-01-31 03:39:40 +08:00
self . touch
2011-02-01 09:57:29 +08:00
end
def assignments_needing_submitting ( opts = { } )
2013-04-03 01:53:08 +08:00
Shackles . activate ( :slave ) do
2013-01-31 03:39:40 +08:00
course_ids = if opts [ :contexts ]
( Array ( opts [ :contexts ] ) . map ( & :id ) &
current_student_enrollment_course_ids )
2013-01-27 00:33:54 +08:00
else
2013-01-31 03:39:40 +08:00
current_student_enrollment_course_ids
end
# allow explicitly passing a nil limit
limit = opts [ :limit ]
limit = 15 unless opts . key? ( :limit )
2013-04-09 04:28:51 +08:00
due_after = opts [ :due_after ] || 4 . weeks . ago
2013-01-31 03:39:40 +08:00
result = Shard . partition_by_shard ( course_ids ) do | shard_course_ids |
Assignment . for_course ( shard_course_ids ) .
active .
2013-04-09 04:28:51 +08:00
due_between_with_overrides ( due_after , 1 . week . from_now ) .
2013-01-31 03:39:40 +08:00
not_ignored_by ( self , 'submitting' ) .
2013-04-09 04:28:51 +08:00
expecting_submission .
2013-01-31 03:39:40 +08:00
need_submitting_info ( id , limit ) .
not_locked
2013-01-27 00:33:54 +08:00
end
2013-01-31 03:39:40 +08:00
# outer limit, since there could be limit * n_shards results
result = result [ 0 .. ( limit - 1 ) ] if limit
result
2012-12-11 01:21:56 +08:00
end
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 = { } )
2013-01-31 03:39:40 +08:00
assignments_needing_submitting ( opts . merge ( :limit = > nil ) ) . size
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 assignments_needing_grading ( opts = { } )
2013-04-03 01:53:08 +08:00
Shackles . activate ( :slave ) do
2013-01-31 03:39:40 +08:00
course_ids = if opts [ :contexts ]
( Array ( opts [ :contexts ] ) . map ( & :id ) &
current_admin_enrollment_course_ids )
2013-01-27 00:33:54 +08:00
else
2013-01-31 03:39:40 +08:00
current_admin_enrollment_course_ids
end
# allow explicitly passing a nil limit
limit = opts [ :limit ]
limit = 15 unless opts . key? ( :limit )
result = Shard . partition_by_shard ( course_ids ) do | shard_course_ids |
Assignment . for_course ( shard_course_ids ) . active .
expecting_submission .
not_ignored_by ( self , 'grading' ) .
need_grading_info ( limit ) .
reject { | a | a . needs_grading_count_for_user ( self ) == 0 }
2013-01-27 00:33:54 +08:00
end
2013-01-31 03:39:40 +08:00
# outer limit, since there could be limit * n_shards results
result = result [ 0 .. ( limit - 1 ) ] if limit
result
2012-12-11 01:21:56 +08:00
end
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 = { } )
2013-01-31 03:39:40 +08:00
assignments_needing_grading ( opts . merge ( :limit = > nil ) ) . size
2012-10-11 03:00:01 +08:00
end
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-12-08 00:40:42 +08:00
def self . serialization_excludes
[
:uuid ,
:phone ,
:features_used ,
:otp_communication_channel_id ,
:otp_secret_key_enc ,
:otp_secret_key_salt ,
:collkey
]
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 secondary_identifier
self . email || self . id
end
2012-01-04 04:30:49 +08:00
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
2013-03-19 03:07:47 +08:00
accts = self . associated_accounts . where ( " 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 = { } )
2012-10-26 05:39:00 +08:00
res = self . shard . activate do
Rails . cache . fetch ( [ self , 'courses_with_primary_enrollment' , association , options ] . cache_key , :expires_in = > 15 . minutes ) do
2013-01-09 05:35:01 +08:00
send ( association ) . with_each_shard do | scope |
courses = scope . distinct_on ( [ " courses.id " ] ,
2012-10-26 05:39:00 +08:00
: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 } " )
2013-01-09 05:35:01 +08:00
unless options [ :include_completed_courses ]
2013-03-19 03:07:47 +08:00
enrollments = Enrollment . where ( :id = > courses . map ( & :primary_enrollment_id ) ) . all
2013-08-02 23:46:23 +08:00
courses_hash = courses . index_by ( & :id )
# prepopulate the reverse association
enrollments . each { | e | e . course = courses_hash [ e . course_id ] }
Canvas :: Builders :: EnrollmentDateBuilder . preload ( enrollments )
2013-01-09 05:35:01 +08:00
date_restricted_ids = enrollments . select { | e | e . completed? || e . inactive? } . map ( & :id )
courses . reject! { | course | date_restricted_ids . include? ( course . primary_enrollment_id . to_i ) }
end
courses
2012-10-26 05:39:00 +08:00
end
end . dup
end
2013-01-09 05:35:01 +08:00
2011-11-08 03:10:20 +08:00
if association == :current_and_invited_courses
2013-03-19 03:07:47 +08:00
if enrollment_uuid && pending_course = Course .
select ( " courses.*, enrollments.type AS primary_enrollment, #{ Enrollment . type_rank_sql } AS primary_enrollment_rank, enrollments.workflow_state AS primary_enrollment_state " ) .
joins ( :enrollments ) .
where ( :enrollments = > { :uuid = > enrollment_uuid , :workflow_state = > 'invited' } ) . first
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
2013-01-09 05:35:01 +08:00
2013-09-27 05:15:55 +08:00
res . sort_by { | c | [ c . primary_enrollment_rank , Canvas :: ICU . collation_key ( c . name ) ] }
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
2012-10-26 05:39:00 +08:00
self . shard . activate do
Rails . cache . fetch ( [ self , 'active_emails' ] . cache_key ) do
self . communication_channels . active . email . map ( & :path )
end
2011-11-08 03:10:20 +08:00
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 = { } )
2012-10-26 05:39:00 +08:00
self . shard . activate do
2012-12-22 23:08:51 +08:00
res = Rails . cache . fetch ( [ self , 'current_enrollments2' , opts [ :include_enrollment_uuid ] , opts [ :include_future ] ] . cache_key ) do
2012-12-07 14:28:37 +08:00
res = ( opts [ :include_future ] ? current_and_future_enrollments : current_and_invited_enrollments ) . with_each_shard
2012-10-26 05:39:00 +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-02-01 09:57:29 +08:00
end
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
2012-10-26 05:39:00 +08:00
self . shard . activate do
2012-12-22 23:08:51 +08:00
@cached_all_enrollments = Rails . cache . fetch ( [ self , 'not_ended_enrollments2' ] . cache_key ) do
2012-10-26 05:39:00 +08:00
self . not_ended_enrollments . with_each_shard
end
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 cached_current_group_memberships
2012-10-26 05:39:00 +08:00
self . shard . activate do
@cached_current_group_memberships = Rails . cache . fetch ( [ self , 'current_group_memberships' ] . cache_key ) do
self . current_group_memberships . with_each_shard
end
2012-02-16 05:40:13 +08:00
end
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2013-01-31 03:39:40 +08:00
def current_student_enrollment_course_ids
@current_student_enrollments || = Rails . cache . fetch ( [ self , 'current_student_enrollments' ] . cache_key ) do
2013-03-19 03:07:47 +08:00
self . enrollments . with_each_shard { | scope | scope . student . select ( :course_id ) }
2011-02-01 09:57:29 +08:00
end
2013-01-31 03:39:40 +08:00
@current_student_enrollments . map ( & :course_id )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2013-01-31 03:39:40 +08:00
def current_admin_enrollment_course_ids
@current_admin_enrollments || = Rails . cache . fetch ( [ self , 'current_admin_enrollments' ] . cache_key ) do
2013-03-19 03:07:47 +08:00
self . enrollments . with_each_shard { | scope | scope . admin . select ( :course_id ) }
2011-02-01 09:57:29 +08:00
end
2013-01-31 03:39:40 +08:00
@current_admin_enrollments . map ( & :course_id )
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
# 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
2013-04-03 01:53:08 +08:00
Shackles . activate ( :slave ) do
2012-12-11 01:12:45 +08:00
submissions = [ ]
submissions += self . submissions . after ( opts [ :start_at ] ) . for_context_codes ( context_codes ) . find (
:all ,
:conditions = > [ " submissions.score IS NOT NULL AND assignments.workflow_state != ? AND assignments.muted = ? " , 'deleted' , false ] ,
:include = > [ :assignment , :user , :submission_comments ] ,
:order = > 'submissions.created_at DESC' ,
:limit = > opts [ :limit ]
)
# THIS IS SLOW, it takes ~230ms for mike
submissions += Submission . for_context_codes ( context_codes ) . find (
:all ,
:select = > " submissions.*, last_updated_at_from_db " ,
:joins = > self . class . send ( :sanitize_sql_array , [ <<-SQL, opts[:start_at], self.id, self.id]),
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
INNER JOIN assignments ON assignments . id = submissions . assignment_id AND assignments . workflow_state < > 'deleted'
SQL
:order = > 'last_updated_at_from_db DESC' ,
:limit = > opts [ :limit ] ,
:conditions = > { " assignments.muted " = > false }
)
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
2011-02-01 09:57:29 +08:00
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 = { } )
2013-01-31 03:39:40 +08:00
context_codes = opts [ :context_codes ]
context_codes || = if opts [ :contexts ]
setup_context_lookups ( opts [ :contexts ] )
else
self . current_student_enrollment_course_ids . map { | id | " course_ #{ id } " }
end
2011-02-01 09:57:29 +08:00
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 = { } )
2013-03-19 03:07:47 +08:00
instances = stream_item_instances . where ( :hidden = > false ) . order ( 'stream_item_instances.id desc' )
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-11-20 04:22:16 +08:00
conditions = setup_context_association_lookups ( " stream_item_instances.context " , opts [ :contexts ] )
2013-03-19 03:07:47 +08:00
instances = instances . where ( conditions ) unless conditions . first . empty?
2012-11-03 05:41:33 +08:00
elsif opts [ :context ]
2013-03-19 03:07:47 +08:00
instances = instances . where ( :context_type = > opts [ :context ] . class . base_class . name , :context_id = > opts [ :context ] )
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
2013-01-04 03:44:41 +08:00
# NOTE: excludes submission stream items
2012-11-03 05:41:33 +08:00
def cached_recent_stream_items ( opts = { } )
expires_in = 1 . day
fix cross-shard stream item dismissal, fixes CNVS-1404
1. make sure the returned stream item ids are relative to the user, not
the domain, since we need to look up the instances by those ids from
the user's shard later
2. make sure we actually handle shortened global ids, rather than
asploding
3. just cache stream items on the user's shard, not on every shard the
user visits. makes cache invalidation practical/possible
test plan:
1. set up canvas with redis and sharding
2. set up two additional shards (your user is in the initial/primary one)
3. enroll your user in a course in the second shard
4. as another user, do something that creates a stream item for the course
(e.g. create an announcement)
5. as the original user, confirm that:
1. you see the stream item on your shard's dashboard
2. you can dismiss the stream item
3. when you refresh the page, it is still dismissed
6. repeat step 4.
7. as the original user, confirm that:
1. you can see the stream item on the shard 3 dashboard
2. you can dismiss the stream item
3. when you refresh the page, it is still dismissed
8. as the original user, confirm that the items are dismissed from your
dashboard on all shards
Change-Id: I2c600685015640af36d9e33ac71e25cd536d7391
Reviewed-on: https://gerrit.instructure.com/24155
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Mark Ericksen <marke@instructure.com>
QA-Review: Jeremy Putnam <jeremyp@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jon Jensen <jon@instructure.com>
2013-09-07 04:01:21 +08:00
# just cache on the user's shard... makes cache invalidation much
# easier if we visit other shards
shard . activate do
if opts [ :contexts ]
items = [ ]
Array ( opts [ :contexts ] ) . each do | context |
items . concat (
Rails . cache . fetch ( StreamItemCache . recent_stream_items_key ( self , context . class . base_class . name , context . id ) ,
:expires_in = > expires_in ) {
recent_stream_items ( :context = > context )
} )
end
items . sort { | a , b | b . id < = > a . id }
else
# no context in cache key
Rails . cache . fetch ( StreamItemCache . recent_stream_items_key ( self ) , :expires_in = > expires_in ) {
recent_stream_items
}
2012-11-03 05:41:33 +08:00
end
end
end
2013-01-04 03:44:41 +08:00
# NOTE: excludes submission stream items
2012-05-11 03:59:21 +08:00
def recent_stream_items ( opts = { } )
2012-11-17 06:00:05 +08:00
self . shard . activate do
2013-04-03 01:53:08 +08:00
Shackles . activate ( :slave ) do
2013-03-19 03:07:47 +08:00
visible_instances = visible_stream_item_instances ( opts ) .
includes ( :stream_item ) .
limit ( Setting . get ( 'recent_stream_item_limit' , 100 ) )
2012-11-17 06:00:05 +08:00
visible_instances . map do | sii |
si = sii . stream_item
2013-01-04 03:44:41 +08:00
next unless si . present?
next if si . asset_type == 'Submission'
si . data . write_attribute ( :unread , sii . unread? )
2012-11-17 06:00:05 +08:00
si
end . compact
end
2012-08-21 01:12:08 +08:00
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 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
2013-09-27 05:15:55 +08:00
events . sort_by { | e | [ e . start_at , Canvas :: ICU . collation_key ( e . title || " " ) ] } . uniq
2011-02-01 09:57:29 +08:00
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
2013-03-08 08:16:40 +08:00
now = Time . zone . now
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
2013-03-19 03:07:47 +08:00
events = CalendarEvent . active . for_user_and_context_codes ( self , context_codes ) . between ( now , opts [ :end_at ] ) . limit ( opts [ :limit ] ) . reject ( & :hidden? )
2013-03-08 08:16:40 +08:00
events += select_upcoming_assignments ( Assignment .
active .
for_context_codes ( context_codes ) .
due_between_with_overrides ( now , opts [ :end_at ] ) .
include_submitted_count .
map { | a | a . overridden_for ( self ) } , opts . merge ( :time = > now ) ) .
2013-01-24 04:24:12 +08:00
first ( opts [ :limit ] )
2013-09-27 05:15:55 +08:00
events . sort_by { | e | [ e . start_at ? 0 : 1 , e . start_at || 0 , Canvas :: ICU . collation_key ( e . title ) ] } . uniq . first ( opts [ :limit ] )
2011-02-01 09:57:29 +08:00
end
2013-03-08 08:16:40 +08:00
def select_upcoming_assignments ( assignments , opts )
time = opts [ :time ] || Time . zone . now
assignments . select do | a |
if a . grants_right? ( self , nil , :delete )
a . all_dates_visible_to ( self ) . any? do | due_hash |
due_hash [ :due_at ] && due_hash [ :due_at ] > = time && due_hash [ :due_at ] < = opts [ :end_at ]
end
else
a . due_at && a . due_at > = time && a . due_at < = opts [ :end_at ]
end
end
end
2011-02-01 09:57:29 +08:00
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
2013-09-27 05:15:55 +08:00
Canvas :: ICU . collate_by ( undated_events , & :title )
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 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
2012-11-17 06:00:05 +08:00
def setup_context_association_lookups ( column , contexts = nil , opts = { } )
contexts = Array ( contexts || cached_contexts )
conditions = [ [ ] ]
backcompat = opts [ :backcompat ]
contexts . map do | context |
if backcompat
conditions . first << " (( #{ column } _type=? AND #{ column } _id=?) OR ( #{ column } _code=? AND #{ column } _type IS NULL)) "
else
conditions . first << " ( #{ column } _type=? AND #{ column } _id=?) "
end
conditions . concat [ context . class . base_class . name , context . id ]
conditions << context . asset_string if backcompat
end
conditions [ 0 ] = conditions [ 0 ] . join ( " OR " )
conditions
end
2011-02-01 09:57:29 +08:00
# 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.
2013-03-19 03:07:47 +08:00
self . courses . includes ( :active_groups ) . select { | c | c . grants_right? ( self , :manage_groups ) } . each { | c | context_groups += c . active_groups }
2011-03-02 05:28:15 +08:00
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
2013-04-30 07:28:51 +08:00
# Public: Return an array of context codes this user belongs to.
#
# include_concluded_codes - If true, include concluded courses (default: true).
#
# Returns an array of context code strings.
def conversation_context_codes ( include_concluded_codes = true )
2013-08-20 02:13:46 +08:00
return @conversation_context_codes [ include_concluded_codes ] if @conversation_context_codes
2013-04-30 07:28:51 +08:00
Rails . cache . fetch ( [ self , include_concluded_codes , 'conversation_context_codes4' ] . cache_key , :expires_in = > 1 . day ) do
2013-04-19 02:34:50 +08:00
Shard . birth . activate do
2013-04-30 07:28:51 +08:00
associations = %w{ courses concluded_courses current_groups }
associations . slice! ( 1 ) unless include_concluded_codes
associations . inject ( [ ] ) do | result , association |
association_type = association . split ( '_' ) [ - 1 ] . slice ( 0 .. - 2 )
result . concat ( send ( association ) . with_each_shard . map { | x | " #{ association_type } _ #{ x . id } " } )
end . uniq
2013-02-22 08:04:22 +08:00
end
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
2013-08-20 02:13:46 +08:00
def self . preload_conversation_context_codes ( users )
users = users . reject { | u | u . instance_variable_get ( :@conversation_context_codes ) }
return if users . length < Setting . get_cached ( " min_users_for_conversation_context_codes_preload " , 5 ) . to_i
preload_shard_associations ( users )
shards = Set . new
users . each do | user |
shards . merge ( user . associated_shards )
end
courses = [ ]
concluded_courses = [ ]
groups = [ ]
Shard . with_each_shard ( shards . to_a ) do
courses . concat (
Enrollment . joins ( :course ) .
where ( enrollment_conditions ( :active ) ) .
where ( user_id : users ) .
select ( [ :user_id , :course_id ] ) .
uniq .
all )
concluded_courses . concat (
Enrollment .
where ( enrollment_conditions ( :completed ) ) .
where ( user_id : users ) .
select ( [ :user_id , :course_id ] ) .
uniq .
all )
groups . concat (
GroupMembership . joins ( :group ) .
where ( User . reflections [ :current_group_memberships ] . options [ :conditions ] ) .
where ( user_id : users ) .
select ( [ :user_id , :group_id ] ) .
uniq .
all )
end
Shard . birth . activate do
courses = courses . group_by ( & :user_id )
concluded_courses = concluded_courses . group_by ( & :user_id )
groups = groups . group_by ( & :user_id )
users . each do | user |
active_contexts = ( courses [ user . id ] || [ ] ) . map { | e | " course_ #{ e . course_id } " } +
( groups [ user . id ] || [ ] ) . map { | gm | " group_ #{ gm . group_id } " }
concluded_courses = ( concluded_courses [ user . id ] || [ ] ) . map { | e | " course_ #{ e . course_id } " }
user . instance_variable_set ( :@conversation_context_codes , {
true = > ( active_contexts + concluded_courses ) . uniq ,
false = > active_contexts
} )
end
end
end
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
2013-03-19 03:07:47 +08:00
note = user_notes . active . order ( 'user_notes.created_at' ) . last
2011-02-01 09:57:29 +08:00
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
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
2013-07-31 01:12:35 +08:00
def initiate_conversation ( users , private = nil , options = { } )
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
users = ( [ self ] + users ) . uniq_by ( & :id )
2012-11-29 06:53:00 +08:00
private = users . size < = 2 if private . nil?
2013-07-31 01:12:35 +08:00
Conversation . initiate ( users , private , options ) . conversation_participants . find_by_user_id ( self )
2012-03-01 03:57:53 +08:00
end
2013-04-27 03:36:16 +08:00
def messageable_user_calculator
@messageable_user_calculator || = MessageableUser :: Calculator . new ( self )
end
2013-02-27 01:30:06 +08:00
def load_messageable_user ( user , options = { } )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . load_messageable_user ( user , options )
2013-02-27 01:30:06 +08:00
end
2011-10-01 07:07:35 +08:00
2013-02-27 01:30:06 +08:00
def load_messageable_users ( users , options = { } )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . load_messageable_users ( users , options )
2011-08-19 06:03:33 +08:00
end
2013-02-27 01:30:06 +08:00
def messageable_users_in_context ( asset_string )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . messageable_users_in_context ( asset_string )
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
end
2013-02-27 01:30:06 +08:00
def count_messageable_users_in_context ( asset_string )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . count_messageable_users_in_context ( asset_string )
2011-09-10 04:44:22 +08:00
end
2013-02-27 01:30:06 +08:00
def messageable_users_in_course ( course_or_id )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . messageable_users_in_course ( course_or_id )
2011-09-10 04:44:22 +08:00
end
2013-02-27 01:30:06 +08:00
def count_messageable_users_in_course ( course_or_id )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . count_messageable_users_in_course ( course_or_id )
2013-02-27 01:30:06 +08:00
end
2011-09-10 04:44:22 +08:00
2013-02-27 01:30:06 +08:00
def messageable_users_in_section ( section_or_id )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . messageable_users_in_section ( section_or_id )
2013-02-27 01:30:06 +08:00
end
2011-09-10 04:44:22 +08:00
2013-02-27 01:30:06 +08:00
def count_messageable_users_in_section ( section_or_id )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . count_messageable_users_in_section ( section_or_id )
2011-09-10 04:44:22 +08:00
end
2011-08-01 08:35:03 +08:00
2013-02-27 01:30:06 +08:00
def messageable_users_in_group ( group_or_id )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . messageable_users_in_group ( group_or_id )
2013-02-27 01:30:06 +08:00
end
2011-06-08 08:00:17 +08:00
2013-02-27 01:30:06 +08:00
def count_messageable_users_in_group ( group_or_id )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . count_messageable_users_in_group ( group_or_id )
2011-05-20 00:33:20 +08:00
end
2011-07-21 00:05:17 +08:00
2013-02-27 01:30:06 +08:00
def search_messageable_users ( options = { } )
2013-04-27 03:36:16 +08:00
messageable_user_calculator . search_messageable_users ( options )
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
end
2013-02-27 01:30:06 +08:00
def messageable_sections
2013-04-27 03:36:16 +08:00
messageable_user_calculator . messageable_sections
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
end
2013-02-27 01:30:06 +08:00
def messageable_groups
2013-04-27 03:36:16 +08:00
messageable_user_calculator . messageable_groups
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
end
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 = [ ]
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
if info = load_messageable_user ( user )
2013-03-19 03:07:47 +08:00
if Rails . version < '3.0'
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?
else
contexts += Course . where ( :id = > info . common_courses . keys ) . all if info . common_courses . present?
contexts += Group . where ( :id = > info . common_groups . keys ) . all if info . common_groups . present?
end
2011-10-21 07:19:13 +08:00
end
2013-09-27 05:15:55 +08:00
Canvas :: ICU . collate ( contexts . map ( & :name ) )
2011-10-21 07:19:13 +08:00
end
2011-08-01 18:01:38 +08:00
def mark_all_conversations_as_read!
conversations . unread . update_all ( :workflow_state = > 'read' )
2013-03-19 03:07:47 +08:00
User . where ( :id = > id ) . update_all ( :unread_conversations_count = > 0 )
2011-08-01 18:01:38 +08:00
end
2011-09-14 01:00:19 +08:00
def conversation_participant ( conversation_id )
all_conversations . find_by_conversation_id ( conversation_id )
end
2013-01-15 00:39:03 +08:00
# Public: Reset the user's cached unread conversations count.
#
# Returns nothing.
def reset_unread_conversations_counter
2013-03-19 03:07:47 +08:00
self . class . where ( :id = > id ) . update_all ( :unread_conversations_count = > conversations . unread . count )
2013-01-15 00:39:03 +08:00
end
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 ,
2013-09-27 05:15:55 +08:00
:sortable = > [ e . rank_sortable , e . state_sortable , Canvas :: ICU . collation_key ( e . long_name ) ] ,
2011-09-27 03:08:41 +08:00
: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
2013-09-27 05:15:55 +08:00
coalesced_group_memberships = Canvas :: ICU . collate_by ( cached_group_memberships .
select { | gm | gm . active_given_enrollments? ( active_enrollments ) } ) { | gm | gm . group . name }
2011-09-29 06:27:13 +08:00
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 )
2012-11-08 04:54:18 +08:00
# try to find one that's already loaded if possible
if self . pseudonyms . loaded?
2013-06-26 23:36:29 +08:00
result = self . pseudonyms . detect { | p | p . active? && p . works_for_account? ( account , allow_implicit ) }
return result if result || self . associated_shards . length == 1
2012-11-08 04:54:18 +08:00
end
2013-06-26 23:36:29 +08:00
self . all_active_pseudonyms . detect { | p | 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
2012-11-08 04:54:18 +08:00
active_pseudonyms = self . all_active_pseudonyms ( :reload ) . select { | p | ! p . password_auto_generated? && ! p . account . delegated_authentication? }
2011-11-19 06:20:09 +08:00
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?
2013-03-19 03:07:47 +08:00
self . preferences [ :fake_student ] && ! ! self . enrollments . where ( :type = > 'StudentViewEnrollment' ) . first
2012-03-14 04:08:19 +08:00
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
MessageableUser refactor with sharding
Separates, streamlines, and makes shard-aware all use cases of
User#messageable_users *other* than searching (call site in
SearchController#matching_participants).
Produces three new methods that take the bulk of that responsibility:
* User#load_messageable_users -- given a set of users, filter out the
ones that aren't messageable, and load any common contexts for those
that are.
* User#load_messageable_user -- as User#load_messageable_users, but for
just one user.
* User#messageable_users_in_context -- given a context (a course,
section, or group), return the list of messageable users in that
context.
refs CNVS-2519
remaining on CNVS-2519 is to tackle the search application of
User#messageable_user. mostly there, but reconciling pagination
with ranking by number of shared contexts is proving problematic, so I'm
going to separate that into another commit.
meanwhile, I've renamed User#messageable_users to
User#deprecated_search_messageable_users to discourage any accidental
new uses and left it otherwise untouched. searching for users on the
same shard should be unaffected. You can still locate messageable users
on other shards to insert into conversations by browsing the shared
contexts.
test-plan:
* create user A in shard X
* create user B in shard Y
* for situations where A could message B if on the same shard:
- setup the situation where the common tie is on shard X (e.g. course
on shard X and both A and B in it). run exercises below
- setup the situation where the common tie is on shard Y. run
exercises.
- if appropriate, setup the situation where the common tie is on
shard Z. run exercises.
* for each setup described above, login as A:
- A should see the "message this user" button on B's profile
- if the common tie is a course, section, or group, A should see B
under that context when the context is selected in the recipient
browser
- if a conversation exists involving both A and B, when A loads the
conversation they should see B tagged with the common contexts
* regression test searching for messageable users from the same shard
Change-Id: Ibba5551f8afc2435fd14a2e827a400bf95eae76a
Reviewed-on: https://gerrit.instructure.com/17569
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Clare Hetherington <clare@instructure.com>
Reviewed-by: Jon Jensen <jon@instructure.com>
2013-02-05 06:18:20 +08:00
t ( '#user.default_collection_name' , " %{user_name}'s Collection " , :user_name = > self . short_name )
2012-05-30 04:56:58 +08:00
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
2013-03-19 23:49:31 +08:00
result = self . pseudonyms . with_each_shard { | scope | scope . includes ( :account ) } . map ( & :account ) . uniq . map do | account |
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
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
2013-08-20 02:13:46 +08:00
def self . preload_shard_associations ( users )
end
2012-10-02 06:13:46 +08:00
def associated_shards
[ Shard . default ]
end
def accounts
2013-03-19 23:49:31 +08:00
self . account_users . with_each_shard { | scope | scope . includes ( :account ) } . map ( & :account ) . uniq
2012-10-02 06:13:46 +08:00
end
memoize :accounts
2012-10-10 04:34:00 +08:00
2013-03-19 23:49:31 +08:00
def all_pseudonyms
self . pseudonyms . with_each_shard
2012-10-10 04:34:00 +08:00
end
memoize :all_pseudonyms
2012-10-24 03:31:55 +08:00
2013-03-19 23:49:31 +08:00
def all_active_pseudonyms
self . pseudonyms . with_each_shard { | scope | scope . active }
2012-11-08 04:54:18 +08:00
end
2013-03-19 23:49:31 +08:00
memoize :all_active_pseudonyms
2012-11-08 04:54:18 +08:00
2012-10-24 03:31:55 +08:00
def prefers_gradebook2?
preferences [ :use_gradebook2 ] != false
end
2013-04-26 07:06:20 +08:00
def stamp_logout_time!
if Rails . version < '3.0'
User . update_all ( { :last_logged_out = > Time . zone . now } , :id = > self )
else
User . where ( :id = > self ) . update_all ( :last_logged_out = > Time . zone . now )
end
end
2011-02-01 09:57:29 +08:00
end