2011-02-01 09:57:29 +08:00
#
2017-04-28 03:49:16 +08:00
# Copyright (C) 2011 - present 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/>.
#
2015-04-09 01:21:08 +08:00
require 'atom'
2011-02-01 09:57:29 +08:00
class User < ActiveRecord :: Base
2015-06-19 03:20:23 +08:00
include TurnitinID
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
use joins, not includes, on user associations in rails3
active record::associations accepts an :include
parameter which automatically retrieves specified
associations of the associated model.
In rails 2, AR checks through the where clause and
joins the table if it is referenced in the where
clause. In rails 3, this parameter is ignored when
specifying a condition on the association (and also
when you call pluck, incidentally), resulting in
database errors when you e.g.
has_many enrollments, include: [:course], :conditions
=> "courses.some_property = 'some value'"
this commit adds support for using an inner join with
these associations (it is not enough to simply define
a method on the model to retrieve the association with
the given conditions, because the associations in
question are referenced in a few has_many_through
associations later)
note this does not add similar support for inner joins on
associations in rails2, so we are still using the
:include option. This is a bit of an antipattern
as it can result in something of an N+1 queries
problem. This commit doesn't address that; it simply
makes the rails 3 postgres adapter less upset with us.
Change-Id: I5beefd689c734d372ed5627fef4bbb450883837d
Reviewed-on: https://gerrit.instructure.com/30185
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Anthus Williams <awilliams@instructure.com>
QA-Review: Anthus Williams <awilliams@instructure.com>
2014-02-15 05:22:52 +08:00
2011-02-01 09:57:29 +08:00
include Context
2017-02-18 02:45:55 +08:00
include ModelCache
2011-02-01 09:57:29 +08:00
2014-09-30 11:54:55 +08:00
attr_accessor :previous_id , :menu_data , :gradebook_importer_submissions , :prior_enrollment
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
before_save :infer_defaults
2017-02-24 22:19:42 +08:00
after_create :set_default_feature_flags
2011-02-01 09:57:29 +08:00
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
2014-05-17 06:36:00 +08:00
def self . enrollment_conditions ( state )
Enrollment :: QueryBuilder . new ( state ) . conditions or raise " invalid enrollment conditions "
2012-08-04 17:41:44 +08:00
end
2012-05-23 03:55:23 +08:00
2015-11-20 08:14:32 +08:00
has_many :communication_channels , - > { order ( 'communication_channels.position ASC' ) } , dependent : :destroy
2014-03-04 01:04:01 +08:00
has_many :notification_policies , through : :communication_channels
2015-11-20 08:14:32 +08:00
has_one :communication_channel , - > { where ( " workflow_state<>'retired' " ) . order ( :position ) }
2017-05-17 21:02:44 +08:00
has_many :planner_notes , :dependent = > :destroy
2015-01-14 06:41:12 +08:00
2011-02-01 09:57:29 +08:00
has_many :enrollments , :dependent = > :destroy
2012-01-04 04:30:49 +08:00
2016-02-18 22:54:52 +08:00
has_many :not_ended_enrollments , - > { where ( " enrollments.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive') " ) } , class_name : 'Enrollment' , multishard : true
2017-05-20 04:31:05 +08:00
has_many :not_removed_enrollments , - > { where . not ( workflow_state : [ 'rejected' , 'deleted' , 'inactive' ] ) } , class_name : 'Enrollment' , multishard : true
2012-05-17 01:22:27 +08:00
has_many :observer_enrollments
has_many :observee_enrollments , :foreign_key = > :associated_user_id , :class_name = > 'ObserverEnrollment'
2016-03-11 06:13:14 +08:00
has_many :user_observers , dependent : :destroy , inverse_of : :user
2016-10-31 23:55:57 +08:00
has_many :observers , - > { where ( " user_observers.workflow_state <> 'deleted' " ) } , :through = > :user_observers , :class_name = > 'User'
2016-03-11 06:13:14 +08:00
has_many :user_observees ,
class_name : 'UserObserver' ,
foreign_key : :observer_id ,
dependent : :destroy ,
inverse_of : :observer
2012-05-17 01:22:27 +08:00
has_many :observed_users , :through = > :user_observees , :source = > :user
2012-01-04 04:30:49 +08:00
has_many :all_courses , :source = > :course , :through = > :enrollments
2015-11-20 08:14:32 +08:00
has_many :group_memberships , - > { preload ( :group ) } , dependent : :destroy
2016-10-26 03:38:51 +08:00
has_many :groups , - > { where ( " group_memberships.workflow_state<>'deleted' " ) } , :through = > :group_memberships
2014-05-06 05:58:49 +08:00
has_many :polls , class_name : 'Polling::Poll'
2012-02-16 05:40:13 +08:00
2015-11-20 08:14:32 +08:00
has_many :current_group_memberships , - > { eager_load ( :group ) . where ( " group_memberships.workflow_state = 'accepted' AND groups.workflow_state<>'deleted' " ) } , class_name : 'GroupMembership'
2012-02-16 05:40:13 +08:00
has_many :current_groups , :through = > :current_group_memberships , :source = > :group
2011-02-01 09:57:29 +08:00
has_many :user_account_associations
2015-11-20 08:14:32 +08:00
has_many :associated_accounts , - > { order ( " user_account_associations.depth " ) } , source : :account , through : :user_account_associations
has_many :associated_root_accounts , - > { order ( " user_account_associations.depth " ) . where ( accounts : { parent_account_id : nil } ) } , source : :account , through : :user_account_associations
2011-05-27 07:41:43 +08:00
has_many :developer_keys
2015-11-20 08:14:32 +08:00
has_many :access_tokens , - > { preload ( :developer_key ) }
2017-07-19 22:13:01 +08:00
has_many :notification_endpoints , :through = > :access_tokens
2016-12-23 04:21:33 +08:00
has_many :context_external_tools , - > { order ( :name ) } , as : :context , inverse_of : :context , dependent : :destroy
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
has_many :student_enrollments
has_many :ta_enrollments
2015-11-20 08:14:32 +08:00
has_many :teacher_enrollments , - > { where ( enrollments : { type : 'TeacherEnrollment' } ) } , class_name : 'TeacherEnrollment'
2017-06-08 02:32:14 +08:00
has_many :all_submissions , - > { preload ( :assignment , :submission_comments ) . order ( 'submissions.updated_at DESC' ) } , class_name : 'Submission' , dependent : :destroy
2017-05-31 06:32:55 +08:00
has_many :submissions , - > { active . preload ( :assignment , :submission_comments , :grading_period ) . order ( 'submissions.updated_at DESC' ) }
2015-11-20 08:14:32 +08:00
has_many :pseudonyms , - > { order ( :position ) } , dependent : :destroy
has_many :active_pseudonyms , - > { where ( " pseudonyms.workflow_state<>'deleted' " ) } , class_name : 'Pseudonym'
2011-02-01 09:57:29 +08:00
has_many :pseudonym_accounts , :source = > :account , :through = > :pseudonyms
2015-11-20 08:14:32 +08:00
has_one :pseudonym , - > { where ( " pseudonyms.workflow_state<>'deleted' " ) . order ( :position ) }
2011-02-01 09:57:29 +08:00
has_many :attachments , :as = > 'context' , :dependent = > :destroy
2016-12-23 04:21:33 +08:00
has_many :active_images , - > { where ( " attachments.file_state != ? AND attachments.content_type LIKE 'image%' " , 'deleted' ) . order ( 'attachments.display_name' ) . preload ( :thumbnail ) } , as : :context , inverse_of : :context , class_name : 'Attachment'
has_many :active_assignments , - > { where ( " assignments.workflow_state<>'deleted' " ) } , as : :context , inverse_of : :context , class_name : 'Assignment'
2011-02-01 09:57:29 +08:00
has_many :all_attachments , :as = > 'context' , :class_name = > 'Attachment'
2014-06-18 05:29:44 +08:00
has_many :assignment_student_visibilities
2014-09-23 03:19:57 +08:00
has_many :quiz_student_visibilities , :class_name = > 'Quizzes::QuizStudentVisibility'
2016-12-23 04:21:33 +08:00
has_many :folders , - > { order ( 'folders.name' ) } , as : :context , inverse_of : :context
has_many :submissions_folders , - > { where . not ( :folders = > { :submission_context_code = > nil } ) } , as : :context , inverse_of : :context , class_name : 'Folder'
has_many :active_folders , - > { where ( " folders.workflow_state<>'deleted' " ) . order ( 'folders.name' ) } , class_name : 'Folder' , as : :context , inverse_of : :context
has_many :calendar_events , - > { preload ( :parent_event ) } , as : :context , inverse_of : :context , dependent : :destroy
2011-02-01 09:57:29 +08:00
has_many :eportfolios , :dependent = > :destroy
2014-01-28 05:07:09 +08:00
has_many :quiz_submissions , :dependent = > :destroy , :class_name = > 'Quizzes::QuizSubmission'
2015-11-20 08:14:32 +08:00
has_many :dashboard_messages , - > { where ( to : " dashboard " , workflow_state : 'dashboard' ) . order ( 'created_at DESC' ) } , class_name : 'Message' , dependent : :destroy
has_many :collaborations , - > { order ( 'created_at DESC' ) }
has_many :user_services , - > { order ( 'created_at' ) } , dependent : :destroy
2016-12-23 04:21:33 +08:00
has_many :rubric_associations , - > { preload ( :rubric ) . order ( 'rubric_associations.created_at DESC' ) } , as : :context , inverse_of : :context
2011-02-01 09:57:29 +08:00
has_many :rubrics
2016-12-23 04:21:33 +08:00
has_many :context_rubrics , :as = > :context , :inverse_of = > :context , :class_name = > 'Rubric'
2015-11-20 08:14:32 +08:00
has_many :grading_standards , - > { where ( " 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 :collaborators
2015-11-20 08:14:32 +08:00
has_many :collaborations , - > { preload ( :user , :collaborators ) } , through : :collaborators
has_many :assigned_submission_assessments , - > { preload ( :user , submission : :assignment ) } , class_name : 'AssessmentRequest' , foreign_key : 'assessor_id'
2011-02-01 09:57:29 +08:00
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
2016-12-23 04:21:33 +08:00
has_many :media_objects , :as = > :context , :inverse_of = > :context
2011-02-01 09:57:29 +08:00
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
2015-11-20 08:14:32 +08:00
has_many :all_conversations , - > { preload ( :conversation ) } , class_name : 'ConversationParticipant'
has_many :conversation_batches , - > { preload ( :root_conversation_message ) }
2011-09-28 04:06:52 +08:00
has_many :favorites
2012-03-10 06:52:44 +08:00
has_many :messages
2014-03-04 01:28:46 +08:00
has_many :sis_batches
2014-11-25 06:23:06 +08:00
has_many :sis_post_grades_statuses
2016-12-23 04:21:33 +08:00
has_many :content_migrations , :as = > :context , :inverse_of = > :context
has_many :content_exports , :as = > :context , :inverse_of = > :context
2015-09-11 05:25:11 +08:00
has_many :usage_rights ,
2016-12-23 04:21:33 +08:00
as : :context , inverse_of : :context ,
2015-09-11 05:25:11 +08:00
class_name : 'UsageRights' ,
dependent : :destroy
2015-04-21 00:56:13 +08:00
has_many :gradebook_csvs , dependent : :destroy
2012-07-12 07:20:39 +08:00
has_one :profile , :class_name = > 'UserProfile'
2016-12-23 04:21:33 +08:00
has_many :progresses , :as = > :context , :inverse_of = > :context
2013-06-20 14:20:21 +08:00
has_many :one_time_passwords , - > { order ( :id ) } , inverse_of : :user
2013-02-06 07:31:21 +08:00
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
feature flags infrastructure and API
test plan:
- install the test_features plugin (since no real features exist yet)
- render and consult the feature flags documentation
- have a test environment with a root account,
sub-account, course in sub-account, and user
- Use the "list features" endpoint as a root account admin
(with no site admin privileges), on the root account context, and
confirm that hidden features do not show up
- Use the "list features" endpoint as a site admin user,
on the root account context, and confirm that hidden features
show up
- Use the "list features" endpoint on the site admin account
and confirm the hidden features show up
- Use the "set feature flag" endpoint on a hidden feature on site
admin and ensure the feature becomes visible in all root accounts
- Use the "set feature flag endpoint" on a hidden feature on a
single root account, and ensure the feature becomes visible to
that root account and not others
- Confirm that root_opt_in features appear "Off" by default
in root accounts, after being "Allowed" in code or site admin
- Confirm a feature flag that is set to "on" or "off" (vs. "allowed")
cannot be overridden in a lower context (and the API returns
locked=true for them)
- Confirm that setting locking_account_id requires admin rights
in the locking account
- Confirm that a feature flag with locking_account_id cannot be
changed without admin rights in the locking account (e.g.,
set a feature flag on a course, locked with the root account's id,
and make sure a teacher who is not an account admin can't change it)
- Confirm feature flags can be deleted with the "remove feature flag"
endpoint (and they are only deleted where they are defined, not
when called on an object that inherits a flag)
Change-Id: I3e12e23b4454889b6e8b263f1315e82d8f2ada52
Reviewed-on: https://gerrit.instructure.com/25502
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Matt Fairbourn <mfairbourn@instructure.com>
Product-Review: Matt Goodwin <mattg@instructure.com>
Reviewed-by: Zach Pendleton <zachp@instructure.com>
2013-10-22 23:28:26 +08:00
include FeatureFlags
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
2014-02-05 02:19:54 +08:00
scope :of_account , lambda { | account | where ( " EXISTS (?) " , account . user_account_associations . where ( " user_account_associations.user_id=users.id " ) ) . shard ( account . shard ) }
2014-07-02 03:38:26 +08:00
scope :recently_logged_in , - > {
2015-07-17 05:53:07 +08:00
eager_load ( :pseudonyms ) .
2013-03-21 03:38:19 +08:00
where ( " pseudonyms.current_login_at>? " , 1 . month . ago ) .
order ( " pseudonyms.current_login_at DESC " ) .
limit ( 25 )
2011-02-01 09:57:29 +08:00
}
2015-07-17 05:53:07 +08:00
scope :include_pseudonym , - > { preload ( :pseudonym ) }
2013-03-21 03:38:19 +08:00
scope :restrict_to_sections , lambda { | sections |
if sections . empty?
2015-12-22 05:03:24 +08:00
all
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 |
2015-10-08 00:18:42 +08:00
next none if name . strip . empty?
2014-09-10 05:11:58 +08:00
scopes = [ ]
2016-07-27 21:59:34 +08:00
all . primary_shard . activate do
2017-11-09 02:09:00 +08:00
base_scope = except ( :select , :order , :group , :having )
scopes << base_scope . where ( wildcard ( 'users.name' , name ) )
scopes << base_scope . where ( wildcard ( 'users.short_name' , name ) )
scopes << base_scope . joins ( :pseudonyms ) . where ( wildcard ( 'pseudonyms.sis_user_id' , name ) ) . where ( pseudonyms : { workflow_state : 'active' } )
scopes << base_scope . joins ( :pseudonyms ) . where ( wildcard ( 'pseudonyms.unique_id' , name ) ) . where ( pseudonyms : { workflow_state : 'active' } )
2016-07-27 21:59:34 +08:00
end
2014-09-10 05:11:58 +08:00
scopes . map! ( & :to_sql )
self . from ( " ( #{ scopes . join ( " \n UNION \n " ) } ) users " )
2011-02-01 09:57:29 +08:00
}
2014-07-02 03:38:26 +08:00
scope :active , - > { where ( " users.workflow_state<>'deleted' " ) }
2016-03-11 06:13:14 +08:00
scope :active_user_observers , - > { where . not ( user_observers : { workflow_state : 'deleted' } ) }
2012-01-04 04:30:49 +08:00
2015-07-17 02:31:37 +08:00
scope :has_current_student_enrollments , - > do
where ( " EXISTS (?) " ,
Enrollment . joins ( " JOIN #{ Course . quoted_table_name } 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' " ) )
end
2012-01-04 04:30:49 +08:00
2015-04-28 22:57:51 +08:00
scope :not_fake_student , - > { where ( " enrollments.type <> 'StudentViewEnrollment' " ) }
2014-06-18 05:29:44 +08:00
# NOTE: only use for courses with differentiated assignments on
scope :able_to_see_assignment_in_course_with_da , lambda { | assignment_id , course_id |
joins ( :assignment_student_visibilities ) .
where ( :assignment_student_visibilities = > { :assignment_id = > assignment_id , :course_id = > course_id } )
}
2014-09-23 03:19:57 +08:00
# NOTE: only use for courses with differentiated assignments on
scope :able_to_see_quiz_in_course_with_da , lambda { | quiz_id , course_id |
joins ( :quiz_student_visibilities ) .
where ( :quiz_student_visibilities = > { :quiz_id = > quiz_id , :course_id = > course_id } )
}
2014-12-16 08:41:42 +08:00
scope :observing_students_in_course , lambda { | observee_ids , course_ids |
joins ( :enrollments ) . where ( enrollments : { type : 'ObserverEnrollment' , associated_user_id : observee_ids , course_id : course_ids , workflow_state : 'active' } )
}
# when an observer is added to a course they get an enrollment where associated_user_id is nil. when they are linked to
# a student, this first enrollment stays the same, but a new one with an associated_user_id is added. thusly to find
# course observers, you take the difference between all active observers and active observers with associated users
scope :observing_full_course , lambda { | course_ids |
active_observer_scope = joins ( :enrollments ) . where ( enrollments : { type : 'ObserverEnrollment' , course_id : course_ids , workflow_state : 'active' } )
users_observing_students = active_observer_scope . where ( " enrollments.associated_user_id IS NOT NULL " ) . pluck ( :id )
if users_observing_students == [ ] || users_observing_students == nil
active_observer_scope
else
active_observer_scope . where ( " users.id NOT IN (?) " , users_observing_students )
end
}
2017-04-07 05:00:25 +08:00
def reload ( * )
2017-08-26 06:39:06 +08:00
@all_pseudonyms = nil
2017-04-07 05:00:25 +08:00
@all_active_pseudonyms = nil
super
end
2015-02-13 23:15:35 +08:00
def assignment_and_quiz_visibilities ( context )
2015-08-18 22:57:12 +08:00
RequestCache . cache ( " assignment_and_quiz_visibilities " , self , context ) do
{ assignment_ids : DifferentiableAssignment . scope_filter ( context . assignments , self , context ) . pluck ( :id ) ,
quiz_ids : DifferentiableAssignment . scope_filter ( context . quizzes , self , context ) . pluck ( :id ) }
end
2014-08-23 03:13:56 +08:00
end
2012-08-24 05:10:00 +08:00
def self . order_by_sortable_name ( options = { } )
2017-06-28 06:37:54 +08:00
clause = sortable_name_order_by_clause
sort_direction = options [ :direction ] == :descending ? 'DESC' : 'ASC'
scope = self . order ( " #{ clause } #{ sort_direction } " ) . order ( " #{ self . table_name } .id #{ sort_direction } " )
2014-07-24 01:14:22 +08:00
if scope . select_values . empty?
2014-01-29 04:47:12 +08:00
scope = scope . select ( self . arel_table [ Arel . star ] )
end
if scope . select_values . present?
2013-03-08 07:23:32 +08:00
scope = scope . select ( clause )
end
2014-01-29 04:47:12 +08:00
if scope . group_values . present?
2013-03-08 07:23:32 +08:00
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
2015-12-22 05:03:24 +08:00
scope = self . all
2014-01-29 04:47:12 +08:00
if scope . select_values . blank?
2013-03-08 07:23:32 +08:00
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
2015-11-13 01:21:53 +08:00
scope :with_last_login , lambda {
select ( " users.*, MAX(current_login_at) as last_login " ) .
2016-03-11 03:19:20 +08:00
joins ( " LEFT OUTER JOIN #{ Pseudonym . quoted_table_name } ON pseudonyms.user_id = users.id " ) .
2015-11-13 01:21:53 +08:00
group ( " users.id " )
}
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 })
2015-11-17 06:59:10 +08:00
LEFT OUTER JOIN #{Pseudonym.quoted_table_name} ON pseudonyms.user_id = users.id AND pseudonyms.account_id = ?
INNER JOIN #{Enrollment.quoted_table_name} 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
}
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 ,
2017-07-14 21:08:32 +08:00
:self_enrollment_course , :validation_root_account , :sortable_name_explicitly_set
2014-01-14 08:39:45 +08:00
attr_reader :self_enrollment
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
2014-08-21 21:07:27 +08:00
if course && course . self_enrollment_enabled?
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
2014-09-26 04:05:28 +08:00
def courses_for_enrollments ( enrollment_scope )
2017-03-11 04:01:31 +08:00
Course . active . joins ( :all_enrollments ) . merge ( enrollment_scope . except ( :joins ) ) . distinct
2014-09-26 04:05:28 +08:00
end
def courses
courses_for_enrollments ( enrollments . current )
end
def current_and_invited_courses
courses_for_enrollments ( enrollments . current_and_invited )
end
def concluded_courses
courses_for_enrollments ( enrollments . concluded )
end
def current_and_concluded_courses
courses_for_enrollments ( enrollments . current_and_concluded )
end
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
2013-11-27 08:03:47 +08:00
def enrollments_for_account_and_sub_accounts ( account )
# enrollments are always on the course's shard
# and courses are always on the root account's shard
account . shard . activate do
round avatars everywhere (almost)
fixes CNVS-10713, CNVS-10636
test plan
- ensure that every page that has avatars displays them in a
consistent, circular way
- ensure that the change didn't break the rest of the page
avatars were left square on purpose on these pages
- speedgrader header (too much work to circle)
- big pic on profile page (too big and interactive, needs
some proper ui design)
- profile pic selector (not really profile pics yet, just images)
- app review (error handling is tricky)
some pages where you might find avatars to check
- manage avatars (/account/:id/avatars)
- discussions index
- discussions show (don't forget threaded sub-entries!)
- profile page (/about/:id)
- group edit (i can't find this page, but it has code)
- course roster (/courses/:id/users)
- context roster (/groups/:id/users)
- old conversations (message list, message detail, people picker)
- new conversations
- gradebook2 (student column, submission comments)
- speed grader (top left student name, submission comments,
discussion assignment)
- app review (enable app center plugin, app rating comments)
- user show page (/users/:id)
- masquerade page (/users/:id/masquerade)
- masquerade footer (on the bottom of any page when you masquerade)
Change-Id: Ic01a4b44433aaf6254798d8267bf473a8bf85c2d
Reviewed-on: https://gerrit.instructure.com/28953
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Braden Anderson <banderson@instructure.com>
Product-Review: Joel Hough <joel@instructure.com>
QA-Review: Joel Hough <joel@instructure.com>
2014-02-06 07:23:59 +08:00
Enrollment . where ( user_id : self ) . active . joins ( :course ) . where ( " courses.account_id=? OR courses.root_account_id=? " , account , account )
end
2013-11-27 08:03:47 +08:00
end
2011-08-18 03:33:10 +08:00
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?
2014-09-12 03:44:34 +08:00
accounts = Account . where ( id : remaining_ids )
2011-08-18 03:33:10 +08:00
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 )
2015-07-13 21:53:50 +08:00
users = users_or_user_ids = User . select ( [ :id , :preferences , :workflow_state , :updated_at ] ) . where ( id : user_ids ) . to_a
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 ] ) .
2017-03-11 04:01:31 +08:00
distinct . to_a
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 ] ) .
2015-07-25 00:01:44 +08:00
where ( :id = > course_section_ids . to_a ) . to_a 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
2015-07-25 00:01:44 +08:00
data [ :courses ] += Course . select ( [ :id , :account_id ] ) . where ( :id = > course_ids . to_a ) . to_a unless course_ids . empty?
2013-01-26 01:32:08 +08:00
2017-03-11 04:01:31 +08:00
data [ :pseudonyms ] += Pseudonym . active . select ( [ :user_id , :account_id ] ) . distinct . where ( :user_id = > shard_user_ids ) . to_a
2017-07-16 14:40:03 +08:00
data [ :account_users ] += AccountUser . active . select ( [ :user_id , :account_id ] ) . distinct . where ( :user_id = > shard_user_ids ) . to_a
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
2015-07-25 00:01:44 +08:00
UserAccountAssociation . where ( :user_id = > shard_user_ids ) . to_a
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
2014-09-12 04:01:11 +08:00
rescue ActiveRecord :: RecordNotUnique
2013-02-18 23:14:18 +08:00
# race condition - someone else created the UAA after we queried for existing ones
2014-09-12 03:44:34 +08:00
old_aa = UserAccountAssociation . where ( user_id : aa . user_id , account_id : aa . account_id ) . first
2013-02-18 23:14:18 +08:00
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
2013-12-20 07:53:56 +08:00
if ( incremental && association [ 1 ] > depth ) || ( ! incremental && association [ 1 ] != depth )
2014-07-24 01:14:22 +08:00
UserAccountAssociation . where ( :id = > association [ 0 ] ) . update_all ( :depth = > depth )
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 ] }
2014-07-24 01:14:22 +08:00
UserAccountAssociation . where ( :id = > to_delete ) . delete_all unless incremental || to_delete . empty?
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
2015-04-28 01:47:25 +08:00
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
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
2014-07-11 01:22:01 +08:00
self . uuid = CanvasSlug . 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 )
2015-07-17 05:53:07 +08:00
eager_load ( :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
2014-12-24 06:00:05 +08:00
# Returns an array of groups which are currently visible for the user.
def visible_groups
@visible_groups || = begin
2016-08-06 00:07:05 +08:00
enrollments = self . cached_current_enrollments ( preload_dates : true , preload_courses : true )
2016-12-12 04:03:44 +08:00
self . current_groups . select do | group |
2016-03-10 09:00:22 +08:00
group . context_type != 'Course' || enrollments . any? do | en |
en . course == group . context && ! ( en . inactive? || en . completed? ) && ( en . admin? || en . course . available? )
end
2014-12-24 06:00:05 +08:00
end
end
end
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
2015-09-12 05:31:50 +08:00
User . name_parts ( self . sortable_name , likely_already_surname_first : true ) [ 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
2015-09-12 05:31:50 +08:00
User . name_parts ( self . sortable_name , likely_already_surname_first : true ) [ 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
2015-09-12 05:31:50 +08:00
def self . name_parts ( name , prior_surname : nil , likely_already_surname_first : false )
2011-10-27 05:09:09 +08:00
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.
2015-09-12 05:31:50 +08:00
if ! likely_already_surname_first && ! suffix && surname =~ / \ s / && given =~ SUFFIXES
2011-10-27 05:09:09 +08:00
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
2015-09-12 05:31:50 +08:00
def self . last_name_first ( name , name_was = nil , likely_already_surname_first : )
previous_surname = name_parts ( name_was , likely_already_surname_first : likely_already_surname_first ) [ 1 ]
given , surname , suffix = name_parts ( name , prior_surname : previous_surname )
2011-10-27 05:09:09 +08:00
given = [ given , suffix ] . compact . join ( ' ' )
surname ? " #{ surname } , #{ given } " . strip : given
end
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
2015-09-12 05:31:50 +08:00
self . sortable_name = nil if ! self . sortable_name_changed? &&
2017-07-14 21:08:32 +08:00
! sortable_name_explicitly_set &&
2015-09-12 05:31:50 +08:00
self . name_changed? &&
User . name_parts ( self . sortable_name , likely_already_surname_first : true ) . compact . join ( ' ' ) == self . name_was
unless read_attribute ( :sortable_name )
self . sortable_name = User . last_name_first ( self . name , self . sortable_name_was , likely_already_surname_first : true )
end
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
true
end
2012-01-04 04:30:49 +08:00
2017-02-24 22:19:42 +08:00
def set_default_feature_flags
self . enable_feature! ( :new_user_tutorial_on_off )
end
2011-02-01 09:57:29 +08:00
def sortable_name
2015-09-12 05:31:50 +08:00
self . sortable_name = read_attribute ( :sortable_name ) ||
User . last_name_first ( self . name , likely_already_surname_first : false )
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.
2014-02-28 04:26:13 +08:00
if communication_channels . loaded?
communication_channels . to_a . find { | cc | cc . path_type == 'email' && cc . workflow_state != 'retired' }
else
communication_channels . email . unretired . first
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 email
2017-01-07 04:42:13 +08:00
value = Rails . cache . fetch ( email_cache_key ) do
2012-08-21 00:41:17 +08:00
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
2017-01-07 04:42:13 +08:00
def email_cache_key
[ 'user_email' , self ] . cache_key
end
def clear_email_cache!
Rails . cache . delete ( email_cache_key )
end
2012-08-21 00:41:17 +08:00
def email_cached?
2017-01-07 04:42:13 +08:00
Rails . cache . exist? ( email_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 gmail_channel
2014-09-12 03:44:34 +08:00
addr = self . user_services .
where ( service_domain : " google.com " ) .
limit ( 1 ) . pluck ( :service_user_id ) . first
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
2015-02-20 00:41:41 +08:00
res || = google_drive_address
res || = google_docs_address
2014-09-12 03:44:34 +08:00
res || email
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 google_docs_address
2015-02-20 00:41:41 +08:00
google_service_address ( 'google_docs' )
end
def google_drive_address
google_service_address ( 'google_drive' )
end
def google_service_address ( service_name )
self . user_services . where ( service : service_name )
. limit ( 1 ) . pluck ( service_name == 'google_drive' ? :service_user_name : :service_user_id ) . 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 email = ( e )
if e . is_a? ( CommunicationChannel ) and e . user_id == self . id
cc = e
else
2015-05-20 07:10:11 +08:00
cc = self . communication_channels . email . by_path ( e ) . first ||
2016-04-02 05:46:23 +08:00
self . communication_channels . email . create! ( path : e )
2011-12-17 06:16:37 +08:00
cc . user = self
2011-02-01 09:57:29 +08:00
end
cc . move_to_top
cc . save!
self . reload
2017-01-07 04:42:13 +08:00
self . clear_email_cache!
2011-02-01 09:57:29 +08:00
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 short_name
read_attribute ( :short_name ) || name
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
2016-01-06 02:31:25 +08:00
alias_method :destroy_permanently! , :destroy
2014-10-14 00:15:02 +08:00
def destroy
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
self . remove_from_root_account ( :all )
self . workflow_state = 'deleted'
self . deleted_at = Time . now . utc
self . save
2013-10-10 06:21:57 +08:00
end
# avoid extraneous callbacks when enrolled in multiple sections
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
def delete_enrollments ( enrollment_scope = self . enrollments )
2017-03-15 01:48:55 +08:00
courses_to_update = enrollment_scope . active . distinct . pluck ( :course_id )
2014-01-18 02:50:57 +08:00
Enrollment . suspend_callbacks ( :update_cached_due_dates ) do
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
enrollment_scope . each { | e | e . destroy }
2013-10-10 06:21:57 +08:00
end
courses_to_update . each do | course |
DueDateCacher . recompute_course ( course )
2011-12-06 04:48:11 +08:00
end
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
def remove_from_root_account ( root_account )
ActiveRecord :: Base . transaction do
if root_account == :all
# make sure to hit all shards
enrollment_scope = self . enrollments . shard ( self )
2016-04-27 04:46:22 +08:00
user_observer_scope = self . user_observers . shard ( self )
user_observee_scope = self . user_observees . shard ( self )
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
pseudonym_scope = self . pseudonyms . active . shard ( self )
2017-07-16 14:40:03 +08:00
account_users = self . account_users . active . shard ( self )
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
has_other_root_accounts = false
else
# make sure to do things on the root account's shard. but note,
# root_account.enrollments won't include the student view user's
# enrollments, so we need to fetch them off the user instead; the
# student view user won't be cross shard, so that will still be the
# right shard
enrollment_scope = fake_student? ? self . enrollments : root_account . enrollments . where ( user_id : self )
2017-03-15 01:48:55 +08:00
user_observer_scope = self . user_observers . shard ( self )
user_observee_scope = self . user_observees . shard ( self )
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
pseudonym_scope = root_account . pseudonyms . active . where ( user_id : self )
2016-02-18 04:55:34 +08:00
account_users = root_account . account_users . where ( user_id : self ) . to_a +
self . account_users . shard ( root_account ) . where ( :account_id = > root_account . all_accounts ) . to_a
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
has_other_root_accounts = self . associated_accounts . shard ( self ) . where ( 'accounts.id <> ?' , root_account ) . exists?
end
self . delete_enrollments ( enrollment_scope )
2016-04-27 04:46:22 +08:00
user_observer_scope . destroy_all
user_observee_scope . destroy_all
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
pseudonym_scope . each ( & :destroy )
2016-02-18 04:55:34 +08:00
account_users . each ( & :destroy )
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
# only delete the user's communication channels when the last account is
# removed (they don't belong to any particular account). they will always
# be on the user's shard
self . communication_channels . each ( & :destroy ) unless has_other_root_accounts
self . update_account_associations
end
2015-07-24 23:27:08 +08:00
self . reload
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2013-09-18 04:16:35 +08:00
def associate_with_shard ( shard , strength = :strong )
2012-10-05 05:47:38 +08:00
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
2014-06-24 08:11:21 +08:00
def check_courses_right? ( user , sought_right )
# Look through the currently enrolled courses first. This should
# catch most of the calls. If none of the current courses grant
# the right then look at the concluded courses.
user && sought_right && (
self . courses . any? { | c | c . grants_right? ( user , sought_right ) } ||
self . concluded_courses . any? { | c | c . grants_right? ( user , sought_right ) }
)
end
2015-10-15 00:45:40 +08:00
def check_accounts_right? ( user , sought_right )
# check if the user we are given is an admin in one of this user's accounts
2015-11-05 01:42:53 +08:00
return false unless user
return true if Account . site_admin . grants_right? ( user , sought_right )
2016-07-29 05:44:15 +08:00
common_shards = associated_shards & user . associated_shards
search_method = - > ( shard ) do
associated_accounts . shard ( shard ) . any? { | a | a . grants_right? ( user , sought_right ) }
end
# search shards the two users have in common first, since they're most likely
return true if common_shards . any? ( & search_method )
# now do an exhaustive search, since it's possible to have admin permissions for accounts
# you're not associated with
return true if ( associated_shards - common_shards ) . any? ( & search_method )
false
2015-10-15 00:45:40 +08:00
end
2011-02-01 09:57:29 +08:00
set_policy do
given { | user | user == self }
2015-10-15 00:45:40 +08:00
can :read and can :read_grades and can :read_profile and can :read_as_admin and can :manage and
can :manage_content and can :manage_files and can :manage_calendar and can :send_messages and
can :update_avatar and can :manage_feature_flags
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 ) } }
2015-01-27 02:11:13 +08:00
can :read_profile
2012-01-04 04:30:49 +08:00
2014-06-24 08:11:21 +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
given { | user | self . check_courses_right? ( user , :read_reports ) }
2015-01-05 23:23:02 +08:00
can :read_profile and can :remove_avatar and can :read_reports
2011-08-12 04:50:02 +08:00
2014-06-24 08:11:21 +08:00
given { | user | self . check_courses_right? ( user , :manage_user_notes ) }
2011-08-12 04:50:02 +08:00
can :create_user_notes and can :read_user_notes
2015-10-15 00:45:40 +08:00
given { | user | self . check_accounts_right? ( user , :manage_user_notes ) }
2011-08-12 04:50:02 +08:00
can :create_user_notes and can :read_user_notes and can :delete_user_notes
2015-10-15 00:45:40 +08:00
given { | user | self . check_accounts_right? ( user , :view_statistics ) }
2013-03-13 03:47:21 +08:00
can :view_statistics
2015-10-15 00:45:40 +08:00
given { | user | self . check_accounts_right? ( user , :manage_students ) }
can :manage_user_details and can :update_avatar and can :remove_avatar and can :rename and can :read_profile and
can :view_statistics and can :read and can :read_reports and can :manage_feature_flags and can :read_grades
2012-01-04 04:30:49 +08:00
2015-10-15 00:45:40 +08:00
given { | user | self . check_accounts_right? ( user , :manage_user_logins ) }
2015-11-12 04:42:48 +08:00
can :read and can :read_reports
given { | user | self . check_accounts_right? ( user , :read_roster ) }
can :read_full_profile
2013-01-09 00:47:47 +08:00
2015-10-15 00:45:40 +08:00
given { | user | self . check_accounts_right? ( user , :view_all_grades ) }
can :read_grades
2011-02-01 09:57:29 +08:00
given do | user |
2017-07-16 14:40:03 +08:00
self . check_accounts_right? ( user , :manage_user_logins ) && self . adminable_accounts . select ( & :root_account? ) . all? { | a | has_subset_of_account_permissions? ( user , a ) }
2011-02-01 09:57:29 +08:00
end
2015-01-05 23:23:02 +08:00
can :manage_user_details and can :rename and can :read_profile
2014-10-13 23:58:11 +08:00
2015-07-10 21:55:36 +08:00
given { | user | self . pseudonyms . shard ( self ) . any? { | p | p . grants_right? ( user , :update ) } }
2014-10-14 00:07:25 +08:00
can :merge
2014-10-13 23:58:11 +08:00
given do | user |
# a user can reset their own MFA, but only if the setting isn't required
( self == user && self . mfa_settings != :required ) ||
2016-01-07 06:52:53 +08:00
# a site_admin with permission to reset_any_mfa
( Account . site_admin . grants_right? ( user , :reset_any_mfa ) ) ||
2014-10-13 23:58:11 +08:00
# an admin can reset another user's MFA only if they can manage *all*
# of the user's pseudonyms
2016-01-07 06:52:53 +08:00
self != user && self . pseudonyms . shard ( self ) . all? do | p |
p . grants_right? ( user , :update ) ||
# the account does not have mfa enabled
p . account . mfa_settings == :disabled ||
# they are an admin user and have reset MFA permission
p . account . grants_right? ( user , :reset_any_mfa )
end
2014-10-13 23:58:11 +08:00
end
can :reset_mfa
2015-10-10 05:36:08 +08:00
2016-03-11 06:13:14 +08:00
given { | user | user && user . user_observees . active . where ( user_id : self . id ) . exists? }
2015-11-18 02:51:38 +08:00
can :read and can :read_as_parent
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
2014-05-03 00:35:29 +08:00
return true if self . fake_student? && self . courses . any? { | c | c . grants_right? ( masquerader , :use_student_view ) }
2012-02-21 06:57:32 +08:00
return false unless
2017-04-07 05:00:25 +08:00
account . grants_right? ( masquerader , nil , :become_user ) && SisPseudonym . for ( self , account , type : :implicit , require_sis : false )
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?
2015-10-23 21:14:24 +08:00
Rails . cache . fetch ( [ 'has_subset_of_account_permissions' , self , user , account ] . cache_key , :expires_in = > 60 . minutes ) do
account_users = account . all_account_users_for ( self )
account_users . all? do | account_user |
account_user . is_subset_of? ( user )
end
2012-02-21 06:57:32 +08:00
end
2011-02-01 09:57:29 +08:00
end
clean up user "deletion"
fixes CNVS-1552
any time the UI/API tries to "delete" a user, it should only be trying
to remove it from some root account (the @domain_root_account if not
otherwise specified). if that root account was the last root account the
user was associated with, then the remnants of the user are fully
deleted, but only then. leave User#destroy as a short-cut to delete the
user from all their accounts at once, but should not be invoked directly
from any UI/API actions.
test-plan:
PERMISSIONS
being able to remove a user from an account entails being able to:
- DELETE http://accounts-domain/users/:user
- DELETE /accounts/:account/users/:user
both should fail or succeed together
* given
- Sally who's an admin with the :manage_user_logins
permission on one account (Account1) and a student on another
account (Account2)
- Bob who's a student on both accounts
- Alice who's an admin on Account1 with greater permissions than
Sally
* Sally should:
- see "Delete My Account" on her Account1 profile
- not see "Delete My Account" on her Account2 profile
- not see "Delete My Account" on Bob's Account1 profile
- not see "Delete My Account" on Alice's Account1 profile
- see "Delete from Account1" at /users/:sally
- see "Delete from Account1" at /users/:bob
- not see "Delete from Account2" at /users/:sally
- not see "Delete from Account2" at /users/:bob
- not see "Delete from Account1" at /users/:alice
- be able to remove herself from Account1
- be able to remove Bob from Account1
- not be able to remove herself from Account2
- not be able to remove Bob from Account2
- not be able to remove Alice from Account1
* given Sally's Account1 pseudonym has a SIS ID but her Account2
pseudonym doesn't, Sally should:
- no longer see "Delete My Account" on her Account1 profile
- no longer see "Delete from Account1" at /users/:sally
- still see "Delete from Account1" at /users/:bob
- no longer be able to remove herself from Account1
- still be able to remove Bob from Account1
EFFECTS
* as Sally, remove Bob from Account1 via
DELETE http://account1-domain/users/:bob
- Bob's pseudonyms, enrollments, etc. in Account1 should be removed
- Bob's pseudonyms, enrollments, etc. in Account2 should be untouched
* repeat using DELETE /accounts/:account1/users/:bob, with the same
expectations
Change-Id: Ib7612f95d1c7e4cca36d8486950565ec096b4ab1
Reviewed-on: https://gerrit.instructure.com/41591
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2014-09-23 07:04:03 +08:00
def allows_user_to_remove_from_account? ( account , other_user )
Pseudonym . new ( account : account , user : self ) . grants_right? ( other_user , :delete ) &&
( Pseudonym . new ( account : account , user : self ) . grants_right? ( other_user , :manage_sis ) ||
! account . pseudonyms . active . where ( user_id : self ) . where ( 'sis_user_id IS NOT NULL' ) . exists? )
end
2011-02-01 09:57:29 +08:00
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 visible_inbox_types = ( val )
types = ( val || " " ) . split ( " , " )
write_attribute ( :visible_inbox_types , types . map { | t | t . classify } . join ( " , " ) )
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def show_in_inbox? ( type )
if self . respond_to? ( :visible_inbox_types ) && self . visible_inbox_types
types = self . visible_inbox_types . split ( " , " )
types . include? ( type )
else
true
end
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def update_avatar_image ( force_reload = false )
if ! self . avatar_image_url || force_reload
2015-03-27 08:10:14 +08:00
if self . avatar_image_source == 'twitter'
2011-02-01 09:57:29 +08:00
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-10-05 04:02:49 +08:00
Setting . get ( 'max_messages_per_day_per_user' , 500 ) . to_i
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2011-02-01 09:57:29 +08:00
def max_messages_per_day
User . max_messages_per_day
end
2012-01-04 04:30:49 +08:00
2012-02-24 03:21:12 +08:00
def gravatar_url ( size = 50 , fallback = nil , request = nil )
2012-03-02 14:54:17 +08:00
fallback = self . class . avatar_fallback_url ( fallback , request )
2011-03-09 02:19:33 +08:00
" https://secure.gravatar.com/avatar/ #{ Digest :: MD5 . hexdigest ( self . email ) rescue '000' } ?s= #{ size } &d= #{ CGI :: escape ( fallback ) } "
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2012-03-29 03:57:05 +08:00
# Public: Set a user's avatar image. This is a convenience method that sets
# the avatar_image_source, avatar_image_url, avatar_updated_at, and
# avatar_state on the user model.
#
# val - A hash of options used to configure the avatar.
2015-03-27 08:10:14 +08:00
# :type - The type of avatar. Should be '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 )
2015-03-27 08:10:14 +08:00
if val [ 'type' ] == 'gravatar'
2011-02-01 09:57:29 +08:00
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 )
2015-03-03 04:02:30 +08:00
if self . avatar_state == :approved || self . avatar_state == :locked
self . avatar_state = 're_reported'
2011-02-01 09:57:29 +08:00
else
2015-03-03 04:02:30 +08:00
self . avatar_state = 'reported'
2011-02-01 09:57:29 +08:00
end
2015-03-03 04:02:30 +08:00
self . save!
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 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 )
2014-09-19 03:47:01 +08:00
Canvas :: Security . verify_hmac_sha1 ( sig , user_id . to_s , truncate : 10 ) ? user_id : nil
2012-02-04 14:05:19 +08:00
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
2016-02-20 03:11:18 +08:00
uri . scheme || = request ? request . protocol [ 0 .. - 4 ] : HostUrl . protocol # -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
round avatars everywhere (almost)
fixes CNVS-10713, CNVS-10636
test plan
- ensure that every page that has avatars displays them in a
consistent, circular way
- ensure that the change didn't break the rest of the page
avatars were left square on purpose on these pages
- speedgrader header (too much work to circle)
- big pic on profile page (too big and interactive, needs
some proper ui design)
- profile pic selector (not really profile pics yet, just images)
- app review (error handling is tricky)
some pages where you might find avatars to check
- manage avatars (/account/:id/avatars)
- discussions index
- discussions show (don't forget threaded sub-entries!)
- profile page (/about/:id)
- group edit (i can't find this page, but it has code)
- course roster (/courses/:id/users)
- context roster (/groups/:id/users)
- old conversations (message list, message detail, people picker)
- new conversations
- gradebook2 (student column, submission comments)
- speed grader (top left student name, submission comments,
discussion assignment)
- app review (enable app center plugin, app rating comments)
- user show page (/users/:id)
- masquerade page (/users/:id/masquerade)
- masquerade footer (on the bottom of any page when you masquerade)
Change-Id: Ic01a4b44433aaf6254798d8267bf473a8bf85c2d
Reviewed-on: https://gerrit.instructure.com/28953
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Braden Anderson <banderson@instructure.com>
Product-Review: Joel Hough <joel@instructure.com>
QA-Review: Joel Hough <joel@instructure.com>
2014-02-06 07:23:59 +08:00
uri . host , port = HostUrl . default_host . split ( / : / )
uri . port = Integer ( port ) if port
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
2014-09-12 03:44:34 +08:00
rubrics += Rubric . active . where ( context_code : context_codes ) . to_a
2014-03-18 04:54:26 +08:00
rubrics . uniq . sort_by { | r | [ ( r . association_count || 0 ) > 3 ? CanvasSort :: First : CanvasSort :: Last , Canvas :: ICU . collation_key ( r . title || CanvasSort :: Last ) ] }
2011-02-01 09:57:29 +08:00
end
def assignments_recently_graded ( opts = { } )
opts = { :start_at = > 1 . week . ago , :limit = > 10 } . merge ( opts )
2017-06-08 02:32:14 +08:00
Submission . active . recently_graded_assignments ( id , opts [ :start_at ] , opts [ :limit ] )
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 preferences
2015-12-29 03:01:30 +08:00
read_or_initialize_attribute ( :preferences , { } )
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2017-02-22 05:59:51 +08:00
def new_user_tutorial_statuses
preferences [ :new_user_tutorial_statuses ] || = { }
end
2015-04-07 05:54:23 +08:00
def custom_colors
preferences [ :custom_colors ] || = { }
end
2016-10-14 00:54:57 +08:00
def dashboard_positions
preferences [ :dashboard_positions ] || = { }
end
def dashboard_positions = ( new_positions )
preferences [ :dashboard_positions ] = new_positions
2016-10-11 06:05:01 +08:00
end
2015-10-15 21:19:41 +08:00
def course_nicknames
preferences [ :course_nicknames ] || = { }
end
def course_nickname ( course )
shard . activate do
course_nicknames [ course . id ]
end
end
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
2016-07-14 00:00:18 +08:00
def send_scores_in_emails? ( root_account )
preferences [ :send_scores_in_emails ] == true && root_account . settings [ :allow_sending_scores_in_emails ] != false
2011-05-04 11:16:50 +08:00
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
2014-04-03 14:37:18 +08:00
def prefers_high_contrast?
2014-09-09 01:01:59 +08:00
! ! feature_enabled? ( :high_contrast )
2014-04-03 14:37:18 +08:00
end
2013-05-07 06:18:56 +08:00
def manual_mark_as_read?
! ! preferences [ :manual_mark_as_read ]
end
2016-09-01 04:52:40 +08:00
def collapse_global_nav?
! ! preferences [ :collapse_global_nav ]
end
2014-07-29 02:38:48 +08:00
def disabled_inbox?
! ! preferences [ :disable_inbox ]
end
2013-08-06 07:13:54 +08:00
def use_new_conversations?
2014-05-06 05:12:00 +08:00
true
2013-08-06 07:13:54 +08:00
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 )
2014-09-12 04:01:11 +08:00
rescue ActiveRecord :: RecordNotUnique
2013-01-31 03:39:40 +08:00
asset . shard . activate do
2014-09-12 03:44:34 +08:00
ignore = asset . ignores . where ( user_id : self , purpose : purpose ) . first
2013-01-31 03:39:40 +08:00
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
2015-07-10 01:15:52 +08:00
def assignments_visible_in_course ( course )
2014-08-28 04:27:46 +08:00
return course . active_assignments if course . grants_any_right? ( self , :read_as_admin , :manage_grades , :manage_assignments )
2014-09-24 04:23:30 +08:00
published_visible_assignments = course . active_assignments . published
2016-02-26 23:44:19 +08:00
published_visible_assignments = DifferentiableAssignment . scope_filter ( published_visible_assignments , self , course , is_teacher : false )
2014-09-24 04:23:30 +08:00
published_visible_assignments
2014-06-18 05:29:44 +08:00
end
2015-10-08 00:55:55 +08:00
def assignments_needing ( purpose , participation_type , expires_in , opts = { } )
2017-07-07 21:59:39 +08:00
original_shard = Shard . current
2014-07-29 07:03:06 +08:00
shard . activate do
2017-07-07 21:59:39 +08:00
course_ids = course_ids_for_todo_lists ( participation_type , opts )
2017-11-07 22:11:37 +08:00
opts = { limit : 15 } . merge ( opts . slice ( :due_after , :due_before , :limit , :include_ungraded , :ungraded_quizzes , :include_ignored ,
:include_locked , :include_concluded , :scope_only , :only_favorites , :needing_submitting ) )
2017-07-07 21:59:39 +08:00
if opts [ :scope_only ]
Shard . partition_by_shard ( course_ids ) do | shard_course_ids |
next unless Shard . current == original_shard
return yield ( * arguments_for_assignments_needing ( purpose , shard_course_ids , opts ) )
end
return Assignment . none # fallback
else
course_ids_cache_key = Digest :: MD5 . hexdigest ( course_ids . sort . join ( '/' ) )
Rails . cache . fetch ( [ self , " assignments_needing_ #{ purpose } " , course_ids_cache_key , opts ] . cache_key , :expires_in = > expires_in ) do
result = Shackles . activate ( :slave ) do
Shard . partition_by_shard ( course_ids ) do | shard_course_ids |
yield ( * arguments_for_assignments_needing ( purpose , shard_course_ids , opts ) )
end
end
result = result [ 0 ... opts [ :limit ] ] if opts [ :limit ]
result
2014-07-29 07:03:06 +08:00
end
2013-01-31 03:39:40 +08:00
end
2017-07-07 21:59:39 +08:00
end
end
2017-06-08 05:15:14 +08:00
2017-07-07 21:59:39 +08:00
def course_ids_for_todo_lists ( participation_type , opts )
shard . activate do
course_ids = Shackles . activate ( :slave ) do
if opts [ :include_concluded ]
participated_course_ids
else
case participation_type
when :student
participating_student_course_ids
when :instructor
participating_instructor_course_ids
end
end
2017-06-08 05:15:14 +08:00
end
2017-06-21 10:03:09 +08:00
if opts [ :only_favorites ]
course_ids = course_ids & favorite_context_ids ( " Course " )
end
2015-10-08 00:55:55 +08:00
if opts [ :contexts ]
course_ids = Array ( opts [ :contexts ] ) . map ( & :id ) & course_ids
end
2017-07-07 21:59:39 +08:00
course_ids
end
end
2017-06-21 10:03:09 +08:00
2017-07-07 21:59:39 +08:00
def arguments_for_assignments_needing ( purpose , shard_course_ids , opts )
scope = nil
if opts [ :ungraded_quizzes ]
scope = Quizzes :: Quiz . where ( context_type : 'Course' , context_id : shard_course_ids ) .
not_for_assignment
scope = scope . not_ignored_by ( self , purpose ) unless opts [ :include_ignored ]
else
scope = Assignment . for_course ( shard_course_ids )
scope = scope . not_ignored_by ( self , purpose ) unless opts [ :include_ignored ]
2012-12-11 01:21:56 +08:00
end
2017-07-07 21:59:39 +08:00
[ scope , opts . merge ( :shard_course_ids = > shard_course_ids ) ]
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2015-10-08 00:55:55 +08:00
def assignments_needing_submitting ( opts = { } )
2015-11-19 01:13:02 +08:00
assignments_needing ( 'submitting' , :student , 15 . minutes , opts ) do | assignment_scope , options |
due_after = options [ :due_after ] || 4 . weeks . ago
due_before = options [ :due_before ] || 1 . week . from_now
2015-10-08 00:55:55 +08:00
assignments = assignment_scope .
2017-03-31 23:54:57 +08:00
filter_by_visibilities_in_given_courses ( id , options [ :shard_course_ids ] ) .
2015-10-08 00:55:55 +08:00
published .
2015-11-19 01:13:02 +08:00
due_between_with_overrides ( due_after , due_before ) .
2017-06-09 08:30:26 +08:00
need_submitting_info ( id , options [ :limit ] )
2017-06-09 10:52:02 +08:00
assignments = assignments . expecting_submission unless options [ :include_ungraded ]
assignments = assignments . not_locked unless options [ :include_locked ]
2017-07-07 21:59:39 +08:00
if options [ :scope_only ]
assignments . for_course ( options [ :shard_course_ids ] )
else
select_available_assignments ( assignments , options ) . reject { | a | a . due_at && a . due_at < Time . now && ! a . expects_submission? }
end
2015-10-08 00:55:55 +08:00
end
end
2013-01-31 03:39:40 +08:00
2017-11-07 22:11:37 +08:00
def ungraded_quizzes ( opts = { } )
2016-09-27 06:00:41 +08:00
assignments_needing ( 'submitting' , :student , 15 . minutes , opts . merge ( :ungraded_quizzes = > true ) ) do | quiz_scope , options |
2016-11-18 01:03:49 +08:00
due_after = options [ :due_after ] || Time . now
2016-09-27 06:00:41 +08:00
due_before = options [ :due_before ] || 1 . week . from_now
quizzes = quiz_scope .
2017-06-09 08:30:26 +08:00
visible_to_students_in_course_with_da ( self . id , options [ :shard_course_ids ] )
quizzes = quizzes . not_locked unless opts [ :include_locked ]
quizzes = quizzes .
available .
due_between_with_overrides ( due_after , due_before ) .
preload ( :context )
2017-11-07 22:11:37 +08:00
quizzes = quizzes . need_submitting_info ( id , options [ :limit ] ) if options [ :needing_submitting ]
2017-07-07 21:59:39 +08:00
if options [ :scope_only ]
quizzes . for_course ( options [ :shard_course_ids ] )
else
select_available_assignments ( quizzes , options )
end
2016-09-27 06:00:41 +08:00
end
end
2015-10-08 00:55:55 +08:00
def assignments_needing_grading ( opts = { } )
# not really any harm in extending the expires_in since we touch the user anyway when grades change
assignments_needing ( 'grading' , :instructor , 120 . minutes , opts ) do | assignment_scope , opts |
as = assignment_scope . active .
expecting_submission .
2015-11-05 00:35:05 +08:00
need_grading_info
2015-12-19 05:47:46 +08:00
ActiveRecord :: Associations :: Preloader . new . preload ( as , :context )
2017-07-07 21:59:39 +08:00
if opts [ :scope_only ]
as # This needs the below `select` somehow to work
else
as . lazy . select { | a | Assignments :: NeedsGradingCountQuery . new ( a , self ) . count != 0 } . take ( opts [ :limit ] ) . to_a
end
2015-10-08 00:55:55 +08:00
end
end
2013-12-04 07:00:14 +08:00
2017-05-19 07:19:20 +08:00
def submitted_assignments ( opts = { } )
assignments_needing ( 'submitted' , :student , 120 . minutes , opts ) do | assignment_scope , options |
2017-06-08 05:15:14 +08:00
due_after = options [ :due_after ] || 2 . weeks . ago
due_before = options [ :due_before ] || 2 . weeks . from_now
as = assignment_scope . active
2017-06-09 10:52:02 +08:00
as = as . expecting_submission unless options [ :include_ungraded ]
as = as . not_locked unless options [ :include_locked ]
2017-06-08 05:15:14 +08:00
as = as . filter_by_visibilities_in_given_courses ( id , options [ :shard_course_ids ] ) .
published .
due_between_with_overrides ( due_after , due_before ) .
2017-07-18 01:07:20 +08:00
having_submissions_for_user ( id ) .
2017-06-08 05:15:14 +08:00
group ( 'submissions.id' )
2017-07-07 21:59:39 +08:00
options [ :scope_only ] ? as : select_available_assignments ( as , options )
2017-05-19 07:19:20 +08:00
end
end
2015-10-08 00:55:55 +08:00
def assignments_needing_moderation ( opts = { } )
2017-06-21 10:03:09 +08:00
assignments_needing ( 'moderation' , :instructor , 120 . minutes , opts ) do | assignment_scope , options |
2017-06-09 10:52:02 +08:00
scope = assignment_scope . active .
2015-10-08 00:55:55 +08:00
expecting_submission .
where ( :moderated_grading = > true ) .
where ( " assignments.grades_published_at IS NULL " ) .
2017-09-15 23:07:35 +08:00
where ( :id = > ModeratedGrading :: ProvisionalGrade . joins ( :submission ) . where ( " submissions.assignment_id=assignments.id " ) .
where ( Submission . needs_grading_conditions ) . distinct . select ( :assignment_id ) ) .
preload ( :context )
2017-07-07 21:59:39 +08:00
if options [ :scope_only ]
scope # Also need to check the rights like below
else
scope . lazy . select { | a | a . context . grants_right? ( self , :moderate_grades ) } . take ( options [ :limit ] ) . to_a
end
2012-12-11 01:21:56 +08:00
end
2011-02-01 09:57:29 +08:00
end
2012-10-11 03:00:01 +08:00
2014-06-16 17:32:02 +08:00
def submissions_needing_peer_review ( opts = { } )
course_ids = Shackles . activate ( :slave ) do
if opts [ :contexts ]
( Array ( opts [ :contexts ] ) . map ( & :id ) &
2015-05-08 04:58:50 +08:00
participating_student_course_ids )
2014-06-16 17:32:02 +08:00
else
2015-05-08 04:58:50 +08:00
participating_student_course_ids
2014-06-16 17:32:02 +08:00
end
end
opts = { limit : 15 } . merge ( opts . slice ( :limit ) )
shard . activate do
Rails . cache . fetch ( [ self , 'submissions_needing_peer_review' , course_ids , opts ] . cache_key , expires_in : 15 . minutes ) do
Shackles . activate ( :slave ) do
limit = opts [ :limit ]
result = Shard . partition_by_shard ( course_ids ) do | shard_course_ids |
shard_course_context_codes = shard_course_ids . map { | course_id | " course_ #{ course_id } " }
AssessmentRequest . where ( assessor_id : id ) . incomplete .
not_ignored_by ( self , 'reviewing' ) .
for_context_codes ( shard_course_context_codes )
end
# outer limit, since there could be limit * n_shards results
2017-06-09 10:52:02 +08:00
result = result [ 0 ... limit ] if limit && ! opts [ :scope_only ]
2014-06-16 17:32:02 +08:00
result
end
end
end
end
2017-06-21 10:03:09 +08:00
def needing_viewing ( object_type , participation_type , expires_in , opts = { } )
2017-07-07 21:59:39 +08:00
original_shard = Shard . current
2017-05-20 04:31:05 +08:00
shard . activate do
2017-07-07 21:59:39 +08:00
course_ids = course_ids_for_todo_lists ( participation_type , opts )
2017-06-21 10:03:09 +08:00
2017-07-07 21:59:39 +08:00
if opts [ :scope_only ]
Shard . partition_by_shard ( course_ids ) do | shard_course_ids |
next unless Shard . current == original_shard # only provideo scope on current shard
return yield ( * arguments_for_needing_viewing ( object_type , shard_course_ids , opts ) )
end
return object_type . constantize . none
else
course_ids_cache_key = Digest :: MD5 . hexdigest ( course_ids . sort . join ( ',' ) )
cache_key = [ self , " #{ object_type . underscore } _needing_viewing " , course_ids_cache_key , opts ] . cache_key
Rails . cache . fetch ( cache_key , :expires_in = > expires_in ) do
result = Shackles . activate ( :slave ) do
Shard . partition_by_shard ( course_ids ) do | shard_course_ids |
yield ( * arguments_for_needing_viewing ( object_type , shard_course_ids , opts ) )
end
2017-05-20 04:31:05 +08:00
end
2017-07-07 21:59:39 +08:00
result = result [ 0 ... opts [ :limit ] ] if opts [ :limit ]
result
2017-05-20 04:31:05 +08:00
end
end
end
end
2017-07-07 21:59:39 +08:00
def arguments_for_needing_viewing ( object_type , shard_course_ids , opts )
scope = object_type . constantize . for_courses_and_groups ( shard_course_ids , cached_current_group_memberships . map ( & :group_id ) )
scope = scope . not_ignored_by ( self , 'viewing' ) unless opts [ :include_ignored ]
scope = scope . todo_date_between ( opts [ :due_after ] , opts [ :due_before ] )
[ scope , opts . merge ( :shard_course_ids = > shard_course_ids ) ]
end
2017-05-26 10:34:01 +08:00
def discussion_topics_needing_viewing ( opts = { } )
2017-06-21 10:03:09 +08:00
needing_viewing ( 'DiscussionTopic' , :student , 120 . minutes , opts ) do | topics_context , options |
2017-07-07 21:59:39 +08:00
topics_context . active . published
2017-05-26 10:34:01 +08:00
end
end
2017-05-20 04:31:05 +08:00
def wiki_pages_needing_viewing ( opts = { } )
2017-06-21 10:03:09 +08:00
needing_viewing ( 'WikiPage' , :student , 120 . minutes , opts ) do | wiki_pages_context , options |
2017-07-07 21:59:39 +08:00
wiki_pages_context . available_to_planner . visible_to_user ( self )
2017-05-26 10:34:01 +08:00
end
2017-05-20 04:31:05 +08:00
end
2017-06-02 08:51:17 +08:00
def submission_statuses ( opts = { } )
2017-06-09 01:56:08 +08:00
Rails . cache . fetch ( [ 'assignment_submission_statuses' , self , opts ] . cache_key , :expires_in = > 120 . minutes ) do
opts [ :due_after ] || = 2 . weeks . ago
2017-06-02 08:51:17 +08:00
{
2017-07-27 09:34:30 +08:00
submitted : Set . new ( submitted_assignments ( opts ) . pluck ( :id ) ) ,
2017-06-08 02:32:14 +08:00
excused : Set . new ( Submission . active . with_assignment . where ( excused : true , user_id : self ) . pluck ( :assignment_id ) ) ,
graded : Set . new ( Submission . active . with_assignment . where ( user_id : self ) . where ( " submissions.excused = true OR (submissions.score IS NOT NULL AND submissions.workflow_state = 'graded') " ) . pluck ( :assignment_id ) ) ,
late : Set . new ( Submission . active . with_assignment . late . where ( user_id : self ) . pluck ( :assignment_id ) ) ,
missing : Set . new ( Submission . active . with_assignment . missing . where ( user_id : self ) . pluck ( :assignment_id ) ) ,
needs_grading : Set . new ( Submission . active . with_assignment . needs_grading . where ( user_id : self ) . pluck ( :assignment_id ) ) ,
2017-07-27 09:34:30 +08:00
has_feedback : Set . new ( self . recent_feedback ( start_at : opts [ :due_after ] ) . pluck ( :assignment_id ) ) ,
new_activity : Set . new ( Submission . active . with_assignment . unread_for ( self ) . pluck ( :assignment_id ) )
2017-06-02 08:51:17 +08:00
} . with_indifferent_access
end
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 )
2014-07-11 01:22:01 +08:00
self . update_attribute ( :uuid , CanvasSlug . 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
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
2015-04-28 01:47:25 +08:00
2011-02-01 09:57:29 +08:00
def log_merge_result ( text )
@merge_results || = [ ]
@merge_results << text
end
2015-04-28 01:47:25 +08:00
2011-02-01 09:57:29 +08:00
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
2013-10-14 22:13:00 +08:00
return if @self_enrolling # avoid infinite recursion when enrolling across shards (pseudonym creation + shard association stuff)
@self_enrolling = true
2014-01-14 08:39:45 +08:00
@self_enrollment = @self_enrollment_course . self_enroll_student ( self , :skip_pseudonym = > @just_created , :skip_touch_user = > true )
2013-10-14 22:13:00 +08:00
@self_enrolling = false
2012-05-30 07:35:00 +08:00
end
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
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
2016-06-10 06:15:32 +08:00
# associated with, which descend from in_root_account, descend from one of the
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
# 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?
2013-10-04 03:46:57 +08:00
children = accts . inject ( { } ) do | hash , acct |
2012-08-19 02:37:31 +08:00
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
2016-06-10 06:15:32 +08:00
enrollment_account_ids = in_root_account .
all_enrollments .
current_and_concluded .
where ( user_id : self ) .
joins ( :course ) .
2017-03-14 06:08:22 +08:00
distinct .
2016-06-10 06:15:32 +08:00
pluck ( :account_id )
2012-08-19 02:37:31 +08:00
longest_chain = [ in_root_account ]
while true
2016-06-10 06:15:32 +08:00
break if enrollment_account_ids . include? ( longest_chain . last . id )
2012-08-19 02:37:31 +08:00
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 = { } )
2013-12-04 07:00:14 +08:00
cache_key = [ association , enrollment_uuid , options ] . cache_key
@courses_with_primary_enrollment || = { }
@courses_with_primary_enrollment . fetch ( cache_key ) do
res = self . shard . activate do
2016-08-17 03:43:47 +08:00
result = Rails . cache . fetch ( [ self , 'courses_with_primary_enrollment2' , association , options , ApplicationController . region ] . cache_key , :expires_in = > 15 . minutes ) do
2013-12-04 07:00:14 +08:00
# Set the actual association based on if its asking for favorite courses or not.
actual_association = association == :favorite_courses ? :current_and_invited_courses : association
2014-09-26 04:05:28 +08:00
scope = send ( actual_association )
2015-01-05 23:34:36 +08:00
shards = in_region_associated_shards
2014-09-26 04:05:28 +08:00
# Limit favorite courses based on current shard.
if association == :favorite_courses
2016-08-17 03:43:47 +08:00
ids = self . favorite_context_ids ( " Course " )
if ids . empty?
scope = scope . none
else
shards = shards & ids . map { | id | Shard . shard_for ( id ) }
scope = scope . where ( id : ids )
2013-12-04 07:00:14 +08:00
end
2014-09-26 04:05:28 +08:00
end
2013-10-04 03:46:57 +08:00
2014-09-26 04:05:28 +08:00
unless options [ :include_completed_courses ]
2016-08-12 21:48:42 +08:00
scope = scope . joins ( :all_enrollments = > :enrollment_state ) . where ( " enrollment_states.restricted_access = ? " , false ) .
where ( " enrollment_states.state IN ('active', 'invited', 'pending_invited', 'pending_active') " )
2013-01-09 05:35:01 +08:00
end
2016-08-12 21:48:42 +08:00
2016-11-21 21:25:30 +08:00
scope . select ( " courses.*, enrollments.id AS primary_enrollment_id, enrollments.type AS primary_enrollment_type, enrollments.role_id AS primary_enrollment_role_id, #{ Enrollment . type_rank_sql } AS primary_enrollment_rank, enrollments.workflow_state AS primary_enrollment_state, enrollments.created_at AS primary_enrollment_date " ) .
2016-08-12 21:48:42 +08:00
order ( " courses.id, #{ Enrollment . type_rank_sql } , #{ Enrollment . state_rank_sql } " ) .
distinct_on ( :id ) . shard ( shards ) . to_a
2014-02-12 05:44:03 +08:00
end
result . dup
2013-12-04 07:00:14 +08:00
end
2013-01-09 05:35:01 +08:00
2013-12-04 07:00:14 +08:00
if association == :current_and_invited_courses
2017-01-18 15:20:38 +08:00
if enrollment_uuid && ( pending_course = Course . active .
select ( " courses.*, enrollments.type AS primary_enrollment,
#{Enrollment.type_rank_sql} AS primary_enrollment_rank,
enrollments . workflow_state AS primary_enrollment_state ,
enrollments . created_at AS primary_enrollment_date " ).
2013-12-04 07:00:14 +08:00
joins ( :enrollments ) .
2017-01-18 15:20:38 +08:00
where ( enrollments : { uuid : enrollment_uuid , workflow_state : 'invited' } ) . first )
2013-12-04 07:00:14 +08:00
res << pending_course
res . uniq!
end
pending_enrollments = temporary_invitations
unless pending_enrollments . empty?
2015-12-19 05:47:46 +08:00
ActiveRecord :: Associations :: Preloader . new . preload ( pending_enrollments , :course )
2014-09-30 12:04:39 +08:00
res . concat ( pending_enrollments . map do | e |
c = e . course
2014-11-17 22:52:50 +08:00
c . primary_enrollment_type = e . type
c . primary_enrollment_role_id = e . role_id
2015-12-18 01:14:52 +08:00
c . primary_enrollment_rank = e . rank_sortable
2014-09-30 12:04:39 +08:00
c . primary_enrollment_state = e . workflow_state
2016-11-21 21:25:30 +08:00
c . primary_enrollment_date = e . created_at
2014-09-30 12:04:39 +08:00
c . invitation = e . uuid
c
end )
2013-12-04 07:00:14 +08:00
res . uniq!
2012-10-26 05:39:00 +08:00
end
2011-11-08 03:10:20 +08:00
end
2013-01-09 05:35:01 +08:00
2013-12-04 07:00:14 +08:00
@courses_with_primary_enrollment [ cache_key ] =
res . sort_by { | c | [ c . primary_enrollment_rank , Canvas :: ICU . collation_key ( c . name ) ] }
end
2011-10-07 07:38:54 +08:00
end
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
# 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 = { } )
2015-07-01 23:31:41 +08:00
RequestCache . cache ( 'cached_current_enrollments' , self , opts ) do
enrollments = self . shard . activate do
res = Rails . cache . fetch ( [ self , 'current_enrollments3' , opts [ :include_future ] , ApplicationController . region ] . cache_key ) do
scope = ( opts [ :include_future ] ? self . enrollments . current_and_future : self . enrollments . current_and_invited )
scope . shard ( in_region_associated_shards ) . to_a
end
if opts [ :include_enrollment_uuid ] && ! res . find { | e | e . uuid == opts [ :include_enrollment_uuid ] } &&
( pending_enrollment = Enrollment . where ( uuid : opts [ :include_enrollment_uuid ] , workflow_state : " invited " ) . first )
res << pending_enrollment
end
res
end + temporary_invitations
2015-10-29 22:14:10 +08:00
if opts [ :preload_dates ]
2016-06-29 22:36:51 +08:00
Canvas :: Builders :: EnrollmentDateBuilder . preload_state ( enrollments )
2016-08-06 00:07:05 +08:00
end
if opts [ :preload_courses ]
2015-12-19 05:47:46 +08:00
ActiveRecord :: Associations :: Preloader . new . preload ( enrollments , :course )
2011-02-01 09:57:29 +08:00
end
2015-07-01 23:31:41 +08:00
enrollments
2014-08-27 10:04:31 +08:00
end
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2016-08-10 23:16:49 +08:00
def cached_invitations ( opts = { } )
enrollments = Rails . cache . fetch ( [ self , 'invited_enrollments' , ApplicationController . region ] . cache_key ) do
2017-01-18 15:20:38 +08:00
self . enrollments . shard ( in_region_associated_shards ) . invited_by_date .
joins ( :course ) . where . not ( courses : { workflow_state : 'deleted' } ) . to_a
2016-08-10 23:16:49 +08:00
end
if opts [ :include_enrollment_uuid ] && ! enrollments . find { | e | e . uuid == opts [ :include_enrollment_uuid ] } &&
( pending_enrollment = Enrollment . invited_by_date . where ( uuid : opts [ :include_enrollment_uuid ] ) . first )
enrollments << pending_enrollment
end
enrollments += temporary_invitations
ActiveRecord :: Associations :: Preloader . new . preload ( enrollments , :course ) if opts [ :preload_course ]
enrollments
end
def has_active_enrollment?
# don't need an expires_at here because user will be touched upon enrollment activation
Rails . cache . fetch ( [ self , 'has_active_enrollment' , ApplicationController . region ] . cache_key ) do
self . enrollments . shard ( in_region_associated_shards ) . current . active_by_date . exists?
end
end
def has_future_enrollment?
Rails . cache . fetch ( [ self , 'has_future_enrollment' , ApplicationController . region ] . cache_key , :expires_in = > 1 . hour ) do
self . enrollments . shard ( in_region_associated_shards ) . active_or_pending_by_date . exists?
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2014-12-11 17:14:29 +08:00
def group_membership_key
[ self , 'current_group_memberships' , ApplicationController . region ] . cache_key
end
2011-02-01 09:57:29 +08:00
def cached_current_group_memberships
2015-07-01 23:31:41 +08:00
@cached_current_group_memberships || = self . shard . activate do
Rails . cache . fetch ( group_membership_key ) do
2014-10-10 06:20:23 +08:00
self . current_group_memberships . shard ( self . in_region_associated_shards ) . to_a
2012-10-26 05:39:00 +08:00
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
2017-06-30 05:05:54 +08:00
def has_student_enrollment?
Rails . cache . fetch ( [ self , 'has_student_enrollment' , ApplicationController . region ] . cache_key ) do
self . enrollments . shard ( in_region_associated_shards ) . where ( :type = > %w{ StudentEnrollment StudentViewEnrollment } ) .
where . not ( :workflow_state = > %w{ rejected inactive deleted } ) . exists?
end
end
2015-05-08 04:58:50 +08:00
def participating_student_course_ids
2016-08-10 23:16:49 +08:00
@participating_student_course_ids || = self . shard . activate do
Rails . cache . fetch ( [ self , 'participating_student_course_ids' , ApplicationController . region ] . cache_key ) do
2016-11-02 02:51:55 +08:00
self . enrollments . shard ( in_region_associated_shards ) . where ( :type = > %w{ StudentEnrollment StudentViewEnrollment } ) .
current . active_by_date . distinct . pluck ( :course_id )
2016-08-10 23:16:49 +08:00
end
end
2015-05-08 04:58:50 +08:00
end
def participating_instructor_course_ids
2016-08-10 23:16:49 +08:00
@participating_instructor_course_ids || = self . shard . activate do
Rails . cache . fetch ( [ self , 'participating_instructor_course_ids' , ApplicationController . region ] . cache_key ) do
self . enrollments . shard ( in_region_associated_shards ) . of_instructor_type . current . active_by_date . distinct . pluck ( :course_id )
end
end
2011-02-01 09:57:29 +08:00
end
2012-01-04 04:30:49 +08:00
2015-05-08 04:58:50 +08:00
def participating_enrollments
@participating_enrollments || = self . shard . activate do
2016-08-10 23:16:49 +08:00
Rails . cache . fetch ( [ self , 'participating_enrollments' , ApplicationController . region ] . cache_key ) do
self . enrollments . shard ( in_region_associated_shards ) . current . active_by_date . to_a
2015-05-08 04:58:50 +08:00
end
2011-02-01 09:57:29 +08:00
end
end
2012-01-04 04:30:49 +08:00
2017-05-20 04:31:05 +08:00
def participated_course_ids
@participated_course_ids || = self . shard . activate do
Rails . cache . fetch ( [ self , 'participated_course_ids' , ApplicationController . region ] . cache_key ) do
self . not_removed_enrollments . shard ( in_region_associated_shards ) . distinct . pluck ( :course_id )
end
end
end
2011-02-01 09:57:29 +08:00
def submissions_for_context_codes ( context_codes , opts = { } )
2013-12-04 07:00:14 +08:00
return [ ] unless context_codes . present?
opts = { limit : 20 } . merge ( opts . slice ( :start_at , :limit ) )
shard . activate do
Rails . cache . fetch ( [ self , 'submissions_for_context_codes' , context_codes , opts ] . cache_key , expires_in : 15 . minutes ) do
2016-04-25 23:20:26 +08:00
opts [ :start_at ] || = 4 . weeks . ago
2013-12-04 07:00:14 +08:00
Shackles . activate ( :slave ) do
submissions = [ ]
2016-04-25 23:20:26 +08:00
submissions += self . submissions . where ( " submissions.submitted_at > ? OR submissions.created_at > ? " , opts [ :start_at ] , opts [ :start_at ] ) .
for_context_codes ( context_codes ) . eager_load ( :assignment ) .
2014-01-03 07:03:10 +08:00
where ( " submissions.score IS NOT NULL AND assignments.workflow_state=? AND assignments.muted=? " , 'published' , false ) .
2013-12-04 07:00:14 +08:00
order ( 'submissions.created_at DESC' ) .
2015-07-25 00:01:44 +08:00
limit ( opts [ :limit ] ) . to_a
2013-12-04 07:00:14 +08:00
2017-06-08 02:32:14 +08:00
subs_with_comment_scope = Submission . active . where ( user_id : self ) . for_context_codes ( context_codes ) .
2017-04-13 03:56:41 +08:00
joins ( :submission_comments , :assignment ) .
2014-01-03 07:03:10 +08:00
where ( assignments : { muted : false , workflow_state : 'published' } ) .
2017-08-31 05:37:12 +08:00
where ( 'submission_comments.created_at>?' , opts [ :start_at ] ) .
2017-04-13 03:56:41 +08:00
where . not ( :submission_comments = > { :author_id = > self , :draft = > true } ) .
distinct_on ( " submissions.id " ) .
order ( " submissions.id, submission_comments.created_at DESC " ) . # get the last created comment
select ( " submissions.*, submission_comments.created_at AS last_updated_at_from_db " )
# have to order by last_updated_at_from_db in another query because of distinct_on in the first one
submissions += Submission . from ( subs_with_comment_scope ) . limit ( opts [ :limit ] ) . order ( " last_updated_at_from_db " ) . select ( " * " ) . to_a
2013-12-04 07:00:14 +08:00
2015-08-19 22:40:12 +08:00
submissions = submissions . sort_by { | t | t [ 'last_updated_at_from_db' ] || t . created_at } . reverse
2013-12-04 07:00:14 +08:00
submissions = submissions . uniq
submissions . first ( opts [ :limit ] )
2015-12-19 05:47:46 +08:00
ActiveRecord :: Associations :: Preloader . new . preload ( submissions , [ :assignment , :user , :submission_comments ] )
2013-12-04 07:00:14 +08:00
submissions
end
end
end
end
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
2015-05-08 04:58:50 +08:00
self . participating_student_course_ids . map { | id | " course_ #{ id } " }
2013-01-31 03:39:40 +08:00
end
2011-02-01 09:57:29 +08:00
submissions_for_context_codes ( context_codes , opts )
end
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 ..."
2016-08-10 23:16:49 +08:00
instances = instances . polymorphic_where ( 'stream_item_instances.context' = > opts [ :contexts ] )
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
2013-10-02 01:03:38 +08:00
items . sort_by ( & :id ) . reverse
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
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 ) .
2015-07-16 03:23:27 +08:00
preload ( stream_item : :context ) .
2013-03-19 03:07:47 +08:00
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'
2016-06-16 03:16:26 +08:00
next if si . context_type == " Course " && ( si . context . concluded? || ! self . participating_enrollments . any? { | e | e . course_id == si . context_id } )
2014-09-26 04:20:09 +08:00
si . 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 ] )
2013-12-20 04:10:09 +08:00
events += Assignment . published . for_context_codes ( context_codes ) . due_between ( opts [ :start_at ] , opts [ :end_at ] ) . updated_after ( opts [ :updated_at ] ) . with_just_calendar_attributes
2014-03-18 04:54:26 +08:00
events . sort_by { | e | [ e . start_at , Canvas :: ICU . collation_key ( e . title || CanvasSort :: First ) ] } . 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
2015-08-29 01:55:29 +08:00
# if we're looking through a lot of courses, we should probably not spend a lot of time
# computing which sections are visible or not before we make the db call;
# instead, i think we should pull for all the sections and filter after the fact
filter_after_db = ! opts [ :use_db_filter ] &&
( context_codes . grep ( / \ Acourse_ \ d+ \ z / ) . count > Setting . get ( 'filter_events_by_section_code_threshold' , '25' ) . to_i )
section_codes = self . section_context_codes ( context_codes , filter_after_db )
limit = filter_after_db ? opts [ :limit ] * 2 : opts [ :limit ] # pull extra events just in case
events = CalendarEvent . active . for_user_and_context_codes ( self , context_codes , section_codes ) .
2016-09-20 01:19:43 +08:00
between ( now , opts [ :end_at ] ) . limit ( limit ) . order ( :start_at ) . to_a . reject ( & :hidden? )
2015-08-29 01:55:29 +08:00
if filter_after_db
original_count = events . count
if events . any? { | e | e . context_code . start_with? ( " course_section_ " ) }
section_ids = events . map ( & :context_code ) . grep ( / \ Acourse_section_ \ d+ \ z / ) . map { | s | s . sub ( / \ Acourse_section_ / , '' ) . to_i }
section_course_codes = Course . joins ( :course_sections ) . where ( :course_sections = > { :id = > section_ids } ) .
pluck ( :id ) . map { | id | " course_ #{ id } " }
visible_section_codes = self . section_context_codes ( section_course_codes )
events . reject! { | e | e . context_code . start_with? ( " course_section_ " ) && ! visible_section_codes . include? ( e . context_code ) }
events = events . first ( opts [ :limit ] ) # strip down to the original limit
end
# if we've filtered too many (which should be unlikely), just fallback on the old behavior
if original_count > = opts [ :limit ] && events . count < opts [ :limit ]
return self . upcoming_events ( opts . merge ( :use_db_filter = > true ) )
end
end
2015-10-29 22:14:10 +08:00
assignments = Assignment . published .
for_context_codes ( context_codes ) .
due_between_with_overrides ( now , opts [ :end_at ] ) .
include_submitted_count
if assignments . any?
if AssignmentOverrideApplicator . should_preload_override_students? ( assignments , self , " upcoming_events " )
AssignmentOverrideApplicator . preload_assignment_override_students ( assignments , self )
end
events += select_available_assignments (
2016-09-23 03:21:18 +08:00
select_upcoming_assignments ( assignments . map { | a | a . overridden_for ( self ) } , opts . merge ( :time = > now ) )
)
2015-10-29 22:14:10 +08:00
end
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
2017-06-08 05:15:14 +08:00
def select_available_assignments ( assignments , opts = { } )
2015-02-10 00:27:15 +08:00
return [ ] if assignments . empty?
2017-06-08 05:15:14 +08:00
available_course_ids = if opts [ :include_concluded ]
participated_course_ids
else
Shard . partition_by_shard ( assignments . map ( & :context_id ) . uniq ) do | course_ids |
self . enrollments . shard ( Shard . current ) . where ( course_id : course_ids ) . active_by_date . pluck ( :course_id )
end
end
2016-08-10 23:16:49 +08:00
assignments . select { | a | available_course_ids . include? ( a . context_id ) }
2015-02-10 00:27:15 +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 |
2014-05-03 00:35:29 +08:00
if a . grants_right? ( self , :delete )
2014-01-16 06:46:50 +08:00
a . dates_hash_visible_to ( self ) . any? do | due_hash |
2013-03-08 08:16:40 +08:00
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 ] )
2013-12-20 04:10:09 +08:00
undated_events += Assignment . published . 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
2016-08-10 23:16:49 +08:00
def setup_context_lookups ( contexts )
2011-02-01 09:57:29 +08:00
# TODO: All the event methods use this and it's really slow.
2016-08-10 23:16:49 +08:00
Array ( contexts ) . map ( & :asset_string )
2011-02-01 09:57:29 +08:00
end
def cached_context_codes
2016-08-10 23:16:49 +08:00
# (hopefully) don't need to include cross-shard because calendar events/assignments/etc are only seached for on current shard anyway
@cached_context_codes || =
Rails . cache . fetch ( [ self , 'cached_context_codes' , Shard . current ] . cache_key , :expires_in = > 15 . minutes ) do
2017-01-25 00:52:04 +08:00
group_ids = self . groups . active . pluck ( :id )
2016-08-10 23:16:49 +08:00
cached_current_course_ids = Rails . cache . fetch ( [ self , 'cached_current_course_ids' , Shard . current ] . cache_key ) do
# don't need an expires at because user will be touched if enrollment state changes from 'active'
2017-02-02 01:10:47 +08:00
self . enrollments . shard ( Shard . current ) . current . active_by_date . distinct . pluck ( :course_id )
2016-08-10 23:16:49 +08:00
end
cached_current_course_ids . map { | id | " course_ #{ id } " } + group_ids . map { | id | " group_ #{ id } " }
end
2011-02-01 09:57:29 +08:00
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
2016-03-25 04:28:41 +08:00
def appointment_context_codes ( include_observers : false )
@appointment_context_codes || = { }
2016-05-26 14:27:45 +08:00
@appointment_context_codes [ include_observers ] || = Rails . cache . fetch ( [ self , 'cached_appointment_codes' , ApplicationController . region , include_observers ] . cache_key , expires_in : 1 . day ) do
2015-06-26 03:35:23 +08:00
ret = { :primary = > [ ] , :secondary = > [ ] }
2015-10-29 22:14:10 +08:00
cached_current_enrollments ( preload_dates : true ) . each do | e |
2016-03-25 04:28:41 +08:00
next unless ( e . student? || ( include_observers && e . observer? ) ) && e . active?
2015-06-26 03:35:23 +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
2012-01-04 04:30:49 +08:00
end
end
def manageable_appointment_context_codes
2016-05-26 14:27:45 +08:00
@manageable_appointment_context_codes || = Rails . cache . fetch ( [ self , 'cached_manageable_appointment_codes' , ApplicationController . region ] . cache_key , expires_in : 1 . day ) do
2015-06-26 03:35:23 +08:00
ret = { :full = > [ ] , :limited = > [ ] , :secondary = > [ ] }
2016-08-10 23:16:49 +08:00
cached_current_enrollments ( preload_courses : true ) . each do | e |
2015-06-26 03:35:23 +08:00
next unless e . course . grants_right? ( self , :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
2012-01-04 04:30:49 +08:00
end
2015-06-26 03:35:23 +08:00
ret
2012-01-04 04:30:49 +08:00
end
end
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 )
2015-07-10 21:55:36 +08:00
result . concat ( send ( association ) . shard ( self ) . pluck ( :id ) . map { | id | " #{ association_type } _ #{ id } " } )
2013-04-30 07:28:51 +08:00
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
2016-08-16 22:11:49 +08:00
def self . convert_global_id_rows ( rows )
rows . map do | row |
row . map do | id |
Shard . relative_id_for ( id , Shard . current , Shard . birth )
2015-04-25 01:39:13 +08:00
end
end
end
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 ) }
2013-10-05 04:02:49 +08:00
return if users . length < Setting . get ( " min_users_for_conversation_context_codes_preload " , 5 ) . to_i
2013-08-20 02:13:46 +08:00
preload_shard_associations ( users )
shards = Set . new
users . each do | user |
shards . merge ( user . associated_shards )
end
2015-04-25 01:39:13 +08:00
active_contexts = { }
concluded_contexts = { }
2013-08-20 02:13:46 +08:00
Shard . with_each_shard ( shards . to_a ) do
2016-08-16 22:11:49 +08:00
course_rows = convert_global_id_rows (
2013-08-20 02:13:46 +08:00
Enrollment . joins ( :course ) .
2015-04-25 01:39:13 +08:00
where ( User . enrollment_conditions ( :active ) ) .
2013-08-20 02:13:46 +08:00
where ( user_id : users ) .
2016-08-16 22:11:49 +08:00
distinct . pluck ( :user_id , :course_id ) )
course_rows . each do | user_id , course_id |
active_contexts [ user_id ] || = [ ]
active_contexts [ user_id ] << " course_ #{ course_id } "
2015-04-25 01:39:13 +08:00
end
2013-08-20 02:13:46 +08:00
2016-08-16 22:11:49 +08:00
cc_rows = convert_global_id_rows (
use joins, not includes, on user associations in rails3
active record::associations accepts an :include
parameter which automatically retrieves specified
associations of the associated model.
In rails 2, AR checks through the where clause and
joins the table if it is referenced in the where
clause. In rails 3, this parameter is ignored when
specifying a condition on the association (and also
when you call pluck, incidentally), resulting in
database errors when you e.g.
has_many enrollments, include: [:course], :conditions
=> "courses.some_property = 'some value'"
this commit adds support for using an inner join with
these associations (it is not enough to simply define
a method on the model to retrieve the association with
the given conditions, because the associations in
question are referenced in a few has_many_through
associations later)
note this does not add similar support for inner joins on
associations in rails2, so we are still using the
:include option. This is a bit of an antipattern
as it can result in something of an N+1 queries
problem. This commit doesn't address that; it simply
makes the rails 3 postgres adapter less upset with us.
Change-Id: I5beefd689c734d372ed5627fef4bbb450883837d
Reviewed-on: https://gerrit.instructure.com/30185
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
Product-Review: Anthus Williams <awilliams@instructure.com>
QA-Review: Anthus Williams <awilliams@instructure.com>
2014-02-15 05:22:52 +08:00
Enrollment . joins ( :course ) .
2015-04-25 01:39:13 +08:00
where ( User . enrollment_conditions ( :completed ) ) .
2013-08-20 02:13:46 +08:00
where ( user_id : users ) .
2016-08-16 22:11:49 +08:00
distinct . pluck ( :user_id , :course_id ) )
cc_rows . each do | user_id , course_id |
concluded_contexts [ user_id ] || = [ ]
concluded_contexts [ user_id ] << " course_ #{ course_id } "
2015-04-25 01:39:13 +08:00
end
2013-08-20 02:13:46 +08:00
2016-08-16 22:11:49 +08:00
group_rows = convert_global_id_rows (
2013-08-20 02:13:46 +08:00
GroupMembership . joins ( :group ) .
2016-09-23 04:09:09 +08:00
merge ( User . instance_exec ( & User . reflections [ 'current_group_memberships' ] . scope ) . only ( :where ) ) .
2013-08-20 02:13:46 +08:00
where ( user_id : users ) .
2016-08-16 22:11:49 +08:00
distinct . pluck ( :user_id , :group_id ) )
group_rows . each do | user_id , group_id |
active_contexts [ user_id ] || = [ ]
active_contexts [ user_id ] << " group_ #{ group_id } "
2015-04-25 01:39:13 +08:00
end
2013-08-20 02:13:46 +08:00
end
Shard . birth . activate do
users . each do | user |
2015-04-25 01:39:13 +08:00
active = active_contexts [ user . id ] || [ ]
concluded = concluded_contexts [ user . id ] || [ ]
2013-08-20 02:13:46 +08:00
user . instance_variable_set ( :@conversation_context_codes , {
2015-04-25 01:39:13 +08:00
true = > ( active + concluded ) . uniq ,
false = > active
2013-08-20 02:13:46 +08:00
} )
end
end
end
2015-08-29 01:55:29 +08:00
def section_context_codes ( context_codes , skip_visibility_filter = false )
2012-04-18 06:38:45 +08:00
course_ids = context_codes . grep ( / \ Acourse_ \ d+ \ z / ) . map { | s | s . sub ( / \ Acourse_ / , '' ) . to_i }
return [ ] unless course_ids . present?
2015-08-19 22:40:12 +08:00
section_ids = [ ]
2015-08-29 01:55:29 +08:00
if skip_visibility_filter
full_course_ids = course_ids
else
full_course_ids = [ ]
Course . where ( id : course_ids ) . each do | course |
result = course . course_section_visibility ( self )
case result
when Array
section_ids . concat ( result )
when :all
full_course_ids << course . id
end
2015-08-19 22:40:12 +08:00
end
end
if full_course_ids . any?
current_shard = Shard . current
Shard . partition_by_shard ( full_course_ids ) do | shard_course_ids |
section_ids . concat ( CourseSection . active . where ( :course_id = > shard_course_ids ) . pluck ( :id ) .
map { | id | Shard . relative_id_for ( id , Shard . current , current_shard ) } )
end
2012-04-18 06:38:45 +08:00
end
2015-08-19 22:40:12 +08:00
section_ids . map { | id | " course_section_ #{ id } " }
2012-04-18 06:38:45 +08:00
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 )
2014-09-12 03:44:34 +08:00
folder = self . active_folders . where ( name : name ) . first
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
2014-05-29 00:56:01 +08:00
2012-07-03 03:16:08 +08:00
def self . default_storage_quota
2013-10-05 04:02:49 +08:00
Setting . get ( '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
2017-04-28 03:11:53 +08:00
def roles ( root_account , exclude_deleted_accounts = nil )
# Don't include roles for deleted accounts and don't cache
# the results.
return user_roles ( root_account , true ) if exclude_deleted_accounts
2013-12-04 07:00:14 +08:00
return @roles if @roles
2016-10-22 06:45:19 +08:00
@roles = Rails . cache . fetch ( [ 'user_roles_for_root_account3' , self , root_account ] . cache_key ) do
2017-04-28 03:11:53 +08:00
user_roles ( root_account )
2016-08-09 23:28:06 +08:00
end
2011-11-11 10:43:36 +08:00
end
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 = { } )
2014-03-20 04:02:27 +08:00
users = ( [ self ] + users ) . uniq ( & :id )
2012-11-29 06:53:00 +08:00
private = users . size < = 2 if private . nil?
2014-09-12 03:44:34 +08:00
Conversation . initiate ( users , private , options ) . conversation_participants . where ( user_id : self ) . first
2012-03-01 03:57:53 +08:00
end
AddressBook facade
fixes CNVS-29824
refs CNVS-29862, CNVS-29867
test-plan:
check for regressions around:
* accessing profile#show endpoint, with profiles enabled, is
authorized if and only if current user knows profile owner.
* eportfolios#show, with profiles enabled, includes link to owner's
profile if and only if current user knows owner.
* conversations:
- filtering of individual recipients when creating a conversation
- expansion of context recipients when creating a conversation
- restriction of individual recipients to those known via course
when creating a conversation in a course
- including as an admin over that course
- filtering of individual recipients when adding a message to an
existing conversation allows existing recipients
* searching/browsing address book (conversation creation, link
observer to students, due date overrides, etc.):
- when loading info about single user (user_id parameter), including
combinations of conversation_id and context parameters, returns
user data if and only if the target is known to current user under
those parameters
- users matched:
- excludes already listed users
- restricts to users known via specified context, if any
- including as an admin over that context
- finds users with weak associations (e.g. invited student that
hasn't logged in for first time yet) when linking observers
- doesn't find users with weak associations otherwise
- contexts matched:
- limited to those known to current user
- have count of known users
- have counts of known users in synthetic contexts
* data returned for known users in various API calls include common
course and groups between current user and returned user
Change-Id: I66bca0921b298be8d529a713fa982a6dfdcbcc8e
Reviewed-on: https://gerrit.instructure.com/84045
Reviewed-by: Jonathan Featherstone <jfeatherstone@instructure.com>
Tested-by: Jenkins
QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com>
Product-Review: Jacob Fugal <jacob@instructure.com>
2016-06-17 00:37:56 +08:00
def address_book
@address_book || = AddressBook . for ( self )
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-08-01 18:01:38 +08:00
def mark_all_conversations_as_read!
2016-03-31 21:20:18 +08:00
updated = conversations . unread . update_all ( :workflow_state = > 'read' )
if updated > 0
User . where ( :id = > id ) . update_all ( :unread_conversations_count = > 0 )
end
2011-08-01 18:01:38 +08:00
end
2011-09-14 01:00:19 +08:00
def conversation_participant ( conversation_id )
2014-09-12 03:44:34 +08:00
all_conversations . where ( conversation_id : conversation_id ) . first
2011-09-14 01:00:19 +08:00
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
2016-03-31 21:20:18 +08:00
unread_count = conversations . unread . count
if self . unread_conversations_count != unread_count
self . class . where ( :id = > id ) . update_all ( :unread_conversations_count = > unread_count )
end
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
2015-10-29 22:14:10 +08:00
cached_enrollments = self . cached_current_enrollments ( :include_enrollment_uuid = > enrollment_uuid , :preload_dates = > true )
2011-09-27 03:08:41 +08:00
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
existing_enrollment_info = coalesced_enrollments . find { | en |
# coalesce together enrollments for the same course and the same state
2013-10-02 01:03:38 +08:00
en [ :enrollment ] . course == e . course && en [ :enrollment ] . workflow_state == e . workflow_state
2011-09-27 03:08:41 +08:00
}
if existing_enrollment_info
existing_enrollment_info [ :types ] << e . readable_type
2014-03-18 04:54:26 +08:00
existing_enrollment_info [ :sortable ] = [ existing_enrollment_info [ :sortable ] || CanvasSort :: Last , [ e . rank_sortable , e . state_sortable , 0 - e . id ] ] . min
2011-09-27 03:08:41 +08:00
else
coalesced_enrollments << { :enrollment = > e , :sortable = > [ e . rank_sortable , e . state_sortable , 0 - e . id ] , :types = > [ e . readable_type ] }
end
end
2013-10-02 01:03:38 +08:00
coalesced_enrollments = coalesced_enrollments . sort_by { | e | e [ :sortable ] }
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 ,
2017-07-16 14:40:03 +08:00
:accounts = > self . adminable_accounts ,
:accounts_count = > self . adminable_accounts . length ,
2011-09-27 03:08:41 +08:00
}
end
2016-08-17 03:43:47 +08:00
# Public: Returns a unique list of favorite context type ids relative to the active shard.
2013-10-04 03:46:57 +08:00
#
# Examples
#
2016-08-17 03:43:47 +08:00
# favorite_context_ids("Course")
2013-10-04 03:46:57 +08:00
# # => [1, 2, 3, 4]
#
2016-08-17 03:43:47 +08:00
# Returns an array of unique global ids.
def favorite_context_ids ( context_type )
@favorite_context_ids || = { }
2013-10-04 03:46:57 +08:00
2016-08-17 03:43:47 +08:00
context_ids = @favorite_context_ids [ context_type ]
2013-10-04 03:46:57 +08:00
unless context_ids
# Only get the users favorites from their shard.
self . shard . activate do
# Get favorites and map them to their global ids.
2016-08-17 03:43:47 +08:00
context_ids = self . favorites . where ( context_type : context_type ) . pluck ( :context_id ) . map { | id | Shard . global_id_for ( id ) }
@favorite_context_ids [ context_type ] = context_ids
2013-10-04 03:46:57 +08:00
end
end
2014-09-26 04:05:28 +08:00
# Return ids relative for the current shard
2013-10-04 03:46:57 +08:00
context_ids . map { | id |
2014-09-26 04:05:28 +08:00
Shard . relative_id_for ( id , self . shard , Shard . current )
}
2013-10-04 03:46:57 +08:00
end
2011-11-08 03:10:20 +08:00
def menu_courses ( enrollment_uuid = nil )
return @menu_courses if @menu_courses
2015-08-19 04:43:56 +08:00
2016-08-17 03:43:47 +08:00
favorites = self . courses_with_primary_enrollment ( :favorite_courses , enrollment_uuid )
if favorites . length > 0
@menu_courses = favorites
else
2016-11-21 21:25:30 +08:00
# this terribleness is so we try to make sure that the newest courses show up in the menu
@menu_courses = self . courses_with_primary_enrollment ( :current_and_invited_courses , enrollment_uuid ) .
2016-12-02 02:27:04 +08:00
sort_by { | c | [ c . primary_enrollment_rank , Time . now - ( c . primary_enrollment_date || Time . now ) ] } .
first ( Setting . get ( 'menu_course_limit' , '20' ) . to_i ) .
2016-11-21 21:25:30 +08:00
sort_by { | c | [ c . primary_enrollment_rank , Canvas :: ICU . collation_key ( c . name ) ] }
2016-08-17 03:43:47 +08:00
end
2015-12-19 05:47:46 +08:00
ActiveRecord :: Associations :: Preloader . new . preload ( @menu_courses , :enrollment_term )
2015-08-19 04:43:56 +08:00
@menu_courses
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 )
2017-03-04 04:15:30 +08:00
return false if type == " StudentEnrollment " && MasterCourses :: MasterTemplate . is_master_course? ( course )
2015-03-24 03:48:59 +08:00
if type != " StudentEnrollment " && course . grants_right? ( self , session , :manage_admin_users )
return true
end
if course . grants_right? ( self , session , :manage_students )
if %w{ StudentEnrollment ObserverEnrollment } . include? ( type ) || ( type == 'TeacherEnrollment' && course . teacherless? )
return true
end
end
2011-12-08 04:47:19 +08:00
end
2014-03-26 06:13:29 +08:00
def can_be_enrolled_in_course? ( course )
2017-04-07 05:00:25 +08:00
! ! SisPseudonym . for ( self , course , type : :implicit , require_sis : false ) ||
2014-03-26 06:13:29 +08:00
( self . creation_pending? && self . enrollments . where ( course_id : course ) . exists? )
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
# 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 )
2017-04-07 05:00:25 +08:00
pseudonym = SisPseudonym . for ( self , account , type : :trusted , require_sis : false )
unless pseudonym
2011-11-19 06:20:09 +08:00
# list of copyable pseudonyms
2017-04-07 05:00:25 +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!
2014-09-12 03:44:34 +08:00
template = templates . detect { | template | ! account . pseudonyms . active . by_unique_id ( template . unique_id ) . first }
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-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
2017-07-21 23:33:57 +08:00
def profile
super || build_profile
2012-07-12 07:20:39 +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
2013-11-01 02:35:48 +08:00
def parse_otp_remember_me_cookie ( cookie )
return 0 , [ ] , nil unless cookie
time , * ips , hmac = cookie . split ( '-' )
[ time , ips , hmac ]
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
end
2014-09-19 03:47:01 +08:00
def otp_secret_key_remember_me_cookie ( time , current_cookie , remote_ip = nil , options = { } )
2013-11-01 02:35:48 +08:00
_ , ips , _ = parse_otp_remember_me_cookie ( current_cookie )
cookie = [ time . to_i , * [ * ips , remote_ip ] . compact . sort ] . join ( '-' )
2014-09-19 03:47:01 +08:00
hmac_string = " #{ cookie } . #{ self . otp_secret_key } "
return hmac_string if options [ :hmac_string ]
" #{ cookie } - #{ Canvas :: Security . hmac_sha1 ( hmac_string ) } "
2013-11-01 02:35:48 +08:00
end
def validate_otp_secret_key_remember_me_cookie ( value , remote_ip = nil )
time , ips , hmac = parse_otp_remember_me_cookie ( value )
time . to_i > = ( Time . now . utc - 30 . days ) . to_i &&
( remote_ip . nil? || ips . include? ( remote_ip ) ) &&
2014-09-19 03:47:01 +08:00
Canvas :: Security . verify_hmac_sha1 ( hmac , otp_secret_key_remember_me_cookie ( time , value , nil , hmac_string : true ) )
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
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
2014-09-19 03:47:01 +08:00
self . otp_secret_key_enc , self . otp_secret_key_salt = Canvas :: Security :: encrypt_password ( key , 'otp_secret_key' )
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
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
2015-08-26 06:36:10 +08:00
s = Setting . lock . where ( name : 'crocodoc_counter' ) . first_or_create ( value : 0 )
2012-08-21 07:41:42 +08:00
cid = s . value = s . value . to_i + 1
s . save!
end
update_attribute ( :crocodoc_id , cid )
cid
end
def crocodoc_user
2017-06-29 04:23:41 +08:00
" #{ crocodoc_id! } , #{ short_name . delete ( ',' ) } "
end
def moderated_grading_ids ( create_crocodoc_id = false )
{
crocodoc_id : create_crocodoc_id ? crocodoc_id! : crocodoc_id ,
global_id : global_id . to_s
}
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
2015-11-17 02:26:47 +08:00
def mfa_settings ( pseudonym_hint : nil )
# try to short-circuit site admins where it is required
if pseudonym_hint
mfa_settings = pseudonym_hint . account . mfa_settings
return :required if mfa_settings == :required ||
mfa_settings == :required_for_admins && ! pseudonym_hint . account . all_account_users_for ( self ) . empty?
end
2015-07-17 05:53:07 +08:00
result = self . pseudonyms . shard ( self ) . preload ( :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
2015-11-17 02:26:47 +08:00
# if pseudonym_hint is given, and we got to here, we don't need
# to redo the expensive all_account_users_for check
if ( pseudonym_hint && pseudonym_hint . account == account ) ||
account . all_account_users_for ( self ) . empty?
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
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
2013-09-18 04:16:35 +08:00
def associated_shards ( strength = :strong )
2012-10-02 06:13:46 +08:00
[ Shard . default ]
end
2014-10-10 06:20:23 +08:00
def in_region_associated_shards
associated_shards . select { | shard | shard . in_current_region? || shard . default? }
end
2017-09-06 03:43:18 +08:00
def adminable_accounts_scope
Account . shard ( self . in_region_associated_shards ) . active . joins ( :account_users ) .
where ( account_users : { user_id : self . id } ) .
where . not ( account_users : { workflow_state : 'deleted' } )
end
2017-07-16 14:40:03 +08:00
def adminable_accounts
@adminable_accounts || = shard . activate do
Rails . cache . fetch ( [ 'adminable_accounts' , self , ApplicationController . region ] . cache_key ) do
2017-09-06 03:43:18 +08:00
adminable_accounts_scope . to_a
2014-02-28 04:20:03 +08:00
end
end
2012-10-02 06:13:46 +08:00
end
2012-10-10 04:34:00 +08:00
2013-11-01 00:47:22 +08:00
def all_paginatable_accounts
2017-09-06 03:43:18 +08:00
ShardedBookmarkedCollection . build ( Account :: Bookmarker , self . adminable_accounts_scope )
2013-11-01 00:47:22 +08:00
end
2017-08-26 06:39:06 +08:00
def all_pseudonyms_loaded?
! ! @all_pseudonyms
end
2013-03-19 23:49:31 +08:00
def all_pseudonyms
2015-07-10 21:55:36 +08:00
@all_pseudonyms || = self . pseudonyms . shard ( self ) . to_a
2012-10-10 04:34:00 +08:00
end
2012-10-24 03:31:55 +08:00
2017-04-07 05:00:25 +08:00
def all_active_pseudonyms_loaded?
! ! @all_active_pseudonyms
end
2017-10-05 01:09:59 +08:00
def current_groups_in_region?
return true if self . current_groups . exists?
return true if self . current_groups . shard ( self . in_region_associated_shards ) . exists?
false
end
2013-12-04 07:00:14 +08:00
def all_active_pseudonyms ( reload = false )
@all_active_pseudonyms = nil if reload
2015-07-10 21:55:36 +08:00
@all_active_pseudonyms || = self . pseudonyms . shard ( self ) . active . to_a
2012-11-08 04:54:18 +08:00
end
2014-05-29 00:56:01 +08:00
def preferred_gradebook_version
2016-07-15 04:06:16 +08:00
preferences . fetch ( :gradebook_version , 'default' )
2014-02-08 04:27:46 +08:00
end
2013-04-26 07:06:20 +08:00
def stamp_logout_time!
2014-07-24 01:14:22 +08:00
User . where ( :id = > self ) . update_all ( :last_logged_out = > Time . zone . now )
2013-04-26 07:06:20 +08:00
end
zip content exports for course, group, user
test plan:
1. use the content exports api with export_type=zip
to export files from courses, groups, and users
a. confirm only users who have permission to
download files from these contexts can perform
the export
b. confirm that deleted files and folders do not show
up in the downloaded archive
c. confirm that students cannot download locked files
or folders from courses this way
d. check the progress endpoint and make sure
it increments sanely
2. perform selective content exports by passing an array
of ids in select[folders] and/or select[attachments].
for example,
?select[folders][]=123&select[folders][]=456
?select[attachments][]=345
etc.
a. any selected files, plus the full contents of any
selected folders (that the caller has permission
to see) should be included
- that means locked files and subfolders should
be excluded from the archive
b. if all selected files and folders are descendants
of the same subfolder X, the export should be named
"X_export.zip" and all paths inside the zip should be
relative to it. for example, if you are exporting A/B/1
and A/C/2, you should get "A_export.zip" containing
files "B/1" and "C/2".
3. use the index and show endpoints to list and view
content exports in courses, groups, and users
a. confirm students cannot view non-zip course exports
(such as common cartridge exports)
b. confirm students cannot view other users' file (zip)
exports, in course, group, and user context
c. confirm teachers cannot view other users' file (zip)
exports, in course, group, and user context
(but can still view course [cc] exports initiated by
other teachers)
4. look at /courses/X/content_exports (web, not API)
a. confirm teachers see file exports they performed
b. confirm teachers do not see file exports performed by
other teachers
c. confirm teachers see all non-zip course exports
(cc/qti) including those initiated by other teachers
5. as a site admin user, perform a zip export of another
user's files. then, as that other user, go to
/dashboard/data_exports and confirm that the export
performed by the site admin user is not shown.
fixes CNVS-12706
Change-Id: Ie9b58e44ac8006a9c9171b3ed23454bf135385b0
Reviewed-on: https://gerrit.instructure.com/34341
Reviewed-by: James Williams <jamesw@instructure.com>
QA-Review: Trevor deHaan <tdehaan@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Product-Review: Jon Willesen <jonw@instructure.com>
2014-07-18 04:00:32 +08:00
def content_exports_visible_to ( user )
self . content_exports . where ( user_id : user )
end
2014-09-04 07:25:20 +08:00
def show_bouncing_channel_message!
2015-04-29 01:25:01 +08:00
unless show_bouncing_channel_message?
self . preferences [ :show_bouncing_channel_message ] = true
self . save!
end
2014-09-04 07:25:20 +08:00
end
def show_bouncing_channel_message?
! ! self . preferences [ :show_bouncing_channel_message ]
end
def dismiss_bouncing_channel_message!
2015-04-29 01:25:01 +08:00
if show_bouncing_channel_message?
self . preferences [ :show_bouncing_channel_message ] = false
self . save!
end
2014-09-04 07:25:20 +08:00
end
def bouncing_channel_message_dismissed?
self . preferences [ :show_bouncing_channel_message ] == false
end
def update_bouncing_channel_message! ( channel = nil )
force_set_bouncing = channel && channel . bouncing? && ! channel . imported?
set_bouncing = force_set_bouncing || self . communication_channels . unretired . any? { | cc | cc . bouncing? && ! cc . imported? }
if force_set_bouncing
show_bouncing_channel_message!
elsif set_bouncing
show_bouncing_channel_message! unless bouncing_channel_message_dismissed?
else
dismiss_bouncing_channel_message!
end
end
2014-09-26 02:20:17 +08:00
def locale
result = super
result = nil unless I18n . locale_available? ( result )
result
end
2016-04-21 04:51:48 +08:00
def submissions_folder ( for_course = nil )
shard . activate do
if for_course
parent_folder = self . submissions_folder
Folder . unique_constraint_retry do
self . folders . where ( parent_folder_id : parent_folder , submission_context_code : for_course . asset_string )
. first_or_create! ( name : for_course . name )
end
else
return @submissions_folder if @submissions_folder
Folder . unique_constraint_retry do
2016-08-09 05:11:13 +08:00
@submissions_folder = self . folders . where ( parent_folder_id : Folder . root_folders ( self ) . first , submission_context_code : 'root' )
2016-04-21 04:51:48 +08:00
. first_or_create! ( name : I18n . t ( 'Submissions' , locale : self . locale ) )
end
end
end
end
2013-06-20 14:20:21 +08:00
def authenticate_one_time_password ( code )
result = one_time_passwords . where ( code : code , used : false ) . take
return unless result
# atomically update used
return unless one_time_passwords . where ( used : false , id : result ) . update_all ( used : true , updated_at : Time . now . utc ) == 1
result
end
def generate_one_time_passwords ( regenerate : false )
regenerate || = ! one_time_passwords . exists?
return unless regenerate
one_time_passwords . scope . delete_all
Setting . get ( 'one_time_password_count' , 10 ) . to_i . times { one_time_passwords . create! }
end
2017-04-28 03:11:53 +08:00
def user_roles ( root_account , exclude_deleted_accounts = nil )
roles = [ 'user' ]
enrollment_types = root_account . all_enrollments . where ( user_id : self , workflow_state : 'active' ) . distinct . pluck ( :type )
roles << 'student' unless ( enrollment_types & %w[ StudentEnrollment StudentViewEnrollment ] ) . empty?
roles << 'teacher' unless ( enrollment_types & %w[ TeacherEnrollment TaEnrollment DesignerEnrollment ] ) . empty?
roles << 'observer' unless ( enrollment_types & %w[ ObserverEnrollment ] ) . empty?
account_users = root_account . all_account_users_for ( self )
if exclude_deleted_accounts
account_users = account_users . select { | a | a . account . workflow_state == 'active' }
end
if account_users . any?
roles << 'admin'
root_ids = [ root_account . id , Account . site_admin . id ]
roles << 'root_admin' if account_users . any? { | au | root_ids . include? ( au . account_id ) }
end
roles
end
2011-02-01 09:57:29 +08:00
end